/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: Marius Kruger
  • Date: 2010-07-10 21:28:56 UTC
  • mto: (5384.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5385.
  • Revision ID: marius.kruger@enerweb.co.za-20100710212856-uq4ji3go0u5se7hx
* Update documentation
* add NEWS

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