feat(vcard): add RFC 6868 parameter value encoding

Adds application/adapters/param_encoding.rs: encode/decode for the three
RFC 6868 caret-escape sequences (^n newline, ^^ literal caret, ^' literal
DQUOTE) plus render_param_value, which quotes and encodes a parameter
value only when the vCard/iCalendar param-value grammar requires it
(comma/semicolon/colon triggers quoting; an embedded DQUOTE/caret/newline
needs escaping even once quoted).

Wires render_param_value into the two vCard generators' TYPE parameter
emission (EMAIL/TEL/ADR) in carddav_adapter.rs::contact_to_vcard and
contact_service.rs::generate_vcard — contact `type` fields are free text,
not a fixed enum, so a value containing a comma or quote previously
produced outright broken vCard syntax (no quoting/escaping was applied
at all).

Scope note: this branch only fixes the generation side. The existing
vCard parser is a naive line-by-line substring scanner (`line.contains
("TYPE=WORK")`) with no real per-parameter tokenizing, so there's no
integration point yet for the decode half without a larger parser
rewrite — that's part of the vCard 4.0 work (RFC 6350), which depends on
this branch for the shared encoding utility.
This commit is contained in:
M.Schmidt
2026-07-11 20:16:11 +02:00
parent b15136acf7
commit 027ba8a528
5 changed files with 280 additions and 5 deletions
+7 -2
View File
@@ -2,6 +2,7 @@ use chrono::Utc;
use std::sync::Arc;
use uuid::Uuid;
use crate::application::adapters::param_encoding::render_param_value;
use crate::application::dtos::address_book_dto::{
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
};
@@ -286,8 +287,10 @@ impl ContactService {
// Email addresses
for email in contact.email() {
let mut ty = String::new();
crate::common::fmt::push_upper(&mut ty, &email.r#type);
vcard.push_str("EMAIL;TYPE=");
crate::common::fmt::push_upper(&mut vcard, &email.r#type);
vcard.push_str(&render_param_value(&ty));
vcard.push(':');
vcard.push_str(&email.email);
vcard.push_str("\r\n");
@@ -313,8 +316,10 @@ impl ContactService {
let postal_code = addr.postal_code.as_deref().unwrap_or_default();
let country = addr.country.as_deref().unwrap_or_default();
let mut ty = String::new();
crate::common::fmt::push_upper(&mut ty, &addr.r#type);
vcard.push_str("ADR;TYPE=");
crate::common::fmt::push_upper(&mut vcard, &addr.r#type);
vcard.push_str(&render_param_value(&ty));
let _ = write!(
vcard,
":;;{};{};{};{};{}\r\n",