/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: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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
 
142
172
            basename = 'attachment'
143
173
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
144
174
        attach_path = osutils.pathjoin(pathname, basename + extension)
145
 
        outfile = open(attach_path, 'wb')
146
 
        try:
 
175
        with open(attach_path, 'wb') as outfile:
147
176
            outfile.write(attachment)
148
 
        finally:
149
 
            outfile.close()
150
177
        if body is not None:
151
178
            kwargs = {'body': body}
152
179
        else:
182
209
                                                         **kwargs))
183
210
            try:
184
211
                subprocess.call(cmdline)
185
 
            except OSError, e:
 
212
            except OSError as e:
186
213
                if e.errno != errno.ENOENT:
187
214
                    raise
188
215
            else:
189
216
                break
190
217
        else:
191
 
            raise errors.MailClientNotFound(self._client_commands)
 
218
            raise MailClientNotFound(self._client_commands)
192
219
 
193
220
    def _get_compose_commandline(self, to, subject, attach_path, body):
194
221
        """Determine the commandline to use for composing a message
208
235
        :param  u:  possible unicode string.
209
236
        :return:    encoded string if u is unicode, u itself otherwise.
210
237
        """
211
 
        if isinstance(u, unicode):
 
238
        if not PY3 and isinstance(u, text_type):
212
239
            return u.encode(osutils.get_user_encoding(), 'replace')
213
240
        return u
214
241
 
221
248
                        path itself otherwise.
222
249
        :raise:         UnableEncodePath.
223
250
        """
224
 
        if isinstance(path, unicode):
 
251
        if not PY3 and isinstance(path, text_type):
225
252
            try:
226
253
                return path.encode(osutils.get_user_encoding())
227
254
            except UnicodeEncodeError:
250
277
        if body is not None:
251
278
            message_options['body'] = body
252
279
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
 
                        sorted(message_options.iteritems())]
 
280
                        sorted(message_options.items())]
254
281
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
255
 
            '&'.join(options_list))]
 
282
                                  '&'.join(options_list))]
 
283
 
 
284
 
256
285
mail_client_registry.register('evolution', Evolution,
257
286
                              help=Evolution.__doc__)
258
287
 
266
295
        """See ExternalMailClient._get_compose_commandline"""
267
296
        message_options = []
268
297
        if subject is not None:
269
 
            message_options.extend(['-s', self._encode_safe(subject)])
 
298
            message_options.extend(
 
299
                ['-s', self._encode_safe(subject)])
270
300
        if attach_path is not None:
271
 
            message_options.extend(['-a',
272
 
                self._encode_path(attach_path, 'attachment')])
 
301
            message_options.extend(
 
302
                ['-a', self._encode_path(attach_path, 'attachment')])
273
303
        if body is not None:
274
304
            # Store the temp file object in self, so that it does not get
275
305
            # garbage collected and delete the file before mutt can read it.
276
306
            self._temp_file = tempfile.NamedTemporaryFile(
277
 
                prefix="mutt-body-", suffix=".txt")
 
307
                prefix="mutt-body-", suffix=".txt", mode="w+")
278
308
            self._temp_file.write(body)
279
309
            self._temp_file.flush()
280
310
            message_options.extend(['-i', self._temp_file.name])
281
311
        if to is not None:
282
312
            message_options.extend(['--', self._encode_safe(to)])
283
313
        return message_options
 
314
 
 
315
 
284
316
mail_client_registry.register('mutt', Mutt,
285
317
                              help=Mutt.__doc__)
286
318
 
295
327
    send attachments.
296
328
    """
297
329
 
298
 
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
 
330
    _client_commands = [
 
331
        'thunderbird', 'mozilla-thunderbird', 'icedove',
299
332
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
333
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
301
334
 
310
343
            message_options['attachment'] = urlutils.local_path_to_url(
311
344
                attach_path)
312
345
        if body is not None:
313
 
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
 
346
            options_list = ['body=%s' %
 
347
                            urlutils.quote(self._encode_safe(body))]
314
348
        else:
315
349
            options_list = []
316
350
        options_list.extend(["%s='%s'" % (k, v) for k, v in
317
 
                        sorted(message_options.iteritems())])
 
351
                             sorted(message_options.items())])
318
352
        return ['-compose', ','.join(options_list)]
 
353
 
 
354
 
319
355
mail_client_registry.register('thunderbird', Thunderbird,
320
356
                              help=Thunderbird.__doc__)
321
357
 
331
367
        if subject is not None:
332
368
            message_options.extend(['-s', self._encode_safe(subject)])
333
369
        if attach_path is not None:
334
 
            message_options.extend(['--attach',
335
 
                self._encode_path(attach_path, 'attachment')])
 
