deltachat/
qr.rs

1//! # QR code module.
2
3mod 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:"; // yes: uppercase
28const 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
40/// Backup transfer based on iroh-net.
41pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
42
43/// Version written to Backups and Backup-QR-Codes.
44/// Imports will fail when they have a larger version.
45pub(crate) const DCBACKUP_VERSION: i32 = 3;
46
47/// Scanned QR code.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Qr {
50    /// Ask the user whether to verify the contact.
51    ///
52    /// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
53    AskVerifyContact {
54        /// ID of the contact.
55        contact_id: ContactId,
56
57        /// Fingerprint of the contact key as scanned from the QR code.
58        fingerprint: Fingerprint,
59
60        /// Invite number.
61        invitenumber: String,
62
63        /// Authentication code.
64        authcode: String,
65    },
66
67    /// Ask the user whether to join the group.
68    AskVerifyGroup {
69        /// Group name.
70        grpname: String,
71
72        /// Group ID.
73        grpid: String,
74
75        /// ID of the contact.
76        contact_id: ContactId,
77
78        /// Fingerprint of the contact key as scanned from the QR code.
79        fingerprint: Fingerprint,
80
81        /// Invite number.
82        invitenumber: String,
83
84        /// Authentication code.
85        authcode: String,
86    },
87
88    /// Ask whether to join the broadcast channel.
89    AskJoinBroadcast {
90        /// The user-visible name of this broadcast channel
91        name: String,
92
93        /// A string of random characters,
94        /// uniquely identifying this broadcast channel across all databases/clients.
95        /// Called `grpid` for historic reasons:
96        /// The id of multi-user chats is always called `grpid` in the database
97        /// because groups were once the only multi-user chats.
98        grpid: String,
99
100        /// ID of the contact who owns the channel and created the QR code.
101        contact_id: ContactId,
102
103        /// Fingerprint of the contact's key as scanned from the QR code.
104        fingerprint: Fingerprint,
105
106        /// Invite number.
107        invitenumber: String,
108        /// Authentication code.
109        authcode: String,
110    },
111
112    /// Contact fingerprint is verified.
113    ///
114    /// Ask the user if they want to start chatting.
115    FprOk {
116        /// Contact ID.
117        contact_id: ContactId,
118    },
119
120    /// Scanned fingerprint does not match the last seen fingerprint.
121    FprMismatch {
122        /// Contact ID.
123        contact_id: Option<ContactId>,
124    },
125
126    /// The scanned QR code contains a fingerprint but no e-mail address.
127    FprWithoutAddr {
128        /// Key fingerprint.
129        fingerprint: String,
130    },
131
132    /// Ask the user if they want to create an account on the given domain.
133    Account {
134        /// Server domain name.
135        domain: String,
136    },
137
138    /// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
139    Backup2 {
140        /// Iroh node address.
141        node_addr: iroh::NodeAddr,
142
143        /// Authentication token.
144        auth_token: String,
145    },
146
147    /// The QR code is a backup, but it is too new. The user has to update its Delta Chat.
148    BackupTooNew {},
149
150    /// Ask the user if they want to use the given proxy.
151    ///
152    /// Note that HTTP(S) URLs without a path
153    /// and query parameters are treated as HTTP(S) proxy URL.
154    /// UI may want to still offer to open the URL
155    /// in the browser if QR code contents
156    /// starts with `http://` or `https://`
157    /// and the QR code was not scanned from
158    /// the proxy configuration screen.
159    Proxy {
160        /// Proxy URL.
161        ///
162        /// This is the URL that is going to be added.
163        url: String,
164
165        /// Host extracted from the URL to display in the UI.
166        host: String,
167
168        /// Port extracted from the URL to display in the UI.
169        port: u16,
170    },
171
172    /// Contact address is scanned.
173    ///
174    /// Optionally, a draft message could be provided.
175    /// Ask the user if they want to start chatting.
176    Addr {
177        /// Contact ID.
178        contact_id: ContactId,
179
180        /// Draft message.
181        draft: Option<String>,
182    },
183
184    /// URL scanned.
185    ///
186    /// Ask the user if they want to open a browser or copy the URL to clipboard.
187    Url {
188        /// URL.
189        url: String,
190    },
191
192    /// Text scanned.
193    ///
194    /// Ask the user if they want to copy the text to clipboard.
195    Text {
196        /// Scanned text.
197        text: String,
198    },
199
200    /// Ask the user if they want to withdraw their own QR code.
201    WithdrawVerifyContact {
202        /// Contact ID.
203        contact_id: ContactId,
204
205        /// Fingerprint of the contact key as scanned from the QR code.
206        fingerprint: Fingerprint,
207
208        /// Invite number.
209        invitenumber: String,
210
211        /// Authentication code.
212        authcode: String,
213    },
214
215    /// Ask the user if they want to withdraw their own group invite QR code.
216    WithdrawVerifyGroup {
217        /// Group name.
218        grpname: String,
219
220        /// Group ID.
221        grpid: String,
222
223        /// Contact ID.
224        contact_id: ContactId,
225
226        /// Fingerprint of the contact key as scanned from the QR code.
227        fingerprint: Fingerprint,
228
229        /// Invite number.
230        invitenumber: String,
231
232        /// Authentication code.
233        authcode: String,
234    },
235
236    /// Ask the user if they want to withdraw their own broadcast channel invite QR code.
237    WithdrawJoinBroadcast {
238        /// The user-visible name of this broadcast channel
239        name: String,
240
241        /// A string of random characters,
242        /// uniquely identifying this broadcast channel across all databases/clients.
243        /// Called `grpid` for historic reasons:
244        /// The id of multi-user chats is always called `grpid` in the database
245        /// because groups were once the only multi-user chats.
246        grpid: String,
247
248        /// Contact ID. Always `ContactId::SELF`.
249        contact_id: ContactId,
250
251        /// Fingerprint of the contact's key as scanned from the QR code.
252        fingerprint: Fingerprint,
253
254        /// Invite number.
255        invitenumber: String,
256
257        /// Authentication code.
258        authcode: String,
259    },
260
261    /// Ask the user if they want to revive their own QR code.
262    ReviveVerifyContact {
263        /// Contact ID.
264        contact_id: ContactId,
265
266        /// Fingerprint of the contact key as scanned from the QR code.
267        fingerprint: Fingerprint,
268
269        /// Invite number.
270        invitenumber: String,
271
272        /// Authentication code.
273        authcode: String,
274    },
275
276    /// Ask the user if they want to revive their own group invite QR code.
277    ReviveVerifyGroup {
278        /// Group name.
279        grpname: String,
280
281        /// Group ID.
282        grpid: String,
283
284        /// Contact ID.
285        contact_id: ContactId,
286
287        /// Fingerprint of the contact key as scanned from the QR code.
288        fingerprint: Fingerprint,
289
290        /// Invite number.
291        invitenumber: String,
292
293        /// Authentication code.
294        authcode: String,
295    },
296
297    /// Ask the user if they want to revive their own broadcast channel invite QR code.
298    ReviveJoinBroadcast {
299        /// The user-visible name of this broadcast channel
300        name: String,
301
302        /// A string of random characters,
303        /// uniquely identifying this broadcast channel across all databases/clients.
304        /// Called `grpid` for historic reasons:
305        /// The id of multi-user chats is always called `grpid` in the database
306        /// because groups were once the only multi-user chats.
307        grpid: String,
308
309        /// Contact ID. Always `ContactId::SELF`.
310        contact_id: ContactId,
311
312        /// Fingerprint of the contact's key as scanned from the QR code.
313        fingerprint: Fingerprint,
314
315        /// Invite number.
316        invitenumber: String,
317
318        /// Authentication code.
319        authcode: String,
320    },
321
322    /// `dclogin:` scheme parameters.
323    ///
324    /// Ask the user if they want to login with the email address.
325    Login {
326        /// Email address.
327        address: String,
328
329        /// Login parameters.
330        options: LoginOptions,
331    },
332}
333
334// hack around the changed JSON accidentally used by an iroh upgrade, see #6518 for more details and for code snippet.
335// this hack is mainly needed to give ppl time to upgrade and can be removed after some months (added 2025-02)
336fn 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
345/// Checks a scanned QR code.
346///
347/// The function should be called after a QR code is scanned.
348/// The function takes the raw text scanned and checks what can be done with it.
349pub 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                // Parsing with a non-standard scheme
387                // is a hack to work around the `url` crate bug
388                // <https://github.com/servo/rust-url/issues/957>.
389                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                    // Should not happen.
395                    url
396                };
397
398                if url.port().is_none() | (url.path() != "") | url.query().is_some() {
399                    // URL without a port, with a path or query cannot be a proxy URL.
400                    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
425/// Formats the text of the [`Qr::Backup2`] variant.
426///
427/// This is the inverse of [`check_qr`] for that variant only.
428///
429/// TODO: Refactor this so all variants have a correct [`Display`] and transform `check_qr`
430/// into `FromStr`.
431pub 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
446/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
447///     or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
448///     or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&b=BROADCAST_NAME&x=BROADCAST_ID&j=INVITENUMBER&s=AUTH`
449///     or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
450async 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    // macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
456    // see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
457    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(&param, "n")?.unwrap_or_default();
486
487    let invitenumber = param
488        .get("i")
489        // For historic reansons, broadcasts currently use j instead of i for the invitenumber:
490        .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(&param, "g")?;
503    let broadcast_name = decode_name(&param, "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"); // sometimes spaces are encoded as `+`
635        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
648/// scheme: `https://i.delta.chat[/]#FINGERPRINT&a=ADDR[&OPTIONAL_PARAMS]`
649async 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
657/// scheme: `DCACCOUNT:example.org`
658/// or `DCACCOUNT:https://example.org/new`
659/// or `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
660fn 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
683/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
684fn 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
721/// Decodes `ss://` URLs for Shadowsocks proxies.
722fn 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
734/// Decodes a `DCBACKUP` QR code.
735fn 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 address.
763    email: String,
764
765    /// Password.
766    password: String,
767}
768#[derive(Debug, Deserialize)]
769struct CreateAccountErrorResponse {
770    /// Reason for the failure to create account returned by the server.
771    reason: String,
772}
773
774/// Takes a QR with `DCACCOUNT:` scheme, parses its parameters,
775/// downloads additional information from the contained URL
776/// and returns the login parameters.
777pub(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
836/// Sets configuration values from a QR code.
837pub 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            // Normalize the URL.
850            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
951/// Extract address for the mailto scheme.
952///
953/// Scheme: `mailto:addr...?subject=...&body=..`
954async 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"); // sometimes spaces are encoded as `+`
987    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
1003/// Extract address for the smtp scheme.
1004///
1005/// Scheme: `SMTP:addr...:subject...:body...`
1006async 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
1017/// Extract address for the matmsg scheme.
1018///
1019/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
1020///
1021/// There may or may not be linebreaks after the fields.
1022async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
1023    // Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
1024    // we ignore this case.
1025    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
1046/// Extract address for the vcard scheme.
1047///
1048/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
1049async 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    /// Creates a new scanned QR code of a contact address.
1071    ///
1072    /// May contain a message draft.
1073    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
1086/// URL decodes a given address, does basic email validation on the result.
1087fn normalize_address(addr: &str) -> Result<String> {
1088    // urldecoding is needed at least for OPENPGP4FPR but should not hurt in the other cases
1089    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;