~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Aaron Bentley
  • Date: 2007-12-09 23:53:50 UTC
  • mto: This revision was merged to the branch mainline in revision 3133.
  • Revision ID: aaron.bentley@utoronto.ca-20071209235350-qp39yk0xzx7a4f6p
Don't use the base if not cherrypicking

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
23
22
 
24
 
import bzrlib
25
23
from bzrlib import (
26
24
    email_message,
27
25
    errors,
28
26
    msgeditor,
29
27
    osutils,
30
28
    urlutils,
31
 
    registry
32
29
    )
33
30
 
34
 
mail_client_registry = registry.Registry()
35
 
 
36
31
 
37
32
class MailClient(object):
38
33
    """A mail client that can send messages with attachements."""
41
36
        self.config = config
42
37
 
43
38
    def compose(self, prompt, to, subject, attachment, mime_subtype,
44
 
                extension, basename=None):
 
39
                extension):
45
40
        """Compose (and possibly send) an email message
46
41
 
47
42
        Must be implemented by subclasses.
56
51
            "plain", "x-patch", etc.
57
52
        :param extension: The file extension associated with the attachment
58
53
            type, e.g. ".patch"
59
 
        :param basename: The name to use for the attachment, e.g.
60
 
            "send-nick-3252"
61
54
        """
62
55
        raise NotImplementedError
63
56
 
64
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
57
    def compose_merge_request(self, to, subject, directive):
65
58
        """Compose (and possibly send) a merge request
66
59
 
67
60
        :param to: The address to send the request to
68
61
        :param subject: The subject line to use for the request
69
62
        :param directive: A merge directive representing the merge request, as
70
63
            a bytestring.
71
 
        :param basename: The name to use for the attachment, e.g.
72
 
            "send-nick-3252"
73
64
        """
74
65
        prompt = self._get_merge_prompt("Please describe these changes:", to,
75
66
                                        subject, directive)
76
67
        self.compose(prompt, to, subject, directive,
77
 
            'x-patch', '.patch', basename)
 
68
            'x-patch', '.patch')
78
69
 
79
70
    def _get_merge_prompt(self, prompt, to, subject, attachment):
80
71
        """Generate a prompt string.  Overridden by Editor.
99
90
                         attachment.decode('utf-8', 'replace')))
100
91
 
101
92
    def compose(self, prompt, to, subject, attachment, mime_subtype,
102
 
                extension, basename=None):
 
93
                extension):
103
94
        """See MailClient.compose"""
104
95
        if not to:
105
96
            raise errors.NoMailAddressSpecified()
113
104
                                        body,
114
105
                                        attachment,
115
106
                                        attachment_mime_subtype=mime_subtype)
116
 
mail_client_registry.register('editor', Editor,
117
 
                              help=Editor.__doc__)
118
107
 
119
108
 
120
109
class ExternalMailClient(MailClient):
129
118
            return self._client_commands
130
119
 
131
120
    def compose(self, prompt, to, subject, attachment, mime_subtype,
132
 
                extension, basename=None):
 
121
                extension):
133
122
        """See MailClient.compose.
134
123
 
135
124
        Writes the attachment to a temporary file, invokes _compose.
136
125
        """
137
 
        if basename is None:
138
 
            basename = 'attachment'
139
 
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
140
 
        attach_path = osutils.pathjoin(pathname, basename + extension)
141
 
        outfile = open(attach_path, 'wb')
 
126
        fd, pathname = tempfile.mkstemp(extension, 'bzr-mail-')
142
127
        try:
143
 
            outfile.write(attachment)
 
128
            os.write(fd, attachment)
144
129
        finally:
145
 
            outfile.close()
146
 
        self._compose(prompt, to, subject, attach_path, mime_subtype,
147
 
                      extension)
 
130
            os.close(fd)
 
131
        self._compose(prompt, to, subject, pathname, mime_subtype, extension)
148
132
 
149
133
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
150
134
                extension):
160
144
            the attachment type.
161
145
        """
162
146
        for name in self._get_client_commands():
163
 
            cmdline = [self._encode_path(name, 'executable')]
 
147
            cmdline = [name]
164
148
            cmdline.extend(self._get_compose_commandline(to, subject,
165
149
                                                         attach_path))
166
150
            try:
183
167
        """
184
168
        raise NotImplementedError
185
169
 
186
 
    def _encode_safe(self, u):
187
 
        """Encode possible unicode string argument to 8-bit string
188
 
        in user_encoding. Unencodable characters will be replaced
189
 
        with '?'.
190
 
 
191
 
        :param  u:  possible unicode string.
192
 
        :return:    encoded string if u is unicode, u itself otherwise.
193
 
        """
194
 
        if isinstance(u, unicode):
195
 
            return u.encode(osutils.get_user_encoding(), 'replace')
196
 
        return u
197
 
 
198
 
    def _encode_path(self, path, kind):
199
 
        """Encode unicode path in user encoding.
200
 
 
201
 
        :param  path:   possible unicode path.
202
 
        :param  kind:   path kind ('executable' or 'attachment').
203
 
        :return:        encoded path if path is unicode,
204
 
                        path itself otherwise.
205
 
        :raise:         UnableEncodePath.
206
 
        """
207
 
        if isinstance(path, unicode):
208
 
            try:
209
 
                return path.encode(osutils.get_user_encoding())
210
 
            except UnicodeEncodeError:
211
 
                raise errors.UnableEncodePath(path, kind)
212
 
        return path
213
 
 
214
170
 
215
171
class Evolution(ExternalMailClient):
216
172
    """Evolution mail client."""
225
181
        if attach_path is not None:
226
182
            message_options['attach'] = attach_path
227
183
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
228
 
                        sorted(message_options.iteritems())]
229
 
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
230
 
            '&'.join(options_list))]
231
 
mail_client_registry.register('evolution', Evolution,
232
 
                              help=Evolution.__doc__)
 
184
                        message_options.iteritems()]
 
185
        return ['mailto:%s?%s' % (to or '', '&'.join(options_list))]
233
186
 
234
187
 
235
188
class Mutt(ExternalMailClient):
241
194
        """See ExternalMailClient._get_compose_commandline"""
242
195
        message_options = []
243
196
        if subject is not None:
244
 
            message_options.extend(['-s', self._encode_safe(subject)])
 
197
            message_options.extend(['-s', subject ])
245
198
        if attach_path is not None:
246
 
            message_options.extend(['-a',
247
 
                self._encode_path(attach_path, 'attachment')])
 
199
            message_options.extend(['-a', attach_path])
248
200
        if to is not None:
249
 
            message_options.append(self._encode_safe(to))
 
201
            message_options.append(to)
250
202
        return message_options
251
 
mail_client_registry.register('mutt', Mutt,
252
 
                              help=Mutt.__doc__)
253
203
 
254
204
 
255
205
class Thunderbird(ExternalMailClient):
263
213
    """
264
214
 
265
215
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
266
 
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
267
 
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
 
216
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
268
217
 
269
218
    def _get_compose_commandline(self, to, subject, attach_path):
270
219
        """See ExternalMailClient._get_compose_commandline"""
271
220
        message_options = {}
272
221
        if to is not None:
273
 
            message_options['to'] = self._encode_safe(to)
 
222
            message_options['to'] = to
274
223
        if subject is not None:
275
 
            message_options['subject'] = self._encode_safe(subject)
 
224
            message_options['subject'] = subject
276
225
        if attach_path is not None:
277
226
            message_options['attachment'] = urlutils.local_path_to_url(
278
227
                attach_path)
279
228
        options_list = ["%s='%s'" % (k, v) for k, v in
280
229
                        sorted(message_options.iteritems())]
281
230
        return ['-compose', ','.join(options_list)]
282
 
mail_client_registry.register('thunderbird', Thunderbird,
283
 
                              help=Thunderbird.__doc__)
284
231
 
285
232
 
286
233
class KMail(ExternalMailClient):
292
239
        """See ExternalMailClient._get_compose_commandline"""
293
240
        message_options = []
294
241
        if subject is not None:
295
 
            message_options.extend(['-s', self._encode_safe(subject)])
296
 
        if attach_path is not None:
297
 
            message_options.extend(['--attach',
298
 
                self._encode_path(attach_path, 'attachment')])
299
 
        if to is not None:
300
 
            message_options.extend([self._encode_safe(to)])
301
 
        return message_options
