Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 3906a080379570cd1f4fb1fe6dce263440bfc562
Timestamp: Mon, 03 Apr 2023 17:36:43 +0000 (1 year ago)

+971 -296 +/-27 browse
Document entire `core` create, add CI, tests
1diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
2new file mode 100644
3index 0000000..4e0d644
4--- /dev/null
5+++ b/.github/FUNDING.yml
6 @@ -0,0 +1 @@
7+ github: [epilys]
8 diff --git a/.github/workflows/builds.yaml b/.github/workflows/builds.yaml
9new file mode 100644
10index 0000000..ab7ed08
11--- /dev/null
12+++ b/.github/workflows/builds.yaml
13 @@ -0,0 +1,75 @@
14+ name: Build release binary
15+
16+ env:
17+ RUST_BACKTRACE: 1
18+ CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
19+
20+ on:
21+ workflow_dispatch:
22+ push:
23+ tags:
24+ - v*
25+
26+ jobs:
27+ build:
28+ name: Build on ${{ matrix.build }}
29+ runs-on: ${{ matrix.os }}
30+ strategy:
31+ fail-fast: false
32+ matrix:
33+ build: [linux-amd64, ]
34+ include:
35+ - build: linux-amd64
36+ os: ubuntu-latest
37+ rust: stable
38+ artifact_name: 'mailpot-linux-amd64'
39+ target: x86_64-unknown-linux-gnu
40+ steps:
41+ - uses: actions/checkout@v2
42+ - id: cache-rustup
43+ name: Cache Rust toolchain
44+ uses: actions/cache@v3
45+ with:
46+ path: ~/.rustup
47+ key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
48+ - if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
49+ name: Install Rust ${{ matrix.rust }}
50+ uses: actions-rs/toolchain@v1
51+ with:
52+ profile: minimal
53+ toolchain: ${{ matrix.rust }}
54+ target: ${{ matrix.target }}
55+ override: true
56+ - name: Configure cargo data directory
57+ # After this point, all cargo registry and crate data is stored in
58+ # $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
59+ # that are needed during the build process. Additionally, this works
60+ # around a bug in the 'cache' action that causes directories outside of
61+ # the workspace dir to be saved/restored incorrectly.
62+ run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
63+ - id: cache-cargo
64+ name: Cache cargo configuration and installations
65+ uses: actions/cache@v3
66+ with:
67+ path: ${{ env.CARGO_HOME }}
68+ key: cargo-${{ matrix.os }}-${{ matrix.rust }}
69+ - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
70+ name: Setup Rust target
71+ run: |
72+ mkdir -p "${{ env.CARGO_HOME }}"
73+ cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
74+ [build]
75+ target = "${{ matrix.target }}"
76+ EOF
77+ - name: Build binary
78+ run: |
79+ cargo build --release --bin mpot --bin mpot-gen -p mailpot-cli -p mpot-archives
80+ mv target/*/release/mailpot target/mailpot || true
81+ mv target/release/mailpot target/mailpot || true
82+ - name: Upload Artifacts
83+ uses: actions/upload-artifact@v2
84+ with:
85+ name: ${{ matrix.artifact_name }}
86+ path: target/mailpot
87+ if-no-files-found: error
88+ retention-days: 7
89 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
90new file mode 100644
91index 0000000..086823d
92--- /dev/null
93+++ b/.github/workflows/test.yaml
94 @@ -0,0 +1,91 @@
95+ name: Tests
96+
97+ env:
98+ RUST_BACKTRACE: 1
99+ CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
100+
101+ on:
102+ workflow_dispatch:
103+ push:
104+ branches:
105+ - '**'
106+ paths:
107+ - 'core/src/**'
108+ - 'core/tests/**'
109+ - 'core/Cargo.toml'
110+ - 'Cargo.lock'
111+
112+ jobs:
113+ test:
114+ name: Test on ${{ matrix.build }}
115+ runs-on: ${{ matrix.os }}
116+ strategy:
117+ fail-fast: false
118+ matrix:
119+ build: [linux-amd64, ]
120+ include:
121+ - build: linux-amd64
122+ os: ubuntu-latest
123+ rust: stable
124+ target: x86_64-unknown-linux-gnu
125+ steps:
126+ - uses: actions/checkout@v2
127+ - id: cache-rustup
128+ name: Cache Rust toolchain
129+ uses: actions/cache@v3
130+ with:
131+ path: ~/.rustup
132+ key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
133+ - if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
134+ name: Install Rust ${{ matrix.rust }}
135+ uses: actions-rs/toolchain@v1
136+ with:
137+ profile: minimal
138+ toolchain: ${{ matrix.rust }}
139+ components: clippy, rustfmt
140+ target: ${{ matrix.target }}
141+ override: true
142+ - name: Configure cargo data directory
143+ # After this point, all cargo registry and crate data is stored in
144+ # $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
145+ # that are needed during the build process. Additionally, this works
146+ # around a bug in the 'cache' action that causes directories outside of
147+ # the workspace dir to be saved/restored incorrectly.
148+ run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
149+ - id: cache-cargo
150+ name: Cache cargo configuration and installations
151+ uses: actions/cache@v3
152+ with:
153+ path: ${{ env.CARGO_HOME }}
154+ key: cargo-${{ matrix.os }}-${{ matrix.rust }}
155+ - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
156+ name: Setup Rust target
157+ run: |
158+ mkdir -p "${{ env.CARGO_HOME }}"
159+ cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
160+ [build]
161+ target = "${{ matrix.target }}"
162+ EOF
163+ - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
164+ name: Add lint dependencies
165+ run: |
166+ cargo install --target "${{ matrix.target }}" cargo-sort
167+ - name: cargo-check
168+ run: |
169+ cargo check --all-features --all --tests --examples --benches --bins
170+ - name: cargo test
171+ 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>
172+ run: |
173+ cargo test --all --no-fail-fast --all-features
174+ - name: cargo-sort
175+ 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>
176+ run: |
177+ cargo sort --check
178+ - name: rustfmt
179+ 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>
180+ run: |
181+ cargo fmt --check --all
182+ - name: clippy
183+ 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>
184+ run: |
185+ cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
186 diff --git a/README.md b/README.md
187index 38b09c8..5e25adc 100644
188--- a/README.md
189+++ b/README.md
190 @@ -1,9 +1,10 @@
191- # Mailpot - WIP mailing list manager
192+ # mailpot - WIP mailing list manager
193
194 Crates:
195
196- - `core`
197+ - `core` the library
198 - `cli` a command line tool to manage lists
199+ - `archive-http` static web archive generation or with a dynamic http server
200 - `rest-http` a REST http server to manage lists
201
202 ## Project goals
203 @@ -12,32 +13,25 @@ Crates:
204 - extensible through Rust API as a library
205 - extensible through HTTP REST API as an HTTP server, with webhooks
206 - basic management through CLI
207- - replaceable lightweight web archiver
208- - custom storage?
209- - useful for both newsletters, discussions
210+ - optional lightweight web archiver
211+ - useful for both newsletters, discussions, article comments
212
213 ## Initial setup
214
215- Check where `mpot` expects your database file to be:
216-
217- ```shell
218- $ cargo run --bin mpot -- db-location
219- Configuration file /home/user/.config/mailpot/config.toml doesn't exist
220- ```
221-
222- Uuugh, oops.
223+ Create a configuration file and a database:
224
225 ```shell
226 $ mkdir -p /home/user/.config/mailpot
227- $ echo 'send_mail = { "type" = "ShellCommand", "value" = "/usr/bin/false" }' > /home/user/.config/mailpot/config.toml
228- $ cargo run --bin mpot -- db-location
229+ $ export MPOT_CONFIG=/home/user/.config/mailpot/config.toml
230+ $ cargo run --bin mpot -- sample-config > "$MPOT_CONFIG"
231+ $ # edit config and set database path e.g. "/home/user/.local/share/mailpot/mpot.db"
232+ $ cargo run --bin mpot -- -c "$MPOT_CONFIG" db-location
233 /home/user/.local/share/mailpot/mpot.db
234 ```
235
236- Now you can initialize the database file:
237+ This creates the database file in the configuration file as if you executed the following:
238
239 ```shell
240- $ mkdir -p /home/user/.local/share/mailpot/
241 $ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql
242 ```
243
244 @@ -188,3 +182,78 @@ TRACE - result Ok(
245 )
246 ```
247 </details>
248+
249+ ## Using `mailpot` as a library
250+
251+ ```rust
252+ use mailpot::{models::*, *};
253+ use tempfile::TempDir;
254+
255+ let tmp_dir = TempDir::new().unwrap();
256+ let db_path = tmp_dir.path().join("mpot.db");
257+ let config = Configuration {
258+ send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
259+ db_path: db_path.clone(),
260+ data_path: tmp_dir.path().to_path_buf(),
261+ };
262+ let db = Connection::open_or_create_db(config)?.trusted();
263+
264+ // Create a new mailing list
265+ let list_pk = db.create_list(MailingList {
266+ pk: 0,
267+ name: "foobar chat".into(),
268+ id: "foo-chat".into(),
269+ address: "foo-chat@example.com".into(),
270+ description: None,
271+ archive_url: None,
272+ })?.pk;
273+
274+ db.set_list_policy(
275+ PostPolicy {
276+ pk: 0,
277+ list: list_pk,
278+ announce_only: false,
279+ subscriber_only: true,
280+ approval_needed: false,
281+ no_subscriptions: false,
282+ custom: false,
283+ },
284+ )?;
285+
286+ // Drop privileges; we can only process new e-mail and modify memberships from now on.
287+ let db = db.untrusted();
288+
289+ assert_eq!(db.list_members(list_pk)?.len(), 0);
290+ assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
291+
292+ // Process a subscription request e-mail
293+ let subscribe_bytes = b"From: Name <user@example.com>
294+ To: <foo-chat+subscribe@example.com>
295+ Subject: subscribe
296+ Date: Thu, 29 Oct 2020 13:58:16 +0000
297+ Message-ID: <1@example.com>
298+
299+ ";
300+ let envelope = melib::Envelope::from_bytes(subscribe_bytes, None)?;
301+ db.post(&envelope, subscribe_bytes, /* dry_run */ false)?;
302+
303+ assert_eq!(db.list_members(list_pk)?.len(), 1);
304+ assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
305+
306+ // Process a post
307+ let post_bytes = b"From: Name <user@example.com>
308+ To: <foo-chat@example.com>
309+ Subject: my first post
310+ Date: Thu, 29 Oct 2020 14:01:09 +0000
311+ Message-ID: <2@example.com>
312+
313+ Hello
314+ ";
315+ let envelope =
316+ melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message");
317+ db.post(&envelope, post_bytes, /* dry_run */ false)?;
318+
319+ assert_eq!(db.list_members(list_pk)?.len(), 1);
320+ assert_eq!(db.list_posts(list_pk, None)?.len(), 1);
321+ # Ok::<(), Error>(())
322+ ```
323 diff --git a/archive-http/Cargo.toml b/archive-http/Cargo.toml
324index 6f10f36..e848e14 100644
325--- a/archive-http/Cargo.toml
326+++ b/archive-http/Cargo.toml
327 @@ -14,18 +14,24 @@ default-run = "mpot-archives"
328 [[bin]]
329 name = "mpot-archives"
330 path = "src/main.rs"
331+ required-features = ["warp"]
332
333 [[bin]]
334 name = "mpot-gen"
335 path = "src/gen.rs"
336
337 [dependencies]
338- chrono = "^0.4"
339+ chrono = { version = "^0.4", optional = true }
340 lazy_static = "*"
341 mailpot = { version = "0.1.0", path = "../core" }
342- minijinja = { version = "0.31.0", features = ["source", ] }
343- percent-encoding = "^2.1"
344+ minijinja = { version = "0.31.0", features = ["source", ], optional = true }
345+ percent-encoding = { version = "^2.1", optional = true }
346 serde = { version = "^1", features = ["derive", ] }
347 serde_json = "^1"
348- tokio = { version = "1", features = ["full"] }
349- warp = "^0.3"
350+ tokio = { version = "1", features = ["full"], optional = true }
351+ warp = { version = "^0.3", optional = true }
352+
353+ [features]
354+ default = ["gen"]
355+ gen = ["dep:chrono", "dep:minijinja"]
356+ warp = ["dep:percent-encoding", "dep:tokio", "dep:warp"]
357 diff --git a/archive-http/src/gen.rs b/archive-http/src/gen.rs
358index 477ac28..c07cd37 100644
359--- a/archive-http/src/gen.rs
360+++ b/archive-http/src/gen.rs
361 @@ -22,9 +22,6 @@ use chrono::Datelike;
362
363 mod cal;
364
365- pub use mailpot::config::*;
366- pub use mailpot::db::*;
367- pub use mailpot::errors::*;
368 pub use mailpot::models::*;
369 pub use mailpot::*;
370
371 @@ -223,8 +220,8 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
372 let conf = Configuration::from_file(config_path)
373 .map_err(|err| format!("Could not load config {config_path}: {err}"))?;
374
375- let db = Database::open_db(conf).map_err(|err| format!("Couldn't open db: {err}"))?;
376- let lists_values = db.list_lists()?;
377+ let db = Connection::open_db(conf).map_err(|err| format!("Couldn't open db: {err}"))?;
378+ let lists_values = db.lists()?;
379 {
380 //index.html
381
382 @@ -276,10 +273,8 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
383 std::fs::create_dir_all(&lists_path)?;
384 lists_path.push("index.html");
385
386- let list = db
387- .get_list(list.pk)?
388- .ok_or_else(|| format!("List with pk {} not found in database", list.pk))?;
389- let post_policy = db.get_list_policy(list.pk)?;
390+ let list = db.list(list.pk)?;
391+ let post_policy = db.list_policy(list.pk)?;
392 let months = db.months(list.pk)?;
393 let posts = db.list_posts(list.pk, None)?;
394 let mut hist = months
395 diff --git a/archive-http/src/main.rs b/archive-http/src/main.rs
396index 5146c52..bc02a21 100644
397--- a/archive-http/src/main.rs
398+++ b/archive-http/src/main.rs
399 @@ -19,9 +19,6 @@
400
401 extern crate mailpot;
402
403- pub use mailpot::config::*;
404- pub use mailpot::db::*;
405- pub use mailpot::errors::*;
406 pub use mailpot::models::*;
407 pub use mailpot::*;
408
409 @@ -47,8 +44,8 @@ async fn main() {
410
411 let conf1 = conf.clone();
412 let list_handler = warp::path!("lists" / i64).map(move |list_pk: i64| {
413- let db = Database::open_db(conf1.clone()).unwrap();
414- let list = db.get_list(list_pk).unwrap().unwrap();
415+ let db = Connection::open_db(conf1.clone()).unwrap();
416+ let list = db.list(list_pk).unwrap();
417 let months = db.months(list_pk).unwrap();
418 let posts = db
419 .list_posts(list_pk, None)
420 @@ -88,8 +85,8 @@ async fn main() {
421 let post_handler =
422 warp::path!("list" / i64 / String).map(move |list_pk: i64, message_id: String| {
423 let message_id = percent_decode_str(&message_id).decode_utf8().unwrap();
424- let db = Database::open_db(conf2.clone()).unwrap();
425- let list = db.get_list(list_pk).unwrap().unwrap();
426+ let db = Connection::open_db(conf2.clone()).unwrap();
427+ let list = db.list(list_pk).unwrap();
428 let posts = db.list_posts(list_pk, None).unwrap();
429 let post = posts
430 .iter()
431 @@ -122,8 +119,8 @@ async fn main() {
432 });
433 let conf3 = conf.clone();
434 let index_handler = warp::path::end().map(move || {
435- let db = Database::open_db(conf3.clone()).unwrap();
436- let lists_values = db.list_lists().unwrap();
437+ let db = Connection::open_db(conf3.clone()).unwrap();
438+ let lists_values = db.lists().unwrap();
439 let lists = lists_values
440 .iter()
441 .map(|list| {
442 diff --git a/cli/src/main.rs b/cli/src/main.rs
443index 744f8b4..c644afb 100644
444--- a/cli/src/main.rs
445+++ b/cli/src/main.rs
446 @@ -21,9 +21,6 @@ extern crate log;
447 extern crate mailpot;
448 extern crate stderrlog;
449
450- pub use mailpot::config::*;
451- pub use mailpot::db::*;
452- pub use mailpot::errors::*;
453 pub use mailpot::mail::*;
454 pub use mailpot::models::changesets::*;
455 pub use mailpot::models::*;
456 @@ -31,14 +28,13 @@ pub use mailpot::*;
457 use std::path::PathBuf;
458 use structopt::StructOpt;
459
460- macro_rules! get_list {
461+ macro_rules! list {
462 ($db:ident, $list_id:expr) => {{
463- $db.get_list_by_id(&$list_id)?.or_else(|| {
464+ $db.list_by_id(&$list_id)?.or_else(|| {
465 $list_id
466 .parse::<i64>()
467 .ok()
468- .map(|pk| $db.get_list(pk).ok())
469- .flatten()
470+ .map(|pk| $db.list(pk).ok())
471 .flatten()
472 })
473 }};
474 @@ -60,7 +56,6 @@ struct Opt {
475
476 /// Set config file
477 #[structopt(short, long, parse(from_os_str))]
478- #[allow(dead_code)]
479 config: PathBuf,
480 #[structopt(flatten)]
481 cmd: Command,
482 @@ -259,11 +254,11 @@ fn run_app(opt: Opt) -> Result<()> {
483 };
484 let config = Configuration::from_file(opt.config.as_path())?;
485 use Command::*;
486- let mut db = Database::open_or_create_db(config)?;
487+ let mut db = Connection::open_or_create_db(config)?;
488 match opt.cmd {
489 SampleConfig => {}
490 DumpDatabase => {
491- let lists = db.list_lists()?;
492+ let lists = db.lists()?;
493 let mut stdout = std::io::stdout();
494 serde_json::to_writer_pretty(&mut stdout, &lists)?;
495 for l in &lists {
496 @@ -271,13 +266,13 @@ fn run_app(opt: Opt) -> Result<()> {
497 }
498 }
499 ListLists => {
500- let lists = db.list_lists()?;
501+ let lists = db.lists()?;
502 if lists.is_empty() {
503 println!("No lists found.");
504 } else {
505 for l in lists {
506 println!("- {} {:?}", l.id, l);
507- let list_owners = db.get_list_owners(l.pk)?;
508+ let list_owners = db.list_owners(l.pk)?;
509 if list_owners.is_empty() {
510 println!("\tList owners: None");
511 } else {
512 @@ -286,7 +281,7 @@ fn run_app(opt: Opt) -> Result<()> {
513 println!("\t- {}", o);
514 }
515 }
516- if let Some(s) = db.get_list_policy(l.pk)? {
517+ if let Some(s) = db.list_policy(l.pk)? {
518 println!("\tList policy: {}", s);
519 } else {
520 println!("\tList policy: None");
521 @@ -296,7 +291,7 @@ fn run_app(opt: Opt) -> Result<()> {
522 }
523 }
524 List { list_id, cmd } => {
525- let list = match get_list!(db, list_id) {
526+ let list = match list!(db, list_id) {
527 Some(v) => v,
528 None => {
529 return Err(format!("No list with id or pk {} was found", list_id).into());
530 @@ -356,12 +351,12 @@ fn run_app(opt: Opt) -> Result<()> {
531 }
532 }
533
534- db.remove_member(list.pk, &address)?;
535+ db.remove_membership(list.pk, &address)?;
536 }
537 Health => {
538 println!("{} health:", list);
539- let list_owners = db.get_list_owners(list.pk)?;
540- let list_policy = db.get_list_policy(list.pk)?;
541+ let list_owners = db.list_owners(list.pk)?;
542+ let list_policy = db.list_policy(list.pk)?;
543 if list_owners.is_empty() {
544 println!("\tList has no owners: you should add at least one.");
545 } else {
546 @@ -377,8 +372,8 @@ fn run_app(opt: Opt) -> Result<()> {
547 }
548 Info => {
549 println!("{} info:", list);
550- let list_owners = db.get_list_owners(list.pk)?;
551- let list_policy = db.get_list_policy(list.pk)?;
552+ let list_owners = db.list_owners(list.pk)?;
553+ let list_policy = db.list_policy(list.pk)?;
554 let members = db.list_members(list.pk)?;
555 if members.is_empty() {
556 println!("No members.");
557 @@ -649,7 +644,7 @@ fn run_app(opt: Opt) -> Result<()> {
558 list_id,
559 mut maildir_path,
560 } => {
561- let list = match get_list!(db, list_id) {
562+ let list = match list!(db, list_id) {
563 Some(v) => v,
564 None => {
565 return Err(format!("No list with id or pk {} was found", list_id).into());
566 diff --git a/core/src/config.rs b/core/src/config.rs
567index f72e8ee..e3cac2a 100644
568--- a/core/src/config.rs
569+++ b/core/src/config.rs
570 @@ -23,28 +23,36 @@ use std::io::{Read, Write};
571 use std::os::unix::fs::PermissionsExt;
572 use std::path::{Path, PathBuf};
573
574+ /// How to send e-mail.
575 #[derive(Debug, Serialize, Deserialize, Clone)]
576 #[serde(tag = "type", content = "value")]
577 pub enum SendMail {
578+ /// A `melib` configuration for talking to an SMTP server.
579 Smtp(melib::smtp::SmtpServerConf),
580+ /// A plain shell command passed to `sh -c` with the e-mail passed in the stdin.
581 ShellCommand(String),
582 }
583
584+ /// The configuration for the mailpot database and the mail server.
585 #[derive(Debug, Serialize, Deserialize, Clone)]
586 pub struct Configuration {
587+ /// How to send e-mail.
588 pub send_mail: SendMail,
589- #[serde(default = "default_storage_fn")]
590- pub storage: String,
591+ /// The location of the sqlite3 file.
592 pub db_path: PathBuf,
593+ /// The directory where data are stored.
594 pub data_path: PathBuf,
595 }
596
597 impl Configuration {
598+ /// Create a new configuration value from a given database path value.
599+ ///
600+ /// If you wish to create a new database with this configuration, use [`Connection::open_or_create_db`](crate::Connection::open_or_create_db).
601+ /// To open an existing database, use [`Database::open_db`](crate::Connection::open_db).
602 pub fn new(db_path: impl Into<PathBuf>) -> Self {
603 let db_path = db_path.into();
604 Configuration {
605 send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
606- storage: "sqlite3".into(),
607 data_path: db_path
608 .parent()
609 .map(Path::to_path_buf)
610 @@ -53,6 +61,7 @@ impl Configuration {
611 }
612 }
613
614+ /// Deserialize configuration from TOML file.
615 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
616 let path = path.as_ref();
617 let mut s = String::new();
618 @@ -66,24 +75,17 @@ impl Configuration {
619 Ok(config)
620 }
621
622+ /// The saved data path.
623 pub fn data_directory(&self) -> &Path {
624 self.data_path.as_path()
625 }
626
627+ /// The sqlite3 database path.
628 pub fn db_path(&self) -> &Path {
629 self.db_path.as_path()
630 }
631
632- pub fn default_path() -> Result<PathBuf> {
633- let mut result =
634- xdg::BaseDirectories::with_prefix("mailpot")?.place_config_file("config.toml")?;
635- if result.starts_with("~") {
636- result = Path::new(&std::env::var("HOME").context("No $HOME set.")?)
637- .join(result.strip_prefix("~").context("Internal error while getting default database path: path starts with ~ but rust couldn't strip_refix(\"~\"")?);
638- }
639- Ok(result)
640- }
641-
642+ /// Save message to a custom path.
643 pub fn save_message_to_path(&self, msg: &str, mut path: PathBuf) -> Result<PathBuf> {
644 if path.is_dir() {
645 let now = Local::now().timestamp();
646 @@ -102,17 +104,15 @@ impl Configuration {
647 Ok(path)
648 }
649
650+ /// Save message to the data directory.
651 pub fn save_message(&self, msg: String) -> Result<PathBuf> {
652 self.save_message_to_path(&msg, self.data_directory().to_path_buf())
653 }
654
655+ /// Serialize configuration to a TOML string.
656 pub fn to_toml(&self) -> String {
657 toml::Value::try_from(self)
658 .expect("Could not serialize config to TOML")
659 .to_string()
660 }
661 }
662-
663- fn default_storage_fn() -> String {
664- "sqlite3".to_string()
665- }
666 diff --git a/core/src/db.rs b/core/src/db.rs
667index 9a6f47f..388d744 100644
668--- a/core/src/db.rs
669+++ b/core/src/db.rs
670 @@ -17,6 +17,8 @@
671 * along with this program. If not, see <https://www.gnu.org/licenses/>.
672 */
673
674+ //! Mailpot database and methods.
675+
676 use super::Configuration;
677 use super::*;
678 use crate::ErrorKind::*;
679 @@ -28,9 +30,9 @@ use std::convert::TryFrom;
680 use std::io::Write;
681 use std::process::{Command, Stdio};
682
683- const DB_NAME: &str = "current.db";
684-
685- pub struct Database {
686+ /// A connection to a `mailpot` database.
687+ pub struct Connection {
688+ /// The `rusqlite` connection handle.
689 pub connection: DbConnection,
690 conf: Configuration,
691 }
692 @@ -55,6 +57,7 @@ fn user_authorizer_callback(
693 ) -> rusqlite::hooks::Authorization {
694 use rusqlite::hooks::{AuthAction, Authorization};
695
696+ // [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this.
697 match auth_context.action {
698 AuthAction::Delete {
699 table_name: "error_queue" | "queue" | "candidate_membership" | "membership",
700 @@ -73,7 +76,12 @@ fn user_authorizer_callback(
701 }
702 }
703
704- impl Database {
705+ impl Connection {
706+ /// Creates a new database connection.
707+ ///
708+ /// `Connection` supports a limited subset of operations by default (see
709+ /// [`Connection::untrusted`]).
710+ /// Use [`Connection::trusted`] to remove these limits.
711 pub fn open_db(conf: Configuration) -> Result<Self> {
712 use rusqlite::config::DbConfig;
713 use std::sync::Once;
714 @@ -94,12 +102,14 @@ impl Database {
715 conn.busy_timeout(core::time::Duration::from_millis(500))?;
716 conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
717 conn.authorizer(Some(user_authorizer_callback));
718- Ok(Database {
719+ Ok(Connection {
720 conf,
721 connection: conn,
722 })
723 }
724
725+ /// Removes operational limits from this connection. (see [`Connection::untrusted`])
726+ #[must_use]
727 pub fn trusted(self) -> Self {
728 self.connection
729 .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>(
730 @@ -108,21 +118,30 @@ impl Database {
731 self
732 }
733
734+ // [tag:sync_auth_doc]
735+ /// Sets operational limits for this connection.
736+ ///
737+ /// - Allow `INSERT`, `DELETE` only for "error_queue", "queue", "candidate_membership", "membership".
738+ /// - Allow `INSERT` only for "post".
739+ /// - Allow read access to all tables.
740+ /// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime` function.
741+ /// - Deny everything else.
742 pub fn untrusted(self) -> Self {
743 self.connection.authorizer(Some(user_authorizer_callback));
744 self
745 }
746
747+ /// Create a database if it doesn't exist and then open it.
748 pub fn open_or_create_db(conf: Configuration) -> Result<Self> {
749 if !conf.db_path.exists() {
750 let db_path = &conf.db_path;
751 use std::os::unix::fs::PermissionsExt;
752
753 info!("Creating database in {}", db_path.display());
754- std::fs::File::create(&db_path).context("Could not create db path")?;
755+ std::fs::File::create(db_path).context("Could not create db path")?;
756
757 let mut child = Command::new("sqlite3")
758- .arg(&db_path)
759+ .arg(db_path)
760 .stdin(Stdio::piped())
761 .stdout(Stdio::piped())
762 .stderr(Stdio::piped())
763 @@ -139,7 +158,7 @@ impl Database {
764 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());
765 }
766
767- let file = std::fs::File::open(&db_path)?;
768+ let file = std::fs::File::open(db_path)?;
769 let metadata = file.metadata()?;
770 let mut permissions = metadata.permissions();
771
772 @@ -149,10 +168,12 @@ impl Database {
773 Self::open_db(conf)
774 }
775
776+ /// Returns a connection's configuration.
777 pub fn conf(&self) -> &Configuration {
778 &self.conf
779 }
780
781+ /// Loads archive databases from [`Configuration::data_path`], if any.
782 pub fn load_archives(&self) -> Result<()> {
783 let mut stmt = self.connection.prepare("ATTACH ? AS ?;")?;
784 for archive in std::fs::read_dir(&self.conf.data_path)? {
785 @@ -171,7 +192,8 @@ impl Database {
786 Ok(())
787 }
788
789- pub fn list_lists(&self) -> Result<Vec<DbVal<MailingList>>> {
790+ /// Returns a vector of existing mailing lists.
791+ pub fn lists(&self) -> Result<Vec<DbVal<MailingList>>> {
792 let mut stmt = self.connection.prepare("SELECT * FROM mailing_lists;")?;
793 let list_iter = stmt.query_map([], |row| {
794 let pk = row.get("pk")?;
795 @@ -196,7 +218,8 @@ impl Database {
796 Ok(ret)
797 }
798
799- pub fn get_list(&self, pk: i64) -> Result<Option<DbVal<MailingList>>> {
800+ /// Fetch a mailing list by primary key.
801+ pub fn list(&self, pk: i64) -> Result<DbVal<MailingList>> {
802 let mut stmt = self
803 .connection
804 .prepare("SELECT * FROM mailing_lists WHERE pk = ?;")?;
805 @@ -216,11 +239,15 @@ impl Database {
806 ))
807 })
808 .optional()?;
809-
810- Ok(ret)
811+ if let Some(ret) = ret {
812+ Ok(ret)
813+ } else {
814+ Err(Error::from(NotFound("list or list policy not found!")))
815+ }
816 }
817
818- pub fn get_list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> {
819+ /// Fetch a mailing list by id.
820+ pub fn list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> {
821 let id = id.as_ref();
822 let mut stmt = self
823 .connection
824 @@ -245,6 +272,7 @@ impl Database {
825 Ok(ret)
826 }
827
828+ /// Create a new list.
829 pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
830 let mut stmt = self
831 .connection
832 @@ -280,7 +308,7 @@ impl Database {
833 /// Remove an existing list policy.
834 ///
835 /// ```
836- /// # use mailpot::{models::*, Configuration, Database, SendMail};
837+ /// # use mailpot::{models::*, Configuration, Connection, SendMail};
838 /// # use tempfile::TempDir;
839 ///
840 /// # let tmp_dir = TempDir::new().unwrap();
841 @@ -288,12 +316,11 @@ impl Database {
842 /// # let config = Configuration {
843 /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
844 /// # db_path: db_path.clone(),
845- /// # storage: "sqlite3".to_string(),
846 /// # data_path: tmp_dir.path().to_path_buf(),
847 /// # };
848 ///
849 /// # fn do_test(config: Configuration) {
850- /// let db = Database::open_or_create_db(config).unwrap().trusted();
851+ /// let db = Connection::open_or_create_db(config).unwrap().trusted();
852 /// let list_pk = db.create_list(MailingList {
853 /// pk: 0,
854 /// name: "foobar chat".into(),
855 @@ -317,25 +344,6 @@ impl Database {
856 /// # }
857 /// # do_test(config);
858 /// ```
859- /// ```should_panic
860- /// # use mailpot::{models::*, Configuration, Database, SendMail};
861- /// # use tempfile::TempDir;
862- ///
863- /// # let tmp_dir = TempDir::new().unwrap();
864- /// # let db_path = tmp_dir.path().join("mpot.db");
865- /// # let config = Configuration {
866- /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
867- /// # db_path: db_path.clone(),
868- /// # storage: "sqlite3".to_string(),
869- /// # data_path: tmp_dir.path().to_path_buf(),
870- /// # };
871- ///
872- /// # fn do_test(config: Configuration) {
873- /// let db = Database::open_or_create_db(config).unwrap().trusted();
874- /// db.remove_list_policy(1, 1).unwrap();
875- /// # }
876- /// # do_test(config);
877- /// ```
878 pub fn remove_list_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> {
879 let mut stmt = self
880 .connection
881 @@ -353,6 +361,28 @@ impl Database {
882 Ok(())
883 }
884
885+ /// ```should_panic
886+ /// # use mailpot::{models::*, Configuration, Connection, SendMail};
887+ /// # use tempfile::TempDir;
888+ ///
889+ /// # let tmp_dir = TempDir::new().unwrap();
890+ /// # let db_path = tmp_dir.path().join("mpot.db");
891+ /// # let config = Configuration {
892+ /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
893+ /// # db_path: db_path.clone(),
894+ /// # data_path: tmp_dir.path().to_path_buf(),
895+ /// # };
896+ ///
897+ /// # fn do_test(config: Configuration) {
898+ /// let db = Connection::open_or_create_db(config).unwrap().trusted();
899+ /// db.remove_list_policy(1, 1).unwrap();
900+ /// # }
901+ /// # do_test(config);
902+ /// ```
903+ #[cfg(doc)]
904+ pub fn remove_list_policy_panic() {}
905+
906+ /// Set the unique post policy for a list.
907 pub fn set_list_policy(&self, policy: PostPolicy) -> Result<DbVal<PostPolicy>> {
908 if !(policy.announce_only
909 || policy.subscriber_only
910 @@ -415,6 +445,7 @@ impl Database {
911 Ok(ret)
912 }
913
914+ /// Fetch all posts of a mailing list.
915 pub fn list_posts(
916 &self,
917 list_pk: i64,
918 @@ -449,16 +480,8 @@ impl Database {
919 Ok(ret)
920 }
921
922- pub fn update_list(&self, _change_set: MailingListChangeset) -> Result<()> {
923- /*
924- diesel::update(mailing_lists::table)
925- .set(&set)
926- .execute(&self.connection)?;
927- */
928- Ok(())
929- }
930-
931- pub fn get_list_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> {
932+ /// Fetch the post policy of a mailing list.
933+ pub fn list_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> {
934 let mut stmt = self
935 .connection
936 .prepare("SELECT * FROM post_policy WHERE list = ?;")?;
937 @@ -483,7 +506,8 @@ impl Database {
938 Ok(ret)
939 }
940
941- pub fn get_list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
942+ /// Fetch the owners of a mailing list.
943+ pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
944 let mut stmt = self
945 .connection
946 .prepare("SELECT * FROM list_owner WHERE list = ?;")?;
947 @@ -508,6 +532,7 @@ impl Database {
948 Ok(ret)
949 }
950
951+ /// Remove an owner of a mailing list.
952 pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> {
953 self.connection
954 .query_row(
955 @@ -525,6 +550,7 @@ impl Database {
956 Ok(())
957 }
958
959+ /// Add an owner of a mailing list.
960 pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> {
961 let mut stmt = self.connection.prepare(
962 "INSERT OR REPLACE INTO list_owner(list, address, name) VALUES (?, ?, ?) RETURNING *;",
963 @@ -567,7 +593,58 @@ impl Database {
964 Ok(ret)
965 }
966
967- pub fn get_list_filters(
968+ /// Update a mailing list.
969+ pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> {
970+ if matches!(
971+ change_set,
972+ MailingListChangeset {
973+ pk: _,
974+ name: None,
975+ id: None,
976+ address: None,
977+ description: None,
978+ archive_url: None
979+ }
980+ ) {
981+ return self.list(change_set.pk).map(|_| ());
982+ }
983+
984+ let MailingListChangeset {
985+ pk,
986+ name,
987+ id,
988+ address,
989+ description,
990+ archive_url,
991+ } = change_set;
992+ let tx = self.connection.transaction()?;
993+
994+ macro_rules! update {
995+ ($field:tt) => {{
996+ if let Some($field) = $field {
997+ tx.execute(
998+ concat!(
999+ "UPDATE mailing_lists SET ",
1000+ stringify!($field),
1001+ " = ? WHERE pk = ?;"
1002+ ),
1003+ rusqlite::params![&$field, &pk],
1004+ )?;
1005+ }
1006+ }};
1007+ }
1008+ update!(name);
1009+ update!(id);
1010+ update!(address);
1011+ update!(description);
1012+ update!(archive_url);
1013+
1014+ tx.commit()?;
1015+ Ok(())
1016+ }
1017+
1018+ /// Return the post filters of a mailing list.
1019+ pub fn list_filters(
1020 &self,
1021 _list: &DbVal<MailingList>,
1022 ) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> {
1023 diff --git a/core/src/db/error_queue.rs b/core/src/db/error_queue.rs
1024index c712547..3dd7144 100644
1025--- a/core/src/db/error_queue.rs
1026+++ b/core/src/db/error_queue.rs
1027 @@ -20,7 +20,8 @@
1028 use super::*;
1029 use serde_json::{json, Value};
1030
1031- impl Database {
1032+ impl Connection {
1033+ /// Insert a received email into the error queue.
1034 pub fn insert_to_error_queue(&self, env: &Envelope, raw: &[u8], reason: String) -> Result<i64> {
1035 let mut stmt = self.connection.prepare("INSERT INTO error_queue(error, to_address, from_address, subject, message_id, message, timestamp, datetime) VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING pk;")?;
1036 let pk = stmt.query_row(
1037 @@ -42,6 +43,7 @@ impl Database {
1038 Ok(pk)
1039 }
1040
1041+ /// Fetch all error queue entries.
1042 pub fn error_queue(&self) -> Result<Vec<DbVal<Value>>> {
1043 let mut stmt = self.connection.prepare("SELECT * FROM error_queue;")?;
1044 let error_iter = stmt.query_map([], |row| {
1045 @@ -70,6 +72,7 @@ impl Database {
1046 Ok(ret)
1047 }
1048
1049+ /// Delete error queue entries.
1050 pub fn delete_from_error_queue(&mut self, index: Vec<i64>) -> Result<()> {
1051 let tx = self.connection.transaction()?;
1052
1053 diff --git a/core/src/db/members.rs b/core/src/db/members.rs
1054index 233b668..d64f469 100644
1055--- a/core/src/db/members.rs
1056+++ b/core/src/db/members.rs
1057 @@ -19,7 +19,8 @@
1058
1059 use super::*;
1060
1061- impl Database {
1062+ impl Connection {
1063+ /// Fetch all members of a mailing list.
1064 pub fn list_members(&self, pk: i64) -> Result<Vec<DbVal<ListMembership>>> {
1065 let mut stmt = self
1066 .connection
1067 @@ -51,6 +52,68 @@ impl Database {
1068 Ok(ret)
1069 }
1070
1071+ /// Fetch mailing list member.
1072+ pub fn list_member(&self, list_pk: i64, pk: i64) -> Result<DbVal<ListMembership>> {
1073+ let mut stmt = self
1074+ .connection
1075+ .prepare("SELECT * FROM membership WHERE list = ? AND pk = ?;")?;
1076+
1077+ let ret = stmt.query_row([&list_pk, &pk], |row| {
1078+ let _pk: i64 = row.get("pk")?;
1079+ debug_assert_eq!(pk, _pk);
1080+ Ok(DbVal(
1081+ ListMembership {
1082+ pk,
1083+ list: row.get("list")?,
1084+ address: row.get("address")?,
1085+ name: row.get("name")?,
1086+ digest: row.get("digest")?,
1087+ hide_address: row.get("hide_address")?,
1088+ receive_duplicates: row.get("receive_duplicates")?,
1089+ receive_own_posts: row.get("receive_own_posts")?,
1090+ receive_confirmation: row.get("receive_confirmation")?,
1091+ enabled: row.get("enabled")?,
1092+ },
1093+ pk,
1094+ ))
1095+ })?;
1096+ Ok(ret)
1097+ }
1098+
1099+ /// Fetch mailing list member by their address.
1100+ pub fn list_member_by_address(
1101+ &self,
1102+ list_pk: i64,
1103+ address: &str,
1104+ ) -> Result<DbVal<ListMembership>> {
1105+ let mut stmt = self
1106+ .connection
1107+ .prepare("SELECT * FROM membership WHERE list = ? AND address = ?;")?;
1108+
1109+ let ret = stmt.query_row(rusqlite::params![&list_pk, &address], |row| {
1110+ let pk = row.get("pk")?;
1111+ let address_ = row.get("address")?;
1112+ debug_assert_eq!(address, &address_);
1113+ Ok(DbVal(
1114+ ListMembership {
1115+ pk,
1116+ list: row.get("list")?,
1117+ address: address_,
1118+ name: row.get("name")?,
1119+ digest: row.get("digest")?,
1120+ hide_address: row.get("hide_address")?,
1121+ receive_duplicates: row.get("receive_duplicates")?,
1122+ receive_own_posts: row.get("receive_own_posts")?,
1123+ receive_confirmation: row.get("receive_confirmation")?,
1124+ enabled: row.get("enabled")?,
1125+ },
1126+ pk,
1127+ ))
1128+ })?;
1129+ Ok(ret)
1130+ }
1131+
1132+ /// Add member to mailing list.
1133 pub fn add_member(
1134 &self,
1135 list_pk: i64,
1136 @@ -96,13 +159,14 @@ impl Database {
1137 Ok(ret)
1138 }
1139
1140+ /// Create membership candidate.
1141 pub fn add_candidate_member(&self, list_pk: i64, mut new_val: ListMembership) -> Result<i64> {
1142 new_val.list = list_pk;
1143 let mut stmt = self
1144 .connection
1145 .prepare("INSERT INTO candidate_membership(list, address, name, accepted) VALUES(?, ?, ?, ?) RETURNING pk;")?;
1146 let ret = stmt.query_row(
1147- rusqlite::params![&new_val.list, &new_val.address, &new_val.name, &false,],
1148+ rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,],
1149 |row| {
1150 let pk: i64 = row.get("pk")?;
1151 Ok(pk)
1152 @@ -113,6 +177,7 @@ impl Database {
1153 Ok(ret)
1154 }
1155
1156+ /// Accept membership candidate.
1157 pub fn accept_candidate_member(&mut self, pk: i64) -> Result<DbVal<ListMembership>> {
1158 let tx = self.connection.transaction()?;
1159 let mut stmt = tx
1160 @@ -138,7 +203,7 @@ impl Database {
1161 drop(stmt);
1162 tx.execute(
1163 "UPDATE candidate_membership SET accepted = ? WHERE pk = ?;",
1164- [&pk],
1165+ [&ret.pk, &pk],
1166 )?;
1167 tx.commit()?;
1168
1169 @@ -146,21 +211,83 @@ impl Database {
1170 Ok(ret)
1171 }
1172
1173- pub fn remove_member(&self, list_pk: i64, address: &str) -> Result<()> {
1174- self.connection.execute(
1175- "DELETE FROM membership WHERE list_pk = ? AND address = ?;",
1176- rusqlite::params![&list_pk, &address],
1177- )?;
1178+ /// Remove a member by their address.
1179+ pub fn remove_membership(&self, list_pk: i64, address: &str) -> Result<()> {
1180+ self.connection
1181+ .query_row(
1182+ "DELETE FROM membership WHERE list_pk = ? AND address = ? RETURNING *;",
1183+ rusqlite::params![&list_pk, &address],
1184+ |_| Ok(()),
1185+ )
1186+ .map_err(|err| {
1187+ if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
1188+ Error::from(err).chain_err(|| NotFound("list or list owner not found!"))
1189+ } else {
1190+ err.into()
1191+ }
1192+ })?;
1193
1194 Ok(())
1195 }
1196
1197- pub fn update_member(&self, _change_set: ListMembershipChangeset) -> Result<()> {
1198- /*
1199- diesel::update(membership::table)
1200- .set(&set)
1201- .execute(&self.connection)?;
1202- */
1203+ /// Update a mailing list membership.
1204+ pub fn update_member(&mut self, change_set: ListMembershipChangeset) -> Result<()> {
1205+ let pk = self
1206+ .list_member_by_address(change_set.list, &change_set.address)?
1207+ .pk;
1208+ if matches!(
1209+ change_set,
1210+ ListMembershipChangeset {
1211+ list: _,
1212+ address: _,
1213+ name: None,
1214+ digest: None,
1215+ hide_address: None,
1216+ receive_duplicates: None,
1217+ receive_own_posts: None,
1218+ receive_confirmation: None,
1219+ enabled: None,
1220+ }
1221+ ) {
1222+ return Ok(());
1223+ }
1224+
1225+ let ListMembershipChangeset {
1226+ list,
1227+ address: _,
1228+ name,
1229+ digest,
1230+ hide_address,
1231+ receive_duplicates,
1232+ receive_own_posts,
1233+ receive_confirmation,
1234+ enabled,
1235+ } = change_set;
1236+ let tx = self.connection.transaction()?;
1237+
1238+ macro_rules! update {
1239+ ($field:tt) => {{
1240+ if let Some($field) = $field {
1241+ tx.execute(
1242+ concat!(
1243+ "UPDATE membership SET ",
1244+ stringify!($field),
1245+ " = ? WHERE list = ? AND pk = ?;"
1246+ ),
1247+ rusqlite::params![&$field, &list, &pk],
1248+ )?;
1249+ }
1250+ }};
1251+ }
1252+ update!(name);
1253+ update!(digest);
1254+ update!(hide_address);
1255+ update!(receive_duplicates);
1256+ update!(receive_own_posts);
1257+ update!(receive_confirmation);
1258+ update!(enabled);
1259+
1260+ tx.commit()?;
1261 Ok(())
1262 }
1263 }
1264 diff --git a/core/src/db/posts.rs b/core/src/db/posts.rs
1265index a5cd9dc..41e806d 100644
1266--- a/core/src/db/posts.rs
1267+++ b/core/src/db/posts.rs
1268 @@ -18,8 +18,10 @@
1269 */
1270
1271 use super::*;
1272+ use crate::mail::ListRequest;
1273
1274- impl Database {
1275+ impl Connection {
1276+ /// Insert a mailing list post into the database.
1277 pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> {
1278 let from_ = env.from();
1279 let address = if from_.is_empty() {
1280 @@ -65,6 +67,7 @@ impl Database {
1281 Ok(pk)
1282 }
1283
1284+ /// Process a new mailing list post.
1285 pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
1286 let result = self.inner_post(env, raw, _dry_run);
1287 if let Err(err) = result {
1288 @@ -89,7 +92,7 @@ impl Database {
1289 if env.from().is_empty() {
1290 return Err("Envelope From: field is empty!".into());
1291 }
1292- let mut lists = self.list_lists()?;
1293+ let mut lists = self.lists()?;
1294 for t in &tos {
1295 if let Some((addr, subaddr)) = t.subaddress("+") {
1296 lists.retain(|list| {
1297 @@ -123,12 +126,13 @@ impl Database {
1298 use crate::mail::{ListContext, Post, PostAction};
1299 for mut list in lists {
1300 trace!("Examining list {}", list.display_name());
1301- let filters = self.get_list_filters(&list);
1302+ let filters = self.list_filters(&list);
1303 let memberships = self.list_members(list.pk)?;
1304+ let owners = self.list_owners(list.pk)?;
1305 trace!("List members {:#?}", &memberships);
1306 let mut list_ctx = ListContext {
1307- policy: self.get_list_policy(list.pk)?,
1308- list_owners: self.get_list_owners(list.pk)?,
1309+ policy: self.list_policy(list.pk)?,
1310+ list_owners: &owners,
1311 list: &mut list,
1312 memberships: &memberships,
1313 scheduled_jobs: vec![],
1314 @@ -201,6 +205,7 @@ impl Database {
1315 Ok(())
1316 }
1317
1318+ /// Process a new mailing list request.
1319 pub fn request(
1320 &self,
1321 list: &DbVal<MailingList>,
1322 @@ -216,7 +221,7 @@ impl Database {
1323 list
1324 );
1325
1326- let list_policy = self.get_list_policy(list.pk)?;
1327+ let list_policy = self.list_policy(list.pk)?;
1328 let approval_needed = list_policy
1329 .as_ref()
1330 .map(|p| p.approval_needed)
1331 @@ -254,7 +259,7 @@ impl Database {
1332 list
1333 );
1334 for f in env.from() {
1335- if let Err(_err) = self.remove_member(list.pk, &f.get_email()) {
1336+ if let Err(_err) = self.remove_membership(list.pk, &f.get_email()) {
1337 //FIXME: send failure notice to f
1338 } else {
1339 //FIXME: send success notice to f
1340 @@ -308,6 +313,7 @@ impl Database {
1341 Ok(())
1342 }
1343
1344+ /// Fetch all year and month values for which at least one post exists in `yyyy-mm` format.
1345 pub fn months(&self, list_pk: i64) -> Result<Vec<String>> {
1346 let mut stmt = self.connection.prepare(
1347 "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post WHERE list = ?;",
1348 diff --git a/core/src/errors.rs b/core/src/errors.rs
1349index 6fb1c23..da0d875 100644
1350--- a/core/src/errors.rs
1351+++ b/core/src/errors.rs
1352 @@ -17,39 +17,44 @@
1353 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1354 */
1355
1356+ //! Errors of this library.
1357+
1358 pub use crate::anyhow::Context;
1359 pub use error_chain::ChainedError;
1360
1361 // Create the Error, ErrorKind, ResultExt, and Result types
1362+
1363 error_chain! {
1364 errors {
1365+ /// Post rejected.
1366 PostRejected(reason: String) {
1367 description("Post rejected")
1368 display("Your post has been rejected: {}", reason)
1369 }
1370
1371+ /// An entry was not found in the database.
1372 NotFound(model: &'static str) {
1373 description("Not found")
1374 display("This {} is not present in the database.", model)
1375 }
1376
1377+ /// A request was invalid.
1378 InvalidRequest(reason: String) {
1379 description("List request is invalid")
1380 display("Your list request has been found invalid: {}.", reason)
1381 }
1382
1383+ /// An error happened and it was handled internally.
1384 Information(reason: String) {
1385 description("")
1386 display("{}.", reason)
1387 }
1388 }
1389 foreign_links {
1390- Logic(anyhow::Error);
1391- Sql(rusqlite::Error);
1392- Io(::std::io::Error);
1393- Xdg(xdg::BaseDirectoriesError);
1394- Melib(melib::error::Error);
1395- Configuration(toml::de::Error);
1396- SerdeJson(serde_json::Error);
1397+ Logic(anyhow::Error) #[doc="Error returned from an external user initiated operation such as deserialization or I/O."];
1398+ Sql(rusqlite::Error) #[doc="Error returned from sqlite3."];
1399+ Io(::std::io::Error) #[doc="Error returned from internal I/O operations."];
1400+ Melib(melib::error::Error) #[doc="Error returned from e-mail protocol operations from `melib` crate."];
1401+ SerdeJson(serde_json::Error) #[doc="Error from deserializing JSON values."];
1402 }
1403 }
1404 diff --git a/core/src/lib.rs b/core/src/lib.rs
1405index 4f66369..2be8f8b 100644
1406--- a/core/src/lib.rs
1407+++ b/core/src/lib.rs
1408 @@ -16,28 +16,118 @@
1409 * You should have received a copy of the GNU Affero General Public License
1410 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1411 */
1412- // `error_chain!` can recurse deeply
1413- #![recursion_limit = "1024"]
1414- //#![warn(missing_docs)]
1415+ #![warn(missing_docs)]
1416+ //! Mailing list manager library.
1417+ //!
1418+ //! ```
1419+ //! use mailpot::{models::*, Configuration, Connection, SendMail};
1420+ //! # use tempfile::TempDir;
1421+ //!
1422+ //! # let tmp_dir = TempDir::new().unwrap();
1423+ //! # let db_path = tmp_dir.path().join("mpot.db");
1424+ //! # let config = Configuration {
1425+ //! # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
1426+ //! # db_path: db_path.clone(),
1427+ //! # data_path: tmp_dir.path().to_path_buf(),
1428+ //! # };
1429+ //! #
1430+ //! # fn do_test(config: Configuration) -> mailpot::Result<()> {
1431+ //! let db = Connection::open_or_create_db(config)?.trusted();
1432+ //!
1433+ //! // Create a new mailing list
1434+ //! let list_pk = db.create_list(MailingList {
1435+ //! pk: 0,
1436+ //! name: "foobar chat".into(),
1437+ //! id: "foo-chat".into(),
1438+ //! address: "foo-chat@example.com".into(),
1439+ //! description: None,
1440+ //! archive_url: None,
1441+ //! })?.pk;
1442+ //!
1443+ //! db.set_list_policy(
1444+ //! PostPolicy {
1445+ //! pk: 0,
1446+ //! list: list_pk,
1447+ //! announce_only: false,
1448+ //! subscriber_only: true,
1449+ //! approval_needed: false,
1450+ //! no_subscriptions: false,
1451+ //! custom: false,
1452+ //! },
1453+ //! )?;
1454+ //!
1455+ //! // Drop privileges; we can only process new e-mail and modify memberships from now on.
1456+ //! let db = db.untrusted();
1457+ //!
1458+ //! assert_eq!(db.list_members(list_pk)?.len(), 0);
1459+ //! assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
1460+ //!
1461+ //! // Process a subscription request e-mail
1462+ //! let subscribe_bytes = b"From: Name <user@example.com>
1463+ //! To: <foo-chat+subscribe@example.com>
1464+ //! Subject: subscribe
1465+ //! Date: Thu, 29 Oct 2020 13:58:16 +0000
1466+ //! Message-ID: <1@example.com>
1467+ //!
1468+ //! ";
1469+ //! let envelope = melib::Envelope::from_bytes(subscribe_bytes, None)?;
1470+ //! db.post(&envelope, subscribe_bytes, /* dry_run */ false)?;
1471+ //!
1472+ //! assert_eq!(db.list_members(list_pk)?.len(), 1);
1473+ //! assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
1474+ //!
1475+ //! // Process a post
1476+ //! let post_bytes = b"From: Name <user@example.com>
1477+ //! To: <foo-chat@example.com>
1478+ //! Subject: my first post
1479+ //! Date: Thu, 29 Oct 2020 14:01:09 +0000
1480+ //! Message-ID: <2@example.com>
1481+ //!
1482+ //! Hello
1483+ //! ";
1484+ //! let envelope =
1485+ //! melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message");
1486+ //! db.post(&envelope, post_bytes, /* dry_run */ false)?;
1487+ //!
1488+ //! assert_eq!(db.list_members(list_pk)?.len(), 1);
1489+ //! assert_eq!(db.list_posts(list_pk, None)?.len(), 1);
1490+ //! # Ok(())
1491+ //! # }
1492+ //! # do_test(config);
1493+ //! ```
1494
1495- use log::{info, trace};
1496 #[macro_use]
1497 extern crate error_chain;
1498 extern crate anyhow;
1499+
1500 #[macro_use]
1501 pub extern crate serde;
1502+ pub extern crate log;
1503+ pub extern crate melib;
1504+ pub extern crate serde_json;
1505
1506- pub use melib;
1507- pub use serde_json;
1508+ use log::{info, trace};
1509
1510- pub mod config;
1511+ mod config;
1512 pub mod mail;
1513 pub mod models;
1514 use models::*;
1515- pub mod errors;
1516- use errors::*;
1517- pub mod db;
1518+ mod db;
1519+ mod errors;
1520
1521 pub use config::{Configuration, SendMail};
1522- pub use db::Database;
1523+ pub use db::*;
1524 pub use errors::*;
1525+
1526+ /// A `mailto:` value.
1527+ #[derive(Debug, Clone, Deserialize, Serialize)]
1528+ pub struct MailtoAddress {
1529+ /// E-mail address.
1530+ pub address: String,
1531+ /// Optional subject value.
1532+ pub subject: Option<String>,
1533+ }
1534+
1535+ #[doc = include_str!("../../README.md")]
1536+ #[cfg(doctest)]
1537+ pub struct ReadmeDoctests;
1538 diff --git a/core/src/mail.rs b/core/src/mail.rs
1539index b133586..ca45412 100644
1540--- a/core/src/mail.rs
1541+++ b/core/src/mail.rs
1542 @@ -17,32 +17,56 @@
1543 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1544 */
1545
1546+ //! Types for processing new posts: [`PostFilter`](message_filters::PostFilter), [`ListContext`],
1547+ //! [`MailJob`] and [`PostAction`].
1548+
1549 use super::*;
1550 use melib::Address;
1551 pub mod message_filters;
1552
1553+ /// Post action returned from a list's [`PostFilter`](message_filters::PostFilter) stack.
1554 #[derive(Debug)]
1555 pub enum PostAction {
1556+ /// Add to `hold` queue.
1557 Hold,
1558+ /// Accept to mailing list.
1559 Accept,
1560- Reject { reason: String },
1561- Defer { reason: String },
1562+ /// Reject and send rejection response to submitter.
1563+ Reject {
1564+ /// Human readable reason for rejection.
1565+ reason: String,
1566+ },
1567+ /// Add to `deferred` queue.
1568+ Defer {
1569+ /// Human readable reason for deferring.
1570+ reason: String,
1571+ },
1572 }
1573
1574+ /// List context passed to a list's [`PostFilter`](message_filters::PostFilter) stack.
1575 #[derive(Debug)]
1576 pub struct ListContext<'list> {
1577+ /// Which mailing list a post was addressed to.
1578 pub list: &'list MailingList,
1579- pub list_owners: Vec<DbVal<ListOwner>>,
1580+ /// The mailing list owners.
1581+ pub list_owners: &'list [DbVal<ListOwner>],
1582+ /// The mailing list memberships.
1583 pub memberships: &'list [DbVal<ListMembership>],
1584+ /// The mailing list post policy.
1585 pub policy: Option<DbVal<PostPolicy>>,
1586+ /// The scheduled jobs added by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack.
1587 pub scheduled_jobs: Vec<MailJob>,
1588 }
1589
1590- ///Post to be considered by the list's `PostFilter` stack.
1591+ /// Post to be considered by the list's [`PostFilter`](message_filters::PostFilter) stack.
1592 pub struct Post {
1593+ /// `From` address of post.
1594 pub from: Address,
1595+ /// Raw bytes of post.
1596 pub bytes: Vec<u8>,
1597+ /// `To` addresses of post.
1598 pub to: Vec<Address>,
1599+ /// Final action set by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack.
1600 pub action: PostAction,
1601 }
1602
1603 @@ -57,12 +81,76 @@ impl core::fmt::Debug for Post {
1604 }
1605 }
1606
1607+ /// Scheduled jobs added to a [`ListContext`] by a list's [`PostFilter`](message_filters::PostFilter) stack.
1608 #[derive(Debug)]
1609 pub enum MailJob {
1610- Send { recipients: Vec<Address> },
1611- Relay { recipients: Vec<Address> },
1612- Error { description: String },
1613- StoreDigest { recipients: Vec<Address> },
1614- ConfirmSubscription { recipient: Address },
1615- ConfirmUnsubscription { recipient: Address },
1616+ /// Send post to recipients.
1617+ Send {
1618+ /// The post recipients addresses.
1619+ recipients: Vec<Address>,
1620+ },
1621+ /// Send error to submitter.
1622+ Error {
1623+ /// Human readable description of the error.
1624+ description: String,
1625+ },
1626+ /// Store post in digest for recipients.
1627+ StoreDigest {
1628+ /// The digest recipients addresses.
1629+ recipients: Vec<Address>,
1630+ },
1631+ /// Reply with subscription confirmation to submitter.
1632+ ConfirmSubscription {
1633+ /// The submitter address.
1634+ recipient: Address,
1635+ },
1636+ /// Reply with unsubscription confirmation to submitter.
1637+ ConfirmUnsubscription {
1638+ /// The submitter address.
1639+ recipient: Address,
1640+ },
1641+ }
1642+
1643+ /// Type of mailing list request.
1644+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1645+ pub enum ListRequest {
1646+ /// Request subscription.
1647+ Subscribe,
1648+ /// Request removal of subscription.
1649+ Unsubscribe,
1650+ /// Request reception of list posts from a month-year range, inclusive.
1651+ RetrieveArchive(String, String),
1652+ /// Request reception of specific mailing list posts from `Message-ID` values.
1653+ RetrieveMessages(Vec<String>),
1654+ /// Request change in digest preferences. (See [`ListMembership`])
1655+ SetDigest(bool),
1656+ /// Other type of request.
1657+ Other(String),
1658+ }
1659+
1660+ impl std::fmt::Display for ListRequest {
1661+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
1662+ write!(fmt, "{:?}", self)
1663+ }
1664+ }
1665+
1666+ impl<S: AsRef<str>> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest {
1667+ type Error = crate::Error;
1668+
1669+ fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result<Self, Self::Error> {
1670+ let val = val.as_ref();
1671+ Ok(match val {
1672+ "subscribe" | "request" if env.subject().trim() == "subscribe" => {
1673+ ListRequest::Subscribe
1674+ }
1675+ "unsubscribe" | "request" if env.subject().trim() == "unsubscribe" => {
1676+ ListRequest::Unsubscribe
1677+ }
1678+ "request" => ListRequest::Other(env.subject().trim().to_string()),
1679+ _ => {
1680+ trace!("unknown action = {} for addresses {:?}", val, env.from(),);
1681+ ListRequest::Other(val.trim().to_string())
1682+ }
1683+ })
1684+ }
1685 }
1686 diff --git a/core/src/mail/message_filters.rs b/core/src/mail/message_filters.rs
1687index f0449a7..142cab3 100644
1688--- a/core/src/mail/message_filters.rs
1689+++ b/core/src/mail/message_filters.rs
1690 @@ -16,13 +16,35 @@
1691 * You should have received a copy of the GNU Affero General Public License
1692 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1693 */
1694+
1695 #![allow(clippy::result_unit_err)]
1696
1697+ //! Filters to pass each mailing list post through. Filters are functions that implement the
1698+ //! [`PostFilter`] trait that can:
1699+ //!
1700+ //! - transform post content.
1701+ //! - modify the final [`PostAction`] to take.
1702+ //! - modify the final scheduled jobs to perform. (See [`MailJob`]).
1703+ //!
1704+ //! Filters are executed in sequence like this:
1705+ //!
1706+ //! ```ignore
1707+ //! let result = filters
1708+ //! .into_iter()
1709+ //! .fold(Ok((&mut post, &mut list_ctx)), |p, f| {
1710+ //! p.and_then(|(p, c)| f.feed(p, c))
1711+ //! });
1712+ //! ```
1713+ //!
1714+ //! so the processing stops at the first returned error.
1715+
1716 use super::*;
1717
1718- ///Filter that modifies and/or verifies a post candidate. On rejection, return a string
1719- ///describing the error and optionally set `post.action` to `Reject` or `Defer`
1720+ /// Filter that modifies and/or verifies a post candidate. On rejection, return a string
1721+ /// describing the error and optionally set `post.action` to `Reject` or `Defer`
1722 pub trait PostFilter {
1723+ /// Feed post into the filter. Perform modifications to `post` and / or `ctx`, and return them
1724+ /// with `Result::Ok` unless you want to the processing to stop and return an `Result::Err`.
1725 fn feed<'p, 'list>(
1726 self: Box<Self>,
1727 post: &'p mut Post,
1728 @@ -30,7 +52,7 @@ pub trait PostFilter {
1729 ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>;
1730 }
1731
1732- ///Check that submitter can post to list, for now it accepts everything.
1733+ /// Check that submitter can post to list, for now it accepts everything.
1734 pub struct PostRightsCheck;
1735 impl PostFilter for PostRightsCheck {
1736 fn feed<'p, 'list>(
1737 @@ -45,7 +67,7 @@ impl PostFilter for PostRightsCheck {
1738 let owner_addresses = ctx
1739 .list_owners
1740 .iter()
1741- .map(|lo| lo.into_address())
1742+ .map(|lo| lo.address())
1743 .collect::<Vec<Address>>();
1744 trace!("Owner addresses are: {:#?}", &owner_addresses);
1745 trace!("Envelope from is: {:?}", &post.from);
1746 @@ -79,7 +101,7 @@ impl PostFilter for PostRightsCheck {
1747 }
1748 }
1749
1750- ///Ensure message contains only `\r\n` line terminators, required by SMTP.
1751+ /// Ensure message contains only `\r\n` line terminators, required by SMTP.
1752 pub struct FixCRLF;
1753 impl PostFilter for FixCRLF {
1754 fn feed<'p, 'list>(
1755 @@ -99,7 +121,7 @@ impl PostFilter for FixCRLF {
1756 }
1757 }
1758
1759- ///Add `List-*` headers
1760+ /// Add `List-*` headers
1761 pub struct AddListHeaders;
1762 impl PostFilter for AddListHeaders {
1763 fn feed<'p, 'list>(
1764 @@ -147,7 +169,7 @@ impl PostFilter for AddListHeaders {
1765 }
1766 }
1767
1768- ///Adds `Archived-At` field, if configured.
1769+ /// Adds `Archived-At` field, if configured.
1770 pub struct ArchivedAtLink;
1771 impl PostFilter for ArchivedAtLink {
1772 fn feed<'p, 'list>(
1773 @@ -160,8 +182,8 @@ impl PostFilter for ArchivedAtLink {
1774 }
1775 }
1776
1777- ///Assuming there are no more changes to be done on the post, it finalizes which list members
1778- ///will receive the post in `post.action` field.
1779+ /// Assuming there are no more changes to be done on the post, it finalizes which list members
1780+ /// will receive the post in `post.action` field.
1781 pub struct FinalizeRecipients;
1782 impl PostFilter for FinalizeRecipients {
1783 fn feed<'p, 'list>(
1784 @@ -181,13 +203,13 @@ impl PostFilter for FinalizeRecipients {
1785 if member.digest {
1786 if member.address != email_from || member.receive_own_posts {
1787 trace!("Member gets digest");
1788- digests.push(member.into_address());
1789+ digests.push(member.address());
1790 }
1791 continue;
1792 }
1793 if member.address != email_from || member.receive_own_posts {
1794 trace!("Member gets copy");
1795- recipients.push(member.into_address());
1796+ recipients.push(member.address());
1797 }
1798 // TODO:
1799 // - check for duplicates (To,Cc,Bcc)
1800 diff --git a/core/src/models.rs b/core/src/models.rs
1801index 0ba3890..425ea60 100644
1802--- a/core/src/models.rs
1803+++ b/core/src/models.rs
1804 @@ -17,16 +17,21 @@
1805 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1806 */
1807
1808+ //! Database models: [`MailingList`], [`ListOwner`], [`ListMembership`], [`PostPolicy`] and
1809+ //! [`Post`].
1810+
1811 use super::*;
1812 pub mod changesets;
1813
1814 use melib::email::Address;
1815
1816+ /// A database entry and its primary key. Derefs to its inner type.
1817 #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1818 #[serde(transparent)]
1819 pub struct DbVal<T>(pub T, #[serde(skip)] pub i64);
1820
1821 impl<T> DbVal<T> {
1822+ /// Primary key.
1823 #[inline(always)]
1824 pub fn pk(&self) -> i64 {
1825 self.1
1826 @@ -49,13 +54,20 @@ where
1827 }
1828 }
1829
1830+ /// A mailing list entry.
1831 #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1832 pub struct MailingList {
1833+ /// Database primary key.
1834 pub pk: i64,
1835+ /// Mailing list name.
1836 pub name: String,
1837+ /// Mailing list ID (what appears in the subject tag, e.g. `[mailing-list] New post!`).
1838 pub id: String,
1839+ /// Mailing list e-mail address.
1840 pub address: String,
1841+ /// Mailing list description.
1842 pub description: Option<String>,
1843+ /// Mailing list archive URL.
1844 pub archive_url: Option<String>,
1845 }
1846
1847 @@ -78,14 +90,21 @@ impl std::fmt::Display for MailingList {
1848 }
1849
1850 impl MailingList {
1851+ /// Mailing list display name (e.g. `list name <list_address@example.com>`).
1852 pub fn display_name(&self) -> String {
1853 format!("\"{}\" <{}>", self.name, self.address)
1854 }
1855
1856+ /// Value of `List-Post` header.
1857+ ///
1858+ /// See RFC2369 Section 3.4: <https://www.rfc-editor.org/rfc/rfc2369#section-3.4>
1859 pub fn post_header(&self) -> Option<String> {
1860 Some(format!("<mailto:{}>", self.address))
1861 }
1862
1863+ /// Value of `List-Unsubscribe` header.
1864+ ///
1865+ /// See RFC2369 Section 3.2: <https://www.rfc-editor.org/rfc/rfc2369#section-3.2>
1866 pub fn unsubscribe_header(&self) -> Option<String> {
1867 let p = self.address.split('@').collect::<Vec<&str>>();
1868 Some(format!(
1869 @@ -94,14 +113,19 @@ impl MailingList {
1870 ))
1871 }
1872
1873+ /// Value of `List-Archive` header.
1874+ ///
1875+ /// See RFC2369 Section 3.6: <https://www.rfc-editor.org/rfc/rfc2369#section-3.6>
1876 pub fn archive_header(&self) -> Option<String> {
1877 self.archive_url.as_ref().map(|url| format!("<{}>", url))
1878 }
1879
1880+ /// List address as a [`melib::Address`]
1881 pub fn address(&self) -> Address {
1882 Address::new(Some(self.name.clone()), self.address.clone())
1883 }
1884
1885+ /// List unsubscribe action as a [`MailtoAddress`](super::MailtoAddress).
1886 pub fn unsubscribe_mailto(&self) -> Option<MailtoAddress> {
1887 let p = self.address.split('@').collect::<Vec<&str>>();
1888 Some(MailtoAddress {
1889 @@ -110,6 +134,7 @@ impl MailingList {
1890 })
1891 }
1892
1893+ /// List subscribe action as a [`MailtoAddress`](super::MailtoAddress).
1894 pub fn subscribe_mailto(&self) -> Option<MailtoAddress> {
1895 let p = self.address.split('@').collect::<Vec<&str>>();
1896 Some(MailtoAddress {
1897 @@ -118,28 +143,36 @@ impl MailingList {
1898 })
1899 }
1900
1901+ /// List archive url value.
1902 pub fn archive_url(&self) -> Option<&str> {
1903 self.archive_url.as_deref()
1904 }
1905 }
1906
1907- #[derive(Debug, Clone, Deserialize, Serialize)]
1908- pub struct MailtoAddress {
1909- pub address: String,
1910- pub subject: Option<String>,
1911- }
1912-
1913+ /// A mailing list membership entry.
1914 #[derive(Debug, Clone, Deserialize, Serialize)]
1915 pub struct ListMembership {
1916+ /// Database primary key.
1917 pub pk: i64,
1918+ /// Mailing list foreign key (See [`MailingList`]).
1919 pub list: i64,
1920+ /// Member's e-mail address.
1921 pub address: String,
1922+ /// Member's name, optional.
1923 pub name: Option<String>,
1924+ /// Whether member wishes to receive list posts as a periodical digest e-mail.
1925 pub digest: bool,
1926+ /// Whether member wishes their e-mail address hidden from public view.
1927 pub hide_address: bool,
1928+ /// Whether member wishes to receive mailing list post duplicates, i.e. posts addressed to them
1929+ /// and the mailing list to which they are subscribed.
1930 pub receive_duplicates: bool,
1931+ /// Whether member wishes to receive their own mailing list posts from the mailing list, as a
1932+ /// confirmation.
1933 pub receive_own_posts: bool,
1934+ /// Whether member wishes to receive a plain confirmation for their own mailing list posts.
1935 pub receive_confirmation: bool,
1936+ /// Whether this membership is enabled.
1937 pub enabled: bool,
1938 }
1939
1940 @@ -148,7 +181,7 @@ impl std::fmt::Display for ListMembership {
1941 write!(
1942 fmt,
1943 "{} [digest: {}, hide_address: {} {}]",
1944- self.into_address(),
1945+ self.address(),
1946 self.digest,
1947 self.hide_address,
1948 if self.enabled {
1949 @@ -161,19 +194,33 @@ impl std::fmt::Display for ListMembership {
1950 }
1951
1952 impl ListMembership {
1953- pub fn into_address(&self) -> Address {
1954+ /// Member address as a [`melib::Address`]
1955+ pub fn address(&self) -> Address {
1956 Address::new(self.name.clone(), self.address.clone())
1957 }
1958 }
1959
1960+ /// A mailing list post policy entry.
1961+ ///
1962+ /// Only one of the boolean flags must be set to true.
1963 #[derive(Debug, Clone, Deserialize, Serialize)]
1964 pub struct PostPolicy {
1965+ /// Database primary key.
1966 pub pk: i64,
1967+ /// Mailing list foreign key (See [`MailingList`]).
1968 pub list: i64,
1969+ /// Whether the policy is announce only (Only list owners can submit posts, and everyone will
1970+ /// receive them).
1971 pub announce_only: bool,
1972+ /// Whether the policy is "subscriber only" (Only list subscribers can post).
1973 pub subscriber_only: bool,
1974+ /// Whether the policy is "approval needed" (Anyone can post, but approval from list owners is
1975+ /// required if they are not subscribed).
1976 pub approval_needed: bool,
1977+ /// Whether the policy is "no subscriptions" (Anyone can post, but approval from list owners is
1978+ /// required. Subscriptions are not enabled).
1979 pub no_subscriptions: bool,
1980+ /// Custom policy.
1981 pub custom: bool,
1982 }
1983
1984 @@ -183,17 +230,22 @@ impl std::fmt::Display for PostPolicy {
1985 }
1986 }
1987
1988+ /// A mailing list owner entry.
1989 #[derive(Debug, Clone, Deserialize, Serialize)]
1990 pub struct ListOwner {
1991+ /// Database primary key.
1992 pub pk: i64,
1993+ /// Mailing list foreign key (See [`MailingList`]).
1994 pub list: i64,
1995+ /// Mailing list owner e-mail address.
1996 pub address: String,
1997+ /// Mailing list owner name, optional.
1998 pub name: Option<String>,
1999 }
2000
2001 impl std::fmt::Display for ListOwner {
2002 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
2003- write!(fmt, "[#{} {}] {}", self.pk, self.list, self.into_address())
2004+ write!(fmt, "[#{} {}] {}", self.pk, self.list, self.address())
2005 }
2006 }
2007
2008 @@ -215,65 +267,30 @@ impl From<ListOwner> for ListMembership {
2009 }
2010
2011 impl ListOwner {
2012- pub fn into_address(&self) -> Address {
2013+ /// Owner address as a [`melib::Address`]
2014+ pub fn address(&self) -> Address {
2015 Address::new(self.name.clone(), self.address.clone())
2016 }
2017 }
2018
2019- #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
2020- pub enum ListRequest {
2021- Subscribe,
2022- Unsubscribe,
2023- RetrieveArchive(String, String),
2024- RetrieveMessages(Vec<String>),
2025- SetDigest(bool),
2026- Other(String),
2027- }
2028-
2029- impl std::fmt::Display for ListRequest {
2030- fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
2031- write!(fmt, "{:?}", self)
2032- }
2033- }
2034-
2035- impl<S: AsRef<str>> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest {
2036- type Error = crate::Error;
2037-
2038- fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result<Self, Self::Error> {
2039- let val = val.as_ref();
2040- Ok(match val {
2041- "subscribe" | "request" if env.subject().trim() == "subscribe" => {
2042- ListRequest::Subscribe
2043- }
2044- "unsubscribe" | "request" if env.subject().trim() == "unsubscribe" => {
2045- ListRequest::Unsubscribe
2046- }
2047- "request" => ListRequest::Other(env.subject().trim().to_string()),
2048- _ => {
2049- trace!("unknown action = {} for addresses {:?}", val, env.from(),);
2050- ListRequest::Other(val.trim().to_string())
2051- }
2052- })
2053- }
2054- }
2055-
2056- #[derive(Debug, Clone, Deserialize, Serialize)]
2057- pub struct NewListPost<'s> {
2058- pub list: i64,
2059- pub address: &'s str,
2060- pub message_id: &'s str,
2061- pub message: &'s [u8],
2062- }
2063-
2064+ /// A mailing list post entry.
2065 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
2066 pub struct Post {
2067+ /// Database primary key.
2068 pub pk: i64,
2069+ /// Mailing list foreign key (See [`MailingList`]).
2070 pub list: i64,
2071+ /// `From` header address of post.
2072 pub address: String,
2073+ /// `Message-ID` header value of post.
2074 pub message_id: String,
2075+ /// Post as bytes.
2076 pub message: Vec<u8>,
2077+ /// Unix timestamp of date.
2078 pub timestamp: u64,
2079+ /// Datetime as string.
2080 pub datetime: String,
2081+ /// Month-year as a `YYYY-mm` formatted string, for use in archives.
2082 pub month_year: String,
2083 }
2084
2085 diff --git a/core/src/models/changesets.rs b/core/src/models/changesets.rs
2086index af4929e..fba0a81 100644
2087--- a/core/src/models/changesets.rs
2088+++ b/core/src/models/changesets.rs
2089 @@ -17,43 +17,73 @@
2090 * along with this program. If not, see <https://www.gnu.org/licenses/>.
2091 */
2092
2093+ //! Changeset structs: update specific struct fields.
2094+
2095+ /// Changeset struct for [`Mailinglist`](super::MailingList).
2096 #[derive(Debug, Clone, Deserialize, Serialize)]
2097 pub struct MailingListChangeset {
2098+ /// Database primary key.
2099 pub pk: i64,
2100+ /// Optional new value.
2101 pub name: Option<String>,
2102+ /// Optional new value.
2103 pub id: Option<String>,
2104+ /// Optional new value.
2105 pub address: Option<String>,
2106+ /// Optional new value.
2107 pub description: Option<Option<String>>,
2108+ /// Optional new value.
2109 pub archive_url: Option<Option<String>>,
2110 }
2111
2112+ /// Changeset struct for [`ListMembership`](super::ListMembership).
2113 #[derive(Debug, Clone, Deserialize, Serialize)]
2114 pub struct ListMembershipChangeset {
2115+ /// Mailing list foreign key (See [`MailingList`](super::MailingList)).
2116 pub list: i64,
2117+ /// Membership e-mail address.
2118 pub address: String,
2119+ /// Optional new value.
2120 pub name: Option<Option<String>>,
2121+ /// Optional new value.
2122 pub digest: Option<bool>,
2123+ /// Optional new value.
2124 pub hide_address: Option<bool>,
2125+ /// Optional new value.
2126 pub receive_duplicates: Option<bool>,
2127+ /// Optional new value.
2128 pub receive_own_posts: Option<bool>,
2129+ /// Optional new value.
2130 pub receive_confirmation: Option<bool>,
2131+ /// Optional new value.
2132 pub enabled: Option<bool>,
2133 }
2134
2135+ /// Changeset struct for [`PostPolicy`](super::PostPolicy).
2136 #[derive(Debug, Clone, Deserialize, Serialize)]
2137 pub struct PostPolicyChangeset {
2138+ /// Database primary key.
2139 pub pk: i64,
2140+ /// Mailing list foreign key (See [`MailingList`](super::MailingList)).
2141 pub list: i64,
2142+ /// Optional new value.
2143 pub announce_only: Option<bool>,
2144+ /// Optional new value.
2145 pub subscriber_only: Option<bool>,
2146+ /// Optional new value.
2147 pub approval_needed: Option<bool>,
2148 }
2149
2150+ /// Changeset struct for [`ListOwner`](super::ListOwner).
2151 #[derive(Debug, Clone, Deserialize, Serialize)]
2152 pub struct ListOwnerChangeset {
2153+ /// Database primary key.
2154 pub pk: i64,
2155+ /// Mailing list foreign key (See [`MailingList`](super::MailingList)).
2156 pub list: i64,
2157+ /// Optional new value.
2158 pub address: Option<String>,
2159+ /// Optional new value.
2160 pub name: Option<Option<String>>,
2161 }
2162
2163 diff --git a/core/src/schema.sql b/core/src/schema.sql
2164index 5f331e7..1197f56 100644
2165--- a/core/src/schema.sql
2166+++ b/core/src/schema.sql
2167 @@ -43,7 +43,8 @@ CREATE TABLE IF NOT EXISTS membership (
2168 receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0,
2169 receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1,
2170 FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE,
2171- FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE
2172+ FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE,
2173+ UNIQUE (list, address) ON CONFLICT ROLLBACK
2174 );
2175
2176 CREATE TABLE IF NOT EXISTS account (
2177 diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4
2178index 92cdc52..9be5bd7 100644
2179--- a/core/src/schema.sql.m4
2180+++ b/core/src/schema.sql.m4
2181 @@ -47,7 +47,8 @@ CREATE TABLE IF NOT EXISTS membership (
2182 BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(),
2183 BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(),
2184 FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE,
2185- FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE
2186+ FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE,
2187+ UNIQUE (list, address) ON CONFLICT ROLLBACK
2188 );
2189
2190 CREATE TABLE IF NOT EXISTS account (
2191 diff --git a/core/tests/authorizer.rs b/core/tests/authorizer.rs
2192index 47962d2..5f84ad4 100644
2193--- a/core/tests/authorizer.rs
2194+++ b/core/tests/authorizer.rs
2195 @@ -19,7 +19,7 @@
2196
2197 mod utils;
2198
2199- use mailpot::{models::*, Configuration, Database, SendMail};
2200+ use mailpot::{models::*, Configuration, Connection, SendMail};
2201 use std::error::Error;
2202 use tempfile::TempDir;
2203
2204 @@ -32,12 +32,11 @@ fn test_authorizer() {
2205 let config = Configuration {
2206 send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
2207 db_path: db_path.clone(),
2208- storage: "sqlite3".to_string(),
2209 data_path: tmp_dir.path().to_path_buf(),
2210 };
2211
2212- let db = Database::open_or_create_db(config).unwrap();
2213- assert!(db.list_lists().unwrap().is_empty());
2214+ let db = Connection::open_or_create_db(config).unwrap();
2215+ assert!(db.lists().unwrap().is_empty());
2216
2217 for err in [
2218 db.create_list(MailingList {
2219 @@ -73,7 +72,7 @@ fn test_authorizer() {
2220 },
2221 );
2222 }
2223- assert!(db.list_lists().unwrap().is_empty());
2224+ assert!(db.lists().unwrap().is_empty());
2225
2226 let db = db.trusted();
2227
2228 diff --git a/core/tests/creation.rs b/core/tests/creation.rs
2229index 5e69328..43b3e71 100644
2230--- a/core/tests/creation.rs
2231+++ b/core/tests/creation.rs
2232 @@ -19,7 +19,7 @@
2233
2234 mod utils;
2235
2236- use mailpot::{models::*, Configuration, Database, SendMail};
2237+ use mailpot::{models::*, Configuration, Connection, SendMail};
2238 use tempfile::TempDir;
2239
2240 #[test]
2241 @@ -31,13 +31,12 @@ fn test_init_empty() {
2242 let config = Configuration {
2243 send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
2244 db_path: db_path.clone(),
2245- storage: "sqlite3".to_string(),
2246 data_path: tmp_dir.path().to_path_buf(),
2247 };
2248
2249- let db = Database::open_or_create_db(config).unwrap();
2250+ let db = Connection::open_or_create_db(config).unwrap();
2251
2252- assert!(db.list_lists().unwrap().is_empty());
2253+ assert!(db.lists().unwrap().is_empty());
2254 }
2255
2256 #[test]
2257 @@ -49,12 +48,11 @@ fn test_list_creation() {
2258 let config = Configuration {
2259 send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
2260 db_path: db_path.clone(),
2261- storage: "sqlite3".to_string(),
2262 data_path: tmp_dir.path().to_path_buf(),
2263 };
2264
2265- let db = Database::open_or_create_db(config).unwrap().trusted();
2266- assert!(db.list_lists().unwrap().is_empty());
2267+ let db = Connection::open_or_create_db(config).unwrap().trusted();
2268+ assert!(db.lists().unwrap().is_empty());
2269 let foo_chat = db
2270 .create_list(MailingList {
2271 pk: 0,
2272 @@ -67,7 +65,7 @@ fn test_list_creation() {
2273 .unwrap();
2274
2275 assert_eq!(foo_chat.pk(), 1);
2276- let lists = db.list_lists().unwrap();
2277+ let lists = db.lists().unwrap();
2278 assert_eq!(lists.len(), 1);
2279 assert_eq!(lists[0], foo_chat);
2280 }
2281 diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs
2282index 2d69723..1254dd3 100644
2283--- a/core/tests/error_queue.rs
2284+++ b/core/tests/error_queue.rs
2285 @@ -19,7 +19,7 @@
2286
2287 mod utils;
2288
2289- use mailpot::{melib, models::*, Configuration, Database, SendMail};
2290+ use mailpot::{melib, models::*, Configuration, Connection, SendMail};
2291 use tempfile::TempDir;
2292
2293 fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
2294 @@ -43,12 +43,11 @@ fn test_error_queue() {
2295 let config = Configuration {
2296 send_mail: SendMail::Smtp(get_smtp_conf()),
2297 db_path: db_path.clone(),
2298- storage: "sqlite3".to_string(),
2299 data_path: tmp_dir.path().to_path_buf(),
2300 };
2301
2302- let db = Database::open_or_create_db(config).unwrap().trusted();
2303- assert!(db.list_lists().unwrap().is_empty());
2304+ let db = Connection::open_or_create_db(config).unwrap().trusted();
2305+ assert!(db.lists().unwrap().is_empty());
2306 let foo_chat = db
2307 .create_list(MailingList {
2308 pk: 0,
2309 diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs
2310index bbb197d..5624c92 100644
2311--- a/core/tests/smtp.rs
2312+++ b/core/tests/smtp.rs
2313 @@ -21,7 +21,7 @@ mod utils;
2314
2315 use log::{trace, warn};
2316 use mailin_embedded::{Handler, Response, Server, SslConfig};
2317- use mailpot::{melib, models::*, Configuration, Database, SendMail};
2318+ use mailpot::{melib, models::*, Configuration, Connection, SendMail};
2319 use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr};
2320 use std::sync::{Arc, Mutex};
2321 use std::thread;
2322 @@ -204,12 +204,11 @@ fn test_smtp() {
2323 let config = Configuration {
2324 send_mail: SendMail::Smtp(get_smtp_conf()),
2325 db_path: db_path.clone(),
2326- storage: "sqlite3".to_string(),
2327 data_path: tmp_dir.path().to_path_buf(),
2328 };
2329
2330- let db = Database::open_or_create_db(config).unwrap().trusted();
2331- assert!(db.list_lists().unwrap().is_empty());
2332+ let db = Connection::open_or_create_db(config).unwrap().trusted();
2333+ assert!(db.lists().unwrap().is_empty());
2334 let foo_chat = db
2335 .create_list(MailingList {
2336 pk: 0,
2337 @@ -323,12 +322,11 @@ fn test_smtp_mailcrab() {
2338 let config = Configuration {
2339 send_mail: SendMail::Smtp(get_smtp_conf()),
2340 db_path: db_path.clone(),
2341- storage: "sqlite3".to_string(),
2342 data_path: tmp_dir.path().to_path_buf(),
2343 };
2344
2345- let db = Database::open_or_create_db(config).unwrap().trusted();
2346- assert!(db.list_lists().unwrap().is_empty());
2347+ let db = Connection::open_or_create_db(config).unwrap().trusted();
2348+ assert!(db.lists().unwrap().is_empty());
2349 let foo_chat = db
2350 .create_list(MailingList {
2351 pk: 0,
2352 diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs
2353index 8b0323b..a3f89ca 100644
2354--- a/core/tests/subscription.rs
2355+++ b/core/tests/subscription.rs
2356 @@ -19,7 +19,7 @@
2357
2358 mod utils;
2359
2360- use mailpot::{models::*, Configuration, Database, SendMail};
2361+ use mailpot::{models::*, Configuration, Connection, SendMail};
2362 use tempfile::TempDir;
2363
2364 #[test]
2365 @@ -32,12 +32,11 @@ fn test_list_subscription() {
2366 let config = Configuration {
2367 send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
2368 db_path: db_path.clone(),
2369- storage: "sqlite3".to_string(),
2370 data_path: tmp_dir.path().to_path_buf(),
2371 };
2372
2373- let db = Database::open_or_create_db(config).unwrap().trusted();
2374- assert!(db.list_lists().unwrap().is_empty());
2375+ let db = Connection::open_or_create_db(config).unwrap().trusted();
2376+ assert!(db.lists().unwrap().is_empty());
2377 let foo_chat = db
2378 .create_list(MailingList {
2379 pk: 0,
2380 @@ -50,7 +49,7 @@ fn test_list_subscription() {
2381 .unwrap();
2382
2383 assert_eq!(foo_chat.pk(), 1);
2384- let lists = db.list_lists().unwrap();
2385+ let lists = db.lists().unwrap();
2386 assert_eq!(lists.len(), 1);
2387 assert_eq!(lists[0], foo_chat);
2388 let post_policy = db
2389 @@ -67,6 +66,7 @@ fn test_list_subscription() {
2390
2391 assert_eq!(post_policy.pk(), 1);
2392 assert_eq!(db.error_queue().unwrap().len(), 0);
2393+ assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 0);
2394
2395 let db = db.untrusted();
2396
2397 @@ -114,6 +114,7 @@ MIME-Version: 1.0
2398 melib::Envelope::from_bytes(input_bytes_2, None).expect("Could not parse message");
2399 db.post(&envelope, input_bytes_2, /* dry_run */ false)
2400 .unwrap();
2401+ assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 1);
2402 assert_eq!(db.error_queue().unwrap().len(), 1);
2403 let envelope =
2404 melib::Envelope::from_bytes(input_bytes_1, None).expect("Could not parse message");
2405 diff --git a/rest-http/src/main.rs b/rest-http/src/main.rs
2406index 7663f97..764facc 100644
2407--- a/rest-http/src/main.rs
2408+++ b/rest-http/src/main.rs
2409 @@ -19,22 +19,11 @@
2410
2411 extern crate mailpot;
2412
2413- pub use mailpot::config::*;
2414- pub use mailpot::db::*;
2415- pub use mailpot::errors::*;
2416 pub use mailpot::models::*;
2417 pub use mailpot::*;
2418
2419 use warp::Filter;
2420
2421- /*
2422- fn json_body() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
2423- // When accepting a body, we want a JSON body
2424- // (and to reject huge payloads)...
2425- warp::body::content_length_limit(1024 * 16).and(warp::body::json())
2426- }
2427- */
2428-
2429 #[tokio::main]
2430 async fn main() {
2431 let config_path = std::env::args()
2432 @@ -45,8 +34,8 @@ async fn main() {
2433 let conf1 = conf.clone();
2434 // GET /lists/:i64/policy
2435 let policy = warp::path!("lists" / i64 / "policy").map(move |list_pk| {
2436- let db = Database::open_db(conf1.clone()).unwrap();
2437- db.get_list_policy(list_pk)
2438+ let db = Connection::open_db(conf1.clone()).unwrap();
2439+ db.list_policy(list_pk)
2440 .ok()
2441 .map(|l| warp::reply::json(&l.unwrap()))
2442 .unwrap()
2443 @@ -55,34 +44,33 @@ async fn main() {
2444 let conf2 = conf.clone();
2445 //get("/lists")]
2446 let lists = warp::path!("lists").map(move || {
2447- let db = Database::open_db(conf2.clone()).unwrap();
2448- let lists = db.list_lists().unwrap();
2449+ let db = Connection::open_db(conf2.clone()).unwrap();
2450+ let lists = db.lists().unwrap();
2451 warp::reply::json(&lists)
2452 });
2453
2454 let conf3 = conf.clone();
2455 //get("/lists/<num>")]
2456 let lists_num = warp::path!("lists" / i64).map(move |list_pk| {
2457- let db = Database::open_db(conf3.clone()).unwrap();
2458- let list = db.get_list(list_pk).unwrap();
2459+ let db = Connection::open_db(conf3.clone()).unwrap();
2460+ let list = db.list(list_pk).unwrap();
2461 warp::reply::json(&list)
2462 });
2463
2464 let conf4 = conf.clone();
2465 //get("/lists/<num>/members")]
2466 let lists_members = warp::path!("lists" / i64 / "members").map(move |list_pk| {
2467- let db = Database::open_db(conf4.clone()).unwrap();
2468+ let db = Connection::open_db(conf4.clone()).unwrap();
2469 db.list_members(list_pk)
2470 .ok()
2471 .map(|l| warp::reply::json(&l))
2472 .unwrap()
2473 });
2474
2475- let conf5 = conf.clone();
2476 //get("/lists/<num>/owners")]
2477 let lists_owners = warp::path!("lists" / i64 / "owners").map(move |list_pk| {
2478- let db = Database::open_db(conf.clone()).unwrap();
2479- db.get_list_owners(list_pk)
2480+ let db = Connection::open_db(conf.clone()).unwrap();
2481+ db.list_owners(list_pk)
2482 .ok()
2483 .map(|l| warp::reply::json(&l))
2484 .unwrap()
2485 @@ -101,9 +89,5 @@ async fn main() {
2486 .or(lists_owner_add),
2487 );
2488
2489- // Note that composing filters for many routes may increase compile times (because it uses a lot of generics).
2490- // If you wish to use dynamic dispatch instead and speed up compile times while
2491- // making it slightly slower at runtime, you can use Filter::boxed().
2492-
2493 warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
2494 }