+971 -296 +/-27 browse
1 | diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml |
2 | new file mode 100644 |
3 | index 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 |
9 | new file mode 100644 |
10 | index 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 |
90 | new file mode 100644 |
91 | index 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 |
187 | index 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 |
324 | index 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 |
358 | index 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 |
396 | index 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 |
443 | index 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 |
567 | index 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 |
667 | index 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 |
1024 | index 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 |
1054 | index 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 |
1265 | index 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 |
1349 | index 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 |
1405 | index 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 |
1539 | index 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 |
1687 | index 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 |
1801 | index 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 |
2086 | index 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 |
2164 | index 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 |
2178 | index 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 |
2192 | index 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 |
2229 | index 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 |
2282 | index 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 |
2310 | index 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 |
2353 | index 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 |
2406 | index 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 | } |