/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 bzrlib/mail_client.py

  • Committer: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

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