예제 #1
0
 def getMailSender(cls):
     """
     Instantiate and return a singleton MailSender object
     @return: a MailSender
     """
     if cls.mailSender is None:
         if config.Scheduling.iMIP.Enabled:
             settings = config.Scheduling.iMIP.Sending
             smtpSender = SMTPSender(settings.Username, settings.Password,
                 settings.UseSSL, settings.Server, settings.Port)
             cls.mailSender = MailSender(settings.Address,
                 settings.SuppressionDays, smtpSender, getLanguage(config))
     return cls.mailSender
예제 #2
0
    def processReply(self, msg):
        # extract the token from the To header
        _ignore_name, addr = email.utils.parseaddr(msg['To'])
        if addr:
            # addr looks like: [email protected]
            token = self._extractToken(addr)
            if not token:
                log.error("Mail gateway didn't find a token in message "
                          "%s (%s)" % (msg['Message-ID'], msg['To']))
                returnValue(self.NO_TOKEN)
        else:
            log.error("Mail gateway couldn't parse To: address (%s) in "
                      "message %s" % (msg['To'], msg['Message-ID']))
            returnValue(self.MALFORMED_TO_ADDRESS)

        txn = self.store.newTransaction(label="MailReceiver.processReply")
        result = (yield txn.imipLookupByToken(token))
        yield txn.commit()
        try:
            # Note the results are returned as utf-8 encoded strings
            organizer, attendee, _ignore_icaluid = result[0]
        except:
            # This isn't a token we recognize
            log.error("Mail gateway found a token (%s) but didn't "
                      "recognize it in message %s" %
                      (token, msg['Message-ID']))
            returnValue(self.UNKNOWN_TOKEN)

        for part in msg.walk():
            if part.get_content_type() == "text/calendar":
                calBody = part.get_payload(decode=True)
                break
        else:
            # No icalendar attachment
            log.warn("Mail gateway didn't find an icalendar attachment "
                     "in message %s" % (msg['Message-ID'], ))

            toAddr = None
            fromAddr = attendee[7:]
            if organizer.startswith("mailto:"):
                toAddr = organizer[7:]
            elif organizer.startswith("urn:x-uid:"):
                uid = organizer[10:]
                record = yield self.directory.recordWithUID(uid)
                try:
                    if record and record.emailAddresses:
                        toAddr = list(record.emailAddresses)[0]
                except AttributeError:
                    pass

            if toAddr is None:
                log.error("Don't have an email address for the organizer; "
                          "ignoring reply.")
                returnValue(self.NO_ORGANIZER_ADDRESS)

            settings = config.Scheduling["iMIP"]["Sending"]
            smtpSender = SMTPSender(settings.Username, settings.Password,
                                    settings.UseSSL, settings.Server,
                                    settings.Port)

            del msg["From"]
            msg["From"] = fromAddr
            del msg["Reply-To"]
            msg["Reply-To"] = fromAddr
            del msg["To"]
            msg["To"] = toAddr
            log.warn("Mail gateway forwarding reply back to organizer")
            yield smtpSender.sendMessage(fromAddr, toAddr, messageid(),
                                         msg.as_string())
            returnValue(self.REPLY_FORWARDED_TO_ORGANIZER)

        # Process the imip attachment; inject to calendar server

        log.debug(calBody)
        calendar = Component.fromString(calBody)
        event = calendar.mainComponent()

        # Don't let a missing PRODID prevent the reply from being processed
        if not calendar.hasProperty("PRODID"):
            calendar.addProperty(Property("PRODID", "Unknown"))

        calendar.removeAllButOneAttendee(attendee)
        organizerProperty = calendar.getOrganizerProperty()
        if organizerProperty is None:
            # ORGANIZER is required per rfc2446 section 3.2.3
            log.warn("Mail gateway didn't find an ORGANIZER in REPLY %s" %
                     (msg['Message-ID'], ))
            event.addProperty(Property("ORGANIZER", organizer))
        else:
            organizerProperty.setValue(organizer)

        if not calendar.getAttendees():
            # The attendee we're expecting isn't there, so add it back
            # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE.
            # The organizer will then see that the reply was not successful.
            attendeeProp = Property("ATTENDEE",
                                    attendee,
                                    params={
                                        "SCHEDULE-STATUS":
                                        iTIPRequestStatus.SERVICE_UNAVAILABLE,
                                    })
            event.addProperty(attendeeProp)

            # TODO: We have talked about sending an email to the reply-to
            # at this point, to let them know that their reply was missing
            # the appropriate ATTENDEE.  This will require a new localizable
            # email template for the message.

        txn = self.store.newTransaction(label="MailReceiver.processReply")
        yield txn.enqueue(IMIPReplyWork,
                          organizer=organizer,
                          attendee=attendee,
                          icalendarText=str(calendar))
        yield txn.commit()
        returnValue(self.INJECTION_SUBMITTED)
