/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: Vincent Ladeuil
  • Date: 2010-07-07 11:21:19 UTC
  • mto: (5193.7.1 unify-confs)
  • mto: This revision was merged to the branch mainline in revision 5349.
  • Revision ID: v.ladeuil+lp@free.fr-20100707112119-jwyh312df41w6l0o
Revert previous change as I can't reproduce the related problem anymore.

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import subprocess
20
20
import sys
21
21
import tempfile
 
22
import urllib
22
23
 
23
 
import breezy
24
 
from . import (
25
 
    config as _mod_config,
 
24
import bzrlib
 
25
from bzrlib import (
26
26
    email_message,
27
27
    errors,
28
28
    msgeditor,
29
29
    osutils,
30
30
    urlutils,
31
 
    registry,
 
31
    registry
32
32
    )
33
33
 
34
34
mail_client_registry = registry.Registry()
35
35
 
36
36
 
37
 
class MailClientNotFound(errors.BzrError):
38
 
 
39
 
    _fmt = "Unable to find mail client with the following names:"\
40
 
        " %(mail_command_list_string)s"
41
 
 
42
 
    def __init__(self, mail_command_list):
43
 
        mail_command_list_string = ', '.join(mail_command_list)
44
 
        errors.BzrError.__init__(
45
 
            self, mail_command_list=mail_command_list,
46
 
            mail_command_list_string=mail_command_list_string)
47
 
 
48
 
 
49
 
class NoMessageSupplied(errors.BzrError):
50
 
 
51
 
    _fmt = "No message supplied."
52
 
 
53
 
 
54
 
class NoMailAddressSpecified(errors.BzrError):
55
 
 
56
 
    _fmt = "No mail-to address (--mail-to) or output (-o) specified."
57
 
 
58
 
 
59
37
class MailClient(object):
60
38
    """A mail client that can send messages with attachements."""
61
39
 
97
75
        prompt = self._get_merge_prompt("Please describe these changes:", to,
98
76
                                        subject, directive)
99
77
        self.compose(prompt, to, subject, directive,
100
 
                     'x-patch', '.patch', basename, body)
 
78
            'x-patch', '.patch', basename, body)
101
79
 
102
80
    def _get_merge_prompt(self, prompt, to, subject, attachment):
103
81
        """Generate a prompt string.  Overridden by Editor.
127
105
                extension, basename=None, body=None):
128
106
        """See MailClient.compose"""
129
107
        if not to:
130
 
            raise NoMailAddressSpecified()
 
108
            raise errors.NoMailAddressSpecified()
131
109
        body = msgeditor.edit_commit_message(prompt, start_message=body)
132
110
        if body == '':
133
 
            raise NoMessageSupplied()
 
111
            raise errors.NoMessageSupplied()
134
112
        email_message.EmailMessage.send(self.config,
135
 
                                        self.config.get('email'),
 
113
                                        self.config.username(),
136
114
                                        to,
137
115
                                        subject,
138
116
                                        body,
139
117
                                        attachment,
140
118
                                        attachment_mime_subtype=mime_subtype)
141
 
 
142
 
 
143
119
mail_client_registry.register('editor', Editor,
144
120
                              help=Editor.__doc__)
145
121
 
166
142
            basename = 'attachment'
167
143
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
168
144
        attach_path = osutils.pathjoin(pathname, basename + extension)
169
 
        with open(attach_path, 'wb') as outfile:
 
145
        outfile = open(attach_path, 'wb')
 
146
        try:
170
147
            outfile.write(attachment)
 
148
        finally:
 
149
            outfile.close()
171
150
        if body is not None:
172
151
            kwargs = {'body': body}
173
152
        else:
203
182
                                                         **kwargs))
204
183
            try:
205
184
                subprocess.call(cmdline)
206
 
            except OSError as e:
 
185
            except OSError, e:
207
186
                if e.errno != errno.ENOENT:
208
187
                    raise
209
188
            else:
210
189
                break
211
190
        else:
212
 
            raise MailClientNotFound(self._client_commands)
 
191
            raise errors.MailClientNotFound(self._client_commands)
213
192
 
214
193
    def _get_compose_commandline(self, to, subject, attach_path, body):
215
194
        """Determine the commandline to use for composing a message
229
208
        :param  u:  possible unicode string.
230
209
        :return:    encoded string if u is unicode, u itself otherwise.
231
210
        """
 
211
        if isinstance(u, unicode):
 
212
            return u.encode(osutils.get_user_encoding(), 'replace')
232
213
        return u
233
214
 
234
215
    def _encode_path(self, path, kind):
240
221
                        path itself otherwise.
241
222
        :raise:         UnableEncodePath.
242
223
        """
 
224
        if isinstance(path, unicode):
 
225
            try:
 
226
                return path.encode(osutils.get_user_encoding())
 
227
            except UnicodeEncodeError:
 
228
                raise errors.UnableEncodePath(path, kind)
243
229
        return path
244
230
 
245
231
 
264
250
        if body is not None:
265
251
            message_options['body'] = body
266
252
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
267
 
                        sorted(message_options.items())]
 
253
                        sorted(message_options.iteritems())]
268
254
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
269
 
                                  '&'.join(options_list))]
270
 
 
271
 
 
 
255
            '&'.join(options_list))]
272
256
mail_client_registry.register('evolution', Evolution,
273
257
                              help=Evolution.__doc__)
274
258
 
282
266
        """See ExternalMailClient._get_compose_commandline"""
283
267
        message_options = []
284
268
        if subject is not None:
285
 
            message_options.extend(
286
 
                ['-s', self._encode_safe(subject)])
 
269
            message_options.extend(['-s', self._encode_safe(subject)])
287
270
        if attach_path is not None:
288
 
            message_options.extend(
289
 
                ['-a', self._encode_path(attach_path, 'attachment')])
 
271
            message_options.extend(['-a',
 
272
                self._encode_path(attach_path, 'attachment')])
290
273
        if body is not None:
291
274
            # Store the temp file object in self, so that it does not get
292
275
            # garbage collected and delete the file before mutt can read it.
293
276
            self._temp_file = tempfile.NamedTemporaryFile(
294
 
                prefix="mutt-body-", suffix=".txt", mode="w+")
 
277
                prefix="mutt-body-", suffix=".txt")
295
278
            self._temp_file.write(body)
296
279
            self._temp_file.flush()
297
280
            message_options.extend(['-i', self._temp_file.name])
298
281
        if to is not None:
299
282
            message_options.extend(['--', self._encode_safe(to)])
300
283
        return message_options
301
 
 
302
 
 
303
284
mail_client_registry.register('mutt', Mutt,
304
285
                              help=Mutt.__doc__)
305
286
 
314
295
    send attachments.
