~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

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
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
from __future__ import absolute_import
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
16
 
19
17
import errno
20
18
import os
24
22
 
25
23
import bzrlib
26
24
from bzrlib import (
27
 
    config as _mod_config,
28
25
    email_message,
29
26
    errors,
30
27
    msgeditor,
43
40
        self.config = config
44
41
 
45
42
    def compose(self, prompt, to, subject, attachment, mime_subtype,
46
 
                extension, basename=None, body=None):
 
43
                extension, basename=None):
47
44
        """Compose (and possibly send) an email message
48
45
 
49
46
        Must be implemented by subclasses.
63
60
        """
64
61
        raise NotImplementedError
65
62
 
66
 
    def compose_merge_request(self, to, subject, directive, basename=None,
67
 
                              body=None):
 
63
    def compose_merge_request(self, to, subject, directive, basename=None):
68
64
        """Compose (and possibly send) a merge request
69
65
 
70
66
        :param to: The address to send the request to
77
73
        prompt = self._get_merge_prompt("Please describe these changes:", to,
78
74
                                        subject, directive)
79
75
        self.compose(prompt, to, subject, directive,
80
 
            'x-patch', '.patch', basename, body)
 
76
            'x-patch', '.patch', basename)
81
77
 
82
78
    def _get_merge_prompt(self, prompt, to, subject, attachment):
83
79
        """Generate a prompt string.  Overridden by Editor.
91
87
 
92
88
 
93
89
class Editor(MailClient):
94
 
    __doc__ = """DIY mail client that uses commit message editor"""
95
 
 
96
 
    supports_body = True
 
90
    """DIY mail client that uses commit message editor"""
97
91
 
98
92
    def _get_merge_prompt(self, prompt, to, subject, attachment):
99
93
        """See MailClient._get_merge_prompt"""
104
98
                         attachment.decode('utf-8', 'replace')))
105
99
 
106
100
    def compose(self, prompt, to, subject, attachment, mime_subtype,
107
 
                extension, basename=None, body=None):
 
101
                extension, basename=None):
108
102
        """See MailClient.compose"""
109
103
        if not to:
110
104
            raise errors.NoMailAddressSpecified()
111
 
        body = msgeditor.edit_commit_message(prompt, start_message=body)
 
105
        body = msgeditor.edit_commit_message(prompt)
112
106
        if body == '':
113
107
            raise errors.NoMessageSupplied()
114
108
        email_message.EmailMessage.send(self.config,
115
 
                                        self.config.get('email'),
 
109
                                        self.config.username(),
116
110
                                        to,
117
111
                                        subject,
118
112
                                        body,
122
116
                              help=Editor.__doc__)
123
117
 
124
118
 
125
 
class BodyExternalMailClient(MailClient):
126
 
 
127
 
    supports_body = True
 
119
class ExternalMailClient(MailClient):
 
120
    """An external mail client."""
128
121
 
129
122
    def _get_client_commands(self):
130
123
        """Provide a list of commands that may invoke the mail client"""
135
128
            return self._client_commands
136
129
 
137
130
    def compose(self, prompt, to, subject, attachment, mime_subtype,
138
 
                extension, basename=None, body=None):
 
131
                extension, basename=None):
139
132
        """See MailClient.compose.
140
133
 
141
134
        Writes the attachment to a temporary file, invokes _compose.
149
142
            outfile.write(attachment)
150
143
        finally:
151
144
            outfile.close()
152
 
        if body is not None:
153
 
            kwargs = {'body': body}
154
 
        else:
155
 
            kwargs = {}
156
145
        self._compose(prompt, to, subject, attach_path, mime_subtype,
157
 
                      extension, **kwargs)
 
146
                      extension)
158
147
 
159
148
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
160
 
                 extension, body=None, from_=None):
 
149
                extension):
161
150
        """Invoke a mail client as a commandline process.
162
151
 
163
152
        Overridden by MAPIClient.
168
157
            "text", but the precise subtype can be specified here
169
158
        :param extension: A file extension (including period) associated with
170
159
            the attachment type.
