Commit 57785248 authored by Alec Mitchell's avatar Alec Mitchell

Modifications to MailHost send method to fix bugs, specify charset encodings, and permit unicode.

parent 7fbe3b4f
...@@ -37,6 +37,11 @@ Restructuring ...@@ -37,6 +37,11 @@ Restructuring
Features Added 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: - Updated packages:
- zope.app.appsetup = 3.12.0 - zope.app.appsetup = 3.12.0
...@@ -56,6 +61,12 @@ Features Added ...@@ -56,6 +61,12 @@ Features Added
Bugs Fixed 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. - 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`` - Unfutzed test failures due to use of naive timezones with ``datetime``
......
...@@ -14,10 +14,25 @@ ...@@ -14,10 +14,25 @@
$Id$ $Id$
""" """
from cStringIO import StringIO
import logging import logging
import mimetools import re
import rfc822 from cStringIO import StringIO
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 from threading import Lock
import time import time
...@@ -49,6 +64,12 @@ queue_threads = {} # maps MailHost path -> queue processor threada ...@@ -49,6 +64,12 @@ queue_threads = {} # maps MailHost path -> queue processor threada
LOG = logging.getLogger('MailHost') 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): class MailHostError(Exception):
pass pass
...@@ -91,7 +112,6 @@ class MailBase(Implicit, Item, RoleManager): ...@@ -91,7 +112,6 @@ class MailBase(Implicit, Item, RoleManager):
lock = Lock() lock = Lock()
# timeout = 1.0 # unused? # timeout = 1.0 # unused?
manage_options = ( manage_options = (
( (
...@@ -185,18 +205,19 @@ class MailBase(Implicit, Item, RoleManager): ...@@ -185,18 +205,19 @@ class MailBase(Implicit, Item, RoleManager):
encode=None, encode=None,
REQUEST=None, REQUEST=None,
immediate=False, immediate=False,
charset=None,
msg_type=None,
): ):
"""Render a mail template, then send it... """Render a mail template, then send it...
""" """
mtemplate = getattr(self, messageTemplate) mtemplate = getattr(self, messageTemplate)
messageText = mtemplate(self, trueself.REQUEST) messageText = mtemplate(self, trueself.REQUEST)
messageText, mto, mfrom = _mungeHeaders( messageText, mto, mfrom) trueself.send(messageText, mto=mto, mfrom=mfrom,
messageText = _encode(messageText, encode) encode=encode, immediate=immediate,
trueself._send(mfrom, mto, messageText, immediate) charset=charset, msg_type=msg_type)
if not statusTemplate: if not statusTemplate:
return "SEND OK" return "SEND OK"
try: try:
stemplate = getattr(self, statusTemplate) stemplate = getattr(self, statusTemplate)
return stemplate(self, trueself.REQUEST) return stemplate(self, trueself.REQUEST)
...@@ -211,10 +232,15 @@ class MailBase(Implicit, Item, RoleManager): ...@@ -211,10 +232,15 @@ class MailBase(Implicit, Item, RoleManager):
subject=None, subject=None,
encode=None, encode=None,
immediate=False, immediate=False,
charset=None,
msg_type=None,
): ):
messageText, mto, mfrom = _mungeHeaders(messageText, mto, mfrom,
messageText, mto, mfrom = _mungeHeaders(messageText, subject, charset, msg_type)
mto, mfrom, subject) # 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) messageText = _encode(messageText, encode)
self._send(mfrom, mto, messageText, immediate) self._send(mfrom, mto, messageText, immediate)
...@@ -327,68 +353,147 @@ InitializeClass(MailBase) ...@@ -327,68 +353,147 @@ InitializeClass(MailBase)
class MailHost(Persistent, MailBase): class MailHost(Persistent, MailBase):
"""persistent version""" """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): def _encode(body, encode=None):
"""Manually sets an encoding and encodes the message if not
already encoded."""
if encode is None: if encode is None:
return body return body
mfile = StringIO(body) mo = message_from_string(body)
mo = mimetools.Message(mfile) current_coding = mo['Content-Transfer-Encoding']
if mo.getencoding() != '7bit': 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' raise MailHostError, 'Message already encoded'
newmfile = StringIO() if encode in ENCODERS:
newmfile.write(''.join(mo.headers)) ENCODERS[encode](mo)
newmfile.write('Content-Transfer-Encoding: %s\n' % encode) if not mo['Content-Transfer-Encoding']:
if not mo.has_key('Mime-Version'): mo['Content-Transfer-Encoding'] = encode
newmfile.write('Mime-Version: 1.0\n') if not mo['Mime-Version']:
newmfile.write('\n') mo['Mime-Version'] = '1.0'
mimetools.encode(mfile, newmfile, encode) return mo.as_string()
return newmfile.getvalue()
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. """Sets missing message headers, and deletes Bcc.
returns fixed message, fixed mto and fixed mfrom""" returns fixed message, fixed mto and fixed mfrom"""
mfile = StringIO(messageText.lstrip()) # If we have been given unicode fields, attempt to encode them
mo = rfc822.Message(mfile) 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. # Parameters given will *always* override headers in the messageText.
# This is so that you can't override or add to subscribers by adding # This is so that you can't override or add to subscribers by adding
# them to # the message text. # them to # the message text.
if subject: if subject:
mo['Subject'] = subject # remove any existing header otherwise we get two
elif not mo.getheader('Subject'): del mo['Subject']
mo['Subject'] = Header(subject, charset)
elif not mo.get('Subject'):
mo['Subject'] = '[No Subject]' mo['Subject'] = '[No Subject]'
if mto: if mto:
if isinstance(mto, basestring): if isinstance(mto, basestring):
mto = [rfc822.dump_address_pair(addr) mto = [formataddr(addr) for addr in _AddressList(mto).addresslist]
for addr in rfc822.AddressList(mto) ] if not mo.get('To'):
if not mo.getheader('To'): mo['To'] = ', '.join(str(_encode_address_string(e, charset))
mo['To'] = ','.join(mto) for e in mto)
else: else:
# If we don't have recipients, extract them from the message
mto = [] mto = []
for header in ('To', 'Cc', 'Bcc'): for header in ('To', 'Cc', 'Bcc'):
v = mo.getheader(header) v = ','.join(mo.get_all(header) or [])
if v: if v:
mto += [rfc822.dump_address_pair(addr) mto += [formataddr(addr) for addr in
for addr in rfc822.AddressList(v)] _AddressList(v).addresslist]
if not mto: if not mto:
raise MailHostError, "No message recipients designated" raise MailHostError, "No message recipients designated"
if mfrom: 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: else:
if mo.getheader('From') is None: if mo.get('From') is None:
raise MailHostError,"Message missing SMTP Header 'From'" raise MailHostError,"Message missing SMTP Header 'From'"
mfrom = mo['From'] mfrom = mo['From']
if mo.getheader('Bcc'): if mo.get('Bcc'):
mo.__delitem__('Bcc') del mo['Bcc']
if not mo.getheader('Date'): if not mo.get('Date'):
mo['Date'] = DateTime().rfc822() mo['Date'] = DateTime().rfc822()
mo.rewindbody() return mo.as_string(), mto, mfrom
finalmessage = mo
finalmessage = mo.__str__() + '\n' + mfile.read() def _try_encode(text, charset):
mfile.close() """Attempt to encode using the default charset if none is
return finalmessage, mto, mfrom 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
...@@ -4,8 +4,14 @@ MailHost ...@@ -4,8 +4,14 @@ MailHost
The MailHost product provides support for sending email from The MailHost product provides support for sending email from
within the Zope environment using MailHost objects. 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 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 MailHost provides integration with the Zope transaction system and optional
support for asynchronous mail delivery. Asynchronous mail delivery is support for asynchronous mail delivery. Asynchronous mail delivery is
......
...@@ -20,6 +20,7 @@ from zope.interface import Interface ...@@ -20,6 +20,7 @@ from zope.interface import Interface
class IMailHost(Interface): 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. """Send mail.
""" """
...@@ -16,6 +16,7 @@ $Id$ ...@@ -16,6 +16,7 @@ $Id$
""" """
import unittest import unittest
from email import message_from_string
from Products.MailHost.MailHost import MailHost from Products.MailHost.MailHost import MailHost
from Products.MailHost.MailHost import MailHostError, _mungeHeaders from Products.MailHost.MailHost import MailHostError, _mungeHeaders
...@@ -30,6 +31,15 @@ class DummyMailHost(MailHost): ...@@ -30,6 +31,15 @@ class DummyMailHost(MailHost):
self.sent = messageText self.sent = messageText
self.immediate = immediate 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): class TestMailHost(unittest.TestCase):
...@@ -59,14 +69,14 @@ This is the message body.""" ...@@ -59,14 +69,14 @@ This is the message body."""
# Add duplicated info # Add duplicated info
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient@domain.com', resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient@domain.com',
'sender@domain.com', 'This is the subject' ) 'sender@domain.com', 'This is the subject' )
self.failUnless(resto == ['recipient@domain.com']) self.failUnlessEqual(resto, ['recipient@domain.com'])
self.failUnless(resfrom == 'sender@domain.com' ) self.failUnlessEqual(resfrom, 'sender@domain.com' )
# Add extra info # Add extra info
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2@domain.com', resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2@domain.com',
'sender2@domain.com', 'This is the real subject' ) 'sender2@domain.com', 'This is the real subject' )
self.failUnless(resto == ['recipient2@domain.com']) self.failUnlessEqual(resto, ['recipient2@domain.com'])
self.failUnless(resfrom == 'sender2@domain.com' ) self.failUnlessEqual(resfrom, 'sender2@domain.com' )
def testMissingHeaders( self ): def testMissingHeaders( self ):
msg = """X-Header: Dummy header msg = """X-Header: Dummy header
...@@ -90,15 +100,15 @@ This is the message body.""" ...@@ -90,15 +100,15 @@ This is the message body."""
# Specify all # Specify all
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2@domain.com', resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2@domain.com',
'sender2@domain.com', 'This is the real subject') 'sender2@domain.com', 'This is the real subject')
self.failUnless(resto == ['recipient2@domain.com']) self.failUnlessEqual(resto, ['recipient2@domain.com'])
self.failUnless(resfrom == 'sender2@domain.com' ) self.failUnlessEqual(resfrom,'sender2@domain.com' )
def testBCCHeader( self ): def testBCCHeader( self ):
msg = "From: me@example.com\nBcc: many@example.com\n\nMessage text" msg = "From: me@example.com\nBcc: many@example.com\n\nMessage text"
# Specify only the "Bcc" header. Useful for bulk emails. # Specify only the "Bcc" header. Useful for bulk emails.
resmsg, resto, resfrom = _mungeHeaders(msg) resmsg, resto, resfrom = _mungeHeaders(msg)
self.failUnless(resto == ['many@example.com']) self.failUnlessEqual(resto, ['many@example.com'])
self.failUnless(resfrom == 'me@example.com' ) self.failUnlessEqual(resfrom, 'me@example.com' )
def testAddressParser( self ): def testAddressParser( self ):
...@@ -112,18 +122,18 @@ This is the message body.""" ...@@ -112,18 +122,18 @@ This is the message body."""
# Test Address-Parser for To & CC given in messageText # Test Address-Parser for To & CC given in messageText
resmsg, resto, resfrom = _mungeHeaders( msg ) resmsg, resto, resfrom = _mungeHeaders( msg )
self.failUnless(resto == ['"Name, Nick" <recipient@domain.com>', self.failUnlessEqual(resto, ['"Name, Nick" <recipient@domain.com>',
'"Foo Bar" <foo@domain.com>', 'Foo Bar <foo@domain.com>',
'"Web, Jack" <jack@web.com>']) '"Web, Jack" <jack@web.com>'])
self.failUnless(resfrom == 'sender@domain.com' ) self.failUnlessEqual(resfrom, 'sender@domain.com')
# Test Address-Parser for a given mto-string # Test Address-Parser for a given mto-string
resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <pjoe@domain.com>, "Foo Bar" <foo@domain.com>') resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <pjoe@domain.com>, Foo Bar <foo@domain.com>')
self.failUnless(resto == ['"Public, Joe" <pjoe@domain.com>', self.failUnlessEqual(resto, ['"Public, Joe" <pjoe@domain.com>',
'"Foo Bar" <foo@domain.com>']) 'Foo Bar <foo@domain.com>'])
self.failUnless(resfrom == 'sender@domain.com' ) self.failUnlessEqual(resfrom, 'sender@domain.com')
def testSendMessageOnly(self): def testSendMessageOnly(self):
msg = """\ msg = """\
...@@ -147,7 +157,7 @@ This is the message body.""" ...@@ -147,7 +157,7 @@ This is the message body."""
outmsg = """\ outmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200 Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject Subject: This is the subject
To: "Name, Nick" <recipient@domain.com>,"Foo Bar" <foo@domain.com> To: "Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>
From: sender@domain.com From: sender@domain.com
This is the message body.""" This is the message body."""
...@@ -167,7 +177,7 @@ This is the message body.""" ...@@ -167,7 +177,7 @@ This is the message body."""
outmsg = """\ outmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200 Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject Subject: This is the subject
To: "Name, Nick" <recipient@domain.com>,"Foo Bar" <foo@domain.com> To: "Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>
From: sender@domain.com From: sender@domain.com
This is the message body.""" This is the message body."""
...@@ -208,6 +218,306 @@ This is the message body.""" ...@@ -208,6 +218,306 @@ This is the message body."""
self.assertEqual(mailhost.sent, outmsg) self.assertEqual(mailhost.sent, outmsg)
self.assertEqual(mailhost.immediate, True) 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@domain.com>, "Foo Bar" <foo@domain.com>',
mfrom='sender@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@domain.com>, Foo Bar <foo@domain.com>')
self.failUnlessEqual(out['From'], 'sender@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@domain.com>, "Foo Bar" <foo@domain.com>',
mfrom='sender@domain.com', subject='This is the subject', charset='utf-8')
out = message_from_string(mailhost.sent)
self.failUnlessEqual(out['To'],
'"Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>')
self.failUnlessEqual(out['From'], 'sender@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@example.com>"
mto = "Ferran Adri\xc3\xa0 <ferran@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@example.com>')
self.failUnlessEqual(out['From'],
'=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@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@example.com>
To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran@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@example.com>
To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran@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@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@example.com>', 'utf-8')
subject = unicode('\xc2\xa1Andr\xc3\xa9s!', 'utf-8')
mailhost = self._makeOne('MailHost')
mailhost.send(messageText=msg,
mto='"Name, Nick" <recipient@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@domain.com>')
self.failUnlessEqual(out['From'],
'=?utf-8?q?Ferran_Adri=C3=A0?= <ferran@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@domain.com>',
mfrom='Foo Bar <foo@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@domain.com>',
mfrom=u'Foo Bar <foo@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@domain.com>
From: Foo Bar <foo@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@example.com>
To: =?utf-8?q?Ferran_Adri=C3=A0?= <ferran@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@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@domain.com>')
self.failUnlessEqual(msg['From'],
'=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <jose@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@domain.com',
mto='Foo Bar <foo@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@domain.com>
From: sender@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@domain.com',
mto='Foo Bar <foo@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@domain.com>
From: sender@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@domain.com',
mto='Foo Bar <foo@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@domain.com>
From: sender@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@domain.com',
mto='Foo Bar <foo@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@domain.com>
From: sender@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@domain.com>',
mfrom='sender@domain.com')
self.failUnlessEqual(result, 'SEND OK')
result = mailhost.sendTemplate(content, 'my_template',
mto='Foo Bar <foo@domain.com>',
mfrom='sender@domain.com',
statusTemplate='wrong_name')
self.failUnlessEqual(result, 'SEND OK')
result = mailhost.sendTemplate(content, 'my_template',
mto='Foo Bar <foo@domain.com>',
mfrom='sender@domain.com',
statusTemplate='check_status')
self.failUnlessEqual(result, 'Message Sent')
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment