+3366 -3306 +/-26 browse
1 | diff --git a/cli/src/lib.rs b/cli/src/lib.rs |
2 | index 3d4dc9a..b9439d7 100644 |
3 | --- a/cli/src/lib.rs |
4 | +++ b/cli/src/lib.rs |
5 | @@ -55,8 +55,12 @@ pub enum Command { |
6 | /// Prints a sample config file to STDOUT. |
7 | /// |
8 | /// You can generate a new configuration file by writing the output to a |
9 | - /// file, e.g: mpot sample-config > config.toml |
10 | - SampleConfig, |
11 | + /// file, e.g: mpot sample-config --with-smtp > config.toml |
12 | + SampleConfig { |
13 | + /// Use an SMTP connection instead of a shell process. |
14 | + #[arg(long)] |
15 | + with_smtp: bool, |
16 | + }, |
17 | /// Dumps database data to STDOUT. |
18 | DumpDatabase, |
19 | /// Lists all registered mailing lists. |
20 | diff --git a/cli/src/main.rs b/cli/src/main.rs |
21 | index 35fbca5..0a5c90a 100644 |
22 | --- a/cli/src/main.rs |
23 | +++ b/cli/src/main.rs |
24 | @@ -27,9 +27,10 @@ use std::{ |
25 | mod lints; |
26 | use lints::*; |
27 | use mailpot::{ |
28 | - melib::{backends::maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash}, |
29 | + melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash}, |
30 | models::{changesets::*, *}, |
31 | - *, |
32 | + queue::{Queue, QueueEntry}, |
33 | + Configuration, Connection, Error, ErrorKind, Result, *, |
34 | }; |
35 | use mailpot_cli::*; |
36 | |
37 | @@ -60,8 +61,27 @@ fn run_app(opt: Opt) -> Result<()> { |
38 | if opt.debug { |
39 | println!("DEBUG: {:?}", &opt); |
40 | } |
41 | - if let Command::SampleConfig = opt.cmd { |
42 | - println!("{}", Configuration::new("/path/to/sqlite.db").to_toml()); |
43 | + if let Command::SampleConfig { with_smtp } = opt.cmd { |
44 | + let mut new = Configuration::new("/path/to/sqlite.db"); |
45 | + new.administrators.push("admin@example.com".to_string()); |
46 | + if with_smtp { |
47 | + new.send_mail = mailpot::SendMail::Smtp(SmtpServerConf { |
48 | + hostname: "mail.example.com".to_string(), |
49 | + port: 587, |
50 | + envelope_from: "".to_string(), |
51 | + auth: SmtpAuth::Auto { |
52 | + username: "user".to_string(), |
53 | + password: Password::Raw("hunter2".to_string()), |
54 | + auth_type: SmtpAuthType::default(), |
55 | + require_auth: true, |
56 | + }, |
57 | + security: SmtpSecurity::StartTLS { |
58 | + danger_accept_invalid_certs: false, |
59 | + }, |
60 | + extensions: Default::default(), |
61 | + }); |
62 | + } |
63 | + println!("{}", new.to_toml()); |
64 | return Ok(()); |
65 | }; |
66 | let config_path = if let Some(path) = opt.config.as_ref() { |
67 | @@ -80,7 +100,7 @@ fn run_app(opt: Opt) -> Result<()> { |
68 | use Command::*; |
69 | let mut db = Connection::open_or_create_db(config)?.trusted(); |
70 | match opt.cmd { |
71 | - SampleConfig => {} |
72 | + SampleConfig { .. } => {} |
73 | DumpDatabase => { |
74 | let lists = db.lists()?; |
75 | let mut stdout = std::io::stdout(); |
76 | diff --git a/cli/tests/out_queue_flush.rs b/cli/tests/out_queue_flush.rs |
77 | index dc99b01..fabc966 100644 |
78 | --- a/cli/tests/out_queue_flush.rs |
79 | +++ b/cli/tests/out_queue_flush.rs |
80 | @@ -23,7 +23,8 @@ use assert_cmd::assert::OutputAssertExt; |
81 | use mailpot::{ |
82 | melib, |
83 | models::{changesets::ListSubscriptionChangeset, *}, |
84 | - Configuration, Connection, Queue, SendMail, |
85 | + queue::Queue, |
86 | + Configuration, Connection, SendMail, |
87 | }; |
88 | use mailpot_tests::*; |
89 | use predicates::prelude::*; |
90 | diff --git a/core/src/connection.rs b/core/src/connection.rs |
91 | new file mode 100644 |
92 | index 0000000..ce550ef |
93 | --- /dev/null |
94 | +++ b/core/src/connection.rs |
95 | @@ -0,0 +1,563 @@ |
96 | + /* |
97 | + * This file is part of mailpot |
98 | + * |
99 | + * Copyright 2020 - Manos Pitsidianakis |
100 | + * |
101 | + * This program is free software: you can redistribute it and/or modify |
102 | + * it under the terms of the GNU Affero General Public License as |
103 | + * published by the Free Software Foundation, either version 3 of the |
104 | + * License, or (at your option) any later version. |
105 | + * |
106 | + * This program is distributed in the hope that it will be useful, |
107 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
108 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
109 | + * GNU Affero General Public License for more details. |
110 | + * |
111 | + * You should have received a copy of the GNU Affero General Public License |
112 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
113 | + */ |
114 | + |
115 | + //! Mailpot database and methods. |
116 | + |
117 | + use std::{ |
118 | + io::Write, |
119 | + process::{Command, Stdio}, |
120 | + }; |
121 | + |
122 | + use log::{info, trace}; |
123 | + use rusqlite::{Connection as DbConnection, OptionalExtension}; |
124 | + |
125 | + use crate::{ |
126 | + config::Configuration, |
127 | + errors::{ErrorKind::*, *}, |
128 | + models::{changesets::MailingListChangeset, DbVal, ListOwner, MailingList, Post}, |
129 | + }; |
130 | + |
131 | + /// A connection to a `mailpot` database. |
132 | + pub struct Connection { |
133 | + /// The `rusqlite` connection handle. |
134 | + pub connection: DbConnection, |
135 | + pub(crate) conf: Configuration, |
136 | + } |
137 | + |
138 | + impl std::fmt::Debug for Connection { |
139 | + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
140 | + fmt.debug_struct("Connection") |
141 | + .field("conf", &self.conf) |
142 | + .finish() |
143 | + } |
144 | + } |
145 | + |
146 | + impl Drop for Connection { |
147 | + fn drop(&mut self) { |
148 | + self.connection |
149 | + .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>( |
150 | + None, |
151 | + ); |
152 | + // make sure pragma optimize does not take too long |
153 | + _ = self.connection.pragma_update(None, "analysis_limit", "400"); |
154 | + // gather statistics to improve query optimization |
155 | + _ = self |
156 | + .connection |
157 | + .pragma(None, "optimize", 0xfffe_i64, |_| Ok(())); |
158 | + } |
159 | + } |
160 | + |
161 | + fn log_callback(error_code: std::ffi::c_int, message: &str) { |
162 | + match error_code { |
163 | + rusqlite::ffi::SQLITE_OK |
164 | + | rusqlite::ffi::SQLITE_DONE |
165 | + | rusqlite::ffi::SQLITE_NOTICE |
166 | + | rusqlite::ffi::SQLITE_NOTICE_RECOVER_WAL |
167 | + | rusqlite::ffi::SQLITE_NOTICE_RECOVER_ROLLBACK => log::info!("{}", message), |
168 | + rusqlite::ffi::SQLITE_WARNING | rusqlite::ffi::SQLITE_WARNING_AUTOINDEX => { |
169 | + log::warn!("{}", message) |
170 | + } |
171 | + _ => log::error!("{error_code} {}", message), |
172 | + } |
173 | + } |
174 | + |
175 | + fn user_authorizer_callback( |
176 | + auth_context: rusqlite::hooks::AuthContext<'_>, |
177 | + ) -> rusqlite::hooks::Authorization { |
178 | + use rusqlite::hooks::{AuthAction, Authorization}; |
179 | + |
180 | + // [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this. |
181 | + match auth_context.action { |
182 | + AuthAction::Delete { |
183 | + table_name: "queue" | "candidate_subscription" | "subscription", |
184 | + } |
185 | + | AuthAction::Insert { |
186 | + table_name: "post" | "queue" | "candidate_subscription" | "subscription" | "account", |
187 | + } |
188 | + | AuthAction::Update { |
189 | + table_name: "candidate_subscription" | "templates", |
190 | + column_name: "accepted" | "last_modified" | "verified" | "address", |
191 | + } |
192 | + | AuthAction::Update { |
193 | + table_name: "account", |
194 | + column_name: "last_modified" | "name" | "public_key" | "password", |
195 | + } |
196 | + | AuthAction::Update { |
197 | + table_name: "subscription", |
198 | + column_name: |
199 | + "last_modified" |
200 | + | "account" |
201 | + | "digest" |
202 | + | "verified" |
203 | + | "hide_address" |
204 | + | "receive_duplicates" |
205 | + | "receive_own_posts" |
206 | + | "receive_confirmation", |
207 | + } |
208 | + | AuthAction::Select |
209 | + | AuthAction::Savepoint { .. } |
210 | + | AuthAction::Transaction { .. } |
211 | + | AuthAction::Read { .. } |
212 | + | AuthAction::Function { |
213 | + function_name: "count" | "strftime" | "unixepoch" | "datetime", |
214 | + } => Authorization::Allow, |
215 | + _ => Authorization::Deny, |
216 | + } |
217 | + } |
218 | + |
219 | + impl Connection { |
220 | + /// The database schema. |
221 | + /// |
222 | + /// ```sql |
223 | + #[doc = include_str!("./schema.sql")] |
224 | + /// ``` |
225 | + pub const SCHEMA: &str = include_str!("./schema.sql"); |
226 | + |
227 | + /// Creates a new database connection. |
228 | + /// |
229 | + /// `Connection` supports a limited subset of operations by default (see |
230 | + /// [`Connection::untrusted`]). |
231 | + /// Use [`Connection::trusted`] to remove these limits. |
232 | + pub fn open_db(conf: Configuration) -> Result<Self> { |
233 | + use std::sync::Once; |
234 | + |
235 | + use rusqlite::config::DbConfig; |
236 | + |
237 | + static INIT_SQLITE_LOGGING: Once = Once::new(); |
238 | + |
239 | + if !conf.db_path.exists() { |
240 | + return Err("Database doesn't exist".into()); |
241 | + } |
242 | + INIT_SQLITE_LOGGING.call_once(|| { |
243 | + unsafe { rusqlite::trace::config_log(Some(log_callback)).unwrap() }; |
244 | + }); |
245 | + let conn = DbConnection::open(conf.db_path.to_str().unwrap())?; |
246 | + rusqlite::vtab::array::load_module(&conn)?; |
247 | + conn.pragma_update(None, "journal_mode", "WAL")?; |
248 | + conn.pragma_update(None, "foreign_keys", "on")?; |
249 | + // synchronise less often to the filesystem |
250 | + conn.pragma_update(None, "synchronous", "normal")?; |
251 | + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?; |
252 | + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?; |
253 | + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?; |
254 | + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?; |
255 | + conn.busy_timeout(core::time::Duration::from_millis(500))?; |
256 | + conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; |
257 | + conn.authorizer(Some(user_authorizer_callback)); |
258 | + Ok(Self { |
259 | + conf, |
260 | + connection: conn, |
261 | + }) |
262 | + } |
263 | + |
264 | + /// Removes operational limits from this connection. (see |
265 | + /// [`Connection::untrusted`]) |
266 | + #[must_use] |
267 | + pub fn trusted(self) -> Self { |
268 | + self.connection |
269 | + .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>( |
270 | + None, |
271 | + ); |
272 | + self |
273 | + } |
274 | + |
275 | + // [tag:sync_auth_doc] |
276 | + /// Sets operational limits for this connection. |
277 | + /// |
278 | + /// - Allow `INSERT`, `DELETE` only for "queue", "candidate_subscription", |
279 | + /// "subscription". |
280 | + /// - Allow `UPDATE` only for "subscription" user facing settings. |
281 | + /// - Allow `INSERT` only for "post". |
282 | + /// - Allow read access to all tables. |
283 | + /// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime` |
284 | + /// function. |
285 | + /// - Deny everything else. |
286 | + pub fn untrusted(self) -> Self { |
287 | + self.connection.authorizer(Some(user_authorizer_callback)); |
288 | + self |
289 | + } |
290 | + |
291 | + /// Create a database if it doesn't exist and then open it. |
292 | + pub fn open_or_create_db(conf: Configuration) -> Result<Self> { |
293 | + if !conf.db_path.exists() { |
294 | + let db_path = &conf.db_path; |
295 | + use std::os::unix::fs::PermissionsExt; |
296 | + |
297 | + info!("Creating database in {}", db_path.display()); |
298 | + std::fs::File::create(db_path).context("Could not create db path")?; |
299 | + |
300 | + let mut child = Command::new("sqlite3") |
301 | + .arg(db_path) |
302 | + .stdin(Stdio::piped()) |
303 | + .stdout(Stdio::piped()) |
304 | + .stderr(Stdio::piped()) |
305 | + .spawn()?; |
306 | + let mut stdin = child.stdin.take().unwrap(); |
307 | + std::thread::spawn(move || { |
308 | + stdin |
309 | + .write_all(include_bytes!("./schema.sql")) |
310 | + .expect("failed to write to stdin"); |
311 | + stdin.flush().expect("could not flush stdin"); |
312 | + }); |
313 | + let output = child.wait_with_output()?; |
314 | + if !output.status.success() { |
315 | + return Err(format!( |
316 | + "Could not initialize sqlite3 database at {}: sqlite3 returned exit code {} \ |
317 | + and stderr {} {}", |
318 | + db_path.display(), |
319 | + output.status.code().unwrap_or_default(), |
320 | + String::from_utf8_lossy(&output.stderr), |
321 | + String::from_utf8_lossy(&output.stdout) |
322 | + ) |
323 | + .into()); |
324 | + } |
325 | + |
326 | + let file = std::fs::File::open(db_path)?; |
327 | + let metadata = file.metadata()?; |
328 | + let mut permissions = metadata.permissions(); |
329 | + |
330 | + permissions.set_mode(0o600); // Read/write for owner only. |
331 | + file.set_permissions(permissions)?; |
332 | + } |
333 | + Self::open_db(conf) |
334 | + } |
335 | + |
336 | + /// Returns a connection's configuration. |
337 | + pub fn conf(&self) -> &Configuration { |
338 | + &self.conf |
339 | + } |
340 | + |
341 | + /// Loads archive databases from [`Configuration::data_path`], if any. |
342 | + pub fn load_archives(&mut self) -> Result<()> { |
343 | + let tx = self.connection.transaction()?; |
344 | + { |
345 | + let mut stmt = tx.prepare("ATTACH ? AS ?;")?; |
346 | + for archive in std::fs::read_dir(&self.conf.data_path)? { |
347 | + let archive = archive?; |
348 | + let path = archive.path(); |
349 | + let name = path.file_name().unwrap_or_default(); |
350 | + if path == self.conf.db_path { |
351 | + continue; |
352 | + } |
353 | + stmt.execute(rusqlite::params![ |
354 | + path.to_str().unwrap(), |
355 | + name.to_str().unwrap() |
356 | + ])?; |
357 | + } |
358 | + } |
359 | + tx.commit()?; |
360 | + |
361 | + Ok(()) |
362 | + } |
363 | + |
364 | + /// Returns a vector of existing mailing lists. |
365 | + pub fn lists(&self) -> Result<Vec<DbVal<MailingList>>> { |
366 | + let mut stmt = self.connection.prepare("SELECT * FROM list;")?; |
367 | + let list_iter = stmt.query_map([], |row| { |
368 | + let pk = row.get("pk")?; |
369 | + Ok(DbVal( |
370 | + MailingList { |
371 | + pk, |
372 | + name: row.get("name")?, |
373 | + id: row.get("id")?, |
374 | + address: row.get("address")?, |
375 | + description: row.get("description")?, |
376 | + archive_url: row.get("archive_url")?, |
377 | + }, |
378 | + pk, |
379 | + )) |
380 | + })?; |
381 | + |
382 | + let mut ret = vec![]; |
383 | + for list in list_iter { |
384 | + let list = list?; |
385 | + ret.push(list); |
386 | + } |
387 | + Ok(ret) |
388 | + } |
389 | + |
390 | + /// Fetch a mailing list by primary key. |
391 | + pub fn list(&self, pk: i64) -> Result<Option<DbVal<MailingList>>> { |
392 | + let mut stmt = self |
393 | + .connection |
394 | + .prepare("SELECT * FROM list WHERE pk = ?;")?; |
395 | + let ret = stmt |
396 | + .query_row([&pk], |row| { |
397 | + let pk = row.get("pk")?; |
398 | + Ok(DbVal( |
399 | + MailingList { |
400 | + pk, |
401 | + name: row.get("name")?, |
402 | + id: row.get("id")?, |
403 | + address: row.get("address")?, |
404 | + description: row.get("description")?, |
405 | + archive_url: row.get("archive_url")?, |
406 | + }, |
407 | + pk, |
408 | + )) |
409 | + }) |
410 | + .optional()?; |
411 | + Ok(ret) |
412 | + } |
413 | + |
414 | + /// Fetch a mailing list by id. |
415 | + pub fn list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> { |
416 | + let id = id.as_ref(); |
417 | + let mut stmt = self |
418 | + .connection |
419 | + .prepare("SELECT * FROM list WHERE id = ?;")?; |
420 | + let ret = stmt |
421 | + .query_row([&id], |row| { |
422 | + let pk = row.get("pk")?; |
423 | + Ok(DbVal( |
424 | + MailingList { |
425 | + pk, |
426 | + name: row.get("name")?, |
427 | + id: row.get("id")?, |
428 | + address: row.get("address")?, |
429 | + description: row.get("description")?, |
430 | + archive_url: row.get("archive_url")?, |
431 | + }, |
432 | + pk, |
433 | + )) |
434 | + }) |
435 | + .optional()?; |
436 | + |
437 | + Ok(ret) |
438 | + } |
439 | + |
440 | + /// Create a new list. |
441 | + pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> { |
442 | + let mut stmt = self.connection.prepare( |
443 | + "INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \ |
444 | + RETURNING *;", |
445 | + )?; |
446 | + let ret = stmt.query_row( |
447 | + rusqlite::params![ |
448 | + &new_val.name, |
449 | + &new_val.id, |
450 | + &new_val.address, |
451 | + new_val.description.as_ref(), |
452 | + new_val.archive_url.as_ref(), |
453 | + ], |
454 | + |row| { |
455 | + let pk = row.get("pk")?; |
456 | + Ok(DbVal( |
457 | + MailingList { |
458 | + pk, |
459 | + name: row.get("name")?, |
460 | + id: row.get("id")?, |
461 | + address: row.get("address")?, |
462 | + description: row.get("description")?, |
463 | + archive_url: row.get("archive_url")?, |
464 | + }, |
465 | + pk, |
466 | + )) |
467 | + }, |
468 | + )?; |
469 | + |
470 | + trace!("create_list {:?}.", &ret); |
471 | + Ok(ret) |
472 | + } |
473 | + |
474 | + /// Fetch all posts of a mailing list. |
475 | + pub fn list_posts( |
476 | + &self, |
477 | + list_pk: i64, |
478 | + _date_range: Option<(String, String)>, |
479 | + ) -> Result<Vec<DbVal<Post>>> { |
480 | + let mut stmt = self.connection.prepare( |
481 | + "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \ |
482 | + FROM post WHERE list = ?;", |
483 | + )?; |
484 | + let iter = stmt.query_map(rusqlite::params![&list_pk], |row| { |
485 | + let pk = row.get("pk")?; |
486 | + Ok(DbVal( |
487 | + Post { |
488 | + pk, |
489 | + list: row.get("list")?, |
490 | + envelope_from: row.get("envelope_from")?, |
491 | + address: row.get("address")?, |
492 | + message_id: row.get("message_id")?, |
493 | + message: row.get("message")?, |
494 | + timestamp: row.get("timestamp")?, |
495 | + datetime: row.get("datetime")?, |
496 | + month_year: row.get("month_year")?, |
497 | + }, |
498 | + pk, |
499 | + )) |
500 | + })?; |
501 | + let mut ret = vec![]; |
502 | + for post in iter { |
503 | + let post = post?; |
504 | + ret.push(post); |
505 | + } |
506 | + |
507 | + trace!("list_posts {:?}.", &ret); |
508 | + Ok(ret) |
509 | + } |
510 | + |
511 | + /// Fetch the owners of a mailing list. |
512 | + pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> { |
513 | + let mut stmt = self |
514 | + .connection |
515 | + .prepare("SELECT * FROM owner WHERE list = ?;")?; |
516 | + let list_iter = stmt.query_map([&pk], |row| { |
517 | + let pk = row.get("pk")?; |
518 | + Ok(DbVal( |
519 | + ListOwner { |
520 | + pk, |
521 | + list: row.get("list")?, |
522 | + address: row.get("address")?, |
523 | + name: row.get("name")?, |
524 | + }, |
525 | + pk, |
526 | + )) |
527 | + })?; |
528 | + |
529 | + let mut ret = vec![]; |
530 | + for list in list_iter { |
531 | + let list = list?; |
532 | + ret.push(list); |
533 | + } |
534 | + Ok(ret) |
535 | + } |
536 | + |
537 | + /// Remove an owner of a mailing list. |
538 | + pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> { |
539 | + self.connection |
540 | + .query_row( |
541 | + "DELETE FROM owner WHERE list = ? AND pk = ? RETURNING *;", |
542 | + rusqlite::params![&list_pk, &owner_pk], |
543 | + |_| Ok(()), |
544 | + ) |
545 | + .map_err(|err| { |
546 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
547 | + Error::from(err).chain_err(|| NotFound("list or list owner not found!")) |
548 | + } else { |
549 | + err.into() |
550 | + } |
551 | + })?; |
552 | + Ok(()) |
553 | + } |
554 | + |
555 | + /// Add an owner of a mailing list. |
556 | + pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> { |
557 | + let mut stmt = self.connection.prepare( |
558 | + "INSERT OR REPLACE INTO owner(list, address, name) VALUES (?, ?, ?) RETURNING *;", |
559 | + )?; |
560 | + let list_pk = list_owner.list; |
561 | + let ret = stmt |
562 | + .query_row( |
563 | + rusqlite::params![&list_pk, &list_owner.address, &list_owner.name,], |
564 | + |row| { |
565 | + let pk = row.get("pk")?; |
566 | + Ok(DbVal( |
567 | + ListOwner { |
568 | + pk, |
569 | + list: row.get("list")?, |
570 | + address: row.get("address")?, |
571 | + name: row.get("name")?, |
572 | + }, |
573 | + pk, |
574 | + )) |
575 | + }, |
576 | + ) |
577 | + .map_err(|err| { |
578 | + if matches!( |
579 | + err, |
580 | + rusqlite::Error::SqliteFailure( |
581 | + rusqlite::ffi::Error { |
582 | + code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
583 | + extended_code: 787 |
584 | + }, |
585 | + _ |
586 | + ) |
587 | + ) { |
588 | + Error::from(err).chain_err(|| NotFound("Could not find a list with this pk.")) |
589 | + } else { |
590 | + err.into() |
591 | + } |
592 | + })?; |
593 | + |
594 | + trace!("add_list_owner {:?}.", &ret); |
595 | + Ok(ret) |
596 | + } |
597 | + |
598 | + /// Update a mailing list. |
599 | + pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> { |
600 | + if matches!( |
601 | + change_set, |
602 | + MailingListChangeset { |
603 | + pk: _, |
604 | + name: None, |
605 | + id: None, |
606 | + address: None, |
607 | + description: None, |
608 | + archive_url: None, |
609 | + owner_local_part: None, |
610 | + request_local_part: None, |
611 | + verify: None, |
612 | + hidden: None, |
613 | + enabled: None, |
614 | + } |
615 | + ) { |
616 | + return self.list(change_set.pk).map(|_| ()); |
617 | + } |
618 | + |
619 | + let MailingListChangeset { |
620 | + pk, |
621 | + name, |
622 | + id, |
623 | + address, |
624 | + description, |
625 | + archive_url, |
626 | + owner_local_part, |
627 | + request_local_part, |
628 | + verify, |
629 | + hidden, |
630 | + enabled, |
631 | + } = change_set; |
632 | + let tx = self.connection.transaction()?; |
633 | + |
634 | + macro_rules! update { |
635 | + ($field:tt) => {{ |
636 | + if let Some($field) = $field { |
637 | + tx.execute( |
638 | + concat!("UPDATE list SET ", stringify!($field), " = ? WHERE pk = ?;"), |
639 | + rusqlite::params![&$field, &pk], |
640 | + )?; |
641 | + } |
642 | + }}; |
643 | + } |
644 | + update!(name); |
645 | + update!(id); |
646 | + update!(address); |
647 | + update!(description); |
648 | + update!(archive_url); |
649 | + update!(owner_local_part); |
650 | + update!(request_local_part); |
651 | + update!(verify); |
652 | + update!(hidden); |
653 | + update!(enabled); |
654 | + |
655 | + tx.commit()?; |
656 | + Ok(()) |
657 | + } |
658 | + } |
659 | diff --git a/core/src/db.rs b/core/src/db.rs |
660 | deleted file mode 100644 |
661 | index 7a9b63e..0000000 |
662 | --- a/core/src/db.rs |
663 | +++ /dev/null |
664 | @@ -1,586 +0,0 @@ |
665 | - /* |
666 | - * This file is part of mailpot |
667 | - * |
668 | - * Copyright 2020 - Manos Pitsidianakis |
669 | - * |
670 | - * This program is free software: you can redistribute it and/or modify |
671 | - * it under the terms of the GNU Affero General Public License as |
672 | - * published by the Free Software Foundation, either version 3 of the |
673 | - * License, or (at your option) any later version. |
674 | - * |
675 | - * This program is distributed in the hope that it will be useful, |
676 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
677 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
678 | - * GNU Affero General Public License for more details. |
679 | - * |
680 | - * You should have received a copy of the GNU Affero General Public License |
681 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
682 | - */ |
683 | - |
684 | - //! Mailpot database and methods. |
685 | - |
686 | - use std::{ |
687 | - io::Write, |
688 | - process::{Command, Stdio}, |
689 | - }; |
690 | - |
691 | - use melib::Envelope; |
692 | - use models::changesets::*; |
693 | - use rusqlite::{Connection as DbConnection, OptionalExtension}; |
694 | - |
695 | - use super::{Configuration, *}; |
696 | - use crate::ErrorKind::*; |
697 | - |
698 | - /// A connection to a `mailpot` database. |
699 | - pub struct Connection { |
700 | - /// The `rusqlite` connection handle. |
701 | - pub connection: DbConnection, |
702 | - conf: Configuration, |
703 | - } |
704 | - |
705 | - impl std::fmt::Debug for Connection { |
706 | - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
707 | - fmt.debug_struct("Connection") |
708 | - .field("conf", &self.conf) |
709 | - .finish() |
710 | - } |
711 | - } |
712 | - |
713 | - impl Drop for Connection { |
714 | - fn drop(&mut self) { |
715 | - self.connection |
716 | - .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>( |
717 | - None, |
718 | - ); |
719 | - // make sure pragma optimize does not take too long |
720 | - _ = self.connection.pragma_update(None, "analysis_limit", "400"); |
721 | - // gather statistics to improve query optimization |
722 | - _ = self |
723 | - .connection |
724 | - .pragma(None, "optimize", 0xfffe_i64, |_| Ok(())); |
725 | - } |
726 | - } |
727 | - |
728 | - mod templates; |
729 | - pub use templates::*; |
730 | - mod queue; |
731 | - pub use queue::*; |
732 | - mod posts; |
733 | - pub use posts::*; |
734 | - mod subscriptions; |
735 | - pub use subscriptions::*; |
736 | - mod policies; |
737 | - pub use policies::*; |
738 | - |
739 | - fn log_callback(error_code: std::ffi::c_int, message: &str) { |
740 | - match error_code { |
741 | - rusqlite::ffi::SQLITE_OK |
742 | - | rusqlite::ffi::SQLITE_DONE |
743 | - | rusqlite::ffi::SQLITE_NOTICE |
744 | - | rusqlite::ffi::SQLITE_NOTICE_RECOVER_WAL |
745 | - | rusqlite::ffi::SQLITE_NOTICE_RECOVER_ROLLBACK => log::info!("{}", message), |
746 | - rusqlite::ffi::SQLITE_WARNING | rusqlite::ffi::SQLITE_WARNING_AUTOINDEX => { |
747 | - log::warn!("{}", message) |
748 | - } |
749 | - _ => log::error!("{error_code} {}", message), |
750 | - } |
751 | - } |
752 | - |
753 | - fn user_authorizer_callback( |
754 | - auth_context: rusqlite::hooks::AuthContext<'_>, |
755 | - ) -> rusqlite::hooks::Authorization { |
756 | - use rusqlite::hooks::{AuthAction, Authorization}; |
757 | - |
758 | - // [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this. |
759 | - match auth_context.action { |
760 | - AuthAction::Delete { |
761 | - table_name: "queue" | "candidate_subscription" | "subscription", |
762 | - } |
763 | - | AuthAction::Insert { |
764 | - table_name: "post" | "queue" | "candidate_subscription" | "subscription" | "account", |
765 | - } |
766 | - | AuthAction::Update { |
767 | - table_name: "candidate_subscription" | "templates", |
768 | - column_name: "accepted" | "last_modified" | "verified" | "address", |
769 | - } |
770 | - | AuthAction::Update { |
771 | - table_name: "account", |
772 | - column_name: "last_modified" | "name" | "public_key" | "password", |
773 | - } |
774 | - | AuthAction::Update { |
775 | - table_name: "subscription", |
776 | - column_name: |
777 | - "last_modified" |
778 | - | "account" |
779 | - | "digest" |
780 | - | "verified" |
781 | - | "hide_address" |
782 | - | "receive_duplicates" |
783 | - | "receive_own_posts" |
784 | - | "receive_confirmation", |
785 | - } |
786 | - | AuthAction::Select |
787 | - | AuthAction::Savepoint { .. } |
788 | - | AuthAction::Transaction { .. } |
789 | - | AuthAction::Read { .. } |
790 | - | AuthAction::Function { |
791 | - function_name: "count" | "strftime" | "unixepoch" | "datetime", |
792 | - } => Authorization::Allow, |
793 | - _ => Authorization::Deny, |
794 | - } |
795 | - } |
796 | - |
797 | - impl Connection { |
798 | - /// The database schema. |
799 | - /// |
800 | - /// ```sql |
801 | - #[doc = include_str!("./schema.sql")] |
802 | - /// ``` |
803 | - pub const SCHEMA: &str = include_str!("./schema.sql"); |
804 | - |
805 | - /// Creates a new database connection. |
806 | - /// |
807 | - /// `Connection` supports a limited subset of operations by default (see |
808 | - /// [`Connection::untrusted`]). |
809 | - /// Use [`Connection::trusted`] to remove these limits. |
810 | - pub fn open_db(conf: Configuration) -> Result<Self> { |
811 | - use std::sync::Once; |
812 | - |
813 | - use rusqlite::config::DbConfig; |
814 | - |
815 | - static INIT_SQLITE_LOGGING: Once = Once::new(); |
816 | - |
817 | - if !conf.db_path.exists() { |
818 | - return Err("Database doesn't exist".into()); |
819 | - } |
820 | - INIT_SQLITE_LOGGING.call_once(|| { |
821 | - unsafe { rusqlite::trace::config_log(Some(log_callback)).unwrap() }; |
822 | - }); |
823 | - let conn = DbConnection::open(conf.db_path.to_str().unwrap())?; |
824 | - rusqlite::vtab::array::load_module(&conn)?; |
825 | - conn.pragma_update(None, "journal_mode", "WAL")?; |
826 | - conn.pragma_update(None, "foreign_keys", "on")?; |
827 | - // synchronise less often to the filesystem |
828 | - conn.pragma_update(None, "synchronous", "normal")?; |
829 | - conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?; |
830 | - conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?; |
831 | - conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?; |
832 | - conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?; |
833 | - conn.busy_timeout(core::time::Duration::from_millis(500))?; |
834 | - conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; |
835 | - conn.authorizer(Some(user_authorizer_callback)); |
836 | - Ok(Self { |
837 | - conf, |
838 | - connection: conn, |
839 | - }) |
840 | - } |
841 | - |
842 | - /// Removes operational limits from this connection. (see |
843 | - /// [`Connection::untrusted`]) |
844 | - #[must_use] |
845 | - pub fn trusted(self) -> Self { |
846 | - self.connection |
847 | - .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>( |
848 | - None, |
849 | - ); |
850 | - self |
851 | - } |
852 | - |
853 | - // [tag:sync_auth_doc] |
854 | - /// Sets operational limits for this connection. |
855 | - /// |
856 | - /// - Allow `INSERT`, `DELETE` only for "queue", "candidate_subscription", |
857 | - /// "subscription". |
858 | - /// - Allow `UPDATE` only for "subscription" user facing settings. |
859 | - /// - Allow `INSERT` only for "post". |
860 | - /// - Allow read access to all tables. |
861 | - /// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime` |
862 | - /// function. |
863 | - /// - Deny everything else. |
864 | - pub fn untrusted(self) -> Self { |
865 | - self.connection.authorizer(Some(user_authorizer_callback)); |
866 | - self |
867 | - } |
868 | - |
869 | - /// Create a database if it doesn't exist and then open it. |
870 | - pub fn open_or_create_db(conf: Configuration) -> Result<Self> { |
871 | - if !conf.db_path.exists() { |
872 | - let db_path = &conf.db_path; |
873 | - use std::os::unix::fs::PermissionsExt; |
874 | - |
875 | - info!("Creating database in {}", db_path.display()); |
876 | - std::fs::File::create(db_path).context("Could not create db path")?; |
877 | - |
878 | - let mut child = Command::new("sqlite3") |
879 | - .arg(db_path) |
880 | - .stdin(Stdio::piped()) |
881 | - .stdout(Stdio::piped()) |
882 | - .stderr(Stdio::piped()) |
883 | - .spawn()?; |
884 | - let mut stdin = child.stdin.take().unwrap(); |
885 | - std::thread::spawn(move || { |
886 | - stdin |
887 | - .write_all(include_bytes!("./schema.sql")) |
888 | - .expect("failed to write to stdin"); |
889 | - stdin.flush().expect("could not flush stdin"); |
890 | - }); |
891 | - let output = child.wait_with_output()?; |
892 | - if !output.status.success() { |
893 | - return Err(format!( |
894 | - "Could not initialize sqlite3 database at {}: sqlite3 returned exit code {} \ |
895 | - and stderr {} {}", |
896 | - db_path.display(), |
897 | - output.status.code().unwrap_or_default(), |
898 | - String::from_utf8_lossy(&output.stderr), |
899 | - String::from_utf8_lossy(&output.stdout) |
900 | - ) |
901 | - .into()); |
902 | - } |
903 | - |
904 | - let file = std::fs::File::open(db_path)?; |
905 | - let metadata = file.metadata()?; |
906 | - let mut permissions = metadata.permissions(); |
907 | - |
908 | - permissions.set_mode(0o600); // Read/write for owner only. |
909 | - file.set_permissions(permissions)?; |
910 | - } |
911 | - Self::open_db(conf) |
912 | - } |
913 | - |
914 | - /// Returns a connection's configuration. |
915 | - pub fn conf(&self) -> &Configuration { |
916 | - &self.conf |
917 | - } |
918 | - |
919 | - /// Loads archive databases from [`Configuration::data_path`], if any. |
920 | - pub fn load_archives(&mut self) -> Result<()> { |
921 | - let tx = self.connection.transaction()?; |
922 | - { |
923 | - let mut stmt = tx.prepare("ATTACH ? AS ?;")?; |
924 | - for archive in std::fs::read_dir(&self.conf.data_path)? { |
925 | - let archive = archive?; |
926 | - let path = archive.path(); |
927 | - let name = path.file_name().unwrap_or_default(); |
928 | - if path == self.conf.db_path { |
929 | - continue; |
930 | - } |
931 | - stmt.execute(rusqlite::params![ |
932 | - path.to_str().unwrap(), |
933 | - name.to_str().unwrap() |
934 | - ])?; |
935 | - } |
936 | - } |
937 | - tx.commit()?; |
938 | - |
939 | - Ok(()) |
940 | - } |
941 | - |
942 | - /// Returns a vector of existing mailing lists. |
943 | - pub fn lists(&self) -> Result<Vec<DbVal<MailingList>>> { |
944 | - let mut stmt = self.connection.prepare("SELECT * FROM list;")?; |
945 | - let list_iter = stmt.query_map([], |row| { |
946 | - let pk = row.get("pk")?; |
947 | - Ok(DbVal( |
948 | - MailingList { |
949 | - pk, |
950 | - name: row.get("name")?, |
951 | - id: row.get("id")?, |
952 | - address: row.get("address")?, |
953 | - description: row.get("description")?, |
954 | - archive_url: row.get("archive_url")?, |
955 | - }, |
956 | - pk, |
957 | - )) |
958 | - })?; |
959 | - |
960 | - let mut ret = vec![]; |
961 | - for list in list_iter { |
962 | - let list = list?; |
963 | - ret.push(list); |
964 | - } |
965 | - Ok(ret) |
966 | - } |
967 | - |
968 | - /// Fetch a mailing list by primary key. |
969 | - pub fn list(&self, pk: i64) -> Result<Option<DbVal<MailingList>>> { |
970 | - let mut stmt = self |
971 | - .connection |
972 | - .prepare("SELECT * FROM list WHERE pk = ?;")?; |
973 | - let ret = stmt |
974 | - .query_row([&pk], |row| { |
975 | - let pk = row.get("pk")?; |
976 | - Ok(DbVal( |
977 | - MailingList { |
978 | - pk, |
979 | - name: row.get("name")?, |
980 | - id: row.get("id")?, |
981 | - address: row.get("address")?, |
982 | - description: row.get("description")?, |
983 | - archive_url: row.get("archive_url")?, |
984 | - }, |
985 | - pk, |
986 | - )) |
987 | - }) |
988 | - .optional()?; |
989 | - Ok(ret) |
990 | - } |
991 | - |
992 | - /// Fetch a mailing list by id. |
993 | - pub fn list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> { |
994 | - let id = id.as_ref(); |
995 | - let mut stmt = self |
996 | - .connection |
997 | - .prepare("SELECT * FROM list WHERE id = ?;")?; |
998 | - let ret = stmt |
999 | - .query_row([&id], |row| { |
1000 | - let pk = row.get("pk")?; |
1001 | - Ok(DbVal( |
1002 | - MailingList { |
1003 | - pk, |
1004 | - name: row.get("name")?, |
1005 | - id: row.get("id")?, |
1006 | - address: row.get("address")?, |
1007 | - description: row.get("description")?, |
1008 | - archive_url: row.get("archive_url")?, |
1009 | - }, |
1010 | - pk, |
1011 | - )) |
1012 | - }) |
1013 | - .optional()?; |
1014 | - |
1015 | - Ok(ret) |
1016 | - } |
1017 | - |
1018 | - /// Create a new list. |
1019 | - pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> { |
1020 | - let mut stmt = self.connection.prepare( |
1021 | - "INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \ |
1022 | - RETURNING *;", |
1023 | - )?; |
1024 | - let ret = stmt.query_row( |
1025 | - rusqlite::params![ |
1026 | - &new_val.name, |
1027 | - &new_val.id, |
1028 | - &new_val.address, |
1029 | - new_val.description.as_ref(), |
1030 | - new_val.archive_url.as_ref(), |
1031 | - ], |
1032 | - |row| { |
1033 | - let pk = row.get("pk")?; |
1034 | - Ok(DbVal( |
1035 | - MailingList { |
1036 | - pk, |
1037 | - name: row.get("name")?, |
1038 | - id: row.get("id")?, |
1039 | - address: row.get("address")?, |
1040 | - description: row.get("description")?, |
1041 | - archive_url: row.get("archive_url")?, |
1042 | - }, |
1043 | - pk, |
1044 | - )) |
1045 | - }, |
1046 | - )?; |
1047 | - |
1048 | - trace!("create_list {:?}.", &ret); |
1049 | - Ok(ret) |
1050 | - } |
1051 | - |
1052 | - /// Fetch all posts of a mailing list. |
1053 | - pub fn list_posts( |
1054 | - &self, |
1055 | - list_pk: i64, |
1056 | - _date_range: Option<(String, String)>, |
1057 | - ) -> Result<Vec<DbVal<Post>>> { |
1058 | - let mut stmt = self.connection.prepare( |
1059 | - "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \ |
1060 | - FROM post WHERE list = ?;", |
1061 | - )?; |
1062 | - let iter = stmt.query_map(rusqlite::params![&list_pk], |row| { |
1063 | - let pk = row.get("pk")?; |
1064 | - Ok(DbVal( |
1065 | - Post { |
1066 | - pk, |
1067 | - list: row.get("list")?, |
1068 | - envelope_from: row.get("envelope_from")?, |
1069 | - address: row.get("address")?, |
1070 | - message_id: row.get("message_id")?, |
1071 | - message: row.get("message")?, |
1072 | - timestamp: row.get("timestamp")?, |
1073 | - datetime: row.get("datetime")?, |
1074 | - month_year: row.get("month_year")?, |
1075 | - }, |
1076 | - pk, |
1077 | - )) |
1078 | - })?; |
1079 | - let mut ret = vec![]; |
1080 | - for post in iter { |
1081 | - let post = post?; |
1082 | - ret.push(post); |
1083 | - } |
1084 | - |
1085 | - trace!("list_posts {:?}.", &ret); |
1086 | - Ok(ret) |
1087 | - } |
1088 | - |
1089 | - /// Fetch the owners of a mailing list. |
1090 | - pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> { |
1091 | - let mut stmt = self |
1092 | - .connection |
1093 | - .prepare("SELECT * FROM owner WHERE list = ?;")?; |
1094 | - let list_iter = stmt.query_map([&pk], |row| { |
1095 | - let pk = row.get("pk")?; |
1096 | - Ok(DbVal( |
1097 | - ListOwner { |
1098 | - pk, |
1099 | - list: row.get("list")?, |
1100 | - address: row.get("address")?, |
1101 | - name: row.get("name")?, |
1102 | - }, |
1103 | - pk, |
1104 | - )) |
1105 | - })?; |
1106 | - |
1107 | - let mut ret = vec![]; |
1108 | - for list in list_iter { |
1109 | - let list = list?; |
1110 | - ret.push(list); |
1111 | - } |
1112 | - Ok(ret) |
1113 | - } |
1114 | - |
1115 | - /// Remove an owner of a mailing list. |
1116 | - pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> { |
1117 | - self.connection |
1118 | - .query_row( |
1119 | - "DELETE FROM owner WHERE list = ? AND pk = ? RETURNING *;", |
1120 | - rusqlite::params![&list_pk, &owner_pk], |
1121 | - |_| Ok(()), |
1122 | - ) |
1123 | - .map_err(|err| { |
1124 | - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
1125 | - Error::from(err).chain_err(|| NotFound("list or list owner not found!")) |
1126 | - } else { |
1127 | - err.into() |
1128 | - } |
1129 | - })?; |
1130 | - Ok(()) |
1131 | - } |
1132 | - |
1133 | - /// Add an owner of a mailing list. |
1134 | - pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> { |
1135 | - let mut stmt = self.connection.prepare( |
1136 | - "INSERT OR REPLACE INTO owner(list, address, name) VALUES (?, ?, ?) RETURNING *;", |
1137 | - )?; |
1138 | - let list_pk = list_owner.list; |
1139 | - let ret = stmt |
1140 | - .query_row( |
1141 | - rusqlite::params![&list_pk, &list_owner.address, &list_owner.name,], |
1142 | - |row| { |
1143 | - let pk = row.get("pk")?; |
1144 | - Ok(DbVal( |
1145 | - ListOwner { |
1146 | - pk, |
1147 | - list: row.get("list")?, |
1148 | - address: row.get("address")?, |
1149 | - name: row.get("name")?, |
1150 | - }, |
1151 | - pk, |
1152 | - )) |
1153 | - }, |
1154 | - ) |
1155 | - .map_err(|err| { |
1156 | - if matches!( |
1157 | - err, |
1158 | - rusqlite::Error::SqliteFailure( |
1159 | - rusqlite::ffi::Error { |
1160 | - code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
1161 | - extended_code: 787 |
1162 | - }, |
1163 | - _ |
1164 | - ) |
1165 | - ) { |
1166 | - Error::from(err).chain_err(|| NotFound("Could not find a list with this pk.")) |
1167 | - } else { |
1168 | - err.into() |
1169 | - } |
1170 | - })?; |
1171 | - |
1172 | - trace!("add_list_owner {:?}.", &ret); |
1173 | - Ok(ret) |
1174 | - } |
1175 | - |
1176 | - /// Update a mailing list. |
1177 | - pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> { |
1178 | - if matches!( |
1179 | - change_set, |
1180 | - MailingListChangeset { |
1181 | - pk: _, |
1182 | - name: None, |
1183 | - id: None, |
1184 | - address: None, |
1185 | - description: None, |
1186 | - archive_url: None, |
1187 | - owner_local_part: None, |
1188 | - request_local_part: None, |
1189 | - verify: None, |
1190 | - hidden: None, |
1191 | - enabled: None, |
1192 | - } |
1193 | - ) { |
1194 | - return self.list(change_set.pk).map(|_| ()); |
1195 | - } |
1196 | - |
1197 | - let MailingListChangeset { |
1198 | - pk, |
1199 | - name, |
1200 | - id, |
1201 | - address, |
1202 | - description, |
1203 | - archive_url, |
1204 | - owner_local_part, |
1205 | - request_local_part, |
1206 | - verify, |
1207 | - hidden, |
1208 | - enabled, |
1209 | - } = change_set; |
1210 | - let tx = self.connection.transaction()?; |
1211 | - |
1212 | - macro_rules! update { |
1213 | - ($field:tt) => {{ |
1214 | - if let Some($field) = $field { |
1215 | - tx.execute( |
1216 | - concat!("UPDATE list SET ", stringify!($field), " = ? WHERE pk = ?;"), |
1217 | - rusqlite::params![&$field, &pk], |
1218 | - )?; |
1219 | - } |
1220 | - }}; |
1221 | - } |
1222 | - update!(name); |
1223 | - update!(id); |
1224 | - update!(address); |
1225 | - update!(description); |
1226 | - update!(archive_url); |
1227 | - update!(owner_local_part); |
1228 | - update!(request_local_part); |
1229 | - update!(verify); |
1230 | - update!(hidden); |
1231 | - update!(enabled); |
1232 | - |
1233 | - tx.commit()?; |
1234 | - Ok(()) |
1235 | - } |
1236 | - |
1237 | - /// Return the post filters of a mailing list. |
1238 | - pub fn list_filters( |
1239 | - &self, |
1240 | - _list: &DbVal<MailingList>, |
1241 | - ) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> { |
1242 | - use crate::mail::message_filters::*; |
1243 | - vec![ |
1244 | - Box::new(FixCRLF), |
1245 | - Box::new(PostRightsCheck), |
1246 | - Box::new(AddListHeaders), |
1247 | - Box::new(FinalizeRecipients), |
1248 | - ] |
1249 | - } |
1250 | - } |
1251 | diff --git a/core/src/db/policies.rs b/core/src/db/policies.rs |
1252 | deleted file mode 100644 |
1253 | index 27326ed..0000000 |
1254 | --- a/core/src/db/policies.rs |
1255 | +++ /dev/null |
1256 | @@ -1,392 +0,0 @@ |
1257 | - /* |
1258 | - * This file is part of mailpot |
1259 | - * |
1260 | - * Copyright 2020 - Manos Pitsidianakis |
1261 | - * |
1262 | - * This program is free software: you can redistribute it and/or modify |
1263 | - * it under the terms of the GNU Affero General Public License as |
1264 | - * published by the Free Software Foundation, either version 3 of the |
1265 | - * License, or (at your option) any later version. |
1266 | - * |
1267 | - * This program is distributed in the hope that it will be useful, |
1268 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1269 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1270 | - * GNU Affero General Public License for more details. |
1271 | - * |
1272 | - * You should have received a copy of the GNU Affero General Public License |
1273 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
1274 | - */ |
1275 | - |
1276 | - pub use post_policy::*; |
1277 | - pub use subscription_policy::*; |
1278 | - |
1279 | - use super::*; |
1280 | - mod post_policy { |
1281 | - use super::*; |
1282 | - |
1283 | - impl Connection { |
1284 | - /// Fetch the post policy of a mailing list. |
1285 | - pub fn list_post_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> { |
1286 | - let mut stmt = self |
1287 | - .connection |
1288 | - .prepare("SELECT * FROM post_policy WHERE list = ?;")?; |
1289 | - let ret = stmt |
1290 | - .query_row([&pk], |row| { |
1291 | - let pk = row.get("pk")?; |
1292 | - Ok(DbVal( |
1293 | - PostPolicy { |
1294 | - pk, |
1295 | - list: row.get("list")?, |
1296 | - announce_only: row.get("announce_only")?, |
1297 | - subscription_only: row.get("subscription_only")?, |
1298 | - approval_needed: row.get("approval_needed")?, |
1299 | - open: row.get("open")?, |
1300 | - custom: row.get("custom")?, |
1301 | - }, |
1302 | - pk, |
1303 | - )) |
1304 | - }) |
1305 | - .optional()?; |
1306 | - |
1307 | - Ok(ret) |
1308 | - } |
1309 | - |
1310 | - /// Remove an existing list policy. |
1311 | - /// |
1312 | - /// ``` |
1313 | - /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
1314 | - /// # use tempfile::TempDir; |
1315 | - /// |
1316 | - /// # let tmp_dir = TempDir::new().unwrap(); |
1317 | - /// # let db_path = tmp_dir.path().join("mpot.db"); |
1318 | - /// # let config = Configuration { |
1319 | - /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
1320 | - /// # db_path: db_path.clone(), |
1321 | - /// # data_path: tmp_dir.path().to_path_buf(), |
1322 | - /// # administrators: vec![], |
1323 | - /// # }; |
1324 | - /// |
1325 | - /// # fn do_test(config: Configuration) { |
1326 | - /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
1327 | - /// # assert!(db.list_post_policy(1).unwrap().is_none()); |
1328 | - /// let list = db |
1329 | - /// .create_list(MailingList { |
1330 | - /// pk: 0, |
1331 | - /// name: "foobar chat".into(), |
1332 | - /// id: "foo-chat".into(), |
1333 | - /// address: "foo-chat@example.com".into(), |
1334 | - /// description: None, |
1335 | - /// archive_url: None, |
1336 | - /// }) |
1337 | - /// .unwrap(); |
1338 | - /// |
1339 | - /// # assert!(db.list_post_policy(list.pk()).unwrap().is_none()); |
1340 | - /// let pol = db |
1341 | - /// .set_list_post_policy(PostPolicy { |
1342 | - /// pk: -1, |
1343 | - /// list: list.pk(), |
1344 | - /// announce_only: false, |
1345 | - /// subscription_only: true, |
1346 | - /// approval_needed: false, |
1347 | - /// open: false, |
1348 | - /// custom: false, |
1349 | - /// }) |
1350 | - /// .unwrap(); |
1351 | - /// # assert_eq!(db.list_post_policy(list.pk()).unwrap().as_ref(), Some(&pol)); |
1352 | - /// db.remove_list_post_policy(list.pk(), pol.pk()).unwrap(); |
1353 | - /// # assert!(db.list_post_policy(list.pk()).unwrap().is_none()); |
1354 | - /// # } |
1355 | - /// # do_test(config); |
1356 | - /// ``` |
1357 | - pub fn remove_list_post_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> { |
1358 | - let mut stmt = self |
1359 | - .connection |
1360 | - .prepare("DELETE FROM post_policy WHERE pk = ? AND list = ? RETURNING *;")?; |
1361 | - stmt.query_row(rusqlite::params![&policy_pk, &list_pk,], |_| Ok(())) |
1362 | - .map_err(|err| { |
1363 | - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
1364 | - Error::from(err).chain_err(|| NotFound("list or list policy not found!")) |
1365 | - } else { |
1366 | - err.into() |
1367 | - } |
1368 | - })?; |
1369 | - |
1370 | - trace!("remove_list_post_policy {} {}.", list_pk, policy_pk); |
1371 | - Ok(()) |
1372 | - } |
1373 | - |
1374 | - /// ```should_panic |
1375 | - /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
1376 | - /// # use tempfile::TempDir; |
1377 | - /// |
1378 | - /// # let tmp_dir = TempDir::new().unwrap(); |
1379 | - /// # let db_path = tmp_dir.path().join("mpot.db"); |
1380 | - /// # let config = Configuration { |
1381 | - /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
1382 | - /// # db_path: db_path.clone(), |
1383 | - /// # data_path: tmp_dir.path().to_path_buf(), |
1384 | - /// # administrators: vec![], |
1385 | - /// # }; |
1386 | - /// |
1387 | - /// # fn do_test(config: Configuration) { |
1388 | - /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
1389 | - /// db.remove_list_post_policy(1, 1).unwrap(); |
1390 | - /// # } |
1391 | - /// # do_test(config); |
1392 | - /// ``` |
1393 | - #[cfg(doc)] |
1394 | - pub fn remove_list_post_policy_panic() {} |
1395 | - |
1396 | - /// Set the unique post policy for a list. |
1397 | - pub fn set_list_post_policy(&self, policy: PostPolicy) -> Result<DbVal<PostPolicy>> { |
1398 | - if !(policy.announce_only |
1399 | - || policy.subscription_only |
1400 | - || policy.approval_needed |
1401 | - || policy.open |
1402 | - || policy.custom) |
1403 | - { |
1404 | - return Err( |
1405 | - "Cannot add empty policy. Having no policies is probably what you want to do." |
1406 | - .into(), |
1407 | - ); |
1408 | - } |
1409 | - let list_pk = policy.list; |
1410 | - |
1411 | - let mut stmt = self.connection.prepare( |
1412 | - "INSERT OR REPLACE INTO post_policy(list, announce_only, subscription_only, \ |
1413 | - approval_needed, open, custom) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;", |
1414 | - )?; |
1415 | - let ret = stmt |
1416 | - .query_row( |
1417 | - rusqlite::params![ |
1418 | - &list_pk, |
1419 | - &policy.announce_only, |
1420 | - &policy.subscription_only, |
1421 | - &policy.approval_needed, |
1422 | - &policy.open, |
1423 | - &policy.custom, |
1424 | - ], |
1425 | - |row| { |
1426 | - let pk = row.get("pk")?; |
1427 | - Ok(DbVal( |
1428 | - PostPolicy { |
1429 | - pk, |
1430 | - list: row.get("list")?, |
1431 | - announce_only: row.get("announce_only")?, |
1432 | - subscription_only: row.get("subscription_only")?, |
1433 | - approval_needed: row.get("approval_needed")?, |
1434 | - open: row.get("open")?, |
1435 | - custom: row.get("custom")?, |
1436 | - }, |
1437 | - pk, |
1438 | - )) |
1439 | - }, |
1440 | - ) |
1441 | - .map_err(|err| { |
1442 | - if matches!( |
1443 | - err, |
1444 | - rusqlite::Error::SqliteFailure( |
1445 | - rusqlite::ffi::Error { |
1446 | - code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
1447 | - extended_code: 787 |
1448 | - }, |
1449 | - _ |
1450 | - ) |
1451 | - ) { |
1452 | - Error::from(err) |
1453 | - .chain_err(|| NotFound("Could not find a list with this pk.")) |
1454 | - } else { |
1455 | - err.into() |
1456 | - } |
1457 | - })?; |
1458 | - |
1459 | - trace!("set_list_post_policy {:?}.", &ret); |
1460 | - Ok(ret) |
1461 | - } |
1462 | - } |
1463 | - } |
1464 | - |
1465 | - mod subscription_policy { |
1466 | - use super::*; |
1467 | - |
1468 | - impl Connection { |
1469 | - /// Fetch the subscription policy of a mailing list. |
1470 | - pub fn list_subscription_policy( |
1471 | - &self, |
1472 | - pk: i64, |
1473 | - ) -> Result<Option<DbVal<SubscriptionPolicy>>> { |
1474 | - let mut stmt = self |
1475 | - .connection |
1476 | - .prepare("SELECT * FROM subscription_policy WHERE list = ?;")?; |
1477 | - let ret = stmt |
1478 | - .query_row([&pk], |row| { |
1479 | - let pk = row.get("pk")?; |
1480 | - Ok(DbVal( |
1481 | - SubscriptionPolicy { |
1482 | - pk, |
1483 | - list: row.get("list")?, |
1484 | - send_confirmation: row.get("send_confirmation")?, |
1485 | - open: row.get("open")?, |
1486 | - manual: row.get("manual")?, |
1487 | - request: row.get("request")?, |
1488 | - custom: row.get("custom")?, |
1489 | - }, |
1490 | - pk, |
1491 | - )) |
1492 | - }) |
1493 | - .optional()?; |
1494 | - |
1495 | - Ok(ret) |
1496 | - } |
1497 | - |
1498 | - /// Remove an existing subscription policy. |
1499 | - /// |
1500 | - /// ``` |
1501 | - /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
1502 | - /// # use tempfile::TempDir; |
1503 | - /// |
1504 | - /// # let tmp_dir = TempDir::new().unwrap(); |
1505 | - /// # let db_path = tmp_dir.path().join("mpot.db"); |
1506 | - /// # let config = Configuration { |
1507 | - /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
1508 | - /// # db_path: db_path.clone(), |
1509 | - /// # data_path: tmp_dir.path().to_path_buf(), |
1510 | - /// # administrators: vec![], |
1511 | - /// # }; |
1512 | - /// |
1513 | - /// # fn do_test(config: Configuration) { |
1514 | - /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
1515 | - /// let list = db |
1516 | - /// .create_list(MailingList { |
1517 | - /// pk: 0, |
1518 | - /// name: "foobar chat".into(), |
1519 | - /// id: "foo-chat".into(), |
1520 | - /// address: "foo-chat@example.com".into(), |
1521 | - /// description: None, |
1522 | - /// archive_url: None, |
1523 | - /// }) |
1524 | - /// .unwrap(); |
1525 | - /// # assert!(db.list_subscription_policy(list.pk()).unwrap().is_none()); |
1526 | - /// let pol = db |
1527 | - /// .set_list_subscription_policy(SubscriptionPolicy { |
1528 | - /// pk: -1, |
1529 | - /// list: list.pk(), |
1530 | - /// send_confirmation: false, |
1531 | - /// open: true, |
1532 | - /// manual: false, |
1533 | - /// request: false, |
1534 | - /// custom: false, |
1535 | - /// }) |
1536 | - /// .unwrap(); |
1537 | - /// # assert_eq!(db.list_subscription_policy(list.pk()).unwrap().as_ref(), Some(&pol)); |
1538 | - /// db.remove_list_subscription_policy(list.pk(), pol.pk()) |
1539 | - /// .unwrap(); |
1540 | - /// # assert!(db.list_subscription_policy(list.pk()).unwrap().is_none()); |
1541 | - /// # } |
1542 | - /// # do_test(config); |
1543 | - /// ``` |
1544 | - pub fn remove_list_subscription_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> { |
1545 | - let mut stmt = self.connection.prepare( |
1546 | - "DELETE FROM subscription_policy WHERE pk = ? AND list = ? RETURNING *;", |
1547 | - )?; |
1548 | - stmt.query_row(rusqlite::params![&policy_pk, &list_pk,], |_| Ok(())) |
1549 | - .map_err(|err| { |
1550 | - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
1551 | - Error::from(err).chain_err(|| NotFound("list or list policy not found!")) |
1552 | - } else { |
1553 | - err.into() |
1554 | - } |
1555 | - })?; |
1556 | - |
1557 | - trace!("remove_list_subscription_policy {} {}.", list_pk, policy_pk); |
1558 | - Ok(()) |
1559 | - } |
1560 | - |
1561 | - /// ```should_panic |
1562 | - /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
1563 | - /// # use tempfile::TempDir; |
1564 | - /// |
1565 | - /// # let tmp_dir = TempDir::new().unwrap(); |
1566 | - /// # let db_path = tmp_dir.path().join("mpot.db"); |
1567 | - /// # let config = Configuration { |
1568 | - /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
1569 | - /// # db_path: db_path.clone(), |
1570 | - /// # data_path: tmp_dir.path().to_path_buf(), |
1571 | - /// # administrators: vec![], |
1572 | - /// # }; |
1573 | - /// |
1574 | - /// # fn do_test(config: Configuration) { |
1575 | - /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
1576 | - /// db.remove_list_post_policy(1, 1).unwrap(); |
1577 | - /// # } |
1578 | - /// # do_test(config); |
1579 | - /// ``` |
1580 | - #[cfg(doc)] |
1581 | - pub fn remove_list_subscription_policy_panic() {} |
1582 | - |
1583 | - /// Set the unique post policy for a list. |
1584 | - pub fn set_list_subscription_policy( |
1585 | - &self, |
1586 | - policy: SubscriptionPolicy, |
1587 | - ) -> Result<DbVal<SubscriptionPolicy>> { |
1588 | - if !(policy.open || policy.manual || policy.request || policy.custom) { |
1589 | - return Err( |
1590 | - "Cannot add empty policy. Having no policy is probably what you want to do." |
1591 | - .into(), |
1592 | - ); |
1593 | - } |
1594 | - let list_pk = policy.list; |
1595 | - |
1596 | - let mut stmt = self.connection.prepare( |
1597 | - "INSERT OR REPLACE INTO subscription_policy(list, send_confirmation, open, \ |
1598 | - manual, request, custom) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;", |
1599 | - )?; |
1600 | - let ret = stmt |
1601 | - .query_row( |
1602 | - rusqlite::params![ |
1603 | - &list_pk, |
1604 | - &policy.send_confirmation, |
1605 | - &policy.open, |
1606 | - &policy.manual, |
1607 | - &policy.request, |
1608 | - &policy.custom, |
1609 | - ], |
1610 | - |row| { |
1611 | - let pk = row.get("pk")?; |
1612 | - Ok(DbVal( |
1613 | - SubscriptionPolicy { |
1614 | - pk, |
1615 | - list: row.get("list")?, |
1616 | - send_confirmation: row.get("send_confirmation")?, |
1617 | - open: row.get("open")?, |
1618 | - manual: row.get("manual")?, |
1619 | - request: row.get("request")?, |
1620 | - custom: row.get("custom")?, |
1621 | - }, |
1622 | - pk, |
1623 | - )) |
1624 | - }, |
1625 | - ) |
1626 | - .map_err(|err| { |
1627 | - if matches!( |
1628 | - err, |
1629 | - rusqlite::Error::SqliteFailure( |
1630 | - rusqlite::ffi::Error { |
1631 | - code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
1632 | - extended_code: 787 |
1633 | - }, |
1634 | - _ |
1635 | - ) |
1636 | - ) { |
1637 | - Error::from(err) |
1638 | - .chain_err(|| NotFound("Could not find a list with this pk.")) |
1639 | - } else { |
1640 | - err.into() |
1641 | - } |
1642 | - })?; |
1643 | - |
1644 | - trace!("set_list_subscription_policy {:?}.", &ret); |
1645 | - Ok(ret) |
1646 | - } |
1647 | - } |
1648 | - } |
1649 | diff --git a/core/src/db/posts.rs b/core/src/db/posts.rs |
1650 | deleted file mode 100644 |
1651 | index ee733ed..0000000 |
1652 | --- a/core/src/db/posts.rs |
1653 | +++ /dev/null |
1654 | @@ -1,769 +0,0 @@ |
1655 | - /* |
1656 | - * This file is part of mailpot |
1657 | - * |
1658 | - * Copyright 2020 - Manos Pitsidianakis |
1659 | - * |
1660 | - * This program is free software: you can redistribute it and/or modify |
1661 | - * it under the terms of the GNU Affero General Public License as |
1662 | - * published by the Free Software Foundation, either version 3 of the |
1663 | - * License, or (at your option) any later version. |
1664 | - * |
1665 | - * This program is distributed in the hope that it will be useful, |
1666 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1667 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1668 | - * GNU Affero General Public License for more details. |
1669 | - * |
1670 | - * You should have received a copy of the GNU Affero General Public License |
1671 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
1672 | - */ |
1673 | - |
1674 | - use std::borrow::Cow; |
1675 | - |
1676 | - use super::*; |
1677 | - use crate::mail::ListRequest; |
1678 | - |
1679 | - impl Connection { |
1680 | - /// Insert a mailing list post into the database. |
1681 | - pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> { |
1682 | - let from_ = env.from(); |
1683 | - let address = if from_.is_empty() { |
1684 | - String::new() |
1685 | - } else { |
1686 | - from_[0].get_email() |
1687 | - }; |
1688 | - let datetime: std::borrow::Cow<'_, str> = if !env.date.as_str().is_empty() { |
1689 | - env.date.as_str().into() |
1690 | - } else { |
1691 | - melib::datetime::timestamp_to_string( |
1692 | - env.timestamp, |
1693 | - Some(melib::datetime::RFC822_DATE), |
1694 | - true, |
1695 | - ) |
1696 | - .into() |
1697 | - }; |
1698 | - let message_id = env.message_id_display(); |
1699 | - let mut stmt = self.connection.prepare( |
1700 | - "INSERT OR REPLACE INTO post(list, address, message_id, message, datetime, timestamp) \ |
1701 | - VALUES(?, ?, ?, ?, ?, ?) RETURNING pk;", |
1702 | - )?; |
1703 | - let pk = stmt.query_row( |
1704 | - rusqlite::params![ |
1705 | - &list_pk, |
1706 | - &address, |
1707 | - &message_id, |
1708 | - &message, |
1709 | - &datetime, |
1710 | - &env.timestamp |
1711 | - ], |
1712 | - |row| { |
1713 | - let pk: i64 = row.get("pk")?; |
1714 | - Ok(pk) |
1715 | - }, |
1716 | - )?; |
1717 | - |
1718 | - trace!( |
1719 | - "insert_post list_pk {}, from {:?} message_id {:?} post_pk {}.", |
1720 | - list_pk, |
1721 | - address, |
1722 | - message_id, |
1723 | - pk |
1724 | - ); |
1725 | - Ok(pk) |
1726 | - } |
1727 | - |
1728 | - /// Process a new mailing list post. |
1729 | - pub fn post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { |
1730 | - let result = self.inner_post(env, raw, _dry_run); |
1731 | - if let Err(err) = result { |
1732 | - return match self.insert_to_queue(QueueEntry::new( |
1733 | - Queue::Error, |
1734 | - None, |
1735 | - Some(Cow::Borrowed(env)), |
1736 | - raw, |
1737 | - Some(err.to_string()), |
1738 | - )?) { |
1739 | - Ok(idx) => { |
1740 | - log::info!( |
1741 | - "Inserted mail from {:?} into error_queue at index {}", |
1742 | - env.from(), |
1743 | - idx |
1744 | - ); |
1745 | - Err(err) |
1746 | - } |
1747 | - Err(err2) => { |
1748 | - log::error!( |
1749 | - "Could not insert mail from {:?} into error_queue: {err2}", |
1750 | - env.from(), |
1751 | - ); |
1752 | - |
1753 | - Err(err.chain_err(|| err2)) |
1754 | - } |
1755 | - }; |
1756 | - } |
1757 | - result |
1758 | - } |
1759 | - |
1760 | - fn inner_post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { |
1761 | - trace!("Received envelope to post: {:#?}", &env); |
1762 | - let tos = env.to().to_vec(); |
1763 | - if tos.is_empty() { |
1764 | - return Err("Envelope To: field is empty!".into()); |
1765 | - } |
1766 | - if env.from().is_empty() { |
1767 | - return Err("Envelope From: field is empty!".into()); |
1768 | - } |
1769 | - let mut lists = self.lists()?; |
1770 | - if lists.is_empty() { |
1771 | - return Err("No active mailing lists found.".into()); |
1772 | - } |
1773 | - let prev_list_len = lists.len(); |
1774 | - for t in &tos { |
1775 | - if let Some((addr, subaddr)) = t.subaddress("+") { |
1776 | - lists.retain(|list| { |
1777 | - if !addr.contains_address(&list.address()) { |
1778 | - return true; |
1779 | - } |
1780 | - if let Err(err) = ListRequest::try_from((subaddr.as_str(), env)) |
1781 | - .and_then(|req| self.request(list, req, env, raw)) |
1782 | - { |
1783 | - info!("Processing request returned error: {}", err); |
1784 | - } |
1785 | - false |
1786 | - }); |
1787 | - if lists.len() != prev_list_len { |
1788 | - // Was request, handled above. |
1789 | - return Ok(()); |
1790 | - } |
1791 | - } |
1792 | - } |
1793 | - |
1794 | - lists.retain(|list| { |
1795 | - trace!( |
1796 | - "Is post related to list {}? {}", |
1797 | - &list, |
1798 | - tos.iter().any(|a| a.contains_address(&list.address())) |
1799 | - ); |
1800 | - |
1801 | - tos.iter().any(|a| a.contains_address(&list.address())) |
1802 | - }); |
1803 | - if lists.is_empty() { |
1804 | - return Err(format!( |
1805 | - "No relevant mailing list found for these addresses: {:?}", |
1806 | - tos |
1807 | - ) |
1808 | - .into()); |
1809 | - } |
1810 | - |
1811 | - trace!("Configuration is {:#?}", &self.conf); |
1812 | - use crate::mail::{ListContext, Post, PostAction}; |
1813 | - for mut list in lists { |
1814 | - trace!("Examining list {}", list.display_name()); |
1815 | - let filters = self.list_filters(&list); |
1816 | - let subscriptions = self.list_subscriptions(list.pk)?; |
1817 | - let owners = self.list_owners(list.pk)?; |
1818 | - trace!("List subscriptions {:#?}", &subscriptions); |
1819 | - let mut list_ctx = ListContext { |
1820 | - post_policy: self.list_post_policy(list.pk)?, |
1821 | - subscription_policy: self.list_subscription_policy(list.pk)?, |
1822 | - list_owners: &owners, |
1823 | - list: &mut list, |
1824 | - subscriptions: &subscriptions, |
1825 | - scheduled_jobs: vec![], |
1826 | - }; |
1827 | - let mut post = Post { |
1828 | - from: env.from()[0].clone(), |
1829 | - bytes: raw.to_vec(), |
1830 | - to: env.to().to_vec(), |
1831 | - action: PostAction::Hold, |
1832 | - }; |
1833 | - let result = filters |
1834 | - .into_iter() |
1835 | - .fold(Ok((&mut post, &mut list_ctx)), |p, f| { |
1836 | - p.and_then(|(p, c)| f.feed(p, c)) |
1837 | - }); |
1838 | - trace!("result {:#?}", result); |
1839 | - |
1840 | - let Post { bytes, action, .. } = post; |
1841 | - trace!("Action is {:#?}", action); |
1842 | - let post_env = melib::Envelope::from_bytes(&bytes, None)?; |
1843 | - match action { |
1844 | - PostAction::Accept => { |
1845 | - let _post_pk = self.insert_post(list_ctx.list.pk, &bytes, &post_env)?; |
1846 | - trace!("post_pk is {:#?}", _post_pk); |
1847 | - for job in list_ctx.scheduled_jobs.iter() { |
1848 | - trace!("job is {:#?}", &job); |
1849 | - if let crate::mail::MailJob::Send { recipients } = job { |
1850 | - trace!("recipients: {:?}", &recipients); |
1851 | - if recipients.is_empty() { |
1852 | - trace!("list has no recipients"); |
1853 | - } |
1854 | - for recipient in recipients { |
1855 | - let mut env = post_env.clone(); |
1856 | - env.set_to(melib::smallvec::smallvec![recipient.clone()]); |
1857 | - self.insert_to_queue(QueueEntry::new( |
1858 | - Queue::Out, |
1859 | - Some(list.pk), |
1860 | - Some(Cow::Owned(env)), |
1861 | - &bytes, |
1862 | - None, |
1863 | - )?)?; |
1864 | - } |
1865 | - } |
1866 | - } |
1867 | - } |
1868 | - PostAction::Reject { reason } => { |
1869 | - log::info!("PostAction::Reject {{ reason: {} }}", reason); |
1870 | - for f in env.from() { |
1871 | - /* send error notice to e-mail sender */ |
1872 | - self.send_reply_with_list_template( |
1873 | - TemplateRenderContext { |
1874 | - template: Template::GENERIC_FAILURE, |
1875 | - default_fn: Some(Template::default_generic_failure), |
1876 | - list: &list, |
1877 | - context: minijinja::context! { |
1878 | - list => &list, |
1879 | - subject => format!("Your post to {} was rejected.", list.id), |
1880 | - details => &reason, |
1881 | - }, |
1882 | - queue: Queue::Out, |
1883 | - comment: format!("PostAction::Reject {{ reason: {} }}", reason) |
1884 | - .into(), |
1885 | - }, |
1886 | - std::iter::once(Cow::Borrowed(f)), |
1887 | - )?; |
1888 | - } |
1889 | - /* error handled by notifying submitter */ |
1890 | - return Ok(()); |
1891 | - } |
1892 | - PostAction::Defer { reason } => { |
1893 | - trace!("PostAction::Defer {{ reason: {} }}", reason); |
1894 | - for f in env.from() { |
1895 | - /* send error notice to e-mail sender */ |
1896 | - self.send_reply_with_list_template( |
1897 | - TemplateRenderContext { |
1898 | - template: Template::GENERIC_FAILURE, |
1899 | - default_fn: Some(Template::default_generic_failure), |
1900 | - list: &list, |
1901 | - context: minijinja::context! { |
1902 | - list => &list, |
1903 | - subject => format!("Your post to {} was deferred.", list.id), |
1904 | - details => &reason, |
1905 | - }, |
1906 | - queue: Queue::Out, |
1907 | - comment: format!("PostAction::Defer {{ reason: {} }}", reason) |
1908 | - .into(), |
1909 | - }, |
1910 | - std::iter::once(Cow::Borrowed(f)), |
1911 | - )?; |
1912 | - } |
1913 | - self.insert_to_queue(QueueEntry::new( |
1914 | - Queue::Deferred, |
1915 | - Some(list.pk), |
1916 | - Some(Cow::Borrowed(&post_env)), |
1917 | - &bytes, |
1918 | - Some(format!("PostAction::Defer {{ reason: {} }}", reason)), |
1919 | - )?)?; |
1920 | - return Ok(()); |
1921 | - } |
1922 | - PostAction::Hold => { |
1923 | - trace!("PostAction::Hold"); |
1924 | - self.insert_to_queue(QueueEntry::new( |
1925 | - Queue::Hold, |
1926 | - Some(list.pk), |
1927 | - Some(Cow::Borrowed(&post_env)), |
1928 | - &bytes, |
1929 | - Some("PostAction::Hold".to_string()), |
1930 | - )?)?; |
1931 | - return Ok(()); |
1932 | - } |
1933 | - } |
1934 | - } |
1935 | - |
1936 | - Ok(()) |
1937 | - } |
1938 | - |
1939 | - /// Process a new mailing list request. |
1940 | - pub fn request( |
1941 | - &mut self, |
1942 | - list: &DbVal<MailingList>, |
1943 | - request: ListRequest, |
1944 | - env: &Envelope, |
1945 | - raw: &[u8], |
1946 | - ) -> Result<()> { |
1947 | - let post_policy = self.list_post_policy(list.pk)?; |
1948 | - match request { |
1949 | - ListRequest::Help => { |
1950 | - trace!( |
1951 | - "help action for addresses {:?} in list {}", |
1952 | - env.from(), |
1953 | - list |
1954 | - ); |
1955 | - let subscription_policy = self.list_subscription_policy(list.pk)?; |
1956 | - let subject = format!("Help for {}", list.name); |
1957 | - let details = list |
1958 | - .generate_help_email(post_policy.as_deref(), subscription_policy.as_deref()); |
1959 | - for f in env.from() { |
1960 | - self.send_reply_with_list_template( |
1961 | - TemplateRenderContext { |
1962 | - template: Template::GENERIC_HELP, |
1963 | - default_fn: Some(Template::default_generic_help), |
1964 | - list, |
1965 | - context: minijinja::context! { |
1966 | - list => &list, |
1967 | - subject => &subject, |
1968 | - details => &details, |
1969 | - }, |
1970 | - queue: Queue::Out, |
1971 | - comment: "Help request".into(), |
1972 | - }, |
1973 | - std::iter::once(Cow::Borrowed(f)), |
1974 | - )?; |
1975 | - } |
1976 | - } |
1977 | - ListRequest::Subscribe => { |
1978 | - trace!( |
1979 | - "subscribe action for addresses {:?} in list {}", |
1980 | - env.from(), |
1981 | - list |
1982 | - ); |
1983 | - let approval_needed = post_policy |
1984 | - .as_ref() |
1985 | - .map(|p| p.approval_needed) |
1986 | - .unwrap_or(false); |
1987 | - for f in env.from() { |
1988 | - let email_from = f.get_email(); |
1989 | - if self |
1990 | - .list_subscription_by_address(list.pk, &email_from) |
1991 | - .is_ok() |
1992 | - { |
1993 | - /* send error notice to e-mail sender */ |
1994 | - self.send_reply_with_list_template( |
1995 | - TemplateRenderContext { |
1996 | - template: Template::GENERIC_FAILURE, |
1997 | - default_fn: Some(Template::default_generic_failure), |
1998 | - list, |
1999 | - context: minijinja::context! { |
2000 | - list => &list, |
2001 | - subject => format!("You are already subscribed to {}.", list.id), |
2002 | - details => "No action has been taken since you are already subscribed to the list.", |
2003 | - }, |
2004 | - queue: Queue::Out, |
2005 | - comment: format!("Address {} is already subscribed to list {}", f, list.id).into(), |
2006 | - }, |
2007 | - std::iter::once(Cow::Borrowed(f)), |
2008 | - )?; |
2009 | - continue; |
2010 | - } |
2011 | - |
2012 | - let subscription = ListSubscription { |
2013 | - pk: 0, |
2014 | - list: list.pk, |
2015 | - address: f.get_email(), |
2016 | - account: None, |
2017 | - name: f.get_display_name(), |
2018 | - digest: false, |
2019 | - hide_address: false, |
2020 | - receive_duplicates: true, |
2021 | - receive_own_posts: false, |
2022 | - receive_confirmation: true, |
2023 | - enabled: !approval_needed, |
2024 | - verified: true, |
2025 | - }; |
2026 | - if approval_needed { |
2027 | - match self.add_candidate_subscription(list.pk, subscription) { |
2028 | - Ok(v) => { |
2029 | - let list_owners = self.list_owners(list.pk)?; |
2030 | - self.send_reply_with_list_template( |
2031 | - TemplateRenderContext { |
2032 | - template: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER, |
2033 | - default_fn: Some( |
2034 | - Template::default_subscription_request_owner, |
2035 | - ), |
2036 | - list, |
2037 | - context: minijinja::context! { |
2038 | - list => &list, |
2039 | - candidate => &v, |
2040 | - }, |
2041 | - queue: Queue::Out, |
2042 | - comment: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER.into(), |
2043 | - }, |
2044 | - list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
2045 | - )?; |
2046 | - } |
2047 | - Err(err) => { |
2048 | - log::error!( |
2049 | - "Could not create candidate subscription for {f:?}: {err}" |
2050 | - ); |
2051 | - /* send error notice to e-mail sender */ |
2052 | - self.send_reply_with_list_template( |
2053 | - TemplateRenderContext { |
2054 | - template: Template::GENERIC_FAILURE, |
2055 | - default_fn: Some(Template::default_generic_failure), |
2056 | - list, |
2057 | - context: minijinja::context! { |
2058 | - list => &list, |
2059 | - }, |
2060 | - queue: Queue::Out, |
2061 | - comment: format!( |
2062 | - "Could not create candidate subscription for {f:?}: \ |
2063 | - {err}" |
2064 | - ) |
2065 | - .into(), |
2066 | - }, |
2067 | - std::iter::once(Cow::Borrowed(f)), |
2068 | - )?; |
2069 | - |
2070 | - /* send error details to list owners */ |
2071 | - |
2072 | - let list_owners = self.list_owners(list.pk)?; |
2073 | - self.send_reply_with_list_template( |
2074 | - TemplateRenderContext { |
2075 | - template: Template::ADMIN_NOTICE, |
2076 | - default_fn: Some(Template::default_admin_notice), |
2077 | - list, |
2078 | - context: minijinja::context! { |
2079 | - list => &list, |
2080 | - details => err.to_string(), |
2081 | - }, |
2082 | - queue: Queue::Out, |
2083 | - comment: format!( |
2084 | - "Could not create candidate subscription for {f:?}: \ |
2085 | - {err}" |
2086 | - ) |
2087 | - .into(), |
2088 | - }, |
2089 | - list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
2090 | - )?; |
2091 | - } |
2092 | - } |
2093 | - } else if let Err(err) = self.add_subscription(list.pk, subscription) { |
2094 | - log::error!("Could not create subscription for {f:?}: {err}"); |
2095 | - |
2096 | - /* send error notice to e-mail sender */ |
2097 | - |
2098 | - self.send_reply_with_list_template( |
2099 | - TemplateRenderContext { |
2100 | - template: Template::GENERIC_FAILURE, |
2101 | - default_fn: Some(Template::default_generic_failure), |
2102 | - list, |
2103 | - context: minijinja::context! { |
2104 | - list => &list, |
2105 | - }, |
2106 | - queue: Queue::Out, |
2107 | - comment: format!("Could not create subscription for {f:?}: {err}") |
2108 | - .into(), |
2109 | - }, |
2110 | - std::iter::once(Cow::Borrowed(f)), |
2111 | - )?; |
2112 | - |
2113 | - /* send error details to list owners */ |
2114 | - |
2115 | - let list_owners = self.list_owners(list.pk)?; |
2116 | - self.send_reply_with_list_template( |
2117 | - TemplateRenderContext { |
2118 | - template: Template::ADMIN_NOTICE, |
2119 | - default_fn: Some(Template::default_admin_notice), |
2120 | - list, |
2121 | - context: minijinja::context! { |
2122 | - list => &list, |
2123 | - details => err.to_string(), |
2124 | - }, |
2125 | - queue: Queue::Out, |
2126 | - comment: format!("Could not create subscription for {f:?}: {err}") |
2127 | - .into(), |
2128 | - }, |
2129 | - list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
2130 | - )?; |
2131 | - } else { |
2132 | - log::trace!( |
2133 | - "Added subscription to list {list:?} for address {f:?}, sending \ |
2134 | - confirmation." |
2135 | - ); |
2136 | - self.send_reply_with_list_template( |
2137 | - TemplateRenderContext { |
2138 | - template: Template::SUBSCRIPTION_CONFIRMATION, |
2139 | - default_fn: Some(Template::default_subscription_confirmation), |
2140 | - list, |
2141 | - context: minijinja::context! { |
2142 | - list => &list, |
2143 | - }, |
2144 | - queue: Queue::Out, |
2145 | - comment: Template::SUBSCRIPTION_CONFIRMATION.into(), |
2146 | - }, |
2147 | - std::iter::once(Cow::Borrowed(f)), |
2148 | - )?; |
2149 | - } |
2150 | - } |
2151 | - } |
2152 | - ListRequest::Unsubscribe => { |
2153 | - trace!( |
2154 | - "unsubscribe action for addresses {:?} in list {}", |
2155 | - env.from(), |
2156 | - list |
2157 | - ); |
2158 | - for f in env.from() { |
2159 | - if let Err(err) = self.remove_subscription(list.pk, &f.get_email()) { |
2160 | - log::error!("Could not unsubscribe {f:?}: {err}"); |
2161 | - /* send error notice to e-mail sender */ |
2162 | - |
2163 | - self.send_reply_with_list_template( |
2164 | - TemplateRenderContext { |
2165 | - template: Template::GENERIC_FAILURE, |
2166 | - default_fn: Some(Template::default_generic_failure), |
2167 | - list, |
2168 | - context: minijinja::context! { |
2169 | - list => &list, |
2170 | - }, |
2171 | - queue: Queue::Out, |
2172 | - comment: format!("Could not unsubscribe {f:?}: {err}").into(), |
2173 | - }, |
2174 | - std::iter::once(Cow::Borrowed(f)), |
2175 | - )?; |
2176 | - |
2177 | - /* send error details to list owners */ |
2178 | - |
2179 | - let list_owners = self.list_owners(list.pk)?; |
2180 | - self.send_reply_with_list_template( |
2181 | - TemplateRenderContext { |
2182 | - template: Template::ADMIN_NOTICE, |
2183 | - default_fn: Some(Template::default_admin_notice), |
2184 | - list, |
2185 | - context: minijinja::context! { |
2186 | - list => &list, |
2187 | - details => err.to_string(), |
2188 | - }, |
2189 | - queue: Queue::Out, |
2190 | - comment: format!("Could not unsubscribe {f:?}: {err}").into(), |
2191 | - }, |
2192 | - list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
2193 | - )?; |
2194 | - } else { |
2195 | - self.send_reply_with_list_template( |
2196 | - TemplateRenderContext { |
2197 | - template: Template::UNSUBSCRIPTION_CONFIRMATION, |
2198 | - default_fn: Some(Template::default_unsubscription_confirmation), |
2199 | - list, |
2200 | - context: minijinja::context! { |
2201 | - list => &list, |
2202 | - }, |
2203 | - queue: Queue::Out, |
2204 | - comment: Template::UNSUBSCRIPTION_CONFIRMATION.into(), |
2205 | - }, |
2206 | - std::iter::once(Cow::Borrowed(f)), |
2207 | - )?; |
2208 | - } |
2209 | - } |
2210 | - } |
2211 | - ListRequest::Other(ref req) if req == "owner" => { |
2212 | - trace!( |
2213 | - "list-owner mail action for addresses {:?} in list {}", |
2214 | - env.from(), |
2215 | - list |
2216 | - ); |
2217 | - return Err("list-owner emails are not implemented yet.".into()); |
2218 | - //FIXME: mail to list-owner |
2219 | - /* |
2220 | - for _owner in self.list_owners(list.pk)? { |
2221 | - self.insert_to_queue( |
2222 | - Queue::Out, |
2223 | - Some(list.pk), |
2224 | - None, |
2225 | - draft.finalise()?.as_bytes(), |
2226 | - "list-owner-forward".to_string(), |
2227 | - )?; |
2228 | - } |
2229 | - */ |
2230 | - } |
2231 | - ListRequest::Other(ref req) if req.trim().eq_ignore_ascii_case("password") => { |
2232 | - trace!( |
2233 | - "list-request password set action for addresses {:?} in list {list}", |
2234 | - env.from(), |
2235 | - ); |
2236 | - let body = env.body_bytes(raw); |
2237 | - let password = body.text(); |
2238 | - // TODO: validate SSH public key with `ssh-keygen`. |
2239 | - for f in env.from() { |
2240 | - let email_from = f.get_email(); |
2241 | - if let Ok(sub) = self.list_subscription_by_address(list.pk, &email_from) { |
2242 | - match self.account_by_address(&email_from)? { |
2243 | - Some(_acc) => { |
2244 | - let changeset = AccountChangeset { |
2245 | - address: email_from.clone(), |
2246 | - name: None, |
2247 | - public_key: None, |
2248 | - password: Some(password.clone()), |
2249 | - enabled: None, |
2250 | - }; |
2251 | - self.update_account(changeset)?; |
2252 | - } |
2253 | - None => { |
2254 | - // Create new account. |
2255 | - self.add_account(Account { |
2256 | - pk: 0, |
2257 | - name: sub.name.clone(), |
2258 | - address: sub.address.clone(), |
2259 | - public_key: None, |
2260 | - password: password.clone(), |
2261 | - enabled: sub.enabled, |
2262 | - })?; |
2263 | - } |
2264 | - } |
2265 | - } |
2266 | - } |
2267 | - } |
2268 | - ListRequest::RetrieveMessages(ref message_ids) => { |
2269 | - trace!( |
2270 | - "retrieve messages {message_ids:?} action for addresses {:?} in list {list}", |
2271 | - env.from(), |
2272 | - ); |
2273 | - return Err("message retrievals are not implemented yet.".into()); |
2274 | - } |
2275 | - ListRequest::RetrieveArchive(ref from, ref to) => { |
2276 | - trace!( |
2277 | - "retrieve archive action from {from:?} to {to:?} for addresses {:?} in list \ |
2278 | - {list}", |
2279 | - env.from(), |
2280 | - ); |
2281 | - return Err("message retrievals are not implemented yet.".into()); |
2282 | - } |
2283 | - ListRequest::ChangeSetting(ref setting, ref toggle) => { |
2284 | - trace!( |
2285 | - "change setting {setting}, request with value {toggle:?} for addresses {:?} \ |
2286 | - in list {list}", |
2287 | - env.from(), |
2288 | - ); |
2289 | - return Err("setting digest options via e-mail is not implemented yet.".into()); |
2290 | - } |
2291 | - ListRequest::Other(ref req) => { |
2292 | - trace!( |
2293 | - "unknown request action {req} for addresses {:?} in list {list}", |
2294 | - env.from(), |
2295 | - ); |
2296 | - return Err(format!("Unknown request {req}.").into()); |
2297 | - } |
2298 | - } |
2299 | - Ok(()) |
2300 | - } |
2301 | - |
2302 | - /// Fetch all year and month values for which at least one post exists in |
2303 | - /// `yyyy-mm` format. |
2304 | - pub fn months(&self, list_pk: i64) -> Result<Vec<String>> { |
2305 | - let mut stmt = self.connection.prepare( |
2306 | - "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post \ |
2307 | - WHERE list = ?;", |
2308 | - )?; |
2309 | - let months_iter = stmt.query_map([list_pk], |row| { |
2310 | - let val: String = row.get(0)?; |
2311 | - Ok(val) |
2312 | - })?; |
2313 | - |
2314 | - let mut ret = vec![]; |
2315 | - for month in months_iter { |
2316 | - let month = month?; |
2317 | - ret.push(month); |
2318 | - } |
2319 | - Ok(ret) |
2320 | - } |
2321 | - |
2322 | - /// Find a post by its `Message-ID` email header. |
2323 | - pub fn list_post_by_message_id( |
2324 | - &self, |
2325 | - list_pk: i64, |
2326 | - message_id: &str, |
2327 | - ) -> Result<Option<DbVal<Post>>> { |
2328 | - let mut stmt = self.connection.prepare( |
2329 | - "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \ |
2330 | - FROM post WHERE list = ? AND message_id = ?;", |
2331 | - )?; |
2332 | - let ret = stmt |
2333 | - .query_row(rusqlite::params![&list_pk, &message_id], |row| { |
2334 | - let pk = row.get("pk")?; |
2335 | - Ok(DbVal( |
2336 | - Post { |
2337 | - pk, |
2338 | - list: row.get("list")?, |
2339 | - envelope_from: row.get("envelope_from")?, |
2340 | - address: row.get("address")?, |
2341 | - message_id: row.get("message_id")?, |
2342 | - message: row.get("message")?, |
2343 | - timestamp: row.get("timestamp")?, |
2344 | - datetime: row.get("datetime")?, |
2345 | - month_year: row.get("month_year")?, |
2346 | - }, |
2347 | - pk, |
2348 | - )) |
2349 | - }) |
2350 | - .optional()?; |
2351 | - |
2352 | - Ok(ret) |
2353 | - } |
2354 | - |
2355 | - /// Helper function to send a template reply. |
2356 | - pub fn send_reply_with_list_template<'ctx, F: Fn() -> Template>( |
2357 | - &self, |
2358 | - render_context: TemplateRenderContext<'ctx, F>, |
2359 | - recipients: impl Iterator<Item = Cow<'ctx, melib::Address>>, |
2360 | - ) -> Result<()> { |
2361 | - let TemplateRenderContext { |
2362 | - template, |
2363 | - default_fn, |
2364 | - list, |
2365 | - context, |
2366 | - queue, |
2367 | - comment, |
2368 | - } = render_context; |
2369 | - |
2370 | - let post_policy = self.list_post_policy(list.pk)?; |
2371 | - let subscription_policy = self.list_subscription_policy(list.pk)?; |
2372 | - |
2373 | - let templ = self |
2374 | - .fetch_template(template, Some(list.pk))? |
2375 | - .map(DbVal::into_inner) |
2376 | - .or_else(|| default_fn.map(|f| f())) |
2377 | - .ok_or_else(|| -> crate::Error { |
2378 | - format!("Template with name {template:?} was not found.").into() |
2379 | - })?; |
2380 | - |
2381 | - let mut draft = templ.render(context)?; |
2382 | - draft.headers.insert( |
2383 | - melib::HeaderName::new_unchecked("From"), |
2384 | - list.request_subaddr(), |
2385 | - ); |
2386 | - for addr in recipients { |
2387 | - let mut draft = draft.clone(); |
2388 | - draft |
2389 | - .headers |
2390 | - .insert(melib::HeaderName::new_unchecked("To"), addr.to_string()); |
2391 | - list.insert_headers( |
2392 | - &mut draft, |
2393 | - post_policy.as_deref(), |
2394 | - subscription_policy.as_deref(), |
2395 | - ); |
2396 | - self.insert_to_queue(QueueEntry::new( |
2397 | - queue, |
2398 | - Some(list.pk), |
2399 | - None, |
2400 | - draft.finalise()?.as_bytes(), |
2401 | - Some(comment.to_string()), |
2402 | - )?)?; |
2403 | - } |
2404 | - Ok(()) |
2405 | - } |
2406 | - } |
2407 | - |
2408 | - /// Helper type for [`Connection::send_reply_with_list_template`]. |
2409 | - #[derive(Debug)] |
2410 | - pub struct TemplateRenderContext<'ctx, F: Fn() -> Template> { |
2411 | - /// Template name. |
2412 | - pub template: &'ctx str, |
2413 | - /// If template is not found, call a function that returns one. |
2414 | - pub default_fn: Option<F>, |
2415 | - /// The pertinent list. |
2416 | - pub list: &'ctx DbVal<MailingList>, |
2417 | - /// [`minijinja`]'s template context. |
2418 | - pub context: minijinja::value::Value, |
2419 | - /// Destination queue in the database. |
2420 | - pub queue: Queue, |
2421 | - /// Comment for the queue entry in the database. |
2422 | - pub comment: Cow<'static, str>, |
2423 | - } |
2424 | diff --git a/core/src/db/queue.rs b/core/src/db/queue.rs |
2425 | deleted file mode 100644 |
2426 | index 97faafb..0000000 |
2427 | --- a/core/src/db/queue.rs |
2428 | +++ /dev/null |
2429 | @@ -1,330 +0,0 @@ |
2430 | - /* |
2431 | - * This file is part of mailpot |
2432 | - * |
2433 | - * Copyright 2020 - Manos Pitsidianakis |
2434 | - * |
2435 | - * This program is free software: you can redistribute it and/or modify |
2436 | - * it under the terms of the GNU Affero General Public License as |
2437 | - * published by the Free Software Foundation, either version 3 of the |
2438 | - * License, or (at your option) any later version. |
2439 | - * |
2440 | - * This program is distributed in the hope that it will be useful, |
2441 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2442 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2443 | - * GNU Affero General Public License for more details. |
2444 | - * |
2445 | - * You should have received a copy of the GNU Affero General Public License |
2446 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
2447 | - */ |
2448 | - |
2449 | - //! # Queues |
2450 | - |
2451 | - use std::borrow::Cow; |
2452 | - |
2453 | - use super::*; |
2454 | - |
2455 | - /// In-database queues of mail. |
2456 | - #[derive(Copy, Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] |
2457 | - #[serde(rename_all = "kebab-case")] |
2458 | - pub enum Queue { |
2459 | - /// Messages that have been submitted but not yet processed, await |
2460 | - /// processing in the `maildrop` queue. Messages can be added to the |
2461 | - /// `maildrop` queue even when mailpot is not running. |
2462 | - Maildrop, |
2463 | - /// List administrators may introduce rules for emails to be placed |
2464 | - /// indefinitely in the `hold` queue. Messages placed in the `hold` |
2465 | - /// queue stay there until the administrator intervenes. No periodic |
2466 | - /// delivery attempts are made for messages in the `hold` queue. |
2467 | - Hold, |
2468 | - /// When all the deliverable recipients for a message are delivered, and for |
2469 | - /// some recipients delivery failed for a transient reason (it might |
2470 | - /// succeed later), the message is placed in the `deferred` queue. |
2471 | - Deferred, |
2472 | - /// Invalid received or generated e-mail saved for debug and troubleshooting |
2473 | - /// reasons. |
2474 | - Corrupt, |
2475 | - /// Emails that must be sent as soon as possible. |
2476 | - Out, |
2477 | - /// Error queue |
2478 | - Error, |
2479 | - } |
2480 | - |
2481 | - impl Queue { |
2482 | - /// Returns the name of the queue used in the database schema. |
2483 | - pub fn as_str(&self) -> &'static str { |
2484 | - match self { |
2485 | - Self::Maildrop => "maildrop", |
2486 | - Self::Hold => "hold", |
2487 | - Self::Deferred => "deferred", |
2488 | - Self::Corrupt => "corrupt", |
2489 | - Self::Out => "out", |
2490 | - Self::Error => "error", |
2491 | - } |
2492 | - } |
2493 | - } |
2494 | - |
2495 | - /// A queue entry. |
2496 | - #[derive(Clone, Deserialize, Serialize, PartialEq, Eq)] |
2497 | - pub struct QueueEntry { |
2498 | - /// Database primary key. |
2499 | - pub pk: i64, |
2500 | - /// Owner queue. |
2501 | - pub queue: Queue, |
2502 | - /// Related list foreign key, optional. |
2503 | - pub list: Option<i64>, |
2504 | - /// Entry comment, optional. |
2505 | - pub comment: Option<String>, |
2506 | - /// Entry recipients in rfc5322 format. |
2507 | - pub to_addresses: String, |
2508 | - /// Entry submitter in rfc5322 format. |
2509 | - pub from_address: String, |
2510 | - /// Entry subject. |
2511 | - pub subject: String, |
2512 | - /// Entry Message-ID in rfc5322 format. |
2513 | - pub message_id: String, |
2514 | - /// Message in rfc5322 format as bytes. |
2515 | - pub message: Vec<u8>, |
2516 | - /// Unix timestamp of date. |
2517 | - pub timestamp: u64, |
2518 | - /// Datetime as string. |
2519 | - pub datetime: DateTime, |
2520 | - } |
2521 | - |
2522 | - impl std::fmt::Display for QueueEntry { |
2523 | - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
2524 | - write!(fmt, "{:?}", self) |
2525 | - } |
2526 | - } |
2527 | - |
2528 | - impl std::fmt::Debug for QueueEntry { |
2529 | - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
2530 | - fmt.debug_struct(stringify!(QueueEntry)) |
2531 | - .field("pk", &self.pk) |
2532 | - .field("queue", &self.queue) |
2533 | - .field("list", &self.list) |
2534 | - .field("comment", &self.comment) |
2535 | - .field("to_addresses", &self.to_addresses) |
2536 | - .field("from_address", &self.from_address) |
2537 | - .field("subject", &self.subject) |
2538 | - .field("message_id", &self.message_id) |
2539 | - .field("message length", &self.message.len()) |
2540 | - .field( |
2541 | - "message", |
2542 | - &format!("{:.15}", String::from_utf8_lossy(&self.message)), |
2543 | - ) |
2544 | - .field("timestamp", &self.timestamp) |
2545 | - .field("datetime", &self.datetime) |
2546 | - .finish() |
2547 | - } |
2548 | - } |
2549 | - |
2550 | - impl QueueEntry { |
2551 | - /// Create new entry. |
2552 | - pub fn new( |
2553 | - queue: Queue, |
2554 | - list: Option<i64>, |
2555 | - env: Option<Cow<'_, Envelope>>, |
2556 | - raw: &[u8], |
2557 | - comment: Option<String>, |
2558 | - ) -> Result<Self> { |
2559 | - let env = env |
2560 | - .map(Ok) |
2561 | - .unwrap_or_else(|| melib::Envelope::from_bytes(raw, None).map(Cow::Owned))?; |
2562 | - let now = chrono::offset::Utc::now(); |
2563 | - Ok(Self { |
2564 | - pk: -1, |
2565 | - list, |
2566 | - queue, |
2567 | - comment, |
2568 | - to_addresses: env.field_to_to_string(), |
2569 | - from_address: env.field_from_to_string(), |
2570 | - subject: env.subject().to_string(), |
2571 | - message_id: env.message_id().to_string(), |
2572 | - message: raw.to_vec(), |
2573 | - timestamp: now.timestamp() as u64, |
2574 | - datetime: now, |
2575 | - }) |
2576 | - } |
2577 | - } |
2578 | - |
2579 | - impl Connection { |
2580 | - /// Insert a received email into a queue. |
2581 | - pub fn insert_to_queue(&self, mut entry: QueueEntry) -> Result<DbVal<QueueEntry>> { |
2582 | - log::trace!("Inserting to queue: {entry}"); |
2583 | - let mut stmt = self.connection.prepare( |
2584 | - "INSERT INTO queue(which, list, comment, to_addresses, from_address, subject, \ |
2585 | - message_id, message, timestamp, datetime) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) \ |
2586 | - RETURNING pk;", |
2587 | - )?; |
2588 | - let pk = stmt.query_row( |
2589 | - rusqlite::params![ |
2590 | - entry.queue.as_str(), |
2591 | - &entry.list, |
2592 | - &entry.comment, |
2593 | - &entry.to_addresses, |
2594 | - &entry.from_address, |
2595 | - &entry.subject, |
2596 | - &entry.message_id, |
2597 | - &entry.message, |
2598 | - &entry.timestamp, |
2599 | - &entry.datetime, |
2600 | - ], |
2601 | - |row| { |
2602 | - let pk: i64 = row.get("pk")?; |
2603 | - Ok(pk) |
2604 | - }, |
2605 | - )?; |
2606 | - entry.pk = pk; |
2607 | - Ok(DbVal(entry, pk)) |
2608 | - } |
2609 | - |
2610 | - /// Fetch all queue entries. |
2611 | - pub fn queue(&self, queue: Queue) -> Result<Vec<DbVal<QueueEntry>>> { |
2612 | - let mut stmt = self |
2613 | - .connection |
2614 | - .prepare("SELECT * FROM queue WHERE which = ?;")?; |
2615 | - let iter = stmt.query_map([&queue.as_str()], |row| { |
2616 | - let pk = row.get::<_, i64>("pk")?; |
2617 | - Ok(DbVal( |
2618 | - QueueEntry { |
2619 | - pk, |
2620 | - queue, |
2621 | - list: row.get::<_, Option<i64>>("list")?, |
2622 | - comment: row.get::<_, Option<String>>("comment")?, |
2623 | - to_addresses: row.get::<_, String>("to_addresses")?, |
2624 | - from_address: row.get::<_, String>("from_address")?, |
2625 | - subject: row.get::<_, String>("subject")?, |
2626 | - message_id: row.get::<_, String>("message_id")?, |
2627 | - message: row.get::<_, Vec<u8>>("message")?, |
2628 | - timestamp: row.get::<_, u64>("timestamp")?, |
2629 | - datetime: row.get::<_, DateTime>("datetime")?, |
2630 | - }, |
2631 | - pk, |
2632 | - )) |
2633 | - })?; |
2634 | - |
2635 | - let mut ret = vec![]; |
2636 | - for item in iter { |
2637 | - let item = item?; |
2638 | - ret.push(item); |
2639 | - } |
2640 | - Ok(ret) |
2641 | - } |
2642 | - |
2643 | - /// Delete queue entries returning the deleted values. |
2644 | - pub fn delete_from_queue(&mut self, queue: Queue, index: Vec<i64>) -> Result<Vec<QueueEntry>> { |
2645 | - let tx = self.connection.transaction()?; |
2646 | - |
2647 | - let cl = |row: &rusqlite::Row<'_>| { |
2648 | - Ok(QueueEntry { |
2649 | - pk: -1, |
2650 | - queue, |
2651 | - list: row.get::<_, Option<i64>>("list")?, |
2652 | - comment: row.get::<_, Option<String>>("comment")?, |
2653 | - to_addresses: row.get::<_, String>("to_addresses")?, |
2654 | - from_address: row.get::<_, String>("from_address")?, |
2655 | - subject: row.get::<_, String>("subject")?, |
2656 | - message_id: row.get::<_, String>("message_id")?, |
2657 | - message: row.get::<_, Vec<u8>>("message")?, |
2658 | - timestamp: row.get::<_, u64>("timestamp")?, |
2659 | - datetime: row.get::<_, DateTime>("datetime")?, |
2660 | - }) |
2661 | - }; |
2662 | - let mut stmt = if index.is_empty() { |
2663 | - tx.prepare("DELETE FROM queue WHERE which = ? RETURNING *;")? |
2664 | - } else { |
2665 | - tx.prepare("DELETE FROM queue WHERE which = ? AND pk IN rarray(?) RETURNING *;")? |
2666 | - }; |
2667 | - let iter = if index.is_empty() { |
2668 | - stmt.query_map([&queue.as_str()], cl)? |
2669 | - } else { |
2670 | - // Note: A `Rc<Vec<Value>>` must be used as the parameter. |
2671 | - let index = std::rc::Rc::new( |
2672 | - index |
2673 | - .into_iter() |
2674 | - .map(rusqlite::types::Value::from) |
2675 | - .collect::<Vec<rusqlite::types::Value>>(), |
2676 | - ); |
2677 | - stmt.query_map(rusqlite::params![queue.as_str(), index], cl)? |
2678 | - }; |
2679 | - |
2680 | - let mut ret = vec![]; |
2681 | - for item in iter { |
2682 | - let item = item?; |
2683 | - ret.push(item); |
2684 | - } |
2685 | - drop(stmt); |
2686 | - tx.commit()?; |
2687 | - Ok(ret) |
2688 | - } |
2689 | - } |
2690 | - |
2691 | - #[cfg(test)] |
2692 | - mod tests { |
2693 | - use super::*; |
2694 | - |
2695 | - #[test] |
2696 | - fn test_queue_delete_array() { |
2697 | - use tempfile::TempDir; |
2698 | - |
2699 | - let tmp_dir = TempDir::new().unwrap(); |
2700 | - let db_path = tmp_dir.path().join("mpot.db"); |
2701 | - let config = Configuration { |
2702 | - send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
2703 | - db_path, |
2704 | - data_path: tmp_dir.path().to_path_buf(), |
2705 | - administrators: vec![], |
2706 | - }; |
2707 | - |
2708 | - let mut db = Connection::open_or_create_db(config).unwrap().trusted(); |
2709 | - for i in 0..5 { |
2710 | - db.insert_to_queue( |
2711 | - QueueEntry::new( |
2712 | - Queue::Hold, |
2713 | - None, |
2714 | - None, |
2715 | - format!("Subject: testing\r\nMessage-Id: {i}@localhost\r\n\r\nHello\r\n") |
2716 | - .as_bytes(), |
2717 | - None, |
2718 | - ) |
2719 | - .unwrap(), |
2720 | - ) |
2721 | - .unwrap(); |
2722 | - } |
2723 | - let entries = db.queue(Queue::Hold).unwrap(); |
2724 | - assert_eq!(entries.len(), 5); |
2725 | - let out_entries = db.delete_from_queue(Queue::Out, vec![]).unwrap(); |
2726 | - assert_eq!(db.queue(Queue::Hold).unwrap().len(), 5); |
2727 | - assert!(out_entries.is_empty()); |
2728 | - let deleted_entries = db.delete_from_queue(Queue::Hold, vec![]).unwrap(); |
2729 | - assert_eq!(deleted_entries.len(), 5); |
2730 | - assert_eq!( |
2731 | - &entries |
2732 | - .iter() |
2733 | - .cloned() |
2734 | - .map(DbVal::into_inner) |
2735 | - .map(|mut e| { |
2736 | - e.pk = -1; |
2737 | - e |
2738 | - }) |
2739 | - .collect::<Vec<_>>(), |
2740 | - &deleted_entries |
2741 | - ); |
2742 | - |
2743 | - for e in deleted_entries { |
2744 | - db.insert_to_queue(e).unwrap(); |
2745 | - } |
2746 | - |
2747 | - let index = db |
2748 | - .queue(Queue::Hold) |
2749 | - .unwrap() |
2750 | - .into_iter() |
2751 | - .skip(2) |
2752 | - .map(|e| e.pk()) |
2753 | - .take(2) |
2754 | - .collect::<Vec<i64>>(); |
2755 | - let deleted_entries = db.delete_from_queue(Queue::Hold, index).unwrap(); |
2756 | - assert_eq!(deleted_entries.len(), 2); |
2757 | - assert_eq!(db.queue(Queue::Hold).unwrap().len(), 3); |
2758 | - } |
2759 | - } |
2760 | diff --git a/core/src/db/subscriptions.rs b/core/src/db/subscriptions.rs |
2761 | deleted file mode 100644 |
2762 | index 1591dde..0000000 |
2763 | --- a/core/src/db/subscriptions.rs |
2764 | +++ /dev/null |
2765 | @@ -1,770 +0,0 @@ |
2766 | - /* |
2767 | - * This file is part of mailpot |
2768 | - * |
2769 | - * Copyright 2020 - Manos Pitsidianakis |
2770 | - * |
2771 | - * This program is free software: you can redistribute it and/or modify |
2772 | - * it under the terms of the GNU Affero General Public License as |
2773 | - * published by the Free Software Foundation, either version 3 of the |
2774 | - * License, or (at your option) any later version. |
2775 | - * |
2776 | - * This program is distributed in the hope that it will be useful, |
2777 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2778 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2779 | - * GNU Affero General Public License for more details. |
2780 | - * |
2781 | - * You should have received a copy of the GNU Affero General Public License |
2782 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
2783 | - */ |
2784 | - |
2785 | - use super::*; |
2786 | - |
2787 | - impl Connection { |
2788 | - /// Fetch all subscriptions of a mailing list. |
2789 | - pub fn list_subscriptions(&self, pk: i64) -> Result<Vec<DbVal<ListSubscription>>> { |
2790 | - let mut stmt = self |
2791 | - .connection |
2792 | - .prepare("SELECT * FROM subscription WHERE list = ?;")?; |
2793 | - let list_iter = stmt.query_map([&pk], |row| { |
2794 | - let pk = row.get("pk")?; |
2795 | - Ok(DbVal( |
2796 | - ListSubscription { |
2797 | - pk: row.get("pk")?, |
2798 | - list: row.get("list")?, |
2799 | - address: row.get("address")?, |
2800 | - account: row.get("account")?, |
2801 | - name: row.get("name")?, |
2802 | - digest: row.get("digest")?, |
2803 | - enabled: row.get("enabled")?, |
2804 | - verified: row.get("verified")?, |
2805 | - hide_address: row.get("hide_address")?, |
2806 | - receive_duplicates: row.get("receive_duplicates")?, |
2807 | - receive_own_posts: row.get("receive_own_posts")?, |
2808 | - receive_confirmation: row.get("receive_confirmation")?, |
2809 | - }, |
2810 | - pk, |
2811 | - )) |
2812 | - })?; |
2813 | - |
2814 | - let mut ret = vec![]; |
2815 | - for list in list_iter { |
2816 | - let list = list?; |
2817 | - ret.push(list); |
2818 | - } |
2819 | - Ok(ret) |
2820 | - } |
2821 | - |
2822 | - /// Fetch mailing list subscription. |
2823 | - pub fn list_subscription(&self, list_pk: i64, pk: i64) -> Result<DbVal<ListSubscription>> { |
2824 | - let mut stmt = self |
2825 | - .connection |
2826 | - .prepare("SELECT * FROM subscription WHERE list = ? AND pk = ?;")?; |
2827 | - |
2828 | - let ret = stmt.query_row([&list_pk, &pk], |row| { |
2829 | - let _pk: i64 = row.get("pk")?; |
2830 | - debug_assert_eq!(pk, _pk); |
2831 | - Ok(DbVal( |
2832 | - ListSubscription { |
2833 | - pk, |
2834 | - list: row.get("list")?, |
2835 | - address: row.get("address")?, |
2836 | - account: row.get("account")?, |
2837 | - name: row.get("name")?, |
2838 | - digest: row.get("digest")?, |
2839 | - enabled: row.get("enabled")?, |
2840 | - verified: row.get("verified")?, |
2841 | - hide_address: row.get("hide_address")?, |
2842 | - receive_duplicates: row.get("receive_duplicates")?, |
2843 | - receive_own_posts: row.get("receive_own_posts")?, |
2844 | - receive_confirmation: row.get("receive_confirmation")?, |
2845 | - }, |
2846 | - pk, |
2847 | - )) |
2848 | - })?; |
2849 | - Ok(ret) |
2850 | - } |
2851 | - |
2852 | - /// Fetch mailing list subscription by their address. |
2853 | - pub fn list_subscription_by_address( |
2854 | - &self, |
2855 | - list_pk: i64, |
2856 | - address: &str, |
2857 | - ) -> Result<DbVal<ListSubscription>> { |
2858 | - let mut stmt = self |
2859 | - .connection |
2860 | - .prepare("SELECT * FROM subscription WHERE list = ? AND address = ?;")?; |
2861 | - |
2862 | - let ret = stmt.query_row(rusqlite::params![&list_pk, &address], |row| { |
2863 | - let pk = row.get("pk")?; |
2864 | - let address_ = row.get("address")?; |
2865 | - debug_assert_eq!(address, &address_); |
2866 | - Ok(DbVal( |
2867 | - ListSubscription { |
2868 | - pk, |
2869 | - list: row.get("list")?, |
2870 | - address: address_, |
2871 | - account: row.get("account")?, |
2872 | - name: row.get("name")?, |
2873 | - digest: row.get("digest")?, |
2874 | - enabled: row.get("enabled")?, |
2875 | - verified: row.get("verified")?, |
2876 | - hide_address: row.get("hide_address")?, |
2877 | - receive_duplicates: row.get("receive_duplicates")?, |
2878 | - receive_own_posts: row.get("receive_own_posts")?, |
2879 | - receive_confirmation: row.get("receive_confirmation")?, |
2880 | - }, |
2881 | - pk, |
2882 | - )) |
2883 | - })?; |
2884 | - Ok(ret) |
2885 | - } |
2886 | - |
2887 | - /// Add subscription to mailing list. |
2888 | - pub fn add_subscription( |
2889 | - &self, |
2890 | - list_pk: i64, |
2891 | - mut new_val: ListSubscription, |
2892 | - ) -> Result<DbVal<ListSubscription>> { |
2893 | - new_val.list = list_pk; |
2894 | - let mut stmt = self |
2895 | - .connection |
2896 | - .prepare( |
2897 | - "INSERT INTO subscription(list, address, account, name, enabled, digest, \ |
2898 | - verified, hide_address, receive_duplicates, receive_own_posts, \ |
2899 | - receive_confirmation) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *;", |
2900 | - ) |
2901 | - .unwrap(); |
2902 | - let val = stmt.query_row( |
2903 | - rusqlite::params![ |
2904 | - &new_val.list, |
2905 | - &new_val.address, |
2906 | - &new_val.account, |
2907 | - &new_val.name, |
2908 | - &new_val.enabled, |
2909 | - &new_val.digest, |
2910 | - &new_val.verified, |
2911 | - &new_val.hide_address, |
2912 | - &new_val.receive_duplicates, |
2913 | - &new_val.receive_own_posts, |
2914 | - &new_val.receive_confirmation |
2915 | - ], |
2916 | - |row| { |
2917 | - let pk = row.get("pk")?; |
2918 | - Ok(DbVal( |
2919 | - ListSubscription { |
2920 | - pk, |
2921 | - list: row.get("list")?, |
2922 | - address: row.get("address")?, |
2923 | - name: row.get("name")?, |
2924 | - account: row.get("account")?, |
2925 | - digest: row.get("digest")?, |
2926 | - enabled: row.get("enabled")?, |
2927 | - verified: row.get("verified")?, |
2928 | - hide_address: row.get("hide_address")?, |
2929 | - receive_duplicates: row.get("receive_duplicates")?, |
2930 | - receive_own_posts: row.get("receive_own_posts")?, |
2931 | - receive_confirmation: row.get("receive_confirmation")?, |
2932 | - }, |
2933 | - pk, |
2934 | - )) |
2935 | - }, |
2936 | - )?; |
2937 | - trace!("add_subscription {:?}.", &val); |
2938 | - // table entry might be modified by triggers, so don't rely on RETURNING value. |
2939 | - self.list_subscription(list_pk, val.pk()) |
2940 | - } |
2941 | - |
2942 | - /// Create subscription candidate. |
2943 | - pub fn add_candidate_subscription( |
2944 | - &self, |
2945 | - list_pk: i64, |
2946 | - mut new_val: ListSubscription, |
2947 | - ) -> Result<DbVal<ListCandidateSubscription>> { |
2948 | - new_val.list = list_pk; |
2949 | - let mut stmt = self.connection.prepare( |
2950 | - "INSERT INTO candidate_subscription(list, address, name, accepted) VALUES(?, ?, ?, ?) \ |
2951 | - RETURNING *;", |
2952 | - )?; |
2953 | - let val = stmt.query_row( |
2954 | - rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,], |
2955 | - |row| { |
2956 | - let pk = row.get("pk")?; |
2957 | - Ok(DbVal( |
2958 | - ListCandidateSubscription { |
2959 | - pk, |
2960 | - list: row.get("list")?, |
2961 | - address: row.get("address")?, |
2962 | - name: row.get("name")?, |
2963 | - accepted: row.get("accepted")?, |
2964 | - }, |
2965 | - pk, |
2966 | - )) |
2967 | - }, |
2968 | - )?; |
2969 | - drop(stmt); |
2970 | - |
2971 | - trace!("add_candidate_subscription {:?}.", &val); |
2972 | - // table entry might be modified by triggers, so don't rely on RETURNING value. |
2973 | - self.candidate_subscription(val.pk()) |
2974 | - } |
2975 | - |
2976 | - /// Fetch subscription candidate by primary key. |
2977 | - pub fn candidate_subscription(&self, pk: i64) -> Result<DbVal<ListCandidateSubscription>> { |
2978 | - let mut stmt = self |
2979 | - .connection |
2980 | - .prepare("SELECT * FROM candidate_subscription WHERE pk = ?;")?; |
2981 | - let val = stmt |
2982 | - .query_row(rusqlite::params![&pk], |row| { |
2983 | - let _pk: i64 = row.get("pk")?; |
2984 | - debug_assert_eq!(pk, _pk); |
2985 | - Ok(DbVal( |
2986 | - ListCandidateSubscription { |
2987 | - pk, |
2988 | - list: row.get("list")?, |
2989 | - address: row.get("address")?, |
2990 | - name: row.get("name")?, |
2991 | - accepted: row.get("accepted")?, |
2992 | - }, |
2993 | - pk, |
2994 | - )) |
2995 | - }) |
2996 | - .map_err(|err| { |
2997 | - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
2998 | - Error::from(err) |
2999 | - .chain_err(|| NotFound("Candidate subscription with this pk not found!")) |
3000 | - } else { |
3001 | - err.into() |
3002 | - } |
3003 | - })?; |
3004 | - |
3005 | - Ok(val) |
3006 | - } |
3007 | - |
3008 | - /// Accept subscription candidate. |
3009 | - pub fn accept_candidate_subscription(&mut self, pk: i64) -> Result<DbVal<ListSubscription>> { |
3010 | - let val = self.connection.query_row( |
3011 | - "INSERT INTO subscription(list, address, name, enabled, digest, verified, \ |
3012 | - hide_address, receive_duplicates, receive_own_posts, receive_confirmation) SELECT \ |
3013 | - list, address, name, 1, 0, 0, 0, 1, 1, 0 FROM candidate_subscription WHERE pk = ? \ |
3014 | - RETURNING *;", |
3015 | - rusqlite::params![&pk], |
3016 | - |row| { |
3017 | - let pk = row.get("pk")?; |
3018 | - Ok(DbVal( |
3019 | - ListSubscription { |
3020 | - pk, |
3021 | - list: row.get("list")?, |
3022 | - address: row.get("address")?, |
3023 | - account: row.get("account")?, |
3024 | - name: row.get("name")?, |
3025 | - digest: row.get("digest")?, |
3026 | - enabled: row.get("enabled")?, |
3027 | - verified: row.get("verified")?, |
3028 | - hide_address: row.get("hide_address")?, |
3029 | - receive_duplicates: row.get("receive_duplicates")?, |
3030 | - receive_own_posts: row.get("receive_own_posts")?, |
3031 | - receive_confirmation: row.get("receive_confirmation")?, |
3032 | - }, |
3033 | - pk, |
3034 | - )) |
3035 | - }, |
3036 | - )?; |
3037 | - |
3038 | - trace!("accept_candidate_subscription {:?}.", &val); |
3039 | - // table entry might be modified by triggers, so don't rely on RETURNING value. |
3040 | - let ret = self.list_subscription(val.list, val.pk())?; |
3041 | - |
3042 | - // assert that [ref:accept_candidate] trigger works. |
3043 | - debug_assert_eq!(Some(ret.pk), self.candidate_subscription(pk)?.accepted); |
3044 | - Ok(ret) |
3045 | - } |
3046 | - |
3047 | - /// Remove a subscription by their address. |
3048 | - pub fn remove_subscription(&self, list_pk: i64, address: &str) -> Result<()> { |
3049 | - self.connection |
3050 | - .query_row( |
3051 | - "DELETE FROM subscription WHERE list = ? AND address = ? RETURNING *;", |
3052 | - rusqlite::params![&list_pk, &address], |
3053 | - |_| Ok(()), |
3054 | - ) |
3055 | - .map_err(|err| { |
3056 | - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
3057 | - Error::from(err).chain_err(|| NotFound("list or list owner not found!")) |
3058 | - } else { |
3059 | - err.into() |
3060 | - } |
3061 | - })?; |
3062 | - |
3063 | - Ok(()) |
3064 | - } |
3065 | - |
3066 | - /// Update a mailing list subscription. |
3067 | - pub fn update_subscription(&mut self, change_set: ListSubscriptionChangeset) -> Result<()> { |
3068 | - let pk = self |
3069 | - .list_subscription_by_address(change_set.list, &change_set.address)? |
3070 | - .pk; |
3071 | - if matches!( |
3072 | - change_set, |
3073 | - ListSubscriptionChangeset { |
3074 | - list: _, |
3075 | - address: _, |
3076 | - account: None, |
3077 | - name: None, |
3078 | - digest: None, |
3079 | - verified: None, |
3080 | - hide_address: None, |
3081 | - receive_duplicates: None, |
3082 | - receive_own_posts: None, |
3083 | - receive_confirmation: None, |
3084 | - enabled: None, |
3085 | - } |
3086 | - ) { |
3087 | - return Ok(()); |
3088 | - } |
3089 | - |
3090 | - let ListSubscriptionChangeset { |
3091 | - list, |
3092 | - address: _, |
3093 | - name, |
3094 | - account, |
3095 | - digest, |
3096 | - enabled, |
3097 | - verified, |
3098 | - hide_address, |
3099 | - receive_duplicates, |
3100 | - receive_own_posts, |
3101 | - receive_confirmation, |
3102 | - } = change_set; |
3103 | - let tx = self.connection.transaction()?; |
3104 | - |
3105 | - macro_rules! update { |
3106 | - ($field:tt) => {{ |
3107 | - if let Some($field) = $field { |
3108 | - tx.execute( |
3109 | - concat!( |
3110 | - "UPDATE subscription SET ", |
3111 | - stringify!($field), |
3112 | - " = ? WHERE list = ? AND pk = ?;" |
3113 | - ), |
3114 | - rusqlite::params![&$field, &list, &pk], |
3115 | - )?; |
3116 | - } |
3117 | - }}; |
3118 | - } |
3119 | - update!(name); |
3120 | - update!(account); |
3121 | - update!(digest); |
3122 | - update!(enabled); |
3123 | - update!(verified); |
3124 | - update!(hide_address); |
3125 | - update!(receive_duplicates); |
3126 | - update!(receive_own_posts); |
3127 | - update!(receive_confirmation); |
3128 | - |
3129 | - tx.commit()?; |
3130 | - Ok(()) |
3131 | - } |
3132 | - |
3133 | - /// Fetch account by pk. |
3134 | - pub fn account(&self, pk: i64) -> Result<Option<DbVal<Account>>> { |
3135 | - let mut stmt = self |
3136 | - .connection |
3137 | - .prepare("SELECT * FROM account WHERE pk = ?;")?; |
3138 | - |
3139 | - let ret = stmt |
3140 | - .query_row(rusqlite::params![&pk], |row| { |
3141 | - let _pk: i64 = row.get("pk")?; |
3142 | - debug_assert_eq!(pk, _pk); |
3143 | - Ok(DbVal( |
3144 | - Account { |
3145 | - pk, |
3146 | - name: row.get("name")?, |
3147 | - address: row.get("address")?, |
3148 | - public_key: row.get("public_key")?, |
3149 | - password: row.get("password")?, |
3150 | - enabled: row.get("enabled")?, |
3151 | - }, |
3152 | - pk, |
3153 | - )) |
3154 | - }) |
3155 | - .optional()?; |
3156 | - Ok(ret) |
3157 | - } |
3158 | - |
3159 | - /// Fetch account by address. |
3160 | - pub fn account_by_address(&self, address: &str) -> Result<Option<DbVal<Account>>> { |
3161 | - let mut stmt = self |
3162 | - .connection |
3163 | - .prepare("SELECT * FROM account WHERE address = ?;")?; |
3164 | - |
3165 | - let ret = stmt |
3166 | - .query_row(rusqlite::params![&address], |row| { |
3167 | - let pk = row.get("pk")?; |
3168 | - Ok(DbVal( |
3169 | - Account { |
3170 | - pk, |
3171 | - name: row.get("name")?, |
3172 | - address: row.get("address")?, |
3173 | - public_key: row.get("public_key")?, |
3174 | - password: row.get("password")?, |
3175 | - enabled: row.get("enabled")?, |
3176 | - }, |
3177 | - pk, |
3178 | - )) |
3179 | - }) |
3180 | - .optional()?; |
3181 | - Ok(ret) |
3182 | - } |
3183 | - |
3184 | - /// Fetch all subscriptions of an account by primary key. |
3185 | - pub fn account_subscriptions(&self, pk: i64) -> Result<Vec<DbVal<ListSubscription>>> { |
3186 | - let mut stmt = self |
3187 | - .connection |
3188 | - .prepare("SELECT * FROM subscription WHERE account = ?;")?; |
3189 | - let list_iter = stmt.query_map([&pk], |row| { |
3190 | - let pk = row.get("pk")?; |
3191 | - Ok(DbVal( |
3192 | - ListSubscription { |
3193 | - pk: row.get("pk")?, |
3194 | - list: row.get("list")?, |
3195 | - address: row.get("address")?, |
3196 | - account: row.get("account")?, |
3197 | - name: row.get("name")?, |
3198 | - digest: row.get("digest")?, |
3199 | - enabled: row.get("enabled")?, |
3200 | - verified: row.get("verified")?, |
3201 | - hide_address: row.get("hide_address")?, |
3202 | - receive_duplicates: row.get("receive_duplicates")?, |
3203 | - receive_own_posts: row.get("receive_own_posts")?, |
3204 | - receive_confirmation: row.get("receive_confirmation")?, |
3205 | - }, |
3206 | - pk, |
3207 | - )) |
3208 | - })?; |
3209 | - |
3210 | - let mut ret = vec![]; |
3211 | - for list in list_iter { |
3212 | - let list = list?; |
3213 | - ret.push(list); |
3214 | - } |
3215 | - Ok(ret) |
3216 | - } |
3217 | - |
3218 | - /// Fetch all accounts. |
3219 | - pub fn accounts(&self) -> Result<Vec<DbVal<Account>>> { |
3220 | - let mut stmt = self |
3221 | - .connection |
3222 | - .prepare("SELECT * FROM account ORDER BY pk ASC;")?; |
3223 | - let list_iter = stmt.query_map([], |row| { |
3224 | - let pk = row.get("pk")?; |
3225 | - Ok(DbVal( |
3226 | - Account { |
3227 | - pk, |
3228 | - name: row.get("name")?, |
3229 | - address: row.get("address")?, |
3230 | - public_key: row.get("public_key")?, |
3231 | - password: row.get("password")?, |
3232 | - enabled: row.get("enabled")?, |
3233 | - }, |
3234 | - pk, |
3235 | - )) |
3236 | - })?; |
3237 | - |
3238 | - let mut ret = vec![]; |
3239 | - for list in list_iter { |
3240 | - let list = list?; |
3241 | - ret.push(list); |
3242 | - } |
3243 | - Ok(ret) |
3244 | - } |
3245 | - |
3246 | - /// Add account. |
3247 | - pub fn add_account(&self, new_val: Account) -> Result<DbVal<Account>> { |
3248 | - let mut stmt = self |
3249 | - .connection |
3250 | - .prepare( |
3251 | - "INSERT INTO account(name, address, public_key, password, enabled) VALUES(?, ?, \ |
3252 | - ?, ?, ?) RETURNING *;", |
3253 | - ) |
3254 | - .unwrap(); |
3255 | - let ret = stmt.query_row( |
3256 | - rusqlite::params![ |
3257 | - &new_val.name, |
3258 | - &new_val.address, |
3259 | - &new_val.public_key, |
3260 | - &new_val.password, |
3261 | - &new_val.enabled, |
3262 | - ], |
3263 | - |row| { |
3264 | - let pk = row.get("pk")?; |
3265 | - Ok(DbVal( |
3266 | - Account { |
3267 | - pk, |
3268 | - name: row.get("name")?, |
3269 | - address: row.get("address")?, |
3270 | - public_key: row.get("public_key")?, |
3271 | - password: row.get("password")?, |
3272 | - enabled: row.get("enabled")?, |
3273 | - }, |
3274 | - pk, |
3275 | - )) |
3276 | - }, |
3277 | - )?; |
3278 | - |
3279 | - trace!("add_account {:?}.", &ret); |
3280 | - Ok(ret) |
3281 | - } |
3282 | - |
3283 | - /// Remove an account by their address. |
3284 | - pub fn remove_account(&self, address: &str) -> Result<()> { |
3285 | - self.connection |
3286 | - .query_row( |
3287 | - "DELETE FROM account WHERE address = ? RETURNING *;", |
3288 | - rusqlite::params![&address], |
3289 | - |_| Ok(()), |
3290 | - ) |
3291 | - .map_err(|err| { |
3292 | - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
3293 | - Error::from(err).chain_err(|| NotFound("account not found!")) |
3294 | - } else { |
3295 | - err.into() |
3296 | - } |
3297 | - })?; |
3298 | - |
3299 | - Ok(()) |
3300 | - } |
3301 | - |
3302 | - /// Update an account. |
3303 | - pub fn update_account(&mut self, change_set: AccountChangeset) -> Result<()> { |
3304 | - let Some(acc) = self.account_by_address(&change_set.address)? else { |
3305 | - return Err(NotFound("account with this address not found!").into()); |
3306 | - }; |
3307 | - let pk = acc.pk; |
3308 | - if matches!( |
3309 | - change_set, |
3310 | - AccountChangeset { |
3311 | - address: _, |
3312 | - name: None, |
3313 | - public_key: None, |
3314 | - password: None, |
3315 | - enabled: None, |
3316 | - } |
3317 | - ) { |
3318 | - return Ok(()); |
3319 | - } |
3320 | - |
3321 | - let AccountChangeset { |
3322 | - address: _, |
3323 | - name, |
3324 | - public_key, |
3325 | - password, |
3326 | - enabled, |
3327 | - } = change_set; |
3328 | - let tx = self.connection.transaction()?; |
3329 | - |
3330 | - macro_rules! update { |
3331 | - ($field:tt) => {{ |
3332 | - if let Some($field) = $field { |
3333 | - tx.execute( |
3334 | - concat!( |
3335 | - "UPDATE account SET ", |
3336 | - stringify!($field), |
3337 | - " = ? WHERE pk = ?;" |
3338 | - ), |
3339 | - rusqlite::params![&$field, &pk], |
3340 | - )?; |
3341 | - } |
3342 | - }}; |
3343 | - } |
3344 | - update!(name); |
3345 | - update!(public_key); |
3346 | - update!(password); |
3347 | - update!(enabled); |
3348 | - |
3349 | - tx.commit()?; |
3350 | - Ok(()) |
3351 | - } |
3352 | - } |
3353 | - |
3354 | - #[cfg(test)] |
3355 | - mod tests { |
3356 | - use super::*; |
3357 | - |
3358 | - #[test] |
3359 | - fn test_subscription_ops() { |
3360 | - use tempfile::TempDir; |
3361 | - |
3362 | - let tmp_dir = TempDir::new().unwrap(); |
3363 | - let db_path = tmp_dir.path().join("mpot.db"); |
3364 | - let config = Configuration { |
3365 | - send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
3366 | - db_path, |
3367 | - data_path: tmp_dir.path().to_path_buf(), |
3368 | - administrators: vec![], |
3369 | - }; |
3370 | - |
3371 | - let mut db = Connection::open_or_create_db(config).unwrap().trusted(); |
3372 | - let list = db |
3373 | - .create_list(MailingList { |
3374 | - pk: -1, |
3375 | - name: "foobar chat".into(), |
3376 | - id: "foo-chat".into(), |
3377 | - address: "foo-chat@example.com".into(), |
3378 | - description: None, |
3379 | - archive_url: None, |
3380 | - }) |
3381 | - .unwrap(); |
3382 | - let secondary_list = db |
3383 | - .create_list(MailingList { |
3384 | - pk: -1, |
3385 | - name: "foobar chat2".into(), |
3386 | - id: "foo-chat2".into(), |
3387 | - address: "foo-chat2@example.com".into(), |
3388 | - description: None, |
3389 | - archive_url: None, |
3390 | - }) |
3391 | - .unwrap(); |
3392 | - for i in 0..4 { |
3393 | - let sub = db |
3394 | - .add_subscription( |
3395 | - list.pk(), |
3396 | - ListSubscription { |
3397 | - pk: -1, |
3398 | - list: list.pk(), |
3399 | - address: format!("{i}@example.com"), |
3400 | - account: None, |
3401 | - name: Some(format!("User{i}")), |
3402 | - digest: false, |
3403 | - hide_address: false, |
3404 | - receive_duplicates: false, |
3405 | - receive_own_posts: false, |
3406 | - receive_confirmation: false, |
3407 | - enabled: true, |
3408 | - verified: false, |
3409 | - }, |
3410 | - ) |
3411 | - .unwrap(); |
3412 | - assert_eq!(db.list_subscription(list.pk(), sub.pk()).unwrap(), sub); |
3413 | - assert_eq!( |
3414 | - db.list_subscription_by_address(list.pk(), &sub.address) |
3415 | - .unwrap(), |
3416 | - sub |
3417 | - ); |
3418 | - } |
3419 | - |
3420 | - assert_eq!(db.accounts().unwrap(), vec![]); |
3421 | - assert_eq!( |
3422 | - db.remove_subscription(list.pk(), "nonexistent@example.com") |
3423 | - .map_err(|err| err.to_string()) |
3424 | - .unwrap_err(), |
3425 | - NotFound("list or list owner not found!").to_string() |
3426 | - ); |
3427 | - |
3428 | - let cand = db |
3429 | - .add_candidate_subscription( |
3430 | - list.pk(), |
3431 | - ListSubscription { |
3432 | - pk: -1, |
3433 | - list: list.pk(), |
3434 | - address: "4@example.com".into(), |
3435 | - account: None, |
3436 | - name: Some("User4".into()), |
3437 | - digest: false, |
3438 | - hide_address: false, |
3439 | - receive_duplicates: false, |
3440 | - receive_own_posts: false, |
3441 | - receive_confirmation: false, |
3442 | - enabled: true, |
3443 | - verified: false, |
3444 | - }, |
3445 | - ) |
3446 | - .unwrap(); |
3447 | - let accepted = db.accept_candidate_subscription(cand.pk()).unwrap(); |
3448 | - |
3449 | - assert_eq!(db.account(5).unwrap(), None); |
3450 | - assert_eq!( |
3451 | - db.remove_account("4@example.com") |
3452 | - .map_err(|err| err.to_string()) |
3453 | - .unwrap_err(), |
3454 | - NotFound("account not found!").to_string() |
3455 | - ); |
3456 | - |
3457 | - let acc = db |
3458 | - .add_account(Account { |
3459 | - pk: -1, |
3460 | - name: accepted.name.clone(), |
3461 | - address: accepted.address.clone(), |
3462 | - public_key: None, |
3463 | - password: String::new(), |
3464 | - enabled: true, |
3465 | - }) |
3466 | - .unwrap(); |
3467 | - |
3468 | - // Test [ref:add_account] SQL trigger (see schema.sql) |
3469 | - assert_eq!( |
3470 | - db.list_subscription(list.pk(), accepted.pk()) |
3471 | - .unwrap() |
3472 | - .account, |
3473 | - Some(acc.pk()) |
3474 | - ); |
3475 | - // Test [ref:add_account_to_subscription] SQL trigger (see schema.sql) |
3476 | - let sub = db |
3477 | - .add_subscription( |
3478 | - secondary_list.pk(), |
3479 | - ListSubscription { |
3480 | - pk: -1, |
3481 | - list: secondary_list.pk(), |
3482 | - address: "4@example.com".into(), |
3483 | - account: None, |
3484 | - name: Some("User4".into()), |
3485 | - digest: false, |
3486 | - hide_address: false, |
3487 | - receive_duplicates: false, |
3488 | - receive_own_posts: false, |
3489 | - receive_confirmation: false, |
3490 | - enabled: true, |
3491 | - verified: true, |
3492 | - }, |
3493 | - ) |
3494 | - .unwrap(); |
3495 | - assert_eq!(sub.account, Some(acc.pk())); |
3496 | - // Test [ref:verify_subscription_email] SQL trigger (see schema.sql) |
3497 | - assert!(!sub.verified); |
3498 | - |
3499 | - assert_eq!(db.accounts().unwrap(), vec![acc.clone()]); |
3500 | - |
3501 | - assert_eq!( |
3502 | - db.update_account(AccountChangeset { |
3503 | - address: "nonexistent@example.com".into(), |
3504 | - ..AccountChangeset::default() |
3505 | - }) |
3506 | - .map_err(|err| err.to_string()) |
3507 | - .unwrap_err(), |
3508 | - NotFound("account with this address not found!").to_string() |
3509 | - ); |
3510 | - assert_eq!( |
3511 | - db.update_account(AccountChangeset { |
3512 | - address: acc.address.clone(), |
3513 | - ..AccountChangeset::default() |
3514 | - }) |
3515 | - .map_err(|err| err.to_string()), |
3516 | - Ok(()) |
3517 | - ); |
3518 | - assert_eq!( |
3519 | - db.update_account(AccountChangeset { |
3520 | - address: acc.address.clone(), |
3521 | - enabled: Some(Some(false)), |
3522 | - ..AccountChangeset::default() |
3523 | - }) |
3524 | - .map_err(|err| err.to_string()), |
3525 | - Ok(()) |
3526 | - ); |
3527 | - assert!(!db.account(acc.pk()).unwrap().unwrap().enabled); |
3528 | - assert_eq!( |
3529 | - db.remove_account("4@example.com") |
3530 | - .map_err(|err| err.to_string()), |
3531 | - Ok(()) |
3532 | - ); |
3533 | - assert_eq!(db.accounts().unwrap(), vec![]); |
3534 | - } |
3535 | - } |
3536 | diff --git a/core/src/db/templates.rs b/core/src/db/templates.rs |
3537 | deleted file mode 100644 |
3538 | index acfb955..0000000 |
3539 | --- a/core/src/db/templates.rs |
3540 | +++ /dev/null |
3541 | @@ -1,178 +0,0 @@ |
3542 | - /* |
3543 | - * This file is part of mailpot |
3544 | - * |
3545 | - * Copyright 2020 - Manos Pitsidianakis |
3546 | - * |
3547 | - * This program is free software: you can redistribute it and/or modify |
3548 | - * it under the terms of the GNU Affero General Public License as |
3549 | - * published by the Free Software Foundation, either version 3 of the |
3550 | - * License, or (at your option) any later version. |
3551 | - * |
3552 | - * This program is distributed in the hope that it will be useful, |
3553 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
3554 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3555 | - * GNU Affero General Public License for more details. |
3556 | - * |
3557 | - * You should have received a copy of the GNU Affero General Public License |
3558 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
3559 | - */ |
3560 | - |
3561 | - //! Named templates, for generated e-mail like confirmations, alerts etc. |
3562 | - |
3563 | - use super::*; |
3564 | - |
3565 | - impl Connection { |
3566 | - /// Fetch all. |
3567 | - pub fn fetch_templates(&self) -> Result<Vec<DbVal<Template>>> { |
3568 | - let mut stmt = self |
3569 | - .connection |
3570 | - .prepare("SELECT * FROM templates ORDER BY pk;")?; |
3571 | - let iter = stmt.query_map(rusqlite::params![], |row| { |
3572 | - let pk = row.get("pk")?; |
3573 | - Ok(DbVal( |
3574 | - Template { |
3575 | - pk, |
3576 | - name: row.get("name")?, |
3577 | - list: row.get("list")?, |
3578 | - subject: row.get("subject")?, |
3579 | - headers_json: row.get("headers_json")?, |
3580 | - body: row.get("body")?, |
3581 | - }, |
3582 | - pk, |
3583 | - )) |
3584 | - })?; |
3585 | - |
3586 | - let mut ret = vec![]; |
3587 | - for templ in iter { |
3588 | - let templ = templ?; |
3589 | - ret.push(templ); |
3590 | - } |
3591 | - Ok(ret) |
3592 | - } |
3593 | - |
3594 | - /// Fetch a named template. |
3595 | - pub fn fetch_template( |
3596 | - &self, |
3597 | - template: &str, |
3598 | - list_pk: Option<i64>, |
3599 | - ) -> Result<Option<DbVal<Template>>> { |
3600 | - let mut stmt = self |
3601 | - .connection |
3602 | - .prepare("SELECT * FROM templates WHERE name = ? AND list IS ?;")?; |
3603 | - let ret = stmt |
3604 | - .query_row(rusqlite::params![&template, &list_pk], |row| { |
3605 | - let pk = row.get("pk")?; |
3606 | - Ok(DbVal( |
3607 | - Template { |
3608 | - pk, |
3609 | - name: row.get("name")?, |
3610 | - list: row.get("list")?, |
3611 | - subject: row.get("subject")?, |
3612 | - headers_json: row.get("headers_json")?, |
3613 | - body: row.get("body")?, |
3614 | - }, |
3615 | - pk, |
3616 | - )) |
3617 | - }) |
3618 | - .optional()?; |
3619 | - if ret.is_none() && list_pk.is_some() { |
3620 | - let mut stmt = self |
3621 | - .connection |
3622 | - .prepare("SELECT * FROM templates WHERE name = ? AND list IS NULL;")?; |
3623 | - Ok(stmt |
3624 | - .query_row(rusqlite::params![&template], |row| { |
3625 | - let pk = row.get("pk")?; |
3626 | - Ok(DbVal( |
3627 | - Template { |
3628 | - pk, |
3629 | - name: row.get("name")?, |
3630 | - list: row.get("list")?, |
3631 | - subject: row.get("subject")?, |
3632 | - headers_json: row.get("headers_json")?, |
3633 | - body: row.get("body")?, |
3634 | - }, |
3635 | - pk, |
3636 | - )) |
3637 | - }) |
3638 | - .optional()?) |
3639 | - } else { |
3640 | - Ok(ret) |
3641 | - } |
3642 | - } |
3643 | - |
3644 | - /// Insert a named template. |
3645 | - pub fn add_template(&self, template: Template) -> Result<DbVal<Template>> { |
3646 | - let mut stmt = self.connection.prepare( |
3647 | - "INSERT INTO templates(name, list, subject, headers_json, body) VALUES(?, ?, ?, ?, ?) \ |
3648 | - RETURNING *;", |
3649 | - )?; |
3650 | - let ret = stmt |
3651 | - .query_row( |
3652 | - rusqlite::params![ |
3653 | - &template.name, |
3654 | - &template.list, |
3655 | - &template.subject, |
3656 | - &template.headers_json, |
3657 | - &template.body |
3658 | - ], |
3659 | - |row| { |
3660 | - let pk = row.get("pk")?; |
3661 | - Ok(DbVal( |
3662 | - Template { |
3663 | - pk, |
3664 | - name: row.get("name")?, |
3665 | - list: row.get("list")?, |
3666 | - subject: row.get("subject")?, |
3667 | - headers_json: row.get("headers_json")?, |
3668 | - body: row.get("body")?, |
3669 | - }, |
3670 | - pk, |
3671 | - )) |
3672 | - }, |
3673 | - ) |
3674 | - .map_err(|err| { |
3675 | - if matches!( |
3676 | - err, |
3677 | - rusqlite::Error::SqliteFailure( |
3678 | - rusqlite::ffi::Error { |
3679 | - code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
3680 | - extended_code: 787 |
3681 | - }, |
3682 | - _ |
3683 | - ) |
3684 | - ) { |
3685 | - Error::from(err).chain_err(|| NotFound("Could not find a list with this pk.")) |
3686 | - } else { |
3687 | - err.into() |
3688 | - } |
3689 | - })?; |
3690 | - |
3691 | - trace!("add_template {:?}.", &ret); |
3692 | - Ok(ret) |
3693 | - } |
3694 | - |
3695 | - /// Remove a named template. |
3696 | - pub fn remove_template(&self, template: &str, list_pk: Option<i64>) -> Result<Template> { |
3697 | - let mut stmt = self |
3698 | - .connection |
3699 | - .prepare("DELETE FROM templates WHERE name = ? AND list IS ? RETURNING *;")?; |
3700 | - let ret = stmt.query_row(rusqlite::params![&template, &list_pk], |row| { |
3701 | - Ok(Template { |
3702 | - pk: -1, |
3703 | - name: row.get("name")?, |
3704 | - list: row.get("list")?, |
3705 | - subject: row.get("subject")?, |
3706 | - headers_json: row.get("headers_json")?, |
3707 | - body: row.get("body")?, |
3708 | - }) |
3709 | - })?; |
3710 | - |
3711 | - trace!( |
3712 | - "remove_template {} list_pk {:?} {:?}.", |
3713 | - template, |
3714 | - &list_pk, |
3715 | - &ret |
3716 | - ); |
3717 | - Ok(ret) |
3718 | - } |
3719 | - } |
3720 | diff --git a/core/src/lib.rs b/core/src/lib.rs |
3721 | index d0caca0..199fccb 100644 |
3722 | --- a/core/src/lib.rs |
3723 | +++ b/core/src/lib.rs |
3724 | @@ -165,20 +165,23 @@ pub extern crate log; |
3725 | pub extern crate melib; |
3726 | pub extern crate serde_json; |
3727 | |
3728 | - use log::{info, trace}; |
3729 | - |
3730 | mod config; |
3731 | - mod db; |
3732 | + mod connection; |
3733 | mod errors; |
3734 | pub mod mail; |
3735 | + pub mod message_filters; |
3736 | pub mod models; |
3737 | + pub mod policies; |
3738 | #[cfg(not(target_os = "windows"))] |
3739 | pub mod postfix; |
3740 | + pub mod posts; |
3741 | + pub mod queue; |
3742 | pub mod submission; |
3743 | + pub mod subscriptions; |
3744 | mod templates; |
3745 | |
3746 | pub use config::{Configuration, SendMail}; |
3747 | - pub use db::*; |
3748 | + pub use connection::*; |
3749 | pub use errors::*; |
3750 | use models::*; |
3751 | pub use templates::*; |
3752 | diff --git a/core/src/mail.rs b/core/src/mail.rs |
3753 | index e5257d0..612261f 100644 |
3754 | --- a/core/src/mail.rs |
3755 | +++ b/core/src/mail.rs |
3756 | @@ -20,11 +20,13 @@ |
3757 | //! Types for processing new posts: [`PostFilter`](message_filters::PostFilter), |
3758 | //! [`ListContext`], [`MailJob`] and [`PostAction`]. |
3759 | |
3760 | + use log::trace; |
3761 | use melib::Address; |
3762 | |
3763 | - use super::*; |
3764 | - pub mod message_filters; |
3765 | - |
3766 | + use crate::{ |
3767 | + models::{ListOwner, ListSubscription, MailingList, PostPolicy, SubscriptionPolicy}, |
3768 | + DbVal, |
3769 | + }; |
3770 | /// Post action returned from a list's |
3771 | /// [`PostFilter`](message_filters::PostFilter) stack. |
3772 | #[derive(Debug)] |
3773 | @@ -66,7 +68,7 @@ pub struct ListContext<'list> { |
3774 | |
3775 | /// Post to be considered by the list's |
3776 | /// [`PostFilter`](message_filters::PostFilter) stack. |
3777 | - pub struct Post { |
3778 | + pub struct PostEntry { |
3779 | /// `From` address of post. |
3780 | pub from: Address, |
3781 | /// Raw bytes of post. |
3782 | @@ -78,9 +80,9 @@ pub struct Post { |
3783 | pub action: PostAction, |
3784 | } |
3785 | |
3786 | - impl core::fmt::Debug for Post { |
3787 | + impl core::fmt::Debug for PostEntry { |
3788 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { |
3789 | - fmt.debug_struct("Post") |
3790 | + fmt.debug_struct(stringify!(PostEntry)) |
3791 | .field("from", &self.from) |
3792 | .field("bytes", &format_args!("{} bytes", self.bytes.len())) |
3793 | .field("to", &self.to.as_slice()) |
3794 | diff --git a/core/src/mail/message_filters.rs b/core/src/mail/message_filters.rs |
3795 | deleted file mode 100644 |
3796 | index 333cd21..0000000 |
3797 | --- a/core/src/mail/message_filters.rs |
3798 | +++ /dev/null |
3799 | @@ -1,256 +0,0 @@ |
3800 | - /* |
3801 | - * This file is part of mailpot |
3802 | - * |
3803 | - * Copyright 2020 - Manos Pitsidianakis |
3804 | - * |
3805 | - * This program is free software: you can redistribute it and/or modify |
3806 | - * it under the terms of the GNU Affero General Public License as |
3807 | - * published by the Free Software Foundation, either version 3 of the |
3808 | - * License, or (at your option) any later version. |
3809 | - * |
3810 | - * This program is distributed in the hope that it will be useful, |
3811 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
3812 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3813 | - * GNU Affero General Public License for more details. |
3814 | - * |
3815 | - * You should have received a copy of the GNU Affero General Public License |
3816 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
3817 | - */ |
3818 | - |
3819 | - #![allow(clippy::result_unit_err)] |
3820 | - |
3821 | - //! Filters to pass each mailing list post through. Filters are functions that |
3822 | - //! implement the [`PostFilter`] trait that can: |
3823 | - //! |
3824 | - //! - transform post content. |
3825 | - //! - modify the final [`PostAction`] to take. |
3826 | - //! - modify the final scheduled jobs to perform. (See [`MailJob`]). |
3827 | - //! |
3828 | - //! Filters are executed in sequence like this: |
3829 | - //! |
3830 | - //! ```ignore |
3831 | - //! let result = filters |
3832 | - //! .into_iter() |
3833 | - //! .fold(Ok((&mut post, &mut list_ctx)), |p, f| { |
3834 | - //! p.and_then(|(p, c)| f.feed(p, c)) |
3835 | - //! }); |
3836 | - //! ``` |
3837 | - //! |
3838 | - //! so the processing stops at the first returned error. |
3839 | - |
3840 | - use super::*; |
3841 | - |
3842 | - /// Filter that modifies and/or verifies a post candidate. On rejection, return |
3843 | - /// a string describing the error and optionally set `post.action` to `Reject` |
3844 | - /// or `Defer` |
3845 | - pub trait PostFilter { |
3846 | - /// Feed post into the filter. Perform modifications to `post` and / or |
3847 | - /// `ctx`, and return them with `Result::Ok` unless you want to the |
3848 | - /// processing to stop and return an `Result::Err`. |
3849 | - fn feed<'p, 'list>( |
3850 | - self: Box<Self>, |
3851 | - post: &'p mut Post, |
3852 | - ctx: &'p mut ListContext<'list>, |
3853 | - ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>; |
3854 | - } |
3855 | - |
3856 | - /// Check that submitter can post to list, for now it accepts everything. |
3857 | - pub struct PostRightsCheck; |
3858 | - impl PostFilter for PostRightsCheck { |
3859 | - fn feed<'p, 'list>( |
3860 | - self: Box<Self>, |
3861 | - post: &'p mut Post, |
3862 | - ctx: &'p mut ListContext<'list>, |
3863 | - ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { |
3864 | - trace!("Running PostRightsCheck filter"); |
3865 | - if let Some(ref policy) = ctx.post_policy { |
3866 | - if policy.announce_only { |
3867 | - trace!("post policy is announce_only"); |
3868 | - let owner_addresses = ctx |
3869 | - .list_owners |
3870 | - .iter() |
3871 | - .map(|lo| lo.address()) |
3872 | - .collect::<Vec<Address>>(); |
3873 | - trace!("Owner addresses are: {:#?}", &owner_addresses); |
3874 | - trace!("Envelope from is: {:?}", &post.from); |
3875 | - if !owner_addresses.iter().any(|addr| *addr == post.from) { |
3876 | - trace!("Envelope From does not include any owner"); |
3877 | - post.action = PostAction::Reject { |
3878 | - reason: "You are not allowed to post on this list.".to_string(), |
3879 | - }; |
3880 | - return Err(()); |
3881 | - } |
3882 | - } else if policy.subscription_only { |
3883 | - trace!("post policy is subscription_only"); |
3884 | - let email_from = post.from.get_email(); |
3885 | - trace!("post from is {:?}", &email_from); |
3886 | - trace!("post subscriptions are {:#?}", &ctx.subscriptions); |
3887 | - if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) { |
3888 | - trace!("Envelope from is not subscribed to this list"); |
3889 | - post.action = PostAction::Reject { |
3890 | - reason: "Only subscriptions can post to this list.".to_string(), |
3891 | - }; |
3892 | - return Err(()); |
3893 | - } |
3894 | - } else if policy.approval_needed { |
3895 | - trace!("post policy says approval_needed"); |
3896 | - let email_from = post.from.get_email(); |
3897 | - trace!("post from is {:?}", &email_from); |
3898 | - trace!("post subscriptions are {:#?}", &ctx.subscriptions); |
3899 | - if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) { |
3900 | - trace!("Envelope from is not subscribed to this list"); |
3901 | - post.action = PostAction::Defer { |
3902 | - reason: "Your posting has been deferred. Approval from the list's \ |
3903 | - moderators is required before it is submitted." |
3904 | - .to_string(), |
3905 | - }; |
3906 | - return Err(()); |
3907 | - } |
3908 | - } |
3909 | - } |
3910 | - Ok((post, ctx)) |
3911 | - } |
3912 | - } |
3913 | - |
3914 | - /// Ensure message contains only `\r\n` line terminators, required by SMTP. |
3915 | - pub struct FixCRLF; |
3916 | - impl PostFilter for FixCRLF { |
3917 | - fn feed<'p, 'list>( |
3918 | - self: Box<Self>, |
3919 | - post: &'p mut Post, |
3920 | - ctx: &'p mut ListContext<'list>, |
3921 | - ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { |
3922 | - trace!("Running FixCRLF filter"); |
3923 | - use std::io::prelude::*; |
3924 | - let mut new_vec = Vec::with_capacity(post.bytes.len()); |
3925 | - for line in post.bytes.lines() { |
3926 | - new_vec.extend_from_slice(line.unwrap().as_bytes()); |
3927 | - new_vec.extend_from_slice(b"\r\n"); |
3928 | - } |
3929 | - post.bytes = new_vec; |
3930 | - Ok((post, ctx)) |
3931 | - } |
3932 | - } |
3933 | - |
3934 | - /// Add `List-*` headers |
3935 | - pub struct AddListHeaders; |
3936 | - impl PostFilter for AddListHeaders { |
3937 | - fn feed<'p, 'list>( |
3938 | - self: Box<Self>, |
3939 | - post: &'p mut Post, |
3940 | - ctx: &'p mut ListContext<'list>, |
3941 | - ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { |
3942 | - trace!("Running AddListHeaders filter"); |
3943 | - let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); |
3944 | - let sender = format!("<{}>", ctx.list.address); |
3945 | - headers.push((&b"Sender"[..], sender.as_bytes())); |
3946 | - let mut subject = format!("[{}] ", ctx.list.id).into_bytes(); |
3947 | - if let Some((_, subj_val)) = headers |
3948 | - .iter_mut() |
3949 | - .find(|(k, _)| k.eq_ignore_ascii_case(b"Subject")) |
3950 | - { |
3951 | - subject.extend(subj_val.iter().cloned()); |
3952 | - *subj_val = subject.as_slice(); |
3953 | - } else { |
3954 | - headers.push((&b"Subject"[..], subject.as_slice())); |
3955 | - } |
3956 | - |
3957 | - let list_id = Some(ctx.list.id_header()); |
3958 | - let list_help = ctx.list.help_header(); |
3959 | - let list_post = ctx.list.post_header(ctx.post_policy.as_deref()); |
3960 | - let list_unsubscribe = ctx |
3961 | - .list |
3962 | - .unsubscribe_header(ctx.subscription_policy.as_deref()); |
3963 | - let list_subscribe = ctx |
3964 | - .list |
3965 | - .subscribe_header(ctx.subscription_policy.as_deref()); |
3966 | - let list_archive = ctx.list.archive_header(); |
3967 | - |
3968 | - for (hdr, val) in [ |
3969 | - (b"List-Id".as_slice(), &list_id), |
3970 | - (b"List-Help".as_slice(), &list_help), |
3971 | - (b"List-Post".as_slice(), &list_post), |
3972 | - (b"List-Unsubscribe".as_slice(), &list_unsubscribe), |
3973 | - (b"List-Subscribe".as_slice(), &list_subscribe), |
3974 | - (b"List-Archive".as_slice(), &list_archive), |
3975 | - ] { |
3976 | - if let Some(val) = val { |
3977 | - headers.push((hdr, val.as_bytes())); |
3978 | - } |
3979 | - } |
3980 | - |
3981 | - let mut new_vec = Vec::with_capacity( |
3982 | - headers |
3983 | - .iter() |
3984 | - .map(|(h, v)| h.len() + v.len() + ": \r\n".len()) |
3985 | - .sum::<usize>() |
3986 | - + "\r\n\r\n".len() |
3987 | - + body.len(), |
3988 | - ); |
3989 | - for (h, v) in headers { |
3990 | - new_vec.extend_from_slice(h); |
3991 | - new_vec.extend_from_slice(b": "); |
3992 | - new_vec.extend_from_slice(v); |
3993 | - new_vec.extend_from_slice(b"\r\n"); |
3994 | - } |
3995 | - new_vec.extend_from_slice(b"\r\n\r\n"); |
3996 | - new_vec.extend_from_slice(body); |
3997 | - |
3998 | - post.bytes = new_vec; |
3999 | - Ok((post, ctx)) |
4000 | - } |
4001 | - } |
4002 | - |
4003 | - /// Adds `Archived-At` field, if configured. |
4004 | - pub struct ArchivedAtLink; |
4005 | - impl PostFilter for ArchivedAtLink { |
4006 | - fn feed<'p, 'list>( |
4007 | - self: Box<Self>, |
4008 | - post: &'p mut Post, |
4009 | - ctx: &'p mut ListContext<'list>, |
4010 | - ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { |
4011 | - trace!("Running ArchivedAtLink filter"); |
4012 | - Ok((post, ctx)) |
4013 | - } |
4014 | - } |
4015 | - |
4016 | - /// Assuming there are no more changes to be done on the post, it finalizes |
4017 | - /// which list subscriptions will receive the post in `post.action` field. |
4018 | - pub struct FinalizeRecipients; |
4019 | - impl PostFilter for FinalizeRecipients { |
4020 | - fn feed<'p, 'list>( |
4021 | - self: Box<Self>, |
4022 | - post: &'p mut Post, |
4023 | - ctx: &'p mut ListContext<'list>, |
4024 | - ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { |
4025 | - trace!("Running FinalizeRecipients filter"); |
4026 | - let mut recipients = vec![]; |
4027 | - let mut digests = vec![]; |
4028 | - let email_from = post.from.get_email(); |
4029 | - for subscription in ctx.subscriptions { |
4030 | - trace!("examining subscription {:?}", &subscription); |
4031 | - if subscription.address == email_from { |
4032 | - trace!("subscription is submitter"); |
4033 | - } |
4034 | - if subscription.digest { |
4035 | - if subscription.address != email_from || subscription.receive_own_posts { |
4036 | - trace!("Subscription gets digest"); |
4037 | - digests.push(subscription.address()); |
4038 | - } |
4039 | - continue; |
4040 | - } |
4041 | - if subscription.address != email_from || subscription.receive_own_posts { |
4042 | - trace!("Subscription gets copy"); |
4043 | - recipients.push(subscription.address()); |
4044 | - } |
4045 | - } |
4046 | - ctx.scheduled_jobs.push(MailJob::Send { recipients }); |
4047 | - if !digests.is_empty() { |
4048 | - ctx.scheduled_jobs.push(MailJob::StoreDigest { |
4049 | - recipients: digests, |
4050 | - }); |
4051 | - } |
4052 | - post.action = PostAction::Accept; |
4053 | - Ok((post, ctx)) |
4054 | - } |
4055 | - } |
4056 | diff --git a/core/src/message_filters.rs b/core/src/message_filters.rs |
4057 | new file mode 100644 |
4058 | index 0000000..2a0ecbd |
4059 | --- /dev/null |
4060 | +++ b/core/src/message_filters.rs |
4061 | @@ -0,0 +1,275 @@ |
4062 | + /* |
4063 | + * This file is part of mailpot |
4064 | + * |
4065 | + * Copyright 2020 - Manos Pitsidianakis |
4066 | + * |
4067 | + * This program is free software: you can redistribute it and/or modify |
4068 | + * it under the terms of the GNU Affero General Public License as |
4069 | + * published by the Free Software Foundation, either version 3 of the |
4070 | + * License, or (at your option) any later version. |
4071 | + * |
4072 | + * This program is distributed in the hope that it will be useful, |
4073 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
4074 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4075 | + * GNU Affero General Public License for more details. |
4076 | + * |
4077 | + * You should have received a copy of the GNU Affero General Public License |
4078 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
4079 | + */ |
4080 | + |
4081 | + #![allow(clippy::result_unit_err)] |
4082 | + |
4083 | + //! Filters to pass each mailing list post through. Filters are functions that |
4084 | + //! implement the [`PostFilter`] trait that can: |
4085 | + //! |
4086 | + //! - transform post content. |
4087 | + //! - modify the final [`PostAction`] to take. |
4088 | + //! - modify the final scheduled jobs to perform. (See [`MailJob`]). |
4089 | + //! |
4090 | + //! Filters are executed in sequence like this: |
4091 | + //! |
4092 | + //! ```ignore |
4093 | + //! let result = filters |
4094 | + //! .into_iter() |
4095 | + //! .fold(Ok((&mut post, &mut list_ctx)), |p, f| { |
4096 | + //! p.and_then(|(p, c)| f.feed(p, c)) |
4097 | + //! }); |
4098 | + //! ``` |
4099 | + //! |
4100 | + //! so the processing stops at the first returned error. |
4101 | + |
4102 | + use log::trace; |
4103 | + use melib::Address; |
4104 | + |
4105 | + use crate::{ |
4106 | + mail::{ListContext, MailJob, PostAction, PostEntry}, |
4107 | + models::{DbVal, MailingList}, |
4108 | + Connection, |
4109 | + }; |
4110 | + |
4111 | + impl Connection { |
4112 | + /// Return the post filters of a mailing list. |
4113 | + pub fn list_filters(&self, _list: &DbVal<MailingList>) -> Vec<Box<dyn PostFilter>> { |
4114 | + vec![ |
4115 | + Box::new(FixCRLF), |
4116 | + Box::new(PostRightsCheck), |
4117 | + Box::new(AddListHeaders), |
4118 | + Box::new(FinalizeRecipients), |
4119 | + ] |
4120 | + } |
4121 | + } |
4122 | + |
4123 | + /// Filter that modifies and/or verifies a post candidate. On rejection, return |
4124 | + /// a string describing the error and optionally set `post.action` to `Reject` |
4125 | + /// or `Defer` |
4126 | + pub trait PostFilter { |
4127 | + /// Feed post into the filter. Perform modifications to `post` and / or |
4128 | + /// `ctx`, and return them with `Result::Ok` unless you want to the |
4129 | + /// processing to stop and return an `Result::Err`. |
4130 | + fn feed<'p, 'list>( |
4131 | + self: Box<Self>, |
4132 | + post: &'p mut PostEntry, |
4133 | + ctx: &'p mut ListContext<'list>, |
4134 | + ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()>; |
4135 | + } |
4136 | + |
4137 | + /// Check that submitter can post to list, for now it accepts everything. |
4138 | + pub struct PostRightsCheck; |
4139 | + impl PostFilter for PostRightsCheck { |
4140 | + fn feed<'p, 'list>( |
4141 | + self: Box<Self>, |
4142 | + post: &'p mut PostEntry, |
4143 | + ctx: &'p mut ListContext<'list>, |
4144 | + ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
4145 | + trace!("Running PostRightsCheck filter"); |
4146 | + if let Some(ref policy) = ctx.post_policy { |
4147 | + if policy.announce_only { |
4148 | + trace!("post policy is announce_only"); |
4149 | + let owner_addresses = ctx |
4150 | + .list_owners |
4151 | + .iter() |
4152 | + .map(|lo| lo.address()) |
4153 | + .collect::<Vec<Address>>(); |
4154 | + trace!("Owner addresses are: {:#?}", &owner_addresses); |
4155 | + trace!("Envelope from is: {:?}", &post.from); |
4156 | + if !owner_addresses.iter().any(|addr| *addr == post.from) { |
4157 | + trace!("Envelope From does not include any owner"); |
4158 | + post.action = PostAction::Reject { |
4159 | + reason: "You are not allowed to post on this list.".to_string(), |
4160 | + }; |
4161 | + return Err(()); |
4162 | + } |
4163 | + } else if policy.subscription_only { |
4164 | + trace!("post policy is subscription_only"); |
4165 | + let email_from = post.from.get_email(); |
4166 | + trace!("post from is {:?}", &email_from); |
4167 | + trace!("post subscriptions are {:#?}", &ctx.subscriptions); |
4168 | + if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) { |
4169 | + trace!("Envelope from is not subscribed to this list"); |
4170 | + post.action = PostAction::Reject { |
4171 | + reason: "Only subscriptions can post to this list.".to_string(), |
4172 | + }; |
4173 | + return Err(()); |
4174 | + } |
4175 | + } else if policy.approval_needed { |
4176 | + trace!("post policy says approval_needed"); |
4177 | + let email_from = post.from.get_email(); |
4178 | + trace!("post from is {:?}", &email_from); |
4179 | + trace!("post subscriptions are {:#?}", &ctx.subscriptions); |
4180 | + if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) { |
4181 | + trace!("Envelope from is not subscribed to this list"); |
4182 | + post.action = PostAction::Defer { |
4183 | + reason: "Your posting has been deferred. Approval from the list's \ |
4184 | + moderators is required before it is submitted." |
4185 | + .to_string(), |
4186 | + }; |
4187 | + return Err(()); |
4188 | + } |
4189 | + } |
4190 | + } |
4191 | + Ok((post, ctx)) |
4192 | + } |
4193 | + } |
4194 | + |
4195 | + /// Ensure message contains only `\r\n` line terminators, required by SMTP. |
4196 | + pub struct FixCRLF; |
4197 | + impl PostFilter for FixCRLF { |
4198 | + fn feed<'p, 'list>( |
4199 | + self: Box<Self>, |
4200 | + post: &'p mut PostEntry, |
4201 | + ctx: &'p mut ListContext<'list>, |
4202 | + ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
4203 | + trace!("Running FixCRLF filter"); |
4204 | + use std::io::prelude::*; |
4205 | + let mut new_vec = Vec::with_capacity(post.bytes.len()); |
4206 | + for line in post.bytes.lines() { |
4207 | + new_vec.extend_from_slice(line.unwrap().as_bytes()); |
4208 | + new_vec.extend_from_slice(b"\r\n"); |
4209 | + } |
4210 | + post.bytes = new_vec; |
4211 | + Ok((post, ctx)) |
4212 | + } |
4213 | + } |
4214 | + |
4215 | + /// Add `List-*` headers |
4216 | + pub struct AddListHeaders; |
4217 | + impl PostFilter for AddListHeaders { |
4218 | + fn feed<'p, 'list>( |
4219 | + self: Box<Self>, |
4220 | + post: &'p mut PostEntry, |
4221 | + ctx: &'p mut ListContext<'list>, |
4222 | + ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
4223 | + trace!("Running AddListHeaders filter"); |
4224 | + let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); |
4225 | + let sender = format!("<{}>", ctx.list.address); |
4226 | + headers.push((&b"Sender"[..], sender.as_bytes())); |
4227 | + let mut subject = format!("[{}] ", ctx.list.id).into_bytes(); |
4228 | + if let Some((_, subj_val)) = headers |
4229 | + .iter_mut() |
4230 | + .find(|(k, _)| k.eq_ignore_ascii_case(b"Subject")) |
4231 | + { |
4232 | + subject.extend(subj_val.iter().cloned()); |
4233 | + *subj_val = subject.as_slice(); |
4234 | + } else { |
4235 | + headers.push((&b"Subject"[..], subject.as_slice())); |
4236 | + } |
4237 | + |
4238 | + let list_id = Some(ctx.list.id_header()); |
4239 | + let list_help = ctx.list.help_header(); |
4240 | + let list_post = ctx.list.post_header(ctx.post_policy.as_deref()); |
4241 | + let list_unsubscribe = ctx |
4242 | + .list |
4243 | + .unsubscribe_header(ctx.subscription_policy.as_deref()); |
4244 | + let list_subscribe = ctx |
4245 | + .list |
4246 | + .subscribe_header(ctx.subscription_policy.as_deref()); |
4247 | + let list_archive = ctx.list.archive_header(); |
4248 | + |
4249 | + for (hdr, val) in [ |
4250 | + (b"List-Id".as_slice(), &list_id), |
4251 | + (b"List-Help".as_slice(), &list_help), |
4252 | + (b"List-Post".as_slice(), &list_post), |
4253 | + (b"List-Unsubscribe".as_slice(), &list_unsubscribe), |
4254 | + (b"List-Subscribe".as_slice(), &list_subscribe), |
4255 | + (b"List-Archive".as_slice(), &list_archive), |
4256 | + ] { |
4257 | + if let Some(val) = val { |
4258 | + headers.push((hdr, val.as_bytes())); |
4259 | + } |
4260 | + } |
4261 | + |
4262 | + let mut new_vec = Vec::with_capacity( |
4263 | + headers |
4264 | + .iter() |
4265 | + .map(|(h, v)| h.len() + v.len() + ": \r\n".len()) |
4266 | + .sum::<usize>() |
4267 | + + "\r\n\r\n".len() |
4268 | + + body.len(), |
4269 | + ); |
4270 | + for (h, v) in headers { |
4271 | + new_vec.extend_from_slice(h); |
4272 | + new_vec.extend_from_slice(b": "); |
4273 | + new_vec.extend_from_slice(v); |
4274 | + new_vec.extend_from_slice(b"\r\n"); |
4275 | + } |
4276 | + new_vec.extend_from_slice(b"\r\n\r\n"); |
4277 | + new_vec.extend_from_slice(body); |
4278 | + |
4279 | + post.bytes = new_vec; |
4280 | + Ok((post, ctx)) |
4281 | + } |
4282 | + } |
4283 | + |
4284 | + /// Adds `Archived-At` field, if configured. |
4285 | + pub struct ArchivedAtLink; |
4286 | + impl PostFilter for ArchivedAtLink { |
4287 | + fn feed<'p, 'list>( |
4288 | + self: Box<Self>, |
4289 | + post: &'p mut PostEntry, |
4290 | + ctx: &'p mut ListContext<'list>, |
4291 | + ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
4292 | + trace!("Running ArchivedAtLink filter"); |
4293 | + Ok((post, ctx)) |
4294 | + } |
4295 | + } |
4296 | + |
4297 | + /// Assuming there are no more changes to be done on the post, it finalizes |
4298 | + /// which list subscriptions will receive the post in `post.action` field. |
4299 | + pub struct FinalizeRecipients; |
4300 | + impl PostFilter for FinalizeRecipients { |
4301 | + fn feed<'p, 'list>( |
4302 | + self: Box<Self>, |
4303 | + post: &'p mut PostEntry, |
4304 | + ctx: &'p mut ListContext<'list>, |
4305 | + ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> { |
4306 | + trace!("Running FinalizeRecipients filter"); |
4307 | + let mut recipients = vec![]; |
4308 | + let mut digests = vec![]; |
4309 | + let email_from = post.from.get_email(); |
4310 | + for subscription in ctx.subscriptions { |
4311 | + trace!("examining subscription {:?}", &subscription); |
4312 | + if subscription.address == email_from { |
4313 | + trace!("subscription is submitter"); |
4314 | + } |
4315 | + if subscription.digest { |
4316 | + if subscription.address != email_from || subscription.receive_own_posts { |
4317 | + trace!("Subscription gets digest"); |
4318 | + digests.push(subscription.address()); |
4319 | + } |
4320 | + continue; |
4321 | + } |
4322 | + if subscription.address != email_from || subscription.receive_own_posts { |
4323 | + trace!("Subscription gets copy"); |
4324 | + recipients.push(subscription.address()); |
4325 | + } |
4326 | + } |
4327 | + ctx.scheduled_jobs.push(MailJob::Send { recipients }); |
4328 | + if !digests.is_empty() { |
4329 | + ctx.scheduled_jobs.push(MailJob::StoreDigest { |
4330 | + recipients: digests, |
4331 | + }); |
4332 | + } |
4333 | + post.action = PostAction::Accept; |
4334 | + Ok((post, ctx)) |
4335 | + } |
4336 | + } |
4337 | diff --git a/core/src/policies.rs b/core/src/policies.rs |
4338 | new file mode 100644 |
4339 | index 0000000..902404a |
4340 | --- /dev/null |
4341 | +++ b/core/src/policies.rs |
4342 | @@ -0,0 +1,407 @@ |
4343 | + /* |
4344 | + * This file is part of mailpot |
4345 | + * |
4346 | + * Copyright 2020 - Manos Pitsidianakis |
4347 | + * |
4348 | + * This program is free software: you can redistribute it and/or modify |
4349 | + * it under the terms of the GNU Affero General Public License as |
4350 | + * published by the Free Software Foundation, either version 3 of the |
4351 | + * License, or (at your option) any later version. |
4352 | + * |
4353 | + * This program is distributed in the hope that it will be useful, |
4354 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
4355 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4356 | + * GNU Affero General Public License for more details. |
4357 | + * |
4358 | + * You should have received a copy of the GNU Affero General Public License |
4359 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
4360 | + */ |
4361 | + |
4362 | + //! How each list handles new posts and new subscriptions. |
4363 | + |
4364 | + pub use post_policy::*; |
4365 | + pub use subscription_policy::*; |
4366 | + |
4367 | + mod post_policy { |
4368 | + use log::trace; |
4369 | + use rusqlite::OptionalExtension; |
4370 | + |
4371 | + use crate::{ |
4372 | + errors::{ErrorKind::*, *}, |
4373 | + models::{DbVal, PostPolicy}, |
4374 | + Connection, |
4375 | + }; |
4376 | + |
4377 | + impl Connection { |
4378 | + /// Fetch the post policy of a mailing list. |
4379 | + pub fn list_post_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> { |
4380 | + let mut stmt = self |
4381 | + .connection |
4382 | + .prepare("SELECT * FROM post_policy WHERE list = ?;")?; |
4383 | + let ret = stmt |
4384 | + .query_row([&pk], |row| { |
4385 | + let pk = row.get("pk")?; |
4386 | + Ok(DbVal( |
4387 | + PostPolicy { |
4388 | + pk, |
4389 | + list: row.get("list")?, |
4390 | + announce_only: row.get("announce_only")?, |
4391 | + subscription_only: row.get("subscription_only")?, |
4392 | + approval_needed: row.get("approval_needed")?, |
4393 | + open: row.get("open")?, |
4394 | + custom: row.get("custom")?, |
4395 | + }, |
4396 | + pk, |
4397 | + )) |
4398 | + }) |
4399 | + .optional()?; |
4400 | + |
4401 | + Ok(ret) |
4402 | + } |
4403 | + |
4404 | + /// Remove an existing list policy. |
4405 | + /// |
4406 | + /// ``` |
4407 | + /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
4408 | + /// # use tempfile::TempDir; |
4409 | + /// |
4410 | + /// # let tmp_dir = TempDir::new().unwrap(); |
4411 | + /// # let db_path = tmp_dir.path().join("mpot.db"); |
4412 | + /// # let config = Configuration { |
4413 | + /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
4414 | + /// # db_path: db_path.clone(), |
4415 | + /// # data_path: tmp_dir.path().to_path_buf(), |
4416 | + /// # administrators: vec![], |
4417 | + /// # }; |
4418 | + /// |
4419 | + /// # fn do_test(config: Configuration) { |
4420 | + /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
4421 | + /// # assert!(db.list_post_policy(1).unwrap().is_none()); |
4422 | + /// let list = db |
4423 | + /// .create_list(MailingList { |
4424 | + /// pk: 0, |
4425 | + /// name: "foobar chat".into(), |
4426 | + /// id: "foo-chat".into(), |
4427 | + /// address: "foo-chat@example.com".into(), |
4428 | + /// description: None, |
4429 | + /// archive_url: None, |
4430 | + /// }) |
4431 | + /// .unwrap(); |
4432 | + /// |
4433 | + /// # assert!(db.list_post_policy(list.pk()).unwrap().is_none()); |
4434 | + /// let pol = db |
4435 | + /// .set_list_post_policy(PostPolicy { |
4436 | + /// pk: -1, |
4437 | + /// list: list.pk(), |
4438 | + /// announce_only: false, |
4439 | + /// subscription_only: true, |
4440 | + /// approval_needed: false, |
4441 | + /// open: false, |
4442 | + /// custom: false, |
4443 | + /// }) |
4444 | + /// .unwrap(); |
4445 | + /// # assert_eq!(db.list_post_policy(list.pk()).unwrap().as_ref(), Some(&pol)); |
4446 | + /// db.remove_list_post_policy(list.pk(), pol.pk()).unwrap(); |
4447 | + /// # assert!(db.list_post_policy(list.pk()).unwrap().is_none()); |
4448 | + /// # } |
4449 | + /// # do_test(config); |
4450 | + /// ``` |
4451 | + pub fn remove_list_post_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> { |
4452 | + let mut stmt = self |
4453 | + .connection |
4454 | + .prepare("DELETE FROM post_policy WHERE pk = ? AND list = ? RETURNING *;")?; |
4455 | + stmt.query_row(rusqlite::params![&policy_pk, &list_pk,], |_| Ok(())) |
4456 | + .map_err(|err| { |
4457 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
4458 | + Error::from(err).chain_err(|| NotFound("list or list policy not found!")) |
4459 | + } else { |
4460 | + err.into() |
4461 | + } |
4462 | + })?; |
4463 | + |
4464 | + trace!("remove_list_post_policy {} {}.", list_pk, policy_pk); |
4465 | + Ok(()) |
4466 | + } |
4467 | + |
4468 | + /// ```should_panic |
4469 | + /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
4470 | + /// # use tempfile::TempDir; |
4471 | + /// |
4472 | + /// # let tmp_dir = TempDir::new().unwrap(); |
4473 | + /// # let db_path = tmp_dir.path().join("mpot.db"); |
4474 | + /// # let config = Configuration { |
4475 | + /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
4476 | + /// # db_path: db_path.clone(), |
4477 | + /// # data_path: tmp_dir.path().to_path_buf(), |
4478 | + /// # administrators: vec![], |
4479 | + /// # }; |
4480 | + /// |
4481 | + /// # fn do_test(config: Configuration) { |
4482 | + /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
4483 | + /// db.remove_list_post_policy(1, 1).unwrap(); |
4484 | + /// # } |
4485 | + /// # do_test(config); |
4486 | + /// ``` |
4487 | + #[cfg(doc)] |
4488 | + pub fn remove_list_post_policy_panic() {} |
4489 | + |
4490 | + /// Set the unique post policy for a list. |
4491 | + pub fn set_list_post_policy(&self, policy: PostPolicy) -> Result<DbVal<PostPolicy>> { |
4492 | + if !(policy.announce_only |
4493 | + || policy.subscription_only |
4494 | + || policy.approval_needed |
4495 | + || policy.open |
4496 | + || policy.custom) |
4497 | + { |
4498 | + return Err( |
4499 | + "Cannot add empty policy. Having no policies is probably what you want to do." |
4500 | + .into(), |
4501 | + ); |
4502 | + } |
4503 | + let list_pk = policy.list; |
4504 | + |
4505 | + let mut stmt = self.connection.prepare( |
4506 | + "INSERT OR REPLACE INTO post_policy(list, announce_only, subscription_only, \ |
4507 | + approval_needed, open, custom) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;", |
4508 | + )?; |
4509 | + let ret = stmt |
4510 | + .query_row( |
4511 | + rusqlite::params![ |
4512 | + &list_pk, |
4513 | + &policy.announce_only, |
4514 | + &policy.subscription_only, |
4515 | + &policy.approval_needed, |
4516 | + &policy.open, |
4517 | + &policy.custom, |
4518 | + ], |
4519 | + |row| { |
4520 | + let pk = row.get("pk")?; |
4521 | + Ok(DbVal( |
4522 | + PostPolicy { |
4523 | + pk, |
4524 | + list: row.get("list")?, |
4525 | + announce_only: row.get("announce_only")?, |
4526 | + subscription_only: row.get("subscription_only")?, |
4527 | + approval_needed: row.get("approval_needed")?, |
4528 | + open: row.get("open")?, |
4529 | + custom: row.get("custom")?, |
4530 | + }, |
4531 | + pk, |
4532 | + )) |
4533 | + }, |
4534 | + ) |
4535 | + .map_err(|err| { |
4536 | + if matches!( |
4537 | + err, |
4538 | + rusqlite::Error::SqliteFailure( |
4539 | + rusqlite::ffi::Error { |
4540 | + code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
4541 | + extended_code: 787 |
4542 | + }, |
4543 | + _ |
4544 | + ) |
4545 | + ) { |
4546 | + Error::from(err) |
4547 | + .chain_err(|| NotFound("Could not find a list with this pk.")) |
4548 | + } else { |
4549 | + err.into() |
4550 | + } |
4551 | + })?; |
4552 | + |
4553 | + trace!("set_list_post_policy {:?}.", &ret); |
4554 | + Ok(ret) |
4555 | + } |
4556 | + } |
4557 | + } |
4558 | + |
4559 | + mod subscription_policy { |
4560 | + use log::trace; |
4561 | + use rusqlite::OptionalExtension; |
4562 | + |
4563 | + use crate::{ |
4564 | + errors::{ErrorKind::*, *}, |
4565 | + models::{DbVal, SubscriptionPolicy}, |
4566 | + Connection, |
4567 | + }; |
4568 | + |
4569 | + impl Connection { |
4570 | + /// Fetch the subscription policy of a mailing list. |
4571 | + pub fn list_subscription_policy( |
4572 | + &self, |
4573 | + pk: i64, |
4574 | + ) -> Result<Option<DbVal<SubscriptionPolicy>>> { |
4575 | + let mut stmt = self |
4576 | + .connection |
4577 | + .prepare("SELECT * FROM subscription_policy WHERE list = ?;")?; |
4578 | + let ret = stmt |
4579 | + .query_row([&pk], |row| { |
4580 | + let pk = row.get("pk")?; |
4581 | + Ok(DbVal( |
4582 | + SubscriptionPolicy { |
4583 | + pk, |
4584 | + list: row.get("list")?, |
4585 | + send_confirmation: row.get("send_confirmation")?, |
4586 | + open: row.get("open")?, |
4587 | + manual: row.get("manual")?, |
4588 | + request: row.get("request")?, |
4589 | + custom: row.get("custom")?, |
4590 | + }, |
4591 | + pk, |
4592 | + )) |
4593 | + }) |
4594 | + .optional()?; |
4595 | + |
4596 | + Ok(ret) |
4597 | + } |
4598 | + |
4599 | + /// Remove an existing subscription policy. |
4600 | + /// |
4601 | + /// ``` |
4602 | + /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
4603 | + /// # use tempfile::TempDir; |
4604 | + /// |
4605 | + /// # let tmp_dir = TempDir::new().unwrap(); |
4606 | + /// # let db_path = tmp_dir.path().join("mpot.db"); |
4607 | + /// # let config = Configuration { |
4608 | + /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
4609 | + /// # db_path: db_path.clone(), |
4610 | + /// # data_path: tmp_dir.path().to_path_buf(), |
4611 | + /// # administrators: vec![], |
4612 | + /// # }; |
4613 | + /// |
4614 | + /// # fn do_test(config: Configuration) { |
4615 | + /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
4616 | + /// let list = db |
4617 | + /// .create_list(MailingList { |
4618 | + /// pk: 0, |
4619 | + /// name: "foobar chat".into(), |
4620 | + /// id: "foo-chat".into(), |
4621 | + /// address: "foo-chat@example.com".into(), |
4622 | + /// description: None, |
4623 | + /// archive_url: None, |
4624 | + /// }) |
4625 | + /// .unwrap(); |
4626 | + /// # assert!(db.list_subscription_policy(list.pk()).unwrap().is_none()); |
4627 | + /// let pol = db |
4628 | + /// .set_list_subscription_policy(SubscriptionPolicy { |
4629 | + /// pk: -1, |
4630 | + /// list: list.pk(), |
4631 | + /// send_confirmation: false, |
4632 | + /// open: true, |
4633 | + /// manual: false, |
4634 | + /// request: false, |
4635 | + /// custom: false, |
4636 | + /// }) |
4637 | + /// .unwrap(); |
4638 | + /// # assert_eq!(db.list_subscription_policy(list.pk()).unwrap().as_ref(), Some(&pol)); |
4639 | + /// db.remove_list_subscription_policy(list.pk(), pol.pk()) |
4640 | + /// .unwrap(); |
4641 | + /// # assert!(db.list_subscription_policy(list.pk()).unwrap().is_none()); |
4642 | + /// # } |
4643 | + /// # do_test(config); |
4644 | + /// ``` |
4645 | + pub fn remove_list_subscription_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> { |
4646 | + let mut stmt = self.connection.prepare( |
4647 | + "DELETE FROM subscription_policy WHERE pk = ? AND list = ? RETURNING *;", |
4648 | + )?; |
4649 | + stmt.query_row(rusqlite::params![&policy_pk, &list_pk,], |_| Ok(())) |
4650 | + .map_err(|err| { |
4651 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
4652 | + Error::from(err).chain_err(|| NotFound("list or list policy not found!")) |
4653 | + } else { |
4654 | + err.into() |
4655 | + } |
4656 | + })?; |
4657 | + |
4658 | + trace!("remove_list_subscription_policy {} {}.", list_pk, policy_pk); |
4659 | + Ok(()) |
4660 | + } |
4661 | + |
4662 | + /// ```should_panic |
4663 | + /// # use mailpot::{models::*, Configuration, Connection, SendMail}; |
4664 | + /// # use tempfile::TempDir; |
4665 | + /// |
4666 | + /// # let tmp_dir = TempDir::new().unwrap(); |
4667 | + /// # let db_path = tmp_dir.path().join("mpot.db"); |
4668 | + /// # let config = Configuration { |
4669 | + /// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
4670 | + /// # db_path: db_path.clone(), |
4671 | + /// # data_path: tmp_dir.path().to_path_buf(), |
4672 | + /// # administrators: vec![], |
4673 | + /// # }; |
4674 | + /// |
4675 | + /// # fn do_test(config: Configuration) { |
4676 | + /// let db = Connection::open_or_create_db(config).unwrap().trusted(); |
4677 | + /// db.remove_list_post_policy(1, 1).unwrap(); |
4678 | + /// # } |
4679 | + /// # do_test(config); |
4680 | + /// ``` |
4681 | + #[cfg(doc)] |
4682 | + pub fn remove_list_subscription_policy_panic() {} |
4683 | + |
4684 | + /// Set the unique post policy for a list. |
4685 | + pub fn set_list_subscription_policy( |
4686 | + &self, |
4687 | + policy: SubscriptionPolicy, |
4688 | + ) -> Result<DbVal<SubscriptionPolicy>> { |
4689 | + if !(policy.open || policy.manual || policy.request || policy.custom) { |
4690 | + return Err( |
4691 | + "Cannot add empty policy. Having no policy is probably what you want to do." |
4692 | + .into(), |
4693 | + ); |
4694 | + } |
4695 | + let list_pk = policy.list; |
4696 | + |
4697 | + let mut stmt = self.connection.prepare( |
4698 | + "INSERT OR REPLACE INTO subscription_policy(list, send_confirmation, open, \ |
4699 | + manual, request, custom) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;", |
4700 | + )?; |
4701 | + let ret = stmt |
4702 | + .query_row( |
4703 | + rusqlite::params![ |
4704 | + &list_pk, |
4705 | + &policy.send_confirmation, |
4706 | + &policy.open, |
4707 | + &policy.manual, |
4708 | + &policy.request, |
4709 | + &policy.custom, |
4710 | + ], |
4711 | + |row| { |
4712 | + let pk = row.get("pk")?; |
4713 | + Ok(DbVal( |
4714 | + SubscriptionPolicy { |
4715 | + pk, |
4716 | + list: row.get("list")?, |
4717 | + send_confirmation: row.get("send_confirmation")?, |
4718 | + open: row.get("open")?, |
4719 | + manual: row.get("manual")?, |
4720 | + request: row.get("request")?, |
4721 | + custom: row.get("custom")?, |
4722 | + }, |
4723 | + pk, |
4724 | + )) |
4725 | + }, |
4726 | + ) |
4727 | + .map_err(|err| { |
4728 | + if matches!( |
4729 | + err, |
4730 | + rusqlite::Error::SqliteFailure( |
4731 | + rusqlite::ffi::Error { |
4732 | + code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
4733 | + extended_code: 787 |
4734 | + }, |
4735 | + _ |
4736 | + ) |
4737 | + ) { |
4738 | + Error::from(err) |
4739 | + .chain_err(|| NotFound("Could not find a list with this pk.")) |
4740 | + } else { |
4741 | + err.into() |
4742 | + } |
4743 | + })?; |
4744 | + |
4745 | + trace!("set_list_subscription_policy {:?}.", &ret); |
4746 | + Ok(ret) |
4747 | + } |
4748 | + } |
4749 | + } |
4750 | diff --git a/core/src/posts.rs b/core/src/posts.rs |
4751 | new file mode 100644 |
4752 | index 0000000..36ab575 |
4753 | --- /dev/null |
4754 | +++ b/core/src/posts.rs |
4755 | @@ -0,0 +1,780 @@ |
4756 | + /* |
4757 | + * This file is part of mailpot |
4758 | + * |
4759 | + * Copyright 2020 - Manos Pitsidianakis |
4760 | + * |
4761 | + * This program is free software: you can redistribute it and/or modify |
4762 | + * it under the terms of the GNU Affero General Public License as |
4763 | + * published by the Free Software Foundation, either version 3 of the |
4764 | + * License, or (at your option) any later version. |
4765 | + * |
4766 | + * This program is distributed in the hope that it will be useful, |
4767 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
4768 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4769 | + * GNU Affero General Public License for more details. |
4770 | + * |
4771 | + * You should have received a copy of the GNU Affero General Public License |
4772 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
4773 | + */ |
4774 | + |
4775 | + //! Processing new posts. |
4776 | + |
4777 | + use std::borrow::Cow; |
4778 | + |
4779 | + use log::{info, trace}; |
4780 | + use melib::Envelope; |
4781 | + use rusqlite::OptionalExtension; |
4782 | + |
4783 | + use crate::{ |
4784 | + errors::*, |
4785 | + mail::{ListContext, ListRequest, PostAction, PostEntry}, |
4786 | + models::{changesets::AccountChangeset, Account, DbVal, ListSubscription, MailingList, Post}, |
4787 | + queue::{Queue, QueueEntry}, |
4788 | + templates::Template, |
4789 | + Connection, |
4790 | + }; |
4791 | + |
4792 | + impl Connection { |
4793 | + /// Insert a mailing list post into the database. |
4794 | + pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> { |
4795 | + let from_ = env.from(); |
4796 | + let address = if from_.is_empty() { |
4797 | + String::new() |
4798 | + } else { |
4799 | + from_[0].get_email() |
4800 | + }; |
4801 | + let datetime: std::borrow::Cow<'_, str> = if !env.date.as_str().is_empty() { |
4802 | + env.date.as_str().into() |
4803 | + } else { |
4804 | + melib::datetime::timestamp_to_string( |
4805 | + env.timestamp, |
4806 | + Some(melib::datetime::RFC822_DATE), |
4807 | + true, |
4808 | + ) |
4809 | + .into() |
4810 | + }; |
4811 | + let message_id = env.message_id_display(); |
4812 | + let mut stmt = self.connection.prepare( |
4813 | + "INSERT OR REPLACE INTO post(list, address, message_id, message, datetime, timestamp) \ |
4814 | + VALUES(?, ?, ?, ?, ?, ?) RETURNING pk;", |
4815 | + )?; |
4816 | + let pk = stmt.query_row( |
4817 | + rusqlite::params![ |
4818 | + &list_pk, |
4819 | + &address, |
4820 | + &message_id, |
4821 | + &message, |
4822 | + &datetime, |
4823 | + &env.timestamp |
4824 | + ], |
4825 | + |row| { |
4826 | + let pk: i64 = row.get("pk")?; |
4827 | + Ok(pk) |
4828 | + }, |
4829 | + )?; |
4830 | + |
4831 | + trace!( |
4832 | + "insert_post list_pk {}, from {:?} message_id {:?} post_pk {}.", |
4833 | + list_pk, |
4834 | + address, |
4835 | + message_id, |
4836 | + pk |
4837 | + ); |
4838 | + Ok(pk) |
4839 | + } |
4840 | + |
4841 | + /// Process a new mailing list post. |
4842 | + pub fn post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { |
4843 | + let result = self.inner_post(env, raw, _dry_run); |
4844 | + if let Err(err) = result { |
4845 | + return match self.insert_to_queue(QueueEntry::new( |
4846 | + Queue::Error, |
4847 | + None, |
4848 | + Some(Cow::Borrowed(env)), |
4849 | + raw, |
4850 | + Some(err.to_string()), |
4851 | + )?) { |
4852 | + Ok(idx) => { |
4853 | + log::info!( |
4854 | + "Inserted mail from {:?} into error_queue at index {}", |
4855 | + env.from(), |
4856 | + idx |
4857 | + ); |
4858 | + Err(err) |
4859 | + } |
4860 | + Err(err2) => { |
4861 | + log::error!( |
4862 | + "Could not insert mail from {:?} into error_queue: {err2}", |
4863 | + env.from(), |
4864 | + ); |
4865 | + |
4866 | + Err(err.chain_err(|| err2)) |
4867 | + } |
4868 | + }; |
4869 | + } |
4870 | + result |
4871 | + } |
4872 | + |
4873 | + fn inner_post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { |
4874 | + trace!("Received envelope to post: {:#?}", &env); |
4875 | + let tos = env.to().to_vec(); |
4876 | + if tos.is_empty() { |
4877 | + return Err("Envelope To: field is empty!".into()); |
4878 | + } |
4879 | + if env.from().is_empty() { |
4880 | + return Err("Envelope From: field is empty!".into()); |
4881 | + } |
4882 | + let mut lists = self.lists()?; |
4883 | + if lists.is_empty() { |
4884 | + return Err("No active mailing lists found.".into()); |
4885 | + } |
4886 | + let prev_list_len = lists.len(); |
4887 | + for t in &tos { |
4888 | + if let Some((addr, subaddr)) = t.subaddress("+") { |
4889 | + lists.retain(|list| { |
4890 | + if !addr.contains_address(&list.address()) { |
4891 | + return true; |
4892 | + } |
4893 | + if let Err(err) = ListRequest::try_from((subaddr.as_str(), env)) |
4894 | + .and_then(|req| self.request(list, req, env, raw)) |
4895 | + { |
4896 | + info!("Processing request returned error: {}", err); |
4897 | + } |
4898 | + false |
4899 | + }); |
4900 | + if lists.len() != prev_list_len { |
4901 | + // Was request, handled above. |
4902 | + return Ok(()); |
4903 | + } |
4904 | + } |
4905 | + } |
4906 | + |
4907 | + lists.retain(|list| { |
4908 | + trace!( |
4909 | + "Is post related to list {}? {}", |
4910 | + &list, |
4911 | + tos.iter().any(|a| a.contains_address(&list.address())) |
4912 | + ); |
4913 | + |
4914 | + tos.iter().any(|a| a.contains_address(&list.address())) |
4915 | + }); |
4916 | + if lists.is_empty() { |
4917 | + return Err(format!( |
4918 | + "No relevant mailing list found for these addresses: {:?}", |
4919 | + tos |
4920 | + ) |
4921 | + .into()); |
4922 | + } |
4923 | + |
4924 | + trace!("Configuration is {:#?}", &self.conf); |
4925 | + for mut list in lists { |
4926 | + trace!("Examining list {}", list.display_name()); |
4927 | + let filters = self.list_filters(&list); |
4928 | + let subscriptions = self.list_subscriptions(list.pk)?; |
4929 | + let owners = self.list_owners(list.pk)?; |
4930 | + trace!("List subscriptions {:#?}", &subscriptions); |
4931 | + let mut list_ctx = ListContext { |
4932 | + post_policy: self.list_post_policy(list.pk)?, |
4933 | + subscription_policy: self.list_subscription_policy(list.pk)?, |
4934 | + list_owners: &owners, |
4935 | + list: &mut list, |
4936 | + subscriptions: &subscriptions, |
4937 | + scheduled_jobs: vec![], |
4938 | + }; |
4939 | + let mut post = PostEntry { |
4940 | + from: env.from()[0].clone(), |
4941 | + bytes: raw.to_vec(), |
4942 | + to: env.to().to_vec(), |
4943 | + action: PostAction::Hold, |
4944 | + }; |
4945 | + let result = filters |
4946 | + .into_iter() |
4947 | + .fold(Ok((&mut post, &mut list_ctx)), |p, f| { |
4948 | + p.and_then(|(p, c)| f.feed(p, c)) |
4949 | + }); |
4950 | + trace!("result {:#?}", result); |
4951 | + |
4952 | + let PostEntry { bytes, action, .. } = post; |
4953 | + trace!("Action is {:#?}", action); |
4954 | + let post_env = melib::Envelope::from_bytes(&bytes, None)?; |
4955 | + match action { |
4956 | + PostAction::Accept => { |
4957 | + let _post_pk = self.insert_post(list_ctx.list.pk, &bytes, &post_env)?; |
4958 | + trace!("post_pk is {:#?}", _post_pk); |
4959 | + for job in list_ctx.scheduled_jobs.iter() { |
4960 | + trace!("job is {:#?}", &job); |
4961 | + if let crate::mail::MailJob::Send { recipients } = job { |
4962 | + trace!("recipients: {:?}", &recipients); |
4963 | + if recipients.is_empty() { |
4964 | + trace!("list has no recipients"); |
4965 | + } |
4966 | + for recipient in recipients { |
4967 | + let mut env = post_env.clone(); |
4968 | + env.set_to(melib::smallvec::smallvec![recipient.clone()]); |
4969 | + self.insert_to_queue(QueueEntry::new( |
4970 | + Queue::Out, |
4971 | + Some(list.pk), |
4972 | + Some(Cow::Owned(env)), |
4973 | + &bytes, |
4974 | + None, |
4975 | + )?)?; |
4976 | + } |
4977 | + } |
4978 | + } |
4979 | + } |
4980 | + PostAction::Reject { reason } => { |
4981 | + log::info!("PostAction::Reject {{ reason: {} }}", reason); |
4982 | + for f in env.from() { |
4983 | + /* send error notice to e-mail sender */ |
4984 | + self.send_reply_with_list_template( |
4985 | + TemplateRenderContext { |
4986 | + template: Template::GENERIC_FAILURE, |
4987 | + default_fn: Some(Template::default_generic_failure), |
4988 | + list: &list, |
4989 | + context: minijinja::context! { |
4990 | + list => &list, |
4991 | + subject => format!("Your post to {} was rejected.", list.id), |
4992 | + details => &reason, |
4993 | + }, |
4994 | + queue: Queue::Out, |
4995 | + comment: format!("PostAction::Reject {{ reason: {} }}", reason) |
4996 | + .into(), |
4997 | + }, |
4998 | + std::iter::once(Cow::Borrowed(f)), |
4999 | + )?; |
5000 | + } |
5001 | + /* error handled by notifying submitter */ |
5002 | + return Ok(()); |
5003 | + } |
5004 | + PostAction::Defer { reason } => { |
5005 | + trace!("PostAction::Defer {{ reason: {} }}", reason); |
5006 | + for f in env.from() { |
5007 | + /* send error notice to e-mail sender */ |
5008 | + self.send_reply_with_list_template( |
5009 | + TemplateRenderContext { |
5010 | + template: Template::GENERIC_FAILURE, |
5011 | + default_fn: Some(Template::default_generic_failure), |
5012 | + list: &list, |
5013 | + context: minijinja::context! { |
5014 | + list => &list, |
5015 | + subject => format!("Your post to {} was deferred.", list.id), |
5016 | + details => &reason, |
5017 | + }, |
5018 | + queue: Queue::Out, |
5019 | + comment: format!("PostAction::Defer {{ reason: {} }}", reason) |
5020 | + .into(), |
5021 | + }, |
5022 | + std::iter::once(Cow::Borrowed(f)), |
5023 | + )?; |
5024 | + } |
5025 | + self.insert_to_queue(QueueEntry::new( |
5026 | + Queue::Deferred, |
5027 | + Some(list.pk), |
5028 | + Some(Cow::Borrowed(&post_env)), |
5029 | + &bytes, |
5030 | + Some(format!("PostAction::Defer {{ reason: {} }}", reason)), |
5031 | + )?)?; |
5032 | + return Ok(()); |
5033 | + } |
5034 | + PostAction::Hold => { |
5035 | + trace!("PostAction::Hold"); |
5036 | + self.insert_to_queue(QueueEntry::new( |
5037 | + Queue::Hold, |
5038 | + Some(list.pk), |
5039 | + Some(Cow::Borrowed(&post_env)), |
5040 | + &bytes, |
5041 | + Some("PostAction::Hold".to_string()), |
5042 | + )?)?; |
5043 | + return Ok(()); |
5044 | + } |
5045 | + } |
5046 | + } |
5047 | + |
5048 | + Ok(()) |
5049 | + } |
5050 | + |
5051 | + /// Process a new mailing list request. |
5052 | + pub fn request( |
5053 | + &mut self, |
5054 | + list: &DbVal<MailingList>, |
5055 | + request: ListRequest, |
5056 | + env: &Envelope, |
5057 | + raw: &[u8], |
5058 | + ) -> Result<()> { |
5059 | + let post_policy = self.list_post_policy(list.pk)?; |
5060 | + match request { |
5061 | + ListRequest::Help => { |
5062 | + trace!( |
5063 | + "help action for addresses {:?} in list {}", |
5064 | + env.from(), |
5065 | + list |
5066 | + ); |
5067 | + let subscription_policy = self.list_subscription_policy(list.pk)?; |
5068 | + let subject = format!("Help for {}", list.name); |
5069 | + let details = list |
5070 | + .generate_help_email(post_policy.as_deref(), subscription_policy.as_deref()); |
5071 | + for f in env.from() { |
5072 | + self.send_reply_with_list_template( |
5073 | + TemplateRenderContext { |
5074 | + template: Template::GENERIC_HELP, |
5075 | + default_fn: Some(Template::default_generic_help), |
5076 | + list, |
5077 | + context: minijinja::context! { |
5078 | + list => &list, |
5079 | + subject => &subject, |
5080 | + details => &details, |
5081 | + }, |
5082 | + queue: Queue::Out, |
5083 | + comment: "Help request".into(), |
5084 | + }, |
5085 | + std::iter::once(Cow::Borrowed(f)), |
5086 | + )?; |
5087 | + } |
5088 | + } |
5089 | + ListRequest::Subscribe => { |
5090 | + trace!( |
5091 | + "subscribe action for addresses {:?} in list {}", |
5092 | + env.from(), |
5093 | + list |
5094 | + ); |
5095 | + let approval_needed = post_policy |
5096 | + .as_ref() |
5097 | + .map(|p| p.approval_needed) |
5098 | + .unwrap_or(false); |
5099 | + for f in env.from() { |
5100 | + let email_from = f.get_email(); |
5101 | + if self |
5102 | + .list_subscription_by_address(list.pk, &email_from) |
5103 | + .is_ok() |
5104 | + { |
5105 | + /* send error notice to e-mail sender */ |
5106 | + self.send_reply_with_list_template( |
5107 | + TemplateRenderContext { |
5108 | + template: Template::GENERIC_FAILURE, |
5109 | + default_fn: Some(Template::default_generic_failure), |
5110 | + list, |
5111 | + context: minijinja::context! { |
5112 | + list => &list, |
5113 | + subject => format!("You are already subscribed to {}.", list.id), |
5114 | + details => "No action has been taken since you are already subscribed to the list.", |
5115 | + }, |
5116 | + queue: Queue::Out, |
5117 | + comment: format!("Address {} is already subscribed to list {}", f, list.id).into(), |
5118 | + }, |
5119 | + std::iter::once(Cow::Borrowed(f)), |
5120 | + )?; |
5121 | + continue; |
5122 | + } |
5123 | + |
5124 | + let subscription = ListSubscription { |
5125 | + pk: 0, |
5126 | + list: list.pk, |
5127 | + address: f.get_email(), |
5128 | + account: None, |
5129 | + name: f.get_display_name(), |
5130 | + digest: false, |
5131 | + hide_address: false, |
5132 | + receive_duplicates: true, |
5133 | + receive_own_posts: false, |
5134 | + receive_confirmation: true, |
5135 | + enabled: !approval_needed, |
5136 | + verified: true, |
5137 | + }; |
5138 | + if approval_needed { |
5139 | + match self.add_candidate_subscription(list.pk, subscription) { |
5140 | + Ok(v) => { |
5141 | + let list_owners = self.list_owners(list.pk)?; |
5142 | + self.send_reply_with_list_template( |
5143 | + TemplateRenderContext { |
5144 | + template: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER, |
5145 | + default_fn: Some( |
5146 | + Template::default_subscription_request_owner, |
5147 | + ), |
5148 | + list, |
5149 | + context: minijinja::context! { |
5150 | + list => &list, |
5151 | + candidate => &v, |
5152 | + }, |
5153 | + queue: Queue::Out, |
5154 | + comment: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER.into(), |
5155 | + }, |
5156 | + list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
5157 | + )?; |
5158 | + } |
5159 | + Err(err) => { |
5160 | + log::error!( |
5161 | + "Could not create candidate subscription for {f:?}: {err}" |
5162 | + ); |
5163 | + /* send error notice to e-mail sender */ |
5164 | + self.send_reply_with_list_template( |
5165 | + TemplateRenderContext { |
5166 | + template: Template::GENERIC_FAILURE, |
5167 | + default_fn: Some(Template::default_generic_failure), |
5168 | + list, |
5169 | + context: minijinja::context! { |
5170 | + list => &list, |
5171 | + }, |
5172 | + queue: Queue::Out, |
5173 | + comment: format!( |
5174 | + "Could not create candidate subscription for {f:?}: \ |
5175 | + {err}" |
5176 | + ) |
5177 | + .into(), |
5178 | + }, |
5179 | + std::iter::once(Cow::Borrowed(f)), |
5180 | + )?; |
5181 | + |
5182 | + /* send error details to list owners */ |
5183 | + |
5184 | + let list_owners = self.list_owners(list.pk)?; |
5185 | + self.send_reply_with_list_template( |
5186 | + TemplateRenderContext { |
5187 | + template: Template::ADMIN_NOTICE, |
5188 | + default_fn: Some(Template::default_admin_notice), |
5189 | + list, |
5190 | + context: minijinja::context! { |
5191 | + list => &list, |
5192 | + details => err.to_string(), |
5193 | + }, |
5194 | + queue: Queue::Out, |
5195 | + comment: format!( |
5196 | + "Could not create candidate subscription for {f:?}: \ |
5197 | + {err}" |
5198 | + ) |
5199 | + .into(), |
5200 | + }, |
5201 | + list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
5202 | + )?; |
5203 | + } |
5204 | + } |
5205 | + } else if let Err(err) = self.add_subscription(list.pk, subscription) { |
5206 | + log::error!("Could not create subscription for {f:?}: {err}"); |
5207 | + |
5208 | + /* send error notice to e-mail sender */ |
5209 | + |
5210 | + self.send_reply_with_list_template( |
5211 | + TemplateRenderContext { |
5212 | + template: Template::GENERIC_FAILURE, |
5213 | + default_fn: Some(Template::default_generic_failure), |
5214 | + list, |
5215 | + context: minijinja::context! { |
5216 | + list => &list, |
5217 | + }, |
5218 | + queue: Queue::Out, |
5219 | + comment: format!("Could not create subscription for {f:?}: {err}") |
5220 | + .into(), |
5221 | + }, |
5222 | + std::iter::once(Cow::Borrowed(f)), |
5223 | + )?; |
5224 | + |
5225 | + /* send error details to list owners */ |
5226 | + |
5227 | + let list_owners = self.list_owners(list.pk)?; |
5228 | + self.send_reply_with_list_template( |
5229 | + TemplateRenderContext { |
5230 | + template: Template::ADMIN_NOTICE, |
5231 | + default_fn: Some(Template::default_admin_notice), |
5232 | + list, |
5233 | + context: minijinja::context! { |
5234 | + list => &list, |
5235 | + details => err.to_string(), |
5236 | + }, |
5237 | + queue: Queue::Out, |
5238 | + comment: format!("Could not create subscription for {f:?}: {err}") |
5239 | + .into(), |
5240 | + }, |
5241 | + list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
5242 | + )?; |
5243 | + } else { |
5244 | + log::trace!( |
5245 | + "Added subscription to list {list:?} for address {f:?}, sending \ |
5246 | + confirmation." |
5247 | + ); |
5248 | + self.send_reply_with_list_template( |
5249 | + TemplateRenderContext { |
5250 | + template: Template::SUBSCRIPTION_CONFIRMATION, |
5251 | + default_fn: Some(Template::default_subscription_confirmation), |
5252 | + list, |
5253 | + context: minijinja::context! { |
5254 | + list => &list, |
5255 | + }, |
5256 | + queue: Queue::Out, |
5257 | + comment: Template::SUBSCRIPTION_CONFIRMATION.into(), |
5258 | + }, |
5259 | + std::iter::once(Cow::Borrowed(f)), |
5260 | + )?; |
5261 | + } |
5262 | + } |
5263 | + } |
5264 | + ListRequest::Unsubscribe => { |
5265 | + trace!( |
5266 | + "unsubscribe action for addresses {:?} in list {}", |
5267 | + env.from(), |
5268 | + list |
5269 | + ); |
5270 | + for f in env.from() { |
5271 | + if let Err(err) = self.remove_subscription(list.pk, &f.get_email()) { |
5272 | + log::error!("Could not unsubscribe {f:?}: {err}"); |
5273 | + /* send error notice to e-mail sender */ |
5274 | + |
5275 | + self.send_reply_with_list_template( |
5276 | + TemplateRenderContext { |
5277 | + template: Template::GENERIC_FAILURE, |
5278 | + default_fn: Some(Template::default_generic_failure), |
5279 | + list, |
5280 | + context: minijinja::context! { |
5281 | + list => &list, |
5282 | + }, |
5283 | + queue: Queue::Out, |
5284 | + comment: format!("Could not unsubscribe {f:?}: {err}").into(), |
5285 | + }, |
5286 | + std::iter::once(Cow::Borrowed(f)), |
5287 | + )?; |
5288 | + |
5289 | + /* send error details to list owners */ |
5290 | + |
5291 | + let list_owners = self.list_owners(list.pk)?; |
5292 | + self.send_reply_with_list_template( |
5293 | + TemplateRenderContext { |
5294 | + template: Template::ADMIN_NOTICE, |
5295 | + default_fn: Some(Template::default_admin_notice), |
5296 | + list, |
5297 | + context: minijinja::context! { |
5298 | + list => &list, |
5299 | + details => err.to_string(), |
5300 | + }, |
5301 | + queue: Queue::Out, |
5302 | + comment: format!("Could not unsubscribe {f:?}: {err}").into(), |
5303 | + }, |
5304 | + list_owners.iter().map(|owner| Cow::Owned(owner.address())), |
5305 | + )?; |
5306 | + } else { |
5307 | + self.send_reply_with_list_template( |
5308 | + TemplateRenderContext { |
5309 | + template: Template::UNSUBSCRIPTION_CONFIRMATION, |
5310 | + default_fn: Some(Template::default_unsubscription_confirmation), |
5311 | + list, |
5312 | + context: minijinja::context! { |
5313 | + list => &list, |
5314 | + }, |
5315 | + queue: Queue::Out, |
5316 | + comment: Template::UNSUBSCRIPTION_CONFIRMATION.into(), |
5317 | + }, |
5318 | + std::iter::once(Cow::Borrowed(f)), |
5319 | + )?; |
5320 | + } |
5321 | + } |
5322 | + } |
5323 | + ListRequest::Other(ref req) if req == "owner" => { |
5324 | + trace!( |
5325 | + "list-owner mail action for addresses {:?} in list {}", |
5326 | + env.from(), |
5327 | + list |
5328 | + ); |
5329 | + return Err("list-owner emails are not implemented yet.".into()); |
5330 | + //FIXME: mail to list-owner |
5331 | + /* |
5332 | + for _owner in self.list_owners(list.pk)? { |
5333 | + self.insert_to_queue( |
5334 | + Queue::Out, |
5335 | + Some(list.pk), |
5336 | + None, |
5337 | + draft.finalise()?.as_bytes(), |
5338 | + "list-owner-forward".to_string(), |
5339 | + )?; |
5340 | + } |
5341 | + */ |
5342 | + } |
5343 | + ListRequest::Other(ref req) if req.trim().eq_ignore_ascii_case("password") => { |
5344 | + trace!( |
5345 | + "list-request password set action for addresses {:?} in list {list}", |
5346 | + env.from(), |
5347 | + ); |
5348 | + let body = env.body_bytes(raw); |
5349 | + let password = body.text(); |
5350 | + // TODO: validate SSH public key with `ssh-keygen`. |
5351 | + for f in env.from() { |
5352 | + let email_from = f.get_email(); |
5353 | + if let Ok(sub) = self.list_subscription_by_address(list.pk, &email_from) { |
5354 | + match self.account_by_address(&email_from)? { |
5355 | + Some(_acc) => { |
5356 | + let changeset = AccountChangeset { |
5357 | + address: email_from.clone(), |
5358 | + name: None, |
5359 | + public_key: None, |
5360 | + password: Some(password.clone()), |
5361 | + enabled: None, |
5362 | + }; |
5363 | + self.update_account(changeset)?; |
5364 | + } |
5365 | + None => { |
5366 | + // Create new account. |
5367 | + self.add_account(Account { |
5368 | + pk: 0, |
5369 | + name: sub.name.clone(), |
5370 | + address: sub.address.clone(), |
5371 | + public_key: None, |
5372 | + password: password.clone(), |
5373 | + enabled: sub.enabled, |
5374 | + })?; |
5375 | + } |
5376 | + } |
5377 | + } |
5378 | + } |
5379 | + } |
5380 | + ListRequest::RetrieveMessages(ref message_ids) => { |
5381 | + trace!( |
5382 | + "retrieve messages {message_ids:?} action for addresses {:?} in list {list}", |
5383 | + env.from(), |
5384 | + ); |
5385 | + return Err("message retrievals are not implemented yet.".into()); |
5386 | + } |
5387 | + ListRequest::RetrieveArchive(ref from, ref to) => { |
5388 | + trace!( |
5389 | + "retrieve archive action from {from:?} to {to:?} for addresses {:?} in list \ |
5390 | + {list}", |
5391 | + env.from(), |
5392 | + ); |
5393 | + return Err("message retrievals are not implemented yet.".into()); |
5394 | + } |
5395 | + ListRequest::ChangeSetting(ref setting, ref toggle) => { |
5396 | + trace!( |
5397 | + "change setting {setting}, request with value {toggle:?} for addresses {:?} \ |
5398 | + in list {list}", |
5399 | + env.from(), |
5400 | + ); |
5401 | + return Err("setting digest options via e-mail is not implemented yet.".into()); |
5402 | + } |
5403 | + ListRequest::Other(ref req) => { |
5404 | + trace!( |
5405 | + "unknown request action {req} for addresses {:?} in list {list}", |
5406 | + env.from(), |
5407 | + ); |
5408 | + return Err(format!("Unknown request {req}.").into()); |
5409 | + } |
5410 | + } |
5411 | + Ok(()) |
5412 | + } |
5413 | + |
5414 | + /// Fetch all year and month values for which at least one post exists in |
5415 | + /// `yyyy-mm` format. |
5416 | + pub fn months(&self, list_pk: i64) -> Result<Vec<String>> { |
5417 | + let mut stmt = self.connection.prepare( |
5418 | + "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post \ |
5419 | + WHERE list = ?;", |
5420 | + )?; |
5421 | + let months_iter = stmt.query_map([list_pk], |row| { |
5422 | + let val: String = row.get(0)?; |
5423 | + Ok(val) |
5424 | + })?; |
5425 | + |
5426 | + let mut ret = vec![]; |
5427 | + for month in months_iter { |
5428 | + let month = month?; |
5429 | + ret.push(month); |
5430 | + } |
5431 | + Ok(ret) |
5432 | + } |
5433 | + |
5434 | + /// Find a post by its `Message-ID` email header. |
5435 | + pub fn list_post_by_message_id( |
5436 | + &self, |
5437 | + list_pk: i64, |
5438 | + message_id: &str, |
5439 | + ) -> Result<Option<DbVal<Post>>> { |
5440 | + let mut stmt = self.connection.prepare( |
5441 | + "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \ |
5442 | + FROM post WHERE list = ? AND message_id = ?;", |
5443 | + )?; |
5444 | + let ret = stmt |
5445 | + .query_row(rusqlite::params![&list_pk, &message_id], |row| { |
5446 | + let pk = row.get("pk")?; |
5447 | + Ok(DbVal( |
5448 | + Post { |
5449 | + pk, |
5450 | + list: row.get("list")?, |
5451 | + envelope_from: row.get("envelope_from")?, |
5452 | + address: row.get("address")?, |
5453 | + message_id: row.get("message_id")?, |
5454 | + message: row.get("message")?, |
5455 | + timestamp: row.get("timestamp")?, |
5456 | + datetime: row.get("datetime")?, |
5457 | + month_year: row.get("month_year")?, |
5458 | + }, |
5459 | + pk, |
5460 | + )) |
5461 | + }) |
5462 | + .optional()?; |
5463 | + |
5464 | + Ok(ret) |
5465 | + } |
5466 | + |
5467 | + /// Helper function to send a template reply. |
5468 | + pub fn send_reply_with_list_template<'ctx, F: Fn() -> Template>( |
5469 | + &self, |
5470 | + render_context: TemplateRenderContext<'ctx, F>, |
5471 | + recipients: impl Iterator<Item = Cow<'ctx, melib::Address>>, |
5472 | + ) -> Result<()> { |
5473 | + let TemplateRenderContext { |
5474 | + template, |
5475 | + default_fn, |
5476 | + list, |
5477 | + context, |
5478 | + queue, |
5479 | + comment, |
5480 | + } = render_context; |
5481 | + |
5482 | + let post_policy = self.list_post_policy(list.pk)?; |
5483 | + let subscription_policy = self.list_subscription_policy(list.pk)?; |
5484 | + |
5485 | + let templ = self |
5486 | + .fetch_template(template, Some(list.pk))? |
5487 | + .map(DbVal::into_inner) |
5488 | + .or_else(|| default_fn.map(|f| f())) |
5489 | + .ok_or_else(|| -> crate::Error { |
5490 | + format!("Template with name {template:?} was not found.").into() |
5491 | + })?; |
5492 | + |
5493 | + let mut draft = templ.render(context)?; |
5494 | + draft.headers.insert( |
5495 | + melib::HeaderName::new_unchecked("From"), |
5496 | + list.request_subaddr(), |
5497 | + ); |
5498 | + for addr in recipients { |
5499 | + let mut draft = draft.clone(); |
5500 | + draft |
5501 | + .headers |
5502 | + .insert(melib::HeaderName::new_unchecked("To"), addr.to_string()); |
5503 | + list.insert_headers( |
5504 | + &mut draft, |
5505 | + post_policy.as_deref(), |
5506 | + subscription_policy.as_deref(), |
5507 | + ); |
5508 | + self.insert_to_queue(QueueEntry::new( |
5509 | + queue, |
5510 | + Some(list.pk), |
5511 | + None, |
5512 | + draft.finalise()?.as_bytes(), |
5513 | + Some(comment.to_string()), |
5514 | + )?)?; |
5515 | + } |
5516 | + Ok(()) |
5517 | + } |
5518 | + } |
5519 | + |
5520 | + /// Helper type for [`Connection::send_reply_with_list_template`]. |
5521 | + #[derive(Debug)] |
5522 | + pub struct TemplateRenderContext<'ctx, F: Fn() -> Template> { |
5523 | + /// Template name. |
5524 | + pub template: &'ctx str, |
5525 | + /// If template is not found, call a function that returns one. |
5526 | + pub default_fn: Option<F>, |
5527 | + /// The pertinent list. |
5528 | + pub list: &'ctx DbVal<MailingList>, |
5529 | + /// [`minijinja`]'s template context. |
5530 | + pub context: minijinja::value::Value, |
5531 | + /// Destination queue in the database. |
5532 | + pub queue: Queue, |
5533 | + /// Comment for the queue entry in the database. |
5534 | + pub comment: Cow<'static, str>, |
5535 | + } |
5536 | diff --git a/core/src/queue.rs b/core/src/queue.rs |
5537 | new file mode 100644 |
5538 | index 0000000..c2c96b8 |
5539 | --- /dev/null |
5540 | +++ b/core/src/queue.rs |
5541 | @@ -0,0 +1,333 @@ |
5542 | + /* |
5543 | + * This file is part of mailpot |
5544 | + * |
5545 | + * Copyright 2020 - Manos Pitsidianakis |
5546 | + * |
5547 | + * This program is free software: you can redistribute it and/or modify |
5548 | + * it under the terms of the GNU Affero General Public License as |
5549 | + * published by the Free Software Foundation, either version 3 of the |
5550 | + * License, or (at your option) any later version. |
5551 | + * |
5552 | + * This program is distributed in the hope that it will be useful, |
5553 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
5554 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
5555 | + * GNU Affero General Public License for more details. |
5556 | + * |
5557 | + * You should have received a copy of the GNU Affero General Public License |
5558 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
5559 | + */ |
5560 | + |
5561 | + //! # Queues |
5562 | + |
5563 | + use std::borrow::Cow; |
5564 | + |
5565 | + use melib::Envelope; |
5566 | + |
5567 | + use crate::{errors::*, models::DbVal, Connection, DateTime}; |
5568 | + |
5569 | + /// In-database queues of mail. |
5570 | + #[derive(Copy, Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] |
5571 | + #[serde(rename_all = "kebab-case")] |
5572 | + pub enum Queue { |
5573 | + /// Messages that have been submitted but not yet processed, await |
5574 | + /// processing in the `maildrop` queue. Messages can be added to the |
5575 | + /// `maildrop` queue even when mailpot is not running. |
5576 | + Maildrop, |
5577 | + /// List administrators may introduce rules for emails to be placed |
5578 | + /// indefinitely in the `hold` queue. Messages placed in the `hold` |
5579 | + /// queue stay there until the administrator intervenes. No periodic |
5580 | + /// delivery attempts are made for messages in the `hold` queue. |
5581 | + Hold, |
5582 | + /// When all the deliverable recipients for a message are delivered, and for |
5583 | + /// some recipients delivery failed for a transient reason (it might |
5584 | + /// succeed later), the message is placed in the `deferred` queue. |
5585 | + Deferred, |
5586 | + /// Invalid received or generated e-mail saved for debug and troubleshooting |
5587 | + /// reasons. |
5588 | + Corrupt, |
5589 | + /// Emails that must be sent as soon as possible. |
5590 | + Out, |
5591 | + /// Error queue |
5592 | + Error, |
5593 | + } |
5594 | + |
5595 | + impl Queue { |
5596 | + /// Returns the name of the queue used in the database schema. |
5597 | + pub fn as_str(&self) -> &'static str { |
5598 | + match self { |
5599 | + Self::Maildrop => "maildrop", |
5600 | + Self::Hold => "hold", |
5601 | + Self::Deferred => "deferred", |
5602 | + Self::Corrupt => "corrupt", |
5603 | + Self::Out => "out", |
5604 | + Self::Error => "error", |
5605 | + } |
5606 | + } |
5607 | + } |
5608 | + |
5609 | + /// A queue entry. |
5610 | + #[derive(Clone, Deserialize, Serialize, PartialEq, Eq)] |
5611 | + pub struct QueueEntry { |
5612 | + /// Database primary key. |
5613 | + pub pk: i64, |
5614 | + /// Owner queue. |
5615 | + pub queue: Queue, |
5616 | + /// Related list foreign key, optional. |
5617 | + pub list: Option<i64>, |
5618 | + /// Entry comment, optional. |
5619 | + pub comment: Option<String>, |
5620 | + /// Entry recipients in rfc5322 format. |
5621 | + pub to_addresses: String, |
5622 | + /// Entry submitter in rfc5322 format. |
5623 | + pub from_address: String, |
5624 | + /// Entry subject. |
5625 | + pub subject: String, |
5626 | + /// Entry Message-ID in rfc5322 format. |
5627 | + pub message_id: String, |
5628 | + /// Message in rfc5322 format as bytes. |
5629 | + pub message: Vec<u8>, |
5630 | + /// Unix timestamp of date. |
5631 | + pub timestamp: u64, |
5632 | + /// Datetime as string. |
5633 | + pub datetime: DateTime, |
5634 | + } |
5635 | + |
5636 | + impl std::fmt::Display for QueueEntry { |
5637 | + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
5638 | + write!(fmt, "{:?}", self) |
5639 | + } |
5640 | + } |
5641 | + |
5642 | + impl std::fmt::Debug for QueueEntry { |
5643 | + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { |
5644 | + fmt.debug_struct(stringify!(QueueEntry)) |
5645 | + .field("pk", &self.pk) |
5646 | + .field("queue", &self.queue) |
5647 | + .field("list", &self.list) |
5648 | + .field("comment", &self.comment) |
5649 | + .field("to_addresses", &self.to_addresses) |
5650 | + .field("from_address", &self.from_address) |
5651 | + .field("subject", &self.subject) |
5652 | + .field("message_id", &self.message_id) |
5653 | + .field("message length", &self.message.len()) |
5654 | + .field( |
5655 | + "message", |
5656 | + &format!("{:.15}", String::from_utf8_lossy(&self.message)), |
5657 | + ) |
5658 | + .field("timestamp", &self.timestamp) |
5659 | + .field("datetime", &self.datetime) |
5660 | + .finish() |
5661 | + } |
5662 | + } |
5663 | + |
5664 | + impl QueueEntry { |
5665 | + /// Create new entry. |
5666 | + pub fn new( |
5667 | + queue: Queue, |
5668 | + list: Option<i64>, |
5669 | + env: Option<Cow<'_, Envelope>>, |
5670 | + raw: &[u8], |
5671 | + comment: Option<String>, |
5672 | + ) -> Result<Self> { |
5673 | + let env = env |
5674 | + .map(Ok) |
5675 | + .unwrap_or_else(|| melib::Envelope::from_bytes(raw, None).map(Cow::Owned))?; |
5676 | + let now = chrono::offset::Utc::now(); |
5677 | + Ok(Self { |
5678 | + pk: -1, |
5679 | + list, |
5680 | + queue, |
5681 | + comment, |
5682 | + to_addresses: env.field_to_to_string(), |
5683 | + from_address: env.field_from_to_string(), |
5684 | + subject: env.subject().to_string(), |
5685 | + message_id: env.message_id().to_string(), |
5686 | + message: raw.to_vec(), |
5687 | + timestamp: now.timestamp() as u64, |
5688 | + datetime: now, |
5689 | + }) |
5690 | + } |
5691 | + } |
5692 | + |
5693 | + impl Connection { |
5694 | + /// Insert a received email into a queue. |
5695 | + pub fn insert_to_queue(&self, mut entry: QueueEntry) -> Result<DbVal<QueueEntry>> { |
5696 | + log::trace!("Inserting to queue: {entry}"); |
5697 | + let mut stmt = self.connection.prepare( |
5698 | + "INSERT INTO queue(which, list, comment, to_addresses, from_address, subject, \ |
5699 | + message_id, message, timestamp, datetime) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) \ |
5700 | + RETURNING pk;", |
5701 | + )?; |
5702 | + let pk = stmt.query_row( |
5703 | + rusqlite::params![ |
5704 | + entry.queue.as_str(), |
5705 | + &entry.list, |
5706 | + &entry.comment, |
5707 | + &entry.to_addresses, |
5708 | + &entry.from_address, |
5709 | + &entry.subject, |
5710 | + &entry.message_id, |
5711 | + &entry.message, |
5712 | + &entry.timestamp, |
5713 | + &entry.datetime, |
5714 | + ], |
5715 | + |row| { |
5716 | + let pk: i64 = row.get("pk")?; |
5717 | + Ok(pk) |
5718 | + }, |
5719 | + )?; |
5720 | + entry.pk = pk; |
5721 | + Ok(DbVal(entry, pk)) |
5722 | + } |
5723 | + |
5724 | + /// Fetch all queue entries. |
5725 | + pub fn queue(&self, queue: Queue) -> Result<Vec<DbVal<QueueEntry>>> { |
5726 | + let mut stmt = self |
5727 | + .connection |
5728 | + .prepare("SELECT * FROM queue WHERE which = ?;")?; |
5729 | + let iter = stmt.query_map([&queue.as_str()], |row| { |
5730 | + let pk = row.get::<_, i64>("pk")?; |
5731 | + Ok(DbVal( |
5732 | + QueueEntry { |
5733 | + pk, |
5734 | + queue, |
5735 | + list: row.get::<_, Option<i64>>("list")?, |
5736 | + comment: row.get::<_, Option<String>>("comment")?, |
5737 | + to_addresses: row.get::<_, String>("to_addresses")?, |
5738 | + from_address: row.get::<_, String>("from_address")?, |
5739 | + subject: row.get::<_, String>("subject")?, |
5740 | + message_id: row.get::<_, String>("message_id")?, |
5741 | + message: row.get::<_, Vec<u8>>("message")?, |
5742 | + timestamp: row.get::<_, u64>("timestamp")?, |
5743 | + datetime: row.get::<_, DateTime>("datetime")?, |
5744 | + }, |
5745 | + pk, |
5746 | + )) |
5747 | + })?; |
5748 | + |
5749 | + let mut ret = vec![]; |
5750 | + for item in iter { |
5751 | + let item = item?; |
5752 | + ret.push(item); |
5753 | + } |
5754 | + Ok(ret) |
5755 | + } |
5756 | + |
5757 | + /// Delete queue entries returning the deleted values. |
5758 | + pub fn delete_from_queue(&mut self, queue: Queue, index: Vec<i64>) -> Result<Vec<QueueEntry>> { |
5759 | + let tx = self.connection.transaction()?; |
5760 | + |
5761 | + let cl = |row: &rusqlite::Row<'_>| { |
5762 | + Ok(QueueEntry { |
5763 | + pk: -1, |
5764 | + queue, |
5765 | + list: row.get::<_, Option<i64>>("list")?, |
5766 | + comment: row.get::<_, Option<String>>("comment")?, |
5767 | + to_addresses: row.get::<_, String>("to_addresses")?, |
5768 | + from_address: row.get::<_, String>("from_address")?, |
5769 | + subject: row.get::<_, String>("subject")?, |
5770 | + message_id: row.get::<_, String>("message_id")?, |
5771 | + message: row.get::<_, Vec<u8>>("message")?, |
5772 | + timestamp: row.get::<_, u64>("timestamp")?, |
5773 | + datetime: row.get::<_, DateTime>("datetime")?, |
5774 | + }) |
5775 | + }; |
5776 | + let mut stmt = if index.is_empty() { |
5777 | + tx.prepare("DELETE FROM queue WHERE which = ? RETURNING *;")? |
5778 | + } else { |
5779 | + tx.prepare("DELETE FROM queue WHERE which = ? AND pk IN rarray(?) RETURNING *;")? |
5780 | + }; |
5781 | + let iter = if index.is_empty() { |
5782 | + stmt.query_map([&queue.as_str()], cl)? |
5783 | + } else { |
5784 | + // Note: A `Rc<Vec<Value>>` must be used as the parameter. |
5785 | + let index = std::rc::Rc::new( |
5786 | + index |
5787 | + .into_iter() |
5788 | + .map(rusqlite::types::Value::from) |
5789 | + .collect::<Vec<rusqlite::types::Value>>(), |
5790 | + ); |
5791 | + stmt.query_map(rusqlite::params![queue.as_str(), index], cl)? |
5792 | + }; |
5793 | + |
5794 | + let mut ret = vec![]; |
5795 | + for item in iter { |
5796 | + let item = item?; |
5797 | + ret.push(item); |
5798 | + } |
5799 | + drop(stmt); |
5800 | + tx.commit()?; |
5801 | + Ok(ret) |
5802 | + } |
5803 | + } |
5804 | + |
5805 | + #[cfg(test)] |
5806 | + mod tests { |
5807 | + use super::*; |
5808 | + use crate::*; |
5809 | + |
5810 | + #[test] |
5811 | + fn test_queue_delete_array() { |
5812 | + use tempfile::TempDir; |
5813 | + |
5814 | + let tmp_dir = TempDir::new().unwrap(); |
5815 | + let db_path = tmp_dir.path().join("mpot.db"); |
5816 | + let config = Configuration { |
5817 | + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
5818 | + db_path, |
5819 | + data_path: tmp_dir.path().to_path_buf(), |
5820 | + administrators: vec![], |
5821 | + }; |
5822 | + |
5823 | + let mut db = Connection::open_or_create_db(config).unwrap().trusted(); |
5824 | + for i in 0..5 { |
5825 | + db.insert_to_queue( |
5826 | + QueueEntry::new( |
5827 | + Queue::Hold, |
5828 | + None, |
5829 | + None, |
5830 | + format!("Subject: testing\r\nMessage-Id: {i}@localhost\r\n\r\nHello\r\n") |
5831 | + .as_bytes(), |
5832 | + None, |
5833 | + ) |
5834 | + .unwrap(), |
5835 | + ) |
5836 | + .unwrap(); |
5837 | + } |
5838 | + let entries = db.queue(Queue::Hold).unwrap(); |
5839 | + assert_eq!(entries.len(), 5); |
5840 | + let out_entries = db.delete_from_queue(Queue::Out, vec![]).unwrap(); |
5841 | + assert_eq!(db.queue(Queue::Hold).unwrap().len(), 5); |
5842 | + assert!(out_entries.is_empty()); |
5843 | + let deleted_entries = db.delete_from_queue(Queue::Hold, vec![]).unwrap(); |
5844 | + assert_eq!(deleted_entries.len(), 5); |
5845 | + assert_eq!( |
5846 | + &entries |
5847 | + .iter() |
5848 | + .cloned() |
5849 | + .map(DbVal::into_inner) |
5850 | + .map(|mut e| { |
5851 | + e.pk = -1; |
5852 | + e |
5853 | + }) |
5854 | + .collect::<Vec<_>>(), |
5855 | + &deleted_entries |
5856 | + ); |
5857 | + |
5858 | + for e in deleted_entries { |
5859 | + db.insert_to_queue(e).unwrap(); |
5860 | + } |
5861 | + |
5862 | + let index = db |
5863 | + .queue(Queue::Hold) |
5864 | + .unwrap() |
5865 | + .into_iter() |
5866 | + .skip(2) |
5867 | + .map(|e| e.pk()) |
5868 | + .take(2) |
5869 | + .collect::<Vec<i64>>(); |
5870 | + let deleted_entries = db.delete_from_queue(Queue::Hold, index).unwrap(); |
5871 | + assert_eq!(deleted_entries.len(), 2); |
5872 | + assert_eq!(db.queue(Queue::Hold).unwrap().len(), 3); |
5873 | + } |
5874 | + } |
5875 | diff --git a/core/src/submission.rs b/core/src/submission.rs |
5876 | index d17c9ea..6a3ca9a 100644 |
5877 | --- a/core/src/submission.rs |
5878 | +++ b/core/src/submission.rs |
5879 | @@ -23,7 +23,7 @@ use std::{future::Future, pin::Pin}; |
5880 | |
5881 | use melib::smtp::*; |
5882 | |
5883 | - use crate::{errors::*, Connection, QueueEntry}; |
5884 | + use crate::{errors::*, queue::QueueEntry, Connection}; |
5885 | |
5886 | type ResultFuture<T> = Result<Pin<Box<dyn Future<Output = Result<T>> + Send + 'static>>>; |
5887 | |
5888 | diff --git a/core/src/subscriptions.rs b/core/src/subscriptions.rs |
5889 | new file mode 100644 |
5890 | index 0000000..a05c0ea |
5891 | --- /dev/null |
5892 | +++ b/core/src/subscriptions.rs |
5893 | @@ -0,0 +1,783 @@ |
5894 | + /* |
5895 | + * This file is part of mailpot |
5896 | + * |
5897 | + * Copyright 2020 - Manos Pitsidianakis |
5898 | + * |
5899 | + * This program is free software: you can redistribute it and/or modify |
5900 | + * it under the terms of the GNU Affero General Public License as |
5901 | + * published by the Free Software Foundation, either version 3 of the |
5902 | + * License, or (at your option) any later version. |
5903 | + * |
5904 | + * This program is distributed in the hope that it will be useful, |
5905 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
5906 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
5907 | + * GNU Affero General Public License for more details. |
5908 | + * |
5909 | + * You should have received a copy of the GNU Affero General Public License |
5910 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
5911 | + */ |
5912 | + |
5913 | + //! User subscriptions. |
5914 | + |
5915 | + use log::trace; |
5916 | + use rusqlite::OptionalExtension; |
5917 | + |
5918 | + use crate::{ |
5919 | + errors::{ErrorKind::*, *}, |
5920 | + models::{ |
5921 | + changesets::{AccountChangeset, ListSubscriptionChangeset}, |
5922 | + Account, ListCandidateSubscription, ListSubscription, |
5923 | + }, |
5924 | + Connection, DbVal, |
5925 | + }; |
5926 | + |
5927 | + impl Connection { |
5928 | + /// Fetch all subscriptions of a mailing list. |
5929 | + pub fn list_subscriptions(&self, pk: i64) -> Result<Vec<DbVal<ListSubscription>>> { |
5930 | + let mut stmt = self |
5931 | + .connection |
5932 | + .prepare("SELECT * FROM subscription WHERE list = ?;")?; |
5933 | + let list_iter = stmt.query_map([&pk], |row| { |
5934 | + let pk = row.get("pk")?; |
5935 | + Ok(DbVal( |
5936 | + ListSubscription { |
5937 | + pk: row.get("pk")?, |
5938 | + list: row.get("list")?, |
5939 | + address: row.get("address")?, |
5940 | + account: row.get("account")?, |
5941 | + name: row.get("name")?, |
5942 | + digest: row.get("digest")?, |
5943 | + enabled: row.get("enabled")?, |
5944 | + verified: row.get("verified")?, |
5945 | + hide_address: row.get("hide_address")?, |
5946 | + receive_duplicates: row.get("receive_duplicates")?, |
5947 | + receive_own_posts: row.get("receive_own_posts")?, |
5948 | + receive_confirmation: row.get("receive_confirmation")?, |
5949 | + }, |
5950 | + pk, |
5951 | + )) |
5952 | + })?; |
5953 | + |
5954 | + let mut ret = vec![]; |
5955 | + for list in list_iter { |
5956 | + let list = list?; |
5957 | + ret.push(list); |
5958 | + } |
5959 | + Ok(ret) |
5960 | + } |
5961 | + |
5962 | + /// Fetch mailing list subscription. |
5963 | + pub fn list_subscription(&self, list_pk: i64, pk: i64) -> Result<DbVal<ListSubscription>> { |
5964 | + let mut stmt = self |
5965 | + .connection |
5966 | + .prepare("SELECT * FROM subscription WHERE list = ? AND pk = ?;")?; |
5967 | + |
5968 | + let ret = stmt.query_row([&list_pk, &pk], |row| { |
5969 | + let _pk: i64 = row.get("pk")?; |
5970 | + debug_assert_eq!(pk, _pk); |
5971 | + Ok(DbVal( |
5972 | + ListSubscription { |
5973 | + pk, |
5974 | + list: row.get("list")?, |
5975 | + address: row.get("address")?, |
5976 | + account: row.get("account")?, |
5977 | + name: row.get("name")?, |
5978 | + digest: row.get("digest")?, |
5979 | + enabled: row.get("enabled")?, |
5980 | + verified: row.get("verified")?, |
5981 | + hide_address: row.get("hide_address")?, |
5982 | + receive_duplicates: row.get("receive_duplicates")?, |
5983 | + receive_own_posts: row.get("receive_own_posts")?, |
5984 | + receive_confirmation: row.get("receive_confirmation")?, |
5985 | + }, |
5986 | + pk, |
5987 | + )) |
5988 | + })?; |
5989 | + Ok(ret) |
5990 | + } |
5991 | + |
5992 | + /// Fetch mailing list subscription by their address. |
5993 | + pub fn list_subscription_by_address( |
5994 | + &self, |
5995 | + list_pk: i64, |
5996 | + address: &str, |
5997 | + ) -> Result<DbVal<ListSubscription>> { |
5998 | + let mut stmt = self |
5999 | + .connection |
6000 | + .prepare("SELECT * FROM subscription WHERE list = ? AND address = ?;")?; |
6001 | + |
6002 | + let ret = stmt.query_row(rusqlite::params![&list_pk, &address], |row| { |
6003 | + let pk = row.get("pk")?; |
6004 | + let address_ = row.get("address")?; |
6005 | + debug_assert_eq!(address, &address_); |
6006 | + Ok(DbVal( |
6007 | + ListSubscription { |
6008 | + pk, |
6009 | + list: row.get("list")?, |
6010 | + address: address_, |
6011 | + account: row.get("account")?, |
6012 | + name: row.get("name")?, |
6013 | + digest: row.get("digest")?, |
6014 | + enabled: row.get("enabled")?, |
6015 | + verified: row.get("verified")?, |
6016 | + hide_address: row.get("hide_address")?, |
6017 | + receive_duplicates: row.get("receive_duplicates")?, |
6018 | + receive_own_posts: row.get("receive_own_posts")?, |
6019 | + receive_confirmation: row.get("receive_confirmation")?, |
6020 | + }, |
6021 | + pk, |
6022 | + )) |
6023 | + })?; |
6024 | + Ok(ret) |
6025 | + } |
6026 | + |
6027 | + /// Add subscription to mailing list. |
6028 | + pub fn add_subscription( |
6029 | + &self, |
6030 | + list_pk: i64, |
6031 | + mut new_val: ListSubscription, |
6032 | + ) -> Result<DbVal<ListSubscription>> { |
6033 | + new_val.list = list_pk; |
6034 | + let mut stmt = self |
6035 | + .connection |
6036 | + .prepare( |
6037 | + "INSERT INTO subscription(list, address, account, name, enabled, digest, \ |
6038 | + verified, hide_address, receive_duplicates, receive_own_posts, \ |
6039 | + receive_confirmation) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *;", |
6040 | + ) |
6041 | + .unwrap(); |
6042 | + let val = stmt.query_row( |
6043 | + rusqlite::params![ |
6044 | + &new_val.list, |
6045 | + &new_val.address, |
6046 | + &new_val.account, |
6047 | + &new_val.name, |
6048 | + &new_val.enabled, |
6049 | + &new_val.digest, |
6050 | + &new_val.verified, |
6051 | + &new_val.hide_address, |
6052 | + &new_val.receive_duplicates, |
6053 | + &new_val.receive_own_posts, |
6054 | + &new_val.receive_confirmation |
6055 | + ], |
6056 | + |row| { |
6057 | + let pk = row.get("pk")?; |
6058 | + Ok(DbVal( |
6059 | + ListSubscription { |
6060 | + pk, |
6061 | + list: row.get("list")?, |
6062 | + address: row.get("address")?, |
6063 | + name: row.get("name")?, |
6064 | + account: row.get("account")?, |
6065 | + digest: row.get("digest")?, |
6066 | + enabled: row.get("enabled")?, |
6067 | + verified: row.get("verified")?, |
6068 | + hide_address: row.get("hide_address")?, |
6069 | + receive_duplicates: row.get("receive_duplicates")?, |
6070 | + receive_own_posts: row.get("receive_own_posts")?, |
6071 | + receive_confirmation: row.get("receive_confirmation")?, |
6072 | + }, |
6073 | + pk, |
6074 | + )) |
6075 | + }, |
6076 | + )?; |
6077 | + trace!("add_subscription {:?}.", &val); |
6078 | + // table entry might be modified by triggers, so don't rely on RETURNING value. |
6079 | + self.list_subscription(list_pk, val.pk()) |
6080 | + } |
6081 | + |
6082 | + /// Create subscription candidate. |
6083 | + pub fn add_candidate_subscription( |
6084 | + &self, |
6085 | + list_pk: i64, |
6086 | + mut new_val: ListSubscription, |
6087 | + ) -> Result<DbVal<ListCandidateSubscription>> { |
6088 | + new_val.list = list_pk; |
6089 | + let mut stmt = self.connection.prepare( |
6090 | + "INSERT INTO candidate_subscription(list, address, name, accepted) VALUES(?, ?, ?, ?) \ |
6091 | + RETURNING *;", |
6092 | + )?; |
6093 | + let val = stmt.query_row( |
6094 | + rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,], |
6095 | + |row| { |
6096 | + let pk = row.get("pk")?; |
6097 | + Ok(DbVal( |
6098 | + ListCandidateSubscription { |
6099 | + pk, |
6100 | + list: row.get("list")?, |
6101 | + address: row.get("address")?, |
6102 | + name: row.get("name")?, |
6103 | + accepted: row.get("accepted")?, |
6104 | + }, |
6105 | + pk, |
6106 | + )) |
6107 | + }, |
6108 | + )?; |
6109 | + drop(stmt); |
6110 | + |
6111 | + trace!("add_candidate_subscription {:?}.", &val); |
6112 | + // table entry might be modified by triggers, so don't rely on RETURNING value. |
6113 | + self.candidate_subscription(val.pk()) |
6114 | + } |
6115 | + |
6116 | + /// Fetch subscription candidate by primary key. |
6117 | + pub fn candidate_subscription(&self, pk: i64) -> Result<DbVal<ListCandidateSubscription>> { |
6118 | + let mut stmt = self |
6119 | + .connection |
6120 | + .prepare("SELECT * FROM candidate_subscription WHERE pk = ?;")?; |
6121 | + let val = stmt |
6122 | + .query_row(rusqlite::params![&pk], |row| { |
6123 | + let _pk: i64 = row.get("pk")?; |
6124 | + debug_assert_eq!(pk, _pk); |
6125 | + Ok(DbVal( |
6126 | + ListCandidateSubscription { |
6127 | + pk, |
6128 | + list: row.get("list")?, |
6129 | + address: row.get("address")?, |
6130 | + name: row.get("name")?, |
6131 | + accepted: row.get("accepted")?, |
6132 | + }, |
6133 | + pk, |
6134 | + )) |
6135 | + }) |
6136 | + .map_err(|err| { |
6137 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
6138 | + Error::from(err) |
6139 | + .chain_err(|| NotFound("Candidate subscription with this pk not found!")) |
6140 | + } else { |
6141 | + err.into() |
6142 | + } |
6143 | + })?; |
6144 | + |
6145 | + Ok(val) |
6146 | + } |
6147 | + |
6148 | + /// Accept subscription candidate. |
6149 | + pub fn accept_candidate_subscription(&mut self, pk: i64) -> Result<DbVal<ListSubscription>> { |
6150 | + let val = self.connection.query_row( |
6151 | + "INSERT INTO subscription(list, address, name, enabled, digest, verified, \ |
6152 | + hide_address, receive_duplicates, receive_own_posts, receive_confirmation) SELECT \ |
6153 | + list, address, name, 1, 0, 0, 0, 1, 1, 0 FROM candidate_subscription WHERE pk = ? \ |
6154 | + RETURNING *;", |
6155 | + rusqlite::params![&pk], |
6156 | + |row| { |
6157 | + let pk = row.get("pk")?; |
6158 | + Ok(DbVal( |
6159 | + ListSubscription { |
6160 | + pk, |
6161 | + list: row.get("list")?, |
6162 | + address: row.get("address")?, |
6163 | + account: row.get("account")?, |
6164 | + name: row.get("name")?, |
6165 | + digest: row.get("digest")?, |
6166 | + enabled: row.get("enabled")?, |
6167 | + verified: row.get("verified")?, |
6168 | + hide_address: row.get("hide_address")?, |
6169 | + receive_duplicates: row.get("receive_duplicates")?, |
6170 | + receive_own_posts: row.get("receive_own_posts")?, |
6171 | + receive_confirmation: row.get("receive_confirmation")?, |
6172 | + }, |
6173 | + pk, |
6174 | + )) |
6175 | + }, |
6176 | + )?; |
6177 | + |
6178 | + trace!("accept_candidate_subscription {:?}.", &val); |
6179 | + // table entry might be modified by triggers, so don't rely on RETURNING value. |
6180 | + let ret = self.list_subscription(val.list, val.pk())?; |
6181 | + |
6182 | + // assert that [ref:accept_candidate] trigger works. |
6183 | + debug_assert_eq!(Some(ret.pk), self.candidate_subscription(pk)?.accepted); |
6184 | + Ok(ret) |
6185 | + } |
6186 | + |
6187 | + /// Remove a subscription by their address. |
6188 | + pub fn remove_subscription(&self, list_pk: i64, address: &str) -> Result<()> { |
6189 | + self.connection |
6190 | + .query_row( |
6191 | + "DELETE FROM subscription WHERE list = ? AND address = ? RETURNING *;", |
6192 | + rusqlite::params![&list_pk, &address], |
6193 | + |_| Ok(()), |
6194 | + ) |
6195 | + .map_err(|err| { |
6196 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
6197 | + Error::from(err).chain_err(|| NotFound("list or list owner not found!")) |
6198 | + } else { |
6199 | + err.into() |
6200 | + } |
6201 | + })?; |
6202 | + |
6203 | + Ok(()) |
6204 | + } |
6205 | + |
6206 | + /// Update a mailing list subscription. |
6207 | + pub fn update_subscription(&mut self, change_set: ListSubscriptionChangeset) -> Result<()> { |
6208 | + let pk = self |
6209 | + .list_subscription_by_address(change_set.list, &change_set.address)? |
6210 | + .pk; |
6211 | + if matches!( |
6212 | + change_set, |
6213 | + ListSubscriptionChangeset { |
6214 | + list: _, |
6215 | + address: _, |
6216 | + account: None, |
6217 | + name: None, |
6218 | + digest: None, |
6219 | + verified: None, |
6220 | + hide_address: None, |
6221 | + receive_duplicates: None, |
6222 | + receive_own_posts: None, |
6223 | + receive_confirmation: None, |
6224 | + enabled: None, |
6225 | + } |
6226 | + ) { |
6227 | + return Ok(()); |
6228 | + } |
6229 | + |
6230 | + let ListSubscriptionChangeset { |
6231 | + list, |
6232 | + address: _, |
6233 | + name, |
6234 | + account, |
6235 | + digest, |
6236 | + enabled, |
6237 | + verified, |
6238 | + hide_address, |
6239 | + receive_duplicates, |
6240 | + receive_own_posts, |
6241 | + receive_confirmation, |
6242 | + } = change_set; |
6243 | + let tx = self.connection.transaction()?; |
6244 | + |
6245 | + macro_rules! update { |
6246 | + ($field:tt) => {{ |
6247 | + if let Some($field) = $field { |
6248 | + tx.execute( |
6249 | + concat!( |
6250 | + "UPDATE subscription SET ", |
6251 | + stringify!($field), |
6252 | + " = ? WHERE list = ? AND pk = ?;" |
6253 | + ), |
6254 | + rusqlite::params![&$field, &list, &pk], |
6255 | + )?; |
6256 | + } |
6257 | + }}; |
6258 | + } |
6259 | + update!(name); |
6260 | + update!(account); |
6261 | + update!(digest); |
6262 | + update!(enabled); |
6263 | + update!(verified); |
6264 | + update!(hide_address); |
6265 | + update!(receive_duplicates); |
6266 | + update!(receive_own_posts); |
6267 | + update!(receive_confirmation); |
6268 | + |
6269 | + tx.commit()?; |
6270 | + Ok(()) |
6271 | + } |
6272 | + |
6273 | + /// Fetch account by pk. |
6274 | + pub fn account(&self, pk: i64) -> Result<Option<DbVal<Account>>> { |
6275 | + let mut stmt = self |
6276 | + .connection |
6277 | + .prepare("SELECT * FROM account WHERE pk = ?;")?; |
6278 | + |
6279 | + let ret = stmt |
6280 | + .query_row(rusqlite::params![&pk], |row| { |
6281 | + let _pk: i64 = row.get("pk")?; |
6282 | + debug_assert_eq!(pk, _pk); |
6283 | + Ok(DbVal( |
6284 | + Account { |
6285 | + pk, |
6286 | + name: row.get("name")?, |
6287 | + address: row.get("address")?, |
6288 | + public_key: row.get("public_key")?, |
6289 | + password: row.get("password")?, |
6290 | + enabled: row.get("enabled")?, |
6291 | + }, |
6292 | + pk, |
6293 | + )) |
6294 | + }) |
6295 | + .optional()?; |
6296 | + Ok(ret) |
6297 | + } |
6298 | + |
6299 | + /// Fetch account by address. |
6300 | + pub fn account_by_address(&self, address: &str) -> Result<Option<DbVal<Account>>> { |
6301 | + let mut stmt = self |
6302 | + .connection |
6303 | + .prepare("SELECT * FROM account WHERE address = ?;")?; |
6304 | + |
6305 | + let ret = stmt |
6306 | + .query_row(rusqlite::params![&address], |row| { |
6307 | + let pk = row.get("pk")?; |
6308 | + Ok(DbVal( |
6309 | + Account { |
6310 | + pk, |
6311 | + name: row.get("name")?, |
6312 | + address: row.get("address")?, |
6313 | + public_key: row.get("public_key")?, |
6314 | + password: row.get("password")?, |
6315 | + enabled: row.get("enabled")?, |
6316 | + }, |
6317 | + pk, |
6318 | + )) |
6319 | + }) |
6320 | + .optional()?; |
6321 | + Ok(ret) |
6322 | + } |
6323 | + |
6324 | + /// Fetch all subscriptions of an account by primary key. |
6325 | + pub fn account_subscriptions(&self, pk: i64) -> Result<Vec<DbVal<ListSubscription>>> { |
6326 | + let mut stmt = self |
6327 | + .connection |
6328 | + .prepare("SELECT * FROM subscription WHERE account = ?;")?; |
6329 | + let list_iter = stmt.query_map([&pk], |row| { |
6330 | + let pk = row.get("pk")?; |
6331 | + Ok(DbVal( |
6332 | + ListSubscription { |
6333 | + pk: row.get("pk")?, |
6334 | + list: row.get("list")?, |
6335 | + address: row.get("address")?, |
6336 | + account: row.get("account")?, |
6337 | + name: row.get("name")?, |
6338 | + digest: row.get("digest")?, |
6339 | + enabled: row.get("enabled")?, |
6340 | + verified: row.get("verified")?, |
6341 | + hide_address: row.get("hide_address")?, |
6342 | + receive_duplicates: row.get("receive_duplicates")?, |
6343 | + receive_own_posts: row.get("receive_own_posts")?, |
6344 | + receive_confirmation: row.get("receive_confirmation")?, |
6345 | + }, |
6346 | + pk, |
6347 | + )) |
6348 | + })?; |
6349 | + |
6350 | + let mut ret = vec![]; |
6351 | + for list in list_iter { |
6352 | + let list = list?; |
6353 | + ret.push(list); |
6354 | + } |
6355 | + Ok(ret) |
6356 | + } |
6357 | + |
6358 | + /// Fetch all accounts. |
6359 | + pub fn accounts(&self) -> Result<Vec<DbVal<Account>>> { |
6360 | + let mut stmt = self |
6361 | + .connection |
6362 | + .prepare("SELECT * FROM account ORDER BY pk ASC;")?; |
6363 | + let list_iter = stmt.query_map([], |row| { |
6364 | + let pk = row.get("pk")?; |
6365 | + Ok(DbVal( |
6366 | + Account { |
6367 | + pk, |
6368 | + name: row.get("name")?, |
6369 | + address: row.get("address")?, |
6370 | + public_key: row.get("public_key")?, |
6371 | + password: row.get("password")?, |
6372 | + enabled: row.get("enabled")?, |
6373 | + }, |
6374 | + pk, |
6375 | + )) |
6376 | + })?; |
6377 | + |
6378 | + let mut ret = vec![]; |
6379 | + for list in list_iter { |
6380 | + let list = list?; |
6381 | + ret.push(list); |
6382 | + } |
6383 | + Ok(ret) |
6384 | + } |
6385 | + |
6386 | + /// Add account. |
6387 | + pub fn add_account(&self, new_val: Account) -> Result<DbVal<Account>> { |
6388 | + let mut stmt = self |
6389 | + .connection |
6390 | + .prepare( |
6391 | + "INSERT INTO account(name, address, public_key, password, enabled) VALUES(?, ?, \ |
6392 | + ?, ?, ?) RETURNING *;", |
6393 | + ) |
6394 | + .unwrap(); |
6395 | + let ret = stmt.query_row( |
6396 | + rusqlite::params![ |
6397 | + &new_val.name, |
6398 | + &new_val.address, |
6399 | + &new_val.public_key, |
6400 | + &new_val.password, |
6401 | + &new_val.enabled, |
6402 | + ], |
6403 | + |row| { |
6404 | + let pk = row.get("pk")?; |
6405 | + Ok(DbVal( |
6406 | + Account { |
6407 | + pk, |
6408 | + name: row.get("name")?, |
6409 | + address: row.get("address")?, |
6410 | + public_key: row.get("public_key")?, |
6411 | + password: row.get("password")?, |
6412 | + enabled: row.get("enabled")?, |
6413 | + }, |
6414 | + pk, |
6415 | + )) |
6416 | + }, |
6417 | + )?; |
6418 | + |
6419 | + trace!("add_account {:?}.", &ret); |
6420 | + Ok(ret) |
6421 | + } |
6422 | + |
6423 | + /// Remove an account by their address. |
6424 | + pub fn remove_account(&self, address: &str) -> Result<()> { |
6425 | + self.connection |
6426 | + .query_row( |
6427 | + "DELETE FROM account WHERE address = ? RETURNING *;", |
6428 | + rusqlite::params![&address], |
6429 | + |_| Ok(()), |
6430 | + ) |
6431 | + .map_err(|err| { |
6432 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
6433 | + Error::from(err).chain_err(|| NotFound("account not found!")) |
6434 | + } else { |
6435 | + err.into() |
6436 | + } |
6437 | + })?; |
6438 | + |
6439 | + Ok(()) |
6440 | + } |
6441 | + |
6442 | + /// Update an account. |
6443 | + pub fn update_account(&mut self, change_set: AccountChangeset) -> Result<()> { |
6444 | + let Some(acc) = self.account_by_address(&change_set.address)? else { |
6445 | + return Err(NotFound("account with this address not found!").into()); |
6446 | + }; |
6447 | + let pk = acc.pk; |
6448 | + if matches!( |
6449 | + change_set, |
6450 | + AccountChangeset { |
6451 | + address: _, |
6452 | + name: None, |
6453 | + public_key: None, |
6454 | + password: None, |
6455 | + enabled: None, |
6456 | + } |
6457 | + ) { |
6458 | + return Ok(()); |
6459 | + } |
6460 | + |
6461 | + let AccountChangeset { |
6462 | + address: _, |
6463 | + name, |
6464 | + public_key, |
6465 | + password, |
6466 | + enabled, |
6467 | + } = change_set; |
6468 | + let tx = self.connection.transaction()?; |
6469 | + |
6470 | + macro_rules! update { |
6471 | + ($field:tt) => {{ |
6472 | + if let Some($field) = $field { |
6473 | + tx.execute( |
6474 | + concat!( |
6475 | + "UPDATE account SET ", |
6476 | + stringify!($field), |
6477 | + " = ? WHERE pk = ?;" |
6478 | + ), |
6479 | + rusqlite::params![&$field, &pk], |
6480 | + )?; |
6481 | + } |
6482 | + }}; |
6483 | + } |
6484 | + update!(name); |
6485 | + update!(public_key); |
6486 | + update!(password); |
6487 | + update!(enabled); |
6488 | + |
6489 | + tx.commit()?; |
6490 | + Ok(()) |
6491 | + } |
6492 | + } |
6493 | + |
6494 | + #[cfg(test)] |
6495 | + mod tests { |
6496 | + use super::*; |
6497 | + use crate::*; |
6498 | + |
6499 | + #[test] |
6500 | + fn test_subscription_ops() { |
6501 | + use tempfile::TempDir; |
6502 | + |
6503 | + let tmp_dir = TempDir::new().unwrap(); |
6504 | + let db_path = tmp_dir.path().join("mpot.db"); |
6505 | + let config = Configuration { |
6506 | + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
6507 | + db_path, |
6508 | + data_path: tmp_dir.path().to_path_buf(), |
6509 | + administrators: vec![], |
6510 | + }; |
6511 | + |
6512 | + let mut db = Connection::open_or_create_db(config).unwrap().trusted(); |
6513 | + let list = db |
6514 | + .create_list(MailingList { |
6515 | + pk: -1, |
6516 | + name: "foobar chat".into(), |
6517 | + id: "foo-chat".into(), |
6518 | + address: "foo-chat@example.com".into(), |
6519 | + description: None, |
6520 | + archive_url: None, |
6521 | + }) |
6522 | + .unwrap(); |
6523 | + let secondary_list = db |
6524 | + .create_list(MailingList { |
6525 | + pk: -1, |
6526 | + name: "foobar chat2".into(), |
6527 | + id: "foo-chat2".into(), |
6528 | + address: "foo-chat2@example.com".into(), |
6529 | + description: None, |
6530 | + archive_url: None, |
6531 | + }) |
6532 | + .unwrap(); |
6533 | + for i in 0..4 { |
6534 | + let sub = db |
6535 | + .add_subscription( |
6536 | + list.pk(), |
6537 | + ListSubscription { |
6538 | + pk: -1, |
6539 | + list: list.pk(), |
6540 | + address: format!("{i}@example.com"), |
6541 | + account: None, |
6542 | + name: Some(format!("User{i}")), |
6543 | + digest: false, |
6544 | + hide_address: false, |
6545 | + receive_duplicates: false, |
6546 | + receive_own_posts: false, |
6547 | + receive_confirmation: false, |
6548 | + enabled: true, |
6549 | + verified: false, |
6550 | + }, |
6551 | + ) |
6552 | + .unwrap(); |
6553 | + assert_eq!(db.list_subscription(list.pk(), sub.pk()).unwrap(), sub); |
6554 | + assert_eq!( |
6555 | + db.list_subscription_by_address(list.pk(), &sub.address) |
6556 | + .unwrap(), |
6557 | + sub |
6558 | + ); |
6559 | + } |
6560 | + |
6561 | + assert_eq!(db.accounts().unwrap(), vec![]); |
6562 | + assert_eq!( |
6563 | + db.remove_subscription(list.pk(), "nonexistent@example.com") |
6564 | + .map_err(|err| err.to_string()) |
6565 | + .unwrap_err(), |
6566 | + NotFound("list or list owner not found!").to_string() |
6567 | + ); |
6568 | + |
6569 | + let cand = db |
6570 | + .add_candidate_subscription( |
6571 | + list.pk(), |
6572 | + ListSubscription { |
6573 | + pk: -1, |
6574 | + list: list.pk(), |
6575 | + address: "4@example.com".into(), |
6576 | + account: None, |
6577 | + name: Some("User4".into()), |
6578 | + digest: false, |
6579 | + hide_address: false, |
6580 | + receive_duplicates: false, |
6581 | + receive_own_posts: false, |
6582 | + receive_confirmation: false, |
6583 | + enabled: true, |
6584 | + verified: false, |
6585 | + }, |
6586 | + ) |
6587 | + .unwrap(); |
6588 | + let accepted = db.accept_candidate_subscription(cand.pk()).unwrap(); |
6589 | + |
6590 | + assert_eq!(db.account(5).unwrap(), None); |
6591 | + assert_eq!( |
6592 | + db.remove_account("4@example.com") |
6593 | + .map_err(|err| err.to_string()) |
6594 | + .unwrap_err(), |
6595 | + NotFound("account not found!").to_string() |
6596 | + ); |
6597 | + |
6598 | + let acc = db |
6599 | + .add_account(Account { |
6600 | + pk: -1, |
6601 | + name: accepted.name.clone(), |
6602 | + address: accepted.address.clone(), |
6603 | + public_key: None, |
6604 | + password: String::new(), |
6605 | + enabled: true, |
6606 | + }) |
6607 | + .unwrap(); |
6608 | + |
6609 | + // Test [ref:add_account] SQL trigger (see schema.sql) |
6610 | + assert_eq!( |
6611 | + db.list_subscription(list.pk(), accepted.pk()) |
6612 | + .unwrap() |
6613 | + .account, |
6614 | + Some(acc.pk()) |
6615 | + ); |
6616 | + // Test [ref:add_account_to_subscription] SQL trigger (see schema.sql) |
6617 | + let sub = db |
6618 | + .add_subscription( |
6619 | + secondary_list.pk(), |
6620 | + ListSubscription { |
6621 | + pk: -1, |
6622 | + list: secondary_list.pk(), |
6623 | + address: "4@example.com".into(), |
6624 | + account: None, |
6625 | + name: Some("User4".into()), |
6626 | + digest: false, |
6627 | + hide_address: false, |
6628 | + receive_duplicates: false, |
6629 | + receive_own_posts: false, |
6630 | + receive_confirmation: false, |
6631 | + enabled: true, |
6632 | + verified: true, |
6633 | + }, |
6634 | + ) |
6635 | + .unwrap(); |
6636 | + assert_eq!(sub.account, Some(acc.pk())); |
6637 | + // Test [ref:verify_subscription_email] SQL trigger (see schema.sql) |
6638 | + assert!(!sub.verified); |
6639 | + |
6640 | + assert_eq!(db.accounts().unwrap(), vec![acc.clone()]); |
6641 | + |
6642 | + assert_eq!( |
6643 | + db.update_account(AccountChangeset { |
6644 | + address: "nonexistent@example.com".into(), |
6645 | + ..AccountChangeset::default() |
6646 | + }) |
6647 | + .map_err(|err| err.to_string()) |
6648 | + .unwrap_err(), |
6649 | + NotFound("account with this address not found!").to_string() |
6650 | + ); |
6651 | + assert_eq!( |
6652 | + db.update_account(AccountChangeset { |
6653 | + address: acc.address.clone(), |
6654 | + ..AccountChangeset::default() |
6655 | + }) |
6656 | + .map_err(|err| err.to_string()), |
6657 | + Ok(()) |
6658 | + ); |
6659 | + assert_eq!( |
6660 | + db.update_account(AccountChangeset { |
6661 | + address: acc.address.clone(), |
6662 | + enabled: Some(Some(false)), |
6663 | + ..AccountChangeset::default() |
6664 | + }) |
6665 | + .map_err(|err| err.to_string()), |
6666 | + Ok(()) |
6667 | + ); |
6668 | + assert!(!db.account(acc.pk()).unwrap().unwrap().enabled); |
6669 | + assert_eq!( |
6670 | + db.remove_account("4@example.com") |
6671 | + .map_err(|err| err.to_string()), |
6672 | + Ok(()) |
6673 | + ); |
6674 | + assert_eq!(db.accounts().unwrap(), vec![]); |
6675 | + } |
6676 | + } |
6677 | diff --git a/core/src/templates.rs b/core/src/templates.rs |
6678 | index 4555f9e..8617b46 100644 |
6679 | --- a/core/src/templates.rs |
6680 | +++ b/core/src/templates.rs |
6681 | @@ -17,9 +17,17 @@ |
6682 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
6683 | */ |
6684 | |
6685 | + //! Named templates, for generated e-mail like confirmations, alerts etc. |
6686 | + //! |
6687 | //! Template database model: [`Template`]. |
6688 | |
6689 | - use super::*; |
6690 | + use log::trace; |
6691 | + use rusqlite::OptionalExtension; |
6692 | + |
6693 | + use crate::{ |
6694 | + errors::{ErrorKind::*, *}, |
6695 | + Connection, DbVal, |
6696 | + }; |
6697 | |
6698 | /// A named template. |
6699 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] |
6700 | @@ -202,3 +210,159 @@ impl Template { |
6701 | } |
6702 | } |
6703 | } |
6704 | + |
6705 | + impl Connection { |
6706 | + /// Fetch all. |
6707 | + pub fn fetch_templates(&self) -> Result<Vec<DbVal<Template>>> { |
6708 | + let mut stmt = self |
6709 | + .connection |
6710 | + .prepare("SELECT * FROM templates ORDER BY pk;")?; |
6711 | + let iter = stmt.query_map(rusqlite::params![], |row| { |
6712 | + let pk = row.get("pk")?; |
6713 | + Ok(DbVal( |
6714 | + Template { |
6715 | + pk, |
6716 | + name: row.get("name")?, |
6717 | + list: row.get("list")?, |
6718 | + subject: row.get("subject")?, |
6719 | + headers_json: row.get("headers_json")?, |
6720 | + body: row.get("body")?, |
6721 | + }, |
6722 | + pk, |
6723 | + )) |
6724 | + })?; |
6725 | + |
6726 | + let mut ret = vec![]; |
6727 | + for templ in iter { |
6728 | + let templ = templ?; |
6729 | + ret.push(templ); |
6730 | + } |
6731 | + Ok(ret) |
6732 | + } |
6733 | + |
6734 | + /// Fetch a named template. |
6735 | + pub fn fetch_template( |
6736 | + &self, |
6737 | + template: &str, |
6738 | + list_pk: Option<i64>, |
6739 | + ) -> Result<Option<DbVal<Template>>> { |
6740 | + let mut stmt = self |
6741 | + .connection |
6742 | + .prepare("SELECT * FROM templates WHERE name = ? AND list IS ?;")?; |
6743 | + let ret = stmt |
6744 | + .query_row(rusqlite::params![&template, &list_pk], |row| { |
6745 | + let pk = row.get("pk")?; |
6746 | + Ok(DbVal( |
6747 | + Template { |
6748 | + pk, |
6749 | + name: row.get("name")?, |
6750 | + list: row.get("list")?, |
6751 | + subject: row.get("subject")?, |
6752 | + headers_json: row.get("headers_json")?, |
6753 | + body: row.get("body")?, |
6754 | + }, |
6755 | + pk, |
6756 | + )) |
6757 | + }) |
6758 | + .optional()?; |
6759 | + if ret.is_none() && list_pk.is_some() { |
6760 | + let mut stmt = self |
6761 | + .connection |
6762 | + .prepare("SELECT * FROM templates WHERE name = ? AND list IS NULL;")?; |
6763 | + Ok(stmt |
6764 | + .query_row(rusqlite::params![&template], |row| { |
6765 | + let pk = row.get("pk")?; |
6766 | + Ok(DbVal( |
6767 | + Template { |
6768 | + pk, |
6769 | + name: row.get("name")?, |
6770 | + list: row.get("list")?, |
6771 | + subject: row.get("subject")?, |
6772 | + headers_json: row.get("headers_json")?, |
6773 | + body: row.get("body")?, |
6774 | + }, |
6775 | + pk, |
6776 | + )) |
6777 | + }) |
6778 | + .optional()?) |
6779 | + } else { |
6780 | + Ok(ret) |
6781 | + } |
6782 | + } |
6783 | + |
6784 | + /// Insert a named template. |
6785 | + pub fn add_template(&self, template: Template) -> Result<DbVal<Template>> { |
6786 | + let mut stmt = self.connection.prepare( |
6787 | + "INSERT INTO templates(name, list, subject, headers_json, body) VALUES(?, ?, ?, ?, ?) \ |
6788 | + RETURNING *;", |
6789 | + )?; |
6790 | + let ret = stmt |
6791 | + .query_row( |
6792 | + rusqlite::params![ |
6793 | + &template.name, |
6794 | + &template.list, |
6795 | + &template.subject, |
6796 | + &template.headers_json, |
6797 | + &template.body |
6798 | + ], |
6799 | + |row| { |
6800 | + let pk = row.get("pk")?; |
6801 | + Ok(DbVal( |
6802 | + Template { |
6803 | + pk, |
6804 | + name: row.get("name")?, |
6805 | + list: row.get("list")?, |
6806 | + subject: row.get("subject")?, |
6807 | + headers_json: row.get("headers_json")?, |
6808 | + body: row.get("body")?, |
6809 | + }, |
6810 | + pk, |
6811 | + )) |
6812 | + }, |
6813 | + ) |
6814 | + .map_err(|err| { |
6815 | + if matches!( |
6816 | + err, |
6817 | + rusqlite::Error::SqliteFailure( |
6818 | + rusqlite::ffi::Error { |
6819 | + code: rusqlite::ffi::ErrorCode::ConstraintViolation, |
6820 | + extended_code: 787 |
6821 | + }, |
6822 | + _ |
6823 | + ) |
6824 | + ) { |
6825 | + Error::from(err).chain_err(|| NotFound("Could not find a list with this pk.")) |
6826 | + } else { |
6827 | + err.into() |
6828 | + } |
6829 | + })?; |
6830 | + |
6831 | + trace!("add_template {:?}.", &ret); |
6832 | + Ok(ret) |
6833 | + } |
6834 | + |
6835 | + /// Remove a named template. |
6836 | + pub fn remove_template(&self, template: &str, list_pk: Option<i64>) -> Result<Template> { |
6837 | + let mut stmt = self |
6838 | + .connection |
6839 | + .prepare("DELETE FROM templates WHERE name = ? AND list IS ? RETURNING *;")?; |
6840 | + let ret = stmt.query_row(rusqlite::params![&template, &list_pk], |row| { |
6841 | + Ok(Template { |
6842 | + pk: -1, |
6843 | + name: row.get("name")?, |
6844 | + list: row.get("list")?, |
6845 | + subject: row.get("subject")?, |
6846 | + headers_json: row.get("headers_json")?, |
6847 | + body: row.get("body")?, |
6848 | + }) |
6849 | + })?; |
6850 | + |
6851 | + trace!( |
6852 | + "remove_template {} list_pk {:?} {:?}.", |
6853 | + template, |
6854 | + &list_pk, |
6855 | + &ret |
6856 | + ); |
6857 | + Ok(ret) |
6858 | + } |
6859 | + } |
6860 | diff --git a/core/tests/account.rs b/core/tests/account.rs |
6861 | index 1c803b5..25e0bff 100644 |
6862 | --- a/core/tests/account.rs |
6863 | +++ b/core/tests/account.rs |
6864 | @@ -17,7 +17,7 @@ |
6865 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
6866 | */ |
6867 | |
6868 | - use mailpot::{models::*, Configuration, Connection, Queue, SendMail}; |
6869 | + use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail}; |
6870 | use mailpot_tests::init_stderr_logging; |
6871 | use tempfile::TempDir; |
6872 | |
6873 | diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs |
6874 | index 6d23fd5..f239247 100644 |
6875 | --- a/core/tests/error_queue.rs |
6876 | +++ b/core/tests/error_queue.rs |
6877 | @@ -17,7 +17,7 @@ |
6878 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
6879 | */ |
6880 | |
6881 | - use mailpot::{melib, models::*, Configuration, Connection, Queue, SendMail}; |
6882 | + use mailpot::{melib, models::*, queue::Queue, Configuration, Connection, SendMail}; |
6883 | use mailpot_tests::init_stderr_logging; |
6884 | use tempfile::TempDir; |
6885 | |
6886 | diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs |
6887 | index 639a123..b9a4b44 100644 |
6888 | --- a/core/tests/smtp.rs |
6889 | +++ b/core/tests/smtp.rs |
6890 | @@ -18,7 +18,7 @@ |
6891 | */ |
6892 | |
6893 | use log::{trace, warn}; |
6894 | - use mailpot::{melib, models::*, Configuration, Connection, Queue, SendMail}; |
6895 | + use mailpot::{melib, models::*, queue::Queue, Configuration, Connection, SendMail}; |
6896 | use mailpot_tests::*; |
6897 | use melib::smol; |
6898 | use tempfile::TempDir; |
6899 | diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs |
6900 | index 234ce69..d4a1e58 100644 |
6901 | --- a/core/tests/subscription.rs |
6902 | +++ b/core/tests/subscription.rs |
6903 | @@ -17,7 +17,7 @@ |
6904 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
6905 | */ |
6906 | |
6907 | - use mailpot::{models::*, Configuration, Connection, Queue, SendMail}; |
6908 | + use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail}; |
6909 | use mailpot_tests::init_stderr_logging; |
6910 | use tempfile::TempDir; |
6911 | |
6912 | diff --git a/core/tests/template_replies.rs b/core/tests/template_replies.rs |
6913 | index fb890cf..438c6c2 100644 |
6914 | --- a/core/tests/template_replies.rs |
6915 | +++ b/core/tests/template_replies.rs |
6916 | @@ -17,7 +17,7 @@ |
6917 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
6918 | */ |
6919 | |
6920 | - use mailpot::{models::*, Configuration, Connection, Queue, SendMail, Template}; |
6921 | + use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail, Template}; |
6922 | use mailpot_tests::init_stderr_logging; |
6923 | use tempfile::TempDir; |
6924 | |
6925 | diff --git a/docs/mpot.1 b/docs/mpot.1 |
6926 | index 18df166..888ba26 100644 |
6927 | --- a/docs/mpot.1 |
6928 | +++ b/docs/mpot.1 |
6929 | @@ -94,7 +94,13 @@ See <https://www.postfix.org/master.5.html>. |
6930 | |
6931 | .br |
6932 | |
6933 | + mpot sample\-config [\-\-with\-smtp \fIWITH_SMTP\fR] |
6934 | + .br |
6935 | + |
6936 | Prints a sample config file to STDOUT. |
6937 | + .TP |
6938 | + \-\-with\-smtp |
6939 | + Use an SMTP connection instead of a shell process. |
6940 | .ie \n(.g .ds Aq \(aq |
6941 | .el .ds Aq ' |
6942 | .\fB |