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