/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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
import errno
 
18
import os
 
19
import subprocess
 
20
import sys
 
21
import tempfile
 
22
 
 
23
import breezy
 
24
from . import (
 
25
    config as _mod_config,
 
26
    email_message,
 
27
    errors,
 
28
    msgeditor,
 
29
    osutils,
 
30
    urlutils,
 
31
    registry,
 
32
    )
 
33
 
 
34
mail_client_registry = registry.Registry()
 
35
 
 
36
 
 
37
class MailClientNotFound(errors.BzrError):
 
38
 
 
39
    _fmt = "Unable to find mail client with the following names:"\
 
40
        " %(mail_command_list_string)s"
 
41
 
 
42
    def __init__(self, mail_command_list):
 
43
        mail_command_list_string = ', '.join(mail_command_list)
 
44
        errors.BzrError.__init__(
 
45
            self, mail_command_list=mail_command_list,
 
46
            mail_command_list_string=mail_command_list_string)
 
47
 
 
48
 
 
49
class NoMessageSupplied(errors.BzrError):
 
50
 
 
51
    _fmt = "No message supplied."
 
52
 
 
53
 
 
54
class NoMailAddressSpecified(errors.BzrError):
 
55
 
 
56
    _fmt = "No mail-to address (--mail-to) or output (-o) specified."
 
57
 
 
58
 
 
59
class MailClient(object):
 
60
    """A mail client that can send messages with attachements."""
 
61
 
 
62
    def __init__(self, config):
 
63
        self.config = config
 
64
 
 
65
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
66
                extension, basename=None, body=None):
 
67
        """Compose (and possibly send) an email message
 
68
 
 
69
        Must be implemented by subclasses.
 
70
 
 
71
        :param prompt: A message to tell the user what to do.  Supported by
 
72
            the Editor client, but ignored by others
 
73
        :param to: The address to send the message to
 
74
        :param subject: The contents of the subject line
 
75
        :param attachment: An email attachment, as a bytestring
 
76
        :param mime_subtype: The attachment is assumed to be a subtype of
 
77
            Text.  This allows the precise subtype to be specified, e.g.
 
78
            "plain", "x-patch", etc.
 
79
        :param extension: The file extension associated with the attachment
 
80
            type, e.g. ".patch"
 
81
        :param basename: The name to use for the attachment, e.g.
 
82
            "send-nick-3252"
 
83
        """
 
84
        raise NotImplementedError
 
85
 
 
86
    def compose_merge_request(self, to, subject, directive, basename=None,
 
87
                              body=None):
 
88
        """Compose (and possibly send) a merge request
 
89
 
 
90
        :param to: The address to send the request to
 
91
        :param subject: The subject line to use for the request
 
92
        :param directive: A merge directive representing the merge request, as
 
93
            a bytestring.
 
94
        :param basename: The name to use for the attachment, e.g.
 
95
            "send-nick-3252"
 
96
        """
 
97
        prompt = self._get_merge_prompt("Please describe these changes:", to,
 
98
                                        subject, directive)
 
99
        self.compose(prompt, to, subject, directive,
 
100
                     'x-patch', '.patch', basename, body)
 
101
 
 
102
    def _get_merge_prompt(self, prompt, to, subject, attachment):
 
103
        """Generate a prompt string.  Overridden by Editor.
 
104
 
 
105
        :param prompt: A string suggesting what user should do
 
106
        :param to: The address the mail will be sent to
 
107
        :param subject: The subject line of the mail
 
108
        :param attachment: The attachment that will be used
 
109
        """
 
110
        return ''
 
111
 
 
112
 
 
113
class Editor(MailClient):
 
114
    __doc__ = """DIY mail client that uses commit message editor"""
 
115
 
 
116
    supports_body = True
 
117
 
 
118
    def _get_merge_prompt(self, prompt, to, subject, attachment):
 
119
        """See MailClient._get_merge_prompt"""
 
120
        return (u"%s\n\n"
 
121
                u"To: %s\n"
 
122
                u"Subject: %s\n\n"
 
123
                u"%s" % (prompt, to, subject,
 
124
                         attachment.decode('utf-8', 'replace')))
 
125
 
 
126
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
127
                extension, basename=None, body=None):
 
128
        """See MailClient.compose"""
 
129
        if not to:
 
130
            raise NoMailAddressSpecified()
 
131
        body = msgeditor.edit_commit_message(prompt, start_message=body)
 
