13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
40
43
self.config = config
42
45
def compose(self, prompt, to, subject, attachment, mime_subtype,
43
extension, basename=None):
46
extension, basename=None, body=None):
44
47
"""Compose (and possibly send) an email message
46
49
Must be implemented by subclasses.
61
64
raise NotImplementedError
63
def compose_merge_request(self, to, subject, directive, basename=None):
66
def compose_merge_request(self, to, subject, directive, basename=None,
64
68
"""Compose (and possibly send) a merge request
66
70
:param to: The address to send the request to
73
77
prompt = self._get_merge_prompt("Please describe these changes:", to,
74
78
subject, directive)
75
79
self.compose(prompt, to, subject, directive,
76
'x-patch', '.patch', basename)
80
'x-patch', '.patch', basename, body)
78
82
def _get_merge_prompt(self, prompt, to, subject, attachment):
79
83
"""Generate a prompt string. Overridden by Editor.
98
104
attachment.decode('utf-8', 'replace')))
100
106
def compose(self, prompt, to, subject, attachment, mime_subtype,
101
extension, basename=None):
107
extension, basename=None, body=None):
102
108
"""See MailClient.compose"""
104
110
raise errors.NoMailAddressSpecified()
105
body = msgeditor.edit_commit_message(prompt)
111
body = msgeditor.edit_commit_message(prompt, start_message=body)
107
113
raise errors.NoMessageSupplied()
108
114
email_message.EmailMessage.send(self.config,
109
self.config.username(),
115
self.config.get('email'),
128
135
return self._client_commands
130
137
def compose(self, prompt, to, subject, attachment, mime_subtype,
131
extension, basename=None):
138
extension, basename=None, body=None):
132
139
"""See MailClient.compose.
134
141
Writes the attachment to a temporary file, invokes _compose.
142
149
outfile.write(attachment)
153
kwargs = {'body': body}
145
156
self._compose(prompt, to, subject, attach_path, mime_subtype,
148
159
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
160
extension, body=None, from_=None):
150
161
"""Invoke a mail client as a commandline process.
152
163
Overridden by MAPIClient.
157
168
"text", but the precise subtype can be specified here
158
169
:param extension: A file extension (including period) associated with
159
170
the attachment type.
171
:param body: Optional body text.
172
:param from_: Optional From: header.
161
174
for name in self._get_client_commands():
162
175
cmdline = [self._encode_path(name, 'executable')]
177
kwargs = {'body': body}
180
if from_ is not None:
181
kwargs['from_'] = from_
163
182
cmdline.extend(self._get_compose_commandline(to, subject,
166
186
subprocess.call(cmdline)
167
187
except OSError, e:
214
class Evolution(ExternalMailClient):
215
"""Evolution mail client."""
234
class ExternalMailClient(BodyExternalMailClient):
235
__doc__ = """An external mail client."""
237
supports_body = False
240
class Evolution(BodyExternalMailClient):
241
__doc__ = """Evolution mail client."""
217
243
_client_commands = ['evolution']
219
def _get_compose_commandline(self, to, subject, attach_path):
245
def _get_compose_commandline(self, to, subject, attach_path, body=None):
220
246
"""See ExternalMailClient._get_compose_commandline"""
221
247
message_options = {}
222
248
if subject is not None:
223
249
message_options['subject'] = subject
224
250
if attach_path is not None:
225
251
message_options['attach'] = attach_path
253
message_options['body'] = body
226
254
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
227
255
sorted(message_options.iteritems())]
228
256
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
231
259
help=Evolution.__doc__)
234
class Mutt(ExternalMailClient):
235
"""Mutt mail client."""
262
class Mutt(BodyExternalMailClient):
263
__doc__ = """Mutt mail client."""
237
265
_client_commands = ['mutt']
239
def _get_compose_commandline(self, to, subject, attach_path):
267
def _get_compose_commandline(self, to, subject, attach_path, body=None):
240
268
"""See ExternalMailClient._get_compose_commandline"""
241
269
message_options = []
242
270
if subject is not None:
244
272
if attach_path is not None:
245
273
message_options.extend(['-a',
246
274
self._encode_path(attach_path, 'attachment')])
276
# Store the temp file object in self, so that it does not get
277
# garbage collected and delete the file before mutt can read it.
278
self._temp_file = tempfile.NamedTemporaryFile(
279
prefix="mutt-body-", suffix=".txt")
280
self._temp_file.write(body)
281
self._temp_file.flush()
282
message_options.extend(['-i', self._temp_file.name])
247
283
if to is not None:
248
message_options.append(self._encode_safe(to))
284
message_options.extend(['--', self._encode_safe(to)])
249
285
return message_options
250
286
mail_client_registry.register('mutt', Mutt,
251
287
help=Mutt.__doc__)
254
class Thunderbird(ExternalMailClient):
255
"""Mozilla Thunderbird (or Icedove)
290
class Thunderbird(BodyExternalMailClient):
291
__doc__ = """Mozilla Thunderbird (or Icedove)
257
293
Note that Thunderbird 1.5 is buggy and does not support setting
258
294
"to" simultaneously with including a attachment.
265
301
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
266
302
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
268
def _get_compose_commandline(self, to, subject, attach_path):
304
def _get_compose_commandline(self, to, subject, attach_path, body=None):
269
305
"""See ExternalMailClient._get_compose_commandline"""
270
306
message_options = {}
271
307
if to is not None:
275
311
if attach_path is not None:
276
312
message_options['attachment'] = urlutils.local_path_to_url(
278
options_list = ["%s='%s'" % (k, v) for k, v in
279
sorted(message_options.iteritems())]
315
options_list = ['body=%s' % urlutils.quote(self._encode_safe(body))]
318
options_list.extend(["%s='%s'" % (k, v) for k, v in
319
sorted(message_options.iteritems())])
280
320
return ['-compose', ','.join(options_list)]
281
321
mail_client_registry.register('thunderbird', Thunderbird,
282
322
help=Thunderbird.__doc__)
285
325
class KMail(ExternalMailClient):
286
"""KDE mail client."""
326
__doc__ = """KDE mail client."""
288
328
_client_commands = ['kmail']
302
342
help=KMail.__doc__)
305
class XDGEmail(ExternalMailClient):
306
"""xdg-email attempts to invoke the user's preferred mail client"""
345
class Claws(ExternalMailClient):
346
__doc__ = """Claws mail client."""
350
_client_commands = ['claws-mail']
352
def _get_compose_commandline(self, to, subject, attach_path, body=None,
354
"""See ExternalMailClient._get_compose_commandline"""
356
if from_ is not None:
357
compose_url.append('from=' + urlutils.quote(from_))
358
if subject is not None:
359
# Don't use urlutils.quote_plus because Claws doesn't seem
360
# to recognise spaces encoded as "+".
362
'subject=' + urlutils.quote(self._encode_safe(subject)))
365
'body=' + urlutils.quote(self._encode_safe(body)))
366
# to must be supplied for the claws-mail --compose syntax to work.
368
raise errors.NoMailAddressSpecified()
369
compose_url = 'mailto:%s?%s' % (
370
self._encode_safe(to), '&'.join(compose_url))
371
# Collect command-line options.
372
message_options = ['--compose', compose_url]
373
if attach_path is not None:
374
message_options.extend(
375
['--attach', self._encode_path(attach_path, 'attachment')])
376
return message_options
378
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
379
extension, body=None, from_=None):
380
"""See ExternalMailClient._compose"""
382
from_ = self.config.get('email')
383
super(Claws, self)._compose(prompt, to, subject, attach_path,
384
mime_subtype, extension, body, from_)
387
mail_client_registry.register('claws', Claws,
391
class XDGEmail(BodyExternalMailClient):
392
__doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
308
394
_client_commands = ['xdg-email']
310
def _get_compose_commandline(self, to, subject, attach_path):
396
def _get_compose_commandline(self, to, subject, attach_path, body=None):
311
397
"""See ExternalMailClient._get_compose_commandline"""
313
399
raise errors.NoMailAddressSpecified()
317
403
if attach_path is not None:
318
404
commandline.extend(['--attach',
319
405
self._encode_path(attach_path, 'attachment')])
407
commandline.extend(['--body', self._encode_safe(body)])
320
408
return commandline
321
409
mail_client_registry.register('xdg-email', XDGEmail,
322
410
help=XDGEmail.__doc__)
325
413
class EmacsMail(ExternalMailClient):
326
"""Call emacsclient to have a mail buffer.
414
__doc__ = """Call emacsclient to have a mail buffer.
328
416
This only work for emacs >= 22.1 due to recent -e/--eval support.
425
520
help=EmacsMail.__doc__)
428
class MAPIClient(ExternalMailClient):
429
"""Default Windows mail client launched using MAPI."""
523
class MAPIClient(BodyExternalMailClient):
524
__doc__ = """Default Windows mail client launched using MAPI."""
431
526
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
527
extension, body=None):
433
528
"""See ExternalMailClient._compose.
435
530
This implementation uses MAPI via the simplemapi ctypes wrapper
437
532
from bzrlib.util import simplemapi
439
simplemapi.SendMail(to or '', subject or '', '', attach_path)
534
simplemapi.SendMail(to or '', subject or '', body or '',
440
536
except simplemapi.MAPIError, e:
441
537
if e.code != simplemapi.MAPI_USER_ABORT:
442
538
raise errors.MailClientNotFound(['MAPI supported mail client'
445
541
help=MAPIClient.__doc__)
544
class MailApp(BodyExternalMailClient):
545
__doc__ = """Use MacOS X's Mail.app for sending email messages.
547
Although it would be nice to use appscript, it's not installed
548
with the shipped Python installations. We instead build an
549
AppleScript and invoke the script using osascript(1). We don't
550
use the _encode_safe() routines as it's not clear what encoding
551
osascript expects the script to be in.
554
_client_commands = ['osascript']
556
def _get_compose_commandline(self, to, subject, attach_path, body=None,
558
"""See ExternalMailClient._get_compose_commandline"""
560
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
563
os.write(fd, 'tell application "Mail"\n')
564
os.write(fd, 'set newMessage to make new outgoing message\n')
565
os.write(fd, 'tell newMessage\n')
567
os.write(fd, 'make new to recipient with properties'
568
' {address:"%s"}\n' % to)
569
if from_ is not None:
570
# though from_ doesn't actually seem to be used
571
os.write(fd, 'set sender to "%s"\n'
572
% sender.replace('"', '\\"'))
573
if subject is not None:
574
os.write(fd, 'set subject to "%s"\n'
575
% subject.replace('"', '\\"'))
577
# FIXME: would be nice to prepend the body to the
578
# existing content (e.g., preserve signature), but
579
# can't seem to figure out the right applescript
581
os.write(fd, 'set content to "%s\\n\n"\n' %
582
body.replace('"', '\\"').replace('\n', '\\n'))
584
if attach_path is not None:
585
# FIXME: would be nice to first append a newline to
586
# ensure the attachment is on a new paragraph, but
587
# can't seem to figure out the right applescript
589
os.write(fd, 'tell content to make new attachment'
590
' with properties {file name:"%s"}'
591
' at after the last paragraph\n'
592
% self._encode_path(attach_path, 'attachment'))
593
os.write(fd, 'set visible to true\n')
594
os.write(fd, 'end tell\n')
595
os.write(fd, 'end tell\n')
597
os.close(fd) # Just close the handle but do not remove the file.
598
return [self.temp_file]
599
mail_client_registry.register('mail.app', MailApp,
600
help=MailApp.__doc__)
448
603
class DefaultMail(MailClient):
449
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
604
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
450
605
falls back to Editor"""
452
609
def _mail_client(self):
453
610
"""Determine the preferred mail client for this platform"""
454
611
if osutils.supports_mapi():
457
614
return XDGEmail(self.config)
459
616
def compose(self, prompt, to, subject, attachment, mime_subtype,
460
extension, basename=None):
617
extension, basename=None, body=None):
461
618
"""See MailClient.compose"""
463
620
return self._mail_client().compose(prompt, to, subject,
464
attachment, mimie_subtype,
621
attachment, mime_subtype,
622
extension, basename, body)
466
623
except errors.MailClientNotFound:
467
624
return Editor(self.config).compose(prompt, to, subject,
468
attachment, mimie_subtype, extension)
625
attachment, mime_subtype, extension, body)
470
def compose_merge_request(self, to, subject, directive, basename=None):
627
def compose_merge_request(self, to, subject, directive, basename=None,
471
629
"""See MailClient.compose_merge_request"""
473
631
return self._mail_client().compose_merge_request(to, subject,
474
directive, basename=basename)
632
directive, basename=basename, body=body)
475
633
except errors.MailClientNotFound:
476
634
return Editor(self.config).compose_merge_request(to, subject,
477
directive, basename=basename)
635
directive, basename=basename, body=body)
478
636
mail_client_registry.register('default', DefaultMail,
479
637
help=DefaultMail.__doc__)
480
638
mail_client_registry.default_key = 'default'
640
opt_mail_client = _mod_config.RegistryOption('mail_client',
641
mail_client_registry, help='E-mail client to use.', invalid='error')