Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | fb1d9ce2ee | |
Manos Pitsidianakis | 1d1d6d7a17 |
|
@ -3,7 +3,7 @@ name: Build release binary
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
SQLITE_BIN: ~/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -31,15 +31,15 @@ jobs:
|
|||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
path: ~/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
mkdir -p ~/.sqlite3
|
||||
cd ~/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
|
|
|
@ -3,7 +3,7 @@ name: Code coverage
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
SQLITE_BIN: ~/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -21,15 +21,15 @@ jobs:
|
|||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
path: ~/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
mkdir -p ~/.sqlite3
|
||||
cd ~/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
|
|
|
@ -3,7 +3,7 @@ name: Build rustdoc for Github Pages
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
SQLITE_BIN: ~/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -17,15 +17,15 @@ jobs:
|
|||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
path: ~/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
mkdir -p ~/.sqlite3
|
||||
cd ~/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
|
|
|
@ -3,7 +3,7 @@ name: Tests
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
SQLITE_BIN: ~/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -35,15 +35,15 @@ jobs:
|
|||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
path: ~/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
mkdir -p ~/.sqlite3
|
||||
cd ~/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
|
|
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
|
@ -1,13 +1,12 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"mailpot",
|
||||
"mailpot-archives",
|
||||
"mailpot-cli",
|
||||
"mailpot-http",
|
||||
"archive-http",
|
||||
"cli",
|
||||
"core",
|
||||
"mailpot-tests",
|
||||
"mailpot-web",
|
||||
"rest-http",
|
||||
"web",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ DJHTMLBIN = djhtml
|
|||
BLACKBIN = black
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
HTML_FILES := $(shell find mailpot-web/src/templates -type f -print0 | tr '\0' ' ')
|
||||
HTML_FILES := $(shell find web/src/templates -type f -print0 | tr '\0' ' ')
|
||||
PY_FILES := $(shell find . -type f -name '*.py' -print0 | tr '\0' ' ')
|
||||
|
||||
.PHONY: check
|
||||
|
|
84
README.md
84
README.md
|
@ -47,8 +47,8 @@ $ mkdir -p /home/user/.config/mailpot
|
|||
$ export MPOT_CONFIG=/home/user/.config/mailpot/config.toml
|
||||
$ cargo run --bin mpot -- sample-config > "$MPOT_CONFIG"
|
||||
$ # edit config and set database path e.g. "/home/user/.local/share/mailpot/mpot.db"
|
||||
$ cargo run --bin mpot -- -c "$MPOT_CONFIG" list-lists
|
||||
No lists found.
|
||||
$ cargo run --bin mpot -- -c "$MPOT_CONFIG" db-location
|
||||
/home/user/.local/share/mailpot/mpot.db
|
||||
```
|
||||
|
||||
This creates the database file in the configuration file as if you executed the following:
|
||||
|
@ -61,73 +61,27 @@ $ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql
|
|||
|
||||
```text
|
||||
% mpot help
|
||||
GNU Affero version 3 or later <https://www.gnu.org/licenses/>
|
||||
mailpot 0.1.0
|
||||
mini mailing list manager
|
||||
|
||||
Tool for mailpot mailing list management.
|
||||
USAGE:
|
||||
mpot [FLAGS] [OPTIONS] <SUBCOMMAND>
|
||||
|
||||
Usage: mpot [OPTIONS] <COMMAND>
|
||||
FLAGS:
|
||||
-d, --debug Activate debug mode
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
Commands:
|
||||
sample-config
|
||||
Prints a sample config file to STDOUT
|
||||
dump-database
|
||||
Dumps database data to STDOUT
|
||||
list-lists
|
||||
Lists all registered mailing lists
|
||||
list
|
||||
Mailing list management
|
||||
create-list
|
||||
Create new list
|
||||
post
|
||||
Post message from STDIN to list
|
||||
flush-queue
|
||||
Flush outgoing e-mail queue
|
||||
error-queue
|
||||
Mail that has not been handled properly end up in the error queue
|
||||
queue
|
||||
Mail that has not been handled properly end up in the error queue
|
||||
import-maildir
|
||||
Import a maildir folder into an existing list
|
||||
update-postfix-config
|
||||
Update postfix maps and master.cf (probably needs root permissions)
|
||||
print-postfix-config
|
||||
Print postfix maps and master.cf entry to STDOUT
|
||||
accounts
|
||||
All Accounts
|
||||
account-info
|
||||
Account info
|
||||
add-account
|
||||
Add account
|
||||
remove-account
|
||||
Remove account
|
||||
update-account
|
||||
Update account info
|
||||
repair
|
||||
Show and fix possible data mistakes or inconsistencies
|
||||
help
|
||||
Print this message or the help of the given subcommand(s)
|
||||
OPTIONS:
|
||||
-c, --config <config> Set config file
|
||||
|
||||
Options:
|
||||
-d, --debug
|
||||
Print logs
|
||||
|
||||
-c, --config <CONFIG>
|
||||
Configuration file to use
|
||||
|
||||
-q, --quiet
|
||||
Silence all output
|
||||
|
||||
-v, --verbose...
|
||||
Verbose mode (-v, -vv, -vvv, etc)
|
||||
|
||||
-t, --ts <TS>
|
||||
Debug log timestamp (sec, ms, ns, none)
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
-V, --version
|
||||
Print version
|
||||
SUBCOMMANDS:
|
||||
create-list Create new list
|
||||
db-location Prints database filesystem location
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
list Mailing list management
|
||||
list-lists Lists all registered mailing lists
|
||||
post Post message from STDIN to list
|
||||
```
|
||||
|
||||
### Receiving mail
|
||||
|
|
|
@ -18,7 +18,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
chrono = { version = "^0.4" }
|
||||
lazy_static = "^1.4"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ] }
|
||||
percent-encoding = { version = "^2.1", optional = true }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
|
@ -38,8 +38,8 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
return Err("Output path is not a directory.".into());
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(output_path.join("lists"))?;
|
||||
std::fs::create_dir_all(output_path.join("list"))?;
|
||||
std::fs::create_dir_all(&output_path.join("lists"))?;
|
||||
std::fs::create_dir_all(&output_path.join("list"))?;
|
||||
let conf = Configuration::from_file(config_path)
|
||||
.map_err(|err| format!("Could not load config {config_path}: {err}"))?;
|
||||
|
||||
|
@ -67,7 +67,7 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output_path.join("index.html"))?;
|
||||
.open(&output_path.join("index.html"))?;
|
||||
let crumbs = vec![Crumb {
|
||||
label: "Lists".into(),
|
||||
url: format!("{root_url_prefix}/").into(),
|
|
@ -20,20 +20,20 @@ doc-scrape-examples = true
|
|||
base64 = { version = "0.21" }
|
||||
clap = { version = "^4.2", default-features = false, features = ["std", "derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] }
|
||||
log = "0.4"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
stderrlog = { version = "^0.6" }
|
||||
stderrlog = "^0.5"
|
||||
ureq = { version = "2.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2"
|
||||
mailpot-tests = { version = "^0.1", path = "../mailpot-tests" }
|
||||
predicates = "3"
|
||||
tempfile = { version = "3.9" }
|
||||
tempfile = "3.3"
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "^4.2", default-features = false, features = ["std", "derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] }
|
||||
clap_mangen = "0.2.10"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
stderrlog = { version = "^0.6" }
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
stderrlog = "^0.5"
|
|
@ -23,7 +23,7 @@ use std::{
|
|||
io::Write,
|
||||
};
|
||||
|
||||
use clap::{ArgAction, CommandFactory};
|
||||
use clap::ArgAction;
|
||||
use clap_mangen::{roff, Man};
|
||||
use roff::{bold, italic, roman, Inline, Roff};
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
pub use std::path::PathBuf;
|
||||
|
||||
pub use clap::{builder::TypedValueParser, Args, Parser, Subcommand};
|
||||
pub use clap::{builder::TypedValueParser, Args, CommandFactory, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
|
@ -102,7 +102,12 @@ pub enum Command {
|
|||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Processed mail is stored in queues.
|
||||
/// Mail that has not been handled properly end up in the error queue.
|
||||
ErrorQueue {
|
||||
#[command(subcommand)]
|
||||
cmd: QueueCommand,
|
||||
},
|
||||
/// Mail that has not been handled properly end up in the error queue.
|
||||
Queue {
|
||||
#[arg(long, value_parser = QueueValueParser)]
|
||||
queue: mailpot::queue::Queue,
|
||||
|
@ -270,6 +275,9 @@ pub enum QueueCommand {
|
|||
/// index of entry.
|
||||
#[arg(long)]
|
||||
index: Vec<i64>,
|
||||
/// Do not print in stdout.
|
||||
#[arg(long)]
|
||||
quiet: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -321,8 +329,6 @@ pub struct AccountOptions {
|
|||
pub enum ListCommand {
|
||||
/// List subscriptions of list.
|
||||
Subscriptions,
|
||||
/// List subscription requests.
|
||||
SubscriptionRequests,
|
||||
/// Add subscription to list.
|
||||
AddSubscription {
|
||||
/// E-mail address.
|
||||
|
@ -344,19 +350,6 @@ pub enum ListCommand {
|
|||
#[clap(flatten)]
|
||||
subscription_options: SubscriptionOptions,
|
||||
},
|
||||
/// Accept a subscription request by its primary key.
|
||||
AcceptSubscriptionRequest {
|
||||
/// The primary key of the request.
|
||||
pk: i64,
|
||||
/// Do not send confirmation e-mail.
|
||||
#[arg(long, default_value = "false")]
|
||||
do_not_send_confirmation: bool,
|
||||
},
|
||||
/// Send subscription confirmation manually.
|
||||
SendConfirmationForSubscription {
|
||||
/// The primary key of the subscription.
|
||||
pk: i64,
|
||||
},
|
||||
/// Add a new post policy.
|
||||
AddPostPolicy {
|
||||
#[arg(long)]
|
||||
|
@ -554,14 +547,6 @@ impl TypedValueParser for QueueValueParser {
|
|||
})?)
|
||||
.map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err))
|
||||
}
|
||||
|
||||
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue>>> {
|
||||
Some(Box::new(
|
||||
mailpot::queue::Queue::possible_values()
|
||||
.iter()
|
||||
.map(clap::builder::PossibleValue::new),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for QueueValueParser {
|
|
@ -22,8 +22,6 @@ extern crate ureq;
|
|||
pub use std::path::PathBuf;
|
||||
|
||||
mod args;
|
||||
pub mod commands;
|
||||
pub mod import;
|
||||
pub mod lints;
|
||||
pub use args::*;
|
||||
pub use clap::{Args, CommandFactory, Parser, Subcommand};
|
|
@ -17,12 +17,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use mailpot::{
|
||||
chrono,
|
||||
melib::{self, Envelope},
|
||||
models::{Account, DbVal, ListSubscription, MailingList},
|
||||
rusqlite, Connection, Result,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<()> {
|
||||
let mut col = vec![];
|
||||
|
@ -31,7 +26,7 @@ pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<
|
|||
let iter = stmt.query_map([], |row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
let date_s: String = row.get("datetime")?;
|
||||
match melib::utils::datetime::rfc822_to_timestamp(date_s.trim()) {
|
||||
match melib::datetime::rfc822_to_timestamp(date_s.trim()) {
|
||||
Err(_) | Ok(0) => {
|
||||
let mut timestamp: i64 = row.get("timestamp")?;
|
||||
let created: i64 = row.get("created")?;
|
||||
|
@ -75,11 +70,7 @@ pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<
|
|||
{
|
||||
v.to_rfc2822()
|
||||
} else if let Some(v) = timestamp.map(|t| {
|
||||
melib::utils::datetime::timestamp_to_string(
|
||||
t,
|
||||
Some(melib::utils::datetime::formats::RFC822_DATE),
|
||||
true,
|
||||
)
|
||||
melib::datetime::timestamp_to_string(t, Some(melib::datetime::RFC822_DATE), true)
|
||||
}) {
|
||||
v
|
||||
} else if let Ok(v) =
|
|
@ -0,0 +1,976 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
io::{Read, Write},
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
mod lints;
|
||||
use lints::*;
|
||||
use mailpot::{
|
||||
melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
|
||||
models::{changesets::*, *},
|
||||
queue::QueueEntry,
|
||||
transaction::TransactionBehavior,
|
||||
Configuration, Connection, Error, ErrorKind, Result, *,
|
||||
};
|
||||
use mailpot_cli::*;
|
||||
|
||||
macro_rules! list {
|
||||
($db:ident, $list_id:expr) => {{
|
||||
$db.list_by_id(&$list_id)?.or_else(|| {
|
||||
$list_id
|
||||
.parse::<i64>()
|
||||
.ok()
|
||||
.map(|pk| $db.list(pk).ok())
|
||||
.flatten()
|
||||
.flatten()
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! string_opts {
|
||||
($field:ident) => {
|
||||
if $field.as_deref().map(str::is_empty).unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some($field)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn run_app(opt: Opt) -> Result<()> {
|
||||
if opt.debug {
|
||||
println!("DEBUG: {:?}", &opt);
|
||||
}
|
||||
if let Command::SampleConfig { with_smtp } = opt.cmd {
|
||||
let mut new = Configuration::new("/path/to/sqlite.db");
|
||||
new.administrators.push("admin@example.com".to_string());
|
||||
if with_smtp {
|
||||
new.send_mail = mailpot::SendMail::Smtp(SmtpServerConf {
|
||||
hostname: "mail.example.com".to_string(),
|
||||
port: 587,
|
||||
envelope_from: "".to_string(),
|
||||
auth: SmtpAuth::Auto {
|
||||
username: "user".to_string(),
|
||||
password: Password::Raw("hunter2".to_string()),
|
||||
auth_type: SmtpAuthType::default(),
|
||||
require_auth: true,
|
||||
},
|
||||
security: SmtpSecurity::StartTLS {
|
||||
danger_accept_invalid_certs: false,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
}
|
||||
println!("{}", new.to_toml());
|
||||
return Ok(());
|
||||
};
|
||||
let config_path = if let Some(path) = opt.config.as_ref() {
|
||||
path.as_path()
|
||||
} else {
|
||||
let mut opt = Opt::command();
|
||||
opt.error(
|
||||
clap::error::ErrorKind::MissingRequiredArgument,
|
||||
"--config is required for mailing list operations",
|
||||
)
|
||||
.exit();
|
||||
};
|
||||
|
||||
let config = Configuration::from_file(config_path)?;
|
||||
|
||||
use Command::*;
|
||||
let mut db = Connection::open_or_create_db(config)?.trusted();
|
||||
match opt.cmd {
|
||||
SampleConfig { .. } => {}
|
||||
DumpDatabase => {
|
||||
let lists = db.lists()?;
|
||||
let mut stdout = std::io::stdout();
|
||||
serde_json::to_writer_pretty(&mut stdout, &lists)?;
|
||||
for l in &lists {
|
||||
serde_json::to_writer_pretty(&mut stdout, &db.list_subscriptions(l.pk)?)?;
|
||||
}
|
||||
}
|
||||
ListLists => {
|
||||
let lists = db.lists()?;
|
||||
if lists.is_empty() {
|
||||
println!("No lists found.");
|
||||
} else {
|
||||
for l in lists {
|
||||
println!("- {} {:?}", l.id, l);
|
||||
let list_owners = db.list_owners(l.pk)?;
|
||||
if list_owners.is_empty() {
|
||||
println!("\tList owners: None");
|
||||
} else {
|
||||
println!("\tList owners:");
|
||||
for o in list_owners {
|
||||
println!("\t- {}", o);
|
||||
}
|
||||
}
|
||||
if let Some(s) = db.list_post_policy(l.pk)? {
|
||||
println!("\tPost policy: {}", s);
|
||||
} else {
|
||||
println!("\tPost policy: None");
|
||||
}
|
||||
if let Some(s) = db.list_subscription_policy(l.pk)? {
|
||||
println!("\tSubscription policy: {}", s);
|
||||
} else {
|
||||
println!("\tSubscription policy: None");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
List { list_id, cmd } => {
|
||||
let list = match list!(db, list_id) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(format!("No list with id or pk {} was found", list_id).into());
|
||||
}
|
||||
};
|
||||
use ListCommand::*;
|
||||
match cmd {
|
||||
Subscriptions => {
|
||||
let subscriptions = db.list_subscriptions(list.pk)?;
|
||||
if subscriptions.is_empty() {
|
||||
println!("No subscriptions found.");
|
||||
} else {
|
||||
println!("Subscriptions of list {}", list.id);
|
||||
for l in subscriptions {
|
||||
println!("- {}", &l);
|
||||
}
|
||||
}
|
||||
}
|
||||
AddSubscription {
|
||||
address,
|
||||
subscription_options:
|
||||
SubscriptionOptions {
|
||||
name,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
receive_confirmation,
|
||||
enabled,
|
||||
verified,
|
||||
},
|
||||
} => {
|
||||
db.add_subscription(
|
||||
list.pk,
|
||||
ListSubscription {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
address,
|
||||
account: None,
|
||||
name,
|
||||
digest: digest.unwrap_or(false),
|
||||
hide_address: hide_address.unwrap_or(false),
|
||||
receive_confirmation: receive_confirmation.unwrap_or(true),
|
||||
receive_duplicates: receive_duplicates.unwrap_or(true),
|
||||
receive_own_posts: receive_own_posts.unwrap_or(false),
|
||||
enabled: enabled.unwrap_or(true),
|
||||
verified: verified.unwrap_or(false),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
RemoveSubscription { address } => {
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
println!(
|
||||
"Are you sure you want to remove subscription of {} from list {}? \
|
||||
[Yy/n]",
|
||||
address, list
|
||||
);
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
|
||||
break;
|
||||
} else if input.trim() == "n" {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_subscription(list.pk, &address)?;
|
||||
}
|
||||
Health => {
|
||||
println!("{} health:", list);
|
||||
let list_owners = db.list_owners(list.pk)?;
|
||||
let post_policy = db.list_post_policy(list.pk)?;
|
||||
let subscription_policy = db.list_subscription_policy(list.pk)?;
|
||||
if list_owners.is_empty() {
|
||||
println!("\tList has no owners: you should add at least one.");
|
||||
} else {
|
||||
for owner in list_owners {
|
||||
println!("\tList owner: {}.", owner);
|
||||
}
|
||||
}
|
||||
if let Some(p) = post_policy {
|
||||
println!("\tList has post policy: {p}.");
|
||||
} else {
|
||||
println!("\tList has no post policy: you should add one.");
|
||||
}
|
||||
if let Some(p) = subscription_policy {
|
||||
println!("\tList has subscription policy: {p}.");
|
||||
} else {
|
||||
println!("\tList has no subscription policy: you should add one.");
|
||||
}
|
||||
}
|
||||
Info => {
|
||||
println!("{} info:", list);
|
||||
let list_owners = db.list_owners(list.pk)?;
|
||||
let post_policy = db.list_post_policy(list.pk)?;
|
||||
let subscription_policy = db.list_subscription_policy(list.pk)?;
|
||||
let subscriptions = db.list_subscriptions(list.pk)?;
|
||||
if subscriptions.is_empty() {
|
||||
println!("No subscriptions.");
|
||||
} else if subscriptions.len() == 1 {
|
||||
println!("1 subscription.");
|
||||
} else {
|
||||
println!("{} subscriptions.", subscriptions.len());
|
||||
}
|
||||
if list_owners.is_empty() {
|
||||
println!("List owners: None");
|
||||
} else {
|
||||
println!("List owners:");
|
||||
for o in list_owners {
|
||||
println!("\t- {}", o);
|
||||
}
|
||||
}
|
||||
if let Some(s) = post_policy {
|
||||
println!("Post policy: {s}");
|
||||
} else {
|
||||
println!("Post policy: None");
|
||||
}
|
||||
if let Some(s) = subscription_policy {
|
||||
println!("Subscription policy: {s}");
|
||||
} else {
|
||||
println!("Subscription policy: None");
|
||||
}
|
||||
}
|
||||
UpdateSubscription {
|
||||
address,
|
||||
subscription_options:
|
||||
SubscriptionOptions {
|
||||
name,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
receive_confirmation,
|
||||
enabled,
|
||||
verified,
|
||||
},
|
||||
} => {
|
||||
let name = if name
|
||||
.as_ref()
|
||||
.map(|s: &String| s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
};
|
||||
let changeset = ListSubscriptionChangeset {
|
||||
list: list.pk,
|
||||
address,
|
||||
account: None,
|
||||
name,
|
||||
digest,
|
||||
verified,
|
||||
hide_address,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
receive_confirmation,
|
||||
enabled,
|
||||
};
|
||||
db.update_subscription(changeset)?;
|
||||
}
|
||||
AddPostPolicy {
|
||||
announce_only,
|
||||
subscription_only,
|
||||
approval_needed,
|
||||
open,
|
||||
custom,
|
||||
} => {
|
||||
let policy = PostPolicy {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
announce_only,
|
||||
subscription_only,
|
||||
approval_needed,
|
||||
open,
|
||||
custom,
|
||||
};
|
||||
let new_val = db.set_list_post_policy(policy)?;
|
||||
println!("Added new policy with pk = {}", new_val.pk());
|
||||
}
|
||||
RemovePostPolicy { pk } => {
|
||||
db.remove_list_post_policy(list.pk, pk)?;
|
||||
println!("Removed policy with pk = {}", pk);
|
||||
}
|
||||
AddSubscriptionPolicy {
|
||||
send_confirmation,
|
||||
open,
|
||||
manual,
|
||||
request,
|
||||
custom,
|
||||
} => {
|
||||
let policy = SubscriptionPolicy {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
send_confirmation,
|
||||
open,
|
||||
manual,
|
||||
request,
|
||||
custom,
|
||||
};
|
||||
let new_val = db.set_list_subscription_policy(policy)?;
|
||||
println!("Added new subscribe policy with pk = {}", new_val.pk());
|
||||
}
|
||||
RemoveSubscriptionPolicy { pk } => {
|
||||
db.remove_list_subscription_policy(list.pk, pk)?;
|
||||
println!("Removed subscribe policy with pk = {}", pk);
|
||||
}
|
||||
AddListOwner { address, name } => {
|
||||
let list_owner = ListOwner {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
address,
|
||||
name,
|
||||
};
|
||||
let new_val = db.add_list_owner(list_owner)?;
|
||||
println!("Added new list owner {}", new_val);
|
||||
}
|
||||
RemoveListOwner { pk } => {
|
||||
db.remove_list_owner(list.pk, pk)?;
|
||||
println!("Removed list owner with pk = {}", pk);
|
||||
}
|
||||
EnableSubscription { address } => {
|
||||
let changeset = ListSubscriptionChangeset {
|
||||
list: list.pk,
|
||||
address,
|
||||
account: None,
|
||||
name: None,
|
||||
digest: None,
|
||||
verified: None,
|
||||
enabled: Some(true),
|
||||
hide_address: None,
|
||||
receive_duplicates: None,
|
||||
receive_own_posts: None,
|
||||
receive_confirmation: None,
|
||||
};
|
||||
db.update_subscription(changeset)?;
|
||||
}
|
||||
DisableSubscription { address } => {
|
||||
let changeset = ListSubscriptionChangeset {
|
||||
list: list.pk,
|
||||
address,
|
||||
account: None,
|
||||
name: None,
|
||||
digest: None,
|
||||
enabled: Some(false),
|
||||
verified: None,
|
||||
hide_address: None,
|
||||
receive_duplicates: None,
|
||||
receive_own_posts: None,
|
||||
receive_confirmation: None,
|
||||
};
|
||||
db.update_subscription(changeset)?;
|
||||
}
|
||||
Update {
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
owner_local_part,
|
||||
request_local_part,
|
||||
verify,
|
||||
hidden,
|
||||
enabled,
|
||||
} => {
|
||||
let description = string_opts!(description);
|
||||
let archive_url = string_opts!(archive_url);
|
||||
let owner_local_part = string_opts!(owner_local_part);
|
||||
let request_local_part = string_opts!(request_local_part);
|
||||
let changeset = MailingListChangeset {
|
||||
pk: list.pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
owner_local_part,
|
||||
request_local_part,
|
||||
verify,
|
||||
hidden,
|
||||
enabled,
|
||||
};
|
||||
db.update_list(changeset)?;
|
||||
}
|
||||
ImportMembers {
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
list_id,
|
||||
dry_run,
|
||||
skip_owners,
|
||||
} => {
|
||||
let conn = import::Mailman3Connection::new(&url, &username, &password).unwrap();
|
||||
if dry_run {
|
||||
let entries = conn.users(&list_id).unwrap();
|
||||
println!("{} result(s)", entries.len());
|
||||
for e in entries {
|
||||
println!(
|
||||
"{}{}<{}>",
|
||||
if let Some(n) = e.display_name() {
|
||||
n
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if e.display_name().is_none() { "" } else { " " },
|
||||
e.email()
|
||||
);
|
||||
}
|
||||
if !skip_owners {
|
||||
let entries = conn.owners(&list_id).unwrap();
|
||||
println!("\nOwners: {} result(s)", entries.len());
|
||||
for e in entries {
|
||||
println!(
|
||||
"{}{}<{}>",
|
||||
if let Some(n) = e.display_name() {
|
||||
n
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if e.display_name().is_none() { "" } else { " " },
|
||||
e.email()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let entries = conn.users(&list_id).unwrap();
|
||||
let tx = db.transaction(Default::default()).unwrap();
|
||||
for sub in entries.into_iter().map(|e| e.into_subscription(list.pk)) {
|
||||
tx.add_subscription(list.pk, sub)?;
|
||||
}
|
||||
if !skip_owners {
|
||||
let entries = conn.owners(&list_id).unwrap();
|
||||
for sub in entries.into_iter().map(|e| e.into_owner(list.pk)) {
|
||||
tx.add_list_owner(sub)?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CreateList {
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
} => {
|
||||
let new = db.create_list(MailingList {
|
||||
pk: 0,
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
topics: vec![],
|
||||
address,
|
||||
archive_url,
|
||||
})?;
|
||||
log::trace!("created new list {:#?}", new);
|
||||
if !opt.quiet {
|
||||
println!(
|
||||
"Created new list {:?} with primary key {}",
|
||||
new.id,
|
||||
new.pk()
|
||||
);
|
||||
}
|
||||
}
|
||||
Post { dry_run } => {
|
||||
if opt.debug {
|
||||
println!("post dry_run{:?}", dry_run);
|
||||
}
|
||||
|
||||
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_to_string(&mut input)?;
|
||||
match Envelope::from_bytes(input.as_bytes(), None) {
|
||||
Ok(env) => {
|
||||
if opt.debug {
|
||||
eprintln!("{:?}", &env);
|
||||
}
|
||||
tx.post(&env, input.as_bytes(), dry_run)?;
|
||||
}
|
||||
Err(err) if input.trim().is_empty() => {
|
||||
eprintln!("Empty input, abort.");
|
||||
return Err(err.into());
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Could not parse message: {}", err);
|
||||
let p = tx.conf().save_message(input)?;
|
||||
eprintln!("Message saved at {}", p.display());
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
}
|
||||
FlushQueue { dry_run } => {
|
||||
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
|
||||
let messages = if opt.debug {
|
||||
println!("flush-queue dry_run {:?}", dry_run);
|
||||
tx.queue(mailpot::queue::Queue::Out)?
|
||||
.into_iter()
|
||||
.map(DbVal::into_inner)
|
||||
.chain(
|
||||
tx.queue(mailpot::queue::Queue::Deferred)?
|
||||
.into_iter()
|
||||
.map(DbVal::into_inner),
|
||||
)
|
||||
.collect()
|
||||
} else {
|
||||
tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?
|
||||
};
|
||||
if opt.verbose > 0 || opt.debug {
|
||||
println!("Queue out has {} messages.", messages.len());
|
||||
}
|
||||
|
||||
let mut failures = Vec::with_capacity(messages.len());
|
||||
|
||||
let send_mail = tx.conf().send_mail.clone();
|
||||
match send_mail {
|
||||
mailpot::SendMail::ShellCommand(cmd) => {
|
||||
fn submit(cmd: &str, msg: &QueueEntry) -> Result<()> {
|
||||
let mut child = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("sh command failed to start")?;
|
||||
let mut stdin = child.stdin.take().context("Failed to open stdin")?;
|
||||
|
||||
let builder = std::thread::Builder::new();
|
||||
|
||||
std::thread::scope(|s| {
|
||||
let handler = builder
|
||||
.spawn_scoped(s, move || {
|
||||
stdin
|
||||
.write_all(&msg.message)
|
||||
.expect("Failed to write to stdin");
|
||||
})
|
||||
.context(
|
||||
"Could not spawn IPC communication thread for SMTP \
|
||||
ShellCommand process",
|
||||
)?;
|
||||
|
||||
handler.join().map_err(|_| {
|
||||
ErrorKind::External(mailpot::anyhow::anyhow!(
|
||||
"Could not join with IPC communication thread for SMTP \
|
||||
ShellCommand process"
|
||||
))
|
||||
})?;
|
||||
Ok::<(), Error>(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
for msg in messages {
|
||||
if let Err(err) = submit(&cmd, &msg) {
|
||||
failures.push((err, msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
mailpot::SendMail::Smtp(_) => {
|
||||
let conn_future = tx.new_smtp_connection()?;
|
||||
failures = smol::future::block_on(smol::spawn(async move {
|
||||
let mut conn = conn_future.await?;
|
||||
for msg in messages {
|
||||
if let Err(err) = Connection::submit(&mut conn, &msg, dry_run).await {
|
||||
failures.push((err, msg));
|
||||
}
|
||||
}
|
||||
Ok::<_, Error>(failures)
|
||||
}))?;
|
||||
}
|
||||
}
|
||||
|
||||
for (err, mut msg) in failures {
|
||||
log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
|
||||
|
||||
msg.queue = mailpot::queue::Queue::Deferred;
|
||||
tx.insert_to_queue(msg)?;
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
}
|
||||
ErrorQueue { cmd } => match cmd {
|
||||
QueueCommand::List => {
|
||||
let errors = db.queue(mailpot::queue::Queue::Error)?;
|
||||
if errors.is_empty() {
|
||||
println!("Error queue is empty.");
|
||||
} else {
|
||||
for e in errors {
|
||||
println!(
|
||||
"- {} {} {} {} {}",
|
||||
e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueCommand::Print { index } => {
|
||||
let mut errors = db.queue(mailpot::queue::Queue::Error)?;
|
||||
if !index.is_empty() {
|
||||
errors.retain(|el| index.contains(&el.pk()));
|
||||
}
|
||||
if errors.is_empty() {
|
||||
println!("Error queue is empty.");
|
||||
} else {
|
||||
for e in errors {
|
||||
println!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueCommand::Delete { index, quiet } => {
|
||||
let mut errors = db.queue(mailpot::queue::Queue::Error)?;
|
||||
if !index.is_empty() {
|
||||
errors.retain(|el| index.contains(&el.pk()));
|
||||
}
|
||||
if errors.is_empty() {
|
||||
if !quiet {
|
||||
println!("Error queue is empty.");
|
||||
}
|
||||
} else {
|
||||
if !quiet {
|
||||
println!("Deleting error queue elements {:?}", &index);
|
||||
}
|
||||
db.delete_from_queue(mailpot::queue::Queue::Error, index)?;
|
||||
if !quiet {
|
||||
for e in errors {
|
||||
println!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Queue { queue, cmd } => match cmd {
|
||||
QueueCommand::List => {
|
||||
let entries = db.queue(queue)?;
|
||||
if entries.is_empty() {
|
||||
println!("Queue {queue} is empty.");
|
||||
} else {
|
||||
for e in entries {
|
||||
println!(
|
||||
"- {} {} {} {} {}",
|
||||
e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueCommand::Print { index } => {
|
||||
let mut entries = db.queue(queue)?;
|
||||
if !index.is_empty() {
|
||||
entries.retain(|el| index.contains(&el.pk()));
|
||||
}
|
||||
if entries.is_empty() {
|
||||
println!("Queue {queue} is empty.");
|
||||
} else {
|
||||
for e in entries {
|
||||
println!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueCommand::Delete { index, quiet } => {
|
||||
let mut entries = db.queue(queue)?;
|
||||
if !index.is_empty() {
|
||||
entries.retain(|el| index.contains(&el.pk()));
|
||||
}
|
||||
if entries.is_empty() {
|
||||
if !quiet {
|
||||
println!("Queue {queue} is empty.");
|
||||
}
|
||||
} else {
|
||||
if !quiet {
|
||||
println!("Deleting queue {queue} elements {:?}", &index);
|
||||
}
|
||||
db.delete_from_queue(queue, index)?;
|
||||
if !quiet {
|
||||
for e in entries {
|
||||
println!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ImportMaildir {
|
||||
list_id,
|
||||
mut maildir_path,
|
||||
} => {
|
||||
let list = match list!(db, list_id) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(format!("No list with id or pk {} was found", list_id).into());
|
||||
}
|
||||
};
|
||||
if !maildir_path.is_absolute() {
|
||||
maildir_path = std::env::current_dir()
|
||||
.expect("could not detect current directory")
|
||||
.join(&maildir_path);
|
||||
}
|
||||
|
||||
fn get_file_hash(file: &std::path::Path) -> EnvelopeHash {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
file.hash(&mut hasher);
|
||||
EnvelopeHash(hasher.finish())
|
||||
}
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let files =
|
||||
melib::backends::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)?;
|
||||
let mut ctr = 0;
|
||||
for file in files {
|
||||
let hash = get_file_hash(&file);
|
||||
let mut reader = std::io::BufReader::new(std::fs::File::open(&file)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
|
||||
env.set_hash(hash);
|
||||
db.insert_post(list.pk, &buf, &env)?;
|
||||
ctr += 1;
|
||||
}
|
||||
}
|
||||
println!("Inserted {} posts to {}.", ctr, list_id);
|
||||
}
|
||||
UpdatePostfixConfig {
|
||||
master_cf,
|
||||
config:
|
||||
PostfixConfig {
|
||||
user,
|
||||
group,
|
||||
binary_path,
|
||||
process_limit,
|
||||
map_output_path,
|
||||
transport_name,
|
||||
},
|
||||
} => {
|
||||
let pfconf = mailpot::postfix::PostfixConfiguration {
|
||||
user: user.into(),
|
||||
group: group.map(Into::into),
|
||||
binary_path,
|
||||
process_limit,
|
||||
map_output_path,
|
||||
transport_name: transport_name.map(std::borrow::Cow::from),
|
||||
};
|
||||
pfconf.save_maps(db.conf())?;
|
||||
pfconf.save_master_cf_entry(db.conf(), config_path, master_cf.as_deref())?;
|
||||
}
|
||||
PrintPostfixConfig {
|
||||
config:
|
||||
PostfixConfig {
|
||||
user,
|
||||
group,
|
||||
binary_path,
|
||||
process_limit,
|
||||
map_output_path,
|
||||
transport_name,
|
||||
},
|
||||
} => {
|
||||
let pfconf = mailpot::postfix::PostfixConfiguration {
|
||||
user: user.into(),
|
||||
group: group.map(Into::into),
|
||||
binary_path,
|
||||
process_limit,
|
||||
map_output_path,
|
||||
transport_name: transport_name.map(std::borrow::Cow::from),
|
||||
};
|
||||
let lists = db.lists()?;
|
||||
let lists_post_policies = lists
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
let pk = l.pk;
|
||||
Ok((l, db.list_post_policy(pk)?))
|
||||
})
|
||||
.collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
|
||||
let maps = pfconf.generate_maps(&lists_post_policies);
|
||||
let mastercf = pfconf.generate_master_cf_entry(db.conf(), config_path);
|
||||
|
||||
println!("{maps}\n\n{mastercf}\n");
|
||||
}
|
||||
Accounts => {
|
||||
let accounts = db.accounts()?;
|
||||
if accounts.is_empty() {
|
||||
println!("No accounts found.");
|
||||
} else {
|
||||
for a in accounts {
|
||||
println!("- {:?}", a);
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountInfo { address } => {
|
||||
if let Some(acc) = db.account_by_address(&address)? {
|
||||
let subs = db.account_subscriptions(acc.pk())?;
|
||||
if subs.is_empty() {
|
||||
println!("No subscriptions found.");
|
||||
} else {
|
||||
for s in subs {
|
||||
let list = db
|
||||
.list(s.list)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Found subscription with list_pk = {} but no such list \
|
||||
exists.\nListSubscription = {:?}\n\n{err}",
|
||||
s.list, s
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Found subscription with list_pk = {} but no such list \
|
||||
exists.\nListSubscription = {:?}",
|
||||
s.list, s
|
||||
)
|
||||
});
|
||||
println!("- {:?} {}", s, list);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("account with this address not found!");
|
||||
};
|
||||
}
|
||||
AddAccount {
|
||||
address,
|
||||
password,
|
||||
name,
|
||||
public_key,
|
||||
enabled,
|
||||
} => {
|
||||
db.add_account(Account {
|
||||
pk: 0,
|
||||
name,
|
||||
address,
|
||||
public_key,
|
||||
password,
|
||||
enabled: enabled.unwrap_or(true),
|
||||
})?;
|
||||
}
|
||||
RemoveAccount { address } => {
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
println!(
|
||||
"Are you sure you want to remove account with address {}? [Yy/n]",
|
||||
address
|
||||
);
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
|
||||
break;
|
||||
} else if input.trim() == "n" {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_account(&address)?;
|
||||
}
|
||||
UpdateAccount {
|
||||
address,
|
||||
password,
|
||||
name,
|
||||
public_key,
|
||||
enabled,
|
||||
} => {
|
||||
let changeset = AccountChangeset {
|
||||
address,
|
||||
name,
|
||||
public_key,
|
||||
password,
|
||||
enabled,
|
||||
};
|
||||
db.update_account(changeset)?;
|
||||
}
|
||||
Repair {
|
||||
fix,
|
||||
all,
|
||||
mut datetime_header_value,
|
||||
mut remove_empty_accounts,
|
||||
mut remove_accepted_subscription_requests,
|
||||
mut warn_list_no_owner,
|
||||
} => {
|
||||
type LintFn =
|
||||
fn(&'_ mut mailpot::Connection, bool) -> std::result::Result<(), mailpot::Error>;
|
||||
let dry_run = !fix;
|
||||
if all {
|
||||
datetime_header_value = true;
|
||||
remove_empty_accounts = true;
|
||||
remove_accepted_subscription_requests = true;
|
||||
warn_list_no_owner = true;
|
||||
}
|
||||
|
||||
if !(datetime_header_value
|
||||
| remove_empty_accounts
|
||||
| remove_accepted_subscription_requests
|
||||
| warn_list_no_owner)
|
||||
{
|
||||
return Err(
|
||||
"No lints selected: specify them with flag arguments. See --help".into(),
|
||||
);
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
println!("running without making modifications (dry run)");
|
||||
}
|
||||
|
||||
for (flag, lint_fn) in [
|
||||
(datetime_header_value, datetime_header_value_lint as LintFn),
|
||||
(remove_empty_accounts, remove_empty_accounts_lint as _),
|
||||
(
|
||||
remove_accepted_subscription_requests,
|
||||
remove_accepted_subscription_requests_lint as _,
|
||||
),
|
||||
(warn_list_no_owner, warn_list_no_owner_lint as _),
|
||||
] {
|
||||
if flag {
|
||||
lint_fn(&mut db, dry_run)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> std::result::Result<(), i32> {
|
||||
let opt = Opt::parse();
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.module("mailpot")
|
||||
.quiet(opt.quiet)
|
||||
.verbosity(opt.verbose as usize)
|
||||
.timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off))
|
||||
.init()
|
||||
.unwrap();
|
||||
if let Err(err) = run_app(opt) {
|
||||
print!("{}", err.display_chain());
|
||||
std::process::exit(-1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -104,32 +104,6 @@ For more information, try '--help'."#,
|
|||
|
||||
let config_str = config.to_toml();
|
||||
|
||||
fn config_not_exists(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list-lists")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(255).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq(
|
||||
format!(
|
||||
"[1] Could not read configuration file from path: {path} Caused by:\n[2] \
|
||||
Configuration file {path} not found. Caused by:\n[3] Error returned from \
|
||||
internal I/O operation: No such file or directory (os error 2)",
|
||||
path = conf.display()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
|
||||
config_not_exists(&conf_path);
|
||||
|
||||
std::fs::write(&conf_path, config_str.as_bytes()).unwrap();
|
||||
|
||||
fn list_lists(conf: &Path, eq: &str) {
|
||||
|
@ -204,65 +178,4 @@ For more information, try '--help'."#,
|
|||
\"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
|
||||
2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
|
||||
);
|
||||
|
||||
fn add_list_owner(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list")
|
||||
.arg("twobar-chat")
|
||||
.arg("add-list-owner")
|
||||
.arg("--address")
|
||||
.arg("list-owner@example.com")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Added new list owner [#1 2] list-owner@example.com")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
add_list_owner(&conf_path);
|
||||
list_lists(
|
||||
&conf_path,
|
||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
||||
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||
owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
|
||||
DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
|
||||
\"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
|
||||
2)\n\tList owners:\n\t- [#1 2] list-owner@example.com\n\tPost policy: \
|
||||
None\n\tSubscription policy: None",
|
||||
);
|
||||
|
||||
fn remove_list_owner(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list")
|
||||
.arg("twobar-chat")
|
||||
.arg("remove-list-owner")
|
||||
.arg("--pk")
|
||||
.arg("1")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Removed list owner with pk = 1")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
remove_list_owner(&conf_path);
|
||||
list_lists(
|
||||
&conf_path,
|
||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
||||
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||
owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
|
||||
DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
|
||||
\"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
|
||||
2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
|
||||
);
|
||||
}
|
|
@ -109,23 +109,11 @@ fn test_out_queue_flush() {
|
|||
assert!(env.subject().starts_with(&format!("[{}] ", foo_chat.id)));
|
||||
let headers = env.other_headers();
|
||||
|
||||
assert_eq!(headers.get("List-Id"), Some(&foo_chat.id_header()));
|
||||
assert_eq!(headers.get("List-Help"), foo_chat.help_header().as_ref());
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_ID)
|
||||
.map(|header| header.to_string()),
|
||||
Some(foo_chat.id_header())
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_HELP)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.help_header()
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_POST)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.post_header(Some(&post_policy))
|
||||
headers.get("List-Post"),
|
||||
foo_chat.post_header(Some(&post_policy)).as_ref()
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -318,21 +306,11 @@ fn test_list_requests_submission() {
|
|||
let headers_fn = |env: &melib::Envelope| {
|
||||
let headers = env.other_headers();
|
||||
|
||||
assert_eq!(headers.get("List-Id"), Some(&foo_chat.id_header()));
|
||||
assert_eq!(headers.get("List-Help"), foo_chat.help_header().as_ref());
|
||||
assert_eq!(
|
||||
headers.get(melib::HeaderName::LIST_ID),
|
||||
Some(foo_chat.id_header().as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_HELP)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.help_header()
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_POST)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.post_header(Some(&post_policy))
|
||||
headers.get("List-Post"),
|
||||
foo_chat.post_header(Some(&post_policy)).as_ref()
|
||||
);
|
||||
};
|
||||
|
|
@ -18,10 +18,10 @@ anyhow = "1.0.58"
|
|||
chrono = { version = "^0.4", features = ["serde", ] }
|
||||
jsonschema = { version = "0.17", default-features = false }
|
||||
log = "0.4"
|
||||
melib = { default-features = false, features = ["mbox", "smtp", "unicode-algorithms", "maildir"], git = "https://git.meli-email.org/meli/meli.git", rev = "64e60cb" }
|
||||
melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms", "maildir_backend"], git = "https://github.com/meli/meli", rev = "2447a2c" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ] }
|
||||
percent-encoding = { version = "^2.1" }
|
||||
rusqlite = { version = "^0.30", features = ["bundled", "functions", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] }
|
||||
rusqlite = { version = "^0.28", features = ["bundled", "functions", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
thiserror = { version = "1.0.48", default-features = false }
|
||||
|
@ -31,5 +31,5 @@ xdg = "2.4.1"
|
|||
[dev-dependencies]
|
||||
mailpot-tests = { version = "^0.1", path = "../mailpot-tests" }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "blocking"] }
|
||||
stderrlog = { version = "^0.6" }
|
||||
tempfile = { version = "3.9" }
|
||||
stderrlog = "^0.5"
|
||||
tempfile = "3.3"
|
|
@ -62,7 +62,7 @@ fn main() {
|
|||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let user_version: i32 = make_migrations("migrations", MIGRATION_RS, &mut output.stdout);
|
||||
make_migrations("migrations", MIGRATION_RS, &mut output.stdout);
|
||||
let mut verify = Command::new(std::env::var("SQLITE_BIN").unwrap_or("sqlite3".into()))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -87,9 +87,4 @@ fn main() {
|
|||
}
|
||||
let mut file = std::fs::File::create("./src/schema.sql").unwrap();
|
||||
file.write_all(&output.stdout).unwrap();
|
||||
file.write_all(
|
||||
&format!("\n\n-- Set current schema version.\n\nPRAGMA user_version = {user_version};\n")
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
|
@ -25,13 +25,11 @@ use std::{fs::read_dir, io::Write, path::Path};
|
|||
///
|
||||
/// If a migration is a data migration (not a CREATE, DROP or ALTER statement) it is appended to
|
||||
/// the schema file.
|
||||
///
|
||||
/// Returns the current `user_version` PRAGMA value.
|
||||
pub fn make_migrations<M: AsRef<Path>, O: AsRef<Path>>(
|
||||
migrations_path: M,
|
||||
output_file: O,
|
||||
schema_file: &mut Vec<u8>,
|
||||
) -> i32 {
|
||||
) {
|
||||
let migrations_folder_path = migrations_path.as_ref();
|
||||
let output_file_path = output_file.as_ref();
|
||||
|
||||
|
@ -106,5 +104,4 @@ pub fn make_migrations<M: AsRef<Path>, O: AsRef<Path>>(
|
|||
}
|
||||
migr_rs.write_all(b"]").unwrap();
|
||||
migr_rs.flush().unwrap();
|
||||
paths.len() as i32
|
||||
}
|
|
@ -76,18 +76,12 @@ impl Configuration {
|
|||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let mut s = String::new();
|
||||
let mut file = std::fs::File::open(path)
|
||||
.with_context(|| format!("Configuration file {} not found.", path.display()))?;
|
||||
file.read_to_string(&mut s)
|
||||
.with_context(|| format!("Could not read from file {}.", path.display()))?;
|
||||
let config: Self = toml::from_str(&s)
|
||||
.map_err(anyhow::Error::from)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not parse configuration file `{}` successfully: ",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
file.read_to_string(&mut s)?;
|
||||
let config: Self = toml::from_str(&s).context(format!(
|
||||
"Could not parse configuration file `{}` succesfully: ",
|
||||
path.display()
|
||||
))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
@ -110,20 +104,14 @@ impl Configuration {
|
|||
}
|
||||
|
||||
debug_assert!(path != self.db_path());
|
||||
let mut file = std::fs::File::create(&path)
|
||||
.with_context(|| format!("Could not create file {}.", path.display()))?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.with_context(|| format!("Could not fstat file {}.", path.display()))?;
|
||||
let mut file = std::fs::File::create(&path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)
|
||||
.with_context(|| format!("Could not chmod 600 file {}.", path.display()))?;
|
||||
file.write_all(msg.as_bytes())
|
||||
.with_context(|| format!("Could not write message to file {}.", path.display()))?;
|
||||
file.flush()
|
||||
.with_context(|| format!("Could not flush message I/O to file {}.", path.display()))?;
|
||||
file.set_permissions(permissions)?;
|
||||
file.write_all(msg.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
@ -139,29 +127,3 @@ impl Configuration {
|
|||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_parse_error() {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let conf_path = tmp_dir.path().join("conf.toml");
|
||||
std::fs::write(&conf_path, b"afjsad skas as a as\n\n\n\n\t\x11\n").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Configuration::from_file(&conf_path)
|
||||
.unwrap_err()
|
||||
.display_chain()
|
||||
.to_string(),
|
||||
format!(
|
||||
"[1] Could not parse configuration file `{}` successfully: Caused by:\n[2] \
|
||||
Error: expected an equals, found an identifier at line 1 column 8\n",
|
||||
conf_path.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -32,7 +32,6 @@ use crate::{
|
|||
config::Configuration,
|
||||
errors::{ErrorKind::*, *},
|
||||
models::{changesets::MailingListChangeset, DbVal, ListOwner, MailingList, Post},
|
||||
StripCarets,
|
||||
};
|
||||
|
||||
/// A connection to a `mailpot` database.
|
||||
|
@ -129,7 +128,7 @@ impl Connection {
|
|||
/// ```sql
|
||||
#[doc = include_str!("./schema.sql")]
|
||||
/// ```
|
||||
pub const SCHEMA: &'static str = include_str!("./schema.sql");
|
||||
pub const SCHEMA: &str = include_str!("./schema.sql");
|
||||
|
||||
/// Database migrations.
|
||||
pub const MIGRATIONS: &'static [(u32, &'static str, &'static str)] =
|
||||
|
@ -188,9 +187,7 @@ impl Connection {
|
|||
INIT_SQLITE_LOGGING.call_once(|| {
|
||||
_ = unsafe { rusqlite::trace::config_log(Some(log_callback)) };
|
||||
});
|
||||
let conn = DbConnection::open(conf.db_path.to_str().unwrap()).with_context(|| {
|
||||
format!("sqlite3 library could not open {}.", conf.db_path.display())
|
||||
})?;
|
||||
let conn = DbConnection::open(conf.db_path.to_str().unwrap())?;
|
||||
rusqlite::vtab::array::load_module(&conn)?;
|
||||
conn.pragma_update(None, "journal_mode", "WAL")?;
|
||||
conn.pragma_update(None, "foreign_keys", "on")?;
|
||||
|
@ -349,14 +346,7 @@ impl Connection {
|
|||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not launch {} {}.",
|
||||
std::env::var("SQLITE_BIN").unwrap_or_else(|_| "sqlite3".into()),
|
||||
db_path.display()
|
||||
)
|
||||
})?;
|
||||
.spawn()?;
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
std::thread::spawn(move || {
|
||||
stdin
|
||||
|
@ -391,16 +381,12 @@ impl Connection {
|
|||
.into());
|
||||
}
|
||||
|
||||
let file = std::fs::File::open(db_path)
|
||||
.with_context(|| format!("Could not open database {}.", db_path.display()))?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.with_context(|| format!("Could not fstat database {}.", db_path.display()))?;
|
||||
let file = std::fs::File::open(db_path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)
|
||||
.with_context(|| format!("Could not chmod 600 database {}.", db_path.display()))?;
|
||||
file.set_permissions(permissions)?;
|
||||
}
|
||||
Self::open_db(conf)
|
||||
}
|
||||
|
@ -593,164 +579,6 @@ impl Connection {
|
|||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch the contents of a single thread in the form of `(depth, post)`
|
||||
/// where `depth` is the reply distance between a message and the thread
|
||||
/// root message.
|
||||
pub fn list_thread(&self, list_pk: i64, root: &str) -> Result<Vec<(i64, DbVal<Post>)>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare(
|
||||
"WITH RECURSIVE cte_replies AS MATERIALIZED
|
||||
(
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
REPLACE(
|
||||
TRIM(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
)
|
||||
+
|
||||
LENGTH('in-reply-to: '),
|
||||
INSTR(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: ')
|
||||
+
|
||||
LENGTH('in-reply-to: ')
|
||||
),
|
||||
'>'
|
||||
)
|
||||
)
|
||||
),
|
||||
' ',
|
||||
''
|
||||
) AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset > 0
|
||||
UNION
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
NULL AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset = 0
|
||||
),
|
||||
cte_thread(parent, root, depth) AS (
|
||||
SELECT DISTINCT
|
||||
message_id AS parent,
|
||||
message_id AS root,
|
||||
0 AS depth
|
||||
FROM cte_replies
|
||||
WHERE
|
||||
in_reply_to IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
t.message_id AS parent,
|
||||
cte_thread.root AS root,
|
||||
(cte_thread.depth + 1) AS depth
|
||||
FROM cte_replies
|
||||
AS t
|
||||
JOIN
|
||||
cte_thread
|
||||
ON cte_thread.parent = t.in_reply_to
|
||||
WHERE t.in_reply_to IS NOT NULL
|
||||
)
|
||||
SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
|
||||
)
|
||||
.unwrap();
|
||||
let iter = stmt.query_map(rusqlite::params![root], |row| {
|
||||
let parent: String = row.get("parent")?;
|
||||
let root: String = row.get("root")?;
|
||||
let depth: i64 = row.get("depth")?;
|
||||
Ok((parent, root, depth))
|
||||
})?;
|
||||
let mut ret = vec![];
|
||||
for post in iter {
|
||||
ret.push(post?);
|
||||
}
|
||||
let posts = self.list_posts(list_pk, None)?;
|
||||
let ret = ret
|
||||
.into_iter()
|
||||
.filter_map(|(m, _, depth)| {
|
||||
posts
|
||||
.iter()
|
||||
.find(|p| m.as_str().strip_carets() == p.message_id.as_str().strip_carets())
|
||||
.map(|p| (depth, p.clone()))
|
||||
})
|
||||
.skip(1)
|
||||
.collect();
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Export a list, message, or thread in mbox format
|
||||
pub fn export_mbox(
|
||||
&self,
|
||||
pk: i64,
|
||||
message_id: Option<&str>,
|
||||
as_thread: bool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let posts: Result<Vec<DbVal<Post>>> = {
|
||||
if let Some(message_id) = message_id {
|
||||
if as_thread {
|
||||
// export a thread
|
||||
let thread = self.list_thread(pk, message_id)?;
|
||||
Ok(thread.iter().map(|item| item.1.clone()).collect())
|
||||
} else {
|
||||
// export a single message
|
||||
let message =
|
||||
self.list_post_by_message_id(pk, message_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::from(format!("no message with id: {}", message_id))
|
||||
})?;
|
||||
Ok(vec![message])
|
||||
}
|
||||
} else {
|
||||
// export the entire mailing list
|
||||
let posts = self.list_posts(pk, None)?;
|
||||
Ok(posts)
|
||||
}
|
||||
};
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mailbox = melib::mbox::MboxFormat::default();
|
||||
for post in posts? {
|
||||
let envelope_from = if let Some(address) = post.0.envelope_from {
|
||||
let address = melib::Address::try_from(address.as_str())?;
|
||||
Some(address)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let envelope = melib::Envelope::from_bytes(&post.0.message, None)?;
|
||||
mailbox.append(
|
||||
&mut buf,
|
||||
&post.0.message.to_vec(),
|
||||
envelope_from.as_ref(),
|
||||
Some(envelope.timestamp),
|
||||
(melib::Flag::PASSED, vec![]),
|
||||
melib::mbox::MboxMetadata::None,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
buf.flush()?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Fetch the owners of a mailing list.
|
||||
pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
|
||||
let mut stmt = self
|
||||
|
@ -789,7 +617,7 @@ SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
|
|||
if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
|
||||
Error::from(err).chain_err(|| NotFound("list or list owner not found!"))
|
||||
} else {
|
||||
Error::from(err)
|
||||
err.into()
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
|
@ -1315,67 +1143,4 @@ mod tests {
|
|||
tx.commit().unwrap();
|
||||
assert_eq!(&db.lists().unwrap(), &[new, new2, new3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mbox_export() {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::SendMail;
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let data_path = tmp_dir.path().to_path_buf();
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path,
|
||||
data_path,
|
||||
administrators: vec![],
|
||||
};
|
||||
let list = MailingList {
|
||||
pk: 0,
|
||||
name: "test".into(),
|
||||
id: "test".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
address: "test@example.com".into(),
|
||||
archive_url: None,
|
||||
};
|
||||
|
||||
let test_emails = vec![
|
||||
r#"From: "User Name" <user@example.com>
|
||||
To: "test" <test@example.com>
|
||||
Subject: Hello World
|
||||
|
||||
Hello, this is a message.
|
||||
|
||||
Goodbye!
|
||||
|
||||
"#,
|
||||
r#"From: "User Name" <user@example.com>
|
||||
To: "test" <test@example.com>
|
||||
Subject: Fuu Bar
|
||||
|
||||
Baz,
|
||||
|
||||
Qux!
|
||||
|
||||
"#,
|
||||
];
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
db.create_list(list).unwrap();
|
||||
for email in test_emails {
|
||||
let envelope = melib::Envelope::from_bytes(email.as_bytes(), None).unwrap();
|
||||
db.post(&envelope, email.as_bytes(), false).unwrap();
|
||||
}
|
||||
let mbox = String::from_utf8(db.export_mbox(1, None, false).unwrap()).unwrap();
|
||||
assert!(
|
||||
mbox.split('\n').fold(0, |accm, line| {
|
||||
if line.starts_with("From MAILER-DAEMON") {
|
||||
accm + 1
|
||||
} else {
|
||||
accm
|
||||
}
|
||||
}) == 2
|
||||
)
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ use std::sync::Arc;
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::anyhow::Context;
|
||||
|
||||
/// Mailpot library error.
|
||||
#[derive(Error, Debug)]
|
||||
pub struct Error {
|
||||
|
@ -51,36 +53,39 @@ pub enum ErrorKind {
|
|||
|
||||
/// Error returned from an external user initiated operation such as
|
||||
/// deserialization or I/O.
|
||||
#[error("Error: {0}")]
|
||||
#[error(
|
||||
"Error returned from an external user initiated operation such as deserialization or I/O. \
|
||||
{0}"
|
||||
)]
|
||||
External(#[from] anyhow::Error),
|
||||
/// Generic
|
||||
#[error("{0}")]
|
||||
Generic(anyhow::Error),
|
||||
/// Error returned from sqlite3.
|
||||
#[error("Error returned from sqlite3: {0}.")]
|
||||
#[error("Error returned from sqlite3 {0}.")]
|
||||
Sql(
|
||||
#[from]
|
||||
#[source]
|
||||
rusqlite::Error,
|
||||
),
|
||||
/// Error returned from sqlite3.
|
||||
#[error("Error returned from sqlite3: {0}")]
|
||||
#[error("Error returned from sqlite3. {0}")]
|
||||
SqlLib(
|
||||
#[from]
|
||||
#[source]
|
||||
rusqlite::ffi::Error,
|
||||
),
|
||||
/// Error returned from internal I/O operations.
|
||||
#[error("Error returned from internal I/O operation: {0}")]
|
||||
#[error("Error returned from internal I/O operations. {0}")]
|
||||
Io(#[from] ::std::io::Error),
|
||||
/// Error returned from e-mail protocol operations from `melib` crate.
|
||||
#[error("Error returned from e-mail protocol operations from `melib` crate: {0}")]
|
||||
#[error("Error returned from e-mail protocol operations from `melib` crate. {0}")]
|
||||
Melib(#[from] melib::error::Error),
|
||||
/// Error from deserializing JSON values.
|
||||
#[error("Error from deserializing JSON values: {0}")]
|
||||
#[error("Error from deserializing JSON values. {0}")]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
/// Error returned from minijinja template engine.
|
||||
#[error("Error returned from minijinja template engine: {0}")]
|
||||
#[error("Error returned from minijinja template engine. {0}")]
|
||||
Template(#[from] minijinja::Error),
|
||||
}
|
||||
|
||||
|
@ -183,7 +188,7 @@ struct ErrorChainDisplay<'e> {
|
|||
impl std::fmt::Display for ErrorChainDisplay<'_> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if let Some(ref source) = self.current.source {
|
||||
writeln!(fmt, "[{}] {} Caused by:", self.counter, self.current.kind)?;
|
||||
writeln!(fmt, "[{}] {}, caused by:", self.counter, self.current.kind)?;
|
||||
Self {
|
||||
current: source,
|
||||
counter: self.counter + 1,
|
||||
|
@ -195,38 +200,3 @@ impl std::fmt::Display for ErrorChainDisplay<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// adfsa
|
||||
pub trait Context<T> {
|
||||
/// Wrap the error value with additional context.
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Into<Error>;
|
||||
|
||||
/// Wrap the error value with additional context that is evaluated lazily
|
||||
/// only once an error does occur.
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Into<Error>,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl<T, E> Context<T> for std::result::Result<T, E>
|
||||
where
|
||||
Error: From<E>,
|
||||
{
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Into<Error>,
|
||||
{
|
||||
self.map_err(|err| Error::from(err).chain_err(|| context.into()))
|
||||
}
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Into<Error>,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|err| Error::from(err).chain_err(|| f().into()))
|
||||
}
|
||||
}
|
|
@ -221,33 +221,6 @@ impl StripCarets for &str {
|
|||
}
|
||||
}
|
||||
|
||||
/// Trait for stripping carets ('<','>') from Message IDs inplace.
|
||||
pub trait StripCaretsInplace {
|
||||
/// If `self` is surrounded by carets, strip them.
|
||||
fn strip_carets_inplace(self) -> Self;
|
||||
}
|
||||
|
||||
impl StripCaretsInplace for &str {
|
||||
fn strip_carets_inplace(self) -> Self {
|
||||
let mut self_ref = self.trim();
|
||||
if self_ref.starts_with('<') && self_ref.ends_with('>') {
|
||||
self_ref = &self_ref[1..self_ref.len().saturating_sub(1)];
|
||||
}
|
||||
self_ref
|
||||
}
|
||||
}
|
||||
|
||||
impl StripCaretsInplace for String {
|
||||
fn strip_carets_inplace(mut self) -> Self {
|
||||
if self.starts_with('<') && self.ends_with('>') {
|
||||
self.drain(0..1);
|
||||
let len = self.len();
|
||||
self.drain(len.saturating_sub(1)..len);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
use percent_encoding::CONTROLS;
|
||||
pub use percent_encoding::{utf8_percent_encode, AsciiSet};
|
||||
|
|
@ -40,8 +40,9 @@
|
|||
|
||||
mod settings;
|
||||
use log::trace;
|
||||
use melib::{Address, HeaderName};
|
||||
use melib::Address;
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
pub use settings::*;
|
||||
|
||||
use crate::{
|
||||
mail::{ListContext, MailJob, PostAction, PostEntry},
|
||||
|
@ -167,7 +168,7 @@ impl PostFilter for AddListHeaders {
|
|||
trace!("Running AddListHeaders filter");
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
let sender = format!("<{}>", ctx.list.address);
|
||||
headers.push((HeaderName::SENDER, sender.as_bytes()));
|
||||
headers.push((&b"Sender"[..], sender.as_bytes()));
|
||||
|
||||
let list_id = Some(ctx.list.id_header());
|
||||
let list_help = ctx.list.help_header();
|
||||
|
@ -181,12 +182,12 @@ impl PostFilter for AddListHeaders {
|
|||
let list_archive = ctx.list.archive_header();
|
||||
|
||||
for (hdr, val) in [
|
||||
(HeaderName::LIST_ID, &list_id),
|
||||
(HeaderName::LIST_HELP, &list_help),
|
||||
(HeaderName::LIST_POST, &list_post),
|
||||
(HeaderName::LIST_UNSUBSCRIBE, &list_unsubscribe),
|
||||
(HeaderName::LIST_SUBSCRIBE, &list_subscribe),
|
||||
(HeaderName::LIST_ARCHIVE, &list_archive),
|
||||
(b"List-Id".as_slice(), &list_id),
|
||||
(b"List-Help".as_slice(), &list_help),
|
||||
(b"List-Post".as_slice(), &list_post),
|
||||
(b"List-Unsubscribe".as_slice(), &list_unsubscribe),
|
||||
(b"List-Subscribe".as_slice(), &list_subscribe),
|
||||
(b"List-Archive".as_slice(), &list_archive),
|
||||
] {
|
||||
if let Some(val) = val {
|
||||
headers.push((hdr, val.as_bytes()));
|
||||
|
@ -196,13 +197,13 @@ impl PostFilter for AddListHeaders {
|
|||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h.as_str().as_bytes());
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
|
@ -238,25 +239,28 @@ impl PostFilter for AddSubjectTagPrefix {
|
|||
trace!("Running AddSubjectTagPrefix filter");
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
let mut subject;
|
||||
if let Some((_, subj_val)) = headers.iter_mut().find(|(k, _)| k == HeaderName::SUBJECT) {
|
||||
if let Some((_, subj_val)) = headers
|
||||
.iter_mut()
|
||||
.find(|(k, _)| k.eq_ignore_ascii_case(b"Subject"))
|
||||
{
|
||||
subject = format!("[{}] ", ctx.list.id).into_bytes();
|
||||
subject.extend(subj_val.iter().cloned());
|
||||
*subj_val = subject.as_slice();
|
||||
} else {
|
||||
subject = format!("[{}] (no subject)", ctx.list.id).into_bytes();
|
||||
headers.push((HeaderName::SUBJECT, subject.as_slice()));
|
||||
headers.push((&b"Subject"[..], subject.as_slice()));
|
||||
}
|
||||
|
||||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h.as_str().as_bytes());
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
|
@ -311,18 +315,18 @@ impl PostFilter for ArchivedAtLink {
|
|||
log::error!("ArchivedAtLink: {}", err);
|
||||
})?;
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
headers.push((HeaderName::ARCHIVED_AT, header_val.as_bytes()));
|
||||
headers.push((&b"Archived-At"[..], header_val.as_bytes()));
|
||||
|
||||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h.as_str().as_bytes());
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
|
@ -320,7 +320,7 @@ impl MailingList {
|
|||
Address::new(Some(self.name.clone()), self.address.clone())
|
||||
}
|
||||
|
||||
/// List unsubscribe action as a [`MailtoAddress`].
|
||||
/// List unsubscribe action as a [`MailtoAddress`](super::MailtoAddress).
|
||||
pub fn unsubscription_mailto(&self) -> MailtoAddress {
|
||||
MailtoAddress {
|
||||
address: self.request_subaddr(),
|
||||
|
@ -328,7 +328,7 @@ impl MailingList {
|
|||
}
|
||||
}
|
||||
|
||||
/// List subscribe action as a [`MailtoAddress`].
|
||||
/// List subscribe action as a [`MailtoAddress`](super::MailtoAddress).
|
||||
pub fn subscription_mailto(&self) -> MailtoAddress {
|
||||
MailtoAddress {
|
||||
address: self.request_subaddr(),
|
||||
|
@ -336,7 +336,7 @@ impl MailingList {
|
|||
}
|
||||
}
|
||||
|
||||
/// List owner as a [`MailtoAddress`].
|
||||
/// List owner as a [`MailtoAddress`](super::MailtoAddress).
|
||||
pub fn owner_mailto(&self) -> MailtoAddress {
|
||||
let p = self.address.split('@').collect::<Vec<&str>>();
|
||||
MailtoAddress {
|
||||
|
@ -371,7 +371,7 @@ impl MailingList {
|
|||
if let Some(val) = val {
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::try_from(hdr).unwrap(), val);
|
||||
.insert(melib::HeaderName::new_unchecked(hdr), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -728,13 +728,6 @@ pub struct ListCandidateSubscription {
|
|||
pub accepted: Option<i64>,
|
||||
}
|
||||
|
||||
impl ListCandidateSubscription {
|
||||
/// Subscription request address as a [`melib::Address`]
|
||||
pub fn address(&self) -> Address {
|
||||
Address::new(self.name.clone(), self.address.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListCandidateSubscription {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
|
@ -19,6 +19,9 @@
|
|||
|
||||
//! How each list handles new posts and new subscriptions.
|
||||
|
||||
pub use post_policy::*;
|
||||
pub use subscription_policy::*;
|
||||
|
||||
mod post_policy {
|
||||
use log::trace;
|
||||
use rusqlite::OptionalExtension;
|
|
@ -67,7 +67,7 @@ pub struct PostfixConfiguration {
|
|||
#[serde(default)]
|
||||
pub process_limit: Option<u64>,
|
||||
/// The directory in which the map files are saved.
|
||||
/// Default is `data_path` from [`Configuration`].
|
||||
/// Default is `data_path` from [`Configuration`](crate::Configuration).
|
||||
#[serde(default)]
|
||||
pub map_output_path: Option<PathBuf>,
|
||||
/// The name of the Postfix service name to use.
|
||||
|
@ -412,7 +412,6 @@ fn test_postfix_generation() -> Result<()> {
|
|||
let mut conf = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&config_path)?;
|
||||
conf.write_all(config.to_toml().as_bytes())?;
|
||||
conf.flush()?;
|
||||
|
@ -576,11 +575,7 @@ mailman unix - n n - - pipe
|
|||
|
||||
let path = tmp_dir.path().join("master.cf");
|
||||
{
|
||||
let mut mastercf = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
let mut mastercf = OpenOptions::new().write(true).create(true).open(&path)?;
|
||||
mastercf.write_all(master_edit_value.as_bytes())?;
|
||||
mastercf.flush()?;
|
||||
}
|
|
@ -43,12 +43,12 @@ impl Connection {
|
|||
} else {
|
||||
from_[0].get_email()
|
||||
};
|
||||
let datetime: std::borrow::Cow<'_, str> = if !env.date.is_empty() {
|
||||
let datetime: std::borrow::Cow<'_, str> = if !env.date.as_str().is_empty() {
|
||||
env.date.as_str().into()
|
||||
} else {
|
||||
melib::utils::datetime::timestamp_to_string(
|
||||
melib::datetime::timestamp_to_string(
|
||||
env.timestamp,
|
||||
Some(melib::utils::datetime::formats::RFC822_DATE),
|
||||
Some(melib::datetime::RFC822_DATE),
|
||||
true,
|
||||
)
|
||||
.into()
|
||||
|
@ -129,6 +129,9 @@ impl Connection {
|
|||
return Err("Envelope From: field is empty!".into());
|
||||
}
|
||||
let mut lists = self.lists()?;
|
||||
if lists.is_empty() {
|
||||
return Err("No active mailing lists found.".into());
|
||||
}
|
||||
let prev_list_len = lists.len();
|
||||
for t in &tos {
|
||||
if let Some((addr, subaddr)) = t.subaddress("+") {
|
||||
|
@ -302,6 +305,7 @@ impl Connection {
|
|||
env: &Envelope,
|
||||
raw: &[u8],
|
||||
) -> Result<()> {
|
||||
let post_policy = self.list_post_policy(list.pk)?;
|
||||
match request {
|
||||
ListRequest::Help => {
|
||||
trace!(
|
||||
|
@ -310,7 +314,6 @@ impl Connection {
|
|||
list
|
||||
);
|
||||
let subscription_policy = self.list_subscription_policy(list.pk)?;
|
||||
let post_policy = self.list_post_policy(list.pk)?;
|
||||
let subject = format!("Help for {}", list.name);
|
||||
let details = list
|
||||
.generate_help_email(post_policy.as_deref(), subscription_policy.as_deref());
|
||||
|
@ -338,10 +341,9 @@ impl Connection {
|
|||
env.from(),
|
||||
list
|
||||
);
|
||||
let subscription_policy = self.list_subscription_policy(list.pk)?;
|
||||
let approval_needed = subscription_policy
|
||||
let approval_needed = post_policy
|
||||
.as_ref()
|
||||
.map(|p| !p.open)
|
||||
.map(|p| p.approval_needed)
|
||||
.unwrap_or(false);
|
||||
for f in env.from() {
|
||||
let email_from = f.get_email();
|
||||
|
@ -488,7 +490,23 @@ impl Connection {
|
|||
list_owners.iter().map(|owner| Cow::Owned(owner.address())),
|
||||
)?;
|
||||
} else {
|
||||
self.send_subscription_confirmation(list, f)?;
|
||||
log::trace!(
|
||||
"Added subscription to list {list:?} for address {f:?}, sending \
|
||||
confirmation."
|
||||
);
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::SUBSCRIPTION_CONFIRMATION,
|
||||
default_fn: Some(Template::default_subscription_confirmation),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::SUBSCRIPTION_CONFIRMATION.into(),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -535,7 +553,19 @@ impl Connection {
|
|||
list_owners.iter().map(|owner| Cow::Owned(owner.address())),
|
||||
)?;
|
||||
} else {
|
||||
self.send_unsubscription_confirmation(list, f)?;
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::UNSUBSCRIPTION_CONFIRMATION,
|
||||
default_fn: Some(Template::default_unsubscription_confirmation),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::UNSUBSCRIPTION_CONFIRMATION.into(),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -658,7 +688,7 @@ impl Connection {
|
|||
) -> Result<Option<DbVal<Post>>> {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \
|
||||
FROM post WHERE list = ?1 AND (message_id = ?2 OR concat('<', ?2, '>') = message_id);",
|
||||
FROM post WHERE list = ? AND message_id = ?;",
|
||||
)?;
|
||||
let ret = stmt
|
||||
.query_row(rusqlite::params![&list_pk, &message_id], |row| {
|
||||
|
@ -710,14 +740,15 @@ impl Connection {
|
|||
})?;
|
||||
|
||||
let mut draft = templ.render(context)?;
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::FROM, list.request_subaddr());
|
||||
draft.headers.insert(
|
||||
melib::HeaderName::new_unchecked("From"),
|
||||
list.request_subaddr(),
|
||||
);
|
||||
for addr in recipients {
|
||||
let mut draft = draft.clone();
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::TO, addr.to_string());
|
||||
.insert(melib::HeaderName::new_unchecked("To"), addr.to_string());
|
||||
list.insert_headers(
|
||||
&mut draft,
|
||||
post_policy.as_deref(),
|
||||
|
@ -733,54 +764,6 @@ impl Connection {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send subscription confirmation.
|
||||
pub fn send_subscription_confirmation(
|
||||
&self,
|
||||
list: &DbVal<MailingList>,
|
||||
address: &melib::Address,
|
||||
) -> Result<()> {
|
||||
log::trace!(
|
||||
"Added subscription to list {list:?} for address {address:?}, sending confirmation."
|
||||
);
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::SUBSCRIPTION_CONFIRMATION,
|
||||
default_fn: Some(Template::default_subscription_confirmation),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::SUBSCRIPTION_CONFIRMATION.into(),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(address)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Send unsubscription confirmation.
|
||||
pub fn send_unsubscription_confirmation(
|
||||
&self,
|
||||
list: &DbVal<MailingList>,
|
||||
address: &melib::Address,
|
||||
) -> Result<()> {
|
||||
log::trace!(
|
||||
"Removed subscription to list {list:?} for address {address:?}, sending confirmation."
|
||||
);
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::UNSUBSCRIPTION_CONFIRMATION,
|
||||
default_fn: Some(Template::default_unsubscription_confirmation),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::UNSUBSCRIPTION_CONFIRMATION.into(),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(address)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type for [`Connection::send_reply_with_list_template`].
|
|
@ -29,7 +29,7 @@ use crate::{errors::*, models::DbVal, Connection, DateTime};
|
|||
#[derive(Copy, Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Queue {
|
||||
/// Messages that have been received but not yet processed, await
|
||||
/// Messages that have been submitted but not yet processed, await
|
||||
/// processing in the `maildrop` queue. Messages can be added to the
|
||||
/// `maildrop` queue even when mailpot is not running.
|
||||
Maildrop,
|
||||
|
@ -69,7 +69,7 @@ impl std::str::FromStr for Queue {
|
|||
|
||||
impl Queue {
|
||||
/// Returns the name of the queue used in the database schema.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Maildrop => "maildrop",
|
||||
Self::Hold => "hold",
|
||||
|
@ -79,19 +79,6 @@ impl Queue {
|
|||
Self::Error => "error",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all possible variants as `&'static str`
|
||||
pub const fn possible_values() -> &'static [&'static str] {
|
||||
const VALUES: &[&str] = &[
|
||||
Queue::Maildrop.as_str(),
|
||||
Queue::Hold.as_str(),
|
||||
Queue::Deferred.as_str(),
|
||||
Queue::Corrupt.as_str(),
|
||||
Queue::Out.as_str(),
|
||||
Queue::Error.as_str(),
|
||||
];
|
||||
VALUES
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Queue {
|
|
@ -650,8 +650,3 @@ INSERT OR REPLACE INTO settings_json_schema(id, value) VALUES('MimeRejectSetting
|
|||
}
|
||||
}
|
||||
}');
|
||||
|
||||
|
||||
-- Set current schema version.
|
||||
|
||||
PRAGMA user_version = 7;
|
|
@ -33,11 +33,11 @@ use crate::{
|
|||
|
||||
impl Connection {
|
||||
/// Fetch all subscriptions of a mailing list.
|
||||
pub fn list_subscriptions(&self, list_pk: i64) -> Result<Vec<DbVal<ListSubscription>>> {
|
||||
pub fn list_subscriptions(&self, pk: i64) -> Result<Vec<DbVal<ListSubscription>>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM subscription WHERE list = ?;")?;
|
||||
let list_iter = stmt.query_map([&list_pk], |row| {
|
||||
let list_iter = stmt.query_map([&pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListSubscription {
|
||||
|
@ -186,36 +186,6 @@ impl Connection {
|
|||
self.list_subscription(list_pk, val.pk())
|
||||
}
|
||||
|
||||
/// Fetch all candidate subscriptions of a mailing list.
|
||||
pub fn list_subscription_requests(
|
||||
&self,
|
||||
list_pk: i64,
|
||||
) -> Result<Vec<DbVal<ListCandidateSubscription>>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM candidate_subscription WHERE list = ?;")?;
|
||||
let list_iter = stmt.query_map([&list_pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListCandidateSubscription {
|
||||
pk: row.get("pk")?,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
accepted: row.get("accepted")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for list in list_iter {
|
||||
let list = list?;
|
||||
ret.push(list);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Create subscription candidate.
|
||||
pub fn add_candidate_subscription(
|
||||
&self,
|
|
@ -54,23 +54,22 @@ impl std::fmt::Display for Template {
|
|||
|
||||
impl Template {
|
||||
/// Template name for generic list help e-mail.
|
||||
pub const GENERIC_HELP: &'static str = "generic-help";
|
||||
pub const GENERIC_HELP: &str = "generic-help";
|
||||
/// Template name for generic failure e-mail.
|
||||
pub const GENERIC_FAILURE: &'static str = "generic-failure";
|
||||
pub const GENERIC_FAILURE: &str = "generic-failure";
|
||||
/// Template name for generic success e-mail.
|
||||
pub const GENERIC_SUCCESS: &'static str = "generic-success";
|
||||
pub const GENERIC_SUCCESS: &str = "generic-success";
|
||||
/// Template name for subscription confirmation e-mail.
|
||||
pub const SUBSCRIPTION_CONFIRMATION: &'static str = "subscription-confirmation";
|
||||
pub const SUBSCRIPTION_CONFIRMATION: &str = "subscription-confirmation";
|
||||
/// Template name for unsubscription confirmation e-mail.
|
||||
pub const UNSUBSCRIPTION_CONFIRMATION: &'static str = "unsubscription-confirmation";
|
||||
pub const UNSUBSCRIPTION_CONFIRMATION: &str = "unsubscription-confirmation";
|
||||
/// Template name for subscription request notice e-mail (for list owners).
|
||||
pub const SUBSCRIPTION_REQUEST_NOTICE_OWNER: &'static str = "subscription-notice-owner";
|
||||
pub const SUBSCRIPTION_REQUEST_NOTICE_OWNER: &str = "subscription-notice-owner";
|
||||
/// Template name for subscription request acceptance e-mail (for the
|
||||
/// candidates).
|
||||
pub const SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT: &'static str =
|
||||
"subscription-notice-candidate-accept";
|
||||
pub const SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT: &str = "subscription-notice-candidate-accept";
|
||||
/// Template name for admin notices.
|
||||
pub const ADMIN_NOTICE: &'static str = "admin-notice";
|
||||
pub const ADMIN_NOTICE: &str = "admin-notice";
|
||||
|
||||
/// Render a message body from a saved named template.
|
||||
pub fn render(&self, context: minijinja::value::Value) -> Result<melib::Draft> {
|
||||
|
@ -83,7 +82,7 @@ impl Template {
|
|||
};
|
||||
if let Some(ref subject) = self.subject {
|
||||
draft.headers.insert(
|
||||
HeaderName::SUBJECT,
|
||||
HeaderName::new_unchecked("Subject"),
|
||||
env.render_named_str("subject", subject, &context)?,
|
||||
);
|
||||
}
|
||||
|
@ -179,11 +178,10 @@ impl Template {
|
|||
pk: -1,
|
||||
name: Self::SUBSCRIPTION_REQUEST_NOTICE_OWNER.to_string(),
|
||||
list: None,
|
||||
subject: Some("Subscription request for {{ list.id }}".to_string()),
|
||||
subject: Some("Subscription request for {{ list.id }} by {{ candidate }}".to_string()),
|
||||
headers_json: None,
|
||||
body: "Candidate {{ candidate.name if candidate.name else \"\" }} <{{ \
|
||||
candidate.address }}> Primary key: {{ candidate.pk }}\n\n{{ details|safe if \
|
||||
details else \"\" }}"
|
||||
body: "Candidate primary key: {{ candidate.pk }}\n\n{{ details|safe if details else \
|
||||
\"\" }}"
|
||||
.to_string(),
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ use mailpot::{Configuration, Connection, SendMail};
|
|||
use mailpot_tests::init_stderr_logging;
|
||||
use tempfile::TempDir;
|
||||
|
||||
include!("../build/make_migrations.rs");
|
||||
include!("../make_migrations.rs");
|
||||
|
||||
#[test]
|
||||
fn test_init_empty() {
|
243
docs/mpot.1
243
docs/mpot.1
|
@ -18,24 +18,12 @@ Tool for mailpot mailing list management.
|
|||
.TP
|
||||
\-d, \-\-debug
|
||||
Print logs.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-c, \-\-config \fICONFIG\fR
|
||||
Configuration file to use.
|
||||
.TP
|
||||
\-q, \-\-quiet
|
||||
Silence all output.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-v, \-\-verbose
|
||||
Verbose mode (\-v, \-vv, \-vvv, etc).
|
||||
|
@ -113,12 +101,6 @@ Prints a sample config file to STDOUT.
|
|||
.TP
|
||||
\-\-with\-smtp
|
||||
Use an SMTP connection instead of a shell process.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -168,16 +150,6 @@ List subscriptions of list.
|
|||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot list subscription-requests
|
||||
.\fR
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
List subscription requests.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot list add-subscription
|
||||
.\fR
|
||||
.br
|
||||
|
@ -358,47 +330,6 @@ Is subscription enabled.
|
|||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot list accept-subscription-request
|
||||
.\fR
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
mpot list accept\-subscription\-request [\-\-do\-not\-send\-confirmation \fIDO_NOT_SEND_CONFIRMATION\fR] \fIPK\fR
|
||||
.br
|
||||
|
||||
Accept a subscription request by its primary key.
|
||||
.TP
|
||||
\fIPK\fR
|
||||
The primary key of the request.
|
||||
.TP
|
||||
\-\-do\-not\-send\-confirmation
|
||||
Do not send confirmation e\-mail.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot list send-confirmation-for-subscription
|
||||
.\fR
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
mpot list send\-confirmation\-for\-subscription \fIPK\fR
|
||||
.br
|
||||
|
||||
Send subscription confirmation manually.
|
||||
.TP
|
||||
\fIPK\fR
|
||||
The primary key of the subscription.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot list add-post-policy
|
||||
.\fR
|
||||
.br
|
||||
|
@ -412,48 +343,18 @@ Add a new post policy.
|
|||
.TP
|
||||
\-\-announce\-only
|
||||
Only list owners can post.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-subscription\-only
|
||||
Only subscriptions can post.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-approval\-needed
|
||||
Subscriptions can post. Other posts must be approved by list owners.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-open
|
||||
Anyone can post without restrictions.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-custom
|
||||
Allow posts, but handle it manually.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -485,48 +386,18 @@ Add subscription policy to list.
|
|||
.TP
|
||||
\-\-send\-confirmation
|
||||
Send confirmation e\-mail when subscription is finalized.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-open
|
||||
Anyone can subscribe without restrictions.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-manual
|
||||
Only list owners can manually add subscriptions.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-request
|
||||
Anyone can request to subscribe.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-custom
|
||||
Allow subscriptions, but handle it manually.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -723,21 +594,9 @@ List ID of remote list to query.
|
|||
.TP
|
||||
\-\-dry\-run
|
||||
Show what would be inserted without performing any changes.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-skip\-owners
|
||||
Don\*(Aqt import list owners.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -782,12 +641,6 @@ Post message from STDIN to list.
|
|||
.TP
|
||||
\-\-dry\-run
|
||||
Show e\-mail processing result without actually consuming it.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -804,12 +657,61 @@ Flush outgoing e\-mail queue.
|
|||
.TP
|
||||
\-\-dry\-run
|
||||
Show e\-mail processing result without actually consuming it.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot error-queue
|
||||
.\fR
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
Mail that has not been handled properly end up in the error queue.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot error-queue list
|
||||
.\fR
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
|
||||
.br
|
||||
|
||||
List.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot error-queue print
|
||||
.\fR
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
mpot error\-queue print [\-\-index \fIINDEX\fR]
|
||||
.br
|
||||
|
||||
Print entry in RFC5322 or JSON format.
|
||||
.TP
|
||||
\-\-index \fIINDEX\fR
|
||||
index of entry.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
.SS mpot error-queue delete
|
||||
.\fR
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
mpot error\-queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
|
||||
.br
|
||||
|
||||
Delete entry and print it in stdout.
|
||||
.TP
|
||||
\-\-index \fIINDEX\fR
|
||||
index of entry.
|
||||
.TP
|
||||
\-\-quiet
|
||||
Do not print in stdout.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -822,12 +724,10 @@ Show e\-mail processing result without actually consuming it.
|
|||
mpot queue \-\-queue \fIQUEUE\fR
|
||||
.br
|
||||
|
||||
Processed mail is stored in queues.
|
||||
Mail that has not been handled properly end up in the error queue.
|
||||
.TP
|
||||
\-\-queue \fIQUEUE\fR
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRmaildrop, hold, deferred, corrupt, out, error]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -863,13 +763,16 @@ index of entry.
|
|||
|
||||
.br
|
||||
|
||||
mpot queue delete [\-\-index \fIINDEX\fR]
|
||||
mpot queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
|
||||
.br
|
||||
|
||||
Delete entry and print it in stdout.
|
||||
.TP
|
||||
\-\-index \fIINDEX\fR
|
||||
index of entry.
|
||||
.TP
|
||||
\-\-quiet
|
||||
Do not print in stdout.
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\fB
|
||||
|
@ -1100,57 +1003,21 @@ Show and fix possible data mistakes or inconsistencies.
|
|||
.TP
|
||||
\-\-fix
|
||||
Fix errors (default: false).
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-all
|
||||
Select all tests (default: false).
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-datetime\-header\-value
|
||||
Post `datetime` column must have the Date: header value, in RFC2822 format.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-remove\-empty\-accounts
|
||||
Remove accounts that have no matching subscriptions.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-remove\-accepted\-subscription\-requests
|
||||
Remove subscription requests that have been accepted.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.TP
|
||||
\-\-warn\-list\-no\-owner
|
||||
Warn if a list has no owners.
|
||||
.br
|
||||
|
||||
.br
|
||||
|
||||
.br
|
||||
[\fIpossible values: \fRtrue, false]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.SH AUTHORS
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
File diff suppressed because it is too large
Load Diff
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use mailpot::{melib::smtp, Configuration, Connection, Context, Result};
|
||||
use mailpot_cli::{commands::*, *};
|
||||
|
||||
fn run_app(
|
||||
config: Option<PathBuf>,
|
||||
cmd: Command,
|
||||
debug: bool,
|
||||
quiet: bool,
|
||||
verbose: u8,
|
||||
) -> Result<()> {
|
||||
if let Command::SampleConfig { with_smtp } = cmd {
|
||||
let mut new = Configuration::new("/path/to/sqlite.db");
|
||||
new.administrators.push("admin@example.com".to_string());
|
||||
if with_smtp {
|
||||
new.send_mail = mailpot::SendMail::Smtp(smtp::SmtpServerConf {
|
||||
hostname: "mail.example.com".to_string(),
|
||||
port: 587,
|
||||
envelope_from: "".to_string(),
|
||||
auth: smtp::SmtpAuth::Auto {
|
||||
username: "user".to_string(),
|
||||
password: smtp::Password::Raw("hunter2".to_string()),
|
||||
auth_type: smtp::SmtpAuthType::default(),
|
||||
require_auth: true,
|
||||
},
|
||||
security: smtp::SmtpSecurity::StartTLS {
|
||||
danger_accept_invalid_certs: false,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
}
|
||||
println!("{}", new.to_toml());
|
||||
return Ok(());
|
||||
};
|
||||
let config_path = if let Some(path) = config.as_deref() {
|
||||
path
|
||||
} else {
|
||||
let mut opt = Opt::command();
|
||||
opt.error(
|
||||
clap::error::ErrorKind::MissingRequiredArgument,
|
||||
"--config is required for mailing list operations",
|
||||
)
|
||||
.exit();
|
||||
};
|
||||
|
||||
let config = Configuration::from_file(config_path).with_context(|| {
|
||||
format!(
|
||||
"Could not read configuration file from path: {}",
|
||||
config_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
use Command::*;
|
||||
let mut db = Connection::open_or_create_db(config)
|
||||
.context("Could not open database connection with this configuration")?
|
||||
.trusted();
|
||||
match cmd {
|
||||
SampleConfig { .. } => {}
|
||||
DumpDatabase => {
|
||||
dump_database(&mut db).context("Could not dump database.")?;
|
||||
}
|
||||
ListLists => {
|
||||
list_lists(&mut db).context("Could not retrieve mailing lists.")?;
|
||||
}
|
||||
List { list_id, cmd } => {
|
||||
list(&mut db, &list_id, cmd, quiet).map_err(|err| {
|
||||
err.chain_err(|| {
|
||||
mailpot::Error::from(format!("Could not perform list command for {list_id}."))
|
||||
})
|
||||
})?;
|
||||
}
|
||||
CreateList {
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
} => {
|
||||
create_list(&mut db, name, id, address, description, archive_url, quiet)
|
||||
.context("Could not create list.")?;
|
||||
}
|
||||
Post { dry_run } => {
|
||||
post(&mut db, dry_run, debug).context("Could not process post.")?;
|
||||
}
|
||||
FlushQueue { dry_run } => {
|
||||
flush_queue(&mut db, dry_run, verbose, debug).with_context(|| {
|
||||
format!("Could not flush queue {}.", mailpot::queue::Queue::Out)
|
||||
})?;
|
||||
}
|
||||
Queue { queue, cmd } => {
|
||||
queue_(&mut db, queue, cmd, quiet)
|
||||
.with_context(|| format!("Could not perform queue command for queue `{queue}`."))?;
|
||||
}
|
||||
ImportMaildir {
|
||||
list_id,
|
||||
maildir_path,
|
||||
} => {
|
||||
import_maildir(
|
||||
&mut db,
|
||||
&list_id,
|
||||
maildir_path.clone(),
|
||||
quiet,
|
||||
debug,
|
||||
verbose,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not import maildir path {} to list `{list_id}`.",
|
||||
maildir_path.display(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
UpdatePostfixConfig { master_cf, config } => {
|
||||
update_postfix_config(config_path, &mut db, master_cf, config)
|
||||
.context("Could not update postfix configuration.")?;
|
||||
}
|
||||
PrintPostfixConfig { config } => {
|
||||
print_postfix_config(config_path, &mut db, config)
|
||||
.context("Could not print postfix configuration.")?;
|
||||
}
|
||||
Accounts => {
|
||||
accounts(&mut db, quiet).context("Could not retrieve accounts.")?;
|
||||
}
|
||||
AccountInfo { address } => {
|
||||
account_info(&mut db, &address, quiet).with_context(|| {
|
||||
format!("Could not retrieve account info for address {address}.")
|
||||
})?;
|
||||
}
|
||||
AddAccount {
|
||||
address,
|
||||
password,
|
||||
name,
|
||||
public_key,
|
||||
enabled,
|
||||
} => {
|
||||
add_account(&mut db, address, password, name, public_key, enabled)
|
||||
.context("Could not add account.")?;
|
||||
}
|
||||
RemoveAccount { address } => {
|
||||
remove_account(&mut db, &address, quiet)
|
||||
.with_context(|| format!("Could not remove account with address {address}."))?;
|
||||
}
|
||||
UpdateAccount {
|
||||
address,
|
||||
password,
|
||||
name,
|
||||
public_key,
|
||||
enabled,
|
||||
} => {
|
||||
update_account(&mut db, address, password, name, public_key, enabled)
|
||||
.context("Could not update account.")?;
|
||||
}
|
||||
Repair {
|
||||
fix,
|
||||
all,
|
||||
datetime_header_value,
|
||||
remove_empty_accounts,
|
||||
remove_accepted_subscription_requests,
|
||||
warn_list_no_owner,
|
||||
} => {
|
||||
repair(
|
||||
&mut db,
|
||||
fix,
|
||||
all,
|
||||
datetime_header_value,
|
||||
remove_empty_accounts,
|
||||
remove_accepted_subscription_requests,
|
||||
warn_list_no_owner,
|
||||
)
|
||||
.context("Could not perform database repair.")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> std::result::Result<(), i32> {
|
||||
let opt = Opt::parse();
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.module("mailpot")
|
||||
.quiet(opt.quiet)
|
||||
.verbosity(opt.verbose as usize)
|
||||
.timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off))
|
||||
.init()
|
||||
.unwrap();
|
||||
if opt.debug {
|
||||
println!("DEBUG: {:?}", &opt);
|
||||
}
|
||||
let Opt {
|
||||
config,
|
||||
cmd,
|
||||
debug,
|
||||
quiet,
|
||||
verbose,
|
||||
..
|
||||
} = opt;
|
||||
if let Err(err) = run_app(config, cmd, debug, quiet, verbose) {
|
||||
print!("{}", err.display_chain());
|
||||
std::process::exit(-1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1,2 +0,0 @@
|
|||
*.db-shm
|
||||
*.db-wal
|
|
@ -13,9 +13,9 @@ publish = false
|
|||
assert_cmd = "2"
|
||||
log = "0.4"
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
predicates = "3"
|
||||
stderrlog = { version = "^0.6" }
|
||||
tempfile = { version = "3.9" }
|
||||
stderrlog = "^0.5"
|
||||
tempfile = "3.3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Utils for templates with the [`minijinja`] crate.
|
||||
|
||||
pub use mailpot::StripCarets;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod compressed;
|
||||
mod filters;
|
||||
mod objects;
|
||||
|
||||
pub use filters::*;
|
||||
pub use objects::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPLATES: Environment<'static> = {
|
||||
let mut env = Environment::new();
|
||||
macro_rules! add {
|
||||
(function $($id:ident),*$(,)?) => {
|
||||
$(env.add_function(stringify!($id), $id);)*
|
||||
};
|
||||
(filter $($id:ident),*$(,)?) => {
|
||||
$(env.add_filter(stringify!($id), $id);)*
|
||||
}
|
||||
}
|
||||
add!(function calendarize,
|
||||
strip_carets,
|
||||
ensure_carets,
|
||||
urlize,
|
||||
url_encode,
|
||||
heading,
|
||||
topics,
|
||||
login_path,
|
||||
logout_path,
|
||||
settings_path,
|
||||
help_path,
|
||||
list_path,
|
||||
list_settings_path,
|
||||
list_edit_path,
|
||||
list_subscribers_path,
|
||||
list_candidates_path,
|
||||
list_post_path,
|
||||
post_raw_path,
|
||||
post_eml_path,
|
||||
post_mbox_path
|
||||
);
|
||||
add!(filter pluralize);
|
||||
// Load compressed templates. They are constructed in build.rs. See
|
||||
// [ref:embed_templates]
|
||||
let mut source = minijinja::Source::new();
|
||||
for (name, bytes) in compressed::COMPRESSED {
|
||||
let mut de_bytes = vec![];
|
||||
zstd::stream::copy_decode(*bytes,&mut de_bytes).unwrap();
|
||||
source.add_template(*name, String::from_utf8(de_bytes).unwrap()).unwrap();
|
||||
}
|
||||
env.set_source(source);
|
||||
|
||||
env.add_global("root_url_prefix", Value::from_safe_string( std::env::var("ROOT_URL_PREFIX").unwrap_or_default()));
|
||||
env.add_global("public_url",Value::from_safe_string(std::env::var("PUBLIC_URL").unwrap_or_default()));
|
||||
env.add_global("site_title", Value::from_safe_string(std::env::var("SITE_TITLE").unwrap_or_else(|_| "mailing list archive".to_string())));
|
||||
env.add_global("site_subtitle", std::env::var("SITE_SUBTITLE").ok().map(Value::from_safe_string).unwrap_or_default());
|
||||
|
||||
env
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue