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