302
 
mail_client_registry.register('kmail', KMail,
303
 
                              help=KMail.__doc__)
304
 
 
305
 
 
306
 
class Claws(ExternalMailClient):
307
 
    """Claws mail client."""
308
 
 
309
 
    _client_commands = ['claws-mail']
310
 
 
311
 
    def _get_compose_commandline(self, to, subject, attach_path):
312
 
        """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('?')
317
 
        if subject is not None:
318
 
            # Don't use urllib.quote_plus because Claws doesn't seem
319
 
            # to recognise spaces encoded as "+".
320
 
            compose_url.append(
321
 
                'subject=%s' % urllib.quote(self._encode_safe(subject)))
322
 
        # Collect command-line options.
323
 
        message_options = ['--compose', ''.join(compose_url)]
324
 
        if attach_path is not None:
325
 
            message_options.extend(
326
 
                ['--attach', self._encode_path(attach_path, 'attachment')])
327
 
        return message_options
328
 
mail_client_registry.register('claws', Claws,
329
 
                              help=Claws.__doc__)
 
242
            message_options.extend( ['-s', subject ] )
 
243
        if attach_path is not None:
 
244
            message_options.extend( ['--attach', attach_path] )
 
245
        if to is not None:
 
246
            message_options.extend( [ to ] )
 
247
 
 
248
        return message_options
330
249
 
331
250
 
332
251
class XDGEmail(ExternalMailClient):
338
257
        """See ExternalMailClient._get_compose_commandline"""
339
258
        if not to:
340
259
            raise errors.NoMailAddressSpecified()
341
 
        commandline = [self._encode_safe(to)]
342
 
        if subject is not None:
343
 
            commandline.extend(['--subject', self._encode_safe(subject)])
344
 
        if attach_path is not None:
345
 
            commandline.extend(['--attach',
346
 
                self._encode_path(attach_path, 'attachment')])
347
 
        return commandline
348
 
mail_client_registry.register('xdg-email', XDGEmail,
349
 
                              help=XDGEmail.__doc__)
350
 
 
351
 
 
352
 
class EmacsMail(ExternalMailClient):
353
 
    """Call emacsclient to have a mail buffer.
354
 
 
355
 
    This only work for emacs >= 22.1 due to recent -e/--eval support.
356
 
 
357
 
    The good news is that this implementation will work with all mail
358
 
    agents registered against ``mail-user-agent``. So there is no need
359
 
    to instantiate ExternalMailClient for each and every GNU Emacs
360
 
    MUA.
361
 
 
362
 
    Users just have to ensure that ``mail-user-agent`` is set according
363
 
    to their tastes.
364
 
    """
365
 
 
366
 
    _client_commands = ['emacsclient']
367
 
 
368
 
    def _prepare_send_function(self):
369
 
        """Write our wrapper function into a temporary file.
370
 
 
371
 
        This temporary file will be loaded at runtime in
372
 
        _get_compose_commandline function.
373
 
 
374
 
        This function does not remove the file.  That's a wanted
375
 
        behaviour since _get_compose_commandline won't run the send
376
 
        mail function directly but return the eligible command line.
377
 
        Removing our temporary file here would prevent our sendmail
378
 
        function to work.  (The file is deleted by some elisp code
379
 
        after being read by Emacs.)
