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 (
40
41
self.config = config
42
43
def compose(self, prompt, to, subject, attachment, mime_subtype,
43
extension, basename=None):
44
extension, basename=None, body=None):
44
45
"""Compose (and possibly send) an email message
46
47
Must be implemented by subclasses.
61
62
raise NotImplementedError
63
def compose_merge_request(self, to, subject, directive, basename=None):
64
def compose_merge_request(self, to, subject, directive, basename=None,
64
66
"""Compose (and possibly send) a merge request
66
68
:param to: The address to send the request to
73
75
prompt = self._get_merge_prompt("Please describe these changes:", to,
74
76
subject, directive)
75
77
self.compose(prompt, to, subject, directive,
76
'x-patch', '.patch', basename)
78
'x-patch', '.patch', basename, body)
78
80
def _get_merge_prompt(self, prompt, to, subject, attachment):
79
81
"""Generate a prompt string. Overridden by Editor.
98
102
attachment.decode('utf-8', 'replace')))
100
104
def compose(self, prompt, to, subject, attachment, mime_subtype,
101
extension, basename=None):
105
extension, basename=None, body=None):
102
106
"""See MailClient.compose"""
104
108
raise errors.NoMailAddressSpecified()
105
body = msgeditor.edit_commit_message(prompt)
109
body = msgeditor.edit_commit_message(prompt, start_message=body)
107
111
raise errors.NoMessageSupplied()
108
112
email_message.EmailMessage.send(self.config,
128
133
return self._client_commands
130
135
def compose(self, prompt, to, subject, attachment, mime_subtype,
131
extension, basename=None):
136
extension, basename=None, body=None):
132
137
"""See MailClient.compose.
134
139
Writes the attachment to a temporary file, invokes _compose.
142
147
outfile.write(attachment)
151
kwargs = {'body': body}
145
154
self._compose(prompt, to, subject, attach_path, mime_subtype,
148
157
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
extension, body=None, from_=None):
150
159
"""Invoke a mail client as a commandline process.
152
161
Overridden by MAPIClient.
157
166
"text", but the precise subtype can be specified here
158
167
:param extension: A file extension (including period) associated with
159
168
the attachment type.
169
:param body: Optional body text.
170
:param from_: Optional From: header.
161
172
for name in self._get_client_commands():
162
173
cmdline = [self._encode_path(name, 'executable')]
175
kwargs = {'body': body}
178
if from_ is not None:
179
kwargs['from_'] = from_
163
180
cmdline.extend(self._get_compose_commandline(to, subject,
166
184
subprocess.call(cmdline)
167
185
except OSError, e:
173
191
raise errors.MailClientNotFound(self._client_commands)
175
def _get_compose_commandline(self, to, subject, attach_path):
193
def _get_compose_commandline(self, to, subject, attach_path, body):
176
194
"""Determine the commandline to use for composing a message
178
196
Implemented by various subclasses
191
209
:return: encoded string if u is unicode, u itself otherwise.
193
211
if isinstance(u, unicode):
194
return u.encode(bzrlib.user_encoding, 'replace')
212
return u.encode(osutils.get_user_encoding(), 'replace')
197
215
def _encode_path(self, path, kind):
206
224
if isinstance(path, unicode):
208
return path.encode(bzrlib.user_encoding)
226
return path.encode(osutils.get_user_encoding())
209
227
except UnicodeEncodeError:
210
228
raise errors.UnableEncodePath(path, kind)
214
class Evolution(ExternalMailClient):
232
class ExternalMailClient(BodyExternalMailClient):
233
"""An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
215
239
"""Evolution mail client."""
217
241
_client_commands = ['evolution']
219
def _get_compose_commandline(self, to, subject, attach_path):
243
def _get_compose_commandline(self, to, subject, attach_path, body=None):
220
244
"""See ExternalMailClient._get_compose_commandline"""
221
245
message_options = {}
222
246
if subject is not None:
223
247
message_options['subject'] = subject
224
248
if attach_path is not None:
225
249
message_options['attach'] = attach_path
251
message_options['body'] = body
226
252
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
227
253
sorted(message_options.iteritems())]
228
254
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
231
257
help=Evolution.__doc__)
234
class Mutt(ExternalMailClient):
260
class Mutt(BodyExternalMailClient):
235
261
"""Mutt mail client."""
237
263
_client_commands = ['mutt']
239
def _get_compose_commandline(self, to, subject, attach_path):
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
240
266
"""See ExternalMailClient._get_compose_commandline"""
241
267
message_options = []
242
268
if subject is not None:
244
270
if attach_path is not None:
245
271
message_options.extend(['-a',
246
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])
247
281
if to is not None:
248
message_options.append(self._encode_safe(to))
282
message_options.extend(['--', self._encode_safe(to)])
249
283
return message_options
250
284
mail_client_registry.register('mutt', Mutt,
251
285
help=Mutt.__doc__)
254
class Thunderbird(ExternalMailClient):
288
class Thunderbird(BodyExternalMailClient):
255
289
"""Mozilla Thunderbird (or Icedove)
257
291
Note that Thunderbird 1.5 is buggy and does not support setting
265
299
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
266
300
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
268
def _get_compose_commandline(self, to, subject, attach_path):
302
def _get_compose_commandline(self, to, subject, attach_path, body=None):
269
303
"""See ExternalMailClient._get_compose_commandline"""
270
304
message_options = {}
271
305
if to is not None:
275
309
if attach_path is not None:
276
310
message_options['attachment'] = urlutils.local_path_to_url(
278
options_list = ["%s='%s'" % (k, v) for k, v in
279
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())])
280
318
return ['-compose', ','.join(options_list)]
281
319
mail_client_registry.register('thunderbird', Thunderbird,
282
320
help=Thunderbird.__doc__)
302
340
help=KMail.__doc__)
305
class XDGEmail(ExternalMailClient):
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):
306
390
"""xdg-email attempts to invoke the user's preferred mail client"""
308
392
_client_commands = ['xdg-email']
310
def _get_compose_commandline(self, to, subject, attach_path):
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
311
395
"""See ExternalMailClient._get_compose_commandline"""
313
397
raise errors.NoMailAddressSpecified()
317
401
if attach_path is not None:
318
402
commandline.extend(['--attach',
319
403
self._encode_path(attach_path, 'attachment')])
405
commandline.extend(['--body', self._encode_safe(body)])
320
406
return commandline
321
407
mail_client_registry.register('xdg-email', XDGEmail,
322
408
help=XDGEmail.__doc__)
364
450
(if (functionp 'etach-attach)
365
451
(etach-attach file)
366
452
(mail-attach-file file))))
367
((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))
369
457
(mml-attach-file file "text/x-patch" "BZR merge" "inline")))
370
458
((eq agent 'mew-user-agent)
425
513
help=EmacsMail.__doc__)
428
class MAPIClient(ExternalMailClient):
516
class MAPIClient(BodyExternalMailClient):
429
517
"""Default Windows mail client launched using MAPI."""
431
519
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
extension, body=None):
433
521
"""See ExternalMailClient._compose.
435
523
This implementation uses MAPI via the simplemapi ctypes wrapper
437
525
from bzrlib.util import simplemapi
439
simplemapi.SendMail(to or '', subject or '', '', attach_path)
527
simplemapi.SendMail(to or '', subject or '', body or '',
440
529
except simplemapi.MAPIError, e:
441
530
if e.code != simplemapi.MAPI_USER_ABORT:
442
531
raise errors.MailClientNotFound(['MAPI supported mail client'
457
548
return XDGEmail(self.config)
459
550
def compose(self, prompt, to, subject, attachment, mime_subtype,
460
extension, basename=None):
551
extension, basename=None, body=None):
461
552
"""See MailClient.compose"""
463
554
return self._mail_client().compose(prompt, to, subject,
464
555
attachment, mimie_subtype,
556
extension, basename, body)
466
557
except errors.MailClientNotFound:
467
558
return Editor(self.config).compose(prompt, to, subject,
468
attachment, mimie_subtype, extension)
559
attachment, mimie_subtype, extension, body)
470
def compose_merge_request(self, to, subject, directive, basename=None):
561
def compose_merge_request(self, to, subject, directive, basename=None,
471
563
"""See MailClient.compose_merge_request"""
473
565
return self._mail_client().compose_merge_request(to, subject,
474
directive, basename=basename)
566
directive, basename=basename, body=body)
475
567
except errors.MailClientNotFound:
476
568
return Editor(self.config).compose_merge_request(to, subject,
477
directive, basename=basename)
569
directive, basename=basename, body=body)
478
570
mail_client_registry.register('default', DefaultMail,
479
571
help=DefaultMail.__doc__)
480
572
mail_client_registry.default_key = 'default'