Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 9eaa580af495677d6f24b72c2654e10f3ec124ea
Timestamp: Wed, 03 May 2023 08:15:55 +0000 (1 year ago)

+3366 -3306 +/-26 browse
core: reorganise old module hierarchy
1diff --git a/cli/src/lib.rs b/cli/src/lib.rs
2index 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
21index 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
77index 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
91new file mode 100644
92index 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
660deleted file mode 100644
661index 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
1252deleted file mode 100644
1253index 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
1650deleted file mode 100644
1651index 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
2425deleted file mode 100644
2426index 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
2761deleted file mode 100644
2762index 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
3537deleted file mode 100644
3538index 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
3721index 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
3753index 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
3795deleted file mode 100644
3796index 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
4057new file mode 100644
4058index 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
4338new file mode 100644
4339index 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
4751new file mode 100644
4752index 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
5537new file mode 100644
5538index 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
5876index 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
5889new file mode 100644
5890index 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
6678index 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
6861index 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
6874index 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
6887index 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
6900index 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
6913index 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
6926index 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