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