from email import message_from_string
from rfc822 import AddressList

from Acquisition import aq_parent
from zExceptions import NotFound

from zope.interface import implements
from zope.component import getUtility
from zope.app.event.interfaces import IObjectModifiedEvent
from zope.app.exception.interfaces import UserError
from zope.app.container.interfaces import IObjectAddedEvent
from zope.app.event.objectevent import IObjectCreatedEvent
from zope.app.container.interfaces import IObjectMovedEvent
from zope.app.container.interfaces import IObjectRemovedEvent
from zope.app.container.interfaces import IContainerModifiedEvent
from zope.app.component.hooks import setSite

from OFS.interfaces import IObjectWillBeRemovedEvent
from OFS.interfaces import IObjectWillBeMovedEvent
from OFS.interfaces import IObjectWillBeAddedEvent
from OFS.SimpleItem import SimpleItem
from BTrees.OOBTree import OOBTree

from Products.Five import BrowserView

from Products.MailBoxer.MailBoxer import MAIL_PARAMETER_NAME

from Products.listen.interfaces import IListLookup

# XXX debugging function added by PW to figure out this bug
# http://trac.openplans.org/openplans/ticket/2055
import logging
logger = logging.getLogger('listen.list_lookup')
import traceback
def log_traceback(msg):
    lines = traceback.format_stack()
    msg = '%s. Stack trace follows:\n%s' % (msg, ''.join(lines))
    logger.info(msg)

