Skip to main content

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