~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Andrew Bennetts
  • Date: 2009-07-27 05:35:00 UTC
  • mfrom: (4570 +trunk)
  • mto: (4634.6.29 2.0)
  • mto: This revision was merged to the branch mainline in revision 4680.
  • Revision ID: andrew.bennetts@canonical.com-20090727053500-q76zsn2dx33jhmj5
Merge bzr.dev.

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__)
454
513
                              help=EmacsMail.__doc__)
455
514
 
456
515
 
457
 
class MAPIClient(ExternalMailClient):
 
516
class MAPIClient(BodyExternalMailClient):
458
517
    """Default Windows mail client launched using MAPI."""
459
518
 
460
519
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
461
 
                 extension):
 
520
                 extension, body=None):
462
521
        """See ExternalMailClient._compose.
463
522
 
464
523
        This implementation uses MAPI via the simplemapi ctypes wrapper
465
524
        """
466
525
        from bzrlib.util import simplemapi
467
526
        try:
468
 
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
 
527
            simplemapi.SendMail(to or '', subject or '', body or '',
 
528
                                attach_path)
469
529
        except simplemapi.MAPIError, e:
470
530
            if e.code != simplemapi.MAPI_USER_ABORT:
471
531
                raise errors.MailClientNotFound(['MAPI supported mail client'
478
538
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
479
539
    falls back to Editor"""
480
540
 
 
541
    supports_body = True
 
542
 
481
543
    def _mail_client(self):
482
544
        """Determine the preferred mail client for this platform"""
483
545
        if osutils.supports_mapi():
486
548
            return XDGEmail(self.config)
487
549
 
488
550
    def compose(self, prompt, to, subject, attachment, mime_subtype,
489
 
                extension, basename=None):
 
551
                extension, basename=None, body=None):
490
552
        """See MailClient.compose"""
491
553
        try:
492
554
            return self._mail_client().compose(prompt, to, subject,
493
555
                                               attachment, mimie_subtype,
494
 
                                               extension, basename)
 
556
                                               extension, basename, body)
495
557
        except errors.MailClientNotFound:
496
558
            return Editor(self.config).compose(prompt, to, subject,
497
 
                          attachment, mimie_subtype, extension)
 
559
                          attachment, mimie_subtype, extension, body)
498
560
 
499
 
    def compose_merge_request(self, to, subject, directive, basename=None):
 
561
    def compose_merge_request(self, to, subject, directive, basename=None,
 
562
                              body=None):
500
563
        """See MailClient.compose_merge_request"""
501
564
        try:
502
565
            return self._mail_client().compose_merge_request(to, subject,
503
 
                    directive, basename=basename)
 
566
                    directive, basename=basename, body=body)
504
567
        except errors.MailClientNotFound:
505
568
            return Editor(self.config).compose_merge_request(to, subject,
506
 
                          directive, basename=basename)
 
569
                          directive, basename=basename, body=body)
507
570
mail_client_registry.register('default', DefaultMail,
508
571
                              help=DefaultMail.__doc__)
509
572
mail_client_registry.default_key = 'default'