~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Patch Queue Manager
  • Date: 2011-09-15 15:37:20 UTC
  • mfrom: (6140.1.3 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20110915153720-n17t6m5oh5bblqad
(vila) Open 2.5b2 for bugfixes (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
 
1
# Copyright (C) 2007-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
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
16
16
 
17
17
import errno
18
18
import os
19
19
import subprocess
20
20
import sys
21
21
import tempfile
 
22
import urllib
22
23
 
23
24
import bzrlib
24
25
from bzrlib import (
27
28
    msgeditor,
28
29
    osutils,
29
30
    urlutils,
 
31
    registry
30
32
    )
31
33
 
 
34
mail_client_registry = registry.Registry()
 
35
 
32
36
 
33
37
class MailClient(object):
34
38
    """A mail client that can send messages with attachements."""
37
41
        self.config = config
38
42
 
39
43
    def compose(self, prompt, to, subject, attachment, mime_subtype,
40
 
                extension, basename=None):
 
44
                extension, basename=None, body=None):
41
45
        """Compose (and possibly send) an email message
42
46
 
43
47
        Must be implemented by subclasses.
57
61
        """
58
62
        raise NotImplementedError
59
63
 
60
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
64
    def compose_merge_request(self, to, subject, directive, basename=None,
 
65
                              body=None):
61
66
        """Compose (and possibly send) a merge request
62
67
 
63
68
        :param to: The address to send the request to
70
75
        prompt = self._get_merge_prompt("Please describe these changes:", to,
71
76
                                        subject, directive)
72
77
        self.compose(prompt, to, subject, directive,
73
 
            'x-patch', '.patch', basename)
 
78
            'x-patch', '.patch', basename, body)
74
79
 
75
80
    def _get_merge_prompt(self, prompt, to, subject, attachment):
76
81
        """Generate a prompt string.  Overridden by Editor.
84
89
 
85
90
 
86
91
class Editor(MailClient):
87
 
    """DIY mail client that uses commit message editor"""
 
92
    __doc__ = """DIY mail client that uses commit message editor"""
 
93
 
 
94
    supports_body = True
88
95
 
89
96
    def _get_merge_prompt(self, prompt, to, subject, attachment):
90
97
        """See MailClient._get_merge_prompt"""
95
102
                         attachment.decode('utf-8', 'replace')))
96
103
 
97
104
    def compose(self, prompt, to, subject, attachment, mime_subtype,
98
 
                extension, basename=None):
 
105
                extension, basename=None, body=None):
99
106
        """See MailClient.compose"""
100
107
        if not to:
101
108
            raise errors.NoMailAddressSpecified()
102
 
        body = msgeditor.edit_commit_message(prompt)
 
109
        body = msgeditor.edit_commit_message(prompt, start_message=body)
103
110
        if body == '':
104
111
            raise errors.NoMessageSupplied()
105
112
        email_message.EmailMessage.send(self.config,
109
116
                                        body,
110
117
                                        attachment,
111
118
                                        attachment_mime_subtype=mime_subtype)
112
 
 
113
 
 
114
 
class ExternalMailClient(MailClient):
115
 
    """An external mail client."""
 
119
mail_client_registry.register('editor', Editor,
 
120
                              help=Editor.__doc__)
 
121
 
 
122
 
 
123
class BodyExternalMailClient(MailClient):
 
124
 
 
125
    supports_body = True
116
126
 
117
127
    def _get_client_commands(self):
118
128
        """Provide a list of commands that may invoke the mail client"""
123
133
            return self._client_commands
124
134
 
125
135
    def compose(self, prompt, to, subject, attachment, mime_subtype,
126
 
                extension, basename=None):
 
136
                extension, basename=None, body=None):
127
137
        """See MailClient.compose.
128
138
 
129
139
        Writes the attachment to a temporary file, invokes _compose.
130
140
        """
131
141
        if basename is None:
132
142
            basename = 'attachment'
133
 
        pathname = tempfile.mkdtemp(prefix='bzr-mail-')
 
143
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
134
144
        attach_path = osutils.pathjoin(pathname, basename + extension)
135
145
        outfile = open(attach_path, 'wb')
136
146
        try:
137
147
            outfile.write(attachment)
138
148
        finally:
139
149
            outfile.close()
 
150
        if body is not None:
 
151
            kwargs = {'body': body}
 
152
        else:
 
153
            kwargs = {}
140
154
        self._compose(prompt, to, subject, attach_path, mime_subtype,
141
 
                      extension)
 
155
                      extension, **kwargs)
142
156
 
143
157
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
144
 
                extension):
 
158
                 extension, body=None, from_=None):
145
159
        """Invoke a mail client as a commandline process.
146
160
 
147
161
        Overridden by MAPIClient.
152
166
            "text", but the precise subtype can be specified here
153
167
        :param extension: A file extension (including period) associated with
154
168
            the attachment type.
 
169
        :param body: Optional body text.
 
170
        :param from_: Optional From: header.
155
171
        """
156
172
        for name in self._get_client_commands():
157
173
            cmdline = [self._encode_path(name, 'executable')]
 
174
            if body is not None:
 
175
                kwargs = {'body': body}
 
176
            else:
 
177
                kwargs = {}
 
178
            if from_ is not None:
 
179
                kwargs['from_'] = from_
158
180
            cmdline.extend(self._get_compose_commandline(to, subject,
159
 
                                                         attach_path))
 
181
                                                         attach_path,
 
182
                                                         **kwargs))
160
183
            try:
161
184
                subprocess.call(cmdline)
162
185
            except OSError, e:
167
190
        else:
168
191
            raise errors.MailClientNotFound(self._client_commands)
169
192
 
170
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
193
    def _get_compose_commandline(self, to, subject, attach_path, body):
171
194
        """Determine the commandline to use for composing a message
172
195
 
173
196
        Implemented by various subclasses
186
209
        :return:    encoded string if u is unicode, u itself otherwise.
187
210
        """
188
211
        if isinstance(u, unicode):
189
 
            return u.encode(bzrlib.user_encoding, 'replace')
 
212
            return u.encode(osutils.get_user_encoding(), 'replace')
190
213
        return u
191
214
 
192
215
    def _encode_path(self, path, kind):
200
223
        """
201
224
        if isinstance(path, unicode):
202
225
            try:
203
 
                return path.encode(bzrlib.user_encoding)
 
226
                return path.encode(osutils.get_user_encoding())
204
227
            except UnicodeEncodeError:
205
228
                raise errors.UnableEncodePath(path, kind)
206
229
        return path
207
230
 
208
231
 
209
 
class Evolution(ExternalMailClient):
210
 
    """Evolution mail client."""
 
232
class ExternalMailClient(BodyExternalMailClient):
 
233
    __doc__ = """An external mail client."""
 
234
 
 
235
    supports_body = False
 
236
 
 
237
 
 
238
class Evolution(BodyExternalMailClient):
 
239
    __doc__ = """Evolution mail client."""
211
240
 
212
241
    _client_commands = ['evolution']
213
242
 
214
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
243
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
215
244
        """See ExternalMailClient._get_compose_commandline"""
216
245
        message_options = {}
217
246
        if subject is not None:
218
247
            message_options['subject'] = subject
219
248
        if attach_path is not None:
220
249
            message_options['attach'] = attach_path
 
250
        if body is not None:
 
251
            message_options['body'] = body
221
252
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
222
253
                        sorted(message_options.iteritems())]
223
254
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
224
255
            '&'.join(options_list))]
225
 
 
226
 
 
227
 
class Mutt(ExternalMailClient):
228
 
    """Mutt mail client."""
 
256
mail_client_registry.register('evolution', Evolution,
 
257
                              help=Evolution.__doc__)
 
258
 
 
259
 
 
260
class Mutt(BodyExternalMailClient):
 
261
    __doc__ = """Mutt mail client."""
229
262
 
230
263
    _client_commands = ['mutt']
231
264
 
232
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
265
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
233
266
        """See ExternalMailClient._get_compose_commandline"""
234
267
        message_options = []
235
268
        if subject is not None:
237
270
        if attach_path is not None:
238
271
            message_options.extend(['-a',
239
272
                self._encode_path(attach_path, 'attachment')])
 
273
        if body is not None:
 
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])
240
281
        if to is not None:
241
 
            message_options.append(self._encode_safe(to))
 
282
            message_options.extend(['--', self._encode_safe(to)])
242
283
        return message_options
243
 
 
244
 
 
245
 
class Thunderbird(ExternalMailClient):
246
 
    """Mozilla Thunderbird (or Icedove)
 
284
mail_client_registry.register('mutt', Mutt,
 
285
                              help=Mutt.__doc__)
 
286
 
 
287
 
 
288
class Thunderbird(BodyExternalMailClient):
 
289
    __doc__ = """Mozilla Thunderbird (or Icedove)
247
290
 
248
291
    Note that Thunderbird 1.5 is buggy and does not support setting
249
292
    "to" simultaneously with including a attachment.
253
296
    """
254
297
 
255
298
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
256
 
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
 
299
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
 
300
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
257
301
 
258
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
302
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
259
303
        """See ExternalMailClient._get_compose_commandline"""
260
304
        message_options = {}
261
305
        if to is not None:
265
309
        if attach_path is not None:
266
310
            message_options['attachment'] = urlutils.local_path_to_url(
267
311
                attach_path)
268
 
        options_list = ["%s='%s'" % (k, v) for k, v in
269
 
                        sorted(message_options.iteritems())]
 
312
        if body is not None:
 
313
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
 
314
        else:
 
315
            options_list = []
 
316
        options_list.extend(["%s='%s'" % (k, v) for k, v in
 
317
                        sorted(message_options.iteritems())])
270
318
        return ['-compose', ','.join(options_list)]
 
319
mail_client_registry.register('thunderbird', Thunderbird,
 
320
                              help=Thunderbird.__doc__)
271
321
 
272
322
 
273
323
class KMail(ExternalMailClient):
274
 
    """KDE mail client."""
 
324
    __doc__ = """KDE mail client."""
275
325
 
276
326
    _client_commands = ['kmail']
277
327
 
286
336
        if to is not None:
287
337
            message_options.extend([self._encode_safe(to)])
288
338
        return message_options
289
 
 
290
 
 
291
 
class XDGEmail(ExternalMailClient):
292
 
    """xdg-email attempts to invoke the user's preferred mail client"""
 
339
mail_client_registry.register('kmail', KMail,
 
340
                              help=KMail.__doc__)
 
341
 
 
342
 
 
343
class Claws(ExternalMailClient):
 
344
    __doc__ = """Claws mail client."""
 
345
 
 
346
    supports_body = True
 
347
 
 
348
    _client_commands = ['claws-mail']
 
349
 
 
350
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
 
351
                                 from_=None):
 
352
        """See ExternalMailClient._get_compose_commandline"""
 
353
        compose_url = []
 
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 "+".
 
359
            compose_url.append(
 
360
                'subject=' + urllib.quote(self._encode_safe(subject)))
 
361
        if body is not None:
 
362
            compose_url.append(
 
363
                'body=' + urllib.quote(self._encode_safe(body)))
 
364
        # to must be supplied for the claws-mail --compose syntax to work.
 
365
        if to is None:
 
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
 
375
 
 
376
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
 
377
                 extension, body=None, from_=None):
 
378
        """See ExternalMailClient._compose"""
 
379
        if from_ is None:
 
380
            from_ = self.config.get_user_option('email')
 
381
        super(Claws, self)._compose(prompt, to, subject, attach_path,
 
382
                                    mime_subtype, extension, body, from_)
 
383
 
 
384
 
 
385
mail_client_registry.register('claws', Claws,
 
386
                              help=Claws.__doc__)
 
387
 
 
388
 
 
389
class XDGEmail(BodyExternalMailClient):
 
390
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
293
391
 
294
392
    _client_commands = ['xdg-email']
295
393
 
296
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
394
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
297
395
        """See ExternalMailClient._get_compose_commandline"""
298
396
        if not to:
299
397
            raise errors.NoMailAddressSpecified()
303
401
        if attach_path is not None:
304
402
            commandline.extend(['--attach',
305
403
                self._encode_path(attach_path, 'attachment')])
 
404
        if body is not None:
 
405
            commandline.extend(['--body', self._encode_safe(body)])
306
406
        return commandline
 
407
mail_client_registry.register('xdg-email', XDGEmail,
 
408
                              help=XDGEmail.__doc__)
