"""Email-related types and enums.""" import hashlib import uuid from base64 import b32decode, b32encode, b64decode, b64encode from dataclasses import dataclass from enum import StrEnum from typing import Literal class EmailType(StrEnum): """Type of email being sent.""" CAMPAIGN = "CAMPAIGN" AUTOMATED = "AUTOMATED" @dataclass(frozen=True, kw_only=True) class EmailInfo: """Email information for encoding/decoding local part.""" email_id: str email_type: EmailType version: Literal["v1", "v2", "v3"] automated_email_trigger_id: str | None = None def encode_email_to_local_part( email_id: str, email_type: EmailType, version: Literal["v1", "v2", "v3"], automated_email_trigger_id: str | None = None, ) -> str: """Encode email information into a local part for email address.""" if version == "v1": encoded_email_id = b64encode(email_id.encode()).decode() return f"{encoded_email_id}.v1" parts = [] if email_type == EmailType.CAMPAIGN: parts.append("camp") parts.append(email_id) elif email_type == EmailType.AUTOMATED: parts.append("auto") parts.append(email_id) if automated_email_trigger_id: parts.append(automated_email_trigger_id) else: raise ValueError(f"Invalid email type: {email_type}") payload = ".".join(parts) if version == "v2": encoded = b64encode(payload.encode()).decode() return f"{encoded}.v2" # v3: base32 survives email-provider lowercasing; produce lowercase output encoded = b32encode(payload.encode()).decode().lower() return f"{encoded}.v3" def decode_local_part_to_email(local_part: str) -> EmailInfo: """Decode a local part back to email information.""" if local_part.endswith(".v1"): version: Literal["v1", "v2", "v3"] = "v1" encoded_part = local_part[:-3] elif local_part.endswith(".v2"): version = "v2" encoded_part = local_part[:-3] elif local_part.endswith(".v3"): version = "v3" encoded_part = local_part[:-3] else: raise ValueError(f"Invalid local part format: {local_part}") if version == "v3": decoded = b32decode(encoded_part.upper()).decode() else: decoded = b64decode(encoded_part).decode() if version == "v1": return EmailInfo( email_id=decoded, email_type=EmailType.CAMPAIGN, # v1 only supports campaigns version=version, automated_email_trigger_id=None, ) # v2 format: parse dot-separated parts parts = decoded.split(".") if len(parts) < 2: raise ValueError(f"Invalid v2 format: {decoded}") email_type_prefix = parts[0] email_id = parts[1] automated_email_trigger_id = parts[2] if len(parts) > 2 else None if email_type_prefix == "camp": email_type = EmailType.CAMPAIGN elif email_type_prefix == "auto": email_type = EmailType.AUTOMATED else: raise ValueError(f"Invalid email type prefix: {email_type_prefix}") return EmailInfo( email_id=email_id, email_type=email_type, version=version, automated_email_trigger_id=automated_email_trigger_id, ) REPLY_TO_SIGNATURE_SIZE = 8 def encode_reply_to_local_part( *, email_id: str, email_type: EmailType, key: str, automated_email_trigger_id: str | None = None, ) -> str: """Encode email information into a signed local part for email address.""" if email_type == EmailType.CAMPAIGN: type_byte = b"c" parts = ["c", uuid.UUID(email_id).hex] elif email_type == EmailType.AUTOMATED: type_byte = b"a" parts = ["a", uuid.UUID(email_id).hex] if automated_email_trigger_id: parts.append(automated_email_trigger_id) else: raise ValueError(f"Invalid email type: {email_type}") sig = hashlib.blake2b( type_byte + uuid.UUID(email_id).bytes, key=key.encode()[:64], digest_size=REPLY_TO_SIGNATURE_SIZE, ).hexdigest() parts.append(sig) return ".".join(parts) def decode_reply_to_local_part(local_part: str, key: str) -> EmailInfo: """Decode a signed local part and verify its HMAC signature.""" parts = local_part.lower().split(".") if len(parts) < 3: raise ValueError(f"Invalid signed format: {local_part}") email_type_prefix = parts[0] if email_type_prefix == "c": type_byte = b"c" email_type = EmailType.CAMPAIGN automated_email_trigger_id = None email_id = str(uuid.UUID(parts[1])) signature = parts[2] elif email_type_prefix == "a": type_byte = b"a" email_type = EmailType.AUTOMATED if len(parts) == 4: email_id = str(uuid.UUID(parts[1])) automated_email_trigger_id = parts[2] signature = parts[3] else: email_id = str(uuid.UUID(parts[1])) automated_email_trigger_id = None signature = parts[2] else: raise ValueError(f"Invalid email type prefix: {email_type_prefix}") expected_sig = hashlib.blake2b( type_byte + uuid.UUID(email_id).bytes, key=key.encode()[:64], digest_size=REPLY_TO_SIGNATURE_SIZE, ).hexdigest() if signature != expected_sig: raise ValueError("Invalid signature") return EmailInfo( email_id=email_id, email_type=email_type, version="v2", automated_email_trigger_id=automated_email_trigger_id, ) def normalize_email(email: str) -> str: return email.strip().lower()