1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use std::sync::atomic::{AtomicUsize, Ordering};
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;

use rand;
use soft_ascii_string::SoftAsciiString;

use internals::error::EncodingError;
use headers::header_components::{MessageId, ContentId, Domain};
use ::context::MailIdGenComponent;


static MAIL_COUNTER: AtomicUsize = AtomicUsize::new(0);

fn counter_next() -> usize {
    MAIL_COUNTER.fetch_add(1, Ordering::AcqRel)
}

fn anonymize_through_random_hash(num: usize) -> u64 {
    let rnum = rand::random::<u32>();
    let mut hasher = DefaultHasher::new();
    hasher.write_usize(num);
    hasher.write_u32(rnum);
    hasher.finish()
}

fn gen_next_program_unique_number() -> u64 {
    anonymize_through_random_hash(counter_next())
}

/// a id gen implementation using hash-ing to generate part of it's left hand side
#[derive(Debug, Clone)]
pub struct HashedIdGen {
    domain: SoftAsciiString,
    part_unique_in_domain: SoftAsciiString
}

impl HashedIdGen {


    /// create a new id gen from a `Domain` and a unique part.
    ///
    /// The domain is used as the right hand side of the message
    /// id and the `unique_in_domain_part` is concatenated with `"."`
    /// and a hash from the left part. The hash is generated from
    /// and integrated and a random number generated from a internal
    /// program global counter.
    ///
    /// The tuple (`domain`,`part_unique_in_domain`) has to be world unique.
    /// I.e. for "your" domain you have to make sure the `part_unique_in_domain`
    /// is unique in it's usage for message id's.
    ///
    /// # Error
    ///
    /// If the domain is not ascii and puny code encoding it fails
    ///
    /// # Design Notes (usage of `part_unique_in_domain`)
    ///
    /// While the internal global counter is enough to generate seemingly
    /// unique message id's it has two problems:
    ///
    /// 1. the id's are only _program_ unique but they need to be
    ///    world unique, i.e. unique between restarts of the program
    ///    and multiple instances running in parallel
    ///
    /// 2. they allow guessing the underlying number exposing private
    ///    information about how many mails are send
    ///
    /// The unique part can solves one of the problems, if it is used correctly:
    ///
    /// 1. by providing unique bytes for `part_unique_in_domain` so
    ///    that every time a program using this library is started
    ///    _different_ bytes are passed in all any collision in
    ///    message/content id's are prevented
    ///
    /// The other problem is solved by hashing the counter with
    /// a random part.
    pub fn new(domain: Domain, part_unique_in_domain: SoftAsciiString)
        -> Result<Self, EncodingError>
    {
        let domain = domain.into_ascii_string()?;
        Ok(HashedIdGen {
            domain,
            part_unique_in_domain
        })
    }
}

impl MailIdGenComponent for HashedIdGen {

    fn generate_message_id(&self) -> MessageId {
        let msg_id = format!("{unique}.{hash:x}@{domain}",
            unique=self.part_unique_in_domain,
            hash=gen_next_program_unique_number(),
            domain=self.domain);
        MessageId::from_unchecked(msg_id)
    }

    fn generate_content_id(&self) -> ContentId {
       self.generate_message_id().into()
    }

}

#[cfg(test)]
mod test {

    mod HashedIdGen {
        #![allow(non_snake_case)]

        use std::sync::Arc;
        use std::collections::HashSet;
        use soft_ascii_string::SoftAsciiString;
        use headers::header_components::Domain;
        use headers::HeaderTryFrom;

        //NOTE: this is a rust bug, the import is not unused
        #[allow(unused_imports)]
        use ::context::MailIdGenComponent;
        use super::super::HashedIdGen;

        fn setup() -> Arc<HashedIdGen> {
            let unique_part = SoftAsciiString::from_unchecked("bfr7tz4");
            let domain = Domain::try_from("fooblabar.test").unwrap();
            Arc::new(HashedIdGen::new(domain, unique_part).unwrap())
        }

        mod get_message_id {
            use super::*;

            #[test]
            fn should_always_return_a_new_id() {
                let id_gen = setup();
                let mut cids = HashSet::new();
                for _ in 0..20 {
                    assert!(cids.insert(id_gen.generate_message_id()))
                }
            }
        }

        mod generate_content_id {
            use super::*;

            #[test]
            fn should_always_return_a_new_id() {
                let id_gen = setup();
                let mut cids = HashSet::new();
                for _ in 0..20 {
                    assert!(cids.insert(id_gen.generate_content_id()))
                }
            }
        }
    }
}