~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Andrew Bennetts
  • Date: 2009-03-04 07:10:07 UTC
  • mto: (4086.1.2 hpss-integration)
  • mto: This revision was merged to the branch mainline in revision 4087.
  • Revision ID: andrew.bennetts@canonical.com-20090304071007-8iqoi1m44ypmzg2a
Rough prototype of allowing a SearchResult to be passed to fetch, and using that to improve network conversations.

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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
import errno
18
18
import os
41
41
        self.config = config
42
42
 
43
43
    def compose(self, prompt, to, subject, attachment, mime_subtype,
44
 
                extension, basename=None, body=None):
 
44
                extension, basename=None):
45
45
        """Compose (and possibly send) an email message
46
46
 
47
47
        Must be implemented by subclasses.
61
61
        """
62
62
        raise NotImplementedError
63
63
 
64
 
    def compose_merge_request(self, to, subject, directive, basename=None,
65
 
                              body=None):
 
64
    def compose_merge_request(self, to, subject, directive, basename=None):
66
65
        """Compose (and possibly send) a merge request
67
66
 
68
67
        :param to: The address to send the request to
75
74
        prompt = self._get_merge_prompt("Please describe these changes:", to,
76
75
                                        subject, directive)
77
76
        self.compose(prompt, to, subject, directive,
78
 
            'x-patch', '.patch', basename, body)
 
77
            'x-patch', '.patch', basename)
79
78
 
80
79
    def _get_merge_prompt(self, prompt, to, subject, attachment):
81
80
        """Generate a prompt string.  Overridden by Editor.
89
88
 
90
89
 
91
90
class Editor(MailClient):
92
 
    __doc__ = """DIY mail client that uses commit message editor"""
93
 
 
94
 
    supports_body = True
 
91
    """DIY mail client that uses commit message editor"""
95
92
 
96
93
    def _get_merge_prompt(self, prompt, to, subject, attachment):
97
94
        """See MailClient._get_merge_prompt"""
102
99
                         attachment.decode('utf-8', 'replace')))
103
100
 
104
101
    def compose(self, prompt, to, subject, attachment, mime_subtype,
105
 
                extension, basename=None, body=None):
 
102
                extension, basename=None):
106
103
        """See MailClient.compose"""
107
104
        if not to:
108
105
            raise errors.NoMailAddressSpecified()
109
 
        body = msgeditor.edit_commit_message(prompt, start_message=body)
 
106
        body = msgeditor.edit_commit_message(prompt)
110
107
        if body == '':
111
108
            raise errors.NoMessageSupplied()
112
109
        email_message.EmailMessage.send(self.config,
120
117
                              help=Editor.__doc__)
121
118
 
122
119
 
123
 
class BodyExternalMailClient(MailClient):
124
 
 
125
 
    supports_body = True
 
120
class ExternalMailClient(MailClient):
 
121
    """An external mail client."""
126
122
 
127
123
    def _get_client_commands(self):
128
124
        """Provide a list of commands that may invoke the mail client"""
133
129
            return self._client_commands
134
130
 
135
131
    def compose(self, prompt, to, subject, attachment, mime_subtype,
136
 
                extension, basename=None, body=None):
 
132
                extension, basename=None):
137
133
        """See MailClient.compose.
138
134
 
139
135
        Writes the attachment to a temporary file, invokes _compose.
147
143
            outfile.write(attachment)
148
144
        finally:
149
145
            outfile.close()
150
 
        if body is not None:
151
 
            kwargs = {'body': body}
152
 
        else:
153
 
            kwargs = {}
154
146
        self._compose(prompt, to, subject, attach_path, mime_subtype,
155
 
                      extension, **kwargs)
 
147
                      extension)
156
148
 
157
149
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
 
                 extension, body=None, from_=None):
 
150
                extension):
159
151
        """Invoke a mail client as a commandline process.
160
152
 
161
153
        Overridden by MAPIClient.
166
158
            "text", but the precise subtype can be specified here
167
159
        :param extension: A file extension (including period) associated with
168
160
            the attachment type.
169
 
        :param body: Optional body text.
170
 
        :param from_: Optional From: header.