380
 
        """
381
 
 
382
 
        _defun = r"""(defun bzr-add-mime-att (file)
383
 
  "Attach FILE to a mail buffer as a MIME attachment."
384
 
  (let ((agent mail-user-agent))
385
 
    (if (and file (file-exists-p file))
386
 
        (cond
387
 
         ((eq agent 'sendmail-user-agent)
388
 
          (progn
389
 
            (mail-text)
390
 
            (newline)
391
 
            (if (functionp 'etach-attach)
392
 
              (etach-attach file)
393
 
              (mail-attach-file file))))
394
 
         ((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
395
 
          (progn
396
 
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
397
 
         ((eq agent 'mew-user-agent)
398
 
          (progn
399
 
            (mew-draft-prepare-attachments)
400
 
            (mew-attach-link file (file-name-nondirectory file))
401
 
            (let* ((nums (mew-syntax-nums))
402
 
                   (syntax (mew-syntax-get-entry mew-encode-syntax nums)))
403
 
              (mew-syntax-set-cd syntax "BZR merge")
404
 
              (mew-encode-syntax-print mew-encode-syntax))
405
 
            (mew-header-goto-body)))
406
 
         (t
407
 
          (message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
408
 
      (error "File %s does not exist." file))))
409
 
"""
410
 
 
411
 
        fd, temp_file = tempfile.mkstemp(prefix="emacs-bzr-send-",
412
 
                                         suffix=".el")
413
 
        try:
414
 
            os.write(fd, _defun)
415
 
        finally:
416
 
            os.close(fd) # Just close the handle but do not remove the file.
417
 
        return temp_file
418
 
 
419
 
    def _get_compose_commandline(self, to, subject, attach_path):
420
 
        commandline = ["--eval"]
421
 
 
422
 
        _to = "nil"
423
 
        _subject = "nil"
424
 
 
425
 
        if to is not None:
426
 
            _to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
427
 
        if subject is not None:
428
 
            _subject = ("\"%s\"" %
429
 
                        self._encode_safe(subject).replace('"', '\\"'))
430
 
 
431
 
        # Funcall the default mail composition function
432
 
        # This will work with any mail mode including default mail-mode
433
 
        # User must tweak mail-user-agent variable to tell what function
434
 
        # will be called inside compose-mail.
435
 
        mail_cmd = "(compose-mail %s %s)" % (_to, _subject)
436
 
        commandline.append(mail_cmd)
437
 
 
438
 
        # Try to attach a MIME attachment using our wrapper function
439
 
        if attach_path is not None:
440
 
            # Do not create a file if there is no attachment
441
 
            elisp = self._prepare_send_function()
442
 
            lmmform = '(load "%s")' % elisp
443
 
            mmform  = '(bzr-add-mime-att "%s")' % \
444
 
                self._encode_path(attach_path, 'attachment')
445
 
            rmform = '(delete-file "%s")' % elisp
446
 
            commandline.append(lmmform)
447
 
            commandline.append(mmform)
448
 
            commandline.append(rmform)
449
 
 
450
 
        return commandline
451
 
mail_client_registry.register('emacsclient', EmacsMail,
452
 
                              help=EmacsMail.__doc__)
 
260
        commandline = [to]
 
261
        if subject is not None:
 
262
            commandline.extend(['--subject', subject])
 
263
        if attach_path is not None:
 
264
            commandline.extend(['--attach', attach_path])
 
265
        return commandline
453
266
 
454
267
 
455
268
class MAPIClient(ExternalMailClient):
468
281
            if e.code != simplemapi.MAPI_USER_ABORT:
469
282
                raise errors.MailClientNotFound(['MAPI supported mail client'
470
283
                                                 ' (error %d)' % (e.code,)])
471
 
mail_client_registry.register('mapi', MAPIClient,
472
 
                              help=MAPIClient.__doc__)
473
284
 
474
285
 
475
286
class DefaultMail(MailClient):
484
295
            return XDGEmail(self.config)
485
296
 
486
297
    def compose(self, prompt, to, subject, attachment, mime_subtype,
487
 
                extension, basename=None):
 
298
                extension):
488
299
        """See MailClient.compose"""
489
300
        try:
490
301
            return self._mail_client().compose(prompt, to, subject,
491
302
                                               attachment, mimie_subtype,
492
 
                                               extension, basename)
 
303
                                               extension)
493
304
        except errors.MailClientNotFound:
494
305
            return Editor(self.config).compose(prompt, to, subject,
495
306
                          attachment, mimie_subtype, extension)
496
307
 
497
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
308
    def compose_merge_request(self, to, subject, directive):
498
309
        """See MailClient.compose_merge_request"""
499
310
        try:
500
311
            return self._mail_client().compose_merge_request(to, subject,
501
 
                    directive, basename=basename)
 
312
                                                             directive)
502
313
        except errors.MailClientNotFound:
503
314
            return Editor(self.config).compose_merge_request(to, subject,
504
 
                          directive, basename=basename)
505
 
mail_client_registry.register('default', DefaultMail,
506
 
                              help=DefaultMail.__doc__)
507
 
mail_client_registry.default_key = 'default'
 
315
                          directive)