123
134
return self._client_commands
125
136
def compose(self, prompt, to, subject, attachment, mime_subtype,
126
extension, basename=None):
137
extension, basename=None, body=None):
127
138
"""See MailClient.compose.
129
140
Writes the attachment to a temporary file, invokes _compose.
131
142
if basename is None:
132
143
basename = 'attachment'
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
144
pathname = osutils.mkdtemp(prefix='bzr-mail-')
134
145
attach_path = osutils.pathjoin(pathname, basename + extension)
135
146
outfile = open(attach_path, 'wb')
137
148
outfile.write(attachment)
152
kwargs = {'body': body}
140
155
self._compose(prompt, to, subject, attach_path, mime_subtype,
143
158
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
159
extension, body=None, from_=None):
145
160
"""Invoke a mail client as a commandline process.
147
162
Overridden by MAPIClient.
201
225
if isinstance(path, unicode):
203
return path.encode(bzrlib.user_encoding)
227
return path.encode(osutils.get_user_encoding())
204
228
except UnicodeEncodeError:
205
229
raise errors.UnableEncodePath(path, kind)
209
class Evolution(ExternalMailClient):
210
"""Evolution mail client."""
233
class ExternalMailClient(BodyExternalMailClient):
234
__doc__ = """An external mail client."""
236
supports_body = False
239
class Evolution(BodyExternalMailClient):
240
__doc__ = """Evolution mail client."""
212
242
_client_commands = ['evolution']
214
def _get_compose_commandline(self, to, subject, attach_path):
244
def _get_compose_commandline(self, to, subject, attach_path, body=None):
215
245
"""See ExternalMailClient._get_compose_commandline"""
216
246
message_options = {}
217
247
if subject is not None:
218
248
message_options['subject'] = subject
219
249
if attach_path is not None:
220
250
message_options['attach'] = attach_path
252
message_options['body'] = body
221
253
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
222
254
sorted(message_options.iteritems())]
223
255
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
224
256
'&'.join(options_list))]
227
class Mutt(ExternalMailClient):
228
"""Mutt mail client."""
257
mail_client_registry.register('evolution', Evolution,
258
help=Evolution.__doc__)
261
class Mutt(BodyExternalMailClient):
262
__doc__ = """Mutt mail client."""
230
264
_client_commands = ['mutt']
232
def _get_compose_commandline(self, to, subject, attach_path):
266
def _get_compose_commandline(self, to, subject, attach_path, body=None):
233
267
"""See ExternalMailClient._get_compose_commandline"""
234
268
message_options = []
235
269
if subject is not None:
237
271
if attach_path is not None:
238
272
message_options.extend(['-a',
239
273
self._encode_path(attach_path, 'attachment')])
275
# Store the temp file object in self, so that it does not get
276
# garbage collected and delete the file before mutt can read it.
277
self._temp_file = tempfile.NamedTemporaryFile(
278
prefix="mutt-body-", suffix=".txt")
279
self._temp_file.write(body)
280
self._temp_file.flush()
281
message_options.extend(['-i', self._temp_file.name])
240
282
if to is not None:
241
message_options.append(self._encode_safe(to))
283
message_options.extend(['--', self._encode_safe(to)])
242
284
return message_options
245
class Thunderbird(ExternalMailClient):
246
"""Mozilla Thunderbird (or Icedove)
285
mail_client_registry.register('mutt', Mutt,
289
class Thunderbird(BodyExternalMailClient):
290
__doc__ = """Mozilla Thunderbird (or Icedove)
248
292
Note that Thunderbird 1.5 is buggy and does not support setting
249
293
"to" simultaneously with including a attachment.
265
310
if attach_path is not None:
266
311
message_options['attachment'] = urlutils.local_path_to_url(
268
options_list = ["%s='%s'" % (k, v) for k, v in
269
sorted(message_options.iteritems())]
314
options_list = ['body=%s' % urlutils.quote(self._encode_safe(body))]
317
options_list.extend(["%s='%s'" % (k, v) for k, v in
318
sorted(message_options.iteritems())])
270
319
return ['-compose', ','.join(options_list)]
320
mail_client_registry.register('thunderbird', Thunderbird,
321
help=Thunderbird.__doc__)
273
324
class KMail(ExternalMailClient):
274
"""KDE mail client."""
325
__doc__ = """KDE mail client."""
276
327
_client_commands = ['kmail']
286
337
if to is not None:
287
338
message_options.extend([self._encode_safe(to)])
288
339
return message_options
291
class XDGEmail(ExternalMailClient):
292
"""xdg-email attempts to invoke the user's preferred mail client"""
340
mail_client_registry.register('kmail', KMail,
344
class Claws(ExternalMailClient):
345
__doc__ = """Claws mail client."""
349
_client_commands = ['claws-mail']
351
def _get_compose_commandline(self, to, subject, attach_path, body=None,
353
"""See ExternalMailClient._get_compose_commandline"""
355
if from_ is not None:
356
compose_url.append('from=' + urlutils.quote(from_))
357
if subject is not None:
358
# Don't use urlutils.quote_plus because Claws doesn't seem
359
# to recognise spaces encoded as "+".
361
'subject=' + urlutils.quote(self._encode_safe(subject)))
364
'body=' + urlutils.quote(self._encode_safe(body)))
365
# to must be supplied for the claws-mail --compose syntax to work.
367
raise errors.NoMailAddressSpecified()
368
compose_url = 'mailto:%s?%s' % (
369
self._encode_safe(to), '&'.join(compose_url))
370
# Collect command-line options.
371
message_options = ['--compose', compose_url]
372
if attach_path is not None:
373
message_options.extend(
374
['--attach', self._encode_path(attach_path, 'attachment')])
375
return message_options
377
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
378
extension, body=None, from_=None):
379
"""See ExternalMailClient._compose"""
381
from_ = self.config.get_user_option('email')
382
super(Claws, self)._compose(prompt, to, subject, attach_path,
383
mime_subtype, extension, body, from_)
386
mail_client_registry.register('claws', Claws,
390
class XDGEmail(BodyExternalMailClient):
391
__doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
294
393
_client_commands = ['xdg-email']
296
def _get_compose_commandline(self, to, subject, attach_path):
395
def _get_compose_commandline(self, to, subject, attach_path, body=None):
297
396
"""See ExternalMailClient._get_compose_commandline"""
299
398
raise errors.NoMailAddressSpecified()
323
426
_client_commands = ['emacsclient']
428
def __init__(self, config):
429
super(EmacsMail, self).__init__(config)
430
self.elisp_tmp_file = None
325
432
def _prepare_send_function(self):
326
433
"""Write our wrapper function into a temporary file.
328
435
This temporary file will be loaded at runtime in
329
436
_get_compose_commandline function.
331
FIXME: this function does not remove the file. That's a wanted
438
This function does not remove the file. That's a wanted
332
439
behaviour since _get_compose_commandline won't run the send
333
440
mail function directly but return the eligible command line.
334
441
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
442
function to work. (The file is deleted by some elisp code
443
after being read by Emacs.)
343
446
_defun = r"""(defun bzr-add-mime-att (file)
344
"Attach FILe to a mail buffer as a MIME attachment."
447
"Attach FILE to a mail buffer as a MIME attachment."
345
448
(let ((agent mail-user-agent))
348
449
(if (and file (file-exists-p file))
350
451
((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"))
455
(if (functionp 'etach-attach)
457
(mail-attach-file file))))
458
((or (eq agent 'message-user-agent)
459
(eq agent 'gnus-user-agent)
460
(eq agent 'mh-e-user-agent))
462
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
463
((eq agent 'mew-user-agent)
465
(mew-draft-prepare-attachments)
466
(mew-attach-link file (file-name-nondirectory file))
467
(let* ((nums (mew-syntax-nums))
468
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
469
(mew-syntax-set-cd syntax "BZR merge")
470
(mew-encode-syntax-print mew-encode-syntax))
471
(mew-header-goto-body)))
355
(message "Unhandled MUA")))
473
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
356
474
(error "File %s does not exist." file))))
385
504
# Try to attach a MIME attachment using our wrapper function
386
505
if attach_path is not None:
387
506
# Do not create a file if there is no attachment
388
lmmform = '(load "%s")' % self._prepare_send_function()
507
elisp = self._prepare_send_function()
508
self.elisp_tmp_file = elisp
509
lmmform = '(load "%s")' % elisp
389
510
mmform = '(bzr-add-mime-att "%s")' % \
390
511
self._encode_path(attach_path, 'attachment')
512
rmform = '(delete-file "%s")' % elisp
391
513
commandline.append(lmmform)
392
514
commandline.append(mmform)
515
commandline.append(rmform)
394
517
return commandline
397
class MAPIClient(ExternalMailClient):
398
"""Default Windows mail client launched using MAPI."""
518
mail_client_registry.register('emacsclient', EmacsMail,
519
help=EmacsMail.__doc__)
522
class MAPIClient(BodyExternalMailClient):
523
__doc__ = """Default Windows mail client launched using MAPI."""
400
525
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
526
extension, body=None):
402
527
"""See ExternalMailClient._compose.
404
529
This implementation uses MAPI via the simplemapi ctypes wrapper
406
531
from bzrlib.util import simplemapi
408
simplemapi.SendMail(to or '', subject or '', '', attach_path)
533
simplemapi.SendMail(to or '', subject or '', body or '',
409
535
except simplemapi.MAPIError, e:
410
536
if e.code != simplemapi.MAPI_USER_ABORT:
411
537
raise errors.MailClientNotFound(['MAPI supported mail client'
412
538
' (error %d)' % (e.code,)])
539
mail_client_registry.register('mapi', MAPIClient,
540
help=MAPIClient.__doc__)
543
class MailApp(BodyExternalMailClient):
544
__doc__ = """Use MacOS X's Mail.app for sending email messages.
546
Although it would be nice to use appscript, it's not installed
547
with the shipped Python installations. We instead build an
548
AppleScript and invoke the script using osascript(1). We don't
549
use the _encode_safe() routines as it's not clear what encoding
550
osascript expects the script to be in.
553
_client_commands = ['osascript']
555
def _get_compose_commandline(self, to, subject, attach_path, body=None,
557
"""See ExternalMailClient._get_compose_commandline"""
559
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
562
os.write(fd, 'tell application "Mail"\n')
563
os.write(fd, 'set newMessage to make new outgoing message\n')
564
os.write(fd, 'tell newMessage\n')
566
os.write(fd, 'make new to recipient with properties'
567
' {address:"%s"}\n' % to)
568
if from_ is not None:
569
# though from_ doesn't actually seem to be used
570
os.write(fd, 'set sender to "%s"\n'
571
% sender.replace('"', '\\"'))
572
if subject is not None:
573
os.write(fd, 'set subject to "%s"\n'
574
% subject.replace('"', '\\"'))
576
# FIXME: would be nice to prepend the body to the
577
# existing content (e.g., preserve signature), but
578
# can't seem to figure out the right applescript
580
os.write(fd, 'set content to "%s\\n\n"\n' %
581
body.replace('"', '\\"').replace('\n', '\\n'))
583
if attach_path is not None:
584
# FIXME: would be nice to first append a newline to
585
# ensure the attachment is on a new paragraph, but
586
# can't seem to figure out the right applescript
588
os.write(fd, 'tell content to make new attachment'
589
' with properties {file name:"%s"}'
590
' at after the last paragraph\n'
591
% self._encode_path(attach_path, 'attachment'))
592
os.write(fd, 'set visible to true\n')
593
os.write(fd, 'end tell\n')
594
os.write(fd, 'end tell\n')
596
os.close(fd) # Just close the handle but do not remove the file.
597
return [self.temp_file]
598
mail_client_registry.register('mail.app', MailApp,
599
help=MailApp.__doc__)
415
602
class DefaultMail(MailClient):
416
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
603
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
417
604
falls back to Editor"""
419
608
def _mail_client(self):
420
609
"""Determine the preferred mail client for this platform"""
421
610
if osutils.supports_mapi():
424
613
return XDGEmail(self.config)
426
615
def compose(self, prompt, to, subject, attachment, mime_subtype,
427
extension, basename=None):
616
extension, basename=None, body=None):
428
617
"""See MailClient.compose"""
430
619
return self._mail_client().compose(prompt, to, subject,
431
620
attachment, mimie_subtype,
621
extension, basename, body)
433
622
except errors.MailClientNotFound:
434
623
return Editor(self.config).compose(prompt, to, subject,
435
attachment, mimie_subtype, extension)
624
attachment, mimie_subtype, extension, body)
437
def compose_merge_request(self, to, subject, directive, basename=None):
626
def compose_merge_request(self, to, subject, directive, basename=None,
438
628
"""See MailClient.compose_merge_request"""
440
630
return self._mail_client().compose_merge_request(to, subject,
441
directive, basename=basename)
631
directive, basename=basename, body=body)
442
632
except errors.MailClientNotFound:
443
633
return Editor(self.config).compose_merge_request(to, subject,
444
directive, basename=basename)
634
directive, basename=basename, body=body)
635
mail_client_registry.register('default', DefaultMail,
636
help=DefaultMail.__doc__)
637
mail_client_registry.default_key = 'default'