/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-01-12 13:56:10 UTC
  • mto: This revision was merged to the branch mainline in revision 7443.
  • Revision ID: jelmer@jelmer.uk-20200112135610-0a9bct6x4cw7he6y
Add strip_segment_parameters function.

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