from zope.interface import implements
from zope.component import getMultiAdapter

from interfaces import IMailingFolder, IMailingDocument, \
                       IMailingDocumentVersion

import Globals
from zExceptions import Unauthorized
from AccessControl import ClassSecurityInfo, getSecurityManager
from Products.Silva.Folder import Folder
from Products.SilvaDocument.Document import Document, DocumentVersion
from Products.Silva.ExtensionRegistry import extensionRegistry
from Products.Silva.Versioning import VersioningError
from Products.Silva.interfaces import IAsset
from Products.Silva import SilvaPermissions
from Products.Silva.helpers import add_and_edit
from Products.Silva import mangle

from Products.Formulator.Form import ZMIForm
from Products.Formulator.XMLToForm import XMLToForm
from Products.Formulator.Errors import FormValidationError

import os
from urllib import quote
import cgi
import random
import time

import htmlemail

from Products.SilvaMailing.i18n import translate as _
from zope.i18n import translate

class MailingFolder(Folder):
    """ A container for MailingDocument objects

        this provides functionality to send specific types of Silva documents
        as mailings
    """

    # the next value is used to determine whether pending subscriptions should
    # be removed
    max_verification_time_days = 7

    implements(IMailingFolder)
    meta_type = 'Silva Mailing Folder'
    security = ClassSecurityInfo()

    def __init__(self, id):
        MailingFolder.inheritedAttribute('__init__')(self, id)

        # pending requests, key the email for the request, value a tuple
        # (ver, timestamp, subscribe) where ver is some string (8 hex chars or
        # sth) for verification purposes, timestamp the request time as int,
        # and subscribe a boolean indicating whether this is a subscription
        # (True) or unsubscription (False) request
        self._pending = {}

    security.declareProtected(SilvaPermissions.ApproveSilvaContent,
                              'send_as_mailing')
    def send_as_mailing(self, path, test_only=False, test_email=None):
        """ send the contents of a Silva object as a mailing mail

            note that this requires only ReadSilvaContent permission to allow
            sending test emails by non-editors; a check for editor role is done
            from the code
        """
        obj = self.restrictedTraverse(path.split('/'))
        viewable = obj.get_viewable()
        if not viewable:
            message = _('No public version available')
            # don't send emails with 'no public version'
            raise VersioningError(message)
        title = obj.get_title()
        req = self.REQUEST
        view = getMultiAdapter((obj, req), name='mail.html')
        html = view.__of__(self)()
        if test_only:
            if not test_email:
                message = _('No test email address provided')
                raise ValueError(message)
            addrs = [test_email]
        else:
            addrs = self.subscriptions()
            if test_email:
                addrs.append(test_email)
        for addr in addrs:
            self.send_mail(addr, title, html)

    security.declarePrivate('send_mail')
    def send_mail(self, toaddr, title, html):
        mailhost = getattr(self, self.get_mailhostid())
        fromaddr = self.get_fromaddr()
        email = htmlemail.HTMLEmail(fromaddr, toaddr, title,
                                    html.encode('utf-8'),
                                    html_to_string(html))
        # we encode to asci because of encoding problems with
        # maildrophost. Non-ascii characters will be escaped as xml entities
        msg = email.serialize().decode('utf8').encode('ascii', 'xmlcharrefreplace')
        mailhost._send(fromaddr, toaddr, msg)

    security.declarePrivate('subscribe')
    def subscribe(self, address):
        """ subscribe to the mailings
        """
        # XXX we probably want some email verification process before this is
        # called
        subscriptions = self.subscriptions()
        if address in subscriptions:
            message = _('You are already subscribed')
            raise ValueError(message)
        subscriptions.append(address)
        binding = self.service_metadata.getMetadata(self)
        binding.setValues('silva-mailing-subscriptions',
                          {'subscriptions':
                            '\n'.join(subscriptions).encode('UTF-8')})

    security.declarePrivate('unsubscribe')
    def unsubscribe(self, address):
        """ unsubscribe from the mailing
        """
        # XXX see remark about email verification in subscribe()
        subscriptions = self.subscriptions()
        if address not in subscriptions:
            message = _('Not subscribed')
            raise ValueError(message)
        subscriptions.remove(address)
        binding = self.service_metadata.getMetadata(self)
        binding.setValues('silva-mailing-subscriptions',
                          {'subscriptions':
                            '\n'.join(subscriptions).encode('UTF-8')})

    security.declarePrivate('start_subscription')
    def start_subscription(self, address, subscribe, viewname=None):
        """ start the (un)subscription process

            send out a verification email to check whether (un)subscription
            of this address is indeed desired, add a link to a script calling
            'finish_subscription' below, together with some id to verify that
            this is indeed coming from the email
        """
        # XXX not the optimal place to have this...
        self._cleanup_pending()
        vernum = self._generate_pending_verification_number()

        # allow cancellation of (un)subscription request without verification
        # email hassle

        if address in self._pending:
           # using the same action makes the email get sent again, with
            # the same vernum
            vernum = self._pending[address][0]

        value = (vernum, time.time(), subscribe)
        self._pending[address] = value
        self._p_changed = True
        req = self.REQUEST
        # XXX what was 'the better way' of doing this again?
        req['subscribe'] = subscribe
        req['address'] = address
        req['verification'] = vernum
        if viewname is None:
            viewname = 'verificationmail.html'
        view = getMultiAdapter((self, req), name=viewname)
        html = view()
        subject = self.get_subject()
        self.send_mail(address, subject, html)

    security.declarePrivate('finish_subscription')
    def finish_subscription(self, address, vernum):
        """ finish the subscription process

            this should be called by some Python script or something
            (something that handles form validation etc.), which a client
            browses to on clicking the link in the verification email
        """
        # XXX let's be merciless here... ;)
        self._cleanup_pending()
        orgvernum, timestamp, subscribe = self._pending.pop(address)
        self._p_changed = True
        if orgvernum != vernum:
            message = _('Wrong verification number')
            raise ValueError(message)
        if subscribe:
            self.subscribe(address)
        else:
            self.unsubscribe(address)

    security.declareProtected(SilvaPermissions.ApproveSilvaContent,
                              'subscriptions')
    def subscriptions(self):
        """ returns a list of the subscribed email addresses
        """
        subscriptions = self.service_metadata.getMetadataValue(
            self, 'silva-mailing-subscriptions', 'subscriptions')
        subscriptions = [s.strip() for s in subscriptions.split('\n')
                         if s.strip()]
        return subscriptions

    security.declarePublic('get_mailing_folder')
    def get_mailing_folder(self):
        return self.aq_inner

    security.declareProtected(SilvaPermissions.ChangeSilvaContent,
                              'get_mailhostid')
    def get_mailhostid(self):
        return self.service_metadata.getMetadataValue(
            self, 'silva-mailing-subscriptions', 'mailhostid')

    security.declareProtected(SilvaPermissions.ChangeSilvaContent,
                              'get_fromaddr')
    def get_fromaddr(self):
        return self.service_metadata.getMetadataValue(
            self, 'silva-mailing-subscriptions', 'fromaddr')

    security.declareProtected(SilvaPermissions.ChangeSilvaContent,
                              'get_subject')
    def get_subject(self):
        return self.service_metadata.getMetadataValue(
            self, 'silva-mailing-subscriptions', 'subject')

    security.declarePublic('subscribe_to_mailing')
    def subscribe_to_mailing(self, viewname=None):
        """ subscribe or unsubscribe

            this is called on submitting subscription_form

            this performs the first step of the (un)subscription action,
            sending out an email for confirmation - the remaining part is
            handled in finish_subscribe_to_mailing below

            note that this has the side-effect of cleaning up the list
            of pending subscription requests
        """
        req = self.REQUEST
        form = self.subscription_form.subscription_form
        try:
            result = form.validate_all(req)
        except FormValidationError, e:
            message_type = 'error'
            message = _('Invalid email address')
        else:
            message_type = 'success'
            address = result['address']
            subscribe = result['subscribe'] == 'subscribe'
            subscriptions = self.subscriptions()
            if subscribe and self._pending.has_key(address):
                message_type = 'error'
                message = _('Your request is pending, please wait for the verification mail')
                self.start_subscription(address, subscribe, viewname)

            elif subscribe and address in subscriptions:
                message_type = 'error'
                message = _('You are already subscribed')
            elif not subscribe and address not in subscriptions:
                message_type = 'error'
                message = _('You are not subscribed yet')
            else:
                self.start_subscription(address, subscribe, viewname)

                max_days = str(self.max_verification_time_days)

                message_type = 'feedback'
                message = _('A verification email was sent to your address, '
                            'please respond within ${maxdays} days to finish '
                            'the process.', mapping={'maxdays':max_days})
        return message_type, message

    security.declarePublic('finish_subscription_verification')
    def finish_subscription_verification(self):
        """ public handler for finishing the subscription verification process

            this calls 'finish_verification' when the form has validated
            properly
        """
        request = self.REQUEST
        if (not request.has_key('address') or
                not request.has_key('verification')):
            message_type = 'error'
            msg = _('Missing values, please use this only for (un)subscription verification')
            msg = translate(msg)
            request.response.redirect('%s?message=%s&message_type=%s' % (
                                      self.absolute_url(), quote(msg),
                                      quote(message_type)))
            return ''
        try:
            self.finish_subscription(unicode(request['address'], 'UTF-8'),
                                     request['verification'])
        except KeyError:
            message_type = 'error'
            msg = _('Unknown email address - your subscription request may have taken too long')
            msg = translate(msg)

        except ValueError:
            message_type = 'error'
            msg = _('Verification failed: please use the link provided in'
            ' the verification email to finish (un)subscribing, if that fails'
            ' please re-start the entire process')
            msg = translate(msg)
        else:
            message_type = 'feedback'
            if request['action'] == 'subscribe':
                msg = _('Your subscription was successful')
                msg = translate(msg)
            elif request['action'] == 'unsubscribe':
                msg = _('You have been unsubscribed')
                msg = translate(msg)
            else:
                msg = ''

        request.response.redirect('%s?message=%s&message_type=%s' % (
                                  self.absolute_url(), quote(msg),
                                  quote(message_type)))
        return ''

    def get_silva_addables_allowed(self):
        secman = getSecurityManager()
        ret = []
        for addable_dict in extensionRegistry.get_addables():
            instance = addable_dict['instance']
            name = addable_dict['name']
            if ((IAsset.implementedBy(instance) or
                    IMailingDocument.implementedBy(instance) or
                    instance.meta_type == 'Silva Folder') and
                    secman.checkPermission('Add %ss' % (name,), self)):
                ret.append(name)
        return ret

    def _generate_pending_verification_number(self):
        # note that this doesn't have to return a _unique_ number, it's just
        # for verification purposes, the email address is the key in the dict
        l = list('0123456789abcdef')
        random.shuffle(l)
        return ''.join(l[:8])

    def _cleanup_pending(self):
        deltime = time.time() - (self.max_verification_time_days * 24 * 3600)
        todel = []
        for k, v in self._pending.iteritems():
            if v[1] < deltime:
                todel.append(k)
        if not todel:
            return
        for k in todel:
            self._pending.pop(k)
        self._p_changed = True

