msg_auth_status/alloc_yes/
verifier.rs1#[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#[derive(Debug)]
19pub struct ReturnPathVerifier<'hdr> {
20 auth_status: &'hdr MessageAuthStatus<'hdr>,
21 return_path: AddrSpec<'hdr>,
22}
23
24#[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#[derive(Debug, PartialEq)]
55pub enum ReturnPathVerifierStatus {
56 Nothing,
58 Pass,
60 Fail,
62}
63
64fn 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 if let Some(new_ret) = new_ret {
87 ret = new_ret;
88 }
89 }
90 }
91 }
92 ret
93}
94
95impl<'hdr> ReturnPathVerifier<'hdr> {
96 #[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 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 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}