source: main/waeup.kofa/branches/0.2/src/waeup/kofa/smtp.py @ 17463

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

Merge current trunk into 0.2 maintenance branch. Now really

  • Property svn:keywords set to Id
File size: 6.1 KB
Line 
1## $Id: smtp.py 11161 2014-02-21 11:07:54Z uli $
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    """Turns `item`, a string into an SMTP header part string.
96
97    Encodings are checked carefully (we try to encode as ASCII,
98    Latin-1 and UTF-8 in that order).
99
100    If `item` is not a basestring, `None` is returned.
101    """
102    if not isinstance(item, basestring):
103        return None
104    if not isinstance(item, unicode):
105        item = unicode(item, 'utf-8')
106    return str(Header(item, 'iso-8859-1')) # try ascii, then latin1, then utf-8
107
108def encode_address(addr, name=u''):
109    """Turn email address parts into a single valid email string.
110
111    The given email address and the name are turned into a single
112    (byte stream) string, suitable for use with ``To:`` or ``From:``
113    headers in emails.
114
115    Any encodings to a mailer-readable format are performed.
116
117    Preferred input format is unicode, although also raw strings (byte
118    streams) work as long as they are decodable from UTF-8.
119
120    That means: if you pass in non-unicode string, take care to
121    deliver utf-8 encoded strings (or plain ASCII).
122
123    Returns a single (raw) string like "My Name <my@sample.com>".
124    """
125    addr = encode_header_item(addr)
126    name = encode_header_item(name)
127    return formataddr((name, addr))
128
129def encode_body(text):
130    """Build MIME message part from text.
131
132    You can pass unicode objects or simple strings as text.
133
134    .. warn:: If the input is a simple string, this string is expected
135              to be encoded 'utf-8'!
136
137    Returns a MIMEText object.
138    """
139    if not isinstance(text, unicode):
140        text = unicode(text, 'utf-8')
141    charset = CHARSETS[-1] # fallback
142    for charset in CHARSETS:
143        try:
144            text = text.encode(charset)
145        except UnicodeError:
146            pass # try next encoding
147        else:
148            break
149    return MIMEText(text, 'plain', charset)
150
151def send_mail(from_name, from_addr, rcpt_name, rcpt_addrs,
152              subject, body, config=None):
153    """Send mail.
154    """
155    # format message
156    rcpt_addrs = rcpt_addrs.replace(' ','').split(',')
157    body_to = ''
158    for email in rcpt_addrs:
159        body_to += '%s, ' % encode_address(email, rcpt_name)
160    body = encode_body(body)
161    body["From"] = body["Cc"] = encode_address(from_addr, from_name)
162    body["To"] = body_to.strip(', ')
163    body["Subject"] = encode_header_item(subject)
164
165    mailer = getUtility(IMailService)
166    try:
167        email_admin = grok.getSite()['configuration'].email_admin
168        if from_addr != email_admin:
169            rcpt_addrs += [from_addr]
170    except TypeError:
171        # In tests we might not have a site object
172        rcpt_addrs += [from_addr]
173        pass
174    result = mailer().send(from_addr, rcpt_addrs, body.as_string())
175    return result
Note: See TracBrowser for help on using the repository browser.