# -*- 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 '' % (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()