Log message for revision 104152: Merge MailHost changes from trunk (r102756:102819)
Changed: U Zope/branches/2.12/doc/CHANGES.rst U Zope/branches/2.12/src/Products/MailHost/MailHost.py U Zope/branches/2.12/src/Products/MailHost/README.txt U Zope/branches/2.12/src/Products/MailHost/interfaces.py U Zope/branches/2.12/src/Products/MailHost/tests/testMailHost.py -=- Modified: Zope/branches/2.12/doc/CHANGES.rst =================================================================== --- Zope/branches/2.12/doc/CHANGES.rst 2009-09-16 11:51:08 UTC (rev 104151) +++ Zope/branches/2.12/doc/CHANGES.rst 2009-09-16 15:38:14 UTC (rev 104152) @@ -49,6 +49,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: - ZODB3 = 3.9.0b5 Modified: Zope/branches/2.12/src/Products/MailHost/MailHost.py =================================================================== --- Zope/branches/2.12/src/Products/MailHost/MailHost.py 2009-09-16 11:51:08 UTC (rev 104151) +++ Zope/branches/2.12/src/Products/MailHost/MailHost.py 2009-09-16 15:38:14 UTC (rev 104152) @@ -14,10 +14,24 @@ $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 +import uu + from threading import Lock import time @@ -49,6 +63,13 @@ 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 +getaddresses = emailutils.getaddresses +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,148 @@ 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'] + # Perhaps we should ignore errors here and pass 8bit strings + # on encoding errors + mo['Subject'] = Header(subject, charset, errors='replace') + 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 getaddresses((mto,))] + 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 getaddresses((v,))] 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: + if charset: + charset = Charset(charset) + name = charset.header_encode(name) + # We again replace rather than raise an error or pass an 8bit string + header.append(formataddr((name, addr)), errors='replace') + return header Modified: Zope/branches/2.12/src/Products/MailHost/README.txt =================================================================== --- Zope/branches/2.12/src/Products/MailHost/README.txt 2009-09-16 11:51:08 UTC (rev 104151) +++ Zope/branches/2.12/src/Products/MailHost/README.txt 2009-09-16 15:38:14 UTC (rev 104152) @@ -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/branches/2.12/src/Products/MailHost/interfaces.py =================================================================== --- Zope/branches/2.12/src/Products/MailHost/interfaces.py 2009-09-16 11:51:08 UTC (rev 104151) +++ Zope/branches/2.12/src/Products/MailHost/interfaces.py 2009-09-16 15:38:14 UTC (rev 104152) @@ -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/branches/2.12/src/Products/MailHost/tests/testMailHost.py =================================================================== --- Zope/branches/2.12/src/Products/MailHost/tests/testMailHost.py 2009-09-16 11:51:08 UTC (rev 104151) +++ Zope/branches/2.12/src/Products/MailHost/tests/testMailHost.py 2009-09-16 15:38:14 UTC (rev 104152) @@ -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, 'recipi...@domain.com', 'sen...@domain.com', 'This is the subject' ) - self.failUnless(resto == ['recipi...@domain.com']) - self.failUnless(resfrom == 'sen...@domain.com' ) + self.failUnlessEqual(resto, ['recipi...@domain.com']) + self.failUnlessEqual(resfrom, 'sen...@domain.com' ) # Add extra info resmsg, resto, resfrom = _mungeHeaders(msg, 'recipie...@domain.com', 'send...@domain.com', 'This is the real subject' ) - self.failUnless(resto == ['recipie...@domain.com']) - self.failUnless(resfrom == 'send...@domain.com' ) + self.failUnlessEqual(resto, ['recipie...@domain.com']) + self.failUnlessEqual(resfrom, 'send...@domain.com' ) def testMissingHeaders( self ): msg = """X-Header: Dummy header @@ -90,15 +100,15 @@ # Specify all resmsg, resto, resfrom = _mungeHeaders(msg, 'recipie...@domain.com', 'send...@domain.com', 'This is the real subject') - self.failUnless(resto == ['recipie...@domain.com']) - self.failUnless(resfrom == 'send...@domain.com' ) + self.failUnlessEqual(resto, ['recipie...@domain.com']) + self.failUnlessEqual(resfrom,'send...@domain.com' ) def testBCCHeader( self ): msg = "From: m...@example.com\nbcc: m...@example.com\n\nmessage text" # Specify only the "Bcc" header. Useful for bulk emails. resmsg, resto, resfrom = _mungeHeaders(msg) - self.failUnless(resto == ['m...@example.com']) - self.failUnless(resfrom == 'm...@example.com' ) + self.failUnlessEqual(resto, ['m...@example.com']) + self.failUnlessEqual(resfrom, 'm...@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" <recipi...@domain.com>', - '"Foo Bar" <f...@domain.com>', + self.failUnlessEqual(resto, ['"Name, Nick" <recipi...@domain.com>', + 'Foo Bar <f...@domain.com>', '"Web, Jack" <j...@web.com>']) - self.failUnless(resfrom == 'sen...@domain.com' ) + self.failUnlessEqual(resfrom, 'sen...@domain.com') # Test Address-Parser for a given mto-string - resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <p...@domain.com>, "Foo Bar" <f...@domain.com>') + resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <p...@domain.com>, Foo Bar <f...@domain.com>') - self.failUnless(resto == ['"Public, Joe" <p...@domain.com>', - '"Foo Bar" <f...@domain.com>']) - self.failUnless(resfrom == 'sen...@domain.com' ) + self.failUnlessEqual(resto, ['"Public, Joe" <p...@domain.com>', + 'Foo Bar <f...@domain.com>']) + self.failUnlessEqual(resfrom, 'sen...@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" <recipi...@domain.com>,"Foo Bar" <f...@domain.com> +To: "Name, Nick" <recipi...@domain.com>, Foo Bar <f...@domain.com> From: sen...@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" <recipi...@domain.com>,"Foo Bar" <f...@domain.com> +To: "Name, Nick" <recipi...@domain.com>, Foo Bar <f...@domain.com> From: sen...@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" <recipi...@domain.com>, "Foo Bar" <f...@domain.com>', + mfrom='sen...@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" <recipi...@domain.com>, Foo Bar <f...@domain.com>') + self.failUnlessEqual(out['From'], 'sen...@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" <recipi...@domain.com>, "Foo Bar" <f...@domain.com>', + mfrom='sen...@domain.com', subject='This is the subject', charset='utf-8') + out = message_from_string(mailhost.sent) + self.failUnlessEqual(out['To'], + '"Name, Nick" <recipi...@domain.com>, Foo Bar <f...@domain.com>') + self.failUnlessEqual(out['From'], 'sen...@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 <j...@example.com>" + mto = "Ferran Adri\xc3\xa0 <fer...@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?= <fer...@example.com>') + self.failUnlessEqual(out['From'], + '=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <j...@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?= <j...@example.com> +To: =?utf-8?q?Ferran_Adri=C3=A0?= <fer...@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?= <j...@example.com> +To: =?utf-8?q?Ferran_Adri=C3=A0?= <fer...@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?= <j...@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 <fer...@example.com>', 'utf-8') + subject = unicode('\xc2\xa1Andr\xc3\xa9s!', 'utf-8') + mailhost = self._makeOne('MailHost') + mailhost.send(messageText=msg, + mto='"Name, Nick" <recipi...@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" <recipi...@domain.com>') + self.failUnlessEqual(out['From'], + '=?utf-8?q?Ferran_Adri=C3=A0?= <fer...@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" <recipi...@domain.com>', + mfrom='Foo Bar <f...@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" <recipi...@domain.com>', + mfrom=u'Foo Bar <f...@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" <recipi...@domain.com> +From: Foo Bar <f...@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?= <j...@example.com> +To: =?utf-8?q?Ferran_Adri=C3=A0?= <fer...@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 <f...@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 <f...@domain.com>') + self.failUnlessEqual(msg['From'], + '=?utf-8?q?Jos=C3=A9_Andr=C3=A9s?= <j...@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='sen...@domain.com', + mto='Foo Bar <f...@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 <f...@domain.com> +From: sen...@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='sen...@domain.com', + mto='Foo Bar <f...@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 <f...@domain.com> +From: sen...@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='sen...@domain.com', + mto='Foo Bar <f...@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 <f...@domain.com> +From: sen...@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='sen...@domain.com', + mto='Foo Bar <f...@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 <f...@domain.com> +From: sen...@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 <f...@domain.com>', + mfrom='sen...@domain.com') + self.failUnlessEqual(result, 'SEND OK') + result = mailhost.sendTemplate(content, 'my_template', + mto='Foo Bar <f...@domain.com>', + mfrom='sen...@domain.com', + statusTemplate='wrong_name') + self.failUnlessEqual(result, 'SEND OK') + result = mailhost.sendTemplate(content, 'my_template', + mto='Foo Bar <f...@domain.com>', + mfrom='sen...@domain.com', + statusTemplate='check_status') + self.failUnlessEqual(result, 'Message Sent') + + def test_suite(): suite = unittest.TestSuite() suite.addTest( unittest.makeSuite( TestMailHost ) ) _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org https://mail.zope.org/mailman/listinfo/zope-checkins