Browse Source

Split into crates

main
Manos Pitsidianakis 1 year ago
parent
commit
3c582f7729
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 980
      Cargo.lock
  2. 27
      Cargo.toml
  3. 8
      README.md
  4. 20
      cli/Cargo.toml
  5. 38
      cli/src/main.rs
  6. 23
      core/Cargo.toml
  7. 17
      core/README.md
  8. 32
      core/build.rs
  9. 5
      core/diesel.toml
  10. 0
      core/migrations/.gitkeep
  11. 8
      core/migrations/2020-08-16-164344_add_models/down.sql
  12. 63
      core/migrations/2020-08-16-164344_add_models/up.sql
  13. 34
      core/src/config.rs
  14. 236
      core/src/db.rs
  15. 5
      core/src/errors.rs
  16. 40
      core/src/lib.rs
  17. 97
      core/src/models.rs
  18. 64
      core/src/post.rs
  19. 76
      core/src/schema.rs
  20. 63
      core/src/schema.sql
  21. 67
      core/src/schema.sql.m4
  22. 25
      rest-http/Cargo.toml
  23. 7
      rest-http/README.md
  24. 93
      rest-http/src/main.rs
  25. 220
      src/db.rs

980
Cargo.lock
File diff suppressed because it is too large
View File

27
Cargo.toml

@ -1,21 +1,6 @@
[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"]
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"
[workspace]
members = [
"core",
"cli",
"rest-http",
]

8
README.md

@ -1,5 +1,11 @@
# 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
% mpot help

20
cli/Cargo.toml

@ -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"

38
src/main.rs → cli/src/main.rs

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

23
core/Cargo.toml

@ -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

@ -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

@ -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

@ -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"

0
core/migrations/.gitkeep

8
core/migrations/2020-08-16-164344_add_models/down.sql

@ -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;

63
core/migrations/2020-08-16-164344_add_models/up.sql

@ -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);

34
src/config.rs → core/src/config.rs

@ -19,10 +19,13 @@
use super::errors::*;
use chrono::prelude::*;
use std::io::Write;
use std::cell::RefCell;
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
thread_local!(pub static CONFIG: RefCell<Configuration> = RefCell::new(Configuration::new()));
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", content = "value")]
pub enum SendMail {
@ -32,10 +35,33 @@ pub enum SendMail {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Configuration {
send_mail: SendMail,
pub send_mail: SendMail,
#[serde(default = "default_storage_fn")]
pub storage: String,
}
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> {
Ok(xdg::BaseDirectories::with_prefix("mailpot")?.get_data_home())
}
@ -74,3 +100,7 @@ impl Configuration {
Self::save_message_to_path(&msg, temp_path)
}
}
fn default_storage_fn() -> String {
"sqlite3".to_string()
}

236
core/src/db.rs

@ -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(())
}
}

5
src/errors.rs → core/src/errors.rs

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

40
core/src/lib.rs

@ -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;

97
src/models.rs → core/src/models.rs

@ -18,12 +18,12 @@
*/
use super::*;
use rusqlite::Row;
use std::convert::TryFrom;
use schema::*;
#[derive(Debug)]
#[derive(Debug, Clone, Insertable, Queryable, Deserialize, Serialize)]
#[table_name = "mailing_lists"]
pub struct MailingList {
pub pk: i64,
pub pk: i32,
pub name: String,
pub id: 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 {
pub fn list_id(&self) -> String {
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 list: i64,
pub list: i32,
pub address: String,
pub name: Option<String>,
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 {
pub fn into_address(&self) -> 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,
}
}
}

64
src/post.rs → core/src/post.rs

@ -22,6 +22,7 @@ use melib::Address;
#[derive(Debug)]
pub enum PostAction {
Hold,
Accept {
recipients: Vec<Address>,
digests: Vec<Address>,
@ -37,8 +38,10 @@ pub enum PostAction {
///Post to be considered by the list's `PostFilter` stack.
pub struct Post<'list> {
pub list: &'list mut MailingList,
pub list_owners: Vec<ListOwner>,
pub from: Address,
pub memberships: &'list [ListMembership],
pub policy: Option<PostPolicy>,
pub bytes: Vec<u8>,
pub to: Vec<Address>,
pub action: PostAction,
@ -51,6 +54,7 @@ impl<'list> core::fmt::Debug for Post<'list> {
.field("from", &self.from)
.field("members", &format_args!("{}", self.memberships.len()))
.field("bytes", &format_args!("{}", self.bytes.len()))
.field("policy", &self.policy)
.field("to", &self.to.as_slice())
.field("action", &self.action)
.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
///describing the error and optionally set `post.action` to `Reject` or `Defer`
pub trait PostFilter {
fn feed<'list>(
fn feed<'p, 'list>(
self: Box<Self>,
post: &'list mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String>;
post: &'p mut Post<'list>,
) -> std::result::Result<&'p mut Post<'list>, String>;
}
///Check that submitter can post to list, for now it accepts everything.
pub struct PostRightsCheck;
impl PostFilter for PostRightsCheck {
fn feed<'list>(
fn feed<'p, 'list>(
self: Box<Self>,
post: &'list mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> {
post: &'p mut Post<'list>,
) -> 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)
}
}
@ -80,10 +108,10 @@ impl PostFilter for PostRightsCheck {
///Ensure message contains only `\r\n` line terminators, required by SMTP.
pub struct FixCRLF;
impl PostFilter for FixCRLF {
fn feed<'list>(
fn feed<'p, 'list>(
self: Box<Self>,
post: &'list mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> {
post: &'p mut Post<'list>,
) -> std::result::Result<&'p mut Post<'list>, String> {
use std::io::prelude::*;
let mut new_vec = Vec::with_capacity(post.bytes.len());
for line in post.bytes.lines() {
@ -98,10 +126,10 @@ impl PostFilter for FixCRLF {
///Add `List-*` headers
pub struct AddListHeaders;
impl PostFilter for AddListHeaders {
fn feed<'list>(
fn feed<'p, 'list>(
self: Box<Self>,
post: &'list mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> {
post: &'p mut Post<'list>,
) -> std::result::Result<&'p mut Post<'list>, String> {
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
let list_id = post.list.list_id();
headers.push((&b"List-ID"[..], list_id.as_bytes()));
@ -142,10 +170,10 @@ impl PostFilter for AddListHeaders {
///Adds `Archived-At` field, if configured.
pub struct ArchivedAtLink;
impl PostFilter for ArchivedAtLink {
fn feed<'list>(
fn feed<'p, 'list>(
self: Box<Self>,
post: &'list mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> {
post: &'p mut Post<'list>,
) -> std::result::Result<&'p mut Post<'list>, String> {
Ok(post)
}
}
@ -154,10 +182,10 @@ impl PostFilter for ArchivedAtLink {
///will receive the post in `post.action` field.
pub struct FinalizeRecipients;
impl PostFilter for FinalizeRecipients {
fn feed<'list>(
fn feed<'p, 'list>(
self: Box<Self>,
post: &'list mut Post<'list>,
) -> std::result::Result<&'list mut Post<'list>, String> {
post: &'p mut Post<'list>,
) -> std::result::Result<&'p mut Post<'list>, String> {
let mut recipients = vec![];
let mut digests = vec![];
let email_from = post.from.get_email();

76
core/src/schema.rs

@ -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,
);

63
core/src/schema.sql

@ -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);

67
core/src/schema.sql.m4

@ -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);

25
rest-http/Cargo.toml

@ -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"]

7
rest-http/README.md

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

93
rest-http/src/main.rs

@ -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

@ -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 {}",