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 | ||
29f00589 | 18 | import sys, os, re, time, datetime, smtplib, email.Utils |
b4bddc06 | 19 | from optparse import OptionParser, make_option |
b4bddc06 CM |
20 | |
21 | from stgit.commands.common import * | |
22 | from stgit.utils import * | |
1f3bb017 | 23 | from stgit import stack, git, version, templates |
b4bddc06 CM |
24 | from stgit.config import config |
25 | ||
26 | ||
27 | help = 'send a patch or series of patches by e-mail' | |
6b1e0111 | 28 | usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>] |
26aab5b0 | 29 | |
6b1e0111 CM |
30 | Send a patch or a range of patches by e-mail using the 'smtpserver' |
31 | configuration option. The From address and the e-mail format are | |
32 | generated from the template file passed as argument to '--template' | |
33 | (defaulting to '.git/patchmail.tmpl' or | |
34 | '~/.stgit/templates/patchmail.tmpl' or or | |
94d18868 | 35 | '/usr/share/stgit/templates/patchmail.tmpl'). The To/Cc/Bcc addresses |
2bb96902 CM |
36 | can either be added to the template file or passed via the |
37 | corresponding command line options. | |
38 | ||
e3e05587 CM |
39 | A preamble e-mail can be sent using the '--cover' and/or '--edit' |
40 | options. The first allows the user to specify a file to be used as a | |
41 | template. The latter option will invoke the editor on the specified | |
94d18868 YD |
42 | file (defaulting to '.git/covermail.tmpl' or |
43 | '~/.stgit/templates/covermail.tmpl' or | |
44 | '/usr/share/stgit/templates/covermail.tmpl'). | |
e3e05587 CM |
45 | |
46 | All the subsequent e-mails appear as replies to the first e-mail sent | |
47 | (either the preamble or the first patch). E-mails can be seen as | |
48 | replies to a different e-mail by using the '--refid' option. | |
26aab5b0 CM |
49 | |
50 | SMTP authentication is also possible with '--smtp-user' and | |
51 | '--smtp-password' options, also available as configuration settings: | |
52 | 'smtpuser' and 'smtppassword'. | |
53 | ||
54 | The template e-mail headers and body must be separated by | |
55 | '%(endofheaders)s' variable, which is replaced by StGIT with | |
56 | additional headers and a blank line. The patch e-mail template accepts | |
57 | the following variables: | |
58 | ||
59 | %(patch)s - patch name | |
dae0f0be | 60 | %(maintainer)s - 'authname <authemail>' as read from the config file |
26aab5b0 CM |
61 | %(shortdescr)s - the first line of the patch description |
62 | %(longdescr)s - the rest of the patch description, after the first line | |
63 | %(endofheaders)s - delimiter between e-mail headers and body | |
64 | %(diff)s - unified diff of the patch | |
65 | %(diffstat)s - diff statistics | |
66 | %(date)s - current date/time | |
d0d139a3 | 67 | %(version)s - ' version' string passed on the command line (or empty) |
d323b5da | 68 | %(prefix)s - 'prefix ' string passed on the command line |
26aab5b0 CM |
69 | %(patchnr)s - patch number |
70 | %(totalnr)s - total number of patches to be sent | |
b8d258e5 | 71 | %(number)s - empty if only one patch is sent or ' patchnr/totalnr' |
26aab5b0 CM |
72 | %(authname)s - author's name |
73 | %(authemail)s - author's email | |
74 | %(authdate)s - patch creation date | |
75 | %(commname)s - committer's name | |
76 | %(commemail)s - committer's e-mail | |
77 | ||
dae0f0be | 78 | For the preamble e-mail template, only the %(maintainer)s, %(date)s, |
d0d139a3 CM |
79 | %(endofheaders)s, %(version)s, %(patchnr)s, %(totalnr)s and %(number)s |
80 | variables are supported.""" | |
b4bddc06 | 81 | |
9a316368 CM |
82 | options = [make_option('-a', '--all', |
83 | help = 'e-mail all the applied patches', | |
84 | action = 'store_true'), | |
2bb96902 | 85 | make_option('--to', |
e83b3149 PO |
86 | help = 'add TO to the To: list', |
87 | action = 'append'), | |
2bb96902 | 88 | make_option('--cc', |
e83b3149 PO |
89 | help = 'add CC to the Cc: list', |
90 | action = 'append'), | |
2bb96902 | 91 | make_option('--bcc', |
e83b3149 PO |
92 | help = 'add BCC to the Bcc: list', |
93 | action = 'append'), | |
f8d1cf65 CM |
94 | make_option('--auto', |
95 | help = 'automatically cc the patch signers', | |
96 | action = 'store_true'), | |
d1ed3a12 CM |
97 | make_option('--noreply', |
98 | help = 'do not send subsequent messages as replies', | |
99 | action = 'store_true'), | |
d0d139a3 CM |
100 | make_option('-v', '--version', metavar = 'VERSION', |
101 | help = 'add VERSION to the [PATCH ...] prefix'), | |
d323b5da RR |
102 | make_option('--prefix', metavar = 'PREFIX', |
103 | help = 'add PREFIX to the [... PATCH ...] prefix'), | |
9a316368 CM |
104 | make_option('-t', '--template', metavar = 'FILE', |
105 | help = 'use FILE as the message template'), | |
e3e05587 CM |
106 | make_option('-c', '--cover', metavar = 'FILE', |
107 | help = 'send FILE as the cover message'), | |
108 | make_option('-e', '--edit', | |
109 | help = 'edit the cover message before sending', | |
110 | action = 'store_true'), | |
b4bddc06 CM |
111 | make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS', |
112 | help = 'sleep for SECONDS between e-mails sending'), | |
113 | make_option('--refid', | |
d0d139a3 | 114 | help = 'use REFID as the reference id'), |
eb026d93 B |
115 | make_option('-u', '--smtp-user', metavar = 'USER', |
116 | help = 'username for SMTP authentication'), | |
117 | make_option('-p', '--smtp-password', metavar = 'PASSWORD', | |
2f7c8b0b CM |
118 | help = 'username for SMTP authentication'), |
119 | make_option('-b', '--branch', | |
29f00589 CM |
120 | help = 'use BRANCH instead of the default one'), |
121 | make_option('-m', '--mbox', | |
122 | help = 'generate an mbox file instead of sending', | |
123 | action = 'store_true')] | |
b4bddc06 CM |
124 | |
125 | ||
dae0f0be CM |
126 | def __get_maintainer(): |
127 | """Return the 'authname <authemail>' string as read from the | |
128 | configuration file | |
129 | """ | |
130 | if config.has_option('stgit', 'authname') \ | |
131 | and config.has_option('stgit', 'authemail'): | |
132 | return '%s <%s>' % (config.get('stgit', 'authname'), | |
133 | config.get('stgit', 'authemail')) | |
134 | else: | |
135 | return None | |
136 | ||
7cc615f3 | 137 | def __parse_addresses(addresses): |
b4bddc06 CM |
138 | """Return a two elements tuple: (from, [to]) |
139 | """ | |
7cc615f3 CL |
140 | def __addr_list(addrs): |
141 | m = re.search('[^@\s<,]+@[^>\s,]+', addrs); | |
d60cd083 CM |
142 | if (m == None): |
143 | return [] | |
7cc615f3 | 144 | return [ m.group() ] + __addr_list(addrs[m.end():]) |
b4bddc06 CM |
145 | |
146 | from_addr_list = [] | |
147 | to_addr_list = [] | |
7cc615f3 | 148 | for line in addresses.split('\n'): |
b4bddc06 CM |
149 | if re.match('from:\s+', line, re.I): |
150 | from_addr_list += __addr_list(line) | |
151 | elif re.match('(to|cc|bcc):\s+', line, re.I): | |
152 | to_addr_list += __addr_list(line) | |
153 | ||
24aadb3f | 154 | if len(from_addr_list) == 0: |
b4bddc06 CM |
155 | raise CmdException, 'No "From" address' |
156 | if len(to_addr_list) == 0: | |
157 | raise CmdException, 'No "To/Cc/Bcc" addresses' | |
158 | ||
159 | return (from_addr_list[0], to_addr_list) | |
160 | ||
eb026d93 B |
161 | def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep, |
162 | smtpuser, smtppassword): | |
b4bddc06 CM |
163 | """Send the message using the given SMTP server |
164 | """ | |
165 | try: | |
166 | s = smtplib.SMTP(smtpserver) | |
167 | except Exception, err: | |
168 | raise CmdException, str(err) | |
169 | ||
170 | s.set_debuglevel(0) | |
171 | try: | |
eb026d93 B |
172 | if smtpuser and smtppassword: |
173 | s.ehlo() | |
174 | s.login(smtpuser, smtppassword) | |
175 | ||
b4bddc06 CM |
176 | s.sendmail(from_addr, to_addr_list, msg) |
177 | # give recipients a chance of receiving patches in the correct order | |
178 | time.sleep(sleep) | |
179 | except Exception, err: | |
180 | raise CmdException, str(err) | |
181 | ||
182 | s.quit() | |
183 | ||
29f00589 CM |
184 | def __write_mbox(from_addr, msg): |
185 | """Write an mbox like file to the standard output | |
186 | """ | |
187 | r = re.compile('^From ', re.M) | |
188 | msg = r.sub('>\g<0>', msg) | |
189 | ||
190 | print 'From %s %s' % (from_addr, datetime.datetime.today().ctime()) | |
191 | print msg | |
192 | ||
193 | ||
f8d1cf65 CM |
194 | def __build_address_headers(tmpl, options, extra_cc = []): |
195 | """Build the address headers and check existing headers in the | |
196 | template. | |
197 | """ | |
198 | def csv(lst): | |
199 | return reduce(lambda x, y: x + ', ' + y, lst) | |
200 | ||
201 | def replace_header(header, addr, tmpl): | |
202 | r = re.compile('^' + header + ':\s+.+$', re.I | re.M) | |
203 | if r.search(tmpl): | |
204 | tmpl = r.sub('\g<0>, ' + addr, tmpl, 1) | |
205 | h = '' | |
206 | else: | |
207 | h = header + ': ' + addr | |
208 | ||
209 | return tmpl, h | |
210 | ||
211 | headers = '' | |
212 | to_addr = '' | |
213 | cc_addr = '' | |
214 | bcc_addr = '' | |
215 | ||
e83b3149 | 216 | if options.to: |
f8d1cf65 | 217 | to_addr = csv(options.to) |
e83b3149 | 218 | if options.cc: |
f8d1cf65 CM |
219 | cc_addr = csv(options.cc + extra_cc) |
220 | elif extra_cc: | |
221 | cc_addr = csv(extra_cc) | |
e83b3149 | 222 | if options.bcc: |
f8d1cf65 CM |
223 | bcc_addr = csv(options.bcc) |
224 | ||
225 | # replace existing headers | |
226 | if to_addr: | |
227 | tmpl, h = replace_header('To', to_addr, tmpl) | |
228 | if h: | |
229 | headers += h + '\n' | |
230 | if cc_addr: | |
231 | tmpl, h = replace_header('Cc', cc_addr, tmpl) | |
232 | if h: | |
233 | headers += h + '\n' | |
234 | if bcc_addr: | |
235 | tmpl, h = replace_header('Bcc', bcc_addr, tmpl) | |
236 | if h: | |
237 | headers += h + '\n' | |
238 | ||
239 | return tmpl, headers | |
240 | ||
241 | def __get_signers_list(msg): | |
242 | """Return the address list generated from signed-off-by and | |
243 | acked-by lines in the message. | |
244 | """ | |
245 | addr_list = [] | |
246 | ||
247 | r = re.compile('^(signed-off-by|acked-by):\s+(.+)$', re.I) | |
248 | for line in msg.split('\n'): | |
249 | m = r.match(line) | |
250 | if m: | |
251 | addr_list.append(m.expand('\g<2>')) | |
252 | ||
253 | return addr_list | |
e83b3149 | 254 | |
19a56fa1 CM |
255 | def __build_extra_headers(): |
256 | """Build extra headers like content-type etc. | |
257 | """ | |
258 | headers = 'Content-Type: text/plain; charset=utf-8; format=fixed\n' | |
259 | headers += 'Content-Transfer-Encoding: 8bit\n' | |
260 | headers += 'User-Agent: StGIT/%s\n' % version.version | |
261 | ||
262 | return headers | |
263 | ||
e3e05587 CM |
264 | def __build_cover(tmpl, total_nr, msg_id, options): |
265 | """Build the cover message (series description) to be sent via SMTP | |
b4bddc06 | 266 | """ |
dae0f0be CM |
267 | maintainer = __get_maintainer() |
268 | if not maintainer: | |
269 | maintainer = '' | |
270 | ||
f8d1cf65 | 271 | tmpl, headers_end = __build_address_headers(tmpl, options) |
2bb96902 | 272 | headers_end += 'Message-Id: %s\n' % msg_id |