emailsec.auth
1from enum import Enum 2from dataclasses import dataclass 3 4from emailsec.spf.checker import check_spf, SPFCheck, SPFResult 5from emailsec.dkim.checker import check_dkim, DKIMCheck, DKIMResult 6from emailsec.dmarc import ( 7 check_dmarc, 8 DMARCPolicy, 9 DMARCResult, 10) 11from emailsec.arc import check_arc, ARCCheck 12import emailsec.utils 13from emailsec import errors 14 15 16class DeliveryAction(Enum): 17 ACCEPT = "accept" 18 QUARANTINE = "quarantine" 19 REJECT = "reject" 20 DEFER = "defer" # SMTP server should return 451 4.3.0 Temporary lookup failure 21 22 23@dataclass 24class SMTPContext: 25 # Connection info 26 sender_ip_address: str 27 client_hostname: str | None # EHLO/HELO hostname 28 29 # Envelope data 30 mail_from: str # MAIL FROM address (envelope sender) 31 32 # TOOD: timestamp to check for expired signature? 33 34 35@dataclass 36class AuthenticationConfiguration: 37 trusted_signers: list[str] | None = None 38 39 40def make_delivery_decision( 41 spf_check: SPFCheck, 42 dkim_check: DKIMCheck, 43 arc_check: ARCCheck, 44 dmarc_result: DMARCResult, 45) -> DeliveryAction: 46 """ 47 Delivery decision logic following RFC 7489. 48 49 1. DMARC policy is enforced first (if the sender has one) 50 2. If DMARC passes → Accept 51 3. If no DMARC or policy is "none" → Check individual authentications 52 4. Default to quarantine for unauthenticated mail 53 54 RFC 7489: "DMARC-compliant Mail Receivers typically disregard any 55 mail-handling directive discovered as part of an authentication mechanism 56 where a DMARC record is also discovered that specifies a policy other than 'none'" 57 58 RFC 7489 warns against rejecting on SPF fail before checking DMARC, 59 as this could prevent legitimate mail that passes DKIM+DMARC. 60 """ 61 # Defer the delivery decision if SPF or DKIM failed with a temp error 62 if ( 63 spf_check.result == SPFResult.TEMPERROR 64 or dkim_check.result == DKIMResult.TEMPFAIL 65 ): 66 return DeliveryAction.DEFER 67 68 # DMARC policy takes precedence (RFC 7489 Section 6.3) 69 if dmarc_result.result == "fail": 70 match dmarc_result.policy: 71 case DMARCPolicy.REJECT: 72 # RFC 7489: "the Mail Receiver SHOULD reject the message" 73 return DeliveryAction.REJECT 74 case DMARCPolicy.QUARANTINE: 75 # RFC 7489: "the Mail Receiver SHOULD place the message in 76 # a quarantine area or folder instead of delivering it" 77 return DeliveryAction.QUARANTINE 78 case DMARCPolicy.NONE: 79 # RFC 7489: "the Domain Owner requests no specific action 80 # be taken regarding delivery of the message" 81 pass # Continue to fallback logic 82 83 # If DMARC passes, accept 84 if dmarc_result.result == "pass": 85 return DeliveryAction.ACCEPT 86 87 # Fallback logic when DMARC is not available or policy is "none" 88 # RFC 7489: "Final disposition of a message is always a matter of local policy" 89 90 # Accept if any DKIM signature passes 91 if dkim_check.result == DKIMResult.SUCCESS: 92 return DeliveryAction.ACCEPT 93 94 # Accept if SPF passes 95 if spf_check.result == SPFResult.PASS: 96 return DeliveryAction.ACCEPT 97 98 # TODO: if no DMARC policy (or none), look for a trusted ARC to fallback to accept 99 100 # Conservative default for unauthenticated mail 101 return DeliveryAction.QUARANTINE 102 103 104@dataclass 105class AuthenticationResult: 106 delivery_action: DeliveryAction 107 spf_check: SPFCheck 108 dkim_check: DKIMCheck 109 dmarc_result: DMARCResult | None 110 arc_check: ARCCheck 111 112 113async def authenticate_message( 114 smtp_context: SMTPContext, 115 raw_email: bytes, 116 configuration: AuthenticationConfiguration | None = None, 117) -> AuthenticationResult: 118 """ 119 Authenticate an incoming email using SPF, DKIM, and DMARC. 120 121 Authentication flow: 122 1. SPF (RFC 7208): Verify the sending IP is authorized for the envelope sender domain 123 2. DKIM (RFC 6376): Verify cryptographic signatures on the email 124 3. ARC (RFC 8617): Check authentication chain if present (for forwarded mail) 125 4. DMARC (RFC 7489): Evaluate if SPF/DKIM align with the From header domain 126 5. Make delivery decision based on combined results 127 """ 128 body_and_headers = emailsec.utils.body_and_headers_for_canonicalization(raw_email) 129 header_from = emailsec.utils.header_value(body_and_headers[1], "from") 130 131 # Step 1: SPF Check (RFC 7208) 132 # RFC 7489: SPF authenticates the envelope sender domain 133 spf_check = await check_spf( 134 smtp_context.sender_ip_address, 135 smtp_context.mail_from, 136 ) 137 138 # Step 2: DKIM Verification (RFC 6376) 139 # Performed independently of SPF per RFC 7489 Section 4.3 140 dkim_check = await check_dkim(raw_email) 141 142 # Step 3: ARC Processing (RFC 8617) - if ARC headers present 143 # RFC 8617 Section 7.2: "allows Internet Mail Handler to potentially base 144 # decisions of message disposition on authentication assessments" 145 arc_check = await check_arc(raw_email, body_and_headers) 146 147 # Step 4: DMARC Evaluation (RFC 7489) 148 # RFC 7489: "A message satisfies the DMARC checks if at least one of the 149 # supported authentication mechanisms produces a 'pass' result" 150 try: 151 dmarc_result = await check_dmarc( 152 header_from=header_from, 153 envelope_from=smtp_context.mail_from, 154 spf_check=spf_check, 155 dkim_check=dkim_check, 156 arc_check=arc_check, 157 configuration=configuration, 158 ) 159 except errors.Temperror: 160 return AuthenticationResult( 161 delivery_action=DeliveryAction.DEFER, 162 spf_check=spf_check, 163 dkim_check=dkim_check, 164 dmarc_result=None, 165 arc_check=arc_check, 166 ) 167 168 # Step 5: Make delivery decision 169 # RFC 7489: "Final disposition of a message is always a matter of local policy" 170 return AuthenticationResult( 171 delivery_action=make_delivery_decision( 172 spf_check, dkim_check, arc_check, dmarc_result 173 ), 174 spf_check=spf_check, 175 dkim_check=dkim_check, 176 dmarc_result=dmarc_result, 177 arc_check=arc_check, 178 )
class
DeliveryAction(enum.Enum):
17class DeliveryAction(Enum): 18 ACCEPT = "accept" 19 QUARANTINE = "quarantine" 20 REJECT = "reject" 21 DEFER = "defer" # SMTP server should return 451 4.3.0 Temporary lookup failure
ACCEPT =
<DeliveryAction.ACCEPT: 'accept'>
QUARANTINE =
<DeliveryAction.QUARANTINE: 'quarantine'>
REJECT =
<DeliveryAction.REJECT: 'reject'>
DEFER =
<DeliveryAction.DEFER: 'defer'>
@dataclass
class
SMTPContext:
@dataclass
class
AuthenticationConfiguration:
def
make_delivery_decision( spf_check: emailsec.spf.SPFCheck, dkim_check: emailsec.dkim.DKIMCheck, arc_check: emailsec.arc.ARCCheck, dmarc_result: emailsec.dmarc.DMARCResult) -> DeliveryAction:
41def make_delivery_decision( 42 spf_check: SPFCheck, 43 dkim_check: DKIMCheck, 44 arc_check: ARCCheck, 45 dmarc_result: DMARCResult, 46) -> DeliveryAction: 47 """ 48 Delivery decision logic following RFC 7489. 49 50 1. DMARC policy is enforced first (if the sender has one) 51 2. If DMARC passes → Accept 52 3. If no DMARC or policy is "none" → Check individual authentications 53 4. Default to quarantine for unauthenticated mail 54 55 RFC 7489: "DMARC-compliant Mail Receivers typically disregard any 56 mail-handling directive discovered as part of an authentication mechanism 57 where a DMARC record is also discovered that specifies a policy other than 'none'" 58 59 RFC 7489 warns against rejecting on SPF fail before checking DMARC, 60 as this could prevent legitimate mail that passes DKIM+DMARC. 61 """ 62 # Defer the delivery decision if SPF or DKIM failed with a temp error 63 if ( 64 spf_check.result == SPFResult.TEMPERROR 65 or dkim_check.result == DKIMResult.TEMPFAIL 66 ): 67 return DeliveryAction.DEFER 68 69 # DMARC policy takes precedence (RFC 7489 Section 6.3) 70 if dmarc_result.result == "fail": 71 match dmarc_result.policy: 72 case DMARCPolicy.REJECT: 73 # RFC 7489: "the Mail Receiver SHOULD reject the message" 74 return DeliveryAction.REJECT 75 case DMARCPolicy.QUARANTINE: 76 # RFC 7489: "the Mail Receiver SHOULD place the message in 77 # a quarantine area or folder instead of delivering it" 78 return DeliveryAction.QUARANTINE 79 case DMARCPolicy.NONE: 80 # RFC 7489: "the Domain Owner requests no specific action 81 # be taken regarding delivery of the message" 82 pass # Continue to fallback logic 83 84 # If DMARC passes, accept 85 if dmarc_result.result == "pass": 86 return DeliveryAction.ACCEPT 87 88 # Fallback logic when DMARC is not available or policy is "none" 89 # RFC 7489: "Final disposition of a message is always a matter of local policy" 90 91 # Accept if any DKIM signature passes 92 if dkim_check.result == DKIMResult.SUCCESS: 93 return DeliveryAction.ACCEPT 94 95 # Accept if SPF passes 96 if spf_check.result == SPFResult.PASS: 97 return DeliveryAction.ACCEPT 98 99 # TODO: if no DMARC policy (or none), look for a trusted ARC to fallback to accept 100 101 # Conservative default for unauthenticated mail 102 return DeliveryAction.QUARANTINE
Delivery decision logic following RFC 7489.
- DMARC policy is enforced first (if the sender has one)
- If DMARC passes → Accept
- If no DMARC or policy is "none" → Check individual authentications
- Default to quarantine for unauthenticated mail
RFC 7489: "DMARC-compliant Mail Receivers typically disregard any mail-handling directive discovered as part of an authentication mechanism where a DMARC record is also discovered that specifies a policy other than 'none'"
RFC 7489 warns against rejecting on SPF fail before checking DMARC, as this could prevent legitimate mail that passes DKIM+DMARC.
@dataclass
class
AuthenticationResult:
105@dataclass 106class AuthenticationResult: 107 delivery_action: DeliveryAction 108 spf_check: SPFCheck 109 dkim_check: DKIMCheck 110 dmarc_result: DMARCResult | None 111 arc_check: ARCCheck
AuthenticationResult( delivery_action: DeliveryAction, spf_check: emailsec.spf.SPFCheck, dkim_check: emailsec.dkim.DKIMCheck, dmarc_result: emailsec.dmarc.DMARCResult | None, arc_check: emailsec.arc.ARCCheck)
delivery_action: DeliveryAction
spf_check: emailsec.spf.SPFCheck
dkim_check: emailsec.dkim.DKIMCheck
dmarc_result: emailsec.dmarc.DMARCResult | None
arc_check: emailsec.arc.ARCCheck
async def
authenticate_message( smtp_context: SMTPContext, raw_email: bytes, configuration: AuthenticationConfiguration | None = None) -> AuthenticationResult:
114async def authenticate_message( 115 smtp_context: SMTPContext, 116 raw_email: bytes, 117 configuration: AuthenticationConfiguration | None = None, 118) -> AuthenticationResult: 119 """ 120 Authenticate an incoming email using SPF, DKIM, and DMARC. 121 122 Authentication flow: 123 1. SPF (RFC 7208): Verify the sending IP is authorized for the envelope sender domain 124 2. DKIM (RFC 6376): Verify cryptographic signatures on the email 125 3. ARC (RFC 8617): Check authentication chain if present (for forwarded mail) 126 4. DMARC (RFC 7489): Evaluate if SPF/DKIM align with the From header domain 127 5. Make delivery decision based on combined results 128 """ 129 body_and_headers = emailsec.utils.body_and_headers_for_canonicalization(raw_email) 130 header_from = emailsec.utils.header_value(body_and_headers[1], "from") 131 132 # Step 1: SPF Check (RFC 7208) 133 # RFC 7489: SPF authenticates the envelope sender domain 134 spf_check = await check_spf( 135 smtp_context.sender_ip_address, 136 smtp_context.mail_from, 137 ) 138 139 # Step 2: DKIM Verification (RFC 6376) 140 # Performed independently of SPF per RFC 7489 Section 4.3 141 dkim_check = await check_dkim(raw_email) 142 143 # Step 3: ARC Processing (RFC 8617) - if ARC headers present 144 # RFC 8617 Section 7.2: "allows Internet Mail Handler to potentially base 145 # decisions of message disposition on authentication assessments" 146 arc_check = await check_arc(raw_email, body_and_headers) 147 148 # Step 4: DMARC Evaluation (RFC 7489) 149 # RFC 7489: "A message satisfies the DMARC checks if at least one of the 150 # supported authentication mechanisms produces a 'pass' result" 151 try: 152 dmarc_result = await check_dmarc( 153 header_from=header_from, 154 envelope_from=smtp_context.mail_from, 155 spf_check=spf_check, 156 dkim_check=dkim_check, 157 arc_check=arc_check, 158 configuration=configuration, 159 ) 160 except errors.Temperror: 161 return AuthenticationResult( 162 delivery_action=DeliveryAction.DEFER, 163 spf_check=spf_check, 164 dkim_check=dkim_check, 165 dmarc_result=None, 166 arc_check=arc_check, 167 ) 168 169 # Step 5: Make delivery decision 170 # RFC 7489: "Final disposition of a message is always a matter of local policy" 171 return AuthenticationResult( 172 delivery_action=make_delivery_decision( 173 spf_check, dkim_check, arc_check, dmarc_result 174 ), 175 spf_check=spf_check, 176 dkim_check=dkim_check, 177 dmarc_result=dmarc_result, 178 arc_check=arc_check, 179 )
Authenticate an incoming email using SPF, DKIM, and DMARC.
Authentication flow:
- SPF (RFC 7208): Verify the sending IP is authorized for the envelope sender domain
- DKIM (RFC 6376): Verify cryptographic signatures on the email
- ARC (RFC 8617): Check authentication chain if present (for forwarded mail)
- DMARC (RFC 7489): Evaluate if SPF/DKIM align with the From header domain
- Make delivery decision based on combined results