~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-08-28 09:22:17 UTC
  • mfrom: (3654.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20080828092217-98wmtek2p8cie8sc
(vila) Fix bug #225020 by catching CURLE_SEND_ERROR

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., 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
19
19
import subprocess
20
20
import sys
21
21
import tempfile
22
 
import urllib
23
22
 
24
23
import bzrlib
25
24
from bzrlib import (
41
40
        self.config = config
42
41
 
43
42
    def compose(self, prompt, to, subject, attachment, mime_subtype,
44
 
                extension, basename=None, body=None):
 
43
                extension, basename=None):
45
44
        """Compose (and possibly send) an email message
46
45
 
47
46
        Must be implemented by subclasses.
61
60
        """
62
61
        raise NotImplementedError
63
62
 
64
 
    def compose_merge_request(self, to, subject, directive, basename=None,
65
 
                              body=None):
 
63
    def compose_merge_request(self, to, subject, directive, basename=None):
66
64
        """Compose (and possibly send) a merge request
67
65
 
68
66
        :param to: The address to send the request to
75
73
        prompt = self._get_merge_prompt("Please describe these changes:", to,
76
74
                                        subject, directive)
77
75
        self.compose(prompt, to, subject, directive,
78
 
            'x-patch', '.patch', basename, body)
 
76
            'x-patch', '.patch', basename)
79
77
 
80
78
    def _get_merge_prompt(self, prompt, to, subject, attachment):
81
79
        """Generate a prompt string.  Overridden by Editor.
91
89
class Editor(MailClient):
92
90
    """DIY mail client that uses commit message editor"""
93
91
 
94
 
    supports_body = True
95
 
 
96
92
    def _get_merge_prompt(self, prompt, to, subject, attachment):
97
93
        """See MailClient._get_merge_prompt"""
98
94
        return (u"%s\n\n"
102
98
                         attachment.decode('utf-8', 'replace')))
103
99
 
104
100
    def compose(self, prompt, to, subject, attachment, mime_subtype,
105
 
                extension, basename=None, body=None):
 
101
                extension, basename=None):
106
102
        """See MailClient.compose"""
107
103
        if not to:
108
104
            raise errors.NoMailAddressSpecified()
109
 
        body = msgeditor.edit_commit_message(prompt, start_message=body)
 
105
        body = msgeditor.edit_commit_message(prompt)
110
106
        if body == '':
111
107
            raise errors.NoMessageSupplied()
112
108
        email_message.EmailMessage.send(self.config,
120
116
                              help=Editor.__doc__)
121
117
 
122
118
 
123
 
class BodyExternalMailClient(MailClient):
124
 
 
125
 
    supports_body = True
 
119
class ExternalMailClient(MailClient):
 
120
    """An external mail client."""
126
121
 
127
122
    def _get_client_commands(self):
128
123
        """Provide a list of commands that may invoke the mail client"""
133
128
            return self._client_commands
134
129
 
135
130
    def compose(self, prompt, to, subject, attachment, mime_subtype,
136
 
                extension, basename=None, body=None):
 
131
                extension, basename=None):
137
132
        """See MailClient.compose.
138
133
 
139
134
        Writes the attachment to a temporary file, invokes _compose.
140
135
        """
141
136
        if basename is None:
142
137
            basename = 'attachment'
143
 
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
 
138
        pathname = tempfile.mkdtemp(prefix='bzr-mail-')
144
139
        attach_path = osutils.pathjoin(pathname, basename + extension)
145
140
        outfile = open(attach_path, 'wb')
146
141
        try:
147
142
            outfile.write(attachment)
148
143
        finally:
149
144
            outfile.close()
150
 
        if body is not None:
151
 
            kwargs = {'body': body}
152
 
        else:
153
 
            kwargs = {}
154
145
        self._compose(prompt, to, subject, attach_path, mime_subtype,
155
 
                      extension, **kwargs)
 
146
                      extension)
156
147
 
157
148
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
 
                 extension, body=None, from_=None):
 
149
                extension):
159
150
        """Invoke a mail client as a commandline process.
160
151
 
161
152
        Overridden by MAPIClient.
166
157
            "text", but the precise subtype can be specified here
167
158
        :param extension: A file extension (including period) associated with
168
159
            the attachment type.
169
 
        :param body: Optional body text.
170
 
        :param from_: Optional From: header.
171
160
        """
172
161
        for name in self._get_client_commands():
173
162
            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
163
            cmdline.extend(self._get_compose_commandline(to, subject,
181
 
                                                         attach_path,
182
 
                                                         **kwargs))
 
