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