git-multimail: update to release 1.3.0

The changes are described in CHANGES. Contributions-by: Matthieu Moy <Matthieu.Moy@imag.fr> Contributions-by: Stefan Tatschner <rumpelsepp@sevenbyte.org> Contributions-by: Simon P <simon.git@le-huit.fr> Contributions-by: Leander Hasty <leander@1stplayable.com> Signed-off-by: Matthieu Moy <Matthieu.Moy@imag.fr> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matthieu Moy committed May 3, 2016 at 10:31 UTC 4453d76c6abf28a3945a336c5321ea05f07bd8b6
8 files changed +476 -143
contrib/hooks/multimail/CHANGES
+35
@@ -1,3 +1,38 @@
1 +Release 1.3.0
2 +=============
3 +
4 +* New options multimailhook.htmlInIntro and multimailhook.htmlInFooter
5 + now allow using HTML in the introduction and footer of emails (e.g.
6 + for a more pleasant formatting or to insert a link to the commit on
7 + a web interface).
8 +
9 +* A new option multimailhook.commitBrowseURL gives a simpler (and less
10 + flexible) way to add a link to a web interface for commit emails
11 + than multimailhook.htmlInIntro and multimailhook.htmlInFooter.
12 +
13 +* A new public function config.add_config_parameters was added to
14 + allow custom hooks to set specific Git configuration variables
15 + without modifying the configuration files. See an example in
16 + post-receive.example.
17 +
18 +* Error handling for SMTP has been improved (we used to print Python
19 + backtraces for legitimate errors).
20 +
21 +* The SMTP mailer can now check TLS certificates when the newly added
22 + configuration variable multimailhook.smtpCACerts.
23 +
24 +* Python 3 portability has been improved.
25 +
26 +* The documentation's formatting has been improved.
27 +
28 +* The testsuite has been improved (we now use pyflakes to check for
29 + errors in the code).
30 +
31 +This version has been tested with Python 2.4 and 2.6 to 3.5, and Git
32 +v1.7.10-406-gdc801e7, 2.1.4 and 2.8.1.339.g3ad15fd.
33 +
34 +No change since 1.3 RC1.
35 +
36 Release 1.2.0
37 =============
38
contrib/hooks/multimail/CONTRIBUTING.rst
+10 -7
@@ -1,3 +1,6 @@
1 +Contributing
2 +============
3 +
4 git-multimail is an open-source project, built by volunteers. We would
5 welcome your help!
6
@@ -6,9 +9,7 @@ and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
9
10 Please note that although a copy of git-multimail is distributed in
11 the "contrib" section of the main Git project, development takes place
9 -in a separate git-multimail repository on GitHub:
10 -
11 - https://github.com/git-multimail/git-multimail
12 +in a separate `git-multimail repository on GitHub`_.
13
14 Whenever enough changes to git-multimail have accumulated, a new
15 code-drop of git-multimail will be submitted for inclusion in the Git
@@ -21,10 +22,12 @@ to the maintainers). Please sign off your patches as per the `Git
22 project practice
23 <https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.
24
24 -General discussion of git-multimail can take place on the main Git
25 -mailing list,
26 -
27 - git@vger.kernel.org
25 +General discussion of git-multimail can take place on the main `Git
26 +mailing list`_.
27
28 Please CC emails regarding git-multimail to the maintainers so that we
29 don't overlook them.
30 +
31 +
32 +.. _`git-multimail repository on GitHub`: https://github.com/git-multimail/git-multimail
33 +.. _`Git mailing list`: git@vger.kernel.org
contrib/hooks/multimail/README
+116 -99
@@ -1,5 +1,5 @@
1 -git-multimail (version 1.2.0)
2 -=============================
1 +git-multimail 1.3.0
2 +===================
3
4 .. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
5 :target: https://travis-ci.org/git-multimail/git-multimail
@@ -127,6 +127,13 @@ changes of this type, please consider sharing them with the
127 community.)
128
129
130 +Troubleshooting/FAQ
131 +-------------------
132 +
133 +Please read `<doc/troubleshooting.rst>`__ for frequently asked
134 +questions and common issues with git-multimail.
135 +
136 +
137 Configuration
138 -------------
139
@@ -134,19 +141,16 @@ By default, git-multimail mostly takes its configuration from the
141 following ``git config`` settings:
142
143 multimailhook.environment
137 -
144 This describes the general environment of the repository. In most
145 cases, you do not need to specify a value for this variable:
146 `git-multimail` will autodetect which environment to use.
147 Currently supported values:
148
143 - * generic
144 -
149 + generic
150 the username of the pusher is read from $USER or $USERNAME and
151 the repository name is derived from the repository's path.
152
148 - * gitolite
149 -
153 + gitolite
154 the username of the pusher is read from $GL_USER, the repository
155 name is read from $GL_REPO, and the From: header value is
156 optionally read from gitolite.conf (see multimailhook.from).
@@ -154,8 +158,7 @@ multimailhook.environment
158 For more information about gitolite and git-multimail, read
159 `<doc/gitolite.rst>`__
160
157 - * stash
158 -
161 + stash
162 Environment to use when ``git-multimail`` is ran as an Atlassian
163 BitBucket Server (formerly known as Atlassian Stash) hook.
164
@@ -169,8 +172,7 @@ multimailhook.environment
172 and repo come from these two command line flags, which must be
173 specified.
174
172 - * gerrit
173 -
175 + gerrit
176 Environment to use when ``git-multimail`` is ran as a
177 ``ref-updated`` Gerrit hook.
178
@@ -205,14 +207,12 @@ multimailhook.environment
207 * If none of the above apply, then ``generic`` is used.
208
209 multimailhook.repoName
208 -
210 A short name of this Git repository, to be used in various places
211 in the notification email text. The default is to use $GL_REPO
212 for gitolite repositories, or otherwise to derive this value from
213 the repository path name.
214
215 multimailhook.mailingList
215 -
216 The list of email addresses to which notification emails should be
217 sent, as RFC 2822 email addresses separated by commas. This
218 configuration option can be multivalued. Leave it unset or set it
@@ -221,7 +221,6 @@ multimailhook.mailingList
221 specific types of notification email.
222
223 multimailhook.refchangeList
224 -
224 The list of email addresses to which summary emails about
225 reference changes should be sent, as RFC 2822 email addresses
226 separated by commas. This configuration option can be
@@ -231,7 +230,6 @@ multimailhook.refchangeList
230 multimailhook.mailingList is set.
231
232 multimailhook.announceList
234 -
233 The list of email addresses to which emails about new annotated
234 tags should be sent, as RFC 2822 email addresses separated by
235 commas. This configuration option can be multivalued. The
@@ -241,7 +239,6 @@ multimailhook.announceList
239 even if one of the other values is set.
240
241 multimailhook.commitList
244 -
242 The list of email addresses to which emails about individual new
243 commits should be sent, as RFC 2822 email addresses separated by
244 commas. This configuration option can be multivalued. The
@@ -251,7 +248,6 @@ multimailhook.commitList
248 multimailhook.mailingList is set.
249
250 multimailhook.announceShortlog
254 -
251 If this option is set to true, then emails about changes to
252 annotated tags include a shortlog of changes since the previous
253 tag. This can be useful if the annotated tags represent releases;
@@ -261,7 +257,6 @@ multimailhook.announceShortlog
257 rather than useful. Default is false.
258
259 multimailhook.commitEmailFormat
264 -
260 The format of email messages for the individual commits, can be "text" or
261 "html". In the latter case, the emails will include diffs using colorized
262 HTML instead of plain text used by default. Note that this currently the
@@ -274,8 +269,43 @@ multimailhook.commitEmailFormat
269 the message starting with ``+++`` or ``---`` colored in red or
270 green).
271
277 -multimailhook.refchangeShowGraph
272 + By default, all the message is HTML-escaped. See
273 + ``multimailhook.htmlInIntro`` to change this behavior.
274 +
275 +multimailhook.commitBrowseURL
276 + Used to generate a link to an online repository browser in commit
277 + emails. This variable must be a string. Format directives like
278 + ``%(<variable>)s`` will be expanded the same way as template
279 + strings. In particular, ``%(id)s`` will be replaced by the full
280 + Git commit identifier (40-chars hexadecimal).
281 +
282 + If the string does not contain any format directive, then
283 + ``%(id)s`` will be automatically added to the string. If you don't
284 + want ``%(id)s`` to be automatically added, use the empty format
285 + directive ``%()s`` anywhere in the string.
286 +
287 + For example, a suitable value for the git-multimail project itself
288 + would be
289 + ``https://github.com/git-multimail/git-multimail/commit/%(id)s``.
290 +
291 +multimailhook.htmlInIntro, multimailhook.htmlInFooter
292 + When generating an HTML message, git-multimail escapes any HTML
293 + sequence by default. This means that if a template contains HTML
294 + like ``<a href="foo">link</a>``, the reader will see the HTML
295 + source code and not a proper link.
296 +
297 + Set ``multimailhook.htmlInIntro`` to true to allow writting HTML
298 + formatting in introduction templates. Similarly, set
299 + ``multimailhook.htmlInFooter`` for HTML in the footer.
300
301 + Variables expanded in the template are still escaped. For example,
302 + if a repository's path contains a ``<``, it will be rendered as
303 + such in the message.
304 +
305 + Read `<doc/customizing-emails.rst>`__ for more details and
306 + examples.
307 +
308 +multimailhook.refchangeShowGraph
309 If this option is set to true, then summary emails about reference
310 changes will additionally include:
311
@@ -287,7 +317,6 @@ multimailhook.refchangeShowGraph
317 specified in graphOpts. The default is false.
318
319 multimailhook.refchangeShowLog
290 -
320 If this option is set to true, then summary emails about reference
321 changes will include a detailed log of the added commits in
322 addition to the one line summary. The log is generated by running
@@ -295,71 +324,80 @@ multimailhook.refchangeShowLog
324 Default is false.
325
326 multimailhook.mailer
298 -
327 This option changes the way emails are sent. Accepted values are:
328
301 - - sendmail (the default): use the command ``/usr/sbin/sendmail`` or
329 + * **sendmail (the default)**: use the command ``/usr/sbin/sendmail`` or
330 ``/usr/lib/sendmail`` (or sendmailCommand, if configured). This
331 mode can be further customized via the following options:
332
305 - * multimailhook.sendmailCommand
306 -
307 - The command used by mailer ``sendmail`` to send emails. Shell
308 - quoting is allowed in the value of this setting, but remember that
309 - Git requires double-quotes to be escaped; e.g.::
333 + multimailhook.sendmailCommand
334 + The command used by mailer ``sendmail`` to send emails. Shell
335 + quoting is allowed in the value of this setting, but remember that
336 + Git requires double-quotes to be escaped; e.g.::
337
311 - git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"'
338 + git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"'
339
313 - Default is '/usr/sbin/sendmail -oi -t' or
314 - '/usr/lib/sendmail -oi -t' (depending on which file is
315 - present and executable).
340 + Default is '/usr/sbin/sendmail -oi -t' or
341 + '/usr/lib/sendmail -oi -t' (depending on which file is
342 + present and executable).
343
317 - * multimailhook.envelopeSender
344 + multimailhook.envelopeSender
345 + If set then pass this value to sendmail via the -f option to set
346 + the envelope sender address.
347
319 - If set then pass this value to sendmail via the -f option to set
320 - the envelope sender address.
321 -
322 - - smtp: use Python's smtplib. This is useful when the sendmail
348 + * **smtp**: use Python's smtplib. This is useful when the sendmail
349 command is not available on the system. This mode can be
350 further customized via the following options:
351
326 - * multimailhook.smtpServer
327 -
328 - The name of the SMTP server to connect to. The value can
329 - also include a colon and a port number; e.g.,
330 - ``mail.example.com:25``. Default is 'localhost' using port 25.
331 -
332 - * multimailhook.smtpUser
333 - * multimailhook.smtpPass
334 -
335 - Server username and password. Required if smtpEncryption is 'ssl'.
336 - Note that the username and password currently need to be
337 - set cleartext in the configuration file, which is not
338 - recommended. If you need to use this option, be sure your
339 - configuration file is read-only.
352 + multimailhook.smtpServer
353 + The name of the SMTP server to connect to. The value can
354 + also include a colon and a port number; e.g.,
355 + ``mail.example.com:25``. Default is 'localhost' using port 25.
356
341 - * multimailhook.envelopeSender
357 + multimailhook.smtpUser, multimailhook.smtpPass
358 + Server username and password. Required if smtpEncryption is 'ssl'.
359 + Note that the username and password currently need to be
360 + set cleartext in the configuration file, which is not
361 + recommended. If you need to use this option, be sure your
362 + configuration file is read-only.
363
364 + multimailhook.envelopeSender
365 The sender address to be passed to the SMTP server. If
366 unset, then the value of multimailhook.from is used.
367
346 - * multimailhook.smtpServerTimeout
347 -
368 + multimailhook.smtpServerTimeout
369 Timeout in seconds.
370
350 - * multimailhook.smtpEncryption
351 -
352 - Set the security type. Allowed values: none, ssl, tls.
353 - Default=none.
354 -
355 - * multimailhook.smtpServerDebugLevel
356 -
371 + multimailhook.smtpEncryption
372 + Set the security type. Allowed values: ``none``, ``ssl``, ``tls`` (starttls).
373 + Default is ``none``.
374 +
375 + multimailhook.smtpCACerts
376 + Set the path to a list of trusted CA certificate to verify the
377 + server certificate, only supported when ``smtpEncryption`` is
378 + ``tls``. If unset or empty, the server certificate is not
379 + verified. If it targets a file containing a list of trusted CA
380 + certificates (PEM format) these CAs will be used to verify the
381 + server certificate. For debian, you can set
382 + ``/etc/ssl/certs/ca-certificates.crt`` for using the system
383 + trusted CAs. For self-signed server, you can add your server
384 + certificate to the system store::
385 +
386 + cd /usr/local/share/ca-certificates/
387 + openssl s_client -starttls smtp \
388 + -connect mail.example.net:587 -showcerts \
389 + </dev/null 2>/dev/null \
390 + | openssl x509 -outform PEM >mail.example.net.crt
391 + update-ca-certificates
392 +
393 + and used the updated ``/etc/ssl/certs/ca-certificates.crt``. Or
394 + directly use your ``/path/to/mail.example.net.crt``. Default is
395 + unset.
396 +
397 + multimailhook.smtpServerDebugLevel
398 Integer number. Set to greater than 0 to activate debugging.
399
359 -multimailhook.from
360 -multimailhook.fromCommit
361 -multimailhook.fromRefchange
362 -
400 +multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange
401 If set, use this value in the From: field of generated emails.
402 ``fromCommit`` is used for commit emails, ``fromRefchange`` is
403 used for refchange emails, and ``from`` is used as fall-back in
@@ -372,7 +410,7 @@ multimailhook.fromRefchange
410 - The value ``pusher``, in which case the pusher's address (if
411 available) will be used.
412
375 - - The value ``author`` (meaningful only for replyToCommit), in which
413 + - The value ``author`` (meaningful only for ``fromCommit``), in which
414 case the commit author's address will be used.
415
416 If config values are unset, the value of the From: header is
@@ -396,14 +434,12 @@ multimailhook.fromRefchange
434 3. Use the value of multimailhook.envelopeSender.
435
436 multimailhook.administrator
399 -
437 The name and/or email address of the administrator of the Git
438 repository; used in FOOTER_TEMPLATE. Default is
439 multimailhook.envelopesender if it is set; otherwise a generic
440 string is used.
441
442 multimailhook.emailPrefix
406 -
443 All emails have this string prepended to their subjects, to aid
444 email filtering (though filtering based on the X-Git-* email
445 headers is probably more robust). Default is the short name of
@@ -411,16 +447,14 @@ multimailhook.emailPrefix
447 value to the empty string to suppress the email prefix.
448
449 multimailhook.emailMaxLines
414 -
450 The maximum number of lines that should be included in the body of
451 a generated email. If not specified, there is no limit. Lines
452 beyond the limit are suppressed and counted, and a final line is
453 added indicating the number of suppressed lines.
454
455 multimailhook.emailMaxLineLength
421 -
456 The maximum length of a line in the email body. Lines longer than
423 - this limit are truncated to this length with a trailing `` [...]``
457 + this limit are truncated to this length with a trailing ``[...]``
458 added to indicate the missing text. The default is 500, because
459 (a) diffs with longer lines are probably from binary files, for
460 which a diff is useless, and (b) even if a text file has such long
@@ -428,7 +462,6 @@ multimailhook.emailMaxLineLength
462 truncation, set this option to 0.
463
464 multimailhook.maxCommitEmails
431 -
465 The maximum number of commit emails to send for a given change.
466 When the number of patches is larger that this value, only the
467 summary refchange email is sent. This can avoid accidental
@@ -436,14 +469,12 @@ multimailhook.maxCommitEmails
469 emails limit, set this option to 0. The default is 500.
470
471 multimailhook.emailStrictUTF8
439 -
472 If this boolean option is set to `true`, then the main part of the
473 email body is forced to be valid UTF-8. Any characters that are
474 not valid UTF-8 are converted to the Unicode replacement
475 character, U+FFFD. The default is `true`.
476
477 multimailhook.diffOpts
446 -
478 Options passed to ``git diff-tree`` when generating the summary
479 information for ReferenceChange emails. Default is ``--stat
480 --summary --find-copies-harder``. Add -p to those options to
@@ -452,7 +483,6 @@ multimailhook.diffOpts
483 details.
484
485 multimailhook.graphOpts
455 -
486 Options passed to ``git log --graph`` when generating graphs for the
487 reference change summary emails (used only if refchangeShowGraph
488 is true). The default is '--oneline --decorate'.
@@ -460,7 +490,6 @@ multimailhook.graphOpts
490 Shell quoting is allowed; see logOpts for details.
491
492 multimailhook.logOpts
463 -
493 Options passed to ``git log`` to generate additional info for
494 reference change emails (used only if refchangeShowLog is set).
495 For example, adding -p will show each commit's complete diff. The
@@ -479,7 +508,6 @@ multimailhook.logOpts
508 logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
509
510 multimailhook.commitLogOpts
482 -
511 Options passed to ``git log`` to generate additional info for
512 revision change emails. For example, adding --ignore-all-spaces
513 will suppress whitespace changes. The default options are ``-C
@@ -487,26 +515,21 @@ multimailhook.commitLogOpts
515 multimailhook.logOpts for details.
516
517 multimailhook.dateSubstitute
490 -
518 String to use as a substitute for ``Date:`` in the output of ``git
519 log`` while formatting commit messages. This is usefull to avoid
520 emitting a line that can be interpreted by mailers as the start of
521 a cited message (Zimbra webmail in particular). Defaults to
495 - ``CommitDate: ``. Set to an empty string or ``none`` to deactivate
522 + ``CommitDate:``. Set to an empty string or ``none`` to deactivate
523 the behavior.
524
525 multimailhook.emailDomain
499 -
526 Domain name appended to the username of the person doing the push
527 to convert it into an email address
528 (via ``"%s@%s" % (username, emaildomain)``). More complicated
529 schemes can be implemented by overriding Environment and
530 overriding its get_pusher_email() method.
531
506 -multimailhook.replyTo
507 -multimailhook.replyToCommit
508 -multimailhook.replyToRefchange
509 -
532 +multimailhook.replyTo, multimailhook.replyToCommit, multimailhook.replyToRefchange
533 Addresses to use in the Reply-To: field for commit emails
534 (replyToCommit) and refchange emails (replyToRefchange).
535 multimailhook.replyTo is used as default when replyToCommit or
@@ -519,32 +542,24 @@ multimailhook.replyToRefchange
542 commit emails.
543
544 multimailhook.quiet
522 -
545 Do not output the list of email recipients from the hook
546
547 multimailhook.stdout
526 -
548 For debugging, send emails to stdout rather than to the
549 mailer. Equivalent to the --stdout command line option
550
551 multimailhook.scanCommitForCc
531 -
552 If this option is set to true, than recipients from lines in commit body
553 that starts with ``CC:`` will be added to CC list.
554 Default: false
555
556 multimailhook.combineWhenSingleCommit
537 -
557 If this option is set to true and a single new commit is pushed to
558 a branch, combine the summary and commit email messages into a
559 single email.
560 Default: true
561
543 -multimailhook.refFilterInclusionRegex
544 -multimailhook.refFilterExclusionRegex
545 -multimailhook.refFilterDoSendRegex
546 -multimailhook.refFilterDontSendRegex
547 -
562 +multimailhook.refFilterInclusionRegex, multimailhook.refFilterExclusionRegex, multimailhook.refFilterDoSendRegex, multimailhook.refFilterDontSendRegex
563 **Warning:** these options are experimental. They should work, but
564 the user-interface is not stable yet (in particular, the option
565 names may change). If you want to participate in stabilizing the
@@ -626,14 +641,16 @@ git-multimail is mostly customized via an "environment" that describes
641 the local environment in which Git is running. Two types of
642 environment are built in:
643
629 -* GenericEnvironment: a stand-alone Git repository.
644 +GenericEnvironment
645 + a stand-alone Git repository.
646
631 -* GitoliteEnvironment: a Git repository that is managed by gitolite
632 - [3]_. For such repositories, the identity of the pusher is read from
633 - environment variable $GL_USER, the name of the repository is read
634 - from $GL_REPO (if it is not overridden by multimailhook.reponame),
635 - and the From: header value is optionally read from gitolite.conf
636 - (see multimailhook.from).
647 +GitoliteEnvironment
648 + a Git repository that is managed by gitolite
649 + [3]_. For such repositories, the identity of the pusher is read from
650 + environment variable $GL_USER, the name of the repository is read
651 + from $GL_REPO (if it is not overridden by multimailhook.reponame),
652 + and the From: header value is optionally read from gitolite.conf
653 + (see multimailhook.from).
654
655 By default, git-multimail assumes GitoliteEnvironment if $GL_USER and
656 $GL_REPO are set, and otherwise assumes GenericEnvironment.
contrib/hooks/multimail/README.Git
+2 -2
@@ -6,10 +6,10 @@ website:
6 https://github.com/git-multimail/git-multimail
7
8 The version in this directory was obtained from the upstream project
9 -on October 11 2015 and consists of the "git-multimail" subdirectory from
9 +on May 03 2016 and consists of the "git-multimail" subdirectory from
10 revision
11
12 - c0791b9ef5821a746fc3475c25765e640452eaae refs/tags/1.2.0
12 + 26f3ae9f86aa7f8a054ba89235c4d3879f98b03d refs/tags/1.3.0
13
14 Please see the README file in this directory for information about how
15 to report bugs or contribute to git-multimail.
contrib/hooks/multimail/doc/customizing-emails.rst new
+56
@@ -0,0 +1,56 @@
1 +Customizing the content and formatting of emails
2 +================================================
3 +
4 +Overloading template strings
5 +----------------------------
6 +
7 +The content of emails is generated based on template strings defined
8 +in ``git_multimail.py``. You can customize these template strings
9 +without changing the script itself, by defining a Python wrapper
10 +around it. The python wrapper should ``import git_multimail`` and then
11 +override the ``git_multimail.*`` strings like this::
12 +
13 + import sys # needed for sys.argv
14 +
15 + # Import and customize git_multimail:
16 + import git_multimail
17 + git_multimail.REVISION_INTRO_TEMPLATE = """..."""
18 + git_multimail.COMBINED_INTRO_TEMPLATE = git_multimail.REVISION_INTRO_TEMPLATE
19 +
20 + # start git_multimail itself:
21 + git_multimail.main(sys.argv[1:])
22 +
23 +The template strings can use any value already used in the existing
24 +templates (read the source code).
25 +
26 +Using HTML in template strings
27 +------------------------------
28 +
29 +If ``multimailhook.commitEmailFormat`` is set to HTML, then
30 +git-multimail will generate HTML emails for commit notifications. The
31 +log and diff will be formatted automatically by git-multimail. By
32 +default, any HTML special character in the templates will be escaped.
33 +
34 +To use HTML formatting in the introduction of the email, set
35 +``multimailhook.htmlInIntro`` to ``true``. Then, the template can
36 +contain any HTML tags, that will be sent as-is in the email. For
37 +example, to add some formatting and a link to the online commit, use
38 +a format like::
39 +
40 + git_multimail.REVISION_INTRO_TEMPLATE = """\
41 + <span style="color:#808080">This is an automated email from the git hooks/post-receive script.</span><br /><br />
42 +
43 + <strong>%(pusher)s</strong> pushed a commit to %(refname_type)s %(short_refname)s
44 + in repository %(repo_shortname)s.<br />
45 +
46 + <a href="https://github.com/git-multimail/git-multimail/commit/%(newrev)s">View on GitHub</a>.
47 + """
48 +
49 +Note that the values expanded from ``%(variable)s`` in the format
50 +strings will still be escaped.
51 +
52 +For a less flexible but easier to set up way to add a link to commit
53 +emails, see ``multimailhook.commitBrowseURL``.
54 +
55 +Similarly, one can set ``multimailhook.htmlInFooter`` and override any
56 +of the ``*_FOOTER*`` template strings.
contrib/hooks/multimail/doc/troubleshooting.rst new
+44
@@ -0,0 +1,44 @@
1 +Troubleshooting issues with git-multimail: a FAQ
2 +================================================
3 +
4 +Git is not using the right address in the From/To/Reply-To field
5 +----------------------------------------------------------------
6 +
7 +First, make sure that git-multimail actually uses what you think it is
8 +using. A lot happens to your email (especially when posting to a
9 +mailing-list) between the time `git_multimail.py` sends it and the
10 +time it reaches your inbox.
11 +
12 +A simple test (to do on a test repository, do not use in production as
13 +it would disable email sending): change your post-receive hook to call
14 +`git_multimail.py` with the `--stdout` option, and try to push to the
15 +repository. You should see something like::
16 +
17 + Counting objects: 3, done.
18 + Writing objects: 100% (3/3), 263 bytes | 0 bytes/s, done.
19 + Total 3 (delta 0), reused 0 (delta 0)
20 + remote: Sending notification emails to: foo.bar@example.com
21 + remote: ===========================================================================
22 + remote: Date: Mon, 25 Apr 2016 18:39:59 +0200
23 + remote: To: foo.bar@example.com
24 + remote: Subject: [git] branch master updated: foo
25 + remote: MIME-Version: 1.0
26 + remote: Content-Type: text/plain; charset=utf-8
27 + remote: Content-Transfer-Encoding: 8bit
28 + remote: Message-ID: <20160425163959.2311.20498@anie>
29 + remote: From: Auth Or <Foo.Bar@example.com>
30 + remote: Reply-To: Auth Or <Foo.Bar@example.com>
31 + remote: X-Git-Host: example
32 + ...
33 + remote: --
34 + remote: To stop receiving notification emails like this one, please contact
35 + remote: the administrator of this repository.
36 + remote: ===========================================================================
37 + To /path/to/repo
38 + 6278f04..e173f20 master -> master
39 +
40 +Note: this does not include the sender (Return-Path: header), as it is
41 +not part of the message content but passed to the mailer. Some mailer
42 +show the ``Sender:`` field instead of the ``From:`` field (for
43 +example, Zimbra Webmail shows ``From: <sender-field> on behalf of
44 +<from-field>``).
contrib/hooks/multimail/git_multimail.py
+207 -35
@@ -1,6 +1,6 @@
1 #! /usr/bin/env python
2
3 -__version__ = '1.2.0'
3 +__version__ = '1.3.0'
4
5 # Copyright (c) 2015 Matthieu Moy and others
6 # Copyright (c) 2012-2014 Michael Haggerty and others
@@ -57,6 +57,11 @@ import subprocess
57 import shlex
58 import optparse
59 import smtplib
60 +try:
61 + import ssl
62 +except ImportError:
63 + # Python < 2.6 do not have ssl, but that's OK if we don't use it.
64 + pass
65 import time
66 import cgi
67
@@ -75,6 +80,9 @@ def is_ascii(s):
80
81
82 if PYTHON3:
83 + def is_string(s):
84 + return isinstance(s, str)
85 +
86 def str_to_bytes(s):
87 return s.encode(ENCODING)
88
@@ -91,6 +99,12 @@ if PYTHON3:
99 except UnicodeEncodeError:
100 f.buffer.write(msg.encode(ENCODING))
101 else:
102 + def is_string(s):
103 + try:
104 + return isinstance(s, basestring)
105 + except NameError: # Silence Pyflakes warning
106 + raise
107 +
108 def str_to_bytes(s):
109 return s
110
@@ -313,6 +327,16 @@ in repository %(repo_shortname)s.
327
328 """
329
330 +LINK_TEXT_TEMPLATE = """\
331 +View the commit online:
332 +%(browse_url)s
333 +
334 +"""
335 +
336 +LINK_HTML_TEMPLATE = """\
337 +<p><a href="%(browse_url)s">View the commit online</a>.</p>
338 +"""
339 +
340
341 REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE
342
@@ -532,6 +556,28 @@ class Config(object):
556 assert words[-1] == ''
557 return words[:-1]
558
559 + @staticmethod
560 + def add_config_parameters(c):
561 + """Add configuration parameters to Git.
562 +
563 + c is either an str or a list of str, each element being of the
564 + form 'var=val' or 'var', with the same syntax and meaning as
565 + the argument of 'git -c var=val'.
566 + """
567 + if isinstance(c, str):
568 + c = (c,)
569 + parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
570 + if parameters:
571 + parameters += ' '
572 + # git expects GIT_CONFIG_PARAMETERS to be of the form
573 + # "'name1=value1' 'name2=value2' 'name3=value3'"
574 + # including everything inside the double quotes (but not the double
575 + # quotes themselves). Spacing is critical. Also, if a value contains
576 + # a literal single quote that quote must be represented using the
577 + # four character sequence: '\''
578 + parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in c)
579 + os.environ['GIT_CONFIG_PARAMETERS'] = parameters
580 +
581 def get(self, name, default=None):
582 try:
583 values = self._split(read_git_output(
@@ -745,6 +791,12 @@ class Change(object):
791 values['multimail_version'] = get_version()
792 return values
793
794 + # Aliases usable in template strings. Tuple of pairs (destination,
795 + # source).
796 + VALUES_ALIAS = (
797 + ("id", "newrev"),
798 + )
799 +
800 def get_values(self, **extra_values):
801 """Return a dictionary {keyword: expansion} for this Change.
802
@@ -760,6 +812,9 @@ class Change(object):
812 values = self._values.copy()
813 if extra_values:
814 values.update(extra_values)
815 +
816 + for alias, val in self.VALUES_ALIAS:
817 + values[alias] = values[val]
818 return values
819
820 def expand(self, template, **extra_values):
@@ -772,10 +827,14 @@ class Change(object):
827
828 return template % self.get_values(**extra_values)
829
775 - def expand_lines(self, template, **extra_values):
830 + def expand_lines(self, template, html_escape_val=False, **extra_values):
831 """Break template into lines and expand each line."""
832
833 values = self.get_values(**extra_values)
834 + if html_escape_val:
835 + for k in values:
836 + if is_string(values[k]):
837 + values[k] = cgi.escape(values[k], True)
838 for line in template.splitlines(True):
839 yield line % values
840
@@ -787,9 +846,10 @@ class Change(object):
846
847 values = self.get_values(**extra_values)
848 if self._contains_html_diff:
790 - values['contenttype'] = 'html'
849 + self._content_type = 'html'
850 else:
792 - values['contenttype'] = 'plain'
851 + self._content_type = 'plain'
852 + values['contenttype'] = self._content_type
853
854 for line in template.splitlines():
855 (name, value) = line.split(': ', 1)
@@ -819,7 +879,11 @@ class Change(object):
879
880 raise NotImplementedError()
881
822 - def generate_email_intro(self):
882 + def generate_browse_link(self, base_url):
883 + """Generate a link to an online repository browser."""
884 + return iter(())
885 +
886 + def generate_email_intro(self, html_escape_val=False):
887 """Generate the email intro for this Change, a line at a time.
888
889 The output will be used as the standard boilerplate at the top
@@ -835,7 +899,7 @@ class Change(object):
899
900 raise NotImplementedError()
901
838 - def generate_email_footer(self):
902 + def generate_email_footer(self, html_escape_val):
903 """Generate the footer of the email, a line at a time.
904
905 The footer is always included, irrespective of
@@ -876,9 +940,18 @@ class Change(object):
940 for line in self.generate_email_header(**extra_header_values):
941 yield line
942 yield '\n'
879 - for line in self._wrap_for_html(self.generate_email_intro()):
943 + html_escape_val = (self.environment.html_in_intro and
944 + self._contains_html_diff)
945 + intro = self.generate_email_intro(html_escape_val)
946 + if not self.environment.html_in_intro:
947 + intro = self._wrap_for_html(intro)
948 + for line in intro:
949 yield line
950
951 + if self.environment.commitBrowseURL:
952 + for line in self.generate_browse_link(self.environment.commitBrowseURL):
953 + yield line
954 +
955 body = self.generate_email_body(push)
956 if body_filter is not None:
957 body = body_filter(body)
@@ -939,8 +1012,12 @@ class Change(object):
1012 yield line
1013 if self._contains_html_diff:
1014 yield '</pre>'
942 -
943 - for line in self._wrap_for_html(self.generate_email_footer()):
1015 + html_escape_val = (self.environment.html_in_footer and
1016 + self._contains_html_diff)
1017 + footer = self.generate_email_footer(html_escape_val)
1018 + if not self.environment.html_in_footer:
1019 + footer = self._wrap_for_html(footer)
1020 + for line in footer:
1021 yield line
1022
1023 def get_alt_fromaddr(self):
@@ -992,6 +1069,7 @@ class Revision(Change):
1069 values['rev_short'] = self.rev.short
1070 values['change_type'] = self.change_type
1071 values['refname'] = self.refname
1072 + values['newrev'] = self.rev.sha1
1073 values['short_refname'] = self.reference_change.short_refname
1074 values['refname_type'] = self.reference_change.refname_type
1075 values['reply_to_msgid'] = self.reference_change.msgid
@@ -1015,8 +1093,26 @@ class Revision(Change):
1093 ):
1094 yield line
1095
1018 - def generate_email_intro(self):
1019 - for line in self.expand_lines(REVISION_INTRO_TEMPLATE):
1096 + def generate_browse_link(self, base_url):
1097 + if '%(' not in base_url:
1098 + base_url += '%(id)s'
1099 + url = "".join(self.expand_lines(base_url))
1100 + if self._content_type == 'html':
1101 + for line in self.expand_lines(LINK_HTML_TEMPLATE,
1102 + html_escape_val=True,
1103 + browse_url=url):
1104 + yield line
1105 + elif self._content_type == 'plain':
1106 + for line in self.expand_lines(LINK_TEXT_TEMPLATE,
1107 + html_escape_val=False,
1108 + browse_url=url):
1109 + yield line
1110 + else:
1111 + raise NotImplementedError("Content-type %s unsupported. Please report it as a bug.")
1112 +
1113 + def generate_email_intro(self, html_escape_val=False):
1114 + for line in self.expand_lines(REVISION_INTRO_TEMPLATE,
1115 + html_escape_val=html_escape_val):
1116 yield line
1117
1118 def generate_email_body(self, push):
@@ -1031,8 +1127,9 @@ class Revision(Change):
1127 else:
1128 yield line
1129
1034 - def generate_email_footer(self):
1035 - return self.expand_lines(REVISION_FOOTER_TEMPLATE)
1130 + def generate_email_footer(self, html_escape_val):
1131 + return self.expand_lines(REVISION_FOOTER_TEMPLATE,
1132 + html_escape_val=html_escape_val)
1133
1134 def generate_email(self, push, body_filter=None, extra_header_values={}):
1135 self._contains_diff()
@@ -1217,8 +1314,9 @@ class ReferenceChange(Change):
1314 ):
1315 yield line
1316
1220 - def generate_email_intro(self):
1221 - for line in self.expand_lines(self.intro_template):
1317 + def generate_email_intro(self, html_escape_val=False):
1318 + for line in self.expand_lines(self.intro_template,
1319 + html_escape_val=html_escape_val):
1320 yield line
1321
1322 def generate_email_body(self, push):
@@ -1238,8 +1336,9 @@ class ReferenceChange(Change):
1336 for line in self.generate_revision_change_summary(push):
1337 yield line
1338
1241 - def generate_email_footer(self):
1242 - return self.expand_lines(self.footer_template)
1339 + def generate_email_footer(self, html_escape_val):
1340 + return self.expand_lines(self.footer_template,
1341 + html_escape_val=html_escape_val)
1342
1343 def generate_revision_change_graph(self, push):
1344 if self.showgraph:
@@ -1896,6 +1995,7 @@ class SMTPMailer(Mailer):
1995 smtpservertimeout=10.0, smtpserverdebuglevel=0,
1996 smtpencryption='none',
1997 smtpuser='', smtppass='',
1998 + smtpcacerts=''
1999 ):
2000 if not envelopesender:
2001 sys.stderr.write(
@@ -1915,6 +2015,7 @@ class SMTPMailer(Mailer):
2015 self.security = smtpencryption
2016 self.username = smtpuser
2017 self.password = smtppass
2018 + self.smtpcacerts = smtpcacerts
2019 try:
2020 def call(klass, server, timeout):
2021 try:
@@ -1925,13 +2026,56 @@ class SMTPMailer(Mailer):
2026 if self.security == 'none':
2027 self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
2028 elif self.security == 'ssl':
2029 + if self.smtpcacerts:
2030 + raise smtplib.SMTPException(
2031 + "Checking certificate is not supported for ssl, prefer starttls"
2032 + )
2033 self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout)
2034 elif self.security == 'tls':
2035 + if 'ssl' not in sys.modules:
2036 + sys.stderr.write(
2037 + '*** Your Python version does not have the ssl library installed\n'
2038 + '*** smtpEncryption=tls is not available.\n'
2039 + '*** Either upgrade Python to 2.6 or later\n'
2040 + ' or use git_multimail.py version 1.2.\n')
2041 if ':' not in self.smtpserver:
2042 self.smtpserver += ':587' # default port for TLS
2043 self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
2044 + # start: ehlo + starttls
2045 + # equivalent to
2046 + # self.smtp.ehlo()
2047 + # self.smtp.starttls()
2048 + # with acces to the ssl layer
2049 self.smtp.ehlo()
1934 - self.smtp.starttls()
2050 + if not self.smtp.has_extn("starttls"):
2051 + raise smtplib.SMTPException("STARTTLS extension not supported by server")
2052 + resp, reply = self.smtp.docmd("STARTTLS")
2053 + if resp != 220:
2054 + raise smtplib.SMTPException("Wrong answer to the STARTTLS command")
2055 + if self.smtpcacerts:
2056 + self.smtp.sock = ssl.wrap_socket(
2057 + self.smtp.sock,
2058 + ca_certs=self.smtpcacerts,
2059 + cert_reqs=ssl.CERT_REQUIRED
2060 + )
2061 + else:
2062 + self.smtp.sock = ssl.wrap_socket(
2063 + self.smtp.sock,
2064 + cert_reqs=ssl.CERT_NONE
2065 + )
2066 + sys.stderr.write(
2067 + '*** Warning, the server certificat is not verified (smtp) ***\n'
2068 + '*** set the option smtpCACerts ***\n'
2069 + )
2070 + if not hasattr(self.smtp.sock, "read"):
2071 + # using httplib.FakeSocket with Python 2.5.x or earlier
2072 + self.smtp.sock.read = self.smtp.sock.recv
2073 + self.smtp.file = smtplib.SSLFakeFile(self.smtp.sock)
2074 + self.smtp.helo_resp = None
2075 + self.smtp.ehlo_resp = None
2076 + self.smtp.esmtp_features = {}
2077 + self.smtp.does_esmtp = 0
2078 + # end: ehlo + starttls
2079 self.smtp.ehlo()
2080 else:
2081 sys.stdout.write('*** Error: Control reached an invalid option. ***')
@@ -1951,6 +2095,7 @@ class SMTPMailer(Mailer):
2095 def __del__(self):
2096 if hasattr(self, 'smtp'):
2097 self.smtp.quit()
2098 + del self.smtp
2099
2100 def send(self, lines, to_addrs):
2101 try:
@@ -1958,13 +2103,24 @@ class SMTPMailer(Mailer):
2103 self.smtp.login(self.username, self.password)
2104 msg = ''.join(lines)
2105 # turn comma-separated list into Python list if needed.
1961 - if isinstance(to_addrs, basestring):
2106 + if is_string(to_addrs):
2107 to_addrs = [email for (name, email) in getaddresses([to_addrs])]
2108 self.smtp.sendmail(self.envelopesender, to_addrs, msg)
1964 - except Exception:
2109 + except smtplib.SMTPResponseException:
2110 sys.stderr.write('*** Error sending email ***\n')
1966 - sys.stderr.write('*** %s\n' % sys.exc_info()[1])
1967 - self.smtp.quit()
2111 + err = sys.exc_info()[1]
2112 + sys.stderr.write('*** Error %d: %s\n' % (err.smtp_code,
2113 + bytes_to_str(err.smtp_error)))
2114 + try:
2115 + smtp = self.smtp
2116 + # delete the field before quit() so that in case of
2117 + # error, self.smtp is deleted anyway.
2118 + del self.smtp
2119 + smtp.quit()
2120 + except:
2121 + sys.stderr.write('*** Error closing the SMTP connection ***\n')
2122 + sys.stderr.write('*** Exiting anyway ... ***\n')
2123 + sys.stderr.write('*** %s\n' % sys.exc_info()[1])
2124 sys.exit(1)
2125
2126
@@ -2097,6 +2253,14 @@ class Environment(object):
2253 If "html", generate commit emails in HTML instead of plain text
2254 used by default.
2255
2256 + html_in_intro (bool)
2257 + html_in_footer (bool)
2258 +
2259 + When generating HTML emails, the introduction (respectively,
2260 + the footer) will be HTML-escaped iff html_in_intro (respectively,
2261 + the footer) is true. When false, only the values used to expand
2262 + the template are escaped.
2263 +
2264 refchange_showgraph (bool)
2265
2266 True iff refchanges emails should include a detailed graph.
@@ -2160,6 +2324,9 @@ class Environment(object):
2324 self.osenv = osenv or os.environ
2325 self.announce_show_shortlog = False
2326 self.commit_email_format = "text"
2327 + self.html_in_intro = False
2328 + self.html_in_footer = False
2329 + self.commitBrowseURL = None
2330 self.maxcommitemails = 500
2331 self.diffopts = ['--stat', '--summary', '--find-copies-harder']
2332 self.graphopts = ['--oneline', '--decorate']
@@ -2236,7 +2403,7 @@ class Environment(object):
2403 The return value is always a new dictionary."""
2404
2405 if self._values is None:
2239 - values = {}
2406 + values = {'': ''} # %()s expands to the empty string.
2407
2408 for key in self.COMPUTED_KEYS:
2409 value = getattr(self, 'get_%s' % (key,))()
@@ -2375,6 +2542,16 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2542 else:
2543 self.commit_email_format = commit_email_format
2544
2545 + html_in_intro = config.get_bool('htmlInIntro')
2546 + if html_in_intro is not None:
2547 + self.html_in_intro = html_in_intro
2548 +
2549 + html_in_footer = config.get_bool('htmlInFooter')
2550 + if html_in_footer is not None:
2551 + self.html_in_footer = html_in_footer
2552 +
2553 + self.commitBrowseURL = config.get('commitBrowseURL')
2554 +
2555 maxcommitemails = config.get('maxcommitemails')
2556 if maxcommitemails is not None:
2557 try:
@@ -2415,7 +2592,6 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2592 ['author'])
2593 self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
2594
2418 - from_addr = self.config.get('from')
2595 self.from_refchange = config.get('fromRefchange')
2596 self.forbid_field_values('fromRefchange',
2597 self.from_refchange,
@@ -3390,6 +3566,8 @@ def run_as_post_receive_hook(environment, mailer):
3566 if changes:
3567 push = Push(environment, changes)
3568 push.send_emails(mailer, body_filter=environment.filter_body)
3569 + if hasattr(mailer, '__del__'):
3570 + mailer.__del__()
3571
3572
3573 def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
@@ -3406,6 +3584,8 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
3584 ]
3585 push = Push(environment, changes, force_send)
3586 push.send_emails(mailer, body_filter=environment.filter_body)
3587 + if hasattr(mailer, '__del__'):
3588 + mailer.__del__()
3589
3590
3591 def choose_mailer(config, environment):
@@ -3418,6 +3598,7 @@ def choose_mailer(config, environment):
3598 smtpencryption = config.get('smtpencryption', default='none')
3599 smtpuser = config.get('smtpuser', default='')
3600 smtppass = config.get('smtppass', default='')
3601 + smtpcacerts = config.get('smtpcacerts', default='')
3602 mailer = SMTPMailer(
3603 envelopesender=(environment.get_sender() or environment.get_fromaddr()),
3604 smtpserver=smtpserver, smtpservertimeout=smtpservertimeout,
@@ -3425,6 +3606,7 @@ def choose_mailer(config, environment):
3606 smtpencryption=smtpencryption,
3607 smtpuser=smtpuser,
3608 smtppass=smtppass,
3609 + smtpcacerts=smtpcacerts
3610 )
3611 elif mailer == 'sendmail':
3612 command = config.get('sendmailcommand')
@@ -3691,17 +3873,7 @@ def main(args):
3873 return
3874
3875 if options.c:
3694 - parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
3695 - if parameters:
3696 - parameters += ' '
3697 - # git expects GIT_CONFIG_PARAMETERS to be of the form
3698 - # "'name1=value1' 'name2=value2' 'name3=value3'"
3699 - # including everything inside the double quotes (but not the double
3700 - # quotes themselves). Spacing is critical. Also, if a value contains
3701 - # a literal single quote that quote must be represented using the
3702 - # four character sequence: '\''
3703 - parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in options.c)
3704 - os.environ['GIT_CONFIG_PARAMETERS'] = parameters
3876 + Config.add_config_parameters(options.c)
3877
3878 config = Config('multimailhook')
3879
contrib/hooks/multimail/post-receive.example
+6
@@ -55,6 +55,12 @@ import git_multimail
55 # git-multimail:
56 config = git_multimail.Config('multimailhook')
57
58 +# Set some Git configuration variables. Equivalent to passing var=val
59 +# to "git -c var=val" each time git is called, or to adding the
60 +# configuration in .git/config (must come before instanciating the
61 +# environment) :
62 +#git_multimail.Config.add_config_parameters('multimailhook.commitEmailFormat=html')
63 +#git_multimail.Config.add_config_parameters(('user.name=foo', 'user.email=foo@example.com'))
64
65 # Select the type of environment:
66 try: