~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Jelmer Vernooij
  • Date: 2012-01-27 19:05:43 UTC
  • mto: This revision was merged to the branch mainline in revision 6450.
  • Revision ID: jelmer@samba.org-20120127190543-vk350mv4a0c7aug2
Fix weave test.

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