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
use mailpot::{
21
    chrono,
22
    melib::{self, Envelope},
23
    models::{Account, DbVal, ListSubscription, MailingList},
24
    rusqlite, Connection, Result,
25
};
26

            
27
pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<()> {
28
    let mut col = vec![];
29
    {
30
        let mut stmt = db.connection.prepare("SELECT * FROM post ORDER BY pk")?;
31
        let iter = stmt.query_map([], |row| {
32
            let pk: i64 = row.get("pk")?;
33
            let date_s: String = row.get("datetime")?;
34
            match melib::utils::datetime::rfc822_to_timestamp(date_s.trim()) {
35
                Err(_) | Ok(0) => {
36
                    let mut timestamp: i64 = row.get("timestamp")?;
37
                    let created: i64 = row.get("created")?;
38
                    if timestamp == 0 {
39
                        timestamp = created;
40
                    }
41
                    timestamp = std::cmp::min(timestamp, created);
42
                    let timestamp = if timestamp <= 0 {
43
                        None
44
                    } else {
45
                        // safe because we checked it's not negative or zero above.
46
                        Some(timestamp as u64)
47
                    };
48
                    let message: Vec<u8> = row.get("message")?;
49
                    Ok(Some((pk, date_s, message, timestamp)))
50
                }
51
                Ok(_) => Ok(None),
52
            }
53
        })?;
54

            
55
        for entry in iter {
56
            if let Some(s) = entry? {
57
                col.push(s);
58
            }
59
        }
60
    }
61
    let mut failures = 0;
62
    let tx = if dry_run {
63
        None
64
    } else {
65
        Some(db.connection.transaction()?)
66
    };
67
    if col.is_empty() {
68
        println!("datetime_header_value: ok");
69
    } else {
70
        println!("datetime_header_value: found {} entries", col.len());
71
        println!("pk\tDate value\tshould be");
72
        for (pk, val, message, timestamp) in col {
73
            let correct = if let Ok(v) =
74
                chrono::DateTime::<chrono::FixedOffset>::parse_from_rfc3339(&val)
75
            {
76
                v.to_rfc2822()
77
            } else if let Some(v) = timestamp.map(|t| {
78
                melib::utils::datetime::timestamp_to_string(
79
                    t,
80
                    Some(melib::utils::datetime::formats::RFC822_DATE),
81
                    true,
82
                )
83
            }) {
84
                v
85
            } else if let Ok(v) =
86
                Envelope::from_bytes(&message, None).map(|env| env.date_as_str().to_string())
87
            {
88
                v
89
            } else {
90
                failures += 1;
91
                println!("{pk}\t{val}\tCould not find any valid date value in the post metadata!");
92
                continue;
93
            };
94
            println!("{pk}\t{val}\t{correct}");
95
            if let Some(tx) = tx.as_ref() {
96
                tx.execute(
97
                    "UPDATE post SET datetime = ? WHERE pk = ?",
98
                    rusqlite::params![&correct, pk],
99
                )?;
100
            }
101
        }
102
    }
103
    if let Some(tx) = tx {
104
        tx.commit()?;
105
    }
106
    if failures > 0 {
107
        println!(
108
            "datetime_header_value: {failures} failure{}",
109
            if failures == 1 { "" } else { "s" }
110
        );
111
    }
112
    Ok(())
113
}
114

            
115
pub fn remove_empty_accounts_lint(db: &mut Connection, dry_run: bool) -> Result<()> {
116
    let mut col = vec![];
117
    {
118
        let mut stmt = db.connection.prepare(
119
            "SELECT * FROM account WHERE NOT EXISTS (SELECT 1 FROM subscription AS s WHERE \
120
             s.address = address) ORDER BY pk",
121
        )?;
122
        let iter = stmt.query_map([], |row| {
123
            let pk = row.get("pk")?;
124
            Ok(DbVal(
125
                Account {
126
                    pk,
127
                    name: row.get("name")?,
128
                    address: row.get("address")?,
129
                    public_key: row.get("public_key")?,
130
                    password: row.get("password")?,
131
                    enabled: row.get("enabled")?,
132
                },
133
                pk,
134
            ))
135
        })?;
136

            
137
        for entry in iter {
138
            let entry = entry?;
139
            col.push(entry);
140
        }
141
    }