171
 
        :param body: Optional body text.
172
 
        :param from_: Optional From: header.
173
160
        """
174
161
        for name in self._get_client_commands():
175
162
            cmdline = [self._encode_path(name, 'executable')]
176
 
            if body is not None:
177
 
                kwargs = {'body': body}
178
 
            else:
179
 
                kwargs = {}
180
 
            if from_ is not None:
181
 
                kwargs['from_'] = from_
182
163
            cmdline.extend(self._get_compose_commandline(to, subject,
183
 
                                                         attach_path,
184
 
                                                         **kwargs))
 
164
                                                         attach_path))
185
165
            try:
186
166
                subprocess.call(cmdline)
187
167
            except OSError, e:
192
172
        else:
193
173
            raise errors.MailClientNotFound(self._client_commands)
194
174
 
195
 
    def _get_compose_commandline(self, to, subject, attach_path, body):
 
175
    def _get_compose_commandline(self, to, subject, attach_path):
196
176
        """Determine the commandline to use for composing a message
197
177
 
198
178
        Implemented by various subclasses
211
191
        :return:    encoded string if u is unicode, u itself otherwise.
212
192
        """
213
193
        if isinstance(u, unicode):
214
 
            return u.encode(osutils.get_user_encoding(), 'replace')
 
194
            return u.encode(bzrlib.user_encoding, 'replace')
215
195
        return u
216
196
 
217
197
    def _encode_path(self, path, kind):
225
205
        """
226
206
        if isinstance(path, unicode):
227
207
            try:
228
 
                return path.encode(osutils.get_user_encoding())
 
208
                return path.encode(bzrlib.user_encoding)
229
209
            except UnicodeEncodeError:
230
210
                raise errors.UnableEncodePath(path, kind)
231
211
        return path
232
212
 
233
213
 
234
 
class ExternalMailClient(BodyExternalMailClient):
235
 
    __doc__ = """An external mail client."""
236
 
 
237
 
    supports_body = False
238
 
 
239
 
 
240
 
class Evolution(BodyExternalMailClient):
241
 
    __doc__ = """Evolution mail client."""
 
214
class Evolution(ExternalMailClient):
 
215
    """Evolution mail client."""
242
216
 
243
217
    _client_commands = ['evolution']
244
218
 
245
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
219
    def _get_compose_commandline(self, to, subject, attach_path):
246
220
        """See ExternalMailClient._get_compose_commandline"""
247
221
        message_options = {}
248
222
        if subject is not None:
249
223
            message_options['subject'] = subject
250
224
        if attach_path is not None:
251
225
            message_options['attach'] = attach_path
252
 
        if body is not None:
253
 
            message_options['body'] = body
254
226
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
255
227
                        sorted(message_options.iteritems())]
256
228
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
259
231
                              help=Evolution.__doc__)
260
232
 
261
233
 
262
 
class Mutt(BodyExternalMailClient):
263
 
    __doc__ = """Mutt mail client."""
 
234
class Mutt(ExternalMailClient):
 
235
    """Mutt mail client."""
264
236
 
265
237
    _client_commands = ['mutt']
266
238
 
267
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
239
    def _get_compose_commandline(self, to, subject, attach_path):
268
240
        """See ExternalMailClient._get_compose_commandline"""
269
241
        message_options = []
270
242
        if subject is not None:
272
244
        if attach_path is not None:
273
245
            message_options.extend(['-a',
274
246
                self._encode_path(attach_path, 'attachment')])
275
 
        if body is not None:
276
 
            # Store the temp file object in self, so that it does not get
277
 
            # garbage collected and delete the file before mutt can read it.
278
 
            self._temp_file = tempfile.NamedTemporaryFile(
279
 
                prefix="mutt-body-", suffix=".txt")
280
 
            self._temp_file.write(body)
281
 
            self._temp_file.flush()
282
 
            message_options.extend(['-i', self._temp_file.name])
283
247
        if to is not None:
284
 
            message_options.extend(['--', self._encode_safe(to)])
 
248
            message_options.append(self._encode_safe(to))
