Split into crates

Manos Pitsidianakis 2020-08-16 09:13:18 +03:00
parent b079ef0c86
commit 3c582f7729
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
25 changed files with 1881 additions and 367 deletions

980
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,6 @@
[package] [workspace]
name = "mailpot" members = [
version = "0.1.0" "core",
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"] "cli",
edition = "2018" "rest-http",
license = "LICENSE" ]
readme = "README.md"
description = "mailing list manager"
repository = "https://github.com/meli/mailpot"
keywords = ["mail", "mailing-lists" ]
categories = ["email"]
default-run = "mpot"
[dependencies]
chrono = { version = "0.4.15", features = ["serde", ] }
error-chain = "0.12.4"
melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms"], git="https://github.com/meli/meli", branch = "master" }
rusqlite = {version = "0.20.0"}
serde = { version = "1.0.114" }
structopt = "0.3.16"
xdg = "2.1.0"

View File

@ -1,5 +1,11 @@
# Mailpot # Mailpot
## flexible mailing list manager ## WIP mailing list manager
Crates:
- `core`
- `cli` a command line tool to manage lists
- `rest-http` a REST http server to manage lists
```text ```text
% mpot help % mpot help

20
cli/Cargo.toml 100644
View File

@ -0,0 +1,20 @@
[package]
name = "mailpot-cli"
version = "0.1.0"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
edition = "2018"
license = "LICENSE"
readme = "README.md"
description = "mailing list manager"
repository = "https://github.com/meli/mailpot"
keywords = ["mail", "mailing-lists" ]
categories = ["email"]
default-run = "mpot"
[[bin]]
name = "mpot"
path = "src/main.rs"
[dependencies]
mailpot = { version = "0.1.0", path = "../core" }
structopt = "0.3.16"

View File

@ -17,26 +17,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// `error_chain!` can recurse deeply extern crate mailpot;
#![recursion_limit = "1024"]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate serde;
use structopt::StructOpt;
pub mod config;
pub use config::*;
pub mod models;
pub mod post;
pub use models::*;
pub mod errors;
pub use errors::*;
pub mod db;
pub use db::*;
pub use mailpot::config::*;
pub use mailpot::db::*;
pub use mailpot::errors::*;
pub use mailpot::models::*;
pub use mailpot::post::*;
pub use mailpot::*;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
#[structopt( #[structopt(
@ -60,6 +50,8 @@ struct Opt {
enum Command { enum Command {
///Prints database filesystem location ///Prints database filesystem location
DbLocation, DbLocation,
///Dumps database data to STDOUT
DumpDatabase,
///Lists all registered mailing lists ///Lists all registered mailing lists
ListLists, ListLists,
///Mailing list management ///Mailing list management
@ -129,11 +121,21 @@ fn run_app(opt: Opt) -> Result<()> {
if opt.debug { if opt.debug {
println!("DEBUG: {:?}", &opt); println!("DEBUG: {:?}", &opt);
} }
Configuration::init()?;
use Command::*; use Command::*;
match opt.cmd { match opt.cmd {
DbLocation => { DbLocation => {
println!("{}", Database::db_path()?.display()); println!("{}", Database::db_path()?.display());
} }
DumpDatabase => {
let db = Database::open_or_create_db()?;
let lists = db.list_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_members(l.pk)?)?;
}
}
ListLists => { ListLists => {
let db = Database::open_or_create_db()?; let db = Database::open_or_create_db()?;
let lists = db.list_lists()?; let lists = db.list_lists()?;

23
core/Cargo.toml 100644
View File

@ -0,0 +1,23 @@
[package]
name = "mailpot"
version = "0.1.0"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
edition = "2018"
license = "LICENSE"
readme = "README.md"
description = "mailing list manager"
repository = "https://github.com/meli/mailpot"
keywords = ["mail", "mailing-lists" ]
categories = ["email"]
[dependencies]
chrono = { version = "0.4.15", features = ["serde", ] }
error-chain = "0.12.4"
diesel = { version = "1.4.5", features = ["sqlite", ] }
melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms"], path="../../meli/melib", branch = "master" }
#melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms"], git="https://github.com/meli/meli", branch = "master" }
rusqlite = {version = "0.20.0"}
serde = { version = "1.0.114" }
serde_json = "1.0.57"
toml = "^0.5"
xdg = "2.1.0"

17
core/README.md 100644
View File

@ -0,0 +1,17 @@
# mailpot-core
Initialize `sqlite3` database:
Either
```shell
sqlite3 mpot.db < ./src/schema.sql
```
or
```shell
# cargo install diesel_cli --no-default-features --features sqlite
diesel migration run
```

32
core/build.rs 100644
View File

@ -0,0 +1,32 @@
/*
* 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::io::Write;
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=src/schema.sql.m4");
let output = Command::new("m4")
.arg("./src/schema.sql.m4")
.output()
.unwrap();
let mut file = std::fs::File::create("./src/schema.sql").unwrap();
file.write_all(&output.stdout).unwrap();
}

5
core/diesel.toml 100644
View File

@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View File

View File

@ -0,0 +1,8 @@
DROP TABLE IF EXISTS mailing_lists;
DROP TABLE IF EXISTS list_owner;
DROP TABLE IF EXISTS post_policy;
DROP TABLE IF EXISTS membership;
DROP TABLE IF EXISTS post;
DROP TABLE IF EXISTS post_event;
DROP INDEX IF EXISTS mailing_lists_idx;
DROP INDEX IF EXISTS membership_idx;

View File

@ -0,0 +1,63 @@
PRAGMA foreign_keys = true;
PRAGMA encoding = 'UTF-8';
CREATE TABLE IF NOT EXISTS mailing_lists (
pk INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
id TEXT NOT NULL,
address TEXT NOT NULL,
archive_url TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS list_owner (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post_policy (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL UNIQUE,
announce_only BOOLEAN CHECK (announce_only in (0, 1)) NOT NULL DEFAULT 0,
subscriber_only BOOLEAN CHECK (subscriber_only in (0, 1)) NOT NULL DEFAULT 0,
approval_needed BOOLEAN CHECK (approval_needed in (0, 1)) NOT NULL DEFAULT 0,
CHECK(((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only))))),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS membership (
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0,
hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0,
receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1,
receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0,
receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1,
PRIMARY KEY (list, address),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL,
address TEXT NOT NULL,
message_id TEXT NOT NULL,
message BLOB NOT NULL,
FOREIGN KEY (list, address) REFERENCES membership(list, address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post_event (
pk INTEGER PRIMARY KEY NOT NULL,
post INTEGER NOT NULL,
date INTEGER NOT NULL,
kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id);
CREATE INDEX IF NOT EXISTS membership_idx ON membership(address);

View File

@ -19,10 +19,13 @@
use super::errors::*; use super::errors::*;
use chrono::prelude::*; use chrono::prelude::*;
use std::io::Write; use std::cell::RefCell;
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf; use std::path::PathBuf;
thread_local!(pub static CONFIG: RefCell<Configuration> = RefCell::new(Configuration::new()));
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", content = "value")] #[serde(tag = "type", content = "value")]
pub enum SendMail { pub enum SendMail {
@ -32,10 +35,33 @@ pub enum SendMail {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Configuration { pub struct Configuration {
send_mail: SendMail, pub send_mail: SendMail,
#[serde(default = "default_storage_fn")]
pub storage: String,
} }
impl Configuration { impl Configuration {
pub(crate) fn new() -> Self {
Configuration {
send_mail: SendMail::ShellCommand(String::new()),
storage: "sqlite3".into(),
}
}
pub fn init() -> Result<()> {
let path =
xdg::BaseDirectories::with_prefix("mailpot")?.place_config_file("config.toml")?;
let mut s = String::new();
let mut file = std::fs::File::open(&path)?;
file.read_to_string(&mut s)?;
let config: Configuration = toml::from_str(&s)?;
CONFIG.with(|f| {
*f.borrow_mut() = config;
});
Ok(())
}
pub fn data_directory() -> Result<PathBuf> { pub fn data_directory() -> Result<PathBuf> {
Ok(xdg::BaseDirectories::with_prefix("mailpot")?.get_data_home()) Ok(xdg::BaseDirectories::with_prefix("mailpot")?.get_data_home())
} }
@ -74,3 +100,7 @@ impl Configuration {
Self::save_message_to_path(&msg, temp_path) Self::save_message_to_path(&msg, temp_path)
} }
} }
fn default_storage_fn() -> String {
"sqlite3".to_string()
}

236
core/src/db.rs 100644
View File

@ -0,0 +1,236 @@
/*
* 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 super::*;
use diesel::{prelude::*, Connection};
use melib::Envelope;
use std::path::PathBuf;
const DB_NAME: &str = "mpot.db";
pub struct Database {
connection: SqliteConnection,
}
impl Database {
pub fn list_lists(&self) -> Result<Vec<MailingList>> {
use schema::mailing_lists;
let ret = mailing_lists::table.load(&self.connection)?;
Ok(ret)
}
pub fn get_list(&self, pk: i32) -> Result<Option<MailingList>> {
use schema::mailing_lists;
let ret = mailing_lists::table
.filter(mailing_lists::pk.eq(pk))
.first(&self.connection)
.optional()?;
Ok(ret)
}
pub fn get_list_policy(&self, pk: i32) -> Result<Option<PostPolicy>> {
use schema::post_policy;
let ret = post_policy::table
.filter(post_policy::list.eq(pk))
.first(&self.connection)
.optional()?;
Ok(ret)
}
pub fn get_list_owners(&self, pk: i32) -> Result<Vec<ListOwner>> {
use schema::list_owner;
let ret = list_owner::table
.filter(list_owner::list.eq(pk))
.load(&self.connection)?;
Ok(ret)
}
pub fn list_members(&self, pk: i32) -> Result<Vec<ListMembership>> {
use schema::membership;
let ret = membership::table
.filter(membership::list.eq(pk))
.load(&self.connection)?;
Ok(ret)
}
pub fn add_member(&self, list_pk: i32, mut new_val: ListMembership) -> Result<()> {
use schema::membership;
new_val.list = list_pk;
diesel::insert_into(membership::table)
.values(&new_val)
.execute(&self.connection)?;
Ok(())
}
pub fn remove_member(&self, list_pk: i32, address: &str) -> Result<()> {
use schema::membership;
diesel::delete(
membership::table
.filter(membership::columns::list.eq(list_pk))
.filter(membership::columns::address.eq(address)),
)
.execute(&self.connection)?;
Ok(())
}
pub fn create_list(&self, new_val: MailingList) -> Result<()> {
use schema::mailing_lists;
diesel::insert_into(mailing_lists::table)
.values(&new_val)
.execute(&self.connection)?;
Ok(())
}
pub fn db_path() -> Result<PathBuf> {
let name = DB_NAME;
let data_dir = xdg::BaseDirectories::with_prefix("mailpot")?;
Ok(data_dir.place_data_file(name)?)
}
pub fn open_db(db_path: PathBuf) -> Result<Self> {
if !db_path.exists() {
return Err("Database doesn't exist".into());
}
Ok(Database {
connection: SqliteConnection::establish(&db_path.to_str().unwrap())?,
})
}
pub fn open_or_create_db() -> Result<Self> {
let db_path = Self::db_path()?;
let mut set_mode = false;
if !db_path.exists() {
println!("Creating {} database in {}", DB_NAME, db_path.display());
set_mode = true;
}
let conn = SqliteConnection::establish(&db_path.to_str().unwrap())?;
if set_mode {
use std::os::unix::fs::PermissionsExt;
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)?;
}
Ok(Database { connection: conn })
}
pub fn get_list_filters(&self, _list: &MailingList) -> Vec<Box<dyn crate::post::PostFilter>> {
use crate::post::*;
vec![
Box::new(FixCRLF),
Box::new(PostRightsCheck),
Box::new(AddListHeaders),
Box::new(FinalizeRecipients),
]
}
pub fn post(&self, env: Envelope, raw: &[u8]) -> Result<()> {
let mut lists = self.list_lists()?;
let tos = env
.to()
.iter()
.map(|addr| addr.get_email())
.collect::<Vec<String>>();
if tos.is_empty() {
return Err("Envelope To: field is empty!".into());
}
if env.from().is_empty() {
return Err("Envelope From: field is empty!".into());
}
lists.retain(|list| tos.iter().any(|a| a == &list.address));
if lists.is_empty() {
return Err("Envelope To: field doesn't contain any known lists!".into());
}
let mut configuration = crate::config::Configuration::new();
crate::config::CONFIG.with(|f| {
configuration = f.borrow().clone();
});
use crate::post::{Post, PostAction};
for mut list in lists {
let filters = self.get_list_filters(&list);
let memberships = self.list_members(list.pk)?;
let mut post = Post {
policy: self.get_list_policy(list.pk)?,
list_owners: self.get_list_owners(list.pk)?,
list: &mut list,
from: env.from()[0].clone(),
memberships: &memberships,
bytes: raw.to_vec(),
to: env.to().to_vec(),
action: PostAction::Hold,
};
let result = filters
.into_iter()
.fold(Ok(&mut post), |p, f| p.and_then(|p| f.feed(p)));
eprintln!("result {:?}", result);
let Post { bytes, action, .. } = post;
eprintln!("send_mail {:?}", &configuration.send_mail);
match configuration.send_mail {
crate::config::SendMail::Smtp(ref smtp_conf) => {
let smtp_conf = smtp_conf.clone();
use melib::futures;
use melib::smol;
use melib::smtp::*;
std::thread::spawn(|| smol::run(futures::future::pending::<()>()));
let mut conn = futures::executor::block_on(SmtpConnection::new_connection(
smtp_conf.clone(),
))?;
match action {
PostAction::Accept {
recipients,
digests: _digests,
} => {
futures::executor::block_on(conn.mail_transaction(
&String::from_utf8_lossy(&bytes),
Some(&recipients),
))?;
/* - Save digest metadata in database */
}
PostAction::Reject { reason: _ } => {
/* - Notify submitter */
//futures::executor::block_on(conn.mail_transaction(&post.bytes, b)).unwrap();
}
PostAction::Defer { reason: _ } => {
/* - Notify submitter
* - Save in database */
}
PostAction::Hold => { /* - Save in database */ }
}
}
_ => {}
}
}
Ok(())
}
}

View File

@ -20,9 +20,12 @@
// Create the Error, ErrorKind, ResultExt, and Result types // Create the Error, ErrorKind, ResultExt, and Result types
error_chain! { error_chain! {
foreign_links { foreign_links {
Sql(rusqlite::Error); Db(diesel::ConnectionError);
Sql(diesel::result::Error);
Io(::std::io::Error); Io(::std::io::Error);
Xdg(xdg::BaseDirectoriesError); Xdg(xdg::BaseDirectoriesError);
Melib(melib::error::MeliError); Melib(melib::error::MeliError);
Configuration(toml::de::Error);
SerdeJson(serde_json::Error);
} }
} }

40
core/src/lib.rs 100644
View File

@ -0,0 +1,40 @@
/*
* 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/>.
*/
// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]
//#![warn(missing_docs)]
#[macro_use]
extern crate error_chain;
#[macro_use]
pub extern crate serde;
#[macro_use]
pub extern crate diesel;
pub use melib;
pub use serde_json;
pub mod config;
pub mod models;
pub mod post;
pub mod schema;
use models::*;
pub mod errors;
use errors::*;
pub mod db;

View File

@ -18,12 +18,12 @@
*/ */
use super::*; use super::*;
use rusqlite::Row; use schema::*;
use std::convert::TryFrom;
#[derive(Debug)] #[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)]
#[table_name = "mailing_lists"]
pub struct MailingList { pub struct MailingList {
pub pk: i64, pub pk: i32,
pub name: String, pub name: String,
pub id: String, pub id: String,
pub address: String, pub address: String,
@ -49,20 +49,6 @@ impl std::fmt::Display for MailingList {
} }
} }
impl TryFrom<&'_ Row<'_>> for MailingList {
type Error = rusqlite::Error;
fn try_from(row: &'_ Row<'_>) -> std::result::Result<MailingList, rusqlite::Error> {
Ok(MailingList {
pk: row.get("pk")?,
name: row.get("name")?,
id: row.get("id")?,
address: row.get("address")?,
description: row.get("description")?,
archive_url: row.get("archive_url")?,
})
}
}
impl MailingList { impl MailingList {
pub fn list_id(&self) -> String { pub fn list_id(&self) -> String {
format!("\"{}\" <{}>", self.name, self.address) format!("\"{}\" <{}>", self.name, self.address)
@ -85,9 +71,10 @@ impl MailingList {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)]
#[table_name = "membership"]
pub struct ListMembership { pub struct ListMembership {
pub list: i64, pub list: i32,
pub address: String, pub address: String,
pub name: Option<String>, pub name: Option<String>,
pub digest: bool, pub digest: bool,
@ -115,22 +102,6 @@ impl std::fmt::Display for ListMembership {
} }
} }
impl TryFrom<&'_ Row<'_>> for ListMembership {
type Error = rusqlite::Error;
fn try_from(row: &'_ Row<'_>) -> std::result::Result<ListMembership, rusqlite::Error> {
Ok(ListMembership {
list: row.get("list")?,
address: row.get("address")?,
name: row.get("name")?,
digest: row.get("digest")?,
hide_address: row.get("hide_address")?,
receive_duplicates: row.get("receive_duplicates")?,
receive_own_posts: row.get("receive_own_posts")?,
receive_confirmation: row.get("receive_confirmation")?,
})
}
}
impl ListMembership { impl ListMembership {
pub fn into_address(&self) -> melib::email::Address { pub fn into_address(&self) -> melib::email::Address {
use melib::email::Address; use melib::email::Address;
@ -143,3 +114,57 @@ impl ListMembership {
} }
} }
} }
#[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)]
#[table_name = "post_policy"]
pub struct PostPolicy {
pub pk: i32,
pub list: i32,
pub announce_only: bool,
pub subscriber_only: bool,
pub approval_needed: bool,
}
impl std::fmt::Display for PostPolicy {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{:?}", self)
}
}
#[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)]
#[table_name = "list_owner"]
pub struct ListOwner {
pub pk: i32,
pub list: i32,
pub address: String,
pub name: Option<String>,
}
impl std::fmt::Display for ListOwner {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(ref name) = self.name {
write!(
fmt,
"[#{} {}] \"{}\" <{}>",
self.pk, self.list, name, self.address
)
} else {
write!(fmt, "[#{} {}] {}", self.pk, self.list, self.address)
}
}
}
impl From<ListOwner> for ListMembership {
fn from(val: ListOwner) -> ListMembership {
ListMembership {
list: val.list,
address: val.address,
name: val.name,
digest: false,
hide_address: false,
receive_duplicates: true,
receive_own_posts: false,
receive_confirmation: true,
}
}
}

View File

@ -22,6 +22,7 @@ use melib::Address;
#[derive(Debug)] #[derive(Debug)]
pub enum PostAction { pub enum PostAction {
Hold,
Accept { Accept {
recipients: Vec<Address>, recipients: Vec<Address>,
digests: Vec<Address>, digests: Vec<Address>,
@ -37,8 +38,10 @@ pub enum PostAction {
///Post to be considered by the list's `PostFilter` stack. ///Post to be considered by the list's `PostFilter` stack.
pub struct Post<'list> { pub struct Post<'list> {
pub list: &'list mut MailingList, pub list: &'list mut MailingList,
pub list_owners: Vec<ListOwner>,
pub from: Address, pub from: Address,
pub memberships: &'list [ListMembership], pub memberships: &'list [ListMembership],
pub policy: Option<PostPolicy>,
pub bytes: Vec<u8>, pub bytes: Vec<u8>,
pub to: Vec<Address>, pub to: Vec<Address>,
pub action: PostAction, pub action: PostAction,
@ -51,6 +54,7 @@ impl<'list> core::fmt::Debug for Post<'list> {
.field("from", &self.from) .field("from", &self.from)
.field("members", &format_args!("{}", self.memberships.len())) .field("members", &format_args!("{}", self.memberships.len()))
.field("bytes", &format_args!("{}", self.bytes.len())) .field("bytes", &format_args!("{}", self.bytes.len()))
.field("policy", &self.policy)
.field("to", &self.to.as_slice()) .field("to", &self.to.as_slice())
.field("action", &self.action) .field("action", &self.action)
.finish() .finish()
@ -60,19 +64,43 @@ impl<'list> core::fmt::Debug for Post<'list> {
///Filter that modifies and/or verifies a post candidate. On rejection, return a string ///Filter that modifies and/or verifies a post candidate. On rejection, return a string
///describing the error and optionally set `post.action` to `Reject` or `Defer` ///describing the error and optionally set `post.action` to `Reject` or `Defer`
pub trait PostFilter { pub trait PostFilter {
fn feed<'list>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'list mut Post<'list>, post: &'p mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String>; ) -> std::result::Result<&'p mut Post<'list>, String>;
} }
///Check that submitter can post to list, for now it accepts everything. ///Check that submitter can post to list, for now it accepts everything.
pub struct PostRightsCheck; pub struct PostRightsCheck;
impl PostFilter for PostRightsCheck { impl PostFilter for PostRightsCheck {
fn feed<'list>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'list mut Post<'list>, post: &'p mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> { ) -> std::result::Result<&'p mut Post<'list>, String> {
if let Some(ref policy) = post.policy {
if policy.announce_only {
let owner_addresses = post
.list_owners
.iter()
.map(|lo| {
let lm: ListMembership = lo.clone().into();
lm.into_address()
})
.collect::<Vec<Address>>();
if !owner_addresses.iter().any(|addr| *addr == post.from) {
return Err("You are not allowed to post on this list.".to_string());
}
} else if policy.subscriber_only {
let email_from = post.from.get_email();
if !post.memberships.iter().any(|lm| lm.address == email_from) {
return Err("You are not subscribed to this list.".to_string());
}
} else if policy.approval_needed {
post.action = PostAction::Defer {
reason: "Approval from the list's moderators is required.".to_string(),
};
}
}
Ok(post) Ok(post)
} }
} }
@ -80,10 +108,10 @@ impl PostFilter for PostRightsCheck {
///Ensure message contains only `\r\n` line terminators, required by SMTP. ///Ensure message contains only `\r\n` line terminators, required by SMTP.
pub struct FixCRLF; pub struct FixCRLF;
impl PostFilter for FixCRLF { impl PostFilter for FixCRLF {
fn feed<'list>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'list mut Post<'list>, post: &'p mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> { ) -> std::result::Result<&'p mut Post<'list>, String> {
use std::io::prelude::*; use std::io::prelude::*;
let mut new_vec = Vec::with_capacity(post.bytes.len()); let mut new_vec = Vec::with_capacity(post.bytes.len());
for line in post.bytes.lines() { for line in post.bytes.lines() {
@ -98,10 +126,10 @@ impl PostFilter for FixCRLF {
///Add `List-*` headers ///Add `List-*` headers
pub struct AddListHeaders; pub struct AddListHeaders;
impl PostFilter for AddListHeaders { impl PostFilter for AddListHeaders {
fn feed<'list>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'list mut Post<'list>, post: &'p mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> { ) -> std::result::Result<&'p mut Post<'list>, String> {
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
let list_id = post.list.list_id(); let list_id = post.list.list_id();
headers.push((&b"List-ID"[..], list_id.as_bytes())); headers.push((&b"List-ID"[..], list_id.as_bytes()));
@ -142,10 +170,10 @@ impl PostFilter for AddListHeaders {
///Adds `Archived-At` field, if configured. ///Adds `Archived-At` field, if configured.
pub struct ArchivedAtLink; pub struct ArchivedAtLink;
impl PostFilter for ArchivedAtLink { impl PostFilter for ArchivedAtLink {
fn feed<'list>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'list mut Post<'list>, post: &'p mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> { ) -> std::result::Result<&'p mut Post<'list>, String> {
Ok(post) Ok(post)
} }
} }
@ -154,10 +182,10 @@ impl PostFilter for ArchivedAtLink {
///will receive the post in `post.action` field. ///will receive the post in `post.action` field.
pub struct FinalizeRecipients; pub struct FinalizeRecipients;
impl PostFilter for FinalizeRecipients { impl PostFilter for FinalizeRecipients {
fn feed<'list>( fn feed<'p, 'list>(
self: Box<Self>, self: Box<Self>,
post: &'list mut Post<'list>, post: &'p mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> { ) -> std::result::Result<&'p mut Post<'list>, String> {
let mut recipients = vec![]; let mut recipients = vec![];
let mut digests = vec![]; let mut digests = vec![];
let email_from = post.from.get_email(); let email_from = post.from.get_email();

76
core/src/schema.rs 100644
View File

@ -0,0 +1,76 @@
table! {
list_owner (pk) {
pk -> Integer,
list -> Integer,
address -> Text,
name -> Nullable<Text>,
}
}
table! {
mailing_lists (pk) {
pk -> Integer,
name -> Text,
id -> Text,
address -> Text,
archive_url -> Nullable<Text>,
description -> Nullable<Text>,
}
}
table! {
membership (list, address) {
list -> Integer,
address -> Text,
name -> Nullable<Text>,
digest -> Bool,
hide_address -> Bool,
receive_duplicates -> Bool,
receive_own_posts -> Bool,
receive_confirmation -> Bool,
}
}
table! {
post (pk) {
pk -> Integer,
list -> Integer,
address -> Text,
message_id -> Text,
message -> Binary,
}
}
table! {
post_event (pk) {
pk -> Integer,
post -> Integer,
date -> Integer,
kind -> Text,
content -> Text,
}
}
table! {
post_policy (pk) {
pk -> Integer,
list -> Integer,
announce_only -> Bool,
subscriber_only -> Bool,
approval_needed -> Bool,
}
}
joinable!(list_owner -> mailing_lists (list));
joinable!(membership -> mailing_lists (list));
joinable!(post_event -> post (post));
joinable!(post_policy -> mailing_lists (list));
allow_tables_to_appear_in_same_query!(
list_owner,
mailing_lists,
membership,
post,
post_event,
post_policy,
);

View File

@ -0,0 +1,63 @@
PRAGMA foreign_keys = true;
PRAGMA encoding = 'UTF-8';
CREATE TABLE IF NOT EXISTS mailing_lists (
pk INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
id TEXT NOT NULL,
address TEXT NOT NULL,
archive_url TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS list_owner (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post_policy (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL UNIQUE,
announce_only BOOLEAN CHECK (announce_only in (0, 1)) NOT NULL DEFAULT 0,
subscriber_only BOOLEAN CHECK (subscriber_only in (0, 1)) NOT NULL DEFAULT 0,
approval_needed BOOLEAN CHECK (approval_needed in (0, 1)) NOT NULL DEFAULT 0,
CHECK(((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only))))),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS membership (
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0,
hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0,
receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1,
receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0,
receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1,
PRIMARY KEY (list, address),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL,
address TEXT NOT NULL,
message_id TEXT NOT NULL,
message BLOB NOT NULL,
FOREIGN KEY (list, address) REFERENCES membership(list, address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post_event (
pk INTEGER PRIMARY KEY NOT NULL,
post INTEGER NOT NULL,
date INTEGER NOT NULL,
kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id);
CREATE INDEX IF NOT EXISTS membership_idx ON membership(address);

View File

@ -0,0 +1,67 @@
define(xor, `(($1) OR ($2)) AND NOT (($1) AND ($2))')dnl
define(BOOLEAN_TYPE, `$1 BOOLEAN CHECK ($1 in (0, 1)) NOT NULL')dnl
define(BOOLEAN_FALSE, `0')dnl
define(BOOLEAN_TRUE, `1')dnl
PRAGMA foreign_keys = true;
PRAGMA encoding = 'UTF-8';
CREATE TABLE IF NOT EXISTS mailing_lists (
pk INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
id TEXT NOT NULL,
address TEXT NOT NULL,
archive_url TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS list_owner (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post_policy (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL UNIQUE,
BOOLEAN_TYPE(announce_only) DEFAULT BOOLEAN_FALSE(),
BOOLEAN_TYPE(subscriber_only) DEFAULT BOOLEAN_FALSE(),
BOOLEAN_TYPE(approval_needed) DEFAULT BOOLEAN_FALSE(),
CHECK(xor(approval_needed, xor(announce_only, subscriber_only))),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS membership (
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
BOOLEAN_TYPE(digest) DEFAULT BOOLEAN_FALSE(),
BOOLEAN_TYPE(hide_address) DEFAULT BOOLEAN_FALSE(),
BOOLEAN_TYPE(receive_duplicates) DEFAULT BOOLEAN_TRUE(),
BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(),
BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(),
PRIMARY KEY (list, address),
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post (
pk INTEGER PRIMARY KEY NOT NULL,
list INTEGER NOT NULL,
address TEXT NOT NULL,
message_id TEXT NOT NULL,
message BLOB NOT NULL,
FOREIGN KEY (list, address) REFERENCES membership(list, address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS post_event (
pk INTEGER PRIMARY KEY NOT NULL,
post INTEGER NOT NULL,
date INTEGER NOT NULL,
kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id);
CREATE INDEX IF NOT EXISTS membership_idx ON membership(address);

View File

@ -0,0 +1,25 @@
[package]
name = "mailpot-http"
version = "0.1.0"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
edition = "2018"
license = "LICENSE"
readme = "README.md"
description = "mailing list manager"
repository = "https://github.com/meli/mailpot"
keywords = ["mail", "mailing-lists" ]
categories = ["email"]
default-run = "mpot-http"
[[bin]]
name = "mpot-http"
path = "src/main.rs"
[dependencies]
mailpot = { version = "0.1.0", path = "../core" }
rocket = "0.4.5"
[dependencies.rocket_contrib]
version = "0.4.5"
default-features = false
features = ["json"]

View File

@ -0,0 +1,7 @@
# mailpot REST http server
Current `rocket` requires Nightly.
```shell
cargo +nightly run --bin mpot-http
```

View File

@ -0,0 +1,93 @@
/*
* 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/>.
*/
#![feature(proc_macro_hygiene, decl_macro)]
extern crate mailpot;
pub use mailpot::config::*;
pub use mailpot::db::*;
pub use mailpot::errors::*;
pub use mailpot::models::*;
pub use mailpot::post::*;
pub use mailpot::*;
use std::path::PathBuf;
use rocket::response::content;
use rocket_contrib::json::Json;
#[macro_use]
extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
#[get("/lists")]
fn lists() -> Json<Vec<MailingList>> {
let db = Database::open_or_create_db().unwrap();
let lists = db.list_lists().unwrap();
Json(lists)
}
#[get("/lists/<num>")]
fn lists_num(num: u64) -> Json<Option<MailingList>> {
let db = Database::open_or_create_db().unwrap();
let list = db.get_list(num as i64).unwrap();
Json(list)
}
#[get("/lists/<num>/members")]
fn lists_members(num: u64) -> Option<Json<Vec<ListMembership>>> {
let db = Database::open_or_create_db().unwrap();
db.list_members(num as i64).ok().map(|l| Json(l))
}
#[get("/lists/<num>/owners")]
fn lists_owners(num: u64) -> Option<Json<Vec<ListOwner>>> {
let db = Database::open_or_create_db().unwrap();
db.get_list_owners(num as i64).ok().map(|l| Json(l))
}
#[post("/lists/<num>/owners/add", data = "<new_owner>")]
fn lists_owner_add(num: u64, new_owner: ListOwner) -> Result<()> {
todo!()
}
#[get("/lists/<num>/policy")]
fn lists_policy(num: u64) -> Option<Json<Option<PostPolicy>>> {
let db = Database::open_or_create_db().unwrap();
db.get_list_policy(num as i64).ok().map(|l| Json(l))
}
fn main() {
rocket::ignite()
.mount(
"/",
routes![
index,
lists_members,
lists_num,
lists_owners,
lists_policy,
lists
],
)
.launch();
}

220
src/db.rs
View File

@ -1,220 +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 super::*;
use melib::Envelope;
pub use rusqlite::{self, params, Connection};
use std::convert::TryFrom;
use std::path::PathBuf;
const DB_NAME: &str = "mpot.db";
const INIT_SCRIPT: &str = "PRAGMA foreign_keys = true;
PRAGMA encoding = 'UTF-8';
CREATE TABLE IF NOT EXISTS mailing_lists (
pk INTEGER PRIMARY KEY,
name TEXT NOT NULL,
id TEXT NOT NULL,
address TEXT NOT NULL,
archive_url TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS membership (
list INTEGER NOT NULL,
address TEXT NOT NULL,
name TEXT,
digest BOOLEAN NOT NULL DEFAULT 0,
hide_address BOOLEAN NOT NULL DEFAULT 0,
receive_duplicates BOOLEAN NOT NULL DEFAULT 1,
receive_own_posts BOOLEAN NOT NULL DEFAULT 0,
receive_confirmation BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (list, address),
FOREIGN KEY (list) REFERENCES mailing_lists ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id);
CREATE INDEX IF NOT EXISTS membership_idx ON membership(address);";
pub struct Database {
connection: Connection,
}
impl Database {
pub fn list_lists(&self) -> Result<Vec<MailingList>> {
let mut stmt = self
.connection
.prepare("SELECT * FROM mailing_lists;")
.unwrap();
let res = stmt
.query_map(params![], |row| MailingList::try_from(row))?
.map(|r| r.map_err(|err| err.into()))
.collect();
res
}
pub fn list_members(&self, pk: i64) -> Result<Vec<ListMembership>> {
let mut stmt = self
.connection
.prepare("SELECT * FROM membership WHERE list = ?;")
.unwrap();
let res = stmt
.query_map(params![pk], |row| ListMembership::try_from(row))?
.map(|r| r.map_err(|err| err.into()))
.collect();
res
}
pub fn add_member(&self, list_pk: i64, new_val: ListMembership) -> Result<()> {
self.connection.execute(
"INSERT INTO membership (list, address, name, digest, hide_address) VALUES (?1, ?2, ?3, ?4, ?5);",
params![
&list_pk,
&new_val.address,
&new_val.name,
&new_val.digest,
&new_val.hide_address
],
)?;
Ok(())
}
pub fn remove_member(&self, list_pk: i64, address: &str) -> Result<()> {
if self.connection.execute(
"DELETE FROM membership WHERE list = ?1 AND address = ?2;",
params![&list_pk, &address,],
)? == 0
{
Err(format!("Address {} is not a member of this list.", address))?;
}
Ok(())
}
pub fn create_list(&self, new_val: MailingList) -> Result<()> {
self.connection.execute(
"INSERT INTO mailing_lists (name, id, address, description) VALUES (?1, ?2, ?3, ?4)",
params![
&new_val.name,
&new_val.id,
&new_val.address,
&new_val.description
],
)?;
Ok(())
}
pub fn db_path() -> Result<PathBuf> {
let name = DB_NAME;
let data_dir = xdg::BaseDirectories::with_prefix("mailpot")?;
Ok(data_dir.place_data_file(name)?)
}
pub fn open_db(db_path: PathBuf) -> Result<Self> {
if !db_path.exists() {
return Err("Database doesn't exist".into());
}
Ok(Database {
connection: Connection::open(&db_path)?,
})
}
pub fn open_or_create_db() -> Result<Self> {
let db_path = Self::db_path()?;
let mut set_mode = false;
if !db_path.exists() {
println!("Creating {} database in {}", DB_NAME, db_path.display());
set_mode = true;
}
let conn = Connection::open(&db_path)?;
if set_mode {
use std::os::unix::fs::PermissionsExt;
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)?;
}
conn.execute_batch(INIT_SCRIPT)?;
Ok(Database { connection: conn })
}
pub fn get_list_filters(&self, _list: &MailingList) -> Vec<Box<dyn crate::post::PostFilter>> {
use crate::post::*;
vec![
Box::new(FixCRLF),
Box::new(PostRightsCheck),
Box::new(AddListHeaders),
Box::new(FinalizeRecipients),
]
}
pub fn post(&self, env: Envelope, raw: &[u8]) -> Result<()> {
let mut lists = self.list_lists()?;
let tos = env
.to()
.iter()
.map(|addr| addr.get_email())
.collect::<Vec<String>>();
if tos.is_empty() {
return Err("Envelope To: field is empty!".into());
}
if env.from().is_empty() {
return Err("Envelope From: field is empty!".into());
}
lists.retain(|list| tos.iter().any(|a| a == &list.address));
if lists.is_empty() {
return Err("Envelope To: field doesn't contain any known lists!".into());
}
use crate::post::{Post, PostAction};
for mut list in lists {
let filters = self.get_list_filters(&list);
let memberships = self.list_members(list.pk)?;
let mut post = Post {
list: &mut list,
from: env.from()[0].clone(),
memberships: &memberships,
bytes: raw.to_vec(),
to: env.to().to_vec(),
action: PostAction::Defer {
reason: "Default action.".into(),
},
};
let result = {
let result: std::result::Result<_, String> = filters
.into_iter()
.fold(Ok(&mut post), |post, f| post.and_then(|p| f.feed(p)));
format!("{:#?}", result)
};
eprintln!("result for list {} is {}", list, result);
}
/*
use melib::futures;
use melib::smol;
use melib::smtp::*;
std::thread::spawn(|| smol::run(futures::future::pending::<()>()));
let mut conn = futures::executor::block_on(SmtpConnection::new_connection(conf)).unwrap();
futures::executor::block_on(conn.mail_transaction(raw, )).unwrap();
*/
Ok(())
}
}