/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: 2018-05-07 15:27:39 UTC
  • mto: This revision was merged to the branch mainline in revision 6958.
  • Revision ID: jelmer@jelmer.uk-20180507152739-fuv9z9r0yzi7ln3t
Specify source in .coveragerc.

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