{"thread":{"id":"50172","subject":"[PATCH] git-multimail: update to release 1.5.0","startedAt":"2019-01-07T19:23:17Z","lastAt":"2019-01-07T19:23:17Z","messageCount":1,"participants":["Matthieu Moy"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"366277","messageId":"20190107174838.9078-1-git@matthieu-moy.fr","threadId":"50172","inReplyTo":null,"subject":"[PATCH] git-multimail: update to release 1.5.0","fromName":"Matthieu Moy","fromEmail":"git@matthieu-moy.fr","sentAt":"2019-01-07T17:48:38Z","receivedAt":"2019-01-07T19:23:17Z","isPatch":true,"sender":{"key":"git@matthieu-moy.fr","avatar":"https://avatars.githubusercontent.com/u/14709?v=4"},"body":"Changes are described in CHANGES.\n\nContributions-by: Matthieu Moy <git@matthieu-moy.fr>\nContributions-by: William Stewart <william.stewart@booking.com>\nContributions-by: Ville Skyttä <ville.skytta@iki.fi>\nContributions-by: Dirk Olmes <dirk.olmes@codedo.de>\nContributions-by: Björn Kautler <Bjoern@Kautler.net>\nContributions-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>\nContributions-by: Gareth Pye <garethp@gpsatsys.com.au>\nContributions-by: David Lazar <lazard@csail.mit.edu>\nSigned-off-by: Matthieu Moy <git@matthieu-moy.fr>\n---\n\nA very long time since I did a git-multimail release, sorry for the\nlack of activity on this project. OTOH, there have not been many\npull-requests nor feature requests, which may be a sign that the\nproject reached some kind of maturity.\n\nHelp is welcome if some people would like to see git-multimail evolve\nfaster.\n\n contrib/hooks/multimail/CHANGES               |  56 ++++++\n contrib/hooks/multimail/CONTRIBUTING.rst      |  28 ++-\n contrib/hooks/multimail/README.Git            |   4 +-\n .../hooks/multimail/{README => README.rst}    |  38 +++-\n contrib/hooks/multimail/doc/gitolite.rst      |   9 +\n contrib/hooks/multimail/git_multimail.py      | 188 ++++++++++++++----\n .../hooks/multimail/migrate-mailhook-config   |  13 +-\n contrib/hooks/multimail/post-receive.example  |   2 +-\n 8 files changed, 281 insertions(+), 57 deletions(-)\n rename contrib/hooks/multimail/{README => README.rst} (95%)\n\ndiff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES\nindex 2076cf972b..35791fd02c 100644\n--- a/contrib/hooks/multimail/CHANGES\n+++ b/contrib/hooks/multimail/CHANGES\n@@ -1,3 +1,59 @@\n+Release 1.5.0\n+=============\n+\n+Backward-incompatible change\n+----------------------------\n+\n+The name of classes for environment was misnamed as `*Environement`.\n+It is now `*Environment`.\n+\n+New features\n+------------\n+\n+* A Thread-Index header is now added to each email sent (except for\n+  combined emails where it would not make sense), so that MS Outlook\n+  properly groups messages by threads even though they have a\n+  different subject line. Unfortunately, even adding this header the\n+  threading still seems to be unreliable, but it is unclear whether\n+  this is an issue on our side or on MS Outlook's side (see discussion\n+  here: https://github.com/git-multimail/git-multimail/pull/194).\n+\n+* A new variable multimailhook.ExcludeMergeRevisions was added to send\n+  notification emails only for non-merge commits.\n+\n+* For gitolite environment, it is now possible to specify the mail map\n+  in a separate file in addition to gitolite.conf, using the variable\n+  multimailhook.MailaddressMap.\n+\n+Internal changes\n+----------------\n+\n+* The testsuite now uses GIT_PRINT_SHA1_ELLIPSIS where needed for\n+  compatibility with recent Git versions. Only tests are affected.\n+\n+* We don't try to install pyflakes in the continuous integration job\n+  for old Python versions where it's no longer available.\n+\n+* Stop using the deprecated cgi.escape in Python 3.\n+\n+* New flake8 warnings have been fixed.\n+\n+* Python 3.6 is now tested against on Travis-CI.\n+\n+* A bunch of lgtm.com warnings have been fixed.\n+\n+Bug fixes\n+---------\n+\n+* SMTPMailer logs in only once now. It used to re-login for each email\n+  sent which triggered errors for some SMTP servers.\n+\n+* migrate-mailhook-config was broken by internal refactoring, it\n+  should now work again.\n+\n+This version was tested with Python 2.6 to 3.7. It was tested with Git\n+1.7.10.406.gdc801, 2.15.1 and 2.20.1.98.gecbdaf0.\n+\n Release 1.4.0\n =============\n \ndiff --git a/contrib/hooks/multimail/CONTRIBUTING.rst b/contrib/hooks/multimail/CONTRIBUTING.rst\nindex da65570e9b..de20a54287 100644\n--- a/contrib/hooks/multimail/CONTRIBUTING.rst\n+++ b/contrib/hooks/multimail/CONTRIBUTING.rst\n@@ -4,9 +4,8 @@ Contributing\n git-multimail is an open-source project, built by volunteers. We would\n welcome your help!\n \n-The current maintainers are Matthieu Moy\n-<matthieu.moy@grenoble-inp.fr> and Michael Haggerty\n-<mhagger@alum.mit.edu>.\n+The current maintainers are `Matthieu Moy <http://matthieu-moy.fr>`__ and\n+`Michael Haggerty <https://github.com/mhagger>`__.\n \n Please note that although a copy of git-multimail is distributed in\n the \"contrib\" section of the main Git project, development takes place\n@@ -33,6 +32,29 @@ mailing list`_.\n Please CC emails regarding git-multimail to the maintainers so that we\n don't overlook them.\n \n+Help needed: testers/maintainer for specific environments/OS\n+------------------------------------------------------------\n+\n+The current maintainer uses and tests git-multimail on Linux with the\n+Generic environment. More testers, or better contributors are needed\n+to test git-multimail on other real-life setups:\n+\n+* Mac OS X, Windows: git-multimail is currently not supported on these\n+  platforms. But since we have no external dependencies and try to\n+  write code as portable as possible, it is possible that\n+  git-multimail already runs there and if not, it is likely that it\n+  could be ported easily.\n+\n+  Patches to improve support for Windows and OS X are welcome.\n+  Ideally, there would be a sub-maintainer for each OS who would test\n+  at least once before each release (around twice a year).\n+\n+* Gerrit, Stash, Gitolite environments: although the testsuite\n+  contains tests for these environments, a tester/maintainer for each\n+  environment would be welcome to test and report failure (or success)\n+  on real-life environments periodically (here also, feedback before\n+  each release would be highly appreciated).\n+\n \n .. _`git-multimail repository on GitHub`: https://github.com/git-multimail/git-multimail\n .. _`Git mailing list`: git@vger.kernel.org\ndiff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git\nindex 161b0230a0..044444245d 100644\n--- a/contrib/hooks/multimail/README.Git\n+++ b/contrib/hooks/multimail/README.Git\n@@ -6,10 +6,10 @@ website:\n     https://github.com/git-multimail/git-multimail\n \n The version in this directory was obtained from the upstream project\n-on August 17 2016 and consists of the \"git-multimail\" subdirectory from\n+on January 07 2019 and consists of the \"git-multimail\" subdirectory from\n revision\n \n-    07b1cb6bfd7be156c62e1afa17cae13b850a869f refs/tags/1.4.0\n+    04e80e6c40be465cc62b6c246f0fcb8fd2cfd454 refs/tags/1.5.0\n \n Please see the README file in this directory for information about how\n to report bugs or contribute to git-multimail.\ndiff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README.rst\nsimilarity index 95%\nrename from contrib/hooks/multimail/README\nrename to contrib/hooks/multimail/README.rst\nindex 5105373aea..7c0fc4a6ef 100644\n--- a/contrib/hooks/multimail/README\n+++ b/contrib/hooks/multimail/README.rst\n@@ -1,4 +1,4 @@\n-git-multimail version 1.4.0\n+git-multimail version 1.5.0\n ===========================\n \n .. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master\n@@ -20,8 +20,8 @@ GPLv2 (see the COPYING file for details).\n \n Please note: although, as a convenience, git-multimail may be\n distributed along with the main Git project, development of\n-git-multimail takes place in its own, separate project.  See section\n-\"Getting involved\" below for more information.\n+git-multimail takes place in its own, separate project.  Please, read\n+`<CONTRIBUTING.rst>`__ for more information.\n \n \n By default, for each push received by the repository, git-multimail:\n@@ -89,6 +89,10 @@ Requirements\n   the multimailhook.mailer configuration variable below for how to\n   configure git-multimail to send emails via an SMTP server.\n \n+* git-multimail is currently tested only on Linux. It may or may not\n+  work on other platforms such as Windows and Mac OS. See\n+  `<CONTRIBUTING.rst>`__ to improve the situation.\n+\n \n Invocation\n ----------\n@@ -369,7 +373,7 @@ multimailhook.mailer\n         unset, then the value of multimailhook.from is used.\n \n       multimailhook.smtpServerTimeout\n-        Timeout in seconds.\n+        Timeout in seconds. Default is 10.\n \n       multimailhook.smtpEncryption\n         Set the security type. Allowed values: ``none``, ``ssl``, ``tls`` (starttls).\n@@ -419,8 +423,20 @@ multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange\n     If config values are unset, the value of the From: header is\n     determined as follows:\n \n-    1. (gitolite environment only) Parse gitolite.conf, looking for a\n-       block of comments that looks like this::\n+    1. (gitolite environment only)\n+       1.a) If ``multimailhook.MailaddressMap`` is set, and is a path\n+       to an existing file (if relative, it is considered relative to\n+       the place where ``gitolite.conf`` is located), then this file\n+       should contain lines like::\n+\n+           username Firstname Lastname <email@example.com>\n+\n+       git-multimail will then look for a line where ``$GL_USER``\n+       matches the ``username`` part, and use the rest of the line for\n+       the ``From:`` header.\n+\n+       1.b) Parse gitolite.conf, looking for a block of comments that\n+       looks like this::\n \n            # BEGIN USER EMAILS\n            # username Firstname Lastname <email@example.com>\n@@ -436,6 +452,11 @@ multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange\n \n     3. Use the value of multimailhook.envelopeSender.\n \n+multimailhook.MailaddressMap\n+    (gitolite environment only)\n+    File to look for a ``From:`` address based on the user doing the\n+    push. Defaults to unset. See ``multimailhook.from`` for details.\n+\n multimailhook.administrator\n     The name and/or email address of the administrator of the Git\n     repository; used in FOOTER_TEMPLATE.  Default is\n@@ -484,6 +505,11 @@ multimailhook.maxCommitEmails\n     mailbombing, for example on an initial push.  To disable commit\n     emails limit, set this option to 0.  The default is 500.\n \n+multimailhook.excludeMergeRevisions\n+    When sending out revision emails, do not consider merge commits (the\n+    functional equivalent of `rev-list --no-merges`).\n+    The default is `false` (send merge commit emails).\n+\n multimailhook.emailStrictUTF8\n     If this boolean option is set to `true`, then the main part of the\n     email body is forced to be valid UTF-8.  Any characters that are\ndiff --git a/contrib/hooks/multimail/doc/gitolite.rst b/contrib/hooks/multimail/doc/gitolite.rst\nindex 00aedd9c57..5054833105 100644\n--- a/contrib/hooks/multimail/doc/gitolite.rst\n+++ b/contrib/hooks/multimail/doc/gitolite.rst\n@@ -46,6 +46,15 @@ and add::\n       config multimailhook.mailingList = # Where emails should be sent\n       config multimailhook.from = # From address to use\n \n+Note that by default, gitolite forbids ``<`` and ``>`` in variable\n+values (for security/paranoia reasons, see\n+`compensating for UNSAFE_PATT\n+<http://gitolite.com/gitolite/git-config/index.html#compensating-for-unsafe95patt>`__\n+in gitolite's documentation for explanations and a way to disable\n+this). As a consequence, you will not be able to use ``First Last\n+<First.Last@example.com>`` as recipient email, but specifying\n+``First.Last@example.com`` alone works.\n+\n Obviously, you can customize all parameters on a per-repository basis by\n adding these ``config multimailhook.*`` lines in the section\n corresponding to a repository or set of repositories.\ndiff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py\nindex 73fdda6b14..8823399e75 100755\n--- a/contrib/hooks/multimail/git_multimail.py\n+++ b/contrib/hooks/multimail/git_multimail.py\n@@ -1,6 +1,6 @@\n #! /usr/bin/env python\n \n-__version__ = '1.4.0'\n+__version__ = '1.5.0'\n \n # Copyright (c) 2015-2016 Matthieu Moy and others\n # Copyright (c) 2012-2014 Michael Haggerty and others\n@@ -64,7 +64,9 @@\n     # Python < 2.6 do not have ssl, but that's OK if we don't use it.\n     pass\n import time\n-import cgi\n+\n+import uuid\n+import base64\n \n PYTHON3 = sys.version_info >= (3, 0)\n \n@@ -73,7 +75,7 @@ def all(iterable):\n         for element in iterable:\n             if not element:\n                 return False\n-            return True\n+        return True\n \n \n def is_ascii(s):\n@@ -108,6 +110,12 @@ def read_line(f):\n             return out.decode(sys.getdefaultencoding())\n         except UnicodeEncodeError:\n             return out.decode(ENCODING)\n+\n+    import html\n+\n+    def html_escape(s):\n+        return html.escape(s)\n+\n else:\n     def is_string(s):\n         try:\n@@ -130,6 +138,10 @@ def read_line(f):\n     def next(it):\n         return it.next()\n \n+    import cgi\n+\n+    def html_escape(s):\n+        return cgi.escape(s, True)\n \n try:\n     from email.charset import Charset\n@@ -190,6 +202,7 @@ def next(it):\n Message-ID: %(msgid)s\n From: %(fromaddr)s\n Reply-To: %(reply_to)s\n+Thread-Index: %(thread_index)s\n X-Git-Host: %(fqdn)s\n X-Git-Repo: %(repo_shortname)s\n X-Git-Refname: %(refname)s\n@@ -322,6 +335,7 @@ def next(it):\n Reply-To: %(reply_to)s\n In-Reply-To: %(reply_to_msgid)s\n References: %(reply_to_msgid)s\n+Thread-Index: %(thread_index)s\n X-Git-Host: %(fqdn)s\n X-Git-Repo: %(repo_shortname)s\n X-Git-Refname: %(refname)s\n@@ -763,6 +777,9 @@ def get_summary(self):\n     def __eq__(self, other):\n         return isinstance(other, GitObject) and self.sha1 == other.sha1\n \n+    def __ne__(self, other):\n+        return not self == other\n+\n     def __hash__(self):\n         return hash(self.sha1)\n \n@@ -852,7 +869,7 @@ def expand_lines(self, template, html_escape_val=False, **extra_values):\n         if html_escape_val:\n             for k in values:\n                 if is_string(values[k]):\n-                    values[k] = cgi.escape(values[k], True)\n+                    values[k] = html_escape(values[k])\n         for line in template.splitlines(True):\n             yield line % values\n \n@@ -909,7 +926,7 @@ def generate_email_intro(self, html_escape_val=False):\n \n         raise NotImplementedError()\n \n-    def generate_email_body(self):\n+    def generate_email_body(self, push):\n         \"\"\"Generate the main part of the email body, a line at a time.\n \n         The text in the body might be truncated after a specified\n@@ -936,7 +953,7 @@ def _wrap_for_html(self, lines):\n             yield \"<pre style='margin:0'>\\n\"\n \n             for line in lines:\n-                yield cgi.escape(line)\n+                yield html_escape(line)\n \n             yield '</pre>\\n'\n         else:\n@@ -1011,7 +1028,7 @@ def generate_email(self, push, body_filter=None, extra_header_values={}):\n                     fgcolor = '404040'\n \n                 # Chop the trailing LF, we don't want it inside <pre>.\n-                line = cgi.escape(line[:-1])\n+                line = html_escape(line[:-1])\n \n                 if bgcolor or fgcolor:\n                     style = 'display:block; white-space:pre;'\n@@ -1060,6 +1077,10 @@ def __init__(self, reference_change, rev, num, tot):\n         self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1])\n         self.recipients = self.environment.get_revision_recipients(self)\n \n+        # -s is short for --no-patch, but -s works on older git's (e.g. 1.7)\n+        self.parents = read_git_lines(['show', '-s', '--format=%P',\n+                                      self.rev.sha1])[0].split()\n+\n         self.cc_recipients = ''\n         if self.environment.get_scancommitforcc():\n             self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients())\n@@ -1090,6 +1111,7 @@ def _compute_values(self):\n             oneline = oneline[:max_subject_length - 6] + ' [...]'\n \n         values['rev'] = self.rev.sha1\n+        values['parents'] = ' '.join(self.parents)\n         values['rev_short'] = self.rev.short\n         values['change_type'] = self.change_type\n         values['refname'] = self.refname\n@@ -1097,6 +1119,7 @@ def _compute_values(self):\n         values['short_refname'] = self.reference_change.short_refname\n         values['refname_type'] = self.reference_change.refname_type\n         values['reply_to_msgid'] = self.reference_change.msgid\n+        values['thread_index'] = self.reference_change.thread_index\n         values['num'] = self.num\n         values['tot'] = self.tot\n         values['recipients'] = self.recipients\n@@ -1244,6 +1267,23 @@ def create(environment, oldrev, newrev, refname):\n             old=old, new=new, rev=rev,\n             )\n \n+    @staticmethod\n+    def make_thread_index():\n+        \"\"\"Return a string appropriate for the Thread-Index header,\n+        needed by MS Outlook to get threading right.\n+\n+        The format is (base64-encoded):\n+        - 1 byte must be 1\n+        - 5 bytes encode a date (hardcoded here)\n+        - 16 bytes for a globally unique identifier\n+\n+        FIXME: Unfortunately, even with the Thread-Index field, MS\n+        Outlook doesn't seem to do the threading reliably (see\n+        https://github.com/git-multimail/git-multimail/pull/194).\n+        \"\"\"\n+        thread_index = b'\\x01\\x00\\x00\\x12\\x34\\x56' + uuid.uuid4().bytes\n+        return base64.standard_b64encode(thread_index).decode('ascii')\n+\n     def __init__(self, environment, refname, short_refname, old, new, rev):\n         Change.__init__(self, environment)\n         self.change_type = {\n@@ -1257,6 +1297,7 @@ def __init__(self, environment, refname, short_refname, old, new, rev):\n         self.new = new\n         self.rev = rev\n         self.msgid = make_msgid()\n+        self.thread_index = self.make_thread_index()\n         self.diffopts = environment.diffopts\n         self.graphopts = environment.graphopts\n         self.logopts = environment.logopts\n@@ -1276,6 +1317,7 @@ def _compute_values(self):\n         values['refname'] = self.refname\n         values['short_refname'] = self.short_refname\n         values['msgid'] = self.msgid\n+        values['thread_index'] = self.thread_index\n         values['recipients'] = self.recipients\n         values['oldrev'] = str(self.old)\n         values['oldrev_short'] = self.old.short\n@@ -1941,6 +1983,9 @@ class Mailer(object):\n     def __init__(self, environment):\n         self.environment = environment\n \n+    def close(self):\n+        pass\n+\n     def send(self, lines, to_addrs):\n         \"\"\"Send an email consisting of lines.\n \n@@ -2054,6 +2099,7 @@ def __init__(self, environment,\n         self.username = smtpuser\n         self.password = smtppass\n         self.smtpcacerts = smtpcacerts\n+        self.loggedin = False\n         try:\n             def call(klass, server, timeout):\n                 try:\n@@ -2130,20 +2176,30 @@ def call(klass, server, timeout):\n                 % (self.smtpserver, sys.exc_info()[1]))\n             sys.exit(1)\n \n-    def __del__(self):\n+    def close(self):\n         if hasattr(self, 'smtp'):\n             self.smtp.quit()\n             del self.smtp\n \n+    def __del__(self):\n+        self.close()\n+\n     def send(self, lines, to_addrs):\n         try:\n             if self.username or self.password:\n-                self.smtp.login(self.username, self.password)\n+                if not self.loggedin:\n+                    self.smtp.login(self.username, self.password)\n+                    self.loggedin = True\n             msg = ''.join(lines)\n             # turn comma-separated list into Python list if needed.\n             if is_string(to_addrs):\n                 to_addrs = [email for (name, email) in getaddresses([to_addrs])]\n             self.smtp.sendmail(self.envelopesender, to_addrs, msg)\n+        except socket.timeout:\n+            self.environment.get_logger().error(\n+                '*** Error sending email ***\\n'\n+                '*** SMTP server timed out (timeout is %s)\\n'\n+                % self.smtpservertimeout)\n         except smtplib.SMTPResponseException:\n             err = sys.exc_info()[1]\n             self.environment.get_logger().error(\n@@ -2171,7 +2227,8 @@ class OutputMailer(Mailer):\n \n     SEPARATOR = '=' * 75 + '\\n'\n \n-    def __init__(self, f):\n+    def __init__(self, f, environment=None):\n+        super(OutputMailer, self).__init__(environment=environment)\n         self.f = f\n \n     def send(self, lines, to_addrs):\n@@ -2382,6 +2439,7 @@ def __init__(self, osenv=None):\n         self.html_in_footer = False\n         self.commitBrowseURL = None\n         self.maxcommitemails = 500\n+        self.excludemergerevisions = False\n         self.diffopts = ['--stat', '--summary', '--find-copies-harder']\n         self.graphopts = ['--oneline', '--decorate']\n         self.logopts = []\n@@ -2621,6 +2679,8 @@ def __init__(self, config, **kw):\n \n         self.commitBrowseURL = config.get('commitBrowseURL')\n \n+        self.excludemergerevisions = config.get('excludeMergeRevisions')\n+\n         maxcommitemails = config.get('maxcommitemails')\n         if maxcommitemails is not None:\n             try:\n@@ -3152,7 +3212,10 @@ def get_pusher(self):\n         return self.osenv.get('GL_USER', 'unknown user')\n \n \n-class GitoliteEnvironmentLowPrecMixin(Environment):\n+class GitoliteEnvironmentLowPrecMixin(\n+        ConfigEnvironmentMixin,\n+        Environment):\n+\n     def get_repo_shortname(self):\n         # The gitolite environment variable $GL_REPO is a pretty good\n         # repo_shortname (though it's probably not as good as a value\n@@ -3162,6 +3225,16 @@ def get_repo_shortname(self):\n             super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname()\n             )\n \n+    @staticmethod\n+    def _compile_regex(re_template):\n+        return (\n+            re.compile(re_template % x)\n+            for x in (\n+                r'BEGIN\\s+USER\\s+EMAILS',\n+                r'([^\\s]+)\\s+(.*)',\n+                r'END\\s+USER\\s+EMAILS',\n+                ))\n+\n     def get_fromaddr(self, change=None):\n         GL_USER = self.osenv.get('GL_USER')\n         if GL_USER is not None:\n@@ -3174,18 +3247,42 @@ def get_fromaddr(self, change=None):\n             GL_CONF = self.osenv.get(\n                 'GL_CONF',\n                 os.path.join(GL_ADMINDIR, 'conf', 'gitolite.conf'))\n+\n+            mailaddress_map = self.config.get('MailaddressMap')\n+            # If relative, consider relative to GL_CONF:\n+            if mailaddress_map:\n+                mailaddress_map = os.path.join(os.path.dirname(GL_CONF),\n+                                               mailaddress_map)\n+                if os.path.isfile(mailaddress_map):\n+                    f = open(mailaddress_map, 'rU')\n+                    try:\n+                        # Leading '#' is optional\n+                        re_begin, re_user, re_end = self._compile_regex(\n+                            r'^(?:\\s*#)?\\s*%s\\s*$')\n+                        for l in f:\n+                            l = l.rstrip('\\n')\n+                            if re_begin.match(l) or re_end.match(l):\n+                                continue  # Ignore these lines\n+                            m = re_user.match(l)\n+                            if m:\n+                                if m.group(1) == GL_USER:\n+                                    return m.group(2)\n+                                else:\n+                                    continue  # Not this user, but not an error\n+                            raise ConfigurationException(\n+                                \"Syntax error in mail address map.\\n\"\n+                                \"Check file {}.\\n\"\n+                                \"Line: {}\".format(mailaddress_map, l))\n+\n+                    finally:\n+                        f.close()\n+\n             if os.path.isfile(GL_CONF):\n                 f = open(GL_CONF, 'rU')\n                 try:\n                     in_user_emails_section = False\n-                    re_template = r'^\\s*#\\s*%s\\s*$'\n-                    re_begin, re_user, re_end = (\n-                        re.compile(re_template % x)\n-                        for x in (\n-                            r'BEGIN\\s+USER\\s+EMAILS',\n-                            re.escape(GL_USER) + r'\\s+(.*)',\n-                            r'END\\s+USER\\s+EMAILS',\n-                            ))\n+                    re_begin, re_user, re_end = self._compile_regex(\n+                        r'^\\s*#\\s*%s\\s*$')\n                     for l in f:\n                         l = l.rstrip('\\n')\n                         if not in_user_emails_section:\n@@ -3195,8 +3292,8 @@ def get_fromaddr(self, change=None):\n                         if re_end.match(l):\n                             break\n                         m = re_user.match(l)\n-                        if m:\n-                            return m.group(1)\n+                        if m and m.group(1) == GL_USER:\n+                            return m.group(2)\n                 finally:\n                     f.close()\n         return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change)\n@@ -3228,7 +3325,7 @@ def __init__(self, user=None, repo=None, **kw):\n         self.__repo = repo\n \n     def get_pusher(self):\n-        return re.match('(.*?)\\s*<', self.__user).group(1)\n+        return re.match(r'(.*?)\\s*<', self.__user).group(1)\n \n     def get_pusher_email(self):\n         return self.__user\n@@ -3262,7 +3359,7 @@ def get_pusher(self):\n             if self.__submitter.find('<') != -1:\n                 # Submitter has a configured email, we transformed\n                 # __submitter into an RFC 2822 string already.\n-                return re.match('(.*?)\\s*<', self.__submitter).group(1)\n+                return re.match(r'(.*?)\\s*<', self.__submitter).group(1)\n             else:\n                 # Submitter has no configured email, it's just his name.\n                 return self.__submitter\n@@ -3615,6 +3712,9 @@ def send_emails(self, mailer, body_filter=None):\n \n             for (num, sha1) in enumerate(sha1s):\n                 rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s))\n+                if len(rev.parents) > 1 and change.environment.excludemergerevisions:\n+                    # skipping a merge commit\n+                    continue\n                 if not rev.recipients and rev.cc_recipients:\n                     change.environment.log_msg('*** Replacing Cc: with To:')\n                     rev.recipients = rev.cc_recipients\n@@ -3664,11 +3764,14 @@ def run_as_post_receive_hook(environment, mailer):\n         changes.append(\n             ReferenceChange.create(environment, oldrev, newrev, refname)\n             )\n-    if changes:\n-        push = Push(environment, changes)\n+    if not changes:\n+        mailer.close()\n+        return\n+    push = Push(environment, changes)\n+    try:\n         push.send_emails(mailer, body_filter=environment.filter_body)\n-    if hasattr(mailer, '__del__'):\n-        mailer.__del__()\n+    finally:\n+        mailer.close()\n \n \n def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):\n@@ -3687,10 +3790,14 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=\n             refname,\n             ),\n         ]\n+    if not changes:\n+        mailer.close()\n+        return\n     push = Push(environment, changes, force_send)\n-    push.send_emails(mailer, body_filter=environment.filter_body)\n-    if hasattr(mailer, '__del__'):\n-        mailer.__del__()\n+    try:\n+        push.send_emails(mailer, body_filter=environment.filter_body)\n+    finally:\n+        mailer.close()\n \n \n def check_ref_filter(environment):\n@@ -3860,7 +3967,7 @@ def build_environment_klass(env_name):\n         low_prec_mixin = known_env['lowprec']\n         environment_mixins.append(low_prec_mixin)\n     environment_mixins.append(Environment)\n-    klass_name = env_name.capitalize() + 'Environement'\n+    klass_name = env_name.capitalize() + 'Environment'\n     environment_klass = type(\n         klass_name,\n         tuple(environment_mixins),\n@@ -4057,21 +4164,21 @@ def flush(self):\n                 environment, 'git_multimail.error', environment.error_log_file, logging.ERROR)\n             self.loggers.append(error_log_file)\n \n-    def info(self, msg):\n+    def info(self, msg, *args, **kwargs):\n         for l in self.loggers:\n-            l.info(msg)\n+            l.info(msg, *args, **kwargs)\n \n-    def debug(self, msg):\n+    def debug(self, msg, *args, **kwargs):\n         for l in self.loggers:\n-            l.debug(msg)\n+            l.debug(msg, *args, **kwargs)\n \n-    def warning(self, msg):\n+    def warning(self, msg, *args, **kwargs):\n         for l in self.loggers:\n-            l.warning(msg)\n+            l.warning(msg, *args, **kwargs)\n \n-    def error(self, msg):\n+    def error(self, msg, *args, **kwargs):\n         for l in self.loggers:\n-            l.error(msg)\n+            l.error(msg, *args, **kwargs)\n \n \n def main(args):\n@@ -4189,7 +4296,7 @@ def main(args):\n             show_env(environment, sys.stderr)\n \n         if options.stdout or environment.stdout:\n-            mailer = OutputMailer(sys.stdout)\n+            mailer = OutputMailer(sys.stdout, environment)\n         else:\n             mailer = choose_mailer(config, environment)\n \n@@ -4234,5 +4341,6 @@ def main(args):\n             sys.stderr.write(msg)\n         sys.exit(1)\n \n+\n if __name__ == '__main__':\n     main(sys.argv[1:])\ndiff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config\nindex 992657bbdc..241ba22fa3 100755\n--- a/contrib/hooks/multimail/migrate-mailhook-config\n+++ b/contrib/hooks/multimail/migrate-mailhook-config\n@@ -110,11 +110,12 @@ def is_section_empty(section, local):\n \n     try:\n         read_output(\n-            ['git', 'config']\n-            + local_option\n-            + ['--get-regexp', '^%s\\.' % (section,)]\n+            ['git', 'config'] +\n+            local_option +\n+            ['--get-regexp', '^%s\\.' % (section,)]\n             )\n-    except CommandError, e:\n+    except CommandError:\n+        t, e, traceback = sys.exc_info()\n         if e.retcode == 1:\n             # This means that no settings were found.\n             return True\n@@ -188,7 +189,9 @@ def migrate_config(strict=False, retain=False, overwrite=False):\n             sys.stderr.write(\n                 '...copying \"%s.%s\" to \"%s.%s\"\\n' % (old.section, name, new.section, name)\n                 )\n-            new.set_recipients(name, old.get_recipients(name))\n+            old_recipients = old.get_all(name, default=None)\n+            old_recipients = ', '.join(o.strip() for o in old_recipients)\n+            new.set_recipients(name, old_recipients)\n \n     if strict:\n         sys.stderr.write(\ndiff --git a/contrib/hooks/multimail/post-receive.example b/contrib/hooks/multimail/post-receive.example\nindex 1ea113d274..b9bb11834e 100755\n--- a/contrib/hooks/multimail/post-receive.example\n+++ b/contrib/hooks/multimail/post-receive.example\n@@ -30,7 +30,6 @@ script's behavior could be changed or customized.\n \"\"\"\n \n import sys\n-import os\n \n # If necessary, add the path to the directory containing\n # git_multimail.py to the Python path as follows.  (This is not\n@@ -86,6 +85,7 @@ mailer = git_multimail.choose_mailer(config, environment)\n \n # Use Python's smtplib to send emails.  Both arguments are required.\n #mailer = git_multimail.SMTPMailer(\n+#    environment=environment,\n #    envelopesender='git-repo@example.com',\n #    # The smtpserver argument can also include a port number; e.g.,\n #    #     smtpserver='mail.example.com:25'\n-- \n2.20.1.98.gecbdaf0\n\n"}]}