171
161
        """
172
162
        for name in self._get_client_commands():
173
163
            cmdline = [self._encode_path(name, 'executable')]
174
 
            if body is not None:
175
 
                kwargs = {'body': body}
176
 
            else:
177
 
                kwargs = {}
178
 
            if from_ is not None:
179
 
                kwargs['from_'] = from_
180
164
            cmdline.extend(self._get_compose_commandline(to, subject,
181
 
                                                         attach_path,
182
 
                                                         **kwargs))
 
165
                                                         attach_path))
183
166
            try:
184
167
                subprocess.call(cmdline)
185
168
            except OSError, e:
190
173
        else:
191
174
            raise errors.MailClientNotFound(self._client_commands)
192
175
 
193
 
    def _get_compose_commandline(self, to, subject, attach_path, body):
 
176
    def _get_compose_commandline(self, to, subject, attach_path):
194
177
        """Determine the commandline to use for composing a message
195
178
 
196
179
        Implemented by various subclasses
229
212
        return path
230
213
 
231
214
 
232
 
class ExternalMailClient(BodyExternalMailClient):
233
 
    __doc__ = """An external mail client."""
234
 
 
235
 
    supports_body = False
236
 
 
237
 
 
238
 
class Evolution(BodyExternalMailClient):
239
 
    __doc__ = """Evolution mail client."""
 
215
class Evolution(ExternalMailClient):
 
216
    """Evolution mail client."""
240
217
 
241
218
    _client_commands = ['evolution']
242
219
 
243
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
220
    def _get_compose_commandline(self, to, subject, attach_path):
244
221
        """See ExternalMailClient._get_compose_commandline"""
245
222
        message_options = {}
246
223
        if subject is not None:
247
224
            message_options['subject'] = subject
248
225
        if attach_path is not None:
249
226
            message_options['attach'] = attach_path
250
 
        if body is not None:
251
 
            message_options['body'] = body
252
227
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
228
                        sorted(message_options.iteritems())]
254
229
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
257
232
                              help=Evolution.__doc__)
258
233
 
259
234
 
260
 
class Mutt(BodyExternalMailClient):
261
 
    __doc__ = """Mutt mail client."""
 
235
class Mutt(ExternalMailClient):
 
236
    """Mutt mail client."""
262
237
 
263
238
    _client_commands = ['mutt']
264
239
 
265
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
240
    def _get_compose_commandline(self, to, subject, attach_path):
266
241
        """See ExternalMailClient._get_compose_commandline"""
267
242
        message_options = []
268
243
        if subject is not None:
270
245
        if attach_path is not None:
271
246
            message_options.extend(['-a',
272
247
                self._encode_path(attach_path, 'attachment')])
273
 
        if body is not None:
274
 
            # Store the temp file object in self, so that it does not get
275
 
            # garbage collected and delete the file before mutt can read it.
276
 
            self._temp_file = tempfile.NamedTemporaryFile(
277
 
                prefix="mutt-body-", suffix=".txt")
278
 
            self._temp_file.write(body)
279
 
            self._temp_file.flush()
280
 
            message_options.extend(['-i', self._temp_file.name])
281
248
        if to is not None:
282
 
            message_options.extend(['--', self._encode_safe(to)])
 
249
            message_options.append(self._encode_safe(to))
283
250
        return message_options
284
251
mail_client_registry.register('mutt', Mutt,
285
252
                              help=Mutt.__doc__)
286
253
 
287
254
 
288
 
class Thunderbird(BodyExternalMailClient):
289
 
    __doc__ = """Mozilla Thunderbird (or Icedove)
 
255
class Thunderbird(ExternalMailClient):
 
256
    """Mozilla Thunderbird (or Icedove)
290
257
 
291
258
    Note that Thunderbird 1.5 is buggy and does not support setting
292
259
    "to" simultaneously with including a attachment.
299
266
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
267
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
301
268
 
302
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
269
    def _get_compose_commandline(self, to, subject, attach_path):
303
270
        """See ExternalMailClient._get_compose_commandline"""
304
271
        message_options = {}
305
272
        if to is not None:
309
276
        if attach_path is not None:
310
277
            message_options['attachment'] = urlutils.local_path_to_url(
311
278
                attach_path)
312
 
        if body is not None:
313
 
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
314
 
        else:
315
 
            options_list = []
316
 
        options_list.extend(["%s='%s'" % (k, v) for k, v in
317
 
                        sorted(message_options.iteritems())])
 
279
        options_list = ["%s='%s'" % (k, v) for k, v in
 
280
                        sorted(message_options.iteritems())]
318
281
        return ['-compose', ','.join(options_list)]
