/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: Jelmer Vernooij
  • Date: 2019-02-04 01:01:24 UTC
  • mto: This revision was merged to the branch mainline in revision 7268.
  • Revision ID: jelmer@jelmer.uk-20190204010124-ni0i4qc6f5tnbvux
Fix source tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
import bzrlib
25
 
from bzrlib import (
 
25
import breezy
 
26
from . import (
 
27
    config as _mod_config,
26
28
    email_message,
27
29
    errors,
28
30
    msgeditor,
29
31
    osutils,
30
32
    urlutils,
31
 
    registry
 
33
    registry,
 
34
    )
 
35
from .sixish import (
 
36
    PY3,
 
37
    text_type,
32
38
    )
33
39
 
34
40
mail_client_registry = registry.Registry()
35
41
 
36
42
 
 
43
class MailClientNotFound(errors.BzrError):
 
44
 
 
45
    _fmt = "Unable to find mail client with the following names:"\
 
46
        " %(mail_command_list_string)s"
 
47
 
 
48
    def __init__(self, mail_command_list):
 
49
        mail_command_list_string = ', '.join(mail_command_list)
 
50
        errors.BzrError.__init__(
 
51
            self, mail_command_list=mail_command_list,
 
52
            mail_command_list_string=mail_command_list_string)
 
53
 
 
54
 
 
55
class NoMessageSupplied(errors.BzrError):
 
56
 
 
57
    _fmt = "No message supplied."
 
58
 
 
59
 
 
60
class NoMailAddressSpecified(errors.BzrError):
 
61
 
 
62
    _fmt = "No mail-to address (--mail-to) or output (-o) specified."
 
63
 
 
64
 
37
65
class MailClient(object):
38
66
    """A mail client that can send messages with attachements."""
39
67
 
75
103
        prompt = self._get_merge_prompt("Please describe these changes:", to,
76
104
                                        subject, directive)
77
105
        self.compose(prompt, to, subject, directive,
78
 
            'x-patch', '.patch', basename, body)
 
106
                     'x-patch', '.patch', basename, body)
79
107
 
80
108
    def _get_merge_prompt(self, prompt, to, subject, attachment):
81
109
        """Generate a prompt string.  Overridden by Editor.
105
133
                extension, basename=None, body=None):
106
134
        """See MailClient.compose"""
107
135
        if not to:
108
 
            raise errors.NoMailAddressSpecified()
 
136
            raise NoMailAddressSpecified()
109
137
        body = msgeditor.edit_commit_message(prompt, start_message=body)
110
138
        if body == '':
111
 
            raise errors.NoMessageSupplied()
 
139
            raise NoMessageSupplied()
112
140
        email_message.EmailMessage.send(self.config,
113
 
                                        self.config.username(),
 
141
                                        self.config.get('email'),
114
142
                                        to,
115
143
                                        subject,
116
144
                                        body,
117
145
                                        attachment,
118
146
                                        attachment_mime_subtype=mime_subtype)
 
147
 
 
148
 
119
149
mail_client_registry.register('editor', Editor,
120
150
                              help=Editor.__doc__)
121
151
 
182
212
                                                         **kwargs))
183
213
            try:
184
214
                subprocess.call(cmdline)
185
 
            except OSError, e:
 
215
            except OSError as e:
186
216
                if e.errno != errno.ENOENT:
187
217
                    raise
188
218
            else:
189
219
                break
190
220
        else:
191
 
            raise errors.MailClientNotFound(self._client_commands)
 
221
            raise MailClientNotFound(self._client_commands)
192
222
 
193
223
    def _get_compose_commandline(self, to, subject, attach_path, body):
194
224
        """Determine the commandline to use for composing a message
208
238
        :param  u:  possible unicode string.
209
239
        :return:    encoded string if u is unicode, u itself otherwise.
210
240
        """
211
 
        if isinstance(u, unicode):
 
241
        if not PY3 and isinstance(u, text_type):
212
242
            return u.encode(osutils.get_user_encoding(), 'replace')
213
243
        return u
214
244
 
221
251
                        path itself otherwise.
222
252
        :raise:         UnableEncodePath.
223
253
        """
224
 
        if isinstance(path, unicode):
 
254
        if not PY3 and isinstance(path, text_type):
225
255
            try:
226
256
                return path.encode(osutils.get_user_encoding())
227
257
            except UnicodeEncodeError:
250
280
        if body is not None:
251
281
            message_options['body'] = body
252
282
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
 
                        sorted(message_options.iteritems())]
 
283
                        sorted(message_options.items())]
254
284
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
255
 
            '&'.join(options_list))]
 
285
                                  '&'.join(options_list))]
 
286
 
 
287
 
256
288
mail_client_registry.register('evolution', Evolution,
257
289
                              help=Evolution.__doc__)
258
290
 
