##// END OF EJS Templates
patchbomb: Validate email config before we start prompting for info.
Bryan O'Sullivan -
r4489:a11e13d5 default
parent child Browse files
Show More
@@ -1,418 +1,421 b''
1 # Command for sending a collection of Mercurial changesets as a series
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
2 # of patch emails.
3 #
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
5 # which describes the series as a whole.
6 #
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
9 # The message contains two or three body parts:
10 #
10 #
11 # The remainder of the changeset description.
11 # The remainder of the changeset description.
12 #
12 #
13 # [Optional] If the diffstat program is installed, the result of
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
14 # running diffstat on the patch.
15 #
15 #
16 # The patch itself, as generated by "hg export".
16 # The patch itself, as generated by "hg export".
17 #
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
20 # threaded mail and news readers, and in mail archives.
21 #
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
24 # changes.
25 #
25 #
26 # To enable this extension:
26 # To enable this extension:
27 #
27 #
28 # [extensions]
28 # [extensions]
29 # hgext.patchbomb =
29 # hgext.patchbomb =
30 #
30 #
31 # To configure other defaults, add a section like this to your hgrc
31 # To configure other defaults, add a section like this to your hgrc
32 # file:
32 # file:
33 #
33 #
34 # [email]
34 # [email]
35 # from = My Name <my@email>
35 # from = My Name <my@email>
36 # to = recipient1, recipient2, ...
36 # to = recipient1, recipient2, ...
37 # cc = cc1, cc2, ...
37 # cc = cc1, cc2, ...
38 # bcc = bcc1, bcc2, ...
38 # bcc = bcc1, bcc2, ...
39 #
39 #
40 # Then you can use the "hg email" command to mail a series of changesets
40 # Then you can use the "hg email" command to mail a series of changesets
41 # as a patchbomb.
41 # as a patchbomb.
42 #
42 #
43 # To avoid sending patches prematurely, it is a good idea to first run
43 # To avoid sending patches prematurely, it is a good idea to first run
44 # the "email" command with the "-n" option (test only). You will be
44 # the "email" command with the "-n" option (test only). You will be
45 # prompted for an email recipient address, a subject an an introductory
45 # prompted for an email recipient address, a subject an an introductory
46 # message describing the patches of your patchbomb. Then when all is
46 # message describing the patches of your patchbomb. Then when all is
47 # done, your pager will be fired up once for each patchbomb message, so
47 # done, your pager will be fired up once for each patchbomb message, so
48 # you can verify everything is alright.
48 # you can verify everything is alright.
49 #
49 #
50 # The "-m" (mbox) option is also very useful. Instead of previewing
50 # The "-m" (mbox) option is also very useful. Instead of previewing
51 # each patchbomb message in a pager or sending the messages directly,
51 # each patchbomb message in a pager or sending the messages directly,
52 # it will create a UNIX mailbox file with the patch emails. This
52 # it will create a UNIX mailbox file with the patch emails. This
53 # mailbox file can be previewed with any mail user agent which supports
53 # mailbox file can be previewed with any mail user agent which supports
54 # UNIX mbox files, i.e. with mutt:
54 # UNIX mbox files, i.e. with mutt:
55 #
55 #
56 # % mutt -R -f mbox
56 # % mutt -R -f mbox
57 #
57 #
58 # When you are previewing the patchbomb messages, you can use `formail'
58 # When you are previewing the patchbomb messages, you can use `formail'
59 # (a utility that is commonly installed as part of the procmail package),
59 # (a utility that is commonly installed as part of the procmail package),
60 # to send each message out:
60 # to send each message out:
61 #
61 #
62 # % formail -s sendmail -bm -t < mbox
62 # % formail -s sendmail -bm -t < mbox
63 #
63 #
64 # That should be all. Now your patchbomb is on its way out.
64 # That should be all. Now your patchbomb is on its way out.
65
65
66 import os, errno, socket, tempfile
66 import os, errno, socket, tempfile
67 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
67 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
68 import email.Utils, email.Encoders
68 import email.Utils, email.Encoders
69 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
69 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
70 from mercurial.i18n import _
70 from mercurial.i18n import _
71 from mercurial.node import *
71 from mercurial.node import *
72
72
73 def patchbomb(ui, repo, *revs, **opts):
73 def patchbomb(ui, repo, *revs, **opts):
74 '''send changesets by email
74 '''send changesets by email
75
75
76 By default, diffs are sent in the format generated by hg export,
76 By default, diffs are sent in the format generated by hg export,
77 one per message. The series starts with a "[PATCH 0 of N]"
77 one per message. The series starts with a "[PATCH 0 of N]"
78 introduction, which describes the series as a whole.
78 introduction, which describes the series as a whole.
79
79
80 Each patch email has a Subject line of "[PATCH M of N] ...", using
80 Each patch email has a Subject line of "[PATCH M of N] ...", using
81 the first line of the changeset description as the subject text.
81 the first line of the changeset description as the subject text.
82 The message contains two or three body parts. First, the rest of
82 The message contains two or three body parts. First, the rest of
83 the changeset description. Next, (optionally) if the diffstat
83 the changeset description. Next, (optionally) if the diffstat
84 program is installed, the result of running diffstat on the patch.
84 program is installed, the result of running diffstat on the patch.
85 Finally, the patch itself, as generated by "hg export".
85 Finally, the patch itself, as generated by "hg export".
86
86
87 With --outgoing, emails will be generated for patches not
87 With --outgoing, emails will be generated for patches not
88 found in the destination repository (or only those which are
88 found in the destination repository (or only those which are
89 ancestors of the specified revisions if any are provided)
89 ancestors of the specified revisions if any are provided)
90
90
91 With --bundle, changesets are selected as for --outgoing,
91 With --bundle, changesets are selected as for --outgoing,
92 but a single email containing a binary Mercurial bundle as an
92 but a single email containing a binary Mercurial bundle as an
93 attachment will be sent.
93 attachment will be sent.
94
94
95 Examples:
95 Examples:
96
96
97 hg email -r 3000 # send patch 3000 only
97 hg email -r 3000 # send patch 3000 only
98 hg email -r 3000 -r 3001 # send patches 3000 and 3001
98 hg email -r 3000 -r 3001 # send patches 3000 and 3001
99 hg email -r 3000:3005 # send patches 3000 through 3005
99 hg email -r 3000:3005 # send patches 3000 through 3005
100 hg email 3000 # send patch 3000 (deprecated)
100 hg email 3000 # send patch 3000 (deprecated)
101
101
102 hg email -o # send all patches not in default
102 hg email -o # send all patches not in default
103 hg email -o DEST # send all patches not in DEST
103 hg email -o DEST # send all patches not in DEST
104 hg email -o -r 3000 # send all ancestors of 3000 not in default
104 hg email -o -r 3000 # send all ancestors of 3000 not in default
105 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
105 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
106
106
107 hg email -b # send bundle of all patches not in default
107 hg email -b # send bundle of all patches not in default
108 hg email -b DEST # send bundle of all patches not in DEST
108 hg email -b DEST # send bundle of all patches not in DEST
109 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
109 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
110 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
110 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
111
111
112 Before using this command, you will need to enable email in your hgrc.
112 Before using this command, you will need to enable email in your hgrc.
113 See the [email] section in hgrc(5) for details.
113 See the [email] section in hgrc(5) for details.
114 '''
114 '''
115
115
116 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
116 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
117 try:
117 try:
118 # readline gives raw_input editing capabilities, but is not
118 # readline gives raw_input editing capabilities, but is not
119 # present on windows
119 # present on windows
120 import readline
120 import readline
121 except ImportError: pass
121 except ImportError: pass
122
122
123 if default: prompt += ' [%s]' % default
123 if default: prompt += ' [%s]' % default
124 prompt += rest
124 prompt += rest
125 while True:
125 while True:
126 r = raw_input(prompt)
126 r = raw_input(prompt)
127 if r: return r
127 if r: return r
128 if default is not None: return default
128 if default is not None: return default
129 if empty_ok: return r
129 if empty_ok: return r
130 ui.warn(_('Please enter a valid value.\n'))
130 ui.warn(_('Please enter a valid value.\n'))
131
131
132 def confirm(s):
132 def confirm(s):
133 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
133 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
134 raise ValueError
134 raise ValueError
135
135
136 def cdiffstat(summary, patchlines):
136 def cdiffstat(summary, patchlines):
137 s = patch.diffstat(patchlines)
137 s = patch.diffstat(patchlines)
138 if s:
138 if s:
139 if summary:
139 if summary:
140 ui.write(summary, '\n')
140 ui.write(summary, '\n')
141 ui.write(s, '\n')
141 ui.write(s, '\n')
142 confirm(_('Does the diffstat above look okay'))
142 confirm(_('Does the diffstat above look okay'))
143 return s
143 return s
144
144
145 def makepatch(patch, idx, total):
145 def makepatch(patch, idx, total):
146 desc = []
146 desc = []
147 node = None
147 node = None
148 body = ''
148 body = ''
149 for line in patch:
149 for line in patch:
150 if line.startswith('#'):
150 if line.startswith('#'):
151 if line.startswith('# Node ID'): node = line.split()[-1]
151 if line.startswith('# Node ID'): node = line.split()[-1]
152 continue
152 continue
153 if (line.startswith('diff -r')
153 if (line.startswith('diff -r')
154 or line.startswith('diff --git')):
154 or line.startswith('diff --git')):
155 break
155 break
156 desc.append(line)
156 desc.append(line)
157 if not node: raise ValueError
157 if not node: raise ValueError
158
158
159 #body = ('\n'.join(desc[1:]).strip() or
159 #body = ('\n'.join(desc[1:]).strip() or
160 # 'Patch subject is complete summary.')
160 # 'Patch subject is complete summary.')
161 #body += '\n\n\n'
161 #body += '\n\n\n'
162
162
163 if opts['plain']:
163 if opts['plain']:
164 while patch and patch[0].startswith('# '): patch.pop(0)
164 while patch and patch[0].startswith('# '): patch.pop(0)
165 if patch: patch.pop(0)
165 if patch: patch.pop(0)
166 while patch and not patch[0].strip(): patch.pop(0)
166 while patch and not patch[0].strip(): patch.pop(0)
167 if opts['diffstat']:
167 if opts['diffstat']:
168 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
168 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
169 if opts['attach']:
169 if opts['attach']:
170 msg = email.MIMEMultipart.MIMEMultipart()
170 msg = email.MIMEMultipart.MIMEMultipart()
171 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
171 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
172 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
172 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
173 binnode = bin(node)
173 binnode = bin(node)
174 # if node is mq patch, it will have patch file name as tag
174 # if node is mq patch, it will have patch file name as tag
175 patchname = [t for t in repo.nodetags(binnode)
175 patchname = [t for t in repo.nodetags(binnode)
176 if t.endswith('.patch') or t.endswith('.diff')]
176 if t.endswith('.patch') or t.endswith('.diff')]
177 if patchname:
177 if patchname:
178 patchname = patchname[0]
178 patchname = patchname[0]
179 elif total > 1:
179 elif total > 1:
180 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
180 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
181 binnode, idx, total)
181 binnode, idx, total)
182 else:
182 else:
183 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
183 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
184 p['Content-Disposition'] = 'inline; filename=' + patchname
184 p['Content-Disposition'] = 'inline; filename=' + patchname
185 msg.attach(p)
185 msg.attach(p)
186 else:
186 else:
187 body += '\n'.join(patch)
187 body += '\n'.join(patch)
188 msg = email.MIMEText.MIMEText(body)
188 msg = email.MIMEText.MIMEText(body)
189
189
190 subj = desc[0].strip().rstrip('. ')
190 subj = desc[0].strip().rstrip('. ')
191 if total == 1:
191 if total == 1:
192 subj = '[PATCH] ' + (opts['subject'] or subj)
192 subj = '[PATCH] ' + (opts['subject'] or subj)
193 else:
193 else:
194 tlen = len(str(total))
194 tlen = len(str(total))
195 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
195 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
196 msg['Subject'] = subj
196 msg['Subject'] = subj
197 msg['X-Mercurial-Node'] = node
197 msg['X-Mercurial-Node'] = node
198 return msg
198 return msg
199
199
200 def outgoing(dest, revs):
200 def outgoing(dest, revs):
201 '''Return the revisions present locally but not in dest'''
201 '''Return the revisions present locally but not in dest'''
202 dest = ui.expandpath(dest or 'default-push', dest or 'default')
202 dest = ui.expandpath(dest or 'default-push', dest or 'default')
203 revs = [repo.lookup(rev) for rev in revs]
203 revs = [repo.lookup(rev) for rev in revs]
204 other = hg.repository(ui, dest)
204 other = hg.repository(ui, dest)
205 ui.status(_('comparing with %s\n') % dest)
205 ui.status(_('comparing with %s\n') % dest)
206 o = repo.findoutgoing(other)
206 o = repo.findoutgoing(other)
207 if not o:
207 if not o:
208 ui.status(_("no changes found\n"))
208 ui.status(_("no changes found\n"))
209 return []
209 return []
210 o = repo.changelog.nodesbetween(o, revs or None)[0]
210 o = repo.changelog.nodesbetween(o, revs or None)[0]
211 return [str(repo.changelog.rev(r)) for r in o]
211 return [str(repo.changelog.rev(r)) for r in o]
212
212
213 def getbundle(dest):
213 def getbundle(dest):
214 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
214 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
215 tmpfn = os.path.join(tmpdir, 'bundle')
215 tmpfn = os.path.join(tmpdir, 'bundle')
216 try:
216 try:
217 commands.bundle(ui, repo, tmpfn, dest, **opts)
217 commands.bundle(ui, repo, tmpfn, dest, **opts)
218 return open(tmpfn).read()
218 return open(tmpfn).read()
219 finally:
219 finally:
220 try:
220 try:
221 os.unlink(tmpfn)
221 os.unlink(tmpfn)
222 except:
222 except:
223 pass
223 pass
224 os.rmdir(tmpdir)
224 os.rmdir(tmpdir)
225
225
226 if not opts['test']:
227 mail.validateconfig(ui)
228
226 # option handling
229 # option handling
227 commands.setremoteconfig(ui, opts)
230 commands.setremoteconfig(ui, opts)
228 if opts.get('outgoint') and opts.get('bundle'):
231 if opts.get('outgoint') and opts.get('bundle'):
229 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
232 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
230
233
231 if opts.get('outgoing') or opts.get('bundle'):
234 if opts.get('outgoing') or opts.get('bundle'):
232 if len(revs) > 1:
235 if len(revs) > 1:
233 raise util.Abort(_("too many destinations"))
236 raise util.Abort(_("too many destinations"))
234 dest = revs and revs[0] or None
237 dest = revs and revs[0] or None
235 revs = []
238 revs = []
236
239
237 if opts.get('rev'):
240 if opts.get('rev'):
238 if revs:
241 if revs:
239 raise util.Abort(_('use only one form to specify the revision'))
242 raise util.Abort(_('use only one form to specify the revision'))
240 revs = opts.get('rev')
243 revs = opts.get('rev')
241
244
242 if opts.get('outgoing'):
245 if opts.get('outgoing'):
243 revs = outgoing(dest, opts.get('rev'))
246 revs = outgoing(dest, opts.get('rev'))
244 if opts.get('bundle'):
247 if opts.get('bundle'):
245 opts['revs'] = revs
248 opts['revs'] = revs
246
249
247 # start
250 # start
248 start_time = util.makedate()
251 start_time = util.makedate()
249
252
250 def genmsgid(id):
253 def genmsgid(id):
251 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
254 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
252
255
253 def getexportmsgs():
256 def getexportmsgs():
254 patches = []
257 patches = []
255
258
256 class exportee:
259 class exportee:
257 def __init__(self, container):
260 def __init__(self, container):
258 self.lines = []
261 self.lines = []
259 self.container = container
262 self.container = container
260 self.name = 'email'
263 self.name = 'email'
261
264
262 def write(self, data):
265 def write(self, data):
263 self.lines.append(data)
266 self.lines.append(data)
264
267
265 def close(self):
268 def close(self):
266 self.container.append(''.join(self.lines).split('\n'))
269 self.container.append(''.join(self.lines).split('\n'))
267 self.lines = []
270 self.lines = []
268
271
269 commands.export(ui, repo, *revs, **{'output': exportee(patches),
272 commands.export(ui, repo, *revs, **{'output': exportee(patches),
270 'switch_parent': False,
273 'switch_parent': False,
271 'text': None,
274 'text': None,
272 'git': opts.get('git')})
275 'git': opts.get('git')})
273
276
274 jumbo = []
277 jumbo = []
275 msgs = []
278 msgs = []
276
279
277 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
280 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
278
281
279 for p, i in zip(patches, xrange(len(patches))):
282 for p, i in zip(patches, xrange(len(patches))):
280 jumbo.extend(p)
283 jumbo.extend(p)
281 msgs.append(makepatch(p, i + 1, len(patches)))
284 msgs.append(makepatch(p, i + 1, len(patches)))
282
285
283 if len(patches) > 1:
286 if len(patches) > 1:
284 tlen = len(str(len(patches)))
287 tlen = len(str(len(patches)))
285
288
286 subj = '[PATCH %0*d of %d] %s' % (
289 subj = '[PATCH %0*d of %d] %s' % (
287 tlen, 0,
290 tlen, 0,
288 len(patches),
291 len(patches),
289 opts['subject'] or
292 opts['subject'] or
290 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
293 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
291 len(patches))))
294 len(patches))))
292
295
293 body = ''
296 body = ''
294 if opts['diffstat']:
297 if opts['diffstat']:
295 d = cdiffstat(_('Final summary:\n'), jumbo)
298 d = cdiffstat(_('Final summary:\n'), jumbo)
296 if d: body = '\n' + d
299 if d: body = '\n' + d
297
300
298 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
301 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
299 body = ui.edit(body, sender)
302 body = ui.edit(body, sender)
300
303
301 msg = email.MIMEText.MIMEText(body)
304 msg = email.MIMEText.MIMEText(body)
302 msg['Subject'] = subj
305 msg['Subject'] = subj
303
306
304 msgs.insert(0, msg)
307 msgs.insert(0, msg)
305 return msgs
308 return msgs
306
309
307 def getbundlemsgs(bundle):
310 def getbundlemsgs(bundle):
308 subj = opts['subject'] or \
311 subj = opts['subject'] or \
309 prompt('Subject:', default='A bundle for your repository')
312 prompt('Subject:', default='A bundle for your repository')
310 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
313 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
311 body = ui.edit('', sender)
314 body = ui.edit('', sender)
312
315
313 msg = email.MIMEMultipart.MIMEMultipart()
316 msg = email.MIMEMultipart.MIMEMultipart()
314 if body:
317 if body:
315 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
318 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
316 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
319 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
317 datapart.set_payload(bundle)
320 datapart.set_payload(bundle)
318 datapart.add_header('Content-Disposition', 'attachment',
321 datapart.add_header('Content-Disposition', 'attachment',
319 filename='bundle.hg')
322 filename='bundle.hg')
320 email.Encoders.encode_base64(datapart)
323 email.Encoders.encode_base64(datapart)
321 msg.attach(datapart)
324 msg.attach(datapart)
322 msg['Subject'] = subj
325 msg['Subject'] = subj
323 return [msg]
326 return [msg]
324
327
325 if opts.get('bundle'):
328 if opts.get('bundle'):
326 msgs = getbundlemsgs(getbundle(dest))
329 msgs = getbundlemsgs(getbundle(dest))
327 else:
330 else:
328 msgs = getexportmsgs()
331 msgs = getexportmsgs()
329
332
330 sender = (opts['from'] or ui.config('email', 'from') or
333 sender = (opts['from'] or ui.config('email', 'from') or
331 ui.config('patchbomb', 'from') or
334 ui.config('patchbomb', 'from') or
332 prompt('From', ui.username()))
335 prompt('From', ui.username()))
333
336
334 def getaddrs(opt, prpt, default = None):
337 def getaddrs(opt, prpt, default = None):
335 addrs = opts[opt] or (ui.config('email', opt) or
338 addrs = opts[opt] or (ui.config('email', opt) or
336 ui.config('patchbomb', opt) or
339 ui.config('patchbomb', opt) or
337 prompt(prpt, default = default)).split(',')
340 prompt(prpt, default = default)).split(',')
338 return [a.strip() for a in addrs if a.strip()]
341 return [a.strip() for a in addrs if a.strip()]
339
342
340 to = getaddrs('to', 'To')
343 to = getaddrs('to', 'To')
341 cc = getaddrs('cc', 'Cc', '')
344 cc = getaddrs('cc', 'Cc', '')
342
345
343 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
346 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
344 ui.config('patchbomb', 'bcc') or '').split(',')
347 ui.config('patchbomb', 'bcc') or '').split(',')
345 bcc = [a.strip() for a in bcc if a.strip()]
348 bcc = [a.strip() for a in bcc if a.strip()]
346
349
347 ui.write('\n')
350 ui.write('\n')
348
351
349 if not opts['test'] and not opts['mbox']:
352 if not opts['test'] and not opts['mbox']:
350 mailer = mail.connect(ui)
353 mailer = mail.connect(ui)
351 parent = None
354 parent = None
352
355
353 sender_addr = email.Utils.parseaddr(sender)[1]
356 sender_addr = email.Utils.parseaddr(sender)[1]
354 for m in msgs:
357 for m in msgs:
355 try:
358 try:
356 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
359 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
357 except TypeError:
360 except TypeError:
358 m['Message-Id'] = genmsgid('patchbomb')
361 m['Message-Id'] = genmsgid('patchbomb')
359 if parent:
362 if parent:
360 m['In-Reply-To'] = parent
363 m['In-Reply-To'] = parent
361 else:
364 else:
362 parent = m['Message-Id']
365 parent = m['Message-Id']
363 m['Date'] = util.datestr(date=start_time,
366 m['Date'] = util.datestr(date=start_time,
364 format="%a, %d %b %Y %H:%M:%S", timezone=True)
367 format="%a, %d %b %Y %H:%M:%S", timezone=True)
365
368
366 start_time = (start_time[0] + 1, start_time[1])
369 start_time = (start_time[0] + 1, start_time[1])
367 m['From'] = sender
370 m['From'] = sender
368 m['To'] = ', '.join(to)
371 m['To'] = ', '.join(to)
369 if cc: m['Cc'] = ', '.join(cc)
372 if cc: m['Cc'] = ', '.join(cc)
370 if bcc: m['Bcc'] = ', '.join(bcc)
373 if bcc: m['Bcc'] = ', '.join(bcc)
371 if opts['test']:
374 if opts['test']:
372 ui.status('Displaying ', m['Subject'], ' ...\n')
375 ui.status('Displaying ', m['Subject'], ' ...\n')
373 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
376 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
374 try:
377 try:
375 fp.write(m.as_string(0))
378 fp.write(m.as_string(0))
376 fp.write('\n')
379 fp.write('\n')
377 except IOError, inst:
380 except IOError, inst:
378 if inst.errno != errno.EPIPE:
381 if inst.errno != errno.EPIPE:
379 raise
382 raise
380 fp.close()
383 fp.close()
381 elif opts['mbox']:
384 elif opts['mbox']:
382 ui.status('Writing ', m['Subject'], ' ...\n')
385 ui.status('Writing ', m['Subject'], ' ...\n')
383 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
386 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
384 date = util.datestr(date=start_time,
387 date = util.datestr(date=start_time,
385 format='%a %b %d %H:%M:%S %Y', timezone=False)
388 format='%a %b %d %H:%M:%S %Y', timezone=False)
386 fp.write('From %s %s\n' % (sender_addr, date))
389 fp.write('From %s %s\n' % (sender_addr, date))
387 fp.write(m.as_string(0))
390 fp.write(m.as_string(0))
388 fp.write('\n\n')
391 fp.write('\n\n')
389 fp.close()
392 fp.close()
390 else:
393 else:
391 ui.status('Sending ', m['Subject'], ' ...\n')
394 ui.status('Sending ', m['Subject'], ' ...\n')
392 # Exim does not remove the Bcc field
395 # Exim does not remove the Bcc field
393 del m['Bcc']
396 del m['Bcc']
394 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
397 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
395
398
396 cmdtable = {
399 cmdtable = {
397 'email':
400 'email':
398 (patchbomb,
401 (patchbomb,
399 [('a', 'attach', None, 'send patches as inline attachments'),
402 [('a', 'attach', None, 'send patches as inline attachments'),
400 ('', 'bcc', [], 'email addresses of blind copy recipients'),
403 ('', 'bcc', [], 'email addresses of blind copy recipients'),
401 ('c', 'cc', [], 'email addresses of copy recipients'),
404 ('c', 'cc', [], 'email addresses of copy recipients'),
402 ('d', 'diffstat', None, 'add diffstat output to messages'),
405 ('d', 'diffstat', None, 'add diffstat output to messages'),
403 ('g', 'git', None, _('use git extended diff format')),
406 ('g', 'git', None, _('use git extended diff format')),
404 ('f', 'from', '', 'email address of sender'),
407 ('f', 'from', '', 'email address of sender'),
405 ('', 'plain', None, 'omit hg patch header'),
408 ('', 'plain', None, 'omit hg patch header'),
406 ('n', 'test', None, 'print messages that would be sent'),
409 ('n', 'test', None, 'print messages that would be sent'),
407 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
410 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
408 ('o', 'outgoing', None, _('send changes not found in the target repository')),
411 ('o', 'outgoing', None, _('send changes not found in the target repository')),
409 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
412 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
410 ('r', 'rev', [], _('a revision to send')),
413 ('r', 'rev', [], _('a revision to send')),
411 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
414 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
412 ('t', 'to', [], 'email addresses of recipients'),
415 ('t', 'to', [], 'email addresses of recipients'),
413 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
416 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
414 ('', 'base', [],
417 ('', 'base', [],
415 _('a base changeset to specify instead of a destination (with -b)'))]
418 _('a base changeset to specify instead of a destination (with -b)'))]
416 + commands.remoteopts,
419 + commands.remoteopts,
417 "hg email [OPTION]... [DEST]...")
420 "hg email [OPTION]... [DEST]...")
418 }
421 }
@@ -1,70 +1,82 b''
1 # mail.py - mail sending bits for mercurial
1 # mail.py - mail sending bits for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import os, smtplib, templater, util, socket
9 import os, smtplib, templater, util, socket
10
10
11 def _smtp(ui):
11 def _smtp(ui):
12 '''send mail using smtp.'''
12 '''send mail using smtp.'''
13
13
14 local_hostname = ui.config('smtp', 'local_hostname')
14 local_hostname = ui.config('smtp', 'local_hostname')
15 s = smtplib.SMTP(local_hostname=local_hostname)
15 s = smtplib.SMTP(local_hostname=local_hostname)
16 mailhost = ui.config('smtp', 'host')
16 mailhost = ui.config('smtp', 'host')
17 if not mailhost:
17 if not mailhost:
18 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
18 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
19 mailport = int(ui.config('smtp', 'port', 25))
19 mailport = int(ui.config('smtp', 'port', 25))
20 ui.note(_('sending mail: smtp host %s, port %s\n') %
20 ui.note(_('sending mail: smtp host %s, port %s\n') %
21 (mailhost, mailport))
21 (mailhost, mailport))
22 s.connect(host=mailhost, port=mailport)
22 s.connect(host=mailhost, port=mailport)
23 if ui.configbool('smtp', 'tls'):
23 if ui.configbool('smtp', 'tls'):
24 if not hasattr(socket, 'ssl'):
24 if not hasattr(socket, 'ssl'):
25 raise util.Abort(_("can't use TLS: Python SSL support "
25 raise util.Abort(_("can't use TLS: Python SSL support "
26 "not installed"))
26 "not installed"))
27 ui.note(_('(using tls)\n'))
27 ui.note(_('(using tls)\n'))
28 s.ehlo()
28 s.ehlo()
29 s.starttls()
29 s.starttls()
30 s.ehlo()
30 s.ehlo()
31 username = ui.config('smtp', 'username')
31 username = ui.config('smtp', 'username')
32 password = ui.config('smtp', 'password')
32 password = ui.config('smtp', 'password')
33 if username and password:
33 if username and password:
34 ui.note(_('(authenticating to mail server as %s)\n') %
34 ui.note(_('(authenticating to mail server as %s)\n') %
35 (username))
35 (username))
36 s.login(username, password)
36 s.login(username, password)
37 return s
37 return s
38
38
39 class _sendmail(object):
39 class _sendmail(object):
40 '''send mail using sendmail.'''
40 '''send mail using sendmail.'''
41
41
42 def __init__(self, ui, program):
42 def __init__(self, ui, program):
43 self.ui = ui
43 self.ui = ui
44 self.program = program
44 self.program = program
45
45
46 def sendmail(self, sender, recipients, msg):
46 def sendmail(self, sender, recipients, msg):
47 cmdline = '%s -f %s %s' % (
47 cmdline = '%s -f %s %s' % (
48 self.program, templater.email(sender),
48 self.program, templater.email(sender),
49 ' '.join(map(templater.email, recipients)))
49 ' '.join(map(templater.email, recipients)))
50 self.ui.note(_('sending mail: %s\n') % cmdline)
50 self.ui.note(_('sending mail: %s\n') % cmdline)
51 fp = os.popen(cmdline, 'w')
51 fp = os.popen(cmdline, 'w')
52 fp.write(msg)
52 fp.write(msg)
53 ret = fp.close()
53 ret = fp.close()
54 if ret:
54 if ret:
55 raise util.Abort('%s %s' % (
55 raise util.Abort('%s %s' % (
56 os.path.basename(self.program.split(None, 1)[0]),
56 os.path.basename(self.program.split(None, 1)[0]),
57 util.explain_exit(ret)[0]))
57 util.explain_exit(ret)[0]))
58
58
59 def connect(ui):
59 def connect(ui):
60 '''make a mail connection. object returned has one method, sendmail.
60 '''make a mail connection. object returned has one method, sendmail.
61 call as sendmail(sender, list-of-recipients, msg).'''
61 call as sendmail(sender, list-of-recipients, msg).'''
62
62
63 method = ui.config('email', 'method', 'smtp')
63 method = ui.config('email', 'method', 'smtp')
64 if method == 'smtp':
64 if method == 'smtp':
65 return _smtp(ui)
65 return _smtp(ui)
66
66
67 return _sendmail(ui, method)
67 return _sendmail(ui, method)
68
68
69 def sendmail(ui, sender, recipients, msg):
69 def sendmail(ui, sender, recipients, msg):
70 return connect(ui).sendmail(sender, recipients, msg)
70 return connect(ui).sendmail(sender, recipients, msg)
71
72 def validateconfig(ui):
73 '''determine if we have enough config data to try sending email.'''
74 method = ui.config('email', 'method', 'smtp')
75 if method == 'smtp':
76 if not ui.config('smtp', 'host'):
77 raise util.Abort(_('smtp specified as email transport, '
78 'but no smtp host configured'))
79 else:
80 if not util.find_exe(method):
81 raise util.Abort(_('%r specified as email transport, '
82 'but not in PATH') % method)
General Comments 0
You need to be logged in to leave comments. Login now