mail.py   [plain text]


# -*- test-case-name: twisted.test.test_mail -*-
# Twisted, the Framework of Your Internet
# Copyright (C) 2001 Matthew W. Lefkowitz
# 
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""Mail support for twisted python.
"""

# Twisted imports
from twisted.protocols import smtp
from twisted.python import components
from twisted.internet import defer
from twisted.application import service, internet
from twisted.python import util
from twisted.python import log

from twisted import cred
import twisted.cred.portal

# Sibling imports
import protocols

# System imports
import os

class DomainWithDefaultDict:
    '''Simulate a dictionary with a default value for non-existing keys.
    '''
    def __init__(self, domains, default):
        self.domains = domains
        self.default = default

    def setDefaultDomain(self, domain):
        self.default = domain
    
    def has_key(self, name):
        return 1

    def fromkeys(klass, keys, value=None):
        d = klass()
        for k in keys:
            d[k] = value
        return d
    fromkeys = classmethod(fromkeys)

    def __contains__(self, name):
        return 1

    def __getitem__(self, name):
        return self.domains.get(name, self.default)

    def __setitem__(self, name, value):
        self.domains[name] = value
    
    def __delitem__(self, name):
        del self.domains[name]
    
    def __iter__(self):
        return iter(self.domains)
    
    def __len__(self):
        return len(self.domains)
    
    def __str__(self):
        return '<DomainWithDefaultsDict %s>' % (self.domains,)
    
    def __repr__(self):
        return 'DomainWithDefaultsDict(%s)>' % (self.domains,)
    
    def get(self, key, default=None):
        return self.domains.get(key, default)
    
    def copy(self):
        return DomainWithDefaultsDict(self.domains.copy(), self.default)
    
    def iteritems(self):
        return self.domains.iteritems()
    
    def iterkeys(self):
        return self.domains.iterkeys()
    
    def itervalues(self):
        return self.domains.itervalues()
    
    def keys(self):
        return self.domains.keys()
    
    def values(self):
        return self.domains.values()

    def items(self):
        return self.domains.items()

    def popitem(self):
        return self.domains.popitem()
    
    def update(self, other):
        return self.domains.update(other)
    
    def clear(self):
        return self.domains.clear()
    
    def setdefault(self, key, default):
        return self.domains.setdefault(key, default)

class IDomain(components.Interface):
    """An email domain."""

    def exists(self, user):
        """
        Check whether or not the specified user exists in this domain.
        
        @type user: C{twisted.protocols.smtp.User}
        @param user: The user to check
        
        @rtype: No-argument callable
        @return: A C{Deferred} which becomes, or a callable which
        takes no arguments and returns an object implementing C{IMessage}.
        This will be called and the returned object used to deliver the
        message when it arrives.
        
        @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
        user does not exist in this domain.
        """

    def addUser(self, user, password):
        """Add a username/password to this domain."""
    
    def startMessage(self, user):
        """Create and return a new message to be delivered to the given user.
        
        DEPRECATED.  Implement validateTo() correctly instead.
        """

    def getCredentialsCheckers(self):
        """Return a list of ICredentialsChecker implementors for this domain.
        """

class IAliasableDomain(IDomain):
    def setAliasGroup(self, aliases):
        """Set the group of defined aliases for this domain
        
        @type aliases: C{dict}
        @param aliases: Mapping of domain names to objects implementing
        C{IAlias}
        """
    
    def exists(self, user, memo=None):
        """
        Check whether or not the specified user exists in this domain.
        
        @type user: C{twisted.protocols.smtp.User}
        @param user: The user to check
        
        @type memo: C{dict}
        @param memo: A record of the addresses already considered while
        resolving aliases.  The default value should be used by all
        external code.
        
        @rtype: No-argument callable
        @return: A C{Deferred} which becomes, or a callable which
        takes no arguments and returns an object implementing C{IMessage}.
        This will be called and the returned object used to deliver the
        message when it arrives.
        
        @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
        user does not exist in this domain.
        """

class BounceDomain:
    """A domain in which no user exists. 

    This can be used to block off certain domains.
    """

    __implements__ = (IDomain,)
    
    def exists(self, user):
        raise smtp.SMTPBadRcpt(user)
    
    def willRelay(self, user, protocol):
        return False
    
    def addUser(self, user, password):
        pass
    
    def startMessage(self, user):
        raise AssertionError, "No code should ever call this method for any reason"
    
    def getCredentialsCheckers(self):
        return []

class FileMessage:
    """A file we can write an email too."""
    
    __implements__ = smtp.IMessage

    def __init__(self, fp, name, finalName):
        self.fp = fp
        self.name = name
        self.finalName = finalName

    def lineReceived(self, line):
        self.fp.write(line+'\n')

    def eomReceived(self):
        self.fp.close()
        os.rename(self.name, self.finalName)
        return defer.succeed(self.finalName)

    def connectionLost(self):
        self.fp.close()
        os.remove(self.name)

class MailService(service.MultiService):
    """An email service."""

    queue = None
    domains = None
    portals = None
    aliases = None
    smtpPortal = None

    def __init__(self):
        service.MultiService.__init__(self)
        # Domains and portals for "client" protocols - POP3, IMAP4, etc
        self.domains = DomainWithDefaultDict({}, BounceDomain())
        self.portals = {}

        self.monitor = FileMonitoringService()
        self.monitor.setServiceParent(self)
        self.smtpPortal = cred.portal.Portal(self)

    def getPOP3Factory(self):
        return protocols.POP3Factory(self)

    def getSMTPFactory(self):
        return protocols.SMTPFactory(self, self.smtpPortal)

    def getESMTPFactory(self):
        return protocols.ESMTPFactory(self, self.smtpPortal)
    
    def addDomain(self, name, domain):
        portal = cred.portal.Portal(domain)
        map(portal.registerChecker, domain.getCredentialsCheckers())
        self.domains[name] = domain
        self.portals[name] = portal
        if self.aliases and components.implements(domain, IAliasableDomain):
            domain.setAliasGroup(self.aliases)

    def setQueue(self, queue):
        """Set the queue for outgoing emails."""
        self.queue = queue

    def requestAvatar(self, avatarId, mind, *interfaces):
        if smtp.IMessageDelivery in interfaces:
            a = protocols.ESMTPDomainDelivery(self, avatarId)
            return smtp.IMessageDelivery, a, lambda: None
        raise NotImplementedError()

    def lookupPortal(self, name):
        return self.portals[name]
    
    def defaultPortal(self):
        return self.portals['']


class FileMonitoringService(internet.TimerService):
    
    def __init__(self):
        self.files = []
        self.intervals = iter(util.IntervalDifferential([], 60))

    def startService(self):
        service.Service.startService(self)
        self._setupMonitor()

    def _setupMonitor(self):
        from twisted.internet import reactor
        t, self.index = self.intervals.next()
        self._call = reactor.callLater(t, self._monitor)
    
    def stopService(self):
        service.Service.stopService(self)
        if self._call:
            self._call.cancel()
            self._call = None
    
    def monitorFile(self, name, callback, interval=10):
        try:
            mtime = os.path.getmtime(name)
        except:
            mtime = 0
        self.files.append([interval, name, callback, mtime])
        self.intervals.addInterval(interval)

    def unmonitorFile(self, name):
        for i in range(len(self.files)):
            if name == self.files[i][1]:
                self.intervals.removeInterval(self.files[i][0])
                del self.files[i]
                break
     
    def _monitor(self):
        self._call = None
        if self.index is not None:
            name, callback, mtime = self.files[self.index][1:]
            try:
                now = os.path.getmtime(name)
            except:
                now = 0
            if now > mtime:
                log.msg("%s changed, notifying listener" % (name,))
                self.files[self.index][3] = now
                callback(name)
        self._setupMonitor()