##
# Copyright (c) 2008-2017 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 twisted.internet.defer import inlineCallbacks, succeed
from twisted.internet import reactor
from twisted.python.modules import getModule
from twisted.trial import unittest

from twistedcaldav.config import ConfigDict
from twistedcaldav.ical import Component

from txdav.caldav.datastore.scheduling.imip.inbound import IMIPReplyWork
from txdav.caldav.datastore.scheduling.imip.inbound import MailReceiver
from txdav.caldav.datastore.scheduling.imip.inbound import MailRetriever
from txdav.caldav.datastore.scheduling.imip.inbound import injectMessage
from txdav.caldav.datastore.scheduling.imip.inbound import shouldDeleteAllMail
from txdav.caldav.datastore.scheduling.imip.inbound import IMAP4DownloadProtocol
from txdav.caldav.datastore.scheduling.imip.inbound import sanitizeCalendar
from txdav.common.datastore.test.util import CommonCommonTests

from twext.enterprise.jobs.jobitem import JobItem

import email
from twisted.mail.imap4 import MessageSet


class InboundTests(CommonCommonTests, unittest.TestCase):

    @inlineCallbacks
    def setUp(self):
        super(InboundTests, self).setUp()

        yield self.buildStoreAndDirectory()
        self.receiver = MailReceiver(self.store, self.directory)
        self.retriever = MailRetriever(
            self.store, self.directory,
            ConfigDict({
                "Type": "pop",
                "UseSSL": False,
                "Server": "example.com",
                "Port": 123,
                "Username": "xyzzy",
            })
        )

        def decorateTransaction(txn):
            txn._mailRetriever = self.retriever

        self.store.callWithNewTransactions(decorateTransaction)
        module = getModule(__name__)
        self.dataPath = module.filePath.sibling("data")

    def dataFile(self, name):
        """
        Get the contents of a given data file from the 'data/mail' test
        fixtures directory.
        """
        return self.dataPath.child(name).getContent()

    def test_checkDSNFailure(self):

        data = {
            'good_reply': (False, None, None),
            'dsn_failure_no_original': (True, 'failed', None),
            'dsn_failure_no_ics': (True, 'failed', None),
            'dsn_failure_with_ics' : (True, 'failed', '''BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
PRODID:-//example Inc.//iCal 3.0//EN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZNAME:PST
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
DTSTART;TZID=US/Pacific:20080812T094500
DTEND;TZID=US/Pacific:20080812T104500
ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:<EMAIL>
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;CN=<EMAIL>:mailto:<EMAIL>
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
 <EMAIL>
SEQUENCE:2
SUMMARY:New Event
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
'''),
        }

        for filename, expected in data.iteritems():
            msg = email.message_from_string(self.dataFile(filename))
            self.assertEquals(self.receiver.checkDSN(msg), expected)

    @inlineCallbacks
    def test_processDSN(self):

        template = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
PRODID:-//example Inc.//iCal 3.0//EN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZNAME:PST
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
DTSTART;TZID=US/Pacific:20080812T094500
DTEND;TZID=US/Pacific:20080812T104500
ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:<EMAIL>
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;CN=<EMAIL>:mailto:<EMAIL>
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:<EMAIL>
SEQUENCE:2
SUMMARY:New Event
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
"""

        # Make sure an unknown token is not processed
        calBody = template % "bogus_token"
        self.assertEquals(
            (yield self.receiver.processDSN(calBody, "xyzzy")),
            MailReceiver.UNKNOWN_TOKEN
        )

        # Make sure a known token *is* processed
        txn = self.store.newTransaction()
        record = (yield txn.imipCreateToken(
            "urn:x-uid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C"
        ))
        yield txn.commit()
        calBody = template % record.token
        result = (yield self.receiver.processDSN(calBody, "xyzzy"))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReply(self):
        # Make sure an unknown token in an older email is deleted
        msg = email.message_from_string(self.dataFile('good_reply_past'))
        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.UNKNOWN_TOKEN_OLD)

        # Make sure an unknown token is not processed
        msg = email.message_from_string(self.dataFile('good_reply_future'))
        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.UNKNOWN_TOKEN)

        # Make sure a known token *is* processed
        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>-<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="<KEY>"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReplyMissingOrganizer(self):
        msg = email.message_from_string(self.dataFile('reply_missing_organizer'))

        # stick the token in the database first
        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>-<PASSWORD>-<PASSWORD>-<PASSWORD>-<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d<PASSWORD>"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReplyMissingAttendee(self):
        msg = email.message_from_string(self.dataFile('reply_missing_attendee'))

        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="<KEY>"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReplyMissingAttachment(self):

        msg = email.message_from_string(
            self.dataFile('reply_missing_attachment')
        )

        # stick the token in the database first
        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>-<PASSWORD>-<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d<PASSWORD>-ad3b-f08002fb285f"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.REPLY_FORWARDED_TO_ORGANIZER)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_injectMessage(self):

        calendar = Component.fromString("""BEGIN:VCALENDAR
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
VERSION:2.0
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:user01
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(
            txn,
            "urn:x-uid:user01",
            "mailto:<EMAIL>",
            calendar
        ))
        yield txn.commit()
        self.assertEquals(
            "1.2;Scheduling message has been delivered",
            result.responses[0].reqstatus.toString()
        )

    @inlineCallbacks
    def test_injectMessageWithError(self):

        calendar = Component.fromString("""BEGIN:VCALENDAR
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
VERSION:2.0
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:unknown_user
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(
            txn,
            "urn:x-uid:unknown_user",
            "mailto:<EMAIL>",
            calendar
        ))
        yield txn.commit()
        self.assertEquals(
            "3.7;Invalid Calendar User",
            result.responses[0].reqstatus.toString()
        )

    @inlineCallbacks
    def test_work(self):

        calendar = """BEGIN:VCALENDAR
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
VERSION:2.0
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:user01
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        txn = self.store.newTransaction()
        yield txn.enqueue(
            IMIPReplyWork,
            organizer="urn:x-uid:user01",
            attendee="mailto:<EMAIL>",
            icalendarText=calendar
        )
        yield txn.commit()
        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    def test_shouldDeleteAllMail(self):

        # Delete if the mail server is on the same host and using our
        # dedicated account:
        self.assertTrue(shouldDeleteAllMail(
            "calendar.example.com",
            "calendar.example.com",
            "com.apple.calendarserver"
        ))
        self.assertTrue(shouldDeleteAllMail(
            "calendar.example.com",
            "localhost",
            "com.apple.calendarserver"
        ))

        # Don't delete all otherwise:
        self.assertFalse(shouldDeleteAllMail(
            "calendar.example.com",
            "calendar.example.com",
            "not_ours"
        ))
        self.assertFalse(shouldDeleteAllMail(
            "calendar.example.com",
            "localhost",
            "not_ours"
        ))
        self.assertFalse(shouldDeleteAllMail(
            "calendar.example.com",
            "mail.example.com",
            "com.apple.calendarserver"
        ))

    @inlineCallbacks
    def test_deletion(self):
        """
        Verify the IMAP protocol will delete messages only when the right
        conditions are met.  Either:

            A) We've been told to delete all mail
            B) We've not been told to delete all mail, but it was a message
                we processed
        """

        def stubFetchNextMessage():
            pass

        def stubCbFlagDeleted(result):
            self.flagDeletedResult = result
            return succeed(None)

        proto = IMAP4DownloadProtocol()
        self.patch(proto, "fetchNextMessage", stubFetchNextMessage)
        self.patch(proto, "cbFlagDeleted", stubCbFlagDeleted)
        results = {
            "ignored": (
                {
                    "RFC822": "a message"
                }
            )
        }

        # Delete all mail = False; action taken = submitted; result = deletion
        proto.factory = StubFactory(MailReceiver.INJECTION_SUBMITTED, False)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, "xyzzy")

        # Delete all mail = False; action taken = not submitted; result = no deletion
        proto.factory = StubFactory(MailReceiver.NO_TOKEN, False)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, None)

        # Delete all mail = True; action taken = submitted; result = deletion
        proto.factory = StubFactory(MailReceiver.INJECTION_SUBMITTED, True)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, "xyzzy")

        # Delete all mail = True; action taken = not submitted; result = deletion
        proto.factory = StubFactory(MailReceiver.NO_TOKEN, True)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, "xyzzy")

    @inlineCallbacks
    def test_missingIMAPMessages(self):
        """
        Make sure L{IMAP4DownloadProtocol.cbGotMessage} can deal with missing messages.
        """

        class DummyResult(object):

            def __init__(self):
                self._values = []

            def values(self):
                return self._values

        noResult = DummyResult()
        missingKey = DummyResult()
        missingKey.values().append({})

        imap4 = IMAP4DownloadProtocol()
        imap4.messageUIDs = []
        imap4.fetchNextMessage = lambda: None

        result = yield imap4.cbGotMessage(noResult, [])
        self.assertTrue(result is None)
        result = yield imap4.cbGotMessage(missingKey, [])
        self.assertTrue(result is None)

    @inlineCallbacks
    def test_Twisted_16_6_bug(self):
        """
        Make sure L{IMAP4DownloadProtocol.cbGotSearch} works.

        There is a bug in Twisted 16.6 where the IMAP4Client._fetch method does not
        accept a MessageSet argument - though it previously did.

        We also need to double check that IMAP4Client._store works with a MessageSet.
        """

        imap4 = IMAP4DownloadProtocol()
        imap4.sendCommand = lambda cmd: succeed(cmd)
        imap4._cbFetch = lambda result, requestedParts, structured: {123: {"UID": "456"}}
        imap4.cbGotMessage = lambda results, messageList: succeed(True)
        result = yield imap4.cbGotSearch((123,))
        self.assertTrue(result is None)

        imap4 = IMAP4DownloadProtocol()
        imap4.sendCommand = lambda cmd: succeed(cmd)
        imap4._cbFetch = lambda result, requestedParts, structured: {"123": {"UID": "456"}}
        imap4.cbMessageUnseen = lambda results, messageList: succeed(True)
        ms = MessageSet()
        ms.add(123)
        result = yield imap4.cbFlagUnseen(ms)
        self.assertTrue(result is None)

        imap4 = IMAP4DownloadProtocol()
        imap4.sendCommand = lambda cmd: succeed(cmd)
        imap4._cbFetch = lambda result, requestedParts, structured: {"123": {"UID": "456"}}
        imap4.cbMessageDeleted = lambda results, messageList: succeed(True)
        ms = MessageSet()
        ms.add(123)
        result = yield imap4.cbFlagDeleted(ms)
        self.assertTrue(result is None)

    def test_IMAP4Logger_err(self):
        """
        Make sure L{IMAP4Logger.err} works when twisted.mail.imap4 calls log.err().
        """

        class DummyTransport(object):

            def __init__(self):
                self.called = False

            def loseConnection(self):
                self.called = True

        imap4 = IMAP4DownloadProtocol()
        imap4.transport = DummyTransport()
        imap4.state = "FOOBAR"
        imap4.dispatchCommand("A001", "OK")
        self.assertTrue(imap4.transport.called)

    def test_sanitizeCalendar(self):
        """
        Verify certain inbound third party mistakes are corrected.
        """

        data = """BEGIN:VCALENDAR
VERSION:2.0
METHOD:Reply
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:user01
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
STATUS:ACCEPTED
STATUS:ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        calendar = Component.fromString(data)
        self.assertFalse(calendar.hasProperty("PRODID"))
        self.assertTrue(calendar.masterComponent().hasProperty("STATUS"))
        prop = calendar.getProperty("METHOD")
        self.assertEquals(prop.value(), "Reply")
        sanitizeCalendar(calendar)
        prop = calendar.getProperty("METHOD")
        self.assertEquals(prop.value(), "REPLY")
        self.assertTrue(calendar.hasProperty("PRODID"))
        self.assertFalse(calendar.masterComponent().hasProperty("STATUS"))


class StubFactory(object):

    def __init__(self, actionTaken, deleteAllMail):
        self.actionTaken = actionTaken
        self.deleteAllMail = deleteAllMail

    def handleMessage(self, messageData):
        return succeed(self.actionTaken)

##
# Copyright (c) 2008-2017 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 twisted.internet.defer import inlineCallbacks, succeed
from twisted.internet import reactor
from twisted.python.modules import getModule
from twisted.trial import unittest

from twistedcaldav.config import ConfigDict
from twistedcaldav.ical import Component

from txdav.caldav.datastore.scheduling.imip.inbound import IMIPReplyWork
from txdav.caldav.datastore.scheduling.imip.inbound import MailReceiver
from txdav.caldav.datastore.scheduling.imip.inbound import MailRetriever
from txdav.caldav.datastore.scheduling.imip.inbound import injectMessage
from txdav.caldav.datastore.scheduling.imip.inbound import shouldDeleteAllMail
from txdav.caldav.datastore.scheduling.imip.inbound import IMAP4DownloadProtocol
from txdav.caldav.datastore.scheduling.imip.inbound import sanitizeCalendar
from txdav.common.datastore.test.util import CommonCommonTests

from twext.enterprise.jobs.jobitem import JobItem

import email
from twisted.mail.imap4 import MessageSet


class InboundTests(CommonCommonTests, unittest.TestCase):

    @inlineCallbacks
    def setUp(self):
        super(InboundTests, self).setUp()

        yield self.buildStoreAndDirectory()
        self.receiver = MailReceiver(self.store, self.directory)
        self.retriever = MailRetriever(
            self.store, self.directory,
            ConfigDict({
                "Type": "pop",
                "UseSSL": False,
                "Server": "example.com",
                "Port": 123,
                "Username": "xyzzy",
            })
        )

        def decorateTransaction(txn):
            txn._mailRetriever = self.retriever

        self.store.callWithNewTransactions(decorateTransaction)
        module = getModule(__name__)
        self.dataPath = module.filePath.sibling("data")

    def dataFile(self, name):
        """
        Get the contents of a given data file from the 'data/mail' test
        fixtures directory.
        """
        return self.dataPath.child(name).getContent()

    def test_checkDSNFailure(self):

        data = {
            'good_reply': (False, None, None),
            'dsn_failure_no_original': (True, 'failed', None),
            'dsn_failure_no_ics': (True, 'failed', None),
            'dsn_failure_with_ics' : (True, 'failed', '''BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
PRODID:-//example Inc.//iCal 3.0//EN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZNAME:PST
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
DTSTART;TZID=US/Pacific:20080812T094500
DTEND;TZID=US/Pacific:20080812T104500
ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:<EMAIL>
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;CN=<EMAIL>:mailto:<EMAIL>
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
 <EMAIL>
SEQUENCE:2
SUMMARY:New Event
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
'''),
        }

        for filename, expected in data.iteritems():
            msg = email.message_from_string(self.dataFile(filename))
            self.assertEquals(self.receiver.checkDSN(msg), expected)

    @inlineCallbacks
    def test_processDSN(self):

        template = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
PRODID:-//example Inc.//iCal 3.0//EN
BEGIN:VTIMEZONE
TZID:US/Pacific
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZNAME:PST
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C
DTSTART;TZID=US/Pacific:20080812T094500
DTEND;TZID=US/Pacific:20080812T104500
ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:<EMAIL>
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;CN=<EMAIL>:mailto:<EMAIL>
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:<EMAIL>
SEQUENCE:2
SUMMARY:New Event
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
"""

        # Make sure an unknown token is not processed
        calBody = template % "bogus_token"
        self.assertEquals(
            (yield self.receiver.processDSN(calBody, "xyzzy")),
            MailReceiver.UNKNOWN_TOKEN
        )

        # Make sure a known token *is* processed
        txn = self.store.newTransaction()
        record = (yield txn.imipCreateToken(
            "urn:x-uid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C"
        ))
        yield txn.commit()
        calBody = template % record.token
        result = (yield self.receiver.processDSN(calBody, "xyzzy"))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReply(self):
        # Make sure an unknown token in an older email is deleted
        msg = email.message_from_string(self.dataFile('good_reply_past'))
        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.UNKNOWN_TOKEN_OLD)

        # Make sure an unknown token is not processed
        msg = email.message_from_string(self.dataFile('good_reply_future'))
        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.UNKNOWN_TOKEN)

        # Make sure a known token *is* processed
        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>-<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="<KEY>"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReplyMissingOrganizer(self):
        msg = email.message_from_string(self.dataFile('reply_missing_organizer'))

        # stick the token in the database first
        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>-<PASSWORD>-<PASSWORD>-<PASSWORD>-<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d<PASSWORD>"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReplyMissingAttendee(self):
        msg = email.message_from_string(self.dataFile('reply_missing_attendee'))

        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="<KEY>"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_processReplyMissingAttachment(self):

        msg = email.message_from_string(
            self.dataFile('reply_missing_attachment')
        )

        # stick the token in the database first
        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:x-uid:<PASSWORD>-<PASSWORD>-<PASSWORD>",
            "mailto:<EMAIL>",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d<PASSWORD>-ad3b-f08002fb285f"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        self.assertEquals(result, MailReceiver.REPLY_FORWARDED_TO_ORGANIZER)

        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    @inlineCallbacks
    def test_injectMessage(self):

        calendar = Component.fromString("""BEGIN:VCALENDAR
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
VERSION:2.0
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:user01
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(
            txn,
            "urn:x-uid:user01",
            "mailto:<EMAIL>",
            calendar
        ))
        yield txn.commit()
        self.assertEquals(
            "1.2;Scheduling message has been delivered",
            result.responses[0].reqstatus.toString()
        )

    @inlineCallbacks
    def test_injectMessageWithError(self):

        calendar = Component.fromString("""BEGIN:VCALENDAR
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
VERSION:2.0
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:unknown_user
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(
            txn,
            "urn:x-uid:unknown_user",
            "mailto:<EMAIL>",
            calendar
        ))
        yield txn.commit()
        self.assertEquals(
            "3.7;Invalid Calendar User",
            result.responses[0].reqstatus.toString()
        )

    @inlineCallbacks
    def test_work(self):

        calendar = """BEGIN:VCALENDAR
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
VERSION:2.0
METHOD:REPLY
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:user01
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        txn = self.store.newTransaction()
        yield txn.enqueue(
            IMIPReplyWork,
            organizer="urn:x-uid:user01",
            attendee="mailto:<EMAIL>",
            icalendarText=calendar
        )
        yield txn.commit()
        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)

    def test_shouldDeleteAllMail(self):

        # Delete if the mail server is on the same host and using our
        # dedicated account:
        self.assertTrue(shouldDeleteAllMail(
            "calendar.example.com",
            "calendar.example.com",
            "com.apple.calendarserver"
        ))
        self.assertTrue(shouldDeleteAllMail(
            "calendar.example.com",
            "localhost",
            "com.apple.calendarserver"
        ))

        # Don't delete all otherwise:
        self.assertFalse(shouldDeleteAllMail(
            "calendar.example.com",
            "calendar.example.com",
            "not_ours"
        ))
        self.assertFalse(shouldDeleteAllMail(
            "calendar.example.com",
            "localhost",
            "not_ours"
        ))
        self.assertFalse(shouldDeleteAllMail(
            "calendar.example.com",
            "mail.example.com",
            "com.apple.calendarserver"
        ))

    @inlineCallbacks
    def test_deletion(self):
        """
        Verify the IMAP protocol will delete messages only when the right
        conditions are met.  Either:

            A) We've been told to delete all mail
            B) We've not been told to delete all mail, but it was a message
                we processed
        """

        def stubFetchNextMessage():
            pass

        def stubCbFlagDeleted(result):
            self.flagDeletedResult = result
            return succeed(None)

        proto = IMAP4DownloadProtocol()
        self.patch(proto, "fetchNextMessage", stubFetchNextMessage)
        self.patch(proto, "cbFlagDeleted", stubCbFlagDeleted)
        results = {
            "ignored": (
                {
                    "RFC822": "a message"
                }
            )
        }

        # Delete all mail = False; action taken = submitted; result = deletion
        proto.factory = StubFactory(MailReceiver.INJECTION_SUBMITTED, False)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, "xyzzy")

        # Delete all mail = False; action taken = not submitted; result = no deletion
        proto.factory = StubFactory(MailReceiver.NO_TOKEN, False)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, None)

        # Delete all mail = True; action taken = submitted; result = deletion
        proto.factory = StubFactory(MailReceiver.INJECTION_SUBMITTED, True)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, "xyzzy")

        # Delete all mail = True; action taken = not submitted; result = deletion
        proto.factory = StubFactory(MailReceiver.NO_TOKEN, True)
        self.flagDeletedResult = None
        yield proto.cbGotMessage(results, "xyzzy")
        self.assertEquals(self.flagDeletedResult, "xyzzy")

    @inlineCallbacks
    def test_missingIMAPMessages(self):
        """
        Make sure L{IMAP4DownloadProtocol.cbGotMessage} can deal with missing messages.
        """

        class DummyResult(object):

            def __init__(self):
                self._values = []

            def values(self):
                return self._values

        noResult = DummyResult()
        missingKey = DummyResult()
        missingKey.values().append({})

        imap4 = IMAP4DownloadProtocol()
        imap4.messageUIDs = []
        imap4.fetchNextMessage = lambda: None

        result = yield imap4.cbGotMessage(noResult, [])
        self.assertTrue(result is None)
        result = yield imap4.cbGotMessage(missingKey, [])
        self.assertTrue(result is None)

    @inlineCallbacks
    def test_Twisted_16_6_bug(self):
        """
        Make sure L{IMAP4DownloadProtocol.cbGotSearch} works.

        There is a bug in Twisted 16.6 where the IMAP4Client._fetch method does not
        accept a MessageSet argument - though it previously did.

        We also need to double check that IMAP4Client._store works with a MessageSet.
        """

        imap4 = IMAP4DownloadProtocol()
        imap4.sendCommand = lambda cmd: succeed(cmd)
        imap4._cbFetch = lambda result, requestedParts, structured: {123: {"UID": "456"}}
        imap4.cbGotMessage = lambda results, messageList: succeed(True)
        result = yield imap4.cbGotSearch((123,))
        self.assertTrue(result is None)

        imap4 = IMAP4DownloadProtocol()
        imap4.sendCommand = lambda cmd: succeed(cmd)
        imap4._cbFetch = lambda result, requestedParts, structured: {"123": {"UID": "456"}}
        imap4.cbMessageUnseen = lambda results, messageList: succeed(True)
        ms = MessageSet()
        ms.add(123)
        result = yield imap4.cbFlagUnseen(ms)
        self.assertTrue(result is None)

        imap4 = IMAP4DownloadProtocol()
        imap4.sendCommand = lambda cmd: succeed(cmd)
        imap4._cbFetch = lambda result, requestedParts, structured: {"123": {"UID": "456"}}
        imap4.cbMessageDeleted = lambda results, messageList: succeed(True)
        ms = MessageSet()
        ms.add(123)
        result = yield imap4.cbFlagDeleted(ms)
        self.assertTrue(result is None)

    def test_IMAP4Logger_err(self):
        """
        Make sure L{IMAP4Logger.err} works when twisted.mail.imap4 calls log.err().
        """

        class DummyTransport(object):

            def __init__(self):
                self.called = False

            def loseConnection(self):
                self.called = True

        imap4 = IMAP4DownloadProtocol()
        imap4.transport = DummyTransport()
        imap4.state = "FOOBAR"
        imap4.dispatchCommand("A001", "OK")
        self.assertTrue(imap4.transport.called)

    def test_sanitizeCalendar(self):
        """
        Verify certain inbound third party mistakes are corrected.
        """

        data = """BEGIN:VCALENDAR
