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