223 lines
8.1 KiB
Python
223 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from uuid import uuid5, NAMESPACE_URL
|
|
|
|
from .models import (
|
|
AssessmentCategory,
|
|
Confidence,
|
|
EmailEvidenceCandidate,
|
|
EndpointQualityUpdate,
|
|
EvidenceStrength,
|
|
MessageClass,
|
|
ParsedMailboxMessage,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EvidenceMapping:
|
|
event_type: str
|
|
assessment_category: AssessmentCategory
|
|
assessment_subclass: str
|
|
evidence_strength: EvidenceStrength
|
|
normalized_meaning: str
|
|
|
|
|
|
EVIDENCE_MAPPINGS: dict[MessageClass, EvidenceMapping | None] = {
|
|
MessageClass.HARD_BOUNCE: EvidenceMapping(
|
|
"notification.endpoint.rejected_permanent",
|
|
AssessmentCategory.FAIL,
|
|
"fail.hard_bounce",
|
|
EvidenceStrength.NEGATIVE,
|
|
"Strong evidence that the recipient endpoint rejected the message permanently.",
|
|
),
|
|
MessageClass.SOFT_BOUNCE: EvidenceMapping(
|
|
"notification.endpoint.rejected_temporary",
|
|
AssessmentCategory.UNDEF,
|
|
"undef.deferred",
|
|
EvidenceStrength.AMBIGUOUS,
|
|
"Temporary endpoint failure; retry or later evidence may change interpretation.",
|
|
),
|
|
MessageClass.DELAYED_DELIVERY_NOTICE: EvidenceMapping(
|
|
"notification.endpoint.deferred",
|
|
AssessmentCategory.UNDEF,
|
|
"undef.deferred",
|
|
EvidenceStrength.AMBIGUOUS,
|
|
"Delivery is delayed and still uncertain.",
|
|
),
|
|
MessageClass.FINAL_DELIVERY_FAILURE: EvidenceMapping(
|
|
"notification.endpoint.rejected_permanent",
|
|
AssessmentCategory.FAIL,
|
|
"fail.expired_without_delivery",
|
|
EvidenceStrength.NEGATIVE,
|
|
"Provider or endpoint gave up after retrying.",
|
|
),
|
|
MessageClass.OUT_OF_OFFICE: EvidenceMapping(
|
|
"interaction.out_of_office_received",
|
|
AssessmentCategory.UNDEF,
|
|
"undef.out_of_office",
|
|
EvidenceStrength.WEAK,
|
|
"Mailbox auto-reply was observed; awareness and action remain uncertain.",
|
|
),
|
|
MessageClass.HUMAN_REPLY: EvidenceMapping(
|
|
"interaction.reply_received",
|
|
AssessmentCategory.SUCCESS,
|
|
"success.reply_received",
|
|
EvidenceStrength.MEDIUM,
|
|
"A reply-like message was observed on the email channel.",
|
|
),
|
|
MessageClass.COMPLAINT_OR_ABUSE: EvidenceMapping(
|
|
"notification.channel.complaint_received",
|
|
AssessmentCategory.FAIL,
|
|
"fail.complaint_received",
|
|
EvidenceStrength.NEGATIVE,
|
|
"Complaint or abuse feedback was observed.",
|
|
),
|
|
MessageClass.UNSUBSCRIBE_OR_OPT_OUT: EvidenceMapping(
|
|
"notification.channel.unsubscribe_received",
|
|
AssessmentCategory.FAIL,
|
|
"fail.unsubscribed",
|
|
EvidenceStrength.NEGATIVE,
|
|
"Opt-out or unsubscribe signal was observed.",
|
|
),
|
|
MessageClass.UNKNOWN_RETURN_MESSAGE: EvidenceMapping(
|
|
"notification.endpoint.unknown",
|
|
AssessmentCategory.UNDEF,
|
|
"undef.conflicting_evidence",
|
|
EvidenceStrength.AMBIGUOUS,
|
|
"Return-channel message could not be classified reliably.",
|
|
),
|
|
MessageClass.CHALLENGE_RESPONSE: EvidenceMapping(
|
|
"interaction.unverified_actor_interaction",
|
|
AssessmentCategory.UNDEF,
|
|
"undef.identity_uncertain",
|
|
EvidenceStrength.AMBIGUOUS,
|
|
"Challenge-response or automated interaction was observed.",
|
|
),
|
|
MessageClass.UNRELATED_MESSAGE: None,
|
|
MessageClass.PARSE_FAILED: EvidenceMapping(
|
|
"diagnostic.message.parse_failed",
|
|
AssessmentCategory.UNDEF,
|
|
"undef.parse_failed",
|
|
EvidenceStrength.NONE,
|
|
"Message source could not be parsed reliably; no delivery or interaction claim is made.",
|
|
),
|
|
}
|
|
|
|
|
|
def evidence_deduplication_key(parsed: ParsedMailboxMessage, mapping: EvidenceMapping) -> str:
|
|
parts = [
|
|
parsed.mailbox_message_id,
|
|
parsed.parser_version,
|
|
mapping.event_type,
|
|
parsed.affected_email_address or "",
|
|
parsed.original_message_id or "",
|
|
parsed.smtp_status_code or "",
|
|
parsed.enhanced_status_code or "",
|
|
parsed.reason_code or "",
|
|
]
|
|
return "|".join(parts)
|
|
|
|
|
|
def candidate_from_parsed(
|
|
parsed: ParsedMailboxMessage,
|
|
*,
|
|
raw_message_ref: str | None,
|
|
observed_at: datetime,
|
|
occurred_at: datetime | None,
|
|
) -> EmailEvidenceCandidate | None:
|
|
mapping = EVIDENCE_MAPPINGS.get(parsed.message_class)
|
|
if mapping is None:
|
|
return None
|
|
|
|
dedup_key = evidence_deduplication_key(parsed, mapping)
|
|
candidate_id = str(uuid5(NAMESPACE_URL, "email-connect:evidence:" + dedup_key))
|
|
return EmailEvidenceCandidate(
|
|
evidence_candidate_id=candidate_id,
|
|
mailbox_message_id=parsed.mailbox_message_id,
|
|
parsed_message_id=parsed.parsed_message_id,
|
|
event_type=mapping.event_type,
|
|
assessment_category=mapping.assessment_category,
|
|
assessment_subclass=mapping.assessment_subclass,
|
|
affected_email_address=parsed.affected_email_address,
|
|
original_message_id=parsed.original_message_id,
|
|
confidence=parsed.confidence,
|
|
evidence_strength=mapping.evidence_strength,
|
|
occurred_at=occurred_at,
|
|
observed_at=observed_at,
|
|
deduplication_key=dedup_key,
|
|
raw_message_ref=raw_message_ref,
|
|
notes=[mapping.normalized_meaning, *parsed.notes],
|
|
metadata={
|
|
"message_class": parsed.message_class.value,
|
|
"smtp_status_code": parsed.smtp_status_code,
|
|
"enhanced_status_code": parsed.enhanced_status_code,
|
|
"reason_code": parsed.reason_code,
|
|
},
|
|
)
|
|
|
|
|
|
def endpoint_quality_from_candidate(candidate: EmailEvidenceCandidate) -> EndpointQualityUpdate | None:
|
|
address = candidate.affected_email_address
|
|
if not address:
|
|
return None
|
|
|
|
message_class = candidate.metadata.get("message_class")
|
|
signal = str(message_class or candidate.event_type)
|
|
reason_code = candidate.metadata.get("reason_code")
|
|
observed_at = candidate.observed_at
|
|
|
|
if message_class in {MessageClass.HARD_BOUNCE.value, MessageClass.FINAL_DELIVERY_FAILURE.value}:
|
|
return EndpointQualityUpdate(
|
|
affected_email_address=address.lower(),
|
|
reachability="unreachable",
|
|
last_failure_at=observed_at,
|
|
reason_code=reason_code,
|
|
confidence=candidate.confidence,
|
|
quality_signals=[signal, candidate.event_type],
|
|
)
|
|
if message_class in {MessageClass.SOFT_BOUNCE.value, MessageClass.DELAYED_DELIVERY_NOTICE.value}:
|
|
return EndpointQualityUpdate(
|
|
affected_email_address=address.lower(),
|
|
reachability="degraded",
|
|
last_failure_at=observed_at,
|
|
reason_code=reason_code,
|
|
confidence=candidate.confidence,
|
|
quality_signals=[signal, candidate.event_type],
|
|
)
|
|
if message_class == MessageClass.COMPLAINT_OR_ABUSE.value:
|
|
return EndpointQualityUpdate(
|
|
affected_email_address=address.lower(),
|
|
suppression_state="suppressed",
|
|
last_failure_at=observed_at,
|
|
reason_code=reason_code,
|
|
confidence=candidate.confidence,
|
|
quality_signals=[signal, candidate.event_type],
|
|
)
|
|
if message_class == MessageClass.UNSUBSCRIBE_OR_OPT_OUT.value:
|
|
return EndpointQualityUpdate(
|
|
affected_email_address=address.lower(),
|
|
suppression_state="opted_out",
|
|
reason_code=reason_code,
|
|
confidence=candidate.confidence,
|
|
quality_signals=[signal, candidate.event_type],
|
|
)
|
|
if message_class == MessageClass.HUMAN_REPLY.value:
|
|
return EndpointQualityUpdate(
|
|
affected_email_address=address.lower(),
|
|
last_success_at=observed_at,
|
|
reason_code=reason_code,
|
|
confidence=candidate.confidence,
|
|
quality_signals=[signal, candidate.event_type],
|
|
)
|
|
if message_class == MessageClass.OUT_OF_OFFICE.value:
|
|
return EndpointQualityUpdate(
|
|
affected_email_address=address.lower(),
|
|
reachability="uncertain",
|
|
last_auto_reply_at=observed_at,
|
|
reason_code=reason_code,
|
|
confidence=candidate.confidence,
|
|
quality_signals=[signal, candidate.event_type],
|
|
)
|
|
return None
|