~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: John Arbash Meinel
  • Date: 2008-03-04 14:25:46 UTC
  • mto: This revision was merged to the branch mainline in revision 3279.
  • Revision ID: john@arbash-meinel.com-20080304142546-zuwwy0o9roo14928
Implement cherrypick support for Merge3
When merging a cherrypick, use a slightly different resolve logic.
When encountering a conflict, the new logic does not include lines that
were present in BASE that are conflicting with OTHER.
This is done since a cherrypick is (by definition) avoiding changes that
are present in the base.
(related to bug #151731)

Show diffs side-by-side

added added

removed removed

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