51
55
"plain", "x-patch", etc.
52
56
:param extension: The file extension associated with the attachment
53
57
type, e.g. ".patch"
58
:param basename: The name to use for the attachment, e.g.
55
61
raise NotImplementedError
57
def compose_merge_request(self, to, subject, directive):
63
def compose_merge_request(self, to, subject, directive, basename=None):
58
64
"""Compose (and possibly send) a merge request
60
66
:param to: The address to send the request to
61
67
:param subject: The subject line to use for the request
62
68
:param directive: A merge directive representing the merge request, as
70
:param basename: The name to use for the attachment, e.g.
65
73
prompt = self._get_merge_prompt("Please describe these changes:", to,
66
74
subject, directive)
67
75
self.compose(prompt, to, subject, directive,
76
'x-patch', '.patch', basename)
70
78
def _get_merge_prompt(self, prompt, to, subject, attachment):
71
79
"""Generate a prompt string. Overridden by Editor.
116
128
return self._client_commands
118
130
def compose(self, prompt, to, subject, attachment, mime_subtype,
131
extension, basename=None):
120
132
"""See MailClient.compose.
122
134
Writes the attachment to a temporary file, invokes _compose.
124
fd, pathname = tempfile.mkstemp(extension, 'bzr-mail-')
137
basename = 'attachment'
138
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
139
attach_path = osutils.pathjoin(pathname, basename + extension)
140
outfile = open(attach_path, 'wb')
126
os.write(fd, attachment)
142
outfile.write(attachment)
129
self._compose(prompt, to, subject, pathname, mime_subtype, extension)
145
self._compose(prompt, to, subject, attach_path, mime_subtype,
131
148
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
166
183
raise NotImplementedError
185
def _encode_safe(self, u):
186
"""Encode possible unicode string argument to 8-bit string
187
in user_encoding. Unencodable characters will be replaced
190
:param u: possible unicode string.
191
:return: encoded string if u is unicode, u itself otherwise.
193
if isinstance(u, unicode):
194
return u.encode(bzrlib.user_encoding, 'replace')
197
def _encode_path(self, path, kind):
198
"""Encode unicode path in user encoding.
200
:param path: possible unicode path.
201
:param kind: path kind ('executable' or 'attachment').
202
:return: encoded path if path is unicode,
203
path itself otherwise.
204
:raise: UnableEncodePath.
206
if isinstance(path, unicode):
208
return path.encode(bzrlib.user_encoding)
209
except UnicodeEncodeError:
210
raise errors.UnableEncodePath(path, kind)
169
214
class Evolution(ExternalMailClient):
170
215
"""Evolution mail client."""
192
240
"""See ExternalMailClient._get_compose_commandline"""
193
241
message_options = []
194
242
if subject is not None:
195
message_options.extend(['-s', subject ])
243
message_options.extend(['-s', self._encode_safe(subject)])
196
244
if attach_path is not None:
197
message_options.extend(['-a', attach_path])
245
message_options.extend(['-a',
246
self._encode_path(attach_path, 'attachment')])
198
247
if to is not None:
199
message_options.append(to)
248
message_options.append(self._encode_safe(to))
200
249
return message_options
250
mail_client_registry.register('mutt', Mutt,
203
254
class Thunderbird(ExternalMailClient):
213
264
_client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
214
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
265
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
266
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
216
268
def _get_compose_commandline(self, to, subject, attach_path):
217
269
"""See ExternalMailClient._get_compose_commandline"""
218
270
message_options = {}
219
271
if to is not None:
220
message_options['to'] = to
272
message_options['to'] = self._encode_safe(to)
221
273
if subject is not None:
222
message_options['subject'] = subject
274
message_options['subject'] = self._encode_safe(subject)
223
275
if attach_path is not None:
224
276
message_options['attachment'] = urlutils.local_path_to_url(
226
278
options_list = ["%s='%s'" % (k, v) for k, v in
227
279
sorted(message_options.iteritems())]
228
280
return ['-compose', ','.join(options_list)]
281
mail_client_registry.register('thunderbird', Thunderbird,
282
help=Thunderbird.__doc__)
231
285
class KMail(ExternalMailClient):
237
291
"""See ExternalMailClient._get_compose_commandline"""
238
292
message_options = []
239
293
if subject is not None:
240
message_options.extend( ['-s', subject ] )
294
message_options.extend(['-s', self._encode_safe(subject)])
241
295
if attach_path is not None:
242
message_options.extend( ['--attach', attach_path] )
296
message_options.extend(['--attach',
297
self._encode_path(attach_path, 'attachment')])
243
298
if to is not None:
244
message_options.extend( [ to ] )
299
message_options.extend([self._encode_safe(to)])
246
300
return message_options
301
mail_client_registry.register('kmail', KMail,
249
305
class XDGEmail(ExternalMailClient):
254
310
def _get_compose_commandline(self, to, subject, attach_path):
255
311
"""See ExternalMailClient._get_compose_commandline"""
257
if subject is not None:
258
commandline.extend(['--subject', subject])
259
if attach_path is not None:
260
commandline.extend(['--attach', attach_path])
313
raise errors.NoMailAddressSpecified()
314
commandline = [self._encode_safe(to)]
315
if subject is not None:
316
commandline.extend(['--subject', self._encode_safe(subject)])
317
if attach_path is not None:
318
commandline.extend(['--attach',
319
self._encode_path(attach_path, 'attachment')])
321
mail_client_registry.register('xdg-email', XDGEmail,
322
help=XDGEmail.__doc__)
325
class EmacsMail(ExternalMailClient):
326
"""Call emacsclient to have a mail buffer.
328
This only work for emacs >= 22.1 due to recent -e/--eval support.
330
The good news is that this implementation will work with all mail
331
agents registered against ``mail-user-agent``. So there is no need
332
to instantiate ExternalMailClient for each and every GNU Emacs
335
Users just have to ensure that ``mail-user-agent`` is set according
339
_client_commands = ['emacsclient']
341
def _prepare_send_function(self):
342
"""Write our wrapper function into a temporary file.
344
This temporary file will be loaded at runtime in
345
_get_compose_commandline function.
347
This function does not remove the file. That's a wanted
348
behaviour since _get_compose_commandline won't run the send
349
mail function directly but return the eligible command line.
350
Removing our temporary file here would prevent our sendmail
351
function to work. (The file is deleted by some elisp code
352
after being read by Emacs.)
355
_defun = r"""(defun bzr-add-mime-att (file)
356
"Attach FILE to a mail buffer as a MIME attachment."
357
(let ((agent mail-user-agent))
358
(if (and file (file-exists-p file))
360
((eq agent 'sendmail-user-agent)
364
(if (functionp 'etach-attach)
366
(mail-attach-file file))))
367
((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
369
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
370
((eq agent 'mew-user-agent)
372
(mew-draft-prepare-attachments)
373
(mew-attach-link file (file-name-nondirectory file))
374
(let* ((nums (mew-syntax-nums))
375
(syntax (mew-syntax-get-entry mew-encode-syntax nums)))
376
(mew-syntax-set-cd syntax "BZR merge")
377
(mew-encode-syntax-print mew-encode-syntax))
378
(mew-header-goto-body)))
380
(message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
381
(error "File %s does not exist." file))))
384
fd, temp_file = tempfile.mkstemp(prefix="emacs-bzr-send-",
389
os.close(fd) # Just close the handle but do not remove the file.
392
def _get_compose_commandline(self, to, subject, attach_path):
393
commandline = ["--eval"]
399
_to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
400
if subject is not None:
401
_subject = ("\"%s\"" %
402
self._encode_safe(subject).replace('"', '\\"'))
404
# Funcall the default mail composition function
405
# This will work with any mail mode including default mail-mode
406
# User must tweak mail-user-agent variable to tell what function
407
# will be called inside compose-mail.
408
mail_cmd = "(compose-mail %s %s)" % (_to, _subject)
409
commandline.append(mail_cmd)
411
# Try to attach a MIME attachment using our wrapper function
412
if attach_path is not None:
413
# Do not create a file if there is no attachment
414
elisp = self._prepare_send_function()
415
lmmform = '(load "%s")' % elisp
416
mmform = '(bzr-add-mime-att "%s")' % \
417
self._encode_path(attach_path, 'attachment')
418
rmform = '(delete-file "%s")' % elisp
419
commandline.append(lmmform)
420
commandline.append(mmform)
421
commandline.append(rmform)
424
mail_client_registry.register('emacsclient', EmacsMail,
425
help=EmacsMail.__doc__)
264
428
class MAPIClient(ExternalMailClient):
291
457
return XDGEmail(self.config)
293
459
def compose(self, prompt, to, subject, attachment, mime_subtype,
460
extension, basename=None):
295
461
"""See MailClient.compose"""
297
463
return self._mail_client().compose(prompt, to, subject,
298
464
attachment, mimie_subtype,
300
466
except errors.MailClientNotFound:
301
467
return Editor(self.config).compose(prompt, to, subject,
302
468
attachment, mimie_subtype, extension)
304
def compose_merge_request(self, to, subject, directive):
470
def compose_merge_request(self, to, subject, directive, basename=None):
305
471
"""See MailClient.compose_merge_request"""
307
473
return self._mail_client().compose_merge_request(to, subject,
474
directive, basename=basename)
309
475
except errors.MailClientNotFound:
310
476
return Editor(self.config).compose_merge_request(to, subject,
477
directive, basename=basename)
478
mail_client_registry.register('default', DefaultMail,
479
help=DefaultMail.__doc__)
480
mail_client_registry.default_key = 'default'