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:
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):
215
"""Evolution mail client."""
232
class ExternalMailClient(BodyExternalMailClient):
233
__doc__ = """An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
239
__doc__ = """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):
235
"""Mutt mail client."""
260
class Mutt(BodyExternalMailClient):
261
__doc__ = """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):
255
"""Mozilla Thunderbird (or Icedove)
288
class Thunderbird(BodyExternalMailClient):
289
__doc__ = """Mozilla Thunderbird (or Icedove)
257
291
Note that Thunderbird 1.5 is buggy and does not support setting
258
292
"to" simultaneously with including a attachment.
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__)
285
323
class KMail(ExternalMailClient):
286
"""KDE mail client."""
324
__doc__ = """KDE mail client."""
288
326
_client_commands = ['kmail']
302
340
help=KMail.__doc__)
305
class XDGEmail(ExternalMailClient):
306
"""xdg-email attempts to invoke the user's preferred mail client"""
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"""
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__)
325
411
class EmacsMail(ExternalMailClient):
326
"""Call emacsclient to have a mail buffer.
412
__doc__ = """Call emacsclient to have a mail buffer.
328
414
This only work for emacs >= 22.1 due to recent -e/--eval support.
425
518
help=EmacsMail.__doc__)
428
class MAPIClient(ExternalMailClient):
429
"""Default Windows mail client launched using MAPI."""
521
class MAPIClient(BodyExternalMailClient):
522
__doc__ = """Default Windows mail client launched using MAPI."""
431
524
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
extension, body=None):
433
526
"""See ExternalMailClient._compose.
435
528
This implementation uses MAPI via the simplemapi ctypes wrapper
437
530
from bzrlib.util import simplemapi
439
simplemapi.SendMail(to or '', subject or '', '', attach_path)
532
simplemapi.SendMail(to or '', subject or '', body or '',
440
534
except simplemapi.MAPIError, e:
441
535
if e.code != simplemapi.MAPI_USER_ABORT:
442
536
raise errors.MailClientNotFound(['MAPI supported mail client'
445
539
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__)
448
601
class DefaultMail(MailClient):
449
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
602
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
450
603
falls back to Editor"""
452
607
def _mail_client(self):
453
608
"""Determine the preferred mail client for this platform"""
454
609
if osutils.supports_mapi():
457
612
return XDGEmail(self.config)
459
614
def compose(self, prompt, to, subject, attachment, mime_subtype,
460
extension, basename=None):
615
extension, basename=None, body=None):
461
616
"""See MailClient.compose"""
463
618
return self._mail_client().compose(prompt, to, subject,
464
619
attachment, mimie_subtype,
620
extension, basename, body)
466
621
except errors.MailClientNotFound:
467
622
return Editor(self.config).compose(prompt, to, subject,
468
attachment, mimie_subtype, extension)
623
attachment, mimie_subtype, extension, body)
470
def compose_merge_request(self, to, subject, directive, basename=None):
625
def compose_merge_request(self, to, subject, directive, basename=None,
471
627
"""See MailClient.compose_merge_request"""
473
629
return self._mail_client().compose_merge_request(to, subject,
474
directive, basename=basename)
630
directive, basename=basename, body=body)
475
631
except errors.MailClientNotFound:
476
632
return Editor(self.config).compose_merge_request(to, subject,
477
directive, basename=basename)
633
directive, basename=basename, body=body)
478
634
mail_client_registry.register('default', DefaultMail,
479
635
help=DefaultMail.__doc__)
480
636
mail_client_registry.default_key = 'default'