/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

Merge from bzr.dev, resolving conflicts.

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