コード例 #1
0
class InboundTests(unittest.TestCase):

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

        self.store = yield buildCalendarStore(self, None)
        self.directory = self.store.directoryService()
        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:user01@exam
 ple.com
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;[email protected]:mailto:[email protected]
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
 [email protected]
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:user01@exam
 ple.com
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;[email protected]:mailto:[email protected]
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+%[email protected]
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()
        token = (yield txn.imipCreateToken(
            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C"
        ))
        yield txn.commit()
        calBody = template % token
        result = (yield self.receiver.processDSN(calBody, "xyzzy"))
        self.assertEquals(result, MailReceiver.INJECTION_SUBMITTED)


    @inlineCallbacks
    def test_processReply(self):
        msg = email.message_from_string(self.dataFile('good_reply'))

        # Make sure an unknown token is not processed
        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:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
        )
        yield txn.commit()

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


    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:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        organizer, _ignore_attendee, calendar = result
        organizerProp = calendar.mainComponent().getOrganizerProperty()
        self.assertTrue(organizerProp is not None)
        self.assertEquals(organizer,
                          "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500")


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

        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
        )
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        _ignore_organizer, attendee, calendar = result

        # Since the expected attendee was missing, the reply processor should
        # have added an attendee back in with a "5.1;Service unavailable"
        # schedule-status
        attendeeProp = calendar.mainComponent().getAttendeeProperty([attendee])
        self.assertEquals(attendeeProp.parameterValue("SCHEDULE-STATUS"),
                          iTIPRequestStatus.SERVICE_UNAVAILABLE)


    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:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f"
        )
        yield txn.commit()

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


    @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:uuid:user01
ATTENDEE:mailto:[email protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(
                txn,
                "urn:uuid:user01",
                "mailto:[email protected]",
                calendar
            )
        )
        yield txn.commit()
        self.assertEquals(
            "1.2;Scheduling message has been delivered",
            result.responses[0].children[1].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:uuid:unknown_user
ATTENDEE:mailto:[email protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(
                txn,
                "urn:uuid:unknown_user",
                "mailto:[email protected]",
                calendar
            )
        )
        yield txn.commit()
        self.assertEquals(
            "3.7;Invalid Calendar User",
            result.responses[0].children[1].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:uuid:user01
ATTENDEE:mailto:[email protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        txn = self.store.newTransaction()
        wp = (yield txn.enqueue(IMIPReplyWork,
            organizer="urn:uuid:user01",
            attendee="mailto:[email protected]",
            icalendarText=calendar
        ))
        yield txn.commit()
        yield wp.whenExecuted()


    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")
コード例 #2
0
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": "******",
            }))

        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:user01@exam
 ple.com
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;[email protected]:mailto:[email protected]
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
 [email protected]
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:user01@exam
 ple.com
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;[email protected]:mailto:[email protected]
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+%[email protected]
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()
        token = (yield txn.imipCreateToken(
            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C"))
        yield txn.commit()
        calBody = template % 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):
        msg = email.message_from_string(self.dataFile('good_reply'))

        # Make sure an unknown token is not processed
        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:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        yield txn.commit()

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

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

    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:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        organizer, _ignore_attendee, calendar = result
        organizerProp = calendar.mainComponent().getOrganizerProperty()
        self.assertTrue(organizerProp is not None)
        self.assertEquals(organizer,
                          "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500")

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

        txn = self.store.newTransaction()
        yield txn.imipCreateToken(
            "urn:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        yield txn.commit()

        result = (yield self.receiver.processReply(msg))
        _ignore_organizer, attendee, calendar = result

        # Since the expected attendee was missing, the reply processor should
        # have added an attendee back in with a "5.1;Service unavailable"
        # schedule-status
        attendeeProp = calendar.mainComponent().getAttendeeProperty([attendee])
        self.assertEquals(attendeeProp.parameterValue("SCHEDULE-STATUS"),
                          iTIPRequestStatus.SERVICE_UNAVAILABLE)

    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:uuid:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        yield txn.commit()

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

    @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 protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(txn, "urn:x-uid:user01",
                                      "mailto:[email protected]", 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 protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(txn, "urn:x-uid:unknown_user",
                                      "mailto:[email protected]", 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 protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        txn = self.store.newTransaction()
        yield txn.enqueue(IMIPReplyWork,
                          organizer="urn:x-uid:user01",
                          attendee="mailto:[email protected]",
                          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")
コード例 #3
0
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": "******",
            }))

        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:user01@exam
 ple.com
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;[email protected]:mailto:[email protected]
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+8e16b897-d544-4217-88e9-a363d08
 [email protected]
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:user01@exam
 ple.com
ATTENDEE;CUTYPE=INDIVIDUAL;RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-A
 CTION;[email protected]:mailto:[email protected]
CREATED:20080812T191857Z
DTSTAMP:20080812T191932Z
ORGANIZER;CN=User 01:mailto:xyzzy+%[email protected]
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 protected]",
            "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:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        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:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        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:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
        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:5A985493-EE2C-4665-94CF-4DFEA3A89500",
            "mailto:[email protected]",
            "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C",
            token="d7cdf68d-8b73-4df1-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 protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(txn, "urn:x-uid:user01",
                                      "mailto:[email protected]", 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 protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
""")

        txn = self.store.newTransaction()
        result = (yield injectMessage(txn, "urn:x-uid:unknown_user",
                                      "mailto:[email protected]", 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 protected];PARTSTAT=ACCEPTED
END:VEVENT
END:VCALENDAR
"""
        txn = self.store.newTransaction()
        yield txn.enqueue(IMIPReplyWork,
                          organizer="urn:x-uid:user01",
                          attendee="mailto:[email protected]",
                          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 protected];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"))