/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: John Ferlito
  • Date: 2009-09-02 04:31:45 UTC
  • mto: (4665.7.1 serve-init)
  • mto: This revision was merged to the branch mainline in revision 4913.
  • Revision ID: johnf@inodes.org-20090902043145-gxdsfw03ilcwbyn5
Add a debian init script for bzr --serve

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2010 Canonical Ltd
 
1
# Copyright (C) 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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.
111
89
 
112
90
 
113
91
class Editor(MailClient):
114
 
    __doc__ = """DIY mail client that uses commit message editor"""
 
92
    """DIY mail client that uses commit message editor"""
115
93
 
116
94
    supports_body = True
117
95
 
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
 
246
232
class ExternalMailClient(BodyExternalMailClient):
247
 
    __doc__ = """An external mail client."""
 
233
    """An external mail client."""
248
234
 
249
235
    supports_body = False
250
236
 
251
237
 
252
238
class Evolution(BodyExternalMailClient):
253
 
    __doc__ = """Evolution mail client."""
 
239
    """Evolution mail client."""
254
240
 
255
241
    _client_commands = ['evolution']
256
242
 
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
 
275
259
 
276
260
class Mutt(BodyExternalMailClient):
277
 
    __doc__ = """Mutt mail client."""
 
261
    """Mutt mail client."""
278
262
 
279
263
    _client_commands = ['mutt']
280
264
 
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
 
306
287
 
307
288
class Thunderbird(BodyExternalMailClient):
308
 
    __doc__ = """Mozilla Thunderbird (or Icedove)
 
289
    """Mozilla Thunderbird (or Icedove)
309
290
 
310
291
    Note that Thunderbird 1.5 is buggy and does not support setting
311
292
    "to" simultaneously with including a attachment.
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
 
345
322
 
346
323
class KMail(ExternalMailClient):
347
 
    __doc__ = """KDE mail client."""
 
324
    """KDE mail client."""
348
325
 
349
326
    _client_commands = ['kmail']
350
327
 
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
 
367
342
 
368
343
class Claws(ExternalMailClient):
369
 
    __doc__ = """Claws mail client."""
 
344
    """Claws mail client."""
370
345
 
371
346
    supports_body = True
372
347
 
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
    """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
 
437
410
 
438
411
class EmacsMail(ExternalMailClient):
439
 
    __doc__ = """Call emacsclient to have a mail buffer.
 
412
    """Call emacsclient to have a mail buffer.
440
413
 
441
414
    This only work for emacs >= 22.1 due to recent -e/--eval support.
442
415
 
451
424
 
452
425
    _client_commands = ['emacsclient']
453
426
 
454
 
    def __init__(self, config):
455
 
        super(EmacsMail, self).__init__(config)
456
 
        self.elisp_tmp_file = None
457
 
 
458
427
    def _prepare_send_function(self):
459
428
        """Write our wrapper function into a temporary file.
460
429
 
469
438
        after being read by Emacs.)
