25 changed files with 1881 additions and 367 deletions
-
980Cargo.lock
-
27Cargo.toml
-
8README.md
-
20cli/Cargo.toml
-
38cli/src/main.rs
-
23core/Cargo.toml
-
17core/README.md
-
32core/build.rs
-
5core/diesel.toml
-
0core/migrations/.gitkeep
-
8core/migrations/2020-08-16-164344_add_models/down.sql
-
63core/migrations/2020-08-16-164344_add_models/up.sql
-
34core/src/config.rs
-
236core/src/db.rs
-
5core/src/errors.rs
-
40core/src/lib.rs
-
97core/src/models.rs
-
64core/src/post.rs
-
76core/src/schema.rs
-
63core/src/schema.sql
-
67core/src/schema.sql.m4
-
25rest-http/Cargo.toml
-
7rest-http/README.md
-
93rest-http/src/main.rs
-
220src/db.rs
980
Cargo.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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", |
|||
] |
@ -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" |
@ -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" |
@ -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 |
|||
``` |
@ -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();
|
|||
}
|
@ -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,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; |
@ -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); |
@ -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(())
|
|||
}
|
|||
}
|
@ -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;
|
@ -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,
|
|||
);
|
@ -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); |
@ -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); |
@ -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"] |
@ -0,0 +1,7 @@ |
|||
# mailpot REST http server |
|||
|
|||
Current `rocket` requires Nightly. |
|||
|
|||
```shell |
|||
cargo +nightly run --bin mpot-http |
|||
``` |
@ -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();
|
|||
}
|
@ -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 {}", |