Compare commits

...

1 Commits

Author SHA1 Message Date
Manos Pitsidianakis 3906a08037
Document entire `core` create, add CI, tests 2023-04-03 20:39:27 +03:00
27 changed files with 971 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

103
README.md
View File

@ -1,9 +1,10 @@
# Mailpot - WIP mailing list manager
# mailpot - WIP mailing list manager
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 +13,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 +182,78 @@ TRACE - result Ok(
)
```
</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]]
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"]

View File

@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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

View File

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

View File

@ -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::<i64>()
.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());

View File

@ -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<PathBuf>) -> 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<P: AsRef<Path>>(path: P) -> Result<Self> {
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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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()
}

View File

@ -17,6 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! 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<Self> {
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::<fn(rusqlite::hooks::AuthContext<'_>) -> 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<Self> {
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<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 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<Option<DbVal<MailingList>>> {
/// Fetch a mailing list by primary key.
pub fn list(&self, pk: i64) -> Result<DbVal<MailingList>> {
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<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 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<DbVal<MailingList>> {
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<DbVal<PostPolicy>> {
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<Option<DbVal<PostPolicy>>> {
/// Fetch the post policy of a mailing list.
pub fn list_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> {
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<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
.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<DbVal<ListOwner>> {
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<MailingList>,
) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> {

View File

@ -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<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 pk = stmt.query_row(
@ -42,6 +43,7 @@ impl Database {
Ok(pk)
}
/// Fetch all error queue entries.
pub fn error_queue(&self) -> Result<Vec<DbVal<Value>>> {
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<i64>) -> Result<()> {
let tx = self.connection.transaction()?;

View File

@ -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<Vec<DbVal<ListMembership>>> {
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<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(
&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<i64> {
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::<i64>,],
|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<DbVal<ListMembership>> {
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(())
}
}

View File

@ -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<i64> {
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<MailingList>,
@ -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<Vec<String>> {
let mut stmt = self.connection.prepare(
"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/>.
*/
//! 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."];
}
}

View File

@ -16,28 +16,118 @@
* 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/>.
*/
// `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 <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]
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<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/>.
*/
//! 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<DbVal<ListOwner>>,
/// The mailing list owners.
pub list_owners: &'list [DbVal<ListOwner>],
/// The mailing list memberships.
pub memberships: &'list [DbVal<ListMembership>],
/// The mailing list post policy.
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>,
}
///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<u8>,
/// `To` addresses of post.
pub to: Vec<Address>,
/// 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<Address> },
Relay { recipients: Vec<Address> },
Error { description: String },
StoreDigest { recipients: Vec<Address> },
ConfirmSubscription { recipient: Address },
ConfirmUnsubscription { recipient: Address },
/// Send post to recipients.
Send {
/// The post recipients addresses.
recipients: Vec<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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#![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<Self>,
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::<Vec<Address>>();
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)

View File

@ -17,16 +17,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! 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<T>(pub T, #[serde(skip)] pub i64);
impl<T> DbVal<T> {
/// 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<String>,
/// Mailing list archive URL.
pub archive_url: Option<String>,
}
@ -78,14 +90,21 @@ impl std::fmt::Display for MailingList {
}
impl MailingList {
/// Mailing list display name (e.g. `list name <list_address@example.com>`).
pub fn display_name(&self) -> String {
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> {
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> {
let p = self.address.split('@').collect::<Vec<&str>>();
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> {
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<MailtoAddress> {
let p = self.address.split('@').collect::<Vec<&str>>();
Some(MailtoAddress {
@ -110,6 +134,7 @@ impl MailingList {
})
}
/// List subscribe action as a [`MailtoAddress`](super::MailtoAddress).
pub fn subscribe_mailto(&self) -> Option<MailtoAddress> {
let p = self.address.split('@').collect::<Vec<&str>>();
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<String>,
}
/// 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<String>,
/// 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<String>,
}
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<ListOwner> 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<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],
}
/// 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<u8>,
/// 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,
}

View File

@ -17,43 +17,73 @@
* 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)]
pub struct MailingListChangeset {
/// Database primary key.
pub pk: i64,
/// Optional new value.
pub name: Option<String>,
/// Optional new value.
pub id: Option<String>,
/// Optional new value.
pub address: Option<String>,
/// Optional new value.
pub description: Option<Option<String>>,
/// Optional new value.
pub archive_url: Option<Option<String>>,
}
/// 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<Option<String>>,
/// Optional new value.
pub digest: Option<bool>,
/// Optional new value.
pub hide_address: Option<bool>,
/// Optional new value.
pub receive_duplicates: Option<bool>,
/// Optional new value.
pub receive_own_posts: Option<bool>,
/// Optional new value.
pub receive_confirmation: Option<bool>,
/// Optional new value.
pub enabled: Option<bool>,
}
/// 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<bool>,
/// Optional new value.
pub subscriber_only: Option<bool>,
/// Optional new value.
pub approval_needed: Option<bool>,
}
/// 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<String>,
/// Optional new value.
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_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 (

View File

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

View File

@ -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();

View File

@ -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);
}

View File

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

View File

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

View File

@ -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");

View File

@ -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<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]
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/<num>")]
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/<num>/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/<num>/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;
}