~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-10 15:46:03 UTC
  • mfrom: (4985.3.21 update)
  • mto: This revision was merged to the branch mainline in revision 5021.
  • Revision ID: v.ladeuil+lp@free.fr-20100210154603-k4no1gvfuqpzrw7p
Update performs two merges in a more logical order but stop on conflicts

Show diffs side-by-side

added added

removed removed

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