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

Last change on this file since 11584 was 11584, checked in by uli, 11 years ago

pep8.

  • Property svn:keywords set to Id
File size: 6.1 KB
Line 
1## $Id: smtp.py 11584 2014-04-07 15:50:53Z uli $
2##
3## Copyright (C) 2014 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
55
56class DefaultMailService(grok.GlobalUtility):
57    """Returns a :class:`zope.sendmail.IMailDelivery`.
58
59    Searches a site from current request (if applicable) and returns
60    the mail delivery set for this site or a fake mailer that does not
61    really send mail (for testing, evaluating, etc.).
62    """
63    grok.implements(IMailService)
64
65    def __call__(self):
66        name = 'No email service'
67        site = grok.getSite()
68        if site is not None:
69            config = site['configuration']
70            name = getattr(config, 'smtp_mailer', name)
71        return getUtility(IMailDelivery, name=name)
72
73
74class FakeSMTPDelivery(grok.GlobalUtility):
75    """A fake mail delivery for testing etc.
76
77    Instead of sending real mails, this mailer only logs received
78    messages to the ``test.smtp`` logger.
79    """
80    grok.implements(IMailDelivery)
81    grok.name('No email service')
82
83    def send(self, fromaddr, toaddrs, message):
84        logger = logging.getLogger('test.smtp')
85        rcpts = ', '.join([x.decode('utf-8') for x in toaddrs])
86        logger.info(
87            u"Sending email from %s to %s:" % (
88                fromaddr.decode('utf-8'), rcpts))
89        logger.info(u"Message:")
90        for line in message.split('\n'):
91            logger.info(u"msg: " + line.decode('utf-8'))
92        return 'fake-message-id@example.com'
93
94CHARSETS = ('US-ASCII', 'ISO-8859-1', 'UTF-8')
95
96
97def encode_header_item(item):
98    """Turns `item`, a string into an SMTP header part string.
99
100    Encodings are checked carefully (we try to encode as ASCII,
101    Latin-1 and UTF-8 in that order).
102
103    If `item` is not a basestring, `None` is returned.
104    """
105    if not isinstance(item, basestring):
106        return None
107    if not isinstance(item, unicode):
108        item = unicode(item, 'utf-8')
109    return str(Header(item, 'iso-8859-1'))  # try ascii, then latin1, utf-8
110
111
112def encode_address(addr, name=u''):
113    """Turn email address parts into a single valid email string.
114
115    The given email address and the name are turned into a single
116    (byte stream) string, suitable for use with ``To:`` or ``From:``
117    headers in emails.
118
119    Any encodings to a mailer-readable format are performed.
120
121    Preferred input format is unicode, although also raw strings (byte
122    streams) work as long as they are decodable from UTF-8.
123
124    That means: if you pass in non-unicode string, take care to
125    deliver utf-8 encoded strings (or plain ASCII).
126
127    Returns a single (raw) string like "My Name <my@sample.com>".
128    """
129    addr = encode_header_item(addr)
130    name = encode_header_item(name)
131    return formataddr((name, addr))
132
133
134def encode_body(text):
135    """Build MIME message part from text.
136
137    You can pass unicode objects or simple strings as text.
138
139    .. warn:: If the input is a simple string, this string is expected
140              to be encoded 'utf-8'!
141
142    Returns a MIMEText object.
143    """
144    if not isinstance(text, unicode):
145        text = unicode(text, 'utf-8')
146    charset = CHARSETS[-1]  # fallback
147    for charset in CHARSETS:
148        try:
149            text = text.encode(charset)
150        except UnicodeError:
151            pass  # try next encoding
152        else:
153            break
154    return MIMEText(text, 'plain', charset)
155
156
157def send_mail(from_name, from_addr, rcpt_name, rcpt_addrs,
158              subject, body, config=None):
159    """Send mail.
160    """
161    # format message
162    rcpt_addrs = rcpt_addrs.replace(' ', '').split(',')
163    body_to = ''
164    for email in rcpt_addrs:
165        body_to += '%s, ' % encode_address(email, rcpt_name)
166    body = encode_body(body)
167    body["From"] = body["Cc"] = encode_address(from_addr, from_name)
168    body["To"] = body_to.strip(', ')
169    body["Subject"] = encode_header_item(subject)
170
171    mailer = getUtility(IMailService)
172    try:
173        email_admin = grok.getSite()['configuration'].email_admin
174        if from_addr != email_admin:
175            rcpt_addrs += [from_addr]
176    except TypeError:
177        # In tests we might not have a site object
178        rcpt_addrs += [from_addr]
179        pass
180    result = mailer().send(from_addr, rcpt_addrs, body.as_string())
181    return result
Note: See TracBrowser for help on using the repository browser.