
# Copyright (c) 2005-2015 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

from twext.python.log import Logger
from txweb2 import responsecode
from txweb2.http import HTTPError, Response
from txweb2.http_headers import MimeType

from twisted.internet.abstract import isIPAddress
from twisted.internet
.defer import inlineCallbacks, returnValue

from twistedcaldav.confi
g import config
from twistedcaldav.ical import normalizeCUAddress

from txdav.caldav.datastore.scheduling import addressmapping
from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, \
    calendarUserFromCalendarUserAddress, RemoteCalendarUser, \
    OtherServerCalendarUser
from txdav.caldav.datastore.scheduling.ischedule import xml
from txdav.caldav.datastore.scheduling.ischedule.dkim import DKIMVerifier, \
    DKIMVerificationError, DKIMMissingError
from txdav.caldav.datastore.scheduling.ischedule.remoteservers import IScheduleServers
from txdav.caldav.datastore.scheduling.ischedule.utils import getIPsFromHost
from txdav.caldav.datastore.scheduling.ischedule.xml import ischedule_namespace
import txdav.caldav.datastore.scheduling.ischedule.xml as ixml
from txdav.caldav.datastore.scheduling.scheduler import RemoteScheduler, \
    ScheduleResponseQueue
from txdav.caldav.datastore.util import normalizationLookup
from txdav.xml.base import WebDAVUnknownElement

import itertools
import re
import socket
import urlparse

"""
L{IScheduleScheduler} - handles deliveries for scheduling messages being POSTed to the iSchedule inbox.
"""

__all__ = [
    "IScheduleScheduler",
]


log = Logger()


class ErrorResponse(Response):
    """
    A L{Response} object which contains a status code and a L{element.Error}
    element.
    Renders itself as a DAV:error XML document.
    """
    error = None
    unregistered = True     # base class is already registered

    def __init__(self, code, error, description=None):
        """
        @param code: a response code.
        @param error: an L{WebDAVElement} identifying the error, or a
            tuple C{(namespace, name)} with which to create an empty element
            denoting the error.  (The latter is useful in the case of
            preconditions and postconditions, not all of which have defined
            XML element classes.)
        @param description: an optional string that, if present, will get
            wrapped in a (twisted_dav_namespace, error-description) element.
        """
        if type(error) is tuple:
            xml_namespace, xml_name = error
            error = WebDAVUnknownElement()
            error.namespace = xml_namespace
            error.name = xml_name

        self.description = description
        if self.description:
            output = ixml.Error(error, ixml.ResponseDescription(self.description)).toxml()
        else:
            output = ixml.Error(error).toxml()

        Response.__init__(self, code=code, stream=output)

        self.headers.setHeader("content-type", MimeType("text", "xml"))

        self.error = error


    def __repr__(self):
        return "<{} {} {}>".format(self.__class__.__name__, self.code, self.error.sname())



class IScheduleResponseQueue (ScheduleResponseQueue):
    """
    Stores a list of (typically error) responses for use in a
    L{ScheduleResponse}.
    """

    schedule_response_element = xml.ScheduleResponse
    response_element = xml.Response
    recipient_element = xml.Recipient
    recipient_uses_href = False
    request_status_element = xml.RequestStatus
    error_element = xml.Error
    response_description_element = xml.ResponseDescription
    calendar_data_element = xml.CalendarData



class IScheduleScheduler(RemoteScheduler):
    """
    Handles iSchedule and podding requests.
    """

    scheduleResponse = IScheduleResponseQueue

    errorResponse = ErrorResponse

    errorElements = {
        "originator-missing": (ischedule_namespace, "originator-missing"),
        "originator-invalid": (ischedule_namespace, "originator-invalid"),
        "originator-denied": (ischedule_namespace, "originator-denied"),
        "recipient-missing": (ischedule_namespace, "recipient-missing"),
        "recipient-invalid": (ischedule_namespace, "recipient-invalid"),
        "organizer-denied": (ischedule_namespace, "organizer-denied"),
        "attendee-denied": (ischedule_namespace, "attendee-denied"),
        "invalid-calendar-data-type": (ischedule_namespace, "invalid-calendar-data-type"),
        "invalid-calendar-data": (ischedule_namespace, "invalid-calendar-data"),
        "invalid-scheduling-message": (ischedule_namespace, "invalid-scheduling-message"),
        "max-recipients": (ischedule_namespace, "max-recipients"),
    }

    def __init__(self, txn, originator_uid, logItems=None, noAttendeeRefresh=False, podding=False):
        super(IScheduleScheduler, self).__init__(txn, originator_uid, logItems=logItems, noAttendeeRefresh=noAttendeeRefresh)
        self._podding = podding


    @inlineCallbacks
    def doSchedulingViaPOST(self, remoteAddr, headers, body, calendar, originator, recipients):
        """
        Carry out iSchedule specific processing.
        """

        self.remoteAddr = remoteAddr
        self.headers = headers
        self.verified = False

        if not self._podding and config.Scheduling.iSchedule.DKIM.Enabled:
            verifier = DKIMVerifier(self.headers, body, protocol_debug=config.Scheduling.iSchedule.DKIM.ProtocolDebug)
            try:
                yield verifier.verify()
                self.verified = True

            except DKIMMissingError:
                # Carry on processing, but we will do extra checks on the originator as we would
                # when DKIM is not enabled, so that any local policy via remoteservers.xml can be used.
                pass

            except DKIMVerificationError, e:
                # If DKIM is enabled and there was a DKIM header present, then fail
                msg = "Failed to verify DKIM signature"
                _debug_msg = str(e)
                log.debug("{msg}:{exc}", msg=msg, exc=_debug_msg,)
                if config.Scheduling.iSchedule.DKIM.ProtocolDebug:
                    msg = "{}:{}".format(msg, _debug_msg,)
                raise HTTPError(self.errorResponse(
                    responsecode.FORBIDDEN,
                    (ischedule_namespace, "verification-failed"),
                    msg,
                ))

        if self._podding and self.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
            self.txn.doing_attendee_refresh = 1

        # Normalize recipient addresses
        results = []
        for recipient in recipients:
            normalized = yield normalizeCUAddress(recipient, normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress)
            self.recipientsNormalizationMap[normalized] = recipient
            results.append(normalized)
        recipients = results

        result = (yield super(IScheduleScheduler, self).doSchedulingViaPOST(originator, recipients, calendar))
        returnValue(result)


    def preProcessCalendarData(self):
        """
        For data coming in from outside we need to normalize the calendar user addresses so that later iTIP
        processing will match calendar users against those in stored calendar data. Only do that for invites
        not freebusy.
        """

        if not self.checkForFreeBusy():
            # Need to normalize the calendar data and recipient values to keep those in sync,
            # as we might later try to match them
            return self.calendar.normalizeCalendarUserAddres