/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/mail_client.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

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