Commit c8a66859 authored by Hanno Schlichting's avatar Hanno Schlichting

Factored out MailHost

parent 4c48339e
......@@ -51,6 +51,7 @@ eggs =
Persistence
Products.BTreeFolder2
Products.ExternalMethod
Products.MailHost
Products.PythonScripts
Products.StandardCacheManagers
Products.ZCTextIndex
......
......@@ -15,6 +15,9 @@ Bugs Fixed
Restructuring
+++++++++++++
- Factored out the `Products.MailHost` package into its own distributions. It
will no longer be included by default in Zope 2.14 but live on as an
independent add-on.
Features Added
++++++++++++++
......
......@@ -101,6 +101,7 @@ setup(name='Zope2',
# BBB optional dependencies to be removed in Zope 2.14
'Products.BTreeFolder2',
'Products.ExternalMethod',
'Products.MailHost',
'Products.MIMETools',
'Products.OFSP',
'Products.PythonScripts',
......
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""SMTP mail objects
$Id$
"""
import logging
from os.path import realpath
import re
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
import uu
from threading import Lock
import time
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from AccessControl.Permissions import change_configuration, view
from AccessControl.Permissions import use_mailhost_services
from Acquisition import Implicit
from App.special_dtml import DTMLFile
from DateTime.DateTime import DateTime
from Persistence import Persistent
from OFS.role import RoleManager
from OFS.SimpleItem import Item
from zope.interface import implements
from zope.sendmail.mailer import SMTPMailer
from zope.sendmail.maildir import Maildir
from zope.sendmail.delivery import DirectMailDelivery, QueuedMailDelivery, \
QueueProcessorThread
from interfaces import IMailHost
from decorator import synchronized
queue_threads = {} # maps MailHost path -> queue processor threada
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
manage_addMailHostForm = DTMLFile('dtml/addMailHost_form', globals())
def manage_addMailHost(self,
id,
title='',
smtp_host='localhost',
localhost='localhost',
smtp_port=25,
timeout=1.0,
REQUEST=None,
):
""" Add a MailHost into the system.
"""
i = MailHost( id, title, smtp_host, smtp_port ) #create new mail host
self._setObject( id,i ) #register it
if REQUEST is not None:
REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
add = manage_addMailHost
class MailBase(Implicit, Item, RoleManager):
"""a mailhost...?"""
implements(IMailHost)
meta_type = 'Mail Host'
manage = manage_main = DTMLFile('dtml/manageMailHost', globals())
manage_main._setName('manage_main')
index_html = None
security = ClassSecurityInfo()
smtp_uid = '' # Class attributes for smooth upgrades
smtp_pwd = ''
smtp_queue = False
smtp_queue_directory = '/tmp'
force_tls = False
lock = Lock()
# timeout = 1.0 # unused?
manage_options = (
(
{'icon':'', 'label':'Edit',
'action':'manage_main',
'help':('MailHost','Mail-Host_Edit.stx')},
)
+ RoleManager.manage_options
+ Item.manage_options
)
def __init__(self,
id='',
title='',
smtp_host='localhost',
smtp_port=25,
force_tls=False,
smtp_uid='',
smtp_pwd='',
smtp_queue=False,
smtp_queue_directory='/tmp',
):
"""Initialize a new MailHost instance.
"""
self.id = id
self.title = title
self.smtp_host = str( smtp_host )
self.smtp_port = int(smtp_port)
self.smtp_uid = smtp_uid
self.smtp_pwd = smtp_pwd
self.force_tls = force_tls
self.smtp_queue = smtp_queue
self.smtp_queue_directory = smtp_queue_directory
# staying for now... (backwards compatibility)
def _init(self, smtp_host, smtp_port):
self.smtp_host = smtp_host
self.smtp_port = smtp_port
security.declareProtected(change_configuration, 'manage_makeChanges')
def manage_makeChanges(self,
title,
smtp_host,
smtp_port,
smtp_uid='',
smtp_pwd='',
smtp_queue=False,
smtp_queue_directory='/tmp',
force_tls=False,
REQUEST=None,
):
"""Make the changes.
"""
title = str(title)
smtp_host = str(smtp_host)
smtp_port = int(smtp_port)
self.title = title
self.smtp_host = smtp_host
self.smtp_port = smtp_port
self.smtp_uid = smtp_uid
self.smtp_pwd = smtp_pwd
self.force_tls = force_tls
self.smtp_queue = smtp_queue
self.smtp_queue_directory = smtp_queue_directory
# restart queue processor thread
if self.smtp_queue:
self._stopQueueProcessorThread()
self._startQueueProcessorThread()
else:
self._stopQueueProcessorThread()
if REQUEST is not None:
msg = 'MailHost %s updated' % self.id
return self.manage_main( self
, REQUEST
, manage_tabs_message=msg
)
security.declareProtected(use_mailhost_services, 'sendTemplate')
def sendTemplate(trueself,
self,
messageTemplate,
statusTemplate=None,
mto=None,
mfrom=None,
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)
trueself.send(messageText, mto=mto, mfrom=mfrom,
encode=encode, immediate=immediate,
charset=charset, msg_type=msg_type)
if not statusTemplate:
return "SEND OK"
try:
stemplate = getattr(self, statusTemplate)
return stemplate(self, trueself.REQUEST)
except:
return "SEND OK"
security.declareProtected(use_mailhost_services, 'send')
def send(self,
messageText,
mto=None,
mfrom=None,
subject=None,
encode=None,
immediate=False,
charset=None,
msg_type=None,
):
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)
# This is here for backwards compatibility only. Possibly it could
# be used to send messages at a scheduled future time, or via a mail queue?
security.declareProtected(use_mailhost_services, 'scheduledSend')
scheduledSend = send
security.declareProtected(use_mailhost_services, 'simple_send')
def simple_send(self, mto, mfrom, subject, body, immediate=False):
body = "From: %s\nTo: %s\nSubject: %s\n\n%s" % (
mfrom, mto, subject, body)
self._send(mfrom, mto, body, immediate)
def _makeMailer(self):
""" Create a SMTPMailer """
return SMTPMailer(hostname=self.smtp_host,
port=int(self.smtp_port),
username=self.smtp_uid or None,
password=self.smtp_pwd or None,
force_tls=self.force_tls
)
security.declarePrivate('_getThreadKey')
def _getThreadKey(self):
""" Return the key used to find our processor thread.
"""
return realpath(self.smtp_queue_directory)
@synchronized(lock)
def _stopQueueProcessorThread(self):
""" Stop thread for processing the mail queue.
"""
key = self._getThreadKey()
if queue_threads.has_key(key):
thread = queue_threads[key]
thread.stop()
while thread.isAlive():
# wait until thread is really dead
time.sleep(0.3)
del queue_threads[key]
LOG.info('Thread for %s stopped' % key)
@synchronized(lock)
def _startQueueProcessorThread(self):
""" Start thread for processing the mail queue.
"""
key = self._getThreadKey()
if not queue_threads.has_key(key):
thread = QueueProcessorThread()
thread.setMailer(self._makeMailer())
thread.setQueuePath(self.smtp_queue_directory)
thread.start()
queue_threads[key] = thread
LOG.info('Thread for %s started' % key)
security.declareProtected(view, 'queueLength')
def queueLength(self):
""" return length of mail queue """
try:
maildir = Maildir(self.smtp_queue_directory)
return len([item for item in maildir])
except ValueError:
return 'n/a - %s is not a maildir - please verify your ' \
'configuration' % self.smtp_queue_directory
security.declareProtected(view, 'queueThreadAlive')
def queueThreadAlive(self):
""" return True/False is queue thread is working
"""
th = queue_threads.get(self._getThreadKey())
if th:
return th.isAlive()
return False
security.declareProtected(change_configuration, 'manage_restartQueueThread')
def manage_restartQueueThread(self, action='start', REQUEST=None):
""" Restart the queue processor thread """
if action == 'stop':
self._stopQueueProcessorThread()
elif action == 'start':
self._startQueueProcessorThread()
else:
raise ValueError('Unsupported action %s' % action)
if REQUEST is not None:
msg = 'Queue processor thread %s' % \
(action == 'stop' and 'stopped' or 'started')
return self.manage_main(self, REQUEST, manage_tabs_message=msg)
security.declarePrivate('_send')
def _send(self, mfrom, mto, messageText, immediate=False):
""" Send the message """
if immediate:
self._makeMailer().send(mfrom, mto, messageText)
else:
if self.smtp_queue:
# Start queue processor thread, if necessary
self._startQueueProcessorThread()
delivery = QueuedMailDelivery(self.smtp_queue_directory)
else:
delivery = DirectMailDelivery(self._makeMailer())
delivery.send(mfrom, mto, messageText)
InitializeClass(MailBase)
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
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'
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,
charset=None, msg_type=None):
"""Sets missing message headers, and deletes Bcc.
returns fixed message, fixed mto and fixed mfrom"""
# 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)
if not mo.is_multipart():
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]
else:
# Do basically the same for each payload as for the complete
# multipart message.
for index, payload in enumerate(mo.get_payload()):
if not isinstance(payload, Message):
payload = message_from_string(payload)
charset_match = CHARSET_RE.search(payload['Content-Type'] or '')
if payload.get_filename() is None:
# No binary file
if charset and not charset_match:
payload.set_charset(charset)
elif charset_match and not charset:
charset = charset_match.groups()[0]
mo.get_payload()[index] = payload
# 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:
# 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 = [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 = ','.join(mo.get_all(header) or [])
if v:
mto += [formataddr(addr) for addr in getaddresses((v,))]
if not mto:
raise MailHostError, "No message recipients designated"
if 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.get('From') is None:
raise MailHostError,"Message missing SMTP Header 'From'"
mfrom = mo['From']
if mo.get('Bcc'):
del mo['Bcc']
if not mo.get('Date'):
mo['Date'] = DateTime().rfc822()
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
MailHost
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 (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
implemented using a queue and a dedicated thread processing the queue. The
thread is (re)-started automatically when sending an email. The thread can be
startet manually (in case of restart) by calling its
manage_restartQueueThread?action=start method through HTTP. There is
currently no possibility to start the thread at Zope startup time.
Supports TLS/SSL encryption (requires Python compiled with SSL support)
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
__rcs_id__='$Id$'
__version__='$Revision: 1.18 $'[11:-2]
from MailHost import MailBase, MailHostError
from DocumentTemplate.DT_Util import parse_params,render_blocks
from DocumentTemplate.DT_String import String
class SendMailTag:
'''the send mail tag, used like thus:
<dtml-sendmail mailhost="someMailHostID">
to: person@their.machine.com
from: me@mymachine.net
subject: just called to say...
boy howdy!
</dtml-sendmail>
Text between the sendmail and /sendmail tags is processed
by the MailHost machinery and delivered. There must be at least
one blank line seperating the headers (to/from/etc..) from the body
of the message.
Instead of specifying a MailHost, an smtphost may be specified
ala 'smtphost="mail.mycompany.com" port=25' (port defaults to 25
automatically). Other parameters are
* mailto -- person (or comma-seperated list of persons) to send the
mail to. If not specified, there **must** be a to: header in the
message.
* mailfrom -- person sending the mail (basically who the recipient can
reply to). If not specified, there **must** be a from: header in the
message.
* subject -- optional subject. If not specified, there **must** be a
subject: header in the message.
* encode -- optional encoding. Possible values are: 'base64',
'quoted-printable' and 'uuencode'.
'''
name='sendmail'
blockContinuations=()
encode=None
def __init__(self, blocks):
tname, args, section=blocks[0]
args=parse_params(args, mailhost=None, mailto=None, mailfrom=None,
subject=None, smtphost=None, port='25',
encode=None)
smtphost=None
has_key=args.has_key
if has_key('mailhost'): mailhost=args['mailhost']
elif has_key('smtphost'): mailhost=smtphost=args['smtphost']
elif has_key(''): mailhost=args['mailhost']=args['']
else: raise MailHostError, 'No mailhost was specified in tag'
for key in ('mailto', 'mailfrom', 'subject', 'port'):
if not args.has_key(key):args[key]=''
if has_key('encode') and args['encode'] not in \
('base64', 'quoted-printable', 'uuencode', 'x-uuencode',
'uue', 'x-uue'):
raise MailHostError, (
'An unsupported encoding was specified in tag')
if not smtphost:
self.__name__=self.mailhost=mailhost
self.smtphost=None
else:
self.__name__=self.smtphost=smtphost
self.mailhost=None
self.section=section
self.args=args
self.mailto=args['mailto']
self.mailfrom=args['mailfrom']
self.subject=None or args['subject']
if args['port'] and type(args['port']) is type('s'):
self.port=args['port']=int(args['port'])
elif args['port']=='':
self.port=args['port']=25
else:
self.port=args['port']
if has_key('encode'):
self.encode=args['encode']
else: self.encode=None
def render(self, md):
args=self.args
has_key=args.has_key
if self.mailhost:
mhost=md[self.mailhost]
elif self.smtphost:
mhost=MailBase( smtp_host=self.smtphost, smtp_port=self.port )
mhost.send(render_blocks(self.section.blocks, md),
self.mailto, self.mailfrom,
self.subject, self.encode
)
return ' '
__call__=render
String.commands['sendmail']=SendMailTag
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
__doc__='''MailHost Product Initialization
$Id$'''
__version__='$Revision: 1.22 $'[11:-2]
import MailHost
import SendMailTag
def initialize(context):
context.registerClass(
MailHost.MailHost,
permission='Add MailHost objects',
constructors=(MailHost.manage_addMailHostForm,
MailHost.manage_addMailHost),
icon='www/MailHost_icon.gif',
)
context.registerHelp()
context.registerHelpTitle('Zope Help')
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
Decorator(s)
$Id: MailHost.py 78992 2007-08-19 11:58:08Z andreasjung $
"""
def synchronized(lock):
""" Decorator for method synchronization. """
def wrapper(f):
def method(*args, **kw):
lock.acquire()
try:
return f(*args, **kw)
finally:
lock.release()
return method
return wrapper
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add MailHost',
help_product='MailHost',
help_topic='Mail-Host_Add.stx'
)">
<p class="form-help">
MailHost object provide a way to send email from Zope code in DTML or
Python Scripts. <em>SMTP host</em> is the name of the mail server machine.
<em>SMTP port</em> is the port on which the mail server is running the
SMTP service.
</p>
<form action="manage_addMailHost" method="post">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" value="MailHost" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Host
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_host" size="40" value="localhost" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Port
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_port:int" size="4" value="25" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Add " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<form action="manage_makeChanges" method="post">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<div class="form-text">
&dtml-id;
</div>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40"
value="&dtml-title;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Host
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_host" size="40"
value="&dtml-smtp_host;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
SMTP Port
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_port:int" size="4"
value="&dtml-smtp_port;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Username
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_uid" size="15"
value="&dtml.null-smtp_uid;"/>
</td>
<td>
<span class="form-help">(optional for SMTP AUTH)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Password
</div>
</td>
<td align="left" valign="top">
<input type="password" name="smtp_pwd" size="15"
value="&dtml.null-smtp_pwd;"/>
</td>
<td>
<span class="form-help">(optional for SMTP AUTH)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Force TLS
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="force_tls:boolean" value="1"
<dtml-if "force_tls">checked</dtml-if>
</td>
<td>
<span class="form-help">(enforce the use of an encrypted connection
to the SMTP server. Mail delivery fails if the SMTP server
does not support encryption)
</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Use mail queue
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="smtp_queue:boolean" value="1"
<dtml-if "smtp_queue">checked</dtml-if>
</td>
<td>
<span class="form-help">(asynchronous mail delivery if checked)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Queue directory<br/>
</div>
</td>
<td align="left" valign="top">
<input type="text" name="smtp_queue_directory" size="30"
value="&dtml-smtp_queue_directory;"/>
</td>
<td>
<span class="form-help">(directory on the filesystem where the mails will be spooled. Only used if 'Use mail queue' is checked.)</span>
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
<dtml-if smtp_queue>
<br />
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Mails in queue <br/>
</div>
</td>
<td align="left" valign="top">
<span class="form-help"><dtml-var queueLength></span>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Status of queue processor thread<br/>
</div>
</td>
<td align="left" valign="top">
<div class="form-help">
<dtml-if "queueThreadAlive()">
Running
<br/>
<a href="manage_restartQueueThread?action=stop">Stop queue processor thread</a> (this may take some seconds)
</dtml-if>
<dtml-if "not queueThreadAlive()">
Stopped
<br/>
<a href="manage_restartQueueThread?action=start">Start queue processor thread</a> (this may take some seconds)
</dtml-if>
</div>
</td>
</tr>
</table>
</dtml-if>
</form>
<dtml-var manage_page_footer>
MailHost: Sends mail through an SMTP server.
MailHosts allow you to send mail via the Simple Mail Transfer
Protocol (SMTP).
This object can be used deliver mail by the <dtml-sendmail> tag
or via the send() and simple_send() methods.
'send(messageText, mto=None, mfrom=None, subject=None, encode=None)'
Sends an email message where the messageText is an rfc822 formatted
message. This allows you complete control over the message headers,
including setting any extra headers such as Cc: and Reply-To:.
The arguments are:
messageText -- The mail message. It can either be a rfc822
formed text with header fields, or just a body without any
header fields. The other arguments given will override the
header fields in the message, if they exist.
mto -- A commaseparated string or list of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
encode -- The rfc822 defined encoding of the message. The
default of 'None' means no encoding is done. Valid values
are 'base64', 'quoted-printable' and 'uuencode'.
'simple_send(self, mto, mfrom, subject, body)'
Sends a message. Only To:, From: and Subject: headers can be set.
Note that simple_send does not process or validate its arguments
in any way.
The arguments are:
mto -- A commaseparated string of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
body -- The body of the message.
MailHost - Add: Create a new MailHost
Description
Create a new MailHost object.
Controls
'ID' -- The id of the MailHost object.
'Title' -- The title of the MailHost.
'SMTP host' -- The domain name or address of the SMTP mail server
to relay mail through.
'SMTP port' -- The port of the SMTP mail server to relay mail
through.
MailHost - Edit: Edit mail host properties
Description
This view allows you edit the MailHost.
Controls
'ID' -- The id of the MailHost.
'Title' -- The title of the MailHost.
'SMTP host' -- The domain name or address of the SMTP mail server
to relay mail through.
'SMTP port' -- The port of the SMTP mail server to relay mail
through.
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
def manage_addMailHost(id, title='', smtp_host=None,
localhost='localhost', smtp_port=25,
timeout=1.0):
"""
Add a mailhost object to an ObjectManager.
"""
class MailHost:
"""
MailHost objects work as adapters to Simple Mail Transfer Protocol
(SMTP) servers. MailHosts are used by DTML 'sendmail' tags
to find the proper host to deliver mail to.
"""
def send(messageText, mto=None, mfrom=None, subject=None,
encode=None):
"""
Sends an email message where the messageText is an rfc822 formatted
message. This allows you complete control over the message headers,
including setting any extra headers such as Cc: and Reply-To:.
The arguments are:
messageText -- The mail message. It can either be a rfc822
formed text with header fields, or just a body without any
header fields. The other arguments given will override the
header fields in the message, if they exist.
mto -- A commaseparated string or list of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
encode -- The rfc822 defined encoding of the message. The
default of 'None' means no encoding is done. Valid values
are 'base64', 'quoted-printable' and 'uuencode'.
"""
def simple_send(self, mto, mfrom, subject, body):
"""
Sends a message. Only To:, From: and Subject: headers can be set.
Note that simple_send does not process or validate its arguments
in any way.
The arguments are:
mto -- A commaseparated string of recipient(s) of the message.
mfrom -- The address of the message sender.
subject -- The subject of the message.
body -- The body of the message.
"""
##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""MailHost z3 interfaces.
$Id$
"""
from zope.interface import Interface
class IMailHost(Interface):
def send(messageText, mto=None, mfrom=None, subject=None, encode=None,
charset=None, msg_type=None):
"""Send mail.
"""
import zope.deferredimport
zope.deferredimport.deprecatedFrom(
"Import from zope.sendmail instead",
'zope.sendmail.mailer',
'SMTPMailer',
)
##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
# This file is needed to make this a package.
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""MailHost unit tests.
$Id$
"""
import unittest
from email import message_from_string
from Products.MailHost.MailHost import MailHost
from Products.MailHost.MailHost import MailHostError, _mungeHeaders
class DummyMailHost(MailHost):
meta_type = 'Dummy Mail Host'
def __init__(self, id):
self.id = id
self.sent = ''
def _send(self, mfrom, mto, messageText, immediate=False):
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):
return DummyMailHost
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_z3interfaces(self):
from Products.MailHost.interfaces import IMailHost
from zope.interface.verify import verifyClass
verifyClass(IMailHost, self._getTargetClass())
def testAllHeaders( self ):
msg = """To: recipient@domain.com
From: sender@domain.com
Subject: This is the subject
This is the message body."""
# No additional info
resmsg, resto, resfrom = _mungeHeaders( msg )
self.failUnless(resto == ['recipient@domain.com'])
self.failUnless(resfrom == 'sender@domain.com' )
# Add duplicated info
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient@domain.com',
'sender@domain.com', 'This is the subject' )
self.failUnlessEqual(resto, ['recipient@domain.com'])
self.failUnlessEqual(resfrom, 'sender@domain.com' )
# Add extra info
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2@domain.com',
'sender2@domain.com', 'This is the real subject' )
self.failUnlessEqual(resto, ['recipient2@domain.com'])
self.failUnlessEqual(resfrom, 'sender2@domain.com' )
def testMissingHeaders( self ):
msg = """X-Header: Dummy header
This is the message body."""
# Doesn't specify to
self.failUnlessRaises(MailHostError, _mungeHeaders, msg,
mfrom='sender@domain.com')
# Doesn't specify from
self.failUnlessRaises(MailHostError, _mungeHeaders, msg,
mto='recipient@domain.com')
def testNoHeaders( self ):
msg = """This is the message body."""
# Doesn't specify to
self.failUnlessRaises(MailHostError, _mungeHeaders, msg,
mfrom='sender@domain.com')
# Doesn't specify from
self.failUnlessRaises(MailHostError, _mungeHeaders, msg,
mto='recipient@domain.com')
# Specify all
resmsg, resto, resfrom = _mungeHeaders(msg, 'recipient2@domain.com',
'sender2@domain.com', 'This is the real subject')
self.failUnlessEqual(resto, ['recipient2@domain.com'])
self.failUnlessEqual(resfrom,'sender2@domain.com' )
def testBCCHeader( self ):
msg = "From: me@example.com\nBcc: many@example.com\n\nMessage text"
# Specify only the "Bcc" header. Useful for bulk emails.
resmsg, resto, resfrom = _mungeHeaders(msg)
self.failUnlessEqual(resto, ['many@example.com'])
self.failUnlessEqual(resfrom, 'me@example.com' )
def test__getThreadKey_uses_fspath(self):
mh1 = self._makeOne('mh1')
mh1.smtp_queue_directory = '/abc'
mh1.absolute_url = lambda self: 'http://example.com/mh1'
mh2 = self._makeOne('mh2')
mh2.smtp_queue_directory = '/abc'
mh2.absolute_url = lambda self: 'http://example.com/mh2'
self.assertEqual(mh1._getThreadKey(), mh2._getThreadKey())
def testAddressParser( self ):
msg = """To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
CC: "Web, Jack" <jack@web.com>
From: sender@domain.com
Subject: This is the subject
This is the message body."""
# Test Address-Parser for To & CC given in messageText
resmsg, resto, resfrom = _mungeHeaders( msg )
self.failUnlessEqual(resto, ['"Name, Nick" <recipient@domain.com>',
'Foo Bar <foo@domain.com>',
'"Web, Jack" <jack@web.com>'])
self.failUnlessEqual(resfrom, 'sender@domain.com')
# Test Address-Parser for a given mto-string
resmsg, resto, resfrom = _mungeHeaders(msg, mto= '"Public, Joe" <pjoe@domain.com>, Foo Bar <foo@domain.com>')
self.failUnlessEqual(resto, ['"Public, Joe" <pjoe@domain.com>',
'Foo Bar <foo@domain.com>'])
self.failUnlessEqual(resfrom, 'sender@domain.com')
def testSendMessageOnly(self):
msg = """\
To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
From: sender@domain.com
Subject: This is the subject
Date: Sun, 27 Aug 2006 17:00:00 +0200
This is the message body."""
mailhost = self._makeOne('MailHost')
mailhost.send(msg)
self.assertEqual(mailhost.sent, msg)
def testSendWithArguments(self):
inmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200
This is the message body."""
outmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject
To: "Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>
From: sender@domain.com
This is the message body."""
mailhost = self._makeOne('MailHost')
mailhost.send(messageText=inmsg,
mto='"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>',
mfrom='sender@domain.com', subject='This is the subject')
self.assertEqual(mailhost.sent, outmsg)
def testSendWithMtoList(self):
inmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200
This is the message body."""
outmsg = """\
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: This is the subject
To: "Name, Nick" <recipient@domain.com>, Foo Bar <foo@domain.com>
From: sender@domain.com
This is the message body."""
mailhost = self._makeOne('MailHost')
mailhost.send(messageText=inmsg,
mto=['"Name, Nick" <recipient@domain.com>', '"Foo Bar" <foo@domain.com>'],
mfrom='sender@domain.com', subject='This is the subject')
self.assertEqual(mailhost.sent, outmsg)
def testSimpleSend(self):
outmsg = """\
From: sender@domain.com
To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
Subject: This is the subject
This is the message body."""
mailhost = self._makeOne('MailHost')
mailhost.simple_send(mto='"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>',
mfrom='sender@domain.com', subject='This is the subject',
body='This is the message body.')
self.assertEqual(mailhost.sent, outmsg)
self.assertEqual(mailhost.immediate, False)
def testSendImmediate(self):
outmsg = """\
From: sender@domain.com
To: "Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>
Subject: This is the subject
This is the message body."""
mailhost = self._makeOne('MailHost')
mailhost.simple_send(mto='"Name, Nick" <recipient@domain.com>, "Foo Bar" <foo@domain.com>',
mfrom='sender@domain.com', subject='This is the subject',
body='This is the message body.', immediate=True)
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@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 testSendMultiPartAlternativeMessage(self):
msg = ("""\
Content-Type: multipart/alternative; boundary="===============0490954888=="
MIME-Version: 1.0
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: My multipart email
To: Foo Bar <foo@domain.com>
From: sender@domain.com
--===============0490954888==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
This is plain text.
--===============0490954888==
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
<p>This is html.</p>
--===============0490954888==--
""")
mailhost = self._makeOne('MailHost')
# Specifying a charset for the header may have unwanted side
# effects in the case of multipart mails.
# (TypeError: expected string or buffer)
mailhost.send(msg, charset='utf-8')
self.assertEqual(mailhost.sent, msg)
def testSendMultiPartMixedMessage(self):
msg = ("""\
Content-Type: multipart/mixed; boundary="XOIedfhf+7KOe/yw"
Content-Disposition: inline
MIME-Version: 1.0
Date: Sun, 27 Aug 2006 17:00:00 +0200
Subject: My multipart email
To: Foo Bar <foo@domain.com>
From: sender@domain.com
--XOIedfhf+7KOe/yw
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
This is a test with as attachment OFS/www/new.gif.
--XOIedfhf+7KOe/yw
Content-Type: image/gif
Content-Disposition: attachment; filename="new.gif"
Content-Transfer-Encoding: base64
R0lGODlhCwAQAPcAAP8A/wAAAFBQUICAgMDAwP8AAIAAQAAAoABAgIAAgEAAQP//AP//gACA
gECAgP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAALABAAAAg7AAEIFKhgoEGC
CwoeRKhwoYKEBhVIfLgg4UQAFCtqbJixYkOEHg9SHDmQJEmMEBkS/IiR5cKXMGPKDAgAOw==
--XOIedfhf+7KOe/yw
Content-Type: text/plain; charset=iso-8859-1
Content-Disposition: attachment; filename="test.txt"
Content-Transfer-Encoding: quoted-printable
D=EDt =EFs =E9=E9n test
--XOIedfhf+7KOe/yw--
""")
mailhost = self._makeOne('MailHost')
# Specifying a charset for the header may have unwanted side
# effects in the case of multipart mails.
# (TypeError: expected string or buffer)
mailhost.send(msg, charset='utf-8')
self.assertEqual(mailhost.sent, msg)
def test_suite():
suite = unittest.TestSuite()
suite.addTest( unittest.makeSuite( TestMailHost ) )
return suite
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
......@@ -17,6 +17,7 @@ nt-svcutils = 2.13.0
Persistence = 2.13.2
Products.BTreeFolder2 = 2.13.0
Products.ExternalMethod = 2.13.0
Products.MailHost = 2.13.0
Products.MIMETools = 2.13.0
Products.OFSP = 2.13.1
Products.PythonScripts = 2.13.0
......
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