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: