diff --git a/MailHost.py b/MailHost.py new file mode 100644 index 0000000..daeb5d5 --- /dev/null +++ b/MailHost.py @@ -0,0 +1,394 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation 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. +# +############################################################################## +"""SMTP mail objects + +$Id$ +""" +from cStringIO import StringIO +import logging +import mimetools +import rfc822 +from threading import Lock +import time + +from AccessControl.SecurityInfo import ClassSecurityInfo +from AccessControl.Permissions import change_configuration, view +from AccessControl.Permissions import use_mailhost_services +from AccessControl.Role import RoleManager +from Acquisition import Implicit +from App.class_init import default__class_init__ as InitializeClass +from App.special_dtml import DTMLFile +from DateTime.DateTime import DateTime +from Persistence import Persistent +from OFS.SimpleItem import Item + +from zope.interface import implements +from zope.sendmail.maildir import Maildir +from zope.sendmail.delivery import DirectMailDelivery, QueuedMailDelivery, \ + QueueProcessorThread + +from interfaces import IMailHost +from decorator import synchronized + +# Use our own TLS/SSL-aware mailer since the zope.sendmail does +# not support TLS/SSL in this version (should be replaced with +# the next version) +from mailer import SMTPMailer + +queue_threads = {} # maps MailHost path -> queue processor threada + +LOG = logging.getLogger('MailHost') + +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, + ): + """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) + + 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, + ): + + messageText, mto, mfrom = _mungeHeaders(messageText, + mto, mfrom, subject) + 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 + ) + + @synchronized(lock) + def _stopQueueProcessorThread(self): + """ Stop thread for processing the mail queue """ + + path = self.absolute_url(1) + if queue_threads.has_key(path): + thread = queue_threads[path] + thread.stop() + while thread.isAlive(): + # wait until thread is really dead + time.sleep(0.3) + del queue_threads[path] + LOG.info('Thread for %s stopped' % path) + + @synchronized(lock) + def _startQueueProcessorThread(self): + """ Start thread for processing the mail queue """ + + path = self.absolute_url(1) + if not queue_threads.has_key(path): + thread = QueueProcessorThread() + thread.setMailer(self._makeMailer()) + thread.setQueuePath(self.smtp_queue_directory) + thread.start() + queue_threads[path] = thread + LOG.info('Thread for %s started' % path) + + 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.absolute_url(1)) + 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 _encode(body, encode=None): + if encode is None: + return body + mfile = StringIO(body) + mo = mimetools.Message(mfile) + if mo.getencoding() != '7bit': + 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() + +def _mungeHeaders( messageText, mto=None, mfrom=None, subject=None): + """Sets missing message headers, and deletes Bcc. + returns fixed message, fixed mto and fixed mfrom""" + mfile = StringIO(messageText.lstrip()) + mo = rfc822.Message(mfile) + + # 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'): + 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) + else: + mto = [] + for header in ('To', 'Cc', 'Bcc'): + v = mo.getheader(header) + if v: + mto += [rfc822.dump_address_pair(addr) + for addr in rfc822.AddressList(v)] + if not mto: + raise MailHostError, "No message recipients designated" + + if mfrom: + mo['From'] = mfrom + else: + if mo.getheader('From') is None: + raise MailHostError,"Message missing SMTP Header 'From'" + mfrom = mo['From'] + + if mo.getheader('Bcc'): + mo.__delitem__('Bcc') + + if not mo.getheader('Date'): + mo['Date'] = DateTime().rfc822() + + mo.rewindbody() + finalmessage = mo + finalmessage = mo.__str__() + '\n' + mfile.read() + mfile.close() + return finalmessage, mto, mfrom