164
                                                         attach_path))
183
165
            try:
184
166
                subprocess.call(cmdline)
185
167
            except OSError, e:
190
172
        else:
191
173
            raise errors.MailClientNotFound(self._client_commands)
192
174
 
193
 
    def _get_compose_commandline(self, to, subject, attach_path, body):
 
175
    def _get_compose_commandline(self, to, subject, attach_path):
194
176
        """Determine the commandline to use for composing a message
195
177
 
196
178
        Implemented by various subclasses
209
191
        :return:    encoded string if u is unicode, u itself otherwise.
210
192
        """
211
193
        if isinstance(u, unicode):
212
 
            return u.encode(osutils.get_user_encoding(), 'replace')
 
194
            return u.encode(bzrlib.user_encoding, 'replace')
213
195
        return u
214
196
 
215
197
    def _encode_path(self, path, kind):
223
205
        """
224
206
        if isinstance(path, unicode):
225
207
            try:
226
 
                return path.encode(osutils.get_user_encoding())
 
208
                return path.encode(bzrlib.user_encoding)
227
209
            except UnicodeEncodeError:
228
210
                raise errors.UnableEncodePath(path, kind)
229
211
        return path
230
212
 
231
213
 
232
 
class ExternalMailClient(BodyExternalMailClient):
233
 
    """An external mail client."""
234
 
 
235
 
    supports_body = False
236
 
 
237
 
 
238
 
class Evolution(BodyExternalMailClient):
 
214
class Evolution(ExternalMailClient):
239
215
    """Evolution mail client."""
240
216
 
241
217
    _client_commands = ['evolution']
242
218
 
243
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
219
    def _get_compose_commandline(self, to, subject, attach_path):
244
220
        """See ExternalMailClient._get_compose_commandline"""
245
221
        message_options = {}
246
222
        if subject is not None:
247
223
            message_options['subject'] = subject
248
224
        if attach_path is not None:
249
225
            message_options['attach'] = attach_path
250
 
        if body is not None:
251
 
            message_options['body'] = body
252
226
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
227
                        sorted(message_options.iteritems())]
254
228
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
257
231
                              help=Evolution.__doc__)
258
232
 
259
233
 
260
 
class Mutt(BodyExternalMailClient):
 
234
class Mutt(ExternalMailClient):
261
235
    """Mutt mail client."""
262
236
 
263
237
    _client_commands = ['mutt']
264
238
 
265
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
239
    def _get_compose_commandline(self, to, subject, attach_path):
266
240
        """See ExternalMailClient._get_compose_commandline"""
267
241
        message_options = []
268
242
        if subject is not None:
270
244
        if attach_path is not None:
271
245
            message_options.extend(['-a',
272
246
                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
247
        if to is not None:
282
 
            message_options.extend(['--', self._encode_safe(to)])
 
248
            message_options.append(self._encode_safe(to))
283
249
        return message_options
284
250
mail_client_registry.register('mutt', Mutt,
285
251
                              help=Mutt.__doc__)
286
252
 
287
253
 
288
 
class Thunderbird(BodyExternalMailClient):
 
254
class Thunderbird(ExternalMailClient):
289
255
    """Mozilla Thunderbird (or Icedove)
290
256
 
291
257
    Note that Thunderbird 1.5 is buggy and does not support setting
299
265
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
266
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
301
267
 
302
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
268
    def _get_compose_commandline(self, to, subject, attach_path):
303
269
        """See ExternalMailClient._get_compose_commandline"""
304
270
        message_options = {}
305
271
        if to is not None:
309
275
        if attach_path is not None:
310
276
            message_options['attachment'] = urlutils.local_path_to_url(
311
277
                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())])
 
278
        options_list = ["%s='%s'" % (k, v) for k, v in
 
279
                        sorted(message_options.iteritems())]
318
280
        return ['-compose', ','.join(options_list)]
319
281
mail_client_registry.register('thunderbird', Thunderbird,
320
282
                              help=Thunderbird.__doc__)
340
302
                              help=KMail.__doc__)
341
303
 
342
304
 
343
 
class Claws(ExternalMailClient):
344
 
    """Claws mail client."""
345
 
 
346
 
    supports_body = True
347
 
 
348
 
    _client_commands = ['claws-mail']
349
 
 
350
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
351
 
                                 from_=None):
352
 
        """See ExternalMailClient._get_compose_commandline"""
353
 
        compose_url = []
354
 
        if from_ is not None:
355
 
            compose_url.append('from=' + urllib.quote(from_))
356
 
        if subject is not None:
