~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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
from __future__ import absolute_import
 
18
 
17
19
import errno
18
20
import os
19
21
import subprocess
20
22
import sys
21
23
import tempfile
22
 
import urllib
23
24
 
24
25
import bzrlib
25
26
from bzrlib import (
 
27
    config as _mod_config,
26
28
    email_message,
27
29
    errors,
28
30
    msgeditor,
89
91
 
90
92
 
91
93
class Editor(MailClient):
92
 
    """DIY mail client that uses commit message editor"""
 
94
    __doc__ = """DIY mail client that uses commit message editor"""
93
95
 
94
96
    supports_body = True
95
97
 
110
112
        if body == '':
111
113
            raise errors.NoMessageSupplied()
112
114
        email_message.EmailMessage.send(self.config,
113
 
                                        self.config.username(),
 
115
                                        self.config.get('email'),
114
116
                                        to,
115
117
                                        subject,
116
118
                                        body,
155
157
                      extension, **kwargs)
156
158
 
157
159
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
 
                extension, body=None):
 
160
                 extension, body=None, from_=None):
159
161
        """Invoke a mail client as a commandline process.
160
162
 
161
163
        Overridden by MAPIClient.
166
168
            "text", but the precise subtype can be specified here
167
169
        :param extension: A file extension (including period) associated with
168
170
            the attachment type.
 
171
        :param body: Optional body text.
 
172
        :param from_: Optional From: header.
169
173
        """
170
174
        for name in self._get_client_commands():
171
175
            cmdline = [self._encode_path(name, 'executable')]
173
177
                kwargs = {'body': body}
174
178
            else:
175
179
                kwargs = {}
 
180
            if from_ is not None:
 
181
                kwargs['from_'] = from_
176
182
            cmdline.extend(self._get_compose_commandline(to, subject,
177
183
                                                         attach_path,
178
184
                                                         **kwargs))
226
232
 
227
233
 
228
234
class ExternalMailClient(BodyExternalMailClient):
229
 
    """An external mail client."""
 
235
    __doc__ = """An external mail client."""
230
236
 
231
237
    supports_body = False
232
238
 
233
239
 
234
240
class Evolution(BodyExternalMailClient):
235
 
    """Evolution mail client."""
 
241
    __doc__ = """Evolution mail client."""
236
242
 
237
243
    _client_commands = ['evolution']
238
244
 
253
259
                              help=Evolution.__doc__)
254
260
 
255
261
 
256
 
class Mutt(ExternalMailClient):
257
 
    """Mutt mail client."""
 
262
class Mutt(BodyExternalMailClient):
 
263
    __doc__ = """Mutt mail client."""
258
264
 
259
265
    _client_commands = ['mutt']
260
266
 
261
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
267
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
262
268
        """See ExternalMailClient._get_compose_commandline"""
263
269
        message_options = []
264
270
        if subject is not None:
266
272
        if attach_path is not None:
267
273
            message_options.extend(['-a',
268
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])
269
283
        if to is not None:
270
284
            message_options.extend(['--', self._encode_safe(to)])
271
285
        return message_options
274
288
 
275
289
 
276
290
class Thunderbird(BodyExternalMailClient):
277
 
    """Mozilla Thunderbird (or Icedove)
 
291
    __doc__ = """Mozilla Thunderbird (or Icedove)
278
292
 
279
293
    Note that Thunderbird 1.5 is buggy and does not support setting
280
294
    "to" simultaneously with including a attachment.
298
312
            message_options['attachment'] = urlutils.local_path_to_url(
299
313
                attach_path)
300
314
        if body is not None:
301
 
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
 
315
            options_list = ['body=%s' % urlutils.quote(self._encode_safe(body))]
302
316
        else:
303
317
            options_list = []