319
282
mail_client_registry.register('thunderbird', Thunderbird,
320
283
                              help=Thunderbird.__doc__)
321
284
 
322
285
 
323
286
class KMail(ExternalMailClient):
324
 
    __doc__ = """KDE mail client."""
 
287
    """KDE mail client."""
325
288
 
326
289
    _client_commands = ['kmail']
327
290
 
341
304
 
342
305
 
343
306
class Claws(ExternalMailClient):
344
 
    __doc__ = """Claws mail client."""
345
 
 
346
 
    supports_body = True
 
307
    """Claws mail client."""
347
308
 
348
309
    _client_commands = ['claws-mail']
349
310
 
350
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
351
 
                                 from_=None):
 
311
    def _get_compose_commandline(self, to, subject, attach_path):
352
312
        """See ExternalMailClient._get_compose_commandline"""
353
 
        compose_url = []
354
 
        if from_ is not None:
355
 
            compose_url.append('from=' + urllib.quote(from_))
 
313
        compose_url = ['mailto:']
 
314
        if to is not None:
 
315
            compose_url.append(self._encode_safe(to))
 
316
        compose_url.append('?')
356
317
        if subject is not None:
357
318
            # Don't use urllib.quote_plus because Claws doesn't seem
358
319
            # to recognise spaces encoded as "+".
359
320
            compose_url.append(
360
 
                'subject=' + urllib.quote(self._encode_safe(subject)))
361
 
        if body is not None:
362
 
            compose_url.append(
363
 
                'body=' + urllib.quote(self._encode_safe(body)))
364
 
        # to must be supplied for the claws-mail --compose syntax to work.
365
 
        if to is None:
366
 
            raise errors.NoMailAddressSpecified()
367
 
        compose_url = 'mailto:%s?%s' % (
368
 
            self._encode_safe(to), '&'.join(compose_url))
 
321
                'subject=%s' % urllib.quote(self._encode_safe(subject)))
369
322
        # Collect command-line options.
370
 
        message_options = ['--compose', compose_url]
 
323
        message_options = ['--compose', ''.join(compose_url)]
371
324
        if attach_path is not None:
372
325
            message_options.extend(
373
326
                ['--attach', self._encode_path(attach_path, 'attachment')])
374
327
        return message_options
375
 
 
376
 
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
377
 
                 extension, body=None, from_=None):
378
 
        """See ExternalMailClient._compose"""
379
 
        if from_ is None:
380
 
            from_ = self.config.get_user_option('email')
381
 
        super(Claws, self)._compose(prompt, to, subject, attach_path,
382
 
                                    mime_subtype, extension, body, from_)
383
 
 
384
 
 
385
328
mail_client_registry.register('claws', Claws,
386
329
                              help=Claws.__doc__)
387
330
 
388
331
 
389
 
class XDGEmail(BodyExternalMailClient):
390
 
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
 
332
class XDGEmail(ExternalMailClient):
 
333
    """xdg-email attempts to invoke the user's preferred mail client"""
391
334
 
392
335
    _client_commands = ['xdg-email']
393
336
 
394
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
337
    def _get_compose_commandline(self, to, subject, attach_path):
395
338
        """See ExternalMailClient._get_compose_commandline"""
396
339
        if not to:
397
340
            raise errors.NoMailAddressSpecified()
401
344
        if attach_path is not None:
402
345
            commandline.extend(['--attach',
403
346
                self._encode_path(attach_path, 'attachment')])
404
 
        if body is not None:
405
 
            commandline.extend(['--body', self._encode_safe(body)])
406
347
        return commandline
407
348
mail_client_registry.register('xdg-email', XDGEmail,
408
349
                              help=XDGEmail.__doc__)
409
350
 
410
351
 
411
352
class EmacsMail(ExternalMailClient):
412
 
    __doc__ = """Call emacsclient to have a mail buffer.
 
353
    """Call emacsclient to have a mail buffer.
413
354
 
414
355
    This only work for emacs >= 22.1 due to recent -e/--eval support.
415
356
 
424
365
 
425
366
    _client_commands = ['emacsclient']
426
367
 
427
 
    def __init__(self, config):
428
 
        super(EmacsMail, self).__init__(config)
429
 
        self.elisp_tmp_file = None
430
 
 
431
368
    def _prepare_send_function(self):
