msg_auth_status/alloc_yes/
verifier.rs

1//! Supplementary Verifier API
2//!
3//! This is a best effort implementation for now and may not work in all scenarios.
4
5#[cfg(any(feature = "alloc", feature = "std"))]
6use crate::alloc_yes::MessageAuthStatus;
7
8use crate::addr::AddrSpec;
9use crate::dkim::DkimResultCode;
10
11use crate::parser::addr_spec::{parse_addr_spec, AddrSpecToken};
12
13use crate::error::ReturnPathVerifierError;
14
15use logos::Logos;
16
17/// Verify that the `Return-Path` is authenticated
18#[derive(Debug)]
19pub struct ReturnPathVerifier<'hdr> {
20    auth_status: &'hdr MessageAuthStatus<'hdr>,
21    return_path: AddrSpec<'hdr>,
22}
23
24// Validate that Return-Path exists exactly only once and return it
25#[cfg(feature = "mail_parser")]
26fn exact_once_return_path<'hdr>(
27    msg: &'hdr mail_parser::Message<'hdr>,
28) -> Result<AddrSpec<'hdr>, ReturnPathVerifierError<'hdr>> {
29    let mut items = msg.header_values("Return-Path");
30
31    let candidate = if let Some(item) = items.next() {
32        match item.as_text() {
33            Some(text) => text,
34            None => return Err(ReturnPathVerifierError::NoHeader),
35        }
36    } else {
37        return Err(ReturnPathVerifierError::NoHeader);
38    };
39
40    if items.next().is_some() {
41        return Err(ReturnPathVerifierError::MultipleNotAllowed);
42    }
43
44    let mut lex = AddrSpecToken::lexer(candidate);
45    let parsed_return_path = match parse_addr_spec(&mut lex, true) {
46        Ok(parsed) => parsed,
47        Err(e) => return Err(ReturnPathVerifierError::InvalidHeader(e)),
48    };
49
50    Ok(parsed_return_path)
51}
52
53/// Return-Path verifier Status
54#[derive(Debug, PartialEq)]
55pub enum ReturnPathVerifierStatus {
56    /// No DKIM results seen related to Return-path and header.d Authentication DKIM Result code
57    Nothing,
58    /// Seen at least one "Pass" DKIM Result code in Authentication-Results relevant to Return-Path
59    Pass,
60    /// Seen no "Pass" DKIM Result code in Authentication-Results relevant to Return-Path
61    Fail,
62}
63
64// Check one AuthenticationResult header's DKIM Resutls against Domain
65// Since hosts may have spotty algorithm support - at least one DKIM pass is required for header.d
66fn check_dkim_res<'hdr>(
67    res: &'hdr crate::alloc_yes::AuthenticationResults<'hdr>,
68    domain: &'hdr str,
69) -> ReturnPathVerifierStatus {
70    let mut ret = ReturnPathVerifierStatus::Nothing;
71    let dkim_res_iter = res.dkim_result.iter();
72    for dkim_res in dkim_res_iter {
73        if let Some(header_d) = dkim_res.header_d {
74            if header_d == domain {
75                let new_ret = match dkim_res.code {
76                    DkimResultCode::Pass => return ReturnPathVerifierStatus::Pass,
77                    DkimResultCode::Fail => Some(ReturnPathVerifierStatus::Fail),
78                    DkimResultCode::TempError => Some(ReturnPathVerifierStatus::Fail),
79                    DkimResultCode::PermError => Some(ReturnPathVerifierStatus::Fail),
80                    DkimResultCode::Neutral => None,
81                    DkimResultCode::NoneDkim => None,
82                    DkimResultCode::Unknown => None,
83                    DkimResultCode::Policy => None,
84                };
85                // One pass is enough for given header.d == domain
86                if let Some(new_ret) = new_ret {
87                    ret = new_ret;
88                }
89            }
90        }
91    }
92    ret
93}
94
95impl<'hdr> ReturnPathVerifier<'hdr> {
96    /// Construct Verifier from alloc_yes AuthenticationResults and mail_parser Headers containing Return-Path
97    #[cfg(all(any(feature = "alloc", feature = "std"), feature = "mail_parser"))]
98    pub fn from_alloc_yes(
99        auth_status: &'hdr MessageAuthStatus<'hdr>,
100        msg: &'hdr mail_parser::Message<'hdr>,
101    ) -> Result<Self, ReturnPathVerifierError<'hdr>> {
102        let return_path = exact_once_return_path(msg)?;
103        Ok(Self {
104            auth_status,
105            return_path,
106        })
107    }
108    /// Verify that Auth-Results contain at least one pass for DKIM header.d relevant to Return-Path header
109    pub fn verify(&self) -> Result<ReturnPathVerifierStatus, ReturnPathVerifierError<'hdr>> {
110        let mut dkim_pass_selector = false;
111        let res_iter = self.auth_status.auth_results.iter();
112
113        for res in res_iter {
114            // Host may have multiple signature methods - one of many must pass
115            match check_dkim_res(res, self.return_path.domain) {
116                ReturnPathVerifierStatus::Fail => {}
117                ReturnPathVerifierStatus::Pass => {
118                    dkim_pass_selector = true;
119                    break;
120                }
121                ReturnPathVerifierStatus::Nothing => {}
122            }
123        }
124
125        match dkim_pass_selector {
126            true => Ok(ReturnPathVerifierStatus::Pass),
127            false => Ok(ReturnPathVerifierStatus::Fail),
128        }
129    }
130}
131
132#[cfg(test)]
133#[cfg(feature = "mail_parser")]
134mod test {
135    use super::*;
136    use rstest::rstest;
137    use std::{fs::File, io::Read, path::PathBuf};
138
139    use crate::alloc_yes::MessageAuthStatus;
140
141    fn load_test_data(file_location: &PathBuf) -> Vec<u8> {
142        let mut file = File::open(file_location).unwrap();
143        let mut data: Vec<u8> = vec![];
144        file.read_to_end(&mut data).unwrap();
145        data
146    }
147
148    #[rstest]
149    #[case("to_in_protonmail.eml", Ok(ReturnPathVerifierStatus::Pass))]
150    #[case("to_in_fastmail.eml", Ok(ReturnPathVerifierStatus::Pass))]
151    #[case("to_in_areweat.eml", Ok(ReturnPathVerifierStatus::Pass))]
152    #[case("fail_to_in_areweat.eml", Ok(ReturnPathVerifierStatus::Fail))]
153    fn from_mail_parser(
154        #[case] file: &'static str,
155        #[case] expected: Result<ReturnPathVerifierStatus, ReturnPathVerifierError<'static>>,
156    ) {
157        let path = PathBuf::from("test_data");
158        let full_path = path.join(file);
159        let raw = load_test_data(&full_path);
160        let parser = mail_parser::MessageParser::default();
161        let parsed_message = parser.parse(&raw).unwrap();
162        let status = MessageAuthStatus::from_mail_parser(&parsed_message).unwrap();
163        let verifier = ReturnPathVerifier::from_alloc_yes(&status, &parsed_message).unwrap();
164
165        assert_eq!(verifier.verify(), expected);
166    }
167}