[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