285
249
        return message_options
286
250
mail_client_registry.register('mutt', Mutt,
287
251
                              help=Mutt.__doc__)
288
252
 
289
253
 
290
 
class Thunderbird(BodyExternalMailClient):
291
 
    __doc__ = """Mozilla Thunderbird (or Icedove)
 
254
class Thunderbird(ExternalMailClient):
 
255
    """Mozilla Thunderbird (or Icedove)
292
256
 
293
257
    Note that Thunderbird 1.5 is buggy and does not support setting
294
258
    "to" simultaneously with including a attachment.
301
265
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
302
266
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
303
267
 
304
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
268
    def _get_compose_commandline(self, to, subject, attach_path):
305
269
        """See ExternalMailClient._get_compose_commandline"""
306
270
        message_options = {}
307
271
        if to is not None:
311
275
        if attach_path is not None:
312
276
            message_options['attachment'] = urlutils.local_path_to_url(
313
277
                attach_path)
314
 
        if body is not None:
315
 
            options_list = ['body=%s' % urlutils.quote(self._encode_safe(body))]
316
 
        else:
317
 
            options_list = []
318
 
        options_list.extend(["%s='%s'" % (k, v) for k, v in
319
 
                        sorted(message_options.iteritems())])
 
278
        options_list = ["%s='%s'" % (k, v) for k, v in
 
279
                        sorted(message_options.iteritems())]
320
280
        return ['-compose', ','.join(options_list)]
321
281
mail_client_registry.register('thunderbird', Thunderbird,
322
282
                              help=Thunderbird.__doc__)
323
283
 
324
284
 
325
285
class KMail(ExternalMailClient):
326
 
    __doc__ = """KDE mail client."""
 
286
    """KDE mail client."""
327
287
 
328
288
    _client_commands = ['kmail']
329
289
 
342
302
                              help=KMail.__doc__)
343
303
 
344
304
 
345
 
class Claws(ExternalMailClient):
346
 
    __doc__ = """Claws mail client."""
347
 
 
348
 
    supports_body = True
349
 
 
350
 
    _client_commands = ['claws-mail']
351
 
 
352
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
353
 
                                 from_=None):
354
 
        """See ExternalMailClient._get_compose_commandline"""
355
 
        compose_url = []
356
 
        if from_ is not None:
357
 
            compose_url.append('from=' + urlutils.quote(from_))
358
 
        if subject is not None:
359
 
            # Don't use urlutils.quote_plus because Claws doesn't seem
360
 
            # to recognise spaces encoded as "+".
361
 
            compose_url.append(
362
 
                'subject=' + urlutils.quote(self._encode_safe(subject)))
363
 
        if body is not None:
364
 
            compose_url.append(
365
 
                'body=' + urlutils.quote(self._encode_safe(body)))
366
 
        # to must be supplied for the claws-mail --compose syntax to work.
367
 
        if to is None:
368
 
            raise errors.NoMailAddressSpecified()
369
 
        compose_url = 'mailto:%s?%s' % (
370
 
            self._encode_safe(to), '&'.join(compose_url))
371
 
        # Collect command-line options.
372
 
        message_options = ['--compose', compose_url]
373
 
        if attach_path is not None:
374
 
            message_options.extend(
375
 
                ['--attach', self._encode_path(attach_path, 'attachment')])
376
 
        return message_options
377
 
 
378
 
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
379
 
                 extension, body=None, from_=None):
380
 
        """See ExternalMailClient._compose"""
381
 
        if from_ is None:
382
 
            from_ = self.config.get('email')
383
 
        super(Claws, self)._compose(prompt, to, subject, attach_path,
384
 
                                    mime_subtype, extension, body, from_)
385
 
 
386
 
 
387
 
mail_client_registry.register('claws', Claws,
388
 
                              help=Claws.__doc__)
389
 
 
390
 
 
391
 
class XDGEmail(BodyExternalMailClient):
392
 
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
 
305
class XDGEmail(ExternalMailClient):
 
306
    """xdg-email attempts to invoke the user's preferred mail client"""
