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()
454
513
help=EmacsMail.__doc__)
457
class MAPIClient(ExternalMailClient):
516
class MAPIClient(BodyExternalMailClient):
458
517
"""Default Windows mail client launched using MAPI."""
460
519
def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
extension, body=None):
462
521
"""See ExternalMailClient._compose.
464
523
This implementation uses MAPI via the simplemapi ctypes wrapper
466
525
from bzrlib.util import simplemapi
468
simplemapi.SendMail(to or '', subject or '', '', attach_path)
527
simplemapi.SendMail(to or '', subject or '', body or '',
469
529
except simplemapi.MAPIError, e:
470
530
if e.code != simplemapi.MAPI_USER_ABORT:
471
531
raise errors.MailClientNotFound(['MAPI supported mail client'
486
548
return XDGEmail(self.config)
488
550
def compose(self, prompt, to, subject, attachment, mime_subtype,
489
extension, basename=None):
551
extension, basename=None, body=None):
490
552
"""See MailClient.compose"""
492
554
return self._mail_client().compose(prompt, to, subject,
493
555
attachment, mimie_subtype,
556
extension, basename, body)
495
557
except errors.MailClientNotFound:
496
558
return Editor(self.config).compose(prompt, to, subject,
497
attachment, mimie_subtype, extension)
559
attachment, mimie_subtype, extension, body)
499
def compose_merge_request(self, to, subject, directive, basename=None):
561
def compose_merge_request(self, to, subject, directive, basename=None,
500
563
"""See MailClient.compose_merge_request"""
502
565
return self._mail_client().compose_merge_request(to, subject,
503
directive, basename=basename)
566
directive, basename=basename, body=body)
504
567
except errors.MailClientNotFound:
505
568
return Editor(self.config).compose_merge_request(to, subject,
506
directive, basename=basename)
569
directive, basename=basename, body=body)
507
570
mail_client_registry.register('default', DefaultMail,
508
571
help=DefaultMail.__doc__)
509
572
mail_client_registry.default_key = 'default'