mail_core/default_impl/
message_id_gen.rs

1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::collections::hash_map::DefaultHasher;
3use std::hash::Hasher;
4
5use rand;
6use soft_ascii_string::SoftAsciiString;
7
8use internals::error::EncodingError;
9use headers::header_components::{MessageId, ContentId, Domain};
10use ::context::MailIdGenComponent;
11
12
13static MAIL_COUNTER: AtomicUsize = AtomicUsize::new(0);
14
15fn counter_next() -> usize {
16    MAIL_COUNTER.fetch_add(1, Ordering::AcqRel)
17}
18
19fn anonymize_through_random_hash(num: usize) -> u64 {
20    let rnum = rand::random::<u32>();
21    let mut hasher = DefaultHasher::new();
22    hasher.write_usize(num);
23    hasher.write_u32(rnum);
24    hasher.finish()
25}
26
27fn gen_next_program_unique_number() -> u64 {
28    anonymize_through_random_hash(counter_next())
29}
30
31/// a id gen implementation using hash-ing to generate part of it's left hand side
32#[derive(Debug, Clone)]
33pub struct HashedIdGen {
34    domain: SoftAsciiString,
35    part_unique_in_domain: SoftAsciiString
36}
37
38impl HashedIdGen {
39
40
41    /// create a new id gen from a `Domain` and a unique part.
42    ///
43    /// The domain is used as the right hand side of the message
44    /// id and the `unique_in_domain_part` is concatenated with `"."`
45    /// and a hash from the left part. The hash is generated from
46    /// and integrated and a random number generated from a internal
47    /// program global counter.
48    ///
49    /// The tuple (`domain`,`part_unique_in_domain`) has to be world unique.
50    /// I.e. for "your" domain you have to make sure the `part_unique_in_domain`
51    /// is unique in it's usage for message id's.
52    ///
53    /// # Error
54    ///
55    /// If the domain is not ascii and puny code encoding it fails
56    ///
57    /// # Design Notes (usage of `part_unique_in_domain`)
58    ///
59    /// While the internal global counter is enough to generate seemingly
60    /// unique message id's it has two problems:
61    ///
62    /// 1. the id's are only _program_ unique but they need to be
63    ///    world unique, i.e. unique between restarts of the program
64    ///    and multiple instances running in parallel
65    ///
66    /// 2. they allow guessing the underlying number exposing private
67    ///    information about how many mails are send
68    ///
69    /// The unique part can solves one of the problems, if it is used correctly:
70    ///
71    /// 1. by providing unique bytes for `part_unique_in_domain` so
72    ///    that every time a program using this library is started
73    ///    _different_ bytes are passed in all any collision in
74    ///    message/content id's are prevented
75    ///
76    /// The other problem is solved by hashing the counter with
77    /// a random part.
78    pub fn new(domain: Domain, part_unique_in_domain: SoftAsciiString)
79        -> Result<Self, EncodingError>
80    {
81        let domain = domain.into_ascii_string()?;
82        Ok(HashedIdGen {
83            domain,
84            part_unique_in_domain
85        })
86    }
87}
88
89impl MailIdGenComponent for HashedIdGen {
90
91    fn generate_message_id(&self) -> MessageId {
92        let msg_id = format!("{unique}.{hash:x}@{domain}",
93            unique=self.part_unique_in_domain,
94            hash=gen_next_program_unique_number(),
95            domain=self.domain);
96        MessageId::from_unchecked(msg_id)
97    }
98
99    fn generate_content_id(&self) -> ContentId {
100       self.generate_message_id().into()
101    }
102
103}
104
105#[cfg(test)]
106mod test {
107
108    mod HashedIdGen {
109        #![allow(non_snake_case)]
110
111        use std::sync::Arc;
112        use std::collections::HashSet;
113        use soft_ascii_string::SoftAsciiString;
114        use headers::header_components::Domain;
115        use headers::HeaderTryFrom;
116
117        //NOTE: this is a rust bug, the import is not unused
118        #[allow(unused_imports)]
119        use ::context::MailIdGenComponent;
120        use super::super::HashedIdGen;
121
122        fn setup() -> Arc<HashedIdGen> {
123            let unique_part = SoftAsciiString::from_unchecked("bfr7tz4");
124            let domain = Domain::try_from("fooblabar.test").unwrap();
125            Arc::new(HashedIdGen::new(domain, unique_part).unwrap())
126        }
127
128        mod get_message_id {
129            use super::*;
130
131            #[test]
132            fn should_always_return_a_new_id() {
133                let id_gen = setup();
134                let mut cids = HashSet::new();
135                for _ in 0..20 {
136                    assert!(cids.insert(id_gen.generate_message_id()))
137                }
138            }
139        }
140
141        mod generate_content_id {
142            use super::*;
143
144            #[test]
145            fn should_always_return_a_new_id() {
146                let id_gen = setup();
147                let mut cids = HashSet::new();
148                for _ in 0..20 {
149                    assert!(cids.insert(id_gen.generate_content_id()))
150                }
151            }
152        }
153    }
154}