summaryrefslogtreecommitdiffstats
path: root/core/src/default_impl/message_id_gen.rs
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/default_impl/message_id_gen.rs')
-rw-r--r--core/src/default_impl/message_id_gen.rs154
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