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
41
41
self.config = config
43
43
def compose(self, prompt, to, subject, attachment, mime_subtype,
44
extension, basename=None, body=None):
44
extension, basename=None):
45
45
"""Compose (and possibly send) an email message
47
47
Must be implemented by subclasses.
62
62
raise NotImplementedError
64
def compose_merge_request(self, to, subject, directive, basename=None,
64
def compose_merge_request(self, to, subject, directive, basename=None):
66
65
"""Compose (and possibly send) a merge request
68
67
:param to: The address to send the request to
75
74
prompt = self._get_merge_prompt("Please describe these changes:", to,
76
75
subject, directive)
77
76
self.compose(prompt, to, subject, directive,
78
'x-patch', '.patch', basename, body)
77
'x-patch', '.patch', basename)
80
79
def _get_merge_prompt(self, prompt, to, subject, attachment):
81
80
"""Generate a prompt string. Overridden by Editor.
102
99
attachment.decode('utf-8', 'replace')))
104
101
def compose(self, prompt, to, subject, attachment, mime_subtype,
105
extension, basename=None, body=None):
102
extension, basename=None):
106
103
"""See MailClient.compose"""
108
105
raise errors.NoMailAddressSpecified()
109
body = msgeditor.edit_commit_message(prompt, start_message=body)
106
body = msgeditor.edit_commit_message(prompt)
111
108
raise errors.NoMessageSupplied()
112
109
email_message.EmailMessage.send(self.config,
133
129
return self._client_commands
135
131
def compose(self, prompt, to, subject, attachment, mime_subtype,
136
extension, basename=None, body=None):
132
extension, basename=None):
137
133
"""See MailClient.compose.
139
135
Writes the attachment to a temporary file, invokes _compose.
147
143
outfile.write(attachment)
151
kwargs = {'body': body}
154
146
self._compose(prompt, to, subject, attach_path, mime_subtype,
157
149
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
extension, body=None, from_=None):
159
151
"""Invoke a mail client as a commandline process.
161
153
Overridden by MAPIClient.
166
158
"text", but the precise subtype can be specified here
167
159
:param extension: A file extension (including period) associated with
168
160
the attachment type.
169
:param body: Optional body text.
170
:param from_: Optional From: header.
172
162
for name in self._get_client_commands():
173
163
cmdline = [self._encode_path(name, 'executable')]
175
kwargs = {'body': body}
178
if from_ is not None:
179
kwargs['from_'] = from_
180
164
cmdline.extend(self._get_compose_commandline(to, subject,
184
167
subprocess.call(cmdline)
185
168
except OSError, e:
232
class ExternalMailClient(BodyExternalMailClient):
233
__doc__ = """An external mail client."""
235
supports_body = False
238
class Evolution(BodyExternalMailClient):
239
__doc__ = """Evolution mail client."""
215
class Evolution(ExternalMailClient):
216
"""Evolution mail client."""
241
218
_client_commands = ['evolution']
243
def _get_compose_commandline(self, to, subject, attach_path, body=None):
220
def _get_compose_commandline(self, to, subject, attach_path):
244
221
"""See ExternalMailClient._get_compose_commandline"""
245
222
message_options = {}
246
223
if subject is not None:
247
224
message_options['subject'] = subject
248
225
if attach_path is not None:
249
226
message_options['attach'] = attach_path
251
message_options['body'] = body
252
227
options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
228
sorted(message_options.iteritems())]
254
229
return ['mailto:%s?%s' % (self._encode_safe(to or ''),
257
232
help=Evolution.__doc__)
260
class Mutt(BodyExternalMailClient):
261
__doc__ = """Mutt mail client."""
235
class Mutt(ExternalMailClient):
236
"""Mutt mail client."""
263
238
_client_commands = ['mutt']
265
def _get_compose_commandline(self, to, subject, attach_path, body=None):
240
def _get_compose_commandline(self, to, subject, attach_path):
266
241
"""See ExternalMailClient._get_compose_commandline"""
267
242
message_options = []
268
243
if subject is not None:
270
245
if attach_path is not None:
271
246
message_options.extend(['-a',
272
247
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
248
if to is not None:
282
message_options.extend(['--', self._encode_safe(to)])
249
message_options.append(self._encode_safe(to))
283
250
return message_options
284
251
mail_client_registry.register('mutt', Mutt,
285
252
help=Mutt.__doc__)
288
class Thunderbird(BodyExternalMailClient):
289
__doc__ = """Mozilla Thunderbird (or Icedove)
255
class Thunderbird(ExternalMailClient):
256
"""Mozilla Thunderbird (or Icedove)
291
258
Note that Thunderbird 1.5 is buggy and does not support setting
292
259
"to" simultaneously with including a attachment.
299
266
'/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
267
'/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
302
def _get_compose_commandline(self, to, subject, attach_path, body=None):
269
def _get_compose_commandline(self, to, subject, attach_path):
303
270
"""See ExternalMailClient._get_compose_commandline"""
304
271
message_options = {}
305
272
if to is not None:
309
276
if attach_path is not None:
310
277
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())])
279
options_list = ["%s='%s'" % (k, v) for k, v in
280
sorted(message_options.iteritems())]
318
281
return ['-compose', ','.join(options_list)]
319
282
mail_client_registry.register('thunderbird', Thunderbird,
320
283
help=Thunderbird.__doc__)
323
286
class KMail(ExternalMailClient):
324
__doc__ = """KDE mail client."""
287
"""KDE mail client."""
326
289
_client_commands = ['kmail']
343
306
class Claws(ExternalMailClient):
344
__doc__ = """Claws mail client."""
307
"""Claws mail client."""
348
309
_client_commands = ['claws-mail']
350
def _get_compose_commandline(self, to, subject, attach_path, body=None,
311
def _get_compose_commandline(self, to, subject, attach_path):
352
312
"""See ExternalMailClient._get_compose_commandline"""
354
if from_ is not None:
355
compose_url.append('from=' + urllib.quote(from_))
313
compose_url = ['mailto:']
315
compose_url.append(self._encode_safe(to))
316
compose_url.append('?')
356
317
if subject is not None:
357
318
# Don't use urllib.quote_plus because Claws doesn't seem
358
319
# to recognise spaces encoded as "+".
359
320
compose_url.append(
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))
321
'subject=%s' % urllib.quote(self._encode_safe(subject)))
369
322
# Collect command-line options.
370
message_options = ['--compose', compose_url]
323
message_options = ['--compose', ''.join(compose_url)]
371
324
if attach_path is not None:
372
325
message_options.extend(
373
326
['--attach', self._encode_path(attach_path, 'attachment')])
374
327
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
328
mail_client_registry.register('claws', Claws,
386
329
help=Claws.__doc__)
389
class XDGEmail(BodyExternalMailClient):
390
__doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
332
class XDGEmail(ExternalMailClient):
333
"""xdg-email attempts to invoke the user's preferred mail client"""
392
335
_client_commands = ['xdg-email']
394
def _get_compose_commandline(self, to, subject, attach_path, body=None):
337
def _get_compose_commandline(self, to, subject, attach_path):
395
338
"""See ExternalMailClient._get_compose_commandline"""
397
340
raise errors.NoMailAddressSpecified()
401
344
if attach_path is not None:
402
345
commandline.extend(['--attach',
403
346
self._encode_path(attach_path, 'attachment')])
405
commandline.extend(['--body', self._encode_safe(body)])
406
347
return commandline
407
348
mail_client_registry.register('xdg-email', XDGEmail,
408
349
help=XDGEmail.__doc__)
411
352
class EmacsMail(ExternalMailClient):
412
__doc__ = """Call emacsclient to have a mail buffer.
353
"""Call emacsclient to have a mail buffer.
414
355
This only work for emacs >= 22.1 due to recent -e/--eval support.
518
452
help=EmacsMail.__doc__)
521
class MAPIClient(BodyExternalMailClient):
522
__doc__ = """Default Windows mail client launched using MAPI."""
455
class MAPIClient(ExternalMailClient):
456
"""Default Windows mail client launched using MAPI."""
524
458
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
extension, body=None):
526
460
"""See ExternalMailClient._compose.
528
462
This implementation uses MAPI via the simplemapi ctypes wrapper
530
464
from bzrlib.util import simplemapi
532
simplemapi.SendMail(to or '', subject or '', body or '',
466
simplemapi.SendMail(to or '', subject or '', '', attach_path)
534
467
except simplemapi.MAPIError, e:
535
468
if e.code != simplemapi.MAPI_USER_ABORT:
536
469
raise errors.MailClientNotFound(['MAPI supported mail client'
539
472
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
475
class DefaultMail(MailClient):
602
__doc__ = """Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
476
"""Default mail handling. Tries XDGEmail (or MAPIClient on Windows),
603
477
falls back to Editor"""
607
479
def _mail_client(self):
608
480
"""Determine the preferred mail client for this platform"""
609
481
if osutils.supports_mapi():
612
484
return XDGEmail(self.config)
614
486
def compose(self, prompt, to, subject, attachment, mime_subtype,
615
extension, basename=None, body=None):
487
extension, basename=None):
616
488
"""See MailClient.compose"""
618
490
return self._mail_client().compose(prompt, to, subject,
619
491
attachment, mimie_subtype,
620
extension, basename, body)
621
493
except errors.MailClientNotFound:
622
494
return Editor(self.config).compose(prompt, to, subject,
623
attachment, mimie_subtype, extension, body)
495
attachment, mimie_subtype, extension)
625
def compose_merge_request(self, to, subject, directive, basename=None,
497
def compose_merge_request(self, to, subject, directive, basename=None):
627
498
"""See MailClient.compose_merge_request"""
629
500
return self._mail_client().compose_merge_request(to, subject,
630
directive, basename=basename, body=body)
501
directive, basename=basename)
631
502
except errors.MailClientNotFound:
632
503
return Editor(self.config).compose_merge_request(to, subject,
633
directive, basename=basename, body=body)
504
directive, basename=basename)
634
505
mail_client_registry.register('default', DefaultMail,
635
506
help=DefaultMail.__doc__)
636
507
mail_client_registry.default_key = 'default'