Merge pull request #3492 from deltachat/adb/qr-mailto-draft

Detect draft from QR with mailto data
This commit is contained in:
Asiel Díaz Benítez
2022-07-09 22:49:04 -04:00
committed by GitHub
4 changed files with 76 additions and 20 deletions

View File

@@ -3,6 +3,7 @@
## Unreleased ## Unreleased
### Changes ### Changes
- handle drafts from mailto links in scanned QR
### Fixes ### Fixes

View File

@@ -2292,7 +2292,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID: * - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
* scanned fingerprint does not match last seen fingerprint. * 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; * the scanned QR code contains a fingerprint but no e-mail address;
* suggest the user to establish an encrypted connection first. * 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(). * if so, call dc_set_config_from_qr().
* *
* - DC_QR_ADDR with dc_lot_t::id=Contact ID: * - 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; * ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id(). * if so, call dc_create_chat_by_contact_id().
* *

View File

@@ -51,7 +51,7 @@ impl Lot {
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint), Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain), Qr::Account { domain } => Some(domain),
Qr::WebrtcInstance { domain, .. } => Some(domain), Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { .. } => None, Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url), Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text), Qr::Text { text } => Some(text),
Qr::WithdrawVerifyContact { .. } => None, Qr::WithdrawVerifyContact { .. } => None,
@@ -79,7 +79,13 @@ impl Lot {
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username, Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self, 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, Self::Error(_err) => Meaning::None,
} }
} }
@@ -118,7 +124,7 @@ impl Lot {
Qr::FprWithoutAddr { .. } => Default::default(), Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(), Qr::Account { .. } => Default::default(),
Qr::WebrtcInstance { .. } => 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::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(), Qr::Text { .. } => Default::default(),
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(), Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),

View File

@@ -61,6 +61,7 @@ pub enum Qr {
}, },
Addr { Addr {
contact_id: ContactId, contact_id: ContactId,
draft: Option<String>,
}, },
Url { Url {
url: String, 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<Qr> { async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
let payload = &qr[MAILTO_SCHEME.len()..]; let payload = &qr[MAILTO_SCHEME.len()..];
let addr = if let Some(query_index) = payload.find('?') { let (addr, query) = if let Some(query_index) = payload.find('?') {
&payload[..query_index] (&payload[..query_index], &payload[query_index + 1..])
} else { } else {
payload (payload, "")
};
let param: BTreeMap<&str, &str> = query
.split('&')
.filter_map(|s| {
if let [key, value] = s.splitn(2, '=').collect::<Vec<_>>()[..] {
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 addr = normalize_address(addr)?;
let name = "".to_string(); 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. /// Extract address for the smtp scheme.
@@ -477,7 +515,7 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
let addr = normalize_address(addr)?; let addr = normalize_address(addr)?;
let name = "".to_string(); 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. /// Extract address for the matmsg scheme.
@@ -502,7 +540,7 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
let addr = normalize_address(addr)?; let addr = normalize_address(addr)?;
let name = "".to_string(); let name = "".to_string();
Qr::from_address(context, name, addr).await Qr::from_address(context, name, addr, None).await
} }
static VCARD_NAME_RE: Lazy<regex::Regex> = static VCARD_NAME_RE: Lazy<regex::Regex> =
@@ -531,14 +569,19 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
bail!("Bad e-mail address"); bail!("Bad e-mail address");
}; };
Qr::from_address(context, name, addr).await Qr::from_address(context, name, addr, None).await
} }
impl Qr { impl Qr {
pub async fn from_address(context: &Context, name: String, addr: String) -> Result<Self> { pub async fn from_address(
context: &Context,
name: String,
addr: String,
draft: Option<String>,
) -> Result<Self> {
let (contact_id, _) = let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await?; 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" "BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD"
).await?; ).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?; let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
assert_eq!(contact.get_name(), "First Last"); assert_eq!(contact.get_name(), "First Last");
assert_eq!(contact.get_authname(), ""); assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_display_name(), "First Last"); assert_eq!(contact.get_display_name(), "First Last");
assert!(draft.is_none());
} else { } else {
bail!("Wrong QR code type"); bail!("Wrong QR code type");
} }
@@ -642,9 +686,10 @@ mod tests {
) )
.await?; .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?; let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
assert!(draft.is_none());
} else { } else {
bail!("Wrong QR code type"); bail!("Wrong QR code type");
} }
@@ -658,20 +703,22 @@ mod tests {
let qr = check_qr( let qr = check_qr(
&ctx.ctx, &ctx.ctx,
"mailto:stress@test.local?subject=hello&body=world", "mailto:stress@test.local?subject=hello&body=beautiful+world",
) )
.await?; .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?; let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
assert_eq!(draft.unwrap(), "hello\nbeautiful world");
} else { } else {
bail!("Wrong QR code type"); bail!("Wrong QR code type");
} }
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?; 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?; let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "no-questionmark@example.org"); assert_eq!(contact.get_addr(), "no-questionmark@example.org");
assert!(draft.is_none());
} else { } else {
bail!("Wrong QR code type"); bail!("Wrong QR code type");
} }
@@ -686,11 +733,12 @@ mod tests {
async fn test_decode_smtp() -> Result<()> { async fn test_decode_smtp() -> Result<()> {
let ctx = TestContext::new().await; 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? check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await?
{ {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
assert!(draft.is_none());
} else { } else {
bail!("Wrong QR code type"); bail!("Wrong QR code type");
} }