1use std::collections::HashMap;
2
3use anyhow::{Context as _, Result, bail};
4
5use deltachat_contact_tools::may_be_valid_addr;
6
7use super::{DCLOGIN_SCHEME, Qr};
8use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
9use crate::provider::Socket;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum LoginOptions {
14 UnsuportedVersion(u32),
16
17 V1 {
19 mail_pw: String,
23
24 imap_host: Option<String>,
26
27 imap_port: Option<u16>,
29
30 imap_username: Option<String>,
32
33 imap_password: Option<String>,
35
36 imap_security: Option<Socket>,
38
39 smtp_host: Option<String>,
41
42 smtp_port: Option<u16>,
44
45 smtp_username: Option<String>,
47
48 smtp_password: Option<String>,
50
51 smtp_security: Option<Socket>,
53
54 certificate_checks: Option<EnteredCertificateChecks>,
56 },
57}
58
59pub(super) fn decode_login(qr: &str) -> Result<Qr> {
62 let qr = qr.replacen("://", ":", 1);
63
64 let url = url::Url::parse(&qr).with_context(|| format!("Malformed url: {qr:?}"))?;
65 let payload = qr
66 .get(DCLOGIN_SCHEME.len()..)
67 .context("invalid DCLOGIN payload E1")?;
68
69 let addr = payload
70 .split(['?', '/'])
71 .next()
72 .context("invalid DCLOGIN payload E3")?;
73
74 if url.scheme().eq_ignore_ascii_case("dclogin") {
75 let options = url.query_pairs();
76 if options.count() == 0 {
77 bail!("invalid DCLOGIN payload E4")
78 }
79 let parameter_map: HashMap<String, String> = options
81 .map(|(key, value)| (key.into_owned(), value.into_owned()))
82 .collect();
83
84 if !may_be_valid_addr(addr) {
86 bail!("invalid DCLOGIN payload: invalid username E5");
87 }
88
89 let options: LoginOptions = match parameter_map.get("v").map(|i| i.parse::<u32>()) {
91 Some(Ok(1)) => LoginOptions::V1 {
92 mail_pw: parameter_map
93 .get("p")
94 .map(|s| s.to_owned())
95 .context("password missing")?,
96 imap_host: parameter_map.get("ih").map(|s| s.to_owned()),
97 imap_port: parse_port(parameter_map.get("ip"))
98 .context("could not parse imap port")?,
99 imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
100 imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
101 imap_security: parse_socket_security(parameter_map.get("is"))?,
102 smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
103 smtp_port: parse_port(parameter_map.get("sp"))
104 .context("could not parse smtp port")?,
105 smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
106 smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
107 smtp_security: parse_socket_security(parameter_map.get("ss"))?,
108 certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
109 },
110 Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
111 Some(Err(_)) => bail!("version could not be parsed as number E6"),
112 None => bail!("invalid DCLOGIN payload: version missing E7"),
113 };
114
115 Ok(Qr::Login {
116 address: addr.to_owned(),
117 options,
118 })
119 } else {
120 bail!("Bad scheme for account URL: {payload:?}.");
121 }
122}
123
124fn parse_port(port: Option<&String>) -> core::result::Result<Option<u16>, std::num::ParseIntError> {
125 match port {
126 Some(p) => Ok(Some(p.parse::<u16>()?)),
127 None => Ok(None),
128 }
129}
130
131fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
132 Ok(match security.map(|s| s.as_str()) {
133 Some("ssl") => Some(Socket::Ssl),
134 Some("starttls") => Some(Socket::Starttls),
135 Some("default") => Some(Socket::Automatic),
136 Some("plain") => Some(Socket::Plain),
137 Some(other) => bail!("Unknown security level: {other}"),
138 None => None,
139 })
140}
141
142fn parse_certificate_checks(
143 certificate_checks: Option<&String>,
144) -> Result<Option<EnteredCertificateChecks>> {
145 Ok(match certificate_checks.map(|s| s.as_str()) {
146 Some("0") => Some(EnteredCertificateChecks::Automatic),
147 Some("1") => Some(EnteredCertificateChecks::Strict),
148 Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
149 Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
150 Some(other) => bail!("Unknown certificatecheck level: {other}"),
151 None => None,
152 })
153}
154
155pub(crate) fn login_param_from_login_qr(
156 addr: &str,
157 options: LoginOptions,
158) -> Result<EnteredLoginParam> {
159 match options {
160 LoginOptions::V1 {
161 mail_pw,
162 imap_host,
163 imap_port,
164 imap_username,
165 imap_password,
166 imap_security,
167 smtp_host,
168 smtp_port,
169 smtp_username,
170 smtp_password,
171 smtp_security,
172 certificate_checks,
173 } => {
174 let param = EnteredLoginParam {
175 addr: addr.to_string(),
176 imap: EnteredServerLoginParam {
177 server: imap_host.unwrap_or_default(),
178 port: imap_port.unwrap_or_default(),
179 security: imap_security.unwrap_or_default(),
180 user: imap_username.unwrap_or_default(),
181 password: imap_password.unwrap_or(mail_pw),
182 },
183 smtp: EnteredServerLoginParam {
184 server: smtp_host.unwrap_or_default(),
185 port: smtp_port.unwrap_or_default(),
186 security: smtp_security.unwrap_or_default(),
187 user: smtp_username.unwrap_or_default(),
188 password: smtp_password.unwrap_or_default(),
189 },
190 certificate_checks: certificate_checks.unwrap_or_default(),
191 oauth2: false,
192 };
193 Ok(param)
194 }
195 _ => bail!(
196 "DeltaChat does not understand this QR Code yet, please update the app and try again."
197 ),
198 }
199}
200
201#[cfg(test)]
202mod test {
203 use anyhow::bail;
204
205 use super::{LoginOptions, decode_login};
206 use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr};
207
208 macro_rules! login_options_just_pw {
209 ($pw: expr) => {
210 LoginOptions::V1 {
211 mail_pw: $pw,
212 imap_host: None,
213 imap_port: None,
214 imap_username: None,
215 imap_password: None,
216 imap_security: None,
217 smtp_host: None,
218 smtp_port: None,
219 smtp_username: None,
220 smtp_password: None,
221 smtp_security: None,
222 certificate_checks: None,
223 }
224 };
225 }
226
227 #[test]
228 fn minimal_no_options() -> anyhow::Result<()> {
229 let result = decode_login("dclogin://email@host.tld?p=123&v=1")?;
230 if let Qr::Login { address, options } = result {
231 assert_eq!(address, "email@host.tld".to_owned());
232 assert_eq!(options, login_options_just_pw!("123".to_owned()));
233 } else {
234 bail!("wrong type")
235 }
236 let result = decode_login("dclogin://email@host.tld/?p=123456&v=1")?;
237 if let Qr::Login { address, options } = result {
238 assert_eq!(address, "email@host.tld".to_owned());
239 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
240 } else {
241 bail!("wrong type")
242 }
243 let result = decode_login("dclogin://email@host.tld/ignored/path?p=123456&v=1")?;
244 if let Qr::Login { address, options } = result {
245 assert_eq!(address, "email@host.tld".to_owned());
246 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
247 } else {
248 bail!("wrong type")
249 }
250 Ok(())
251 }
252 #[test]
253 fn minimal_no_options_no_double_slash() -> anyhow::Result<()> {
254 let result = decode_login("dclogin:email@host.tld?p=123&v=1")?;
255 if let Qr::Login { address, options } = result {
256 assert_eq!(address, "email@host.tld".to_owned());
257 assert_eq!(options, login_options_just_pw!("123".to_owned()));
258 } else {
259 bail!("wrong type")
260 }
261 let result = decode_login("dclogin:email@host.tld/?p=123456&v=1")?;
262 if let Qr::Login { address, options } = result {
263 assert_eq!(address, "email@host.tld".to_owned());
264 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
265 } else {
266 bail!("wrong type")
267 }
268 let result = decode_login("dclogin:email@host.tld/ignored/path?p=123456&v=1")?;
269 if let Qr::Login { address, options } = result {
270 assert_eq!(address, "email@host.tld".to_owned());
271 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
272 } else {
273 bail!("wrong type")
274 }
275 Ok(())
276 }
277
278 #[test]
279 fn no_version_set() {
280 assert!(decode_login("dclogin:email@host.tld?p=123").is_err());
281 }
282
283 #[test]
284 fn invalid_version_set() {
285 assert!(decode_login("dclogin:email@host.tld?p=123&v=").is_err());
286 assert!(decode_login("dclogin:email@host.tld?p=123&v=%40").is_err());
287 assert!(decode_login("dclogin:email@host.tld?p=123&v=-20").is_err());
288 assert!(decode_login("dclogin:email@host.tld?p=123&v=hi").is_err());
289 }
290
291 #[test]
292 fn version_too_new() -> anyhow::Result<()> {
293 let result = decode_login("dclogin:email@host.tld/?p=123456&v=2")?;
294 if let Qr::Login { options, .. } = result {
295 assert_eq!(options, LoginOptions::UnsuportedVersion(2));
296 } else {
297 bail!("wrong type");
298 }
299 let result = decode_login("dclogin:email@host.tld/?p=123456&v=5")?;
300 if let Qr::Login { options, .. } = result {
301 assert_eq!(options, LoginOptions::UnsuportedVersion(5));
302 } else {
303 bail!("wrong type");
304 }
305 Ok(())
306 }
307
308 #[test]
309 fn all_advanced_options() -> anyhow::Result<()> {
310 let result = decode_login(
311 "dclogin:email@host.tld?p=secret&v=1&ih=imap.host.tld&ip=4000&iu=max&ipw=87654&is=ssl&ic=1&sh=mail.host.tld&sp=3000&su=max@host.tld&spw=3242HS&ss=plain&sc=3",
312 )?;
313 if let Qr::Login { address, options } = result {
314 assert_eq!(address, "email@host.tld".to_owned());
315 assert_eq!(
316 options,
317 LoginOptions::V1 {
318 mail_pw: "secret".to_owned(),
319 imap_host: Some("imap.host.tld".to_owned()),
320 imap_port: Some(4000),
321 imap_username: Some("max".to_owned()),
322 imap_password: Some("87654".to_owned()),
323 imap_security: Some(Socket::Ssl),
324 smtp_host: Some("mail.host.tld".to_owned()),
325 smtp_port: Some(3000),
326 smtp_username: Some("max@host.tld".to_owned()),
327 smtp_password: Some("3242HS".to_owned()),
328 smtp_security: Some(Socket::Plain),
329 certificate_checks: Some(EnteredCertificateChecks::Strict),
330 }
331 );
332 } else {
333 bail!("wrong type")
334 }
335 Ok(())
336 }
337
338 #[test]
339 fn uri_encoded_password() -> anyhow::Result<()> {
340 let result = decode_login(
341 "dclogin:email@host.tld?p=%7BDaehFl%3B%22as%40%21fhdodn5%24234%22%7B%7Dfg&v=1",
342 )?;
343 if let Qr::Login { address, options } = result {
344 assert_eq!(address, "email@host.tld".to_owned());
345 assert_eq!(
346 options,
347 login_options_just_pw!("{DaehFl;\"as@!fhdodn5$234\"{}fg".to_owned())
348 );
349 } else {
350 bail!("wrong type")
351 }
352 Ok(())
353 }
354
355 #[test]
356 fn email_with_plus_extension() -> anyhow::Result<()> {
357 let result = decode_login("dclogin:usename+extension@host?p=1234&v=1")?;
358 if let Qr::Login { address, options } = result {
359 assert_eq!(address, "usename+extension@host".to_owned());
360 assert_eq!(options, login_options_just_pw!("1234".to_owned()));
361 } else {
362 bail!("wrong type")
363 }
364 Ok(())
365 }
366
367 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
368 async fn test_decode_dclogin_ipv4() -> anyhow::Result<()> {
369 let result = decode_login("dclogin://test@[127.0.0.1]?p=1234&v=1")?;
370 if let Qr::Login { address, options } = result {
371 assert_eq!(address, "test@[127.0.0.1]".to_owned());
372 assert_eq!(options, login_options_just_pw!("1234".to_owned()));
373 } else {
374 unreachable!("wrong type");
375 }
376 Ok(())
377 }
378
379 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
380 async fn test_decode_dclogin_ipv6() -> anyhow::Result<()> {
381 let result =
382 decode_login("dclogin://test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]?p=1234&v=1")?;
383 if let Qr::Login { address, options } = result {
384 assert_eq!(
385 address,
386 "test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]".to_owned()
387 );
388 assert_eq!(options, login_options_just_pw!("1234".to_owned()));
389 } else {
390 unreachable!("wrong type");
391 }
392 Ok(())
393 }
394}