/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2018-11-16 18:26:22 UTC
  • mfrom: (7167.1.4 run-flake8)
  • Revision ID: breezy.the.bot@gmail.com-20181116182622-qw3gan3hz78a2imw
Add a flake8 test.

Merged from https://code.launchpad.net/~jelmer/brz/run-flake8/+merge/358902

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