/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use indexmap::IndexMap; use rusqlite::{Connection, Row, params}; use serde_json::{Map, Value}; use crate::jmap::blob::{export_blob_ids, import_blob_ids}; use crate::jmap::error::JmapError; use crate::jmap::wire::JmapId; use crate::jmap::wire::address_book::AddressBook; use crate::jmap::wire::calendar::Calendar; use crate::jmap::wire::calendar_event::CalendarEvent; use crate::jmap::wire::contact_card::ContactCard; use crate::jmap::wire::email::Email; use crate::jmap::wire::file_node::FileNode; use crate::jmap::wire::identity::Identity; use crate::jmap::wire::mailbox::Mailbox; use crate::jmap::wire::participant_identity::ParticipantIdentity; use crate::jmap::wire::sieve_script::SieveScript; use crate::types::ObjectType; pub trait LocalResolver { fn local(&self, ty: ObjectType, jmap_id: &str) -> Option; } pub trait TargetResolver { fn target(&self, ty: ObjectType, local_id: i64) -> Option; } pub trait BlobIntern { fn intern(&mut self, jmap_blob_id: &str) -> Result; } pub trait BlobUpload { fn upload(&mut self, local_id: i64) -> Result; } fn translate_in( ids: &IndexMap, ty: ObjectType, resolver: &impl LocalResolver, ) -> Result, JmapError> { let mut out = Vec::with_capacity(ids.len()); for (id, _) in ids { match resolver.local(ty, &id.0) { Some(local) => out.push(local), None => { return Err(JmapError::malformed(format!( "unresolved {} reference {} on insert", ty.jmap_name(), id.0 ))); } } } Ok(out) } fn translate_out( locals: &[i64], ty: ObjectType, resolver: &impl TargetResolver, ) -> Result, JmapError> { let mut out = IndexMap::new(); for local in locals { match resolver.target(ty, *local) { Some(target) => { out.insert(target, true); } None => { return Err(JmapError::malformed(format!( "unresolved local {} id {local} on export", ty.jmap_name() ))); } } } Ok(out) } fn id_array_json(ids: &[i64]) -> String { let vals: Vec = ids.iter().map(|i| Value::from(*i)).collect(); Value::Array(vals).to_string() } fn parse_local_id_array(text: &str) -> Result, JmapError> { let v: Value = serde_json::from_str(text)?; v.as_array() .ok_or_else(|| JmapError::malformed("id column is not a JSON array"))? .iter() .map(|x| { x.as_i64() .ok_or_else(|| JmapError::malformed("id array element is not an integer")) }) .collect() } fn opt_parent( resolver: &impl LocalResolver, ty: ObjectType, parent: &Option, ) -> Option { parent.as_ref().and_then(|p| resolver.local(ty, &p.0)) } pub fn insert_mailbox( conn: &Connection, wire: &Mailbox, resolver: &impl LocalResolver, ) -> Result { let parent = opt_parent(resolver, ObjectType::Mailbox, &wire.parent_id); conn.execute( "INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed) VALUES (?1, ?2, ?3, ?4, ?5)", params![ wire.name, parent, wire.role, wire.sort_order, wire.is_subscribed as i64 ], )?; Ok(conn.last_insert_rowid()) } pub const MAILBOX_SELECT: &str = "SELECT id, name, parent_id, role, sort_order, is_subscribed FROM mailboxes"; pub fn row_to_mailbox(row: &Row, resolver: &impl TargetResolver) -> Result { let parent_local: Option = row.get(2)?; let parent_id = match parent_local { Some(p) => Some( resolver .target(ObjectType::Mailbox, p) .ok_or_else(|| JmapError::malformed("unresolved mailbox parent on export"))?, ), None => None, }; Ok(Mailbox { id: None, name: row.get(1)?, parent_id, role: row.get(3)?, sort_order: row.get::<_, i64>(4)? as u32, is_subscribed: row.get::<_, i64>(5)? != 0, }) } pub fn insert_email( conn: &Connection, wire: &Email, blob_local_id: i64, message_match: &str, resolver: &impl LocalResolver, ) -> Result { let mailbox_locals = translate_in(&wire.mailbox_ids, ObjectType::Mailbox, resolver)?; if mailbox_locals.is_empty() { return Err(JmapError::malformed("email has no resolvable mailbox")); } let keywords: Vec = wire.keywords.keys().cloned().collect(); conn.execute( "INSERT INTO emails (blob_id, received_at, mailbox_ids, keywords, message_match) VALUES (?1, ?2, ?3, ?4, ?5)", params![ blob_local_id, format_utc(&wire.received_at)?, id_array_json(&mailbox_locals), Value::Array(keywords.iter().map(|k| Value::from(k.as_str())).collect()).to_string(), message_match ], )?; Ok(conn.last_insert_rowid()) } #[derive(Debug, Clone)] pub struct EmailRow { pub blob_local_id: i64, pub received_at: String, pub mailbox_locals: Vec, pub keywords: Vec, pub message_match: String, } pub const EMAIL_SELECT: &str = "SELECT id, blob_id, received_at, mailbox_ids, keywords, message_match FROM emails"; pub fn row_to_email(row: &Row) -> Result { let mailbox_text: String = row.get(3)?; let keyword_text: String = row.get(4)?; let keywords: Vec = serde_json::from_str(&keyword_text)?; Ok(EmailRow { blob_local_id: row.get(1)?, received_at: row.get(2)?, mailbox_locals: parse_local_id_array(&mailbox_text)?, keywords, message_match: row.get(5)?, }) } pub fn insert_identity(conn: &Connection, wire: &Identity) -> Result { conn.execute( "INSERT INTO identities (name, email, reply_to, bcc, text_signature, html_signature) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ wire.name, wire.email, opt_json(&wire.reply_to)?, opt_json(&wire.bcc)?, wire.text_signature, wire.html_signature ], )?; Ok(conn.last_insert_rowid()) } pub const IDENTITY_SELECT: &str = "SELECT id, name, email, reply_to, bcc, text_signature, html_signature FROM identities"; pub fn row_to_identity(row: &Row) -> Result { Ok(Identity { id: None, name: row.get(1)?, email: row.get(2)?, reply_to: from_opt_json(row.get::<_, Option>(3)?)?, bcc: from_opt_json(row.get::<_, Option>(4)?)?, text_signature: row.get(5)?, html_signature: row.get(6)?, }) } pub fn insert_sieve_script( conn: &Connection, wire: &SieveScript, blob_local_id: i64, ) -> Result { conn.execute( "INSERT INTO sieve_scripts (name, is_active, blob_id) VALUES (?1, ?2, ?3)", params![wire.name, wire.is_active as i64, blob_local_id], )?; Ok(conn.last_insert_rowid()) } pub const SIEVE_SELECT: &str = "SELECT id, name, is_active, blob_id FROM sieve_scripts"; pub fn insert_address_book(conn: &Connection, wire: &AddressBook) -> Result { conn.execute( "INSERT INTO address_books (name, description, sort_order, is_default, is_subscribed) VALUES (?1, ?2, ?3, ?4, ?5)", params![ wire.name, wire.description, wire.sort_order, wire.is_default as i64, wire.is_subscribed as i64 ], )?; Ok(conn.last_insert_rowid()) } pub const ADDRESS_BOOK_SELECT: &str = "SELECT id, name, description, sort_order, is_default, is_subscribed FROM address_books"; pub fn insert_calendar(conn: &Connection, wire: &Calendar) -> Result { conn.execute( "INSERT INTO calendars (name, description, color, sort_order, is_subscribed, is_visible, is_default, include_in_availability, default_alerts_with_time, default_alerts_without_time, time_zone) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![ wire.name, wire.description, wire.color, wire.sort_order, wire.is_subscribed as i64, wire.is_visible as i64, wire.is_default as i64, wire.include_in_availability, opt_json(&wire.default_alerts_with_time)?, opt_json(&wire.default_alerts_without_time)?, wire.time_zone ], )?; Ok(conn.last_insert_rowid()) } pub const CALENDAR_SELECT: &str = "SELECT id, name, description, color, sort_order, is_subscribed, \ is_visible, is_default, include_in_availability, default_alerts_with_time, \ default_alerts_without_time, time_zone FROM calendars"; pub fn insert_participant_identity( conn: &Connection, wire: &ParticipantIdentity, ) -> Result { conn.execute( "INSERT INTO participant_identities (name, calendar_address, is_default) VALUES (?1, ?2, ?3)", params![wire.name, wire.calendar_address, wire.is_default as i64], )?; Ok(conn.last_insert_rowid()) } pub const PARTICIPANT_IDENTITY_SELECT: &str = "SELECT id, name, calendar_address, is_default FROM participant_identities"; pub fn insert_file_node( conn: &Connection, wire: &FileNode, blob_local_id: Option, resolver: &impl LocalResolver, ) -> Result { let parent = opt_parent(resolver, ObjectType::FileNode, &wire.parent_id); let node_type = serde_json::to_value(wire.node_type)? .as_str() .unwrap_or("file") .to_owned(); let target = match &wire.target { Some(t) => Some(serde_json::to_string(t)?), None => None, }; conn.execute( "INSERT INTO file_nodes (parent_id, node_type, blob_id, target, name, media_type, created, modified, is_subscribed, role) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", params![ parent, node_type, blob_local_id, target, wire.name, wire.media_type, format_utc(&wire.created)?, opt_format_utc(&wire.modified)?, wire.is_subscribed as i64, wire.role ], )?; Ok(conn.last_insert_rowid()) } pub const FILE_NODE_SELECT: &str = "SELECT id, parent_id, node_type, blob_id, target, name, \ media_type, created, modified, is_subscribed, role FROM file_nodes"; pub fn insert_contact_card( conn: &Connection, wire: &ContactCard, resolver: &impl LocalResolver, blobs: &mut impl BlobIntern, ) -> Result { let address_books = translate_in(&wire.address_book_ids, ObjectType::AddressBook, resolver)?; let mut data = Value::Object(value_map(&wire.rest)); let uid = take_string(&mut data, "uid") .ok_or_else(|| JmapError::malformed("ContactCard has no uid"))?; rewrite_blobs_in(&mut data, blobs)?; conn.execute( "INSERT INTO contact_cards (uid, address_book_ids, data) VALUES (?1, ?2, ?3)", params![uid, id_array_json(&address_books), data.to_string()], )?; Ok(conn.last_insert_rowid()) } pub const CONTACT_CARD_SELECT: &str = "SELECT id, uid, address_book_ids, data FROM contact_cards"; pub fn insert_calendar_event( conn: &Connection, wire: &CalendarEvent, resolver: &impl LocalResolver, blobs: &mut impl BlobIntern, ) -> Result { let calendars = translate_in(&wire.calendar_ids, ObjectType::Calendar, resolver)?; let mut data = Value::Object(value_map(&wire.rest)); for drop_key in ["method", "utcStart", "utcEnd", "isOrigin", "baseEventId"] { if let Value::Object(m) = &mut data { m.remove(drop_key); } } rewrite_blobs_in(&mut data, blobs)?; conn.execute( "INSERT INTO calendar_events (calendar_ids, is_draft, use_default_alerts, data) VALUES (?1, ?2, ?3, ?4)", params![ id_array_json(&calendars), wire.is_draft as i64, wire.use_default_alerts as i64, data.to_string() ], )?; Ok(conn.last_insert_rowid()) } pub const CALENDAR_EVENT_SELECT: &str = "SELECT id, calendar_ids, is_draft, use_default_alerts, data FROM calendar_events \ WHERE data_type = 'Event'"; pub fn contact_card_to_wire( uid: &str, address_book_ids: &str, data: &str, resolver: &impl TargetResolver, blobs: &mut impl BlobUpload, ) -> Result { let mut value: Value = serde_json::from_str(data)?; restore_blobs_in(&mut value, blobs)?; let locals = parse_local_id_array(address_book_ids)?; let abids = translate_out(&locals, ObjectType::AddressBook, resolver)?; if let Value::Object(map) = &mut value { map.insert("uid".to_owned(), Value::from(uid)); map.insert("addressBookIds".to_owned(), id_bool_map(&abids)); } Ok(value) } pub fn calendar_event_to_wire( calendar_ids: &str, is_draft: bool, use_default_alerts: bool, data: &str, resolver: &impl TargetResolver, blobs: &mut impl BlobUpload, ) -> Result { let mut value: Value = serde_json::from_str(data)?; restore_blobs_in(&mut value, blobs)?; let locals = parse_local_id_array(calendar_ids)?; let calids = translate_out(&locals, ObjectType::Calendar, resolver)?; if let Value::Object(map) = &mut value { map.insert("calendarIds".to_owned(), id_bool_map(&calids)); map.insert("isDraft".to_owned(), Value::Bool(is_draft)); map.insert( "useDefaultAlerts".to_owned(), Value::Bool(use_default_alerts), ); } Ok(value) } fn id_bool_map(ids: &IndexMap) -> Value { let mut m = Map::new(); for (id, _) in ids { m.insert(id.0.clone(), Value::Bool(true)); } Value::Object(m) } fn value_map(rest: &IndexMap) -> Map { let mut m = Map::new(); for (k, v) in rest { m.insert(k.clone(), v.clone()); } m } fn take_string(value: &mut Value, key: &str) -> Option { if let Value::Object(map) = value && let Some(Value::String(s)) = map.remove(key) { return Some(s); } None } fn rewrite_blobs_in(data: &mut Value, blobs: &mut impl BlobIntern) -> Result<(), JmapError> { import_blob_ids(data, |jmap_blob_id| { blobs .intern(jmap_blob_id) .map_err(|e| crate::jmap::blob::BlobWalkError::Resolver(e.to_string())) }) .map_err(JmapError::from) } fn restore_blobs_in(data: &mut Value, blobs: &mut impl BlobUpload) -> Result<(), JmapError> { export_blob_ids(data, |local_id| { blobs .upload(local_id) .map(|id| id.0) .map_err(|e| crate::jmap::blob::BlobWalkError::Resolver(e.to_string())) }) .map_err(JmapError::from) } fn opt_json(value: &Option) -> Result, JmapError> { match value { Some(v) => Ok(Some(serde_json::to_string(v)?)), None => Ok(None), } } fn from_opt_json( text: Option, ) -> Result, JmapError> { match text { Some(t) => Ok(Some(serde_json::from_str(&t)?)), None => Ok(None), } } pub fn row_to_address_book(row: &Row) -> Result { Ok(AddressBook { id: None, name: row.get(1)?, description: row.get(2)?, sort_order: row.get::<_, i64>(3)? as u32, is_default: row.get::<_, i64>(4)? != 0, is_subscribed: row.get::<_, i64>(5)? != 0, }) } pub fn row_to_calendar(row: &Row) -> Result { Ok(Calendar { id: None, name: row.get(1)?, description: row.get(2)?, color: row.get(3)?, sort_order: row.get::<_, i64>(4)? as u32, is_subscribed: row.get::<_, i64>(5)? != 0, is_visible: row.get::<_, i64>(6)? != 0, is_default: row.get::<_, i64>(7)? != 0, include_in_availability: row.get(8)?, default_alerts_with_time: from_opt_json(row.get::<_, Option>(9)?)?, default_alerts_without_time: from_opt_json(row.get::<_, Option>(10)?)?, time_zone: row.get(11)?, }) } pub fn row_to_participant_identity(row: &Row) -> Result { Ok(ParticipantIdentity { id: None, name: row.get(1)?, calendar_address: row.get(2)?, is_default: row.get::<_, i64>(3)? != 0, }) } #[derive(Debug, Clone)] pub struct SieveRow { pub name: Option, pub is_active: bool, pub blob_local_id: i64, } pub fn row_to_sieve_script(row: &Row) -> Result { Ok(SieveRow { name: row.get(1)?, is_active: row.get::<_, i64>(2)? != 0, blob_local_id: row.get(3)?, }) } #[derive(Debug, Clone)] pub struct FileNodeRow { pub wire: FileNode, pub blob_local_id: Option, } pub fn row_to_file_node( row: &Row, resolver: &impl TargetResolver, ) -> Result { let parent_local: Option = row.get(1)?; let parent_id = match parent_local { Some(p) => Some( resolver .target(ObjectType::FileNode, p) .ok_or_else(|| JmapError::malformed("unresolved file_node parent on export"))?, ), None => None, }; let node_type = serde_json::from_value(Value::from(row.get::<_, String>(2)?))?; let target: Option> = from_opt_json(row.get::<_, Option>(4)?)?; let blob_local_id: Option = row.get(3)?; Ok(FileNodeRow { wire: FileNode { id: None, parent_id, node_type, blob_id: None, target, name: row.get(5)?, media_type: row.get(6)?, created: parse_utc(&row.get::<_, String>(7)?)?, modified: opt_parse_utc(row.get::<_, Option>(8)?)?, is_subscribed: row.get::<_, i64>(9)? != 0, role: row.get(10)?, }, blob_local_id, }) } fn parse_utc(text: &str) -> Result { time::OffsetDateTime::parse(text, &time::format_description::well_known::Rfc3339) .map_err(|e| JmapError::malformed(format!("cannot parse date {text}: {e}"))) } fn opt_parse_utc(text: Option) -> Result, JmapError> { match text { Some(t) => Ok(Some(parse_utc(&t)?)), None => Ok(None), } } fn format_utc(value: &time::OffsetDateTime) -> Result { value .format(&time::format_description::well_known::Rfc3339) .map_err(|e| JmapError::malformed(format!("cannot format date: {e}"))) } fn opt_format_utc(value: &Option) -> Result, JmapError> { match value { Some(v) => Ok(Some(format_utc(v)?)), None => Ok(None), } } #[cfg(test)] mod tests { use super::*; use crate::db::init; use std::collections::HashMap; struct MapResolver { to_local: HashMap<(ObjectType, String), i64>, to_target: HashMap<(ObjectType, i64), String>, } impl LocalResolver for MapResolver { fn local(&self, ty: ObjectType, jmap_id: &str) -> Option { self.to_local.get(&(ty, jmap_id.to_owned())).copied() } } impl TargetResolver for MapResolver { fn target(&self, ty: ObjectType, local_id: i64) -> Option { self.to_target .get(&(ty, local_id)) .map(|s| JmapId(s.clone())) } } struct FakeBlobs; impl BlobIntern for FakeBlobs { fn intern(&mut self, _jmap_blob_id: &str) -> Result { Ok(42) } } impl BlobUpload for FakeBlobs { fn upload(&mut self, local_id: i64) -> Result { Ok(JmapId(format!("T{local_id}"))) } } fn mem() -> Connection { let c = Connection::open_in_memory().unwrap(); init::apply_schema(&c).unwrap(); c } #[test] fn mailbox_roundtrips_with_parent_translation() { let c = mem(); let root: Mailbox = serde_json::from_value(serde_json::json!({ "id": "M1", "name": "Root", "role": "archive", "sortOrder": 3 })) .unwrap(); let empty = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; let root_local = insert_mailbox(&c, &root, &empty).unwrap(); let child: Mailbox = serde_json::from_value(serde_json::json!({ "id": "M2", "name": "Child", "parentId": "M1" })) .unwrap(); let mut res = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; res.to_local .insert((ObjectType::Mailbox, "M1".to_owned()), root_local); let child_local = insert_mailbox(&c, &child, &res).unwrap(); res.to_target .insert((ObjectType::Mailbox, root_local), "TGT1".to_owned()); let wire = c .query_row( &format!("{MAILBOX_SELECT} WHERE id = ?1"), params![child_local], |row| Ok(row_to_mailbox(row, &res)), ) .unwrap() .unwrap(); assert_eq!(wire.name, "Child"); assert_eq!(wire.parent_id, Some(JmapId("TGT1".to_owned()))); } #[test] fn email_keywords_keep_verbatim_case_and_mailbox_translation() { let c = mem(); let mut res = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; res.to_local .insert((ObjectType::Mailbox, "MB".to_owned()), 7); let email: Email = serde_json::from_value(serde_json::json!({ "id": "E1", "blobId": "B1", "receivedAt": "2021-05-04T10:00:00Z", "mailboxIds": { "MB": true }, "keywords": { "$Seen": true, "MyTag": true } })) .unwrap(); let blob_id = crate::db::blobs::intern_blob(&c, b"rfc5322 bytes").unwrap(); let local = insert_email(&c, &email, blob_id, "{}", &res).unwrap(); let got = c .query_row( &format!("{EMAIL_SELECT} WHERE id = ?1"), params![local], |row| Ok(row_to_email(row)), ) .unwrap() .unwrap(); assert_eq!(got.blob_local_id, blob_id); assert_eq!(got.mailbox_locals, vec![7]); assert_eq!(got.keywords, vec!["$Seen".to_owned(), "MyTag".to_owned()]); } #[test] fn unresolved_foreign_key_on_insert_is_malformed() { let c = mem(); let empty = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; let email: Email = serde_json::from_value(serde_json::json!({ "id": "E1", "blobId": "B1", "receivedAt": "2021-05-04T10:00:00Z", "mailboxIds": { "UNKNOWN": true }, "keywords": {} })) .unwrap(); let err = insert_email(&c, &email, 1, "{}", &empty).unwrap_err(); assert!(matches!(err, JmapError::Malformed(_)), "got {err:?}"); } #[test] fn unresolved_target_on_export_is_malformed() { let c = mem(); let root: Mailbox = serde_json::from_value(serde_json::json!({ "id": "M1", "name": "Root", "parentId": "P9" })) .unwrap(); let mut res = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; res.to_local .insert((ObjectType::Mailbox, "P9".to_owned()), 1); let local = insert_mailbox(&c, &root, &res).unwrap(); let err = c .query_row( &format!("{MAILBOX_SELECT} WHERE id = ?1"), params![local], |row| Ok(row_to_mailbox(row, &res)), ) .unwrap() .unwrap_err(); assert!(matches!(err, JmapError::Malformed(_)), "got {err:?}"); } #[test] fn contact_card_strips_uid_and_rewrites_blob() { let c = mem(); let res = MapResolver { to_local: HashMap::from([((ObjectType::AddressBook, "AB".to_owned()), 1)]), to_target: HashMap::from([((ObjectType::AddressBook, 1), "TAB".to_owned())]), }; let card: ContactCard = serde_json::from_value(serde_json::json!({ "id": "C1", "addressBookIds": { "AB": true }, "uid": "urn:uuid:42", "name": { "full": "Jane" }, "photos": { "p1": { "blobId": "PB", "mediaType": "image/png" } } })) .unwrap(); let mut blobs = FakeBlobs; let local = insert_contact_card(&c, &card, &res, &mut blobs).unwrap(); let (uid, abids, data): (String, String, String) = c .query_row( &format!("{CONTACT_CARD_SELECT} WHERE id = ?1"), params![local], |r| Ok((r.get(1)?, r.get(2)?, r.get(3)?)), ) .unwrap(); assert_eq!(uid, "urn:uuid:42"); let stored: Value = serde_json::from_str(&data).unwrap(); assert!(stored.get("uid").is_none()); assert_eq!(stored["photos"]["p1"]["@blob"], Value::from(42)); let mut up = FakeBlobs; let wire = contact_card_to_wire(&uid, &abids, &data, &res, &mut up).unwrap(); assert_eq!(wire["uid"], Value::from("urn:uuid:42")); assert_eq!(wire["addressBookIds"]["TAB"], Value::Bool(true)); assert_eq!(wire["photos"]["p1"]["blobId"], Value::from("T42")); assert!(wire["photos"]["p1"].get("@blob").is_none()); } #[test] fn calendar_event_extracts_columns_and_drops_method() { let c = mem(); let res = MapResolver { to_local: HashMap::from([((ObjectType::Calendar, "CAL".to_owned()), 5)]), to_target: HashMap::from([((ObjectType::Calendar, 5), "TCAL".to_owned())]), }; let ev: CalendarEvent = serde_json::from_value(serde_json::json!({ "id": "EV1", "calendarIds": { "CAL": true }, "isDraft": true, "useDefaultAlerts": false, "method": "request", "utcStart": "2020-01-01T00:00:00Z", "title": "Sprint", "@type": "Event" })) .unwrap(); let mut blobs = FakeBlobs; let local = insert_calendar_event(&c, &ev, &res, &mut blobs).unwrap(); let (cal, dr, ud, data): (String, i64, i64, String) = c .query_row( &format!("{CALENDAR_EVENT_SELECT} AND id = ?1"), params![local], |r| Ok((r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?)), ) .unwrap(); assert_eq!(dr, 1); assert_eq!(ud, 0); let stored: Value = serde_json::from_str(&data).unwrap(); assert!(stored.get("method").is_none()); assert!(stored.get("utcStart").is_none()); assert!(stored.get("calendarIds").is_none()); assert_eq!(stored["title"], Value::from("Sprint")); let mut up = FakeBlobs; let wire = calendar_event_to_wire(&cal, dr != 0, ud != 0, &data, &res, &mut up).unwrap(); assert_eq!(wire["calendarIds"]["TCAL"], Value::Bool(true)); assert_eq!(wire["isDraft"], Value::Bool(true)); assert_eq!(wire["title"], Value::from("Sprint")); } #[test] fn identity_json_columns_roundtrip() { let c = mem(); let id: Identity = serde_json::from_value(serde_json::json!({ "id": "I1", "name": "Alice", "email": "a@x.test", "replyTo": [ { "name": "A", "email": "a@x.test" } ] })) .unwrap(); let local = insert_identity(&c, &id).unwrap(); let wire = c .query_row( &format!("{IDENTITY_SELECT} WHERE id = ?1"), params![local], |row| Ok(row_to_identity(row)), ) .unwrap() .unwrap(); assert_eq!(wire.email, "a@x.test"); assert_eq!(wire.reply_to.unwrap()[0].email, "a@x.test"); assert!(wire.bcc.is_none()); } #[test] fn file_node_enum_and_target_roundtrip() { let c = mem(); let empty = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; let node: FileNode = serde_json::from_value(serde_json::json!({ "id": "F1", "nodeType": "symlink", "target": ["..", "etc", "hosts"], "name": "link", "created": "2022-02-02T02:02:02Z" })) .unwrap(); let local = insert_file_node(&c, &node, None, &empty).unwrap(); let (nt, tgt): (String, Option) = c .query_row( &format!("{FILE_NODE_SELECT} WHERE id = ?1"), params![local], |r| Ok((r.get(2)?, r.get(4)?)), ) .unwrap(); assert_eq!(nt, "symlink"); assert_eq!( serde_json::from_str::>(&tgt.unwrap()).unwrap(), vec!["..", "etc", "hosts"] ); } #[test] fn address_book_and_calendar_row_to_wire_roundtrip() { let c = mem(); let ab: AddressBook = serde_json::from_value(serde_json::json!({ "id": "A1", "name": "Personal", "description": "d", "sortOrder": 2, "isDefault": true, "isSubscribed": false })) .unwrap(); let abl = insert_address_book(&c, &ab).unwrap(); let got = c .query_row( &format!("{ADDRESS_BOOK_SELECT} WHERE id = ?1"), params![abl], |r| Ok(row_to_address_book(r)), ) .unwrap() .unwrap(); assert_eq!(got.name, "Personal"); assert!(got.is_default); assert!(!got.is_subscribed); let cal: Calendar = serde_json::from_value(serde_json::json!({ "id": "C1", "name": "Work", "color": "#ff0000", "timeZone": "Europe/Rome", "isVisible": false })) .unwrap(); let cl = insert_calendar(&c, &cal).unwrap(); let gc = c .query_row( &format!("{CALENDAR_SELECT} WHERE id = ?1"), params![cl], |r| Ok(row_to_calendar(r)), ) .unwrap() .unwrap(); assert_eq!(gc.name, "Work"); assert_eq!(gc.color.as_deref(), Some("#ff0000")); assert_eq!(gc.time_zone.as_deref(), Some("Europe/Rome")); assert!(!gc.is_visible); } #[test] fn participant_identity_and_sieve_row_to_wire_roundtrip() { let c = mem(); let pi: ParticipantIdentity = serde_json::from_value(serde_json::json!({ "id": "P1", "name": "Me", "calendarAddress": "mailto:me@x.test", "isDefault": true })) .unwrap(); let pil = insert_participant_identity(&c, &pi).unwrap(); let gp = c .query_row( &format!("{PARTICIPANT_IDENTITY_SELECT} WHERE id = ?1"), params![pil], |r| Ok(row_to_participant_identity(r)), ) .unwrap() .unwrap(); assert_eq!(gp.calendar_address, "mailto:me@x.test"); assert!(gp.is_default); let ss: SieveScript = serde_json::from_value(serde_json::json!({ "id": "S1", "name": "main", "isActive": true, "blobId": "B1" })) .unwrap(); let blob = crate::db::blobs::intern_blob(&c, b"keep;").unwrap(); let ssl = insert_sieve_script(&c, &ss, blob).unwrap(); let gs = c .query_row( &format!("{SIEVE_SELECT} WHERE id = ?1"), params![ssl], |r| Ok(row_to_sieve_script(r)), ) .unwrap() .unwrap(); assert_eq!(gs.name.as_deref(), Some("main")); assert!(gs.is_active); assert_eq!(gs.blob_local_id, blob); } #[test] fn file_node_row_to_wire_translates_parent() { let c = mem(); let mut res = MapResolver { to_local: HashMap::new(), to_target: HashMap::new(), }; let dir: FileNode = serde_json::from_value(serde_json::json!({ "id": "D1", "nodeType": "directory", "name": "Docs", "created": "2022-02-02T02:02:02Z" })) .unwrap(); let dirl = insert_file_node(&c, &dir, None, &res).unwrap(); res.to_local .insert((ObjectType::FileNode, "D1".to_owned()), dirl); let file: FileNode = serde_json::from_value(serde_json::json!({ "id": "F1", "nodeType": "file", "parentId": "D1", "name": "a.bin", "type": "application/octet-stream", "created": "2022-02-02T02:02:02Z" })) .unwrap(); let blob = crate::db::blobs::intern_blob(&c, b"bytes").unwrap(); let fl = insert_file_node(&c, &file, Some(blob), &res).unwrap(); res.to_target .insert((ObjectType::FileNode, dirl), "TGTD".to_owned()); let got = c .query_row( &format!("{FILE_NODE_SELECT} WHERE id = ?1"), params![fl], |r| Ok(row_to_file_node(r, &res)), ) .unwrap() .unwrap(); assert_eq!(got.wire.name, "a.bin"); assert_eq!(got.wire.parent_id, Some(JmapId("TGTD".to_owned()))); assert_eq!(got.blob_local_id, Some(blob)); } }