Document entire `core` create, add CI, tests

axum
Manos Pitsidianakis 2023-04-03 20:36:43 +03:00
parent 46b942b843
commit e47f1c68dc
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
27 changed files with 973 additions and 296 deletions

1
.github/FUNDING.yml vendored 100644
View File

@ -0,0 +1 @@
github: [epilys]

75
.github/workflows/builds.yaml vendored 100644
View File

@ -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

91
.github/workflows/test.yaml vendored 100644
View File

@ -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 <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
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 <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
run: |
cargo sort --check
- name: rustfmt
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
run: |
cargo fmt --check --all
- name: clippy
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
run: |
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins

105
README.md
View File

@ -1,9 +1,12 @@
# Mailpot - WIP mailing list manager # mailpot - WIP mailing list manager
Rendered rustdoc of `core` crate: <https://meli.github.io/mailpot/docs/mailpot/>
Crates: Crates:
- `core` - `core` the library
- `cli` a command line tool to manage lists - `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 - `rest-http` a REST http server to manage lists
## Project goals ## Project goals
@ -12,32 +15,25 @@ Crates:
- extensible through Rust API as a library - extensible through Rust API as a library
- extensible through HTTP REST API as an HTTP server, with webhooks - extensible through HTTP REST API as an HTTP server, with webhooks
- basic management through CLI - basic management through CLI
- replaceable lightweight web archiver - optional lightweight web archiver
- custom storage? - useful for both newsletters, discussions, article comments
- useful for both newsletters, discussions
## Initial setup ## Initial setup
Check where `mpot` expects your database file to be: Create a configuration file and a database:
```shell
$ cargo run --bin mpot -- db-location
Configuration file /home/user/.config/mailpot/config.toml doesn't exist
```
Uuugh, oops.
```shell ```shell
$ mkdir -p /home/user/.config/mailpot $ mkdir -p /home/user/.config/mailpot
$ echo 'send_mail = { "type" = "ShellCommand", "value" = "/usr/bin/false" }' > /home/user/.config/mailpot/config.toml $ export MPOT_CONFIG=/home/user/.config/mailpot/config.toml
$ cargo run --bin mpot -- db-location $ 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 /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 ```shell
$ mkdir -p /home/user/.local/share/mailpot/
$ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql $ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql
``` ```
@ -188,3 +184,78 @@ TRACE - result Ok(
) )
``` ```
</details> </details>
## 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 <user@example.com>
To: <foo-chat+subscribe@example.com>
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 <user@example.com>
To: <foo-chat@example.com>
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>(())
```

View File

@ -14,18 +14,24 @@ default-run = "mpot-archives"
[[bin]] [[bin]]
name = "mpot-archives" name = "mpot-archives"
path = "src/main.rs" path = "src/main.rs"
required-features = ["warp"]
[[bin]] [[bin]]
name = "mpot-gen" name = "mpot-gen"
path = "src/gen.rs" path = "src/gen.rs"
[dependencies] [dependencies]
chrono = "^0.4" chrono = { version = "^0.4", optional = true }
lazy_static = "*" lazy_static = "*"
mailpot = { version = "0.1.0", path = "../core" } mailpot = { version = "0.1.0", path = "../core" }
minijinja = { version = "0.31.0", features = ["source", ] } minijinja = { version = "0.31.0", features = ["source", ], optional = true }
percent-encoding = "^2.1" percent-encoding = { version = "^2.1", optional = true }
serde = { version = "^1", features = ["derive", ] } serde = { version = "^1", features = ["derive", ] }
serde_json = "^1" serde_json = "^1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"], optional = true }
warp = "^0.3" warp = { version = "^0.3", optional = true }
[features]
default = ["gen"]
gen = ["dep:chrono", "dep:minijinja"]
warp = ["dep:percent-encoding", "dep:tokio", "dep:warp"]

View File

@ -22,9 +22,6 @@ use chrono::Datelike;
mod cal; mod cal;
pub use mailpot::config::*;
pub use mailpot::db::*;
pub use mailpot::errors::*;
pub use mailpot::models::*; pub use mailpot::models::*;
pub use mailpot::*; pub use mailpot::*;
@ -223,8 +220,8 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
let conf = Configuration::from_file(config_path) let conf = Configuration::from_file(config_path)
.map_err(|err| format!("Could not load config {config_path}: {err}"))?; .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 db = Connection::open_db(conf).map_err(|err| format!("Couldn't open db: {err}"))?;
let lists_values = db.list_lists()?; let lists_values = db.lists()?;
{ {
//index.html //index.html
@ -276,10 +273,8 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(&lists_path)?; std::fs::create_dir_all(&lists_path)?;
lists_path.push("index.html"); lists_path.push("index.html");
let list = db let list = db.list(list.pk)?;
.get_list(list.pk)? let post_policy = db.list_policy(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 months = db.months(list.pk)?; let months = db.months(list.pk)?;
let posts = db.list_posts(list.pk, None)?; let posts = db.list_posts(list.pk, None)?;
let mut hist = months let mut hist = months

View File

@ -19,9 +19,6 @@
extern crate mailpot; extern crate mailpot;
pub use mailpot::config::*;
pub use mailpot::db::*;
pub use mailpot::errors::*;
pub use mailpot::models::*; pub use mailpot::models::*;
pub use mailpot::*; pub use mailpot::*;
@ -47,8 +44,8 @@ async fn main() {
let conf1 = conf.clone(); let conf1 = conf.clone();
let list_handler = warp::path!("lists" / i64).map(move |list_pk: i64| { let list_handler = warp::path!("lists" / i64).map(move |list_pk: i64| {
let db = Database::open_db(conf1.clone()).unwrap(); let db = Connection::open_db(conf1.clone()).unwrap();
let list = db.get_list(list_pk).unwrap().unwrap(); let list = db.list(list_pk).unwrap();
let months = db.months(list_pk).unwrap(); let months = db.months(list_pk).unwrap();
let posts = db let posts = db
.list_posts(list_pk, None) .list_posts(list_pk, None)
@ -88,8 +85,8 @@ async fn main() {
let post_handler = let post_handler =
warp::path!("list" / i64 / String).map(move |list_pk: i64, message_id: String| { 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 message_id = percent_decode_str(&message_id).decode_utf8().unwrap();
let db = Database::open_db(conf2.clone()).unwrap(); let db = Connection::open_db(conf2.clone()).unwrap();
let list = db.get_list(list_pk).unwrap().unwrap(); let list = db.list(list_pk).unwrap();
let posts = db.list_posts(list_pk, None).unwrap(); let posts = db.list_posts(list_pk, None).unwrap();
let post = posts let post = posts
.iter() .iter()
@ -122,8 +119,8 @@ async fn main() {
}); });
let conf3 = conf.clone(); let conf3 = conf.clone();
let index_handler = warp::path::end().map(move || { let index_handler = warp::path::end().map(move || {
let db = Database::open_db(conf3.clone()).unwrap(); let db = Connection::open_db(conf3.clone()).unwrap();
let lists_values = db.list_lists().unwrap(); let lists_values = db.lists().unwrap();
let lists = lists_values let lists = lists_values
.iter() .iter()
.map(|list| { .map(|list| {

View File

@ -21,9 +21,6 @@ extern crate log;
extern crate mailpot; extern crate mailpot;
extern crate stderrlog; extern crate stderrlog;
pub use mailpot::config::*;
pub use mailpot::db::*;
pub use mailpot::errors::*;
pub use mailpot::mail::*; pub use mailpot::mail::*;
pub use mailpot::models::changesets::*; pub use mailpot::models::changesets::*;
pub use mailpot::models::*; pub use mailpot::models::*;
@ -31,14 +28,13 @@ pub use mailpot::*;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
macro_rules! get_list { macro_rules! list {
($db:ident, $list_id:expr) => {{ ($db:ident, $list_id:expr) => {{
$db.get_list_by_id(&$list_id)?.or_else(|| { $db.list_by_id(&$list_id)?.or_else(|| {
$list_id $list_id
.parse::<i64>() .parse::<i64>()
.ok() .ok()
.map(|pk| $db.get_list(pk).ok()) .map(|pk| $db.list(pk).ok())
.flatten()
.flatten() .flatten()
}) })
}}; }};
@ -60,7 +56,6 @@ struct Opt {
/// Set config file /// Set config file
#[structopt(short, long, parse(from_os_str))] #[structopt(short, long, parse(from_os_str))]
#[allow(dead_code)]
config: PathBuf, config: PathBuf,
#[structopt(flatten)] #[structopt(flatten)]
cmd: Command, cmd: Command,
@ -259,11 +254,11 @@ fn run_app(opt: Opt) -> Result<()> {
}; };
let config = Configuration::from_file(opt.config.as_path())?; let config = Configuration::from_file(opt.config.as_path())?;
use Command::*; use Command::*;
let mut db = Database::open_or_create_db(config)?; let mut db = Connection::open_or_create_db(config)?;
match opt.cmd { match opt.cmd {
SampleConfig => {} SampleConfig => {}
DumpDatabase => { DumpDatabase => {
let lists = db.list_lists()?; let lists = db.lists()?;
let mut stdout = std::io::stdout(); let mut stdout = std::io::stdout();
serde_json::to_writer_pretty(&mut stdout, &lists)?; serde_json::to_writer_pretty(&mut stdout, &lists)?;
for l in &lists { for l in &lists {
@ -271,13 +266,13 @@ fn run_app(opt: Opt) -> Result<()> {
} }
} }
ListLists => { ListLists => {
let lists = db.list_lists()?; let lists = db.lists()?;
if lists.is_empty() { if lists.is_empty() {
println!("No lists found."); println!("No lists found.");
} else { } else {
for l in lists { for l in lists {
println!("- {} {:?}", l.id, l); 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() { if list_owners.is_empty() {
println!("\tList owners: None"); println!("\tList owners: None");
} else { } else {
@ -286,7 +281,7 @@ fn run_app(opt: Opt) -> Result<()> {
println!("\t- {}", o); 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); println!("\tList policy: {}", s);
} else { } else {
println!("\tList policy: None"); println!("\tList policy: None");
@ -296,7 +291,7 @@ fn run_app(opt: Opt) -> Result<()> {
} }
} }
List { list_id, cmd } => { List { list_id, cmd } => {
let list = match get_list!(db, list_id) { let list = match list!(db, list_id) {
Some(v) => v, Some(v) => v,
None => { None => {
return Err(format!("No list with id or pk {} was found", list_id).into()); 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 => { Health => {
println!("{} health:", list); println!("{} health:", list);
let list_owners = db.get_list_owners(list.pk)?; let list_owners = db.list_owners(list.pk)?;
let list_policy = db.get_list_policy(list.pk)?; let list_policy = db.list_policy(list.pk)?;
if list_owners.is_empty() { if list_owners.is_empty() {
println!("\tList has no owners: you should add at least one."); println!("\tList has no owners: you should add at least one.");
} else { } else {
@ -377,8 +372,8 @@ fn run_app(opt: Opt) -> Result<()> {
} }
Info => { Info => {
println!("{} info:", list); println!("{} info:", list);
let list_owners = db.get_list_owners(list.pk)?; let list_owners = db.list_owners(list.pk)?;
let list_policy = db.get_list_policy(list.pk)?; let list_policy = db.list_policy(list.pk)?;
let members = db.list_members(list.pk)?; let members = db.list_members(list.pk)?;
if members.is_empty() { if members.is_empty() {
println!("No members."); println!("No members.");
@ -649,7 +644,7 @@ fn run_app(opt: Opt) -> Result<()> {
list_id, list_id,
mut maildir_path, mut maildir_path,
} => { } => {
let list = match get_list!(db, list_id) { let list = match list!(db, list_id) {
Some(v) => v, Some(v) => v,
None => { None => {
return Err(format!("No list with id or pk {} was found", list_id).into()); return Err(format!("No list with id or pk {} was found", list_id).into());

View File

@ -23,28 +23,36 @@ use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
/// How to send e-mail.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", content = "value")] #[serde(tag = "type", content = "value")]
pub enum SendMail { pub enum SendMail {
/// A `melib` configuration for talking to an SMTP server.
Smtp(melib::smtp::SmtpServerConf), Smtp(melib::smtp::SmtpServerConf),
/// A plain shell command passed to `sh -c` with the e-mail passed in the stdin.
ShellCommand(String), ShellCommand(String),
} }
/// The configuration for the mailpot database and the mail server.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Configuration { pub struct Configuration {
/// How to send e-mail.
pub send_mail: SendMail, pub send_mail: SendMail,
#[serde(default = "default_storage_fn")] /// The location of the sqlite3 file.
pub storage: String,
pub db_path: PathBuf, pub db_path: PathBuf,
/// The directory where data are stored.
pub data_path: PathBuf, pub data_path: PathBuf,
} }
impl Configuration { 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<PathBuf>) -> Self { pub fn new(db_path: impl Into<PathBuf>) -> Self {
let db_path = db_path.into(); let db_path = db_path.into();
Configuration { Configuration {
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
storage: "sqlite3".into(),
data_path: db_path data_path: db_path
.parent() .parent()
.map(Path::to_path_buf) .map(Path::to_path_buf)
@ -53,6 +61,7 @@ impl Configuration {
} }
} }
/// Deserialize configuration from TOML file.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref(); let path = path.as_ref();
let mut s = String::new(); let mut s = String::new();
@ -66,24 +75,17 @@ impl Configuration {
Ok(config) Ok(config)
} }
/// The saved data path.
pub fn data_directory(&self) -> &Path { pub fn data_directory(&self) -> &Path {
self.data_path.as_path() self.data_path.as_path()
} }
/// The sqlite3 database path.
pub fn db_path(&self) -> &Path { pub fn db_path(&self) -> &Path {
self.db_path.as_path() self.db_path.as_path()
} }
pub fn default_path() -> Result<PathBuf> { /// Save message to a custom path.
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)
}
pub fn save_message_to_path(&self, msg: &str, mut path: PathBuf) -> Result<PathBuf> { pub fn save_message_to_path(&self, msg: &str, mut path: PathBuf) -> Result<PathBuf> {
if path.is_dir() { if path.is_dir() {
let now = Local::now().timestamp(); let now = Local::now().timestamp();
@ -102,17 +104,15 @@ impl Configuration {
Ok(path) Ok(path)
} }
/// Save message to the data directory.
pub fn save_message(&self, msg: String) -> Result<PathBuf> { pub fn save_message(&self, msg: String) -> Result<PathBuf> {
self.save_message_to_path(&msg, self.data_directory().to_path_buf()) self.save_message_to_path(&msg, self.data_directory().to_path_buf())
} }
/// Serialize configuration to a TOML string.
pub fn to_toml(&self) -> String { pub fn to_toml(&self) -> String {
toml::Value::try_from(self) toml::Value::try_from(self)
.expect("Could not serialize config to TOML") .expect("Could not serialize config to TOML")
.to_string() .to_string()
} }
} }
fn default_storage_fn() -> String {
"sqlite3".to_string()
}

View File

@ -17,6 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//! Mailpot database and methods.
use super::Configuration; use super::Configuration;
use super::*; use super::*;
use crate::ErrorKind::*; use crate::ErrorKind::*;
@ -28,9 +30,9 @@ use std::convert::TryFrom;
use std::io::Write; use std::io::Write;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
const DB_NAME: &str = "current.db"; /// A connection to a `mailpot` database.
pub struct Connection {
pub struct Database { /// The `rusqlite` connection handle.
pub connection: DbConnection, pub connection: DbConnection,
conf: Configuration, conf: Configuration,
} }
@ -55,6 +57,7 @@ fn user_authorizer_callback(
) -> rusqlite::hooks::Authorization { ) -> rusqlite::hooks::Authorization {
use rusqlite::hooks::{AuthAction, Authorization}; use rusqlite::hooks::{AuthAction, Authorization};
// [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this.
match auth_context.action { match auth_context.action {
AuthAction::Delete { AuthAction::Delete {
table_name: "error_queue" | "queue" | "candidate_membership" | "membership", 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<Self> { pub fn open_db(conf: Configuration) -> Result<Self> {
use rusqlite::config::DbConfig; use rusqlite::config::DbConfig;
use std::sync::Once; use std::sync::Once;
@ -94,12 +102,14 @@ impl Database {
conn.busy_timeout(core::time::Duration::from_millis(500))?; conn.busy_timeout(core::time::Duration::from_millis(500))?;
conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
conn.authorizer(Some(user_authorizer_callback)); conn.authorizer(Some(user_authorizer_callback));
Ok(Database { Ok(Connection {
conf, conf,
connection: conn, connection: conn,
}) })
} }
/// Removes operational limits from this connection. (see [`Connection::untrusted`])
#[must_use]
pub fn trusted(self) -> Self { pub fn trusted(self) -> Self {
self.connection self.connection
.authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>( .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>(
@ -108,21 +118,30 @@ impl Database {
self 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 { pub fn untrusted(self) -> Self {
self.connection.authorizer(Some(user_authorizer_callback)); self.connection.authorizer(Some(user_authorizer_callback));
self self
} }
/// Create a database if it doesn't exist and then open it.
pub fn open_or_create_db(conf: Configuration) -> Result<Self> { pub fn open_or_create_db(conf: Configuration) -> Result<Self> {
if !conf.db_path.exists() { if !conf.db_path.exists() {
let db_path = &conf.db_path; let db_path = &conf.db_path;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
info!("Creating database in {}", db_path.display()); 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") let mut child = Command::new("sqlite3")
.arg(&db_path) .arg(db_path)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(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()); 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 metadata = file.metadata()?;
let mut permissions = metadata.permissions(); let mut permissions = metadata.permissions();
@ -149,10 +168,12 @@ impl Database {
Self::open_db(conf) Self::open_db(conf)
} }
/// Returns a connection's configuration.
pub fn conf(&self) -> &Configuration { pub fn conf(&self) -> &Configuration {
&self.conf &self.conf
} }
/// Loads archive databases from [`Configuration::data_path`], if any.
pub fn load_archives(&self) -> Result<()> { pub fn load_archives(&self) -> Result<()> {
let mut stmt = self.connection.prepare("ATTACH ? AS ?;")?; let mut stmt = self.connection.prepare("ATTACH ? AS ?;")?;
for archive in std::fs::read_dir(&self.conf.data_path)? { for archive in std::fs::read_dir(&self.conf.data_path)? {
@ -171,7 +192,8 @@ impl Database {
Ok(()) Ok(())
} }
pub fn list_lists(&self) -> Result<Vec<DbVal<MailingList>>> { /// Returns a vector of existing mailing lists.
pub fn lists(&self) -> Result<Vec<DbVal<MailingList>>> {
let mut stmt = self.connection.prepare("SELECT * FROM mailing_lists;")?; let mut stmt = self.connection.prepare("SELECT * FROM mailing_lists;")?;
let list_iter = stmt.query_map([], |row| { let list_iter = stmt.query_map([], |row| {
let pk = row.get("pk")?; let pk = row.get("pk")?;
@ -196,7 +218,8 @@ impl Database {
Ok(ret) Ok(ret)
} }
pub fn get_list(&self, pk: i64) -> Result<Option<DbVal<MailingList>>> { /// Fetch a mailing list by primary key.
pub fn list(&self, pk: i64) -> Result<DbVal<MailingList>> {
let mut stmt = self let mut stmt = self
.connection .connection
.prepare("SELECT * FROM mailing_lists WHERE pk = ?;")?; .prepare("SELECT * FROM mailing_lists WHERE pk = ?;")?;
@ -216,11 +239,15 @@ impl Database {
)) ))
}) })
.optional()?; .optional()?;
if let Some(ret) = ret {
Ok(ret) Ok(ret)
} else {
Err(Error::from(NotFound("list or list policy not found!")))
}
} }
pub fn get_list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> { /// Fetch a mailing list by id.
pub fn list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> {
let id = id.as_ref(); let id = id.as_ref();
let mut stmt = self let mut stmt = self
.connection .connection
@ -245,6 +272,7 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Create a new list.
pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> { pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
let mut stmt = self let mut stmt = self
.connection .connection
@ -280,7 +308,7 @@ impl Database {
/// Remove an existing list policy. /// Remove an existing list policy.
/// ///
/// ``` /// ```
/// # use mailpot::{models::*, Configuration, Database, SendMail}; /// # use mailpot::{models::*, Configuration, Connection, SendMail};
/// # use tempfile::TempDir; /// # use tempfile::TempDir;
/// ///
/// # let tmp_dir = TempDir::new().unwrap(); /// # let tmp_dir = TempDir::new().unwrap();
@ -288,12 +316,11 @@ impl Database {
/// # let config = Configuration { /// # let config = Configuration {
/// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
/// # db_path: db_path.clone(), /// # db_path: db_path.clone(),
/// # storage: "sqlite3".to_string(),
/// # data_path: tmp_dir.path().to_path_buf(), /// # data_path: tmp_dir.path().to_path_buf(),
/// # }; /// # };
/// ///
/// # fn do_test(config: Configuration) { /// # 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 { /// let list_pk = db.create_list(MailingList {
/// pk: 0, /// pk: 0,
/// name: "foobar chat".into(), /// name: "foobar chat".into(),
@ -317,25 +344,6 @@ impl Database {
/// # } /// # }
/// # do_test(config); /// # 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<()> { pub fn remove_list_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> {
let mut stmt = self let mut stmt = self
.connection .connection
@ -353,6 +361,28 @@ impl Database {
Ok(()) 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<DbVal<PostPolicy>> { pub fn set_list_policy(&self, policy: PostPolicy) -> Result<DbVal<PostPolicy>> {
if !(policy.announce_only if !(policy.announce_only
|| policy.subscriber_only || policy.subscriber_only
@ -415,6 +445,7 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Fetch all posts of a mailing list.
pub fn list_posts( pub fn list_posts(
&self, &self,
list_pk: i64, list_pk: i64,
@ -449,16 +480,8 @@ impl Database {
Ok(ret) Ok(ret)
} }
pub fn update_list(&self, _change_set: MailingListChangeset) -> Result<()> { /// Fetch the post policy of a mailing list.
/* pub fn list_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> {
diesel::update(mailing_lists::table)
.set(&set)
.execute(&self.connection)?;
*/
Ok(())
}
pub fn get_list_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> {
let mut stmt = self let mut stmt = self
.connection .connection
.prepare("SELECT * FROM post_policy WHERE list = ?;")?; .prepare("SELECT * FROM post_policy WHERE list = ?;")?;
@ -483,7 +506,8 @@ impl Database {
Ok(ret) Ok(ret)
} }
pub fn get_list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> { /// Fetch the owners of a mailing list.
pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
let mut stmt = self let mut stmt = self
.connection .connection
.prepare("SELECT * FROM list_owner WHERE list = ?;")?; .prepare("SELECT * FROM list_owner WHERE list = ?;")?;
@ -508,6 +532,7 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Remove an owner of a mailing list.
pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> { pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> {
self.connection self.connection
.query_row( .query_row(
@ -525,6 +550,7 @@ impl Database {
Ok(()) Ok(())
} }
/// Add an owner of a mailing list.
pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> { pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> {
let mut stmt = self.connection.prepare( let mut stmt = self.connection.prepare(
"INSERT OR REPLACE INTO list_owner(list, address, name) VALUES (?, ?, ?) RETURNING *;", "INSERT OR REPLACE INTO list_owner(list, address, name) VALUES (?, ?, ?) RETURNING *;",
@ -567,7 +593,58 @@ impl Database {
Ok(ret) 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, &self,
_list: &DbVal<MailingList>, _list: &DbVal<MailingList>,
) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> { ) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> {

View File

@ -20,7 +20,8 @@
use super::*; use super::*;
use serde_json::{json, Value}; 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<i64> { pub fn insert_to_error_queue(&self, env: &Envelope, raw: &[u8], reason: String) -> Result<i64> {
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 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( let pk = stmt.query_row(
@ -42,6 +43,7 @@ impl Database {
Ok(pk) Ok(pk)
} }
/// Fetch all error queue entries.
pub fn error_queue(&self) -> Result<Vec<DbVal<Value>>> { pub fn error_queue(&self) -> Result<Vec<DbVal<Value>>> {
let mut stmt = self.connection.prepare("SELECT * FROM error_queue;")?; let mut stmt = self.connection.prepare("SELECT * FROM error_queue;")?;
let error_iter = stmt.query_map([], |row| { let error_iter = stmt.query_map([], |row| {
@ -70,6 +72,7 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Delete error queue entries.
pub fn delete_from_error_queue(&mut self, index: Vec<i64>) -> Result<()> { pub fn delete_from_error_queue(&mut self, index: Vec<i64>) -> Result<()> {
let tx = self.connection.transaction()?; let tx = self.connection.transaction()?;

View File

@ -19,7 +19,8 @@
use super::*; use super::*;
impl Database { impl Connection {
/// Fetch all members of a mailing list.
pub fn list_members(&self, pk: i64) -> Result<Vec<DbVal<ListMembership>>> { pub fn list_members(&self, pk: i64) -> Result<Vec<DbVal<ListMembership>>> {
let mut stmt = self let mut stmt = self
.connection .connection
@ -51,6 +52,68 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Fetch mailing list member.
pub fn list_member(&self, list_pk: i64, pk: i64) -> Result<DbVal<ListMembership>> {
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<DbVal<ListMembership>> {
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( pub fn add_member(
&self, &self,
list_pk: i64, list_pk: i64,
@ -96,13 +159,14 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Create membership candidate.
pub fn add_candidate_member(&self, list_pk: i64, mut new_val: ListMembership) -> Result<i64> { pub fn add_candidate_member(&self, list_pk: i64, mut new_val: ListMembership) -> Result<i64> {
new_val.list = list_pk; new_val.list = list_pk;
let mut stmt = self let mut stmt = self
.connection .connection
.prepare("INSERT INTO candidate_membership(list, address, name, accepted) VALUES(?, ?, ?, ?) RETURNING pk;")?; .prepare("INSERT INTO candidate_membership(list, address, name, accepted) VALUES(?, ?, ?, ?) RETURNING pk;")?;
let ret = stmt.query_row( 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::<i64>,],
|row| { |row| {
let pk: i64 = row.get("pk")?; let pk: i64 = row.get("pk")?;
Ok(pk) Ok(pk)
@ -113,6 +177,7 @@ impl Database {
Ok(ret) Ok(ret)
} }
/// Accept membership candidate.
pub fn accept_candidate_member(&mut self, pk: i64) -> Result<DbVal<ListMembership>> { pub fn accept_candidate_member(&mut self, pk: i64) -> Result<DbVal<ListMembership>> {
let tx = self.connection.transaction()?; let tx = self.connection.transaction()?;
let mut stmt = tx let mut stmt = tx
@ -138,7 +203,7 @@ impl Database {
drop(stmt); drop(stmt);
tx.execute( tx.execute(
"UPDATE candidate_membership SET accepted = ? WHERE pk = ?;", "UPDATE candidate_membership SET accepted = ? WHERE pk = ?;",
[&pk], [&ret.pk, &pk],
)?; )?;
tx.commit()?; tx.commit()?;
@ -146,21 +211,83 @@ impl Database {
Ok(ret) Ok(ret)
} }
pub fn remove_member(&self, list_pk: i64, address: &str) -> Result<()> { /// Remove a member by their address.
self.connection.execute( pub fn remove_membership(&self, list_pk: i64, address: &str) -> Result<()> {
"DELETE FROM membership WHERE list_pk = ? AND address = ?;", self.connection
rusqlite::params![&list_pk, &address], .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(()) Ok(())
} }
pub fn update_member(&self, _change_set: ListMembershipChangeset) -> Result<()> { /// Update a mailing list membership.
/* pub fn update_member(&mut self, change_set: ListMembershipChangeset) -> Result<()> {
diesel::update(membership::table) let pk = self
.set(&set) .list_member_by_address(change_set.list, &change_set.address)?
.execute(&self.connection)?; .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(()) Ok(())
} }
} }

View File

@ -18,8 +18,10 @@
*/ */
use super::*; 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<i64> { pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> {
let from_ = env.from(); let from_ = env.from();
let address = if from_.is_empty() { let address = if from_.is_empty() {
@ -65,6 +67,7 @@ impl Database {
Ok(pk) Ok(pk)
} }
/// Process a new mailing list post.
pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
let result = self.inner_post(env, raw, _dry_run); let result = self.inner_post(env, raw, _dry_run);
if let Err(err) = result { if let Err(err) = result {
@ -89,7 +92,7 @@ impl Database {
if env.from().is_empty() { if env.from().is_empty() {
return Err("Envelope From: field is empty!".into()); return Err("Envelope From: field is empty!".into());
} }
let mut lists = self.list_lists()?; let mut lists = self.lists()?;
for t in &tos { for t in &tos {
if let Some((addr, subaddr)) = t.subaddress("+") { if let Some((addr, subaddr)) = t.subaddress("+") {
lists.retain(|list| { lists.retain(|list| {
@ -123,12 +126,13 @@ impl Database {
use crate::mail::{ListContext, Post, PostAction}; use crate::mail::{ListContext, Post, PostAction};
for mut list in lists { for mut list in lists {
trace!("Examining list {}", list.display_name()); 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 memberships = self.list_members(list.pk)?;
let owners = self.list_owners(list.pk)?;
trace!("List members {:#?}", &memberships); trace!("List members {:#?}", &memberships);
let mut list_ctx = ListContext { let mut list_ctx = ListContext {
policy: self.get_list_policy(list.pk)?, policy: self.list_policy(list.pk)?,
list_owners: self.get_list_owners(list.pk)?, list_owners: &owners,
list: &mut list, list: &mut list,
memberships: &memberships, memberships: &memberships,
scheduled_jobs: vec![], scheduled_jobs: vec![],
@ -201,6 +205,7 @@ impl Database {
Ok(()) Ok(())
} }
/// Process a new mailing list request.
pub fn request( pub fn request(
&self, &self,
list: &DbVal<MailingList>, list: &DbVal<MailingList>,
@ -216,7 +221,7 @@ impl Database {
list list
); );
let list_policy = self.get_list_policy(list.pk)?; let list_policy = self.list_policy(list.pk)?;
let approval_needed = list_policy let approval_needed = list_policy
.as_ref() .as_ref()
.map(|p| p.approval_needed) .map(|p| p.approval_needed)
@ -254,7 +259,7 @@ impl Database {
list list
); );
for f in env.from() { 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 //FIXME: send failure notice to f
} else { } else {
//FIXME: send success notice to f //FIXME: send success notice to f
@ -308,6 +313,7 @@ impl Database {
Ok(()) 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<Vec<String>> { pub fn months(&self, list_pk: i64) -> Result<Vec<String>> {
let mut stmt = self.connection.prepare( let mut stmt = self.connection.prepare(
"SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post WHERE list = ?;", "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post WHERE list = ?;",

View File

@ -17,39 +17,44 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//! Errors of this library.
pub use crate::anyhow::Context; pub use crate::anyhow::Context;
pub use error_chain::ChainedError; pub use error_chain::ChainedError;
// Create the Error, ErrorKind, ResultExt, and Result types // Create the Error, ErrorKind, ResultExt, and Result types
error_chain! { error_chain! {
errors { errors {
/// Post rejected.
PostRejected(reason: String) { PostRejected(reason: String) {
description("Post rejected") description("Post rejected")
display("Your post has been rejected: {}", reason) display("Your post has been rejected: {}", reason)
} }
/// An entry was not found in the database.
NotFound(model: &'static str) { NotFound(model: &'static str) {
description("Not found") description("Not found")
display("This {} is not present in the database.", model) display("This {} is not present in the database.", model)
} }
/// A request was invalid.
InvalidRequest(reason: String) { InvalidRequest(reason: String) {
description("List request is invalid") description("List request is invalid")
display("Your list request has been found invalid: {}.", reason) display("Your list request has been found invalid: {}.", reason)
} }
/// An error happened and it was handled internally.
Information(reason: String) { Information(reason: String) {
description("") description("")
display("{}.", reason) display("{}.", reason)
} }
} }
foreign_links { foreign_links {
Logic(anyhow::Error); Logic(anyhow::Error) #[doc="Error returned from an external user initiated operation such as deserialization or I/O."];
Sql(rusqlite::Error); Sql(rusqlite::Error) #[doc="Error returned from sqlite3."];
Io(::std::io::Error); Io(::std::io::Error) #[doc="Error returned from internal I/O operations."];
Xdg(xdg::BaseDirectoriesError); Melib(melib::error::Error) #[doc="Error returned from e-mail protocol operations from `melib` crate."];
Melib(melib::error::Error); SerdeJson(serde_json::Error) #[doc="Error from deserializing JSON values."];
Configuration(toml::de::Error);
SerdeJson(serde_json::Error);
} }
} }

View File

@ -16,28 +16,118 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// `error_chain!` can recurse deeply #![warn(missing_docs)]
#![recursion_limit = "1024"] //! Mailing list manager library.
//#![warn(missing_docs)] //!
//! ```
//! 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 <user@example.com>
//! To: <foo-chat+subscribe@example.com>
//! 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 <user@example.com>
//! To: <foo-chat@example.com>
//! 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] #[macro_use]
extern crate error_chain; extern crate error_chain;
extern crate anyhow; extern crate anyhow;
#[macro_use] #[macro_use]
pub extern crate serde; pub extern crate serde;
pub extern crate log;
pub extern crate melib;
pub extern crate serde_json;
pub use melib; use log::{info, trace};
pub use serde_json;
pub mod config; mod config;
pub mod mail; pub mod mail;
pub mod models; pub mod models;
use models::*; use models::*;
pub mod errors; mod db;
use errors::*; mod errors;
pub mod db;
pub use config::{Configuration, SendMail}; pub use config::{Configuration, SendMail};
pub use db::Database; pub use db::*;
pub use errors::*; 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<String>,
}
#[doc = include_str!("../../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;

View File

@ -17,32 +17,56 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//! Types for processing new posts: [`PostFilter`](message_filters::PostFilter), [`ListContext`],
//! [`MailJob`] and [`PostAction`].
use super::*; use super::*;
use melib::Address; use melib::Address;
pub mod message_filters; pub mod message_filters;
/// Post action returned from a list's [`PostFilter`](message_filters::PostFilter) stack.
#[derive(Debug)] #[derive(Debug)]
pub enum PostAction { pub enum PostAction {
/// Add to `hold` queue.
Hold, Hold,
/// Accept to mailing list.
Accept, Accept,
Reject { reason: String }, /// Reject and send rejection response to submitter.
Defer { reason: String }, 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)] #[derive(Debug)]
pub struct ListContext<'list> { pub struct ListContext<'list> {
/// Which mailing list a post was addressed to.
pub list: &'list MailingList, pub list: &'list MailingList,
pub list_owners: Vec<DbVal<ListOwner>>, /// The mailing list owners.
pub list_owners: &'list [DbVal<ListOwner>],
/// The mailing list memberships.
pub memberships: &'list [DbVal<ListMembership>], pub memberships: &'list [DbVal<ListMembership>],
/// The mailing list post policy.
pub policy: Option<DbVal<PostPolicy>>, pub policy: Option<DbVal<PostPolicy>>,
/// The scheduled jobs added by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack.
pub scheduled_jobs: Vec<MailJob>, pub scheduled_jobs: Vec<MailJob>,
} }
///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 { pub struct Post {
/// `From` address of post.
pub from: Address, pub from: Address,
/// Raw bytes of post.
pub bytes: Vec<u8>, pub bytes: Vec<u8>,
/// `To` addresses of post.
pub to: Vec<Address>, pub to: Vec<Address>,
/// Final action set by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack.
pub action: PostAction, 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)] #[derive(Debug)]
pub enum MailJob { pub enum MailJob {
Send { recipients: Vec<Address> }, /// Send post to recipients.
Relay { recipients: Vec<Address> }, Send {
Error { description: String }, /// The post recipients addresses.
StoreDigest { recipients: Vec<Address> }, recipients: Vec<Address>,
ConfirmSubscription { recipient: Address }, },
ConfirmUnsubscription { recipient: Address }, /// 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<Address>,
},
/// 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<String>),
/// 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<S: AsRef<str>> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest {
type Error = crate::Error;
fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result<Self, Self::Error> {
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())
}
})
}
} }

View File

@ -16,13 +16,35 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#![allow(clippy::result_unit_err)] #![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::*; use super::*;
///Filter that modifies and/or verifies a post candidate. On rejection, return a string /// 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` /// describing the error and optionally set `post.action` to `Reject` or `Defer`
pub trait PostFilter { 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>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'p mut Post, post: &'p mut Post,
@ -30,7 +52,7 @@ pub trait PostFilter {
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>; ) -> 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; pub struct PostRightsCheck;
impl PostFilter for PostRightsCheck { impl PostFilter for PostRightsCheck {
fn feed<'p, 'list>( fn feed<'p, 'list>(
@ -45,7 +67,7 @@ impl PostFilter for PostRightsCheck {
let owner_addresses = ctx let owner_addresses = ctx
.list_owners .list_owners
.iter() .iter()
.map(|lo| lo.into_address()) .map(|lo| lo.address())
.collect::<Vec<Address>>(); .collect::<Vec<Address>>();
trace!("Owner addresses are: {:#?}", &owner_addresses); trace!("Owner addresses are: {:#?}", &owner_addresses);
trace!("Envelope from is: {:?}", &post.from); 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; pub struct FixCRLF;
impl PostFilter for FixCRLF { impl PostFilter for FixCRLF {
fn feed<'p, 'list>( fn feed<'p, 'list>(
@ -99,7 +121,7 @@ impl PostFilter for FixCRLF {
} }
} }
///Add `List-*` headers /// Add `List-*` headers
pub struct AddListHeaders; pub struct AddListHeaders;
impl PostFilter for AddListHeaders { impl PostFilter for AddListHeaders {
fn feed<'p, 'list>( 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; pub struct ArchivedAtLink;
impl PostFilter for ArchivedAtLink { impl PostFilter for ArchivedAtLink {
fn feed<'p, 'list>( 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 /// 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. /// will receive the post in `post.action` field.
pub struct FinalizeRecipients; pub struct FinalizeRecipients;
impl PostFilter for FinalizeRecipients { impl PostFilter for FinalizeRecipients {
fn feed<'p, 'list>( fn feed<'p, 'list>(
@ -181,13 +203,13 @@ impl PostFilter for FinalizeRecipients {
if member.digest { if member.digest {
if member.address != email_from || member.receive_own_posts { if member.address != email_from || member.receive_own_posts {
trace!("Member gets digest"); trace!("Member gets digest");
digests.push(member.into_address()); digests.push(member.address());
} }
continue; continue;
} }
if member.address != email_from || member.receive_own_posts { if member.address != email_from || member.receive_own_posts {
trace!("Member gets copy"); trace!("Member gets copy");
recipients.push(member.into_address()); recipients.push(member.address());
} }
// TODO: // TODO:
// - check for duplicates (To,Cc,Bcc) // - check for duplicates (To,Cc,Bcc)

View File

@ -17,16 +17,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//! Database models: [`MailingList`], [`ListOwner`], [`ListMembership`], [`PostPolicy`] and
//! [`Post`].
use super::*; use super::*;
pub mod changesets; pub mod changesets;
use melib::email::Address; use melib::email::Address;
/// A database entry and its primary key. Derefs to its inner type.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct DbVal<T>(pub T, #[serde(skip)] pub i64); pub struct DbVal<T>(pub T, #[serde(skip)] pub i64);
impl<T> DbVal<T> { impl<T> DbVal<T> {
/// Primary key.
#[inline(always)] #[inline(always)]
pub fn pk(&self) -> i64 { pub fn pk(&self) -> i64 {
self.1 self.1
@ -49,13 +54,20 @@ where
} }
} }
/// A mailing list entry.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct MailingList { pub struct MailingList {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list name.
pub name: String, pub name: String,
/// Mailing list ID (what appears in the subject tag, e.g. `[mailing-list] New post!`).
pub id: String, pub id: String,
/// Mailing list e-mail address.
pub address: String, pub address: String,
/// Mailing list description.
pub description: Option<String>, pub description: Option<String>,
/// Mailing list archive URL.
pub archive_url: Option<String>, pub archive_url: Option<String>,
} }
@ -78,14 +90,21 @@ impl std::fmt::Display for MailingList {
} }
impl MailingList { impl MailingList {
/// Mailing list display name (e.g. `list name <list_address@example.com>`).
pub fn display_name(&self) -> String { pub fn display_name(&self) -> String {
format!("\"{}\" <{}>", self.name, self.address) format!("\"{}\" <{}>", self.name, self.address)
} }
/// Value of `List-Post` header.
///
/// See RFC2369 Section 3.4: <https://www.rfc-editor.org/rfc/rfc2369#section-3.4>
pub fn post_header(&self) -> Option<String> { pub fn post_header(&self) -> Option<String> {
Some(format!("<mailto:{}>", self.address)) Some(format!("<mailto:{}>", self.address))
} }
/// Value of `List-Unsubscribe` header.
///
/// See RFC2369 Section 3.2: <https://www.rfc-editor.org/rfc/rfc2369#section-3.2>
pub fn unsubscribe_header(&self) -> Option<String> { pub fn unsubscribe_header(&self) -> Option<String> {
let p = self.address.split('@').collect::<Vec<&str>>(); let p = self.address.split('@').collect::<Vec<&str>>();
Some(format!( Some(format!(
@ -94,14 +113,19 @@ impl MailingList {
)) ))
} }
/// Value of `List-Archive` header.
///
/// See RFC2369 Section 3.6: <https://www.rfc-editor.org/rfc/rfc2369#section-3.6>
pub fn archive_header(&self) -> Option<String> { pub fn archive_header(&self) -> Option<String> {
self.archive_url.as_ref().map(|url| format!("<{}>", url)) self.archive_url.as_ref().map(|url| format!("<{}>", url))
} }
/// List address as a [`melib::Address`]
pub fn address(&self) -> Address { pub fn address(&self) -> Address {
Address::new(Some(self.name.clone()), self.address.clone()) Address::new(Some(self.name.clone()), self.address.clone())
} }
/// List unsubscribe action as a [`MailtoAddress`](super::MailtoAddress).
pub fn unsubscribe_mailto(&self) -> Option<MailtoAddress> { pub fn unsubscribe_mailto(&self) -> Option<MailtoAddress> {
let p = self.address.split('@').collect::<Vec<&str>>(); let p = self.address.split('@').collect::<Vec<&str>>();
Some(MailtoAddress { Some(MailtoAddress {
@ -110,6 +134,7 @@ impl MailingList {
}) })
} }
/// List subscribe action as a [`MailtoAddress`](super::MailtoAddress).
pub fn subscribe_mailto(&self) -> Option<MailtoAddress> { pub fn subscribe_mailto(&self) -> Option<MailtoAddress> {
let p = self.address.split('@').collect::<Vec<&str>>(); let p = self.address.split('@').collect::<Vec<&str>>();
Some(MailtoAddress { Some(MailtoAddress {
@ -118,28 +143,36 @@ impl MailingList {
}) })
} }
/// List archive url value.
pub fn archive_url(&self) -> Option<&str> { pub fn archive_url(&self) -> Option<&str> {
self.archive_url.as_deref() self.archive_url.as_deref()
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] /// A mailing list membership entry.
pub struct MailtoAddress {
pub address: String,
pub subject: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ListMembership { pub struct ListMembership {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list foreign key (See [`MailingList`]).
pub list: i64, pub list: i64,
/// Member's e-mail address.
pub address: String, pub address: String,
/// Member's name, optional.
pub name: Option<String>, pub name: Option<String>,
/// Whether member wishes to receive list posts as a periodical digest e-mail.
pub digest: bool, pub digest: bool,
/// Whether member wishes their e-mail address hidden from public view.
pub hide_address: bool, 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, 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, pub receive_own_posts: bool,
/// Whether member wishes to receive a plain confirmation for their own mailing list posts.
pub receive_confirmation: bool, pub receive_confirmation: bool,
/// Whether this membership is enabled.
pub enabled: bool, pub enabled: bool,
} }
@ -148,7 +181,7 @@ impl std::fmt::Display for ListMembership {
write!( write!(
fmt, fmt,
"{} [digest: {}, hide_address: {} {}]", "{} [digest: {}, hide_address: {} {}]",
self.into_address(), self.address(),
self.digest, self.digest,
self.hide_address, self.hide_address,
if self.enabled { if self.enabled {
@ -161,19 +194,33 @@ impl std::fmt::Display for ListMembership {
} }
impl 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()) 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)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PostPolicy { pub struct PostPolicy {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list foreign key (See [`MailingList`]).
pub list: i64, pub list: i64,
/// Whether the policy is announce only (Only list owners can submit posts, and everyone will
/// receive them).
pub announce_only: bool, pub announce_only: bool,
/// Whether the policy is "subscriber only" (Only list subscribers can post).
pub subscriber_only: bool, 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, 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, pub no_subscriptions: bool,
/// Custom policy.
pub custom: bool, pub custom: bool,
} }
@ -183,17 +230,22 @@ impl std::fmt::Display for PostPolicy {
} }
} }
/// A mailing list owner entry.
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ListOwner { pub struct ListOwner {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list foreign key (See [`MailingList`]).
pub list: i64, pub list: i64,
/// Mailing list owner e-mail address.
pub address: String, pub address: String,
/// Mailing list owner name, optional.
pub name: Option<String>, pub name: Option<String>,
} }
impl std::fmt::Display for ListOwner { impl std::fmt::Display for ListOwner {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 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<ListOwner> for ListMembership {
} }
impl ListOwner { 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()) Address::new(self.name.clone(), self.address.clone())
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] /// A mailing list post entry.
pub enum ListRequest {
Subscribe,
Unsubscribe,
RetrieveArchive(String, String),
RetrieveMessages(Vec<String>),
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<S: AsRef<str>> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest {
type Error = crate::Error;
fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result<Self, Self::Error> {
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],
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Post { pub struct Post {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list foreign key (See [`MailingList`]).
pub list: i64, pub list: i64,
/// `From` header address of post.
pub address: String, pub address: String,
/// `Message-ID` header value of post.
pub message_id: String, pub message_id: String,
/// Post as bytes.
pub message: Vec<u8>, pub message: Vec<u8>,
/// Unix timestamp of date.
pub timestamp: u64, pub timestamp: u64,
/// Datetime as string.
pub datetime: String, pub datetime: String,
/// Month-year as a `YYYY-mm` formatted string, for use in archives.
pub month_year: String, pub month_year: String,
} }

View File

@ -17,43 +17,73 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//! Changeset structs: update specific struct fields.
/// Changeset struct for [`Mailinglist`](super::MailingList).
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MailingListChangeset { pub struct MailingListChangeset {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Optional new value.
pub name: Option<String>, pub name: Option<String>,
/// Optional new value.
pub id: Option<String>, pub id: Option<String>,
/// Optional new value.
pub address: Option<String>, pub address: Option<String>,
/// Optional new value.
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
/// Optional new value.
pub archive_url: Option<Option<String>>, pub archive_url: Option<Option<String>>,
} }
/// Changeset struct for [`ListMembership`](super::ListMembership).
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ListMembershipChangeset { pub struct ListMembershipChangeset {
/// Mailing list foreign key (See [`MailingList`](super::MailingList)).
pub list: i64, pub list: i64,
/// Membership e-mail address.
pub address: String, pub address: String,
/// Optional new value.
pub name: Option<Option<String>>, pub name: Option<Option<String>>,
/// Optional new value.
pub digest: Option<bool>, pub digest: Option<bool>,
/// Optional new value.
pub hide_address: Option<bool>, pub hide_address: Option<bool>,
/// Optional new value.
pub receive_duplicates: Option<bool>, pub receive_duplicates: Option<bool>,
/// Optional new value.
pub receive_own_posts: Option<bool>, pub receive_own_posts: Option<bool>,
/// Optional new value.
pub receive_confirmation: Option<bool>, pub receive_confirmation: Option<bool>,
/// Optional new value.
pub enabled: Option<bool>, pub enabled: Option<bool>,
} }
/// Changeset struct for [`PostPolicy`](super::PostPolicy).
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PostPolicyChangeset { pub struct PostPolicyChangeset {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list foreign key (See [`MailingList`](super::MailingList)).
pub list: i64, pub list: i64,
/// Optional new value.
pub announce_only: Option<bool>, pub announce_only: Option<bool>,
/// Optional new value.
pub subscriber_only: Option<bool>, pub subscriber_only: Option<bool>,
/// Optional new value.
pub approval_needed: Option<bool>, pub approval_needed: Option<bool>,
} }
/// Changeset struct for [`ListOwner`](super::ListOwner).
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ListOwnerChangeset { pub struct ListOwnerChangeset {
/// Database primary key.
pub pk: i64, pub pk: i64,
/// Mailing list foreign key (See [`MailingList`](super::MailingList)).
pub list: i64, pub list: i64,
/// Optional new value.
pub address: Option<String>, pub address: Option<String>,
/// Optional new value.
pub name: Option<Option<String>>, pub name: Option<Option<String>>,
} }

View File

@ -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_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, 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 (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 ( CREATE TABLE IF NOT EXISTS account (

View File

@ -47,7 +47,8 @@ CREATE TABLE IF NOT EXISTS membership (
BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(), BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(),
BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(), BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE, 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 ( CREATE TABLE IF NOT EXISTS account (

View File

@ -19,7 +19,7 @@
mod utils; mod utils;
use mailpot::{models::*, Configuration, Database, SendMail}; use mailpot::{models::*, Configuration, Connection, SendMail};
use std::error::Error; use std::error::Error;
use tempfile::TempDir; use tempfile::TempDir;
@ -32,12 +32,11 @@ fn test_authorizer() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), 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());
for err in [ for err in [
db.create_list(MailingList { 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(); let db = db.trusted();

View File

@ -19,7 +19,7 @@
mod utils; mod utils;
use mailpot::{models::*, Configuration, Database, SendMail}; use mailpot::{models::*, Configuration, Connection, SendMail};
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[test]
@ -31,13 +31,12 @@ fn test_init_empty() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), 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] #[test]
@ -49,12 +48,11 @@ fn test_list_creation() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), data_path: tmp_dir.path().to_path_buf(),
}; };
let db = Database::open_or_create_db(config).unwrap().trusted(); let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.list_lists().unwrap().is_empty()); assert!(db.lists().unwrap().is_empty());
let foo_chat = db let foo_chat = db
.create_list(MailingList { .create_list(MailingList {
pk: 0, pk: 0,
@ -67,7 +65,7 @@ fn test_list_creation() {
.unwrap(); .unwrap();
assert_eq!(foo_chat.pk(), 1); 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.len(), 1);
assert_eq!(lists[0], foo_chat); assert_eq!(lists[0], foo_chat);
} }

View File

@ -19,7 +19,7 @@
mod utils; mod utils;
use mailpot::{melib, models::*, Configuration, Database, SendMail}; use mailpot::{melib, models::*, Configuration, Connection, SendMail};
use tempfile::TempDir; use tempfile::TempDir;
fn get_smtp_conf() -> melib::smtp::SmtpServerConf { fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
@ -43,12 +43,11 @@ fn test_error_queue() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::Smtp(get_smtp_conf()), send_mail: SendMail::Smtp(get_smtp_conf()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), data_path: tmp_dir.path().to_path_buf(),
}; };
let db = Database::open_or_create_db(config).unwrap().trusted(); let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.list_lists().unwrap().is_empty()); assert!(db.lists().unwrap().is_empty());
let foo_chat = db let foo_chat = db
.create_list(MailingList { .create_list(MailingList {
pk: 0, pk: 0,

View File

@ -21,7 +21,7 @@ mod utils;
use log::{trace, warn}; use log::{trace, warn};
use mailin_embedded::{Handler, Response, Server, SslConfig}; 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::net::IpAddr; //, Ipv4Addr, Ipv6Addr};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
@ -204,12 +204,11 @@ fn test_smtp() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::Smtp(get_smtp_conf()), send_mail: SendMail::Smtp(get_smtp_conf()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), data_path: tmp_dir.path().to_path_buf(),
}; };
let db = Database::open_or_create_db(config).unwrap().trusted(); let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.list_lists().unwrap().is_empty()); assert!(db.lists().unwrap().is_empty());
let foo_chat = db let foo_chat = db
.create_list(MailingList { .create_list(MailingList {
pk: 0, pk: 0,
@ -323,12 +322,11 @@ fn test_smtp_mailcrab() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::Smtp(get_smtp_conf()), send_mail: SendMail::Smtp(get_smtp_conf()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), data_path: tmp_dir.path().to_path_buf(),
}; };
let db = Database::open_or_create_db(config).unwrap().trusted(); let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.list_lists().unwrap().is_empty()); assert!(db.lists().unwrap().is_empty());
let foo_chat = db let foo_chat = db
.create_list(MailingList { .create_list(MailingList {
pk: 0, pk: 0,

View File

@ -19,7 +19,7 @@
mod utils; mod utils;
use mailpot::{models::*, Configuration, Database, SendMail}; use mailpot::{models::*, Configuration, Connection, SendMail};
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[test]
@ -32,12 +32,11 @@ fn test_list_subscription() {
let config = Configuration { let config = Configuration {
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
db_path: db_path.clone(), db_path: db_path.clone(),
storage: "sqlite3".to_string(),
data_path: tmp_dir.path().to_path_buf(), data_path: tmp_dir.path().to_path_buf(),
}; };
let db = Database::open_or_create_db(config).unwrap().trusted(); let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.list_lists().unwrap().is_empty()); assert!(db.lists().unwrap().is_empty());
let foo_chat = db let foo_chat = db
.create_list(MailingList { .create_list(MailingList {
pk: 0, pk: 0,
@ -50,7 +49,7 @@ fn test_list_subscription() {
.unwrap(); .unwrap();
assert_eq!(foo_chat.pk(), 1); 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.len(), 1);
assert_eq!(lists[0], foo_chat); assert_eq!(lists[0], foo_chat);
let post_policy = db let post_policy = db
@ -67,6 +66,7 @@ fn test_list_subscription() {
assert_eq!(post_policy.pk(), 1); assert_eq!(post_policy.pk(), 1);
assert_eq!(db.error_queue().unwrap().len(), 0); assert_eq!(db.error_queue().unwrap().len(), 0);
assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 0);
let db = db.untrusted(); 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"); melib::Envelope::from_bytes(input_bytes_2, None).expect("Could not parse message");
db.post(&envelope, input_bytes_2, /* dry_run */ false) db.post(&envelope, input_bytes_2, /* dry_run */ false)
.unwrap(); .unwrap();
assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 1);
assert_eq!(db.error_queue().unwrap().len(), 1); assert_eq!(db.error_queue().unwrap().len(), 1);
let envelope = let envelope =
melib::Envelope::from_bytes(input_bytes_1, None).expect("Could not parse message"); melib::Envelope::from_bytes(input_bytes_1, None).expect("Could not parse message");

View File

@ -19,22 +19,11 @@
extern crate mailpot; extern crate mailpot;
pub use mailpot::config::*;
pub use mailpot::db::*;
pub use mailpot::errors::*;
pub use mailpot::models::*; pub use mailpot::models::*;
pub use mailpot::*; pub use mailpot::*;
use warp::Filter; use warp::Filter;
/*
fn json_body() -> impl Filter<Extract = (String,), Error = warp::Rejection> + 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] #[tokio::main]
async fn main() { async fn main() {
let config_path = std::env::args() let config_path = std::env::args()
@ -45,8 +34,8 @@ async fn main() {
let conf1 = conf.clone(); let conf1 = conf.clone();
// GET /lists/:i64/policy // GET /lists/:i64/policy
let policy = warp::path!("lists" / i64 / "policy").map(move |list_pk| { let policy = warp::path!("lists" / i64 / "policy").map(move |list_pk| {
let db = Database::open_db(conf1.clone()).unwrap(); let db = Connection::open_db(conf1.clone()).unwrap();
db.get_list_policy(list_pk) db.list_policy(list_pk)
.ok() .ok()
.map(|l| warp::reply::json(&l.unwrap())) .map(|l| warp::reply::json(&l.unwrap()))
.unwrap() .unwrap()
@ -55,34 +44,33 @@ async fn main() {
let conf2 = conf.clone(); let conf2 = conf.clone();
//get("/lists")] //get("/lists")]
let lists = warp::path!("lists").map(move || { let lists = warp::path!("lists").map(move || {
let db = Database::open_db(conf2.clone()).unwrap(); let db = Connection::open_db(conf2.clone()).unwrap();
let lists = db.list_lists().unwrap(); let lists = db.lists().unwrap();
warp::reply::json(&lists) warp::reply::json(&lists)
}); });
let conf3 = conf.clone(); let conf3 = conf.clone();
//get("/lists/<num>")] //get("/lists/<num>")]
let lists_num = warp::path!("lists" / i64).map(move |list_pk| { let lists_num = warp::path!("lists" / i64).map(move |list_pk| {
let db = Database::open_db(conf3.clone()).unwrap(); let db = Connection::open_db(conf3.clone()).unwrap();
let list = db.get_list(list_pk).unwrap(); let list = db.list(list_pk).unwrap();
warp::reply::json(&list) warp::reply::json(&list)
}); });
let conf4 = conf.clone(); let conf4 = conf.clone();
//get("/lists/<num>/members")] //get("/lists/<num>/members")]
let lists_members = warp::path!("lists" / i64 / "members").map(move |list_pk| { 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) db.list_members(list_pk)
.ok() .ok()
.map(|l| warp::reply::json(&l)) .map(|l| warp::reply::json(&l))
.unwrap() .unwrap()
}); });
let conf5 = conf.clone();
//get("/lists/<num>/owners")] //get("/lists/<num>/owners")]
let lists_owners = warp::path!("lists" / i64 / "owners").map(move |list_pk| { let lists_owners = warp::path!("lists" / i64 / "owners").map(move |list_pk| {
let db = Database::open_db(conf.clone()).unwrap(); let db = Connection::open_db(conf.clone()).unwrap();
db.get_list_owners(list_pk) db.list_owners(list_pk)
.ok() .ok()
.map(|l| warp::reply::json(&l)) .map(|l| warp::reply::json(&l))
.unwrap() .unwrap()
@ -101,9 +89,5 @@ async fn main() {
.or(lists_owner_add), .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; warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
} }