304
318
        options_list.extend(["%s='%s'" % (k, v) for k, v in
309
323
 
310
324
 
311
325
class KMail(ExternalMailClient):
312
 
    """KDE mail client."""
 
326
    __doc__ = """KDE mail client."""
313
327
 
314
328
    _client_commands = ['kmail']
315
329
 
329
343
 
330
344
 
331
345
class Claws(ExternalMailClient):
332
 
    """Claws mail client."""
 
346
    __doc__ = """Claws mail client."""
 
347
 
 
348
    supports_body = True
333
349
 
334
350
    _client_commands = ['claws-mail']
335
351
 
336
 
    def _get_compose_commandline(self, to, subject, attach_path):
 
352
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
 
353
                                 from_=None):
337
354
        """See ExternalMailClient._get_compose_commandline"""
338
 
        compose_url = ['mailto:']
339
 
        if to is not None:
340
 
            compose_url.append(self._encode_safe(to))
341
 
        compose_url.append('?')
 
355
        compose_url = []
 
356
        if from_ is not None:
 
357
            compose_url.append('from=' + urlutils.quote(from_))
342
358
        if subject is not None:
343
 
            # Don't use urllib.quote_plus because Claws doesn't seem
 
359
            # Don't use urlutils.quote_plus because Claws doesn't seem
344
360
            # to recognise spaces encoded as "+".
345
361
            compose_url.append(
346
 
                'subject=%s' % urllib.quote(self._encode_safe(subject)))
 
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))
347
371
        # Collect command-line options.
348
 
        message_options = ['--compose', ''.join(compose_url)]
 
372
        message_options = ['--compose', compose_url]
349
373
        if attach_path is not None:
350
374
            message_options.extend(
351
375
                ['--attach', self._encode_path(attach_path, 'attachment')])
352
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
 
353
387
mail_client_registry.register('claws', Claws,
354
388
                              help=Claws.__doc__)
355
389
 
356
390
 
357
391
class XDGEmail(BodyExternalMailClient):
358
 
    """xdg-email attempts to invoke the user's preferred mail client"""
 
392
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
359
393
 
360
394
    _client_commands = ['xdg-email']
361
395
 
377
411
 
378
412
 
379
413
class EmacsMail(ExternalMailClient):
380
 
    """Call emacsclient to have a mail buffer.
 
414
    __doc__ = """Call emacsclient to have a mail buffer.
381
415
 
382
416
    This only work for emacs >= 22.1 due to recent -e/--eval support.
383
417
 
392
426
 
393
427
    _client_commands = ['emacsclient']
394
428
 
 
429
    def __init__(self, config):
 
430
        super(EmacsMail, self).__init__(config)
 
431
        self.elisp_tmp_file = None
 
432
 
395
433
    def _prepare_send_function(self):
396
434
        """Write our wrapper function into a temporary file.
397
435
 
468
506
        if attach_path is not None:
469
507
            # Do not create a file if there is no attachment
470
508
            elisp = self._prepare_send_function()
 
509
            self.elisp_tmp_file = elisp
471
510
            lmmform = '(load "%s")' % elisp
472
511
            mmform  = '(bzr-add-mime-att "%s")' % \
473
512
                self._encode_path(attach_path, 'attachment')
482
521
 
483
522
 
484
523
class MAPIClient(BodyExternalMailClient):
485
 
    """Default Windows mail client launched using MAPI."""
 
524
    __doc__ = """Default Windows mail client launched using MAPI."""
486
525
 
487
526
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
488
 
                 extension, body):
 
527
                 extension, body=None):
489
528
        """See ExternalMailClient._compose.
490
529
 
491
530
        This implementation uses MAPI via the simplemapi ctypes wrapper
502
541
                              help=MAPIClient.__doc__)
503
542
 
504
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
 
505
603
class DefaultMail(MailClient):
506
 
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
604
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
507
605
    falls back to Editor"""
508
606
 
 
607
    supports_body = True
 
608
 
509
609
    def _mail_client(self):
510
610
        """Determine the preferred mail client for this platform"""
511
611
        if osutils.supports_mapi():
518
618
        """See MailClient.compose"""
519
619
        try:
520
620
            return self._mail_client().compose(prompt, to, subject,
521
 
                                               attachment, mimie_subtype,
 
621
                                               attachment, mime_subtype,
522
622
                                               extension, basename, body)
523
623
        except errors.MailClientNotFound:
524
624
            return Editor(self.config).compose(prompt, to, subject,
525
 
                          attachment, mimie_subtype, extension, body)
 
625
                          attachment, mime_subtype, extension, body)
526
626
 
527
627
    def compose_merge_request(self, to, subject, directive, basename=None,
528
628
                              body=None):
536
636
mail_client_registry.register('default', DefaultMail,
537
637
                              help=DefaultMail.__doc__)
538
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')