14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
27
config as _mod_config,
34
40
mail_client_registry = registry.Registry()
43
class MailClientNotFound(errors.BzrError):
45
_fmt = "Unable to find mail client with the following names:"\
46
" %(mail_command_list_string)s"
48
def __init__(self, mail_command_list):
49
mail_command_list_string = ', '.join(mail_command_list)
50
errors.BzrError.__init__(
51
self, mail_command_list=mail_command_list,
52
mail_command_list_string=mail_command_list_string)
55
class NoMessageSupplied(errors.BzrError):
57
_fmt = "No message supplied."
60
class NoMailAddressSpecified(errors.BzrError):
62
_fmt = "No mail-to address (--mail-to) or output (-o) specified."
37
65
class MailClient(object):
38
66
"""A mail client that can send messages with attachements."""
105
133
extension, basename=None, body=None):
106
134
"""See MailClient.compose"""
108
raise errors.NoMailAddressSpecified()
136
raise NoMailAddressSpecified()
109
137
body = msgeditor.edit_commit_message(prompt, start_message=body)
111
raise errors.NoMessageSupplied()
139
raise NoMessageSupplied()
112
140
email_message.EmailMessage.send(self.config,
113
self.config.username(),
141
self.config.get('email'),
118
146
attachment_mime_subtype=mime_subtype)
119
149
mail_client_registry.register('editor', Editor,
120
150
help=Editor.__doc__)
266
295
"""See ExternalMailClient._get_compose_commandline"""
267
296
message_options = []
268
297
if subject is not None:
269
message_options.extend(['-s', self._encode_safe(subject)])
298
message_options.extend(
299
['-s', self._encode_safe(subject)])
270
300
if attach_path is not None:
271
message_options.extend(['-a',
272
self._encode_path(attach_path, 'attachment')])
301
message_options.extend(
302
['-a', self._encode_path(attach_path, 'attachment')])
273
303
if body is not None:
274
304
# Store the temp file object in self, so that it does not get
275
305
# garbage collected and delete the file before mutt can read it.
276
306
self._temp_file = tempfile.NamedTemporaryFile(
277
prefix="mutt-body-", suffix=".txt")
307
prefix="mutt-body-", suffix=".txt", mode="w+")
278
308
self._temp_file.write(body)
279
309
self._temp_file.flush()
280
310
message_options.extend(['-i', self._temp_file.name])
281
311
if to is not None:
282
312
message_options.extend(['--', self._encode_safe(to)])
283
313
return message_options
284
316
mail_client_registry.register('mutt', Mutt,
285
317
help=Mutt.__doc__)
352
390
"""See ExternalMailClient._get_compose_commandline"""
354
392
if from_ is not None:
355
compose_url.append('from=' + urllib.quote(from_))
393
compose_url.append('from=' + urlutils.quote(from_))
356
394
if subject is not None:
357
# Don't use urllib.quote_plus because Claws doesn't seem
395
# Don't use urlutils.quote_plus because Claws doesn't seem
358
396
# to recognise spaces encoded as "+".
359
397
compose_url.append(
360
'subject=' + urllib.quote(self._encode_safe(subject)))
398
'subject=' + urlutils.quote(self._encode_safe(subject)))
361
399
if body is not None:
362
400
compose_url.append(
363
'body=' + urllib.quote(self._encode_safe(body)))
401
'body=' + urlutils.quote(self._encode_safe(body)))
364
402
# to must be supplied for the claws-mail --compose syntax to work.
366
raise errors.NoMailAddressSpecified()
404
raise NoMailAddressSpecified()
367
405
compose_url = 'mailto:%s?%s' % (
368
406
self._encode_safe(to), '&'.join(compose_url))
369
407
# Collect command-line options.
389
427
class XDGEmail(BodyExternalMailClient):
390
__doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
428
__doc__ = """xdg-email attempts to invoke the preferred mail client"""
392
430
_client_commands = ['xdg-email']
394
432
def _get_compose_commandline(self, to, subject, attach_path, body=None):
395
433
"""See ExternalMailClient._get_compose_commandline"""
397
raise errors.NoMailAddressSpecified()
435
raise NoMailAddressSpecified()
398
436
commandline = [self._encode_safe(to)]
399
437
if subject is not None:
400
438
commandline.extend(['--subject', self._encode_safe(subject)])
401
439
if attach_path is not None:
402
440
commandline.extend(['--attach',
403
self._encode_path(attach_path, 'attachment')])
441
self._encode_path(attach_path, 'attachment')])
404
442
if body is not None:
405
443
commandline.extend(['--body', self._encode_safe(body)])
406
444
return commandline
407
447
mail_client_registry.register('xdg-email', XDGEmail,
408
448
help=XDGEmail.__doc__)
528
570
This implementation uses MAPI via the simplemapi ctypes wrapper
530
from bzrlib.util import simplemapi
572
from .util import simplemapi
532
574
simplemapi.SendMail(to or '', subject or '', body or '',
534
except simplemapi.MAPIError, e:
576
except simplemapi.MAPIError as e:
535
577
if e.code != simplemapi.MAPI_USER_ABORT:
536
raise errors.MailClientNotFound(['MAPI supported mail client'
537
' (error %d)' % (e.code,)])
578
raise MailClientNotFound(['MAPI supported mail client'
579
' (error %d)' % (e.code,)])
538
582
mail_client_registry.register('mapi', MAPIClient,
539
583
help=MAPIClient.__doc__)
552
596
_client_commands = ['osascript']
554
598
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]
600
"""See ExternalMailClient._get_compose_commandline"""
602
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
605
os.write(fd, 'tell application "Mail"\n')
606
os.write(fd, 'set newMessage to make new outgoing message\n')
607
os.write(fd, 'tell newMessage\n')
609
os.write(fd, 'make new to recipient with properties'
610
' {address:"%s"}\n' % to)
611
if from_ is not None:
612
# though from_ doesn't actually seem to be used
613
os.write(fd, 'set sender to "%s"\n'
614
% from_.replace('"', '\\"'))
615
if subject is not None:
616
os.write(fd, 'set subject to "%s"\n'
617
% subject.replace('"', '\\"'))
619
# FIXME: would be nice to prepend the body to the
620
# existing content (e.g., preserve signature), but
621
# can't seem to figure out the right applescript
623
os.write(fd, 'set content to "%s\\n\n"\n' %
624
body.replace('"', '\\"').replace('\n', '\\n'))
626
if attach_path is not None:
627
# FIXME: would be nice to first append a newline to
628
# ensure the attachment is on a new paragraph, but
629
# can't seem to figure out the right applescript
631
os.write(fd, 'tell content to make new attachment'
632
' with properties {file name:"%s"}'
633
' at after the last paragraph\n'
634
% self._encode_path(attach_path, 'attachment'))
635
os.write(fd, 'set visible to true\n')
636
os.write(fd, 'end tell\n')
637
os.write(fd, 'end tell\n')
639
os.close(fd) # Just close the handle but do not remove the file.
640
return [self.temp_file]
597
643
mail_client_registry.register('mail.app', MailApp,
598
644
help=MailApp.__doc__)
616
662
"""See MailClient.compose"""
618
664
return self._mail_client().compose(prompt, to, subject,
619
attachment, mimie_subtype,
665
attachment, mime_subtype,
620
666
extension, basename, body)
621
except errors.MailClientNotFound:
622
return Editor(self.config).compose(prompt, to, subject,
623
attachment, mimie_subtype, extension, body)
667
except MailClientNotFound:
668
return Editor(self.config).compose(
669
prompt, to, subject, attachment, mime_subtype, extension, body)
625
671
def compose_merge_request(self, to, subject, directive, basename=None,
627
673
"""See MailClient.compose_merge_request"""
629
return self._mail_client().compose_merge_request(to, subject,
630
directive, basename=basename, body=body)
631
except errors.MailClientNotFound:
632
return Editor(self.config).compose_merge_request(to, subject,
633
directive, basename=basename, body=body)
634
mail_client_registry.register('default', DefaultMail,
675
return self._mail_client().compose_merge_request(
676
to, subject, directive, basename=basename, body=body)
677
except MailClientNotFound:
678
return Editor(self.config).compose_merge_request(
679
to, subject, directive, basename=basename, body=body)
682
mail_client_registry.register(u'default', DefaultMail,
635
683
help=DefaultMail.__doc__)
636
mail_client_registry.default_key = 'default'
684
mail_client_registry.default_key = u'default'
686
opt_mail_client = _mod_config.RegistryOption(
687
'mail_client', mail_client_registry, help='E-mail client to use.',