315
296
    """
316
297
 
317
 
    _client_commands = [
318
 
        'thunderbird', 'mozilla-thunderbird', 'icedove',
 
298
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
319
299
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
320
300
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
321
301
 
330
310
            message_options['attachment'] = urlutils.local_path_to_url(
331
311
                attach_path)
332
312
        if body is not None:
333
 
            options_list = ['body=%s' %
334
 
                            urlutils.quote(self._encode_safe(body))]
 
313
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
335
314
        else:
336
315
            options_list = []
337
316
        options_list.extend(["%s='%s'" % (k, v) for k, v in
338
 
                             sorted(message_options.items())])
 
317
                        sorted(message_options.iteritems())])
339
318
        return ['-compose', ','.join(options_list)]
340
 
 
341
 
 
342
319
mail_client_registry.register('thunderbird', Thunderbird,
343
320
                              help=Thunderbird.__doc__)
344
321
 
354
331
        if subject is not None:
355
332
            message_options.extend(['-s', self._encode_safe(subject)])
356
333
        if attach_path is not None:
357
 
            message_options.extend(
358
 
                ['--attach', self._encode_path(attach_path, 'attachment')])
 
334
            message_options.extend(['--attach',
 
335
                self._encode_path(attach_path, 'attachment')])
359
336
        if to is not None:
360
337
            message_options.extend([self._encode_safe(to)])
361
338
        return message_options
362
 
 
363
 
 
364
339
mail_client_registry.register('kmail', KMail,
365
340
                              help=KMail.__doc__)
366
341
 
377
352
        """See ExternalMailClient._get_compose_commandline"""
378
353
        compose_url = []
379
354
        if from_ is not None:
380
 
            compose_url.append('from=' + urlutils.quote(from_))
 
355
            compose_url.append('from=' + urllib.quote(from_))
381
356
        if subject is not None:
382
 
            # Don't use urlutils.quote_plus because Claws doesn't seem
 
357
            # Don't use urllib.quote_plus because Claws doesn't seem
383
358
            # to recognise spaces encoded as "+".
384
359
            compose_url.append(
385
 
                'subject=' + urlutils.quote(self._encode_safe(subject)))
 
360
                'subject=' + urllib.quote(self._encode_safe(subject)))
386
361
        if body is not None:
387
362
            compose_url.append(
388
 
                'body=' + urlutils.quote(self._encode_safe(body)))
 
363
                'body=' + urllib.quote(self._encode_safe(body)))
389
364
        # to must be supplied for the claws-mail --compose syntax to work.
390
365
        if to is None:
391
 
            raise NoMailAddressSpecified()
 
366
            raise errors.NoMailAddressSpecified()
392
367
        compose_url = 'mailto:%s?%s' % (
393
368
            self._encode_safe(to), '&'.join(compose_url))
394
369
        # Collect command-line options.
402
377
                 extension, body=None, from_=None):
403
378
        """See ExternalMailClient._compose"""
404
379
        if from_ is None:
405
 
            from_ = self.config.get('email')
 
380
            from_ = self.config.get_user_option('email')
406
381
        super(Claws, self)._compose(prompt, to, subject, attach_path,
407
382
                                    mime_subtype, extension, body, from_)
408
383
 
412
387
 
413
388
 
414
389
class XDGEmail(BodyExternalMailClient):
415
 
    __doc__ = """xdg-email attempts to invoke the preferred mail client"""
 
390
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
416
391
 
417
392
    _client_commands = ['xdg-email']
418
393
 
419
394
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
420
395
        """See ExternalMailClient._get_compose_commandline"""
421
396
        if not to:
422
 
            raise NoMailAddressSpecified()
 
397
            raise errors.NoMailAddressSpecified()
423
398
        commandline = [self._encode_safe(to)]
424
399
        if subject is not None:
425
400
            commandline.extend(['--subject', self._encode_safe(subject)])
426
401
        if attach_path is not None:
427
402
            commandline.extend(['--attach',
428
 
                                self._encode_path(attach_path, 'attachment')])
 
403
                self._encode_path(attach_path, 'attachment')])
429
404
        if body is not None:
430
405
            commandline.extend(['--body', self._encode_safe(body)])
431
406
        return commandline
432
 
 
433
 
 
434
407
mail_client_registry.register('xdg-email', XDGEmail,
435
408
                              help=XDGEmail.__doc__)
436
409
 
469
442
        after being read by Emacs.)
470
443
        """
471
444
 