VERSION:2.0
METHOD:Reply
BEGIN:VEVENT
UID:12345-67890
DTSTAMP:20130208T120000Z
DTSTART:20180601T120000Z
DTEND:20180601T130000Z
ORGANIZER:urn:x-uid:user01
ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED
STATUS:ACCEPTED
STATUS:ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        calendar = Component.fromString(data)
        self.assertFalse(calendar.hasProperty("PRODID"))
        self.assertTrue(calendar.masterComponent().hasProperty("STATUS"))
        prop = calendar.getProperty("METHOD")
        self.assertEquals(prop.value(), "Reply")
        sanitizeCalendar(calendar)
        prop = calendar.getProperty("METHOD")
        self.assertEquals(prop.value(), "REPLY")
        self.assertTrue(calendar.hasProperty("PRODID"))
        self.assertFalse(calendar.masterComponent().hasProperty("STATUS"))


class StubFactory(object):

    def __init__(self, actionTaken, deleteAllMail):
        self.actionTaken = actionTaken
        self.deleteAllMail = deleteAllMail

    def handleMessage(self, messageData):
        return succeed(self.actionTaken)

## # Copyright (c) 2008-2017 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. ## Get the contents of a given data file from the 'data/mail' test         fixtures directory. BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN METHOD:REQUEST PRODID:-//example Inc.//iCal 3.0//EN BEGIN:VTIMEZONE TZID:US/Pacific BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C DTSTART;TZID=US/Pacific:20080812T094500 DTEND;TZID=US/Pacific:20080812T104500 ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:<EMAIL> ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A  CTION;CN=<EMAIL>:mailto:<EMAIL> CREATED:20080812T191857Z DTSTAMP:20080812T191932Z ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08  <EMAIL> SEQUENCE:2 SUMMARY:New Event TRANSP:OPAQUE END:VEVENT END:VCALENDAR BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN METHOD:REQUEST PRODID:-//example Inc.//iCal 3.0//EN BEGIN:VTIMEZONE TZID:US/Pacific BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C DTSTART;TZID=US/Pacific:20080812T094500 DTEND;TZID=US/Pacific:20080812T104500 ATTENDEE;CUTYPE=INDIVIDUAL;CN=User 01;PARTSTAT=ACCEPTED:mailto:<EMAIL> ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A  CTION;CN=<EMAIL>:mailto:<EMAIL> CREATED:20080812T191857Z DTSTAMP:20080812T191932Z ORGANIZER;CN=User 01:mailto:<EMAIL> SEQUENCE:2 SUMMARY:New Event TRANSP:OPAQUE END:VEVENT END:VCALENDAR # Make sure an unknown token is not processed # Make sure a known token *is* processed # Make sure an unknown token in an older email is deleted # Make sure an unknown token is not processed # Make sure a known token *is* processed # stick the token in the database first # stick the token in the database first BEGIN:VCALENDAR PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN VERSION:2.0 METHOD:REPLY BEGIN:VEVENT UID:12345-67890 DTSTAMP:20130208T120000Z DTSTART:20180601T120000Z DTEND:20180601T130000Z ORGANIZER:urn:x-uid:user01 ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED END:VEVENT END:VCALENDAR BEGIN:VCALENDAR PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN VERSION:2.0 METHOD:REPLY BEGIN:VEVENT UID:12345-67890 DTSTAMP:20130208T120000Z DTSTART:20180601T120000Z DTEND:20180601T130000Z ORGANIZER:urn:x-uid:unknown_user ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED END:VEVENT END:VCALENDAR BEGIN:VCALENDAR PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN VERSION:2.0 METHOD:REPLY BEGIN:VEVENT UID:12345-67890 DTSTAMP:20130208T120000Z DTSTART:20180601T120000Z DTEND:20180601T130000Z ORGANIZER:urn:x-uid:user01 ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED END:VEVENT END:VCALENDAR # Delete if the mail server is on the same host and using our # dedicated account: # Don't delete all otherwise: Verify the IMAP protocol will delete messages only when the right         conditions are met.  Either:              A) We've been told to delete all mail             B) We've not been told to delete all mail, but it was a message                 we processed # Delete all mail = False; action taken = submitted; result = deletion # Delete all mail = False; action taken = not submitted; result = no deletion # Delete all mail = True; action taken = submitted; result = deletion # Delete all mail = True; action taken = not submitted; result = deletion Make sure L{IMAP4DownloadProtocol.cbGotMessage} can deal with missing messages. Make sure L{IMAP4DownloadProtocol.cbGotSearch} works.          There is a bug in Twisted 16.6 where the IMAP4Client._fetch method does not         accept a MessageSet argument - though it previously did.          We also need to double check that IMAP4Client._store works with a MessageSet. Make sure L{IMAP4Logger.err} works when twisted.mail.imap4 calls log.err(). Verify certain inbound third party mistakes are corrected. BEGIN:VCALENDAR VERSION:2.0 METHOD:Reply BEGIN:VEVENT UID:12345-67890 DTSTAMP:20130208T120000Z DTSTART:20180601T120000Z DTEND:20180601T130000Z ORGANIZER:urn:x-uid:user01 ATTENDEE:mailto:<EMAIL>;PARTSTAT=ACCEPTED STATUS:ACCEPTED STATUS:ACCEPTED END:VEVENT END:VCALENDAR