307
409
 
308
410
 
309
411
class EmacsMail(ExternalMailClient):
310
 
    """Call emacsclient to have a mail buffer.
 
412
    __doc__ = """Call emacsclient to have a mail buffer.
311
413
 
312
414
    This only work for emacs >= 22.1 due to recent -e/--eval support.
313
415
 
322
424
 
323
425
    _client_commands = ['emacsclient']
324
426
 
 
427
    def __init__(self, config):
 
428
        super(EmacsMail, self).__init__(config)
 
429
        self.elisp_tmp_file = None
 
430
 
325
431
    def _prepare_send_function(self):
326
432
        """Write our wrapper function into a temporary file.
327
433
 
328
434
        This temporary file will be loaded at runtime in
329
435
        _get_compose_commandline function.
330
436
 
331
 
        FIXME: this function does not remove the file. That's a wanted
 
437
        This function does not remove the file.  That's a wanted
332
438
        behaviour since _get_compose_commandline won't run the send
333
439
        mail function directly but return the eligible command line.
334
440
        Removing our temporary file here would prevent our sendmail
335
 
        function to work.
336
 
 
337
 
        A possible workaround could be to load the function here with
338
 
        emacsclient --eval '(load temp)' but this is not robust since
339
 
        emacs could have been stopped between here and the call to
340
 
        mail client.
 
441
        function to work.  (The file is deleted by some elisp code
 
442
        after being read by Emacs.)
341
443
        """
342
444
 
343
445
        _defun = r"""(defun bzr-add-mime-att (file)
344
 
  "Attach FILe to a mail buffer as a MIME attachment."
 
446
  "Attach FILE to a mail buffer as a MIME attachment."
345
447
  (let ((agent mail-user-agent))
346
 
    (mail-text)
347
 
    (newline)
348
448
    (if (and file (file-exists-p file))
349
449
        (cond
350
450
         ((eq agent 'sendmail-user-agent)
351
 
          (etach-attach file))
352
 
         ((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
353
 
          (mml-attach-file file "text/x-patch" "BZR merge" "attachment"))
 
451
          (progn
 
452
            (mail-text)
 
453
            (newline)
 
454
            (if (functionp 'etach-attach)
 
455
              (etach-attach file)
 
456
              (mail-attach-file file))))
 
457
         ((or (eq agent 'message-user-agent)
 
458
              (eq agent 'gnus-user-agent)
 
459
              (eq agent 'mh-e-user-agent))
 
460
          (progn
 
461
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
 
462
         ((eq agent 'mew-user-agent)
 
463
          (progn
 
464
            (mew-draft-prepare-attachments)
 
465
            (mew-attach-link file (file-name-nondirectory file))
 
466
            (let* ((nums (mew-syntax-nums))
 
467
                   (syntax (mew-syntax-get-entry mew-encode-syntax nums)))
 
468
              (mew-syntax-set-cd syntax "BZR merge")
 
469
              (mew-encode-syntax-print mew-encode-syntax))
 
470
            (mew-header-goto-body)))
354
471
         (t
355
 
          (message "Unhandled MUA")))
 
472
          (message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
356
473
      (error "File %s does not exist." file))))
357
474
"""
358
475
 
371
488
        _subject = "nil"
372
489
 
373
490
        if to is not None:
374
 
            _to = ("\"%s\"" % self._encode_safe(to))
 
491
            _to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
375
492
        if subject is not None:
376
 
            _subject = ("\"%s\"" % self._encode_safe(subject))
 
493
            _subject = ("\"%s\"" %
 
494
                        self._encode_safe(subject).replace('"', '\\"'))
377
495
 
378
496
        # Funcall the default mail composition function
379
497
        # This will work with any mail mode including default mail-mode
385
503
        # Try to attach a MIME attachment using our wrapper function
386
504
        if attach_path is not None:
387
505
            # Do not create a file if there is no attachment
388
 
            lmmform = '(load "%s")' % self._prepare_send_function()
 
506
            elisp = self._prepare_send_function()
 
507
            self.elisp_tmp_file = elisp
 
508
            lmmform = '(load "%s")' % elisp
389
509
            mmform  = '(bzr-add-mime-att "%s")' % \
390
510
                self._encode_path(attach_path, 'attachment')
 
