13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
24
25
from bzrlib import (
37
41
self.config = config
39
43
def compose(self, prompt, to, subject, attachment, mime_subtype,
40
extension, basename=None):
44
extension, basename=None, body=None):
41
45
"""Compose (and possibly send) an email message
43
47
Must be implemented by subclasses.
58
62
raise NotImplementedError
60
def compose_merge_request(self, to, subject, directive, basename=None):
64
def compose_merge_request(self, to, subject, directive, basename=None,
61
66
"""Compose (and possibly send) a merge request
63
68
:param to: The address to send the request to
70
75
prompt = self._get_merge_prompt("Please describe these changes:", to,
71
76
subject, directive)
72
77
self.compose(prompt, to, subject, directive,
73
'x-patch', '.patch', basename)
78
'x-patch', '.patch', basename, body)
75
80
def _get_merge_prompt(self, prompt, to, subject, attachment):
76
81
"""Generate a prompt string. Overridden by Editor.
95
102
attachment.decode('utf-8', 'replace')))
97
104
def compose(self, prompt, to, subject, attachment, mime_subtype,
98
extension, basename=None):
105
extension, basename=None, body=None):
99
106
"""See MailClient.compose"""
101
108
raise errors.NoMailAddressSpecified()
102
body = msgeditor.edit_commit_message(prompt)
109
body = msgeditor.edit_commit_message(prompt, start_message=body)
104
111
raise errors.NoMessageSupplied()
105
112
email_message.EmailMessage.send(self.config,
111
118
attachment_mime_subtype=mime_subtype)
114
class ExternalMailClient(MailClient):
115
"""An external mail client."""
119
mail_client_registry.register('editor', Editor,
123
class BodyExternalMailClient(MailClient):
117
127
def _get_client_commands(self):
118
128
"""Provide a list of commands that may invoke the mail client"""
123
133
return self._client_commands
125
135
def compose(self, prompt, to, subject, attachment, mime_subtype,
126
extension, basename=None):
136
extension, basename=None, body=None):
127
137
"""See MailClient.compose.
129
139
Writes the attachment to a temporary file, invokes _compose.
131
141
if basename is None:
132
142
basename = 'attachment'
133
pathname = tempfile.mkdtemp(prefix='bzr-mail-')
143
pathname = osutils.mkdtemp(prefix='bzr-mail-')
134
144
attach_path = osutils.pathjoin(pathname, basename + extension)
135
145
outfile = open(attach_path, 'wb')
137
147
outfile.write(attachment)
151
kwargs = {'body': body}
140
154
self._compose(prompt, to, subject, attach_path, mime_subtype,
143
157
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
extension, body=None, from_=None):
145
159
"""Invoke a mail client as a commandline process.
147
161
Overridden by MAPIClient.
152
166
"text", but the precise subtype can be specified here
153
167
:param extension: A file extension (including period) associated with
154
168
the attachment type.
169
:param body: Optional body text.
170
:param from_: Optional From: header.
156
172
for name in self._get_client_commands():
157
173
cmdline = [self._encode_path(name, 'executable')]
175
kwargs = {'body': body}
178
if from_ is not None:
179
kwargs['from_'] = from_
158
180
cmdline.extend(self._get_compose_commandline(to, subject,
161
184
subprocess.call(cmdline)
162
185
except OSError, e:
168
191
raise errors.MailClientNotFound(self._client_commands)
170
def _get_compose_commandline(self, to, subject, attach_path):
193
def _get_compose_commandline(self, to, subject, attach_path, body):
171
194
"""Determine the commandline to use for composing a message
173
196
Implemented by various subclasses
186
209
:return: encoded string if u is unicode, u itself otherwise.
188
211
if isinstance(u, unicode):
189
return u.encode(bzrlib.user_encoding, 'replace')
212
return u.encode(osutils.get_user_encoding(), 'replace')
192
215
def _encode_path(self, path, kind):
201
224
if isinstance(path, unicode):
203
return path.encode(bzrlib.user_encoding)
226
return path.encode(osutils.get_user_encoding())
204
227
except UnicodeEncodeError:
205
228
raise errors.UnableEncodePath(path, kind)
209
class Evolution(ExternalMailClient):
232
class ExternalMailClient(BodyExternalMailClient):
233
"""An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
210
239
"""Evolution mail client."""
212
241
_client_commands = ['evolution']
214
def _get_compose_commandline(self, to, subject, attach_path):
243
def _get_compose_commandline(self, to, subject, attach_path, body=None):
215
244
"""See ExternalMailClient._get_compose_commandline"""
216
245
message_options = {}
217
246
if subject is not None:
218
247
message_options['subject'] = subject
219
248
if attach_path is not None:
220
249
message_options['attach'] = attach_path
251
message_options['body'] = body
221
252
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
222
253
sorted(message_options.iteritems())]
223
254
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
224
255
'&'.join(options_list))]
227
class Mutt(ExternalMailClient):
256
mail_client_registry.register('evolution', Evolution,
257
help=Evolution.__doc__)
260
class Mutt(BodyExternalMailClient):
228
261
"""Mutt mail client."""
230
263
_client_commands = ['mutt']
232
def _get_compose_commandline(self, to, subject, attach_path):
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
233
266
"""See ExternalMailClient._get_compose_commandline"""
234
267
message_options = []
235
268
if subject is not None:
237
270
if attach_path is not None:
238
271
message_options.extend(['-a',
239
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])
240
281
if to is not None:
241
message_options.append(self._encode_safe(to))
282
message_options.extend(['--', self._encode_safe(to)])
242
283
return message_options
245
class Thunderbird(ExternalMailClient):
284
mail_client_registry.register('mutt', Mutt,
288
class Thunderbird(BodyExternalMailClient):
246
289
"""Mozilla Thunderbird (or Icedove)
248
291
Note that Thunderbird 1.5 is buggy and does not support setting
255
298
_client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
256
'/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']
258
def _get_compose_commandline(self, to, subject, attach_path):
302
def _get_compose_commandline(self, to, subject, attach_path, body=None):
259
303
"""See ExternalMailClient._get_compose_commandline"""
260
304
message_options = {}
261
305
if to is not None:
265
309
if attach_path is not None:
266
310
message_options['attachment'] = urlutils.local_path_to_url(
268
options_list = ["%s='%s'" % (k, v) for k, v in
269
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())])
270
318
return ['-compose', ','.join(options_list)]
319
mail_client_registry.register('thunderbird', Thunderbird,
320
help=Thunderbird.__doc__)
273
323
class KMail(ExternalMailClient):
286
336
if to is not None:
287
337
message_options.extend([self._encode_safe(to)])
288
338
return message_options
291
class XDGEmail(ExternalMailClient):
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):
292
390
"""xdg-email attempts to invoke the user's preferred mail client"""
294
392
_client_commands = ['xdg-email']
296
def _get_compose_commandline(self, to, subject, attach_path):
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
297
395
"""See ExternalMailClient._get_compose_commandline"""
299
397
raise errors.NoMailAddressSpecified()
303
401
if attach_path is not None:
304
402
commandline.extend(['--attach',
305
403
self._encode_path(attach_path, 'attachment')])
405
commandline.extend(['--body', self._encode_safe(body)])
306
406
return commandline
407
mail_client_registry.register('xdg-email', XDGEmail,
408
help=XDGEmail.__doc__)
309
411
class EmacsMail(ExternalMailClient):
348
450
(if (functionp 'etach-attach)
349
451
(etach-attach file)
350
452
(mail-attach-file file))))
351
((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
453
((or (eq agent 'message-user-agent)
454
(eq agent 'gnus-user-agent)
455
(eq agent 'mh-e-user-agent))
353
457
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
354
458
((eq agent 'mew-user-agent)
405
509
commandline.append(rmform)
407
511
return commandline
410
class MAPIClient(ExternalMailClient):
512
mail_client_registry.register('emacsclient', EmacsMail,
513
help=EmacsMail.__doc__)
516
class MAPIClient(BodyExternalMailClient):
411
517
"""Default Windows mail client launched using MAPI."""
413
519
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
extension, body=None):
415
521
"""See ExternalMailClient._compose.
417
523
This implementation uses MAPI via the simplemapi ctypes wrapper
419
525
from bzrlib.util import simplemapi
421
simplemapi.SendMail(to or '', subject or '', '', attach_path)
527
simplemapi.SendMail(to or '', subject or '', body or '',
422
529
except simplemapi.MAPIError, e:
423
530
if e.code != simplemapi.MAPI_USER_ABORT:
424
531
raise errors.MailClientNotFound(['MAPI supported mail client'
425
532
' (error %d)' % (e.code,)])
533
mail_client_registry.register('mapi', MAPIClient,
534
help=MAPIClient.__doc__)
428
537
class DefaultMail(MailClient):
429
538
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
430
539
falls back to Editor"""
432
543
def _mail_client(self):
433
544
"""Determine the preferred mail client for this platform"""
434
545
if osutils.supports_mapi():
437
548
return XDGEmail(self.config)
439
550
def compose(self, prompt, to, subject, attachment, mime_subtype,
440
extension, basename=None):
551
extension, basename=None, body=None):
441
552
"""See MailClient.compose"""
443
554
return self._mail_client().compose(prompt, to, subject,
444
555
attachment, mimie_subtype,
556
extension, basename, body)
446
557
except errors.MailClientNotFound:
447
558
return Editor(self.config).compose(prompt, to, subject,
448
attachment, mimie_subtype, extension)
559
attachment, mimie_subtype, extension, body)
450
def compose_merge_request(self, to, subject, directive, basename=None):
561
def compose_merge_request(self, to, subject, directive, basename=None,
451
563
"""See MailClient.compose_merge_request"""
453
565
return self._mail_client().compose_merge_request(to, subject,
454
directive, basename=basename)
566
directive, basename=basename, body=body)
455
567
except errors.MailClientNotFound:
456
568
return Editor(self.config).compose_merge_request(to, subject,
457
directive, basename=basename)
569
directive, basename=basename, body=body)
570
mail_client_registry.register('default', DefaultMail,
571
help=DefaultMail.__doc__)
572
mail_client_registry.default_key = 'default'