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 Mutt(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):
187
261
"""Mutt mail client."""
189
263
_client_commands = ['mutt']
191
def _get_compose_commandline(self, to, subject, attach_path):
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
192
266
"""See ExternalMailClient._get_compose_commandline"""
193
267
message_options = []
194
268
if subject is not None:
195
message_options.extend(['-s', subject ])
269
message_options.extend(['-s', self._encode_safe(subject)])
196
270
if attach_path is not None:
197
message_options.extend(['-a', attach_path])
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])
198
281
if to is not None:
199
message_options.append(to)
282
message_options.extend(['--', self._encode_safe(to)])
200
283
return message_options
203
class Thunderbird(ExternalMailClient):
284
mail_client_registry.register('mutt', Mutt,
288
class Thunderbird(BodyExternalMailClient):
204
289
"""Mozilla Thunderbird (or Icedove)
206
291
Note that Thunderbird 1.5 is buggy and does not support setting
213
298
_client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
214
'/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']
216
def _get_compose_commandline(self, to, subject, attach_path):
302
def _get_compose_commandline(self, to, subject, attach_path, body=None):
217
303
"""See ExternalMailClient._get_compose_commandline"""
218
304
message_options = {}
219
305
if to is not None:
220
message_options['to'] = to
306
message_options['to'] = self._encode_safe(to)
221
307
if subject is not None:
222
message_options['subject'] = subject
308
message_options['subject'] = self._encode_safe(subject)
223
309
if attach_path is not None:
224
310
message_options['attachment'] = urlutils.local_path_to_url(
226
options_list = ["%s='%s'" % (k, v) for k, v in
227
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())])
228
318
return ['-compose', ','.join(options_list)]
319
mail_client_registry.register('thunderbird', Thunderbird,
320
help=Thunderbird.__doc__)
231
323
class KMail(ExternalMailClient):
237
329
"""See ExternalMailClient._get_compose_commandline"""
238
330
message_options = []
239
331
if subject is not None:
240
message_options.extend( ['-s', subject ] )
332
message_options.extend(['-s', self._encode_safe(subject)])
241
333
if attach_path is not None:
242
message_options.extend( ['--attach', attach_path] )
334
message_options.extend(['--attach',
335
self._encode_path(attach_path, 'attachment')])
243
336
if to is not None:
244
message_options.extend( [ to ] )
246
return message_options
249
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):
250
390
"""xdg-email attempts to invoke the user's preferred mail client"""
252
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.
254
480
def _get_compose_commandline(self, to, subject, attach_path):
255
"""See ExternalMailClient._get_compose_commandline"""
481
commandline = ["--eval"]
487
_to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
257
488
if subject is not None:
258
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
259
500
if attach_path is not None:
260
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)
261
511
return commandline
264
class MAPIClient(ExternalMailClient):
512
mail_client_registry.register('emacsclient', EmacsMail,
513
help=EmacsMail.__doc__)
516
class MAPIClient(BodyExternalMailClient):
265
517
"""Default Windows mail client launched using MAPI."""
267
519
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
extension, body=None):
269
521
"""See ExternalMailClient._compose.
271
523
This implementation uses MAPI via the simplemapi ctypes wrapper
273
525
from bzrlib.util import simplemapi
275
simplemapi.SendMail(to or '', subject or '', '', attach_path)
527
simplemapi.SendMail(to or '', subject or '', body or '',
276
529
except simplemapi.MAPIError, e:
277
530
if e.code != simplemapi.MAPI_USER_ABORT:
278
531
raise errors.MailClientNotFound(['MAPI supported mail client'
279
532
' (error %d)' % (e.code,)])
533
mail_client_registry.register('mapi', MAPIClient,
534
help=MAPIClient.__doc__)
282
537
class DefaultMail(MailClient):
283
538
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
284
539
falls back to Editor"""
286
543
def _mail_client(self):
287
544
"""Determine the preferred mail client for this platform"""
288
545
if osutils.supports_mapi():
291
548
return XDGEmail(self.config)
293
550
def compose(self, prompt, to, subject, attachment, mime_subtype,
551
extension, basename=None, body=None):
295
552
"""See MailClient.compose"""
297
554
return self._mail_client().compose(prompt, to, subject,
298
555
attachment, mimie_subtype,
556
extension, basename, body)
300
557
except errors.MailClientNotFound:
301
558
return Editor(self.config).compose(prompt, to, subject,
302
attachment, mimie_subtype, extension)
559
attachment, mimie_subtype, extension, body)
304
def compose_merge_request(self, to, subject, directive):
561
def compose_merge_request(self, to, subject, directive, basename=None,
305
563
"""See MailClient.compose_merge_request"""
307
565
return self._mail_client().compose_merge_request(to, subject,
566
directive, basename=basename, body=body)
309
567
except errors.MailClientNotFound:
310
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'