1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
//! Mailpot database and methods.
21

            
22
use std::{
23
    io::Write,
24
    process::{Command, Stdio},
25
};
26

            
27
use melib::Envelope;
28
use models::changesets::*;
29
use rusqlite::{Connection as DbConnection, OptionalExtension};
30

            
31
use super::{Configuration, *};
32
use crate::ErrorKind::*;
33

            
34
/// A connection to a `mailpot` database.
35
pub struct Connection {
36
    /// The `rusqlite` connection handle.
37
    pub connection: DbConnection,
38
    conf: Configuration,
39
}
40

            
41
impl std::fmt::Debug for Connection {
42
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
43
        fmt.debug_struct("Connection")
44
            .field("conf", &self.conf)
45
            .finish()
46
    }
47
}
48

            
49
impl Drop for Connection {
50
30
    fn drop(&mut self) {
51
30
        self.connection
52
            .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>(
53
30
                None,
54
            );
55
        // make sure pragma optimize does not take too long
56
30
        _ = self.connection.pragma_update(None, "analysis_limit", "400");
57
        // gather statistics to improve query optimization
58
30
        _ = self
59
            .connection
60
60
            .pragma(None, "optimize", 0xfffe_i64, |_| Ok(()));
61
30
    }
62
}
63

            
64
mod templates;
65
pub use templates::*;
66
mod queue;
67
pub use queue::*;
68
mod posts;
69
pub use posts::*;
70
mod subscriptions;
71
pub use subscriptions::*;
72
mod policies;
73
pub use policies::*;
74

            
75
14
fn log_callback(error_code: std::ffi::c_int, message: &str) {
76
14
    match error_code {
77
        rusqlite::ffi::SQLITE_OK
78
        | rusqlite::ffi::SQLITE_DONE
79
        | rusqlite::ffi::SQLITE_NOTICE
80
        | rusqlite::ffi::SQLITE_NOTICE_RECOVER_WAL
81
        | rusqlite::ffi::SQLITE_NOTICE_RECOVER_ROLLBACK => log::info!("{}", message),
82
        rusqlite::ffi::SQLITE_WARNING | rusqlite::ffi::SQLITE_WARNING_AUTOINDEX => {
83
10
            log::warn!("{}", message)
84
        }
85
4
        _ => log::error!("{error_code} {}", message),
86
    }
87
14
}
88

            
89
1570
fn user_authorizer_callback(
90
    auth_context: rusqlite::hooks::AuthContext<'_>,
91
) -> rusqlite::hooks::Authorization {
92
    use rusqlite::hooks::{AuthAction, Authorization};
93

            
94
    // [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this.
95
1570
    match auth_context.action {
96
        AuthAction::Delete {
97
2
            table_name: "queue" | "candidate_subscription" | "subscription",
98
        }
99
        | AuthAction::Insert {
100
18
            table_name: "post" | "queue" | "candidate_subscription" | "subscription" | "account",
101
        }
102
        | AuthAction::Update {
103
37
            table_name: "candidate_subscription" | "templates",
104
12
            column_name: "accepted" | "last_modified" | "verified" | "address",
105
        }
106
        | AuthAction::Update {
107
25
            table_name: "account",
108
2
            column_name: "last_modified" | "name" | "public_key" | "password",
109
        }
110
        | AuthAction::Update {
111
23
            table_name: "subscription",
112
            column_name:
113
23
                "last_modified"
114
9
                | "account"
115
4
                | "digest"
116
4
                | "verified"
117
                | "hide_address"
118
                | "receive_duplicates"
119
                | "receive_own_posts"
120
                | "receive_confirmation",
121
        }
122
        | AuthAction::Select
123
        | AuthAction::Savepoint { .. }
124
        | AuthAction::Transaction { .. }
125
        | AuthAction::Read { .. }
126
        | AuthAction::Function {
127
30
            function_name: "count" | "strftime" | "unixepoch" | "datetime",
128
1566
        } => Authorization::Allow,
129
4
        _ => Authorization::Deny,
130
    }
131
1570
}
132

            
133
impl Connection {
134
    /// Creates a new database connection.
135
    ///
136
    /// `Connection` supports a limited subset of operations by default (see
137
    /// [`Connection::untrusted`]).
138
    /// Use [`Connection::trusted`] to remove these limits.
139
30
    pub fn open_db(conf: Configuration) -> Result<Self> {
140
        use std::sync::Once;
141

            
142
        use rusqlite::config::DbConfig;
143

            
144
        static INIT_SQLITE_LOGGING: Once = Once::new();
145

            
146
30
        if !conf.db_path.exists() {
147
            return Err("Database doesn't exist".into());
148
        }
149
56
        INIT_SQLITE_LOGGING.call_once(|| {
150
26
            unsafe { rusqlite::trace::config_log(Some(log_callback)).unwrap() };
151
26
        });
152
30
        let conn = DbConnection::open(conf.db_path.to_str().unwrap())?;
153
30
        rusqlite::vtab::array::load_module(&conn)?;
154
30
        conn.pragma_update(None, "journal_mode", "WAL")?;
155
30
        conn.pragma_update(None, "foreign_keys", "on")?;
156
        // synchronise less often to the filesystem
157
30
        conn.pragma_update(None, "synchronous", "normal")?;
158
30
        conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?;
159
30
        conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?;
160
30
        conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
161
30
        conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?;
162
30
        conn.busy_timeout(core::time::Duration::from_millis(500))?;
163
30
        conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
164
30
        conn.authorizer(Some(user_authorizer_callback));
165
30
        Ok(Self {
166
30
            conf,
167
30
            connection: conn,
168
        })
169
30
    }
170

            
171
    /// Removes operational limits from this connection. (see
172
    /// [`Connection::untrusted`])
173
    #[must_use]
174
29
    pub fn trusted(self) -> Self {
175
29
        self.connection
176
            .authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>(
177
29
                None,
178
            );
179
29
        self
180
29
    }
181

            
182
    // [tag:sync_auth_doc]
183
    /// Sets operational limits for this connection.
184
    ///
185
    /// - Allow `INSERT`, `DELETE` only for "queue", "candidate_subscription",
186
    ///   "subscription".
187
    /// - Allow `UPDATE` only for "subscription" user facing settings.
188
    /// - Allow `INSERT` only for "post".
189
    /// - Allow read access to all tables.
190
    /// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime`
191
    ///   function.
192
    /// - Deny everything else.
193
5
    pub fn untrusted(self) -> Self {
194
5
        self.connection.authorizer(Some(user_authorizer_callback));
195
5
        self
196
5
    }
197

            
198
    /// Create a database if it doesn't exist and then open it.
199
30
    pub fn open_or_create_db(conf: Configuration) -> Result<Self> {
200
30
        if !conf.db_path.exists() {
201
17
            let db_path = &conf.db_path;
202
            use std::os::unix::fs::PermissionsExt;
203

            
204
17
            info!("Creating database in {}", db_path.display());
205
17
            std::fs::File::create(db_path).context("Could not create db path")?;
206

            
207
68
            let mut child = Command::new("sqlite3")
208
                .arg(db_path)
209
17
                .stdin(Stdio::piped())
210
17
                .stdout(Stdio::piped())
211
17
                .stderr(Stdio::piped())
212
17
                .spawn()?;
213
17
            let mut stdin = child.stdin.take().unwrap();
214
34
            std::thread::spawn(move || {
215
17
                stdin
216
                    .write_all(include_bytes!("./schema.sql"))
217
                    .expect("failed to write to stdin");
218
17
                stdin.flush().expect("could not flush stdin");
219
34
            });
220
17
            let output = child.wait_with_output()?;
221
17
            if !output.status.success() {
222
                return Err(format!(
223
                    "Could not initialize sqlite3 database at {}: sqlite3 returned exit code {} \
224
                     and stderr {} {}",
225
                    db_path.display(),
226
                    output.status.code().unwrap_or_default(),
227
                    String::from_utf8_lossy(&output.stderr),
228
                    String::from_utf8_lossy(&output.stdout)
229
                )
230
                .into());
231
            }
232

            
233
17
            let file = std::fs::File::open(db_path)?;
234
17
            let metadata = file.metadata()?;
235
17
            let mut permissions = metadata.permissions();
236

            
237
17
            permissions.set_mode(0o600); // Read/write for owner only.
238
17
            file.set_permissions(permissions)?;
239
17
        }
240
30
        Self::open_db(conf)
241
30
    }
242

            
243
    /// Returns a connection's configuration.
244
16
    pub fn conf(&self) -> &Configuration {
245
16
        &self.conf
246
16
    }
247

            
248
    /// Loads archive databases from [`Configuration::data_path`], if any.
249
    pub fn load_archives(&self) -> Result<()> {
250
        let mut stmt = self.connection.prepare("ATTACH ? AS ?;")?;
251
        for archive in std::fs::read_dir(&self.conf.data_path)? {
252
            let archive = archive?;
253
            let path = archive.path();
254
            let name = path.file_name().unwrap_or_default();
255
            if path == self.conf.db_path {
256
                continue;
257
            }
258
            stmt.execute(rusqlite::params![
259
                path.to_str().unwrap(),
260
                name.to_str().unwrap()
261
            ])?;
262
        }
263

            
264
        Ok(())
265
    }
266

            
267
    /// Returns a vector of existing mailing lists.
268
38
    pub fn lists(&self) -> Result<Vec<DbVal<MailingList>>> {
269
38
        let mut stmt = self.connection.prepare("SELECT * FROM list;")?;
270
68
        let list_iter = stmt.query_map([], |row| {
271
30
            let pk = row.get("pk")?;
272
30
            Ok(DbVal(
273
30
                MailingList {
274
                    pk,
275
30
                    name: row.get("name")?,
276
30
                    id: row.get("id")?,
277
30
                    address: row.get("address")?,
278
30
                    description: row.get("description")?,
279
30
                    archive_url: row.get("archive_url")?,
280
                },
281
                pk,
282
            ))
283
30
        })?;
284

            
285
38
        let mut ret = vec![];
286
68
        for list in list_iter {
287
30
            let list = list?;
288
30
            ret.push(list);
289
        }
290
38
        Ok(ret)
291
38
    }
292

            
293
    /// Fetch a mailing list by primary key.
294
    pub fn list(&self, pk: i64) -> Result<Option<DbVal<MailingList>>> {
295
        let mut stmt = self
296
            .connection
297
            .prepare("SELECT * FROM list WHERE pk = ?;")?;
298
        let ret = stmt
299
            .query_row([&pk], |row| {
300
                let pk = row.get("pk")?;
301
                Ok(DbVal(
302
                    MailingList {
303
                        pk,
304
                        name: row.get("name")?,
305
                        id: row.get("id")?,
306
                        address: row.get("address")?,
307
                        description: row.get("description")?,
308
                        archive_url: row.get("archive_url")?,
309
                    },
310
                    pk,
311
                ))
312
            })
313
            .optional()?;
314
        Ok(ret)
315
    }
316

            
317
    /// Fetch a mailing list by id.
318
    pub fn list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> {
319
        let id = id.as_ref();
320
        let mut stmt = self
321
            .connection
322
            .prepare("SELECT * FROM list WHERE id = ?;")?;
323
        let ret = stmt
324
            .query_row([&id], |row| {
325
                let pk = row.get("pk")?;
326
                Ok(DbVal(
327
                    MailingList {
328
                        pk,
329
                        name: row.get("name")?,
330
                        id: row.get("id")?,
331
                        address: row.get("address")?,
332
                        description: row.get("description")?,
333
                        archive_url: row.get("archive_url")?,
334
                    },
335
                    pk,
336
                ))
337
            })
338
            .optional()?;
339

            
340
        Ok(ret)
341
    }
342

            
343
    /// Create a new list.
344
19
    pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
345
19
        let mut stmt = self.connection.prepare(
346
            "INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \
347
             RETURNING *;",
348
1
        )?;
349
18
        let ret = stmt.query_row(
350
18
            rusqlite::params![
351
18
                &new_val.name,
352
18
                &new_val.id,
353
18
                &new_val.address,
354
18
                new_val.description.as_ref(),
355
18
                new_val.archive_url.as_ref(),
356
            ],
357
18
            |row| {
358
18
                let pk = row.get("pk")?;
359
18
                Ok(DbVal(
360
18
                    MailingList {
361
                        pk,
362
18
                        name: row.get("name")?,
363
18
                        id: row.get("id")?,
364
18
                        address: row.get("address")?,
365
18
                        description: row.get("description")?,
366
18
                        archive_url: row.get("archive_url")?,
367
                    },
368
                    pk,
369
                ))
370
18
            },
371
        )?;
372

            
373
18
        trace!("create_list {:?}.", &ret);
374
18
        Ok(ret)
375
19
    }
