1
use std::sync::Arc;
2

            
3
pub use axum::extract::{Path, Query, State};
4
use axum::{http::StatusCode, Json, Router};
5
use mailpot_web::{typed_paths::*, ResponseError, RouterExt, TypedPath};
6
use serde::{Deserialize, Serialize};
7

            
8
use crate::*;
9

            
10
6
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
11
#[typed_path("/list/")]
12
pub struct ListsPath;
13

            
14
10
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
15
#[typed_path("/list/:id/owner/")]
16
pub struct ListOwnerPath(pub ListPathIdentifier);
17

            
18
10
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
19
#[typed_path("/list/:id/subscription/")]
20
pub struct ListSubscriptionPath(pub ListPathIdentifier);
21

            
22
8
pub fn create_route(conf: Arc<Configuration>) -> Router {
23
8
    Router::new()
24
        .typed_get(all_lists)
25
        .typed_post(new_list)
26
        .typed_get(get_list)
27
        .typed_post({
28
            move |_: ListPath| async move {
29
                Err::<(), ResponseError>(mailpot_web::ResponseError::new(
30
                    "Invalid method".to_string(),
31
                    StatusCode::BAD_REQUEST,
32
                ))
33
            }
34
        })
35
        .typed_get(get_list_owner)
36
        .typed_post(new_list_owner)
37
        .typed_get(get_list_subs)
38
        .typed_post(new_list_sub)
39
        .with_state(conf)
40
8
}
41

            
42
1
async fn get_list(
43
2
    ListPath(id): ListPath,
44
1
    State(state): State<Arc<Configuration>>,
45
3
) -> Result<Json<MailingList>, ResponseError> {
46
2
    let db = Connection::open_db(Configuration::clone(&state))?;
47
2
    let Some(list) = (match id {
48
1
        ListPathIdentifier::Pk(id) => db.list(id)?,
49
        ListPathIdentifier::Id(id) => db.list_by_id(id)?,
50
    }) else {
51
        return Err(mailpot_web::ResponseError::new(
52
            "Not found".to_string(),
53
            StatusCode::NOT_FOUND,
54
        ));
55
    };
56
1
    Ok(Json(list.into_inner()))
57
3
}
58

            
59
2
async fn all_lists(
60
4
    _: ListsPath,
61
2
    Query(GetRequest {
62
        filter: _,
63
2
        count,
64
2
        page,
65
    }): Query<GetRequest>,
66
2
    State(state): State<Arc<Configuration>>,
67
6
) -> Result<Json<GetResponse>, ResponseError> {
68
4
    let db = Connection::open_db(Configuration::clone(&state))?;
69
2
    let lists_values = db.lists()?;
70
2
    let page = page.unwrap_or(0);
71
2
    let Some(count) = count else {
72
1
        let mut stmt = db.connection.prepare("SELECT count(*) FROM list;")?;
73
1
        return Ok(Json(GetResponse {
74
1
            entries: vec![],
75
2
            total: stmt.query_row([], |row| {
76
1
                let count: usize = row.get(0)?;
77
1
                Ok(count)
78
1
            })?,
79
            start: 0,
80
1
        }));
81
1
    };
82
1
    let offset = page * count;
83
1
    let res: Vec<_> = lists_values
84
        .into_iter()
85
        .skip(offset)
86
        .take(count)
87
        .map(DbVal::into_inner)
88
        .collect();
89

            
90
1
    Ok(Json(GetResponse {
91
1
        total: res.len(),
92
        start: offset,
93
1
        entries: res,
94
    }))
95
7
}
96

            
97
1
async fn new_list(
98
2
    _: ListsPath,
99
1
    State(_state): State<Arc<Configuration>>,
100
    //Json(_body): Json<GetRequest>,
101
2
) -> Result<Json<()>, ResponseError> {
102
    // TODO create new list
103
1
    Err(mailpot_web::ResponseError::new(
104
2
        "Not allowed".to_string(),
105
        StatusCode::UNAUTHORIZED,
106
    ))
107
3
}
108

            
109
#[derive(Debug, Serialize, Deserialize)]
110
enum GetFilter {
111
    Pk(i64),
112
    Address(String),
113
    Id(String),
114
    Name(String),
115
}
116

            
117
7
#[derive(Debug, Serialize, Deserialize)]
118
struct GetRequest {
119
    filter: Option<GetFilter>,
120
    count: Option<usize>,
121
    page: Option<usize>,
122
}
123

            
124
24
#[derive(Debug, Serialize, Deserialize)]
125
struct GetResponse {
126
    entries: Vec<MailingList>,
127
    total: usize,
128
    start: usize,
129
}
130

            
131
1
async fn get_list_owner(
132
2
    ListOwnerPath(id): ListOwnerPath,
133
1
    State(state): State<Arc<Configuration>>,
134
3
) -> Result<Json<Vec<ListOwner>>, ResponseError> {
135
2
    let db = Connection::open_db(Configuration::clone(&state))?;
136
1
    let owners = match id {
137
1
        ListPathIdentifier::Pk(id) => db.list_owners(id)?,
138
        ListPathIdentifier::Id(id) => {
139
            if let Some(owners) = db.list_by_id(id)?.map(|l| db.list_owners(l.pk())) {
140
                owners?
141
            } else {
142
                return Err(mailpot_web::ResponseError::new(
143
                    "Not found".to_string(),
144
                    StatusCode::NOT_FOUND,
145
                ));
146
            }
147
        }
148
    };
149
1
    Ok(Json(owners.into_iter().map(DbVal::into_inner).collect()))
150
3
}
151

            
152
1
async fn new_list_owner(
153
2
    ListOwnerPath(_id): ListOwnerPath,
154
1
    State(_state): State<Arc<Configuration>>,
155
    //Json(_body): Json<GetRequest>,
156
2
) -> Result<Json<Vec<ListOwner>>, ResponseError> {
157
1
    Err(mailpot_web::ResponseError::new(
158
2
        "Not allowed".to_string(),
159
        StatusCode::UNAUTHORIZED,
160
    ))
161
3
}
162

            
163
1
async fn get_list_subs(
164
2
    ListSubscriptionPath(id): ListSubscriptionPath,
165
1
    State(state): State<Arc<Configuration>>,
166
3
) -> Result<Json<Vec<ListSubscription>>, ResponseError> {
167
2
    let db = Connection::open_db(Configuration::clone(&state))?;
168
1
    let subs = match id {
169
1
        ListPathIdentifier::Pk(id) => db.list_subscriptions(id)?,
170
        ListPathIdentifier::Id(id) => {
171
            if let Some(v) = db.list_by_id(id)?.map(|l| db.list_subscriptions(l.pk())) {
172
                v?
173
            } else {
174
                return Err(mailpot_web::ResponseError::new(
175
                    "Not found".to_string(),
176
                    StatusCode::NOT_FOUND,
177
                ));
178
            }
179
        }
180
    };
181
1
    Ok(Json(subs.into_iter().map(DbVal::into_inner).collect()))
182
3
}
183

            
184
1
async fn new_list_sub(
185
2
    ListSubscriptionPath(_id): ListSubscriptionPath,
186
1
    State(_state): State<Arc<Configuration>>,
187
    //Json(_body): Json<GetRequest>,
188
2
) -> Result<Json<ListSubscription>, ResponseError> {
189
1
    Err(mailpot_web::ResponseError::new(
190
2
        "Not allowed".to_string(),
191
        StatusCode::UNAUTHORIZED,
192
    ))
193
3
}
194

            
195
#[cfg(test)]
196
mod tests {
197

            
198
    use axum::{
199
        body::Body,
200
        http::{method::Method, Request, StatusCode},
201
    };
202
    use mailpot::{models::*, Configuration, Connection, SendMail};
203
    use mailpot_tests::init_stderr_logging;
204
    use serde_json::json;
205
    use tempfile::TempDir;
206
    use tower::ServiceExt; // for `oneshot` and `ready`
207

            
208
    use super::*;
209

            
210
3
    #[tokio::test]
211
2
    async fn test_list_router() {
212
1
        init_stderr_logging();
213

            
214
1
        let tmp_dir = TempDir::new().unwrap();
215

            
216
1
        let db_path = tmp_dir.path().join("mpot.db");
217
1
        std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap();
218
1
        let mut perms = std::fs::metadata(&db_path).unwrap().permissions();
219
        #[allow(clippy::permissions_set_readonly_false)]
220
1
        perms.set_readonly(false);
221
1
        std::fs::set_permissions(&db_path, perms).unwrap();
222
1
        let config = Configuration {
223
1
            send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
224
1
            db_path,
225
1
            data_path: tmp_dir.path().to_path_buf(),
226
1
            administrators: vec![],
227
        };
228

            
229
1
        let db = Connection::open_db(config.clone()).unwrap().trusted();
230
1
        assert!(!db.lists().unwrap().is_empty());
231
1
        let foo_chat = MailingList {
232
            pk: 1,
233
1
            name: "foobar chat".into(),
234
1
            id: "foo-chat".into(),
235
1
            address: "foo-chat@example.com".into(),
236
1
            topics: vec![],
237
1
            description: None,
238
1
            archive_url: None,
239
        };
240
1
        assert_eq!(&db.lists().unwrap().remove(0).into_inner(), &foo_chat);
241
1
        drop(db);
242

            
243
1
        let config = Arc::new(config);
244

            
245
        // ------------------------------------------------------------
246
        // all_lists() get total
247

            
248
5
        let response = crate::create_app(config.clone())
249
            .oneshot(
250
2
                Request::builder()
251
                    .uri("/v1/list/")
252
1
                    .body(Body::empty())
253
                    .unwrap(),
254
            )
255
2
            .await
256
            .unwrap();
257

            
258
1
        assert_eq!(response.status(), StatusCode::OK);
259

            
260
1
        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
261
1
        let r: GetResponse = serde_json::from_slice(&body).unwrap();
262

            
263
1
        assert_eq!(&r.entries, &[]);
264
        assert_eq!(r.total, 1);
265
        assert_eq!(r.start, 0);
266

            
267
        // ------------------------------------------------------------
268
        // all_lists() with count
269

            
270
5
        let response = crate::create_app(config.clone())
271
            .oneshot(
272
2
                Request::builder()
273
                    .uri("/v1/list/?count=20")
274
1
                    .body(Body::empty())
275
                    .unwrap(),
276
            )
277
2
            .await
278
            .unwrap();
279
1
        assert_eq!(response.status(), StatusCode::OK);
280

            
281
1
        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
282
1
        let r: GetResponse = serde_json::from_slice(&body).unwrap();
283

            
284
1
        assert_eq!(&r.entries, &[foo_chat.clone()]);
285
1
        assert_eq!(r.total, 1);
286
        assert_eq!(r.start, 0);
287

            
288
        // ------------------------------------------------------------
289
        // new_list()
290

            
291
5
        let response = crate::create_app(config.clone())
292
            .oneshot(
293
3
                Request::builder()
294
                    .uri("/v1/list/")
295
                    .header("Content-Type", "application/json")
296
1
                    .method(Method::POST)
297
1
                    .body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
298
                    .unwrap(),
299
            )
300
2
            .await
301
1
            .unwrap();
302
1
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
303

            
304
        // ------------------------------------------------------------
305
        // get_list()
306

            
307
5
        let response = crate::create_app(config.clone())
308
            .oneshot(
309
3
                Request::builder()
310
                    .uri("/v1/list/1/")
311
                    .header("Content-Type", "application/json")
312
1
                    .method(Method::GET)
313
1
                    .body(Body::empty())
314
                    .unwrap(),
315
            )
316
2
            .await
317
            .unwrap();
318
1
        assert_eq!(response.status(), StatusCode::OK);
319
1
        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
320
1
        let r: MailingList = serde_json::from_slice(&body).unwrap();
321
1
        assert_eq!(&r, &foo_chat);
322

            
323
        // ------------------------------------------------------------
324
        // get_list_subs()
325

            
326
5
        let response = crate::create_app(config.clone())
327
            .oneshot(
328
3
                Request::builder()
329
                    .uri("/v1/list/1/subscription/")
330
                    .header("Content-Type", "application/json")
331
1
                    .method(Method::GET)
332
1
                    .body(Body::empty())
333
                    .unwrap(),
334
            )
335
2
            .await
336
            .unwrap();
337
1
        assert_eq!(response.status(), StatusCode::OK);
338
1
        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
339
1
        let r: Vec<ListSubscription> = serde_json::from_slice(&body).unwrap();
340
1
        assert_eq!(
341
1
            &r,
342
1
            &[ListSubscription {
343
                pk: 1,
344
                list: 1,
345
1
                address: "user@example.com".to_string(),
346
1
                name: Some("Name".to_string()),
347
1
                account: Some(1),
348
                enabled: true,
349
                verified: false,
350
                digest: false,
351
                hide_address: false,
352
                receive_duplicates: true,
353
                receive_own_posts: false,
354
                receive_confirmation: true
355
            }]
356
        );
357

            
358
        // ------------------------------------------------------------
359
        // new_list_sub()
360

            
361
5
        let response = crate::create_app(config.clone())
362
            .oneshot(
363
3
                Request::builder()
364
                    .uri("/v1/list/1/subscription/")
365
                    .header("Content-Type", "application/json")
366
1
                    .method(Method::POST)
367
1
                    .body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
368
                    .unwrap(),
369
            )
370
2
            .await
371
1
            .unwrap();
372
1
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
373

            
374
        // ------------------------------------------------------------
375
        // get_list_owner()
376

            
377
5
        let response = crate::create_app(config.clone())
378
            .oneshot(
379
3
                Request::builder()
380
                    .uri("/v1/list/1/owner/")
381
                    .header("Content-Type", "application/json")
382
1
                    .method(Method::GET)
383
1
                    .body(Body::empty())
384
                    .unwrap(),
385
            )
386
2
            .await
387
            .unwrap();
388
1
        assert_eq!(response.status(), StatusCode::OK);
389
1
        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
390
1
        let r: Vec<ListOwner> = serde_json::from_slice(&body).unwrap();
391
1
        assert_eq!(
392
1
            &r,
393
1
            &[ListOwner {
394
                pk: 1,
395
                list: 1,
396
1
                address: "user@example.com".into(),
397
1
                name: None
398
            }]
399
        );
400

            
401
        // ------------------------------------------------------------
402
        // new_list_owner()
403

            
404
5
        let response = crate::create_app(config.clone())
405
            .oneshot(
406
3
                Request::builder()
407
                    .uri("/v1/list/1/owner/")
408
                    .header("Content-Type", "application/json")
409
1
                    .method(Method::POST)
410
1
                    .body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
411
                    .unwrap(),
412
            )
413
2
            .await
414
1
            .unwrap();
415
2
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
416
3
    }
417
}