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.
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:
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
__doc__ = """An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
239
__doc__ = """Evolution mail client."""
214
class Evolution(ExternalMailClient):
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):
261
__doc__ = """Mutt mail client."""
234
class Mutt(ExternalMailClient):
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):
289
__doc__ = """Mozilla Thunderbird (or Icedove)
254
class Thunderbird(ExternalMailClient):
255
"""Mozilla Thunderbird (or Icedove)
291
257
Note that Thunderbird 1.5 is buggy and does not support setting
292
258
"to" simultaneously with including a attachment.
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__)
323
285
class KMail(ExternalMailClient):
324
__doc__ = """KDE mail client."""
286
"""KDE mail client."""
326
288
_client_commands = ['kmail']
340
302
help=KMail.__doc__)
343
class Claws(ExternalMailClient):
344
__doc__ = """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):
390
__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"""
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__)
411
325
class EmacsMail(ExternalMailClient):
412
__doc__ = """Call emacsclient to have a mail buffer.
326
"""Call emacsclient to have a mail buffer.
414
328
This only work for emacs >= 22.1 due to recent -e/--eval support.
518
425
help=EmacsMail.__doc__)
521
class MAPIClient(BodyExternalMailClient):
522
__doc__ = """Default Windows mail client launched using MAPI."""
428
class MAPIClient(ExternalMailClient):
429
"""Default Windows mail client launched using MAPI."""
524
431
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
extension, body=None):
526
433
"""See ExternalMailClient._compose.
528
435
This implementation uses MAPI via the simplemapi ctypes wrapper
530
437
from bzrlib.util import simplemapi
532
simplemapi.SendMail(to or '', subject or '', body or '',
439
simplemapi.SendMail(to or '', subject or '', '', attach_path)
534
440
except simplemapi.MAPIError, e:
535
441
if e.code != simplemapi.MAPI_USER_ABORT:
536
442
raise errors.MailClientNotFound(['MAPI supported mail client'
539
445
help=MAPIClient.__doc__)
542
class MailApp(BodyExternalMailClient):
543
__doc__ = """Use MacOS X's Mail.app for sending email messages.
545
Although it would be nice to use appscript, it's not installed
546
with the shipped Python installations. We instead build an
547
AppleScript and invoke the script using osascript(1). We don't
548
use the _encode_safe() routines as it's not clear what encoding
549
osascript expects the script to be in.
552
_client_commands = ['osascript']
554
def _get_compose_commandline(self, to, subject, attach_path, body=None,
556
"""See ExternalMailClient._get_compose_commandline"""
558
fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
561
os.write(fd, 'tell application "Mail"\n')
562
os.write(fd, 'set newMessage to make new outgoing message\n')
563
os.write(fd, 'tell newMessage\n')
565
os.write(fd, 'make new to recipient with properties'
566
' {address:"%s"}\n' % to)
567
if from_ is not None:
568
# though from_ doesn't actually seem to be used
569
os.write(fd, 'set sender to "%s"\n'
570
% sender.replace('"', '\\"'))
571
if subject is not None:
572
os.write(fd, 'set subject to "%s"\n'
573
% subject.replace('"', '\\"'))
575
# FIXME: would be nice to prepend the body to the
576
# existing content (e.g., preserve signature), but
577
# can't seem to figure out the right applescript
579
os.write(fd, 'set content to "%s\\n\n"\n' %
580
body.replace('"', '\\"').replace('\n', '\\n'))
582
if attach_path is not None:
583
# FIXME: would be nice to first append a newline to
584
# ensure the attachment is on a new paragraph, but
585
# can't seem to figure out the right applescript
587
os.write(fd, 'tell content to make new attachment'
588
' with properties {file name:"%s"}'
589
' at after the last paragraph\n'
590
% self._encode_path(attach_path, 'attachment'))
591
os.write(fd, 'set visible to true\n')
592
os.write(fd, 'end tell\n')
593
os.write(fd, 'end tell\n')
595
os.close(fd) # Just close the handle but do not remove the file.
596
return [self.temp_file]
597
mail_client_registry.register('mail.app', MailApp,
598
help=MailApp.__doc__)
601
448
class DefaultMail(MailClient):
602
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
449
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
603
450
falls back to Editor"""
607
452
def _mail_client(self):
608
453
"""Determine the preferred mail client for this platform"""
609
454
if osutils.supports_mapi():
612
457
return XDGEmail(self.config)
614
459
def compose(self, prompt, to, subject, attachment, mime_subtype,
615
extension, basename=None, body=None):
460
extension, basename=None):
616
461
"""See MailClient.compose"""
618
463
return self._mail_client().compose(prompt, to, subject,
619
464
attachment, mimie_subtype,
620
extension, basename, body)
621
466
except errors.MailClientNotFound:
622
467
return Editor(self.config).compose(prompt, to, subject,
623
attachment, mimie_subtype, extension, body)
468
attachment, mimie_subtype, extension)
625
def compose_merge_request(self, to, subject, directive, basename=None,
470
def compose_merge_request(self, to, subject, directive, basename=None):
627
471
"""See MailClient.compose_merge_request"""
629
473
return self._mail_client().compose_merge_request(to, subject,
630
directive, basename=basename, body=body)
474
directive, basename=basename)
631
475
except errors.MailClientNotFound:
632
476
return Editor(self.config).compose_merge_request(to, subject,
633
directive, basename=basename, body=body)
477
directive, basename=basename)
634
478
mail_client_registry.register('default', DefaultMail,
635
479
help=DefaultMail.__doc__)
636
480
mail_client_registry.default_key = 'default'