/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: 2019-05-29 03:22:34 UTC
  • mfrom: (7303 work)
  • mto: This revision was merged to the branch mainline in revision 7306.
  • Revision ID: jelmer@jelmer.uk-20190529032234-mt3fuws8gq03tapi
Merge trunk.

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