132
        if body == '':
 
133
            raise NoMessageSupplied()
 
134
        email_message.EmailMessage.send(self.config,
 
135
                                        self.config.get('email'),
 
136
                                        to,
 
137
                                        subject,
 
138
                                        body,
 
139
                                        attachment,
 
140
                                        attachment_mime_subtype=mime_subtype)
 
141
 
 
142
 
 
143
mail_client_registry.register('editor', Editor,
 
144
                              help=Editor.__doc__)
 
145
 
 
146
 
 
147
class BodyExternalMailClient(MailClient):
 
148
 
 
149
    supports_body = True
 
150
 
 
151
    def _get_client_commands(self):
 
152
        """Provide a list of commands that may invoke the mail client"""
 
153
        if sys.platform == 'win32':
 
154
            import win32utils
 
155
            return [win32utils.get_app_path(i) for i in self._client_commands]
 
156
        else:
 
157
            return self._client_commands
 
158
 
 
159
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
160
                extension, basename=None, body=None):
 
161
        """See MailClient.compose.
 
162
 
 
163
        Writes the attachment to a temporary file, invokes _compose.
 
164
        """
 
165
        if basename is None:
 
166
            basename = 'attachment'
 
167
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
 
168
        attach_path = osutils.pathjoin(pathname, basename + extension)
 
169
        with open(attach_path, 'wb') as outfile:
 
170
            outfile.write(attachment)
 
171
        if body is not None:
 
172
            kwargs = {'body': body}
 
173
        else:
 
174
            kwargs = {}
 
175
        self._compose(prompt, to, subject, attach_path, mime_subtype,
 
176
                      extension, **kwargs)
 
177
 
 
178
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
 
179
                 extension, body=None, from_=None):
 
180
        """Invoke a mail client as a commandline process.
 
181
 
 
182
        Overridden by MAPIClient.
 
183
        :param to: The address to send the mail to
 
184
        :param subject: The subject line for the mail
 
185
        :param pathname: The path to the attachment
 
186
        :param mime_subtype: The attachment is assumed to have a major type of
 
187
            "text", but the precise subtype can be specified here
 
188
        :param extension: A file extension (including period) associated with
 
189
            the attachment type.
 
190
        :param body: Optional body text.
 
191
        :param from_: Optional From: header.
 
192
        """
 
193
        for name in self._get_client_commands():
 
194
            cmdline = [self._encode_path(name, 'executable')]
 
195
            if body is not None:
 
196
                kwargs = {'body': body}
 
197
            else:
 
198
                kwargs = {}
 
199
            if from_ is not None:
 
200
                kwargs['from_'] = from_
 
201
            cmdline.extend(self._get_compose_commandline(to, subject,
 
202
                                                         attach_path,
 
203
                                                         **kwargs))
 
204
            try:
 
205
                subprocess.call(cmdline)
 
206
            except OSError as e:
 
207
                if e.errno != errno.ENOENT:
 
208
                    raise
 
209
            else:
 
210
                break
 
211
        else:
 
212
            raise MailClientNotFound(self._client_commands)
 
213
 
 
214
    def _get_compose_commandline(self, to, subject, attach_path, body):
 
215
        """Determine the commandline to use for composing a message
 
216
 
 
217
        Implemented by various subclasses
 
218
        :param to: The address to send the mail to
 
219
        :param subject: The subject line for the mail
 
220
        :param attach_path: The path to the attachment
 
221
        """
 
222
        raise NotImplementedError
 
223
 
 
224
    def _encode_safe(self, u):
 
225
        """Encode possible unicode string argument to 8-bit string
 
226
        in user_encoding. Unencodable characters will be replaced
 
227
        with '?'.
 
228
 
 
229
        :param  u:  possible unicode string.
 
230
        :return:    encoded string if u is unicode, u itself otherwise.
 
231
        """
 
232
        return u
 
233
 
 
234
    def _encode_path(self, path, kind):
 
235
        """Encode unicode path in user encoding.
 
236
 
 
237
        :param  path:   possible unicode path.
 
238
        :param  kind:   path kind ('executable' or 'attachment').
 
239
        :return:        encoded path if path is unicode,
 
240
                        path itself otherwise.
 
241
        :raise:         UnableEncodePath.
 
242
        """
 
243
        return path
 
244
 
 
245
 
 
246
class ExternalMailClient(BodyExternalMailClient):
 
