mailrs_mail_builder/
strict.rs1use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum LintError {
13 MissingFrom,
15 MissingRecipient,
18 BadMessageId(String),
20 ControlCharsInHeader(String),
24 BadAttachmentFilename(String),
27 BodyLineTooLong {
30 line_no: usize,
32 len: usize,
34 },
35}
36
37impl fmt::Display for LintError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 Self::MissingFrom => f.write_str("missing From: header"),
41 Self::MissingRecipient => f.write_str("missing recipient (no To:/Cc:/Bcc:)"),
42 Self::BadMessageId(s) => write!(f, "malformed Message-ID: {s:?}"),
43 Self::ControlCharsInHeader(name) => {
44 write!(f, "control characters in header {name:?}")
45 }
46 Self::BadAttachmentFilename(name) => {
47 write!(f, "control characters in attachment filename {name:?}")
48 }
49 Self::BodyLineTooLong { line_no, len } => {
50 write!(f, "body line {line_no} too long ({len} > 998 octets)")
51 }
52 }
53 }
54}
55
56impl std::error::Error for LintError {}
57
58pub fn lint(raw: &[u8]) -> Result<(), LintError> {
61 let (headers, body) = match find_header_terminator(raw) {
63 Some(idx) => (&raw[..idx], &raw[idx + 4..]),
64 None => (raw, &[][..]),
65 };
66 check_header_block(headers)?;
67 check_body_line_lengths(body)?;
68 Ok(())
69}
70
71fn find_header_terminator(raw: &[u8]) -> Option<usize> {
72 raw.windows(4).position(|w| w == b"\r\n\r\n")
73}
74
75fn check_header_block(headers: &[u8]) -> Result<(), LintError> {
76 let mut i = 0;
80 while i < headers.len() {
81 let b = headers[i];
82 if b == b'\n' && (i == 0 || headers[i - 1] != b'\r') {
83 return Err(LintError::ControlCharsInHeader("?".to_string()));
85 }
86 if b == b'\r' && (i + 1 >= headers.len() || headers[i + 1] != b'\n') {
87 return Err(LintError::ControlCharsInHeader("?".to_string()));
89 }
90 i += 1;
91 }
92 Ok(())
93}
94
95fn check_body_line_lengths(body: &[u8]) -> Result<(), LintError> {
96 let mut line_no = 1usize;
97 let mut cur = 0usize;
98 for &b in body {
99 if b == b'\n' {
100 if cur > 998 {
101 return Err(LintError::BodyLineTooLong { line_no, len: cur });
102 }
103 cur = 0;
104 line_no += 1;
105 } else if b != b'\r' {
106 cur += 1;
107 }
108 }
109 if cur > 998 {
110 return Err(LintError::BodyLineTooLong { line_no, len: cur });
111 }
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn clean_message_passes() {
121 let raw = b"From: a@x\r\nTo: b@y\r\nSubject: s\r\n\r\nbody\r\n";
122 assert_eq!(lint(raw), Ok(()));
123 }
124
125 #[test]
126 fn bare_lf_in_headers_fails() {
127 let raw = b"From: a@x\nTo: b@y\r\n\r\nbody\r\n";
128 assert!(matches!(lint(raw), Err(LintError::ControlCharsInHeader(_))));
129 }
130
131 #[test]
132 fn lone_cr_in_headers_fails() {
133 let raw = b"From: a@x\rTo: b@y\r\n\r\nbody\r\n";
134 assert!(matches!(lint(raw), Err(LintError::ControlCharsInHeader(_))));
135 }
136
137 #[test]
138 fn body_line_999_chars_fails() {
139 let mut raw = b"From: a@x\r\n\r\n".to_vec();
140 raw.extend(std::iter::repeat_n(b'x', 999));
141 raw.extend_from_slice(b"\r\n");
142 assert!(matches!(
143 lint(&raw),
144 Err(LintError::BodyLineTooLong { len: 999, .. })
145 ));
146 }
147
148 #[test]
149 fn body_line_998_chars_passes() {
150 let mut raw = b"From: a@x\r\n\r\n".to_vec();
151 raw.extend(std::iter::repeat_n(b'x', 998));
152 raw.extend_from_slice(b"\r\n");
153 assert_eq!(lint(&raw), Ok(()));
154 }
155
156 #[test]
157 fn display_format_is_human_readable() {
158 let e = LintError::MissingFrom;
159 assert_eq!(e.to_string(), "missing From: header");
160 let e = LintError::BodyLineTooLong { line_no: 5, len: 1200 };
161 assert!(e.to_string().contains("line 5"));
162 assert!(e.to_string().contains("1200"));
163 }
164}