511
            rmform = '(delete-file "%s")' % elisp
391
512
            commandline.append(lmmform)
392
513
            commandline.append(mmform)
 
514
            commandline.append(rmform)
393
515
 
394
516
        return commandline
395
 
 
396
 
 
397
 
class MAPIClient(ExternalMailClient):
398
 
    """Default Windows mail client launched using MAPI."""
 
517
mail_client_registry.register('emacsclient', EmacsMail,
 
518
                              help=EmacsMail.__doc__)
 
519
 
 
520
 
 
521
class MAPIClient(BodyExternalMailClient):
 
522
    __doc__ = """Default Windows mail client launched using MAPI."""
399
523
 
400
524
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
401
 
                 extension):
 
525
                 extension, body=None):
402
526
        """See ExternalMailClient._compose.
403
527
 
404
528
        This implementation uses MAPI via the simplemapi ctypes wrapper
405
529
        """
406
530
        from bzrlib.util import simplemapi
407
531
        try:
408
 
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
 
532
            simplemapi.SendMail(to or '', subject or '', body or '',
 
533
                                attach_path)
409
534
        except simplemapi.MAPIError, e:
410
535
            if e.code != simplemapi.MAPI_USER_ABORT:
411
536
                raise errors.MailClientNotFound(['MAPI supported mail client'
412
537
                                                 ' (error %d)' % (e.code,)])
 
538
mail_client_registry.register('mapi', MAPIClient,
 
539
                              help=MAPIClient.__doc__)
 
540
 
 
541
 
 
542
class MailApp(BodyExternalMailClient):
 
543
    __doc__ = """Use MacOS X's Mail.app for sending email messages.
 
544
 
 
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.
 
550
    """
 
551
 
 
552
    _client_commands = ['osascript']
 
553
 
 
554
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
 
555
                                from_=None):
 
556
       """See ExternalMailClient._get_compose_commandline"""
 
557
 
 
558
       fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
 
559
                                         suffix=".scpt")
 
560
       try:
 
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')
 
564
           if to is not None:
 
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('"', '\\"'))
 
574
           if body is not None:
 
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
 
578
               # incantation.
 
579
               os.write(fd, 'set content to "%s\\n\n"\n' %
 
580
                   body.replace('"', '\\"').replace('\n', '\\n'))
 
581
 
 
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
 
586
               # incantation.
 
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')
 
594
       finally:
 
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__)
413
599
 
414
600
 
415
601
class DefaultMail(MailClient):
416
 
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
602
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
417
603
    falls back to Editor"""
418
604
 
 
605
    supports_body = True
 
606
 
419
607
    def _mail_client(self):
420
608
        """Determine the preferred mail client for this platform"""
421
609
        if osutils.supports_mapi():
424
612
            return XDGEmail(self.config)
425
613
 
426
614
    def compose(self, prompt, to, subject, attachment, mime_subtype,
427
 
                extension, basename=None):
 
615
                extension, basename=None, body=None):
428
616
        """See MailClient.compose"""
429
617
        try:
430
618
            return self._mail_client().compose(prompt, to, subject,
431
619
                                               attachment, mimie_subtype,
432
 
                                               extension, basename)
 
620
                                               extension, basename, body)
433
621
        except errors.MailClientNotFound:
434
622
            return Editor(self.config).compose(prompt, to, subject,
435
 
                          attachment, mimie_subtype, extension)
 
623
                          attachment, mimie_subtype, extension, body)
436
624
 
437
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
625
    def compose_merge_request(self, to, subject, directive, basename=None,
 
626
                              body=None):
438
627
        """See MailClient.compose_merge_request"""
439
628
        try:
440
629
            return self._mail_client().compose_merge_request(to, subject,
441
 
                    directive, basename=basename)
 
630
                    directive, basename=basename, body=body)
442
631
        except errors.MailClientNotFound:
443
632
            return Editor(self.config).compose_merge_request(to, subject,
444
 
                          directive, basename=basename)
 
633
                          directive, basename=basename, body=body)
 
634
mail_client_registry.register('default', DefaultMail,
 
635
                              help=DefaultMail.__doc__)
 
636
mail_client_registry.default_key = 'default'
 
637
 
 
638