~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
 
1
# Copyright (C) 2007-2010 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
16
18
 
17
19
import errno
18
20
import os
22
24
 
23
25
import bzrlib
24
26
from bzrlib import (
 
27
    config as _mod_config,
25
28
    email_message,
26
29
    errors,
27
30
    msgeditor,
40
43
        self.config = config
41
44
 
42
45
    def compose(self, prompt, to, subject, attachment, mime_subtype,
43
 
                extension, basename=None):
 
46
                extension, basename=None, body=None):
44
47
        """Compose (and possibly send) an email message
45
48
 
46
49
        Must be implemented by subclasses.
60
63
        """
61
64
        raise NotImplementedError
62
65
 
63
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
66
    def compose_merge_request(self, to, subject, directive, basename=None,
 
67
                              body=None):
64
68
        """Compose (and possibly send) a merge request
65
69
 
66
70
        :param to: The address to send the request to
73
77
        prompt = self._get_merge_prompt("Please describe these changes:", to,
74
78
                                        subject, directive)
75
79
        self.compose(prompt, to, subject, directive,
76
 
            'x-patch', '.patch', basename)
 
80
            'x-patch', '.patch', basename, body)
77
81
 
78
82
    def _get_merge_prompt(self, prompt, to, subject, attachment):
79
83
        """Generate a prompt string.  Overridden by Editor.
87
91
 
88
92
 
89
93
class Editor(MailClient):
90
 
    """DIY mail client that uses commit message editor"""
 
94
    __doc__ = """DIY mail client that uses commit message editor"""
 
95
 
 
96
    supports_body = True
91
97
 
92
98
    def _get_merge_prompt(self, prompt, to, subject, attachment):
93
99
        """See MailClient._get_merge_prompt"""
98
104
                         attachment.decode('utf-8', 'replace')))
99
105
 
100
106
    def compose(self, prompt, to, subject, attachment, mime_subtype,
101
 
                extension, basename=None):
 
107
                extension, basename=None, body=None):
102
108
        """See MailClient.compose"""
103
109
        if not to:
104
110
            raise errors.NoMailAddressSpecified()
105
 
        body = msgeditor.edit_commit_message(prompt)
 
111
        body = msgeditor.edit_commit_message(prompt, start_message=body)
106
112
        if body == '':
107
113
            raise errors.NoMessageSupplied()
108
114
        email_message.EmailMessage.send(self.config,
109
 
                                        self.config.username(),
 
115
                                        self.config.get('email'),
110
116
                                        to,
111
117
                                        subject,
112
118
                                        body,
116
122
                              help=Editor.__doc__)
117
123
 
118
124
 
119
 
class ExternalMailClient(MailClient):
120
 
    """An external mail client."""
 
125
class BodyExternalMailClient(MailClient):
 
126
 
 
127
    supports_body = True
121
128
 
122
129
    def _get_client_commands(self):
123
130
        """Provide a list of commands that may invoke the mail client"""
128
135
            return self._client_commands
129
136
 
130
137
    def compose(self, prompt, to, subject, attachment, mime_subtype,
131
 
                extension, basename=None):
 
138
                extension, basename=None, body=None):
132
139
        """See MailClient.compose.
133
140
 
134
141
        Writes the attachment to a temporary file, invokes _compose.
142
149
            outfile.write(attachment)
143
150
        finally:
144
151
            outfile.close()
 
152
        if body is not None:
 
153
            kwargs = {'body': body}
 
154
        else:
 
155
            kwargs = {}
145
156
        self._compose(prompt, to, subject, attach_path, mime_subtype,
146
 
                      extension)
 
157
                      extension, **kwargs)
147
158
 
148
159
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
149
 
                extension):
 
160
                 extension, body=None, from_=None):
150
161
        """Invoke a mail client as a commandline process.
151
162
 
152
163
        Overridden by MAPIClient.
157
168
            "text", but the precise subtype can be specified here
158
169
        :param extension: A file extension (including period) associated with
159
170
            the attachment type.
 
171
        :param body: Optional body text.
 
172
        :param from_: Optional From: header.
160
173
        """
161
174
        for name in self._get_client_commands():
162
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_
163
182
            cmdline.extend(self._get_compose_commandline(to, subject,
164
 
                                                         attach_path))
 
183
                                                         attach_path,
 
184
                                                         **kwargs))
165
185
            try:
166
186
                subprocess.call(cmdline)
167
187
            except OSError, e:
172
192
        else:
173
193
            raise errors.MailClientNotFound(self._client_commands)
174
194
 
175
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
195
    def _get_compose_commandline(self, to, subject, attach_path, body):
176
196
        """Determine the commandline to use for composing a message
177
197
 
178
198
        Implemented by various subclasses
211
231
        return path
212
232
 
213
233
 
214
 
class Evolution(ExternalMailClient):
215
 
    """Evolution mail client."""
 
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."""
216
242
 
217
243
    _client_commands = ['evolution']
218
244
 
219
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
245
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
220
246
        """See ExternalMailClient._get_compose_commandline"""
221
247
        message_options = {}
222
248
        if subject is not None:
223
249
            message_options['subject'] = subject
224
250
        if attach_path is not None:
225
251
            message_options['attach'] = attach_path
 
252
        if body is not None:
 
253
            message_options['body'] = body
226
254
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
227
255
                        sorted(message_options.iteritems())]
228
256
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
231
259
                              help=Evolution.__doc__)
232
260
 
233
261
 
234
 
class Mutt(ExternalMailClient):
235
 
    """Mutt mail client."""
 
262
class Mutt(BodyExternalMailClient):
 
263
    __doc__ = """Mutt mail client."""
236
264
 
237
265
    _client_commands = ['mutt']
238
266
 
239
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
267
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
240
268
        """See ExternalMailClient._get_compose_commandline"""
241
269
        message_options = []
242
270
        if subject is not None:
244
272
        if attach_path is not None:
245
273
            message_options.extend(['-a',
246
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])
247
283
        if to is not None:
248
 
            message_options.append(self._encode_safe(to))
 
284
            message_options.extend(['--', self._encode_safe(to)])
249
285
        return message_options
250
286
mail_client_registry.register('mutt', Mutt,
251
287
                              help=Mutt.__doc__)
252
288
 
253
289
 
254
 
class Thunderbird(ExternalMailClient):
255
 
    """Mozilla Thunderbird (or Icedove)
 
290
class Thunderbird(BodyExternalMailClient):
 
291
    __doc__ = """Mozilla Thunderbird (or Icedove)
256
292
 
257
293
    Note that Thunderbird 1.5 is buggy and does not support setting
258
294
    "to" simultaneously with including a attachment.
265
301
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
266
302
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
267
303
 
268
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
304
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
269
305
        """See ExternalMailClient._get_compose_commandline"""
270
306
        message_options = {}
271
307
        if to is not None:
275
311
        if attach_path is not None:
276
312
            message_options['attachment'] = urlutils.local_path_to_url(
277
313
                attach_path)
278
 
        options_list = ["%s='%s'" % (k, v) for k, v in
279
 
                        sorted(message_options.iteritems())]
 
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())])
280
320
        return ['-compose', ','.join(options_list)]
281
321
mail_client_registry.register('thunderbird', Thunderbird,
282
322
                              help=Thunderbird.__doc__)
283
323
 
284
324
 
285
325
class KMail(ExternalMailClient):
286
 
    """KDE mail client."""
 
326
    __doc__ = """KDE mail client."""
287
327
 
288
328
    _client_commands = ['kmail']
289
329
 
302
342
                              help=KMail.__doc__)
303
343
 
304
344
 
305
 
class XDGEmail(ExternalMailClient):
306
 
    """xdg-email attempts to invoke the user's preferred mail client"""
 
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"""
307
393
 
308
394
    _client_commands = ['xdg-email']
309
395
 
310
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
396
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
311
397
        """See ExternalMailClient._get_compose_commandline"""
312
398
        if not to:
313
399
            raise errors.NoMailAddressSpecified()
317
403
        if attach_path is not None:
318
404
            commandline.extend(['--attach',
319
405
                self._encode_path(attach_path, 'attachment')])
 
406
        if body is not None:
 
407
            commandline.extend(['--body', self._encode_safe(body)])
320
408
        return commandline
321
409
mail_client_registry.register('xdg-email', XDGEmail,
322
410
                              help=XDGEmail.__doc__)
323
411
 
324
412
 
325
413
class EmacsMail(ExternalMailClient):
326
 
    """Call emacsclient to have a mail buffer.
 
