25
config as _mod_config,
34
34
mail_client_registry = registry.Registry()
37
class MailClientNotFound(errors.BzrError):
39
_fmt = "Unable to find mail client with the following names:"\
40
" %(mail_command_list_string)s"
42
def __init__(self, mail_command_list):
43
mail_command_list_string = ', '.join(mail_command_list)
44
errors.BzrError.__init__(
45
self, mail_command_list=mail_command_list,
46
mail_command_list_string=mail_command_list_string)
49
class NoMessageSupplied(errors.BzrError):
51
_fmt = "No message supplied."
54
class NoMailAddressSpecified(errors.BzrError):
56
_fmt = "No mail-to address (--mail-to) or output (-o) specified."
59
37
class MailClient(object):
60
38
"""A mail client that can send messages with attachements."""
97
75
prompt = self._get_merge_prompt("Please describe these changes:", to,
98
76
subject, directive)
99
77
self.compose(prompt, to, subject, directive,
100
'x-patch', '.patch', basename, body)
78
'x-patch', '.patch', basename, body)
102
80
def _get_merge_prompt(self, prompt, to, subject, attachment):
103
81
"""Generate a prompt string. Overridden by Editor.
127
105
extension, basename=None, body=None):
128
106
"""See MailClient.compose"""
130
raise NoMailAddressSpecified()
108
raise errors.NoMailAddressSpecified()
131
109
body = msgeditor.edit_commit_message(prompt, start_message=body)
133
raise NoMessageSupplied()
111
raise errors.NoMessageSupplied()
134
112
email_message.EmailMessage.send(self.config,
135
self.config.get('email'),
113
self.config.username(),
140
118
attachment_mime_subtype=mime_subtype)
143
119
mail_client_registry.register('editor', Editor,
144
120
help=Editor.__doc__)
240
221
path itself otherwise.
241
222
:raise: UnableEncodePath.
224
if isinstance(path, unicode):
226
return path.encode(osutils.get_user_encoding())
227
except UnicodeEncodeError:
228
raise errors.UnableEncodePath(path, kind)
246
232
class ExternalMailClient(BodyExternalMailClient):
247
__doc__ = """An external mail client."""
233
"""An external mail client."""
249
235
supports_body = False
252
238
class Evolution(BodyExternalMailClient):
253
__doc__ = """Evolution mail client."""
239
"""Evolution mail client."""
255
241
_client_commands = ['evolution']
264
250
if body is not None:
265
251
message_options['body'] = body
266
252
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
267
sorted(message_options.items())]
253
sorted(message_options.iteritems())]
268
254
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
269
'&'.join(options_list))]
255
'&'.join(options_list))]
272
256
mail_client_registry.register('evolution', Evolution,
273
257
help=Evolution.__doc__)
276
260
class Mutt(BodyExternalMailClient):
277
__doc__ = """Mutt mail client."""
261
"""Mutt mail client."""
279
263
_client_commands = ['mutt']
282
266
"""See ExternalMailClient._get_compose_commandline"""
283
267
message_options = []
284
268
if subject is not None:
285
message_options.extend(
286
['-s', self._encode_safe(subject)])
269
message_options.extend(['-s', self._encode_safe(subject)])
287
270
if attach_path is not None:
288
message_options.extend(
289
['-a', self._encode_path(attach_path, 'attachment')])
271
message_options.extend(['-a',
272
self._encode_path(attach_path, 'attachment')])
290
273
if body is not None:
291
274
# Store the temp file object in self, so that it does not get
292
275
# garbage collected and delete the file before mutt can read it.
293
276
self._temp_file = tempfile.NamedTemporaryFile(
294
prefix="mutt-body-", suffix=".txt", mode="w+")
277
prefix="mutt-body-", suffix=".txt")
295
278
self._temp_file.write(body)
296
279
self._temp_file.flush()
297
280
message_options.extend(['-i', self._temp_file.name])
298
281
if to is not None:
299
282
message_options.extend(['--', self._encode_safe(to)])
300
283
return message_options
303
284
mail_client_registry.register('mutt', Mutt,
304
285
help=Mutt.__doc__)
307
288
class Thunderbird(BodyExternalMailClient):
308
__doc__ = """Mozilla Thunderbird (or Icedove)
289
"""Mozilla Thunderbird (or Icedove)
310
291
Note that Thunderbird 1.5 is buggy and does not support setting
311
292
"to" simultaneously with including a attachment.
330
310
message_options['attachment'] = urlutils.local_path_to_url(
332
312
if body is not None:
333
options_list = ['body=%s' %
334
urlutils.quote(self._encode_safe(body))]
313
options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
336
315
options_list = []
337
316
options_list.extend(["%s='%s'" % (k, v) for k, v in
338
sorted(message_options.items())])
317
sorted(message_options.iteritems())])
339
318
return ['-compose', ','.join(options_list)]
342
319
mail_client_registry.register('thunderbird', Thunderbird,
343
320
help=Thunderbird.__doc__)
346
323
class KMail(ExternalMailClient):
347
__doc__ = """KDE mail client."""
324
"""KDE mail client."""
349
326
_client_commands = ['kmail']
354
331
if subject is not None:
355
332
message_options.extend(['-s', self._encode_safe(subject)])
356
333
if attach_path is not None:
357
message_options.extend(
358
['--attach', self._encode_path(attach_path, 'attachment')])
334
message_options.extend(['--attach',
335
self._encode_path(attach_path, 'attachment')])
359
336
if to is not None:
360
337
message_options.extend([self._encode_safe(to)])
361
338
return message_options
364
339
mail_client_registry.register('kmail', KMail,
365
340
help=KMail.__doc__)
368
343
class Claws(ExternalMailClient):
369
__doc__ = """Claws mail client."""
344
"""Claws mail client."""
371
346
supports_body = True
377
352
"""See ExternalMailClient._get_compose_commandline"""
379
354
if from_ is not None:
380
compose_url.append('from=' + urlutils.quote(from_))
355
compose_url.append('from=' + urllib.quote(from_))
381
356
if subject is not None:
382
# Don't use urlutils.quote_plus because Claws doesn't seem
357
# Don't use urllib.quote_plus because Claws doesn't seem
383
358
# to recognise spaces encoded as "+".
384
359
compose_url.append(
385
'subject=' + urlutils.quote(self._encode_safe(subject)))
360
'subject=' + urllib.quote(self._encode_safe(subject)))
386
361
if body is not None:
387
362
compose_url.append(
388
'body=' + urlutils.quote(self._encode_safe(body)))
363
'body=' + urllib.quote(self._encode_safe(body)))
389
364
# to must be supplied for the claws-mail --compose syntax to work.
391
raise NoMailAddressSpecified()
366
raise errors.NoMailAddressSpecified()
392
367
compose_url = 'mailto:%s?%s' % (
393
368
self._encode_safe(to), '&'.join(compose_url))
394
369
# Collect command-line options.
414
389
class XDGEmail(BodyExternalMailClient):
415
__doc__ = """xdg-email attempts to invoke the preferred mail client"""
390
"""xdg-email attempts to invoke the user's preferred mail client"""
417
392
_client_commands = ['xdg-email']
419
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
420
395
"""See ExternalMailClient._get_compose_commandline"""
422
raise NoMailAddressSpecified()
397
raise errors.NoMailAddressSpecified()
423
398
commandline = [self._encode_safe(to)]
424
399
if subject is not None:
425
400
commandline.extend(['--subject', self._encode_safe(subject)])
426
401
if attach_path is not None:
427
402
commandline.extend(['--attach',
428
self._encode_path(attach_path, 'attachment')])
403
self._encode_path(attach_path, 'attachment')])
429
404
if body is not None:
430
405
commandline.extend(['--body', self._encode_safe(body)])
431
406
return commandline
434
407
mail_client_registry.register('xdg-email', XDGEmail,
435
408
help=XDGEmail.__doc__)
438
411
class EmacsMail(ExternalMailClient):
439
__doc__ = """Call emacsclient to have a mail buffer.
412
"""Call emacsclient to have a mail buffer.
441
414
This only work for emacs >= 22.1 due to recent -e/--eval support.
557
523
This implementation uses MAPI via the simplemapi ctypes wrapper
559
from .util import simplemapi
525
from bzrlib.util import simplemapi
561
527
simplemapi.SendMail(to or '', subject or '', body or '',
563
except simplemapi.MAPIError as e:
529
except simplemapi.MAPIError, e:
564
530
if e.code != simplemapi.MAPI_USER_ABORT:
565
raise MailClientNotFound(['MAPI supported mail client'
566
' (error %d)' % (e.code,)])
531
raise errors.MailClientNotFound(['MAPI supported mail client'
532
' (error %d)' % (e.code,)])
569
533
mail_client_registry.register('mapi', MAPIClient,
570
534
help=MAPIClient.__doc__)
573
class MailApp(BodyExternalMailClient):
574
__doc__ = """Use MacOS X's Mail.app for sending email messages.
576
Although it would be nice to use appscript, it's not installed
577
with the shipped Python installations. We instead build an
578
AppleScript and invoke the script using osascript(1). We don't
579
use the _encode_safe() routines as it's not clear what encoding
580
osascript expects the script to be in.
583
_client_commands = ['osascript']
585
def _get_compose_commandline(self, to, subject, attach_path, body=None,
587
"""See ExternalMailClient._get_compose_commandline"""
589
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
592
os.write(fd, 'tell application "Mail"\n')
593
os.write(fd, 'set newMessage to make new outgoing message\n')
594
os.write(fd, 'tell newMessage\n')
596
os.write(fd, 'make new to recipient with properties'
597
' {address:"%s"}\n' % to)
598
if from_ is not None:
599
# though from_ doesn't actually seem to be used
600
os.write(fd, 'set sender to "%s"\n'
601
% from_.replace('"', '\\"'))
602
if subject is not None:
603
os.write(fd, 'set subject to "%s"\n'
604
% subject.replace('"', '\\"'))
606
# FIXME: would be nice to prepend the body to the
607
# existing content (e.g., preserve signature), but
608
# can't seem to figure out the right applescript
610
os.write(fd, 'set content to "%s\\n\n"\n' %
611
body.replace('"', '\\"').replace('\n', '\\n'))
613
if attach_path is not None:
614
# FIXME: would be nice to first append a newline to
615
# ensure the attachment is on a new paragraph, but
616
# can't seem to figure out the right applescript
618
os.write(fd, 'tell content to make new attachment'
619
' with properties {file name:"%s"}'
620
' at after the last paragraph\n'
621
% self._encode_path(attach_path, 'attachment'))
622
os.write(fd, 'set visible to true\n')
623
os.write(fd, 'end tell\n')
624
os.write(fd, 'end tell\n')
626
os.close(fd) # Just close the handle but do not remove the file.
627
return [self.temp_file]
630
mail_client_registry.register('mail.app', MailApp,
631
help=MailApp.__doc__)
634
537
class DefaultMail(MailClient):
635
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
538
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
636
539
falls back to Editor"""
638
541
supports_body = True
649
552
"""See MailClient.compose"""
651
554
return self._mail_client().compose(prompt, to, subject,
652
attachment, mime_subtype,
555
attachment, mimie_subtype,
653
556
extension, basename, body)
654
except MailClientNotFound:
655
return Editor(self.config).compose(
656
prompt, to, subject, attachment, mime_subtype, extension, body)
557
except errors.MailClientNotFound:
558
return Editor(self.config).compose(prompt, to, subject,
559
attachment, mimie_subtype, extension, body)
658
561
def compose_merge_request(self, to, subject, directive, basename=None,
660
563
"""See MailClient.compose_merge_request"""
662
return self._mail_client().compose_merge_request(
663
to, subject, directive, basename=basename, body=body)
664
except MailClientNotFound:
665
return Editor(self.config).compose_merge_request(
666
to, subject, directive, basename=basename, body=body)
669
mail_client_registry.register(u'default', DefaultMail,
565
return self._mail_client().compose_merge_request(to, subject,
566
directive, basename=basename, body=body)
567
except errors.MailClientNotFound:
568
return Editor(self.config).compose_merge_request(to, subject,
569
directive, basename=basename, body=body)
570
mail_client_registry.register('default', DefaultMail,
670
571
help=DefaultMail.__doc__)
671
mail_client_registry.default_key = u'default'
673
opt_mail_client = _mod_config.RegistryOption(
674
'mail_client', mail_client_registry, help='E-mail client to use.',
572
mail_client_registry.default_key = 'default'