diff --git a/Cargo.lock b/Cargo.lock index efdfabe..e183bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,12 +131,12 @@ checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" [[package]] name = "atty" -version = "0.2.14" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" dependencies = [ - "hermit-abi", "libc", + "termion", "winapi 0.3.9", ] @@ -378,7 +378,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "lazy_static", + "lazy_static 1.4.0", "maybe-uninit", "memoffset", "scopeguard", @@ -403,7 +403,7 @@ checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ "autocfg", "cfg-if", - "lazy_static", + "lazy_static 1.4.0", ] [[package]] @@ -893,6 +893,7 @@ checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] @@ -946,6 +947,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1036,6 +1043,7 @@ dependencies = [ "chrono", "diesel", "error-chain", + "log 0.4.11", "melib", "rusqlite", "serde", @@ -1049,6 +1057,7 @@ name = "mailpot-cli" version = "0.1.0" dependencies = [ "mailpot", + "stderrlog", "structopt", ] @@ -1085,11 +1094,13 @@ dependencies = [ "data-encoding", "encoding", "futures", + "indexmap", "libc", "libloading", "native-tls", "nix", "nom", + "once_cell", "serde", "serde_derive", "smallvec", @@ -1181,7 +1192,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "libc", "log 0.4.11", "openssl", @@ -1275,6 +1286,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "object" version = "0.20.0" @@ -1302,7 +1319,7 @@ dependencies = [ "bitflags", "cfg-if", "foreign-types", - "lazy_static", + "lazy_static 1.4.0", "libc", "openssl-sys", ] @@ -1545,6 +1562,15 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +dependencies = [ + "redox_syscall", +] + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1668,7 +1694,7 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "winapi 0.3.9", ] @@ -1738,6 +1764,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + [[package]] name = "sha2" version = "0.8.2" @@ -1810,6 +1842,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stderrlog" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25" +dependencies = [ + "atty", + "chrono", + "log 0.4.11", + "termcolor", + "thread_local", +] + [[package]] name = "strsim" version = "0.8.0" @@ -1823,7 +1868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de5472fb24d7e80ae84a7801b7978f95a19ec32cb1876faea59ab711eb901976" dependencies = [ "clap", - "lazy_static", + "lazy_static 1.4.0", "structopt-derive", ] @@ -1888,6 +1933,27 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termion" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1897,6 +1963,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thread_local" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14" +dependencies = [ + "lazy_static 0.2.11", + "unreachable", +] + [[package]] name = "time" version = "0.1.43" @@ -2010,6 +2086,15 @@ dependencies = [ "subtle 2.2.3", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "url" version = "1.7.2" @@ -2029,6 +2114,7 @@ checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" dependencies = [ "rand", "serde", + "sha1", ] [[package]] diff --git a/README.md b/README.md index a36991f..5fea023 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# Mailpot -## WIP mailing list manager +# Mailpot - WIP mailing list manager Crates: @@ -7,6 +6,19 @@ Crates: - `cli` a command line tool to manage lists - `rest-http` a REST http server to manage lists +## Project goals + +- easy setup +- extensible through Rust API as a library +- extensible through HTTP REST API as an HTTP server, with webhooks +- basic management through CLI +- replaceable lightweight web archiver +- custom storage? +- useful for both newsletters, discussions + + +## Examples + ```text % mpot help mailpot 0.1.0 @@ -31,3 +43,124 @@ SUBCOMMANDS: list-lists Lists all registered mailing lists post Post message from STDIN to list ``` + +### Receiving mail + +```shell +$ cat list-request.eml | cargo run --bin mpot -- -vvvvvv post --dry-run +``` + +
output + +```shell +TRACE - Received envelope to post: Envelope { + Subject: "unsubscribe", + Date: "Tue, 04 Aug 2020 14:10:13 +0300", + From: [ + Address::Mailbox { + display_name: "Mxxxx Pxxxxxxxxxxxx", + address_spec: "exxxxx@localhost", + }, + ], + To: [ + Address::Mailbox { + display_name: "", + address_spec: "test-announce+request@localhost", + }, + ], + Message-ID: "", + In-Reply-To: None, + References: None, + Hash: 12581897380059220314, +} +TRACE - unsubscribe action for addresses [Address::Mailbox { display_name: "Mxxxx Pxxxxxxxxxxxx", address_spec: "exxxxx@localhost" }] in list [#2 test-announce] test announcements +TRACE - Is post related to list [#1 test] Test list ? false +``` +
+ +```shell +$ cat list-post.eml | cargo run --bin mpot -- -vvvvvv post --dry-run +``` + +
output + +```shell +TRACE - Received envelope to post: Envelope { + Subject: "[test-announce] new test releases", + Date: "Tue, 04 Aug 2020 14:10:13 +0300", + From: [ + Address::Mailbox { + display_name: "Mxxxx Pxxxxxxxxxxxx", + address_spec: "exxxxx@localhost", + }, + ], + To: [ + Address::Mailbox { + display_name: "", + address_spec: "test-announce@localhost", + }, + ], + Message-ID: "", + In-Reply-To: None, + References: None, + Hash: 10220641455578979007, +} +TRACE - Is post related to list [#1 test] Test list ? false +TRACE - Is post related to list [#2 test-announce] test announcements ? true +TRACE - Examining list "test announcements" +TRACE - List members [ + ListMembership { + list: 2, + address: "exxxxx@localhost", + name: None, + digest: false, + hide_address: false, + receive_duplicates: false, + receive_own_posts: true, + receive_confirmation: true, + enabled: true, + }, +] +TRACE - Running FixCRLF filter +TRACE - Running PostRightsCheck filter +TRACE - Running AddListHeaders filter +TRACE - Running FinalizeRecipients filter +TRACE - examining member ListMembership { list: 2, address: "exxxxx@localhost", name: None, digest: false, hide_address: false, receive_duplicates: false, receive_own_posts: true, receive_confirmation: true, enabled: true } +TRACE - member is submitter +TRACE - Member gets copy +TRACE - result Ok( + Post { + list: MailingList { + pk: 2, + name: "test announcements", + id: "test-announce", + address: "test-announce@localhost", + description: None, + archive_url: None, + }, + from: Address::Mailbox { + display_name: "Mxxxx Pxxxxxxxxxxxx", + address_spec: "exxxxx@localhost", + }, + members: 1, + bytes: 851, + policy: None, + to: [ + Address::Mailbox { + display_name: "", + address_spec: "test-announce@localhost", + }, + ], + action: Accept { + recipients: [ + Address::Mailbox { + display_name: "", + address_spec: "exxxxx@localhost", + }, + ], + digests: [], + }, + }, +) +``` +
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 9ea7587..f226625 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,3 +18,4 @@ path = "src/main.rs" [dependencies] mailpot = { version = "0.1.0", path = "../core" } structopt = "0.3.16" +stderrlog = "0.4" diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..f5e323d --- /dev/null +++ b/cli/README.md @@ -0,0 +1,5 @@ +# mailpot-cli + +```shell +cargo run --bin mpot -- help +``` diff --git a/cli/src/main.rs b/cli/src/main.rs index 550e42b..dd9e320 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,6 +18,7 @@ */ extern crate mailpot; +extern crate stderrlog; pub use mailpot::config::*; pub use mailpot::db::*; @@ -44,6 +45,15 @@ struct Opt { config: Option, #[structopt(flatten)] cmd: Command, + /// Silence all output + #[structopt(short = "q", long = "quiet")] + quiet: bool, + /// Verbose mode (-v, -vv, -vvv, etc) + #[structopt(short = "v", long = "verbose", parse(from_occurrences))] + verbose: usize, + /// Timestamp (sec, ms, ns, none) + #[structopt(short = "t", long = "timestamp")] + ts: Option, } #[derive(Debug, StructOpt)] @@ -108,6 +118,8 @@ enum ListCommand { receive_duplicates: Option, #[structopt(long)] receive_own_posts: Option, + #[structopt(long)] + enabled: Option, }, RemoveMember { #[structopt(long)] @@ -144,6 +156,21 @@ fn run_app(opt: Opt) -> Result<()> { } else { for l in lists { println!("- {} {:?}", l.id, l); + let list_owners = db.get_list_owners(l.pk)?; + if list_owners.is_empty() { + println!("\tList owners: None"); + } else { + println!("\tList owners:"); + for o in db.get_list_owners(l.pk)? { + println!("\t- {}", o); + } + } + if let Some(s) = db.get_list_policy(l.pk)? { + println!("\tList policy: {}", s); + } else { + println!("\tList policy: None"); + } + println!(""); } } } @@ -176,6 +203,7 @@ fn run_app(opt: Opt) -> Result<()> { receive_confirmation, receive_duplicates, receive_own_posts, + enabled, } => { db.add_member( list.pk, @@ -188,6 +216,7 @@ fn run_app(opt: Opt) -> Result<()> { receive_confirmation: receive_confirmation.unwrap_or(true), receive_duplicates: receive_duplicates.unwrap_or(true), receive_own_posts: receive_own_posts.unwrap_or(false), + enabled: enabled.unwrap_or(true), }, )?; } @@ -260,6 +289,14 @@ fn run_app(opt: Opt) -> Result<()> { fn main() -> std::result::Result<(), i32> { let opt = Opt::from_args(); + stderrlog::new() + .module(module_path!()) + .module("mailpot") + .quiet(opt.quiet) + .verbosity(opt.verbose) + .timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off)) + .init() + .unwrap(); if let Err(err) = run_app(opt) { println!("{}", err); std::process::exit(-1); diff --git a/core/Cargo.toml b/core/Cargo.toml index 5296a56..fb81e1c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,4 +20,5 @@ rusqlite = {version = "0.20.0"} serde = { version = "1.0.114" } serde_json = "1.0.57" toml = "^0.5" +log = "0.4" xdg = "2.1.0" diff --git a/core/migrations/2020-09-09-165759_membership_enabled/down.sql b/core/migrations/2020-09-09-165759_membership_enabled/down.sql new file mode 100644 index 0000000..ae50177 --- /dev/null +++ b/core/migrations/2020-09-09-165759_membership_enabled/down.sql @@ -0,0 +1,35 @@ +-- This file should undo anything in `up.sql` + +BEGIN TRANSACTION; +PRAGMA foreign_keys = false; +CREATE TEMPORARY TABLE membership_backup( + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + enabled BOOLEAN CHECK (enabled in (0, 1)) NOT NULL DEFAULT 1, + digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0, + hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0, + receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1, + receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0, + receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1, + PRIMARY KEY (list, address), + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); +INSERT INTO membership_backup SELECT list,address,name,enabled,digest,hide_address,receive_own_posts,receive_duplicates,receive_confirmation FROM membership; +DROP TABLE membership; +CREATE TABLE membership( + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0, + hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0, + receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1, + receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0, + receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1, + PRIMARY KEY (list, address), + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); +INSERT INTO membership SELECT list,address,name,digest,hide_address,receive_own_posts,receive_duplicates,receive_confirmation FROM membership_backup; +DROP TABLE membership_backup; +PRAGMA foreign_keys = true; +COMMIT TRANSACTION; diff --git a/core/migrations/2020-09-09-165759_membership_enabled/up.sql b/core/migrations/2020-09-09-165759_membership_enabled/up.sql new file mode 100644 index 0000000..3871de1 --- /dev/null +++ b/core/migrations/2020-09-09-165759_membership_enabled/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here + +PRAGMA foreign_keys = false; +ALTER TABLE membership ADD COLUMN enabled BOOLEAN CHECK (enabled in (0, 1)) NOT NULL DEFAULT 1; +PRAGMA foreign_keys = true; diff --git a/core/src/db.rs b/core/src/db.rs index 5d4e7c5..3d991aa 100644 --- a/core/src/db.rs +++ b/core/src/db.rs @@ -123,7 +123,7 @@ impl Database { let db_path = Self::db_path()?; let mut set_mode = false; if !db_path.exists() { - println!("Creating {} database in {}", DB_NAME, db_path.display()); + info!("Creating {} database in {}", DB_NAME, db_path.display()); set_mode = true; } let conn = SqliteConnection::establish(&db_path.to_str().unwrap())?; @@ -152,32 +152,75 @@ impl Database { } pub fn post(&self, env: Envelope, raw: &[u8]) -> Result<()> { + trace!("Received envelope to post: {:#?}", &env); let mut lists = self.list_lists()?; - let tos = env - .to() - .iter() - .map(|addr| addr.get_email()) - .collect::>(); + let tos = env.to().iter().cloned().collect::>(); if tos.is_empty() { return Err("Envelope To: field is empty!".into()); } if env.from().is_empty() { return Err("Envelope From: field is empty!".into()); } + for t in &tos { + if let Some((addr, subaddr)) = t.subaddress("+") { + lists.retain(|list| { + if !addr.contains_address(&list.list_address()) { + return true; + } + if let Err(err) = self.request( + list, + match subaddr.as_str() { + "subscribe" | "request" if env.subject().trim() == "subscribe" => { + ListRequest::Subscribe + } + "unsubscribe" | "request" if env.subject().trim() == "unsubscribe" => { + ListRequest::Unsubscribe + } + "request" => ListRequest::Other(env.subject().trim().to_string()), + _ => { + trace!( + "unknown action = {} for addresses {:?} in list {}", + subaddr, + env.from(), + list + ); + ListRequest::Other(subaddr.trim().to_string()) + } + }, + &env, + raw, + ) { + info!("Processing request returned error: {}", err); + } + false + }); + } + } - lists.retain(|list| tos.iter().any(|a| a == &list.address)); + lists.retain(|list| { + trace!( + "Is post related to list {}? {}", + &list, + tos.iter().any(|a| a.contains_address(&list.list_address())) + ); + + tos.iter().any(|a| a.contains_address(&list.list_address())) + }); if lists.is_empty() { - return Err("Envelope To: field doesn't contain any known lists!".into()); + return Ok(()); } let mut configuration = crate::config::Configuration::new(); crate::config::CONFIG.with(|f| { configuration = f.borrow().clone(); }); + trace!("Configuration is {:#?}", &configuration); use crate::post::{Post, PostAction}; for mut list in lists { + trace!("Examining list {}", list.list_id()); let filters = self.get_list_filters(&list); let memberships = self.list_members(list.pk)?; + trace!("List members {:#?}", &memberships); let mut post = Post { policy: self.get_list_policy(list.pk)?, list_owners: self.get_list_owners(list.pk)?, @@ -191,10 +234,9 @@ impl Database { let result = filters .into_iter() .fold(Ok(&mut post), |p, f| p.and_then(|p| f.feed(p))); - eprintln!("result {:?}", result); + trace!("result {:#?}", result); let Post { bytes, action, .. } = post; - eprintln!("send_mail {:?}", &configuration.send_mail); match configuration.send_mail { crate::config::SendMail::Smtp(ref smtp_conf) => { let smtp_conf = smtp_conf.clone(); @@ -233,4 +275,80 @@ impl Database { Ok(()) } + + pub fn request( + &self, + list: &MailingList, + request: ListRequest, + env: &Envelope, + _raw: &[u8], + ) -> Result<()> { + match request { + ListRequest::Subscribe => { + trace!( + "subscribe action for addresses {:?} in list {}", + env.from(), + list + ); + + let list_policy = self.get_list_policy(list.pk)?; + let approval_needed = list_policy + .as_ref() + .map(|p| p.approval_needed) + .unwrap_or(false); + for f in env.from() { + let membership = ListMembership { + list: list.pk, + address: f.get_email(), + name: f.get_display_name(), + digest: false, + hide_address: false, + receive_duplicates: true, + receive_own_posts: false, + receive_confirmation: true, + enabled: !approval_needed, + }; + if approval_needed { + //FIXME: send notification to list-owner + } + if let Err(_err) = self.add_member(list.pk, membership) { + //FIXME: send failure notice to f + } else { + //FIXME: send success notice + } + } + } + ListRequest::Unsubscribe => { + trace!( + "unsubscribe action for addresses {:?} in list {}", + env.from(), + list + ); + for f in env.from() { + if let Err(_err) = self.remove_member(list.pk, &f.get_email()) { + //FIXME: send failure notice to f + } else { + //FIXME: send success notice to f + } + } + } + ListRequest::Other(ref req) if req == "owner" => { + trace!( + "list-owner mail action for addresses {:?} in list {}", + env.from(), + list + ); + //FIXME: mail to list-owner + } + ListRequest::Other(ref req) => { + trace!( + "unknown request action {} for addresses {:?} in list {}", + req, + env.from(), + list + ); + } + } + Ok(()) + } } diff --git a/core/src/lib.rs b/core/src/lib.rs index ebd2f8c..c33084f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,6 +20,7 @@ #![recursion_limit = "1024"] //#![warn(missing_docs)] +use log::{info, trace}; #[macro_use] extern crate error_chain; #[macro_use] diff --git a/core/src/models.rs b/core/src/models.rs index 908ef80..1a85891 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -20,6 +20,8 @@ use super::*; use schema::*; +use melib::email::Address; + #[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)] #[table_name = "mailing_lists"] pub struct MailingList { @@ -69,6 +71,10 @@ impl MailingList { pub fn list_archive(&self) -> Option { self.archive_url.as_ref().map(|url| format!("<{}>", url)) } + + pub fn list_address(&self) -> Address { + Address::new(Some(self.name.clone()), self.address.clone()) + } } #[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)] @@ -82,36 +88,29 @@ pub struct ListMembership { pub receive_duplicates: bool, pub receive_own_posts: bool, pub receive_confirmation: bool, + pub enabled: bool, } impl std::fmt::Display for ListMembership { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - if let Some(name) = self.name.as_ref() { - write!( - fmt, - "{} <{}> [digest: {}, hide_address: {}]", - name, self.address, self.digest, self.hide_address - ) - } else { - write!( - fmt, - "{} [digest: {}, hide_address: {}]", - self.address, self.digest, self.hide_address - ) - } + write!( + fmt, + "{} [digest: {}, hide_address: {} {}]", + self.into_address(), + self.digest, + self.hide_address, + if self.enabled { + "enabled" + } else { + "not enabled" + }, + ) } } impl ListMembership { - pub fn into_address(&self) -> melib::email::Address { - use melib::email::Address; - use melib::email::StrBuilder; - use melib::MailboxAddress; - if let Some(name) = self.name.as_ref() { - melib::make_address!(name, self.address) - } else { - melib::make_address!("", self.address) - } + pub fn into_address(&self) -> Address { + Address::new(self.name.clone(), self.address.clone()) } } @@ -142,15 +141,7 @@ pub struct ListOwner { impl std::fmt::Display for ListOwner { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - if let Some(ref name) = self.name { - write!( - fmt, - "[#{} {}] \"{}\" <{}>", - self.pk, self.list, name, self.address - ) - } else { - write!(fmt, "[#{} {}] {}", self.pk, self.list, self.address) - } + write!(fmt, "[#{} {}] {}", self.pk, self.list, self.into_address()) } } @@ -165,6 +156,20 @@ impl From for ListMembership { receive_duplicates: true, receive_own_posts: false, receive_confirmation: true, + enabled: true, } } } + +impl ListOwner { + pub fn into_address(&self) -> Address { + Address::new(self.name.clone(), self.address.clone()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ListRequest { + Subscribe, + Unsubscribe, + Other(String), +} diff --git a/core/src/post.rs b/core/src/post.rs index 1306fc4..0177a63 100644 --- a/core/src/post.rs +++ b/core/src/post.rs @@ -77,8 +77,10 @@ impl PostFilter for PostRightsCheck { self: Box, post: &'p mut Post<'list>, ) -> std::result::Result<&'p mut Post<'list>, String> { + trace!("Running PostRightsCheck filter"); if let Some(ref policy) = post.policy { if policy.announce_only { + trace!("post policy is announce_only"); let owner_addresses = post .list_owners .iter() @@ -87,15 +89,23 @@ impl PostFilter for PostRightsCheck { lm.into_address() }) .collect::>(); + trace!("Owner addresses are: {:#?}", &owner_addresses); + trace!("Envelope from is: {:?}", &post.from); if !owner_addresses.iter().any(|addr| *addr == post.from) { + trace!("Envelope From does not include any owner"); return Err("You are not allowed to post on this list.".to_string()); } } else if policy.subscriber_only { + trace!("post policy is subscriber_only"); let email_from = post.from.get_email(); + trace!("post from is {:?}", &email_from); + trace!("post memberships are {:#?}", &post.memberships); if !post.memberships.iter().any(|lm| lm.address == email_from) { + trace!("Envelope from is not subscribed to this list"); return Err("You are not subscribed to this list.".to_string()); } } else if policy.approval_needed { + trace!("post policy says approval_needed"); post.action = PostAction::Defer { reason: "Approval from the list's moderators is required.".to_string(), }; @@ -112,6 +122,7 @@ impl PostFilter for FixCRLF { self: Box, post: &'p mut Post<'list>, ) -> std::result::Result<&'p mut Post<'list>, String> { + trace!("Running FixCRLF filter"); use std::io::prelude::*; let mut new_vec = Vec::with_capacity(post.bytes.len()); for line in post.bytes.lines() { @@ -130,6 +141,7 @@ impl PostFilter for AddListHeaders { self: Box, post: &'p mut Post<'list>, ) -> std::result::Result<&'p mut Post<'list>, String> { + trace!("Running AddListHeaders filter"); let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); let list_id = post.list.list_id(); headers.push((&b"List-ID"[..], list_id.as_bytes())); @@ -174,6 +186,7 @@ impl PostFilter for ArchivedAtLink { self: Box, post: &'p mut Post<'list>, ) -> std::result::Result<&'p mut Post<'list>, String> { + trace!("Running ArchivedAtLink filter"); Ok(post) } } @@ -186,14 +199,20 @@ impl PostFilter for FinalizeRecipients { self: Box, post: &'p mut Post<'list>, ) -> std::result::Result<&'p mut Post<'list>, String> { + trace!("Running FinalizeRecipients filter"); let mut recipients = vec![]; let mut digests = vec![]; let email_from = post.from.get_email(); for member in post.memberships { + trace!("examining member {:?}", &member); + if member.address != email_from { + trace!("member is submitter"); + } if member.digest { if (member.address == email_from && member.receive_own_posts) || (member.address != email_from) { + trace!("Member gets digest"); digests.push(member.into_address()); } continue; @@ -201,6 +220,7 @@ impl PostFilter for FinalizeRecipients { if (member.address == email_from && member.receive_own_posts) || (member.address != email_from) { + trace!("Member gets copy"); recipients.push(member.into_address()); } // TODO: diff --git a/core/src/schema.rs b/core/src/schema.rs index 0208b8f..501718d 100644 --- a/core/src/schema.rs +++ b/core/src/schema.rs @@ -28,6 +28,7 @@ table! { receive_duplicates -> Bool, receive_own_posts -> Bool, receive_confirmation -> Bool, + enabled -> Bool, } } diff --git a/core/src/schema.sql b/core/src/schema.sql index da8ec67..0bc65ef 100644 --- a/core/src/schema.sql +++ b/core/src/schema.sql @@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS membership ( list INTEGER NOT NULL, address TEXT NOT NULL, name TEXT, + enabled BOOLEAN CHECK (enabled in (0, 1)) NOT NULL DEFAULT 1, digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0, hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0, receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1, diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 index 6fa1876..608ce93 100644 --- a/core/src/schema.sql.m4 +++ b/core/src/schema.sql.m4 @@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS membership ( list INTEGER NOT NULL, address TEXT NOT NULL, name TEXT, + BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE(), BOOLEAN_TYPE(digest) DEFAULT BOOLEAN_FALSE(), BOOLEAN_TYPE(hide_address) DEFAULT BOOLEAN_FALSE(), BOOLEAN_TYPE(receive_duplicates) DEFAULT BOOLEAN_TRUE(),