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 | ||
18 | import sys, os, re, time, smtplib, email.Utils | |
19 | from optparse import OptionParser, make_option | |
20 | from time import gmtime, strftime | |
21 | ||
22 | from stgit.commands.common import * | |
23 | from stgit.utils import * | |
24 | from stgit import stack, git | |
25 | from stgit.config import config | |
26 | ||
27 | ||
28 | help = 'send a patch or series of patches by e-mail' | |
26aab5b0 CM |
29 | usage = """%prog [options] [<patch>] |
30 | ||
31 | Send a patch or a range of patches (defaulting to the applied patches) | |
2bb96902 CM |
32 | by e-mail using the 'smtpserver' configuration option. The From |
33 | address and the e-mail format are generated from the template file | |
34 | passed as argument to '--template' (defaulting to .git/patchmail.tmpl | |
35 | or /usr/share/stgit/templates/patchmail.tmpl). The To/Cc/Bcc addresses | |
36 | can either be added to the template file or passed via the | |
37 | corresponding command line options. | |
38 | ||
39 | A preamble e-mail can be sent using the '--first' option. All the | |
40 | subsequent e-mails appear as replies to the first e-mail sent (either | |
41 | the preamble or the first patch). E-mails can be seen as replies to a | |
42 | different e-mail by using the '--refid' option. | |
26aab5b0 CM |
43 | |
44 | SMTP authentication is also possible with '--smtp-user' and | |
45 | '--smtp-password' options, also available as configuration settings: | |
46 | 'smtpuser' and 'smtppassword'. | |
47 | ||
48 | The template e-mail headers and body must be separated by | |
49 | '%(endofheaders)s' variable, which is replaced by StGIT with | |
50 | additional headers and a blank line. The patch e-mail template accepts | |
51 | the following variables: | |
52 | ||
53 | %(patch)s - patch name | |
54 | %(shortdescr)s - the first line of the patch description | |
55 | %(longdescr)s - the rest of the patch description, after the first line | |
56 | %(endofheaders)s - delimiter between e-mail headers and body | |
57 | %(diff)s - unified diff of the patch | |
58 | %(diffstat)s - diff statistics | |
59 | %(date)s - current date/time | |
60 | %(patchnr)s - patch number | |
61 | %(totalnr)s - total number of patches to be sent | |
62 | %(authname)s - author's name | |
63 | %(authemail)s - author's email | |
64 | %(authdate)s - patch creation date | |
65 | %(commname)s - committer's name | |
66 | %(commemail)s - committer's e-mail | |
67 | ||
68 | For the preamble e-mail template, only the %(date)s, %(endofheaders)s | |
69 | and %(totalnr)s variables are supported.""" | |
b4bddc06 | 70 | |
9a316368 CM |
71 | options = [make_option('-a', '--all', |
72 | help = 'e-mail all the applied patches', | |
73 | action = 'store_true'), | |
b4bddc06 CM |
74 | make_option('-r', '--range', |
75 | metavar = '[PATCH1][:[PATCH2]]', | |
76 | help = 'e-mail patches between PATCH1 and PATCH2'), | |
2bb96902 CM |
77 | make_option('--to', |
78 | help = 'Add TO to the To: list'), | |
79 | make_option('--cc', | |
80 | help = 'Add CC to the Cc: list'), | |
81 | make_option('--bcc', | |
82 | help = 'Add BCC to the Bcc: list'), | |
9a316368 CM |
83 | make_option('-t', '--template', metavar = 'FILE', |
84 | help = 'use FILE as the message template'), | |
b4bddc06 CM |
85 | make_option('-f', '--first', metavar = 'FILE', |
86 | help = 'send FILE as the first message'), | |
87 | make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS', | |
88 | help = 'sleep for SECONDS between e-mails sending'), | |
89 | make_option('--refid', | |
eb026d93 B |
90 | help = 'Use REFID as the reference id'), |
91 | make_option('-u', '--smtp-user', metavar = 'USER', | |
92 | help = 'username for SMTP authentication'), | |
93 | make_option('-p', '--smtp-password', metavar = 'PASSWORD', | |
94 | help = 'username for SMTP authentication')] | |
b4bddc06 CM |
95 | |
96 | ||
97 | def __parse_addresses(string): | |
98 | """Return a two elements tuple: (from, [to]) | |
99 | """ | |
100 | def __addr_list(string): | |
101 | return re.split('.*?([\w\.]+@[\w\.]+)', string)[1:-1:2] | |
102 | ||
103 | from_addr_list = [] | |
104 | to_addr_list = [] | |
105 | for line in string.split('\n'): | |
106 | if re.match('from:\s+', line, re.I): | |
107 | from_addr_list += __addr_list(line) | |
108 | elif re.match('(to|cc|bcc):\s+', line, re.I): | |
109 | to_addr_list += __addr_list(line) | |
110 | ||
111 | if len(from_addr_list) != 1: | |
112 | raise CmdException, 'No "From" address' | |
113 | if len(to_addr_list) == 0: | |
114 | raise CmdException, 'No "To/Cc/Bcc" addresses' | |
115 | ||
116 | return (from_addr_list[0], to_addr_list) | |
117 | ||
eb026d93 B |
118 | def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep, |
119 | smtpuser, smtppassword): | |
b4bddc06 CM |
120 | """Send the message using the given SMTP server |
121 | """ | |
122 | try: | |
123 | s = smtplib.SMTP(smtpserver) | |
124 | except Exception, err: | |
125 | raise CmdException, str(err) | |
126 | ||
127 | s.set_debuglevel(0) | |
128 | try: | |
eb026d93 B |
129 | if smtpuser and smtppassword: |
130 | s.ehlo() | |
131 | s.login(smtpuser, smtppassword) | |
132 | ||
b4bddc06 CM |
133 | s.sendmail(from_addr, to_addr_list, msg) |
134 | # give recipients a chance of receiving patches in the correct order | |
135 | time.sleep(sleep) | |
136 | except Exception, err: | |
137 | raise CmdException, str(err) | |
138 | ||
139 | s.quit() | |
140 | ||
2bb96902 | 141 | def __build_first(tmpl, total_nr, msg_id, options): |
b4bddc06 CM |
142 | """Build the first message (series description) to be sent via SMTP |
143 | """ | |
2bb96902 CM |
144 | headers_end = '' |
145 | if options.to: | |
146 | headers_end += 'To: %s\n' % options.to | |
147 | if options.cc: | |
148 | headers_end += 'Cc: %s\n' % options.cc | |
149 | if options.bcc: | |
150 | headers_end += 'Bcc: %s\n' % options.bcc | |
151 | headers_end += 'Message-Id: %s\n' % msg_id | |
152 | ||
b4bddc06 CM |
153 | total_nr_str = str(total_nr) |
154 | ||
155 | tmpl_dict = {'endofheaders': headers_end, | |
156 | 'date': email.Utils.formatdate(localtime = True), | |
157 | 'totalnr': total_nr_str} | |
158 | ||
159 | try: | |
160 | msg = tmpl % tmpl_dict | |
161 | except KeyError, err: | |
162 | raise CmdException, 'Unknown patch template variable: %s' \ | |
163 | % err | |
164 | except TypeError: | |
165 | raise CmdException, 'Only "%(name)s" variables are ' \ | |
166 | 'supported in the patch template' | |
167 | ||
168 | return msg | |
169 | ||
170 | ||
2bb96902 | 171 | def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options): |
b4bddc06 CM |
172 | """Build the message to be sent via SMTP |
173 | """ | |
174 | p = crt_series.get_patch(patch) | |
175 | ||
176 | descr = p.get_description().strip() | |
177 | descr_lines = descr.split('\n') | |
178 | ||
179 | short_descr = descr_lines[0].rstrip() | |
180 | long_descr = reduce(lambda x, y: x + '\n' + y, | |
181 | descr_lines[1:], '').lstrip() | |
182 | ||
2bb96902 CM |
183 | headers_end = '' |
184 | if options.to: | |
185 | headers_end += 'To: %s\n' % options.to | |
186 | if options.cc: | |
187 | headers_end += 'Cc: %s\n' % options.cc | |
188 | if options.bcc: | |
189 | headers_end += 'Bcc: %s\n' % options.bcc | |
190 | headers_end += 'Message-Id: %s\n' % msg_id | |
b4bddc06 | 191 | if ref_id: |
2bb96902 CM |
192 | headers_end += "In-Reply-To: %s\n" % ref_id |
193 | headers_end += "References: %s\n" % ref_id | |
b4bddc06 CM |
194 | |
195 | total_nr_str = str(total_nr) | |
196 | patch_nr_str = str(patch_nr).zfill(len(total_nr_str)) | |
197 | ||
198 | tmpl_dict = {'patch': patch, | |
199 | 'shortdescr': short_descr, | |
200 | 'longdescr': long_descr, | |
201 | 'endofheaders': headers_end, | |
202 | 'diff': git.diff(rev1 = git_id('%s/bottom' % patch), | |
203 | rev2 = git_id('%s/top' % patch)), | |
204 | 'diffstat': git.diffstat(rev1 = git_id('%s/bottom'%patch), | |
205 | rev2 = git_id('%s/top' % patch)), | |
206 | 'date': email.Utils.formatdate(localtime = True), | |
207 | 'patchnr': patch_nr_str, | |
208 | 'totalnr': total_nr_str, | |
209 | 'authname': p.get_authname(), | |
210 | 'authemail': p.get_authemail(), | |
211 | 'authdate': p.get_authdate(), | |
212 | 'commname': p.get_commname(), | |
213 | 'commemail': p.get_commemail()} | |
214 | for key in tmpl_dict: | |
215 | if not tmpl_dict[key]: | |
216 | tmpl_dict[key] = '' | |
217 | ||
218 | try: | |
219 | msg = tmpl % tmpl_dict | |
220 | except KeyError, err: | |
221 | raise CmdException, 'Unknown patch template variable: %s' \ | |
222 | % err | |
223 | except TypeError: | |
224 | raise CmdException, 'Only "%(name)s" variables are ' \ | |
225 | 'supported in the patch template' | |
226 | ||
227 | return msg | |
228 | ||
229 | ||
230 | def func(parser, options, args): | |
231 | """Send the patches by e-mail using the patchmail.tmpl file as | |
232 | a template | |
233 | """ | |
9a316368 | 234 | if len(args) > 1: |
b4bddc06 CM |
235 | parser.error('incorrect number of arguments') |
236 | ||
237 | if not config.has_option('stgit', 'smtpserver'): | |
238 | raise CmdException, 'smtpserver not defined' | |
239 | smtpserver = config.get('stgit', 'smtpserver') | |
240 | ||
eb026d93 B |
241 | smtpuser = None |
242 | smtppassword = None | |
243 | if config.has_option('stgit', 'smtpuser'): | |
244 | smtpuser = config.get('stgit', 'smtpuser') | |
245 | if config.has_option('stgit', 'smtppassword'): | |
246 | smtppassword = config.get('stgit', 'smtppassword') | |
247 | ||
b4bddc06 CM |
248 | applied = crt_series.get_applied() |
249 | ||
9a316368 CM |
250 | if len(args) == 1: |
251 | if args[0] in applied: | |
252 | patches = [args[0]] | |
253 | else: | |
254 | raise CmdException, 'Patch "%s" not applied' % args[0] | |
255 | elif options.all: | |
256 | patches = applied | |
257 | elif options.range: | |
b4bddc06 CM |
258 | boundaries = options.range.split(':') |
259 | if len(boundaries) == 1: | |
260 | start = boundaries[0] | |
261 | stop = boundaries[0] | |
262 | elif len(boundaries) == 2: | |
263 | if boundaries[0] == '': | |
264 | start = applied[0] | |
265 | else: | |
266 | start = boundaries[0] | |
267 | if boundaries[1] == '': | |
268 | stop = applied[-1] | |
269 | else: | |
270 | stop = boundaries[1] | |
271 | else: | |
272 | raise CmdException, 'incorrect parameters to "--range"' | |
273 | ||
274 | if start in applied: | |
275 | start_idx = applied.index(start) | |
276 | else: | |
277 | raise CmdException, 'Patch "%s" not applied' % start | |
278 | if stop in applied: | |
279 | stop_idx = applied.index(stop) + 1 | |
280 | else: | |
281 | raise CmdException, 'Patch "%s" not applied' % stop | |
282 | ||
283 | if start_idx >= stop_idx: | |
284 | raise CmdException, 'Incorrect patch range order' | |
9a316368 CM |
285 | |
286 | patches = applied[start_idx:stop_idx] | |
b4bddc06 | 287 | else: |
9a316368 | 288 | raise CmdException, 'Incorrect options. Unknown patches to send' |
b4bddc06 | 289 | |
eb026d93 B |
290 | if options.smtp_password: |
291 | smtppassword = options.smtp_password | |
292 | ||
293 | if options.smtp_user: | |
294 | smtpuser = options.smtp_user | |
295 | ||
296 | if (smtppassword and not smtpuser): | |
297 | raise CmdException, 'SMTP password supplied, username needed' | |
298 | if (smtpuser and not smtppassword): | |
299 | raise CmdException, 'SMTP username supplied, password needed' | |
300 | ||
b4bddc06 | 301 | total_nr = len(patches) |
9a316368 CM |
302 | if total_nr == 0: |
303 | raise CmdException, 'No patches to send' | |
b4bddc06 CM |
304 | |
305 | ref_id = options.refid | |
306 | ||
307 | if options.sleep != None: | |
308 | sleep = options.sleep | |
309 | else: | |
310 | sleep = 2 | |
311 | ||
312 | # send the first message (if any) | |
313 | if options.first: | |
314 | tmpl = file(options.first).read() | |
b4bddc06 CM |
315 | |
316 | msg_id = email.Utils.make_msgid('stgit') | |
2bb96902 CM |
317 | msg = __build_first(tmpl, total_nr, msg_id, options) |
318 | from_addr, to_addr_list = __parse_addresses(msg) | |
b4bddc06 CM |
319 | |
320 | # subsequent e-mails are seen as replies to the first one | |
321 | ref_id = msg_id | |
322 | ||
323 | print 'Sending file "%s"...' % options.first, | |
324 | sys.stdout.flush() | |
325 | ||
eb026d93 B |
326 | __send_message(smtpserver, from_addr, to_addr_list, msg, sleep, |
327 | smtpuser, smtppassword) | |
b4bddc06 CM |
328 | |
329 | print 'done' | |
330 | ||
331 | # send the patches | |
332 | if options.template: | |
2bb96902 | 333 | tfile_list = [options.template] |
b4bddc06 | 334 | else: |
2bb96902 CM |
335 | tfile_list = [] |
336 | ||
337 | tfile_list += [os.path.join(git.base_dir, 'patchmail.tmpl'), | |
338 | os.path.join(sys.prefix, | |
339 | 'share/stgit/templates/patchmail.tmpl')] | |
340 | tmpl = None | |
341 | for tfile in tfile_list: | |
342 | if os.path.isfile(tfile): | |
343 | tmpl = file(tfile).read() | |
344 | break | |
345 | if not tmpl: | |
346 | raise CmdException, 'No e-mail template file: %s or %s' \ | |
347 | % (tfile_list[-1], tfile_list[-2]) | |
b4bddc06 CM |
348 | |
349 | for (p, patch_nr) in zip(patches, range(1, len(patches) + 1)): | |
350 | msg_id = email.Utils.make_msgid('stgit') | |
2bb96902 CM |
351 | msg = __build_message(tmpl, p, patch_nr, total_nr, msg_id, ref_id, |
352 | options) | |
353 | from_addr, to_addr_list = __parse_addresses(msg) | |
354 | ||
b4bddc06 CM |
355 | # subsequent e-mails are seen as replies to the first one |
356 | if not ref_id: | |
357 | ref_id = msg_id | |
358 | ||
359 | print 'Sending patch "%s"...' % p, | |
360 | sys.stdout.flush() | |
361 | ||
eb026d93 B |
362 | __send_message(smtpserver, from_addr, to_addr_list, msg, sleep, |
363 | smtpuser, smtppassword) | |
b4bddc06 CM |
364 | |
365 | print 'done' |