142
    if col.is_empty() {
143
        println!("remove_empty_accounts: ok");
144
    } else {
145
        let tx = if dry_run {
146
            None
147
        } else {
148
            Some(db.connection.transaction()?)
149
        };
150
        println!("remove_empty_accounts: found {} entries", col.len());
151
        println!("pk\tAddress");
152
        for DbVal(Account { pk, address, .. }, _) in &col {
153
            println!("{pk}\t{address}");
154
        }
155
        if let Some(tx) = tx {
156
            for DbVal(_, pk) in col {
157
                tx.execute("DELETE FROM account WHERE pk = ?", [pk])?;
158
            }
159
            tx.commit()?;
160
        }
161
    }
162
    Ok(())
163
}
164

            
165
pub fn remove_accepted_subscription_requests_lint(
166
    db: &mut Connection,
167
    dry_run: bool,
168
) -> Result<()> {
169
    let mut col = vec![];
170
    {
171
        let mut stmt = db.connection.prepare(
172
            "SELECT * FROM candidate_subscription WHERE accepted IS NOT NULL ORDER BY pk",
173
        )?;
174
        let iter = stmt.query_map([], |row| {
175
            let pk = row.get("pk")?;
176
            Ok(DbVal(
177
                ListSubscription {
178
                    pk,
179
                    list: row.get("list")?,
180
                    address: row.get("address")?,
181
                    account: row.get("account")?,
182
                    name: row.get("name")?,
183
                    digest: row.get("digest")?,
184
                    enabled: row.get("enabled")?,
185
                    verified: row.get("verified")?,
186
                    hide_address: row.get("hide_address")?,
187
                    receive_duplicates: row.get("receive_duplicates")?,
188
                    receive_own_posts: row.get("receive_own_posts")?,
189
                    receive_confirmation: row.get("receive_confirmation")?,
190
                },
191
                pk,
192
            ))
193
        })?;
194

            
195
        for entry in iter {
196
            let entry = entry?;
197
            col.push(entry);
198
        }
199
    }
200
    if col.is_empty() {
201
        println!("remove_accepted_subscription_requests: ok");
202
    } else {
203
        let tx = if dry_run {
204
            None
205
        } else {
206
            Some(db.connection.transaction()?)
207
        };
208
        println!(
209
            "remove_accepted_subscription_requests: found {} entries",
210
            col.len()
211
        );
212
        println!("pk\tAddress");
213
        for DbVal(ListSubscription { pk, address, .. }, _) in &col {
214
            println!("{pk}\t{address}");
215
        }
216
        if let Some(tx) = tx {
217
            for DbVal(_, pk) in col {
218
                tx.execute("DELETE FROM candidate_subscription WHERE pk = ?", [pk])?;
219
            }
220
            tx.commit()?;
221
        }
222
    }
223
    Ok(())
224
}
225

            
226
pub fn warn_list_no_owner_lint(db: &mut Connection, _: bool) -> Result<()> {
227
    let mut stmt = db.connection.prepare(
228
        "SELECT * FROM list WHERE NOT EXISTS (SELECT 1 FROM owner AS o WHERE o.list = pk) ORDER \
229
         BY pk",
230
    )?;
231
    let iter = stmt.query_map([], |row| {
232
        let pk = row.get("pk")?;
233
        Ok(DbVal(
234
            MailingList {
235
                pk,
236
                name: row.get("name")?,
237
                id: row.get("id")?,
238
                address: row.get("address")?,
239
                description: row.get("description")?,
240
                topics: vec![],
241
                archive_url: row.get("archive_url")?,
242
            },
243
            pk,
244
        ))
245
    })?;
246

            
247
    let mut col = vec![];
248
    for entry in iter {
249
        let entry = entry?;
250
        col.push(entry);
251
    }
252
    if col.is_empty() {
253
        println!("warn_list_no_owner: ok");
254
    } else {
255
        println!("warn_list_no_owner: found {} entries", col.len());
256
        println!("pk\tName");
257
        for DbVal(MailingList { pk, name, .. }, _) in col {
258
            println!("{pk}\t{name}");
259
        }
260
    }
261
    Ok(())
262
}