Skip to main content

dkim/
canonicalization.rs

1// Canonicalize headers using the simple canonicalization algorithm.
2pub fn canonicalize_headers_simple(headers: &[(&str, &str, &str)], signed_headers: &[String]) -> String {
3    let mut canonicalized_headers = String::new();
4    let mut already_used = Vec::new();
5
6    for signed_header in signed_headers {
7        for (idx, (name, separator, value)) in headers
8            .iter()
9            .enumerate()
10            .filter(|(idx, _)| !already_used.contains(idx))
11        {
12            if unicase::eq_ascii(signed_header.as_str(), name) {
13                canonicalized_headers.push_str(name);
14                canonicalized_headers.push_str(separator);
15                canonicalized_headers.push_str(value);
16                canonicalized_headers.push_str("\r\n");
17                
18                already_used.push(idx);
19                break;
20            }
21        }
22    }
23    canonicalized_headers
24}
25
26/// Canonicalize body using the simple canonicalization algorithm.  
27///   
28/// The first argument **must** be the body of the mail.
29pub fn canonicalize_body_simple(mut body: &str) -> &str {
30    if body.is_empty() {
31        return "\r\n";
32    }
33
34    while body.ends_with("\r\n\r\n") {
35        body = &body[..body.len() - 2];
36    }
37
38    body
39}
40
41/// Canonicalize a single header using the relaxed canonicalization algorithm.  
42///   
43/// Note: There is no corresponding function for the simple canonicalization algorithm because the simple canonicalization algorithm does not change a single header.
44pub fn canonicalize_header_relaxed(mut value: String) -> String {
45    value = value.replace('\t', " ");
46    value = value.replace("\r\n", "");
47
48    while value.ends_with(' ') {
49        value.remove(value.len() - 1);
50    }
51    while value.starts_with(' ') {
52        value.remove(0);
53    }
54    let mut previous = false;
55    value.retain(|c| {
56        if c == ' ' {
57            if previous {
58                false
59            } else {
60                previous = true;
61                true
62            }
63        } else {
64            previous = false;
65            true
66        }
67    });
68
69    value
70}
71
72// Canonicalize headers using the relaxed canonicalization algorithm.
73pub fn canonicalize_headers_relaxed(headers: &[(&str, &str, &str)], signed_headers: &[String]) -> String {
74    let mut canonicalized_headers = String::new();
75    let mut already_used = Vec::new();
76
77    for signed_header in signed_headers {
78        for (idx, (name, _separator, value)) in headers
79            .iter()
80            .enumerate()
81            .filter(|(idx, _)| !already_used.contains(idx))
82        {
83            if unicase::eq_ascii(signed_header.as_str(), name) {
84                canonicalized_headers.push_str(&name.to_lowercase());
85                canonicalized_headers.push_str(":");
86                canonicalized_headers.push_str(&canonicalize_header_relaxed(value.to_string()));
87                canonicalized_headers.push_str("\r\n");
88
89                already_used.push(idx);
90                break;
91            }
92        }
93    }
94    canonicalized_headers
95}
96
97/// Canonicalize body using the relaxed canonicalization algorithm.  
98///   
99/// The first argument **must** be the body of the mail.
100pub fn canonicalize_body_relaxed(mut body: String) -> String {
101    // See https://tools.ietf.org/html/rfc6376#section-3.4.4 for implementation details
102
103    // Reduce all sequences of WSP within a line to a single SP character.
104    body = body.replace('\t', " ");
105    let mut previous = false;
106    body.retain(|c| {
107        if c == ' ' {
108            if previous {
109                false
110            } else {
111                previous = true;
112                true
113            }
114        } else {
115            previous = false;
116            true
117        }
118    });
119
120    // Ignore all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line.
121    while let Some(idx) = body.find(" \r\n") {
122        body.remove(idx);
123    }
124
125    // Ignore all empty lines at the end of the message body. "Empty line" is defined in Section 3.4.3.
126    while body.ends_with("\r\n\r\n") {
127        body.remove(body.len() - 1);
128        body.remove(body.len() - 1);
129    }
130
131    // If the body is non-empty but does not end with a CRLF, a CRLF is added. (For email, this is only possible when using extensions to SMTP or non-SMTP transport mechanisms.)
132    if !body.is_empty() && !body.ends_with("\r\n") {
133        body.push_str("\r\n");
134    }
135
136    body
137}
138
139/*pub fn canonicalize_relaxed(mail: &str, signed_headers: &[String]) -> (String, String) {
140    let header_end_idx = mail.find("\r\n\r\n").map(|i| i+4).unwrap_or_else(|| mail.len());
141    let headers_part = mail[..header_end_idx].to_string();
142    let body_part = mail[header_end_idx..].to_string();
143
144    (headers_part, body_part)
145}*/
146
147#[cfg(test)]
148mod test {
149    use super::*;
150    use pretty_assertions::assert_eq;
151    use std::convert::TryFrom;
152    use string_tools::get_all_after;
153    use crate::email::Email;
154
155    const MAIL: &str = "A: X\r\nB : Y\t\r\n\tZ  \r\n\r\n C \r\nD \t E\r\n\r\n\r\n";
156
157    #[test]
158    fn canonicalize_body_relaxed_test() {
159        assert_eq!(
160            canonicalize_body_relaxed(get_all_after(MAIL, "\r\n\r\n").to_string()),
161            " C\r\nD E\r\n"
162        );
163    }
164
165    #[test]
166    fn canonicalize_headers_relaxed_test() {
167        let mail = Email::try_from(MAIL).unwrap();
168        assert_eq!(
169            canonicalize_headers_relaxed(&mail.parsed.0, &["a".to_string(), "b".to_string()]),
170            "a:X\r\nb:Y Z\r\n"
171        );
172        assert_eq!(
173            canonicalize_headers_relaxed(&mail.parsed.0, &["b".to_string(), "a".to_string()]),
174            "b:Y Z\r\na:X\r\n"
175        );
176    }
177
178    #[test]
179    fn canonicalize_body_simple_test() {
180        assert_eq!(
181            canonicalize_body_simple(get_all_after(MAIL, "\r\n\r\n")),
182            " C \r\nD \t E\r\n"
183        );
184    }
185
186    #[test]
187    fn canonicalize_headers_simple_test() {
188        let mail = Email::try_from(MAIL).unwrap();
189        assert_eq!(
190            canonicalize_headers_simple(&mail.parsed.0, &["a".to_string(), "b".to_string()]),
191            "A: X\r\nB : Y\t\r\n\tZ  \r\n"
192        );
193        assert_eq!(
194            canonicalize_headers_simple(&mail.parsed.0, &["b".to_string(), "a".to_string()]),
195            "B : Y\t\r\n\tZ  \r\nA: X\r\n"
196        );
197    }
198}