summaryrefslogtreecommitdiff
path: root/tests/src/directory/sql.rs
blob: 18c277220830ee9699d62927b543d661dc1d8885 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
/*
 * Copyright (c) 2023 Stalwart Labs Ltd.
 *
 * This file is part of Stalwart Mail Server.
 *
 * 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.
 * in the LICENSE file at the top-level directory of this distribution.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * You can be released from the requirements of the AGPLv3 license by
 * purchasing a commercial license. Please contact licensing@stalw.art
 * for more details.
*/

use directory::{Directory, Principal, Type};
use mail_send::Credentials;

use crate::directory::parse_config;

#[tokio::test]
async fn sql_directory() {
    // Enable logging
    /*tracing::subscriber::set_global_default(
        tracing_subscriber::FmtSubscriber::builder()
            .with_max_level(tracing::Level::TRACE)
            .finish(),
    )
    .unwrap();*/

    // Obtain directory handle
    let mut config = parse_config();
    let lookups = config.lookups;
    let handle = config.directories.remove("sql").unwrap();

    // Create tables
    create_test_directory(handle.as_ref()).await;

    // Create test users
    create_test_user(handle.as_ref(), "john", "12345", "John Doe").await;
    create_test_user(handle.as_ref(), "jane", "abcde", "Jane Doe").await;
    create_test_user(
        handle.as_ref(),
        "bill",
        "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe",
        "Bill Foobar",
    )
    .await;
    set_test_quota(handle.as_ref(), "bill", 500000).await;

    // Create test groups
    create_test_group(handle.as_ref(), "sales", "Sales Team").await;
    create_test_group(handle.as_ref(), "support", "Support Team").await;

    // Link users to groups
    add_to_group(handle.as_ref(), "john", "sales").await;
    add_to_group(handle.as_ref(), "jane", "sales").await;
    add_to_group(handle.as_ref(), "jane", "support").await;

    // Add email addresses
    link_test_address(handle.as_ref(), "john", "john@example.org", "primary").await;
    link_test_address(handle.as_ref(), "jane", "jane@example.org", "primary").await;
    link_test_address(handle.as_ref(), "bill", "bill@example.org", "primary").await;

    // Add aliases and lists
    link_test_address(handle.as_ref(), "john", "john.doe@example.org", "alias").await;
    link_test_address(handle.as_ref(), "john", "jdoe@example.org", "alias").await;
    link_test_address(handle.as_ref(), "john", "info@example.org", "list").await;
    link_test_address(handle.as_ref(), "jane", "info@example.org", "list").await;
    link_test_address(handle.as_ref(), "bill", "info@example.org", "list").await;

    // Add catch-all user
    create_test_user(handle.as_ref(), "robert", "abcde", "Robert Foobar").await;
    link_test_address(handle.as_ref(), "robert", "robert@catchall.org", "primary").await;
    link_test_address(handle.as_ref(), "robert", "@catchall.org", "alias").await;

    // Text lookup
    assert!(lookups
        .get("sql/domains")
        .unwrap()
        .contains("example.org")
        .await
        .unwrap());

    // Test authentication
    assert_eq!(
        handle
            .authenticate(&Credentials::Plain {
                username: "john".to_string(),
                secret: "12345".to_string()
            })
            .await
            .unwrap()
            .unwrap(),
        Principal {
            name: "john".to_string(),
            description: "John Doe".to_string().into(),
            secrets: vec!["12345".to_string()],
            typ: Type::Individual,
            member_of: vec!["sales".to_string()],
            ..Default::default()
        }
    );
    assert_eq!(
        handle
            .authenticate(&Credentials::Plain {
                username: "bill".to_string(),
                secret: "password".to_string()
            })
            .await
            .unwrap()
            .unwrap(),
        Principal {
            name: "bill".to_string(),
            description: "Bill Foobar".to_string().into(),
            secrets: vec![
                "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".to_string()
            ],
            typ: Type::Individual,
            quota: 500000,
            ..Default::default()
        }
    );
    assert!(handle
        .authenticate(&Credentials::Plain {
            username: "bill".to_string(),
            secret: "invalid".to_string()
        })
        .await
        .unwrap()
        .is_none());

    // Get user by name
    assert_eq!(
        handle.principal("jane").await.unwrap().unwrap(),
        Principal {
            name: "jane".to_string(),
            description: "Jane Doe".to_string().into(),
            typ: Type::Individual,
            secrets: vec!["abcde".to_string()],
            member_of: vec!["sales".to_string(), "support".to_string()],
            ..Default::default()
        }
    );

    // Get group by name
    assert_eq!(
        handle.principal("sales").await.unwrap().unwrap(),
        Principal {
            name: "sales".to_string(),
            description: "Sales Team".to_string().into(),
            typ: Type::Group,
            ..Default::default()
        }
    );

    // Emails by id
    assert_eq!(
        handle.emails_by_name("john").await.unwrap(),
        vec![
            "john@example.org".to_string(),
            "jdoe@example.org".to_string(),
            "john.doe@example.org".to_string(),
        ]
    );
    assert_eq!(
        handle.emails_by_name("bill").await.unwrap(),
        vec!["bill@example.org".to_string(),]
    );

    // Ids by email
    assert_eq!(
        handle.names_by_email("jane@example.org").await.unwrap(),
        vec!["jane".to_string()]
    );
    assert_eq!(
        handle.names_by_email("info@example.org").await.unwrap(),
        vec!["bill".to_string(), "jane".to_string(), "john".to_string()]
    );
    assert_eq!(
        handle
            .names_by_email("jane+alias@example.org")
            .await
            .unwrap(),
        vec!["jane".to_string()]
    );
    assert_eq!(
        handle
            .names_by_email("info+alias@example.org")
            .await
            .unwrap(),
        vec!["bill".to_string(), "jane".to_string(), "john".to_string()]
    );
    assert_eq!(
        handle.names_by_email("unknown@example.org").await.unwrap(),
        Vec::<String>::new()
    );
    assert_eq!(
        handle
            .names_by_email("anything@catchall.org")
            .await
            .unwrap(),
        vec!["robert".to_string()]
    );

    // Domain validation
    assert!(handle.is_local_domain("example.org").await.unwrap());
    assert!(!handle.is_local_domain("other.org").await.unwrap());

    // RCPT TO
    assert!(handle.rcpt("jane@example.org").await.unwrap());
    assert!(handle.rcpt("info@example.org").await.unwrap());
    assert!(handle.rcpt("jane+alias@example.org").await.unwrap());
    assert!(handle.rcpt("info+alias@example.org").await.unwrap());
    assert!(handle.rcpt("random_user@catchall.org").await.unwrap());
    assert!(!handle.rcpt("invalid@example.org").await.unwrap());

    // VRFY
    assert_eq!(
        handle.vrfy("jane").await.unwrap(),
        vec!["jane@example.org".to_string()]
    );
    assert_eq!(
        handle.vrfy("john").await.unwrap(),
        vec!["john@example.org".to_string()]
    );
    assert_eq!(
        handle.vrfy("jane+alias@example").await.unwrap(),
        vec!["jane@example.org".to_string()]
    );
    assert_eq!(handle.vrfy("info").await.unwrap(), Vec::<String>::new());
    assert_eq!(handle.vrfy("invalid").await.unwrap(), Vec::<String>::new());

    // EXPN
    assert_eq!(
        handle.expn("info@example.org").await.unwrap(),
        vec![
            "bill@example.org".to_string(),
            "jane@example.org".to_string(),
            "john@example.org".to_string()
        ]
    );
    assert_eq!(
        handle.expn("john@example.org").await.unwrap(),
        Vec::<String>::new()
    );
}

