"""Parse bounce messages generated by Postfix.

This also matches something called 'Keftamail' which looks just like Postfix
bounces with the word Postfix scratched out and the word 'Keftamail' written
in in crayon.

It also matches something claiming to be 'The BNS Postfix program', and
'SMTP_Gateway'.  Everybody's gotta be different, huh?
"""

import re

from email.message import Message
from enum import Enum
from io import BytesIO
from typing import cast

from public import public

from flufl.bounce.interfaces import NoFailures, NoTemporaryFailures, Recipients


# Are these heuristics correct or guaranteed?
pcre = re.compile(b'[ \\t]*the\\s*(bns)?\\s*(postfix|keftamail|smtp_gateway)', re.IGNORECASE)
rcre = re.compile(b'failure reason:$', re.IGNORECASE)
acre = re.compile(b'<(?P<addr>[^>]*)>:')

REPORT_TYPES = ('multipart/mixed', 'multipart/report')


class ParseState(Enum):
    start = 0
    salutation_found = 1


def findaddr(msg: Message) -> set[bytes]:
    addresses: set[bytes] = set()
    # findaddr is only called with a text/plain leaf subpart, so the decoded
    # payload is bytes at runtime; pyrefly sees the wider union.
    body = BytesIO(cast(bytes, msg.get_payload(decode=True)))
    state = ParseState.start
    for line in body:
        # Preserve leading whitespace.
        line = line.rstrip()
        # Yes, use match() to match at beginning of string.
        if state is ParseState.start and (pcre.match(line) or rcre.match(line)):
            # Then...
            state = ParseState.salutation_found
        elif state is ParseState.salutation_found and line:
            if mo := acre.search(line):
                addresses.add(mo.group('addr'))
            # Probably a continuation line.
    return addresses


@public
class Postfix:
    """Parse bounce messages generated by Postfix."""

    def process(self, msg: Message) -> tuple[Recipients, Recipients]:
        """See `BounceDetector`."""
        if msg.get_content_type() not in REPORT_TYPES:
            return NoFailures
        # We're looking for the plain/text subpart with a Content-Description:
        # of 'notification'.
        leaves = [p for p in msg.walk() if not p.is_multipart()]
        for subpart in leaves:
            content_type = subpart.get_content_type()
            content_desc = subpart.get('content-description', '').lower()
            if content_type == 'text/plain' and content_desc == 'notification':
                return NoTemporaryFailures, frozenset(findaddr(subpart))
        return NoFailures
