~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Michael Ellerman
  • Date: 2006-02-28 14:45:51 UTC
  • mto: (1558.1.18 Aaron's integration)
  • mto: This revision was merged to the branch mainline in revision 1586.
  • Revision ID: michael@ellerman.id.au-20060228144551-3d9941ecde4a0b0a
Update contrib/pwk for -p1 diffs from bzr

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
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
16
 
 
17
 
import errno
18
 
import os
19
 
import subprocess
20
 
import sys
21
 
import tempfile
22
 
import urllib
23
 
 
24
 
import bzrlib
25
 
from bzrlib import (
26
 
    email_message,
27
 
    errors,
28
 
    msgeditor,
29
 
    osutils,
30
 
    urlutils,
31
 
    registry
32
 
    )
33
 
 
34
 
mail_client_registry = registry.Registry()
35
 
 
36
 
 
37
 
class MailClient(object):
38
 
    """A mail client that can send messages with attachements."""
39
 
 
40
 
    def __init__(self, config):
41
 
        self.config = config
42
 
 
43
 
    def compose(self, prompt, to, subject, attachment, mime_subtype,
44
 
                extension, basename=None, body=None):
45
 
        """Compose (and possibly send) an email message
46
 
 
47
 
        Must be implemented by subclasses.
48
 
 
49
 
        :param prompt: A message to tell the user what to do.  Supported by
50
 
            the Editor client, but ignored by others
51
 
        :param to: The address to send the message to
52
 
        :param subject: The contents of the subject line
53
 
        :param attachment: An email attachment, as a bytestring
54
 
        :param mime_subtype: The attachment is assumed to be a subtype of
55
 
            Text.  This allows the precise subtype to be specified, e.g.
56
 
            "plain", "x-patch", etc.
57
 
        :param extension: The file extension associated with the attachment
58
 
            type, e.g. ".patch"
59
 
        :param basename: The name to use for the attachment, e.g.
60
 
            "send-nick-3252"
61
 
        """
62
 
        raise NotImplementedError
63
 
 
64
 
    def compose_merge_request(self, to, subject, directive, basename=None,
65
 
                              body=None):
66
 
        """Compose (and possibly send) a merge request
67
 
 
68
 
        :param to: The address to send the request to
69
 
        :param subject: The subject line to use for the request
70
 
        :param directive: A merge directive representing the merge request, as
71
 
            a bytestring.
72
 
        :param basename: The name to use for the attachment, e.g.
73
 
            "send-nick-3252"
74
 
        """
75
 
        prompt = self._get_merge_prompt("Please describe these changes:", to,
76
 
                                        subject, directive)
77
 
        self.compose(prompt, to, subject, directive,
78
 
            'x-patch', '.patch', basename, body)
79
 
 
80
 
    def _get_merge_prompt(self, prompt, to, subject, attachment):
81
 
        """Generate a prompt string.  Overridden by Editor.
82
 
 
83
 
        :param prompt: A string suggesting what user should do
84
 
        :param to: The address the mail will be sent to
85
 
        :param subject: The subject line of the mail
86
 
        :param attachment: The attachment that will be used
87
 
        """
88
 
        return ''
89
 
 
90
 
 
91
 
class Editor(MailClient):
92
 
    """DIY mail client that uses commit message editor"""
93
 
 
94
 
    supports_body = True
95
 
 
96
 
    def _get_merge_prompt(self, prompt, to, subject, attachment):
97
 
        """See MailClient._get_merge_prompt"""
98
 
        return (u"%s\n\n"
99
 
                u"To: %s\n"
100
 
                u"Subject: %s\n\n"
101
 
                u"%s" % (prompt, to, subject,
102
 
                         attachment.decode('utf-8', 'replace')))
103
 
 
104
 
    def compose(self, prompt, to, subject, attachment, mime_subtype,
105
 
                extension, basename=None, body=None):
106
 
        """See MailClient.compose"""
107
 
        if not to:
108
 
            raise errors.NoMailAddressSpecified()
109
 
        body = msgeditor.edit_commit_message(prompt, start_message=body)
110
 
        if body == '':
111
 
            raise errors.NoMessageSupplied()
112
 
        email_message.EmailMessage.send(self.config,
113
 
                                        self.config.username(),
114
 
                                        to,
115
 
                                        subject,
116
 
                                        body,
117
 
                                        attachment,
118
 
                                        attachment_mime_subtype=mime_subtype)
119
 
mail_client_registry.register('editor', Editor,
120
 
                              help=Editor.__doc__)
121
 
 
122
 
 
123
 
class BodyExternalMailClient(MailClient):
124
 
 
125
 
    supports_body = True
126
 
 
127
 
    def _get_client_commands(self):
128
 
        """Provide a list of commands that may invoke the mail client"""
129
 
        if sys.platform == 'win32':
130
 
            import win32utils
131
 
            return [win32utils.get_app_path(i) for i in self._client_commands]
132
 
        else:
133
 
            return self._client_commands
134
 
 
135
 
    def compose(self, prompt, to, subject, attachment, mime_subtype,
136
 
                extension, basename=None, body=None):
137
 
        """See MailClient.compose.
138
 
 
139
 
        Writes the attachment to a temporary file, invokes _compose.
140
 
        """
141
 
        if basename is None:
142
 
            basename = 'attachment'
143
 
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
144
 
        attach_path = osutils.pathjoin(pathname, basename + extension)
145
 
        outfile = open(attach_path, 'wb')
146
 
        try:
147
 
            outfile.write(attachment)
148
 
        finally:
149
 
            outfile.close()
150
 
        if body is not None:
151
 
            kwargs = {'body': body}
152
 
        else:
153
 
            kwargs = {}
154
 
        self._compose(prompt, to, subject, attach_path, mime_subtype,
155
 
                      extension, **kwargs)
156
 
 
157
 
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
158
 
                 extension, body=None, from_=None):
159
 
        """Invoke a mail client as a commandline process.
160
 
 
161
 
        Overridden by MAPIClient.
162
 
        :param to: The address to send the mail to
163
 
        :param subject: The subject line for the mail
164
 
        :param pathname: The path to the attachment
165
 
        :param mime_subtype: The attachment is assumed to have a major type of
166
 
            "text", but the precise subtype can be specified here
167
 
        :param extension: A file extension (including period) associated with
168
 
            the attachment type.
169
 
        :param body: Optional body text.
170
 
        :param from_: Optional From: header.
171
 
        """
172
 
        for name in self._get_client_commands():
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_
180
 
            cmdline.extend(self._get_compose_commandline(to, subject,
181
 
                                                         attach_path,
182
 
                                                         **kwargs))
183
 
            try:
184
 
                subprocess.call(cmdline)
185
 
            except OSError, e:
186
 
                if e.errno != errno.ENOENT:
187
 
                    raise
188
 
            else:
189
 
                break
190
 
        else:
191
 
            raise errors.MailClientNotFound(self._client_commands)
192
 
 
193
 
    def _get_compose_commandline(self, to, subject, attach_path, body):
194
 
        """Determine the commandline to use for composing a message
195
 
 
196
 
        Implemented by various subclasses
197
 
        :param to: The address to send the mail to
198
 
        :param subject: The subject line for the mail
199
 
        :param attach_path: The path to the attachment
200
 
        """
201
 
        raise NotImplementedError
202
 
 
203
 
    def _encode_safe(self, u):
204
 
        """Encode possible unicode string argument to 8-bit string
205
 
        in user_encoding. Unencodable characters will be replaced
206
 
        with '?'.
207
 
 
208
 
        :param  u:  possible unicode string.
209
 
        :return:    encoded string if u is unicode, u itself otherwise.
210
 
        """
211
 
        if isinstance(u, unicode):
212
 
            return u.encode(osutils.get_user_encoding(), 'replace')
213
 
        return u
214
 
 
215
 
    def _encode_path(self, path, kind):
216
 
        """Encode unicode path in user encoding.
217
 
 
218
 
        :param  path:   possible unicode path.
219
 
        :param  kind:   path kind ('executable' or 'attachment').
220
 
        :return:        encoded path if path is unicode,
221
 
                        path itself otherwise.
222
 
        :raise:         UnableEncodePath.
223
 
        """
