oauth_toolkit/
providers.rs1use serde::{Deserialize, Serialize};
8
9pub mod endpoints {
14 pub const GITHUB_AUTHORIZE_URL: &str = "https://github.com/login/oauth/authorize";
16 pub const GITHUB_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
18 pub const GITHUB_USER_INFO_URL: &str = "https://api.github.com/user";
20
21 pub const GOOGLE_AUTHORIZE_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth";
23 pub const GOOGLE_TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
25 pub const GOOGLE_USER_INFO_URL: &str = "https://www.googleapis.com/oauth2/v2/userinfo";
27
28 pub const YAHOO_AUTHORIZE_URL: &str = "https://api.login.yahoo.com/oauth2/request_auth";
30 pub const YAHOO_TOKEN_URL: &str = "https://api.login.yahoo.com/oauth2/get_token";
32
33 pub const AOL_AUTHORIZE_URL: &str = "https://api.login.aol.com/oauth2/request_auth";
35 pub const AOL_TOKEN_URL: &str = "https://api.login.aol.com/oauth2/get_token";
37
38 pub const FASTMAIL_AUTHORIZE_URL: &str = "https://app.fastmail.com/oauth/authorize";
40 pub const FASTMAIL_TOKEN_URL: &str = "https://api.fastmail.com/oauth/token";
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub struct MailProvider {
47 pub auth_url: String,
49 pub token_url: String,
51 pub client_id: String,
53 pub imap_scopes: Vec<String>,
55 pub smtp_scopes: Vec<String>,
57 pub extra_scopes: Vec<String>,
60}
61
62impl MailProvider {
63 pub fn gmail(client_id: &str) -> Self {
66 Self {
67 auth_url: endpoints::GOOGLE_AUTHORIZE_URL.to_owned(),
68 token_url: endpoints::GOOGLE_TOKEN_URL.to_owned(),
69 client_id: client_id.to_owned(),
70 imap_scopes: vec!["https://mail.google.com/".into()],
71 smtp_scopes: vec!["https://mail.google.com/".into()],
72 extra_scopes: vec!["openid".into(), "email".into()],
73 }
74 }
75
76 pub fn outlook(client_id: &str, tenant: &str) -> Self {
79 Self {
80 auth_url: format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"),
81 token_url: format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"),
82 client_id: client_id.to_owned(),
83 imap_scopes: vec!["https://outlook.office.com/IMAP.AccessAsUser.All".into()],
84 smtp_scopes: vec!["https://outlook.office.com/SMTP.Send".into()],
85 extra_scopes: vec!["offline_access".into()],
86 }
87 }
88
89 pub fn yahoo(client_id: &str) -> Self {
91 Self {
92 auth_url: endpoints::YAHOO_AUTHORIZE_URL.to_owned(),
93 token_url: endpoints::YAHOO_TOKEN_URL.to_owned(),
94 client_id: client_id.to_owned(),
95 imap_scopes: vec!["mail-w".into()],
96 smtp_scopes: vec!["mail-w".into()],
97 extra_scopes: vec![],
98 }
99 }
100
101 pub fn aol(client_id: &str) -> Self {
103 Self {
104 auth_url: endpoints::AOL_AUTHORIZE_URL.to_owned(),
105 token_url: endpoints::AOL_TOKEN_URL.to_owned(),
106 client_id: client_id.to_owned(),
107 imap_scopes: vec!["mail-w".into()],
108 smtp_scopes: vec!["mail-w".into()],
109 extra_scopes: vec![],
110 }
111 }
112
113 pub fn fastmail(client_id: &str) -> Self {
115 Self {
116 auth_url: endpoints::FASTMAIL_AUTHORIZE_URL.to_owned(),
117 token_url: endpoints::FASTMAIL_TOKEN_URL.to_owned(),
118 client_id: client_id.to_owned(),
119 imap_scopes: vec!["https://www.fastmail.com/dev/protocolIMAP".into()],
120 smtp_scopes: vec!["https://www.fastmail.com/dev/protocolSMTP".into()],
121 extra_scopes: vec![],
122 }
123 }
124
125 pub fn authorization_scopes(&self) -> Vec<String> {
128 let mut out: Vec<String> = Vec::new();
129 for scope in self
130 .imap_scopes
131 .iter()
132 .chain(self.smtp_scopes.iter())
133 .chain(self.extra_scopes.iter())
134 {
135 if !out.contains(scope) {
136 out.push(scope.clone());
137 }
138 }
139 out
140 }
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used, clippy::expect_used)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn gmail_preset_shape() {
151 let g = MailProvider::gmail("g");
152 assert_eq!(g.auth_url, endpoints::GOOGLE_AUTHORIZE_URL);
153 assert!(g.token_url.contains("googleapis"));
154 let scopes = g.authorization_scopes();
155 assert!(scopes.contains(&"https://mail.google.com/".to_string()));
156 assert!(scopes.contains(&"openid".to_string()));
157 assert_eq!(
159 scopes
160 .iter()
161 .filter(|s| *s == "https://mail.google.com/")
162 .count(),
163 1
164 );
165 }
166
167 #[test]
168 fn fastmail_preset_split_scopes() {
169 let f = MailProvider::fastmail("f");
170 assert!(f.token_url.contains("fastmail"));
171 assert_eq!(
172 f.imap_scopes,
173 vec!["https://www.fastmail.com/dev/protocolIMAP"]
174 );
175 assert_eq!(
176 f.smtp_scopes,
177 vec!["https://www.fastmail.com/dev/protocolSMTP"]
178 );
179 let scopes = f.authorization_scopes();
180 assert_eq!(scopes.len(), 2);
181 assert!(scopes.contains(&"https://www.fastmail.com/dev/protocolIMAP".to_string()));
182 assert!(scopes.contains(&"https://www.fastmail.com/dev/protocolSMTP".to_string()));
183 }
184
185 #[test]
186 fn gmail_vs_fastmail_scope_shapes_differ() {
187 let g = MailProvider::gmail("g");
189 let f = MailProvider::fastmail("f");
190 assert!(g.imap_scopes == g.smtp_scopes);
191 assert_ne!(f.imap_scopes, f.smtp_scopes);
192 }
193
194 #[test]
195 fn outlook_preset_tenant_aware() {
196 let o = MailProvider::outlook("o", "common");
197 assert!(o.auth_url.contains("login.microsoftonline.com/common"));
198 assert!(o.token_url.contains("login.microsoftonline.com/common"));
199 assert!(o.authorization_scopes().iter().any(|s| s.contains("IMAP")));
200 assert!(
201 o.authorization_scopes()
202 .iter()
203 .any(|s| s.contains("SMTP.Send"))
204 );
205 assert!(o.extra_scopes.contains(&"offline_access".to_string()));
206
207 let t = MailProvider::outlook("o", "9188040d-6c67-4c5b-b112-36a304b66dad");
208 assert!(t.auth_url.contains("9188040d"));
209 }
210
211 #[test]
212 fn yahoo_and_aol_presets() {
213 let y = MailProvider::yahoo("y");
214 assert!(y.auth_url.contains("login.yahoo.com"));
215 assert!(y.token_url.contains("login.yahoo.com"));
216 assert!(y.authorization_scopes().contains(&"mail-w".to_string()));
217
218 let a = MailProvider::aol("a");
219 assert!(a.auth_url.contains("login.aol.com"));
220 assert!(a.token_url.contains("login.aol.com"));
221 assert!(a.authorization_scopes().contains(&"mail-w".to_string()));
222 }
223
224 #[test]
225 fn gmail_shares_google_urls_with_social_module() {
226 assert_eq!(
229 MailProvider::gmail("x").auth_url,
230 crate::social::SocialProvider::Google.authorize_url()
231 );
232 assert_eq!(
233 MailProvider::gmail("x").token_url,
234 crate::social::SocialProvider::Google.token_url()
235 );
236 }
237
238 #[test]
239 fn authorization_scopes_order_preserved() {
240 let p = MailProvider {
241 auth_url: "https://x".into(),
242 token_url: "https://x".into(),
243 client_id: "c".into(),
244 imap_scopes: vec!["a".into(), "b".into()],
245 smtp_scopes: vec!["b".into(), "c".into()],
246 extra_scopes: vec!["d".into(), "a".into()],
247 };
248 assert_eq!(p.authorization_scopes(), vec!["a", "b", "c", "d"]);
249 }
250}