133
123
return self._client_commands
135
125
def compose(self, prompt, to, subject, attachment, mime_subtype,
136
extension, basename=None, body=None):
126
extension, basename=None):
137
127
"""See MailClient.compose.
139
129
Writes the attachment to a temporary file, invokes _compose.
141
131
if basename is None:
142
132
basename = 'attachment'
143
pathname = osutils.mkdtemp(prefix='bzr-mail-')
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
144
134
attach_path = osutils.pathjoin(pathname, basename + extension)
145
135
outfile = open(attach_path, 'wb')
147
137
outfile.write(attachment)
151
kwargs = {'body': body}
154
140
self._compose(prompt, to, subject, attach_path, mime_subtype,
157
143
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
extension, body=None):
159
145
"""Invoke a mail client as a commandline process.
161
147
Overridden by MAPIClient.
220
201
if isinstance(path, unicode):
222
return path.encode(osutils.get_user_encoding())
203
return path.encode(bzrlib.user_encoding)
223
204
except UnicodeEncodeError:
224
205
raise errors.UnableEncodePath(path, kind)
228
class ExternalMailClient(BodyExternalMailClient):
229
"""An external mail client."""
231
supports_body = False
234
class Evolution(BodyExternalMailClient):
209
class Evolution(ExternalMailClient):
235
210
"""Evolution mail client."""
237
212
_client_commands = ['evolution']
239
def _get_compose_commandline(self, to, subject, attach_path, body=None):
214
def _get_compose_commandline(self, to, subject, attach_path):
240
215
"""See ExternalMailClient._get_compose_commandline"""
241
216
message_options = {}
242
217
if subject is not None:
243
218
message_options['subject'] = subject
244
219
if attach_path is not None:
245
220
message_options['attach'] = attach_path
247
message_options['body'] = body
248
221
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
249
222
sorted(message_options.iteritems())]
250
223
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
251
224
'&'.join(options_list))]
252
mail_client_registry.register('evolution', Evolution,
253
help=Evolution.__doc__)
256
227
class Mutt(ExternalMailClient):
324
286
if to is not None:
325
287
message_options.extend([self._encode_safe(to)])
326
288
return message_options
327
mail_client_registry.register('kmail', KMail,
331
class Claws(ExternalMailClient):
332
"""Claws mail client."""
334
_client_commands = ['claws-mail']
336
def _get_compose_commandline(self, to, subject, attach_path):
337
"""See ExternalMailClient._get_compose_commandline"""
338
compose_url = ['mailto:']
340
compose_url.append(self._encode_safe(to))
341
compose_url.append('?')
342
if subject is not None:
343
# Don't use urllib.quote_plus because Claws doesn't seem
344
# to recognise spaces encoded as "+".
346
'subject=%s' % urllib.quote(self._encode_safe(subject)))
347
# Collect command-line options.
348
message_options = ['--compose', ''.join(compose_url)]
349
if attach_path is not None:
350
message_options.extend(
351
['--attach', self._encode_path(attach_path, 'attachment')])
352
return message_options
353
mail_client_registry.register('claws', Claws,
357
class XDGEmail(BodyExternalMailClient):
291
class XDGEmail(ExternalMailClient):
358
292
"""xdg-email attempts to invoke the user's preferred mail client"""
360
294
_client_commands = ['xdg-email']
362
def _get_compose_commandline(self, to, subject, attach_path, body=None):
296
def _get_compose_commandline(self, to, subject, attach_path):
363
297
"""See ExternalMailClient._get_compose_commandline"""
365
299
raise errors.NoMailAddressSpecified()
369
303
if attach_path is not None:
370
304
commandline.extend(['--attach',
371
305
self._encode_path(attach_path, 'attachment')])
373
commandline.extend(['--body', self._encode_safe(body)])
375
mail_client_registry.register('xdg-email', XDGEmail,
376
help=XDGEmail.__doc__)
379
class EmacsMail(ExternalMailClient):
380
"""Call emacsclient to have a mail buffer.
382
This only work for emacs >= 22.1 due to recent -e/--eval support.
384
The good news is that this implementation will work with all mail
385
agents registered against ``mail-user-agent``. So there is no need
386
to instantiate ExternalMailClient for each and every GNU Emacs
389
Users just have to ensure that ``mail-user-agent`` is set according
393
_client_commands = ['emacsclient']
395
def _prepare_send_function(self):
396
"""Write our wrapper function into a temporary file.
398
This temporary file will be loaded at runtime in
399
_get_compose_commandline function.
401
This function does not remove the file. That's a wanted
402
behaviour since _get_compose_commandline won't run the send
403
mail function directly but return the eligible command line.
404
Removing our temporary file here would prevent our sendmail
405
function to work. (The file is deleted by some elisp code
406
after being read by Emacs.)
409
_defun = r"""(defun bzr-add-mime-att (file)
410
"Attach FILE to a mail buffer as a MIME attachment."
411
(let ((agent mail-user-agent))
412
(if (and file (file-exists-p file))
414
((eq agent 'sendmail-user-agent)
418
(if (functionp 'etach-attach)
420
(mail-attach-file file))))
421
((or (eq agent 'message-user-agent)
422
(eq agent 'gnus-user-agent)
423
(eq agent 'mh-e-user-agent))
425
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
426
((eq agent 'mew-user-agent)
428
(mew-draft-prepare-attachments)
429
(mew-attach-link file (file-name-nondirectory file))
430
(let* ((nums (mew-syntax-nums))
431
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
432
(mew-syntax-set-cd syntax "BZR merge")
433
(mew-encode-syntax-print mew-encode-syntax))
434
(mew-header-goto-body)))
436
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
437
(error "File %s does not exist." file))))
440
fd, temp_file = tempfile.mkstemp(prefix="emacs-bzr-send-",
445
os.close(fd) # Just close the handle but do not remove the file.
448
def _get_compose_commandline(self, to, subject, attach_path):
449
commandline = ["--eval"]
455
_to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
456
if subject is not None:
457
_subject = ("\"%s\"" %
458
self._encode_safe(subject).replace('"', '\\"'))
460
# Funcall the default mail composition function
461
# This will work with any mail mode including default mail-mode
462
# User must tweak mail-user-agent variable to tell what function
463
# will be called inside compose-mail.
464
mail_cmd = "(compose-mail %s %s)" % (_to, _subject)
465
commandline.append(mail_cmd)
467
# Try to attach a MIME attachment using our wrapper function
468
if attach_path is not None:
469
# Do not create a file if there is no attachment
470
elisp = self._prepare_send_function()
471
lmmform = '(load "%s")' % elisp
472
mmform = '(bzr-add-mime-att "%s")' % \
473
self._encode_path(attach_path, 'attachment')
474
rmform = '(delete-file "%s")' % elisp
475
commandline.append(lmmform)
476
commandline.append(mmform)
477
commandline.append(rmform)
480
mail_client_registry.register('emacsclient', EmacsMail,
481
help=EmacsMail.__doc__)
484
class MAPIClient(BodyExternalMailClient):
309
class MAPIClient(ExternalMailClient):
485
310
"""Default Windows mail client launched using MAPI."""
487
312
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
488
extension, body=None):
489
314
"""See ExternalMailClient._compose.
491
316
This implementation uses MAPI via the simplemapi ctypes wrapper
493
318
from bzrlib.util import simplemapi
495
simplemapi.SendMail(to or '', subject or '', body or '',
320
simplemapi.SendMail(to or '', subject or '', '', attach_path)
497
321
except simplemapi.MAPIError, e:
498
322
if e.code != simplemapi.MAPI_USER_ABORT:
499
323
raise errors.MailClientNotFound(['MAPI supported mail client'
500
324
' (error %d)' % (e.code,)])
501
mail_client_registry.register('mapi', MAPIClient,
502
help=MAPIClient.__doc__)
505
327
class DefaultMail(MailClient):
514
336
return XDGEmail(self.config)
516
338
def compose(self, prompt, to, subject, attachment, mime_subtype,
517
extension, basename=None, body=None):
339
extension, basename=None):
518
340
"""See MailClient.compose"""
520
342
return self._mail_client().compose(prompt, to, subject,
521
343
attachment, mimie_subtype,
522
extension, basename, body)
523
345
except errors.MailClientNotFound:
524
346
return Editor(self.config).compose(prompt, to, subject,
525
attachment, mimie_subtype, extension, body)
347
attachment, mimie_subtype, extension)
527
def compose_merge_request(self, to, subject, directive, basename=None,
349
def compose_merge_request(self, to, subject, directive):
529
350
"""See MailClient.compose_merge_request"""
531
352
return self._mail_client().compose_merge_request(to, subject,
532
directive, basename=basename, body=body)
533
354
except errors.MailClientNotFound:
534
355
return Editor(self.config).compose_merge_request(to, subject,
535
directive, basename=basename, body=body)
536
mail_client_registry.register('default', DefaultMail,
537
help=DefaultMail.__doc__)
538
mail_client_registry.default_key = 'default'