247
    __doc__ = """An external mail client."""
 
248
 
 
249
    supports_body = False
 
250
 
 
251
 
 
252
class Evolution(BodyExternalMailClient):
 
253
    __doc__ = """Evolution mail client."""
 
254
 
 
255
    _client_commands = ['evolution']
 
256
 
 
257
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
258
        """See ExternalMailClient._get_compose_commandline"""
 
259
        message_options = {}
 
260
        if subject is not None:
 
261
            message_options['subject'] = subject
 
262
        if attach_path is not None:
 
263
            message_options['attach'] = attach_path
 
264
        if body is not None:
 
265
            message_options['body'] = body
 
266
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
 
267
                        sorted(message_options.items())]
 
268
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
 
269
                                  '&'.join(options_list))]
 
270
 
 
271
 
 
272
mail_client_registry.register('evolution', Evolution,
 
273
                              help=Evolution.__doc__)
 
274
 
 
275
 
 
276
class Mutt(BodyExternalMailClient):
 
277
    __doc__ = """Mutt mail client."""
 
278
 
 
279
    _client_commands = ['mutt']
 
280
 
 
281
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
282
        """See ExternalMailClient._get_compose_commandline"""
 
283
        message_options = []
 
284
        if subject is not None:
 
285
            message_options.extend(
 
286
                ['-s', self._encode_safe(subject)])
 
287
        if attach_path is not None:
 
288
            message_options.extend(
 
289
                ['-a', self._encode_path(attach_path, 'attachment')])
 
290
        if body is not None:
 
291
            # Store the temp file object in self, so that it does not get
 
292
            # garbage collected and delete the file before mutt can read it.
 
293
            self._temp_file = tempfile.NamedTemporaryFile(
 
294
                prefix="mutt-body-", suffix=".txt", mode="w+")
 
295
            self._temp_file.write(body)
 
296
            self._temp_file.flush()
 
297
            message_options.extend(['-i', self._temp_file.name])
 
298
        if to is not None:
 
299
            message_options.extend(['--', self._encode_safe(to)])
 
300
        return message_options
 
301
 
 
302
 
 
303
mail_client_registry.register('mutt', Mutt,
 
304
                              help=Mutt.__doc__)
 
305
 
 
306
 
 
307
class Thunderbird(BodyExternalMailClient):
 
308
    __doc__ = """Mozilla Thunderbird (or Icedove)
 
309
 
 
310
    Note that Thunderbird 1.5 is buggy and does not support setting
 
311
    "to" simultaneously with including a attachment.
 
312
 
 
313
    There is a workaround if no attachment is present, but we always need to
 
314
    send attachments.
 
315
    """
 
316
 
 
317
    _client_commands = [
 
318
        'thunderbird', 'mozilla-thunderbird', 'icedove',
 
319
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
 
320
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
 
321
 
 
322
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
323
        """See ExternalMailClient._get_compose_commandline"""
 
324
        message_options = {}
 
325
        if to is not None:
 
326
            message_options['to'] = self._encode_safe(to)
 
327
        if subject is not None:
 
328
            message_options['subject'] = self._encode_safe(subject)
 
329
        if attach_path is not None:
 
330
            message_options['attachment'] = urlutils.local_path_to_url(
 
331
                attach_path)
 
332
        if body is not None:
 
333
            options_list = ['body=%s' %
 
334
                            urlutils.quote(self._encode_safe(body))]
 
335
        else:
 
336
            options_list = []
 
337
        options_list.extend(["%s='%s'" % (k, v) for k, v in
 
338
                             sorted(message_options.items())])
 
339
        return ['-compose', ','.join(options_list)]
 
340
 
 
341
 
 
342
mail_client_registry.register('thunderbird', Thunderbird,
 
343
                              help=Thunderbird.__doc__)
 
344
 
 
345
 
 
346
class KMail(ExternalMailClient):
 
347
    __doc__ = """KDE mail client."""
 
348
 
 
349
    _client_commands = ['kmail']
 
350
 
 
351
    def _get_compose_commandline(self, to, subject, attach_path):
 
352
        """See ExternalMailClient._get_compose_commandline"""
 
353
        message_options = []
 
354
        if subject is not None:
 
355
            message_options.extend(['-s', self._encode_safe(subject)])
 
356
        if attach_path is not None:
 
357
            message_options.extend(
 
358
                ['--attach', self._encode_path(attach_path, 'attachment')])
 
359
        if to is not None:
 
360
            message_options.extend([self._encode_safe(to)])
 
361
        return message_options
 
362
 
 
363
 
 
364
mail_client_registry.register('kmail', KMail,
 
365
                              help=KMail.__doc__)
 
366
 
 
367
 
 
368
class Claws(ExternalMailClient):
 
369
    __doc__ = """Claws mail client."""
 
370
 
 
371
    supports_body = True
 
372
 
 
373
    _client_commands = ['claws-mail']
 
374
 
 
375
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
 
376
                                 from_=None):
 