266
298
        """See ExternalMailClient._get_compose_commandline"""
267
299
        message_options = []
268
300
        if subject is not None:
269
 
            message_options.extend(['-s', self._encode_safe(subject)])
 
301
            message_options.extend(
 
302
                ['-s', self._encode_safe(subject)])
270
303
        if attach_path is not None:
271
 
            message_options.extend(['-a',
272
 
                self._encode_path(attach_path, 'attachment')])
 
304
            message_options.extend(
 
305
                ['-a', self._encode_path(attach_path, 'attachment')])
273
306
        if body is not None:
274
307
            # Store the temp file object in self, so that it does not get
275
308
            # garbage collected and delete the file before mutt can read it.
276
309
            self._temp_file = tempfile.NamedTemporaryFile(
277
 
                prefix="mutt-body-", suffix=".txt")
 
310
                prefix="mutt-body-", suffix=".txt", mode="w+")
278
311
            self._temp_file.write(body)
279
312
            self._temp_file.flush()
280
313
            message_options.extend(['-i', self._temp_file.name])
281
314
        if to is not None:
282
315
            message_options.extend(['--', self._encode_safe(to)])
283
316
        return message_options
 
317
 
 
318
 
284
319
mail_client_registry.register('mutt', Mutt,
285
320
                              help=Mutt.__doc__)
286
321
 
295
330
    send attachments.
296
331
    """
297
332
 
298
 
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
 
333
    _client_commands = [
 
334
        'thunderbird', 'mozilla-thunderbird', 'icedove',
299
335
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
336
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
301
337
 
310
346
            message_options['attachment'] = urlutils.local_path_to_url(
311
347
                attach_path)
312
348
        if body is not None:
313
 
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
 
349
            options_list = ['body=%s' %
 
350
                            urlutils.quote(self._encode_safe(body))]
314
351
        else:
315
352
            options_list = []
316
353
        options_list.extend(["%s='%s'" % (k, v) for k, v in
317
 
                        sorted(message_options.iteritems())])
 
354
                             sorted(message_options.items())])
318
355
        return ['-compose', ','.join(options_list)]
 
356
 
 
357
 
319
358
mail_client_registry.register('thunderbird', Thunderbird,
320
359
                              help=Thunderbird.__doc__)
321
360
 
331
370
        if subject is not None:
332
371
            message_options.extend(['-s', self._encode_safe(subject)])
333
372
        if attach_path is not None:
334
 
            message_options.extend(['--attach',
335
 
                self._encode_path(attach_path, 'attachment')])
 
373
            message_options.extend(
 
374
                ['--attach', self._encode_path(attach_path, 'attachment')])
336
375
        if to is not None:
337
376
            message_options.extend([self._encode_safe(to)])
338
377
        return message_options
 
378
 
 
379
 
339
380
mail_client_registry.register('kmail', KMail,
340
381
                              help=KMail.__doc__)
341
382
 
352
393
        """See ExternalMailClient._get_compose_commandline"""
353
394
        compose_url = []
354
395
        if from_ is not None:
355
 
            compose_url.append('from=' + urllib.quote(from_))
 
396
            compose_url.append('from=' + urlutils.quote(from_))
356
397
        if subject is not None:
357
 
            # Don't use urllib.quote_plus because Claws doesn't seem
 
398
            # Don't use urlutils.quote_plus because Claws doesn't seem
358
399
            # to recognise spaces encoded as "+".
359
400
            compose_url.append(
360
 
                'subject=' + urllib.quote(self._encode_safe(subject)))
 
401
                'subject=' + urlutils.quote(self._encode_safe(subject)))
361
402
        if body is not None:
362
403
            compose_url.append(
363
 
                'body=' + urllib.quote(self._encode_safe(body)))
 
404
                'body=' + urlutils.quote(self._encode_safe(body)))
364
405
        # to must be supplied for the claws-mail --compose syntax to work.
365
406
        if to is None:
366
 
            raise errors.NoMailAddressSpecified()
 
407
            raise NoMailAddressSpecified()
367
408
        compose_url = 'mailto:%s?%s' % (
368
409
            self._encode_safe(to), '&'.join(compose_url))
369
410
        # Collect command-line options.
377
418
                 extension, body=None, from_=None):
378
419
        """See ExternalMailClient._compose"""
379
420
        if from_ is None:
380
 
            from_ = self.config.get_user_option('email')
 
421
            from_ = self.config.get('email')
381
422
        super(Claws, self)._compose(prompt, to, subject, attach_path,
382
423
                                    mime_subtype, extension, body, from_)
383
424
 
387
428
 
388
429
 
389
430
class XDGEmail(BodyExternalMailClient):
390
 
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
 
431
    __doc__ = """xdg-email attempts to invoke the preferred mail client"""
391
432
 
392
433
    _client_commands = ['xdg-email']
393
434
 
394
435
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
395
436
        """See ExternalMailClient._get_compose_commandline"""
396
437
        if not to:
397
 
            raise errors.NoMailAddressSpecified()
 
438
            raise NoMailAddressSpecified()
398
439
        commandline = [self._encode_safe(to)]
399
440
        if subject is not None:
400
441
            commandline.extend(['--subject', self._encode_safe(subject)])
401
442
        if attach_path is not None:
402
443
            commandline.extend(['--attach',
403
 
                self._encode_path(attach_path, 'attachment')])
 
444
                                self._encode_path(attach_path, 'attachment')])
404
445
        if body is not None:
405
446
            commandline.extend(['--body', self._encode_safe(body)])
406
447
        return commandline
 
448
 
 
449
 
407
450
mail_client_registry.register('xdg-email', XDGEmail,
408
451
                              help=XDGEmail.__doc__)
409
452
 
442
485
        after being read by Emacs.)
443
486
        """
