Document entire `core` create, add CI, tests
parent
46b942b843
commit
e47f1c68dc
|
@ -0,0 +1 @@
|
||||||
|
github: [epilys]
|
|
@ -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
|
|
@ -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
105
README.md
|
@ -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>(())
|
||||||
|
```
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
171
core/src/db.rs
171
core/src/db.rs
|
@ -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>> {
|
||||||
|
|
|
@ -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()?;
|
||||||
|
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = ?;",
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
112
core/src/lib.rs
112
core/src/lib.rs
|
@ -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;
|
||||||
|
|
108
core/src/mail.rs
108
core/src/mail.rs
|
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue