Commit | Line | Data |
---|---|---|
b4bddc06 CM |
1 | __copyright__ = """ |
2 | Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com> | |
3 | ||
4 | This program is free software; you can redistribute it and/or modify | |
5 | it under the terms of the GNU General Public License version 2 as | |
6 | published by the Free Software Foundation. | |
7 | ||
8 | This program is distributed in the hope that it will be useful, | |
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | GNU General Public License for more details. | |
12 | ||
13 | You should have received a copy of the GNU General Public License | |
14 | along with this program; if not, write to the Free Software | |
15 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
16 | """ | |
17 | ||
6cf5ec9b | 18 | import sys, os, re, time, datetime, socket, smtplib, getpass |
61eed152 | 19 | import email, email.Utils, email.Header |
575bbdae | 20 | from stgit.argparse import opt |
b4bddc06 CM |
21 | from stgit.commands.common import * |
22 | from stgit.utils import * | |
5e888f30 | 23 | from stgit.out import * |
20a52e06 | 24 | from stgit import argparse, stack, git, version, templates |
b4bddc06 | 25 | from stgit.config import config |
a0fe60a2 | 26 | from stgit.run import Run |
ef954fe6 | 27 | from stgit.lib import git as gitlib |
b4bddc06 | 28 | |
575bbdae | 29 | help = 'Send a patch or series of patches by e-mail' |
33ff9cdd | 30 | kind = 'patch' |
54239abf | 31 | usage = [' [options] [--] [<patch1>] [<patch2>] [<patch3>..<patch4>]'] |
575bbdae | 32 | description = r""" |
cec913c4 KH |
33 | Send a patch or a range of patches by e-mail using the SMTP server |
34 | specified by the 'stgit.smtpserver' configuration option, or the | |
a0fe60a2 CM |
35 | '--smtp-server' command line option. This option can also be an |
36 | absolute path to 'sendmail' followed by command line arguments. | |
37 | ||
38 | The From address and the e-mail format are generated from the template | |
39 | file passed as argument to '--template' (defaulting to | |
40 | '.git/patchmail.tmpl' or '~/.stgit/templates/patchmail.tmpl' or | |
e5c32acf | 41 | '/usr/share/stgit/templates/patchmail.tmpl'). A patch can be sent as |
a0fe60a2 CM |
42 | attachment using the --attach option in which case the |
43 | 'mailattch.tmpl' template will be used instead of 'patchmail.tmpl'. | |
79df2f0d CM |
44 | |
45 | The To/Cc/Bcc addresses can either be added to the template file or | |
46 | passed via the corresponding command line options. They can be e-mail | |
47 | addresses or aliases which are automatically expanded to the values | |
48 | stored in the [mail "alias"] section of GIT configuration files. | |
2bb96902 | 49 | |
0ba13ee9 KH |
50 | A preamble e-mail can be sent using the '--cover' and/or |
51 | '--edit-cover' options. The first allows the user to specify a file to | |
52 | be used as a template. The latter option will invoke the editor on the | |
53 | specified file (defaulting to '.git/covermail.tmpl' or | |
94d18868 YD |
54 | '~/.stgit/templates/covermail.tmpl' or |
55 | '/usr/share/stgit/templates/covermail.tmpl'). | |
e3e05587 CM |
56 | |
57 | All the subsequent e-mails appear as replies to the first e-mail sent | |
58 | (either the preamble or the first patch). E-mails can be seen as | |
ae5305d2 | 59 | replies to a different e-mail by using the '--in-reply-to' option. |
26aab5b0 CM |
60 | |
61 | SMTP authentication is also possible with '--smtp-user' and | |
62 | '--smtp-password' options, also available as configuration settings: | |
fc44c2ca PR |
63 | 'smtpuser' and 'smtppassword'. TLS encryption can be enabled by |
64 | '--smtp-tls' option and 'smtptls' setting. | |
26aab5b0 | 65 | |
27827959 KH |
66 | The following variables are accepted by both the preamble and the |
67 | patch e-mail templates: | |
26aab5b0 | 68 | |
26aab5b0 | 69 | %(diffstat)s - diff statistics |
27827959 | 70 | %(number)s - empty if only one patch is sent or ' patchnr/totalnr' |
26aab5b0 | 71 | %(patchnr)s - patch number |
27827959 | 72 | %(sender)s - 'sender' or 'authname <authemail>' as per the config file |
26aab5b0 | 73 | %(totalnr)s - total number of patches to be sent |
27827959 KH |
74 | %(version)s - ' version' string passed on the command line (or empty) |
75 | ||
76 | In addition to the common variables, the preamble e-mail template | |
77 | accepts the following: | |
78 | ||
79 | %(shortlog)s - first line of each patch description, listed by author | |
80 | ||
81 | In addition to the common variables, the patch e-mail template accepts | |
82 | the following: | |
83 | ||
26aab5b0 | 84 | %(authdate)s - patch creation date |
27827959 KH |
85 | %(authemail)s - author's email |
86 | %(authname)s - author's name | |
26aab5b0 | 87 | %(commemail)s - committer's e-mail |
27827959 KH |
88 | %(commname)s - committer's name |
89 | %(diff)s - unified diff of the patch | |
77eeb7f4 | 90 | %(fromauth)s - 'From: author\n\n' if different from sender |
27827959 KH |
91 | %(longdescr)s - the rest of the patch description, after the first line |
92 | %(patch)s - patch name | |
93 | %(prefix)s - 'prefix ' string passed on the command line | |
94 | %(shortdescr)s - the first line of the patch description""" | |
b4bddc06 | 95 | |
6c8a90e1 KH |
96 | args = [argparse.patch_range(argparse.applied_patches, |
97 | argparse.unapplied_patches, | |
98 | argparse.hidden_patches)] | |
575bbdae KH |
99 | options = [ |
100 | opt('-a', '--all', action = 'store_true', | |
101 | short = 'E-mail all the applied patches'), | |
102 | opt('--to', action = 'append', | |
103 | short = 'Add TO to the To: list'), | |
104 | opt('--cc', action = 'append', | |
105 | short = 'Add CC to the Cc: list'), | |
106 | opt('--bcc', action = 'append', | |
107 | short = 'Add BCC to the Bcc: list'), | |
108 | opt('--auto', action = 'store_true', | |
109 | short = 'Automatically cc the patch signers'), | |
ae5305d2 | 110 | opt('--no-thread', action = 'store_true', |
575bbdae KH |
111 | short = 'Do not send subsequent messages as replies'), |
112 | opt('--unrelated', action = 'store_true', | |
113 | short = 'Send patches without sequence numbering'), | |
114 | opt('--attach', action = 'store_true', | |
115 | short = 'Send a patch as attachment'), | |
116 | opt('-v', '--version', metavar = 'VERSION', | |
117 | short = 'Add VERSION to the [PATCH ...] prefix'), | |
118 | opt('--prefix', metavar = 'PREFIX', | |
119 | short = 'Add PREFIX to the [... PATCH ...] prefix'), | |
120 | opt('-t', '--template', metavar = 'FILE', | |
121 | short = 'Use FILE as the message template'), | |
122 | opt('-c', '--cover', metavar = 'FILE', | |
123 | short = 'Send FILE as the cover message'), | |
124 | opt('-e', '--edit-cover', action = 'store_true', | |
125 | short = 'Edit the cover message before sending'), | |
126 | opt('-E', '--edit-patches', action = 'store_true', | |
127 | short = 'Edit each patch before sending'), | |
128 | opt('-s', '--sleep', type = 'int', metavar = 'SECONDS', | |
129 | short = 'Sleep for SECONDS between e-mails sending'), | |
ae5305d2 | 130 | opt('--in-reply-to', metavar = 'REFID', |
575bbdae KH |
131 | short = 'Use REFID as the reference id'), |
132 | opt('--smtp-server', metavar = 'HOST[:PORT] or "/path/to/sendmail -t -i"', | |
133 | short = 'SMTP server or command to use for sending mail'), | |
134 | opt('-u', '--smtp-user', metavar = 'USER', | |
135 | short = 'Username for SMTP authentication'), | |
136 | opt('-p', '--smtp-password', metavar = 'PASSWORD', | |
137 | short = 'Password for SMTP authentication'), | |
138 | opt('-T', '--smtp-tls', action = 'store_true', | |
139 | short = 'Use SMTP with TLS encryption'), | |
6c8a90e1 | 140 | opt('-b', '--branch', args = [argparse.stg_branches], |
575bbdae KH |
141 | short = 'Use BRANCH instead of the default branch'), |
142 | opt('-m', '--mbox', action = 'store_true', | |
97ccbbbc AC |
143 | short = 'Generate an mbox file instead of sending'), |
144 | opt('--git', action = 'store_true', | |
145 | short = 'Use git send-email (EXPERIMENTAL)') | |
575bbdae KH |
146 | ] + argparse.diff_opts_option() |
147 | ||
117ed129 | 148 | directory = DirectoryHasRepository(log = False) |
b4bddc06 | 149 | |
901288c2 | 150 | def __get_sender(): |
dae0f0be CM |
151 | """Return the 'authname <authemail>' string as read from the |
152 | configuration file | |
153 | """ | |
c73e63b7 YD |
154 | sender=config.get('stgit.sender') |
155 | if not sender: | |
9e3f506f KH |
156 | try: |
157 | sender = str(git.user()) | |
158 | except git.GitException: | |
ea09f8ce GH |
159 | try: |
160 | sender = str(git.author()) | |
161 | except git.GitException: | |
162 | pass | |
9e3f506f | 163 | if not sender: |
ea09f8ce GH |
164 | raise CmdException, ('Unknown sender name and e-mail; you should' |
165 | ' for example set git config user.name and' | |
166 | ' user.email') | |
cd74a041 CM |
167 | sender = email.Utils.parseaddr(sender) |
168 | ||
169 | return email.Utils.formataddr(address_or_alias(sender)) | |
dae0f0be | 170 | |
cd74a041 CM |
171 | def __addr_list(msg, header): |
172 | return [addr for name, addr in | |
173 | email.Utils.getaddresses(msg.get_all(header, []))] | |
9e3f506f | 174 | |
d650d6ed | 175 | def __parse_addresses(msg): |
b4bddc06 CM |
176 | """Return a two elements tuple: (from, [to]) |
177 | """ | |
d650d6ed | 178 | from_addr_list = __addr_list(msg, 'From') |
24aadb3f | 179 | if len(from_addr_list) == 0: |
b4bddc06 | 180 | raise CmdException, 'No "From" address' |
d650d6ed CM |
181 | |
182 | to_addr_list = __addr_list(msg, 'To') + __addr_list(msg, 'Cc') \ | |
183 | + __addr_list(msg, 'Bcc') | |
b4bddc06 CM |
184 | if len(to_addr_list) == 0: |
185 | raise CmdException, 'No "To/Cc/Bcc" addresses' | |
186 | ||
cd74a041 | 187 | return (from_addr_list[0], set(to_addr_list)) |
b4bddc06 | 188 | |
a0fe60a2 CM |
189 | def __send_message_sendmail(sendmail, msg): |
190 | """Send the message using the sendmail command. | |
191 | """ | |
192 | cmd = sendmail.split() | |
193 | Run(*cmd).raw_input(msg).discard_output() | |
194 | ||
46e9c9f2 CM |
195 | __smtp_credentials = None |
196 | ||
197 | def __set_smtp_credentials(options): | |
198 | """Set the (smtpuser, smtppassword, smtpusetls) credentials if the method | |
199 | of sending is SMTP. | |
b4bddc06 | 200 | """ |
46e9c9f2 CM |
201 | global __smtp_credentials |
202 | ||
203 | smtpserver = options.smtp_server or config.get('stgit.smtpserver') | |
204 | if options.mbox or options.git or smtpserver.startswith('/'): | |
205 | return | |
206 | ||
89d7ec43 AC |
207 | smtppassword = options.smtp_password or config.get('stgit.smtppassword') |
208 | smtpuser = options.smtp_user or config.get('stgit.smtpuser') | |
209 | smtpusetls = options.smtp_tls or config.get('stgit.smtptls') == 'yes' | |
210 | ||
211 | if (smtppassword and not smtpuser): | |
212 | raise CmdException('SMTP password supplied, username needed') | |
213 | if (smtpusetls and not smtpuser): | |
214 | raise CmdException('SMTP over TLS requested, username needed') | |
215 | if (smtpuser and not smtppassword): | |
216 | smtppassword = getpass.getpass("Please enter SMTP password: ") | |
217 | ||
46e9c9f2 CM |
218 | __smtp_credentials = (smtpuser, smtppassword, smtpusetls) |
219 | ||
220 | def __send_message_smtp(smtpserver, from_addr, to_addr_list, msg, options): | |
221 | """Send the message using the given SMTP server | |
222 | """ | |
223 | smtpuser, smtppassword, smtpusetls = __smtp_credentials | |
224 | ||
b4bddc06 CM |
225 | try: |
226 | s = smtplib.SMTP(smtpserver) | |
227 | except Exception, err: | |
228 | raise CmdException, str(err) | |
229 | ||
230 | s.set_debuglevel(0) | |
231 | try: | |
eb026d93 B |
232 | if smtpuser and smtppassword: |
233 | s.ehlo() | |
89d7ec43 | 234 | if smtpusetls: |
fc44c2ca PR |
235 | if not hasattr(socket, 'ssl'): |
236 | raise CmdException, "cannot use TLS - no SSL support in Python" | |
237 | s.starttls() | |
238 | s.ehlo() | |
eb026d93 B |
239 | s.login(smtpuser, smtppassword) |
240 | ||
0bc1343c YD |
241 | result = s.sendmail(from_addr, to_addr_list, msg) |
242 | if len(result): | |
243 | print "mail server refused delivery for the following recipients: %s" % result | |
b4bddc06 CM |
244 | except Exception, err: |
245 | raise CmdException, str(err) | |
246 | ||
247 | s.quit() | |
248 | ||
97ccbbbc AC |
249 | def __send_message_git(msg, options): |
250 | """Send the message using git send-email | |
251 | """ | |
252 | from subprocess import call | |
253 | from tempfile import mkstemp | |
254 | ||
255 | cmd = ["git", "send-email", "--from=%s" % msg['From']] | |
256 | cmd.append("--quiet") | |
257 | cmd.append("--suppress-cc=self") | |
258 | if not options.auto: | |
259 | cmd.append("--suppress-cc=body") | |
35112e50 CM |
260 | if options.in_reply_to: |
261 | cmd.extend(["--in-reply-to", options.in_reply_to]) | |
262 | if options.no_thread: | |
263 | cmd.append("--no-thread") | |
97ccbbbc AC |
264 | |
265 | # We only support To/Cc/Bcc in git send-email for now. | |
266 | for x in ['to', 'cc', 'bcc']: | |
267 | if getattr(options, x): | |
268 | cmd.extend('--%s=%s' % (x, a) for a in getattr(options, x)) | |
269 | ||
97ccbbbc AC |
270 | (fd, path) = mkstemp() |
271 | os.write(fd, msg.as_string(options.mbox)) | |
272 | os.close(fd) | |
273 | ||
274 | try: | |
275 | try: | |
276 | cmd.append(path) | |
277 | call(cmd) | |
278 | except Exception, err: | |
279 | raise CmdException, str(err) | |
280 | finally: | |
281 | os.unlink(path) | |
282 | ||
9cb369d4 | 283 | def __send_message(type, tmpl, options, *args): |
a0fe60a2 CM |
284 | """Message sending dispatcher. |
285 | """ | |
9cb369d4 AC |
286 | (build, outstr) = {'cover': (__build_cover, 'the cover message'), |
287 | 'patch': (__build_message, 'patch "%s"' % args[0])}[type] | |
288 | if type == 'patch': | |
289 | (patch_nr, total_nr) = (args[1], args[2]) | |
290 | ||
291 | msg_id = email.Utils.make_msgid('stgit') | |
292 | msg = build(tmpl, msg_id, options, *args) | |
293 | ||
9cb369d4 AC |
294 | msg_str = msg.as_string(options.mbox) |
295 | if options.mbox: | |
296 | out.stdout_raw(msg_str + '\n') | |
297 | return msg_id | |
298 | ||
97ccbbbc | 299 | if not options.git: |
e2a3c618 | 300 | from_addr, to_addrs = __parse_addresses(msg) |
97ccbbbc | 301 | out.start('Sending ' + outstr) |
89d7ec43 | 302 | |
9cb369d4 | 303 | smtpserver = options.smtp_server or config.get('stgit.smtpserver') |
97ccbbbc AC |
304 | if options.git: |
305 | __send_message_git(msg, options) | |
306 | elif smtpserver.startswith('/'): | |
a0fe60a2 | 307 | # Use the sendmail tool |
9cb369d4 | 308 | __send_message_sendmail(smtpserver, msg_str) |
a0fe60a2 CM |
309 | else: |
310 | # Use the SMTP server (we have host and port information) | |
9cb369d4 AC |
311 | __send_message_smtp(smtpserver, from_addr, to_addrs, msg_str, options) |
312 | ||
313 | # give recipients a chance of receiving related patches in correct order | |
314 | if type == 'cover' or (type == 'patch' and patch_nr < total_nr): | |
315 | sleep = options.sleep or config.getint('stgit.smtpdelay') | |
316 | time.sleep(sleep) | |
97ccbbbc AC |
317 | if not options.git: |
318 | out.done() | |
9cb369d4 | 319 | return msg_id |
a0fe60a2 | 320 | |
c28b8841 | 321 | def __update_header(msg, header, addr = '', ignore = ()): |
cd74a041 CM |
322 | def __addr_pairs(msg, header, extra): |
323 | pairs = email.Utils.getaddresses(msg.get_all(header, []) + extra) | |
324 | # remove pairs without an address and resolve the aliases | |
325 | return [address_or_alias(p) for p in pairs if p[1]] | |
326 | ||
c28b8841 AC |
327 | addr_pairs = __addr_pairs(msg, header, [addr]) |
328 | del msg[header] | |
329 | # remove the duplicates and filter the addresses | |
330 | addr_dict = dict((addr, email.Utils.formataddr((name, addr))) | |
331 | for name, addr in addr_pairs if addr not in ignore) | |
332 | if addr_dict: | |
333 | msg[header] = ', '.join(addr_dict.itervalues()) | |
334 | return set(addr_dict.iterkeys()) | |
f8d1cf65 | 335 | |
c28b8841 AC |
336 | def __build_address_headers(msg, options, extra_cc = []): |
337 | """Build the address headers and check existing headers in the | |
338 | template. | |
339 | """ | |
f8d1cf65 CM |
340 | to_addr = '' |
341 | cc_addr = '' | |
cd74a041 | 342 | extra_cc_addr = '' |
f8d1cf65 CM |
343 | bcc_addr = '' |
344 | ||
c73e63b7 | 345 | autobcc = config.get('stgit.autobcc') or '' |
d884c4d8 | 346 | |
e83b3149 | 347 | if options.to: |
61eed152 | 348 | to_addr = ', '.join(options.to) |
e83b3149 | 349 | if options.cc: |
cd74a041 CM |
350 | cc_addr = ', '.join(options.cc) |
351 | if extra_cc: | |
352 | extra_cc_addr = ', '.join(extra_cc) | |
e83b3149 | 353 | if options.bcc: |
61eed152 | 354 | bcc_addr = ', '.join(options.bcc + [autobcc]) |
d884c4d8 CM |
355 | elif autobcc: |
356 | bcc_addr = autobcc | |
f8d1cf65 | 357 | |
cd74a041 | 358 | # if an address is on a header, ignore it from the rest |
c28b8841 AC |
359 | to_set = __update_header(msg, 'To', to_addr) |
360 | cc_set = __update_header(msg, 'Cc', cc_addr, to_set) | |
361 | bcc_set = __update_header(msg, 'Bcc', bcc_addr, to_set.union(cc_set)) | |
cd74a041 CM |
362 | |
363 | # --auto generated addresses, don't include the sender | |
c28b8841 AC |
364 | from_set = __update_header(msg, 'From') |
365 | __update_header(msg, 'Cc', extra_cc_addr, | |
366 | to_set.union(bcc_set).union(from_set)) | |
f8d1cf65 CM |
367 | |
368 | def __get_signers_list(msg): | |
369 | """Return the address list generated from signed-off-by and | |
370 | acked-by lines in the message. | |
371 | """ | |
372 | addr_list = [] | |
de41ecc8 DW |
373 | tags = '%s|%s|%s|%s|%s|%s|%s' % ( |
374 | 'signed-off-by', | |
375 | 'acked-by', | |
376 | 'cc', | |
377 | 'reviewed-by', | |
378 | 'reported-by', | |
379 | 'tested-by', | |
380 | 'reported-and-tested-by') | |
381 | regex = '^(%s):\s+(.+)$' % tags | |
382 | ||
383 | r = re.compile(regex, re.I) | |
f8d1cf65 CM |
384 | for line in msg.split('\n'): |
385 | m = r.match(line) | |
386 | if m: | |
387 | addr_list.append(m.expand('\g<2>')) | |
388 | ||
389 | return addr_list | |
e83b3149 | 390 | |
61eed152 CM |
391 | def __build_extra_headers(msg, msg_id, ref_id = None): |
392 | """Build extra email headers and encoding | |
19a56fa1 | 393 | """ |
61eed152 CM |
394 | del msg['Date'] |
395 | msg['Date'] = email.Utils.formatdate(localtime = True) | |
396 | msg['Message-ID'] = msg_id | |
397 | if ref_id: | |
00375337 CM |
398 | # make sure the ref id has the angle brackets |
399 | ref_id = '<%s>' % ref_id.strip(' \t\n<>') | |
61eed152 CM |
400 | msg['In-Reply-To'] = ref_id |
401 | msg['References'] = ref_id | |
d5214f56 | 402 | msg['User-Agent'] = 'StGit/%s' % version.version |
61eed152 | 403 | |
c28b8841 AC |
404 | # update other address headers |
405 | __update_header(msg, 'Reply-To') | |
406 | __update_header(msg, 'Mail-Reply-To') | |
407 | __update_header(msg, 'Mail-Followup-To') | |
408 | ||
409 | ||
61eed152 CM |
410 | def __encode_message(msg): |
411 | # 7 or 8 bit encoding | |
412 | charset = email.Charset.Charset('utf-8') | |
413 | charset.body_encoding = None | |
414 | ||
415 | # encode headers | |
416 | for header, value in msg.items(): | |
417 | words = [] | |
418 | for word in value.split(' '): | |
419 | try: | |
420 | uword = unicode(word, 'utf-8') | |
421 | except UnicodeDecodeError: | |
422 | # maybe we should try a different encoding or report | |
423 | # the error. At the moment, we just ignore it | |
424 | pass | |
425 | words.append(email.Header.Header(uword).encode()) | |
426 | new_val = ' '.join(words) | |
427 | msg.replace_header(header, new_val) | |
428 | ||
429 | # encode the body and set the MIME and encoding headers | |
e5c32acf CM |
430 | if msg.is_multipart(): |
431 | for p in msg.get_payload(): | |
432 | p.set_charset(charset) | |
433 | else: | |
434 | msg.set_charset(charset) | |
19a56fa1 | 435 | |
58c61f10 | 436 | def __edit_message(msg): |
0ba13ee9 KH |
437 | fname = '.stgitmail.txt' |
438 | ||
439 | # create the initial file | |
440 | f = file(fname, 'w') | |
441 | f.write(msg) | |
442 | f.close() | |
443 | ||
83bb4e4c | 444 | call_editor(fname) |
0ba13ee9 KH |
445 | |
446 | # read the message back | |
447 | f = file(fname) | |
448 | msg = f.read() | |
449 | f.close() | |
450 | ||
451 | return msg | |
452 | ||
c18d3a36 | 453 | def __build_cover(tmpl, msg_id, options, patches): |
e3e05587 | 454 | """Build the cover message (series description) to be sent via SMTP |
b4bddc06 | 455 | """ |
901288c2 | 456 | sender = __get_sender() |
dae0f0be | 457 | |
d0d139a3 CM |
458 | if options.version: |
459 | version_str = ' %s' % options.version | |
ed5de0cc CM |
460 | else: |
461 | version_str = '' | |
d0d139a3 | 462 | |
d323b5da RR |
463 | if options.prefix: |
464 | prefix_str = options.prefix + ' ' | |
465 | else: | |
a7e0d4ee YD |
466 | confprefix = config.get('stgit.mail.prefix') |
467 | if confprefix: | |
468 | prefix_str = confprefix + ' ' | |
469 | else: | |
470 | prefix_str = '' | |
d323b5da | 471 | |
99c4a4c5 | 472 | total_nr_str = str(len(patches)) |
b8d258e5 | 473 | patch_nr_str = '0'.zfill(len(total_nr_str)) |
99c4a4c5 | 474 | if len(patches) > 1: |
b8d258e5 CM |
475 | number_str = ' %s/%s' % (patch_nr_str, total_nr_str) |
476 | else: | |
477 | number_str = '' | |
b4bddc06 | 478 | |
901288c2 CM |
479 | tmpl_dict = {'sender': sender, |
480 | # for backward template compatibility | |
481 | 'maintainer': sender, | |
61eed152 CM |
482 | # for backward template compatibility |
483 | 'endofheaders': '', | |
484 | # for backward template compatibility | |
485 | 'date': '', | |
d0d139a3 | 486 | 'version': version_str, |
d323b5da | 487 | 'prefix': prefix_str, |
b8d258e5 CM |
488 | 'patchnr': patch_nr_str, |
489 | 'totalnr': total_nr_str, | |
99c4a4c5 | 490 | 'number': number_str, |
27827959 | 491 | 'shortlog': stack.shortlog(crt_series.get_patch(p) |
c9379a15 | 492 | for p in reversed(patches)), |
ef954fe6 | 493 | 'diffstat': gitlib.diffstat(git.diff( |
e4560d7e | 494 | rev1 = git_id(crt_series, '%s^' % patches[0]), |
baf8241d CM |
495 | rev2 = git_id(crt_series, '%s' % patches[-1]), |
496 | diff_flags = options.diff_flags))} | |
b4bddc06 CM |
497 | |
498 | try: | |
61eed152 | 499 | msg_string = tmpl % tmpl_dict |
b4bddc06 CM |
500 | except KeyError, err: |
501 | raise CmdException, 'Unknown patch template variable: %s' \ | |
502 | % err | |
503 | except TypeError: | |
504 | raise CmdException, 'Only "%(name)s" variables are ' \ | |
505 | 'supported in the patch template' | |
506 | ||
58c61f10 CM |
507 | if options.edit_cover: |
508 | msg_string = __edit_message(msg_string) | |
509 | ||
61eed152 CM |
510 | # The Python email message |
511 | try: | |
512 | msg = email.message_from_string(msg_string) | |
513 | except Exception, ex: | |
514 | raise CmdException, 'template parsing error: %s' % str(ex) | |
515 | ||
e2a3c618 AC |
516 | if not options.git: |
517 | __build_address_headers(msg, options) | |
ae5305d2 | 518 | __build_extra_headers(msg, msg_id, options.in_reply_to) |
61eed152 CM |
519 | __encode_message(msg) |
520 | ||
d650d6ed | 521 | return msg |
b4bddc06 | 522 | |
c18d3a36 | 523 | def __build_message(tmpl, msg_id, options, patch, patch_nr, total_nr, ref_id): |
b4bddc06 CM |
524 | """Build the message to be sent via SMTP |
525 | """ | |
526 | p = crt_series.get_patch(patch) | |
527 | ||
c897c87c AS |
528 | if p.get_description(): |
529 | descr = p.get_description().strip() | |
530 | else: | |
531 | # provide a place holder and force the edit message option on | |
532 | descr = '<empty message>' | |
533 | options.edit_patches = True | |
b4bddc06 | 534 | |
c897c87c | 535 | descr_lines = descr.split('\n') |
42857cbe ST |
536 | short_descr = descr_lines[0].strip() |
537 | long_descr = '\n'.join(l.rstrip() for l in descr_lines[1:]).lstrip('\n') | |
b4bddc06 | 538 | |
1d1485c3 CM |
539 | authname = p.get_authname(); |
540 | authemail = p.get_authemail(); | |
541 | commname = p.get_commname(); | |
542 | commemail = p.get_commemail(); | |
543 | ||
901288c2 | 544 | sender = __get_sender() |
1d1485c3 CM |
545 | |
546 | fromauth = '%s <%s>' % (authname, authemail) | |
901288c2 | 547 | if fromauth != sender: |
1d1485c3 CM |
548 | fromauth = 'From: %s\n\n' % fromauth |
549 | else: | |
550 | fromauth = '' | |
dae0f0be | 551 | |
d0d139a3 CM |
552 | if options.version: |
553 | version_str = ' %s' % options.version | |
ed5de0cc CM |
554 | else: |
555 | version_str = '' | |
d0d139a3 | 556 | |
d323b5da RR |
557 | if options.prefix: |
558 | prefix_str = options.prefix + ' ' | |
559 | else: | |
a7e0d4ee YD |
560 | confprefix = config.get('stgit.mail.prefix') |
561 | if confprefix: | |
562 | prefix_str = confprefix + ' ' | |
563 | else: | |
564 | prefix_str = '' | |
0d219030 | 565 | |
b4bddc06 CM |
566 | total_nr_str = str(total_nr) |
567 | patch_nr_str = str(patch_nr).zfill(len(total_nr_str)) | |
c2a8af1d | 568 | if not options.unrelated and total_nr > 1: |
b8d258e5 CM |
569 | number_str = ' %s/%s' % (patch_nr_str, total_nr_str) |
570 | else: | |
571 | number_str = '' | |
b4bddc06 | 572 | |
e4560d7e CM |
573 | diff = git.diff(rev1 = git_id(crt_series, '%s^' % patch), |
574 | rev2 = git_id(crt_series, '%s' % patch), | |
a45cea15 | 575 | diff_flags = options.diff_flags) |
b4bddc06 | 576 | tmpl_dict = {'patch': patch, |
901288c2 CM |
577 | 'sender': sender, |
578 | # for backward template compatibility | |
579 | 'maintainer': sender, | |
b4bddc06 CM |
580 | 'shortdescr': short_descr, |
581 | 'longdescr': long_descr, | |
61eed152 CM |
582 | # for backward template compatibility |
583 | 'endofheaders': '', | |
a45cea15 | 584 | 'diff': diff, |
ef954fe6 | 585 | 'diffstat': gitlib.diffstat(diff), |
61eed152 CM |
586 | # for backward template compatibility |
587 | 'date': '', | |
d0d139a3 | 588 | 'version': version_str, |
d323b5da | 589 | 'prefix': prefix_str, |
b4bddc06 CM |
590 | 'patchnr': patch_nr_str, |
591 | 'totalnr': total_nr_str, | |
b8d258e5 | 592 | 'number': number_str, |
1d1485c3 CM |
593 | 'fromauth': fromauth, |
594 | 'authname': authname, | |
595 | 'authemail': authemail, | |
b4bddc06 | 596 | 'authdate': p.get_authdate(), |
1d1485c3 CM |
597 | 'commname': commname, |
598 | 'commemail': commemail} | |
61eed152 | 599 | # change None to '' |
b4bddc06 CM |
600 | for key in tmpl_dict: |
601 | if not tmpl_dict[key]: | |
602 | tmpl_dict[key] = '' | |
603 | ||
604 | try: | |
61eed152 | 605 | msg_string = tmpl % tmpl_dict |
b4bddc06 CM |
606 | except KeyError, err: |
607 | raise CmdException, 'Unknown patch template variable: %s' \ | |
608 | % err | |
609 | except TypeError: | |
610 | raise CmdException, 'Only "%(name)s" variables are ' \ | |
611 | 'supported in the patch template' | |
612 | ||
58c61f10 CM |
613 | if options.edit_patches: |
614 | msg_string = __edit_message(msg_string) | |
615 | ||
61eed152 CM |
616 | # The Python email message |
617 | try: | |
618 | msg = email.message_from_string(msg_string) | |
619 | except Exception, ex: | |
620 | raise CmdException, 'template parsing error: %s' % str(ex) | |
621 | ||
622 | if options.auto: | |
623 | extra_cc = __get_signers_list(descr) | |
624 | else: | |
625 | extra_cc = [] | |
626 | ||
e2a3c618 AC |
627 | if not options.git: |
628 | __build_address_headers(msg, options, extra_cc) | |
61eed152 CM |
629 | __build_extra_headers(msg, msg_id, ref_id) |
630 | __encode_message(msg) | |
631 | ||
d650d6ed | 632 | return msg |
b4bddc06 | 633 | |
b4bddc06 CM |
634 | def func(parser, options, args): |
635 | """Send the patches by e-mail using the patchmail.tmpl file as | |
636 | a template | |
637 | """ | |
b4bddc06 | 638 | applied = crt_series.get_applied() |
b4bddc06 | 639 | |
6b1e0111 CM |
640 | if options.all: |
641 | patches = applied | |
642 | elif len(args) >= 1: | |
b4f656f0 CM |
643 | unapplied = crt_series.get_unapplied() |
644 | patches = parse_patches(args, applied + unapplied, len(applied)) | |
b4bddc06 | 645 | else: |
9a316368 | 646 | raise CmdException, 'Incorrect options. Unknown patches to send' |
b4bddc06 | 647 | |
ea09f8ce GH |
648 | # early test for sender identity |
649 | __get_sender() | |
650 | ||
3c04f430 CM |
651 | out.start('Checking the validity of the patches') |
652 | for p in patches: | |
653 | if crt_series.empty_patch(p): | |
654 | raise CmdException, 'Cannot send empty patch "%s"' % p | |
655 | out.done() | |
656 | ||
b4bddc06 | 657 | total_nr = len(patches) |
9a316368 CM |
658 | if total_nr == 0: |
659 | raise CmdException, 'No patches to send' | |
b4bddc06 | 660 | |
ae5305d2 CM |
661 | if options.in_reply_to: |
662 | if options.no_thread or options.unrelated: | |
c2a8af1d | 663 | raise CmdException, \ |
ae5305d2 CM |
664 | '--in-reply-to option not allowed with --no-thread or --unrelated' |
665 | ref_id = options.in_reply_to | |
c2a8af1d CM |
666 | else: |
667 | ref_id = None | |
b4bddc06 | 668 | |
46e9c9f2 CM |
669 | # get username/password if sending by SMTP |
670 | __set_smtp_credentials(options) | |
b4bddc06 | 671 | |
e3e05587 | 672 | # send the cover message (if any) |
0ba13ee9 | 673 | if options.cover or options.edit_cover: |
c2a8af1d CM |
674 | if options.unrelated: |
675 | raise CmdException, 'cover sending not allowed with --unrelated' | |
676 | ||
e3e05587 CM |
677 | # find the template file |
678 | if options.cover: | |
16fee874 | 679 | tmpl = file(options.cover).read() |
e3e05587 | 680 | else: |
1f3bb017 CM |
681 | tmpl = templates.get_template('covermail.tmpl') |
682 | if not tmpl: | |
683 | raise CmdException, 'No cover message template file found' | |
b4bddc06 | 684 | |
9cb369d4 | 685 | msg_id = __send_message('cover', tmpl, options, patches) |
d650d6ed | 686 | |
b4bddc06 | 687 | # subsequent e-mails are seen as replies to the first one |
ae5305d2 | 688 | if not options.no_thread: |
d1ed3a12 | 689 | ref_id = msg_id |
b4bddc06 | 690 | |
b4bddc06 CM |
691 | # send the patches |
692 | if options.template: | |
1f3bb017 | 693 | tmpl = file(options.template).read() |
b4bddc06 | 694 | else: |
e5c32acf CM |
695 | if options.attach: |
696 | tmpl = templates.get_template('mailattch.tmpl') | |
697 | else: | |
698 | tmpl = templates.get_template('patchmail.tmpl') | |
1f3bb017 CM |
699 | if not tmpl: |
700 | raise CmdException, 'No e-mail template file found' | |
b4bddc06 | 701 | |
9cb369d4 AC |
702 | for (p, n) in zip(patches, range(1, total_nr + 1)): |
703 | msg_id = __send_message('patch', tmpl, options, p, n, total_nr, ref_id) | |
d650d6ed | 704 | |
b4bddc06 | 705 | # subsequent e-mails are seen as replies to the first one |
ae5305d2 | 706 | if not options.no_thread and not options.unrelated and not ref_id: |
b4bddc06 | 707 | ref_id = msg_id |