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
17
from __future__ import absolute_import
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
43
40
self.config = config
45
42
def compose(self, prompt, to, subject, attachment, mime_subtype,
46
extension, basename=None, body=None):
43
extension, basename=None):
47
44
"""Compose (and possibly send) an email message
49
46
Must be implemented by subclasses.
64
61
raise NotImplementedError
66
def compose_merge_request(self, to, subject, directive, basename=None,
63
def compose_merge_request(self, to, subject, directive, basename=None):
68
64
"""Compose (and possibly send) a merge request
70
66
:param to: The address to send the request to
77
73
prompt = self._get_merge_prompt("Please describe these changes:", to,
78
74
subject, directive)
79
75
self.compose(prompt, to, subject, directive,
80
'x-patch', '.patch', basename, body)
76
'x-patch', '.patch', basename)
82
78
def _get_merge_prompt(self, prompt, to, subject, attachment):
83
79
"""Generate a prompt string. Overridden by Editor.
104
98
attachment.decode('utf-8', 'replace')))
106
100
def compose(self, prompt, to, subject, attachment, mime_subtype,
107
extension, basename=None, body=None):
101
extension, basename=None):
108
102
"""See MailClient.compose"""
110
104
raise errors.NoMailAddressSpecified()
111
body = msgeditor.edit_commit_message(prompt, start_message=body)
105
body = msgeditor.edit_commit_message(prompt)
113
107
raise errors.NoMessageSupplied()
114
108
email_message.EmailMessage.send(self.config,
115
self.config.get('email'),
109
self.config.username(),
135
128
return self._client_commands
137
130
def compose(self, prompt, to, subject, attachment, mime_subtype,
138
extension, basename=None, body=None):
131
extension, basename=None):
139
132
"""See MailClient.compose.
141
134
Writes the attachment to a temporary file, invokes _compose.
149
142
outfile.write(attachment)
153
kwargs = {'body': body}
156
145
self._compose(prompt, to, subject, attach_path, mime_subtype,
159
148
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
160
extension, body=None, from_=None):
161
150
"""Invoke a mail client as a commandline process.
163
152
Overridden by MAPIClient.
168
157
"text", but the precise subtype can be specified here
169
158
:param extension: A file extension (including period) associated with
170
159
the attachment type.
171
:param body: Optional body text.
172
:param from_: Optional From: header.
174
161
for name in self._get_client_commands():
175
162
cmdline = [self._encode_path(name, 'executable')]
177
kwargs = {'body': body}
180
if from_ is not None:
181
kwargs['from_'] = from_
182
163
cmdline.extend(self._get_compose_commandline(to, subject,
186
166
subprocess.call(cmdline)
187
167
except OSError, e:
226
206
if isinstance(path, unicode):
228
return path.encode(osutils.get_user_encoding())
208
return path.encode(bzrlib.user_encoding)
229
209
except UnicodeEncodeError:
230
210
raise errors.UnableEncodePath(path, kind)
234
class ExternalMailClient(BodyExternalMailClient):
235
__doc__ = """An external mail client."""
237
supports_body = False
240
class Evolution(BodyExternalMailClient):
241
__doc__ = """Evolution mail client."""
214
class Evolution(ExternalMailClient):
215
"""Evolution mail client."""
243
217
_client_commands = ['evolution']
245
def _get_compose_commandline(self, to, subject, attach_path, body=None):
219
def _get_compose_commandline(self, to, subject, attach_path):
246
220
"""See ExternalMailClient._get_compose_commandline"""
247
221
message_options = {}
248
222
if subject is not None:
249
223
message_options['subject'] = subject
250
224
if attach_path is not None:
251
225
message_options['attach'] = attach_path
253
message_options['body'] = body
254
226
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
255
227
sorted(message_options.iteritems())]
256
228
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
259
231
help=Evolution.__doc__)
262
class Mutt(BodyExternalMailClient):
263
__doc__ = """Mutt mail client."""
234
class Mutt(ExternalMailClient):
235
"""Mutt mail client."""
265
237
_client_commands = ['mutt']
267
def _get_compose_commandline(self, to, subject, attach_path, body=None):
239
def _get_compose_commandline(self, to, subject, attach_path):
268
240
"""See ExternalMailClient._get_compose_commandline"""
269
241
message_options = []
270
242
if subject is not None:
272
244
if attach_path is not None:
273
245
message_options.extend(['-a',
274
246
self._encode_path(attach_path, 'attachment')])
276
# Store the temp file object in self, so that it does not get
277
# garbage collected and delete the file before mutt can read it.
278
self._temp_file = tempfile.NamedTemporaryFile(
279
prefix="mutt-body-", suffix=".txt")
280
self._temp_file.write(body)
281
self._temp_file.flush()
282
message_options.extend(['-i', self._temp_file.name])
283
247
if to is not None:
284
message_options.extend(['--', self._encode_safe(to)])
248
message_options.append(self._encode_safe(to))
285
249
return message_options
286
250
mail_client_registry.register('mutt', Mutt,
287
251
help=Mutt.__doc__)
290
class Thunderbird(BodyExternalMailClient):
291
__doc__ = """Mozilla Thunderbird (or Icedove)
254
class Thunderbird(ExternalMailClient):
255
"""Mozilla Thunderbird (or Icedove)
293
257
Note that Thunderbird 1.5 is buggy and does not support setting
294
258
"to" simultaneously with including a attachment.
301
265
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
302
266
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
304
def _get_compose_commandline(self, to, subject, attach_path, body=None):
268
def _get_compose_commandline(self, to, subject, attach_path):
305
269
"""See ExternalMailClient._get_compose_commandline"""
306
270
message_options = {}
307
271
if to is not None:
311
275
if attach_path is not None:
312
276
message_options['attachment'] = urlutils.local_path_to_url(
315
options_list = ['body=%s' % urlutils.quote(self._encode_safe(body))]
318
options_list.extend(["%s='%s'" % (k, v) for k, v in
319
sorted(message_options.iteritems())])
278
options_list = ["%s='%s'" % (k, v) for k, v in
279
sorted(message_options.iteritems())]
320
280
return ['-compose', ','.join(options_list)]
321
281
mail_client_registry.register('thunderbird', Thunderbird,
322
282
help=Thunderbird.__doc__)
325
285
class KMail(ExternalMailClient):
326
__doc__ = """KDE mail client."""
286
"""KDE mail client."""
328
288
_client_commands = ['kmail']
342
302
help=KMail.__doc__)
345
class Claws(ExternalMailClient):
346
__doc__ = """Claws mail client."""
350
_client_commands = ['claws-mail']
352
def _get_compose_commandline(self, to, subject, attach_path, body=None,
354
"""See ExternalMailClient._get_compose_commandline"""
356
if from_ is not None:
357
compose_url.append('from=' + urlutils.quote(from_))
358
if subject is not None:
359
# Don't use urlutils.quote_plus because Claws doesn't seem
360
# to recognise spaces encoded as "+".
362
'subject=' + urlutils.quote(self._encode_safe(subject)))
365
'body=' + urlutils.quote(self._encode_safe(body)))
366
# to must be supplied for the claws-mail --compose syntax to work.
368
raise errors.NoMailAddressSpecified()
369
compose_url = 'mailto:%s?%s' % (
370
self._encode_safe(to), '&'.join(compose_url))
371
# Collect command-line options.
372
message_options = ['--compose', compose_url]
373
if attach_path is not None:
374
message_options.extend(
375
['--attach', self._encode_path(attach_path, 'attachment')])
376
return message_options
378
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
379
extension, body=None, from_=None):
380
"""See ExternalMailClient._compose"""
382
from_ = self.config.get('email')
383
super(Claws, self)._compose(prompt, to, subject, attach_path,
384
mime_subtype, extension, body, from_)
387
mail_client_registry.register('claws', Claws,
391
class XDGEmail(BodyExternalMailClient):
392
__doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
305
class XDGEmail(ExternalMailClient):
306
"""xdg-email attempts to invoke the user's preferred mail client"""
394
308
_client_commands = ['xdg-email']
396
def _get_compose_commandline(self, to, subject, attach_path, body=None):
310
def _get_compose_commandline(self, to, subject, attach_path):
397
311
"""See ExternalMailClient._get_compose_commandline"""
399
313
raise errors.NoMailAddressSpecified()
403
317
if attach_path is not None:
404
318
commandline.extend(['--attach',
405
319
self._encode_path(attach_path, 'attachment')])
407
commandline.extend(['--body', self._encode_safe(body)])
408
320
return commandline
409
321
mail_client_registry.register('xdg-email', XDGEmail,
410
322
help=XDGEmail.__doc__)
413
325
class EmacsMail(ExternalMailClient):
414
__doc__ = """Call emacsclient to have a mail buffer.
326
"""Call emacsclient to have a mail buffer.
416
328
This only work for emacs >= 22.1 due to recent -e/--eval support.
520
425
help=EmacsMail.__doc__)
523
class MAPIClient(BodyExternalMailClient):
524
__doc__ = """Default Windows mail client launched using MAPI."""
428
class MAPIClient(ExternalMailClient):
429
"""Default Windows mail client launched using MAPI."""
526
431
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
527
extension, body=None):
528
433
"""See ExternalMailClient._compose.
530
435
This implementation uses MAPI via the simplemapi ctypes wrapper
532
437
from bzrlib.util import simplemapi
534
simplemapi.SendMail(to or '', subject or '', body or '',
439
simplemapi.SendMail(to or '', subject or '', '', attach_path)
536
440
except simplemapi.MAPIError, e:
537
441
if e.code != simplemapi.MAPI_USER_ABORT:
538
442
raise errors.MailClientNotFound(['MAPI supported mail client'
541
445
help=MAPIClient.__doc__)
544
class MailApp(BodyExternalMailClient):
545
__doc__ = """Use MacOS X's Mail.app for sending email messages.
547
Although it would be nice to use appscript, it's not installed
548
with the shipped Python installations. We instead build an
549
AppleScript and invoke the script using osascript(1). We don't
550
use the _encode_safe() routines as it's not clear what encoding
551
osascript expects the script to be in.
554
_client_commands = ['osascript']
556
def _get_compose_commandline(self, to, subject, attach_path, body=None,
558
"""See ExternalMailClient._get_compose_commandline"""
560
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
563
os.write(fd, 'tell application "Mail"\n')
564
os.write(fd, 'set newMessage to make new outgoing message\n')
565
os.write(fd, 'tell newMessage\n')
567
os.write(fd, 'make new to recipient with properties'
568
' {address:"%s"}\n' % to)
569
if from_ is not None:
570
# though from_ doesn't actually seem to be used
571
os.write(fd, 'set sender to "%s"\n'
572
% sender.replace('"', '\\"'))
573
if subject is not None:
574
os.write(fd, 'set subject to "%s"\n'
575
% subject.replace('"', '\\"'))
577
# FIXME: would be nice to prepend the body to the
578
# existing content (e.g., preserve signature), but
579
# can't seem to figure out the right applescript
581
os.write(fd, 'set content to "%s\\n\n"\n' %
582
body.replace('"', '\\"').replace('\n', '\\n'))
584
if attach_path is not None:
585
# FIXME: would be nice to first append a newline to
586
# ensure the attachment is on a new paragraph, but
587
# can't seem to figure out the right applescript
589
os.write(fd, 'tell content to make new attachment'
590
' with properties {file name:"%s"}'
591
' at after the last paragraph\n'
592
% self._encode_path(attach_path, 'attachment'))
593
os.write(fd, 'set visible to true\n')
594
os.write(fd, 'end tell\n')
595
os.write(fd, 'end tell\n')
597
os.close(fd) # Just close the handle but do not remove the file.
598
return [self.temp_file]
599
mail_client_registry.register('mail.app', MailApp,
600
help=MailApp.__doc__)
603
448
class DefaultMail(MailClient):
604
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
449
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
605
450
falls back to Editor"""
609
452
def _mail_client(self):
610
453
"""Determine the preferred mail client for this platform"""
611
454
if osutils.supports_mapi():
614
457
return XDGEmail(self.config)
616
459
def compose(self, prompt, to, subject, attachment, mime_subtype,
617
extension, basename=None, body=None):
460
extension, basename=None):
618
461
"""See MailClient.compose"""
620
463
return self._mail_client().compose(prompt, to, subject,
621
attachment, mime_subtype,
622
extension, basename, body)
464
attachment, mimie_subtype,
623
466
except errors.MailClientNotFound:
624
467
return Editor(self.config).compose(prompt, to, subject,
625
attachment, mime_subtype, extension, body)
468
attachment, mimie_subtype, extension)
627
def compose_merge_request(self, to, subject, directive, basename=None,
470
def compose_merge_request(self, to, subject, directive, basename=None):
629
471
"""See MailClient.compose_merge_request"""
631
473
return self._mail_client().compose_merge_request(to, subject,
632
directive, basename=basename, body=body)
474
directive, basename=basename)
633
475
except errors.MailClientNotFound:
634
476
return Editor(self.config).compose_merge_request(to, subject,
635
directive, basename=basename, body=body)
477
directive, basename=basename)
636
478
mail_client_registry.register('default', DefaultMail,
637
479
help=DefaultMail.__doc__)
638
480
mail_client_registry.default_key = 'default'
640
opt_mail_client = _mod_config.RegistryOption('mail_client',
641
mail_client_registry, help='E-mail client to use.', invalid='error')