diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d4c0144..48a9d3ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Changes +- handle drafts from mailto links in scanned QR ### Fixes diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 13d2d56ee..9620c560d 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2292,7 +2292,7 @@ void dc_stop_ongoing_process (dc_context_t* context); * - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID: * scanned fingerprint does not match last seen fingerprint. * - * - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint + * - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint * the scanned QR code contains a fingerprint but no e-mail address; * suggest the user to establish an encrypted connection first. * @@ -2305,7 +2305,8 @@ void dc_stop_ongoing_process (dc_context_t* context); * if so, call dc_set_config_from_qr(). * * - DC_QR_ADDR with dc_lot_t::id=Contact ID: - * e-mail address scanned, + * e-mail address scanned, optionally, a draft message could be set in + * dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT; * ask the user if they want to start chatting; * if so, call dc_create_chat_by_contact_id(). * diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index 9426ef425..2f1a1ff76 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -51,7 +51,7 @@ impl Lot { Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint), Qr::Account { domain } => Some(domain), Qr::WebrtcInstance { domain, .. } => Some(domain), - Qr::Addr { .. } => None, + Qr::Addr { draft, .. } => draft.as_deref(), Qr::Url { url } => Some(url), Qr::Text { text } => Some(text), Qr::WithdrawVerifyContact { .. } => None, @@ -79,7 +79,13 @@ impl Lot { Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username, Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self, }, - Self::Qr(_qr) => Meaning::None, + Self::Qr(qr) => match qr { + Qr::Addr { + draft: Some(_draft), + .. + } => Meaning::Text1Draft, + _ => Meaning::None, + }, Self::Error(_err) => Meaning::None, } } @@ -118,7 +124,7 @@ impl Lot { Qr::FprWithoutAddr { .. } => Default::default(), Qr::Account { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(), - Qr::Addr { contact_id } => contact_id.to_u32(), + Qr::Addr { contact_id, .. } => contact_id.to_u32(), Qr::Url { .. } => Default::default(), Qr::Text { .. } => Default::default(), Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(), diff --git a/src/qr.rs b/src/qr.rs index d3693fff8..3f4089f4d 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -61,6 +61,7 @@ pub enum Qr { }, Addr { contact_id: ContactId, + draft: Option, }, Url { url: String, @@ -451,15 +452,52 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { async fn decode_mailto(context: &Context, qr: &str) -> Result { let payload = &qr[MAILTO_SCHEME.len()..]; - let addr = if let Some(query_index) = payload.find('?') { - &payload[..query_index] + let (addr, query) = if let Some(query_index) = payload.find('?') { + (&payload[..query_index], &payload[query_index + 1..]) } else { - payload + (payload, "") + }; + + let param: BTreeMap<&str, &str> = query + .split('&') + .filter_map(|s| { + if let [key, value] = s.splitn(2, '=').collect::>()[..] { + Some((key, value)) + } else { + None + } + }) + .collect(); + + let subject = if let Some(subject) = param.get("subject") { + subject.to_string() + } else { + "".to_string() + }; + let draft = if let Some(body) = param.get("body") { + if subject.is_empty() { + body.to_string() + } else { + subject + "\n" + body + } + } else { + subject + }; + let draft = draft.replace('+', "%20"); // sometimes spaces are encoded as `+` + let draft = match percent_decode_str(&draft).decode_utf8() { + Ok(decoded_draft) => decoded_draft.to_string(), + Err(_err) => draft, }; let addr = normalize_address(addr)?; let name = "".to_string(); - Qr::from_address(context, name, addr).await + Qr::from_address( + context, + name, + addr, + if draft.is_empty() { None } else { Some(draft) }, + ) + .await } /// Extract address for the smtp scheme. @@ -477,7 +515,7 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result { let addr = normalize_address(addr)?; let name = "".to_string(); - Qr::from_address(context, name, addr).await + Qr::from_address(context, name, addr, None).await } /// Extract address for the matmsg scheme. @@ -502,7 +540,7 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result { let addr = normalize_address(addr)?; let name = "".to_string(); - Qr::from_address(context, name, addr).await + Qr::from_address(context, name, addr, None).await } static VCARD_NAME_RE: Lazy = @@ -531,14 +569,19 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result { bail!("Bad e-mail address"); }; - Qr::from_address(context, name, addr).await + Qr::from_address(context, name, addr, None).await } impl Qr { - pub async fn from_address(context: &Context, name: String, addr: String) -> Result { + pub async fn from_address( + context: &Context, + name: String, + addr: String, + draft: Option, + ) -> Result { let (contact_id, _) = Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await?; - Ok(Qr::Addr { contact_id }) + Ok(Qr::Addr { contact_id, draft }) } } @@ -619,12 +662,13 @@ mod tests { "BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD" ).await?; - if let Qr::Addr { contact_id } = qr { + if let Qr::Addr { contact_id, draft } = qr { let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_name(), "First Last"); assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_display_name(), "First Last"); + assert!(draft.is_none()); } else { bail!("Wrong QR code type"); } @@ -642,9 +686,10 @@ mod tests { ) .await?; - if let Qr::Addr { contact_id } = qr { + if let Qr::Addr { contact_id, draft } = qr { let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; assert_eq!(contact.get_addr(), "stress@test.local"); + assert!(draft.is_none()); } else { bail!("Wrong QR code type"); } @@ -658,20 +703,22 @@ mod tests { let qr = check_qr( &ctx.ctx, - "mailto:stress@test.local?subject=hello&body=world", + "mailto:stress@test.local?subject=hello&body=beautiful+world", ) .await?; - if let Qr::Addr { contact_id } = qr { + if let Qr::Addr { contact_id, draft } = qr { let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; assert_eq!(contact.get_addr(), "stress@test.local"); + assert_eq!(draft.unwrap(), "hello\nbeautiful world"); } else { bail!("Wrong QR code type"); } let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?; - if let Qr::Addr { contact_id } = res { + if let Qr::Addr { contact_id, draft } = res { let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; assert_eq!(contact.get_addr(), "no-questionmark@example.org"); + assert!(draft.is_none()); } else { bail!("Wrong QR code type"); } @@ -686,11 +733,12 @@ mod tests { async fn test_decode_smtp() -> Result<()> { let ctx = TestContext::new().await; - if let Qr::Addr { contact_id } = + if let Qr::Addr { contact_id, draft } = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await? { let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; assert_eq!(contact.get_addr(), "stress@test.local"); + assert!(draft.is_none()); } else { bail!("Wrong QR code type"); }