[Zope-Checkins] SVN: Zope/trunk/ Modifications to MailHost send method to fix bugs, specify charset encodings, and permit unicode.
Alec Mitchell
alecpm at gmail.com
Thu Aug 13 23:55:32 EDT 2009
Log message for revision 102757:
Modifications to MailHost send method to fix bugs, specify charset encodings, and permit unicode.
Changed:
U Zope/trunk/doc/CHANGES.rst
U Zope/trunk/src/Products/MailHost/MailHost.py
U Zope/trunk/src/Products/MailHost/README.txt
U Zope/trunk/src/Products/MailHost/interfaces.py
U Zope/trunk/src/Products/MailHost/tests/testMailHost.py
-=-
Modified: Zope/trunk/doc/CHANGES.rst
===================================================================
--- Zope/trunk/doc/CHANGES.rst 2009-08-13 22:31:27 UTC (rev 102756)
+++ Zope/trunk/doc/CHANGES.rst 2009-08-14 03:55:27 UTC (rev 102757)
@@ -37,6 +37,11 @@
Features Added
++++++++++++++
+- The send method of MailHost now supports unicode messages and
+ email.Message.Message objects. It also now accepts charset and
+ msg_type parameters to help with character, header and body
+ encoding.
+
- Updated packages:
- zope.app.appsetup = 3.12.0
@@ -56,6 +61,12 @@
Bugs Fixed
++++++++++
+- Fixed issue with sending text containing ':' from MailHost.
+
+- MailHost will now ensure the headers it sets are 7bit.
+
+- MailHost no longer generates garbage when given unicode input.
+
- Made C extensions work for 64-bit Python 2.5.x / 2.6.x.
- Unfutzed test failures due to use of naive timezones with ``datetime``
Modified: Zope/trunk/src/Products/MailHost/MailHost.py
===================================================================
--- Zope/trunk/src/Products/MailHost/MailHost.py 2009-08-13 22:31:27 UTC (rev 102756)
+++ Zope/trunk/src/Products/MailHost/MailHost.py 2009-08-14 03:55:27 UTC (rev 102757)
@@ -14,10 +14,25 @@
$Id$
"""
+import logging
+import re
from cStringIO import StringIO
-import logging
-import mimetools
-import rfc822
+from copy import deepcopy
+from email.Header import Header
+from email.Charset import Charset
+from email import message_from_string
+from email.Message import Message
+from email import Encoders
+try:
+ import email.utils as emailutils
+except ImportError:
+ import email.Utils as emailutils
+import email.Charset
+# We import from a private module here because the email module
+# doesn't provide a good public address list parser
+from email._parseaddr import AddressList as _AddressList
+import uu
+
from threading import Lock
import time
@@ -49,6 +64,12 @@
LOG = logging.getLogger('MailHost')
+# Encode utf-8 emails as Quoted Printable by default
+email.Charset.add_charset("utf-8", email.Charset.QP, email.Charset.QP, "utf-8")
+formataddr = emailutils.formataddr
+parseaddr = emailutils.parseaddr
+CHARSET_RE = re.compile('charset=[\'"]?([\w-]+)[\'"]?', re.IGNORECASE)
+
class MailHostError(Exception):
pass
@@ -91,7 +112,6 @@
lock = Lock()
# timeout = 1.0 # unused?
-
manage_options = (
(
@@ -185,18 +205,19 @@
encode=None,
REQUEST=None,
immediate=False,
+ charset=None,
+ msg_type=None,
):
"""Render a mail template, then send it...
"""
mtemplate = getattr(self, messageTemplate)
messageText = mtemplate(self, trueself.REQUEST)
- messageText, mto, mfrom = _mungeHeaders( messageText, mto, mfrom)
- messageText = _encode(messageText, encode)
- trueself._send(mfrom, mto, messageText, immediate)
+ trueself.send(messageText, mto=mto, mfrom=mfrom,
+ encode=encode, immediate=immediate,
+ charset=charset, msg_type=msg_type)
- if not statusTemplate:
+ if not statusTemplate:
return "SEND OK"
-
try:
stemplate = getattr(self, statusTemplate)
return stemplate(self, trueself.REQUEST)
@@ -211,10 +232,15 @@
subject=None,
encode=None,
immediate=False,
+ charset=None,
+ msg_type=None,
):
-
- messageText, mto, mfrom = _mungeHeaders(messageText,
- mto, mfrom, subject)
+ messageText, mto, mfrom = _mungeHeaders(messageText, mto, mfrom,
+ subject, charset, msg_type)
+ # This encode step is mainly for BBB, encoding should be
+ # automatic if charset is passed. The automated charset-based
+ # encoding will be preferred if both encode and charset are
+ # provided.
messageText = _encode(messageText, encode)
self._send(mfrom, mto, messageText, immediate)
@@ -327,68 +353,147 @@
class MailHost(Persistent, MailBase):
"""persistent version"""
+def uu_encoder(msg):
+ """For BBB only, don't send uuencoded emails"""
+ orig = StringIO(msg.get_payload())
+ encdata = StringIO()
+ uu.encode(orig, encdata)
+ msg.set_payload(encdata.getvalue())
+# All encodings supported by mimetools for BBB
+ENCODERS = {
+ 'base64': Encoders.encode_base64,
+ 'quoted-printable': Encoders.encode_quopri,
+ '7bit': Encoders.encode_7or8bit,
+ '8bit': Encoders.encode_7or8bit,
+ 'x-uuencode': uu_encoder,
+ 'uuencode': uu_encoder,
+ 'x-uue': uu_encoder,
+ 'uue': uu_encoder,
+ }
+
def _encode(body, encode=None):
+ """Manually sets an encoding and encodes the message if not
+ already encoded."""
if encode is None:
return body
- mfile = StringIO(body)
- mo = mimetools.Message(mfile)
- if mo.getencoding() != '7bit':
+ mo = message_from_string(body)
+ current_coding = mo['Content-Transfer-Encoding']
+ if current_coding == encode:
+ # already encoded correctly, may have been automated
+ return body
+ if mo['Content-Transfer-Encoding'] not in ['7bit', None]:
raise MailHostError, 'Message already encoded'
- newmfile = StringIO()
- newmfile.write(''.join(mo.headers))
- newmfile.write('Content-Transfer-Encoding: %s\n' % encode)
- if not mo.has_key('Mime-Version'):
- newmfile.write('Mime-Version: 1.0\n')
- newmfile.write('\n')
- mimetools.encode(mfile, newmfile, encode)
- return newmfile.getvalue()
+ if encode in ENCODERS:
+ ENCODERS[encode](mo)
+ if not mo['Content-Transfer-Encoding']:
+ mo['Content-Transfer-Encoding'] = encode
+ if not mo['Mime-Version']:
+ mo['Mime-Version'] = '1.0'
+ return mo.as_string()
-def _mungeHeaders( messageText, mto=None, mfrom=None, subject=None):
+def _mungeHeaders(messageText, mto=None, mfrom=None, subject=None,
+ charset=None, msg_type=None):
"""Sets missing message headers, and deletes Bcc.
returns fixed message, fixed mto and fixed mfrom"""
- mfile = StringIO(messageText.lstrip())
- mo = rfc822.Message(mfile)
+ # If we have been given unicode fields, attempt to encode them
+ if isinstance(messageText, unicode):
+ messageText = _try_encode(messageText, charset)
+ if isinstance(mto, unicode):
+ mto = _try_encode(mto, charset)
+ if isinstance(mfrom, unicode):
+ mfrom = _try_encode(mfrom, charset)
+ if isinstance(subject, unicode):
+ subject = _try_encode(subject, charset)
+ if isinstance(messageText, Message):
+ # We already have a message, make a copy to operate on
+ mo = deepcopy(messageText)
+ else:
+ # Otherwise parse the input message
+ mo = message_from_string(messageText)
+
+ if msg_type and not mo.get('Content-Type'):
+ # we don't use get_content_type because that has a default
+ # value of 'text/plain'
+ mo.set_type(msg_type)
+ charset_match = CHARSET_RE.search(mo['Content-Type'] or '')
+ if charset and not charset_match:
+ # Don't change the charset if already set
+ # This encodes the payload automatically based on the default
+ # encoding for the charset
+ mo.set_charset(charset)
+ elif charset_match and not charset:
+ # If a charset parameter was provided use it for header encoding below,
+ # Otherwise, try to use the charset provided in the message.
+ charset = charset_match.groups()[0]
+
# Parameters given will *always* override headers in the messageText.
# This is so that you can't override or add to subscribers by adding
# them to # the message text.
if subject:
- mo['Subject'] = subject
- elif not mo.getheader('Subject'):
+ # remove any existing header otherwise we get two
+ del mo['Subject']
+ mo['Subject'] = Header(subject, charset)
+ elif not mo.get('Subject'):
mo['Subject'] = '[No Subject]'
if mto:
if isinstance(mto, basestring):
- mto = [rfc822.dump_address_pair(addr)
- for addr in rfc822.AddressList(mto) ]
- if not mo.getheader('To'):
- mo['To'] = ','.join(mto)
+ mto = [formataddr(addr) for addr in _AddressList(mto).addresslist]
+ if not mo.get('To'):
+ mo['To'] = ', '.join(str(_encode_address_string(e, charset))
+ for e in mto)
else:
+ # If we don't have recipients, extract them from the message
mto = []
for header in ('To', 'Cc', 'Bcc'):
- v = mo.getheader(header)
+ v = ','.join(mo.get_all(header) or [])
if v:
- mto += [rfc822.dump_address_pair(addr)
- for addr in rfc822.AddressList(v)]
+ mto += [formataddr(addr) for addr in
+ _AddressList(v).addresslist]
if not mto:
raise MailHostError, "No message recipients designated"
if mfrom:
- mo['From'] = mfrom
+ # XXX: do we really want to override an explicitly set From
+ # header in the messageText
+ del mo['From']
+ mo['From'] = _encode_address_string(mfrom, charset)
else:
- if mo.getheader('From') is None:
+ if mo.get('From') is None:
raise MailHostError,"Message missing SMTP Header 'From'"
mfrom = mo['From']
- if mo.getheader('Bcc'):
- mo.__delitem__('Bcc')
+ if mo.get('Bcc'):
+ del mo['Bcc']
- if not mo.getheader('Date'):
+ if not mo.get('Date'):
mo['Date'] = DateTime().rfc822()
- mo.rewindbody()
- finalmessage = mo
- finalmessage = mo.__str__() + '\n' + mfile.read()
- mfile.close()
- return finalmessage, mto, mfrom
+ return mo.as_string(), mto, mfrom
+
+def _try_encode(text, charset):
+ """Attempt to encode using the default charset if none is
+ provided. Should we permit encoding errors?"""
+ if charset:
+ return text.encode(charset)
+ else:
+ return text.encode()
+
+def _encode_address_string(text, charset):
+ """Split the email into parts and use header encoding on the name
+ part if needed. We do this because the actual addresses need to be
+ ASCII with no encoding for most SMTP servers, but the non-address
+ parts should be encoded appropriately."""
+ header = Header()
+ name, addr = parseaddr(text)
+ try:
+ name.decode('us-ascii')
+ except UnicodeDecodeError:
+ # Encoded strings need an extra space
+ # XXX: should we be this tolerant of encoding errors here?
+ charset = Charset(charset)
+ name = charset.header_encode(name)
+ header.append(formataddr((name, addr)))
+ return header
Modified: Zope/trunk/src/Products/MailHost/README.txt
===================================================================
--- Zope/trunk/src/Products/MailHost/README.txt 2009-08-13 22:31:27 UTC (rev 102756)
+++ Zope/trunk/src/Products/MailHost/README.txt 2009-08-14 03:55:27 UTC (rev 102757)
@@ -4,8 +4,14 @@
The MailHost product provides support for sending email from
within the Zope environment using MailHost objects.
+ An optional character set can be specified to automatically encode unicode
+ input, and perform appropriate RFC 2822 header and body encoding for
+ the specified character set. Full python email.Message.Message objects
+ may be sent.
+
Email can optionally be encoded using Base64, Quoted-Printable
- or UUEncode encoding.
+ or UUEncode encoding (though automatic body encoding will be applied if a
+ character set is specified).
MailHost provides integration with the Zope transaction system and optional
support for asynchronous mail delivery. Asynchronous mail delivery is
Modified: Zope/trunk/src/Products/MailHost/interfaces.py
===================================================================
--- Zope/trunk/src/Products/MailHost/interfaces.py 2009-08-13 22:31:27 UTC (rev 102756)
+++ Zope/trunk/src/Products/MailHost/interfaces.py 2009-08-14 03:55:27 UTC (rev 102757)
@@ -20,6 +20,7 @@
class IMailHost(Interface):
- def send(messageText, mto=None, mfrom=None, subject=None, encode=None):
+ def send(messageText, mto=None, mfrom=None, subject=None, encode=None,
+ charset=None, msg_type=None):
"""Send mail.
"""
Modified: Zope/trunk/src/Products/MailHost/tests/testMailHost.py
===================================================================
--- Zope/trunk/src/Products/MailHost/tests/testMailHost.py 2009-08-13 22:31:27 UTC (rev 102756)
+++ Zope/trunk/src/Products/MailHost/tests/testMailHost.py 2009-08-14 03:55:27 UTC (rev 102757)
@@ -16,6 +16,7 @@
"""
import unittest
+from email import message_from_string
from Products.MailHost.MailHost import MailHost
from Products.MailHost.MailHost import MailHostError, _mungeHeaders
@@ -30,7 +31,16 @@
self.sent = messageText
self.immediate = immediate
+class FakeContent(object):
+ def __init__(self, template_name, message):
+ def template(self, context, REQUEST=None):
+ return message
+ setattr(self, template_name, template)
+ @staticmethod
+ def check_status(context, REQUEST=None):
+ return 'Message Sent'
+
class TestMailHost(unittest.TestCase):
def _getTargetClass(self):
@@ -59,14 +69,14 @@
# Add duplicated info
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient at domain.com',
'sender at domain.com', 'This is the subject' )
- self.failUnless(resto == ['recipient at domain.com'])
- self.failUnless(resfrom == 'sender at domain.com' )
+ self.failUnlessEqual(resto, ['recipient at domain.com'])
+ self.failUnlessEqual(resfrom, 'sender at domain.com' )
# Add extra info
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2 at domain.com',
'sender2 at domain.com', 'This is the real subject' )
- self.failUnless(resto == ['recipient2 at domain.com'])
- self.failUnless(resfrom == 'sender2 at domain.com' )
+ self.failUnlessEqual(resto, ['recipient2 at domain.com'])
+ self.failUnlessEqual(resfrom, 'sender2 at domain.com' )
def testMissingHeaders( self ):
msg = """X-Header: Dummy header
@@ -90,15 +100,15 @@
# Specify all
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2 at domain.com',
'sender2 at domain.com', 'This is the real subject')
- self.failUnless(resto == ['recipient2 at domain.com'])
- self.failUnless(resfrom == 'sender2 at domain.com' )
+ self.failUnlessEqual(resto, ['recipient2 at domain.com'])
+ self.failUnlessEqual(resfrom,'sender2 at domain.com' )
def testBCCHeader( self ):
msg = "From: me at example.com\nBcc: many at example.com\n\nMessage text"
# Specify only the "Bcc" header. Useful for bulk emails.
resmsg, resto, resfrom = _mungeHeaders(msg)
- self.failUnless(resto == ['many at example.com'])
- self.failUnless(resfrom == 'me at example.com' )
+ self.failUnlessEqual(resto, ['many at example.com'])
+ self.failUnlessEqual(resfrom, 'me at example.com' )
def testAddressParser( self ):
@@ -112,18 +122,18 @@
# Test Address-Parser for To & CC given in messageText
resmsg, resto, resfrom = _mungeHeaders( msg )
- self.failUnless(resto == ['"Name, Nick" <recipient at domain.com>',
- '"Foo Bar" <foo at domain.com>',
+ self.failUnlessEqual(resto, ['"Name, Nick" <recipient at domain.com>',
+ 'Foo Bar <foo at domain.com>',
'"Web, Jack" <jack at web.com>'])
- self.failUnless(resfrom == 'sender at domain.com' )
+ self.failUnlessEqual(resfrom, 'sender at domain.com')
# Test Address-Parser for a given mto-string
- resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <pjoe at domain.com>, "Foo Bar" <foo at domain.com>')
+ resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <pjoe at domain.com>, Foo Bar <foo at domain.com>')
- self.failUnless(resto == ['"Public, Joe" <pjoe at domain.com>',
- '"Foo Bar" <foo at domain.com>'])
- self.failUnless(resfrom == 'sender at domain.com' )
+ self.failUnlessEqual(resto, ['"Public, Joe" <pjoe at domain.com>',
+ 'Foo Bar <foo at domain.com>'])
+ self.failUnlessEqual(resfrom, 'sender at domain.com')
def testSendMessageOnly(self):
msg = """\
@@ -147,7 +157,7 @@
outmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject
-To: "Name, Nick" <recipient at domain.com>,"Foo Bar" <foo at domain.com>
+To: "Name, Nick" <recipient at domain.com>, Foo Bar <foo at domain.com>
From: sender at domain.com
This is the message body."""
@@ -167,7 +177,7 @@
outmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject
-To: "Name, Nick" <recipient at domain.com>,"Foo Bar" <foo at domain.com>
+To: "Name, Nick" <recipient at domain.com>, Foo Bar <foo at domain.com>
From: sender at domain.com
This is the message body."""
@@ -208,7 +218,307 @@
self.assertEqual(mailhost.sent, outmsg)
self.assertEqual(mailhost.immediate, True)
+ def testSendBodyWithUrl(self):
+ # The implementation of rfc822.Message reacts poorly to
+ # message bodies containing ':' characters as in a url
+ msg = "Here's a nice link: http://www.zope.org/"
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(messageText=msg,
+ mto='"Name, Nick" <recipient at domain.com>, "Foo Bar" <foo at domain.com>',
+ mfrom='sender at domain.com', subject='This is the subject')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(out.get_payload(), msg)
+ self.failUnlessEqual(out['To'],
+ '"Name, Nick" <recipient at domain.com>, Foo Bar <foo at domain.com>')
+ self.failUnlessEqual(out['From'], 'sender at domain.com')
+
+ def testSendEncodedBody(self):
+ # If a charset is specified the correct headers for content
+ # encoding will be set if not already set. Additionally, if
+ # there is a default transfer encoding for the charset, then
+ # the content will be encoded and the transfer encoding header
+ # will be set.
+ msg = "Here's some encoded t\xc3\xa9xt."
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(messageText=msg,
+ mto='"Name, Nick" <recipient at domain.com>, "Foo Bar" <foo at domain.com>',
+ mfrom='sender at domain.com', subject='This is the subject', charset='utf-8')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(out['To'],
+ '"Name, Nick" <recipient at domain.com>, Foo Bar <foo at domain.com>')
+ self.failUnlessEqual(out['From'], 'sender at domain.com')
+ # utf-8 will default to Quoted Printable encoding
+ self.failUnlessEqual(out['Content-Transfer-Encoding'],
+ 'quoted-printable')
+ self.failUnlessEqual(out['Content-Type'], 'text/plain; charset="utf-8"')
+ self.failUnlessEqual(out.get_payload(),
+ "Here's some encoded t=C3=A9xt.")
+
+ def testEncodedHeaders(self):
+ # Headers are encoded automatically, email headers are encoded
+ # piece-wise to ensure the adresses remain ASCII
+ mfrom = "Jos\xc3\xa9 Andr\xc3\xa9s <jose at example.com>"
+ mto = "Ferran Adri\xc3\xa0 <ferran at example.com>"
+ subject = "\xc2\xbfEsferificaci\xc3\xb3n?"
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(messageText='A message.', mto=mto, mfrom=mfrom,
+ subject=subject, charset='utf-8')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(out['To'],
+ '=?utf-8?q?Ferran_Adri=C3=A0?= <ferran at example.com>')
+ self.failUnlessEqual(out['From'],
+ '=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose at example.com>')
+ self.failUnlessEqual(out['Subject'],
+ '=?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=')
+ # utf-8 will default to Quoted Printable encoding
+ self.failUnlessEqual(out['Content-Transfer-Encoding'],
+ 'quoted-printable')
+ self.failUnlessEqual(out['Content-Type'], 'text/plain; charset="utf-8"')
+ self.failUnlessEqual(out.get_payload(), "A message.")
+
+ def testAlreadyEncodedMessage(self):
+ # If the message already specifies encodings, it is
+ # essentially not altered this is true even if charset or
+ # msg_type is specified
+ msg = """\
+From: =?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose at example.com>
+To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran at example.com>
+Subject: =?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+MIME-Version: 1.0 (Generated by testMailHost.py)
+
+wqFVbiB0cnVjbyA8c3Ryb25nPmZhbnTDoXN0aWNvPC9zdHJvbmc+IQ=3D=3D
+"""
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(messageText=msg)
+ self.failUnlessEqual(mailhost.sent, msg)
+ mailhost.send(messageText=msg, msg_type='text/plain')
+ # The msg_type is ignored if already set
+ self.failUnlessEqual(mailhost.sent, msg)
+
+ def testAlreadyEncodedMessageWithCharset(self):
+ # If the message already specifies encodings, it is
+ # essentially not altered this is true even if charset or
+ # msg_type is specified
+ msg = """\
+From: =?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose at example.com>
+To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran at example.com>
+Subject: =?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+MIME-Version: 1.0 (Generated by testMailHost.py)
+
+wqFVbiB0cnVjbyA8c3Ryb25nPmZhbnTDoXN0aWNvPC9zdHJvbmc+IQ=3D=3D
+"""
+ mailhost = self._makeOne('MailHost')
+ # Pass a different charset, which will apply to any explicitly
+ # set headers
+ mailhost.send(messageText=msg,
+ subject='\xbfEsferificaci\xf3n?',
+ charset='iso-8859-1', msg_type='text/plain')
+ # The charset for the body should remain the same, but any
+ # headers passed into the method will be encoded using the
+ # specified charset
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(out['Content-Type'], 'text/html; charset="utf-8"')
+ self.failUnlessEqual(out['Content-Transfer-Encoding'],
+ 'base64')
+ # Headers set by parameter will be set using charset parameter
+ self.failUnlessEqual(out['Subject'],
+ '=?iso-8859-1?q?=BFEsferificaci=F3n=3F?=')
+ # original headers will be unaltered
+ self.failUnlessEqual(out['From'],
+ '=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose at example.com>')
+
+ def testUnicodeMessage(self):
+ # unicode messages and headers are decoded using the given charset
+ msg = unicode("Here's some unencoded <strong>t\xc3\xa9xt</strong>.",
+ 'utf-8')
+ mfrom = unicode('Ferran Adri\xc3\xa0 <ferran at example.com>', 'utf-8')
+ subject = unicode('\xc2\xa1Andr\xc3\xa9s!', 'utf-8')
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(messageText=msg,
+ mto='"Name, Nick" <recipient at domain.com>',
+ mfrom=mfrom, subject=subject, charset='utf-8',
+ msg_type='text/html')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(out['To'],
+ '"Name, Nick" <recipient at domain.com>')
+ self.failUnlessEqual(out['From'],
+ '=?utf-8?q?Ferran_Adri=C3=A0?= <ferran at example.com>')
+ self.failUnlessEqual(out['Subject'], '=?utf-8?q?=C2=A1Andr=C3=A9s!?=')
+ self.failUnlessEqual(out['Content-Transfer-Encoding'], 'quoted-printable')
+ self.failUnlessEqual(out['Content-Type'], 'text/html; charset="utf-8"')
+ self.failUnlessEqual(out.get_payload(),
+ "Here's some unencoded <strong>t=C3=A9xt</strong>.")
+
+ def testUnicodeNoEncodingErrors(self):
+ # Unicode messages and headers raise errors if no charset is passed to
+ # send
+ msg = unicode("Here's some unencoded <strong>t\xc3\xa9xt</strong>.",
+ 'utf-8')
+ subject = unicode('\xc2\xa1Andr\xc3\xa9s!', 'utf-8')
+ mailhost = self._makeOne('MailHost')
+ self.assertRaises(UnicodeEncodeError,
+ mailhost.send, msg,
+ mto='"Name, Nick" <recipient at domain.com>',
+ mfrom='Foo Bar <foo at domain.com>',
+ subject=subject)
+
+ def testUnicodeDefaultEncoding(self):
+ # However if we pass unicode that can be encoded to the
+ # default encoding (generally 'us-ascii'), no error is raised.
+ # We include a date in the messageText to make inspecting the
+ # results more convenient.
+ msg = u"""\
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+
+Here's some unencoded <strong>text</strong>."""
+ subject = u'Andres!'
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(msg, mto=u'"Name, Nick" <recipient at domain.com>',
+ mfrom=u'Foo Bar <foo at domain.com>', subject=subject)
+ out = mailhost.sent
+ # Ensure the results are not unicode
+ self.failUnlessEqual(out,"""\
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Subject: Andres!
+To: "Name, Nick" <recipient at domain.com>
+From: Foo Bar <foo at domain.com>
+
+Here's some unencoded <strong>text</strong>.""")
+ self.failUnlessEqual(type(out), str)
+
+ def testSendMessageObject(self):
+ # send will accept an email.Message.Message object directly
+ msg = message_from_string("""\
+From: =?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose at example.com>
+To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran at example.com>
+Subject: =?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+MIME-Version: 1.1
+
+wqFVbiB0cnVjbyA8c3Ryb25nPmZhbnTDoXN0aWNvPC9zdHJvbmc+IQ=3D=3D
+""")
+ mailhost = self._makeOne('MailHost')
+ mailhost.send(msg)
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(out.as_string(), msg.as_string())
+
+ # we can even alter a from and subject headers without affecting the
+ # original object
+ mailhost.send(msg, mfrom='Foo Bar <foo at domain.com>', subject='Changed!')
+ out = message_from_string(mailhost.sent)
+
+ # We need to make sure we didn't mutate the message we were passed
+ self.failIfEqual(out.as_string(), msg.as_string())
+ self.failUnlessEqual(out['From'], 'Foo Bar <foo at domain.com>')
+ self.failUnlessEqual(msg['From'],
+ '=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose at example.com>')
+ # The subject is encoded with the body encoding since no
+ # explicit encoding was specified
+ self.failUnlessEqual(out['Subject'], '=?utf-8?q?Changed!?=')
+ self.failUnlessEqual(msg['Subject'],
+ '=?utf-8?q?=C2=BFEsferificaci=C3=B3n=3F?=')
+
+ def testExplicitUUEncoding(self):
+ # We can request a payload encoding explicitly, though this
+ # should probably be considered deprecated functionality.
+ mailhost = self._makeOne('MailHost')
+ # uuencoding
+ mailhost.send('Date: Sun, 27 Aug 2006 17:00:00 +0200\n\nA Message',
+ mfrom='sender at domain.com',
+ mto='Foo Bar <foo at domain.com>', encode='uue')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(mailhost.sent, """\
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Subject: [No Subject]
+To: Foo Bar <foo at domain.com>
+From: sender at domain.com
+Content-Transfer-Encoding: uue
+Mime-Version: 1.0
+
+begin 666 -
+)02!-97-S86=E
+
+end
+""")
+
+ def testExplicitBase64Encoding(self):
+ mailhost = self._makeOne('MailHost')
+ mailhost.send('Date: Sun, 27 Aug 2006 17:00:00 +0200\n\nA Message',
+ mfrom='sender at domain.com',
+ mto='Foo Bar <foo at domain.com>', encode='base64')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(mailhost.sent, """\
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Subject: [No Subject]
+To: Foo Bar <foo at domain.com>
+From: sender at domain.com
+Content-Transfer-Encoding: base64
+Mime-Version: 1.0
+
+QSBNZXNzYWdl""")
+
+ def testExplicit7bitEncoding(self):
+ mailhost = self._makeOne('MailHost')
+ mailhost.send('Date: Sun, 27 Aug 2006 17:00:00 +0200\n\nA Message',
+ mfrom='sender at domain.com',
+ mto='Foo Bar <foo at domain.com>', encode='7bit')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(mailhost.sent, """\
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Subject: [No Subject]
+To: Foo Bar <foo at domain.com>
+From: sender at domain.com
+Content-Transfer-Encoding: 7bit
+Mime-Version: 1.0
+
+A Message""")
+
+ def testExplicit8bitEncoding(self):
+ mailhost = self._makeOne('MailHost')
+ # We pass an encoded string with unspecified charset, it should be
+ # encoded 8bit
+ mailhost.send('Date: Sun, 27 Aug 2006 17:00:00 +0200\n\nA M\xc3\xa9ssage',
+ mfrom='sender at domain.com',
+ mto='Foo Bar <foo at domain.com>', encode='8bit')
+ out = message_from_string(mailhost.sent)
+ self.failUnlessEqual(mailhost.sent, """\
+Date: Sun, 27 Aug 2006 17:00:00 +0200
+Subject: [No Subject]
+To: Foo Bar <foo at domain.com>
+From: sender at domain.com
+Content-Transfer-Encoding: 8bit
+Mime-Version: 1.0
+
+A M\xc3\xa9ssage""")
+
+ def testSendTemplate(self):
+ content = FakeContent('my_template', 'A Message')
+ mailhost = self._makeOne('MailHost')
+ result = mailhost.sendTemplate(content, 'my_template',
+ mto='Foo Bar <foo at domain.com>',
+ mfrom='sender at domain.com')
+ self.failUnlessEqual(result, 'SEND OK')
+ result = mailhost.sendTemplate(content, 'my_template',
+ mto='Foo Bar <foo at domain.com>',
+ mfrom='sender at domain.com',
+ statusTemplate='wrong_name')
+ self.failUnlessEqual(result, 'SEND OK')
+ result = mailhost.sendTemplate(content, 'my_template',
+ mto='Foo Bar <foo at domain.com>',
+ mfrom='sender at domain.com',
+ statusTemplate='check_status')
+ self.failUnlessEqual(result, 'Message Sent')
+
+
def test_suite():
suite = unittest.TestSuite()
suite.addTest( unittest.makeSuite( TestMailHost ) )
More information about the Zope-Checkins
mailing list