133
123
return self._client_commands
135
125
def compose(self, prompt, to, subject, attachment, mime_subtype,
136
extension, basename=None, body=None):
126
extension, basename=None):
137
127
"""See MailClient.compose.
139
129
Writes the attachment to a temporary file, invokes _compose.
141
131
if basename is None:
142
132
basename = 'attachment'
143
pathname = osutils.mkdtemp(prefix='bzr-mail-')
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
144
134
attach_path = osutils.pathjoin(pathname, basename + extension)
145
135
outfile = open(attach_path, 'wb')
147
137
outfile.write(attachment)
151
kwargs = {'body': body}
154
140
self._compose(prompt, to, subject, attach_path, mime_subtype,
157
143
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
extension, body=None, from_=None):
159
145
"""Invoke a mail client as a commandline process.
161
147
Overridden by MAPIClient.
224
201
if isinstance(path, unicode):
226
return path.encode(osutils.get_user_encoding())
203
return path.encode(bzrlib.user_encoding)
227
204
except UnicodeEncodeError:
228
205
raise errors.UnableEncodePath(path, kind)
232
class ExternalMailClient(BodyExternalMailClient):
233
__doc__ = """An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
239
__doc__ = """Evolution mail client."""
209
class Evolution(ExternalMailClient):
210
"""Evolution mail client."""
241
212
_client_commands = ['evolution']
243
def _get_compose_commandline(self, to, subject, attach_path, body=None):
214
def _get_compose_commandline(self, to, subject, attach_path):
244
215
"""See ExternalMailClient._get_compose_commandline"""
245
216
message_options = {}
246
217
if subject is not None:
247
218
message_options['subject'] = subject
248
219
if attach_path is not None:
249
220
message_options['attach'] = attach_path
251
message_options['body'] = body
252
221
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
222
sorted(message_options.iteritems())]
254
223
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
255
224
'&'.join(options_list))]
256
mail_client_registry.register('evolution', Evolution,
257
help=Evolution.__doc__)
260
class Mutt(BodyExternalMailClient):
261
__doc__ = """Mutt mail client."""
227
class Mutt(ExternalMailClient):
228
"""Mutt mail client."""
263
230
_client_commands = ['mutt']
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
232
def _get_compose_commandline(self, to, subject, attach_path):
266
233
"""See ExternalMailClient._get_compose_commandline"""
267
234
message_options = []
268
235
if subject is not None:
270
237
if attach_path is not None:
271
238
message_options.extend(['-a',
272
239
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])
281
240
if to is not None:
282
message_options.extend(['--', self._encode_safe(to)])
241
message_options.append(self._encode_safe(to))
283
242
return message_options
284
mail_client_registry.register('mutt', Mutt,
288
class Thunderbird(BodyExternalMailClient):
289
__doc__ = """Mozilla Thunderbird (or Icedove)
245
class Thunderbird(ExternalMailClient):
246
"""Mozilla Thunderbird (or Icedove)
291
248
Note that Thunderbird 1.5 is buggy and does not support setting
292
249
"to" simultaneously with including a attachment.
309
265
if attach_path is not None:
310
266
message_options['attachment'] = urlutils.local_path_to_url(
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())])
268
options_list = ["%s='%s'" % (k, v) for k, v in
269
sorted(message_options.iteritems())]
318
270
return ['-compose', ','.join(options_list)]
319
mail_client_registry.register('thunderbird', Thunderbird,
320
help=Thunderbird.__doc__)
323
273
class KMail(ExternalMailClient):
324
__doc__ = """KDE mail client."""
274
"""KDE mail client."""
326
276
_client_commands = ['kmail']
336
286
if to is not None:
337
287
message_options.extend([self._encode_safe(to)])
338
288
return message_options
339
mail_client_registry.register('kmail', KMail,
343
class Claws(ExternalMailClient):
344
__doc__ = """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):
390
__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"""
392
294
_client_commands = ['xdg-email']
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
296
def _get_compose_commandline(self, to, subject, attach_path):
395
297
"""See ExternalMailClient._get_compose_commandline"""
397
299
raise errors.NoMailAddressSpecified()
425
323
_client_commands = ['emacsclient']
427
def __init__(self, config):
428
super(EmacsMail, self).__init__(config)
429
self.elisp_tmp_file = None
431
325
def _prepare_send_function(self):
432
326
"""Write our wrapper function into a temporary file.
434
328
This temporary file will be loaded at runtime in
435
329
_get_compose_commandline function.
437
This function does not remove the file. That's a wanted
331
FIXME: this function does not remove the file. That's a wanted
438
332
behaviour since _get_compose_commandline won't run the send
439
333
mail function directly but return the eligible command line.
440
334
Removing our temporary file here would prevent our sendmail
441
function to work. (The file is deleted by some elisp code
442
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
445
343
_defun = r"""(defun bzr-add-mime-att (file)
446
"Attach FILE to a mail buffer as a MIME attachment."
344
"Attach FILe to a mail buffer as a MIME attachment."
447
345
(let ((agent mail-user-agent))
448
348
(if (and file (file-exists-p file))
450
350
((eq agent 'sendmail-user-agent)
454
(if (functionp 'etach-attach)
456
(mail-attach-file file))))
457
((or (eq agent 'message-user-agent)
458
(eq agent 'gnus-user-agent)
459
(eq agent 'mh-e-user-agent))
461
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
462
((eq agent 'mew-user-agent)
464
(mew-draft-prepare-attachments)
465
(mew-attach-link file (file-name-nondirectory file))
466
(let* ((nums (mew-syntax-nums))
467
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
468
(mew-syntax-set-cd syntax "BZR merge")
469
(mew-encode-syntax-print mew-encode-syntax))
470
(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"))
472
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
355
(message "Unhandled MUA")))
473
356
(error "File %s does not exist." file))))
503
385
# Try to attach a MIME attachment using our wrapper function
504
386
if attach_path is not None:
505
387
# Do not create a file if there is no attachment
506
elisp = self._prepare_send_function()
507
self.elisp_tmp_file = elisp
508
lmmform = '(load "%s")' % elisp
388
lmmform = '(load "%s")' % self._prepare_send_function()
509
389
mmform = '(bzr-add-mime-att "%s")' % \
510
390
self._encode_path(attach_path, 'attachment')
511
rmform = '(delete-file "%s")' % elisp
512
391
commandline.append(lmmform)
513
392
commandline.append(mmform)
514
commandline.append(rmform)
516
394
return commandline
517
mail_client_registry.register('emacsclient', EmacsMail,
518
help=EmacsMail.__doc__)
521
class MAPIClient(BodyExternalMailClient):
522
__doc__ = """Default Windows mail client launched using MAPI."""
397
class MAPIClient(ExternalMailClient):
398
"""Default Windows mail client launched using MAPI."""
524
400
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
extension, body=None):
526
402
"""See ExternalMailClient._compose.
528
404
This implementation uses MAPI via the simplemapi ctypes wrapper
530
406
from bzrlib.util import simplemapi
532
simplemapi.SendMail(to or '', subject or '', body or '',
408
simplemapi.SendMail(to or '', subject or '', '', attach_path)
534
409
except simplemapi.MAPIError, e:
535
410
if e.code != simplemapi.MAPI_USER_ABORT:
536
411
raise errors.MailClientNotFound(['MAPI supported mail client'
537
412
' (error %d)' % (e.code,)])
538
mail_client_registry.register('mapi', MAPIClient,
539
help=MAPIClient.__doc__)
542
class MailApp(BodyExternalMailClient):
543
__doc__ = """Use MacOS X's Mail.app for sending email messages.
545
Although it would be nice to use appscript, it's not installed
546
with the shipped Python installations. We instead build an
547
AppleScript and invoke the script using osascript(1). We don't
548
use the _encode_safe() routines as it's not clear what encoding
549
osascript expects the script to be in.
552
_client_commands = ['osascript']
554
def _get_compose_commandline(self, to, subject, attach_path, body=None,
556
"""See ExternalMailClient._get_compose_commandline"""
558
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
561
os.write(fd, 'tell application "Mail"\n')
562
os.write(fd, 'set newMessage to make new outgoing message\n')
563
os.write(fd, 'tell newMessage\n')
565
os.write(fd, 'make new to recipient with properties'
566
' {address:"%s"}\n' % to)
567
if from_ is not None:
568
# though from_ doesn't actually seem to be used
569
os.write(fd, 'set sender to "%s"\n'
570
% sender.replace('"', '\\"'))
571
if subject is not None:
572
os.write(fd, 'set subject to "%s"\n'
573
% subject.replace('"', '\\"'))
575
# FIXME: would be nice to prepend the body to the
576
# existing content (e.g., preserve signature), but
577
# can't seem to figure out the right applescript
579
os.write(fd, 'set content to "%s\\n\n"\n' %
580
body.replace('"', '\\"').replace('\n', '\\n'))
582
if attach_path is not None:
583
# FIXME: would be nice to first append a newline to
584
# ensure the attachment is on a new paragraph, but
585
# can't seem to figure out the right applescript
587
os.write(fd, 'tell content to make new attachment'
588
' with properties {file name:"%s"}'
589
' at after the last paragraph\n'
590
% self._encode_path(attach_path, 'attachment'))
591
os.write(fd, 'set visible to true\n')
592
os.write(fd, 'end tell\n')
593
os.write(fd, 'end tell\n')
595
os.close(fd) # Just close the handle but do not remove the file.
596
return [self.temp_file]
597
mail_client_registry.register('mail.app', MailApp,
598
help=MailApp.__doc__)
601
415
class DefaultMail(MailClient):
602
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
416
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
603
417
falls back to Editor"""
607
419
def _mail_client(self):
608
420
"""Determine the preferred mail client for this platform"""
609
421
if osutils.supports_mapi():
612
424
return XDGEmail(self.config)
614
426
def compose(self, prompt, to, subject, attachment, mime_subtype,
615
extension, basename=None, body=None):
427
extension, basename=None):
616
428
"""See MailClient.compose"""
618
430
return self._mail_client().compose(prompt, to, subject,
619
431
attachment, mimie_subtype,
620
extension, basename, body)
621
433
except errors.MailClientNotFound:
622
434
return Editor(self.config).compose(prompt, to, subject,
623
attachment, mimie_subtype, extension, body)
435
attachment, mimie_subtype, extension)
625
def compose_merge_request(self, to, subject, directive, basename=None,
437
def compose_merge_request(self, to, subject, directive, basename=None):
627
438
"""See MailClient.compose_merge_request"""
629
440
return self._mail_client().compose_merge_request(to, subject,
630
directive, basename=basename, body=body)
441
directive, basename=basename)
631
442
except errors.MailClientNotFound:
632
443
return Editor(self.config).compose_merge_request(to, subject,
633
directive, basename=basename, body=body)
634
mail_client_registry.register('default', DefaultMail,
635
help=DefaultMail.__doc__)
636
mail_client_registry.default_key = 'default'
444
directive, basename=basename)