432
369
        """Write our wrapper function into a temporary file.
433
370
 
454
391
            (if (functionp 'etach-attach)
455
392
              (etach-attach file)
456
393
              (mail-attach-file file))))
457
 
         ((or (eq agent 'message-user-agent)
458
 
              (eq agent 'gnus-user-agent)
459
 
              (eq agent 'mh-e-user-agent))
 
394
         ((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
460
395
          (progn
461
396
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
462
397
         ((eq agent 'mew-user-agent)
504
439
        if attach_path is not None:
505
440
            # Do not create a file if there is no attachment
506
441
            elisp = self._prepare_send_function()
507
 
            self.elisp_tmp_file = elisp
508
442
            lmmform = '(load "%s")' % elisp
509
443
            mmform  = '(bzr-add-mime-att "%s")' % \
510
444
                self._encode_path(attach_path, 'attachment')
518
452
                              help=EmacsMail.__doc__)
519
453
 
520
454
 
521
 
class MAPIClient(BodyExternalMailClient):
522
 
    __doc__ = """Default Windows mail client launched using MAPI."""
 
455
class MAPIClient(ExternalMailClient):
 
456
    """Default Windows mail client launched using MAPI."""
523
457
 
524
458
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
 
                 extension, body=None):
 
459
                 extension):
526
460
        """See ExternalMailClient._compose.
527
461
 
528
462
        This implementation uses MAPI via the simplemapi ctypes wrapper
529
463
        """
530
464
        from bzrlib.util import simplemapi
531
465
        try:
532
 
            simplemapi.SendMail(to or '', subject or '', body or '',
533
 
                                attach_path)
 
466
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
534
467
        except simplemapi.MAPIError, e:
535
468
            if e.code != simplemapi.MAPI_USER_ABORT:
536
469
                raise errors.MailClientNotFound(['MAPI supported mail client'
539
472
                              help=MAPIClient.__doc__)
540
473
 
541
474
 
542
 
class MailApp(BodyExternalMailClient):
543
 
    __doc__ = """Use MacOS X's Mail.app for sending email messages.
544
 
 
545
 
    Although it would be nice to use appscript, it's not installed
546
 
    with the shipped Python installations.  We instead build an
547
 
    AppleScript and invoke the script using osascript(1).  We don't
548
 
    use the _encode_safe() routines as it's not clear what encoding
549
 
    osascript expects the script to be in.
550
 
    """
551
 
 
552
 
    _client_commands = ['osascript']
553
 
 
554
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
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]
597
 
mail_client_registry.register('mail.app', MailApp,
598
 
                              help=MailApp.__doc__)
599
 
 
600
 
 
601
475
class DefaultMail(MailClient):
602
 
    __doc__ = """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
476
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
603
477
    falls back to Editor"""
604
478
 
605
 
    supports_body = True
606
 
 
607
479
    def _mail_client(self):
608
480
        """Determine the preferred mail client for this platform"""
609
481
        if osutils.supports_mapi():
612
484
            return XDGEmail(self.config)
613
485
 
614
486
    def compose(self, prompt, to, subject, attachment, mime_subtype,
615
 
                extension, basename=None, body=None):
 
487
                extension, basename=None):
616
488
        """See MailClient.compose"""
617
489
        try:
618
490
            return self._mail_client().compose(prompt, to, subject,
619
491
                                               attachment, mimie_subtype,
620
 
                                               extension, basename, body)
 
492
                                               extension, basename)
621
493
        except errors.MailClientNotFound:
622
494
            return Editor(self.config).compose(prompt, to, subject,
623
 
                          attachment, mimie_subtype, extension, body)
 
495
                          attachment, mimie_subtype, extension)
624
496
 
625
 
    def compose_merge_request(self, to, subject, directive, basename=None,
626
 
                              body=None):
 
497
    def compose_merge_request(self, to, subject, directive, basename=None):
627
498
        """See MailClient.compose_merge_request"""
628
499
        try:
629
500
            return self._mail_client().compose_merge_request(to, subject,
630
 
                    directive, basename=basename, body=body)
 
501
                    directive, basename=basename)
631
502
        except errors.MailClientNotFound:
632
503
            return Editor(self.config).compose_merge_request(to, subject,
633
 
                          directive, basename=basename, body=body)
 
504
                          directive, basename=basename)
634
505
mail_client_registry.register('default', DefaultMail,
635
506
                              help=DefaultMail.__doc__)
636
507
mail_client_registry.default_key = 'default'
637
 
 
638