224
 
        if isinstance(path, unicode):
225
 
            try:
226
 
                return path.encode(osutils.get_user_encoding())
227
 
            except UnicodeEncodeError:
228
 
                raise errors.UnableEncodePath(path, kind)
229
 
        return path
230
 
 
231
 
 
232
 
class ExternalMailClient(BodyExternalMailClient):
233
 
    """An external mail client."""
234
 
 
235
 
    supports_body = False
236
 
 
237
 
 
238
 
class Evolution(BodyExternalMailClient):
239
 
    """Evolution mail client."""
240
 
 
241
 
    _client_commands = ['evolution']
242
 
 
243
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
244
 
        """See ExternalMailClient._get_compose_commandline"""
245
 
        message_options = {}
246
 
        if subject is not None:
247
 
            message_options['subject'] = subject
248
 
        if attach_path is not None:
249
 
            message_options['attach'] = attach_path
250
 
        if body is not None:
251
 
            message_options['body'] = body
252
 
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
 
                        sorted(message_options.iteritems())]
254
 
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
255
 
            '&'.join(options_list))]
256
 
mail_client_registry.register('evolution', Evolution,
257
 
                              help=Evolution.__doc__)
258
 
 
259
 
 
260
 
class Mutt(BodyExternalMailClient):
261
 
    """Mutt mail client."""
262
 
 
263
 
    _client_commands = ['mutt']
264
 
 
265
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
266
 
        """See ExternalMailClient._get_compose_commandline"""
267
 
        message_options = []
268
 
        if subject is not None:
269
 
            message_options.extend(['-s', self._encode_safe(subject)])
270
 
        if attach_path is not None:
271
 
            message_options.extend(['-a',
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])
281
 
        if to is not None:
282
 
            message_options.extend(['--', self._encode_safe(to)])
283
 
        return message_options
284
 
mail_client_registry.register('mutt', Mutt,
285
 
                              help=Mutt.__doc__)
286
 
 
287
 
 
288
 
class Thunderbird(BodyExternalMailClient):
289
 
    """Mozilla Thunderbird (or Icedove)
290
 
 
291
 
    Note that Thunderbird 1.5 is buggy and does not support setting
292
 
    "to" simultaneously with including a attachment.
293
 
 
294
 
    There is a workaround if no attachment is present, but we always need to
295
 
    send attachments.
296
 
    """
297
 
 
298
 
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
299
 
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
 
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
301
 
 
302
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
303
 
        """See ExternalMailClient._get_compose_commandline"""
304
 
        message_options = {}
305
 
        if to is not None:
306
 
            message_options['to'] = self._encode_safe(to)
307
 
        if subject is not None:
308
 
            message_options['subject'] = self._encode_safe(subject)
309
 
        if attach_path is not None:
310
 
            message_options['attachment'] = urlutils.local_path_to_url(
311
 
                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())])
318
 
        return ['-compose', ','.join(options_list)]
319
 
mail_client_registry.register('thunderbird', Thunderbird,
320
 
                              help=Thunderbird.__doc__)
321
 
 
322
 
 
323
 
class KMail(ExternalMailClient):
324
 
    """KDE mail client."""
325
 
 
326
 
    _client_commands = ['kmail']
327
 
 
328
 
    def _get_compose_commandline(self, to, subject, attach_path):
329
 
        """See ExternalMailClient._get_compose_commandline"""
330
 
        message_options = []
331
 
        if subject is not None:
332
 
            message_options.extend(['-s', self._encode_safe(subject)])
333
 
        if attach_path is not None:
334
 
            message_options.extend(['--attach',
335
 
                self._encode_path(attach_path, 'attachment')])
336
 
        if to is not None:
337
 
            message_options.extend([self._encode_safe(to)])
338
 
        return message_options
339
 
mail_client_registry.register('kmail', KMail,
340
 
                              help=KMail.__doc__)
341
 
 
342
 
 
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):
390
 
    """xdg-email attempts to invoke the user's preferred mail client"""
391
 
 
392
 
    _client_commands = ['xdg-email']
