56
51
"plain", "x-patch", etc.
57
52
:param extension: The file extension associated with the attachment
58
53
type, e.g. ".patch"
59
:param basename: The name to use for the attachment, e.g.
62
55
raise NotImplementedError
64
def compose_merge_request(self, to, subject, directive, basename=None):
57
def compose_merge_request(self, to, subject, directive):
65
58
"""Compose (and possibly send) a merge request
67
60
:param to: The address to send the request to
68
61
:param subject: The subject line to use for the request
69
62
:param directive: A merge directive representing the merge request, as
71
:param basename: The name to use for the attachment, e.g.
74
65
prompt = self._get_merge_prompt("Please describe these changes:", to,
75
66
subject, directive)
76
67
self.compose(prompt, to, subject, directive,
77
'x-patch', '.patch', basename)
79
70
def _get_merge_prompt(self, prompt, to, subject, attachment):
80
71
"""Generate a prompt string. Overridden by Editor.
129
118
return self._client_commands
131
120
def compose(self, prompt, to, subject, attachment, mime_subtype,
132
extension, basename=None):
133
122
"""See MailClient.compose.
135
124
Writes the attachment to a temporary file, invokes _compose.
138
basename = 'attachment'
139
pathname = osutils.mkdtemp(prefix='bzr-mail-')
140
attach_path = osutils.pathjoin(pathname, basename + extension)
141
outfile = open(attach_path, 'wb')
126
fd, pathname = tempfile.mkstemp(extension, 'bzr-mail-')
143
outfile.write(attachment)
128
os.write(fd, attachment)
146
self._compose(prompt, to, subject, attach_path, mime_subtype,
131
self._compose(prompt, to, subject, pathname, mime_subtype, extension)
149
133
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
184
168
raise NotImplementedError
186
def _encode_safe(self, u):
187
"""Encode possible unicode string argument to 8-bit string
188
in user_encoding. Unencodable characters will be replaced
191
:param u: possible unicode string.
192
:return: encoded string if u is unicode, u itself otherwise.
194
if isinstance(u, unicode):
195
return u.encode(osutils.get_user_encoding(), 'replace')
198
def _encode_path(self, path, kind):
199
"""Encode unicode path in user encoding.
201
:param path: possible unicode path.
202
:param kind: path kind ('executable' or 'attachment').
203
:return: encoded path if path is unicode,
204
path itself otherwise.
205
:raise: UnableEncodePath.
207
if isinstance(path, unicode):
209
return path.encode(osutils.get_user_encoding())
210
except UnicodeEncodeError:
211
raise errors.UnableEncodePath(path, kind)
215
171
class Evolution(ExternalMailClient):
216
172
"""Evolution mail client."""
241
194
"""See ExternalMailClient._get_compose_commandline"""
242
195
message_options = []
243
196
if subject is not None:
244
message_options.extend(['-s', self._encode_safe(subject)])
197
message_options.extend(['-s', subject ])
245
198
if attach_path is not None:
246
message_options.extend(['-a',
247
self._encode_path(attach_path, 'attachment')])
199
message_options.extend(['-a', attach_path])
248
200
if to is not None:
249
message_options.append(self._encode_safe(to))
201
message_options.append(to)
250
202
return message_options
251
mail_client_registry.register('mutt', Mutt,
255
205
class Thunderbird(ExternalMailClient):
265
215
_client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
266
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
267
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
216
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
269
218
def _get_compose_commandline(self, to, subject, attach_path):
270
219
"""See ExternalMailClient._get_compose_commandline"""
271
220
message_options = {}
272
221
if to is not None:
273
message_options['to'] = self._encode_safe(to)
222
message_options['to'] = to
274
223
if subject is not None:
275
message_options['subject'] = self._encode_safe(subject)
224
message_options['subject'] = subject
276
225
if attach_path is not None:
277
226
message_options['attachment'] = urlutils.local_path_to_url(
279
228
options_list = ["%s='%s'" % (k, v) for k, v in
280
229
sorted(message_options.iteritems())]
281
230
return ['-compose', ','.join(options_list)]
282
mail_client_registry.register('thunderbird', Thunderbird,
283
help=Thunderbird.__doc__)
286
233
class KMail(ExternalMailClient):
292
239
"""See ExternalMailClient._get_compose_commandline"""
293
240
message_options = []
294
241
if subject is not None:
295
message_options.extend(['-s', self._encode_safe(subject)])
296
if attach_path is not None:
297
message_options.extend(['--attach',
298
self._encode_path(attach_path, 'attachment')])
300
message_options.extend([self._encode_safe(to)])
301
return message_options
302
mail_client_registry.register('kmail', KMail,
306
class Claws(ExternalMailClient):
307
"""Claws mail client."""
309
_client_commands = ['claws-mail']
311
def _get_compose_commandline(self, to, subject, attach_path):
312
"""See ExternalMailClient._get_compose_commandline"""
313
compose_url = ['mailto:']
315
compose_url.append(self._encode_safe(to))
316
compose_url.append('?')
317
if subject is not None:
318
# Don't use urllib.quote_plus because Claws doesn't seem
319
# to recognise spaces encoded as "+".
321
'subject=%s' % urllib.quote(self._encode_safe(subject)))
322
# Collect command-line options.
323
message_options = ['--compose', ''.join(compose_url)]
324
if attach_path is not None:
325
message_options.extend(
326
['--attach', self._encode_path(attach_path, 'attachment')])
327
return message_options
328
mail_client_registry.register('claws', Claws,
242
message_options.extend( ['-s', subject ] )
243
if attach_path is not None:
244
message_options.extend( ['--attach', attach_path] )
246
message_options.extend( [ to ] )
248
return message_options
332
251
class XDGEmail(ExternalMailClient):
338
257
"""See ExternalMailClient._get_compose_commandline"""
340
259
raise errors.NoMailAddressSpecified()
341
commandline = [self._encode_safe(to)]
342
if subject is not None:
343
commandline.extend(['--subject', self._encode_safe(subject)])
344
if attach_path is not None:
345
commandline.extend(['--attach',
346
self._encode_path(attach_path, 'attachment')])
348
mail_client_registry.register('xdg-email', XDGEmail,
349
help=XDGEmail.__doc__)
352
class EmacsMail(ExternalMailClient):
353
"""Call emacsclient to have a mail buffer.
355
This only work for emacs >= 22.1 due to recent -e/--eval support.
357
The good news is that this implementation will work with all mail
358
agents registered against ``mail-user-agent``. So there is no need
359
to instantiate ExternalMailClient for each and every GNU Emacs
362
Users just have to ensure that ``mail-user-agent`` is set according
366
_client_commands = ['emacsclient']
368
def _prepare_send_function(self):
369
"""Write our wrapper function into a temporary file.
371
This temporary file will be loaded at runtime in
372
_get_compose_commandline function.
374
This function does not remove the file. That's a wanted
375
behaviour since _get_compose_commandline won't run the send
376
mail function directly but return the eligible command line.
377
Removing our temporary file here would prevent our sendmail
378
function to work. (The file is deleted by some elisp code
379
after being read by Emacs.)
382
_defun = r"""(defun bzr-add-mime-att (file)
383
"Attach FILE to a mail buffer as a MIME attachment."
384
(let ((agent mail-user-agent))
385
(if (and file (file-exists-p file))
387
((eq agent 'sendmail-user-agent)
391
(if (functionp 'etach-attach)
393
(mail-attach-file file))))
394
((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
396
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
397
((eq agent 'mew-user-agent)
399
(mew-draft-prepare-attachments)
400
(mew-attach-link file (file-name-nondirectory file))
401
(let* ((nums (mew-syntax-nums))
402
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
403
(mew-syntax-set-cd syntax "BZR merge")
404
(mew-encode-syntax-print mew-encode-syntax))
405
(mew-header-goto-body)))
407
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
408
(error "File %s does not exist." file))))
411
fd, temp_file = tempfile.mkstemp(prefix="emacs-bzr-send-",
416
os.close(fd) # Just close the handle but do not remove the file.
419
def _get_compose_commandline(self, to, subject, attach_path):
420
commandline = ["--eval"]
426
_to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
427
if subject is not None:
428
_subject = ("\"%s\"" %
429
self._encode_safe(subject).replace('"', '\\"'))
431
# Funcall the default mail composition function
432
# This will work with any mail mode including default mail-mode
433
# User must tweak mail-user-agent variable to tell what function
434
# will be called inside compose-mail.
435
mail_cmd = "(compose-mail %s %s)" % (_to, _subject)
436
commandline.append(mail_cmd)
438
# Try to attach a MIME attachment using our wrapper function
439
if attach_path is not None:
440
# Do not create a file if there is no attachment
441
elisp = self._prepare_send_function()
442
lmmform = '(load "%s")' % elisp
443
mmform = '(bzr-add-mime-att "%s")' % \
444
self._encode_path(attach_path, 'attachment')
445
rmform = '(delete-file "%s")' % elisp
446
commandline.append(lmmform)
447
commandline.append(mmform)
448
commandline.append(rmform)
451
mail_client_registry.register('emacsclient', EmacsMail,
452
help=EmacsMail.__doc__)
261
if subject is not None:
262
commandline.extend(['--subject', subject])
263
if attach_path is not None:
264
commandline.extend(['--attach', attach_path])
455
268
class MAPIClient(ExternalMailClient):
484
295
return XDGEmail(self.config)
486
297
def compose(self, prompt, to, subject, attachment, mime_subtype,
487
extension, basename=None):
488
299
"""See MailClient.compose"""
490
301
return self._mail_client().compose(prompt, to, subject,
491
302
attachment, mimie_subtype,
493
304
except errors.MailClientNotFound:
494
305
return Editor(self.config).compose(prompt, to, subject,
495
306
attachment, mimie_subtype, extension)
497
def compose_merge_request(self, to, subject, directive, basename=None):
308
def compose_merge_request(self, to, subject, directive):
498
309
"""See MailClient.compose_merge_request"""
500
311
return self._mail_client().compose_merge_request(to, subject,
501
directive, basename=basename)
502
313
except errors.MailClientNotFound:
503
314
return Editor(self.config).compose_merge_request(to, subject,
504
directive, basename=basename)
505
mail_client_registry.register('default', DefaultMail,
506
help=DefaultMail.__doc__)
507
mail_client_registry.default_key = 'default'