Back to index

plone3  3.1.7
SecureMailHost.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001-2004 Zope Corporation and Contributors.
00004 # Copyright (c) 2004 Christian Heimes and Contributors.
00005 # All Rights Reserved.
00006 #
00007 # This software is subject to the provisions of the Zope Public License,
00008 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
00009 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00010 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00011 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00012 # FOR A PARTICULAR PURPOSE.
00013 #
00014 ##############################################################################
00015 """SMTP mail objects
00016 $Id: SecureMailHost.py 45282 2007-07-08 20:26:19Z optilude $
00017 """
00018 
00019 from config import BAD_HEADERS
00020 from copy import deepcopy
00021 
00022 import email.Message
00023 import email.Header
00024 import email.MIMEText
00025 import email
00026 from email.Utils import getaddresses
00027 from email.Utils import formataddr
00028 
00029 import re
00030 
00031 from AccessControl import ClassSecurityInfo
00032 from AccessControl.Permissions import use_mailhost_services
00033 from Globals import Persistent, DTMLFile, InitializeClass
00034 from Products.MailHost.MailHost import MailHostError, MailBase
00035 from Products.SecureMailHost.mail import Mail
00036 
00037 class SMTPError(Exception):
00038     pass
00039 
00040 EMAIL_RE = re.compile(r"^(\w&.%#$&'\*+-/=?^_`{}|~]+!)*[\w&.%#$&'\*+-/=?^_`{}|~]+@(([0-9a-z]([0-9a-z-]*[0-9a-z])?\.)+[a-z]{2,6}|([0-9]{1,3}\.){3}[0-9]{1,3})$", re.IGNORECASE)
00041 # used to find double new line (in any variant)
00042 EMAIL_CUTOFF_RE = re.compile(r".*[\n\r][\n\r]")
00043 
00044 # We need to encode usernames in email addresses.
00045 # This is especially important for Chinese and other languanges.
00046 # Sample email addresses:
00047 #
00048 # aaa<a@b.c>, "a,db"<a@b.c>, apn@zopechina.com, "ff s" <a@b.c>, asdf<a@zopechina.com>
00049 EMAIL_ADDRESSES_RE = re.compile(r'(".*?" *|[^,^"^>]+?)(<.*?>)')
00050 
00051 class MailAddressTransformer:
00052     """ a transformer for substitution """
00053     def __init__(self, charset):
00054         self.charset = charset
00055 
00056     def __call__(self, matchobj):
00057         name = matchobj.group(1)
00058         address = matchobj.group(2)
00059         return str(email.Header.Header(name, self.charset)) + address
00060 
00061 def encodeHeaderAddress(address, charset):
00062     """ address encoder """
00063     return address and \
00064       EMAIL_ADDRESSES_RE.sub(MailAddressTransformer(charset), address)
00065 
00066 def formataddresses(fieldvalues):
00067     """Takes a list of (REALNAME, EMAIL) and returns one string
00068     suitable for To or CC
00069     """
00070     return ', '.join([formataddr(pair) for pair in fieldvalues])
00071 
00072 manage_addMailHostForm = DTMLFile('www/addMailHost_form', globals())
00073 def manage_addMailHost(self, id, title='', smtp_host='localhost',
00074                        smtp_port=25, smtp_userid=None,
00075                        smtp_pass=None, smtp_notls=None, REQUEST=None):
00076     """Add a MailHost
00077     """
00078     ob = SecureMailHost(id, title, smtp_host, smtp_port,
00079                         smtp_userid, smtp_pass, smtp_notls)
00080     self._setObject(id, ob)
00081 
00082     if REQUEST is not None:
00083         REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
00084 
00085 add = manage_addMailHost
00086 
00087 class SecureMailBase(MailBase):
00088     """A more secure mailhost with ESMTP features and header checking
00089     """
00090     meta_type = 'Secure Mail Host'
00091     manage=manage_main = DTMLFile('www/manageMailHost', globals())
00092     manage_main._setName('manage_main')
00093     index_html = None
00094     security = ClassSecurityInfo()
00095 
00096     def __init__(self, id='', title='', smtp_host='localhost',
00097                   smtp_port=25, smtp_userid='', smtp_pass='', smtp_notls=False):
00098         """Initialize a new MailHost instance
00099         """
00100         self.id = id
00101         self.setConfiguration(title, smtp_host, smtp_port,
00102                               smtp_userid, smtp_pass, smtp_notls)
00103 
00104     security.declareProtected('Change configuration', 'manage_makeChanges')
00105     def manage_makeChanges(self, title, smtp_host, smtp_port,
00106                            smtp_userid, smtp_pass, smtp_notls=None,
00107                            REQUEST=None):
00108         """Make the changes
00109         """
00110         self.setConfiguration(title, smtp_host, smtp_port,
00111                               smtp_userid, smtp_pass, smtp_notls)
00112         if REQUEST is not None:
00113             msg = 'MailHost %s updated' % self.id
00114             return self.manage_main(self, REQUEST, manage_tabs_message=msg)
00115 
00116     security.declarePrivate('setConfiguration')
00117     def setConfiguration(self, title, smtp_host, smtp_port,
00118                          smtp_userid, smtp_pass, smtp_notls):
00119         """Set configuration
00120         """
00121         self.title = title
00122         self.smtp_host = str(smtp_host)
00123         self.smtp_port = int(smtp_port)
00124         if smtp_userid:
00125             self._smtp_userid = smtp_userid
00126             self.smtp_userid = smtp_userid
00127         else:
00128             self._smtp_userid = None
00129             self.smtp_userid = None
00130         if smtp_pass:
00131             self._smtp_pass = smtp_pass
00132             self.smtp_pass = smtp_pass
00133         else:
00134             self._smtp_pass = None
00135             self.smtp_pass = None
00136         if smtp_notls is not None:
00137             self.smtp_notls = smtp_notls
00138         else:
00139             self.smtp_notls = False
00140 
00141     security.declareProtected(use_mailhost_services, 'sendTemplate')
00142     def sendTemplate(trueself, self, messageTemplate,
00143                      statusTemplate=None, mto=None, mfrom=None,
00144                      encode=None, REQUEST=None):
00145         """Render a mail template, then send it...
00146         """
00147         return MailBase.sendTemplate(trueself, self, messageTemplate,
00148                                      statusTemplate=statusTemplate, mto=mto,
00149                                      mfrom=mfrom,  encode=encode,
00150                                      REQUEST=REQUEST)
00151 
00152     security.declareProtected(use_mailhost_services, 'send')
00153     def send(self, message, mto=None, mfrom=None, subject=None,
00154              encode=None):
00155         """Send email
00156         """
00157         return MailBase.send(self, message, mto=mto, mfrom=mfrom,
00158                              subject=subject, encode=encode)
00159 
00160     security.declareProtected(use_mailhost_services, 'secureSend')
00161     def secureSend(self, message, mto, mfrom, subject='[No Subject]',
00162                    mcc=None, mbcc=None, subtype='plain', charset='us-ascii',
00163                    debug=False, **kwargs):
00164         """A more secure way to send a message
00165 
00166         message:
00167             The plain message text without any headers or an
00168             email.Message.Message based instance
00169         mto:
00170             To: field (string or list)
00171         mfrom:
00172             From: field
00173         subject:
00174             Message subject (default: [No Subject])
00175         mcc:
00176             Cc: (carbon copy) field (string or list)
00177         mbcc:
00178             Bcc: (blind carbon copy) field (string or list)
00179         subtype:
00180             Content subtype of the email e.g. 'plain' for text/plain (ignored
00181             if message is a email.Message.Message instance)
00182         charset:
00183             Charset used for the email, subject and email addresses
00184         kwargs:
00185             Additional headers
00186         """
00187         mto  = self.emailListToString(mto)
00188         mcc  = self.emailListToString(mcc)
00189         mbcc = self.emailListToString(mbcc)
00190         # validate email addresses
00191         # XXX check Return-Path
00192         for addr in mto, mcc, mbcc:
00193             if addr:
00194                 result = self.validateEmailAddresses(addr)
00195                 if not result:
00196                     raise MailHostError, 'Invalid email address: %s' % addr
00197         result = self.validateSingleEmailAddress(mfrom)
00198         if not result:
00199             raise MailHostError, 'Invalid email address: %s' % mfrom
00200 
00201         # create message
00202         if isinstance(message, email.Message.Message):
00203             # got an email message. Make a deepcopy because we don't want to
00204             # change the message
00205             msg = deepcopy(message)
00206         else:
00207             if isinstance(message, unicode):
00208                 message = message.encode(charset)
00209             msg = email.MIMEText.MIMEText(message, subtype, charset)
00210 
00211         mfrom = encodeHeaderAddress(mfrom, charset)
00212         mto = encodeHeaderAddress(mto, charset)
00213         mcc = encodeHeaderAddress(mcc, charset)
00214         mbcc = encodeHeaderAddress(mbcc, charset)
00215 
00216         # set important headers
00217         self.setHeaderOf(msg, skipEmpty=True, From=mfrom, To=mto,
00218                  Subject=str(email.Header.Header(subject, charset)),
00219                  Cc=mcc, Bcc=mbcc)
00220 
00221         for bad in BAD_HEADERS:
00222             if bad in kwargs:
00223                 raise MailHostError, 'Header %s is forbidden' % bad
00224         self.setHeaderOf(msg, **kwargs)
00225 
00226         # we have to pass *all* recipient email addresses to the
00227         # send method because the smtp server doesn't add CC and BCC to
00228         # the list of recipients
00229         to = msg.get_all('to', [])
00230         cc = msg.get_all('cc', [])
00231         bcc = msg.get_all('bcc', [])
00232         #resent_tos = msg.get_all('resent-to', [])
00233         #resent_ccs = msg.get_all('resent-cc', [])
00234         recipient_list = getaddresses(to + cc + bcc)
00235         all_recipients = [formataddr(pair) for pair in recipient_list]
00236 
00237         # finally send email
00238         return self._send(mfrom, all_recipients, msg, debug=debug)
00239 
00240     security.declarePrivate('setHeaderOf')
00241     def setHeaderOf(self, msg, skipEmpty=False, **kwargs):
00242         """Set the headers of the email.Message based instance
00243 
00244         All occurences of the key are deleted first!
00245         """
00246         for key, val in kwargs.items():
00247             del msg[key] # save - email.Message won't raise a KeyError
00248             if skipEmpty and not val:
00249                 continue
00250             msg[key] = val
00251         return msg
00252 
00253     def _send(self, mfrom, mto, messageText, debug=False):
00254         """Send the message
00255         """
00256         if not isinstance(messageText, email.Message.Message):
00257             message = email.message_from_string(messageText)
00258         else:
00259             message = messageText
00260         smtp_notls = getattr(self, 'smtp_notls', False)
00261         mail = Mail(mfrom, mto, message, smtp_host=self.smtp_host,
00262                     smtp_port=self.smtp_port, userid=self._smtp_userid,
00263                     password=self._smtp_pass, notls=smtp_notls
00264                    )
00265         if debug:
00266             return mail
00267         else:
00268             mail.send()
00269 
00270     security.declarePublic('emailListToString')
00271     def emailListToString(self, addr_list):
00272         """Converts a list of emails to rfc822 conform data
00273 
00274         Input:
00275             ('email', 'email', ...)
00276             or
00277             (('name', 'email'), ('name', 'email'), ...)
00278             or mixed
00279         """
00280         # stage 1: test for type
00281         if not isinstance(addr_list, (list, tuple)):
00282             # a string is supposed to be a valid list of email addresses
00283             # or None
00284             return addr_list
00285         # stage 2: get a list of address strings using email.formataddr
00286         addresses = []
00287         for addr in addr_list:
00288             if isinstance(addr, basestring):
00289                 addresses.append(email.Utils.formataddr(('', addr)))
00290             else:
00291                 if len(addr) != 2:
00292                     raise ValueError(
00293                         "Wrong format: ('name', 'email') is required")
00294                 addresses.append(email.Utils.formataddr(addr))
00295         # stage 3: return the addresses as comma seperated string
00296         return ', '.join(addresses)
00297 
00298     ######################################################################
00299     # copied from CMFPlone 2.0.2 PloneTool.py
00300 
00301     security.declarePublic('validateSingleNormalizedEmailAddress')
00302     def validateSingleNormalizedEmailAddress(self, address):
00303         """Lower-level function to validate a single normalized email
00304         address, see validateEmailAddress
00305         """
00306         if not isinstance(address, basestring):
00307             return False
00308 
00309         sub = EMAIL_CUTOFF_RE.match(address);
00310         if sub != None:
00311             # Address contains two newlines (possible spammer relay attack)
00312             return False
00313 
00314         # sub is an empty string if the address is valid
00315         sub = EMAIL_RE.sub('', address)
00316         if sub == '':
00317             return True
00318         return False
00319 
00320     security.declarePublic('validateSingleEmailAddress')
00321     def validateSingleEmailAddress(self, address):
00322         """Validate a single email address, see also validateEmailAddresses
00323         """
00324         if not isinstance(address, basestring):
00325             return False
00326 
00327         sub = EMAIL_CUTOFF_RE.match(address);
00328         if sub != None:
00329             # Address contains two newlines (spammer attack using
00330             # "address\n\nSpam message")
00331             return False
00332 
00333         if len(getaddresses([address])) != 1:
00334             # none or more than one address
00335             return False
00336 
00337         # Validate the address
00338         for name,addr in getaddresses([address]):
00339             if not self.validateSingleNormalizedEmailAddress(addr):
00340                 return False
00341         return True
00342 
00343     security.declarePublic('validateEmailAddresses')
00344     def validateEmailAddresses(self, addresses):
00345         """Validate a list of possibly several email addresses, see
00346         also validateSingleEmailAddress
00347         """
00348         if not isinstance(addresses, basestring):
00349             return False
00350 
00351         sub = EMAIL_CUTOFF_RE.match(addresses);
00352         if sub != None:
00353             # Addresses contains two newlines (spammer attack using
00354             # "To: list\n\nSpam message")
00355             return False
00356 
00357         # Validate each address
00358         for name,addr in getaddresses([addresses]):
00359             if not self.validateSingleNormalizedEmailAddress(addr):
00360                 return False
00361         return True
00362 
00363     # copied from CMFPlone 2.0.2 PloneTool.py
00364     ######################################################################
00365 
00366 
00367 InitializeClass(SecureMailBase)
00368 
00369 class SecureMailHost(Persistent, SecureMailBase):
00370     "persistent version"