472
 
        _defun = br"""(defun bzr-add-mime-att (file)
 
445
        _defun = r"""(defun bzr-add-mime-att (file)
473
446
  "Attach FILE to a mail buffer as a MIME attachment."
474
447
  (let ((agent mail-user-agent))
475
448
    (if (and file (file-exists-p file))
505
478
        try:
506
479
            os.write(fd, _defun)
507
480
        finally:
508
 
            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.
509
482
        return temp_file
510
483
 
511
484
    def _get_compose_commandline(self, to, subject, attach_path):
533
506
            elisp = self._prepare_send_function()
534
507
            self.elisp_tmp_file = elisp
535
508
            lmmform = '(load "%s")' % elisp
536
 
            mmform = '(bzr-add-mime-att "%s")' % \
 
509
            mmform  = '(bzr-add-mime-att "%s")' % \
537
510
                self._encode_path(attach_path, 'attachment')
538
511
            rmform = '(delete-file "%s")' % elisp
539
512
            commandline.append(lmmform)
541
514
            commandline.append(rmform)
542
515
 
543
516
        return commandline
544
 
 
545
 
 
546
517
mail_client_registry.register('emacsclient', EmacsMail,
547
518
                              help=EmacsMail.__doc__)
548
519
 
556
527
 
557
528
        This implementation uses MAPI via the simplemapi ctypes wrapper
558
529
        """
559
 
        from .util import simplemapi
 
530
        from bzrlib.util import simplemapi
560
531
        try:
561
532
            simplemapi.SendMail(to or '', subject or '', body or '',
562
533
                                attach_path)
563
 
        except simplemapi.MAPIError as e:
 
534
        except simplemapi.MAPIError, e:
564
535
            if e.code != simplemapi.MAPI_USER_ABORT:
565
 
                raise MailClientNotFound(['MAPI supported mail client'
566
 
                                          ' (error %d)' % (e.code,)])
567
 
 
568
 
 
 
536
                raise errors.MailClientNotFound(['MAPI supported mail client'
 
537
                                                 ' (error %d)' % (e.code,)])
569
538
mail_client_registry.register('mapi', MAPIClient,
570
539
                              help=MAPIClient.__doc__)
571
540
 
583
552
    _client_commands = ['osascript']
584
553
 
585
554
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
586
 
                                 from_=None):
587
 
        """See ExternalMailClient._get_compose_commandline"""
588
 
 
589
 
        fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
590
 
                                              suffix=".scpt")
591
 
        try:
592
 
            os.write(fd, 'tell application "Mail"\n')
593
 
            os.write(fd, 'set newMessage to make new outgoing message\n')
594
 
            os.write(fd, 'tell newMessage\n')
595
 
            if to is not None:
596
 
                os.write(fd, 'make new to recipient with properties'
597
 
                         ' {address:"%s"}\n' % to)
598
 
            if from_ is not None:
599
 
                # though from_ doesn't actually seem to be used
600
 
                os.write(fd, 'set sender to "%s"\n'
601
 
                         % from_.replace('"', '\\"'))
602
 
            if subject is not None:
603
 
                os.write(fd, 'set subject to "%s"\n'
604
 
                         % subject.replace('"', '\\"'))
605
 
            if body is not None:
606
 
                # FIXME: would be nice to prepend the body to the
607
 
                # existing content (e.g., preserve signature), but
608
 
                # can't seem to figure out the right applescript
609
 
                # incantation.
610
 
                os.write(fd, 'set content to "%s\\n\n"\n' %
611
 
                         body.replace('"', '\\"').replace('\n', '\\n'))
612
 
 
613
 
            if attach_path is not None:
614
 
                # FIXME: would be nice to first append a newline to
615
 
                # ensure the attachment is on a new paragraph, but
616
 
                # can't seem to figure out the right applescript
617
 
                # incantation.
618
 
                os.write(fd, 'tell content to make new attachment'
619
 
                         ' with properties {file name:"%s"}'
620
 
                         ' at after the last paragraph\n'
621
 
                         % self._encode_path(attach_path, 'attachment'))
622
 
            os.write(fd, 'set visible to true\n')
623
 
            os.write(fd, 'end tell\n')
624
 
            os.write(fd, 'end tell\n')
625
 
        finally:
626
 
            os.close(fd)  # Just close the handle but do not remove the file.
627
 
        return [self.temp_file]
628
 
 
629
 
 
 
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]
630
597
mail_client_registry.register('mail.app', MailApp,
631
598
                              help=MailApp.__doc__)
632
599
 
649
616
        """See MailClient.compose"""
650
617
        try:
651
618
            return self._mail_client().compose(prompt, to, subject,
652
 
                                               attachment, mime_subtype,
 
619
                                               attachment, mimie_subtype,
653
620
                                               extension, basename, body)
654
 
        except MailClientNotFound:
655
 
            return Editor(self.config).compose(
656
 
                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)
657
624
 
658
625
    def compose_merge_request(self, to, subject, directive, basename=None,
659
626
                              body=None):
660
627
        """See MailClient.compose_merge_request"""
661
628
        try:
662
 
            return self._mail_client().compose_merge_request(
663
 
                to, subject, directive, basename=basename, body=body)
664
 
        except MailClientNotFound:
665
 
            return Editor(self.config).compose_merge_request(
666
 
                to, subject, directive, basename=basename, body=body)
667
 
 
668
 
 
669
 
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,
670
635
                              help=DefaultMail.__doc__)
671
 
mail_client_registry.default_key = u'default'
672
 
 
673
 
opt_mail_client = _mod_config.RegistryOption(
674
 
    'mail_client', mail_client_registry, help='E-mail client to use.',
675
 
    invalid='error')
 
636
mail_client_registry.default_key = 'default'
 
637
 
 
638