예제 #3
0
class MailReceiver(object):

    NO_TOKEN = 0
    UNKNOWN_TOKEN = 1
    UNKNOWN_TOKEN_OLD = 2
    MALFORMED_TO_ADDRESS = 3
    NO_ORGANIZER_ADDRESS = 4
    REPLY_FORWARDED_TO_ORGANIZER = 5
    INJECTION_SUBMITTED = 6
    INCOMPLETE_DSN = 7
    UNKNOWN_FAILURE = 8

    def __init__(self, store, directory):
        self.store = store
        self.directory = directory

    def checkDSN(self, message):
        # returns (isdsn, action, icalendar attachment)

        report = deliveryStatus = calBody = None

        for part in message.walk():
            contentType = part.get_content_type()
            if contentType == "multipart/report":
                report = part
                continue
            elif contentType == "message/delivery-status":
                deliveryStatus = part
                continue
            elif contentType == "message/rfc822":
                # original = part
                continue
            elif contentType == "text/calendar":
                calBody = part.get_payload(decode=True)
                continue

        if report is not None and deliveryStatus is not None:
            # we have what appears to be a dsn

            lines = str(deliveryStatus).split("\n")
            for line in lines:
                lower = line.lower()
                if lower.startswith("action:"):
                    # found action:
                    action = lower.split(' ')[1]
                    break
            else:
                action = None

            return True, action, calBody

        else:
            # not a dsn
            return False, None, None

    def _extractToken(self, text):
        try:
            pre, _ignore_post = text.split('@')
            pre, token = pre.split('+')
            return token
        except ValueError:
            return None

    @inlineCallbacks
    def processDSN(self, calBody, msgId):
        calendar = Component.fromString(calBody)
        # Extract the token (from organizer property)
        organizer = calendar.getOrganizer()
        token = self._extractToken(organizer)
        if not token:
            log.error("Mail gateway can't find token in DSN {msgid}",
                      msgid=msgId)
            return

        txn = self.store.newTransaction(label="MailReceiver.processDSN")
        records = (yield txn.imipLookupByToken(token))
        yield txn.commit()
        try:
            # Note the results are returned as utf-8 encoded strings
            record = records[0]
        except:
            # This isn't a token we recognize
            log.error(
                "Mail gateway found a token ({token}) but didn't recognize it in message {msgid}",
                token=token,
                msgid=msgId,
            )
            returnValue(self.UNKNOWN_TOKEN)

        calendar.removeAllButOneAttendee(record.attendee)
        calendar.getOrganizerProperty().setValue(organizer)
        for comp in calendar.subcomponents():
            if comp.name() == "VEVENT":
                comp.addProperty(
                    Property("REQUEST-STATUS", ["5.1", "Service unavailable"]))
                break
        else:
            # no VEVENT in the calendar body.
            # TODO: what to do in this case?
            pass

        log.warn("Mail gateway processing DSN {msgid}", msgid=msgId)
        txn = self.store.newTransaction(label="MailReceiver.processDSN")
        yield txn.enqueue(IMIPReplyWork,
                          organizer=record.organizer,
                          attendee=record.attendee,
                          icalendarText=str(calendar))
        yield txn.commit()
        returnValue(self.INJECTION_SUBMITTED)

    @inlineCallbacks
    def processReply(self, msg):
        # extract the token from the To header
        _ignore_name, addr = email.utils.parseaddr(msg['To'])
        if addr:
            # addr looks like: [email protected]
            token = self._extractToken(addr)
            if not token:
                log.error(
                    "Mail gateway didn't find a token in message "
                    "{msgid} ({to})",
                    msgid=msg['Message-ID'],
                    to=msg['To'])
                returnValue(self.NO_TOKEN)
        else:
            log.error(
                "Mail gateway couldn't parse To: address ({to}) in "
                "message {msgid}",
                to=msg['To'],
                msgid=msg['Message-ID'])
            returnValue(self.MALFORMED_TO_ADDRESS)

        txn = self.store.newTransaction(label="MailReceiver.processReply")
        records = (yield txn.imipLookupByToken(token))
        yield txn.commit()
        try:
            # Note the results are returned as utf-8 encoded strings
            record = records[0]
        except:
            # This isn't a token we recognize
            log.info(
                "Mail gateway found a token ({token}) but didn't "
                "recognize it in message {msgid}",
                token=token,
                msgid=msg['Message-ID'])
            # Any email with an unknown token which was sent over 72 hours ago
            # is deleted.  If we can't parse the date we leave it in the inbox.
            dateString = msg.get("Date")
            if dateString is not None:
                try:
                    dateSent = dateutil.parser.parse(dateString)
                except Exception, e:
                    log.info(
                        "Could not parse date in IMIP email '{date}' ({ex})",
                        date=dateString,
                        ex=e,
                    )
                    returnValue(self.UNKNOWN_TOKEN)
                now = datetime.datetime.now(dateutil.tz.tzutc())
                if dateSent < now - datetime.timedelta(hours=72):
                    returnValue(self.UNKNOWN_TOKEN_OLD)
            returnValue(self.UNKNOWN_TOKEN)

        for part in msg.walk():
            if part.get_content_type() == "text/calendar":
                calBody = part.get_payload(decode=True)
                break
        else:
            # No icalendar attachment
            log.warn(
                "Mail gateway didn't find an icalendar attachment "
                "in message {msgid}",
                msgid=msg['Message-ID'])

            toAddr = None
            fromAddr = record.attendee[7:]
            if record.organizer.startswith("mailto:"):
                toAddr = record.organizer[7:]
            elif record.organizer.startswith("urn:x-uid:"):
                uid = record.organizer[10:]
                record = yield self.directory.recordWithUID(uid)
                try:
                    if record and record.emailAddresses:
                        toAddr = list(record.emailAddresses)[0]
                except AttributeError:
                    pass

            if toAddr is None:
                log.error(
                    "Don't have an email address for the organizer; ignoring reply."
                )
                returnValue(self.NO_ORGANIZER_ADDRESS)

            settings = config.Scheduling["iMIP"]["Sending"]
            smtpSender = SMTPSender(settings.Username, settings.Password,
                                    settings.UseSSL, settings.Server,
                                    settings.Port)

            del msg["From"]
            msg["From"] = fromAddr
            del msg["Reply-To"]
            msg["Reply-To"] = fromAddr
            del msg["To"]
            msg["To"] = toAddr
            log.warn("Mail gateway forwarding reply back to organizer")
            yield smtpSender.sendMessage(fromAddr, toAddr,
                                         SMTPSender.betterMessageID(),
                                         msg.as_string())
            returnValue(self.REPLY_FORWARDED_TO_ORGANIZER)

        # Process the imip attachment; inject to calendar server

        log.debug(calBody)
        calendar = Component.fromString(calBody)
        event = calendar.mainComponent()

        sanitizeCalendar(calendar)

        calendar.removeAllButOneAttendee(record.attendee)
        organizerProperty = calendar.getOrganizerProperty()
        if organizerProperty is None:
            # ORGANIZER is required per rfc2446 section 3.2.3
            log.warn("Mail gateway didn't find an ORGANIZER in REPLY {msgid}",
                     msgid=msg['Message-ID'])
            event.addProperty(Property("ORGANIZER", record.organizer))
        else:
            organizerProperty.setValue(record.organizer)

        if not calendar.getAttendees():
            # The attendee we're expecting isn't there, so add it back
            # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE.
            # The organizer will then see that the reply was not successful.
            attendeeProp = Property("ATTENDEE",
                                    record.attendee,
                                    params={
                                        "SCHEDULE-STATUS":
                                        iTIPRequestStatus.SERVICE_UNAVAILABLE,
                                    })
            event.addProperty(attendeeProp)

            # TODO: We have talked about sending an email to the reply-to
            # at this point, to let them know that their reply was missing
            # the appropriate ATTENDEE.  This will require a new localizable
            # email template for the message.

        txn = self.store.newTransaction(label="MailReceiver.processReply")
        yield txn.enqueue(IMIPReplyWork,
                          organizer=record.organizer,
                          attendee=record.attendee,
                          icalendarText=str(calendar))
        yield txn.commit()
        returnValue(self.INJECTION_SUBMITTED)