357
 
            # Don't use urllib.quote_plus because Claws doesn't seem
358
 
            # to recognise spaces encoded as "+".
359
 
            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))
369
 
        # Collect command-line options.
370
 
        message_options = ['--compose', compose_url]
371
 
        if attach_path is not None:
372
 
            message_options.extend(
373
 
                ['--attach', self._encode_path(attach_path, 'attachment')])
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
 
 
385
 
mail_client_registry.register('claws', Claws,
386
 
                              help=Claws.__doc__)
387
 
 
388
 
 
389
 
class XDGEmail(BodyExternalMailClient):
 
305
class XDGEmail(ExternalMailClient):
390
306
    """xdg-email attempts to invoke the user's preferred mail client"""
391
307
 
392
308
    _client_commands = ['xdg-email']
393
309
 
394
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
 
310
    def _get_compose_commandline(self, to, subject, attach_path):
395
311
        """See ExternalMailClient._get_compose_commandline"""
396
312
        if not to:
397
313
            raise errors.NoMailAddressSpecified()
401
317
        if attach_path is not None:
402
318
            commandline.extend(['--attach',
403
319
                self._encode_path(attach_path, 'attachment')])
404
 
        if body is not None:
405
 
            commandline.extend(['--body', self._encode_safe(body)])
406
320
        return commandline
407
321
mail_client_registry.register('xdg-email', XDGEmail,
408
322
                              help=XDGEmail.__doc__)
450
364
            (if (functionp 'etach-attach)
451
365
              (etach-attach file)
452
366
              (mail-attach-file file))))
453
 
         ((or (eq agent 'message-user-agent)
454
 
              (eq agent 'gnus-user-agent)
455
 
              (eq agent 'mh-e-user-agent))
 
367
         ((or (eq agent 'message-user-agent)(eq agent 'gnus-user-agent))
456
368
          (progn
457
369
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
458
370
         ((eq agent 'mew-user-agent)
513
425
                              help=EmacsMail.__doc__)
514
426
 
515
427
 
516
 
class MAPIClient(BodyExternalMailClient):
 
428
class MAPIClient(ExternalMailClient):
517
429
    """Default Windows mail client launched using MAPI."""
518
430
 
519
431
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
520
 
                 extension, body=None):
 
432
                 extension):
521
433
        """See ExternalMailClient._compose.
522
434
 
523
435
        This implementation uses MAPI via the simplemapi ctypes wrapper
524
436
        """
525
437
        from bzrlib.util import simplemapi
526
438
        try:
527
 
            simplemapi.SendMail(to or '', subject or '', body or '',
528
 
                                attach_path)
 
439
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
529
440
        except simplemapi.MAPIError, e:
530
441
            if e.code != simplemapi.MAPI_USER_ABORT:
531
442
                raise errors.MailClientNotFound(['MAPI supported mail client'
538
449
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
539
450
    falls back to Editor"""
540
451
 
541
 
    supports_body = True
542
 
 
543
452
    def _mail_client(self):
544
453
        """Determine the preferred mail client for this platform"""
545
454
        if osutils.supports_mapi():
548
457
            return XDGEmail(self.config)
549
458
 
550
459
    def compose(self, prompt, to, subject, attachment, mime_subtype,
551
 
                extension, basename=None, body=None):
 
460
                extension, basename=None):
552
461
        """See MailClient.compose"""
553
462
        try:
554
463
            return self._mail_client().compose(prompt, to, subject,
555
464
                                               attachment, mimie_subtype,
556
 
                                               extension, basename, body)
 
465
                                               extension, basename)
557
466
        except errors.MailClientNotFound:
558
467
            return Editor(self.config).compose(prompt, to, subject,
559
 
                          attachment, mimie_subtype, extension, body)
 
468
                          attachment, mimie_subtype, extension)
560
469
 
561
 
    def compose_merge_request(self, to, subject, directive, basename=None,
562
 
                              body=None):
 
470
    def compose_merge_request(self, to, subject, directive, basename=None):
563
471
        """See MailClient.compose_merge_request"""
564
472
        try:
565
473
            return self._mail_client().compose_merge_request(to, subject,
566
 
                    directive, basename=basename, body=body)
 
474
                    directive, basename=basename)
567
475
        except errors.MailClientNotFound:
568
476
            return Editor(self.config).compose_merge_request(to, subject,
569
 
                          directive, basename=basename, body=body)
 
477
                          directive, basename=basename)
570
478
mail_client_registry.register('default', DefaultMail,
571
479
                              help=DefaultMail.__doc__)
572
480
mail_client_registry.default_key = 'default'