104
95
attachment.decode('utf-8', 'replace')))
106
97
def compose(self, prompt, to, subject, attachment, mime_subtype,
107
extension, basename=None, body=None):
98
extension, basename=None):
108
99
"""See MailClient.compose"""
110
101
raise errors.NoMailAddressSpecified()
111
body = msgeditor.edit_commit_message(prompt, start_message=body)
102
body = msgeditor.edit_commit_message(prompt)
113
104
raise errors.NoMessageSupplied()
114
105
email_message.EmailMessage.send(self.config,
115
self.config.get('email'),
106
self.config.username(),
120
111
attachment_mime_subtype=mime_subtype)
121
mail_client_registry.register('editor', Editor,
125
class BodyExternalMailClient(MailClient):
114
class ExternalMailClient(MailClient):
115
"""An external mail client."""
129
117
def _get_client_commands(self):
130
118
"""Provide a list of commands that may invoke the mail client"""
135
123
return self._client_commands
137
125
def compose(self, prompt, to, subject, attachment, mime_subtype,
138
extension, basename=None, body=None):
126
extension, basename=None):
139
127
"""See MailClient.compose.
141
129
Writes the attachment to a temporary file, invokes _compose.
143
131
if basename is None:
144
132
basename = 'attachment'
145
pathname = osutils.mkdtemp(prefix='bzr-mail-')
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
146
134
attach_path = osutils.pathjoin(pathname, basename + extension)
147
135
outfile = open(attach_path, 'wb')
149
137
outfile.write(attachment)
153
kwargs = {'body': body}
156
140
self._compose(prompt, to, subject, attach_path, mime_subtype,
159
143
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
160
extension, body=None, from_=None):
161
145
"""Invoke a mail client as a commandline process.
163
147
Overridden by MAPIClient.
226
201
if isinstance(path, unicode):
228
return path.encode(osutils.get_user_encoding())
203
return path.encode(bzrlib.user_encoding)
229
204
except UnicodeEncodeError:
230
205
raise errors.UnableEncodePath(path, kind)
234
class ExternalMailClient(BodyExternalMailClient):
235
__doc__ = """An external mail client."""
237
supports_body = False
240
class Evolution(BodyExternalMailClient):
241
__doc__ = """Evolution mail client."""
209
class Evolution(ExternalMailClient):
210
"""Evolution mail client."""
243
212
_client_commands = ['evolution']
245
def _get_compose_commandline(self, to, subject, attach_path, body=None):
214
def _get_compose_commandline(self, to, subject, attach_path):
246
215
"""See ExternalMailClient._get_compose_commandline"""
247
216
message_options = {}
248
217
if subject is not None:
249
218
message_options['subject'] = subject
250
219
if attach_path is not None:
251
220
message_options['attach'] = attach_path
253
message_options['body'] = body
254
221
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
255
222
sorted(message_options.iteritems())]
256
223
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
257
224
'&'.join(options_list))]
258
mail_client_registry.register('evolution', Evolution,
259
help=Evolution.__doc__)
262
class Mutt(BodyExternalMailClient):
263
__doc__ = """Mutt mail client."""
227
class Mutt(ExternalMailClient):
228
"""Mutt mail client."""
265
230
_client_commands = ['mutt']
267
def _get_compose_commandline(self, to, subject, attach_path, body=None):
232
def _get_compose_commandline(self, to, subject, attach_path):
268
233
"""See ExternalMailClient._get_compose_commandline"""
269
234
message_options = []
270
235
if subject is not None:
272
237
if attach_path is not None:
273
238
message_options.extend(['-a',
274
239
self._encode_path(attach_path, 'attachment')])
276
# Store the temp file object in self, so that it does not get
277
# garbage collected and delete the file before mutt can read it.
278
self._temp_file = tempfile.NamedTemporaryFile(
279
prefix="mutt-body-", suffix=".txt")
280
self._temp_file.write(body)
281
self._temp_file.flush()
282
message_options.extend(['-i', self._temp_file.name])
283
240
if to is not None:
284
message_options.extend(['--', self._encode_safe(to)])
241
message_options.append(self._encode_safe(to))
285
242
return message_options
286
mail_client_registry.register('mutt', Mutt,
290
class Thunderbird(BodyExternalMailClient):
291
__doc__ = """Mozilla Thunderbird (or Icedove)
245
class Thunderbird(ExternalMailClient):
246
"""Mozilla Thunderbird (or Icedove)
293
248
Note that Thunderbird 1.5 is buggy and does not support setting
294
249
"to" simultaneously with including a attachment.
311
265
if attach_path is not None:
312
266
message_options['attachment'] = urlutils.local_path_to_url(
315
options_list = ['body=%s' % urlutils.quote(self._encode_safe(body))]
318
options_list.extend(["%s='%s'" % (k, v) for k, v in
319
sorted(message_options.iteritems())])
268
options_list = ["%s='%s'" % (k, v) for k, v in
269
sorted(message_options.iteritems())]
320
270
return ['-compose', ','.join(options_list)]
321
mail_client_registry.register('thunderbird', Thunderbird,
322
help=Thunderbird.__doc__)
325
273
class KMail(ExternalMailClient):
326
__doc__ = """KDE mail client."""
274
"""KDE mail client."""
328
276
_client_commands = ['kmail']
338
286
if to is not None:
339
287
message_options.extend([self._encode_safe(to)])
340
288
return message_options
341
mail_client_registry.register('kmail', KMail,
345
class Claws(ExternalMailClient):
346
__doc__ = """Claws mail client."""
350
_client_commands = ['claws-mail']
352
def _get_compose_commandline(self, to, subject, attach_path, body=None,
354
"""See ExternalMailClient._get_compose_commandline"""
356
if from_ is not None:
357
compose_url.append('from=' + urlutils.quote(from_))
358
if subject is not None:
359
# Don't use urlutils.quote_plus because Claws doesn't seem
360
# to recognise spaces encoded as "+".
362
'subject=' + urlutils.quote(self._encode_safe(subject)))
365
'body=' + urlutils.quote(self._encode_safe(body)))
366
# to must be supplied for the claws-mail --compose syntax to work.
368
raise errors.NoMailAddressSpecified()
369
compose_url = 'mailto:%s?%s' % (
370
self._encode_safe(to), '&'.join(compose_url))
371
# Collect command-line options.
372
message_options = ['--compose', compose_url]
373
if attach_path is not None:
374
message_options.extend(
375
['--attach', self._encode_path(attach_path, 'attachment')])
376
return message_options
378
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
379
extension, body=None, from_=None):
380
"""See ExternalMailClient._compose"""
382
from_ = self.config.get('email')
383
super(Claws, self)._compose(prompt, to, subject, attach_path,
384
mime_subtype, extension, body, from_)
387
mail_client_registry.register('claws', Claws,
391
class XDGEmail(BodyExternalMailClient):
392
__doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
291
class XDGEmail(ExternalMailClient):
292
"""xdg-email attempts to invoke the user's preferred mail client"""
394
294
_client_commands = ['xdg-email']
396
def _get_compose_commandline(self, to, subject, attach_path, body=None):
296
def _get_compose_commandline(self, to, subject, attach_path):
397
297
"""See ExternalMailClient._get_compose_commandline"""
399
299
raise errors.NoMailAddressSpecified()
427
323
_client_commands = ['emacsclient']
429
def __init__(self, config):
430
super(EmacsMail, self).__init__(config)
431
self.elisp_tmp_file = None
433
325
def _prepare_send_function(self):
434
326
"""Write our wrapper function into a temporary file.
436
328
This temporary file will be loaded at runtime in
437
329
_get_compose_commandline function.
439
This function does not remove the file. That's a wanted
331
FIXME: this function does not remove the file. That's a wanted
440
332
behaviour since _get_compose_commandline won't run the send
441
333
mail function directly but return the eligible command line.
442
334
Removing our temporary file here would prevent our sendmail
443
function to work. (The file is deleted by some elisp code
444
after being read by Emacs.)
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
447
343
_defun = r"""(defun bzr-add-mime-att (file)
448
"Attach FILE to a mail buffer as a MIME attachment."
344
"Attach FILe to a mail buffer as a MIME attachment."
449
345
(let ((agent mail-user-agent))
450
348
(if (and file (file-exists-p file))
452
350
((eq agent 'sendmail-user-agent)
456
(if (functionp 'etach-attach)
458
(mail-attach-file file))))
459
((or (eq agent 'message-user-agent)
460
(eq agent 'gnus-user-agent)
461
(eq agent 'mh-e-user-agent))
463
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
464
((eq agent 'mew-user-agent)
466
(mew-draft-prepare-attachments)
467
(mew-attach-link file (file-name-nondirectory file))
468
(let* ((nums (mew-syntax-nums))
469
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
470
(mew-syntax-set-cd syntax "BZR merge")
471
(mew-encode-syntax-print mew-encode-syntax))
472
(mew-header-goto-body)))
352
((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
353
(mml-attach-file file "text/x-patch" "BZR merge" "attachment"))
474
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
355
(message "Unhandled MUA")))
475
356
(error "File %s does not exist." file))))
505
385
# Try to attach a MIME attachment using our wrapper function
506
386
if attach_path is not None:
507
387
# Do not create a file if there is no attachment
508
elisp = self._prepare_send_function()
509
self.elisp_tmp_file = elisp
510
lmmform = '(load "%s")' % elisp
388
lmmform = '(load "%s")' % self._prepare_send_function()
511
389
mmform = '(bzr-add-mime-att "%s")' % \
512
390
self._encode_path(attach_path, 'attachment')
513
rmform = '(delete-file "%s")' % elisp
514
391
commandline.append(lmmform)
515
392
commandline.append(mmform)
516
commandline.append(rmform)
518
394
return commandline
519
mail_client_registry.register('emacsclient', EmacsMail,
520
help=EmacsMail.__doc__)
523
class MAPIClient(BodyExternalMailClient):
524
__doc__ = """Default Windows mail client launched using MAPI."""
397
class MAPIClient(ExternalMailClient):
398
"""Default Windows mail client launched using MAPI."""
526
400
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
527
extension, body=None):
528
402
"""See ExternalMailClient._compose.
530
404
This implementation uses MAPI via the simplemapi ctypes wrapper
532
406
from bzrlib.util import simplemapi
534
simplemapi.SendMail(to or '', subject or '', body or '',
408
simplemapi.SendMail(to or '', subject or '', '', attach_path)
536
409
except simplemapi.MAPIError, e:
537
410
if e.code != simplemapi.MAPI_USER_ABORT:
538
411
raise errors.MailClientNotFound(['MAPI supported mail client'
539
412
' (error %d)' % (e.code,)])
540
mail_client_registry.register('mapi', MAPIClient,
541
help=MAPIClient.__doc__)
544
class MailApp(BodyExternalMailClient):
545
__doc__ = """Use MacOS X's Mail.app for sending email messages.
547
Although it would be nice to use appscript, it's not installed
548
with the shipped Python installations. We instead build an
549
AppleScript and invoke the script using osascript(1). We don't
550
use the _encode_safe() routines as it's not clear what encoding
551
osascript expects the script to be in.
554
_client_commands = ['osascript']
556
def _get_compose_commandline(self, to, subject, attach_path, body=None,
558
"""See ExternalMailClient._get_compose_commandline"""
560
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
563
os.write(fd, 'tell application "Mail"\n')
564
os.write(fd, 'set newMessage to make new outgoing message\n')
565
os.write(fd, 'tell newMessage\n')
567
os.write(fd, 'make new to recipient with properties'
568
' {address:"%s"}\n' % to)
569
if from_ is not None:
570
# though from_ doesn't actually seem to be used
571
os.write(fd, 'set sender to "%s"\n'
572
% sender.replace('"', '\\"'))
573
if subject is not None:
574
os.write(fd, 'set subject to "%s"\n'
575
% subject.replace('"', '\\"'))
577
# FIXME: would be nice to prepend the body to the
578
# existing content (e.g., preserve signature), but
579
# can't seem to figure out the right applescript
581
os.write(fd, 'set content to "%s\\n\n"\n' %
582
body.replace('"', '\\"').replace('\n', '\\n'))
584
if attach_path is not None:
585
# FIXME: would be nice to first append a newline to
586
# ensure the attachment is on a new paragraph, but
587
# can't seem to figure out the right applescript
589
os.write(fd, 'tell content to make new attachment'
590
' with properties {file name:"%s"}'
591
' at after the last paragraph\n'
592
% self._encode_path(attach_path, 'attachment'))
593
os.write(fd, 'set visible to true\n')
594
os.write(fd, 'end tell\n')
595
os.write(fd, 'end tell\n')
597
os.close(fd) # Just close the handle but do not remove the file.
598
return [self.temp_file]
599
mail_client_registry.register('mail.app', MailApp,
600
help=MailApp.__doc__)
603
415
class DefaultMail(MailClient):
604
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
416
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
605
417
falls back to Editor"""
609
419
def _mail_client(self):
610
420
"""Determine the preferred mail client for this platform"""
611
421
if osutils.supports_mapi():
614
424
return XDGEmail(self.config)
616
426
def compose(self, prompt, to, subject, attachment, mime_subtype,
617
extension, basename=None, body=None):
427
extension, basename=None):
618
428
"""See MailClient.compose"""
620
430
return self._mail_client().compose(prompt, to, subject,
621
attachment, mime_subtype,
622
extension, basename, body)
431
attachment, mimie_subtype,
623
433
except errors.MailClientNotFound:
624
434
return Editor(self.config).compose(prompt, to, subject,
625
attachment, mime_subtype, extension, body)
435
attachment, mimie_subtype, extension)
627
def compose_merge_request(self, to, subject, directive, basename=None,
437
def compose_merge_request(self, to, subject, directive, basename=None):
629
438
"""See MailClient.compose_merge_request"""
631
440
return self._mail_client().compose_merge_request(to, subject,
632
directive, basename=basename, body=body)
441
directive, basename=basename)
633
442
except errors.MailClientNotFound:
634
443
return Editor(self.config).compose_merge_request(to, subject,
635
directive, basename=basename, body=body)
636
mail_client_registry.register('default', DefaultMail,
637
help=DefaultMail.__doc__)
638
mail_client_registry.default_key = 'default'
640
opt_mail_client = _mod_config.RegistryOption('mail_client',
641
mail_client_registry, help='E-mail client to use.', invalid='error')
444
directive, basename=basename)