Allow mail template to be empty
[stgit.git] / stgit / commands / mail.py
blob049c89747dfbab43c21cb505007a0c783a4557d8
1 import email
2 import email.charset
3 import email.header
4 import email.utils
5 import getpass
6 import io
7 import os
8 import re
9 import smtplib
10 import time
12 from stgit import templates, version
13 from stgit.argparse import diff_opts_option, opt, patch_range
14 from stgit.commands.common import (
15 CmdException,
16 DirectoryHasRepository,
17 address_or_alias,
18 parse_patches,
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
26 __copyright__ = """
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/.
40 """
42 help = 'Send a patch or series of patches by e-mail'
43 kind = 'patch'
44 usage = [' [options] [--] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
45 description = r"""
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
98 the following:
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')]
114 options = [
115 opt(
116 '-a',
117 '--all',
118 action='store_true',
119 short='E-mail all the applied patches',
121 opt(
122 '--to',
123 action='append',
124 args=['mail_aliases'],
125 short='Add TO to the To: list',
127 opt(
128 '--cc',
129 action='append',
130 args=['mail_aliases'],
131 short='Add CC to the Cc: list',
133 opt(
134 '--bcc',
135 action='append',
136 args=['mail_aliases'],
137 short='Add BCC to the Bcc: list',
139 opt(
140 '--auto',
141 action='store_true',
142 short='Automatically cc the patch signers',
144 opt(
145 '--no-thread',
146 action='store_true',
147 short='Do not send subsequent messages as replies',
149 opt(
150 '--unrelated',
151 action='store_true',
152 short='Send patches without sequence numbering',
154 opt(
155 '--attach',
156 action='store_true',
157 short='Send a patch as attachment',
159 opt(
160 '--attach-inline',
161 action='store_true',
162 short='Send a patch inline and as an attachment',
164 opt(
165 '-v',
166 '--version',
167 metavar='VERSION',
168 short='Add VERSION to the [PATCH ...] prefix',
170 opt(
171 '--prefix',
172 metavar='PREFIX',
173 short='Add PREFIX to the [... PATCH ...] prefix',
175 opt(
176 '-t',
177 '--template',
178 metavar='FILE',
179 short='Use FILE as the message template',
181 opt(
182 '-c',
183 '--cover',
184 metavar='FILE',
185 short='Send FILE as the cover message',
187 opt(
188 '-e',
189 '--edit-cover',
190 action='store_true',
191 short='Edit the cover message before sending',
193 opt(
194 '-E',
195 '--edit-patches',
196 action='store_true',
197 short='Edit each patch before sending',
199 opt(
200 '-s',
201 '--sleep',
202 type='int',
203 metavar='SECONDS',
204 short='Sleep for SECONDS between e-mails sending',
206 opt(
207 '--in-reply-to',
208 metavar='REFID',
209 short='Use REFID as the reference id',
211 opt(
212 '--smtp-server',
213 metavar='HOST[:PORT] or "/path/to/sendmail -t -i"',
214 short='SMTP server or command to use for sending mail',
216 opt(
217 '-u',
218 '--smtp-user',
219 metavar='USER',
220 short='Username for SMTP authentication',
222 opt(
223 '-p',
224 '--smtp-password',
225 metavar='PASSWORD',
226 short='Password for SMTP authentication',
228 opt(
229 '-T',
230 '--smtp-tls',
231 action='store_true',
232 short='Use SMTP with TLS encryption',
234 opt(
235 '-b',
236 '--branch',
237 args=['stg_branches'],
238 short='Use BRANCH instead of the default branch',
240 opt(
241 '-m',
242 '--mbox',
243 action='store_true',
244 short='Generate an mbox file instead of sending',
246 opt(
247 '--domain',
248 metavar='DOMAIN',
249 short='Use DOMAIN when generating message IDs '
250 '(instead of the system hostname)',
252 opt(
253 '--git',
254 action='store_true',
255 short='Use git send-email (EXPERIMENTAL)',
257 ] + diff_opts_option()
259 directory = DirectoryHasRepository()
262 def __get_sender():
263 """Return the 'authname <authemail>' string as read from the
264 configuration file
266 sender = config.get('stgit.sender')
267 if not sender:
268 user = Person.user()
269 if user.email:
270 sender = user.name_email
271 else:
272 author = Person.author()
273 if author.email:
274 sender = author.name_email
275 else:
276 raise CmdException(
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')
296 to_addr_list = (
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
316 of sending is SMTP.
318 global __smtp_credentials
320 smtpserver = options.smtp_server or config.get('stgit.smtpserver')
321 if options.mbox or options.git or smtpserver.startswith('/'):
322 return
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
342 try:
343 s = smtplib.SMTP(smtpserver)
344 except Exception as err:
345 raise CmdException(str(err))
347 s.set_debuglevel(0)
348 try:
349 if smtpuser and smtppassword:
350 s.ehlo()
351 if smtpusetls:
352 try:
353 s.starttls()
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)
358 s.ehlo()
359 s.login(smtpuser, smtppassword)
361 result = s.sendmail(from_addr, to_addr_list, msg)
362 if len(result):
363 print(
364 "mail server refused delivery for the following recipients:",
365 result,
367 except Exception as err:
368 raise CmdException(str(err))
370 s.quit()
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")
381 if not options.auto:
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()
394 try:
395 os.write(fd, msg_bytes)
396 os.close(fd)
397 try:
398 cmd.append(path)
399 call(cmd)
400 except Exception as err:
401 raise CmdException(str(err))
402 finally:
403 os.unlink(path)
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"'
414 patches = args[0]
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
421 else:
422 raise AssertionError('invalid msg_type: %s' % msg_type) # pragma: no cover
424 msg_bytes = msg.as_bytes(options.mbox)
426 if options.mbox:
427 out.stdout_bytes(msg_bytes + b'\n')
428 return msg_id
430 if not options.git:
431 from_addr, to_addrs = __parse_addresses(msg)
432 out.start('Sending ' + outstr)
434 smtpserver = options.smtp_server or config.get('stgit.smtpserver')
435 if options.git:
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)
440 else:
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')
447 time.sleep(sleep)
448 if not options.git:
449 out.done()
450 return msg_id
453 def __update_header(msg, header, addr='', ignore=()):
454 addr_pairs = email.utils.getaddresses(msg.get_all(header, []) + [addr])
455 del msg[header]
456 # remove pairs without an address and resolve the aliases
457 addr_pairs = [
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]
462 if addr_pairs:
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
469 template.
471 to_addr = ''
472 cc_addr = ''
473 extra_cc_addr = ''
474 bcc_addr = ''
476 autobcc = config.get('stgit.autobcc') or ''
478 if options.to:
479 to_addr = ', '.join(options.to)
480 if options.cc:
481 cc_addr = ', '.join(options.cc)
482 if extra_cc:
483 extra_cc_addr = ', '.join(extra_cc)
484 if options.bcc:
485 bcc_addr = ', '.join(options.bcc + [autobcc])
486 elif autobcc:
487 bcc_addr = 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.
502 addr_list = []
503 tags = '%s|%s|%s|%s|%s|%s|%s|%s' % (
504 'signed-off-by',
505 'acked-by',
506 'cc',
507 'reviewed-by',
508 'reported-by',
509 'tested-by',
510 'suggested-by',
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
518 # function.
519 regex = r'^(%s):\s+([^#]+).*$' % tags
521 r = re.compile(regex, re.I)
522 for line in msg.split('\n'):
523 m = r.match(line)
524 if m:
525 addr_list.append(m.group(2))
527 return addr_list
530 def __build_extra_headers(msg, msg_id, ref_id=None):
531 """Build extra email headers and encoding"""
532 del msg['Date']
533 msg['Date'] = email.utils.formatdate(localtime=True)
534 msg['Message-ID'] = msg_id
535 if ref_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
553 # encode headers
554 for header, value in msg.items():
555 words = []
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)
573 else:
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(
589 tree1,
590 tree2,
591 diff_opts=['--stat-width=72'],
592 stat=True,
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()
600 if options.version:
601 version_str = '%s' % options.version
602 version_space = ' '
603 else:
604 version_str = ''
605 version_space = ''
607 if options.prefix:
608 prefix_str = options.prefix
609 else:
610 prefix_str = config.get('stgit.mail.prefix')
611 if prefix_str:
612 prefix_space = ' '
613 else:
614 prefix_str = ''
615 prefix_space = ''
617 total_nr_str = str(len(patches))
618 patch_nr_str = '0'.zfill(len(total_nr_str))
619 if len(patches) > 1:
620 number_str = '%s/%s' % (patch_nr_str, total_nr_str)
621 number_space = ' '
622 else:
623 number_str = ''
624 number_space = ''
626 repository = directory.repository
627 stack = repository.current_stack
629 tmpl_dict = {
630 'sender': sender, # for backward template compatibility
631 'maintainer': sender, # for backward template compatibility
632 'endofheaders': '', # for backward template compatibility
633 'date': '',
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),
647 try:
648 msg_bytes = templates.specialize_template(tmpl, tmpl_dict)
649 except KeyError as err:
650 raise CmdException('Unknown patch template variable: %s' % err)
651 except TypeError:
652 raise CmdException(
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
660 try:
661 msg = email.message_from_bytes(msg_bytes)
662 except Exception as ex:
663 raise CmdException('template parsing error: %s' % str(ex))
665 extra_cc = []
666 if options.auto:
667 for pn in patches:
668 message_str = stack.patchs[pn].data.message_str
669 if message_str:
670 descr = message_str.strip()
671 extra_cc.extend(__get_signers_list(descr))
672 extra_cc = list(set(extra_cc))
674 if not options.git:
675 __build_address_headers(msg, options, extra_cc)
676 __build_extra_headers(msg, msg_id, options.in_reply_to)
677 __encode_message(msg)
679 return 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
689 if cd.message_str:
690 descr = cd.message_str.strip()
691 else:
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')
700 author = cd.author
701 committer = cd.committer
703 sender = __get_sender()
705 if author.name_email != sender:
706 fromauth = 'From: %s\n\n' % author.name_email
707 else:
708 fromauth = ''
710 if options.version:
711 version_str = '%s' % options.version
712 version_space = ' '
713 else:
714 version_str = ''
715 version_space = ''
717 if options.prefix:
718 prefix_str = options.prefix
719 else:
720 prefix_str = config.get('stgit.mail.prefix')
721 if prefix_str:
722 prefix_space = ' '
723 else:
724 prefix_str = ''
725 prefix_space = ''
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)
731 number_space = ' '
732 else:
733 number_str = ''
734 number_space = ''
736 diff = repository.diff_tree(
737 cd.parent.data.tree,
738 cd.tree,
739 diff_opts=options.diff_flags,
741 tmpl_dict = {
742 'patch': patch,
743 'sender': sender,
744 'maintainer': sender, # for backward template compatibility
745 'shortdescr': short_descr,
746 'longdescr': long_descr,
747 'endofheaders': '', # for backward template compatibility
748 'diff': diff,
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,
768 try:
769 msg_bytes = templates.specialize_template(tmpl, tmpl_dict)
770 except KeyError as err:
771 raise CmdException('Unknown patch template variable: %s' % err)
772 except TypeError:
773 raise CmdException(
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
781 try:
782 msg = email.message_from_bytes(msg_bytes)
783 except Exception as ex:
784 raise CmdException('template parsing error: %s' % str(ex))
786 if options.auto:
787 extra_cc = __get_signers_list(descr)
788 else:
789 extra_cc = []
791 if not options.git:
792 __build_address_headers(msg, options, extra_cc)
793 __build_extra_headers(msg, msg_id, ref_id)
794 __encode_message(msg)
796 return msg
799 def func(parser, options, args):
800 """Send the patches by e-mail using the patchmail.tmpl file as
801 a template
803 stack = directory.repository.current_stack
804 applied = stack.patchorder.applied
806 if options.all:
807 patches = applied
808 elif len(args) >= 1:
809 unapplied = stack.patchorder.unapplied
810 patches = parse_patches(args, applied + unapplied, len(applied))
811 else:
812 raise CmdException('Incorrect options. Unknown patches to send')
814 # early test for sender identity
815 __get_sender()
817 out.start('Checking the validity of the patches')
818 for pn in patches:
819 if stack.patches[pn].data.is_nochange():
820 raise CmdException('Cannot send empty patch "%s"' % pn)
821 out.done()
823 total_nr = len(patches)
824 if total_nr == 0:
825 raise CmdException('No patches to send')
827 if options.in_reply_to:
828 if options.no_thread or options.unrelated:
829 raise CmdException(
830 '--in-reply-to option not allowed with ' '--no-thread or --unrelated'
832 ref_id = options.in_reply_to
833 else:
834 ref_id = None
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
845 if options.cover:
846 with io.open(options.cover, 'r') as f:
847 tmpl = f.read()
848 else:
849 tmpl = templates.get_template('covermail.tmpl')
850 if not 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:
857 ref_id = msg_id
859 # send the patches
860 if options.template:
861 with io.open(options.template, 'r') as f:
862 tmpl = f.read()
863 else:
864 if options.attach:
865 tmpl = templates.get_template('mailattch.tmpl')
866 elif options.attach_inline:
867 tmpl = templates.get_template('patchandattch.tmpl')
868 else:
869 tmpl = templates.get_template('patchmail.tmpl')
870 if tmpl is None:
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:
878 ref_id = msg_id