377
        """See ExternalMailClient._get_compose_commandline"""
 
378
        compose_url = []
 
379
        if from_ is not None:
 
380
            compose_url.append('from=' + urlutils.quote(from_))
 
381
        if subject is not None:
 
382
            # Don't use urlutils.quote_plus because Claws doesn't seem
 
383
            # to recognise spaces encoded as "+".
 
384
            compose_url.append(
 
385
                'subject=' + urlutils.quote(self._encode_safe(subject)))
 
386
        if body is not None:
 
387
            compose_url.append(
 
388
                'body=' + urlutils.quote(self._encode_safe(body)))
 
389
        # to must be supplied for the claws-mail --compose syntax to work.
 
390
        if to is None:
 
391
            raise NoMailAddressSpecified()
 
392
        compose_url = 'mailto:%s?%s' % (
 
393
            self._encode_safe(to), '&'.join(compose_url))
 
394
        # Collect command-line options.
 
395
        message_options = ['--compose', compose_url]
 
396
        if attach_path is not None:
 
397
            message_options.extend(
 
398
                ['--attach', self._encode_path(attach_path, 'attachment')])
 
399
        return message_options
 
400
 
 
401
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
 
402
                 extension, body=None, from_=None):
 
403
        """See ExternalMailClient._compose"""
 
404
        if from_ is None:
 
405
            from_ = self.config.get('email')
 
406
        super(Claws, self)._compose(prompt, to, subject, attach_path,
 
407
                                    mime_subtype, extension, body, from_)
 
408
 
 
409
 
 
410
mail_client_registry.register('claws', Claws,
 
411
                              help=Claws.__doc__)
 
412
 
 
413
 
 
414
class XDGEmail(BodyExternalMailClient):
 
415
    __doc__ = """xdg-email attempts to invoke the preferred mail client"""
 
416
 
 
417
    _client_commands = ['xdg-email']
 
418
 
 
419
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
420
        """See ExternalMailClient._get_compose_commandline"""
 
421
        if not to:
 
422
            raise NoMailAddressSpecified()
 
423
        commandline = [self._encode_safe(to)]
 
424
        if subject is not None:
 
425
            commandline.extend(['--subject', self._encode_safe(subject)])
 
426
        if attach_path is not None:
 
427
            commandline.extend(['--attach',
 
428
                                self._encode_path(attach_path, 'attachment')])
 
429
        if body is not None:
 
430
            commandline.extend(['--body', self._encode_safe(body)])
 
431
        return commandline
 
432
 
 
433
 
 
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 = br"""(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
 
 
545
 
 
546
mail_client_registry.register('emacsclient', EmacsMail,
 
547
                              help=EmacsMail.__doc__)
 
548
 
 
549
 
 
550
class MAPIClient(BodyExternalMailClient):
 
551
    __doc__ = """Default Windows mail client launched using MAPI."""
 
552
 
 
553
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
 
554
                 extension, body=None):
 
555
        """See ExternalMailClient._compose.
 
556
 
 
557
        This implementation uses MAPI via the simplemapi ctypes wrapper
 
558
        """
 
559
        from .util import simplemapi
 
560
        try:
 
561
            simplemapi.SendMail(to or '', subject or '', body or '',
 
562
                                attach_path)
 
563
        except simplemapi.MAPIError as e:
 
564
            if e.code != simplemapi.MAPI_USER_ABORT:
 
565
                raise MailClientNotFound(['MAPI supported mail client'
 
566
                                          ' (error %d)' % (e.code,)])
 
567
 
 
568
 
 
569
mail_client_registry.register('mapi', MAPIClient,
 
570
                              help=MAPIClient.__doc__)
 
571
 
 
572
 
 
573
class MailApp(BodyExternalMailClient):
 
574
    __doc__ = """Use MacOS X's Mail.app for sending email messages.
 
