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