Globals.InitializeClass(MailingFolder)

def manage_addMailingFolder(context, id, title, REQUEST=None):
    """Add a Mailing Folder."""
    if not mangle.Id(context, id).isValid():
        return
    obj = MailingFolder(id)
    context._setObject(id, obj)
    obj = getattr(context, id)
    obj.set_title(title)

    # add codesource for submit form
    obj.manage_addProduct['SilvaMailing'].manage_addMailingSubscriptionForm(
        'subscription_form', 'Subscription form')
    # add document to display form
    obj.manage_addProduct['SilvaDocument'].manage_addDocument('index', title)
    # add form as content
    index = obj.index
    editable = index.get_editable()
    editable.content.manage_edit(
        '<doc>'
        '<p type="normal">Fill in the form below to subscribe to the mailing. '
        'An email will be sent to your address for verification: to '
        'finish the process you will have to click the link inside that '
        'email.</p>'
        '<source id="subscription_form"/>'
        '</doc>')

    add_and_edit(context, id, REQUEST)
    return ''

class MailingDocument(Document):
    implements(IMailingDocument)
    meta_type = 'Silva Mailing Document'
    security = ClassSecurityInfo()

    security.declareProtected(SilvaPermissions.ApproveSilvaContent,
                              'tab_status_form_mailing')
    def tab_status_form_mailing(self):
        """ form for mailing sending
        """
        fp = open(os.path.join(Globals.package_home(globals()), 'www',
                               'form_mailing.form'))
        try:
            xml = fp.read()
        finally:
            fp.close()
        req = self.REQUEST
        req['model'] = self
        form = ZMIForm('tab_status_form_mailing', 'Mailing form')
        XMLToForm(xml, form)
        return form.__of__(self)

    security.declareProtected(SilvaPermissions.ReadSilvaContent,
                              'is_editor')
    def is_editor(self):
        secman = getSecurityManager()
        return secman.checkPermission('Approve Silva Content', self)

Globals.InitializeClass(MailingDocument)

class MailingDocumentVersion(DocumentVersion):
    implements(IMailingDocumentVersion)
    meta_type = 'Silva Mailing Document Version'
    security = ClassSecurityInfo()

Globals.InitializeClass(MailingDocumentVersion)

# utility functions
def html_to_string(html):
    from xhtml2rest.xhtml2rest import main
    from StringIO import StringIO
    html = html.encode('utf-8')
    s = StringIO()
    main(html, s)
    ret = s.getvalue()
    return ret