376

            
377
    /// Fetch all posts of a mailing list.
378
7
    pub fn list_posts(
379
        &self,
380
        list_pk: i64,
381
        _date_range: Option<(String, String)>,
382
    ) -> Result<Vec<DbVal<Post>>> {
383
7
        let mut stmt = self.connection.prepare(
384
            "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \
385
             FROM post WHERE list = ?;",
386
        )?;
387
10
        let iter = stmt.query_map(rusqlite::params![&list_pk], |row| {
388
3
            let pk = row.get("pk")?;
389
3
            Ok(DbVal(
390
3
                Post {
391
                    pk,
392
3
                    list: row.get("list")?,
393
3
                    envelope_from: row.get("envelope_from")?,
394
3
                    address: row.get("address")?,
395
3
                    message_id: row.get("message_id")?,
396
3
                    message: row.get("message")?,
397
3
                    timestamp: row.get("timestamp")?,
398
3
                    datetime: row.get("datetime")?,
399
3
                    month_year: row.get("month_year")?,
400
                },
401
                pk,
402
            ))
403
3
        })?;
404
7
        let mut ret = vec![];
405
10
        for post in iter {
406
3
            let post = post?;
407
3
            ret.push(post);
408
        }
409

            
410
7
        trace!("list_posts {:?}.", &ret);
411
7
        Ok(ret)
412
7
    }