414
    __doc__ = """Call emacsclient to have a mail buffer.
327
415
 
328
416
    This only work for emacs >= 22.1 due to recent -e/--eval support.
329
417
 
338
426
 
339
427
    _client_commands = ['emacsclient']
340
428
 
 
429
    def __init__(self, config):
 
430
        super(EmacsMail, self).__init__(config)
 
431
        self.elisp_tmp_file = None
 
432
 
341
433
    def _prepare_send_function(self):
342
434
        """Write our wrapper function into a temporary file.
343
435
 
364
456
            (if (functionp 'etach-attach)
365
457
              (etach-attach file)
366
458
              (mail-attach-file file))))
367
 
         ((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
 
459
         ((or (eq agent 'message-user-agent)
 
460
              (eq agent 'gnus-user-agent)
 
461
              (eq agent 'mh-e-user-agent))
368
462
          (progn
369
463
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
370
464
         ((eq agent 'mew-user-agent)
412
506
        if attach_path is not None:
413
507
            # Do not create a file if there is no attachment
414
508
            elisp = self._prepare_send_function()
 
509
            self.elisp_tmp_file = elisp
415
510
            lmmform = '(load "%s")' % elisp
416
511
            mmform  = '(bzr-add-mime-att "%s")' % \
417
512
                self._encode_path(attach_path, 'attachment')
425
520
                              help=EmacsMail.__doc__)
426
521
 
427
522
 
428
 
class MAPIClient(ExternalMailClient):
429
 
    """Default Windows mail client launched using MAPI."""
 
523
class MAPIClient(BodyExternalMailClient):
 
524
    __doc__ = """Default Windows mail client launched using MAPI."""
430
525
 
431
526
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
432
 
                 extension):
 
527
                 extension, body=None):
433
528
        """See ExternalMailClient._compose.
434
529
 
435
530
        This implementation uses MAPI via the simplemapi ctypes wrapper
436
531
        """
437
532
        from bzrlib.util import simplemapi
438
533
        try:
439
 
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
 
534
            simplemapi.SendMail(to or '', subject or '', body or '',
 
535
                                attach_path)
440
536
        except simplemapi.MAPIError, e:
441
537
            if e.code != simplemapi.MAPI_USER_ABORT:
442
538
                raise errors.MailClientNotFound(['MAPI supported mail client'
445
541
                              help=MAPIClient.__doc__)
446
542
 
447
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
 
 
602
 
448
603
class DefaultMail(MailClient):
449
 
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
604
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
450
605
    falls back to Editor"""
451
606
 
 
607
    supports_body = True
 
608
 
452
609
    def _mail_client(self):
453
610
        """Determine the preferred mail client for this platform"""
454
611
        if osutils.supports_mapi():
457
614
            return XDGEmail(self.config)
458
615
 
459
616
    def compose(self, prompt, to, subject, attachment, mime_subtype,
460
 
                extension, basename=None):
 
617
                extension, basename=None, body=None):
461
618
        """See MailClient.compose"""
462
619
        try:
463
620
            return self._mail_client().compose(prompt, to, subject,
464
 
                                               attachment, mimie_subtype,
465
 
                                               extension, basename)
 
621
                                               attachment, mime_subtype,
 
622
                                               extension, basename, body)
466
623
        except errors.MailClientNotFound:
467
624
            return Editor(self.config).compose(prompt, to, subject,
468
 
                          attachment, mimie_subtype, extension)
 
625
                          attachment, mime_subtype, extension, body)
469
626
 
470
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
627
    def compose_merge_request(self, to, subject, directive, basename=None,
 
628
                              body=None):
471
629
        """See MailClient.compose_merge_request"""
472
630
        try:
473
631
            return self._mail_client().compose_merge_request(to, subject,
474
 
                    directive, basename=basename)
 
632
                    directive, basename=basename, body=body)
475
633
        except errors.MailClientNotFound:
476
634
            return Editor(self.config).compose_merge_request(to, subject,
477
 
                          directive, basename=basename)
 
635
                          directive, basename=basename, body=body)
478
636
mail_client_registry.register('default', DefaultMail,
479
637
                              help=DefaultMail.__doc__)
480
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')