370
            message_options.extend(
 
371
                ['--attach', self._encode_path(attach_path, 'attachment')])
336
372
        if to is not None:
337
373
            message_options.extend([self._encode_safe(to)])
338
374
        return message_options
 
375
 
 
376
 
339
377
mail_client_registry.register('kmail', KMail,
340
378
                              help=KMail.__doc__)
341
379
 
352
390
        """See ExternalMailClient._get_compose_commandline"""
353
391
        compose_url = []
354
392
        if from_ is not None:
355
 
            compose_url.append('from=' + urllib.quote(from_))
 
393
            compose_url.append('from=' + urlutils.quote(from_))
356
394
        if subject is not None:
357
 
            # Don't use urllib.quote_plus because Claws doesn't seem
 
395
            # Don't use urlutils.quote_plus because Claws doesn't seem
358
396
            # to recognise spaces encoded as "+".
359
397
            compose_url.append(
360
 
                'subject=' + urllib.quote(self._encode_safe(subject)))
 
398
                'subject=' + urlutils.quote(self._encode_safe(subject)))
361
399
        if body is not None:
362
400
            compose_url.append(
363
 
                'body=' + urllib.quote(self._encode_safe(body)))
 
401
                'body=' + urlutils.quote(self._encode_safe(body)))
364
402
        # to must be supplied for the claws-mail --compose syntax to work.
365
403
        if to is None:
366
 
            raise errors.NoMailAddressSpecified()
 
404
            raise NoMailAddressSpecified()
367
405
        compose_url = 'mailto:%s?%s' % (
368
406
            self._encode_safe(to), '&'.join(compose_url))
369
407
        # Collect command-line options.
377
415
                 extension, body=None, from_=None):
378
416
        """See ExternalMailClient._compose"""
379
417
        if from_ is None:
380
 
            from_ = self.config.get_user_option('email')
 
418
            from_ = self.config.get('email')
381
419
        super(Claws, self)._compose(prompt, to, subject, attach_path,
382
420
                                    mime_subtype, extension, body, from_)
383
421
 
387
425
 
388
426
 
389
427
class XDGEmail(BodyExternalMailClient):
390
 
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
 
428
    __doc__ = """xdg-email attempts to invoke the preferred mail client"""
391
429
 
392
430
    _client_commands = ['xdg-email']
393
431
 
394
432
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
395
433
        """See ExternalMailClient._get_compose_commandline"""
396
434
        if not to:
397
 
            raise errors.NoMailAddressSpecified()
 
435
            raise NoMailAddressSpecified()
398
436
        commandline = [self._encode_safe(to)]
399
437
        if subject is not None:
400
438
            commandline.extend(['--subject', self._encode_safe(subject)])
401
439
        if attach_path is not None:
402
440
            commandline.extend(['--attach',
403
 
                self._encode_path(attach_path, 'attachment')])
 
441
                                self._encode_path(attach_path, 'attachment')])
404
442
        if body is not None:
405
443
            commandline.extend(['--body', self._encode_safe(body)])
406
444
        return commandline
 
445
 
 
446
 
407
447
mail_client_registry.register('xdg-email', XDGEmail,
408
448
                              help=XDGEmail.__doc__)
409
449
 
442
482
        after being read by Emacs.)
443
483
        """
444
484
 
445
 
        _defun = r"""(defun bzr-add-mime-att (file)
 
485
        _defun = br"""(defun bzr-add-mime-att (file)
446
486
  "Attach FILE to a mail buffer as a MIME attachment."
447
487
  (let ((agent mail-user-agent))
448
488
    (if (and file (file-exists-p file))
478
518
        try:
479
519
            os.write(fd, _defun)
480
520
        finally:
481
 
            os.close(fd) # Just close the handle but do not remove the file.
 
521
            os.close(fd)  # Just close the handle but do not remove the file.
482
522
        return temp_file
483
523
 
484
524
    def _get_compose_commandline(self, to, subject, attach_path):
506
546
            elisp = self._prepare_send_function()
507
547
            self.elisp_tmp_file = elisp
508
548
            lmmform = '(load "%s")' % elisp
509
 
            mmform  = '(bzr-add-mime-att "%s")' % \
 
549
            mmform = '(bzr-add-mime-att "%s")' % \
510
550
                self._encode_path(attach_path, 'attachment')
511
551
            rmform = '(delete-file "%s")' % elisp
512
552
            commandline.append(lmmform)
514
554
            commandline.append(rmform)
515
555
 
516
556
        return commandline
 
557
 
 
558
 
517
559
mail_client_registry.register('emacsclient', EmacsMail,
518
560
                              help=EmacsMail.__doc__)
519
561
 
527
569
 
528
570
        This implementation uses MAPI via the simplemapi ctypes wrapper
529
571
        """
530
 
        from bzrlib.util import simplemapi
 
572
        from .util import simplemapi
531
573
        try:
532
574
            simplemapi.SendMail(to or '', subject or '', body or '',
533
575
                                attach_path)
534
 
        except simplemapi.MAPIError, e:
 
576
        except simplemapi.MAPIError as e:
535
577
            if e.code != simplemapi.MAPI_USER_ABORT:
536
 
                raise errors.MailClientNotFound(['MAPI supported mail client'
537
 
                                                 ' (error %d)' % (e.code,)])
 
578
                raise MailClientNotFound(['MAPI supported mail client'
 
579
                                          ' (error %d)' % (e.code,)])
 
580
 
 
581
 
538
582
mail_client_registry.register('mapi', MAPIClient,
539
583
                              help=MAPIClient.__doc__)
540
584
 
552
596
    _client_commands = ['osascript']
553
597
 
554
598
    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]
 
