166
201
raise NotImplementedError
169
class Evolution(ExternalMailClient):
203
def _encode_safe(self, u):
204
"""Encode possible unicode string argument to 8-bit string
205
in user_encoding. Unencodable characters will be replaced
208
:param u: possible unicode string.
209
:return: encoded string if u is unicode, u itself otherwise.
211
if isinstance(u, unicode):
212
return u.encode(osutils.get_user_encoding(), 'replace')
215
def _encode_path(self, path, kind):
216
"""Encode unicode path in user encoding.
218
:param path: possible unicode path.
219
:param kind: path kind ('executable' or 'attachment').
220
:return: encoded path if path is unicode,
221
path itself otherwise.
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)
232
class ExternalMailClient(BodyExternalMailClient):
233
"""An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
170
239
"""Evolution mail client."""
172
241
_client_commands = ['evolution']
174
def _get_compose_commandline(self, to, subject, attach_path):
243
def _get_compose_commandline(self, to, subject, attach_path, body=None):
175
244
"""See ExternalMailClient._get_compose_commandline"""
176
245
message_options = {}
177
246
if subject is not None:
178
247
message_options['subject'] = subject
179
248
if attach_path is not None:
180
249
message_options['attach'] = attach_path
251
message_options['body'] = body
181
252
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
182
message_options.iteritems()]
183
return ['mailto:%s?%s' % (to or '', '&'.join(options_list))]
186
class Thunderbird(ExternalMailClient):
253
sorted(message_options.iteritems())]
254
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
255
'&'.join(options_list))]
256
mail_client_registry.register('evolution', Evolution,
257
help=Evolution.__doc__)
260
class Mutt(BodyExternalMailClient):
261
"""Mutt mail client."""
263
_client_commands = ['mutt']
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
266
"""See ExternalMailClient._get_compose_commandline"""
268
if subject is not None:
269
message_options.extend(['-s', self._encode_safe(subject)])
270
if attach_path is not None:
271
message_options.extend(['-a',
272
self._encode_path(attach_path, 'attachment')])
274
# Store the temp file object in self, so that it does not get
275
# garbage collected and delete the file before mutt can read it.
276
self._temp_file = tempfile.NamedTemporaryFile(
277
prefix="mutt-body-", suffix=".txt")
278
self._temp_file.write(body)
279
self._temp_file.flush()
280
message_options.extend(['-i', self._temp_file.name])
282
message_options.extend(['--', self._encode_safe(to)])
283
return message_options
284
mail_client_registry.register('mutt', Mutt,
288
class Thunderbird(BodyExternalMailClient):
187
289
"""Mozilla Thunderbird (or Icedove)
189
291
Note that Thunderbird 1.5 is buggy and does not support setting
196
298
_client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
197
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
299
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
199
def _get_compose_commandline(self, to, subject, attach_path):
302
def _get_compose_commandline(self, to, subject, attach_path, body=None):
200
303
"""See ExternalMailClient._get_compose_commandline"""
201
304
message_options = {}
202
305
if to is not None:
203
message_options['to'] = to
306
message_options['to'] = self._encode_safe(to)
204
307
if subject is not None:
205
message_options['subject'] = subject
308
message_options['subject'] = self._encode_safe(subject)
206
309
if attach_path is not None:
207
310
message_options['attachment'] = urlutils.local_path_to_url(
209
options_list = ["%s='%s'" % (k, v) for k, v in
210
sorted(message_options.iteritems())]
313
options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
316
options_list.extend(["%s='%s'" % (k, v) for k, v in
317
sorted(message_options.iteritems())])
211
318
return ['-compose', ','.join(options_list)]
319
mail_client_registry.register('thunderbird', Thunderbird,
320
help=Thunderbird.__doc__)
214
323
class KMail(ExternalMailClient):
220
329
"""See ExternalMailClient._get_compose_commandline"""
221
330
message_options = []
222
331
if subject is not None:
223
message_options.extend( ['-s', subject ] )
332
message_options.extend(['-s', self._encode_safe(subject)])
224
333
if attach_path is not None:
225
message_options.extend( ['--attach', attach_path] )
334
message_options.extend(['--attach',
335
self._encode_path(attach_path, 'attachment')])
226
336
if to is not None:
227
message_options.extend( [ to ] )
229
return message_options
232
class XDGEmail(ExternalMailClient):
337
message_options.extend([self._encode_safe(to)])
338
return message_options
339
mail_client_registry.register('kmail', KMail,
343
class Claws(ExternalMailClient):
344
"""Claws mail client."""
348
_client_commands = ['claws-mail']
350
def _get_compose_commandline(self, to, subject, attach_path, body=None,
352
"""See ExternalMailClient._get_compose_commandline"""
354
if from_ is not None:
355
compose_url.append('from=' + urllib.quote(from_))
356
if subject is not None:
357
# Don't use urllib.quote_plus because Claws doesn't seem
358
# to recognise spaces encoded as "+".
360
'subject=' + urllib.quote(self._encode_safe(subject)))
363
'body=' + urllib.quote(self._encode_safe(body)))
364
# to must be supplied for the claws-mail --compose syntax to work.
366
raise errors.NoMailAddressSpecified()
367
compose_url = 'mailto:%s?%s' % (
368
self._encode_safe(to), '&'.join(compose_url))
369
# Collect command-line options.
370
message_options = ['--compose', compose_url]
371
if attach_path is not None:
372
message_options.extend(
373
['--attach', self._encode_path(attach_path, 'attachment')])
374
return message_options
376
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
377
extension, body=None, from_=None):
378
"""See ExternalMailClient._compose"""
380
from_ = self.config.get_user_option('email')
381
super(Claws, self)._compose(prompt, to, subject, attach_path,
382
mime_subtype, extension, body, from_)
385
mail_client_registry.register('claws', Claws,
389
class XDGEmail(BodyExternalMailClient):
233
390
"""xdg-email attempts to invoke the user's preferred mail client"""
235
392
_client_commands = ['xdg-email']
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
395
"""See ExternalMailClient._get_compose_commandline"""
397
raise errors.NoMailAddressSpecified()
398
commandline = [self._encode_safe(to)]
399
if subject is not None:
400
commandline.extend(['--subject', self._encode_safe(subject)])
401
if attach_path is not None:
402
commandline.extend(['--attach',
403
self._encode_path(attach_path, 'attachment')])
405
commandline.extend(['--body', self._encode_safe(body)])
407
mail_client_registry.register('xdg-email', XDGEmail,
408
help=XDGEmail.__doc__)
411
class EmacsMail(ExternalMailClient):
412
"""Call emacsclient to have a mail buffer.
414
This only work for emacs >= 22.1 due to recent -e/--eval support.
416
The good news is that this implementation will work with all mail
417
agents registered against ``mail-user-agent``. So there is no need
418
to instantiate ExternalMailClient for each and every GNU Emacs
421
Users just have to ensure that ``mail-user-agent`` is set according
425
_client_commands = ['emacsclient']
427
def _prepare_send_function(self):
428
"""Write our wrapper function into a temporary file.
430
This temporary file will be loaded at runtime in
431
_get_compose_commandline function.
433
This function does not remove the file. That's a wanted
434
behaviour since _get_compose_commandline won't run the send
435
mail function directly but return the eligible command line.
436
Removing our temporary file here would prevent our sendmail
437
function to work. (The file is deleted by some elisp code
438
after being read by Emacs.)
441
_defun = r"""(defun bzr-add-mime-att (file)
442
"Attach FILE to a mail buffer as a MIME attachment."
443
(let ((agent mail-user-agent))
444
(if (and file (file-exists-p file))
446
((eq agent 'sendmail-user-agent)
450
(if (functionp 'etach-attach)
452
(mail-attach-file file))))
453
((or (eq agent 'message-user-agent)
454
(eq agent 'gnus-user-agent)
455
(eq agent 'mh-e-user-agent))
457
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
458
((eq agent 'mew-user-agent)
460
(mew-draft-prepare-attachments)
461
(mew-attach-link file (file-name-nondirectory file))
462
(let* ((nums (mew-syntax-nums))
463
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
464
(mew-syntax-set-cd syntax "BZR merge")
465
(mew-encode-syntax-print mew-encode-syntax))
466
(mew-header-goto-body)))
468
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
469
(error "File %s does not exist." file))))
472
fd, temp_file = tempfile.mkstemp(prefix="emacs-bzr-send-",
477
os.close(fd) # Just close the handle but do not remove the file.
237
480
def _get_compose_commandline(self, to, subject, attach_path):
238
"""See ExternalMailClient._get_compose_commandline"""
481
commandline = ["--eval"]
487
_to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
240
488
if subject is not None:
241
commandline.extend(['--subject', subject])
489
_subject = ("\"%s\"" %
490
self._encode_safe(subject).replace('"', '\\"'))
492
# Funcall the default mail composition function
493
# This will work with any mail mode including default mail-mode
494
# User must tweak mail-user-agent variable to tell what function
495
# will be called inside compose-mail.
496
mail_cmd = "(compose-mail %s %s)" % (_to, _subject)
497
commandline.append(mail_cmd)
499
# Try to attach a MIME attachment using our wrapper function
242
500
if attach_path is not None:
243
commandline.extend(['--attach', attach_path])
501
# Do not create a file if there is no attachment
502
elisp = self._prepare_send_function()
503
lmmform = '(load "%s")' % elisp
504
mmform = '(bzr-add-mime-att "%s")' % \
505
self._encode_path(attach_path, 'attachment')
506
rmform = '(delete-file "%s")' % elisp
507
commandline.append(lmmform)
508
commandline.append(mmform)
509
commandline.append(rmform)
244
511
return commandline
247
class MAPIClient(ExternalMailClient):
512
mail_client_registry.register('emacsclient', EmacsMail,
513
help=EmacsMail.__doc__)
516
class MAPIClient(BodyExternalMailClient):
248
517
"""Default Windows mail client launched using MAPI."""
250
519
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
extension, body=None):
252
521
"""See ExternalMailClient._compose.
254
523
This implementation uses MAPI via the simplemapi ctypes wrapper
256
525
from bzrlib.util import simplemapi
258
simplemapi.SendMail(to or '', subject or '', '', attach_path)
527
simplemapi.SendMail(to or '', subject or '', body or '',
259
529
except simplemapi.MAPIError, e:
260
530
if e.code != simplemapi.MAPI_USER_ABORT:
261
531
raise errors.MailClientNotFound(['MAPI supported mail client'
262
532
' (error %d)' % (e.code,)])
533
mail_client_registry.register('mapi', MAPIClient,
534
help=MAPIClient.__doc__)
265
537
class DefaultMail(MailClient):
266
538
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
267
539
falls back to Editor"""
269
543
def _mail_client(self):
270
544
"""Determine the preferred mail client for this platform"""
271
545
if osutils.supports_mapi():
274
548
return XDGEmail(self.config)
276
550
def compose(self, prompt, to, subject, attachment, mime_subtype,
551
extension, basename=None, body=None):
278
552
"""See MailClient.compose"""
280
554
return self._mail_client().compose(prompt, to, subject,
281
555
attachment, mimie_subtype,
556
extension, basename, body)
283
557
except errors.MailClientNotFound:
284
558
return Editor(self.config).compose(prompt, to, subject,
285
attachment, mimie_subtype, extension)
559
attachment, mimie_subtype, extension, body)
287
def compose_merge_request(self, to, subject, directive):
561
def compose_merge_request(self, to, subject, directive, basename=None,
288
563
"""See MailClient.compose_merge_request"""
290
565
return self._mail_client().compose_merge_request(to, subject,
566
directive, basename=basename, body=body)
292
567
except errors.MailClientNotFound:
293
568
return Editor(self.config).compose_merge_request(to, subject,
569
directive, basename=basename, body=body)
570
mail_client_registry.register('default', DefaultMail,
571
help=DefaultMail.__doc__)
572
mail_client_registry.default_key = 'default'