~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: John Ferlito
  • Date: 2009-09-02 04:31:45 UTC
  • mto: (4665.7.1 serve-init)
  • mto: This revision was merged to the branch mainline in revision 4913.
  • Revision ID: johnf@inodes.org-20090902043145-gxdsfw03ilcwbyn5
Add a debian init script for bzr --serve

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