393
 
 
394
 
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
395
 
        """See ExternalMailClient._get_compose_commandline"""
396
 
        if not to:
397
 
            raise errors.NoMailAddressSpecified()
398
 
        commandline = [self._encode_safe(to)]
399
 
        if subject is not None:
400
 
            commandline.extend(['--subject', self._encode_safe(subject)])
401
 
        if attach_path is not None:
402
 
            commandline.extend(['--attach',
403
 
                self._encode_path(attach_path, 'attachment')])
404
 
        if body is not None:
405
 
            commandline.extend(['--body', self._encode_safe(body)])
406
 
        return commandline
407
 
mail_client_registry.register('xdg-email', XDGEmail,
408
 
                              help=XDGEmail.__doc__)
409
 
 
410
 
 
411
 
class EmacsMail(ExternalMailClient):
412
 
    """Call emacsclient to have a mail buffer.
413
 
 
414
 
    This only work for emacs >= 22.1 due to recent -e/--eval support.
415
 
 
416
 
    The good news is that this implementation will work with all mail
417
 
    agents registered against ``mail-user-agent``. So there is no need
418
 
    to instantiate ExternalMailClient for each and every GNU Emacs
419
 
    MUA.
420
 
 
421
 
    Users just have to ensure that ``mail-user-agent`` is set according
422
 
    to their tastes.
423
 
    """
424
 
 
425
 
    _client_commands = ['emacsclient']
426
 
 
427
 
    def __init__(self, config):
428
 
        super(EmacsMail, self).__init__(config)
429
 
        self.elisp_tmp_file = None
430
 
 
431
 
    def _prepare_send_function(self):
432
 
        """Write our wrapper function into a temporary file.
433
 
 
434
 
        This temporary file will be loaded at runtime in
435
 
        _get_compose_commandline function.
436
 
 
437
 
        This function does not remove the file.  That's a wanted
438
 
        behaviour since _get_compose_commandline won't run the send
439
 
        mail function directly but return the eligible command line.
440
 
        Removing our temporary file here would prevent our sendmail
441
 
        function to work.  (The file is deleted by some elisp code
442
 
        after being read by Emacs.)
443
 
        """
444
 
 
445
 
        _defun = r"""(defun bzr-add-mime-att (file)
446
 
  "Attach FILE to a mail buffer as a MIME attachment."
447
 
  (let ((agent mail-user-agent))
448
 
    (if (and file (file-exists-p file))
449
 
        (cond
450
 
         ((eq agent 'sendmail-user-agent)
451
 
          (progn
452
 
            (mail-text)
453
 
            (newline)
454
 
            (if (functionp 'etach-attach)
455
 
              (etach-attach file)
456
 
              (mail-attach-file file))))
457
 
         ((or (eq agent 'message-user-agent)
458
 
              (eq agent 'gnus-user-agent)
459
 
              (eq agent 'mh-e-user-agent))
460
 
          (progn
461
 
            (mml-attach-file file "text/x-patch" "BZR merge" "inline")))
462
 
         ((eq agent 'mew-user-agent)
463
 
          (progn
464
 
            (mew-draft-prepare-attachments)
465
 
            (mew-attach-link file (file-name-nondirectory file))
466
 
            (let* ((nums (mew-syntax-nums))
467
 
                   (syntax (mew-syntax-get-entry mew-encode-syntax nums)))
468
 
              (mew-syntax-set-cd syntax "BZR merge")
469
 
              (mew-encode-syntax-print mew-encode-syntax))
470
 
            (mew-header-goto-body)))
471
 
         (t
472
 
          (message "Unhandled MUA, report it on bazaar@lists.canonical.com")))
473
 
      (error "File %s does not exist." file))))
474
 
"""
475
 
 
476
 
        fd, temp_file = tempfile.mkstemp(prefix="emacs-bzr-send-",
477
 
                                         suffix=".el")
478
 
        try:
479
 
            os.write(fd, _defun)
480
 
        finally:
481
 
            os.close(fd) # Just close the handle but do not remove the file.
482
 
        return temp_file
483
 
 
484
 
    def _get_compose_commandline(self, to, subject, attach_path):
485
 
        commandline = ["--eval"]
