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.
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):
210
"""Evolution mail client."""
232
class ExternalMailClient(BodyExternalMailClient):
233
__doc__ = """An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
239
__doc__ = """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):
228
"""Mutt mail client."""
256
mail_client_registry.register('evolution', Evolution,
257
help=Evolution.__doc__)
260
class Mutt(BodyExternalMailClient):
261
__doc__ = """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):
246
"""Mozilla Thunderbird (or Icedove)
284
mail_client_registry.register('mutt', Mutt,
288
class Thunderbird(BodyExternalMailClient):
289
__doc__ = """Mozilla Thunderbird (or Icedove)
248
291
Note that Thunderbird 1.5 is buggy and does not support setting
249
292
"to" simultaneously with including a attachment.
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):
274
"""KDE mail client."""
324
__doc__ = """KDE mail client."""
276
326
_client_commands = ['kmail']
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):
292
"""xdg-email attempts to invoke the user's preferred mail client"""
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"""
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()
323
425
_client_commands = ['emacsclient']
427
def __init__(self, config):
428
super(EmacsMail, self).__init__(config)
429
self.elisp_tmp_file = None
325
431
def _prepare_send_function(self):
326
432
"""Write our wrapper function into a temporary file.
328
434
This temporary file will be loaded at runtime in
329
435
_get_compose_commandline function.
331
FIXME: this function does not remove the file. That's a wanted
437
This function does not remove the file. That's a wanted
332
438
behaviour since _get_compose_commandline won't run the send
333
439
mail function directly but return the eligible command line.
334
440
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
441
function to work. (The file is deleted by some elisp code
442
after being read by Emacs.)
343
445
_defun = r"""(defun bzr-add-mime-att (file)
344
"Attach FILe to a mail buffer as a MIME attachment."
446
"Attach FILE to a mail buffer as a MIME attachment."
345
447
(let ((agent mail-user-agent))
348
448
(if (and file (file-exists-p file))
350
450
((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"))
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)))
355
(message "Unhandled MUA")))
472
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
356
473
(error "File %s does not exist." file))))
385
503
# Try to attach a MIME attachment using our wrapper function
386
504
if attach_path is not None:
387
505
# Do not create a file if there is no attachment
388
lmmform = '(load "%s")' % self._prepare_send_function()
506
elisp = self._prepare_send_function()
507
self.elisp_tmp_file = elisp
508
lmmform = '(load "%s")' % elisp
389
509
mmform = '(bzr-add-mime-att "%s")' % \
390
510
self._encode_path(attach_path, 'attachment')
511
rmform = '(delete-file "%s")' % elisp
391
512
commandline.append(lmmform)
392
513
commandline.append(mmform)
514
commandline.append(rmform)
394
516
return commandline
397
class MAPIClient(ExternalMailClient):
398
"""Default Windows mail client launched using MAPI."""
517
mail_client_registry.register('emacsclient', EmacsMail,
518
help=EmacsMail.__doc__)
521
class MAPIClient(BodyExternalMailClient):
522
__doc__ = """Default Windows mail client launched using MAPI."""
400
524
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
extension, body=None):
402
526
"""See ExternalMailClient._compose.
404
528
This implementation uses MAPI via the simplemapi ctypes wrapper
406
530
from bzrlib.util import simplemapi
408
simplemapi.SendMail(to or '', subject or '', '', attach_path)
532
simplemapi.SendMail(to or '', subject or '', body or '',
409
534
except simplemapi.MAPIError, e:
410
535
if e.code != simplemapi.MAPI_USER_ABORT:
411
536
raise errors.MailClientNotFound(['MAPI supported mail client'
412
537
' (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__)
415
601
class DefaultMail(MailClient):
416
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
602
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
417
603
falls back to Editor"""
419
607
def _mail_client(self):
420
608
"""Determine the preferred mail client for this platform"""
421
609
if osutils.supports_mapi():
424
612
return XDGEmail(self.config)
426
614
def compose(self, prompt, to, subject, attachment, mime_subtype,
427
extension, basename=None):
615
extension, basename=None, body=None):
428
616
"""See MailClient.compose"""
430
618
return self._mail_client().compose(prompt, to, subject,
431
619
attachment, mimie_subtype,
620
extension, basename, body)
433
621
except errors.MailClientNotFound:
434
622
return Editor(self.config).compose(prompt, to, subject,
435
attachment, mimie_subtype, extension)
623
attachment, mimie_subtype, extension, body)
437
def compose_merge_request(self, to, subject, directive, basename=None):
625
def compose_merge_request(self, to, subject, directive, basename=None,
438
627
"""See MailClient.compose_merge_request"""
440
629
return self._mail_client().compose_merge_request(to, subject,
441
directive, basename=basename)
630
directive, basename=basename, body=body)
442
631
except errors.MailClientNotFound:
443
632
return Editor(self.config).compose_merge_request(to, subject,
444
directive, basename=basename)
633
directive, basename=basename, body=body)
634
mail_client_registry.register('default', DefaultMail,
635
help=DefaultMail.__doc__)
636
mail_client_registry.default_key = 'default'