# -*- test-case-name: twisted.mail.test.test_mail -*-
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
# See LICENSE for details.


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

# Twisted imports
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
from twisted.mail import protocols, smtp

# System imports
import os
from zope.interface import implements


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()
