1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2023 - 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 std::{borrow::Cow, time::Duration};
21

            
22
use base64::{engine::general_purpose, Engine as _};
23
use mailpot::models::{ListOwner, ListSubscription};
24
use ureq::Agent;
25

            
26
pub struct Mailman3Connection {
27
    agent: Agent,
28
    url: Cow<'static, str>,
29
    auth: String,
30
}
31

            
32
impl Mailman3Connection {
33
    pub fn new(
34
        url: &str,
35
        username: &str,
36
        password: &str,
37
    ) -> Result<Self, Box<dyn std::error::Error>> {
38
        let agent: Agent = ureq::AgentBuilder::new()
39
            .timeout_read(Duration::from_secs(5))
40
            .timeout_write(Duration::from_secs(5))
41
            .build();
42
        let mut buf = String::new();
43
        general_purpose::STANDARD
44
            .encode_string(format!("{username}:{password}").as_bytes(), &mut buf);
45

            
46
        let auth: String = format!("Basic {buf}");
47

            
48
        Ok(Self {
49
            agent,
50
            url: url.trim_end_matches('/').to_string().into(),
51
            auth,
52
        })
53
    }
54

            
55
    pub fn users(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> {
56
        let response: String = self
57
            .agent
58
            .get(&format!(
59
                "{}/lists/{list_address}/roster/member?fields=email&fields=display_name",
60
                self.url
61
            ))
62
            .set("Authorization", &self.auth)
63
            .call()?
64
            .into_string()?;
65
        Ok(serde_json::from_str::<Roster>(&response)?.entries)
66
    }
67

            
68
    pub fn owners(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> {
69
        let response: String = self
70
            .agent
71
            .get(&format!(
72
                "{}/lists/{list_address}/roster/owner?fields=email&fields=display_name",
73
                self.url
74
            ))
75
            .set("Authorization", &self.auth)
76
            .call()?
77
            .into_string()?;
78
        Ok(serde_json::from_str::<Roster>(&response)?.entries)
79
    }
80
}
81

            
82
#[derive(serde::Deserialize, Debug)]
83
pub struct Roster {
84
    pub entries: Vec<Entry>,
85
}
86

            
87
#[derive(serde::Deserialize, Debug)]
88
pub struct Entry {
89
    display_name: String,
90
    email: String,
91
}
92

            
93
impl Entry {
94
    pub fn display_name(&self) -> Option<&str> {
95
        if !self.display_name.trim().is_empty() && &self.display_name != "None" {
96
            Some(&self.display_name)
97
        } else {
98
            None
99
        }
100
    }
101

            
102
    pub fn email(&self) -> &str {
103
        &self.email
104
    }
105

            
106
    pub fn into_subscription(self, list: i64) -> ListSubscription {
107
        let Self {
108
            display_name,
109
            email,
110
        } = self;
111

            
112
        ListSubscription {
113
            pk: -1,
114
            list,
115
            address: email,
116
            name: if !display_name.trim().is_empty() && &display_name != "None" {
117
                Some(display_name)
118
            } else {
119
                None
120
            },
121
            account: None,
122
            enabled: true,
123
            verified: true,
124
            digest: false,
125
            hide_address: false,
126
            receive_duplicates: false,
127
            receive_own_posts: false,
128
            receive_confirmation: false,
129
        }
130
    }
131

            
132
    pub fn into_owner(self, list: i64) -> ListOwner {
133
        let Self {
134
            display_name,
135
            email,
136
        } = self;
137

            
138
        ListOwner {
139
            pk: -1,
140
            list,
141
            address: email,
142
            name: if !display_name.trim().is_empty() && &display_name != "None" {
143
                Some(display_name)
144
            } else {
145
                None
146
            },
147
        }
148
    }
149
}