486
 
 
487
 
        _to = "nil"
488
 
        _subject = "nil"
489
 
 
490
 
        if to is not None:
491
 
            _to = ("\"%s\"" % self._encode_safe(to).replace('"', '\\"'))
492
 
        if subject is not None:
493
 
            _subject = ("\"%s\"" %
494
 
                        self._encode_safe(subject).replace('"', '\\"'))
495
 
 
496
 
        # Funcall the default mail composition function
497
 
        # This will work with any mail mode including default mail-mode
498
 
        # User must tweak mail-user-agent variable to tell what function
499
 
        # will be called inside compose-mail.
500
 
        mail_cmd = "(compose-mail %s %s)" % (_to, _subject)
501
 
        commandline.append(mail_cmd)
502
 
 
503
 
        # Try to attach a MIME attachment using our wrapper function
504
 
        if attach_path is not None:
505
 
            # Do not create a file if there is no attachment
506
 
            elisp = self._prepare_send_function()
507
 
            self.elisp_tmp_file = elisp
508
 
            lmmform = '(load "%s")' % elisp
509
 
            mmform  = '(bzr-add-mime-att "%s")' % \
510
 
                self._encode_path(attach_path, 'attachment')
511
 
            rmform = '(delete-file "%s")' % elisp
512
 
            commandline.append(lmmform)
513
 
            commandline.append(mmform)
514
 
            commandline.append(rmform)
515
 
 
516
 
        return commandline
517
 
mail_client_registry.register('emacsclient', EmacsMail,
518
 
                              help=EmacsMail.__doc__)
519
 
 
520
 
 
521
 
class MAPIClient(BodyExternalMailClient):
522
 
    """Default Windows mail client launched using MAPI."""
523
 
 
524
 
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
525
 
                 extension, body=None):
526
 
        """See ExternalMailClient._compose.
527
 
 
528
 
        This implementation uses MAPI via the simplemapi ctypes wrapper
529
 
        """
530
 
        from bzrlib.util import simplemapi
531
 
        try:
532
 
            simplemapi.SendMail(to or '', subject or '', body or '',
533
 
                                attach_path)
534
 
        except simplemapi.MAPIError, e:
535
 
            if e.code != simplemapi.MAPI_USER_ABORT:
536
 
                raise errors.MailClientNotFound(['MAPI supported mail client'
537
 
                                                 ' (error %d)' % (e.code,)])
538
 
mail_client_registry.register('mapi', MAPIClient,
539
 
                              help=MAPIClient.__doc__)
540
 
 
541
 
 
542
 
class DefaultMail(MailClient):
543
 
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
544
 
    falls back to Editor"""
545
 
 
546
 
    supports_body = True
547
 
 
548
 
    def _mail_client(self):
549
 
        """Determine the preferred mail client for this platform"""
550
 
        if osutils.supports_mapi():
551
 
            return MAPIClient(self.config)
552
 
        else:
553
 
            return XDGEmail(self.config)
554
 
 
555
 
    def compose(self, prompt, to, subject, attachment, mime_subtype,
556
 
                extension, basename=None, body=None):
557
 
        """See MailClient.compose"""
558
 
        try:
559
 
            return self._mail_client().compose(prompt, to, subject,
560
 
                                               attachment, mimie_subtype,
561
 
                                               extension, basename, body)
562
 
        except errors.MailClientNotFound:
563
 
            return Editor(self.config).compose(prompt, to, subject,
564
 
                          attachment, mimie_subtype, extension, body)
565
 
 
566
 
    def compose_merge_request(self, to, subject, directive, basename=None,
567
 
                              body=None):
568
 
        """See MailClient.compose_merge_request"""
569
 
        try:
570
 
            return self._mail_client().compose_merge_request(to, subject,
571
 
                    directive, basename=basename, body=body)
572
 
        except errors.MailClientNotFound:
573
 
            return Editor(self.config).compose_merge_request(to, subject,
574
 
                          directive, basename=basename, body=body)
575
 
mail_client_registry.register('default', DefaultMail,
576
 
                              help=DefaultMail.__doc__)
577
 
mail_client_registry.default_key = 'default'