diff options
Diffstat (limited to 'core/src/default_impl/message_id_gen.rs')
-rw-r--r-- | core/src/default_impl/message_id_gen.rs | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/core/src/default_impl/message_id_gen.rs b/core/src/default_impl/message_id_gen.rs new file mode 100644 index 0000000..ca46b2b --- /dev/null +++ b/core/src/default_impl/message_id_gen.rs @@ -0,0 +1,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())) + } + } + } + } +}
\ No newline at end of file |