class ListLookup(SimpleItem):
    """ An implementation of IListLookup which uses to To address in a message,
        to lookup which list to send a message to.

    Some framework setup:
        >>> import Products.Five
        >>> from Products.Five import zcml
        >>> zcml.load_config('meta.zcml', Products.Five)
        >>> zcml.load_config('permissions.zcml', Products.Five)
        >>> zcml.load_config("configure.zcml", Products.Five.site)
        >>> from Products.listen.utilities import tests
        >>> zcml.load_config('configure.zcml', tests)

    Now let's make a fake mailing list in our site
        >>> from Products.listen.utilities.tests import FakeMailingList
        >>> ml = FakeMailingList('ml')
        >>> ml_id = self.folder._setObject('ml', ml)
        >>> ml = getattr(self.folder, ml_id)
        >>> ml.mailto = 'fake_address@example.com'

    Create our utility:
        >>> from Products.listen.utilities.list_lookup import ListLookup
        >>> ll = ListLookup('list_lookup').__of__(self.folder)

    Register the list:
        >>> ll.registerList(ml)
        >>> ll.getListForAddress(ml.mailto) == ml
        True

    Attempt to register it under another address:
        >>> from zope.app.exception.interfaces import UserError
        >>> ml.mailto = 'another@example.com'
        >>> try:
        ...     ll.registerList(ml)
        ... except UserError:
        ...     print "Raised expected error"
        ...
        Raised expected error
        >>> ll.getListForAddress(ml.mailto) == ml
        False

    Update the list address to the new address:
        >>> ll.updateList(ml)
        >>> ll.getListForAddress(ml.mailto) == ml
        True

    Add another list with the same address:
        >>> ml2 = FakeMailingList('ml2')
        >>> ml_id = self.folder._setObject('ml2', ml2)
        >>> ml2 = getattr(self.folder, ml_id)
        >>> ml2.mailto = ml.mailto
        >>> try:
        ...     ll.registerList(ml2)
        ... except UserError:
        ...     print "Raised expected error"
        ...
        Raised expected error

    Try to update an unregistered list:
        >>> try:
        ...     ll.updateList(ml2)
        ... except UserError:
        ...     print "Raised expected error"
        ...
        Raised expected error

    Let's try unregistering:
        >>> ll.unregisterList(ml)
        >>> ll.getListForAddress(ml.mailto)

    Unregistering a list that isn't registered shouldn't cause any problems:
        >>> ll.unregisterList(ml2)

    Let's send a mail:
        >>> ll.registerList(ml)
        >>> ll.deliverMessage({'Mail':'x-original-to: another@example.com\\r\\nTo: another@example.com\\r\\nFrom: me@me.com\\r\\nSubject: Bogus\\r\\n\\r\\nTest'})
        'Success another@example.com'

    And with an SMTP that doesn't set x-original-to:
        >>> ll.deliverMessage({'Mail':'To: another@example.com\\r\\nFrom: me@me.com\\r\\nSubject: Bogus\\r\\n\\r\\nTest'})
        'Success another@example.com'

    And another to a bad address:
        >>> from zExceptions import NotFound
        >>> try:
        ...     ll.deliverMessage({'Mail':'x-original-to: a_bad_address@example.com\\r\\nTo: another2@example.com\\r\\nFrom: me@me.com\\r\\nSubject: Bogus\\r\\n\\r\\nTest'})
        ... except NotFound:
        ...     print "Raised expected error"
        ...
        Raised expected error
    """

    implements(IListLookup)

    def __init__(self, id):
        log_traceback("ListLookup.__init__ called, should only ever happen once")
        self.id = id
        self._mapping = OOBTree()
        self._reverse = OOBTree()
        self.__name__ = 'listen_lookup'

    # We need to provide a __parent__ property to be registerable
    def _getParent(self):
        return aq_parent(self)
    __parent__ = property(_getParent)

    def registerList(self, ml):
        """See IListLookup interface documentation"""
        address = ml.mailto
        # normalize case
        if not address:
            # Our list does not have an address yet, this only happens when
            # the add form wasn't used.
            return
        address = address.lower()
        path = '/'.join(ml.getPhysicalPath())
        current_addr = self._reverse.get(path, None)
        current_path = self._mapping.get(address, None)
        if current_addr is not None:
            raise UserError, "This list is already registered, use "\
                             "updateList to change the address."
        if current_path is not None:
            raise UserError, "A list is already registered for this address,"\
                             " you must unregister it first."
        self._mapping[address] = path
        self._reverse[path] = address

    def updateList(self, ml):
        """See IListLookup interface documentation"""
        address = ml.mailto or ''
        # normalize case
        address = address.lower()
        path = '/'.join(ml.getPhysicalPath())
        current_addr = self._reverse.get(path, None)
        current_path = self._mapping.get(address, None)
        if (current_path is None and current_addr is not None and
                                                     current_addr != address):
            # The mailing list address has changed to one which is unknown
            del self._mapping[current_addr]
            self._reverse[path] = address
            self._mapping[address] = path
        elif current_addr == address and current_path == path:
            # Nothing has changed, do nothing
            pass
        elif current_addr is None and current_path is None:
            # The list is not registered at all, this happens when the addform
            # was not used, stupid CMF
            self.registerList(ml)
        else:
            # The new address is already registered
            raise UserError, "A list is already registered for this address"

    def unregisterList(self, ml):
        """See IListLookup interface documentation"""
        address = ml.mailto
        # normalize case
        current_ml = self._mapping.get(address, None)
        if not address:
            # We are deleting a list without an address
            if current_ml is not None:
                log_traceback("unregistering reverse entry for addressless list %r" % current_ml)
                del self._reverse[current_ml]
            return
        address = address.lower()
        if current_ml == '/'.join(ml.getPhysicalPath()):
            log_traceback("unregistering entries for address %r, path %r" %
                          (address, current_ml))
            del self._mapping[address]
            del self._reverse[current_ml]

    def getListForAddress(self, address):
        """See IListLookup interface documentation"""
        list_path = self._mapping.get(address, None)
        if list_path is not None:
            ml = self.unrestrictedTraverse(list_path)
            return ml
        return None

    def deliverMessage(self, request):
        """See IListLookup interface documentation"""
        # XXX raising NotFound annoyingly hides the real problem so
        # I've added a bunch of logging.  I propose in the future we
        # change NotFound to something that actually gets logged.  But
        # I'm afraid to do that now because I don't know if we somehow
        # depend on getting a 404 here.
        message = str(request.get(MAIL_PARAMETER_NAME, None))
        if message is not None:
            message = message_from_string(message)
        else:
            logger.error("request.get(%s) returned None" % MAIL_PARAMETER_NAME)
            raise NotFound, "The message destination cannot be deterimined."
        # preferentially use the x-original-to header (is this postfix only?),
        # so that mails to multiple lists are handled properly
        address = message.get('x-original-to', None)
        if not address:
            address = message['to']
            cc = message['cc']
            if address and cc:
                    address = address + ', ' + cc
            elif cc:
                address = cc
        # normalize case
        if not address:
            import pprint
            logger.error("No destination found in headers:\n%s" % pprint.pformat(message))
            raise NotFound, "The message destination cannot be deterimined."
        address = address.lower()
        if '-manager@' in address:
            address = address.replace('-manager@','@')
        address_list = AddressList(address)
        for ml_address in address_list:
           ml = self.getListForAddress(ml_address[1])
           if ml is not None:
               break
        else:
            # raise an error on bad requests, so that the SMTP server can
            # send a proper failure message.
            logger.error("no list found for any of %r" % str(address_list))
            raise NotFound, "The message address does not correspond to a "\
                             "known mailing list."
        setSite(ml)
        return ml.manage_mailboxer(request)

    def showAddressMapping(self):
        return [{'address':k, 'path':v} for k,v in self._mapping.items()]

    def purgeInvalidEntries(self):
        counter = 0
        for path in self._reverse.keys():
            list_obj = self.unrestrictedTraverse(path, None)
            if list_obj is None:
                address = self._reverse[path]
                log_traceback("Purging a possibly missing list at path %r, address %r" % ( path, address))
                del self._mapping[address]
                del self._reverse[path]
                counter += 1
        return counter


