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 | ||
61eed152 CM |
18 | import sys, os, re, time, datetime, smtplib |
19 | import email, email.Utils, email.Header | |
b4bddc06 | 20 | from optparse import OptionParser, make_option |
b4bddc06 CM |
21 | |
22 | from stgit.commands.common import * | |
23 | from stgit.utils import * | |
1f3bb017 | 24 | from stgit import stack, git, version, templates |
b4bddc06 CM |
25 | from stgit.config import config |
26 | ||
27 | ||
28 | help = 'send a patch or series of patches by e-mail' | |
6b1e0111 | 29 | usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>] |
26aab5b0 | 30 | |
6b1e0111 CM |
31 | Send a patch or a range of patches by e-mail using the 'smtpserver' |
32 | configuration option. The From address and the e-mail format are | |
33 | generated from the template file passed as argument to '--template' | |
34 | (defaulting to '.git/patchmail.tmpl' or | |
35 | '~/.stgit/templates/patchmail.tmpl' or or | |
94d18868 | 36 | '/usr/share/stgit/templates/patchmail.tmpl'). The To/Cc/Bcc addresses |
2bb96902 CM |
37 | can either be added to the template file or passed via the |
38 | corresponding command line options. | |
39 | ||
0ba13ee9 KH |
40 | A preamble e-mail can be sent using the '--cover' and/or |
41 | '--edit-cover' options. The first allows the user to specify a file to | |
42 | be used as a template. The latter option will invoke the editor on the | |
43 | specified file (defaulting to '.git/covermail.tmpl' or | |
94d18868 YD |
44 | '~/.stgit/templates/covermail.tmpl' or |
45 | '/usr/share/stgit/templates/covermail.tmpl'). | |
e3e05587 CM |
46 | |
47 | All the subsequent e-mails appear as replies to the first e-mail sent | |
48 | (either the preamble or the first patch). E-mails can be seen as | |
49 | replies to a different e-mail by using the '--refid' option. | |
26aab5b0 CM |
50 | |
51 | SMTP authentication is also possible with '--smtp-user' and | |
52 | '--smtp-password' options, also available as configuration settings: | |
53 | 'smtpuser' and 'smtppassword'. | |
54 | ||
55 | The template e-mail headers and body must be separated by | |
56 | '%(endofheaders)s' variable, which is replaced by StGIT with | |
57 | additional headers and a blank line. The patch e-mail template accepts | |
58 | the following variables: | |
59 | ||
60 | %(patch)s - patch name | |
dae0f0be | 61 | %(maintainer)s - 'authname <authemail>' as read from the config file |
26aab5b0 CM |
62 | %(shortdescr)s - the first line of the patch description |
63 | %(longdescr)s - the rest of the patch description, after the first line | |
64 | %(endofheaders)s - delimiter between e-mail headers and body | |
65 | %(diff)s - unified diff of the patch | |
66 | %(diffstat)s - diff statistics | |
67 | %(date)s - current date/time | |
d0d139a3 | 68 | %(version)s - ' version' string passed on the command line (or empty) |
d323b5da | 69 | %(prefix)s - 'prefix ' string passed on the command line |
26aab5b0 CM |
70 | %(patchnr)s - patch number |
71 | %(totalnr)s - total number of patches to be sent | |
b8d258e5 | 72 | %(number)s - empty if only one patch is sent or ' patchnr/totalnr' |
26aab5b0 CM |
73 | %(authname)s - author's name |
74 | %(authemail)s - author's email | |
75 | %(authdate)s - patch creation date | |
76 | %(commname)s - committer's name | |
77 | %(commemail)s - committer's e-mail | |
78 | ||
dae0f0be | 79 | For the preamble e-mail template, only the %(maintainer)s, %(date)s, |
d0d139a3 CM |
80 | %(endofheaders)s, %(version)s, %(patchnr)s, %(totalnr)s and %(number)s |
81 | variables are supported.""" | |
b4bddc06 | 82 | |
9a316368 CM |
83 | options = [make_option('-a', '--all', |
84 | help = 'e-mail all the applied patches', | |
85 | action = 'store_true'), | |
2bb96902 | 86 | make_option('--to', |
e83b3149 PO |
87 | help = 'add TO to the To: list', |
88 | action = 'append'), | |
2bb96902 | 89 | make_option('--cc', |
e83b3149 PO |
90 | help = 'add CC to the Cc: list', |
91 | action = 'append'), | |
2bb96902 | 92 | make_option('--bcc', |
e83b3149 PO |
93 | help = 'add BCC to the Bcc: list', |
94 | action = 'append'), | |
f8d1cf65 CM |
95 | make_option('--auto', |
96 | help = 'automatically cc the patch signers', | |
97 | action = 'store_true'), | |
d1ed3a12 CM |
98 | make_option('--noreply', |
99 | help = 'do not send subsequent messages as replies', | |
100 | action = 'store_true'), | |
d0d139a3 CM |
101 | make_option('-v', '--version', metavar = 'VERSION', |
102 | help = 'add VERSION to the [PATCH ...] prefix'), | |
d323b5da RR |
103 | make_option('--prefix', metavar = 'PREFIX', |
104 | help = 'add PREFIX to the [... PATCH ...] prefix'), | |
9a316368 CM |
105 | make_option('-t', '--template', metavar = 'FILE', |
106 | help = 'use FILE as the message template'), | |
e3e05587 CM |
107 | make_option('-c', '--cover', metavar = 'FILE', |
108 | help = 'send FILE as the cover message'), | |
0ba13ee9 | 109 | make_option('-e', '--edit-cover', |
e3e05587 CM |
110 | help = 'edit the cover message before sending', |
111 | action = 'store_true'), | |
0ba13ee9 KH |
112 | make_option('-E', '--edit-patches', |
113 | help = 'edit each patch before sending', | |
114 | action = 'store_true'), | |
b4bddc06 CM |
115 | make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS', |
116 | help = 'sleep for SECONDS between e-mails sending'), | |
117 | make_option('--refid', | |
d0d139a3 | 118 | help = 'use REFID as the reference id'), |
eb026d93 B |
119 | make_option('-u', '--smtp-user', metavar = 'USER', |
120 | help = 'username for SMTP authentication'), | |
121 | make_option('-p', '--smtp-password', metavar = 'PASSWORD', | |
2f7c8b0b CM |
122 | help = 'username for SMTP authentication'), |
123 | make_option('-b', '--branch', | |
29f00589 CM |
124 | help = 'use BRANCH instead of the default one'), |
125 | make_option('-m', '--mbox', | |
126 | help = 'generate an mbox file instead of sending', | |
127 | action = 'store_true')] | |
b4bddc06 CM |
128 | |
129 | ||
dae0f0be CM |
130 | def __get_maintainer(): |
131 | """Return the 'authname <authemail>' string as read from the | |
132 | configuration file | |
133 | """ | |
134 | if config.has_option('stgit', 'authname') \ | |
135 | and config.has_option('stgit', 'authemail'): | |
136 | return '%s <%s>' % (config.get('stgit', 'authname'), | |
137 | config.get('stgit', 'authemail')) | |
138 | else: | |
139 | return None | |
140 | ||
7cc615f3 | 141 | def __parse_addresses(addresses): |
b4bddc06 CM |
142 | """Return a two elements tuple: (from, [to]) |
143 | """ | |
7cc615f3 CL |
144 | def __addr_list(addrs): |
145 | m = re.search('[^@\s<,]+@[^>\s,]+', addrs); | |
d60cd083 CM |
146 | if (m == None): |
147 | return [] | |
7cc615f3 | 148 | return [ m.group() ] + __addr_list(addrs[m.end():]) |
b4bddc06 CM |
149 | |
150 | from_addr_list = [] | |
151 | to_addr_list = [] | |
7cc615f3 | 152 | for line in addresses.split('\n'): |
b4bddc06 CM |
153 | if re.match('from:\s+', line, re.I): |
154 | from_addr_list += __addr_list(line) | |
155 | elif re.match('(to|cc|bcc):\s+', line, re.I): | |
156 | to_addr_list += __addr_list(line) | |
157 | ||
24aadb3f | 158 | if len(from_addr_list) == 0: |
b4bddc06 CM |
159 | raise CmdException, 'No "From" address' |
160 | if len(to_addr_list) == 0: | |
161 | raise CmdException, 'No "To/Cc/Bcc" addresses' | |
162 | ||
163 | return (from_addr_list[0], to_addr_list) | |
164 | ||
eb026d93 B |
165 | def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep, |
166 | smtpuser, smtppassword): | |
b4bddc06 CM |
167 | """Send the message using the given SMTP server |
168 | """ | |
169 | try: | |
170 | s = smtplib.SMTP(smtpserver) | |
171 | except Exception, err: | |
172 | raise CmdException, str(err) | |
173 | ||
174 | s.set_debuglevel(0) | |
175 | try: | |
eb026d93 B |
176 | if smtpuser and smtppassword: |
177 | s.ehlo() | |
178 | s.login(smtpuser, smtppassword) | |
179 | ||
b4bddc06 CM |
180 | s.sendmail(from_addr, to_addr_list, msg) |
181 | # give recipients a chance of receiving patches in the correct order | |
182 | time.sleep(sleep) | |
183 | except Exception, err: | |
184 | raise CmdException, str(err) | |
185 | ||
186 | s.quit() | |
187 | ||
61eed152 | 188 | def __build_address_headers(msg, options, extra_cc = []): |
f8d1cf65 CM |
189 | """Build the address headers and check existing headers in the |
190 | template. | |
191 | """ | |
61eed152 CM |
192 | def __replace_header(header, addr): |
193 | if addr: | |
194 | crt_addr = msg[header] | |
195 | del msg[header] | |
f8d1cf65 | 196 | |
61eed152 CM |
197 | if crt_addr: |
198 | msg[header] = ', '.join([crt_addr, addr]) | |
199 | else: | |
200 | msg[header] = addr | |
f8d1cf65 | 201 | |
f8d1cf65 CM |
202 | to_addr = '' |
203 | cc_addr = '' | |
204 | bcc_addr = '' | |
205 | ||
d884c4d8 CM |
206 | if config.has_option('stgit', 'autobcc'): |
207 | autobcc = config.get('stgit', 'autobcc') | |
208 | else: | |
209 | autobcc = '' | |
210 | ||
e83b3149 | 211 | if options.to: |
61eed152 | 212 | to_addr = ', '.join(options.to) |
e83b3149 | 213 | if options.cc: |
61eed152 | 214 | cc_addr = ', '.join(options.cc + extra_cc) |
f8d1cf65 | 215 | elif extra_cc: |
61eed152 | 216 | cc_addr = ', '.join(extra_cc) |
e83b3149 | 217 | if options.bcc: |
61eed152 | 218 | bcc_addr = ', '.join(options.bcc + [autobcc]) |
d884c4d8 CM |
219 | elif autobcc: |
220 | bcc_addr = autobcc | |
f8d1cf65 | 221 | |
61eed152 CM |
222 | __replace_header('To', to_addr) |
223 | __replace_header('Cc', cc_addr) | |
224 | __replace_header('Bcc', bcc_addr) | |
f8d1cf65 CM |
225 | |
226 | def __get_signers_list(msg): | |
227 | """Return the address list generated from signed-off-by and | |
228 | acked-by lines in the message. | |
229 | """ | |
230 | addr_list = [] | |
231 | ||
232 | r = re.compile('^(signed-off-by|acked-by):\s+(.+)$', re.I) | |
233 | for line in msg.split('\n'): | |
234 | m = r.match(line) | |
235 | if m: | |
236 | addr_list.append(m.expand('\g<2>')) | |
237 | ||
238 | return addr_list | |
e83b3149 | 239 | |
61eed152 CM |
240 | def __build_extra_headers(msg, msg_id, ref_id = None): |
241 | """Build extra email headers and encoding | |
19a56fa1 | 242 | """ |
61eed152 CM |
243 | del msg['Date'] |
244 | msg['Date'] = email.Utils.formatdate(localtime = True) | |
245 | msg['Message-ID'] = msg_id | |
246 | if ref_id: | |
247 | msg['In-Reply-To'] = ref_id | |
248 | msg['References'] = ref_id | |
249 | msg['User-Agent'] = 'StGIT/%s' % version.version | |
250 | ||
251 | def __encode_message(msg): | |
252 | # 7 or 8 bit encoding | |
253 | charset = email.Charset.Charset('utf-8') | |
254 | charset.body_encoding = None | |
255 | ||
256 | # encode headers | |
257 | for header, value in msg.items(): | |
258 | words = [] | |
259 | for word in value.split(' '): | |
260 | try: | |
261 | uword = unicode(word, 'utf-8') | |
262 | except UnicodeDecodeError: | |
263 | # maybe we should try a different encoding or report | |
264 | # the error. At the moment, we just ignore it | |
265 | pass | |
266 | words.append(email.Header.Header(uword).encode()) | |
267 | new_val = ' '.join(words) | |
268 | msg.replace_header(header, new_val) | |
269 | ||
270 | # encode the body and set the MIME and encoding headers | |
271 | msg.set_charset(charset) | |
19a56fa1 | 272 | |
0ba13ee9 KH |
273 | def edit_message(msg): |
274 | fname = '.stgitmail.txt' | |
275 | ||
276 | # create the initial file | |
277 | f = file(fname, 'w') | |
278 | f.write(msg) | |
279 | f.close() | |
280 | ||
281 | # the editor | |
282 | if config.has_option('stgit', 'editor'): | |
283 | editor = config.get('stgit', 'editor') | |
284 | elif 'EDITOR' in os.environ: | |
285 | editor = os.environ['EDITOR'] | |
286 | else: | |
287 | editor = 'vi' | |
288 | editor += ' %s' % fname | |
289 | ||
290 | print 'Invoking the editor: "%s"...' % editor, | |
291 | sys.stdout.flush() | |
292 | print 'done (exit code: %d)' % os.system(editor) | |
293 | ||
294 | # read the message back | |
295 | f = file(fname) | |
296 | msg = f.read() | |
297 | f.close() | |
298 | ||
299 | return msg | |
300 | ||
e3e05587 CM |
301 | def __build_cover(tmpl, total_nr, msg_id, options): |
302 | """Build the cover message (series description) to be sent via SMTP | |
b4bddc06 | 303 | """ |
dae0f0be CM |
304 | maintainer = __get_maintainer() |
305 | if not maintainer: | |
306 | maintainer = '' | |
307 | ||
d0d139a3 CM |
308 | if options.version: |
309 | version_str = ' %s' % options.version | |
ed5de0cc CM |
310 | else: |
311 | version_str = '' | |
d0d139a3 | 312 | |
d323b5da RR |
313 | if options.prefix: |
314 | prefix_str = options.prefix + ' ' | |
315 | else: | |
316 | prefix_str = '' | |
317 | ||
b4bddc06 | 318 | total_nr_str = str(total_nr) |
b8d258e5 CM |
319 | patch_nr_str = '0'.zfill(len(total_nr_str)) |
320 | if total_nr > 1: | |
321 | number_str = ' %s/%s' % (patch_nr_str, total_nr_str) | |
322 | else: | |
323 | number_str = '' | |
b4bddc06 | 324 | |
dae0f0be | 325 | tmpl_dict = {'maintainer': maintainer, |
61eed152 CM |
326 | # for backward template compatibility |
327 | 'endofheaders': '', | |
328 | # for backward template compatibility | |
329 | 'date': '', | |
d0d139a3 | 330 | 'version': version_str, |
d323b5da | 331 | 'prefix': prefix_str, |
b8d258e5 CM |
332 | 'patchnr': patch_nr_str, |
333 | 'totalnr': total_nr_str, | |
334 | 'number': number_str} | |
b4bddc06 CM |
335 | |
336 | try: | |
61eed152 | 337 | msg_string = tmpl % tmpl_dict |
b4bddc06 CM |
338 | except KeyError, err: |
339 | raise CmdException, 'Unknown patch template variable: %s' \ | |
340 | % err | |
341 | except TypeError: | |
342 | raise CmdException, 'Only "%(name)s" variables are ' \ | |
343 | 'supported in the patch template' | |
344 | ||
61eed152 CM |
345 | # The Python email message |
346 | try: | |
347 | msg = email.message_from_string(msg_string) | |
348 | except Exception, ex: | |
349 | raise CmdException, 'template parsing error: %s' % str(ex) | |
350 | ||
351 | __build_address_headers(msg, options) | |
352 | __build_extra_headers(msg, msg_id, options.refid) | |
353 | __encode_message(msg) | |
354 | ||
355 | msg_string = msg.as_string(options.mbox) | |
356 | ||
0ba13ee9 | 357 | if options.edit_cover: |
61eed152 | 358 | msg_string = edit_message(msg_string) |
e3e05587 | 359 | |
61eed152 | 360 | return msg_string.strip('\n') |
b4bddc06 | 361 | |
2bb96902 | 362 | def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options): |
b4bddc06 CM |
363 | """Build the message to be sent via SMTP |
364 | """ | |
365 | p = crt_series.get_patch(patch) | |
366 | ||
367 | descr = p.get_description().strip() | |
368 | descr_lines = descr.split('\n') | |
369 | ||
370 | short_descr = descr_lines[0].rstrip() | |
61eed152 | 371 | long_descr = '\n'.join(descr_lines[1:]).lstrip() |
b4bddc06 | 372 | |
dae0f0be CM |
373 | maintainer = __get_maintainer() |
374 | if not maintainer: | |
375 | maintainer = '%s <%s>' % (p.get_commname(), p.get_commemail()) | |
376 | ||
d0d139a3 CM |
377 | if options.version: |
378 | version_str = ' %s' % options.version | |
ed5de0cc CM |
379 | else: |
380 | version_str = '' | |
d0d139a3 | 381 | |
d323b5da RR |
382 | if options.prefix: |
383 | prefix_str = options.prefix + ' ' | |
384 | else: | |
385 | prefix_str = '' | |
386 | ||
b4bddc06 CM |
387 | total_nr_str = str(total_nr) |
388 | patch_nr_str = str(patch_nr).zfill(len(total_nr_str)) | |
b8d258e5 CM |
389 | if total_nr > 1: |
390 | number_str = ' %s/%s' % (patch_nr_str, total_nr_str) | |
391 | else: | |
392 | number_str = '' | |
b4bddc06 CM |
393 | |
394 | tmpl_dict = {'patch': patch, | |
dae0f0be | 395 | 'maintainer': maintainer, |
b4bddc06 CM |
396 | 'shortdescr': short_descr, |
397 | 'longdescr': long_descr, | |
61eed152 CM |
398 | # for backward template compatibility |
399 | 'endofheaders': '', | |