deltachat/param.rs
1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::PathBuf;
4use std::str;
5
6use anyhow::ensure;
7use anyhow::{Error, Result, bail};
8use num_traits::FromPrimitive;
9use serde::{Deserialize, Serialize};
10
11use crate::blob::BlobObject;
12use crate::context::Context;
13use crate::mimeparser::SystemMessage;
14
15/// Available param keys.
16#[derive(
17 PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive, Serialize, Deserialize,
18)]
19#[repr(u8)]
20pub enum Param {
21 /// For messages
22 File = b'f',
23
24 /// For messages: original filename (as shown in chat)
25 Filename = b'v',
26
27 /// For messages: This name should be shown instead of contact.get_display_name()
28 /// (used if this is a mailinglist
29 /// or explicitly set using set_override_sender_name(), eg. by bots)
30 OverrideSenderDisplayname = b'O',
31
32 /// For Messages
33 Width = b'w',
34
35 /// For Messages
36 Height = b'h',
37
38 /// For Messages
39 Duration = b'd',
40
41 /// For Messages
42 MimeType = b'm',
43
44 /// For Messages: HTML to be written to the database and to be send.
45 /// `SendHtml` param is not used for received messages.
46 /// Use `MsgId::get_html()` to get HTML of received messages.
47 SendHtml = b'T',
48
49 /// For Messages: message is encrypted, outgoing: guarantee E2EE or the message is not send
50 GuaranteeE2ee = b'c',
51
52 /// For Messages: quoted message is encrypted.
53 ///
54 /// If this message is sent unencrypted, quote text should be replaced.
55 ProtectQuote = b'0',
56
57 /// For Messages: decrypted with validation errors or without mutual set, if neither
58 /// 'c' nor 'e' are preset, the messages is only transport encrypted.
59 ///
60 /// Deprecated on 2024-12-25.
61 ErroneousE2ee = b'e',
62
63 /// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
64 ForcePlaintext = b'u',
65
66 /// For Messages: do not include Autocrypt header.
67 SkipAutocrypt = b'o',
68
69 /// For Messages
70 WantsMdn = b'r',
71
72 /// For Messages: the message is a reaction.
73 Reaction = b'x',
74
75 /// For Chats: the timestamp of the last reaction.
76 LastReactionTimestamp = b'y',
77
78 /// For Chats: Message ID of the last reaction.
79 LastReactionMsgId = b'Y',
80
81 /// For Chats: Contact ID of the last reaction.
82 LastReactionContactId = b'1',
83
84 /// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
85 Bot = b'b',
86
87 /// For Messages: unset or 0=not forwarded,
88 /// 1=forwarded from unknown msg_id, >9 forwarded from msg_id
89 Forwarded = b'a',
90
91 /// For Messages: quoted text.
92 Quote = b'q',
93
94 /// For Messages: the 1st part of summary text (i.e. before the dash if any).
95 Summary1 = b'4',
96
97 /// For Messages
98 Cmd = b'S',
99
100 /// For Messages
101 ///
102 /// For "MemberAddedToGroup" and "MemberRemovedFromGroup",
103 /// this is the email address added to / removed from the group.
104 ///
105 /// For securejoin messages other than `vg-member-added`, this is the step,
106 /// which is put into the `Secure-Join` header.
107 Arg = b'E',
108
109 /// For Messages
110 ///
111 /// For `BobHandshakeMsg::Request`, this is the `Secure-Join-Invitenumber` header.
112 ///
113 /// For `BobHandshakeMsg::RequestWithAuth`, this is the `Secure-Join-Auth` header.
114 ///
115 /// For [`SystemMessage::MultiDeviceSync`], this contains the ids that are synced.
116 ///
117 /// For [`SystemMessage::MemberAddedToGroup`],
118 /// this is '1' if it was added because of a securejoin-handshake, and '0' otherwise.
119 ///
120 /// For call messages, this is the accept timestamp.
121 Arg2 = b'F',
122
123 /// For Messages
124 ///
125 /// For `BobHandshakeMsg::RequestWithAuth`,
126 /// this contains the `Secure-Join-Fingerprint` header.
127 ///
128 /// For [`SystemMessage::MemberAddedToGroup`] that add to a broadcast channel,
129 /// this contains the broadcast channel's shared secret.
130 Arg3 = b'G',
131
132 /// For Messages
133 ///
134 /// Deprecated `Secure-Join-Group` header for `BobHandshakeMsg::RequestWithAuth` messages.
135 ///
136 /// For "MemberAddedToGroup" and "MemberRemovedFromGroup",
137 /// this is the fingerprint added to / removed from the group.
138 ///
139 /// For messages resent when adding a new member to a broadcast channel,
140 /// this is the fingerprint of the added member;
141 /// the message must only be sent to this one member then.
142 ///
143 /// For call messages, this is the end timsetamp.
144 Arg4 = b'H',
145
146 /// For Messages
147 AttachChatAvatarAndDescription = b'A',
148
149 /// For Messages
150 WebrtcRoom = b'V',
151
152 /// For Messages
153 WebrtcAccepted = b'7',
154
155 /// For Messages
156 WebrtcHasVideoInitially = b'z',
157
158 /// For Messages: space-separated list of messaged IDs of forwarded copies.
159 ///
160 /// This is used when a [crate::message::Message] is in the
161 /// [crate::message::MessageState::OutPending] state but is already forwarded.
162 /// In this case the forwarded messages are written to the
163 /// database and their message IDs are added to this parameter of
164 /// the original message, which is also saved in the database.
165 /// When the original message is then finally sent this parameter
166 /// is used to also send all the forwarded messages.
167 PrepForwards = b'P',
168
169 /// For Messages
170 SetLatitude = b'l',
171
172 /// For Messages
173 SetLongitude = b'n',
174
175 /// For Groups
176 ///
177 /// An unpromoted group has not had any messages sent to it and thus only exists on the
178 /// creator's device. Any changes made to an unpromoted group do not need to send
179 /// system messages to the group members to update them of the changes. Once a message
180 /// has been sent to a group it is promoted and group changes require sending system
181 /// messages to all members.
182 Unpromoted = b'U',
183
184 /// For Groups and Contacts
185 ProfileImage = b'i',
186
187 /// For Chats
188 /// Signals whether the chat is the `saved messages` chat
189 Selftalk = b'K',
190
191 /// For Chats: On sending a new message we set the subject to `Re: <last subject>`.
192 /// Usually we just use the subject of the parent message, but if the parent message
193 /// is deleted, we use the LastSubject of the chat.
194 LastSubject = b't',
195
196 /// For Chats
197 Devicetalk = b'D',
198
199 /// For Chats: If this is a mailing list chat, contains the List-Post address.
200 /// None if there simply is no `List-Post` header in the mailing list.
201 /// Some("") if the mailing list is using multiple different List-Post headers.
202 ///
203 /// The List-Post address is the email address where the user can write to in order to
204 /// post something to the mailing list.
205 ListPost = b'p',
206
207 /// For Contacts: If this is the List-Post address of a mailing list, contains
208 /// the List-Id of the mailing list (which is also used as the group id of the chat).
209 ListId = b's',
210
211 /// For Contacts: timestamp of status (aka signature or footer) update.
212 StatusTimestamp = b'j',
213
214 /// For Contacts and Chats: timestamp of avatar update.
215 AvatarTimestamp = b'J',
216
217 /// For Chats: timestamp of status/signature/footer update.
218 EphemeralSettingsTimestamp = b'B',
219
220 /// For Chats: timestamp of subject update.
221 SubjectTimestamp = b'C',
222
223 /// For Chats: timestamp of group name update.
224 GroupNameTimestamp = b'g',
225
226 /// For Chats: timestamp of chat description update.
227 GroupDescriptionTimestamp = b'6',
228
229 /// For Chats: timestamp of member list update.
230 MemberListTimestamp = b'k',
231
232 /// For Webxdc Message Instances: Current document name
233 WebxdcDocument = b'R',
234
235 /// For Webxdc Message Instances: timestamp of document name update.
236 WebxdcDocumentTimestamp = b'W',
237
238 /// For Webxdc Message Instances: Current summary
239 WebxdcSummary = b'N',
240
241 /// For Webxdc Message Instances: timestamp of summary update.
242 WebxdcSummaryTimestamp = b'Q',
243
244 /// For Webxdc Message Instances: Webxdc is an integration, see init_webxdc_integration()
245 WebxdcIntegration = b'3',
246
247 /// For Webxdc Message Instances: Chat to integrate the Webxdc for.
248 WebxdcIntegrateFor = b'2',
249
250 /// For messages: Message is a deletion request. The value is a list of rfc724_mid of the messages to delete.
251 DeleteRequestFor = b'M',
252
253 /// For messages: Message is a text edit message. the value of this parameter is the rfc724_mid of the original message.
254 TextEditFor = b'I',
255
256 /// For messages: Message text was edited.
257 IsEdited = b'L',
258
259 /// For info messages: Contact ID in added or removed to a group.
260 ContactAddedRemoved = b'5',
261
262 /// For (pre-)Message: ViewType of the Post-Message,
263 /// because pre message is always `Viewtype::Text`.
264 PostMessageViewtype = b'8',
265
266 /// For (pre-)Message: File byte size of Post-Message attachment
267 PostMessageFileBytes = b'9',
268}
269
270/// An object for handling key=value parameter lists.
271///
272/// The structure is serialized by calling `to_string()` on it.
273///
274/// Only for library-internal use.
275#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
276pub struct Params {
277 inner: BTreeMap<Param, String>,
278}
279
280impl fmt::Display for Params {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 for (i, (key, value)) in self.inner.iter().enumerate() {
283 if i > 0 {
284 writeln!(f)?;
285 }
286 write!(
287 f,
288 "{}={}",
289 *key as u8 as char,
290 value.split('\n').collect::<Vec<&str>>().join("\n\n")
291 )?;
292 }
293 Ok(())
294 }
295}
296
297impl str::FromStr for Params {
298 type Err = Error;
299
300 /// Parse a raw string to Param.
301 ///
302 /// Silently ignore unknown keys:
303 /// they may come from a downgrade (when a shortly new version adds a key)
304 /// or from an upgrade (when a key is dropped but was used in the past)
305 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
306 let mut inner = BTreeMap::new();
307 let mut lines = s.split('\n').peekable();
308
309 while let Some(line) = lines.next() {
310 if let [key, value] = line.splitn(2, '=').collect::<Vec<_>>()[..] {
311 let key = key.to_string();
312 let mut value = value.to_string();
313 while let Some(s) = lines.peek() {
314 if !s.is_empty() {
315 break;
316 }
317 lines.next();
318 value.push('\n');
319 value += lines.next().unwrap_or_default();
320 }
321
322 if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
323 inner.insert(key, value);
324 }
325 } else {
326 bail!("Not a key-value pair: {line:?}");
327 }
328 }
329
330 Ok(Params { inner })
331 }
332}
333
334impl Params {
335 /// Create new empty params.
336 pub fn new() -> Self {
337 Default::default()
338 }
339
340 /// Get the value of the given key, return `None` if no value is set.
341 pub fn get(&self, key: Param) -> Option<&str> {
342 self.inner.get(&key).map(|s| s.as_str())
343 }
344
345 /// Check if the given key is set.
346 pub fn exists(&self, key: Param) -> bool {
347 self.inner.contains_key(&key)
348 }
349
350 /// Set the given key to the passed in value.
351 pub fn set(&mut self, key: Param, value: impl ToString) -> &mut Self {
352 if key == Param::File {
353 debug_assert!(value.to_string().starts_with("$BLOBDIR/"));
354 }
355 self.inner.insert(key, value.to_string());
356 self
357 }
358
359 /// Removes the given key, if it exists.
360 pub fn remove(&mut self, key: Param) -> &mut Self {
361 self.inner.remove(&key);
362 self
363 }
364
365 /// Sets the given key from an optional value.
366 /// Removes the key if the value is `None`.
367 pub fn set_optional(&mut self, key: Param, value: Option<impl ToString>) -> &mut Self {
368 if let Some(value) = value {
369 self.set(key, value)
370 } else {
371 self.remove(key)
372 }
373 }
374
375 /// Check if there are any values in this.
376 pub fn is_empty(&self) -> bool {
377 self.inner.is_empty()
378 }
379
380 /// Returns how many key-value pairs are set.
381 pub fn len(&self) -> usize {
382 self.inner.len()
383 }
384
385 /// Get the given parameter and parse as `i32`.
386 pub fn get_int(&self, key: Param) -> Option<i32> {
387 self.get(key).and_then(|s| s.parse().ok())
388 }
389
390 /// Get the given parameter and parse as `i64`.
391 pub fn get_i64(&self, key: Param) -> Option<i64> {
392 self.get(key).and_then(|s| s.parse().ok())
393 }
394
395 /// Get the given parameter and parse as `bool`.
396 pub fn get_bool(&self, key: Param) -> Option<bool> {
397 self.get_int(key).map(|v| v != 0)
398 }
399
400 /// Get the parameter behind `Param::Cmd` interpreted as `SystemMessage`.
401 pub fn get_cmd(&self) -> SystemMessage {
402 self.get_int(Param::Cmd)
403 .and_then(SystemMessage::from_i32)
404 .unwrap_or_default()
405 }
406
407 /// Set the parameter behind `Param::Cmd`.
408 pub fn set_cmd(&mut self, value: SystemMessage) {
409 self.set_int(Param::Cmd, value as i32);
410 }
411
412 /// Get the given parameter and parse as `f64`.
413 pub fn get_float(&self, key: Param) -> Option<f64> {
414 self.get(key).and_then(|s| s.parse().ok())
415 }
416
417 /// Returns a [BlobObject] for the [Param::File] parameter.
418 pub fn get_file_blob<'a>(&self, context: &'a Context) -> Result<Option<BlobObject<'a>>> {
419 let Some(val) = self.get(Param::File) else {
420 return Ok(None);
421 };
422 ensure!(val.starts_with("$BLOBDIR/"));
423 let blob = BlobObject::from_name(context, val)?;
424 Ok(Some(blob))
425 }
426
427 /// Returns a [PathBuf] for the [Param::File] parameter.
428 pub fn get_file_path(&self, context: &Context) -> Result<Option<PathBuf>> {
429 let blob = self.get_file_blob(context)?;
430 Ok(blob.map(|p| p.to_abs_path()))
431 }
432
433 /// Set the given parameter to the passed in `i32`.
434 pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
435 self.set(key, format!("{value}"));
436 self
437 }
438
439 /// Set the given parameter to the passed in `i64`.
440 pub fn set_i64(&mut self, key: Param, value: i64) -> &mut Self {
441 self.set(key, value.to_string());
442 self
443 }
444
445 /// Set the given parameter to the passed in `f64` .
446 pub fn set_float(&mut self, key: Param, value: f64) -> &mut Self {
447 self.set(key, format!("{value}"));
448 self
449 }
450
451 pub fn steal(&mut self, src: &mut Self, key: Param) -> &mut Self {
452 let val = src.inner.remove(&key);
453 if let Some(val) = val {
454 self.inner.insert(key, val);
455 }
456 self
457 }
458
459 /// Merge in parameters from other Params struct,
460 /// overwriting the keys that are in both
461 /// with the values from the new Params struct.
462 pub fn merge_in_params(&mut self, new_params: Self) -> &mut Self {
463 let mut new_params = new_params;
464 self.inner.append(&mut new_params.inner);
465 self
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use std::str::FromStr;
472
473 use super::*;
474
475 #[test]
476 fn test_dc_param() {
477 let mut p1: Params = "a=1\nw=2\nc=3".parse().unwrap();
478
479 assert_eq!(p1.get_int(Param::Forwarded), Some(1));
480 assert_eq!(p1.get_int(Param::Width), Some(2));
481 assert_eq!(p1.get_int(Param::Height), None);
482 assert!(!p1.exists(Param::Height));
483
484 p1.set_int(Param::Duration, 4);
485
486 assert_eq!(p1.get_int(Param::Duration), Some(4));
487
488 let mut p1 = Params::new();
489
490 p1.set(Param::Forwarded, "foo")
491 .set_int(Param::Width, 2)
492 .remove(Param::GuaranteeE2ee)
493 .set_int(Param::Duration, 4);
494
495 assert_eq!(p1.to_string(), "a=foo\nd=4\nw=2");
496
497 p1.remove(Param::Width);
498
499 assert_eq!(p1.to_string(), "a=foo\nd=4",);
500 assert_eq!(p1.len(), 2);
501
502 p1.remove(Param::Forwarded);
503 p1.remove(Param::Duration);
504
505 assert_eq!(p1.to_string(), "",);
506
507 assert!(p1.is_empty());
508 assert_eq!(p1.len(), 0)
509 }
510
511 #[test]
512 fn test_roundtrip() {
513 let mut params = Params::new();
514 params.set(Param::Height, "foo\nbar=baz\nquux");
515 params.set(Param::Width, "\n\n\na=\n=");
516 params.set(Param::WebrtcRoom, "foo\r\nbar\r\n\r\nbaz\r\n");
517 assert_eq!(params.to_string().parse::<Params>().unwrap(), params);
518 }
519
520 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
521 async fn test_params_unknown_key() -> Result<()> {
522 // 'Z' is used as a key that is known to be unused; these keys should be ignored silently by definition.
523 let p = Params::from_str("w=12\nZ=13\nh=14")?;
524 assert_eq!(p.len(), 2);
525 assert_eq!(p.get(Param::Width), Some("12"));
526 assert_eq!(p.get(Param::Height), Some("14"));
527 Ok(())
528 }
529
530 #[test]
531 fn test_merge() -> Result<()> {
532 let mut p = Params::from_str("w=12\na=5\nh=14")?;
533 let p2 = Params::from_str("L=1\nh=17")?;
534 assert_eq!(p.len(), 3);
535 p.merge_in_params(p2);
536 assert_eq!(p.len(), 4);
537 assert_eq!(p.get(Param::Width), Some("12"));
538 assert_eq!(p.get(Param::Height), Some("17"));
539 assert_eq!(p.get(Param::Forwarded), Some("5"));
540 assert_eq!(p.get(Param::IsEdited), Some("1"));
541 Ok(())
542 }
543}