diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4e0d644 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [epilys] diff --git a/.github/workflows/builds.yaml b/.github/workflows/builds.yaml new file mode 100644 index 0000000..ab7ed08 --- /dev/null +++ b/.github/workflows/builds.yaml @@ -0,0 +1,75 @@ +name: Build release binary + +env: + RUST_BACKTRACE: 1 + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + +on: + workflow_dispatch: + push: + tags: + - v* + +jobs: + build: + name: Build on ${{ matrix.build }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + build: [linux-amd64, ] + include: + - build: linux-amd64 + os: ubuntu-latest + rust: stable + artifact_name: 'mailpot-linux-amd64' + target: x86_64-unknown-linux-gnu + steps: + - uses: actions/checkout@v2 + - id: cache-rustup + name: Cache Rust toolchain + uses: actions/cache@v3 + with: + path: ~/.rustup + key: toolchain-${{ matrix.os }}-${{ matrix.rust }} + - if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }} + name: Install Rust ${{ matrix.rust }} + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + override: true + - name: Configure cargo data directory + # After this point, all cargo registry and crate data is stored in + # $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files + # that are needed during the build process. Additionally, this works + # around a bug in the 'cache' action that causes directories outside of + # the workspace dir to be saved/restored incorrectly. + run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV + - id: cache-cargo + name: Cache cargo configuration and installations + uses: actions/cache@v3 + with: + path: ${{ env.CARGO_HOME }} + key: cargo-${{ matrix.os }}-${{ matrix.rust }} + - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target + name: Setup Rust target + run: | + mkdir -p "${{ env.CARGO_HOME }}" + cat << EOF > "${{ env.CARGO_HOME }}"/config.toml + [build] + target = "${{ matrix.target }}" + EOF + - name: Build binary + run: | + cargo build --release --bin mpot --bin mpot-gen -p mailpot-cli -p mpot-archives + mv target/*/release/mailpot target/mailpot || true + mv target/release/mailpot target/mailpot || true + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.artifact_name }} + path: target/mailpot + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..086823d --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,91 @@ +name: Tests + +env: + RUST_BACKTRACE: 1 + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + +on: + workflow_dispatch: + push: + branches: + - '**' + paths: + - 'core/src/**' + - 'core/tests/**' + - 'core/Cargo.toml' + - 'Cargo.lock' + +jobs: + test: + name: Test on ${{ matrix.build }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + build: [linux-amd64, ] + include: + - build: linux-amd64 + os: ubuntu-latest + rust: stable + target: x86_64-unknown-linux-gnu + steps: + - uses: actions/checkout@v2 + - id: cache-rustup + name: Cache Rust toolchain + uses: actions/cache@v3 + with: + path: ~/.rustup + key: toolchain-${{ matrix.os }}-${{ matrix.rust }} + - if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }} + name: Install Rust ${{ matrix.rust }} + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + components: clippy, rustfmt + target: ${{ matrix.target }} + override: true + - name: Configure cargo data directory + # After this point, all cargo registry and crate data is stored in + # $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files + # that are needed during the build process. Additionally, this works + # around a bug in the 'cache' action that causes directories outside of + # the workspace dir to be saved/restored incorrectly. + run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV + - id: cache-cargo + name: Cache cargo configuration and installations + uses: actions/cache@v3 + with: + path: ${{ env.CARGO_HOME }} + key: cargo-${{ matrix.os }}-${{ matrix.rust }} + - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target + name: Setup Rust target + run: | + mkdir -p "${{ env.CARGO_HOME }}" + cat << EOF > "${{ env.CARGO_HOME }}"/config.toml + [build] + target = "${{ matrix.target }}" + EOF + - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target + name: Add lint dependencies + run: | + cargo install --target "${{ matrix.target }}" cargo-sort + - name: cargo-check + run: | + cargo check --all-features --all --tests --examples --benches --bins + - name: cargo test + if: success() || failure() # always run even if other steps fail, except when cancelled + run: | + cargo test --all --no-fail-fast --all-features + - name: cargo-sort + if: success() || failure() # always run even if other steps fail, except when cancelled + run: | + cargo sort --check + - name: rustfmt + if: success() || failure() # always run even if other steps fail, except when cancelled + run: | + cargo fmt --check --all + - name: clippy + if: success() || failure() # always run even if other steps fail, except when cancelled + run: | + cargo clippy --no-deps --all-features --all --tests --examples --benches --bins diff --git a/README.md b/README.md index 38b09c8..0e105eb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -# Mailpot - WIP mailing list manager +# mailpot - WIP mailing list manager + +Rendered rustdoc of `core` crate: Crates: -- `core` +- `core` the library - `cli` a command line tool to manage lists +- `archive-http` static web archive generation or with a dynamic http server - `rest-http` a REST http server to manage lists ## Project goals @@ -12,32 +15,25 @@ Crates: - 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 +- optional lightweight web archiver +- useful for both newsletters, discussions, article comments ## Initial setup -Check where `mpot` expects your database file to be: - -```shell -$ cargo run --bin mpot -- db-location -Configuration file /home/user/.config/mailpot/config.toml doesn't exist -``` - -Uuugh, oops. +Create a configuration file and a database: ```shell $ mkdir -p /home/user/.config/mailpot -$ echo 'send_mail = { "type" = "ShellCommand", "value" = "/usr/bin/false" }' > /home/user/.config/mailpot/config.toml -$ cargo run --bin mpot -- db-location +$ export MPOT_CONFIG=/home/user/.config/mailpot/config.toml +$ cargo run --bin mpot -- sample-config > "$MPOT_CONFIG" +$ # edit config and set database path e.g. "/home/user/.local/share/mailpot/mpot.db" +$ cargo run --bin mpot -- -c "$MPOT_CONFIG" db-location /home/user/.local/share/mailpot/mpot.db ``` -Now you can initialize the database file: +This creates the database file in the configuration file as if you executed the following: ```shell -$ mkdir -p /home/user/.local/share/mailpot/ $ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql ``` @@ -188,3 +184,78 @@ TRACE - result Ok( ) ``` + +## Using `mailpot` as a library + +```rust +use mailpot::{models::*, *}; +use tempfile::TempDir; + +let tmp_dir = TempDir::new().unwrap(); +let db_path = tmp_dir.path().join("mpot.db"); +let config = Configuration { + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), + db_path: db_path.clone(), + data_path: tmp_dir.path().to_path_buf(), +}; +let db = Connection::open_or_create_db(config)?.trusted(); + +// Create a new mailing list +let list_pk = db.create_list(MailingList { + pk: 0, + name: "foobar chat".into(), + id: "foo-chat".into(), + address: "foo-chat@example.com".into(), + description: None, + archive_url: None, +})?.pk; + +db.set_list_policy( + PostPolicy { + pk: 0, + list: list_pk, + announce_only: false, + subscriber_only: true, + approval_needed: false, + no_subscriptions: false, + custom: false, + }, +)?; + +// Drop privileges; we can only process new e-mail and modify memberships from now on. +let db = db.untrusted(); + +assert_eq!(db.list_members(list_pk)?.len(), 0); +assert_eq!(db.list_posts(list_pk, None)?.len(), 0); + +// Process a subscription request e-mail +let subscribe_bytes = b"From: Name +To: +Subject: subscribe +Date: Thu, 29 Oct 2020 13:58:16 +0000 +Message-ID: <1@example.com> + +"; +let envelope = melib::Envelope::from_bytes(subscribe_bytes, None)?; +db.post(&envelope, subscribe_bytes, /* dry_run */ false)?; + +assert_eq!(db.list_members(list_pk)?.len(), 1); +assert_eq!(db.list_posts(list_pk, None)?.len(), 0); + +// Process a post +let post_bytes = b"From: Name +To: +Subject: my first post +Date: Thu, 29 Oct 2020 14:01:09 +0000 +Message-ID: <2@example.com> + +Hello +"; +let envelope = + melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message"); +db.post(&envelope, post_bytes, /* dry_run */ false)?; + +assert_eq!(db.list_members(list_pk)?.len(), 1); +assert_eq!(db.list_posts(list_pk, None)?.len(), 1); +# Ok::<(), Error>(()) +``` diff --git a/archive-http/Cargo.toml b/archive-http/Cargo.toml index 6f10f36..e848e14 100644 --- a/archive-http/Cargo.toml +++ b/archive-http/Cargo.toml @@ -14,18 +14,24 @@ default-run = "mpot-archives" [[bin]] name = "mpot-archives" path = "src/main.rs" +required-features = ["warp"] [[bin]] name = "mpot-gen" path = "src/gen.rs" [dependencies] -chrono = "^0.4" +chrono = { version = "^0.4", optional = true } lazy_static = "*" mailpot = { version = "0.1.0", path = "../core" } -minijinja = { version = "0.31.0", features = ["source", ] } -percent-encoding = "^2.1" +minijinja = { version = "0.31.0", features = ["source", ], optional = true } +percent-encoding = { version = "^2.1", optional = true } serde = { version = "^1", features = ["derive", ] } serde_json = "^1" -tokio = { version = "1", features = ["full"] } -warp = "^0.3" +tokio = { version = "1", features = ["full"], optional = true } +warp = { version = "^0.3", optional = true } + +[features] +default = ["gen"] +gen = ["dep:chrono", "dep:minijinja"] +warp = ["dep:percent-encoding", "dep:tokio", "dep:warp"] diff --git a/archive-http/src/gen.rs b/archive-http/src/gen.rs index 477ac28..c07cd37 100644 --- a/archive-http/src/gen.rs +++ b/archive-http/src/gen.rs @@ -22,9 +22,6 @@ use chrono::Datelike; mod cal; -pub use mailpot::config::*; -pub use mailpot::db::*; -pub use mailpot::errors::*; pub use mailpot::models::*; pub use mailpot::*; @@ -223,8 +220,8 @@ fn run_app() -> std::result::Result<(), Box> { let conf = Configuration::from_file(config_path) .map_err(|err| format!("Could not load config {config_path}: {err}"))?; - let db = Database::open_db(conf).map_err(|err| format!("Couldn't open db: {err}"))?; - let lists_values = db.list_lists()?; + let db = Connection::open_db(conf).map_err(|err| format!("Couldn't open db: {err}"))?; + let lists_values = db.lists()?; { //index.html @@ -276,10 +273,8 @@ fn run_app() -> std::result::Result<(), Box> { std::fs::create_dir_all(&lists_path)?; lists_path.push("index.html"); - let list = db - .get_list(list.pk)? - .ok_or_else(|| format!("List with pk {} not found in database", list.pk))?; - let post_policy = db.get_list_policy(list.pk)?; + let list = db.list(list.pk)?; + let post_policy = db.list_policy(list.pk)?; let months = db.months(list.pk)?; let posts = db.list_posts(list.pk, None)?; let mut hist = months diff --git a/archive-http/src/main.rs b/archive-http/src/main.rs index 5146c52..bc02a21 100644 --- a/archive-http/src/main.rs +++ b/archive-http/src/main.rs @@ -19,9 +19,6 @@ extern crate mailpot; -pub use mailpot::config::*; -pub use mailpot::db::*; -pub use mailpot::errors::*; pub use mailpot::models::*; pub use mailpot::*; @@ -47,8 +44,8 @@ async fn main() { let conf1 = conf.clone(); let list_handler = warp::path!("lists" / i64).map(move |list_pk: i64| { - let db = Database::open_db(conf1.clone()).unwrap(); - let list = db.get_list(list_pk).unwrap().unwrap(); + let db = Connection::open_db(conf1.clone()).unwrap(); + let list = db.list(list_pk).unwrap(); let months = db.months(list_pk).unwrap(); let posts = db .list_posts(list_pk, None) @@ -88,8 +85,8 @@ async fn main() { let post_handler = warp::path!("list" / i64 / String).map(move |list_pk: i64, message_id: String| { let message_id = percent_decode_str(&message_id).decode_utf8().unwrap(); - let db = Database::open_db(conf2.clone()).unwrap(); - let list = db.get_list(list_pk).unwrap().unwrap(); + let db = Connection::open_db(conf2.clone()).unwrap(); + let list = db.list(list_pk).unwrap(); let posts = db.list_posts(list_pk, None).unwrap(); let post = posts .iter() @@ -122,8 +119,8 @@ async fn main() { }); let conf3 = conf.clone(); let index_handler = warp::path::end().map(move || { - let db = Database::open_db(conf3.clone()).unwrap(); - let lists_values = db.list_lists().unwrap(); + let db = Connection::open_db(conf3.clone()).unwrap(); + let lists_values = db.lists().unwrap(); let lists = lists_values .iter() .map(|list| { diff --git a/cli/src/main.rs b/cli/src/main.rs index 744f8b4..c644afb 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -21,9 +21,6 @@ extern crate log; extern crate mailpot; extern crate stderrlog; -pub use mailpot::config::*; -pub use mailpot::db::*; -pub use mailpot::errors::*; pub use mailpot::mail::*; pub use mailpot::models::changesets::*; pub use mailpot::models::*; @@ -31,14 +28,13 @@ pub use mailpot::*; use std::path::PathBuf; use structopt::StructOpt; -macro_rules! get_list { +macro_rules! list { ($db:ident, $list_id:expr) => {{ - $db.get_list_by_id(&$list_id)?.or_else(|| { + $db.list_by_id(&$list_id)?.or_else(|| { $list_id .parse::() .ok() - .map(|pk| $db.get_list(pk).ok()) - .flatten() + .map(|pk| $db.list(pk).ok()) .flatten() }) }}; @@ -60,7 +56,6 @@ struct Opt { /// Set config file #[structopt(short, long, parse(from_os_str))] - #[allow(dead_code)] config: PathBuf, #[structopt(flatten)] cmd: Command, @@ -259,11 +254,11 @@ fn run_app(opt: Opt) -> Result<()> { }; let config = Configuration::from_file(opt.config.as_path())?; use Command::*; - let mut db = Database::open_or_create_db(config)?; + let mut db = Connection::open_or_create_db(config)?; match opt.cmd { SampleConfig => {} DumpDatabase => { - let lists = db.list_lists()?; + let lists = db.lists()?; let mut stdout = std::io::stdout(); serde_json::to_writer_pretty(&mut stdout, &lists)?; for l in &lists { @@ -271,13 +266,13 @@ fn run_app(opt: Opt) -> Result<()> { } } ListLists => { - let lists = db.list_lists()?; + let lists = db.lists()?; if lists.is_empty() { println!("No lists found."); } else { for l in lists { println!("- {} {:?}", l.id, l); - let list_owners = db.get_list_owners(l.pk)?; + let list_owners = db.list_owners(l.pk)?; if list_owners.is_empty() { println!("\tList owners: None"); } else { @@ -286,7 +281,7 @@ fn run_app(opt: Opt) -> Result<()> { println!("\t- {}", o); } } - if let Some(s) = db.get_list_policy(l.pk)? { + if let Some(s) = db.list_policy(l.pk)? { println!("\tList policy: {}", s); } else { println!("\tList policy: None"); @@ -296,7 +291,7 @@ fn run_app(opt: Opt) -> Result<()> { } } List { list_id, cmd } => { - let list = match get_list!(db, list_id) { + let list = match list!(db, list_id) { Some(v) => v, None => { return Err(format!("No list with id or pk {} was found", list_id).into()); @@ -356,12 +351,12 @@ fn run_app(opt: Opt) -> Result<()> { } } - db.remove_member(list.pk, &address)?; + db.remove_membership(list.pk, &address)?; } Health => { println!("{} health:", list); - let list_owners = db.get_list_owners(list.pk)?; - let list_policy = db.get_list_policy(list.pk)?; + let list_owners = db.list_owners(list.pk)?; + let list_policy = db.list_policy(list.pk)?; if list_owners.is_empty() { println!("\tList has no owners: you should add at least one."); } else { @@ -377,8 +372,8 @@ fn run_app(opt: Opt) -> Result<()> { } Info => { println!("{} info:", list); - let list_owners = db.get_list_owners(list.pk)?; - let list_policy = db.get_list_policy(list.pk)?; + let list_owners = db.list_owners(list.pk)?; + let list_policy = db.list_policy(list.pk)?; let members = db.list_members(list.pk)?; if members.is_empty() { println!("No members."); @@ -649,7 +644,7 @@ fn run_app(opt: Opt) -> Result<()> { list_id, mut maildir_path, } => { - let list = match get_list!(db, list_id) { + let list = match list!(db, list_id) { Some(v) => v, None => { return Err(format!("No list with id or pk {} was found", list_id).into()); diff --git a/core/src/config.rs b/core/src/config.rs index f72e8ee..e3cac2a 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -23,28 +23,36 @@ use std::io::{Read, Write}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; +/// How to send e-mail. #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "type", content = "value")] pub enum SendMail { + /// A `melib` configuration for talking to an SMTP server. Smtp(melib::smtp::SmtpServerConf), + /// A plain shell command passed to `sh -c` with the e-mail passed in the stdin. ShellCommand(String), } +/// The configuration for the mailpot database and the mail server. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Configuration { + /// How to send e-mail. pub send_mail: SendMail, - #[serde(default = "default_storage_fn")] - pub storage: String, + /// The location of the sqlite3 file. pub db_path: PathBuf, + /// The directory where data are stored. pub data_path: PathBuf, } impl Configuration { + /// Create a new configuration value from a given database path value. + /// + /// If you wish to create a new database with this configuration, use [`Connection::open_or_create_db`](crate::Connection::open_or_create_db). + /// To open an existing database, use [`Database::open_db`](crate::Connection::open_db). pub fn new(db_path: impl Into) -> Self { let db_path = db_path.into(); Configuration { send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), - storage: "sqlite3".into(), data_path: db_path .parent() .map(Path::to_path_buf) @@ -53,6 +61,7 @@ impl Configuration { } } + /// Deserialize configuration from TOML file. pub fn from_file>(path: P) -> Result { let path = path.as_ref(); let mut s = String::new(); @@ -66,24 +75,17 @@ impl Configuration { Ok(config) } + /// The saved data path. pub fn data_directory(&self) -> &Path { self.data_path.as_path() } + /// The sqlite3 database path. pub fn db_path(&self) -> &Path { self.db_path.as_path() } - pub fn default_path() -> Result { - let mut result = - xdg::BaseDirectories::with_prefix("mailpot")?.place_config_file("config.toml")?; - if result.starts_with("~") { - result = Path::new(&std::env::var("HOME").context("No $HOME set.")?) - .join(result.strip_prefix("~").context("Internal error while getting default database path: path starts with ~ but rust couldn't strip_refix(\"~\"")?); - } - Ok(result) - } - + /// Save message to a custom path. pub fn save_message_to_path(&self, msg: &str, mut path: PathBuf) -> Result { if path.is_dir() { let now = Local::now().timestamp(); @@ -102,17 +104,15 @@ impl Configuration { Ok(path) } + /// Save message to the data directory. pub fn save_message(&self, msg: String) -> Result { self.save_message_to_path(&msg, self.data_directory().to_path_buf()) } + /// Serialize configuration to a TOML string. pub fn to_toml(&self) -> String { toml::Value::try_from(self) .expect("Could not serialize config to TOML") .to_string() } } - -fn default_storage_fn() -> String { - "sqlite3".to_string() -} diff --git a/core/src/db.rs b/core/src/db.rs index 9a6f47f..388d744 100644 --- a/core/src/db.rs +++ b/core/src/db.rs @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +//! Mailpot database and methods. + use super::Configuration; use super::*; use crate::ErrorKind::*; @@ -28,9 +30,9 @@ use std::convert::TryFrom; use std::io::Write; use std::process::{Command, Stdio}; -const DB_NAME: &str = "current.db"; - -pub struct Database { +/// A connection to a `mailpot` database. +pub struct Connection { + /// The `rusqlite` connection handle. pub connection: DbConnection, conf: Configuration, } @@ -55,6 +57,7 @@ fn user_authorizer_callback( ) -> rusqlite::hooks::Authorization { use rusqlite::hooks::{AuthAction, Authorization}; + // [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this. match auth_context.action { AuthAction::Delete { table_name: "error_queue" | "queue" | "candidate_membership" | "membership", @@ -73,7 +76,12 @@ fn user_authorizer_callback( } } -impl Database { +impl Connection { + /// Creates a new database connection. + /// + /// `Connection` supports a limited subset of operations by default (see + /// [`Connection::untrusted`]). + /// Use [`Connection::trusted`] to remove these limits. pub fn open_db(conf: Configuration) -> Result { use rusqlite::config::DbConfig; use std::sync::Once; @@ -94,12 +102,14 @@ impl Database { conn.busy_timeout(core::time::Duration::from_millis(500))?; conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; conn.authorizer(Some(user_authorizer_callback)); - Ok(Database { + Ok(Connection { conf, connection: conn, }) } + /// Removes operational limits from this connection. (see [`Connection::untrusted`]) + #[must_use] pub fn trusted(self) -> Self { self.connection .authorizer::) -> rusqlite::hooks::Authorization>( @@ -108,21 +118,30 @@ impl Database { self } + // [tag:sync_auth_doc] + /// Sets operational limits for this connection. + /// + /// - Allow `INSERT`, `DELETE` only for "error_queue", "queue", "candidate_membership", "membership". + /// - Allow `INSERT` only for "post". + /// - Allow read access to all tables. + /// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime` function. + /// - Deny everything else. pub fn untrusted(self) -> Self { self.connection.authorizer(Some(user_authorizer_callback)); self } + /// Create a database if it doesn't exist and then open it. pub fn open_or_create_db(conf: Configuration) -> Result { if !conf.db_path.exists() { let db_path = &conf.db_path; use std::os::unix::fs::PermissionsExt; info!("Creating database in {}", db_path.display()); - std::fs::File::create(&db_path).context("Could not create db path")?; + std::fs::File::create(db_path).context("Could not create db path")?; let mut child = Command::new("sqlite3") - .arg(&db_path) + .arg(db_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -139,7 +158,7 @@ impl Database { return Err(format!("Could not initialize sqlite3 database at {}: sqlite3 returned exit code {} and stderr {} {}", db_path.display(), output.status.code().unwrap_or_default(), String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout)).into()); } - let file = std::fs::File::open(&db_path)?; + let file = std::fs::File::open(db_path)?; let metadata = file.metadata()?; let mut permissions = metadata.permissions(); @@ -149,10 +168,12 @@ impl Database { Self::open_db(conf) } + /// Returns a connection's configuration. pub fn conf(&self) -> &Configuration { &self.conf } + /// Loads archive databases from [`Configuration::data_path`], if any. pub fn load_archives(&self) -> Result<()> { let mut stmt = self.connection.prepare("ATTACH ? AS ?;")?; for archive in std::fs::read_dir(&self.conf.data_path)? { @@ -171,7 +192,8 @@ impl Database { Ok(()) } - pub fn list_lists(&self) -> Result>> { + /// Returns a vector of existing mailing lists. + pub fn lists(&self) -> Result>> { let mut stmt = self.connection.prepare("SELECT * FROM mailing_lists;")?; let list_iter = stmt.query_map([], |row| { let pk = row.get("pk")?; @@ -196,7 +218,8 @@ impl Database { Ok(ret) } - pub fn get_list(&self, pk: i64) -> Result>> { + /// Fetch a mailing list by primary key. + pub fn list(&self, pk: i64) -> Result> { let mut stmt = self .connection .prepare("SELECT * FROM mailing_lists WHERE pk = ?;")?; @@ -216,11 +239,15 @@ impl Database { )) }) .optional()?; - - Ok(ret) + if let Some(ret) = ret { + Ok(ret) + } else { + Err(Error::from(NotFound("list or list policy not found!"))) + } } - pub fn get_list_by_id>(&self, id: S) -> Result>> { + /// Fetch a mailing list by id. + pub fn list_by_id>(&self, id: S) -> Result>> { let id = id.as_ref(); let mut stmt = self .connection @@ -245,6 +272,7 @@ impl Database { Ok(ret) } + /// Create a new list. pub fn create_list(&self, new_val: MailingList) -> Result> { let mut stmt = self .connection @@ -280,7 +308,7 @@ impl Database { /// Remove an existing list policy. /// /// ``` - /// # use mailpot::{models::*, Configuration, Database, SendMail}; + /// # use mailpot::{models::*, Configuration, Connection, SendMail}; /// # use tempfile::TempDir; /// /// # let tmp_dir = TempDir::new().unwrap(); @@ -288,12 +316,11 @@ impl Database { /// # let config = Configuration { /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), /// # db_path: db_path.clone(), - /// # storage: "sqlite3".to_string(), /// # data_path: tmp_dir.path().to_path_buf(), /// # }; /// /// # fn do_test(config: Configuration) { - /// let db = Database::open_or_create_db(config).unwrap().trusted(); + /// let db = Connection::open_or_create_db(config).unwrap().trusted(); /// let list_pk = db.create_list(MailingList { /// pk: 0, /// name: "foobar chat".into(), @@ -317,25 +344,6 @@ impl Database { /// # } /// # do_test(config); /// ``` - /// ```should_panic - /// # use mailpot::{models::*, Configuration, Database, SendMail}; - /// # use tempfile::TempDir; - /// - /// # let tmp_dir = TempDir::new().unwrap(); - /// # let db_path = tmp_dir.path().join("mpot.db"); - /// # let config = Configuration { - /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), - /// # db_path: db_path.clone(), - /// # storage: "sqlite3".to_string(), - /// # data_path: tmp_dir.path().to_path_buf(), - /// # }; - /// - /// # fn do_test(config: Configuration) { - /// let db = Database::open_or_create_db(config).unwrap().trusted(); - /// db.remove_list_policy(1, 1).unwrap(); - /// # } - /// # do_test(config); - /// ``` pub fn remove_list_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> { let mut stmt = self .connection @@ -353,6 +361,28 @@ impl Database { Ok(()) } + /// ```should_panic + /// # use mailpot::{models::*, Configuration, Connection, SendMail}; + /// # use tempfile::TempDir; + /// + /// # let tmp_dir = TempDir::new().unwrap(); + /// # let db_path = tmp_dir.path().join("mpot.db"); + /// # let config = Configuration { + /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), + /// # db_path: db_path.clone(), + /// # data_path: tmp_dir.path().to_path_buf(), + /// # }; + /// + /// # fn do_test(config: Configuration) { + /// let db = Connection::open_or_create_db(config).unwrap().trusted(); + /// db.remove_list_policy(1, 1).unwrap(); + /// # } + /// # do_test(config); + /// ``` + #[cfg(doc)] + pub fn remove_list_policy_panic() {} + + /// Set the unique post policy for a list. pub fn set_list_policy(&self, policy: PostPolicy) -> Result> { if !(policy.announce_only || policy.subscriber_only @@ -415,6 +445,7 @@ impl Database { Ok(ret) } + /// Fetch all posts of a mailing list. pub fn list_posts( &self, list_pk: i64, @@ -449,16 +480,8 @@ impl Database { Ok(ret) } - pub fn update_list(&self, _change_set: MailingListChangeset) -> Result<()> { - /* - diesel::update(mailing_lists::table) - .set(&set) - .execute(&self.connection)?; - */ - Ok(()) - } - - pub fn get_list_policy(&self, pk: i64) -> Result>> { + /// Fetch the post policy of a mailing list. + pub fn list_policy(&self, pk: i64) -> Result>> { let mut stmt = self .connection .prepare("SELECT * FROM post_policy WHERE list = ?;")?; @@ -483,7 +506,8 @@ impl Database { Ok(ret) } - pub fn get_list_owners(&self, pk: i64) -> Result>> { + /// Fetch the owners of a mailing list. + pub fn list_owners(&self, pk: i64) -> Result>> { let mut stmt = self .connection .prepare("SELECT * FROM list_owner WHERE list = ?;")?; @@ -508,6 +532,7 @@ impl Database { Ok(ret) } + /// Remove an owner of a mailing list. pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> { self.connection .query_row( @@ -525,6 +550,7 @@ impl Database { Ok(()) } + /// Add an owner of a mailing list. pub fn add_list_owner(&self, list_owner: ListOwner) -> Result> { let mut stmt = self.connection.prepare( "INSERT OR REPLACE INTO list_owner(list, address, name) VALUES (?, ?, ?) RETURNING *;", @@ -567,7 +593,58 @@ impl Database { Ok(ret) } - pub fn get_list_filters( + /// Update a mailing list. + pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> { + if matches!( + change_set, + MailingListChangeset { + pk: _, + name: None, + id: None, + address: None, + description: None, + archive_url: None + } + ) { + return self.list(change_set.pk).map(|_| ()); + } + + let MailingListChangeset { + pk, + name, + id, + address, + description, + archive_url, + } = change_set; + let tx = self.connection.transaction()?; + + macro_rules! update { + ($field:tt) => {{ + if let Some($field) = $field { + tx.execute( + concat!( + "UPDATE mailing_lists SET ", + stringify!($field), + " = ? WHERE pk = ?;" + ), + rusqlite::params![&$field, &pk], + )?; + } + }}; + } + update!(name); + update!(id); + update!(address); + update!(description); + update!(archive_url); + + tx.commit()?; + Ok(()) + } + + /// Return the post filters of a mailing list. + pub fn list_filters( &self, _list: &DbVal, ) -> Vec> { diff --git a/core/src/db/error_queue.rs b/core/src/db/error_queue.rs index c712547..3dd7144 100644 --- a/core/src/db/error_queue.rs +++ b/core/src/db/error_queue.rs @@ -20,7 +20,8 @@ use super::*; use serde_json::{json, Value}; -impl Database { +impl Connection { + /// Insert a received email into the error queue. pub fn insert_to_error_queue(&self, env: &Envelope, raw: &[u8], reason: String) -> Result { let mut stmt = self.connection.prepare("INSERT INTO error_queue(error, to_address, from_address, subject, message_id, message, timestamp, datetime) VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING pk;")?; let pk = stmt.query_row( @@ -42,6 +43,7 @@ impl Database { Ok(pk) } + /// Fetch all error queue entries. pub fn error_queue(&self) -> Result>> { let mut stmt = self.connection.prepare("SELECT * FROM error_queue;")?; let error_iter = stmt.query_map([], |row| { @@ -70,6 +72,7 @@ impl Database { Ok(ret) } + /// Delete error queue entries. pub fn delete_from_error_queue(&mut self, index: Vec) -> Result<()> { let tx = self.connection.transaction()?; diff --git a/core/src/db/members.rs b/core/src/db/members.rs index 233b668..d64f469 100644 --- a/core/src/db/members.rs +++ b/core/src/db/members.rs @@ -19,7 +19,8 @@ use super::*; -impl Database { +impl Connection { + /// Fetch all members of a mailing list. pub fn list_members(&self, pk: i64) -> Result>> { let mut stmt = self .connection @@ -51,6 +52,68 @@ impl Database { Ok(ret) } + /// Fetch mailing list member. + pub fn list_member(&self, list_pk: i64, pk: i64) -> Result> { + let mut stmt = self + .connection + .prepare("SELECT * FROM membership WHERE list = ? AND pk = ?;")?; + + let ret = stmt.query_row([&list_pk, &pk], |row| { + let _pk: i64 = row.get("pk")?; + debug_assert_eq!(pk, _pk); + Ok(DbVal( + ListMembership { + pk, + list: row.get("list")?, + address: row.get("address")?, + name: row.get("name")?, + digest: row.get("digest")?, + hide_address: row.get("hide_address")?, + receive_duplicates: row.get("receive_duplicates")?, + receive_own_posts: row.get("receive_own_posts")?, + receive_confirmation: row.get("receive_confirmation")?, + enabled: row.get("enabled")?, + }, + pk, + )) + })?; + Ok(ret) + } + + /// Fetch mailing list member by their address. + pub fn list_member_by_address( + &self, + list_pk: i64, + address: &str, + ) -> Result> { + let mut stmt = self + .connection + .prepare("SELECT * FROM membership WHERE list = ? AND address = ?;")?; + + let ret = stmt.query_row(rusqlite::params![&list_pk, &address], |row| { + let pk = row.get("pk")?; + let address_ = row.get("address")?; + debug_assert_eq!(address, &address_); + Ok(DbVal( + ListMembership { + pk, + list: row.get("list")?, + address: address_, + name: row.get("name")?, + digest: row.get("digest")?, + hide_address: row.get("hide_address")?, + receive_duplicates: row.get("receive_duplicates")?, + receive_own_posts: row.get("receive_own_posts")?, + receive_confirmation: row.get("receive_confirmation")?, + enabled: row.get("enabled")?, + }, + pk, + )) + })?; + Ok(ret) + } + + /// Add member to mailing list. pub fn add_member( &self, list_pk: i64, @@ -96,13 +159,14 @@ impl Database { Ok(ret) } + /// Create membership candidate. pub fn add_candidate_member(&self, list_pk: i64, mut new_val: ListMembership) -> Result { new_val.list = list_pk; let mut stmt = self .connection .prepare("INSERT INTO candidate_membership(list, address, name, accepted) VALUES(?, ?, ?, ?) RETURNING pk;")?; let ret = stmt.query_row( - rusqlite::params![&new_val.list, &new_val.address, &new_val.name, &false,], + rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::,], |row| { let pk: i64 = row.get("pk")?; Ok(pk) @@ -113,6 +177,7 @@ impl Database { Ok(ret) } + /// Accept membership candidate. pub fn accept_candidate_member(&mut self, pk: i64) -> Result> { let tx = self.connection.transaction()?; let mut stmt = tx @@ -138,7 +203,7 @@ impl Database { drop(stmt); tx.execute( "UPDATE candidate_membership SET accepted = ? WHERE pk = ?;", - [&pk], + [&ret.pk, &pk], )?; tx.commit()?; @@ -146,21 +211,83 @@ impl Database { Ok(ret) } - pub fn remove_member(&self, list_pk: i64, address: &str) -> Result<()> { - self.connection.execute( - "DELETE FROM membership WHERE list_pk = ? AND address = ?;", - rusqlite::params![&list_pk, &address], - )?; + /// Remove a member by their address. + pub fn remove_membership(&self, list_pk: i64, address: &str) -> Result<()> { + self.connection + .query_row( + "DELETE FROM membership WHERE list_pk = ? AND address = ? RETURNING *;", + rusqlite::params![&list_pk, &address], + |_| Ok(()), + ) + .map_err(|err| { + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { + Error::from(err).chain_err(|| NotFound("list or list owner not found!")) + } else { + err.into() + } + })?; Ok(()) } - pub fn update_member(&self, _change_set: ListMembershipChangeset) -> Result<()> { - /* - diesel::update(membership::table) - .set(&set) - .execute(&self.connection)?; - */ + /// Update a mailing list membership. + pub fn update_member(&mut self, change_set: ListMembershipChangeset) -> Result<()> { + let pk = self + .list_member_by_address(change_set.list, &change_set.address)? + .pk; + if matches!( + change_set, + ListMembershipChangeset { + list: _, + address: _, + name: None, + digest: None, + hide_address: None, + receive_duplicates: None, + receive_own_posts: None, + receive_confirmation: None, + enabled: None, + } + ) { + return Ok(()); + } + + let ListMembershipChangeset { + list, + address: _, + name, + digest, + hide_address, + receive_duplicates, + receive_own_posts, + receive_confirmation, + enabled, + } = change_set; + let tx = self.connection.transaction()?; + + macro_rules! update { + ($field:tt) => {{ + if let Some($field) = $field { + tx.execute( + concat!( + "UPDATE membership SET ", + stringify!($field), + " = ? WHERE list = ? AND pk = ?;" + ), + rusqlite::params![&$field, &list, &pk], + )?; + } + }}; + } + update!(name); + update!(digest); + update!(hide_address); + update!(receive_duplicates); + update!(receive_own_posts); + update!(receive_confirmation); + update!(enabled); + + tx.commit()?; Ok(()) } } diff --git a/core/src/db/posts.rs b/core/src/db/posts.rs index a5cd9dc..41e806d 100644 --- a/core/src/db/posts.rs +++ b/core/src/db/posts.rs @@ -18,8 +18,10 @@ */ use super::*; +use crate::mail::ListRequest; -impl Database { +impl Connection { + /// Insert a mailing list post into the database. pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result { let from_ = env.from(); let address = if from_.is_empty() { @@ -65,6 +67,7 @@ impl Database { Ok(pk) } + /// Process a new mailing list post. pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { let result = self.inner_post(env, raw, _dry_run); if let Err(err) = result { @@ -89,7 +92,7 @@ impl Database { if env.from().is_empty() { return Err("Envelope From: field is empty!".into()); } - let mut lists = self.list_lists()?; + let mut lists = self.lists()?; for t in &tos { if let Some((addr, subaddr)) = t.subaddress("+") { lists.retain(|list| { @@ -123,12 +126,13 @@ impl Database { use crate::mail::{ListContext, Post, PostAction}; for mut list in lists { trace!("Examining list {}", list.display_name()); - let filters = self.get_list_filters(&list); + let filters = self.list_filters(&list); let memberships = self.list_members(list.pk)?; + let owners = self.list_owners(list.pk)?; trace!("List members {:#?}", &memberships); let mut list_ctx = ListContext { - policy: self.get_list_policy(list.pk)?, - list_owners: self.get_list_owners(list.pk)?, + policy: self.list_policy(list.pk)?, + list_owners: &owners, list: &mut list, memberships: &memberships, scheduled_jobs: vec![], @@ -201,6 +205,7 @@ impl Database { Ok(()) } + /// Process a new mailing list request. pub fn request( &self, list: &DbVal, @@ -216,7 +221,7 @@ impl Database { list ); - let list_policy = self.get_list_policy(list.pk)?; + let list_policy = self.list_policy(list.pk)?; let approval_needed = list_policy .as_ref() .map(|p| p.approval_needed) @@ -254,7 +259,7 @@ impl Database { list ); for f in env.from() { - if let Err(_err) = self.remove_member(list.pk, &f.get_email()) { + if let Err(_err) = self.remove_membership(list.pk, &f.get_email()) { //FIXME: send failure notice to f } else { //FIXME: send success notice to f @@ -308,6 +313,7 @@ impl Database { Ok(()) } + /// Fetch all year and month values for which at least one post exists in `yyyy-mm` format. pub fn months(&self, list_pk: i64) -> Result> { let mut stmt = self.connection.prepare( "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post WHERE list = ?;", diff --git a/core/src/errors.rs b/core/src/errors.rs index 6fb1c23..da0d875 100644 --- a/core/src/errors.rs +++ b/core/src/errors.rs @@ -17,39 +17,44 @@ * along with this program. If not, see . */ +//! Errors of this library. + pub use crate::anyhow::Context; pub use error_chain::ChainedError; // Create the Error, ErrorKind, ResultExt, and Result types + error_chain! { errors { + /// Post rejected. PostRejected(reason: String) { description("Post rejected") display("Your post has been rejected: {}", reason) } + /// An entry was not found in the database. NotFound(model: &'static str) { description("Not found") display("This {} is not present in the database.", model) } + /// A request was invalid. InvalidRequest(reason: String) { description("List request is invalid") display("Your list request has been found invalid: {}.", reason) } + /// An error happened and it was handled internally. Information(reason: String) { description("") display("{}.", reason) } } foreign_links { - Logic(anyhow::Error); - Sql(rusqlite::Error); - Io(::std::io::Error); - Xdg(xdg::BaseDirectoriesError); - Melib(melib::error::Error); - Configuration(toml::de::Error); - SerdeJson(serde_json::Error); + Logic(anyhow::Error) #[doc="Error returned from an external user initiated operation such as deserialization or I/O."]; + Sql(rusqlite::Error) #[doc="Error returned from sqlite3."]; + Io(::std::io::Error) #[doc="Error returned from internal I/O operations."]; + Melib(melib::error::Error) #[doc="Error returned from e-mail protocol operations from `melib` crate."]; + SerdeJson(serde_json::Error) #[doc="Error from deserializing JSON values."]; } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 4f66369..2be8f8b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -16,28 +16,118 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -// `error_chain!` can recurse deeply -#![recursion_limit = "1024"] -//#![warn(missing_docs)] +#![warn(missing_docs)] +//! Mailing list manager library. +//! +//! ``` +//! use mailpot::{models::*, Configuration, Connection, SendMail}; +//! # use tempfile::TempDir; +//! +//! # let tmp_dir = TempDir::new().unwrap(); +//! # let db_path = tmp_dir.path().join("mpot.db"); +//! # let config = Configuration { +//! # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), +//! # db_path: db_path.clone(), +//! # data_path: tmp_dir.path().to_path_buf(), +//! # }; +//! # +//! # fn do_test(config: Configuration) -> mailpot::Result<()> { +//! let db = Connection::open_or_create_db(config)?.trusted(); +//! +//! // Create a new mailing list +//! let list_pk = db.create_list(MailingList { +//! pk: 0, +//! name: "foobar chat".into(), +//! id: "foo-chat".into(), +//! address: "foo-chat@example.com".into(), +//! description: None, +//! archive_url: None, +//! })?.pk; +//! +//! db.set_list_policy( +//! PostPolicy { +//! pk: 0, +//! list: list_pk, +//! announce_only: false, +//! subscriber_only: true, +//! approval_needed: false, +//! no_subscriptions: false, +//! custom: false, +//! }, +//! )?; +//! +//! // Drop privileges; we can only process new e-mail and modify memberships from now on. +//! let db = db.untrusted(); +//! +//! assert_eq!(db.list_members(list_pk)?.len(), 0); +//! assert_eq!(db.list_posts(list_pk, None)?.len(), 0); +//! +//! // Process a subscription request e-mail +//! let subscribe_bytes = b"From: Name +//! To: +//! Subject: subscribe +//! Date: Thu, 29 Oct 2020 13:58:16 +0000 +//! Message-ID: <1@example.com> +//! +//! "; +//! let envelope = melib::Envelope::from_bytes(subscribe_bytes, None)?; +//! db.post(&envelope, subscribe_bytes, /* dry_run */ false)?; +//! +//! assert_eq!(db.list_members(list_pk)?.len(), 1); +//! assert_eq!(db.list_posts(list_pk, None)?.len(), 0); +//! +//! // Process a post +//! let post_bytes = b"From: Name +//! To: +//! Subject: my first post +//! Date: Thu, 29 Oct 2020 14:01:09 +0000 +//! Message-ID: <2@example.com> +//! +//! Hello +//! "; +//! let envelope = +//! melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message"); +//! db.post(&envelope, post_bytes, /* dry_run */ false)?; +//! +//! assert_eq!(db.list_members(list_pk)?.len(), 1); +//! assert_eq!(db.list_posts(list_pk, None)?.len(), 1); +//! # Ok(()) +//! # } +//! # do_test(config); +//! ``` -use log::{info, trace}; #[macro_use] extern crate error_chain; extern crate anyhow; + #[macro_use] pub extern crate serde; +pub extern crate log; +pub extern crate melib; +pub extern crate serde_json; -pub use melib; -pub use serde_json; +use log::{info, trace}; -pub mod config; +mod config; pub mod mail; pub mod models; use models::*; -pub mod errors; -use errors::*; -pub mod db; +mod db; +mod errors; pub use config::{Configuration, SendMail}; -pub use db::Database; +pub use db::*; pub use errors::*; + +/// A `mailto:` value. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MailtoAddress { + /// E-mail address. + pub address: String, + /// Optional subject value. + pub subject: Option, +} + +#[doc = include_str!("../../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/core/src/mail.rs b/core/src/mail.rs index b133586..ca45412 100644 --- a/core/src/mail.rs +++ b/core/src/mail.rs @@ -17,32 +17,56 @@ * along with this program. If not, see . */ +//! Types for processing new posts: [`PostFilter`](message_filters::PostFilter), [`ListContext`], +//! [`MailJob`] and [`PostAction`]. + use super::*; use melib::Address; pub mod message_filters; +/// Post action returned from a list's [`PostFilter`](message_filters::PostFilter) stack. #[derive(Debug)] pub enum PostAction { + /// Add to `hold` queue. Hold, + /// Accept to mailing list. Accept, - Reject { reason: String }, - Defer { reason: String }, + /// Reject and send rejection response to submitter. + Reject { + /// Human readable reason for rejection. + reason: String, + }, + /// Add to `deferred` queue. + Defer { + /// Human readable reason for deferring. + reason: String, + }, } +/// List context passed to a list's [`PostFilter`](message_filters::PostFilter) stack. #[derive(Debug)] pub struct ListContext<'list> { + /// Which mailing list a post was addressed to. pub list: &'list MailingList, - pub list_owners: Vec>, + /// The mailing list owners. + pub list_owners: &'list [DbVal], + /// The mailing list memberships. pub memberships: &'list [DbVal], + /// The mailing list post policy. pub policy: Option>, + /// The scheduled jobs added by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack. pub scheduled_jobs: Vec, } -///Post to be considered by the list's `PostFilter` stack. +/// Post to be considered by the list's [`PostFilter`](message_filters::PostFilter) stack. pub struct Post { + /// `From` address of post. pub from: Address, + /// Raw bytes of post. pub bytes: Vec, + /// `To` addresses of post. pub to: Vec
, + /// Final action set by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack. pub action: PostAction, } @@ -57,12 +81,76 @@ impl core::fmt::Debug for Post { } } +/// Scheduled jobs added to a [`ListContext`] by a list's [`PostFilter`](message_filters::PostFilter) stack. #[derive(Debug)] pub enum MailJob { - Send { recipients: Vec
}, - Relay { recipients: Vec
}, - Error { description: String }, - StoreDigest { recipients: Vec
}, - ConfirmSubscription { recipient: Address }, - ConfirmUnsubscription { recipient: Address }, + /// Send post to recipients. + Send { + /// The post recipients addresses. + recipients: Vec
, + }, + /// Send error to submitter. + Error { + /// Human readable description of the error. + description: String, + }, + /// Store post in digest for recipients. + StoreDigest { + /// The digest recipients addresses. + recipients: Vec
, + }, + /// Reply with subscription confirmation to submitter. + ConfirmSubscription { + /// The submitter address. + recipient: Address, + }, + /// Reply with unsubscription confirmation to submitter. + ConfirmUnsubscription { + /// The submitter address. + recipient: Address, + }, +} + +/// Type of mailing list request. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum ListRequest { + /// Request subscription. + Subscribe, + /// Request removal of subscription. + Unsubscribe, + /// Request reception of list posts from a month-year range, inclusive. + RetrieveArchive(String, String), + /// Request reception of specific mailing list posts from `Message-ID` values. + RetrieveMessages(Vec), + /// Request change in digest preferences. (See [`ListMembership`]) + SetDigest(bool), + /// Other type of request. + Other(String), +} + +impl std::fmt::Display for ListRequest { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} + +impl> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest { + type Error = crate::Error; + + fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result { + let val = val.as_ref(); + Ok(match val { + "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 {:?}", val, env.from(),); + ListRequest::Other(val.trim().to_string()) + } + }) + } } diff --git a/core/src/mail/message_filters.rs b/core/src/mail/message_filters.rs index f0449a7..142cab3 100644 --- a/core/src/mail/message_filters.rs +++ b/core/src/mail/message_filters.rs @@ -16,13 +16,35 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + #![allow(clippy::result_unit_err)] +//! Filters to pass each mailing list post through. Filters are functions that implement the +//! [`PostFilter`] trait that can: +//! +//! - transform post content. +//! - modify the final [`PostAction`] to take. +//! - modify the final scheduled jobs to perform. (See [`MailJob`]). +//! +//! Filters are executed in sequence like this: +//! +//! ```ignore +//! let result = filters +//! .into_iter() +//! .fold(Ok((&mut post, &mut list_ctx)), |p, f| { +//! p.and_then(|(p, c)| f.feed(p, c)) +//! }); +//! ``` +//! +//! so the processing stops at the first returned error. + use super::*; -///Filter that modifies and/or verifies a post candidate. On rejection, return a string -///describing the error and optionally set `post.action` to `Reject` or `Defer` +/// Filter that modifies and/or verifies a post candidate. On rejection, return a string +/// describing the error and optionally set `post.action` to `Reject` or `Defer` pub trait PostFilter { + /// Feed post into the filter. Perform modifications to `post` and / or `ctx`, and return them + /// with `Result::Ok` unless you want to the processing to stop and return an `Result::Err`. fn feed<'p, 'list>( self: Box, post: &'p mut Post, @@ -30,7 +52,7 @@ pub trait PostFilter { ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>; } -///Check that submitter can post to list, for now it accepts everything. +/// Check that submitter can post to list, for now it accepts everything. pub struct PostRightsCheck; impl PostFilter for PostRightsCheck { fn feed<'p, 'list>( @@ -45,7 +67,7 @@ impl PostFilter for PostRightsCheck { let owner_addresses = ctx .list_owners .iter() - .map(|lo| lo.into_address()) + .map(|lo| lo.address()) .collect::>(); trace!("Owner addresses are: {:#?}", &owner_addresses); trace!("Envelope from is: {:?}", &post.from); @@ -79,7 +101,7 @@ impl PostFilter for PostRightsCheck { } } -///Ensure message contains only `\r\n` line terminators, required by SMTP. +/// Ensure message contains only `\r\n` line terminators, required by SMTP. pub struct FixCRLF; impl PostFilter for FixCRLF { fn feed<'p, 'list>( @@ -99,7 +121,7 @@ impl PostFilter for FixCRLF { } } -///Add `List-*` headers +/// Add `List-*` headers pub struct AddListHeaders; impl PostFilter for AddListHeaders { fn feed<'p, 'list>( @@ -147,7 +169,7 @@ impl PostFilter for AddListHeaders { } } -///Adds `Archived-At` field, if configured. +/// Adds `Archived-At` field, if configured. pub struct ArchivedAtLink; impl PostFilter for ArchivedAtLink { fn feed<'p, 'list>( @@ -160,8 +182,8 @@ impl PostFilter for ArchivedAtLink { } } -///Assuming there are no more changes to be done on the post, it finalizes which list members -///will receive the post in `post.action` field. +/// Assuming there are no more changes to be done on the post, it finalizes which list members +/// will receive the post in `post.action` field. pub struct FinalizeRecipients; impl PostFilter for FinalizeRecipients { fn feed<'p, 'list>( @@ -181,13 +203,13 @@ impl PostFilter for FinalizeRecipients { if member.digest { if member.address != email_from || member.receive_own_posts { trace!("Member gets digest"); - digests.push(member.into_address()); + digests.push(member.address()); } continue; } if member.address != email_from || member.receive_own_posts { trace!("Member gets copy"); - recipients.push(member.into_address()); + recipients.push(member.address()); } // TODO: // - check for duplicates (To,Cc,Bcc) diff --git a/core/src/models.rs b/core/src/models.rs index 0ba3890..425ea60 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -17,16 +17,21 @@ * along with this program. If not, see . */ +//! Database models: [`MailingList`], [`ListOwner`], [`ListMembership`], [`PostPolicy`] and +//! [`Post`]. + use super::*; pub mod changesets; use melib::email::Address; +/// A database entry and its primary key. Derefs to its inner type. #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(transparent)] pub struct DbVal(pub T, #[serde(skip)] pub i64); impl DbVal { + /// Primary key. #[inline(always)] pub fn pk(&self) -> i64 { self.1 @@ -49,13 +54,20 @@ where } } +/// A mailing list entry. #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct MailingList { + /// Database primary key. pub pk: i64, + /// Mailing list name. pub name: String, + /// Mailing list ID (what appears in the subject tag, e.g. `[mailing-list] New post!`). pub id: String, + /// Mailing list e-mail address. pub address: String, + /// Mailing list description. pub description: Option, + /// Mailing list archive URL. pub archive_url: Option, } @@ -78,14 +90,21 @@ impl std::fmt::Display for MailingList { } impl MailingList { + /// Mailing list display name (e.g. `list name `). pub fn display_name(&self) -> String { format!("\"{}\" <{}>", self.name, self.address) } + /// Value of `List-Post` header. + /// + /// See RFC2369 Section 3.4: pub fn post_header(&self) -> Option { Some(format!("", self.address)) } + /// Value of `List-Unsubscribe` header. + /// + /// See RFC2369 Section 3.2: pub fn unsubscribe_header(&self) -> Option { let p = self.address.split('@').collect::>(); Some(format!( @@ -94,14 +113,19 @@ impl MailingList { )) } + /// Value of `List-Archive` header. + /// + /// See RFC2369 Section 3.6: pub fn archive_header(&self) -> Option { self.archive_url.as_ref().map(|url| format!("<{}>", url)) } + /// List address as a [`melib::Address`] pub fn address(&self) -> Address { Address::new(Some(self.name.clone()), self.address.clone()) } + /// List unsubscribe action as a [`MailtoAddress`](super::MailtoAddress). pub fn unsubscribe_mailto(&self) -> Option { let p = self.address.split('@').collect::>(); Some(MailtoAddress { @@ -110,6 +134,7 @@ impl MailingList { }) } + /// List subscribe action as a [`MailtoAddress`](super::MailtoAddress). pub fn subscribe_mailto(&self) -> Option { let p = self.address.split('@').collect::>(); Some(MailtoAddress { @@ -118,28 +143,36 @@ impl MailingList { }) } + /// List archive url value. pub fn archive_url(&self) -> Option<&str> { self.archive_url.as_deref() } } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct MailtoAddress { - pub address: String, - pub subject: Option, -} - +/// A mailing list membership entry. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ListMembership { + /// Database primary key. pub pk: i64, + /// Mailing list foreign key (See [`MailingList`]). pub list: i64, + /// Member's e-mail address. pub address: String, + /// Member's name, optional. pub name: Option, + /// Whether member wishes to receive list posts as a periodical digest e-mail. pub digest: bool, + /// Whether member wishes their e-mail address hidden from public view. pub hide_address: bool, + /// Whether member wishes to receive mailing list post duplicates, i.e. posts addressed to them + /// and the mailing list to which they are subscribed. pub receive_duplicates: bool, + /// Whether member wishes to receive their own mailing list posts from the mailing list, as a + /// confirmation. pub receive_own_posts: bool, + /// Whether member wishes to receive a plain confirmation for their own mailing list posts. pub receive_confirmation: bool, + /// Whether this membership is enabled. pub enabled: bool, } @@ -148,7 +181,7 @@ impl std::fmt::Display for ListMembership { write!( fmt, "{} [digest: {}, hide_address: {} {}]", - self.into_address(), + self.address(), self.digest, self.hide_address, if self.enabled { @@ -161,19 +194,33 @@ impl std::fmt::Display for ListMembership { } impl ListMembership { - pub fn into_address(&self) -> Address { + /// Member address as a [`melib::Address`] + pub fn address(&self) -> Address { Address::new(self.name.clone(), self.address.clone()) } } +/// A mailing list post policy entry. +/// +/// Only one of the boolean flags must be set to true. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PostPolicy { + /// Database primary key. pub pk: i64, + /// Mailing list foreign key (See [`MailingList`]). pub list: i64, + /// Whether the policy is announce only (Only list owners can submit posts, and everyone will + /// receive them). pub announce_only: bool, + /// Whether the policy is "subscriber only" (Only list subscribers can post). pub subscriber_only: bool, + /// Whether the policy is "approval needed" (Anyone can post, but approval from list owners is + /// required if they are not subscribed). pub approval_needed: bool, + /// Whether the policy is "no subscriptions" (Anyone can post, but approval from list owners is + /// required. Subscriptions are not enabled). pub no_subscriptions: bool, + /// Custom policy. pub custom: bool, } @@ -183,17 +230,22 @@ impl std::fmt::Display for PostPolicy { } } +/// A mailing list owner entry. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ListOwner { + /// Database primary key. pub pk: i64, + /// Mailing list foreign key (See [`MailingList`]). pub list: i64, + /// Mailing list owner e-mail address. pub address: String, + /// Mailing list owner name, optional. pub name: Option, } impl std::fmt::Display for ListOwner { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "[#{} {}] {}", self.pk, self.list, self.into_address()) + write!(fmt, "[#{} {}] {}", self.pk, self.list, self.address()) } } @@ -215,65 +267,30 @@ impl From for ListMembership { } impl ListOwner { - pub fn into_address(&self) -> Address { + /// Owner address as a [`melib::Address`] + pub fn address(&self) -> Address { Address::new(self.name.clone(), self.address.clone()) } } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub enum ListRequest { - Subscribe, - Unsubscribe, - RetrieveArchive(String, String), - RetrieveMessages(Vec), - SetDigest(bool), - Other(String), -} - -impl std::fmt::Display for ListRequest { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "{:?}", self) - } -} - -impl> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest { - type Error = crate::Error; - - fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result { - let val = val.as_ref(); - Ok(match val { - "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 {:?}", val, env.from(),); - ListRequest::Other(val.trim().to_string()) - } - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct NewListPost<'s> { - pub list: i64, - pub address: &'s str, - pub message_id: &'s str, - pub message: &'s [u8], -} - +/// A mailing list post entry. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Post { + /// Database primary key. pub pk: i64, + /// Mailing list foreign key (See [`MailingList`]). pub list: i64, + /// `From` header address of post. pub address: String, + /// `Message-ID` header value of post. pub message_id: String, + /// Post as bytes. pub message: Vec, + /// Unix timestamp of date. pub timestamp: u64, + /// Datetime as string. pub datetime: String, + /// Month-year as a `YYYY-mm` formatted string, for use in archives. pub month_year: String, } diff --git a/core/src/models/changesets.rs b/core/src/models/changesets.rs index af4929e..fba0a81 100644 --- a/core/src/models/changesets.rs +++ b/core/src/models/changesets.rs @@ -17,43 +17,73 @@ * along with this program. If not, see . */ +//! Changeset structs: update specific struct fields. + +/// Changeset struct for [`Mailinglist`](super::MailingList). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MailingListChangeset { + /// Database primary key. pub pk: i64, + /// Optional new value. pub name: Option, + /// Optional new value. pub id: Option, + /// Optional new value. pub address: Option, + /// Optional new value. pub description: Option>, + /// Optional new value. pub archive_url: Option>, } +/// Changeset struct for [`ListMembership`](super::ListMembership). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ListMembershipChangeset { + /// Mailing list foreign key (See [`MailingList`](super::MailingList)). pub list: i64, + /// Membership e-mail address. pub address: String, + /// Optional new value. pub name: Option>, + /// Optional new value. pub digest: Option, + /// Optional new value. pub hide_address: Option, + /// Optional new value. pub receive_duplicates: Option, + /// Optional new value. pub receive_own_posts: Option, + /// Optional new value. pub receive_confirmation: Option, + /// Optional new value. pub enabled: Option, } +/// Changeset struct for [`PostPolicy`](super::PostPolicy). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PostPolicyChangeset { + /// Database primary key. pub pk: i64, + /// Mailing list foreign key (See [`MailingList`](super::MailingList)). pub list: i64, + /// Optional new value. pub announce_only: Option, + /// Optional new value. pub subscriber_only: Option, + /// Optional new value. pub approval_needed: Option, } +/// Changeset struct for [`ListOwner`](super::ListOwner). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ListOwnerChangeset { + /// Database primary key. pub pk: i64, + /// Mailing list foreign key (See [`MailingList`](super::MailingList)). pub list: i64, + /// Optional new value. pub address: Option, + /// Optional new value. pub name: Option>, } diff --git a/core/src/schema.sql b/core/src/schema.sql index 5f331e7..1197f56 100644 --- a/core/src/schema.sql +++ b/core/src/schema.sql @@ -43,7 +43,8 @@ CREATE TABLE IF NOT EXISTS membership ( 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, FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE, - FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE + FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE, + UNIQUE (list, address) ON CONFLICT ROLLBACK ); CREATE TABLE IF NOT EXISTS account ( diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 index 92cdc52..9be5bd7 100644 --- a/core/src/schema.sql.m4 +++ b/core/src/schema.sql.m4 @@ -47,7 +47,8 @@ CREATE TABLE IF NOT EXISTS membership ( BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(), BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(), FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE, - FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE + FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE, + UNIQUE (list, address) ON CONFLICT ROLLBACK ); CREATE TABLE IF NOT EXISTS account ( diff --git a/core/tests/authorizer.rs b/core/tests/authorizer.rs index 47962d2..5f84ad4 100644 --- a/core/tests/authorizer.rs +++ b/core/tests/authorizer.rs @@ -19,7 +19,7 @@ mod utils; -use mailpot::{models::*, Configuration, Database, SendMail}; +use mailpot::{models::*, Configuration, Connection, SendMail}; use std::error::Error; use tempfile::TempDir; @@ -32,12 +32,11 @@ fn test_authorizer() { let config = Configuration { send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap(); - assert!(db.list_lists().unwrap().is_empty()); + let db = Connection::open_or_create_db(config).unwrap(); + assert!(db.lists().unwrap().is_empty()); for err in [ db.create_list(MailingList { @@ -73,7 +72,7 @@ fn test_authorizer() { }, ); } - assert!(db.list_lists().unwrap().is_empty()); + assert!(db.lists().unwrap().is_empty()); let db = db.trusted(); diff --git a/core/tests/creation.rs b/core/tests/creation.rs index 5e69328..43b3e71 100644 --- a/core/tests/creation.rs +++ b/core/tests/creation.rs @@ -19,7 +19,7 @@ mod utils; -use mailpot::{models::*, Configuration, Database, SendMail}; +use mailpot::{models::*, Configuration, Connection, SendMail}; use tempfile::TempDir; #[test] @@ -31,13 +31,12 @@ fn test_init_empty() { let config = Configuration { send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap(); + let db = Connection::open_or_create_db(config).unwrap(); - assert!(db.list_lists().unwrap().is_empty()); + assert!(db.lists().unwrap().is_empty()); } #[test] @@ -49,12 +48,11 @@ fn test_list_creation() { let config = Configuration { send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap().trusted(); - assert!(db.list_lists().unwrap().is_empty()); + let db = Connection::open_or_create_db(config).unwrap().trusted(); + assert!(db.lists().unwrap().is_empty()); let foo_chat = db .create_list(MailingList { pk: 0, @@ -67,7 +65,7 @@ fn test_list_creation() { .unwrap(); assert_eq!(foo_chat.pk(), 1); - let lists = db.list_lists().unwrap(); + let lists = db.lists().unwrap(); assert_eq!(lists.len(), 1); assert_eq!(lists[0], foo_chat); } diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs index 2d69723..1254dd3 100644 --- a/core/tests/error_queue.rs +++ b/core/tests/error_queue.rs @@ -19,7 +19,7 @@ mod utils; -use mailpot::{melib, models::*, Configuration, Database, SendMail}; +use mailpot::{melib, models::*, Configuration, Connection, SendMail}; use tempfile::TempDir; fn get_smtp_conf() -> melib::smtp::SmtpServerConf { @@ -43,12 +43,11 @@ fn test_error_queue() { let config = Configuration { send_mail: SendMail::Smtp(get_smtp_conf()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap().trusted(); - assert!(db.list_lists().unwrap().is_empty()); + let db = Connection::open_or_create_db(config).unwrap().trusted(); + assert!(db.lists().unwrap().is_empty()); let foo_chat = db .create_list(MailingList { pk: 0, diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs index bbb197d..5624c92 100644 --- a/core/tests/smtp.rs +++ b/core/tests/smtp.rs @@ -21,7 +21,7 @@ mod utils; use log::{trace, warn}; use mailin_embedded::{Handler, Response, Server, SslConfig}; -use mailpot::{melib, models::*, Configuration, Database, SendMail}; +use mailpot::{melib, models::*, Configuration, Connection, SendMail}; use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr}; use std::sync::{Arc, Mutex}; use std::thread; @@ -204,12 +204,11 @@ fn test_smtp() { let config = Configuration { send_mail: SendMail::Smtp(get_smtp_conf()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap().trusted(); - assert!(db.list_lists().unwrap().is_empty()); + let db = Connection::open_or_create_db(config).unwrap().trusted(); + assert!(db.lists().unwrap().is_empty()); let foo_chat = db .create_list(MailingList { pk: 0, @@ -323,12 +322,11 @@ fn test_smtp_mailcrab() { let config = Configuration { send_mail: SendMail::Smtp(get_smtp_conf()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap().trusted(); - assert!(db.list_lists().unwrap().is_empty()); + let db = Connection::open_or_create_db(config).unwrap().trusted(); + assert!(db.lists().unwrap().is_empty()); let foo_chat = db .create_list(MailingList { pk: 0, diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs index 8b0323b..a3f89ca 100644 --- a/core/tests/subscription.rs +++ b/core/tests/subscription.rs @@ -19,7 +19,7 @@ mod utils; -use mailpot::{models::*, Configuration, Database, SendMail}; +use mailpot::{models::*, Configuration, Connection, SendMail}; use tempfile::TempDir; #[test] @@ -32,12 +32,11 @@ fn test_list_subscription() { let config = Configuration { send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), db_path: db_path.clone(), - storage: "sqlite3".to_string(), data_path: tmp_dir.path().to_path_buf(), }; - let db = Database::open_or_create_db(config).unwrap().trusted(); - assert!(db.list_lists().unwrap().is_empty()); + let db = Connection::open_or_create_db(config).unwrap().trusted(); + assert!(db.lists().unwrap().is_empty()); let foo_chat = db .create_list(MailingList { pk: 0, @@ -50,7 +49,7 @@ fn test_list_subscription() { .unwrap(); assert_eq!(foo_chat.pk(), 1); - let lists = db.list_lists().unwrap(); + let lists = db.lists().unwrap(); assert_eq!(lists.len(), 1); assert_eq!(lists[0], foo_chat); let post_policy = db @@ -67,6 +66,7 @@ fn test_list_subscription() { assert_eq!(post_policy.pk(), 1); assert_eq!(db.error_queue().unwrap().len(), 0); + assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 0); let db = db.untrusted(); @@ -114,6 +114,7 @@ MIME-Version: 1.0 melib::Envelope::from_bytes(input_bytes_2, None).expect("Could not parse message"); db.post(&envelope, input_bytes_2, /* dry_run */ false) .unwrap(); + assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 1); assert_eq!(db.error_queue().unwrap().len(), 1); let envelope = melib::Envelope::from_bytes(input_bytes_1, None).expect("Could not parse message"); diff --git a/rest-http/src/main.rs b/rest-http/src/main.rs index 7663f97..764facc 100644 --- a/rest-http/src/main.rs +++ b/rest-http/src/main.rs @@ -19,22 +19,11 @@ extern crate mailpot; -pub use mailpot::config::*; -pub use mailpot::db::*; -pub use mailpot::errors::*; pub use mailpot::models::*; pub use mailpot::*; use warp::Filter; -/* -fn json_body() -> impl Filter + Clone { - // When accepting a body, we want a JSON body - // (and to reject huge payloads)... - warp::body::content_length_limit(1024 * 16).and(warp::body::json()) -} -*/ - #[tokio::main] async fn main() { let config_path = std::env::args() @@ -45,8 +34,8 @@ async fn main() { let conf1 = conf.clone(); // GET /lists/:i64/policy let policy = warp::path!("lists" / i64 / "policy").map(move |list_pk| { - let db = Database::open_db(conf1.clone()).unwrap(); - db.get_list_policy(list_pk) + let db = Connection::open_db(conf1.clone()).unwrap(); + db.list_policy(list_pk) .ok() .map(|l| warp::reply::json(&l.unwrap())) .unwrap() @@ -55,34 +44,33 @@ async fn main() { let conf2 = conf.clone(); //get("/lists")] let lists = warp::path!("lists").map(move || { - let db = Database::open_db(conf2.clone()).unwrap(); - let lists = db.list_lists().unwrap(); + let db = Connection::open_db(conf2.clone()).unwrap(); + let lists = db.lists().unwrap(); warp::reply::json(&lists) }); let conf3 = conf.clone(); //get("/lists/")] let lists_num = warp::path!("lists" / i64).map(move |list_pk| { - let db = Database::open_db(conf3.clone()).unwrap(); - let list = db.get_list(list_pk).unwrap(); + let db = Connection::open_db(conf3.clone()).unwrap(); + let list = db.list(list_pk).unwrap(); warp::reply::json(&list) }); let conf4 = conf.clone(); //get("/lists//members")] let lists_members = warp::path!("lists" / i64 / "members").map(move |list_pk| { - let db = Database::open_db(conf4.clone()).unwrap(); + let db = Connection::open_db(conf4.clone()).unwrap(); db.list_members(list_pk) .ok() .map(|l| warp::reply::json(&l)) .unwrap() }); - let conf5 = conf.clone(); //get("/lists//owners")] let lists_owners = warp::path!("lists" / i64 / "owners").map(move |list_pk| { - let db = Database::open_db(conf.clone()).unwrap(); - db.get_list_owners(list_pk) + let db = Connection::open_db(conf.clone()).unwrap(); + db.list_owners(list_pk) .ok() .map(|l| warp::reply::json(&l)) .unwrap() @@ -101,9 +89,5 @@ async fn main() { .or(lists_owner_add), ); - // Note that composing filters for many routes may increase compile times (because it uses a lot of generics). - // If you wish to use dynamic dispatch instead and speed up compile times while - // making it slightly slower at runtime, you can use Filter::boxed(). - warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }