Use raw strings for regexps with unescaped \'s
[stgit.git] / stgit / commands / mail.py
blobc9b67f53680df141edd39ca6b4a3606780645925
1 # -*- coding: utf-8 -*-
2 from __future__ import absolute_import, division, print_function
3 import email
4 import email.charset
5 import email.header
6 import email.utils
7 import getpass
8 import os
9 import re
10 import smtplib
11 import socket
12 import time
14 from stgit import argparse, stack, git, version, templates
15 from stgit.argparse import opt
16 from stgit.commands.common import (CmdException,
17 DirectoryHasRepository,
18 address_or_alias,
19 git_id,
20 parse_patches)
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
27 __copyright__ = """
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/.
41 """
43 help = 'Send a patch or series of patches by e-mail'
44 kind = 'patch'
45 usage = [' [options] [--] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
46 description = r"""
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
99 the following:
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)]
117 options = [
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)
170 def __get_sender():
171 """Return the 'authname <authemail>' string as read from the
172 configuration file
174 sender=config.get('stgit.sender')
175 if not sender:
176 try:
177 sender = str(git.user())
178 except git.GitException:
179 try:
180 sender = str(git.author())
181 except git.GitException:
182 pass
183 if not sender:
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
218 of sending is SMTP.
220 global __smtp_credentials
222 smtpserver = options.smtp_server or config.get('stgit.smtpserver')
223 if options.mbox or options.git or smtpserver.startswith('/'):
224 return
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
244 try:
245 s = smtplib.SMTP(smtpserver)
246 except Exception as err:
247 raise CmdException(str(err))
249 s.set_debuglevel(0)
250 try:
251 if smtpuser and smtppassword:
252 s.ehlo()
253 if smtpusetls:
254 if not hasattr(socket, 'ssl'):
255 raise CmdException(
256 "cannot use TLS - no SSL support in Python")
257 s.starttls()
258 s.ehlo()
259 s.login(smtpuser, smtppassword)
261 result = s.sendmail(from_addr, to_addr_list, msg)
262 if len(result):
263 print("mail server refused delivery for the following recipients: %s" % result)
264 except Exception as err:
265 raise CmdException(str(err))
267 s.quit()
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")
278 if not options.auto:
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))
292 os.close(fd)
294 try:
295 try:
296 cmd.append(path)
297 call(cmd)
298 except Exception as err:
299 raise CmdException(str(err))
300 finally:
301 os.unlink(path)
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]
308 if type == 'patch':
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)
315 if options.mbox:
316 out.stdout_raw(msg_str + '\n')
317 return msg_id
319 if not options.git:
320 from_addr, to_addrs = __parse_addresses(msg)
321 out.start('Sending ' + outstr)
323 smtpserver = options.smtp_server or config.get('stgit.smtpserver')
324 if options.git:
325 __send_message_git(msg, options)
326 elif smtpserver.startswith('/'):
327 # Use the sendmail tool
328 __send_message_sendmail(smtpserver, msg_str)
329 else:
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')
336 time.sleep(sleep)
337 if not options.git:
338 out.done()
339 return msg_id
341 def __update_header(msg, header, addr = '', ignore = ()):
342 addr_pairs = email.utils.getaddresses(msg.get_all(header, []) + [addr])
343 del msg[header]
344 # remove pairs without an address and resolve the aliases
345 addr_pairs = [address_or_alias(name_addr) for name_addr in addr_pairs
346 if name_addr[1]]
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]
350 if addr_pairs:
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
356 template.
358 to_addr = ''
359 cc_addr = ''
360 extra_cc_addr = ''
361 bcc_addr = ''
363 autobcc = config.get('stgit.autobcc') or ''
365 if options.to:
366 to_addr = ', '.join(options.to)
367 if options.cc:
368 cc_addr = ', '.join(options.cc)
369 if extra_cc:
370 extra_cc_addr = ', '.join(extra_cc)
371 if options.bcc:
372 bcc_addr = ', '.join(options.bcc + [autobcc])
373 elif autobcc:
374 bcc_addr = 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.
388 addr_list = []
389 tags = '%s|%s|%s|%s|%s|%s|%s|%s' % (
390 'signed-off-by',
391 'acked-by',
392 'cc',
393 'reviewed-by',
394 'reported-by',
395 'tested-by',
396 'suggested-by',
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'):
402 m = r.match(line)
403 if m:
404 addr_list.append(m.expand('\g<2>'))
406 return addr_list
408 def __build_extra_headers(msg, msg_id, ref_id = None):
409 """Build extra email headers and encoding
411 del msg['Date']
412 msg['Date'] = email.utils.formatdate(localtime = True)
413 msg['Message-ID'] = msg_id
414 if ref_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
432 # encode headers
433 for header, value in msg.items():
434 words = []
435 for word in value.split(' '):
436 try:
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
441 pass
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)
457 else:
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:
465 f.write(msg)
467 call_editor(fname)
469 # read the message back
470 with open(fname) as f:
471 msg = f.read()
473 return msg
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()
480 if options.version:
481 version_str = '%s' % options.version
482 version_space = ' '
483 else:
484 version_str = ''
485 version_space = ''
487 if options.prefix:
488 prefix_str = options.prefix
489 else:
490 prefix_str = config.get('stgit.mail.prefix')
491 if prefix_str:
492 prefix_space = ' '
493 else:
494 prefix_str = ''
495 prefix_space = ''
497 total_nr_str = str(len(patches))
498 patch_nr_str = '0'.zfill(len(total_nr_str))
499 if len(patches) > 1:
500 number_str = '%s/%s' % (patch_nr_str, total_nr_str)
501 number_space = ' '
502 else:
503 number_str = ''
504 number_space = ''
506 tmpl_dict = {'sender': sender,
507 # for backward template compatibility
508 'maintainer': sender,
509 # for backward template compatibility
510 'endofheaders': '',
511 # for backward template compatibility
512 'date': '',
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))}
529 try:
530 msg_string = tmpl % tmpl_dict
531 except KeyError as err:
532 raise CmdException('Unknown patch template variable: %s' % err)
533 except TypeError:
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
541 try:
542 msg = email.message_from_string(msg_string)
543 except Exception as ex:
544 raise CmdException('template parsing error: %s' % str(ex))
546 extra_cc = []
547 if options.auto:
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))
556 if not options.git:
557 __build_address_headers(msg, options, extra_cc)
558 __build_extra_headers(msg, msg_id, options.in_reply_to)
559 __encode_message(msg)
561 return 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()
570 else:
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
589 else:
590 fromauth = ''
592 if options.version:
593 version_str = '%s' % options.version
594 version_space = ' '
595 else:
596 version_str = ''
597 version_space = ''
599 if options.prefix:
600 prefix_str = options.prefix
601 else:
602 prefix_str = config.get('stgit.mail.prefix')
603 if prefix_str:
604 prefix_space = ' '
605 else:
606 prefix_str = ''
607 prefix_space = ''
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)
613 number_space = ' '
614 else:
615 number_str = ''
616 number_space = ''
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,
622 'sender': sender,
623 # for backward template compatibility
624 'maintainer': sender,
625 'shortdescr': short_descr,
626 'longdescr': long_descr,
627 # for backward template compatibility
628 'endofheaders': '',
629 'diff': diff,
630 'diffstat': gitlib.diffstat(diff),
631 # for backward template compatibility
632 'date': '',
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}
648 # change None to ''
649 for key in tmpl_dict:
650 if not tmpl_dict[key]:
651 tmpl_dict[key] = ''
653 try:
654 msg_string = tmpl % tmpl_dict
655 except KeyError as err:
656 raise CmdException('Unknown patch template variable: %s' % err)
657 except TypeError:
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
665 try:
666 msg = email.message_from_string(msg_string)
667 except Exception as ex:
668 raise CmdException('template parsing error: %s' % str(ex))
670 if options.auto:
671 extra_cc = __get_signers_list(descr)
672 else:
673 extra_cc = []
675 if not options.git:
676 __build_address_headers(msg, options, extra_cc)
677 __build_extra_headers(msg, msg_id, ref_id)
678 __encode_message(msg)
680 return msg
682 def func(parser, options, args):
683 """Send the patches by e-mail using the patchmail.tmpl file as
684 a template
686 applied = crt_series.get_applied()
688 if options.all:
689 patches = applied
690 elif len(args) >= 1:
691 unapplied = crt_series.get_unapplied()
692 patches = parse_patches(args, applied + unapplied, len(applied))
693 else:
694 raise CmdException('Incorrect options. Unknown patches to send')
696 # early test for sender identity
697 __get_sender()
699 out.start('Checking the validity of the patches')
700 for p in patches:
701 if crt_series.empty_patch(p):
702 raise CmdException('Cannot send empty patch "%s"' % p)
703 out.done()
705 total_nr = len(patches)
706 if total_nr == 0:
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
714 else:
715 ref_id = None
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
726 if options.cover:
727 with open(options.cover) as f:
728 tmpl = f.read()
729 else:
730 tmpl = templates.get_template('covermail.tmpl')
731 if not 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:
738 ref_id = msg_id
740 # send the patches
741 if options.template:
742 with open(options.template) as f:
743 tmpl = f.read()
744 else:
745 if options.attach:
746 tmpl = templates.get_template('mailattch.tmpl')
747 elif options.attach_inline:
748 tmpl = templates.get_template('patchandattch.tmpl')
749 else:
750 tmpl = templates.get_template('patchmail.tmpl')
751 if not 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:
759 ref_id = msg_id