pub async fn create_test_directory(handle: &dyn Directory) {
    // Create tables
    for query in [
        "CREATE TABLE accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)",
        "CREATE TABLE group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))",
        "CREATE TABLE emails (name TEXT NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (name, address))",
        "INSERT INTO accounts (name, secret, type) VALUES ('admin', 'secret', 'individual')", 
    ] {
        handle.query(query, &[]).await.unwrap_or_else(|_| panic!("failed for {query}"));
    }
}

pub async fn create_test_user(handle: &dyn Directory, login: &str, secret: &str, name: &str) {
    handle
        .query(
            "INSERT OR IGNORE INTO accounts (name, secret, description, type, active) VALUES (?, ?, ?, 'individual', true)",
            &[login, secret, name],
        )
        .await
        .unwrap();
}

pub async fn create_test_user_with_email(
    handle: &dyn Directory,
    login: &str,
    secret: &str,
    name: &str,
) {
    create_test_user(handle, login, secret, name).await;
    link_test_address(handle, login, login, "primary").await;
}

pub async fn create_test_group(handle: &dyn Directory, login: &str, name: &str) {
    handle
        .query(
            "INSERT OR IGNORE INTO accounts (name, description, type, active) VALUES (?, ?, 'group', true)",
            &[login,  name],
        )
        .await
        .unwrap();
}

pub async fn create_test_group_with_email(handle: &dyn Directory, login: &str, name: &str) {
    create_test_group(handle, login, name).await;
    link_test_address(handle, login, login, "primary").await;
}

pub async fn link_test_address(handle: &dyn Directory, login: &str, address: &str, typ: &str) {
    handle
        .query(
            "INSERT OR IGNORE INTO emails (name, address, type) VALUES (?, ?, ?)",
            &[login, address, typ],
        )
        .await
        .unwrap();
}

pub async fn set_test_quota(handle: &dyn Directory, login: &str, quota: u32) {
    handle
        .query(
            &format!("UPDATE accounts SET quota = {} where name = ?", quota,),
            &[login],
        )
        .await
        .unwrap();
}

pub async fn add_to_group(handle: &dyn Directory, login: &str, group: &str) {
    handle
        .query(
            "INSERT INTO group_members (name, member_of) VALUES (?, ?)",
            &[login, group],
        )
        .await
        .unwrap();
}

pub async fn remove_from_group(handle: &dyn Directory, login: &str, group: &str) {
    handle
        .query(
            "DELETE FROM group_members WHERE name = ? AND member_of = ?",
            &[login, group],
        )
        .await
        .unwrap();
}

pub async fn remove_test_alias(handle: &dyn Directory, login: &str, alias: &str) {
    handle
        .query(
            "DELETE FROM emails WHERE name = ? AND address = ?",
            &[login, alias],
        )
        .await
        .unwrap();
}