13
13
# You should have received a copy of the GNU General Public License
14
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
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
24
25
from bzrlib import (
37
41
self.config = config
39
43
def compose(self, prompt, to, subject, attachment, mime_subtype,
40
extension, basename=None):
44
extension, basename=None, body=None):
41
45
"""Compose (and possibly send) an email message
43
47
Must be implemented by subclasses.
58
62
raise NotImplementedError
60
def compose_merge_request(self, to, subject, directive, basename=None):
64
def compose_merge_request(self, to, subject, directive, basename=None,
61
66
"""Compose (and possibly send) a merge request
63
68
:param to: The address to send the request to
70
75
prompt = self._get_merge_prompt("Please describe these changes:", to,
71
76
subject, directive)
72
77
self.compose(prompt, to, subject, directive,
73
'x-patch', '.patch', basename)
78
'x-patch', '.patch', basename, body)
75
80
def _get_merge_prompt(self, prompt, to, subject, attachment):
76
81
"""Generate a prompt string. Overridden by Editor.
95
102
attachment.decode('utf-8', 'replace')))
97
104
def compose(self, prompt, to, subject, attachment, mime_subtype,
98
extension, basename=None):
105
extension, basename=None, body=None):
99
106
"""See MailClient.compose"""
101
108
raise errors.NoMailAddressSpecified()
102
body = msgeditor.edit_commit_message(prompt)
109
body = msgeditor.edit_commit_message(prompt, start_message=body)
104
111
raise errors.NoMessageSupplied()
105
112
email_message.EmailMessage.send(self.config,
111
118
attachment_mime_subtype=mime_subtype)
114
class ExternalMailClient(MailClient):
115
"""An external mail client."""
119
mail_client_registry.register('editor', Editor,
123
class BodyExternalMailClient(MailClient):
117
127
def _get_client_commands(self):
118
128
"""Provide a list of commands that may invoke the mail client"""
123
133
return self._client_commands
125
135
def compose(self, prompt, to, subject, attachment, mime_subtype,
126
extension, basename=None):
136
extension, basename=None, body=None):
127
137
"""See MailClient.compose.
129
139
Writes the attachment to a temporary file, invokes _compose.
131
141
if basename is None:
132
142
basename = 'attachment'
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
143
pathname = osutils.mkdtemp(prefix='bzr-mail-')
134
144
attach_path = osutils.pathjoin(pathname, basename + extension)
135
145
outfile = open(attach_path, 'wb')
137
147
outfile.write(attachment)
151
kwargs = {'body': body}
140
154
self._compose(prompt, to, subject, attach_path, mime_subtype,
143
157
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
extension, body=None, from_=None):
145
159
"""Invoke a mail client as a commandline process.
147
161
Overridden by MAPIClient.
152
166
"text", but the precise subtype can be specified here
153
167
:param extension: A file extension (including period) associated with
154
168
the attachment type.
169
:param body: Optional body text.
170
:param from_: Optional From: header.
156
172
for name in self._get_client_commands():
157
173
cmdline = [self._encode_path(name, 'executable')]
175
kwargs = {'body': body}
178
if from_ is not None:
179
kwargs['from_'] = from_
158
180
cmdline.extend(self._get_compose_commandline(to, subject,
161
184
subprocess.call(cmdline)
162
185
except OSError, e:
168
191
raise errors.MailClientNotFound(self._client_commands)
170
def _get_compose_commandline(self, to, subject, attach_path):
193
def _get_compose_commandline(self, to, subject, attach_path, body):
171
194
"""Determine the commandline to use for composing a message
173
196
Implemented by various subclasses
186
209
:return: encoded string if u is unicode, u itself otherwise.
188
211
if isinstance(u, unicode):
189
return u.encode(bzrlib.user_encoding, 'replace')
212
return u.encode(osutils.get_user_encoding(), 'replace')
192
215
def _encode_path(self, path, kind):
201
224
if isinstance(path, unicode):
203
return path.encode(bzrlib.user_encoding)
226
return path.encode(osutils.get_user_encoding())
204
227
except UnicodeEncodeError:
205
228
raise errors.UnableEncodePath(path, kind)
209
class Evolution(ExternalMailClient):
232
class ExternalMailClient(BodyExternalMailClient):
233
"""An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
210
239
"""Evolution mail client."""
212
241
_client_commands = ['evolution']
214
def _get_compose_commandline(self, to, subject, attach_path):
243
def _get_compose_commandline(self, to, subject, attach_path, body=None):
215
244
"""See ExternalMailClient._get_compose_commandline"""
216
245
message_options = {}
217
246
if subject is not None:
218
247
message_options['subject'] = subject
219
248
if attach_path is not None:
220
249
message_options['attach'] = attach_path
251
message_options['body'] = body
221
252
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
222
253
sorted(message_options.iteritems())]
223
254
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
224
255
'&'.join(options_list))]
227
class Mutt(ExternalMailClient):
256
mail_client_registry.register('evolution', Evolution,
257
help=Evolution.__doc__)
260
class Mutt(BodyExternalMailClient):
228
261
"""Mutt mail client."""
230
263
_client_commands = ['mutt']
232
def _get_compose_commandline(self, to, subject, attach_path):
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
233
266
"""See ExternalMailClient._get_compose_commandline"""
234
267
message_options = []
235
268
if subject is not None:
237
270
if attach_path is not None:
238
271
message_options.extend(['-a',
239
272
self._encode_path(attach_path, 'attachment')])
274
# Store the temp file object in self, so that it does not get
275
# garbage collected and delete the file before mutt can read it.
276
self._temp_file = tempfile.NamedTemporaryFile(
277
prefix="mutt-body-", suffix=".txt")
278
self._temp_file.write(body)
279
self._temp_file.flush()
280
message_options.extend(['-i', self._temp_file.name])
240
281
if to is not None:
241
message_options.append(self._encode_safe(to))
282
message_options.extend(['--', self._encode_safe(to)])
242
283
return message_options
245
class Thunderbird(ExternalMailClient):
284
mail_client_registry.register('mutt', Mutt,
288
class Thunderbird(BodyExternalMailClient):
246
289
"""Mozilla Thunderbird (or Icedove)
248
291
Note that Thunderbird 1.5 is buggy and does not support setting
255
298
_client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
256
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
299
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
258
def _get_compose_commandline(self, to, subject, attach_path):
302
def _get_compose_commandline(self, to, subject, attach_path, body=None):
259
303
"""See ExternalMailClient._get_compose_commandline"""
260
304
message_options = {}
261
305
if to is not None:
265
309
if attach_path is not None:
266
310
message_options['attachment'] = urlutils.local_path_to_url(
268
options_list = ["%s='%s'" % (k, v) for k, v in
269
sorted(message_options.iteritems())]
313
options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
316
options_list.extend(["%s='%s'" % (k, v) for k, v in
317
sorted(message_options.iteritems())])
270
318
return ['-compose', ','.join(options_list)]
319
mail_client_registry.register('thunderbird', Thunderbird,
320
help=Thunderbird.__doc__)
273
323
class KMail(ExternalMailClient):
286
336
if to is not None:
287
337
message_options.extend([self._encode_safe(to)])
288
338
return message_options
291
class XDGEmail(ExternalMailClient):
339
mail_client_registry.register('kmail', KMail,
343
class Claws(ExternalMailClient):
344
"""Claws mail client."""
348
_client_commands = ['claws-mail']
350
def _get_compose_commandline(self, to, subject, attach_path, body=None,
352
"""See ExternalMailClient._get_compose_commandline"""
354
if from_ is not None:
355
compose_url.append('from=' + urllib.quote(from_))
356
if subject is not None:
357
# Don't use urllib.quote_plus because Claws doesn't seem
358
# to recognise spaces encoded as "+".
360
'subject=' + urllib.quote(self._encode_safe(subject)))
363
'body=' + urllib.quote(self._encode_safe(body)))
364
# to must be supplied for the claws-mail --compose syntax to work.
366
raise errors.NoMailAddressSpecified()
367
compose_url = 'mailto:%s?%s' % (
368
self._encode_safe(to), '&'.join(compose_url))
369
# Collect command-line options.
370
message_options = ['--compose', compose_url]
371
if attach_path is not None:
372
message_options.extend(
373
['--attach', self._encode_path(attach_path, 'attachment')])
374
return message_options
376
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
377
extension, body=None, from_=None):
378
"""See ExternalMailClient._compose"""
380
from_ = self.config.get_user_option('email')
381
super(Claws, self)._compose(prompt, to, subject, attach_path,
382
mime_subtype, extension, body, from_)
385
mail_client_registry.register('claws', Claws,
389
class XDGEmail(BodyExternalMailClient):
292
390
"""xdg-email attempts to invoke the user's preferred mail client"""
294
392
_client_commands = ['xdg-email']
296
def _get_compose_commandline(self, to, subject, attach_path):
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
297
395
"""See ExternalMailClient._get_compose_commandline"""
299
397
raise errors.NoMailAddressSpecified()
303
401
if attach_path is not None:
304
402
commandline.extend(['--attach',
305
403
self._encode_path(attach_path, 'attachment')])
405
commandline.extend(['--body', self._encode_safe(body)])
306
406
return commandline
407
mail_client_registry.register('xdg-email', XDGEmail,
408
help=XDGEmail.__doc__)
309
411
class EmacsMail(ExternalMailClient):
328
430
This temporary file will be loaded at runtime in
329
431
_get_compose_commandline function.
331
FIXME: this function does not remove the file. That's a wanted
433
This function does not remove the file. That's a wanted
332
434
behaviour since _get_compose_commandline won't run the send
333
435
mail function directly but return the eligible command line.
334
436
Removing our temporary file here would prevent our sendmail
337
A possible workaround could be to load the function here with
338
emacsclient --eval '(load temp)' but this is not robust since
339
emacs could have been stopped between here and the call to
437
function to work. (The file is deleted by some elisp code
438
after being read by Emacs.)
343
441
_defun = r"""(defun bzr-add-mime-att (file)
344
"Attach FILe to a mail buffer as a MIME attachment."
442
"Attach FILE to a mail buffer as a MIME attachment."
345
443
(let ((agent mail-user-agent))
348
444
(if (and file (file-exists-p file))
350
446
((eq agent 'sendmail-user-agent)
352
((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
353
(mml-attach-file file "text/x-patch" "BZR merge" "attachment"))
450
(if (functionp 'etach-attach)
452
(mail-attach-file file))))
453
((or (eq agent 'message-user-agent)
454
(eq agent 'gnus-user-agent)
455
(eq agent 'mh-e-user-agent))
457
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
458
((eq agent 'mew-user-agent)
460
(mew-draft-prepare-attachments)
461
(mew-attach-link file (file-name-nondirectory file))
462
(let* ((nums (mew-syntax-nums))
463
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
464
(mew-syntax-set-cd syntax "BZR merge")
465
(mew-encode-syntax-print mew-encode-syntax))
466
(mew-header-goto-body)))
355
(message "Unhandled MUA")))
468
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
356
469
(error "File %s does not exist." file))))
373
486
if to is not None:
374
_to = ("\"%s\"" % self._encode_safe(to))
487
_to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
375
488
if subject is not None:
376
_subject = ("\"%s\"" % self._encode_safe(subject))
489
_subject = ("\"%s\"" %
490
self._encode_safe(subject).replace('"', '\\"'))
378
492
# Funcall the default mail composition function
379
493
# This will work with any mail mode including default mail-mode
385
499
# Try to attach a MIME attachment using our wrapper function
386
500
if attach_path is not None:
387
501
# Do not create a file if there is no attachment
388
lmmform = '(load "%s")' % self._prepare_send_function()
502
elisp = self._prepare_send_function()
503
lmmform = '(load "%s")' % elisp
389
504
mmform = '(bzr-add-mime-att "%s")' % \
390
505
self._encode_path(attach_path, 'attachment')
506
rmform = '(delete-file "%s")' % elisp
391
507
commandline.append(lmmform)
392
508
commandline.append(mmform)
509
commandline.append(rmform)
394
511
return commandline
397
class MAPIClient(ExternalMailClient):
512
mail_client_registry.register('emacsclient', EmacsMail,
513
help=EmacsMail.__doc__)
516
class MAPIClient(BodyExternalMailClient):
398
517
"""Default Windows mail client launched using MAPI."""
400
519
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
extension, body=None):
402
521
"""See ExternalMailClient._compose.
404
523
This implementation uses MAPI via the simplemapi ctypes wrapper
406
525
from bzrlib.util import simplemapi
408
simplemapi.SendMail(to or '', subject or '', '', attach_path)
527
simplemapi.SendMail(to or '', subject or '', body or '',
409
529
except simplemapi.MAPIError, e:
410
530
if e.code != simplemapi.MAPI_USER_ABORT:
411
531
raise errors.MailClientNotFound(['MAPI supported mail client'
412
532
' (error %d)' % (e.code,)])
533
mail_client_registry.register('mapi', MAPIClient,
534
help=MAPIClient.__doc__)
415
537
class DefaultMail(MailClient):
416
538
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
417
539
falls back to Editor"""
419
543
def _mail_client(self):
420
544
"""Determine the preferred mail client for this platform"""
421
545
if osutils.supports_mapi():
424
548
return XDGEmail(self.config)
426
550
def compose(self, prompt, to, subject, attachment, mime_subtype,
427
extension, basename=None):
551
extension, basename=None, body=None):
428
552
"""See MailClient.compose"""
430
554
return self._mail_client().compose(prompt, to, subject,
431
555
attachment, mimie_subtype,
556
extension, basename, body)
433
557
except errors.MailClientNotFound:
434
558
return Editor(self.config).compose(prompt, to, subject,
435
attachment, mimie_subtype, extension)
559
attachment, mimie_subtype, extension, body)
437
def compose_merge_request(self, to, subject, directive, basename=None):
561
def compose_merge_request(self, to, subject, directive, basename=None,
438
563
"""See MailClient.compose_merge_request"""
440
565
return self._mail_client().compose_merge_request(to, subject,
441
directive, basename=basename)
566
directive, basename=basename, body=body)
442
567
except errors.MailClientNotFound:
443
568
return Editor(self.config).compose_merge_request(to, subject,
444
directive, basename=basename)
569
directive, basename=basename, body=body)
570
mail_client_registry.register('default', DefaultMail,
571
help=DefaultMail.__doc__)
572
mail_client_registry.default_key = 'default'