/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: Robert Collins
  • Date: 2005-10-19 10:11:57 UTC
  • mfrom: (1185.16.78)
  • mto: This revision was merged to the branch mainline in revision 1470.
  • Revision ID: robertc@robertcollins.net-20051019101157-17438d311e746b4f
mergeĀ fromĀ upstream

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')