~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: John Arbash Meinel
  • Date: 2008-08-25 21:50:11 UTC
  • mfrom: (0.11.3 tools)
  • mto: This revision was merged to the branch mainline in revision 3659.
  • Revision ID: john@arbash-meinel.com-20080825215011-de9esmzgkue3e522
Merge in Lukáš's helper scripts.
Update the packaging documents to describe how to do the releases
using bzr-builddeb to package all distro platforms
simultaneously.

Show diffs side-by-side

added added

removed removed

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