413

            
414
    /// Fetch the owners of a mailing list.
415
11
    pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
416
11
        let mut stmt = self
417
            .connection
418
            .prepare("SELECT * FROM owner WHERE list = ?;")?;
419
11
        let list_iter = stmt.query_map([&pk], |row| {
420
            let pk = row.get("pk")?;
421
            Ok(DbVal(
422
                ListOwner {
423
                    pk,
424
                    list: row.get("list")?,
425
                    address: row.get("address")?,
426
                    name: row.get("name")?,
427
                },
428
                pk,
429
            ))
430
        })?;
431

            
432
11
        let mut ret = vec![];
433
11
        for list in list_iter {
434
            let list = list?;
435
            ret.push(list);
436
        }
437
11
        Ok(ret)
438
11
    }
439

            
440
    /// Remove an owner of a mailing list.
441
2
    pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> {
442
4
        self.connection
443
            .query_row(
444
                "DELETE FROM owner WHERE list = ? AND pk = ? RETURNING *;",
445
2
                rusqlite::params![&list_pk, &owner_pk],
446
1
                |_| Ok(()),
447
            )
448
1
            .map_err(|err| {
449
1
                if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
450
                    Error::from(err).chain_err(|| NotFound("list or list owner not found!"))
451
                } else {
452
1
                    err.into()
453
                }
454
2
            })?;