393
307
 
394
308
    _client_commands = ['xdg-email']
395
309
 
396
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
310
    def _get_compose_commandline(self, to, subject, attach_path):
397
311
        """See ExternalMailClient._get_compose_commandline"""
398
312
        if not to:
399
313
            raise errors.NoMailAddressSpecified()
403
317
        if attach_path is not None:
404
318
            commandline.extend(['--attach',
405
319
                self._encode_path(attach_path, 'attachment')])
406
 
        if body is not None:
407
 
            commandline.extend(['--body', self._encode_safe(body)])
408
320
        return commandline
409
321
mail_client_registry.register('xdg-email', XDGEmail,
410
322
                              help=XDGEmail.__doc__)
411
323
 
412
324
 
413
325
class EmacsMail(ExternalMailClient):
414
 
    __doc__ = """Call emacsclient to have a mail buffer.
 
326
    """Call emacsclient to have a mail buffer.
415
327
 
416
328
    This only work for emacs >= 22.1 due to recent -e/--eval support.
417
329
 
426
338
 
427
339
    _client_commands = ['emacsclient']
428
340
 
429
 
    def __init__(self, config):
430
 
        super(EmacsMail, self).__init__(config)
431
 
        self.elisp_tmp_file = None
432
 
 
433
341
    def _prepare_send_function(self):
434
342
        """Write our wrapper function into a temporary file.
435
343
 
456
364
            (if (functionp 'etach-attach)
457
365
              (etach-attach file)
458
366
              (mail-attach-file file))))
459
 
         ((or (eq agent 'message-user-agent)
460
 
              (eq agent 'gnus-user-agent)
461
 
              (eq agent 'mh-e-user-agent))
 
367
         ((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
462
368
          (progn
463
369
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
464
370
         ((eq agent 'mew-user-agent)
506
412
        if attach_path is not None:
507
413
            # Do not create a file if there is no attachment
508
414
            elisp = self._prepare_send_function()
509
 
            self.elisp_tmp_file = elisp
510
415
            lmmform = '(load "%s")' % elisp
511
416
            mmform  = '(bzr-add-mime-att "%s")' % \
512
417
                self._encode_path(attach_path, 'attachment')
520
425
                              help=EmacsMail.__doc__)
521
426
 
522
427
 
523
 
class MAPIClient(BodyExternalMailClient):
524
 
    __doc__ = """Default Windows mail client launched using MAPI."""
 
428
class MAPIClient(ExternalMailClient):
 
429
    """Default Windows mail client launched using MAPI."""
525
430
 
526
431
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
527
 
                 extension, body=None):
 
432
                 extension):
528
433
        """See ExternalMailClient._compose.
529
434
 
530
435
        This implementation uses MAPI via the simplemapi ctypes wrapper
531
436
        """
532
437
        from bzrlib.util import simplemapi
533
438
        try:
534
 
            simplemapi.SendMail(to or '', subject or '', body or '',
535
 
                                attach_path)
 
439
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
536
440
        except simplemapi.MAPIError, e:
537
441
            if e.code != simplemapi.MAPI_USER_ABORT:
538
442
                raise errors.MailClientNotFound(['MAPI supported mail client'
541
445
                              help=MAPIClient.__doc__)
542
446
 
543
447
 
544
 
class MailApp(BodyExternalMailClient):
545
 
    __doc__ = """Use MacOS X's Mail.app for sending email messages.
546
 
 
547
 
    Although it would be nice to use appscript, it's not installed
548
 
    with the shipped Python installations.  We instead build an
549
 
    AppleScript and invoke the script using osascript(1).  We don't
550
 
    use the _encode_safe() routines as it's not clear what encoding
551
 
    osascript expects the script to be in.