444
487
 
445
 
        _defun = r"""(defun bzr-add-mime-att (file)
 
488
        _defun = br"""(defun bzr-add-mime-att (file)
446
489
  "Attach FILE to a mail buffer as a MIME attachment."
447
490
  (let ((agent mail-user-agent))
448
491
    (if (and file (file-exists-p file))
478
521
        try:
479
522
            os.write(fd, _defun)
480
523
        finally:
481
 
            os.close(fd) # Just close the handle but do not remove the file.
 
524
            os.close(fd)  # Just close the handle but do not remove the file.
482
525
        return temp_file
483
526
 
484
527
    def _get_compose_commandline(self, to, subject, attach_path):
506
549
            elisp = self._prepare_send_function()
507
550
            self.elisp_tmp_file = elisp
508
551
            lmmform = '(load "%s")' % elisp
509
 
            mmform  = '(bzr-add-mime-att "%s")' % \
 
552
            mmform = '(bzr-add-mime-att "%s")' % \
510
553
                self._encode_path(attach_path, 'attachment')
511
554
            rmform = '(delete-file "%s")' % elisp
512
555
            commandline.append(lmmform)
514
557
            commandline.append(rmform)
515
558
 
516
559
        return commandline
 
560
 
 
561
 
517
562
mail_client_registry.register('emacsclient', EmacsMail,
518
563
                              help=EmacsMail.__doc__)
519
564
 
527
572
 
528
573
        This implementation uses MAPI via the simplemapi ctypes wrapper
529
574
        """
530
 
        from bzrlib.util import simplemapi
 
575
        from .util import simplemapi
531
576
        try:
532
577
            simplemapi.SendMail(to or '', subject or '', body or '',
533
578
                                attach_path)
534
 
        except simplemapi.MAPIError, e:
 
579
        except simplemapi.MAPIError as e:
535
580
            if e.code != simplemapi.MAPI_USER_ABORT:
536
 
                raise errors.MailClientNotFound(['MAPI supported mail client'
537
 
                                                 ' (error %d)' % (e.code,)])
 
581
                raise MailClientNotFound(['MAPI supported mail client'
 
582
                                          ' (error %d)' % (e.code,)])
 
583
 
 
584
 
538
585
mail_client_registry.register('mapi', MAPIClient,
539
586
                              help=MAPIClient.__doc__)
540
587
 
552
599
    _client_commands = ['osascript']
553
600
 
554
601
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
555
 
                                from_=None):
556
 
       """See ExternalMailClient._get_compose_commandline"""
557
 
 
558
 
       fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
559
 
                                         suffix=".scpt")
560
 
       try:
561
 
           os.write(fd, 'tell application "Mail"\n')
562
 
           os.write(fd, 'set newMessage to make new outgoing message\n')
563
 
           os.write(fd, 'tell newMessage\n')
564
 
           if to is not None:
565
 
               os.write(fd, 'make new to recipient with properties'
566
 
                   ' {address:"%s"}\n' % to)
567
 
           if from_ is not None:
568
 
               # though from_ doesn't actually seem to be used
569
 
               os.write(fd, 'set sender to "%s"\n'
570
 
                   % sender.replace('"', '\\"'))
571
 
           if subject is not None:
572
 
               os.write(fd, 'set subject to "%s"\n'
573
 
                   % subject.replace('"', '\\"'))
574
 
           if body is not None:
575
 
               # FIXME: would be nice to prepend the body to the
576
 
               # existing content (e.g., preserve signature), but
577
 
               # can't seem to figure out the right applescript
578
 
               # incantation.
579
 
               os.write(fd, 'set content to "%s\\n\n"\n' %
580
 
                   body.replace('"', '\\"').replace('\n', '\\n'))
581
 
 
582
 
           if attach_path is not None:
583
 
               # FIXME: would be nice to first append a newline to
584
 
               # ensure the attachment is on a new paragraph, but
585
 
               # can't seem to figure out the right applescript
586
 
               # incantation.
587
 
               os.write(fd, 'tell content to make new attachment'
588
 
                   ' with properties {file name:"%s"}'
589
 
                   ' at after the last paragraph\n'
590
 
                   % self._encode_path(attach_path, 'attachment'))
591
 
           os.write(fd, 'set visible to true\n')
592
 
           os.write(fd, 'end tell\n')
593
 
           os.write(fd, 'end tell\n')
594
 
       finally:
595
 
           os.close(fd) # Just close the handle but do not remove the file.
596
 
       return [self.temp_file]
 
602
                                 from_=None):
 
603
        """See ExternalMailClient._get_compose_commandline"""
 
604
 
 
605
        fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
 
606
                                              suffix=".scpt")
 
607
        try:
 
608
            os.write(fd, 'tell application "Mail"\n')
 
609
            os.write(fd, 'set newMessage to make new outgoing message\n')
 
610
            os.write(fd, 'tell newMessage\n')
 
611
            if to is not None:
 
612
                os.write(fd, 'make new to recipient with properties'
 
613
                         ' {address:"%s"}\n' % to)
 
614
            if from_ is not None:
 
615
                # though from_ doesn't actually seem to be used
 
616
                os.write(fd, 'set sender to "%s"\n'
 
617
                         % from_.replace('"', '\\"'))
 
618
            if subject is not None:
 
619
                os.write(fd, 'set subject to "%s"\n'
 
620
                         % subject.replace('"', '\\"'))
 
621
            if body is not None:
 
622
                # FIXME: would be nice to prepend the body to the
 
623
                # existing content (e.g., preserve signature), but
 
624
                # can't seem to figure out the right applescript
 
625
                # incantation.
 
626
                os.write(fd, 'set content to "%s\\n\n"\n' %
 
627
                         body.replace('"', '\\"').replace('\n', '\\n'))
 
628
 
 
629
            if attach_path is not None:
 
630
                # FIXME: would be nice to first append a newline to
 
631
                # ensure the attachment is on a new paragraph, but
 
632
                # can't seem to figure out the right applescript
 
633
                # incantation.
 
634
                os.write(fd, 'tell content to make new attachment'
 
635
                         ' with properties {file name:"%s"}'
 
636
                         ' at after the last paragraph\n'
 
637
                         % self._encode_path(attach_path, 'attachment'))
 
638
            os.write(fd, 'set visible to true\n')
 
639
            os.write(fd, 'end tell\n')
 
640
            os.write(fd, 'end tell\n')
 
641
        finally:
 
642
            os.close(fd)  # Just close the handle but do not remove the file.
 
643
        return [self.temp_file]
 
644
 
 
645
 
597
646
mail_client_registry.register('mail.app', MailApp,
598
647
                              help=MailApp.__doc__)
599
648
 
616
665
        """See MailClient.compose"""
617
666
        try:
618
667
            return self._mail_client().compose(prompt, to, subject,
619
 
                                               attachment, mimie_subtype,
 
668
                                               attachment, mime_subtype,
620
669
                                               extension, basename, body)
621
 
        except errors.MailClientNotFound:
622
 
            return Editor(self.config).compose(prompt, to, subject,
623
 
                          attachment, mimie_subtype, extension, body)
 
670
        except MailClientNotFound:
 
671
            return Editor(self.config).compose(
 
672
                prompt, to, subject, attachment, mime_subtype, extension, body)
624
673
 
625
674
    def compose_merge_request(self, to, subject, directive, basename=None,
626
675
                              body=None):
627
676
        """See MailClient.compose_merge_request"""
628
677
        try:
629
 
            return self._mail_client().compose_merge_request(to, subject,
630
 
                    directive, basename=basename, body=body)
631
 
        except errors.MailClientNotFound:
632
 
            return Editor(self.config).compose_merge_request(to, subject,
633
 
                          directive, basename=basename, body=body)
634
 
mail_client_registry.register('default', DefaultMail,
 
678
            return self._mail_client().compose_merge_request(
 
679
                to, subject, directive, basename=basename, body=body)
 
680
        except MailClientNotFound:
 
681
            return Editor(self.config).compose_merge_request(
 
682
                to, subject, directive, basename=basename, body=body)
 
683
 
 
684
 
 
685
mail_client_registry.register(u'default', DefaultMail,
635
686
                              help=DefaultMail.__doc__)
636
 
mail_client_registry.default_key = 'default'
637
 
 
638
 
 
 
687
mail_client_registry.default_key = u'default'
 
688
 
 
689
opt_mail_client = _mod_config.RegistryOption(
 
690
    'mail_client', mail_client_registry, help='E-mail client to use.',
 
691
    invalid='error')