455
1
        Ok(())
456
2
    }
457

            
458
    /// Add an owner of a mailing list.
459
1
    pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> {
460
1
        let mut stmt = self.connection.prepare(
461
            "INSERT OR REPLACE INTO owner(list, address, name) VALUES (?, ?, ?) RETURNING *;",
462
        )?;
463
1
        let list_pk = list_owner.list;
464
1
        let ret = stmt
465
            .query_row(
466
1
                rusqlite::params![&list_pk, &list_owner.address, &list_owner.name,],
467
1
                |row| {
468
1
                    let pk = row.get("pk")?;
469
1
                    Ok(DbVal(
470
1
                        ListOwner {
471
                            pk,
472
1
                            list: row.get("list")?,
473
1
                            address: row.get("address")?,
474
1
                            name: row.get("name")?,
475
                        },
476
                        pk,
477
                    ))
478
1
                },
479
            )
480
            .map_err(|err| {
481
                if matches!(
482
                    err,
483
                    rusqlite::Error::SqliteFailure(
484
                        rusqlite::ffi::Error {
485
                            code: rusqlite::ffi::ErrorCode::ConstraintViolation,
486
                            extended_code: 787
487
                        },
488
                        _
489
                    )
490
                ) {
491
                    Error::from(err).chain_err(|| NotFound("Could not find a list with this pk."))
492
                } else {
493
                    err.into()
494
                }
495
            })?;
496

            
497
1
        trace!("add_list_owner {:?}.", &ret);
498
1
        Ok(ret)
499
1
    }
500

            
501
    /// Update a mailing list.
502
    pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> {
503
        if matches!(
504
            change_set,
505
            MailingListChangeset {
506
                pk: _,
507
                name: None,
508
                id: None,
509
                address: None,
510
                description: None,
511
                archive_url: None,
512
                owner_local_part: None,
513
                request_local_part: None,
514
                verify: None,
515
                hidden: None,
516
                enabled: None,
517
            }
518
        ) {
519
            return self.list(change_set.pk).map(|_| ());
520
        }
521

            
522
        let MailingListChangeset {
523
            pk,
524
            name,
525
            id,
526
            address,
527
            description,
528
            archive_url,
529
            owner_local_part,
530
            request_local_part,
531
            verify,
532
            hidden,
533
            enabled,
534
        } = change_set;
535
        let tx = self.connection.transaction()?;
536

            
537
        macro_rules! update {
538
            ($field:tt) => {{
539
                if let Some($field) = $field {
540
                    tx.execute(
541
                        concat!("UPDATE list SET ", stringify!($field), " = ? WHERE pk = ?;"),
542
                        rusqlite::params![&$field, &pk],
543
                    )?;
544
                }
545
            }};
546
        }
547
        update!(name);
548
        update!(id);
549
        update!(address);
550
        update!(description);
551
        update!(archive_url);
552
        update!(owner_local_part);
553
        update!(request_local_part);
554
        update!(verify);
555
        update!(hidden);
556
        update!(enabled);
557

            
558
        tx.commit()?;
559
        Ok(())
560
    }
561

            
562
    /// Return the post filters of a mailing list.
563
8
    pub fn list_filters(
564
        &self,
565
        _list: &DbVal<MailingList>,
566
    ) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> {
567
        use crate::mail::message_filters::*;
568
16
        vec![
569
8
            Box::new(FixCRLF),
570
8
            Box::new(PostRightsCheck),
571
8
            Box::new(AddListHeaders),
572
8
            Box::new(FinalizeRecipients),
573
        ]
574
8
    }
575
}