source: main/waeup.kofa/trunk/src/waeup/kofa/smtp.py @ 8381

Last change on this file since 8381 was 8379, checked in by Henrik Bettermann, 13 years ago

Catch TypeError? exception if from address is empty.

  • Property svn:keywords set to Id
File size: 5.5 KB
Line 
1## $Id: smtp.py 8379 2012-05-07 08:45:42Z henrik $
2##
3## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""
19Email (SMTP) services for Kofa.
20
21Note About Encodings
22--------------------
23
24All functions in this module expect any raw strings passed in to be
25encoded 'utf-8' (if you pass in unicode objects instead, this is not a
26problem).
27
28This is because we cannot easily tell from a raw string (it is in fact
29only a byte stream) what encoding it has. In latin-1 and utf-8, for
30instance, there exist some chars (byte values) that have different
31meanings in both encodings. If we see such a char in a byte stream:
32what is it meant to represent? The respective character from latin1 or
33the one from utf-8?
34
35We therefore interpret all internally used raw strings to be encoded as
36utf-8.
37
38The functions in here nevertheless try hard to produce output (mail
39parts, headers, etc.) encoded in the least complex manner. For
40instance if you pass in some address or mail body that is
41representable (correctly) as ASCII or latin-1, we will turn the text
42into that encoding (given, you passed it in as utf-8) to stay as
43compatible as possible with old mailers that do not understand utf-8.
44
45"""
46import grok
47import logging
48from email.Header import Header
49from email.Utils import formataddr
50from email.mime.text import MIMEText
51from zope.component import getUtility
52from zope.sendmail.interfaces import IMailDelivery
53from waeup.kofa.interfaces import IMailService
54
55class DefaultMailService(grok.GlobalUtility):
56    """Returns a :class:`zope.sendmail.IMailDelivery`.
57
58    Searches a site from current request (if applicable) and returns
59    the mail delivery set for this site or a fake mailer that does not
60    really send mail (for testing, evaluating, etc.).
61    """
62    grok.implements(IMailService)
63
64    def __call__(self):
65        name = 'No email service'
66        site = grok.getSite()
67        if site is not None:
68            config = site['configuration']
69            name = getattr(config, 'smtp_mailer', name)
70        return getUtility(IMailDelivery, name=name)
71
72class FakeSMTPDelivery(grok.GlobalUtility):
73    """A fake mail delivery for testing etc.
74
75    Instead of sending real mails, this mailer only logs received
76    messages to the ``test.smtp`` logger.
77    """
78    grok.implements(IMailDelivery)
79    grok.name('No email service')
80
81    def send(self, fromaddr, toaddrs, message):
82        logger = logging.getLogger('test.smtp')
83        rcpts = ', '.join([x.decode('utf-8') for x in toaddrs])
84        logger.info(
85            u"Sending email from %s to %s:" % (
86                fromaddr.decode('utf-8'), rcpts))
87        logger.info(u"Message:")
88        for line in message.split('\n'):
89            logger.info(u"msg: " + line.decode('utf-8'))
90        return 'fake-message-id@example.com'
91
92CHARSETS = ('US-ASCII', 'ISO-8859-1', 'UTF-8')
93
94def encode_header_item(item):
95    if not isinstance(item, unicode):
96        try:
97            item = unicode(item, 'utf-8')
98        except TypeError:
99            return None
100    return str(Header(item, 'iso-8859-1')) # try ascii, then latin1, then utf-8
101
102def encode_address(addr, name=u''):
103    """Turn email address parts into a single valid email string.
104
105    The given email address and the name are turned into a single
106    (byte stream) string, suitable for use with ``To:`` or ``From:``
107    headers in emails.
108
109    Any encodings to a mailer-readable format are performed.
110
111    Preferred input format is unicode, although also raw strings (byte
112    streams) work as long as they are decodable from UTF-8.
113
114    That means: if you pass in non-unicode string, take care to
115    deliver utf-8 encoded strings (or plain ASCII).
116
117    Returns a single (raw) string like "My Name <my@sample.com>".
118    """
119    addr = encode_header_item(addr)
120    name = encode_header_item(name)
121    return formataddr((name, addr))
122
123def encode_body(text):
124    """Build MIME message part from text.
125
126    You can pass unicode objects or simple strings as text.
127
128    .. warn:: If the input is a simple string, this string is expected
129              to be encoded 'utf-8'!
130
131    Returns a MIMEText object.
132    """
133    if not isinstance(text, unicode):
134        text = unicode(text, 'utf-8')
135    charset = CHARSETS[-1] # fallback
136    for charset in CHARSETS:
137        try:
138            text = text.encode(charset)
139        except UnicodeError:
140            pass # try next encoding
141        else:
142            break
143    return MIMEText(text, 'plain', charset)
144
145def send_mail(from_name, from_addr, rcpt_name, rcpt_addr,
146              subject, body, config=None):
147    """Send mail.
148    """
149    # format message
150    body = encode_body(body)
151    body["From"] = encode_address(from_addr, from_name)
152    body["To"] = encode_address(rcpt_addr, rcpt_name)
153    body["Subject"] = encode_header_item(subject)
154
155    mailer = getUtility(IMailService)
156    result = mailer().send(from_addr, [rcpt_addr], body.as_string())
157    return result
Note: See TracBrowser for help on using the repository browser.