mail_core/
mime.rs

1//! Module containing some utilities for MIME usage/creation.
2use rand::{self, Rng};
3
4
5
6// The maximal boundary with wich " boundary=\"...\"" fits into 78 chars line length limit
7const MULTIPART_BOUNDARY_MAX_LENGTH: usize = 66;
8
9// Does not include ' ' to remove special handling for last char.
10static BOUNDARY_CHARS: &[char] = &[
11                                        '\'',
12    '(', ')',      '+', ',', '-', '.', '/',
13    '0', '1', '2', '3', '4', '5', '6', '7',
14    '8', '9', ':',           '=',      '?',
15         'A', 'B', 'C', 'D', 'E', 'F', 'G',
16    'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
17    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
18    'X', 'Y', 'Z',                     '_',
19         'a', 'b', 'c', 'd', 'e', 'f', 'g',
20    'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
21    'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
22    'x', 'y', 'z',
23];
24
25/// Prevent collisions with Base64/Quoted-Printable
26static ANTI_COLLISION_CHARS: &str = "=_^";
27
28/// Generate a boundary from a counter, "=_^" and a random sequence of boundary chars.
29///
30/// # Usage Note
31///
32/// _Be aware that it might be required to quote the boundary._
33///
34/// # Implementation Details
35///
36/// The boundary  will start with `=_^` which is neither valid for base64 nor
37/// quoted-printable encoding followed by a hex repr. of the given count,
38/// a `.` and a random sequence of boundary chars.
39///
40/// The boundary will be 66 chars long, this is so that if a boundary parameter is
41/// placed on it's own line it won't be more then 78 chars. (66 chars boundary,
42/// + 2 chars quotation + 9 chars for 'boundary=' + 1 char because of `\r\n<WS>`
43/// == 78 chars)
44///
45/// The remaining characters will be picked based one the grammar defined in rfc2046,
46/// which relevant part is:
47///
48/// ```BNF
49/// boundary := 0*69<bchars> bcharsnospace
50/// bchars := bcharsnospace / " "
51/// bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" /
52///                  "+" / "_" / "," / "-" / "." /
53///                  "/" / ":" / "=" / "?"
54/// ```
55///
56/// Note that `' '` isn't used for simplicity.
57///
58pub fn create_structured_random_boundary(count: usize) -> String {
59    let mut out = format!("{anti_collision}{count:x}.",
60        anti_collision=ANTI_COLLISION_CHARS,
61        count=count
62    );
63
64    let rem = MULTIPART_BOUNDARY_MAX_LENGTH-out.len();
65    out.reserve(rem);
66
67    let mut rng = rand::thread_rng();
68    let len = BOUNDARY_CHARS.len();
69    for _ in 0..rem {
70        let idx = rng.gen_range(0, len);
71        out.push(BOUNDARY_CHARS[idx]);
72    }
73
74    out
75}
76
77
78#[cfg(test)]
79mod test {
80
81    mod write_random_boundary_to {
82        use super::super::*;
83
84        #[test]
85        fn boundary_is_not_quoted() {
86            let out = create_structured_random_boundary(0);
87            assert!(!out.starts_with("\""));
88            assert!(!out.ends_with("\""));
89        }
90
91        #[test]
92        fn boundary_start_special() {
93            let out = create_structured_random_boundary(0);
94            assert!(out.starts_with("=_^0."));
95        }
96
97        #[test]
98        fn boundary_has_a_resonable_length() {
99            let out = create_structured_random_boundary(0);
100            assert!(out.len() > 22 && out.len() <= MULTIPART_BOUNDARY_MAX_LENGTH);
101            let out = create_structured_random_boundary(1000);
102            assert!(out.len() > 22 && out.len() <= MULTIPART_BOUNDARY_MAX_LENGTH);
103        }
104
105        #[test]
106        fn boundary_does_not_contain_space_or_slach_or_quotes() {
107            // while it could contain them it's recommended not to do it
108            let out = create_structured_random_boundary(0);
109
110            for ch in out[1..out.len()-1].chars() {
111                assert!(ch as u32 >= 32);
112                assert!(ch as u32 <= 126);
113                assert_ne!(ch, '\t');
114                assert_ne!(ch, '\\');
115                assert_ne!(ch, '"');
116            }
117
118            assert_ne!(out.as_bytes()[out.len()-1], b' ');
119        }
120    }
121}