~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Martin Packman
  • Date: 2012-02-01 13:24:42 UTC
  • mto: (6437.23.4 2.5)
  • mto: This revision was merged to the branch mainline in revision 6462.
  • Revision ID: martin.packman@canonical.com-20120201132442-ela7jc4mxv4b058o
Treat path for .bzr.log as unicode

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