599
                                 from_=None):
 
600
        """See ExternalMailClient._get_compose_commandline"""
 
601
 
 
602
        fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
 
603
                                              suffix=".scpt")
 
604
        try:
 
605
            os.write(fd, 'tell application "Mail"\n')
 
606
            os.write(fd, 'set newMessage to make new outgoing message\n')
 
607
            os.write(fd, 'tell newMessage\n')
 
608
            if to is not None:
 
609
                os.write(fd, 'make new to recipient with properties'
 
610
                         ' {address:"%s"}\n' % to)
 
611
            if from_ is not None:
 
612
                # though from_ doesn't actually seem to be used
 
613
                os.write(fd, 'set sender to "%s"\n'
 
614
                         % from_.replace('"', '\\"'))
 
615
            if subject is not None:
 
616
                os.write(fd, 'set subject to "%s"\n'
 
617
                         % subject.replace('"', '\\"'))
 
618
            if body is not None:
 
619
                # FIXME: would be nice to prepend the body to the
 
620
                # existing content (e.g., preserve signature), but
 
621
                # can't seem to figure out the right applescript
 
622
                # incantation.
 
623
                os.write(fd, 'set content to "%s\\n\n"\n' %
 
624
                         body.replace('"', '\\"').replace('\n', '\\n'))
 
625
 
 
626
            if attach_path is not None:
 
627
                # FIXME: would be nice to first append a newline to
 
628
                # ensure the attachment is on a new paragraph, but
 
629
                # can't seem to figure out the right applescript
 
630
                # incantation.
 
631
                os.write(fd, 'tell content to make new attachment'
 
632
                         ' with properties {file name:"%s"}'
 
633
                         ' at after the last paragraph\n'
 
634
                         % self._encode_path(attach_path, 'attachment'))
 
635
            os.write(fd, 'set visible to true\n')
 
636
            os.write(fd, 'end tell\n')
 
637
            os.write(fd, 'end tell\n')
 
638
        finally:
 
639
            os.close(fd)  # Just close the handle but do not remove the file.
 
640
        return [self.temp_file]
 
641
 
 
642
 
597
643
mail_client_registry.register('mail.app', MailApp,
598
644
                              help=MailApp.__doc__)
599
645
 
616
662
        """See MailClient.compose"""
617
663
        try:
618
664
            return self._mail_client().compose(prompt, to, subject,
619
 
                                               attachment, mimie_subtype,
 
665
                                               attachment, mime_subtype,
620
666
                                               extension, basename, body)
621
 
        except errors.MailClientNotFound:
622
 
            return Editor(self.config).compose(prompt, to, subject,
623
 
                          attachment, mimie_subtype, extension, body)
 
667
        except MailClientNotFound:
 
668
            return Editor(self.config).compose(
 
669
                prompt, to, subject, attachment, mime_subtype, extension, body)
624
670
 
625
671
    def compose_merge_request(self, to, subject, directive, basename=None,
626
672
                              body=None):
627
673
        """See MailClient.compose_merge_request"""
628
674
        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,
 
675
            return self._mail_client().compose_merge_request(
 
676
                to, subject, directive, basename=basename, body=body)
 
677
        except MailClientNotFound:
 
678
            return Editor(self.config).compose_merge_request(
 
679
                to, subject, directive, basename=basename, body=body)
 
680
 
 
681
 
 
682
mail_client_registry.register(u'default', DefaultMail,
635
683
                              help=DefaultMail.__doc__)
636
 
mail_client_registry.default_key = 'default'
637
 
 
638
 
 
 
684
mail_client_registry.default_key = u'default'
 
685
 
 
686
opt_mail_client = _mod_config.RegistryOption(
 
687
    'mail_client', mail_client_registry, help='E-mail client to use.',
 
688
    invalid='error')