575
 
 
576
    Although it would be nice to use appscript, it's not installed
 
577
    with the shipped Python installations.  We instead build an
 
578
    AppleScript and invoke the script using osascript(1).  We don't
 
579
    use the _encode_safe() routines as it's not clear what encoding
 
580
    osascript expects the script to be in.
 
581
    """
 
582
 
 
583
    _client_commands = ['osascript']
 
584
 
 
585
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
 
586
                                 from_=None):
 
587
        """See ExternalMailClient._get_compose_commandline"""
 
588
 
 
589
        fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
 
590
                                              suffix=".scpt")
 
591
        try:
 
592
            os.write(fd, 'tell application "Mail"\n')
 
593
            os.write(fd, 'set newMessage to make new outgoing message\n')
 
594
            os.write(fd, 'tell newMessage\n')
 
595
            if to is not None:
 
596
                os.write(fd, 'make new to recipient with properties'
 
597
                         ' {address:"%s"}\n' % to)
 
598
            if from_ is not None:
 
599
                # though from_ doesn't actually seem to be used
 
600
                os.write(fd, 'set sender to "%s"\n'
 
601
                         % from_.replace('"', '\\"'))
 
602
            if subject is not None:
 
603
                os.write(fd, 'set subject to "%s"\n'
 
604
                         % subject.replace('"', '\\"'))
 
605
            if body is not None:
 
606
                # FIXME: would be nice to prepend the body to the
 
607
                # existing content (e.g., preserve signature), but
 
608
                # can't seem to figure out the right applescript
 
609
                # incantation.
 
610
                os.write(fd, 'set content to "%s\\n\n"\n' %
 
611
                         body.replace('"', '\\"').replace('\n', '\\n'))
 
612
 
 
613
            if attach_path is not None:
 
614
                # FIXME: would be nice to first append a newline to
 
615
                # ensure the attachment is on a new paragraph, but
 
616
                # can't seem to figure out the right applescript
 
617
                # incantation.
 
618
                os.write(fd, 'tell content to make new attachment'
 
619
                         ' with properties {file name:"%s"}'
 
620
                         ' at after the last paragraph\n'
 
621
                         % self._encode_path(attach_path, 'attachment'))
 
622
            os.write(fd, 'set visible to true\n')
 
623
            os.write(fd, 'end tell\n')
 
624
            os.write(fd, 'end tell\n')
 
625
        finally:
 
626
            os.close(fd)  # Just close the handle but do not remove the file.
 
627
        return [self.temp_file]
 
628
 
 
629
 
 
630
mail_client_registry.register('mail.app', MailApp,
 
631
                              help=MailApp.__doc__)
 
632
 
 
633
 
 
634
class DefaultMail(MailClient):
 
635
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
636
    falls back to Editor"""
 
637
 
 
638
    supports_body = True
 
639
 
 
640
    def _mail_client(self):
 
641
        """Determine the preferred mail client for this platform"""
 
642
        if osutils.supports_mapi():
 
643
            return MAPIClient(self.config)
 
644
        else:
 
645
            return XDGEmail(self.config)
 
646
 
 
647
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
648
                extension, basename=None, body=None):
 
649
        """See MailClient.compose"""
 
650
        try:
 
651
            return self._mail_client().compose(prompt, to, subject,
 
652
                                               attachment, mime_subtype,
 
653
                                               extension, basename, body)
 
654
        except MailClientNotFound:
 
655
            return Editor(self.config).compose(
 
656
                prompt, to, subject, attachment, mime_subtype, extension, body)
 
657
 
 
658
    def compose_merge_request(self, to, subject, directive, basename=None,
 
659
                              body=None):
 
660
        """See MailClient.compose_merge_request"""
 
661
        try:
 
662
            return self._mail_client().compose_merge_request(
 
663
                to, subject, directive, basename=basename, body=body)
 
664
        except MailClientNotFound:
 
665
            return Editor(self.config).compose_merge_request(
 
666
                to, subject, directive, basename=basename, body=body)
 
667
 
 
668
 
 
669
mail_client_registry.register(u'default', DefaultMail,
 
670
                              help=DefaultMail.__doc__)
 
671
mail_client_registry.default_key = u'default'
 
672
 
 
673
opt_mail_client = _mod_config.RegistryOption(
 
674
    'mail_client', mail_client_registry, help='E-mail client to use.',
 
675
    invalid='error')