12 from stgit
import templates
, version
13 from stgit
.argparse
import diff_opts_option
, opt
, patch_range
14 from stgit
.commands
.common
import (
16 DirectoryHasRepository
,
20 from stgit
.config
import config
21 from stgit
.lib
.git
import Person
22 from stgit
.out
import out
23 from stgit
.run
import Run
24 from stgit
.utils
import edit_bytes
27 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
29 This program is free software; you can redistribute it and/or modify
30 it under the terms of the GNU General Public License version 2 as
31 published by the Free Software Foundation.
33 This program is distributed in the hope that it will be useful,
34 but WITHOUT ANY WARRANTY; without even the implied warranty of
35 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36 GNU General Public License for more details.
38 You should have received a copy of the GNU General Public License
39 along with this program; if not, see http://www.gnu.org/licenses/.
42 help = 'Send a patch or series of patches by e-mail'
44 usage
= [' [options] [--] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
46 Send a patch or a range of patches by e-mail using the SMTP server
47 specified by the 'stgit.smtpserver' configuration option, or the
48 '--smtp-server' command line option. This option can also be an
49 absolute path to 'sendmail' followed by command line arguments.
51 The From address and the e-mail format are generated from the template
52 file passed as argument to '--template' (defaulting to
53 '.git/patchmail.tmpl' or '~/.stgit/templates/patchmail.tmpl' or
54 '/usr/share/stgit/templates/patchmail.tmpl'). A patch can be sent as
55 attachment using the --attach option in which case the
56 'mailattch.tmpl' template will be used instead of 'patchmail.tmpl'.
58 The To/Cc/Bcc addresses can either be added to the template file or
59 passed via the corresponding command line options. They can be e-mail
60 addresses or aliases which are automatically expanded to the values
61 stored in the [mail "alias"] section of Git configuration files.
63 A preamble e-mail can be sent using the '--cover' and/or
64 '--edit-cover' options. The first allows the user to specify a file to
65 be used as a template. The latter option will invoke the editor on the
66 specified file (defaulting to '.git/covermail.tmpl' or
67 '~/.stgit/templates/covermail.tmpl' or
68 '/usr/share/stgit/templates/covermail.tmpl').
70 All the subsequent e-mails appear as replies to the first e-mail sent
71 (either the preamble or the first patch). E-mails can be seen as
72 replies to a different e-mail by using the '--in-reply-to' option.
74 SMTP authentication is also possible with '--smtp-user' and
75 '--smtp-password' options, also available as configuration settings:
76 'smtpuser' and 'smtppassword'. TLS encryption can be enabled by
77 '--smtp-tls' option and 'smtptls' setting.
79 The following variables are accepted by both the preamble and the
80 patch e-mail templates:
82 %(diffstat)s - diff statistics
83 %(number)s - empty if only one patch is sent or 'patchnr/totalnr'
84 %(snumber)s - stripped version of '%(number)s'
85 %(nspace)s - ' ' if %(number)s is non-empty, otherwise empty string
86 %(patchnr)s - patch number
87 %(sender)s - 'sender' or 'authname <authemail>' as per the config file
88 %(totalnr)s - total number of patches to be sent
89 %(version)s - 'version' string passed on the command line (or empty)
90 %(vspace)s - ' ' if %(version)s is non-empty, otherwise empty string
92 In addition to the common variables, the preamble e-mail template
93 accepts the following:
95 %(shortlog)s - first line of each patch description, listed by author
97 In addition to the common variables, the patch e-mail template accepts
100 %(authdate)s - patch creation date
101 %(authemail)s - author's email
102 %(authname)s - author's name
103 %(commemail)s - committer's e-mail
104 %(commname)s - committer's name
105 %(diff)s - unified diff of the patch
106 %(fromauth)s - 'From: author\n\n' if different from sender
107 %(longdescr)s - the rest of the patch description, after the first line
108 %(patch)s - patch name
109 %(prefix)s - 'prefix' string passed on the command line
110 %(pspace)s - ' ' if %(prefix)s is non-empty, otherwise empty string
111 %(shortdescr)s - the first line of the patch description"""
113 args
= [patch_range('applied_patches', 'unapplied_patches', 'hidden_patches')]
119 short
='E-mail all the applied patches',
124 args
=['mail_aliases'],
125 short
='Add TO to the To: list',
130 args
=['mail_aliases'],
131 short
='Add CC to the Cc: list',
136 args
=['mail_aliases'],
137 short
='Add BCC to the Bcc: list',
142 short
='Automatically cc the patch signers',
147 short
='Do not send subsequent messages as replies',
152 short
='Send patches without sequence numbering',
157 short
='Send a patch as attachment',
162 short
='Send a patch inline and as an attachment',
168 short
='Add VERSION to the [PATCH ...] prefix',
173 short
='Add PREFIX to the [... PATCH ...] prefix',
179 short
='Use FILE as the message template',
185 short
='Send FILE as the cover message',
191 short
='Edit the cover message before sending',
197 short
='Edit each patch before sending',
204 short
='Sleep for SECONDS between e-mails sending',
209 short
='Use REFID as the reference id',
213 metavar
='HOST[:PORT] or "/path/to/sendmail -t -i"',
214 short
='SMTP server or command to use for sending mail',
220 short
='Username for SMTP authentication',
226 short
='Password for SMTP authentication',
232 short
='Use SMTP with TLS encryption',
237 args
=['stg_branches'],
238 short
='Use BRANCH instead of the default branch',
244 short
='Generate an mbox file instead of sending',
249 short
='Use DOMAIN when generating message IDs '
250 '(instead of the system hostname)',
255 short
='Use git send-email (EXPERIMENTAL)',
257 ] + diff_opts_option()
259 directory
= DirectoryHasRepository()
263 """Return the 'authname <authemail>' string as read from the
266 sender
= config
.get('stgit.sender')
270 sender
= user
.name_email
272 author
= Person
.author()
274 sender
= author
.name_email
277 'Unknown sender name and e-mail; you should for '
278 'example set git config user.name and user.email'
281 sender
= email
.utils
.parseaddr(sender
)
283 return email
.utils
.formataddr(address_or_alias(sender
))
286 def __addr_list(msg
, header
):
287 return [addr
for name
, addr
in email
.utils
.getaddresses(msg
.get_all(header
, []))]
290 def __parse_addresses(msg
):
291 """Return a two elements tuple: (from, [to])"""
292 from_addr_list
= __addr_list(msg
, 'From')
293 if len(from_addr_list
) == 0:
294 raise CmdException('No "From" address')
297 __addr_list(msg
, 'To') + __addr_list(msg
, 'Cc') + __addr_list(msg
, 'Bcc')
299 if len(to_addr_list
) == 0:
300 raise CmdException('No "To/Cc/Bcc" addresses')
302 return (from_addr_list
[0], set(to_addr_list
))
305 def __send_message_sendmail(sendmail
, msg_bytes
):
306 """Send the message using the sendmail command."""
307 cmd
= sendmail
.split()
308 Run(*cmd
).encoding(None).raw_input(msg_bytes
).discard_output()
311 __smtp_credentials
= None
314 def __set_smtp_credentials(options
):
315 """Set the (smtpuser, smtppassword, smtpusetls) credentials if the method
318 global __smtp_credentials
320 smtpserver
= options
.smtp_server
or config
.get('stgit.smtpserver')
321 if options
.mbox
or options
.git
or smtpserver
.startswith('/'):
324 smtppassword
= options
.smtp_password
or config
.get('stgit.smtppassword')
325 smtpuser
= options
.smtp_user
or config
.get('stgit.smtpuser')
326 smtpusetls
= options
.smtp_tls
or config
.get('stgit.smtptls') == 'yes'
328 if smtppassword
and not smtpuser
:
329 raise CmdException('SMTP password supplied, username needed')
330 if smtpusetls
and not smtpuser
:
331 raise CmdException('SMTP over TLS requested, username needed')
332 if smtpuser
and not smtppassword
:
333 smtppassword
= getpass
.getpass("Please enter SMTP password: ")
335 __smtp_credentials
= (smtpuser
, smtppassword
, smtpusetls
)
338 def __send_message_smtp(smtpserver
, from_addr
, to_addr_list
, msg
, options
):
339 """Send the message using the given SMTP server"""
340 smtpuser
, smtppassword
, smtpusetls
= __smtp_credentials
343 s
= smtplib
.SMTP(smtpserver
)
344 except Exception as err
:
345 raise CmdException(str(err
))
349 if smtpuser
and smtppassword
:
354 except (smtplib
.SMTPException
, RuntimeError) as e
:
355 # RuntimeError indicates that Python lacks SSL support.
356 # SMTPException indicates that server does not do STARTTLS.
357 raise CmdException("cannot use TLS: %s" % e
)
359 s
.login(smtpuser
, smtppassword
)
361 result
= s
.sendmail(from_addr
, to_addr_list
, msg
)
364 "mail server refused delivery for the following recipients:",
367 except Exception as err
:
368 raise CmdException(str(err
))
373 def __send_message_git(msg_bytes
, from_
, options
):
374 """Send the message using git send-email"""
375 from subprocess
import call
376 from tempfile
import mkstemp
378 cmd
= ["git", "send-email", "--from=%s" % from_
]
379 cmd
.append("--quiet")
380 cmd
.append("--suppress-cc=self")
382 cmd
.append("--suppress-cc=body")
383 if options
.in_reply_to
:
384 cmd
.extend(["--in-reply-to", options
.in_reply_to
])
385 if options
.no_thread
:
386 cmd
.append("--no-thread")
388 # We only support To/Cc/Bcc in git send-email for now.
389 for x
in ['to', 'cc', 'bcc']:
390 if getattr(options
, x
):
391 cmd
.extend('--%s=%s' % (x
, a
) for a
in getattr(options
, x
))
393 (fd
, path
) = mkstemp()
395 os
.write(fd
, msg_bytes
)
400 except Exception as err
:
401 raise CmdException(str(err
))
406 def __send_message(msg_type
, tmpl
, options
, *args
):
407 """Message sending dispatcher."""
408 msg_id
= email
.utils
.make_msgid(
409 'stgit', domain
=options
.domain
or config
.get('stgit.domain')
412 if msg_type
== 'cover':
413 assert len(args
) == 1, 'too many args for msg_type == "cover"'
415 msg
= __build_cover(tmpl
, msg_id
, options
, patches
)
416 outstr
= 'the cover message'
417 elif msg_type
== 'patch':
418 patch
, patch_nr
, total_nr
, ref_id
= args
419 msg
= __build_message(tmpl
, msg_id
, options
, patch
, patch_nr
, total_nr
, ref_id
)
420 outstr
= 'patch "%s"' % patch
422 raise AssertionError('invalid msg_type: %s' % msg_type
) # pragma: no cover
424 msg_bytes
= msg
.as_bytes(options
.mbox
)
427 out
.stdout_bytes(msg_bytes
+ b
'\n')
431 from_addr
, to_addrs
= __parse_addresses(msg
)
432 out
.start('Sending ' + outstr
)
434 smtpserver
= options
.smtp_server
or config
.get('stgit.smtpserver')
436 __send_message_git(msg_bytes
, msg
['From'], options
)
437 elif smtpserver
.startswith('/'):
438 # Use the sendmail tool
439 __send_message_sendmail(smtpserver
, msg_bytes
)
441 # Use the SMTP server (we have host and port information)
442 __send_message_smtp(smtpserver
, from_addr
, to_addrs
, msg_bytes
, options
)
444 # give recipients a chance of receiving related patches in correct order
445 if msg_type
== 'cover' or (msg_type
== 'patch' and patch_nr
< total_nr
):
446 sleep
= options
.sleep
or config
.getint('stgit.smtpdelay')
453 def __update_header(msg
, header
, addr
='', ignore
=()):
454 addr_pairs
= email
.utils
.getaddresses(msg
.get_all(header
, []) + [addr
])
456 # remove pairs without an address and resolve the aliases
458 address_or_alias(name_addr
) for name_addr
in addr_pairs
if name_addr
[1]
460 # remove the duplicates and filter the addresses
461 addr_pairs
= [name_addr
for name_addr
in addr_pairs
if name_addr
[1] not in ignore
]
463 msg
[header
] = ', '.join(map(email
.utils
.formataddr
, addr_pairs
))
464 return set(addr
for _
, addr
in addr_pairs
)
467 def __build_address_headers(msg
, options
, extra_cc
):
468 """Build the address headers and check existing headers in the
476 autobcc
= config
.get('stgit.autobcc') or ''
479 to_addr
= ', '.join(options
.to
)
481 cc_addr
= ', '.join(options
.cc
)
483 extra_cc_addr
= ', '.join(extra_cc
)
485 bcc_addr
= ', '.join(options
.bcc
+ [autobcc
])
489 # if an address is on a header, ignore it from the rest
490 from_set
= __update_header(msg
, 'From')
491 to_set
= __update_header(msg
, 'To', to_addr
)
492 # --auto generated addresses, don't include the sender
493 __update_header(msg
, 'Cc', extra_cc_addr
, from_set
)
494 cc_set
= __update_header(msg
, 'Cc', cc_addr
, to_set
)
495 __update_header(msg
, 'Bcc', bcc_addr
, to_set
.union(cc_set
))
498 def __get_signers_list(msg
):
499 """Return the address list generated from signed-off-by and
500 acked-by lines in the message.
503 tags
= '%s|%s|%s|%s|%s|%s|%s|%s' % (
511 'reported-and-tested-by',
514 # N.B. This regex treats '#' as the start of a comment and thus discards everything
515 # after the '#'. This does not allow for valid email addresses that contain '#' in
516 # the local-part (i.e. the part before the '@') as is allowed by RFC2822. Such email
517 # addresses containing '#' in their local-part are thus not supported by this
519 regex
= r
'^(%s):\s+([^#]+).*$' % tags
521 r
= re
.compile(regex
, re
.I
)
522 for line
in msg
.split('\n'):
525 addr_list
.append(m
.group(2))
530 def __build_extra_headers(msg
, msg_id
, ref_id
=None):
531 """Build extra email headers and encoding"""
533 msg
['Date'] = email
.utils
.formatdate(localtime
=True)
534 msg
['Message-ID'] = msg_id
536 # make sure the ref id has the angle brackets
537 ref_id
= '<%s>' % ref_id
.strip(' \t\n<>')
538 msg
['In-Reply-To'] = ref_id
539 msg
['References'] = ref_id
540 msg
['User-Agent'] = 'StGit/%s' % version
.get_version()
542 # update other address headers
543 __update_header(msg
, 'Reply-To')
544 __update_header(msg
, 'Mail-Reply-To')
545 __update_header(msg
, 'Mail-Followup-To')
548 def __encode_message(msg
):
549 # 7 or 8 bit encoding
550 charset
= email
.charset
.Charset('utf-8')
551 charset
.body_encoding
= None
554 for header
, value
in msg
.items():
556 for word
in value
.split(' '):
557 words
.append(email
.header
.Header(word
).encode())
558 new_val
= ' '.join(words
)
559 msg
.replace_header(header
, new_val
)
561 # replace the Subject string with a Header() object otherwise the long
562 # line folding is done using "\n\t" rather than "\n ", causing issues with
563 # some e-mail clients
564 subject
= msg
.get('subject', '')
565 msg
.replace_header('subject', email
.header
.Header(subject
, header_name
='subject'))
567 # encode the body and set the MIME and encoding headers
568 if msg
.is_multipart():
569 for p
in msg
.get_payload():
570 # TODO test if payload can be encoded with charset. Perhaps
571 # iterate email.charset.CHARSETS to find an encodable one?
572 p
.set_charset(charset
)
574 msg
.set_charset(charset
)
577 def __shortlog(stack
, patches
):
578 cmd
= ['git', 'show', '--pretty=short']
579 for pn
in reversed(patches
):
580 cmd
.append(stack
.patches
[pn
].sha1
)
581 log
= stack
.repository
.run(cmd
).raw_output()
582 return stack
.repository
.run(['git', 'shortlog']).raw_input(log
).raw_output()
585 def __diffstat(stack
, patches
):
586 tree1
= stack
.patches
[patches
[0]].data
.parent
.data
.tree
587 tree2
= stack
.patches
[patches
[-1]].data
.tree
588 return stack
.repository
.diff_tree(
591 diff_opts
=['--stat-width=72'],
596 def __build_cover(tmpl
, msg_id
, options
, patches
):
597 """Build the cover message (series description) to be sent via SMTP"""
598 sender
= __get_sender()
601 version_str
= '%s' % options
.version
608 prefix_str
= options
.prefix
610 prefix_str
= config
.get('stgit.mail.prefix')
617 total_nr_str
= str(len(patches
))
618 patch_nr_str
= '0'.zfill(len(total_nr_str
))
620 number_str
= '%s/%s' % (patch_nr_str
, total_nr_str
)
626 repository
= directory
.repository
627 stack
= repository
.current_stack
630 'sender': sender
, # for backward template compatibility
631 'maintainer': sender
, # for backward template compatibility
632 'endofheaders': '', # for backward template compatibility
634 'version': version_str
,
635 'vspace': version_space
,
636 'prefix': prefix_str
,
637 'pspace': prefix_space
,
638 'patchnr': patch_nr_str
,
639 'totalnr': total_nr_str
,
640 'number': number_str
,
641 'nspace': number_space
,
642 'snumber': number_str
.strip(),
643 'shortlog': __shortlog(stack
, patches
),
644 'diffstat': __diffstat(stack
, patches
),
648 msg_bytes
= templates
.specialize_template(tmpl
, tmpl_dict
)
649 except KeyError as err
:
650 raise CmdException('Unknown patch template variable: %s' % err
)
653 'Only "%(name)s" variables are ' 'supported in the patch template'
656 if options
.edit_cover
:
657 msg_bytes
= edit_bytes(msg_bytes
, '.stgitmail.txt')
659 # The Python email message
661 msg
= email
.message_from_bytes(msg_bytes
)
662 except Exception as ex
:
663 raise CmdException('template parsing error: %s' % str(ex
))
668 message_str
= stack
.patchs
[pn
].data
.message_str
670 descr
= message_str
.strip()
671 extra_cc
.extend(__get_signers_list(descr
))
672 extra_cc
= list(set(extra_cc
))
675 __build_address_headers(msg
, options
, extra_cc
)
676 __build_extra_headers(msg
, msg_id
, options
.in_reply_to
)
677 __encode_message(msg
)
682 def __build_message(tmpl
, msg_id
, options
, patch
, patch_nr
, total_nr
, ref_id
):
683 """Build the message to be sent via SMTP"""
684 repository
= directory
.repository
685 stack
= repository
.current_stack
687 cd
= stack
.patches
[patch
].data
690 descr
= cd
.message_str
.strip()
692 # provide a place holder and force the edit message option on
693 descr
= '<empty message>'
694 options
.edit_patches
= True
696 descr_lines
= descr
.split('\n')
697 short_descr
= descr_lines
[0].strip()
698 long_descr
= '\n'.join(line
.rstrip() for line
in descr_lines
[1:]).lstrip('\n')
701 committer
= cd
.committer
703 sender
= __get_sender()
705 if author
.name_email
!= sender
:
706 fromauth
= 'From: %s\n\n' % author
.name_email
711 version_str
= '%s' % options
.version
718 prefix_str
= options
.prefix
720 prefix_str
= config
.get('stgit.mail.prefix')
727 total_nr_str
= str(total_nr
)
728 patch_nr_str
= str(patch_nr
).zfill(len(total_nr_str
))
729 if not options
.unrelated
and total_nr
> 1:
730 number_str
= '%s/%s' % (patch_nr_str
, total_nr_str
)
736 diff
= repository
.diff_tree(
739 diff_opts
=options
.diff_flags
,
744 'maintainer': sender
, # for backward template compatibility
745 'shortdescr': short_descr
,
746 'longdescr': long_descr
,
747 'endofheaders': '', # for backward template compatibility
749 'diffstat': repository
.default_iw
.diffstat(diff
),
750 'date': '', # for backward template compatibility
751 'version': version_str
,
752 'vspace': version_space
,
753 'prefix': prefix_str
,
754 'pspace': prefix_space
,
755 'patchnr': patch_nr_str
,
756 'totalnr': total_nr_str
,
757 'number': number_str
,
758 'nspace': number_space
,
759 'snumber': number_str
.strip(),
760 'fromauth': fromauth
,
761 'authname': author
.name
,
762 'authemail': author
.email
,
763 'authdate': author
.date
.rfc2822_format(),
764 'commname': committer
.name
,
765 'commemail': committer
.email
,
769 msg_bytes
= templates
.specialize_template(tmpl
, tmpl_dict
)
770 except KeyError as err
:
771 raise CmdException('Unknown patch template variable: %s' % err
)
774 'Only "%(name)s" variables are ' 'supported in the patch template'
777 if options
.edit_patches
:
778 msg_bytes
= edit_bytes(msg_bytes
, '.stgitmail.txt')
780 # The Python email message
782 msg
= email
.message_from_bytes(msg_bytes
)
783 except Exception as ex
:
784 raise CmdException('template parsing error: %s' % str(ex
))
787 extra_cc
= __get_signers_list(descr
)
792 __build_address_headers(msg
, options
, extra_cc
)
793 __build_extra_headers(msg
, msg_id
, ref_id
)
794 __encode_message(msg
)
799 def func(parser
, options
, args
):
800 """Send the patches by e-mail using the patchmail.tmpl file as
803 stack
= directory
.repository
.current_stack
804 applied
= stack
.patchorder
.applied
809 unapplied
= stack
.patchorder
.unapplied
810 patches
= parse_patches(args
, applied
+ unapplied
, len(applied
))
812 raise CmdException('Incorrect options. Unknown patches to send')
814 # early test for sender identity
817 out
.start('Checking the validity of the patches')
819 if stack
.patches
[pn
].data
.is_nochange():
820 raise CmdException('Cannot send empty patch "%s"' % pn
)
823 total_nr
= len(patches
)
825 raise CmdException('No patches to send')
827 if options
.in_reply_to
:
828 if options
.no_thread
or options
.unrelated
:
830 '--in-reply-to option not allowed with ' '--no-thread or --unrelated'
832 ref_id
= options
.in_reply_to
836 # get username/password if sending by SMTP
837 __set_smtp_credentials(options
)
839 # send the cover message (if any)
840 if options
.cover
or options
.edit_cover
:
841 if options
.unrelated
:
842 raise CmdException('cover sending not allowed with --unrelated')
844 # find the template file
846 with io
.open(options
.cover
, 'r') as f
:
849 tmpl
= templates
.get_template('covermail.tmpl')
851 raise CmdException('No cover message template file found')
853 msg_id
= __send_message('cover', tmpl
, options
, patches
)
855 # subsequent e-mails are seen as replies to the first one
856 if not options
.no_thread
:
861 with io
.open(options
.template
, 'r') as f
:
865 tmpl
= templates
.get_template('mailattch.tmpl')
866 elif options
.attach_inline
:
867 tmpl
= templates
.get_template('patchandattch.tmpl')
869 tmpl
= templates
.get_template('patchmail.tmpl')
871 raise CmdException('No e-mail template file found')
873 for (p
, n
) in zip(patches
, range(1, total_nr
+ 1)):
874 msg_id
= __send_message('patch', tmpl
, options
, p
, n
, total_nr
, ref_id
)
876 # subsequent e-mails are seen as replies to the first one
877 if not options
.no_thread
and not options
.unrelated
and not ref_id
: