2020-08-15 22:10:49 +03:00
/*
* 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/>.
* /
2023-04-03 20:36:43 +03:00
//! Mailpot database and methods.
2022-11-20 18:26:51 +02:00
use super ::Configuration ;
2020-08-15 22:10:49 +03:00
use super ::* ;
2022-05-15 09:45:41 +03:00
use crate ::ErrorKind ::* ;
2020-08-15 22:10:49 +03:00
use melib ::Envelope ;
use models ::changesets ::* ;
use rusqlite ::Connection as DbConnection ;
use rusqlite ::OptionalExtension ;
2022-05-08 00:46:49 +03:00
use std ::io ::Write ;
use std ::process ::{ Command , Stdio } ;
2020-08-15 22:10:49 +03:00
2023-04-03 20:36:43 +03:00
/// A connection to a `mailpot` database.
pub struct Connection {
/// The `rusqlite` connection handle.
2020-08-15 22:10:49 +03:00
pub connection : DbConnection ,
2022-11-20 18:26:51 +02:00
conf : Configuration ,
2020-08-15 22:10:49 +03:00
}
2023-04-10 13:02:25 +03:00
impl std ::fmt ::Debug for Connection {
fn fmt ( & self , fmt : & mut std ::fmt ::Formatter ) -> std ::fmt ::Result {
fmt . debug_struct ( " Connection " )
. field ( " conf " , & self . conf )
. finish ( )
}
}
2022-06-05 14:14:00 +03:00
mod error_queue ;
pub use error_queue ::* ;
2022-06-05 14:17:31 +03:00
mod posts ;
pub use posts ::* ;
2023-04-13 22:25:20 +03:00
mod subscriptions ;
pub use subscriptions ::* ;
2023-04-05 10:41:38 +03:00
mod policies ;
pub use policies ::* ;
2022-06-05 14:14:00 +03:00
2023-04-03 17:18:14 +03:00
fn log_callback ( error_code : std ::ffi ::c_int , message : & str ) {
match error_code {
rusqlite ::ffi ::SQLITE_NOTICE = > log ::info! ( " {} " , message ) ,
rusqlite ::ffi ::SQLITE_WARNING = > log ::warn! ( " {} " , message ) ,
_ = > log ::error! ( " {error_code} {} " , message ) ,
}
}
2023-04-13 22:25:20 +03:00
// INSERT INTO subscription(list, address, name, enabled, digest, verified, hide_address, receive_duplicates, receive_own_posts, receive_confirmation) VALUES
2023-04-03 17:18:14 +03:00
fn user_authorizer_callback (
auth_context : rusqlite ::hooks ::AuthContext < '_ > ,
) -> rusqlite ::hooks ::Authorization {
use rusqlite ::hooks ::{ AuthAction , Authorization } ;
2023-04-03 20:36:43 +03:00
// [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this.
2023-04-03 17:18:14 +03:00
match auth_context . action {
AuthAction ::Delete {
2023-04-13 22:25:20 +03:00
table_name : " queue " | " candidate_subscription " | " subscription " ,
2023-04-03 17:18:14 +03:00
}
| AuthAction ::Insert {
2023-04-14 16:35:45 +03:00
table_name : " post " | " queue " | " candidate_subscription " | " subscription " | " account " ,
2023-04-03 17:18:14 +03:00
}
2023-04-04 13:58:22 +03:00
| AuthAction ::Update {
2023-04-14 14:30:05 +03:00
table_name : " candidate_subscription " | " templates " ,
2023-04-05 10:41:38 +03:00
column_name : " accepted " | " last_modified " | " verified " | " address " ,
2023-04-04 13:58:22 +03:00
}
2023-04-10 13:02:25 +03:00
| AuthAction ::Update {
2023-04-14 14:30:05 +03:00
table_name : " account " ,
column_name : " last_modified " | " name " | " public_key " | " password " ,
}
| AuthAction ::Update {
2023-04-13 22:25:20 +03:00
table_name : " subscription " ,
2023-04-10 13:02:25 +03:00
column_name :
" last_modified "
2023-04-13 22:25:20 +03:00
| " account "
2023-04-10 13:02:25 +03:00
| " digest "
2023-04-13 22:25:20 +03:00
| " verified "
2023-04-10 13:02:25 +03:00
| " hide_address "
| " receive_duplicates "
| " receive_own_posts "
| " receive_confirmation " ,
}
2023-04-03 17:18:14 +03:00
| AuthAction ::Select
| AuthAction ::Savepoint { .. }
| AuthAction ::Transaction { .. }
| AuthAction ::Read { .. }
| AuthAction ::Function {
2023-04-05 10:41:38 +03:00
function_name : " strftime " | " unixepoch " | " datetime " ,
2023-04-03 17:18:14 +03:00
} = > Authorization ::Allow ,
_ = > Authorization ::Deny ,
}
}
2023-04-03 20:36:43 +03:00
impl Connection {
/// Creates a new database connection.
///
/// `Connection` supports a limited subset of operations by default (see
/// [`Connection::untrusted`]).
/// Use [`Connection::trusted`] to remove these limits.
2023-04-03 17:18:14 +03:00
pub fn open_db ( conf : Configuration ) -> Result < Self > {
use rusqlite ::config ::DbConfig ;
use std ::sync ::Once ;
static INIT_SQLITE_LOGGING : Once = Once ::new ( ) ;
2022-11-20 18:26:51 +02:00
if ! conf . db_path . exists ( ) {
2022-05-15 10:41:59 +03:00
return Err ( " Database doesn't exist " . into ( ) ) ;
}
2023-04-03 17:18:14 +03:00
INIT_SQLITE_LOGGING . call_once ( | | {
unsafe { rusqlite ::trace ::config_log ( Some ( log_callback ) ) . unwrap ( ) } ;
} ) ;
let conn = DbConnection ::open ( conf . db_path . to_str ( ) . unwrap ( ) ) ? ;
conn . set_db_config ( DbConfig ::SQLITE_DBCONFIG_ENABLE_FKEY , true ) ? ;
conn . set_db_config ( DbConfig ::SQLITE_DBCONFIG_ENABLE_TRIGGER , true ) ? ;
conn . set_db_config ( DbConfig ::SQLITE_DBCONFIG_DEFENSIVE , true ) ? ;
conn . set_db_config ( DbConfig ::SQLITE_DBCONFIG_TRUSTED_SCHEMA , false ) ? ;
conn . busy_timeout ( core ::time ::Duration ::from_millis ( 500 ) ) ? ;
conn . busy_handler ( Some ( | times : i32 | -> bool { times < 5 } ) ) ? ;
conn . authorizer ( Some ( user_authorizer_callback ) ) ;
2023-04-04 13:58:22 +03:00
Ok ( Self {
2023-04-03 17:18:14 +03:00
conf ,
connection : conn ,
2022-05-15 10:41:59 +03:00
} )
}
2023-04-03 20:36:43 +03:00
/// Removes operational limits from this connection. (see [`Connection::untrusted`])
#[ must_use ]
2023-04-03 17:18:14 +03:00
pub fn trusted ( self ) -> Self {
self . connection
. authorizer ::< fn ( rusqlite ::hooks ::AuthContext < '_ > ) -> rusqlite ::hooks ::Authorization > (
None ,
) ;
self
}
2023-04-03 20:36:43 +03:00
// [tag:sync_auth_doc]
/// Sets operational limits for this connection.
///
2023-04-13 22:25:20 +03:00
/// - Allow `INSERT`, `DELETE` only for "queue", "candidate_subscription", "subscription".
/// - Allow `UPDATE` only for "subscription" user facing settings.
2023-04-03 20:36:43 +03:00
/// - Allow `INSERT` only for "post".
/// - Allow read access to all tables.
/// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime` function.
/// - Deny everything else.
2023-04-03 17:18:14 +03:00
pub fn untrusted ( self ) -> Self {
self . connection . authorizer ( Some ( user_authorizer_callback ) ) ;
self
}
2023-04-03 20:36:43 +03:00
/// Create a database if it doesn't exist and then open it.
2023-04-03 17:18:14 +03:00
pub fn open_or_create_db ( conf : Configuration ) -> Result < Self > {
if ! conf . db_path . exists ( ) {
let db_path = & conf . db_path ;
2022-05-15 10:41:59 +03:00
use std ::os ::unix ::fs ::PermissionsExt ;
2023-04-03 17:18:14 +03:00
info! ( " Creating database in {} " , db_path . display ( ) ) ;
2023-04-03 20:36:43 +03:00
std ::fs ::File ::create ( db_path ) . context ( " Could not create db path " ) ? ;
2023-04-03 17:18:14 +03:00
2022-05-15 10:41:59 +03:00
let mut child = Command ::new ( " sqlite3 " )
2023-04-03 20:36:43 +03:00
. arg ( db_path )
2022-05-15 10:41:59 +03:00
. stdin ( Stdio ::piped ( ) )
2022-07-17 15:33:04 +03:00
. stdout ( Stdio ::piped ( ) )
. stderr ( Stdio ::piped ( ) )
2022-05-15 10:41:59 +03:00
. spawn ( ) ? ;
let mut stdin = child . stdin . take ( ) . unwrap ( ) ;
std ::thread ::spawn ( move | | {
stdin
. write_all ( include_bytes! ( " ./schema.sql " ) )
. expect ( " failed to write to stdin " ) ;
2022-07-17 15:33:04 +03:00
stdin . flush ( ) . expect ( " could not flush stdin " ) ;
2022-05-15 10:41:59 +03:00
} ) ;
let output = child . wait_with_output ( ) ? ;
if ! output . status . success ( ) {
2022-07-17 15:33:04 +03:00
return Err ( format! ( " Could not initialize sqlite3 database at {} : sqlite3 returned exit code {} and stderr {} {} " , db_path . display ( ) , output . status . code ( ) . unwrap_or_default ( ) , String ::from_utf8_lossy ( & output . stderr ) , String ::from_utf8_lossy ( & output . stdout ) ) . into ( ) ) ;
2022-05-15 10:41:59 +03:00
}
2023-04-03 20:36:43 +03:00
let file = std ::fs ::File ::open ( db_path ) ? ;
2022-05-15 10:41:59 +03:00
let metadata = file . metadata ( ) ? ;
let mut permissions = metadata . permissions ( ) ;
permissions . set_mode ( 0o600 ) ; // Read/write for owner only.
file . set_permissions ( permissions ) ? ;
}
2023-04-03 17:18:14 +03:00
Self ::open_db ( conf )
}
2022-05-15 10:41:59 +03:00
2023-04-03 20:36:43 +03:00
/// Returns a connection's configuration.
2023-04-03 17:18:14 +03:00
pub fn conf ( & self ) -> & Configuration {
& self . conf
2022-05-15 10:41:59 +03:00
}
2023-04-03 20:36:43 +03:00
/// Loads archive databases from [`Configuration::data_path`], if any.
2023-04-03 17:18:14 +03:00
pub fn load_archives ( & self ) -> Result < ( ) > {
2022-05-15 10:41:59 +03:00
let mut stmt = self . connection . prepare ( " ATTACH ? AS ?; " ) ? ;
2023-04-03 17:18:14 +03:00
for archive in std ::fs ::read_dir ( & self . conf . data_path ) ? {
2022-05-15 10:41:59 +03:00
let archive = archive ? ;
let path = archive . path ( ) ;
let name = path . file_name ( ) . unwrap_or_default ( ) ;
2023-04-03 17:18:14 +03:00
if path = = self . conf . db_path {
2022-07-17 15:33:04 +03:00
continue ;
}
2022-05-15 10:41:59 +03:00
stmt . execute ( rusqlite ::params! [
path . to_str ( ) . unwrap ( ) ,
name . to_str ( ) . unwrap ( )
] ) ? ;
}
2023-04-03 17:18:14 +03:00
Ok ( ( ) )
2022-05-15 10:41:59 +03:00
}
2023-04-03 20:36:43 +03:00
/// Returns a vector of existing mailing lists.
pub fn lists ( & self ) -> Result < Vec < DbVal < MailingList > > > {
2023-04-05 10:41:38 +03:00
let mut stmt = self . connection . prepare ( " SELECT * FROM list; " ) ? ;
2020-08-15 22:10:49 +03:00
let list_iter = stmt . query_map ( [ ] , | row | {
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
MailingList {
pk ,
name : row . get ( " name " ) ? ,
id : row . get ( " id " ) ? ,
address : row . get ( " address " ) ? ,
description : row . get ( " description " ) ? ,
archive_url : row . get ( " archive_url " ) ? ,
} ,
pk ,
) )
} ) ? ;
let mut ret = vec! [ ] ;
for list in list_iter {
let list = list ? ;
ret . push ( list ) ;
}
Ok ( ret )
}
2023-04-03 20:36:43 +03:00
/// Fetch a mailing list by primary key.
2023-04-15 17:25:37 +03:00
pub fn list ( & self , pk : i64 ) -> Result < Option < DbVal < MailingList > > > {
2020-08-15 22:10:49 +03:00
let mut stmt = self
. connection
2023-04-05 10:41:38 +03:00
. prepare ( " SELECT * FROM list WHERE pk = ?; " ) ? ;
2020-08-15 22:10:49 +03:00
let ret = stmt
. query_row ( [ & pk ] , | row | {
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
MailingList {
pk ,
name : row . get ( " name " ) ? ,
id : row . get ( " id " ) ? ,
address : row . get ( " address " ) ? ,
description : row . get ( " description " ) ? ,
archive_url : row . get ( " archive_url " ) ? ,
} ,
pk ,
) )
} )
. optional ( ) ? ;
2023-04-15 17:25:37 +03:00
Ok ( ret )
2022-05-15 09:46:08 +03:00
}
2023-04-03 20:36:43 +03:00
/// Fetch a mailing list by id.
pub fn list_by_id < S : AsRef < str > > ( & self , id : S ) -> Result < Option < DbVal < MailingList > > > {
2022-05-15 09:46:08 +03:00
let id = id . as_ref ( ) ;
let mut stmt = self
. connection
2023-04-05 10:41:38 +03:00
. prepare ( " SELECT * FROM list WHERE id = ?; " ) ? ;
2022-05-15 09:46:08 +03:00
let ret = stmt
. query_row ( [ & id ] , | row | {
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
MailingList {
pk ,
name : row . get ( " name " ) ? ,
id : row . get ( " id " ) ? ,
address : row . get ( " address " ) ? ,
description : row . get ( " description " ) ? ,
archive_url : row . get ( " archive_url " ) ? ,
} ,
pk ,
) )
} )
. optional ( ) ? ;
Ok ( ret )
2020-08-15 22:10:49 +03:00
}
2023-04-03 20:36:43 +03:00
/// Create a new list.
2020-08-15 22:10:49 +03:00
pub fn create_list ( & self , new_val : MailingList ) -> Result < DbVal < MailingList > > {
let mut stmt = self
. connection
2023-04-05 10:41:38 +03:00
. prepare ( " INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) RETURNING *; " ) ? ;
2020-08-15 22:10:49 +03:00
let ret = stmt . query_row (
rusqlite ::params! [
& new_val . name ,
& new_val . id ,
& new_val . address ,
new_val . description . as_ref ( ) ,
new_val . archive_url . as_ref ( ) ,
] ,
| row | {
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
MailingList {
pk ,
name : row . get ( " name " ) ? ,
id : row . get ( " id " ) ? ,
address : row . get ( " address " ) ? ,
description : row . get ( " description " ) ? ,
archive_url : row . get ( " archive_url " ) ? ,
} ,
pk ,
) )
} ,
) ? ;
trace! ( " create_list {:?}. " , & ret ) ;
Ok ( ret )
}
2023-04-03 20:36:43 +03:00
/// Fetch all posts of a mailing list.
2022-05-08 02:51:25 +03:00
pub fn list_posts (
& self ,
list_pk : i64 ,
2022-05-14 08:47:51 +03:00
_date_range : Option < ( String , String ) > ,
2022-05-08 02:51:25 +03:00
) -> Result < Vec < DbVal < Post > > > {
let mut stmt = self
. connection
2023-04-14 22:46:22 +03:00
. prepare ( " SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year FROM post WHERE list = ?; " ) ? ;
let iter = stmt . query_map ( rusqlite ::params! [ & list_pk ] , | row | {
2022-05-08 02:51:25 +03:00
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
Post {
pk ,
list : row . get ( " list " ) ? ,
2023-04-14 22:46:22 +03:00
envelope_from : row . get ( " envelope_from " ) ? ,
2022-05-08 02:51:25 +03:00
address : row . get ( " address " ) ? ,
message_id : row . get ( " message_id " ) ? ,
message : row . get ( " message " ) ? ,
timestamp : row . get ( " timestamp " ) ? ,
datetime : row . get ( " datetime " ) ? ,
2023-04-01 17:12:22 +03:00
month_year : row . get ( " month_year " ) ? ,
2022-05-08 02:51:25 +03:00
} ,
pk ,
) )
} ) ? ;
let mut ret = vec! [ ] ;
for post in iter {
let post = post ? ;
ret . push ( post ) ;
}
trace! ( " list_posts {:?}. " , & ret ) ;
Ok ( ret )
}
2023-04-03 20:36:43 +03:00
/// Fetch the owners of a mailing list.
pub fn list_owners ( & self , pk : i64 ) -> Result < Vec < DbVal < ListOwner > > > {
2020-08-15 22:10:49 +03:00
let mut stmt = self
. connection
2023-04-05 10:41:38 +03:00
. prepare ( " SELECT * FROM owner WHERE list = ?; " ) ? ;
2020-08-15 22:10:49 +03:00
let list_iter = stmt . query_map ( [ & pk ] , | row | {
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
ListOwner {
pk ,
list : row . get ( " list " ) ? ,
address : row . get ( " address " ) ? ,
name : row . get ( " name " ) ? ,
} ,
pk ,
) )
} ) ? ;
let mut ret = vec! [ ] ;
for list in list_iter {
let list = list ? ;
ret . push ( list ) ;
}
Ok ( ret )
}
2023-04-03 20:36:43 +03:00
/// Remove an owner of a mailing list.
2022-05-14 08:45:09 +03:00
pub fn remove_list_owner ( & self , list_pk : i64 , owner_pk : i64 ) -> Result < ( ) > {
2022-05-15 09:45:41 +03:00
self . connection
2023-04-03 17:18:14 +03:00
. query_row (
2023-04-05 10:41:38 +03:00
" DELETE FROM owner WHERE list = ? AND pk = ? RETURNING *; " ,
2022-05-15 09:45:41 +03:00
rusqlite ::params! [ & list_pk , & owner_pk ] ,
2023-04-03 17:18:14 +03:00
| _ | Ok ( ( ) ) ,
2022-05-15 09:45:41 +03:00
)
2023-04-03 17:18:14 +03:00
. map_err ( | err | {
if matches! ( err , rusqlite ::Error ::QueryReturnedNoRows ) {
Error ::from ( err ) . chain_err ( | | NotFound ( " list or list owner not found! " ) )
} else {
err . into ( )
}
} ) ? ;
2022-05-14 08:45:09 +03:00
Ok ( ( ) )
}
2023-04-03 20:36:43 +03:00
/// Add an owner of a mailing list.
2023-04-03 17:18:14 +03:00
pub fn add_list_owner ( & self , list_owner : ListOwner ) -> Result < DbVal < ListOwner > > {
2022-05-14 08:45:09 +03:00
let mut stmt = self . connection . prepare (
2023-04-05 10:41:38 +03:00
" INSERT OR REPLACE INTO owner(list, address, name) VALUES (?, ?, ?) RETURNING *; " ,
2022-05-14 08:45:09 +03:00
) ? ;
2023-04-03 17:18:14 +03:00
let list_pk = list_owner . list ;
let ret = stmt
. query_row (
rusqlite ::params! [ & list_pk , & list_owner . address , & list_owner . name , ] ,
| row | {
let pk = row . get ( " pk " ) ? ;
Ok ( DbVal (
ListOwner {
pk ,
list : row . get ( " list " ) ? ,
address : row . get ( " address " ) ? ,
name : row . get ( " name " ) ? ,
} ,
2022-05-14 08:45:09 +03:00
pk ,
2023-04-03 17:18:14 +03:00
) )
} ,
)
. map_err ( | err | {
if matches! (
err ,
rusqlite ::Error ::SqliteFailure (
rusqlite ::ffi ::Error {
code : rusqlite ::ffi ::ErrorCode ::ConstraintViolation ,
extended_code : 787
} ,
_
)
) {
Error ::from ( err ) . chain_err ( | | NotFound ( " Could not find a list with this pk. " ) )
} else {
err . into ( )
}
} ) ? ;
2022-05-14 08:45:09 +03:00
trace! ( " add_list_owner {:?}. " , & ret ) ;
Ok ( ret )
}
2023-04-03 20:36:43 +03:00
/// Update a mailing list.
pub fn update_list ( & mut self , change_set : MailingListChangeset ) -> Result < ( ) > {
if matches! (
change_set ,
MailingListChangeset {
pk : _ ,
name : None ,
id : None ,
address : None ,
description : None ,
2023-04-05 10:41:38 +03:00
archive_url : None ,
owner_local_part : None ,
request_local_part : None ,
verify : None ,
hidden : None ,
enabled : None ,
2023-04-03 20:36:43 +03:00
}
) {
return self . list ( change_set . pk ) . map ( | _ | ( ) ) ;
}
let MailingListChangeset {
pk ,
name ,
id ,
address ,
description ,
archive_url ,
2023-04-05 10:41:38 +03:00
owner_local_part ,
request_local_part ,
verify ,
hidden ,
enabled ,
2023-04-03 20:36:43 +03:00
} = change_set ;
let tx = self . connection . transaction ( ) ? ;
macro_rules ! update {
( $field :tt ) = > { {
if let Some ( $field ) = $field {
tx . execute (
2023-04-05 10:41:38 +03:00
concat! ( " UPDATE list SET " , stringify! ( $field ) , " = ? WHERE pk = ?; " ) ,
2023-04-03 20:36:43 +03:00
rusqlite ::params! [ & $field , & pk ] ,
) ? ;
}
} } ;
}
update! ( name ) ;
update! ( id ) ;
update! ( address ) ;
update! ( description ) ;
update! ( archive_url ) ;
2023-04-05 10:41:38 +03:00
update! ( owner_local_part ) ;
update! ( request_local_part ) ;
update! ( verify ) ;
update! ( hidden ) ;
update! ( enabled ) ;
2023-04-03 20:36:43 +03:00
tx . commit ( ) ? ;
Ok ( ( ) )
}
/// Return the post filters of a mailing list.
pub fn list_filters (
2020-08-15 22:10:49 +03:00
& self ,
_list : & DbVal < MailingList > ,
) -> Vec < Box < dyn crate ::mail ::message_filters ::PostFilter > > {
use crate ::mail ::message_filters ::* ;
vec! [
Box ::new ( FixCRLF ) ,
Box ::new ( PostRightsCheck ) ,
Box ::new ( AddListHeaders ) ,
Box ::new ( FinalizeRecipients ) ,
]
}
}