source: main/waeup.sirp/trunk/src/waeup/sirp/smtp.py @ 7470

Last change on this file since 7470 was 7470, checked in by uli, 13 years ago

First sketches of smart SMTP handling for our portal deploying zope.sendmail. Requires new buildout run. Also check new mail.zcml.

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