1use ipnet::IpNet;
18use log::LevelFilter;
19use std::{
20 collections::HashSet,
21 error::Error,
22 fmt::{self, Display, Formatter},
23 net::IpAddr,
24 str::FromStr,
25};
26use syslog::Facility;
27use viaspf::{record::ExplainString, DomainName, ParseParamError, SpfResult};
28
29#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
31pub struct ParseSocketError;
32
33impl Error for ParseSocketError {}
34
35impl Display for ParseSocketError {
36 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
37 write!(f, "failed to parse socket")
38 }
39}
40
41#[derive(Clone, Debug, Eq, Hash, PartialEq)]
43pub enum Socket {
44 Inet(String),
45 Unix(String),
46}
47
48impl Display for Socket {
49 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::Inet(s) => write!(f, "inet:{s}"),
52 Self::Unix(s) => write!(f, "unix:{s}"),
53 }
54 }
55}
56
57impl FromStr for Socket {
58 type Err = ParseSocketError;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 if let Some(s) = s.strip_prefix("inet:") {
62 Ok(Self::Inet(s.into()))
63 } else if let Some(s) = s.strip_prefix("unix:") {
64 Ok(Self::Unix(s.into()))
65 } else {
66 Err(ParseSocketError)
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
72pub struct ParseStatusCodeError;
73
74impl Error for ParseStatusCodeError {}
75
76impl Display for ParseStatusCodeError {
77 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78 write!(f, "failed to parse status code")
79 }
80}
81
82#[derive(Clone, Debug, Eq, Hash, PartialEq)]
83pub enum ReplyCode {
84 Transient(String),
85 Permanent(String),
86}
87
88impl AsRef<str> for ReplyCode {
89 fn as_ref(&self) -> &str {
90 match self {
91 Self::Transient(s) | Self::Permanent(s) => s,
92 }
93 }
94}
95
96impl Display for ReplyCode {
97 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
98 self.as_ref().fmt(f)
99 }
100}
101
102impl FromStr for ReplyCode {
103 type Err = ParseStatusCodeError;
104
105 fn from_str(s: &str) -> Result<Self, Self::Err> {
107 match s.as_bytes() {
108 [x, y, z]
109 if matches!(x, b'4'..=b'5')
110 && matches!(y, b'0'..=b'5')
111 && matches!(z, b'0'..=b'9') =>
112 {
113 Ok(match x {
114 b'4' => Self::Transient(s.into()),
115 b'5' => Self::Permanent(s.into()),
116 _ => unreachable!(),
117 })
118 }
119 _ => Err(ParseStatusCodeError),
120 }
121 }
122}
123
124#[derive(Clone, Debug, Eq, Hash, PartialEq)]
125pub enum EnhancedStatusCode {
126 Transient(String),
127 Permanent(String),
128}
129
130impl EnhancedStatusCode {
131 pub fn is_compatible_with(&self, reply_code: &ReplyCode) -> bool {
132 matches!(
133 (self, reply_code),
134 (Self::Transient(_), ReplyCode::Transient(_))
135 | (Self::Permanent(_), ReplyCode::Permanent(_))
136 )
137 }
138}
139
140impl AsRef<str> for EnhancedStatusCode {
141 fn as_ref(&self) -> &str {
142 match self {
143 Self::Transient(s) | Self::Permanent(s) => s,
144 }
145 }
146}
147
148impl Display for EnhancedStatusCode {
149 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
150 self.as_ref().fmt(f)
151 }
152}
153
154impl FromStr for EnhancedStatusCode {
155 type Err = ParseStatusCodeError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
159 fn is_three_digits(s: &str) -> bool {
160 s == "0"
161 || matches!(s.len(), 1..=3)
162 && s.chars().all(|c| c.is_ascii_digit())
163 && !s.starts_with('0')
164 }
165
166 let mut iter = s.splitn(3, '.');
167 match (iter.next(), iter.next(), iter.next()) {
168 (Some(class), Some(subject), Some(detail))
169 if matches!(class, "4" | "5")
170 && is_three_digits(subject)
171 && is_three_digits(detail) =>
172 {
173 Ok(match class {
174 "4" => Self::Transient(s.into()),
175 "5" => Self::Permanent(s.into()),
176 _ => unreachable!(),
177 })
178 }
179 _ => Err(ParseStatusCodeError),
180 }
181 }
182}
183
184#[derive(Clone, Debug, Eq, Hash, PartialEq)]
185pub struct Header(Vec<HeaderType>);
186
187impl Header {
188 pub fn new(header_types: Vec<HeaderType>) -> Result<Self, Vec<HeaderType>> {
189 if header_types.len() == header_types.iter().collect::<HashSet<_>>().len() {
191 Ok(Self(header_types))
192 } else {
193 Err(header_types)
194 }
195 }
196
197 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &HeaderType> {
198 self.0.iter()
199 }
200}
201
202impl Default for Header {
203 fn default() -> Self {
204 HeaderType::ReceivedSpf.into()
205 }
206}
207
208impl From<Vec<HeaderType>> for Header {
209 fn from(header_types: Vec<HeaderType>) -> Self {
210 Self(header_types)
211 }
212}
213
214impl From<HeaderType> for Header {
215 fn from(header_type: HeaderType) -> Self {
216 vec![header_type].into()
217 }
218}
219
220#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
221pub struct ParseHeaderTypeError;
222
223impl Error for ParseHeaderTypeError {}
224
225impl Display for ParseHeaderTypeError {
226 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
227 write!(f, "failed to parse header type")
228 }
229}
230
231#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
232pub enum HeaderType {
233 ReceivedSpf,
234 AuthenticationResults,
235}
236
237impl Display for HeaderType {
238 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
239 match self {
240 Self::ReceivedSpf => write!(f, "Received-SPF"),
241 Self::AuthenticationResults => write!(f, "Authentication-Results"),
242 }
243 }
244}
245
246impl FromStr for HeaderType {
247 type Err = ParseHeaderTypeError;
248
249 fn from_str(s: &str) -> Result<Self, Self::Err> {
250 if s.eq_ignore_ascii_case("Received-SPF") {
252 Ok(HeaderType::ReceivedSpf)
253 } else if s.eq_ignore_ascii_case("Authentication-Results") {
254 Ok(HeaderType::AuthenticationResults)
255 } else {
256 Err(ParseHeaderTypeError)
257 }
258 }
259}
260
261#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
262pub struct ParseResultKindError;
263
264impl Error for ParseResultKindError {}
265
266impl Display for ParseResultKindError {
267 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
268 write!(f, "failed to parse SPF result kind")
269 }
270}
271
272#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
273pub enum RejectResultKind {
274 Fail,
275 Softfail,
276 Temperror,
277 Permerror,
278}
279
280impl RejectResultKind {
281 fn from_spf_result(spf_result: &SpfResult) -> Option<Self> {
282 use SpfResult::*;
283 match spf_result {
284 None | Neutral | Pass => Option::None,
285 Fail(_) => Some(Self::Fail),
286 Softfail => Some(Self::Softfail),
287 Temperror => Some(Self::Temperror),
288 Permerror => Some(Self::Permerror),
289 }
290 }
291}
292
293impl FromStr for RejectResultKind {
294 type Err = ParseResultKindError;
295
296 fn from_str(s: &str) -> Result<Self, Self::Err> {
297 match s {
298 "fail" => Ok(Self::Fail),
299 "softfail" => Ok(Self::Softfail),
300 "temperror" => Ok(Self::Temperror),
301 "permerror" => Ok(Self::Permerror),
302 _ => Err(ParseResultKindError),
303 }
304 }
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
308pub struct RejectResults(HashSet<RejectResultKind>);
309
310impl RejectResults {
311 pub fn includes(&self, result: &SpfResult) -> bool {
312 matches!(RejectResultKind::from_spf_result(result), Some(k) if self.0.contains(&k))
313 }
314}
315
316impl Default for RejectResults {
317 fn default() -> Self {
318 HashSet::from([
319 RejectResultKind::Fail,
320 RejectResultKind::Temperror,
321 RejectResultKind::Permerror,
322 ])
323 .into()
324 }
325}
326
327impl From<HashSet<RejectResultKind>> for RejectResults {
328 fn from(results: HashSet<RejectResultKind>) -> Self {
329 Self(results)
330 }
331}
332
333#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
334pub enum DefinitiveHeloResultKind {
335 Pass,
336 Fail,
337 Softfail,
338 Temperror,
339 Permerror,
340}
341
342impl DefinitiveHeloResultKind {
343 fn from_spf_result(spf_result: &SpfResult) -> Option<Self> {
344 use SpfResult::*;
345 match spf_result {
346 None | Neutral => Option::None,
347 Pass => Some(Self::Pass),
348 Fail(_) => Some(Self::Fail),
349 Softfail => Some(Self::Softfail),
350 Temperror => Some(Self::Temperror),
351 Permerror => Some(Self::Permerror),
352 }
353 }
354}
355
356impl FromStr for DefinitiveHeloResultKind {
357 type Err = ParseResultKindError;
358
359 fn from_str(s: &str) -> Result<Self, Self::Err> {
360 match s {
361 "pass" => Ok(Self::Pass),
362 "fail" => Ok(Self::Fail),
363 "softfail" => Ok(Self::Softfail),
364 "temperror" => Ok(Self::Temperror),
365 "permerror" => Ok(Self::Permerror),
366 _ => Err(ParseResultKindError),
367 }
368 }
369}
370
371#[derive(Clone, Debug, Default, Eq, PartialEq)]
372pub struct DefinitiveHeloResults(HashSet<DefinitiveHeloResultKind>);
373
374impl DefinitiveHeloResults {
375 pub fn includes(&self, result: &SpfResult) -> bool {
376 matches!(DefinitiveHeloResultKind::from_spf_result(result), Some(k) if self.0.contains(&k))
377 }
378}
379
380impl From<HashSet<DefinitiveHeloResultKind>> for DefinitiveHeloResults {
381 fn from(results: HashSet<DefinitiveHeloResultKind>) -> Self {
382 Self(results)
383 }
384}
385
386#[derive(Clone, Debug, Eq, Hash, PartialEq)]
387pub enum ExplainStringMod {
388 Substitute(ExplainString),
389 Decorate {
390 prefix: ExplainString,
391 suffix: ExplainString,
392 },
393}
394
395#[derive(Clone, Debug, Eq, Hash, PartialEq)]
396pub struct ExpExplainString(pub ExplainStringMod);
397
398impl AsRef<ExplainStringMod> for ExpExplainString {
399 fn as_ref(&self) -> &ExplainStringMod {
400 &self.0
401 }
402}
403
404impl From<ExplainStringMod> for ExpExplainString {
405 fn from(m: ExplainStringMod) -> Self {
406 Self(m)
407 }
408}
409
410#[derive(Clone, Debug, Eq, Hash, PartialEq)]
411pub struct ReasonExplainString(pub ExplainStringMod);
412
413impl AsRef<ExplainStringMod> for ReasonExplainString {
414 fn as_ref(&self) -> &ExplainStringMod {
415 &self.0
416 }
417}
418
419impl From<ExplainStringMod> for ReasonExplainString {
420 fn from(m: ExplainStringMod) -> Self {
421 Self(m)
422 }
423}
424
425#[derive(Clone, Debug, Eq, PartialEq)]
426pub struct TrustedNetworks {
427 pub trust_loopback: bool,
428 pub networks: HashSet<IpNet>,
429}
430
431impl TrustedNetworks {
432 pub fn contains(&self, addr: IpAddr) -> bool {
433 self.trust_loopback && addr.is_loopback() || self.networks.iter().any(|n| n.contains(&addr))
434 }
435}
436
437impl Default for TrustedNetworks {
438 fn default() -> Self {
439 Self {
440 trust_loopback: true,
441 networks: Default::default(),
442 }
443 }
444}
445
446#[derive(Clone, Debug, Eq, Hash, PartialEq)]
447pub struct SkipEntry {
448 pub local_part: Option<String>,
449 pub domain: DomainName,
450 pub match_subdomains: bool,
451}
452
453impl SkipEntry {
454 fn matches(&self, sender: &str) -> bool {
455 fn matches_domain(entry: &SkipEntry, domain: &DomainName) -> bool {
456 if entry.match_subdomains {
457 domain.as_ref().is_subdomain_of(entry.domain.as_ref())
458 } else {
459 domain == &entry.domain
460 }
461 }
462
463 fn matches_local_part(entry: &SkipEntry, local_part: Option<&str>) -> bool {
464 match (&local_part, &entry.local_part) {
465 (Some(lp1), Some(lp2)) => {
466 lp1.eq_ignore_ascii_case(lp2)
469 }
470 (None, Some(_)) => false,
471 (_, None) => true,
472 }
473 }
474
475 let (local_part, domain) = match sender.rsplit_once('@') {
476 Some((local_part, domain)) => (Some(local_part), domain),
477 None => (None, sender),
478 };
479
480 match DomainName::new(domain) {
481 Ok(domain) => matches_domain(self, &domain) && matches_local_part(self, local_part),
482 Err(_) => false,
483 }
484 }
485}
486
487impl FromStr for SkipEntry {
488 type Err = ParseParamError;
489
490 fn from_str(s: &str) -> Result<Self, Self::Err> {
491 let mut local_part = None;
492 let mut match_subdomains = false;
493
494 let domain = match s.rsplit_once('@') {
495 Some((l, d)) => {
496 local_part = Some(l.into());
498 d
499 }
500 None => match s.strip_prefix('.') {
501 Some(s) => {
502 match_subdomains = true;
503 s
504 }
505 None => s,
506 },
507 };
508
509 let domain = DomainName::new(domain)?;
510
511 Ok(Self {
512 local_part,
513 domain,
514 match_subdomains,
515 })
516 }
517}
518
519#[derive(Clone, Debug, Default, Eq, PartialEq)]
520pub struct SkipSenders(HashSet<SkipEntry>);
521
522impl SkipSenders {
523 pub fn extended_with(mut self, other: Self) -> Self {
524 self.0.extend(other.0);
525 self
526 }
527
528 pub fn includes(&self, sender: &str) -> bool {
529 self.0.iter().any(|e| e.matches(sender))
530 }
531}
532
533impl From<HashSet<SkipEntry>> for SkipSenders {
534 fn from(results: HashSet<SkipEntry>) -> Self {
535 Self(results)
536 }
537}
538
539#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
541pub struct ParseLogDestinationError;
542
543impl Error for ParseLogDestinationError {}
544
545impl Display for ParseLogDestinationError {
546 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
547 write!(f, "failed to parse log destination")
548 }
549}
550
551#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
553pub enum LogDestination {
554 #[default]
555 Syslog,
556 Stderr,
557}
558
559impl FromStr for LogDestination {
560 type Err = ParseLogDestinationError;
561
562 fn from_str(s: &str) -> Result<Self, Self::Err> {
563 match s {
564 "syslog" => Ok(Self::Syslog),
565 "stderr" => Ok(Self::Stderr),
566 _ => Err(ParseLogDestinationError),
567 }
568 }
569}
570
571#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
573pub struct ParseLogLevelError;
574
575impl Error for ParseLogLevelError {}
576
577impl Display for ParseLogLevelError {
578 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
579 write!(f, "failed to parse log level")
580 }
581}
582
583#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
585pub enum LogLevel {
586 Error,
587 Warn,
588 #[default]
589 Info,
590 Debug,
591}
592
593impl FromStr for LogLevel {
594 type Err = ParseLogLevelError;
595
596 fn from_str(s: &str) -> Result<Self, Self::Err> {
597 match s {
598 "error" => Ok(Self::Error),
599 "warn" => Ok(Self::Warn),
600 "info" => Ok(Self::Info),
601 "debug" => Ok(Self::Debug),
602 _ => Err(ParseLogLevelError),
603 }
604 }
605}
606
607impl From<LogLevel> for LevelFilter {
608 fn from(log_level: LogLevel) -> Self {
609 match log_level {
610 LogLevel::Error => Self::Error,
611 LogLevel::Warn => Self::Warn,
612 LogLevel::Info => Self::Info,
613 LogLevel::Debug => Self::Debug,
614 }
615 }
616}
617
618#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
620pub struct ParseSyslogFacilityError;
621
622impl Error for ParseSyslogFacilityError {}
623
624impl Display for ParseSyslogFacilityError {
625 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
626 write!(f, "failed to parse syslog facility")
627 }
628}
629
630#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
632pub enum SyslogFacility {
633 Auth,
634 Authpriv,
635 Cron,
636 Daemon,
637 Ftp,
638 Kern,
639 Local0,
640 Local1,
641 Local2,
642 Local3,
643 Local4,
644 Local5,
645 Local6,
646 Local7,
647 Lpr,
648 #[default]
649 Mail,
650 News,
651 Syslog,
652 User,
653 Uucp,
654}
655
656impl FromStr for SyslogFacility {
657 type Err = ParseSyslogFacilityError;
658
659 fn from_str(s: &str) -> Result<Self, Self::Err> {
660 match s {
661 "auth" => Ok(Self::Auth),
662 "authpriv" => Ok(Self::Authpriv),
663 "cron" => Ok(Self::Cron),
664 "daemon" => Ok(Self::Daemon),
665 "ftp" => Ok(Self::Ftp),
666 "kern" => Ok(Self::Kern),
667 "local0" => Ok(Self::Local0),
668 "local1" => Ok(Self::Local1),
669 "local2" => Ok(Self::Local2),
670 "local3" => Ok(Self::Local3),
671 "local4" => Ok(Self::Local4),
672 "local5" => Ok(Self::Local5),
673 "local6" => Ok(Self::Local6),
674 "local7" => Ok(Self::Local7),
675 "lpr" => Ok(Self::Lpr),
676 "mail" => Ok(Self::Mail),
677 "news" => Ok(Self::News),
678 "syslog" => Ok(Self::Syslog),
679 "user" => Ok(Self::User),
680 "uucp" => Ok(Self::Uucp),
681 _ => Err(ParseSyslogFacilityError),
682 }
683 }
684}
685
686impl From<SyslogFacility> for Facility {
687 fn from(syslog_facility: SyslogFacility) -> Self {
688 match syslog_facility {
689 SyslogFacility::Auth => Self::LOG_AUTH,
690 SyslogFacility::Authpriv => Self::LOG_AUTHPRIV,
691 SyslogFacility::Cron => Self::LOG_CRON,
692 SyslogFacility::Daemon => Self::LOG_DAEMON,
693 SyslogFacility::Ftp => Self::LOG_FTP,
694 SyslogFacility::Kern => Self::LOG_KERN,
695 SyslogFacility::Local0 => Self::LOG_LOCAL0,
696 SyslogFacility::Local1 => Self::LOG_LOCAL1,
697 SyslogFacility::Local2 => Self::LOG_LOCAL2,
698 SyslogFacility::Local3 => Self::LOG_LOCAL3,
699 SyslogFacility::Local4 => Self::LOG_LOCAL4,
700 SyslogFacility::Local5 => Self::LOG_LOCAL5,
701 SyslogFacility::Local6 => Self::LOG_LOCAL6,
702 SyslogFacility::Local7 => Self::LOG_LOCAL7,
703 SyslogFacility::Lpr => Self::LOG_LPR,
704 SyslogFacility::Mail => Self::LOG_MAIL,
705 SyslogFacility::News => Self::LOG_NEWS,
706 SyslogFacility::Syslog => Self::LOG_SYSLOG,
707 SyslogFacility::User => Self::LOG_USER,
708 SyslogFacility::Uucp => Self::LOG_UUCP,
709 }
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use ipnet::Ipv4Net;
717 use std::net::Ipv6Addr;
718
719 #[test]
720 fn reply_code_parse_ok() {
721 let code = "441".parse();
722 assert_eq!(code, Ok(ReplyCode::Transient("441".into())));
723 let code = "499".parse::<ReplyCode>();
724 assert_eq!(code, Err(ParseStatusCodeError));
725 }
726
727 #[test]
728 fn enhanced_status_code_parse_ok() {
729 let code = "4.1.23".parse();
730 assert_eq!(code, Ok(EnhancedStatusCode::Transient("4.1.23".into())));
731 let code = "4.0.23".parse();
732 assert_eq!(code, Ok(EnhancedStatusCode::Transient("4.0.23".into())));
733 let code = "4.01.23".parse::<EnhancedStatusCode>();
734 assert_eq!(code, Err(ParseStatusCodeError));
735 }
736
737 #[test]
738 fn trusted_networks_loopback_ok() {
739 let trusted_networks = TrustedNetworks::default();
740
741 assert!(trusted_networks.contains(IpAddr::from([127, 0, 1, 0])));
742 assert!(trusted_networks.contains(Ipv6Addr::LOCALHOST.into()));
743 }
744
745 #[test]
746 fn trusted_networks_subnet_ok() {
747 let net = Ipv4Net::new([43, 5, 0, 0].into(), 16).unwrap();
748 let trusted_networks = TrustedNetworks {
749 networks: HashSet::from([net.into()]),
750 ..Default::default()
751 };
752
753 assert!(trusted_networks.contains(IpAddr::from([43, 5, 117, 8])));
754 }
755
756 #[test]
757 fn skip_senders_ok() {
758 let skip_senders = SkipSenders::from(HashSet::from([
759 SkipEntry {
760 local_part: None,
761 domain: DomainName::new("example.com").unwrap(),
762 match_subdomains: false,
763 },
764 SkipEntry {
765 local_part: None,
766 domain: DomainName::new("super.example.com").unwrap(),
767 match_subdomains: true,
768 },
769 SkipEntry {
770 local_part: Some("from".into()),
771 domain: DomainName::new("example.org").unwrap(),
772 match_subdomains: false,
773 },
774 ]));
775
776 assert!(skip_senders.includes("Example.Com"));
777 assert!(skip_senders.includes("Me@Example.Com"));
778 assert!(!skip_senders.includes("super.example.com"));
779 assert!(skip_senders.includes("mx1.super.example.com"));
780 assert!(!skip_senders.includes("me@super.example.com"));
781 assert!(skip_senders.includes("me@mx1.super.example.com"));
782 assert!(!skip_senders.includes("example.org"));
783 assert!(!skip_senders.includes("mail.example.org"));
784 assert!(!skip_senders.includes("to@example.org"));
785 assert!(skip_senders.includes("FROM@example.org"));
786 }
787}