552
 
    """
553
 
 
554
 
    _client_commands = ['osascript']
555
 
 
556
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
557
 
                                from_=None):
558
 
       """See ExternalMailClient._get_compose_commandline"""
559
 
 
560
 
       fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
561
 
                                         suffix=".scpt")
562
 
       try:
563
 
           os.write(fd, 'tell application "Mail"\n')
564
 
           os.write(fd, 'set newMessage to make new outgoing message\n')
565
 
           os.write(fd, 'tell newMessage\n')
566
 
           if to is not None:
567
 
               os.write(fd, 'make new to recipient with properties'
568
 
                   ' {address:"%s"}\n' % to)
569
 
           if from_ is not None:
570
 
               # though from_ doesn't actually seem to be used
571
 
               os.write(fd, 'set sender to "%s"\n'
572
 
                   % sender.replace('"', '\\"'))
573
 
           if subject is not None:
574
 
               os.write(fd, 'set subject to "%s"\n'
575
 
                   % subject.replace('"', '\\"'))
576
 
           if body is not None:
577
 
               # FIXME: would be nice to prepend the body to the
578
 
               # existing content (e.g., preserve signature), but
579
 
               # can't seem to figure out the right applescript
580
 
               # incantation.
581
 
               os.write(fd, 'set content to "%s\\n\n"\n' %
582
 
                   body.replace('"', '\\"').replace('\n', '\\n'))
583
 
 
584
 
           if attach_path is not None:
585
 
               # FIXME: would be nice to first append a newline to
586
 
               # ensure the attachment is on a new paragraph, but
587
 
               # can't seem to figure out the right applescript
588
 
               # incantation.
589
 
               os.write(fd, 'tell content to make new attachment'
590
 
                   ' with properties {file name:"%s"}'
591
 
                   ' at after the last paragraph\n'
592
 
                   % self._encode_path(attach_path, 'attachment'))
593
 
           os.write(fd, 'set visible to true\n')
594
 
           os.write(fd, 'end tell\n')
595
 
           os.write(fd, 'end tell\n')
596
 
       finally:
597
 
           os.close(fd) # Just close the handle but do not remove the file.
598
 
       return [self.temp_file]
599
 
mail_client_registry.register('mail.app', MailApp,
600
 
                              help=MailApp.__doc__)
601
 
 
602
 
 
603
448
class DefaultMail(MailClient):
604
 
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
449
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
605
450
    falls back to Editor"""
606
451
 
607
 
    supports_body = True
608
 
 
609
452
    def _mail_client(self):
610
453
        """Determine the preferred mail client for this platform"""
611
454
        if osutils.supports_mapi():
614
457
            return XDGEmail(self.config)
615
458
 
616
459
    def compose(self, prompt, to, subject, attachment, mime_subtype,
617
 
                extension, basename=None, body=None):
 
460
                extension, basename=None):
618
461
        """See MailClient.compose"""
619
462
        try:
620
463
            return self._mail_client().compose(prompt, to, subject,
621
 
                                               attachment, mime_subtype,
622
 
                                               extension, basename, body)
 
464
                                               attachment, mimie_subtype,
 
465
                                               extension, basename)
623
466
        except errors.MailClientNotFound:
624
467
            return Editor(self.config).compose(prompt, to, subject,
625
 
                          attachment, mime_subtype, extension, body)
 
468
                          attachment, mimie_subtype, extension)
626
469
 
627
 
    def compose_merge_request(self, to, subject, directive, basename=None,
628
 
                              body=None):
 
470
    def compose_merge_request(self, to, subject, directive, basename=None):
629
471
        """See MailClient.compose_merge_request"""
630
472
        try:
631
473
            return self._mail_client().compose_merge_request(to, subject,
632
 
                    directive, basename=basename, body=body)
 
474
                    directive, basename=basename)
633
475
        except errors.MailClientNotFound:
634
476
            return Editor(self.config).compose_merge_request(to, subject,
635
 
                          directive, basename=basename, body=body)
 
477
                          directive, basename=basename)
636
478
mail_client_registry.register('default', DefaultMail,
637
479
                              help=DefaultMail.__doc__)
638
480
mail_client_registry.default_key = 'default'
639
 
 
640
 
opt_mail_client = _mod_config.RegistryOption('mail_client',
641
 
        mail_client_registry, help='E-mail client to use.', invalid='error')