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