def changeMailingList(ml, event):
    """An event listener which registers and unregisters lists with the
       ListLookup utility on list changes.

    Some framework setup:
        >>> from zope.app.testing.placelesssetup import setUp, tearDown
        >>> import Products.Five
        >>> from Products.Five import zcml
        >>> setUp()
        >>> zcml.load_config('meta.zcml', package=Products.Five)
        >>> zcml.load_config('permissions.zcml', package=Products.Five)
        >>> zcml.load_config("configure.zcml", package=Products.Five.site)
        >>> from Products.listen.utilities import tests
        >>> zcml.load_config('configure.zcml', package=tests)
        >>> from zope.app.tests import ztapi

    Now let's make a fake mailing list in our site
        >>> from Products.listen.interfaces import IMailingList
        >>> from Products.listen.utilities.tests import FakeMailingList
        >>> from zope.interface import directlyProvides
        >>> ml = FakeMailingList('ml')
        >>> directlyProvides(ml, IMailingList)
        >>> ml_id = self.folder._setObject('ml', ml)
        >>> ml = self.folder.ml
        >>> ml.mailto = 'fake_address@example.com'

    Then we setup out listener
        >>> from zope.app.event.interfaces import IObjectModifiedEvent
        >>> from zope.app.container.interfaces import IObjectMovedEvent
        >>> from OFS.interfaces import IObjectWillBeRemovedEvent
        >>> from Products.listen.utilities.list_lookup import changeMailingList
        >>> ztapi.handle([IMailingList, IObjectWillBeRemovedEvent], changeMailingList)
        >>> ztapi.handle([IMailingList, IObjectMovedEvent], changeMailingList)
        >>> ztapi.handle([IMailingList, IObjectModifiedEvent], changeMailingList)

    Create and register our utility:
        >>> from Products.listen.interfaces import IListLookup
        >>> from Products.listen.utilities.list_lookup import ListLookup
        >>> from zope.app.component.hooks import setSite
        >>> from zope.component import getUtility
        >>> from Products.Five.site.localsite import enableLocalSiteHook
        >>> app = self.folder
        >>> enableLocalSiteHook(app)
        >>> setSite(app)
        >>> sm = app.getSiteManager()
        >>> sm.registerUtility(IListLookup, ListLookup('list_lookup'))
        >>> ll = getUtility(IListLookup, context=self.folder)
        >>> print ll.getListForAddress(ml.mailto)
        None

    Send our added event:
        >>> from zope.event import notify
        >>> from zope.app.container.contained import ObjectAddedEvent
        >>> notify(ObjectAddedEvent(ml, app, 'ml'))
        >>> ll.getListForAddress(ml.mailto) == ml
        True

    Change the list and send an event:
        >>> ml.mailto = 'test2@example.com'
        >>> from zope.app.event.objectevent import ObjectModifiedEvent
        >>> notify(ObjectModifiedEvent(ml))
        >>> ll.getListForAddress(ml.mailto) == ml
        True

    Send a before removal event, which should do nothing, as we rely on before
    removal:
        >>> from zope.app.container.contained import ObjectRemovedEvent
        >>> notify(ObjectRemovedEvent(ml, app, 'ml'))
        >>> ll.getListForAddress(ml.mailto) == ml
        True

    Send a before removal event:
        >>> from OFS.event import ObjectWillBeRemovedEvent
        >>> notify(ObjectWillBeRemovedEvent(ml))
        >>> ll.getListForAddress(ml.mailto) is None
        True

        >>> tearDown()
    """
    # Use the new parent object as the context, unless it is unavailable, then
    # use the list itself
    parent = None
    if hasattr(event, 'newParent'):
        parent = event.newParent
    if parent is None:
        parent = ml
    
    ll = getUtility(IListLookup, context=parent)
    if IObjectWillBeAddedEvent.providedBy(event):
        # Registration is taken care of after add
        pass
    elif IObjectWillBeRemovedEvent.providedBy(event):
        ll.unregisterList(ml)
    elif IObjectWillBeMovedEvent.providedBy(event):
        ll.unregisterList(ml)
    elif IObjectRemovedEvent.providedBy(event):
        # Unregistration is taken care of before removal
        pass
    elif IObjectModifiedEvent.providedBy(event):
        if not IContainerModifiedEvent.providedBy(event):
            ll.updateList(ml)
    elif IObjectAddedEvent.providedBy(event) or IObjectCreatedEvent.providedBy(event):
        ll.registerList(ml)
    elif IObjectMovedEvent.providedBy(event):
        ll.registerList(ml)


class ListLookupView(BrowserView):
    """A view for the list lookup which provides a form action"""
    def purge_entries(self):
        count = self.context.purgeInvalidEntries()
        return "Purged %s invalid entries"%count


class MailDeliveryView(BrowserView):
    """A simple view which hands off requests to the list lookup utility"""

    def __call__(self):
        ll = getUtility(IListLookup, context=self.context)
        return ll.deliverMessage(self.request)
