Source code for timApp.messaging.messagelist.mailman_events

# Mailman event handler
# Listens to events sent by https://github.com/dezhidki/mailman-rest-events

from dataclasses import dataclass
from typing import Optional

from flask import request, Response, Blueprint

from timApp.messaging.messagelist.messagelist_models import MessageListModel
from timApp.messaging.messagelist.messagelist_utils import (
    parse_mailman_message,
    archive_message,
)
from timApp.tim_app import app, csrf
from timApp.util.flask.requesthelper import RouteException
from timApp.util.flask.responsehelper import ok_response
from timApp.util.logger import log_warning
from tim_common.marshmallow_dataclass import class_schema

mailman_events = Blueprint("mailman_events", __name__, url_prefix="/mailman/event")

AUTH_USER = app.config.get("MAILMAN_EVENT_API_USER", None)
AUTH_KEY = app.config.get("MAILMAN_EVENT_API_KEY", None)


[docs]def has_valid_event_auth() -> bool: return AUTH_USER is not None and AUTH_KEY is not None
[docs]def check_auth() -> bool: auth = request.authorization return ( auth is not None and has_valid_event_auth() and auth.type == "basic" and auth.username == AUTH_USER and auth.password == AUTH_KEY )
[docs]@dataclass class MailmanMessageList: id: str name: str host: str
[docs]@dataclass class MailmanMemberAddress: email: str name: None | ( str ) # Names associated with an (member) email addresses are optional in Mailman.
[docs]@dataclass class MailmanMember: user_id: int address: MailmanMemberAddress
[docs]@dataclass class SubscriptionEvent: event: str mlist: MailmanMessageList member: MailmanMember
SubscriptionEventSchema = class_schema(SubscriptionEvent)
[docs]@dataclass class NewMessageEvent: event: str mlist: MailmanMessageList message: dict
NewMessageEventSchema = class_schema(NewMessageEvent) EVENTS = { "user_subscribed": SubscriptionEventSchema(), "user_unsubscribed": SubscriptionEventSchema(), "new_message": NewMessageEventSchema(), }
[docs]@mailman_events.post("") @csrf.exempt def handle_event() -> Response: """Handle events sent by Mailman.""" if not check_auth(): return Response( status=401, headers={"WWW-Authenticate": 'Basic realm="Needs auth"'} ) if not request.is_json: raise RouteException("Body must be JSON") data = request.json if not data or not isinstance(data, dict): raise RouteException("Body must be JSON object") if "event" not in data or data["event"] not in EVENTS: raise RouteException("Event not handled") evt = EVENTS[data["event"]].load(data) if isinstance(evt, SubscriptionEvent): if evt.event == "user_subscribed": # TODO: Handle subscription event. pass elif evt.event == "user_unsubscribed": # TODO: Handle unsubscription event. pass # TODO: Check if this message is a duplicate. If it is, then handle (e.g. drop) it. How to check if the message # is a duplicate? If we are checking for a duplicate, should we be counting how "manyeth" duplicate the message # is, so we can e.g. catch if there is a spammer channel that bombards with duplicate messages? elif isinstance(evt, NewMessageEvent): handle_new_message(evt) return ok_response()
[docs]def handle_new_message(event: NewMessageEvent) -> None: """Handles an event raised by a new message. :param event: Contains information about a new message sent to Mailman's list. """ m_list_name, _, _ = event.mlist.name.partition("@") message_list = MessageListModel.get_by_name(m_list_name) if message_list is None: raise RouteException("Message list does not exist.") if not message_list.email_list_domain == event.mlist.host: # If we are here, something is now funky. Message list doesn't have a email list (domain) configured, # but messages are directed at it. Not sure what do exactly do here, honestly, except log the event for # further investigation. log_warning( f"Message list '{message_list.name}' with id '{message_list.id}' doesn't have a domain " f"configured properly. Domain '{event.mlist.host}' was expected." ) raise RouteException("List not configured properly.") parsed_message = parse_mailman_message(event.message, message_list) archive_message(message_list, parsed_message)
# TODO: Relay this message forward, if there are other message channels in use for a message list.