470
439
        """
471
440
 
472
 
        _defun = br"""(defun bzr-add-mime-att (file)
 
441
        _defun = r"""(defun bzr-add-mime-att (file)
473
442
  "Attach FILE to a mail buffer as a MIME attachment."
474
443
  (let ((agent mail-user-agent))
475
444
    (if (and file (file-exists-p file))
505
474
        try:
506
475
            os.write(fd, _defun)
507
476
        finally:
508
 
            os.close(fd)  # Just close the handle but do not remove the file.
 
477
            os.close(fd) # Just close the handle but do not remove the file.
509
478
        return temp_file
510
479
 
511
480
    def _get_compose_commandline(self, to, subject, attach_path):
531
500
        if attach_path is not None:
532
501
            # Do not create a file if there is no attachment
533
502
            elisp = self._prepare_send_function()
534
 
            self.elisp_tmp_file = elisp
535
503
            lmmform = '(load "%s")' % elisp
536
 
            mmform = '(bzr-add-mime-att "%s")' % \
 
504
            mmform  = '(bzr-add-mime-att "%s")' % \
537
505
                self._encode_path(attach_path, 'attachment')
538
506
            rmform = '(delete-file "%s")' % elisp
539
507
            commandline.append(lmmform)
541
509
            commandline.append(rmform)
542
510
 
543
511
        return commandline
544
 
 
545
 
 
546
512
mail_client_registry.register('emacsclient', EmacsMail,
547
513
                              help=EmacsMail.__doc__)
548
514
 
549
515
 
550
516
class MAPIClient(BodyExternalMailClient):
551
 
    __doc__ = """Default Windows mail client launched using MAPI."""
 
517
    """Default Windows mail client launched using MAPI."""
552
518
 
553
519
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
554
520
                 extension, body=None):
556
522
 
557
523
        This implementation uses MAPI via the simplemapi ctypes wrapper
558
524
        """
559
 
        from .util import simplemapi
 
525
        from bzrlib.util import simplemapi
560
526
        try:
561
527
            simplemapi.SendMail(to or '', subject or '', body or '',
562
528
                                attach_path)
563
 
        except simplemapi.MAPIError as e:
 
529
        except simplemapi.MAPIError, e:
564
530
            if e.code != simplemapi.MAPI_USER_ABORT:
565
 
                raise MailClientNotFound(['MAPI supported mail client'
566
 
                                          ' (error %d)' % (e.code,)])
567
 
 
568
 
 
 
531
                raise errors.MailClientNotFound(['MAPI supported mail client'
 
532
                                                 ' (error %d)' % (e.code,)])
569
533
mail_client_registry.register('mapi', MAPIClient,
570
534
                              help=MAPIClient.__doc__)
571
535
 
572
536
 
573
 
class MailApp(BodyExternalMailClient):
574
 
    __doc__ = """Use MacOS X's Mail.app for sending email messages.
575
 
 
576
 
    Although it would be nice to use appscript, it's not installed
577
 
    with the shipped Python installations.  We instead build an
578
 
    AppleScript and invoke the script using osascript(1).  We don't
579
 
    use the _encode_safe() routines as it's not clear what encoding
580
 
    osascript expects the script to be in.
581
 
    """
582
 
 
583
 
    _client_commands = ['osascript']
584
 
 
585
 
    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
 
 
630
 
mail_client_registry.register('mail.app', MailApp,
631
 
                              help=MailApp.__doc__)
632
 
 
633
 
 
634
537
class DefaultMail(MailClient):
635
 
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
538
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
636
539
    falls back to Editor"""
637
540
 
638
541
    supports_body = True
649
552
        """See MailClient.compose"""
650
553
        try:
651
554
            return self._mail_client().compose(prompt, to, subject,
652
 
                                               attachment, mime_subtype,
 
555
                                               attachment, mimie_subtype,
653
556
                                               extension, basename, body)
654
 
        except MailClientNotFound:
655
 
            return Editor(self.config).compose(
656
 
                prompt, to, subject, attachment, mime_subtype, extension, body)
 
557
        except errors.MailClientNotFound:
 
558
            return Editor(self.config).compose(prompt, to, subject,
 
559
                          attachment, mimie_subtype, extension, body)
657
560
 
658
561
    def compose_merge_request(self, to, subject, directive, basename=None,
659
562
                              body=None):
660
563
        """See MailClient.compose_merge_request"""
661
564
        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,
 
565
            return self._mail_client().compose_merge_request(to, subject,
 
566
                    directive, basename=basename, body=body)
 
567
        except errors.MailClientNotFound:
 
568
            return Editor(self.config).compose_merge_request(to, subject,
 
569
                          directive, basename=basename, body=body)
 
570
mail_client_registry.register('default', DefaultMail,
670
571
                              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')
 
572
mail_client_registry.default_key = 'default'