1mod dclogin_scheme;
4use std::collections::BTreeMap;
5use std::sync::LazyLock;
6
7use anyhow::{Context as _, Result, anyhow, bail, ensure};
8pub use dclogin_scheme::LoginOptions;
9pub(crate) use dclogin_scheme::login_param_from_login_qr;
10use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
11use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
12use rand::TryRngCore as _;
13use rand::distr::{Alphanumeric, SampleString};
14use serde::Deserialize;
15
16use crate::config::Config;
17use crate::contact::{Contact, ContactId, Origin};
18use crate::context::Context;
19use crate::events::EventType;
20use crate::key::Fingerprint;
21use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
22use crate::net::http::post_empty;
23use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig};
24use crate::token;
25use crate::tools::{time, validate_id};
26
27const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
29const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
30const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
31pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
32const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
33const MAILTO_SCHEME: &str = "mailto:";
34const MATMSG_SCHEME: &str = "MATMSG:";
35const VCARD_SCHEME: &str = "BEGIN:VCARD";
36const SMTP_SCHEME: &str = "SMTP:";
37const HTTPS_SCHEME: &str = "https://";
38const SHADOWSOCKS_SCHEME: &str = "ss://";
39
40pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
42
43pub(crate) const DCBACKUP_VERSION: i32 = 3;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Qr {
50 AskVerifyContact {
54 contact_id: ContactId,
56
57 fingerprint: Fingerprint,
59
60 invitenumber: String,
62
63 authcode: String,
65 },
66
67 AskVerifyGroup {
69 grpname: String,
71
72 grpid: String,
74
75 contact_id: ContactId,
77
78 fingerprint: Fingerprint,
80
81 invitenumber: String,
83
84 authcode: String,
86 },
87
88 AskJoinBroadcast {
90 name: String,
92
93 grpid: String,
99
100 contact_id: ContactId,
102
103 fingerprint: Fingerprint,
105
106 invitenumber: String,
108 authcode: String,
110 },
111
112 FprOk {
116 contact_id: ContactId,
118 },
119
120 FprMismatch {
122 contact_id: Option<ContactId>,
124 },
125
126 FprWithoutAddr {
128 fingerprint: String,
130 },
131
132 Account {
134 domain: String,
136 },
137
138 Backup2 {
140 node_addr: iroh::NodeAddr,
142
143 auth_token: String,
145 },
146
147 BackupTooNew {},
149
150 Proxy {
160 url: String,
164
165 host: String,
167
168 port: u16,
170 },
171
172 Addr {
177 contact_id: ContactId,
179
180 draft: Option<String>,
182 },
183
184 Url {
188 url: String,
190 },
191
192 Text {
196 text: String,
198 },
199
200 WithdrawVerifyContact {
202 contact_id: ContactId,
204
205 fingerprint: Fingerprint,
207
208 invitenumber: String,
210
211 authcode: String,
213 },
214
215 WithdrawVerifyGroup {
217 grpname: String,
219
220 grpid: String,
222
223 contact_id: ContactId,
225
226 fingerprint: Fingerprint,
228
229 invitenumber: String,
231
232 authcode: String,
234 },
235
236 WithdrawJoinBroadcast {
238 name: String,
240
241 grpid: String,
247
248 contact_id: ContactId,
250
251 fingerprint: Fingerprint,
253
254 invitenumber: String,
256
257 authcode: String,
259 },
260
261 ReviveVerifyContact {
263 contact_id: ContactId,
265
266 fingerprint: Fingerprint,
268
269 invitenumber: String,
271
272 authcode: String,
274 },
275
276 ReviveVerifyGroup {
278 grpname: String,
280
281 grpid: String,
283
284 contact_id: ContactId,
286
287 fingerprint: Fingerprint,
289
290 invitenumber: String,
292
293 authcode: String,
295 },
296
297 ReviveJoinBroadcast {
299 name: String,
301
302 grpid: String,
308
309 contact_id: ContactId,
311
312 fingerprint: Fingerprint,
314
315 invitenumber: String,
317
318 authcode: String,
320 },
321
322 Login {
326 address: String,
328
329 options: LoginOptions,
331 },
332}
333
334fn fix_add_second_device_qr(qr: &str) -> String {
337 qr.replacen(r#","info":{"relay_url":"#, r#","relay_url":"#, 1)
338 .replacen(r#""]}}"#, r#""]}"#, 1)
339}
340
341fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
342 string.to_lowercase().starts_with(&pattern.to_lowercase())
343}
344
345pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
350 let qr = qr.trim();
351 let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
352 decode_openpgp(context, qr)
353 .await
354 .context("failed to decode OPENPGP4FPR QR code")?
355 } else if qr.starts_with(IDELTACHAT_SCHEME) {
356 decode_ideltachat(context, IDELTACHAT_SCHEME, qr).await?
357 } else if qr.starts_with(IDELTACHAT_NOSLASH_SCHEME) {
358 decode_ideltachat(context, IDELTACHAT_NOSLASH_SCHEME, qr).await?
359 } else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
360 decode_account(qr)?
361 } else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
362 dclogin_scheme::decode_login(qr)?
363 } else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
364 decode_tg_socks_proxy(context, qr)?
365 } else if qr.starts_with(SHADOWSOCKS_SCHEME) {
366 decode_shadowsocks_proxy(qr)?
367 } else if starts_with_ignore_case(qr, DCBACKUP_SCHEME_PREFIX) {
368 let qr_fixed = fix_add_second_device_qr(qr);
369 decode_backup2(&qr_fixed)?
370 } else if qr.starts_with(MAILTO_SCHEME) {
371 decode_mailto(context, qr).await?
372 } else if qr.starts_with(SMTP_SCHEME) {
373 decode_smtp(context, qr).await?
374 } else if qr.starts_with(MATMSG_SCHEME) {
375 decode_matmsg(context, qr).await?
376 } else if qr.starts_with(VCARD_SCHEME) {
377 decode_vcard(context, qr).await?
378 } else if let Ok(url) = url::Url::parse(qr) {
379 match url.scheme() {
380 "socks5" => Qr::Proxy {
381 url: qr.to_string(),
382 host: url.host_str().context("URL has no host")?.to_string(),
383 port: url.port().unwrap_or(DEFAULT_SOCKS_PORT),
384 },
385 "http" | "https" => {
386 let url = if let Some(rest) = qr.strip_prefix("http://") {
390 url::Url::parse(&format!("foobarbaz://{rest}"))?
391 } else if let Some(rest) = qr.strip_prefix("https://") {
392 url::Url::parse(&format!("foobarbaz://{rest}"))?
393 } else {
394 url
396 };
397
398 if url.port().is_none() | (url.path() != "") | url.query().is_some() {
399 Qr::Url {
401 url: qr.to_string(),
402 }
403 } else {
404 Qr::Proxy {
405 url: qr.to_string(),
406 host: url.host_str().context("URL has no host")?.to_string(),
407 port: url
408 .port_or_known_default()
409 .context("HTTP(S) URLs are guaranteed to return Some port")?,
410 }
411 }
412 }
413 _ => Qr::Url {
414 url: qr.to_string(),
415 },
416 }
417 } else {
418 Qr::Text {
419 text: qr.to_string(),
420 }
421 };
422 Ok(qrcode)
423}
424
425pub fn format_backup(qr: &Qr) -> Result<String> {
432 match qr {
433 Qr::Backup2 {
434 node_addr,
435 auth_token,
436 } => {
437 let node_addr = serde_json::to_string(node_addr)?;
438 Ok(format!(
439 "{DCBACKUP_SCHEME_PREFIX}{DCBACKUP_VERSION}:{auth_token}&{node_addr}"
440 ))
441 }
442 _ => Err(anyhow!("Not a backup QR code")),
443 }
444}
445
446async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
451 let payload = qr
452 .get(OPENPGP4FPR_SCHEME.len()..)
453 .context("Invalid OPENPGP4FPR scheme")?;
454
455 let (fingerprint, fragment) = match payload
458 .split_once('#')
459 .or_else(|| payload.split_once("%23"))
460 {
461 Some(pair) => pair,
462 None => (payload, ""),
463 };
464 let fingerprint: Fingerprint = fingerprint
465 .parse()
466 .context("Failed to parse fingerprint in the QR code")?;
467
468 let param: BTreeMap<&str, &str> = fragment
469 .split('&')
470 .filter_map(|s| {
471 if let [key, value] = s.splitn(2, '=').collect::<Vec<_>>()[..] {
472 Some((key, value))
473 } else {
474 None
475 }
476 })
477 .collect();
478
479 let addr = if let Some(addr) = param.get("a") {
480 Some(normalize_address(addr)?)
481 } else {
482 None
483 };
484
485 let name = decode_name(¶m, "n")?.unwrap_or_default();
486
487 let invitenumber = param
488 .get("i")
489 .or_else(|| param.get("j"))
491 .filter(|&s| validate_id(s))
492 .map(|s| s.to_string());
493 let authcode = param
494 .get("s")
495 .filter(|&s| validate_id(s))
496 .map(|s| s.to_string());
497 let grpid = param
498 .get("x")
499 .filter(|&s| validate_id(s))
500 .map(|s| s.to_string());
501
502 let grpname = decode_name(¶m, "g")?;
503 let broadcast_name = decode_name(¶m, "b")?;
504
505 if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
506 let addr = ContactAddress::new(addr)?;
507 let (contact_id, _) = Contact::add_or_lookup_ex(
508 context,
509 &name,
510 &addr,
511 &fingerprint.hex(),
512 Origin::UnhandledSecurejoinQrScan,
513 )
514 .await
515 .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
516
517 if let (Some(grpid), Some(grpname)) = (grpid.clone(), grpname) {
518 if context
519 .is_self_addr(&addr)
520 .await
521 .with_context(|| format!("can't check if address {addr:?} is our address"))?
522 {
523 if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
524 Ok(Qr::WithdrawVerifyGroup {
525 grpname,
526 grpid,
527 contact_id,
528 fingerprint,
529 invitenumber,
530 authcode,
531 })
532 } else {
533 Ok(Qr::ReviveVerifyGroup {
534 grpname,
535 grpid,
536 contact_id,
537 fingerprint,
538 invitenumber,
539 authcode,
540 })
541 }
542 } else {
543 Ok(Qr::AskVerifyGroup {
544 grpname,
545 grpid,
546 contact_id,
547 fingerprint,
548 invitenumber,
549 authcode,
550 })
551 }
552 } else if let (Some(grpid), Some(name)) = (grpid, broadcast_name) {
553 if context
554 .is_self_addr(&addr)
555 .await
556 .with_context(|| format!("Can't check if {addr:?} is our address"))?
557 {
558 if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
559 Ok(Qr::WithdrawJoinBroadcast {
560 name,
561 grpid,
562 contact_id,
563 fingerprint,
564 invitenumber,
565 authcode,
566 })
567 } else {
568 Ok(Qr::ReviveJoinBroadcast {
569 name,
570 grpid,
571 contact_id,
572 fingerprint,
573 invitenumber,
574 authcode,
575 })
576 }
577 } else {
578 Ok(Qr::AskJoinBroadcast {
579 name,
580 grpid,
581 contact_id,
582 fingerprint,
583 invitenumber,
584 authcode,
585 })
586 }
587 } else if context.is_self_addr(&addr).await? {
588 if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
589 Ok(Qr::WithdrawVerifyContact {
590 contact_id,
591 fingerprint,
592 invitenumber,
593 authcode,
594 })
595 } else {
596 Ok(Qr::ReviveVerifyContact {
597 contact_id,
598 fingerprint,
599 invitenumber,
600 authcode,
601 })
602 }
603 } else {
604 Ok(Qr::AskVerifyContact {
605 contact_id,
606 fingerprint,
607 invitenumber,
608 authcode,
609 })
610 }
611 } else if let Some(addr) = addr {
612 let fingerprint = fingerprint.hex();
613 let (contact_id, _) =
614 Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan)
615 .await?;
616 let contact = Contact::get_by_id(context, contact_id).await?;
617
618 if contact.public_key(context).await?.is_some() {
619 Ok(Qr::FprOk { contact_id })
620 } else {
621 Ok(Qr::FprMismatch {
622 contact_id: Some(contact_id),
623 })
624 }
625 } else {
626 Ok(Qr::FprWithoutAddr {
627 fingerprint: fingerprint.to_string(),
628 })
629 }
630}
631
632fn decode_name(param: &BTreeMap<&str, &str>, key: &str) -> Result<Option<String>> {
633 if let Some(encoded_name) = param.get(key) {
634 let encoded_name = encoded_name.replace('+', "%20"); let mut name = match percent_decode_str(&encoded_name).decode_utf8() {
636 Ok(name) => name.to_string(),
637 Err(err) => bail!("Invalid QR param {key}: {err}"),
638 };
639 if let Some(n) = name.strip_suffix('_') {
640 name = format!("{n}…");
641 }
642 Ok(Some(name))
643 } else {
644 Ok(None)
645 }
646}
647
648async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<Qr> {
650 let qr = qr.replacen(prefix, OPENPGP4FPR_SCHEME, 1);
651 let qr = qr.replacen('&', "#", 1);
652 decode_openpgp(context, &qr)
653 .await
654 .with_context(|| format!("failed to decode {prefix} QR code"))
655}
656
657fn decode_account(qr: &str) -> Result<Qr> {
661 let payload = qr
662 .get(DCACCOUNT_SCHEME.len()..)
663 .context("Invalid DCACCOUNT payload")?;
664 if payload.starts_with("https://") {
665 let url = url::Url::parse(payload).context("Invalid account URL")?;
666 if url.scheme() == "https" {
667 Ok(Qr::Account {
668 domain: url
669 .host_str()
670 .context("can't extract account setup domain")?
671 .to_string(),
672 })
673 } else {
674 bail!("Bad scheme for account URL: {:?}.", url.scheme());
675 }
676 } else {
677 Ok(Qr::Account {
678 domain: payload.to_string(),
679 })
680 }
681}
682
683fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
685 let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
686
687 let mut host: Option<String> = None;
688 let mut port: u16 = DEFAULT_SOCKS_PORT;
689 let mut user: Option<String> = None;
690 let mut pass: Option<String> = None;
691 for (key, value) in url.query_pairs() {
692 if key == "server" {
693 host = Some(value.to_string());
694 } else if key == "port" {
695 port = value.parse().unwrap_or(DEFAULT_SOCKS_PORT);
696 } else if key == "user" {
697 user = Some(value.to_string());
698 } else if key == "pass" {
699 pass = Some(value.to_string());
700 }
701 }
702
703 let Some(host) = host else {
704 bail!("Bad t.me/socks url: {url:?}");
705 };
706
707 let mut url = "socks5://".to_string();
708 if let Some(pass) = pass {
709 url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC).to_string();
710 url += ":";
711 url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
712 url += "@";
713 };
714 url += &host;
715 url += ":";
716 url += &port.to_string();
717
718 Ok(Qr::Proxy { url, host, port })
719}
720
721fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
723 let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
724 let addr = server_config.addr();
725 let host = addr.host().to_string();
726 let port = addr.port();
727 Ok(Qr::Proxy {
728 url: qr.to_string(),
729 host,
730 port,
731 })
732}
733
734fn decode_backup2(qr: &str) -> Result<Qr> {
736 let version_and_payload = qr
737 .strip_prefix(DCBACKUP_SCHEME_PREFIX)
738 .ok_or_else(|| anyhow!("Invalid DCBACKUP scheme"))?;
739 let (version, payload) = version_and_payload
740 .split_once(':')
741 .context("DCBACKUP scheme separator missing")?;
742 let version: i32 = version.parse().context("Not a valid number")?;
743 if version > DCBACKUP_VERSION {
744 return Ok(Qr::BackupTooNew {});
745 }
746
747 let (auth_token, node_addr) = payload
748 .split_once('&')
749 .context("Backup QR code has no separator")?;
750 let auth_token = auth_token.to_string();
751 let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
752 .context("Invalid node addr in backup QR code")?;
753
754 Ok(Qr::Backup2 {
755 node_addr,
756 auth_token,
757 })
758}
759
760#[derive(Debug, Deserialize)]
761struct CreateAccountSuccessResponse {
762 email: String,
764
765 password: String,
767}
768#[derive(Debug, Deserialize)]
769struct CreateAccountErrorResponse {
770 reason: String,
772}
773
774pub(crate) async fn login_param_from_account_qr(
778 context: &Context,
779 qr: &str,
780) -> Result<EnteredLoginParam> {
781 let payload = qr
782 .get(DCACCOUNT_SCHEME.len()..)
783 .context("Invalid DCACCOUNT scheme")?;
784
785 if !payload.starts_with(HTTPS_SCHEME) {
786 let rng = &mut rand::rngs::OsRng.unwrap_err();
787 let username = Alphanumeric.sample_string(rng, 9);
788 let addr = username + "@" + payload;
789 let password = Alphanumeric.sample_string(rng, 50);
790
791 let param = EnteredLoginParam {
792 addr,
793 imap: EnteredServerLoginParam {
794 password,
795 ..Default::default()
796 },
797 smtp: Default::default(),
798 certificate_checks: EnteredCertificateChecks::Strict,
799 oauth2: false,
800 };
801 return Ok(param);
802 }
803
804 let (response_text, response_success) = post_empty(context, payload).await?;
805 if response_success {
806 let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
807 .with_context(|| {
808 format!("Cannot create account, response is malformed:\n{response_text:?}")
809 })?;
810
811 let param = EnteredLoginParam {
812 addr: email,
813 imap: EnteredServerLoginParam {
814 password,
815 ..Default::default()
816 },
817 smtp: Default::default(),
818 certificate_checks: EnteredCertificateChecks::Strict,
819 oauth2: false,
820 };
821
822 Ok(param)
823 } else {
824 match serde_json::from_str::<CreateAccountErrorResponse>(&response_text) {
825 Ok(error) => Err(anyhow!(error.reason)),
826 Err(parse_error) => {
827 context.emit_event(EventType::Error(format!(
828 "Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
829 )));
830 bail!("Cannot create account, unexpected server response:\n{response_text:?}")
831 }
832 }
833 }
834}
835
836pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
838 match check_qr(context, qr).await? {
839 Qr::Account { .. } => {
840 let mut param = login_param_from_account_qr(context, qr).await?;
841 context.add_transport_inner(&mut param).await?
842 }
843 Qr::Proxy { url, .. } => {
844 let old_proxy_url_value = context
845 .get_config(Config::ProxyUrl)
846 .await?
847 .unwrap_or_default();
848
849 let url = ProxyConfig::from_url(&url)?.to_url();
851
852 let proxy_urls: Vec<&str> = std::iter::once(url.as_str())
853 .chain(
854 old_proxy_url_value
855 .split('\n')
856 .filter(|s| !s.is_empty() && *s != url),
857 )
858 .collect();
859 context
860 .set_config(Config::ProxyUrl, Some(&proxy_urls.join("\n")))
861 .await?;
862 context.set_config_bool(Config::ProxyEnabled, true).await?;
863 }
864 Qr::WithdrawVerifyContact {
865 invitenumber,
866 authcode,
867 ..
868 } => {
869 token::delete(context, "").await?;
870 context
871 .sync_qr_code_token_deletion(invitenumber, authcode)
872 .await?;
873 }
874 Qr::WithdrawVerifyGroup {
875 grpid,
876 invitenumber,
877 authcode,
878 ..
879 }
880 | Qr::WithdrawJoinBroadcast {
881 grpid,
882 invitenumber,
883 authcode,
884 ..
885 } => {
886 token::delete(context, &grpid).await?;
887 context
888 .sync_qr_code_token_deletion(invitenumber, authcode)
889 .await?;
890 }
891 Qr::ReviveVerifyContact {
892 invitenumber,
893 authcode,
894 ..
895 } => {
896 let timestamp = time();
897 token::save(
898 context,
899 token::Namespace::InviteNumber,
900 None,
901 &invitenumber,
902 timestamp,
903 )
904 .await?;
905 token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?;
906 context.sync_qr_code_tokens(None).await?;
907 context.scheduler.interrupt_inbox().await;
908 }
909 Qr::ReviveVerifyGroup {
910 invitenumber,
911 authcode,
912 grpid,
913 ..
914 }
915 | Qr::ReviveJoinBroadcast {
916 invitenumber,
917 authcode,
918 grpid,
919 ..
920 } => {
921 let timestamp = time();
922 token::save(
923 context,
924 token::Namespace::InviteNumber,
925 Some(&grpid),
926 &invitenumber,
927 timestamp,
928 )
929 .await?;
930 token::save(
931 context,
932 token::Namespace::Auth,
933 Some(&grpid),
934 &authcode,
935 timestamp,
936 )
937 .await?;
938 context.sync_qr_code_tokens(Some(&grpid)).await?;
939 context.scheduler.interrupt_inbox().await;
940 }
941 Qr::Login { address, options } => {
942 let mut param = login_param_from_login_qr(&address, options)?;
943 context.add_transport_inner(&mut param).await?
944 }
945 _ => bail!("QR code does not contain config"),
946 }
947
948 Ok(())
949}
950
951async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
955 let payload = qr
956 .get(MAILTO_SCHEME.len()..)
957 .context("Invalid mailto: scheme")?;
958
959 let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
960
961 let param: BTreeMap<&str, &str> = query
962 .split('&')
963 .filter_map(|s| {
964 if let [key, value] = s.splitn(2, '=').collect::<Vec<_>>()[..] {
965 Some((key, value))
966 } else {
967 None
968 }
969 })
970 .collect();
971
972 let subject = if let Some(subject) = param.get("subject") {
973 subject.to_string()
974 } else {
975 "".to_string()
976 };
977 let draft = if let Some(body) = param.get("body") {
978 if subject.is_empty() {
979 body.to_string()
980 } else {
981 subject + "\n" + body
982 }
983 } else {
984 subject
985 };
986 let draft = draft.replace('+', "%20"); let draft = match percent_decode_str(&draft).decode_utf8() {
988 Ok(decoded_draft) => decoded_draft.to_string(),
989 Err(_err) => draft,
990 };
991
992 let addr = normalize_address(addr)?;
993 let name = "";
994 Qr::from_address(
995 context,
996 name,
997 &addr,
998 if draft.is_empty() { None } else { Some(draft) },
999 )
1000 .await
1001}
1002
1003async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
1007 let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
1008
1009 let (addr, _rest) = payload
1010 .split_once(':')
1011 .context("Invalid SMTP scheme payload")?;
1012 let addr = normalize_address(addr)?;
1013 let name = "";
1014 Qr::from_address(context, name, &addr, None).await
1015}
1016
1017async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
1023 let addr = if let Some(to_index) = qr.find("TO:") {
1026 let addr = qr.get(to_index + 3..).unwrap_or_default().trim();
1027 if let Some(semi_index) = addr.find(';') {
1028 addr.get(..semi_index).unwrap_or_default().trim()
1029 } else {
1030 addr
1031 }
1032 } else {
1033 bail!("Invalid MATMSG found");
1034 };
1035
1036 let addr = normalize_address(addr)?;
1037 let name = "";
1038 Qr::from_address(context, name, &addr, None).await
1039}
1040
1041static VCARD_NAME_RE: LazyLock<regex::Regex> =
1042 LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
1043static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
1044 LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
1045
1046async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
1050 let name = VCARD_NAME_RE
1051 .captures(qr)
1052 .and_then(|caps| {
1053 let last_name = caps.get(1)?.as_str().trim();
1054 let first_name = caps.get(2)?.as_str().trim();
1055
1056 Some(format!("{first_name} {last_name}"))
1057 })
1058 .unwrap_or_default();
1059
1060 let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
1061 normalize_address(cap.as_str().trim())?
1062 } else {
1063 bail!("Bad e-mail address");
1064 };
1065
1066 Qr::from_address(context, &name, &addr, None).await
1067}
1068
1069impl Qr {
1070 pub async fn from_address(
1074 context: &Context,
1075 name: &str,
1076 addr: &str,
1077 draft: Option<String>,
1078 ) -> Result<Self> {
1079 let addr = ContactAddress::new(addr)?;
1080 let (contact_id, _) =
1081 Contact::add_or_lookup(context, name, &addr, Origin::UnhandledQrScan).await?;
1082 Ok(Qr::Addr { contact_id, draft })
1083 }
1084}
1085
1086fn normalize_address(addr: &str) -> Result<String> {
1088 let new_addr = percent_decode_str(addr).decode_utf8()?;
1090 let new_addr = addr_normalize(&new_addr);
1091
1092 ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
1093
1094 Ok(new_addr.to_string())
1095}
1096
1097#[cfg(test)]
1098mod qr_tests;