From 0ff548961a68213487faa85b7b01cf717bb27268 Mon Sep 17 00:00:00 2001 From: Mroik Date: Thu, 9 Apr 2026 23:54:57 +0200 Subject: Rename modules to more accurate names Signed-off-by: Mroik --- src/list.rs | 774 ----------------------------------------------------- src/main.rs | 4 +- src/model.rs | 774 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/smtp.rs | 242 ----------------- src/smtp_server.rs | 242 +++++++++++++++++ 5 files changed, 1018 insertions(+), 1018 deletions(-) delete mode 100644 src/list.rs create mode 100644 src/model.rs delete mode 100644 src/smtp.rs create mode 100644 src/smtp_server.rs (limited to 'src') diff --git a/src/list.rs b/src/list.rs deleted file mode 100644 index 43a9819..0000000 --- a/src/list.rs +++ /dev/null @@ -1,774 +0,0 @@ -use anyhow::Result; -use rusqlite::fallible_iterator::FallibleIterator; - -use crate::database::{Query, QueryResult}; - -#[derive(PartialEq, Debug)] -struct User { - name: Option, - email: String, -} - -impl User { - fn new(email: &str) -> Self { - Self { - name: None, - email: String::from(email), - } - } - - fn insert(&self) -> UserQuery<'_> { - let name = self.name.as_deref(); - UserQuery::Insert(name, self.email.as_str()) - } - - fn delete(&self) -> UserQuery<'_> { - UserQuery::Delete(&self.email) - } - - fn query_by_email(&self) -> UserQuery<'_> { - UserQuery::QueryByEmail(&self.email) - } - - fn query_all<'a>() -> UserQuery<'a> { - UserQuery::QueryAll - } - - fn subscribe<'a>(&'a self, list: &'a List) -> SubscriptionQuery<'a> { - SubscriptionQuery::Insert(&self.email, &list.name) - } - - fn unsubscribe<'a>(&'a self, list: &'a List) -> SubscriptionQuery<'a> { - SubscriptionQuery::Delete(&self.email, &list.name) - } - - fn subscribed_to(&self) -> SubscriptionQuery<'_> { - SubscriptionQuery::Lists(&self.email) - } -} - -enum UserQuery<'a> { - Insert(Option<&'a str>, &'a str), - Delete(&'a str), - QueryByEmail(&'a str), - QueryAll, -} - -impl Query for UserQuery<'_> { - type T = User; - fn callback(&self, tx: &rusqlite::Transaction) -> Result> { - match self { - UserQuery::Insert(name, email) => self.db_insert(tx, name, email), - UserQuery::Delete(email) => self.db_delete(tx, email), - UserQuery::QueryByEmail(email) => self.db_query_by_email(tx, email), - UserQuery::QueryAll => self.db_query_all(tx), - } - } -} - -impl UserQuery<'_> { - fn db_insert( - &self, - tx: &rusqlite::Transaction<'_>, - name: &Option<&str>, - email: &str, - ) -> Result> { - if let Some(name) = name { - let q = "INSERT INTO user (name, email) VALUES (?, ?)"; - tx.execute(q, [name, email])?; - return Ok(QueryResult::Empty); - } - - let q = "INSERT INTO user (email) VALUES (?)"; - tx.execute(q, [email])?; - Ok(QueryResult::Empty) - } - - fn db_delete(&self, tx: &rusqlite::Transaction<'_>, email: &str) -> Result> { - let q = "DELETE FROM user WHERE email = ?"; - tx.execute(q, [email])?; - Ok(QueryResult::Empty) - } - - fn db_query_by_email( - &self, - tx: &rusqlite::Transaction<'_>, - email: &str, - ) -> Result> { - let q = "SELECT * FROM user WHERE email LIKE ?"; - let mut stmt = tx.prepare(q)?; - let ris = stmt - .query([email])? - .map(|row| { - Ok(User { - name: row.get(1).unwrap(), - email: row.get(2).unwrap(), - }) - }) - .collect()?; - Ok(QueryResult::Vec(ris)) - } - - fn db_query_all(&self, tx: &rusqlite::Transaction<'_>) -> Result> { - let q = "SELECT * FROM user"; - let mut stmt = tx.prepare(q)?; - let ris = stmt - .query(())? - .map(|row| { - Ok(User { - name: row.get(1).unwrap(), - email: row.get(2).unwrap(), - }) - }) - .collect()?; - Ok(QueryResult::Vec(ris)) - } -} - -#[derive(Debug, PartialEq)] -struct List { - name: String, - desc: Option, -} - -impl List { - fn new(name: &str) -> Self { - List { - name: String::from(name), - desc: None, - } - } - - fn insert(&self) -> ListQuery<'_> { - ListQuery::Insert(&self.name, self.desc.as_deref()) - } - - fn delete(&self) -> ListQuery<'_> { - ListQuery::Delete(&self.name) - } - - fn query_all<'a>() -> ListQuery<'a> { - ListQuery::QueryAll - } - - fn subscribers(&self) -> SubscriptionQuery<'_> { - SubscriptionQuery::Subscribers(&self.name) - } -} - -enum ListQuery<'a> { - Insert(&'a str, Option<&'a str>), - Delete(&'a str), - QueryByName(&'a str), - QueryAll, -} - -impl Query for ListQuery<'_> { - type T = List; - - fn callback(&self, tx: &rusqlite::Transaction) -> Result> { - match self { - ListQuery::Insert(name, desc) => self.db_insert(tx, name, desc), - ListQuery::Delete(name) => self.db_delete(tx, name), - ListQuery::QueryByName(name) => self.db_query_by_address(tx, name), - ListQuery::QueryAll => self.db_query_all(tx), - } - } -} - -impl ListQuery<'_> { - fn db_insert( - &self, - tx: &rusqlite::Transaction, - name: &str, - desc: &Option<&str>, - ) -> Result> { - if let Some(desc) = desc { - let q = "INSERT INTO list (name, description) VALUES (?, ?)"; - tx.execute(q, [name, desc])?; - } else { - let q = "INSERT INTO list (name) VALUES (?)"; - tx.execute(q, [name])?; - } - Ok(QueryResult::Empty) - } - - fn db_delete(&self, tx: &rusqlite::Transaction, name: &str) -> Result> { - let q = "DELETE FROM list WHERE name = ?"; - tx.execute(q, [name])?; - Ok(QueryResult::Empty) - } - - fn db_query_by_address( - &self, - tx: &rusqlite::Transaction, - name: &str, - ) -> Result> { - let q = "SELECT * FROM list WHERE name = ?"; - let mut stmt = tx.prepare(q)?; - let ris = stmt - .query([name])? - .map(|row| { - Ok(List { - name: row.get(1).unwrap(), - desc: row.get(2).unwrap(), - }) - }) - .collect()?; - Ok(QueryResult::Vec(ris)) - } - - fn db_query_all(&self, tx: &rusqlite::Transaction) -> Result> { - let q = "SELECT * FROM list"; - let mut stmt = tx.prepare(q)?; - let ris = stmt - .query(())? - .map(|row| { - Ok(List { - name: row.get(1).unwrap(), - desc: row.get(2).unwrap(), - }) - }) - .collect()?; - Ok(QueryResult::Vec(ris)) - } -} - -enum SubscriptionResult { - List(List), - Subscriber(User), - Count(i64), -} - -enum SubscriptionQuery<'a> { - Insert(&'a str, &'a str), - Delete(&'a str, &'a str), - /// This is just for debug - Count, - Lists(&'a str), - Subscribers(&'a str), -} - -impl Query for SubscriptionQuery<'_> { - type T = SubscriptionResult; - - fn callback(&self, tx: &rusqlite::Transaction) -> Result> { - match self { - SubscriptionQuery::Insert(email, list_name) => self.db_insert(tx, email, list_name), - SubscriptionQuery::Count => self.db_query_all(tx), - SubscriptionQuery::Delete(email, list_name) => self.db_delete(tx, email, list_name), - SubscriptionQuery::Lists(email) => self.db_lists(tx, email), - SubscriptionQuery::Subscribers(list_name) => self.db_subscribers(tx, list_name), - } - } -} - -impl SubscriptionQuery<'_> { - fn db_insert( - &self, - tx: &rusqlite::Transaction, - email: &str, - list_name: &str, - ) -> Result> { - let q = "INSERT INTO subscription - SELECT user.id, list.id FROM user CROSS JOIN list - WHERE user.email = ? AND list.name = ?"; - tx.execute(q, [email, list_name])?; - - Ok(QueryResult::Empty) - } - - fn db_query_all(&self, tx: &rusqlite::Transaction) -> Result> { - let q = "SELECT COUNT(*) FROM subscription"; - let ris = tx.query_one(q, (), |row| Ok(row.get(0).unwrap()))?; - Ok(QueryResult::Single(SubscriptionResult::Count(ris))) - } - - fn db_delete( - &self, - tx: &rusqlite::Transaction, - email: &str, - list_name: &str, - ) -> Result> { - let q = "WITH helper AS (SELECT user.id AS user_id, list.id AS list_id FROM user CROSS JOIN list - WHERE user.email = ? AND list.name = ?) - DELETE FROM subscription WHERE EXISTS ( - SELECT 1 FROM helper - WHERE helper.user_id = subscription.user_id AND helper.list_id = subscription.list_id)"; - tx.execute(q, [email, list_name])?; - Ok(QueryResult::Empty) - } - - fn db_lists( - &self, - tx: &rusqlite::Transaction, - email: &str, - ) -> Result> { - let q = "SELECT list.name, list.description - FROM subscription JOIN list ON list.id = subscription.list_id - JOIN user ON user.id = subscription.user_id - WHERE user.email = ?"; - let ris = tx - .prepare(q)? - .query([email])? - .map(|row| { - Ok(SubscriptionResult::List(List { - name: row.get(0).unwrap(), - desc: row.get(1).unwrap(), - })) - }) - .collect()?; - - Ok(QueryResult::Vec(ris)) - } - - fn db_subscribers( - &self, - tx: &rusqlite::Transaction, - list_name: &str, - ) -> Result> { - let q = "SELECT user.name, user.email - FROM subscription JOIN list ON list.id = subscription.list_id - JOIN user ON user.id = subscription.user_id - WHERE list.name = ?"; - let ris = tx - .prepare(q)? - .query([list_name])? - .map(|row| { - Ok(SubscriptionResult::Subscriber(User { - name: row.get(0).unwrap(), - email: row.get(1).unwrap(), - })) - }) - .collect()?; - - Ok(QueryResult::Vec(ris)) - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - - use crate::{ - database::{DB_NAME, Database, QueryResult}, - list::{List, SubscriptionQuery, SubscriptionResult, User}, - }; - - fn setup() -> Result { - std::fs::create_dir("test")?; - Database::new(&format!("test/{}", DB_NAME)) - } - - fn cleanup() -> Result<()> { - std::fs::remove_dir_all("test")?; - Ok(()) - } - - #[test] - fn user_insert_wo_name() { - let mut database = setup().expect("Failed setup"); - - let user = User { - name: None, - email: String::from("mroik@poul.org"), - }; - database.execute(user.insert()).expect("Failed insert"); - let user2 = database - .execute(user.query_by_email()) - .expect("Failed query"); - match user2 { - QueryResult::Vec(items) => { - assert_eq!(items.len(), 1); - assert_eq!(items[0], user); - } - _ => assert!(false), - } - - drop(database); - cleanup().expect("Failed cleanup"); - } - - #[test] - fn user_insert_with_name() { - let mut database = setup().expect("Failed setup"); - - let user = User { - name: Some(String::from("Mirko Faina")), - email: String::from("mroik@poul.org"), - }; - database.execute(user.insert()).expect("Failed insert"); - let user2 = database - .execute(user.query_by_email()) - .expect("Failed query"); - match &user2 { - QueryResult::Vec(items) => { - assert_eq!(items.len(), 1); - assert_eq!(items[0], user); - } - _ => assert!(false), - } - - drop(database); - cleanup().expect("Failed cleanup"); - } - - #[test] - fn user_insert_twice() { - let mut database = setup().expect("Failed setup"); - - let user = User { - name: Some(String::from("Mirko Faina")), - email: String::from("mroik@poul.org"), - }; - database.execute(user.insert()).expect("Failed insert"); - let ris = database.execute(user.insert()); - assert!(ris.is_err()); - - drop(database); - cleanup().expect("Failed cleanup"); - } - - #[test] - fn user_delete() { - let mut database = setup().expect("Failed setup"); - - let user = User { - name: Some(String::from("Mirko Faina")), - email: String::from("mroik@poul.org"), - }; - database.execute(user.insert()).expect("Failed insert"); - database.execute(user.delete()).expect("Failed delete"); - - let ris = database - .execute(User::query_all()) - .expect("Failed query all"); - match ris { - QueryResult::Vec(items) => assert!(items.is_empty()), - _ => assert!(false), - } - - drop(database); - cleanup().expect("Failed cleanup"); - } - - #[test] - fn list_insert_wo_desc() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: None, - }; - database.execute(list.insert()).expect("failed insert"); - let ris = database.execute(List::query_all()).expect("failed query"); - match ris { - QueryResult::Vec(items) => { - assert_eq!(items.len(), 1); - assert_eq!(items[0], list); - } - _ => assert!(false), - } - - cleanup().expect("failed cleanup"); - } - - #[test] - fn list_insert_with_desc() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: Some(String::from("The mailing list of the POuL")), - }; - database.execute(list.insert()).expect("failed insert"); - let ris = database.execute(List::query_all()).expect("failed query"); - match ris { - QueryResult::Vec(items) => { - assert_eq!(items.len(), 1); - assert_eq!(items[0], list); - } - _ => assert!(false), - } - - drop(database); - cleanup().expect("failed cleanup"); - } - - #[test] - fn list_insert_twice() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: Some(String::from("The mailing list of the POuL")), - }; - database.execute(list.insert()).expect("failed insert"); - assert!(database.execute(list.insert()).is_err()); - - drop(database); - cleanup().expect("failed cleanup"); - } - - #[test] - fn list_delete() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: Some(String::from("The mailing list of the POuL")), - }; - database.execute(list.insert()).expect("failed insert"); - database.execute(list.delete()).expect("failed delete"); - - let ris = database - .execute(List::query_all()) - .expect("Failed query all"); - match ris { - QueryResult::Vec(items) => assert!(items.is_empty()), - _ => assert!(false), - } - - drop(database); - cleanup().expect("failed cleanup"); - } - - #[test] - fn subscription_insert() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: None, - }; - let list2 = List { - name: String::from("dev"), - desc: None, - }; - - database.execute(list.insert()).expect("failed insert"); - database.execute(list2.insert()).expect("failed insert"); - - let user = User { - name: None, - email: String::from("mroik@delayed.space"), - }; - let user2 = User { - name: None, - email: String::from("mroik@poul.org"), - }; - - database.execute(user.insert()).expect("failed insert"); - database.execute(user2.insert()).expect("failed insert"); - - database - .execute(user.subscribe(&list)) - .expect("failed insert"); - - let ris = database - .execute(SubscriptionQuery::Count) - .expect("failed query"); - match ris { - QueryResult::Single(SubscriptionResult::Count(c)) => { - assert_eq!(c, 1) - } - _ => assert!(false), - } - - drop(database); - cleanup().expect("failed cleanup"); - } - - #[test] - fn subscription_delete() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: None, - }; - let list2 = List { - name: String::from("dev"), - desc: None, - }; - - database.execute(list.insert()).expect("failed insert"); - database.execute(list2.insert()).expect("failed insert"); - - let user = User { - name: None, - email: String::from("mroik@delayed.space"), - }; - let user2 = User { - name: None, - email: String::from("mroik@poul.org"), - }; - - database.execute(user.insert()).expect("failed insert"); - database.execute(user2.insert()).expect("failed insert"); - - database - .execute(user.subscribe(&list)) - .expect("failed insert"); - database - .execute(user.subscribe(&list2)) - .expect("failed insert"); - database - .execute(user2.subscribe(&list)) - .expect("failed insert"); - database - .execute(user2.subscribe(&list2)) - .expect("failed insert"); - - let mut ris = database - .execute(SubscriptionQuery::Count) - .expect("failed query"); - match ris { - QueryResult::Single(SubscriptionResult::Count(c)) => { - assert_eq!(c, 4) - } - _ => assert!(false), - } - - database - .execute(user.unsubscribe(&list)) - .expect("failed delete"); - - ris = database - .execute(SubscriptionQuery::Count) - .expect("failed query"); - match ris { - QueryResult::Single(SubscriptionResult::Count(c)) => { - assert_eq!(c, 3) - } - _ => assert!(false), - } - - drop(database); - cleanup().expect("failed cleanup"); - } - - #[test] - fn subscription_lists() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: None, - }; - let list2 = List { - name: String::from("dev"), - desc: None, - }; - - database.execute(list.insert()).expect("failed insert"); - database.execute(list2.insert()).expect("failed insert"); - - let user = User { - name: None, - email: String::from("mroik@delayed.space"), - }; - let user2 = User { - name: None, - email: String::from("mroik@poul.org"), - }; - - database.execute(user.insert()).expect("failed insert"); - database.execute(user2.insert()).expect("failed insert"); - - database - .execute(user.subscribe(&list)) - .expect("failed insert"); - database - .execute(user.subscribe(&list2)) - .expect("failed insert"); - database - .execute(user2.subscribe(&list)) - .expect("failed insert"); - database - .execute(user2.subscribe(&list2)) - .expect("failed insert"); - - let mut ris = database - .execute(SubscriptionQuery::Count) - .expect("failed query"); - match ris { - QueryResult::Single(SubscriptionResult::Count(c)) => { - assert_eq!(c, 4) - } - _ => assert!(false), - } - - ris = database - .execute(user.subscribed_to()) - .expect("failed query"); - match ris { - QueryResult::Vec(items) => assert_eq!(items.len(), 2), - _ => assert!(false), - } - - drop(database); - cleanup().expect("failed cleanup"); - } - - #[test] - fn subscription_subscribers() { - let mut database = setup().expect("failed setup"); - - let list = List { - name: String::from("poul"), - desc: None, - }; - let list2 = List { - name: String::from("dev"), - desc: None, - }; - - database.execute(list.insert()).expect("failed insert"); - database.execute(list2.insert()).expect("failed insert"); - - let user = User { - name: None, - email: String::from("mroik@delayed.space"), - }; - let user2 = User { - name: None, - email: String::from("mroik@poul.org"), - }; - - database.execute(user.insert()).expect("failed insert"); - database.execute(user2.insert()).expect("failed insert"); - - database - .execute(user.subscribe(&list)) - .expect("failed insert"); - database - .execute(user.subscribe(&list2)) - .expect("failed insert"); - database - .execute(user2.subscribe(&list)) - .expect("failed insert"); - database - .execute(user2.subscribe(&list2)) - .expect("failed insert"); - - let mut ris = database - .execute(SubscriptionQuery::Count) - .expect("failed query"); - match ris { - QueryResult::Single(SubscriptionResult::Count(c)) => { - assert_eq!(c, 4) - } - _ => assert!(false), - } - - ris = database.execute(list.subscribers()).expect("failed query"); - match ris { - QueryResult::Vec(items) => assert_eq!(items.len(), 2), - _ => assert!(false), - } - - drop(database); - cleanup().expect("failed cleanup"); - } -} diff --git a/src/main.rs b/src/main.rs index 075dc7d..fcce4d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod database; -mod list; -mod smtp; +mod model; +mod smtp_server; fn main() { println!("Hello, world!"); diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..7fcf80f --- /dev/null +++ b/src/model.rs @@ -0,0 +1,774 @@ +use anyhow::Result; +use rusqlite::fallible_iterator::FallibleIterator; + +use crate::database::{Query, QueryResult}; + +#[derive(PartialEq, Debug)] +struct User { + name: Option, + email: String, +} + +impl User { + fn new(email: &str) -> Self { + Self { + name: None, + email: String::from(email), + } + } + + fn insert(&self) -> UserQuery<'_> { + let name = self.name.as_deref(); + UserQuery::Insert(name, self.email.as_str()) + } + + fn delete(&self) -> UserQuery<'_> { + UserQuery::Delete(&self.email) + } + + fn query_by_email(&self) -> UserQuery<'_> { + UserQuery::QueryByEmail(&self.email) + } + + fn query_all<'a>() -> UserQuery<'a> { + UserQuery::QueryAll + } + + fn subscribe<'a>(&'a self, list: &'a List) -> SubscriptionQuery<'a> { + SubscriptionQuery::Insert(&self.email, &list.name) + } + + fn unsubscribe<'a>(&'a self, list: &'a List) -> SubscriptionQuery<'a> { + SubscriptionQuery::Delete(&self.email, &list.name) + } + + fn subscribed_to(&self) -> SubscriptionQuery<'_> { + SubscriptionQuery::Lists(&self.email) + } +} + +enum UserQuery<'a> { + Insert(Option<&'a str>, &'a str), + Delete(&'a str), + QueryByEmail(&'a str), + QueryAll, +} + +impl Query for UserQuery<'_> { + type T = User; + fn callback(&self, tx: &rusqlite::Transaction) -> Result> { + match self { + UserQuery::Insert(name, email) => self.db_insert(tx, name, email), + UserQuery::Delete(email) => self.db_delete(tx, email), + UserQuery::QueryByEmail(email) => self.db_query_by_email(tx, email), + UserQuery::QueryAll => self.db_query_all(tx), + } + } +} + +impl UserQuery<'_> { + fn db_insert( + &self, + tx: &rusqlite::Transaction<'_>, + name: &Option<&str>, + email: &str, + ) -> Result> { + if let Some(name) = name { + let q = "INSERT INTO user (name, email) VALUES (?, ?)"; + tx.execute(q, [name, email])?; + return Ok(QueryResult::Empty); + } + + let q = "INSERT INTO user (email) VALUES (?)"; + tx.execute(q, [email])?; + Ok(QueryResult::Empty) + } + + fn db_delete(&self, tx: &rusqlite::Transaction<'_>, email: &str) -> Result> { + let q = "DELETE FROM user WHERE email = ?"; + tx.execute(q, [email])?; + Ok(QueryResult::Empty) + } + + fn db_query_by_email( + &self, + tx: &rusqlite::Transaction<'_>, + email: &str, + ) -> Result> { + let q = "SELECT * FROM user WHERE email LIKE ?"; + let mut stmt = tx.prepare(q)?; + let ris = stmt + .query([email])? + .map(|row| { + Ok(User { + name: row.get(1).unwrap(), + email: row.get(2).unwrap(), + }) + }) + .collect()?; + Ok(QueryResult::Vec(ris)) + } + + fn db_query_all(&self, tx: &rusqlite::Transaction<'_>) -> Result> { + let q = "SELECT * FROM user"; + let mut stmt = tx.prepare(q)?; + let ris = stmt + .query(())? + .map(|row| { + Ok(User { + name: row.get(1).unwrap(), + email: row.get(2).unwrap(), + }) + }) + .collect()?; + Ok(QueryResult::Vec(ris)) + } +} + +#[derive(Debug, PartialEq)] +struct List { + name: String, + desc: Option, +} + +impl List { + fn new(name: &str) -> Self { + List { + name: String::from(name), + desc: None, + } + } + + fn insert(&self) -> ListQuery<'_> { + ListQuery::Insert(&self.name, self.desc.as_deref()) + } + + fn delete(&self) -> ListQuery<'_> { + ListQuery::Delete(&self.name) + } + + fn query_all<'a>() -> ListQuery<'a> { + ListQuery::QueryAll + } + + fn subscribers(&self) -> SubscriptionQuery<'_> { + SubscriptionQuery::Subscribers(&self.name) + } +} + +enum ListQuery<'a> { + Insert(&'a str, Option<&'a str>), + Delete(&'a str), + QueryByName(&'a str), + QueryAll, +} + +impl Query for ListQuery<'_> { + type T = List; + + fn callback(&self, tx: &rusqlite::Transaction) -> Result> { + match self { + ListQuery::Insert(name, desc) => self.db_insert(tx, name, desc), + ListQuery::Delete(name) => self.db_delete(tx, name), + ListQuery::QueryByName(name) => self.db_query_by_address(tx, name), + ListQuery::QueryAll => self.db_query_all(tx), + } + } +} + +impl ListQuery<'_> { + fn db_insert( + &self, + tx: &rusqlite::Transaction, + name: &str, + desc: &Option<&str>, + ) -> Result> { + if let Some(desc) = desc { + let q = "INSERT INTO list (name, description) VALUES (?, ?)"; + tx.execute(q, [name, desc])?; + } else { + let q = "INSERT INTO list (name) VALUES (?)"; + tx.execute(q, [name])?; + } + Ok(QueryResult::Empty) + } + + fn db_delete(&self, tx: &rusqlite::Transaction, name: &str) -> Result> { + let q = "DELETE FROM list WHERE name = ?"; + tx.execute(q, [name])?; + Ok(QueryResult::Empty) + } + + fn db_query_by_address( + &self, + tx: &rusqlite::Transaction, + name: &str, + ) -> Result> { + let q = "SELECT * FROM list WHERE name = ?"; + let mut stmt = tx.prepare(q)?; + let ris = stmt + .query([name])? + .map(|row| { + Ok(List { + name: row.get(1).unwrap(), + desc: row.get(2).unwrap(), + }) + }) + .collect()?; + Ok(QueryResult::Vec(ris)) + } + + fn db_query_all(&self, tx: &rusqlite::Transaction) -> Result> { + let q = "SELECT * FROM list"; + let mut stmt = tx.prepare(q)?; + let ris = stmt + .query(())? + .map(|row| { + Ok(List { + name: row.get(1).unwrap(), + desc: row.get(2).unwrap(), + }) + }) + .collect()?; + Ok(QueryResult::Vec(ris)) + } +} + +enum SubscriptionResult { + List(List), + Subscriber(User), + Count(i64), +} + +enum SubscriptionQuery<'a> { + Insert(&'a str, &'a str), + Delete(&'a str, &'a str), + /// This is just for debug + Count, + Lists(&'a str), + Subscribers(&'a str), +} + +impl Query for SubscriptionQuery<'_> { + type T = SubscriptionResult; + + fn callback(&self, tx: &rusqlite::Transaction) -> Result> { + match self { + SubscriptionQuery::Insert(email, list_name) => self.db_insert(tx, email, list_name), + SubscriptionQuery::Count => self.db_query_all(tx), + SubscriptionQuery::Delete(email, list_name) => self.db_delete(tx, email, list_name), + SubscriptionQuery::Lists(email) => self.db_lists(tx, email), + SubscriptionQuery::Subscribers(list_name) => self.db_subscribers(tx, list_name), + } + } +} + +impl SubscriptionQuery<'_> { + fn db_insert( + &self, + tx: &rusqlite::Transaction, + email: &str, + list_name: &str, + ) -> Result> { + let q = "INSERT INTO subscription + SELECT user.id, list.id FROM user CROSS JOIN list + WHERE user.email = ? AND list.name = ?"; + tx.execute(q, [email, list_name])?; + + Ok(QueryResult::Empty) + } + + fn db_query_all(&self, tx: &rusqlite::Transaction) -> Result> { + let q = "SELECT COUNT(*) FROM subscription"; + let ris = tx.query_one(q, (), |row| Ok(row.get(0).unwrap()))?; + Ok(QueryResult::Single(SubscriptionResult::Count(ris))) + } + + fn db_delete( + &self, + tx: &rusqlite::Transaction, + email: &str, + list_name: &str, + ) -> Result> { + let q = "WITH helper AS (SELECT user.id AS user_id, list.id AS list_id FROM user CROSS JOIN list + WHERE user.email = ? AND list.name = ?) + DELETE FROM subscription WHERE EXISTS ( + SELECT 1 FROM helper + WHERE helper.user_id = subscription.user_id AND helper.list_id = subscription.list_id)"; + tx.execute(q, [email, list_name])?; + Ok(QueryResult::Empty) + } + + fn db_lists( + &self, + tx: &rusqlite::Transaction, + email: &str, + ) -> Result> { + let q = "SELECT list.name, list.description + FROM subscription JOIN list ON list.id = subscription.list_id + JOIN user ON user.id = subscription.user_id + WHERE user.email = ?"; + let ris = tx + .prepare(q)? + .query([email])? + .map(|row| { + Ok(SubscriptionResult::List(List { + name: row.get(0).unwrap(), + desc: row.get(1).unwrap(), + })) + }) + .collect()?; + + Ok(QueryResult::Vec(ris)) + } + + fn db_subscribers( + &self, + tx: &rusqlite::Transaction, + list_name: &str, + ) -> Result> { + let q = "SELECT user.name, user.email + FROM subscription JOIN list ON list.id = subscription.list_id + JOIN user ON user.id = subscription.user_id + WHERE list.name = ?"; + let ris = tx + .prepare(q)? + .query([list_name])? + .map(|row| { + Ok(SubscriptionResult::Subscriber(User { + name: row.get(0).unwrap(), + email: row.get(1).unwrap(), + })) + }) + .collect()?; + + Ok(QueryResult::Vec(ris)) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::{ + database::{DB_NAME, Database, QueryResult}, + model::{List, SubscriptionQuery, SubscriptionResult, User}, + }; + + fn setup() -> Result { + std::fs::create_dir("test")?; + Database::new(&format!("test/{}", DB_NAME)) + } + + fn cleanup() -> Result<()> { + std::fs::remove_dir_all("test")?; + Ok(()) + } + + #[test] + fn user_insert_wo_name() { + let mut database = setup().expect("Failed setup"); + + let user = User { + name: None, + email: String::from("mroik@poul.org"), + }; + database.execute(user.insert()).expect("Failed insert"); + let user2 = database + .execute(user.query_by_email()) + .expect("Failed query"); + match user2 { + QueryResult::Vec(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], user); + } + _ => assert!(false), + } + + drop(database); + cleanup().expect("Failed cleanup"); + } + + #[test] + fn user_insert_with_name() { + let mut database = setup().expect("Failed setup"); + + let user = User { + name: Some(String::from("Mirko Faina")), + email: String::from("mroik@poul.org"), + }; + database.execute(user.insert()).expect("Failed insert"); + let user2 = database + .execute(user.query_by_email()) + .expect("Failed query"); + match &user2 { + QueryResult::Vec(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], user); + } + _ => assert!(false), + } + + drop(database); + cleanup().expect("Failed cleanup"); + } + + #[test] + fn user_insert_twice() { + let mut database = setup().expect("Failed setup"); + + let user = User { + name: Some(String::from("Mirko Faina")), + email: String::from("mroik@poul.org"), + }; + database.execute(user.insert()).expect("Failed insert"); + let ris = database.execute(user.insert()); + assert!(ris.is_err()); + + drop(database); + cleanup().expect("Failed cleanup"); + } + + #[test] + fn user_delete() { + let mut database = setup().expect("Failed setup"); + + let user = User { + name: Some(String::from("Mirko Faina")), + email: String::from("mroik@poul.org"), + }; + database.execute(user.insert()).expect("Failed insert"); + database.execute(user.delete()).expect("Failed delete"); + + let ris = database + .execute(User::query_all()) + .expect("Failed query all"); + match ris { + QueryResult::Vec(items) => assert!(items.is_empty()), + _ => assert!(false), + } + + drop(database); + cleanup().expect("Failed cleanup"); + } + + #[test] + fn list_insert_wo_desc() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: None, + }; + database.execute(list.insert()).expect("failed insert"); + let ris = database.execute(List::query_all()).expect("failed query"); + match ris { + QueryResult::Vec(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], list); + } + _ => assert!(false), + } + + cleanup().expect("failed cleanup"); + } + + #[test] + fn list_insert_with_desc() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: Some(String::from("The mailing list of the POuL")), + }; + database.execute(list.insert()).expect("failed insert"); + let ris = database.execute(List::query_all()).expect("failed query"); + match ris { + QueryResult::Vec(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], list); + } + _ => assert!(false), + } + + drop(database); + cleanup().expect("failed cleanup"); + } + + #[test] + fn list_insert_twice() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: Some(String::from("The mailing list of the POuL")), + }; + database.execute(list.insert()).expect("failed insert"); + assert!(database.execute(list.insert()).is_err()); + + drop(database); + cleanup().expect("failed cleanup"); + } + + #[test] + fn list_delete() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: Some(String::from("The mailing list of the POuL")), + }; + database.execute(list.insert()).expect("failed insert"); + database.execute(list.delete()).expect("failed delete"); + + let ris = database + .execute(List::query_all()) + .expect("Failed query all"); + match ris { + QueryResult::Vec(items) => assert!(items.is_empty()), + _ => assert!(false), + } + + drop(database); + cleanup().expect("failed cleanup"); + } + + #[test] + fn subscription_insert() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: None, + }; + let list2 = List { + name: String::from("dev"), + desc: None, + }; + + database.execute(list.insert()).expect("failed insert"); + database.execute(list2.insert()).expect("failed insert"); + + let user = User { + name: None, + email: String::from("mroik@delayed.space"), + }; + let user2 = User { + name: None, + email: String::from("mroik@poul.org"), + }; + + database.execute(user.insert()).expect("failed insert"); + database.execute(user2.insert()).expect("failed insert"); + + database + .execute(user.subscribe(&list)) + .expect("failed insert"); + + let ris = database + .execute(SubscriptionQuery::Count) + .expect("failed query"); + match ris { + QueryResult::Single(SubscriptionResult::Count(c)) => { + assert_eq!(c, 1) + } + _ => assert!(false), + } + + drop(database); + cleanup().expect("failed cleanup"); + } + + #[test] + fn subscription_delete() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: None, + }; + let list2 = List { + name: String::from("dev"), + desc: None, + }; + + database.execute(list.insert()).expect("failed insert"); + database.execute(list2.insert()).expect("failed insert"); + + let user = User { + name: None, + email: String::from("mroik@delayed.space"), + }; + let user2 = User { + name: None, + email: String::from("mroik@poul.org"), + }; + + database.execute(user.insert()).expect("failed insert"); + database.execute(user2.insert()).expect("failed insert"); + + database + .execute(user.subscribe(&list)) + .expect("failed insert"); + database + .execute(user.subscribe(&list2)) + .expect("failed insert"); + database + .execute(user2.subscribe(&list)) + .expect("failed insert"); + database + .execute(user2.subscribe(&list2)) + .expect("failed insert"); + + let mut ris = database + .execute(SubscriptionQuery::Count) + .expect("failed query"); + match ris { + QueryResult::Single(SubscriptionResult::Count(c)) => { + assert_eq!(c, 4) + } + _ => assert!(false), + } + + database + .execute(user.unsubscribe(&list)) + .expect("failed delete"); + + ris = database + .execute(SubscriptionQuery::Count) + .expect("failed query"); + match ris { + QueryResult::Single(SubscriptionResult::Count(c)) => { + assert_eq!(c, 3) + } + _ => assert!(false), + } + + drop(database); + cleanup().expect("failed cleanup"); + } + + #[test] + fn subscription_lists() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: None, + }; + let list2 = List { + name: String::from("dev"), + desc: None, + }; + + database.execute(list.insert()).expect("failed insert"); + database.execute(list2.insert()).expect("failed insert"); + + let user = User { + name: None, + email: String::from("mroik@delayed.space"), + }; + let user2 = User { + name: None, + email: String::from("mroik@poul.org"), + }; + + database.execute(user.insert()).expect("failed insert"); + database.execute(user2.insert()).expect("failed insert"); + + database + .execute(user.subscribe(&list)) + .expect("failed insert"); + database + .execute(user.subscribe(&list2)) + .expect("failed insert"); + database + .execute(user2.subscribe(&list)) + .expect("failed insert"); + database + .execute(user2.subscribe(&list2)) + .expect("failed insert"); + + let mut ris = database + .execute(SubscriptionQuery::Count) + .expect("failed query"); + match ris { + QueryResult::Single(SubscriptionResult::Count(c)) => { + assert_eq!(c, 4) + } + _ => assert!(false), + } + + ris = database + .execute(user.subscribed_to()) + .expect("failed query"); + match ris { + QueryResult::Vec(items) => assert_eq!(items.len(), 2), + _ => assert!(false), + } + + drop(database); + cleanup().expect("failed cleanup"); + } + + #[test] + fn subscription_subscribers() { + let mut database = setup().expect("failed setup"); + + let list = List { + name: String::from("poul"), + desc: None, + }; + let list2 = List { + name: String::from("dev"), + desc: None, + }; + + database.execute(list.insert()).expect("failed insert"); + database.execute(list2.insert()).expect("failed insert"); + + let user = User { + name: None, + email: String::from("mroik@delayed.space"), + }; + let user2 = User { + name: None, + email: String::from("mroik@poul.org"), + }; + + database.execute(user.insert()).expect("failed insert"); + database.execute(user2.insert()).expect("failed insert"); + + database + .execute(user.subscribe(&list)) + .expect("failed insert"); + database + .execute(user.subscribe(&list2)) + .expect("failed insert"); + database + .execute(user2.subscribe(&list)) + .expect("failed insert"); + database + .execute(user2.subscribe(&list2)) + .expect("failed insert"); + + let mut ris = database + .execute(SubscriptionQuery::Count) + .expect("failed query"); + match ris { + QueryResult::Single(SubscriptionResult::Count(c)) => { + assert_eq!(c, 4) + } + _ => assert!(false), + } + + ris = database.execute(list.subscribers()).expect("failed query"); + match ris { + QueryResult::Vec(items) => assert_eq!(items.len(), 2), + _ => assert!(false), + } + + drop(database); + cleanup().expect("failed cleanup"); + } +} diff --git a/src/smtp.rs b/src/smtp.rs deleted file mode 100644 index 2795ecf..0000000 --- a/src/smtp.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::{ - io::{BufRead, BufReader, Write}, - net::{IpAddr, SocketAddr, TcpListener, TcpStream}, -}; - -use anyhow::Result; -use tokio::spawn; - -const SERVER_NAME: &str = ""; - -pub struct SmtpServer { - listener: TcpListener, - running: bool, -} - -impl SmtpServer { - pub fn new(ip: [u8; 4], port: u16) -> Result { - Ok(Self { - listener: TcpListener::bind((IpAddr::from(ip), port))?, - running: false, - }) - } - - // TODO: trap SIGINT to stop server? - pub async fn run(&mut self) -> Result<()> { - self.running = true; - while self.running { - let (stream, addr) = self.listener.accept()?; - let session = SessionHandler { - addr, - state: SessionState::default(), - client_host: String::new(), - from: None, - to: Vec::new(), - data: String::new(), - }; - spawn(session.run(stream)); - } - Ok(()) - } -} - -struct SessionHandler { - addr: SocketAddr, - state: SessionState, - client_host: String, - from: Option, - to: Vec, - data: String, -} - -impl SessionHandler { - async fn run(mut self, stream: TcpStream) -> Result<()> { - let mut writer = stream.try_clone()?; - let mut r = BufReader::new(&stream); - let mut buffer = String::new(); - - writer.write_all( - Reply::Ready(String::from(SERVER_NAME)) - .to_string() - .as_bytes(), - )?; - - loop { - buffer.clear(); - if r.read_line(&mut buffer)? == 0 { - break; - } - log::debug!("Received '{}' from '{}'", buffer.trim(), self.addr); - - // In this state the server is not expecting commands as the input is part of the email - // message - if self.state == SessionState::AwaitingMailInput { - if buffer.starts_with(".\r\n") { - self.process_mail().await?; - continue; - } - - self.data.push_str(&buffer); - continue; - } - - let command = match Command::try_from(buffer.as_str()) { - Err(_) => { - writer.write_all(Reply::InvalidCommand.to_string().as_bytes())?; - continue; - } - Ok(v) => v, - }; - let res = self.apply(command).await?; - writer.write_all(res.to_string().as_bytes())?; - } - - log::info!("Connection closed by {}", self.addr); - Ok(()) - } - - async fn apply(&mut self, command: Command) -> Result { - match &command { - Command::HELO(hostname) => self.helo(hostname).await, - Command::MAIL(from) => self.mail(from).await, - Command::RCPT(to) => self.rcpt(to).await, - Command::DATA => self.start_data().await, - } - } - - /// HELO resets buffers - async fn helo(&mut self, hostname: &str) -> Result { - self.client_host = String::from(hostname); - self.state = SessionState::Normal; - self.from = None; - self.to.clear(); - Ok(Reply::Completed(String::from(SERVER_NAME))) - } - - /// TODO: Validate email address - /// MAIL resets buffers - async fn mail(&mut self, from: &str) -> Result { - if from.is_empty() { - return Ok(Reply::InvalidParameter); - } - - self.from = Some(String::from(from)); - self.to.clear(); - self.state = SessionState::MailTransaction; - - Ok(Reply::Completed(String::from("Ok"))) - } - - /// TODO: Validate email addresses - /// Only after having started a mail transaction - async fn rcpt(&mut self, to: &str) -> Result { - if self.state != SessionState::MailTransaction { - return Ok(Reply::BadSequence); - } - - if to.is_empty() { - return Ok(Reply::InvalidParameter); - } - - self.to.push(String::from(to)); - Ok(Reply::Completed(String::from("Ok"))) - } - - /// Only after having started a mail transaction - async fn start_data(&mut self) -> std::result::Result { - if self.state != SessionState::MailTransaction { - return Ok(Reply::BadSequence); - } - - // Should I return a more meaningful string with this error code? - if self.from.is_none() || self.to.is_empty() { - return Ok(Reply::InvalidCommand); - } - - self.state = SessionState::AwaitingMailInput; - - Ok(Reply::StartMailInput) - } - - // TODO: Forward and store email - async fn process_mail(&mut self) -> Result { - if self.from.is_none() || self.to.is_empty() || self.data.trim().is_empty() { - return Ok(Reply::InvalidCommand); - } - - self.state = SessionState::Normal; - self.from = None; - self.to.clear(); - self.data.clear(); - - todo!() - } -} - -#[derive(PartialEq, Default)] -enum SessionState { - #[default] - WaitingHelo, - MailTransaction, - AwaitingMailInput, - Normal, -} - -enum Reply { - Ready(String), - Completed(String), - StartMailInput, - InvalidCommand, - InvalidParameter, - BadSequence, -} - -impl std::fmt::Display for Reply { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Reply::Ready(hostname) => write!(f, "220 {}", hostname), - Reply::Completed(hostname) => write!(f, "250 {}", hostname), - Reply::InvalidCommand => write!(f, "500 Invalid command"), - Reply::InvalidParameter => write!(f, "501 Parameter error"), - Reply::BadSequence => write!(f, "503 Bad sequence of commands"), - Reply::StartMailInput => write!(f, "354 ."), - } - } -} - -enum Command { - HELO(String), - MAIL(String), - RCPT(String), - DATA, -} - -impl TryFrom<&str> for Command { - type Error = anyhow::Error; - - fn try_from(value: &str) -> std::result::Result { - if value.len() >= 5 && value.to_lowercase().starts_with("helo ") { - return Ok(Command::HELO(String::from(&value[5..]))); - } - - if value.len() >= 11 - && value.to_lowercase().starts_with("mail from:<") - && value.contains('>') - { - let from = &value[11..value.find('>').unwrap()]; - return Ok(Command::MAIL(String::from(from))); - } - - if value.len() >= 9 && value.to_lowercase().starts_with("rcpt to:<") && value.contains('>') - { - let to = &value[9..value.find('>').unwrap()]; - return Ok(Command::RCPT(String::from(to))); - } - - if value.to_lowercase().starts_with("data") { - return Ok(Command::DATA); - } - - Err(anyhow::format_err!("Invalid command")) - } -} diff --git a/src/smtp_server.rs b/src/smtp_server.rs new file mode 100644 index 0000000..2795ecf --- /dev/null +++ b/src/smtp_server.rs @@ -0,0 +1,242 @@ +use std::{ + io::{BufRead, BufReader, Write}, + net::{IpAddr, SocketAddr, TcpListener, TcpStream}, +}; + +use anyhow::Result; +use tokio::spawn; + +const SERVER_NAME: &str = ""; + +pub struct SmtpServer { + listener: TcpListener, + running: bool, +} + +impl SmtpServer { + pub fn new(ip: [u8; 4], port: u16) -> Result { + Ok(Self { + listener: TcpListener::bind((IpAddr::from(ip), port))?, + running: false, + }) + } + + // TODO: trap SIGINT to stop server? + pub async fn run(&mut self) -> Result<()> { + self.running = true; + while self.running { + let (stream, addr) = self.listener.accept()?; + let session = SessionHandler { + addr, + state: SessionState::default(), + client_host: String::new(), + from: None, + to: Vec::new(), + data: String::new(), + }; + spawn(session.run(stream)); + } + Ok(()) + } +} + +struct SessionHandler { + addr: SocketAddr, + state: SessionState, + client_host: String, + from: Option, + to: Vec, + data: String, +} + +impl SessionHandler { + async fn run(mut self, stream: TcpStream) -> Result<()> { + let mut writer = stream.try_clone()?; + let mut r = BufReader::new(&stream); + let mut buffer = String::new(); + + writer.write_all( + Reply::Ready(String::from(SERVER_NAME)) + .to_string() + .as_bytes(), + )?; + + loop { + buffer.clear(); + if r.read_line(&mut buffer)? == 0 { + break; + } + log::debug!("Received '{}' from '{}'", buffer.trim(), self.addr); + + // In this state the server is not expecting commands as the input is part of the email + // message + if self.state == SessionState::AwaitingMailInput { + if buffer.starts_with(".\r\n") { + self.process_mail().await?; + continue; + } + + self.data.push_str(&buffer); + continue; + } + + let command = match Command::try_from(buffer.as_str()) { + Err(_) => { + writer.write_all(Reply::InvalidCommand.to_string().as_bytes())?; + continue; + } + Ok(v) => v, + }; + let res = self.apply(command).await?; + writer.write_all(res.to_string().as_bytes())?; + } + + log::info!("Connection closed by {}", self.addr); + Ok(()) + } + + async fn apply(&mut self, command: Command) -> Result { + match &command { + Command::HELO(hostname) => self.helo(hostname).await, + Command::MAIL(from) => self.mail(from).await, + Command::RCPT(to) => self.rcpt(to).await, + Command::DATA => self.start_data().await, + } + } + + /// HELO resets buffers + async fn helo(&mut self, hostname: &str) -> Result { + self.client_host = String::from(hostname); + self.state = SessionState::Normal; + self.from = None; + self.to.clear(); + Ok(Reply::Completed(String::from(SERVER_NAME))) + } + + /// TODO: Validate email address + /// MAIL resets buffers + async fn mail(&mut self, from: &str) -> Result { + if from.is_empty() { + return Ok(Reply::InvalidParameter); + } + + self.from = Some(String::from(from)); + self.to.clear(); + self.state = SessionState::MailTransaction; + + Ok(Reply::Completed(String::from("Ok"))) + } + + /// TODO: Validate email addresses + /// Only after having started a mail transaction + async fn rcpt(&mut self, to: &str) -> Result { + if self.state != SessionState::MailTransaction { + return Ok(Reply::BadSequence); + } + + if to.is_empty() { + return Ok(Reply::InvalidParameter); + } + + self.to.push(String::from(to)); + Ok(Reply::Completed(String::from("Ok"))) + } + + /// Only after having started a mail transaction + async fn start_data(&mut self) -> std::result::Result { + if self.state != SessionState::MailTransaction { + return Ok(Reply::BadSequence); + } + + // Should I return a more meaningful string with this error code? + if self.from.is_none() || self.to.is_empty() { + return Ok(Reply::InvalidCommand); + } + + self.state = SessionState::AwaitingMailInput; + + Ok(Reply::StartMailInput) + } + + // TODO: Forward and store email + async fn process_mail(&mut self) -> Result { + if self.from.is_none() || self.to.is_empty() || self.data.trim().is_empty() { + return Ok(Reply::InvalidCommand); + } + + self.state = SessionState::Normal; + self.from = None; + self.to.clear(); + self.data.clear(); + + todo!() + } +} + +#[derive(PartialEq, Default)] +enum SessionState { + #[default] + WaitingHelo, + MailTransaction, + AwaitingMailInput, + Normal, +} + +enum Reply { + Ready(String), + Completed(String), + StartMailInput, + InvalidCommand, + InvalidParameter, + BadSequence, +} + +impl std::fmt::Display for Reply { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Reply::Ready(hostname) => write!(f, "220 {}", hostname), + Reply::Completed(hostname) => write!(f, "250 {}", hostname), + Reply::InvalidCommand => write!(f, "500 Invalid command"), + Reply::InvalidParameter => write!(f, "501 Parameter error"), + Reply::BadSequence => write!(f, "503 Bad sequence of commands"), + Reply::StartMailInput => write!(f, "354 ."), + } + } +} + +enum Command { + HELO(String), + MAIL(String), + RCPT(String), + DATA, +} + +impl TryFrom<&str> for Command { + type Error = anyhow::Error; + + fn try_from(value: &str) -> std::result::Result { + if value.len() >= 5 && value.to_lowercase().starts_with("helo ") { + return Ok(Command::HELO(String::from(&value[5..]))); + } + + if value.len() >= 11 + && value.to_lowercase().starts_with("mail from:<") + && value.contains('>') + { + let from = &value[11..value.find('>').unwrap()]; + return Ok(Command::MAIL(String::from(from))); + } + + if value.len() >= 9 && value.to_lowercase().starts_with("rcpt to:<") && value.contains('>') + { + let to = &value[9..value.find('>').unwrap()]; + return Ok(Command::RCPT(String::from(to))); + } + + if value.to_lowercase().starts_with("data") { + return Ok(Command::DATA); + } + + Err(anyhow::format_err!("Invalid command")) + } +} -- cgit v1.3