1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
, division
, print_function
14 from stgit
import argparse
, stack
, git
, version
, templates
15 from stgit
.argparse
import opt
16 from stgit
.commands
.common
import (CmdException
,
17 DirectoryHasRepository
,
21 from stgit
.config
import config
22 from stgit
.lib
import git
as gitlib
23 from stgit
.out
import out
24 from stgit
.run
import Run
25 from stgit
.utils
import call_editor
28 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
30 This program is free software; you can redistribute it and/or modify
31 it under the terms of the GNU General Public License version 2 as
32 published by the Free Software Foundation.
34 This program is distributed in the hope that it will be useful,
35 but WITHOUT ANY WARRANTY; without even the implied warranty of
36 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37 GNU General Public License for more details.
39 You should have received a copy of the GNU General Public License
40 along with this program; if not, see http://www.gnu.org/licenses/.
43 help = 'Send a patch or series of patches by e-mail'
45 usage
= [' [options] [--] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
47 Send a patch or a range of patches by e-mail using the SMTP server
48 specified by the 'stgit.smtpserver' configuration option, or the
49 '--smtp-server' command line option. This option can also be an
50 absolute path to 'sendmail' followed by command line arguments.
52 The From address and the e-mail format are generated from the template
53 file passed as argument to '--template' (defaulting to
54 '.git/patchmail.tmpl' or '~/.stgit/templates/patchmail.tmpl' or
55 '/usr/share/stgit/templates/patchmail.tmpl'). A patch can be sent as
56 attachment using the --attach option in which case the
57 'mailattch.tmpl' template will be used instead of 'patchmail.tmpl'.
59 The To/Cc/Bcc addresses can either be added to the template file or
60 passed via the corresponding command line options. They can be e-mail
61 addresses or aliases which are automatically expanded to the values
62 stored in the [mail "alias"] section of GIT configuration files.
64 A preamble e-mail can be sent using the '--cover' and/or
65 '--edit-cover' options. The first allows the user to specify a file to
66 be used as a template. The latter option will invoke the editor on the
67 specified file (defaulting to '.git/covermail.tmpl' or
68 '~/.stgit/templates/covermail.tmpl' or
69 '/usr/share/stgit/templates/covermail.tmpl').
71 All the subsequent e-mails appear as replies to the first e-mail sent
72 (either the preamble or the first patch). E-mails can be seen as
73 replies to a different e-mail by using the '--in-reply-to' option.
75 SMTP authentication is also possible with '--smtp-user' and
76 '--smtp-password' options, also available as configuration settings:
77 'smtpuser' and 'smtppassword'. TLS encryption can be enabled by
78 '--smtp-tls' option and 'smtptls' setting.
80 The following variables are accepted by both the preamble and the
81 patch e-mail templates:
83 %(diffstat)s - diff statistics
84 %(number)s - empty if only one patch is sent or 'patchnr/totalnr'
85 %(snumber)s - stripped version of '%(number)s'
86 %(nspace)s - ' ' if %(number)s is non-empty, otherwise empty string
87 %(patchnr)s - patch number
88 %(sender)s - 'sender' or 'authname <authemail>' as per the config file
89 %(totalnr)s - total number of patches to be sent
90 %(version)s - 'version' string passed on the command line (or empty)
91 %(vspace)s - ' ' if %(version)s is non-empty, otherwise empty string
93 In addition to the common variables, the preamble e-mail template
94 accepts the following:
96 %(shortlog)s - first line of each patch description, listed by author
98 In addition to the common variables, the patch e-mail template accepts
101 %(authdate)s - patch creation date
102 %(authemail)s - author's email
103 %(authname)s - author's name
104 %(commemail)s - committer's e-mail
105 %(commname)s - committer's name
106 %(diff)s - unified diff of the patch
107 %(fromauth)s - 'From: author\n\n' if different from sender
108 %(longdescr)s - the rest of the patch description, after the first line
109 %(patch)s - patch name
110 %(prefix)s - 'prefix' string passed on the command line
111 %(pspace)s - ' ' if %(prefix)s is non-empty, otherwise empty string
112 %(shortdescr)s - the first line of the patch description"""
114 args
= [argparse
.patch_range(argparse
.applied_patches
,
115 argparse
.unapplied_patches
,
116 argparse
.hidden_patches
)]
118 opt('-a', '--all', action
= 'store_true',
119 short
= 'E-mail all the applied patches'),
120 opt('--to', action
= 'append', args
= [argparse
.mail_aliases
],
121 short
= 'Add TO to the To: list'),
122 opt('--cc', action
= 'append', args
= [argparse
.mail_aliases
],
123 short
= 'Add CC to the Cc: list'),
124 opt('--bcc', action
= 'append', args
= [argparse
.mail_aliases
],
125 short
= 'Add BCC to the Bcc: list'),
126 opt('--auto', action
= 'store_true',
127 short
= 'Automatically cc the patch signers'),
128 opt('--no-thread', action
= 'store_true',
129 short
= 'Do not send subsequent messages as replies'),
130 opt('--unrelated', action
= 'store_true',
131 short
= 'Send patches without sequence numbering'),
132 opt('--attach', action
= 'store_true',
133 short
= 'Send a patch as attachment'),
134 opt('--attach-inline', action
= 'store_true',
135 short
= 'Send a patch inline and as an attachment'),
136 opt('-v', '--version', metavar
= 'VERSION',
137 short
= 'Add VERSION to the [PATCH ...] prefix'),
138 opt('--prefix', metavar
= 'PREFIX',
139 short
= 'Add PREFIX to the [... PATCH ...] prefix'),
140 opt('-t', '--template', metavar
= 'FILE',
141 short
= 'Use FILE as the message template'),
142 opt('-c', '--cover', metavar
= 'FILE',
143 short
= 'Send FILE as the cover message'),
144 opt('-e', '--edit-cover', action
= 'store_true',
145 short
= 'Edit the cover message before sending'),
146 opt('-E', '--edit-patches', action
= 'store_true',
147 short
= 'Edit each patch before sending'),
148 opt('-s', '--sleep', type = 'int', metavar
= 'SECONDS',
149 short
= 'Sleep for SECONDS between e-mails sending'),
150 opt('--in-reply-to', metavar
= 'REFID',
151 short
= 'Use REFID as the reference id'),
152 opt('--smtp-server', metavar
= 'HOST[:PORT] or "/path/to/sendmail -t -i"',
153 short
= 'SMTP server or command to use for sending mail'),
154 opt('-u', '--smtp-user', metavar
= 'USER',
155 short
= 'Username for SMTP authentication'),
156 opt('-p', '--smtp-password', metavar
= 'PASSWORD',
157 short
= 'Password for SMTP authentication'),
158 opt('-T', '--smtp-tls', action
= 'store_true',
159 short
= 'Use SMTP with TLS encryption'),
160 opt('-b', '--branch', args
= [argparse
.stg_branches
],
161 short
= 'Use BRANCH instead of the default branch'),
162 opt('-m', '--mbox', action
= 'store_true',
163 short
= 'Generate an mbox file instead of sending'),
164 opt('--git', action
= 'store_true',
165 short
= 'Use git send-email (EXPERIMENTAL)')
166 ] + argparse
.diff_opts_option()
168 directory
= DirectoryHasRepository(log
= False)
171 """Return the 'authname <authemail>' string as read from the
174 sender
=config
.get('stgit.sender')
177 sender
= str(git
.user())
178 except git
.GitException
:
180 sender
= str(git
.author())
181 except git
.GitException
:
184 raise CmdException('Unknown sender name and e-mail; you should for '
185 'example set git config user.name and user.email')
186 sender
= email
.utils
.parseaddr(sender
)
188 return email
.utils
.formataddr(address_or_alias(sender
))
190 def __addr_list(msg
, header
):
191 return [addr
for name
, addr
in
192 email
.utils
.getaddresses(msg
.get_all(header
, []))]
194 def __parse_addresses(msg
):
195 """Return a two elements tuple: (from, [to])
197 from_addr_list
= __addr_list(msg
, 'From')
198 if len(from_addr_list
) == 0:
199 raise CmdException('No "From" address')
201 to_addr_list
= __addr_list(msg
, 'To') + __addr_list(msg
, 'Cc') \
202 + __addr_list(msg
, 'Bcc')
203 if len(to_addr_list
) == 0:
204 raise CmdException('No "To/Cc/Bcc" addresses')
206 return (from_addr_list
[0], set(to_addr_list
))
208 def __send_message_sendmail(sendmail
, msg
):
209 """Send the message using the sendmail command.
211 cmd
= sendmail
.split()
212 Run(*cmd
).raw_input(msg
).discard_output()
214 __smtp_credentials
= None
216 def __set_smtp_credentials(options
):
217 """Set the (smtpuser, smtppassword, smtpusetls) credentials if the method
220 global __smtp_credentials
222 smtpserver
= options
.smtp_server
or config
.get('stgit.smtpserver')
223 if options
.mbox
or options
.git
or smtpserver
.startswith('/'):
226 smtppassword
= options
.smtp_password
or config
.get('stgit.smtppassword')
227 smtpuser
= options
.smtp_user
or config
.get('stgit.smtpuser')
228 smtpusetls
= options
.smtp_tls
or config
.get('stgit.smtptls') == 'yes'
230 if (smtppassword
and not smtpuser
):
231 raise CmdException('SMTP password supplied, username needed')
232 if (smtpusetls
and not smtpuser
):
233 raise CmdException('SMTP over TLS requested, username needed')
234 if (smtpuser
and not smtppassword
):
235 smtppassword
= getpass
.getpass("Please enter SMTP password: ")
237 __smtp_credentials
= (smtpuser
, smtppassword
, smtpusetls
)
239 def __send_message_smtp(smtpserver
, from_addr
, to_addr_list
, msg
, options
):
240 """Send the message using the given SMTP server
242 smtpuser
, smtppassword
, smtpusetls
= __smtp_credentials
245 s
= smtplib
.SMTP(smtpserver
)
246 except Exception as err
:
247 raise CmdException(str(err
))
251 if smtpuser
and smtppassword
:
254 if not hasattr(socket
, 'ssl'):
256 "cannot use TLS - no SSL support in Python")
259 s
.login(smtpuser
, smtppassword
)
261 result
= s
.sendmail(from_addr
, to_addr_list
, msg
)
263 print("mail server refused delivery for the following recipients: %s" % result
)
264 except Exception as err
:
265 raise CmdException(str(err
))
269 def __send_message_git(msg
, options
):
270 """Send the message using git send-email
272 from subprocess
import call
273 from tempfile
import mkstemp
275 cmd
= ["git", "send-email", "--from=%s" % msg
['From']]
276 cmd
.append("--quiet")
277 cmd
.append("--suppress-cc=self")
279 cmd
.append("--suppress-cc=body")
280 if options
.in_reply_to
:
281 cmd
.extend(["--in-reply-to", options
.in_reply_to
])
282 if options
.no_thread
:
283 cmd
.append("--no-thread")
285 # We only support To/Cc/Bcc in git send-email for now.
286 for x
in ['to', 'cc', 'bcc']:
287 if getattr(options
, x
):
288 cmd
.extend('--%s=%s' % (x
, a
) for a
in getattr(options
, x
))
290 (fd
, path
) = mkstemp()
291 os
.write(fd
, msg
.as_string(options
.mbox
))
298 except Exception as err
:
299 raise CmdException(str(err
))
303 def __send_message(type, tmpl
, options
, *args
):
304 """Message sending dispatcher.
306 (build
, outstr
) = {'cover': (__build_cover
, 'the cover message'),
307 'patch': (__build_message
, 'patch "%s"' % args
[0])}[type]
309 (patch_nr
, total_nr
) = (args
[1], args
[2])
311 msg_id
= email
.utils
.make_msgid('stgit')
312 msg
= build(tmpl
, msg_id
, options
, *args
)
314 msg_str
= msg
.as_string(options
.mbox
)
316 out
.stdout_raw(msg_str
+ '\n')
320 from_addr
, to_addrs
= __parse_addresses(msg
)
321 out
.start('Sending ' + outstr
)
323 smtpserver
= options
.smtp_server
or config
.get('stgit.smtpserver')
325 __send_message_git(msg
, options
)
326 elif smtpserver
.startswith('/'):
327 # Use the sendmail tool
328 __send_message_sendmail(smtpserver
, msg_str
)
330 # Use the SMTP server (we have host and port information)
331 __send_message_smtp(smtpserver
, from_addr
, to_addrs
, msg_str
, options
)
333 # give recipients a chance of receiving related patches in correct order
334 if type == 'cover' or (type == 'patch' and patch_nr
< total_nr
):
335 sleep
= options
.sleep
or config
.getint('stgit.smtpdelay')
341 def __update_header(msg
, header
, addr
= '', ignore
= ()):
342 addr_pairs
= email
.utils
.getaddresses(msg
.get_all(header
, []) + [addr
])
344 # remove pairs without an address and resolve the aliases
345 addr_pairs
= [address_or_alias(name_addr
) for name_addr
in addr_pairs
347 # remove the duplicates and filter the addresses
348 addr_pairs
= [name_addr
for name_addr
in addr_pairs
349 if name_addr
[1] not in ignore
]
351 msg
[header
] = ', '.join(map(email
.utils
.formataddr
, addr_pairs
))
352 return set(addr
for _
, addr
in addr_pairs
)
354 def __build_address_headers(msg
, options
, extra_cc
= []):
355 """Build the address headers and check existing headers in the
363 autobcc
= config
.get('stgit.autobcc') or ''
366 to_addr
= ', '.join(options
.to
)
368 cc_addr
= ', '.join(options
.cc
)
370 extra_cc_addr
= ', '.join(extra_cc
)
372 bcc_addr
= ', '.join(options
.bcc
+ [autobcc
])
376 # if an address is on a header, ignore it from the rest
377 from_set
= __update_header(msg
, 'From')
378 to_set
= __update_header(msg
, 'To', to_addr
)
379 # --auto generated addresses, don't include the sender
380 __update_header(msg
, 'Cc', extra_cc_addr
, from_set
)
381 cc_set
= __update_header(msg
, 'Cc', cc_addr
, to_set
)
382 bcc_set
= __update_header(msg
, 'Bcc', bcc_addr
, to_set
.union(cc_set
))
384 def __get_signers_list(msg
):
385 """Return the address list generated from signed-off-by and
386 acked-by lines in the message.
389 tags
= '%s|%s|%s|%s|%s|%s|%s|%s' % (
397 'reported-and-tested-by')
398 regex
= r
'^(%s):\s+(.+)$' % tags
400 r
= re
.compile(regex
, re
.I
)
401 for line
in msg
.split('\n'):
404 addr_list
.append(m
.expand('\g<2>'))
408 def __build_extra_headers(msg
, msg_id
, ref_id
= None):
409 """Build extra email headers and encoding
412 msg
['Date'] = email
.utils
.formatdate(localtime
= True)
413 msg
['Message-ID'] = msg_id
415 # make sure the ref id has the angle brackets
416 ref_id
= '<%s>' % ref_id
.strip(' \t\n<>')
417 msg
['In-Reply-To'] = ref_id
418 msg
['References'] = ref_id
419 msg
['User-Agent'] = 'StGit/%s' % version
.version
421 # update other address headers
422 __update_header(msg
, 'Reply-To')
423 __update_header(msg
, 'Mail-Reply-To')
424 __update_header(msg
, 'Mail-Followup-To')
427 def __encode_message(msg
):
428 # 7 or 8 bit encoding
429 charset
= email
.charset
.Charset('utf-8')
430 charset
.body_encoding
= None
433 for header
, value
in msg
.items():
435 for word
in value
.split(' '):
437 uword
= unicode(word
, 'utf-8')
438 except UnicodeDecodeError:
439 # maybe we should try a different encoding or report
440 # the error. At the moment, we just ignore it
442 words
.append(email
.header
.Header(uword
).encode())
443 new_val
= ' '.join(words
)
444 msg
.replace_header(header
, new_val
)
446 # replace the Subject string with a Header() object otherwise the long
447 # line folding is done using "\n\t" rather than "\n ", causing issues with
448 # some e-mail clients
449 subject
= msg
.get('subject', '')
450 msg
.replace_header('subject',
451 email
.header
.Header(subject
, header_name
= 'subject'))
453 # encode the body and set the MIME and encoding headers
454 if msg
.is_multipart():
455 for p
in msg
.get_payload():
456 p
.set_charset(charset
)
458 msg
.set_charset(charset
)
460 def __edit_message(msg
):
461 fname
= '.stgitmail.txt'
463 # create the initial file
464 with
open(fname
, 'w') as f
:
469 # read the message back
470 with
open(fname
) as f
:
475 def __build_cover(tmpl
, msg_id
, options
, patches
):
476 """Build the cover message (series description) to be sent via SMTP
478 sender
= __get_sender()
481 version_str
= '%s' % options
.version
488 prefix_str
= options
.prefix
490 prefix_str
= config
.get('stgit.mail.prefix')
497 total_nr_str
= str(len(patches
))
498 patch_nr_str
= '0'.zfill(len(total_nr_str
))
500 number_str
= '%s/%s' % (patch_nr_str
, total_nr_str
)
506 tmpl_dict
= {'sender': sender
,
507 # for backward template compatibility
508 'maintainer': sender
,
509 # for backward template compatibility
511 # for backward template compatibility
513 'version': version_str
,
514 'vspace': version_space
,
515 'prefix': prefix_str
,
516 'pspace': prefix_space
,
517 'patchnr': patch_nr_str
,
518 'totalnr': total_nr_str
,
519 'number': number_str
,
520 'nspace': number_space
,
521 'snumber': number_str
.strip(),
522 'shortlog': stack
.shortlog(crt_series
.get_patch(p
)
523 for p
in reversed(patches
)),
524 'diffstat': gitlib
.diffstat(git
.diff(
525 rev1
= git_id(crt_series
, '%s^' % patches
[0]),
526 rev2
= git_id(crt_series
, '%s' % patches
[-1]),
527 diff_flags
= options
.diff_flags
))}
530 msg_string
= tmpl
% tmpl_dict
531 except KeyError as err
:
532 raise CmdException('Unknown patch template variable: %s' % err
)
534 raise CmdException('Only "%(name)s" variables are '
535 'supported in the patch template')
537 if options
.edit_cover
:
538 msg_string
= __edit_message(msg_string
)
540 # The Python email message
542 msg
= email
.message_from_string(msg_string
)
543 except Exception as ex
:
544 raise CmdException('template parsing error: %s' % str(ex
))
548 for patch
in patches
:
549 p
= crt_series
.get_patch(patch
)
550 if p
.get_description():
551 descr
= p
.get_description().strip()
552 extra_cc
.extend(__get_signers_list(descr
))
553 extra_cc
= list(set(extra_cc
))
557 __build_address_headers(msg
, options
, extra_cc
)
558 __build_extra_headers(msg
, msg_id
, options
.in_reply_to
)
559 __encode_message(msg
)
563 def __build_message(tmpl
, msg_id
, options
, patch
, patch_nr
, total_nr
, ref_id
):
564 """Build the message to be sent via SMTP
566 p
= crt_series
.get_patch(patch
)
568 if p
.get_description():
569 descr
= p
.get_description().strip()
571 # provide a place holder and force the edit message option on
572 descr
= '<empty message>'
573 options
.edit_patches
= True
575 descr_lines
= descr
.split('\n')
576 short_descr
= descr_lines
[0].strip()
577 long_descr
= '\n'.join(l
.rstrip() for l
in descr_lines
[1:]).lstrip('\n')
579 authname
= p
.get_authname()
580 authemail
= p
.get_authemail()
581 commname
= p
.get_commname()
582 commemail
= p
.get_commemail()
584 sender
= __get_sender()
586 fromauth
= '%s <%s>' % (authname
, authemail
)
587 if fromauth
!= sender
:
588 fromauth
= 'From: %s\n\n' % fromauth
593 version_str
= '%s' % options
.version
600 prefix_str
= options
.prefix
602 prefix_str
= config
.get('stgit.mail.prefix')
609 total_nr_str
= str(total_nr
)
610 patch_nr_str
= str(patch_nr
).zfill(len(total_nr_str
))
611 if not options
.unrelated
and total_nr
> 1:
612 number_str
= '%s/%s' % (patch_nr_str
, total_nr_str
)
618 diff
= git
.diff(rev1
= git_id(crt_series
, '%s^' % patch
),
619 rev2
= git_id(crt_series
, '%s' % patch
),
620 diff_flags
= options
.diff_flags
)
621 tmpl_dict
= {'patch': patch
,
623 # for backward template compatibility
624 'maintainer': sender
,
625 'shortdescr': short_descr
,
626 'longdescr': long_descr
,
627 # for backward template compatibility
630 'diffstat': gitlib
.diffstat(diff
),
631 # for backward template compatibility
633 'version': version_str
,
634 'vspace': version_space
,
635 'prefix': prefix_str
,
636 'pspace': prefix_space
,
637 'patchnr': patch_nr_str
,
638 'totalnr': total_nr_str
,
639 'number': number_str
,
640 'nspace': number_space
,
641 'snumber': number_str
.strip(),
642 'fromauth': fromauth
,
643 'authname': authname
,
644 'authemail': authemail
,
645 'authdate': p
.get_authdate(),
646 'commname': commname
,
647 'commemail': commemail
}
649 for key
in tmpl_dict
:
650 if not tmpl_dict
[key
]:
654 msg_string
= tmpl
% tmpl_dict
655 except KeyError as err
:
656 raise CmdException('Unknown patch template variable: %s' % err
)
658 raise CmdException('Only "%(name)s" variables are '
659 'supported in the patch template')
661 if options
.edit_patches
:
662 msg_string
= __edit_message(msg_string
)
664 # The Python email message
666 msg
= email
.message_from_string(msg_string
)
667 except Exception as ex
:
668 raise CmdException('template parsing error: %s' % str(ex
))
671 extra_cc
= __get_signers_list(descr
)
676 __build_address_headers(msg
, options
, extra_cc
)
677 __build_extra_headers(msg
, msg_id
, ref_id
)
678 __encode_message(msg
)
682 def func(parser
, options
, args
):
683 """Send the patches by e-mail using the patchmail.tmpl file as
686 applied
= crt_series
.get_applied()
691 unapplied
= crt_series
.get_unapplied()
692 patches
= parse_patches(args
, applied
+ unapplied
, len(applied
))
694 raise CmdException('Incorrect options. Unknown patches to send')
696 # early test for sender identity
699 out
.start('Checking the validity of the patches')
701 if crt_series
.empty_patch(p
):
702 raise CmdException('Cannot send empty patch "%s"' % p
)
705 total_nr
= len(patches
)
707 raise CmdException('No patches to send')
709 if options
.in_reply_to
:
710 if options
.no_thread
or options
.unrelated
:
711 raise CmdException('--in-reply-to option not allowed with '
712 '--no-thread or --unrelated')
713 ref_id
= options
.in_reply_to
717 # get username/password if sending by SMTP
718 __set_smtp_credentials(options
)
720 # send the cover message (if any)
721 if options
.cover
or options
.edit_cover
:
722 if options
.unrelated
:
723 raise CmdException('cover sending not allowed with --unrelated')
725 # find the template file
727 with
open(options
.cover
) as f
:
730 tmpl
= templates
.get_template('covermail.tmpl')
732 raise CmdException('No cover message template file found')
734 msg_id
= __send_message('cover', tmpl
, options
, patches
)
736 # subsequent e-mails are seen as replies to the first one
737 if not options
.no_thread
:
742 with
open(options
.template
) as f
:
746 tmpl
= templates
.get_template('mailattch.tmpl')
747 elif options
.attach_inline
:
748 tmpl
= templates
.get_template('patchandattch.tmpl')
750 tmpl
= templates
.get_template('patchmail.tmpl')
752 raise CmdException('No e-mail template file found')
754 for (p
, n
) in zip(patches
, range(1, total_nr
+ 1)):
755 msg_id
= __send_message('patch', tmpl
, options
, p
, n
, total_nr
, ref_id
)
757 # subsequent e-mails are seen as replies to the first one
758 if not options
.no_thread
and not options
.unrelated
and not ref_id
: