deltachat/
ephemeral.rs

1//! # Ephemeral messages.
2//!
3//! Ephemeral messages are messages that have an Ephemeral-Timer
4//! header attached to them, which specifies time in seconds after
5//! which the message should be deleted both from the device and from
6//! the server. The timer is started when the message is marked as
7//! seen, which usually happens when its contents is displayed on
8//! device screen.
9//!
10//! Each chat, including 1:1, group chats and "saved messages" chat,
11//! has its own ephemeral timer setting, which is applied to all
12//! messages sent to the chat. The setting is synchronized to all the
13//! devices participating in the chat by applying the timer value from
14//! all received messages, including BCC-self ones, to the chat. This
15//! way the setting is eventually synchronized among all participants.
16//!
17//! When user changes ephemeral timer setting for the chat, a system
18//! message is automatically sent to update the setting for all
19//! participants. This allows changing the setting for a chat like any
20//! group chat setting, e.g. name and avatar, without the need to
21//! write an actual message.
22//!
23//! ## Device settings
24//!
25//! In addition to per-chat ephemeral message setting, each device has
26//! two global user-configured settings that complement per-chat
27//! settings: `delete_device_after` and `delete_server_after`. These
28//! settings are not synchronized among devices and apply to all
29//! messages known to the device, including messages sent or received
30//! before configuring the setting.
31//!
32//! `delete_device_after` configures the maximum time device is
33//! storing the messages locally. `delete_server_after` configures the
34//! time after which device will delete the messages it knows about
35//! from the server.
36//!
37//! ## How messages are deleted
38//!
39//! When Delta Chat deletes the message locally, it moves the message
40//! to the trash chat and removes actual message contents. Messages in
41//! the trash chat are called "tombstones" and track the Message-ID to
42//! prevent accidental redownloading of the message from the server,
43//! e.g. in case of UID validity change.
44//!
45//! Vice versa, when Delta Chat deletes the message from the server,
46//! it removes IMAP folder and UID row from the `imap` table, but
47//! keeps the message in the `msgs` table.
48//!
49//! Delta Chat eventually removes tombstones from the `msgs` table,
50//! leaving no trace of the message, when it thinks there are no more
51//! copies of the message stored on the server, i.e. when there is no
52//! corresponding `imap` table entry. This is done in the
53//! `prune_tombstones()` procedure during housekeeping.
54//!
55//! ## When messages are deleted
56//!
57//! The `ephemeral_loop` task schedules the next due running of
58//! `delete_expired_messages` which in turn emits `MsgsChanged` events
59//! when deleting local messages to make UIs reload displayed messages.
60//!
61//! Server deletion happens by updating the `imap` table based on
62//! the database entries which are expired either according to their
63//! ephemeral message timers or global `delete_server_after` setting.
64
65use std::cmp::max;
66use std::collections::BTreeSet;
67use std::fmt;
68use std::num::ParseIntError;
69use std::str::FromStr;
70use std::time::{Duration, UNIX_EPOCH};
71
72use anyhow::{Context as _, Result, ensure};
73use async_channel::Receiver;
74use serde::{Deserialize, Serialize};
75use tokio::time::timeout;
76
77use crate::chat::{ChatId, ChatIdBlocked, send_msg};
78use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
79use crate::contact::ContactId;
80use crate::context::Context;
81use crate::download::MIN_DELETE_SERVER_AFTER;
82use crate::events::EventType;
83use crate::log::{LogExt, warn};
84use crate::message::{Message, MessageState, MsgId, Viewtype};
85use crate::mimeparser::SystemMessage;
86use crate::stock_str;
87use crate::tools::{SystemTime, duration_to_str, time};
88use crate::{location, stats};
89
90/// Ephemeral timer value.
91#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Default)]
92pub enum Timer {
93    /// Timer is disabled.
94    #[default]
95    Disabled,
96
97    /// Timer is enabled.
98    Enabled {
99        /// Timer duration in seconds.
100        ///
101        /// The value cannot be 0.
102        duration: u32,
103    },
104}
105
106impl Timer {
107    /// Converts epehmeral timer value to integer.
108    ///
109    /// If the timer is disabled, return 0.
110    pub fn to_u32(self) -> u32 {
111        match self {
112            Self::Disabled => 0,
113            Self::Enabled { duration } => duration,
114        }
115    }
116
117    /// Converts integer to ephemeral timer value.
118    ///
119    /// 0 value is treated as disabled timer.
120    pub fn from_u32(duration: u32) -> Self {
121        if duration == 0 {
122            Self::Disabled
123        } else {
124            Self::Enabled { duration }
125        }
126    }
127}
128
129impl fmt::Display for Timer {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "{}", self.to_u32())
132    }
133}
134
135impl FromStr for Timer {
136    type Err = ParseIntError;
137
138    fn from_str(input: &str) -> Result<Timer, ParseIntError> {
139        input.parse::<u32>().map(Self::from_u32)
140    }
141}
142
143impl rusqlite::types::ToSql for Timer {
144    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
145        let val = rusqlite::types::Value::Integer(match self {
146            Self::Disabled => 0,
147            Self::Enabled { duration } => i64::from(*duration),
148        });
149        let out = rusqlite::types::ToSqlOutput::Owned(val);
150        Ok(out)
151    }
152}
153
154impl rusqlite::types::FromSql for Timer {
155    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
156        i64::column_result(value).and_then(|value| {
157            if value == 0 {
158                Ok(Self::Disabled)
159            } else if let Ok(duration) = u32::try_from(value) {
160                Ok(Self::Enabled { duration })
161            } else {
162                Err(rusqlite::types::FromSqlError::OutOfRange(value))
163            }
164        })
165    }
166}
167
168impl ChatId {
169    /// Get ephemeral message timer value in seconds.
170    pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
171        let timer = context
172            .sql
173            .query_get_value(
174                "SELECT IFNULL(ephemeral_timer, 0) FROM chats WHERE id=?",
175                (self,),
176            )
177            .await?
178            .with_context(|| format!("Chat {self} not found"))?;
179        Ok(timer)
180    }
181
182    /// Set ephemeral timer value without sending a message.
183    ///
184    /// Used when a message arrives indicating that someone else has
185    /// changed the timer value for a chat.
186    pub(crate) async fn inner_set_ephemeral_timer(
187        self,
188        context: &Context,
189        timer: Timer,
190    ) -> Result<()> {
191        ensure!(!self.is_special(), "Invalid chat ID");
192
193        context
194            .sql
195            .execute(
196                "UPDATE chats
197             SET ephemeral_timer=?
198             WHERE id=?;",
199                (timer, self),
200            )
201            .await?;
202
203        context.emit_event(EventType::ChatEphemeralTimerModified {
204            chat_id: self,
205            timer,
206        });
207        Ok(())
208    }
209
210    /// Set ephemeral message timer value in seconds.
211    ///
212    /// If timer value is 0, disable ephemeral message timer.
213    pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<()> {
214        if timer == self.get_ephemeral_timer(context).await? {
215            return Ok(());
216        }
217        self.inner_set_ephemeral_timer(context, timer).await?;
218
219        if self.is_promoted(context).await? {
220            let mut msg = Message::new_text(
221                stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await,
222            );
223            msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
224            if let Err(err) = send_msg(context, self, &mut msg).await {
225                error!(
226                    context,
227                    "Failed to send a message about ephemeral message timer change: {:?}", err
228                );
229            }
230        }
231        Ok(())
232    }
233}
234
235/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
236pub(crate) async fn stock_ephemeral_timer_changed(
237    context: &Context,
238    timer: Timer,
239    from_id: ContactId,
240) -> String {
241    match timer {
242        Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
243        Timer::Enabled { duration } => match duration {
244            0..=60 => {
245                stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
246            }
247            61..=3599 => {
248                stock_str::msg_ephemeral_timer_minutes(
249                    context,
250                    &format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
251                    from_id,
252                )
253                .await
254            }
255            3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await,
256            3601..=86399 => {
257                stock_str::msg_ephemeral_timer_hours(
258                    context,
259                    &format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
260                    from_id,
261                )
262                .await
263            }
264            86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await,
265            86401..=604_799 => {
266                stock_str::msg_ephemeral_timer_days(
267                    context,
268                    &format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
269                    from_id,
270                )
271                .await
272            }
273            604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
274            31_536_000..=31_708_800 => stock_str::msg_ephemeral_timer_year(context, from_id).await,
275            _ => {
276                stock_str::msg_ephemeral_timer_weeks(
277                    context,
278                    &format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
279                    from_id,
280                )
281                .await
282            }
283        },
284    }
285}
286
287impl MsgId {
288    /// Returns ephemeral message timer value for the message.
289    pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
290        let res = match context
291            .sql
292            .query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,))
293            .await?
294        {
295            None | Some(0) => Timer::Disabled,
296            Some(duration) => Timer::Enabled { duration },
297        };
298        Ok(res)
299    }
300
301    /// Starts ephemeral message timer for the message if it is not started yet.
302    pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> Result<()> {
303        if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
304            let ephemeral_timestamp = time().saturating_add(duration.into());
305
306            context
307                .sql
308                .execute(
309                    "UPDATE msgs SET ephemeral_timestamp = ? \
310                WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
311                AND id = ?",
312                    (ephemeral_timestamp, ephemeral_timestamp, self),
313                )
314                .await?;
315            context.scheduler.interrupt_ephemeral_task().await;
316        }
317        Ok(())
318    }
319}
320
321pub(crate) async fn start_ephemeral_timers_msgids(
322    context: &Context,
323    msg_ids: &[MsgId],
324) -> Result<()> {
325    let now = time();
326    let should_interrupt =
327    context
328        .sql
329        .transaction(move |transaction| {
330            let mut should_interrupt = false;
331            let mut stmt =
332                transaction.prepare(
333                    "UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
334                     WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer) AND ephemeral_timer > 0
335                     AND id=?2")?;
336            for msg_id in msg_ids {
337                should_interrupt |= stmt.execute((now, msg_id))? > 0;
338            }
339            Ok(should_interrupt)
340        }).await?;
341    if should_interrupt {
342        context.scheduler.interrupt_ephemeral_task().await;
343    }
344    Ok(())
345}
346
347/// Starts ephemeral timer for all messages in the chat.
348///
349/// This should be called when chat is marked as noticed.
350pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: ChatId) -> Result<()> {
351    let now = time();
352    let should_interrupt = context
353        .sql
354        .execute(
355            "UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
356             WHERE chat_id = ?2
357             AND ephemeral_timer > 0
358             AND (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer)",
359            (now, chat_id),
360        )
361        .await?
362        > 0;
363    if should_interrupt {
364        context.scheduler.interrupt_ephemeral_task().await;
365    }
366    Ok(())
367}
368
369/// Selects messages which are expired according to
370/// `delete_device_after` setting or `ephemeral_timestamp` column.
371///
372/// For each message a row ID, chat id, viewtype and location ID is returned.
373///
374/// Unknown viewtypes are returned as `Viewtype::Unknown`
375/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
376/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source)
377async fn select_expired_messages(
378    context: &Context,
379    now: i64,
380) -> Result<Vec<(MsgId, ChatId, Viewtype, u32)>> {
381    let mut rows = context
382        .sql
383        .query_map_vec(
384            r#"
385SELECT id, chat_id, type, location_id
386FROM msgs
387WHERE
388  ephemeral_timestamp != 0
389  AND ephemeral_timestamp <= ?
390  AND chat_id != ?
391"#,
392            (now, DC_CHAT_ID_TRASH),
393            |row| {
394                let id: MsgId = row.get("id")?;
395                let chat_id: ChatId = row.get("chat_id")?;
396                let viewtype: Viewtype = row
397                    .get("type")
398                    .context("Using default viewtype for ephemeral handling.")
399                    .log_err(context)
400                    .unwrap_or_default();
401                let location_id: u32 = row.get("location_id")?;
402                Ok((id, chat_id, viewtype, location_id))
403            },
404        )
405        .await?;
406
407    if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
408        let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
409            .await?
410            .map(|c| c.id)
411            .unwrap_or_default();
412        let device_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE)
413            .await?
414            .map(|c| c.id)
415            .unwrap_or_default();
416
417        let threshold_timestamp = now.saturating_sub(delete_device_after);
418
419        let rows_expired = context
420            .sql
421            .query_map_vec(
422                r#"
423SELECT id, chat_id, type, location_id
424FROM msgs
425WHERE
426  timestamp < ?1
427  AND timestamp_rcvd < ?1
428  AND chat_id > ?
429  AND chat_id != ?
430  AND chat_id != ?
431"#,
432                (
433                    threshold_timestamp,
434                    DC_CHAT_ID_LAST_SPECIAL,
435                    self_chat_id,
436                    device_chat_id,
437                ),
438                |row| {
439                    let id: MsgId = row.get("id")?;
440                    let chat_id: ChatId = row.get("chat_id")?;
441                    let viewtype: Viewtype = row
442                        .get("type")
443                        .context("Using default viewtype for delete-old handling.")
444                        .log_err(context)
445                        .unwrap_or_default();
446                    let location_id: u32 = row.get("location_id")?;
447                    Ok((id, chat_id, viewtype, location_id))
448                },
449            )
450            .await?;
451
452        rows.extend(rows_expired);
453    }
454
455    Ok(rows)
456}
457
458/// Deletes messages which are expired according to
459/// `delete_device_after` setting or `ephemeral_timestamp` column.
460///
461/// Emits relevant `MsgsChanged` and `WebxdcInstanceDeleted` events
462/// if messages are deleted.
463pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
464    let rows = select_expired_messages(context, now).await?;
465
466    if !rows.is_empty() {
467        info!(context, "Attempting to delete {} messages.", rows.len());
468
469        let (msgs_changed, webxdc_deleted) = context
470            .sql
471            .transaction(|transaction| {
472                let mut msgs_changed = Vec::with_capacity(rows.len());
473                let mut webxdc_deleted = Vec::new();
474                // If you change which information is preserved here, also change `MsgId::trash()`
475                // and other places it references.
476                let mut del_msg_stmt = transaction.prepare(
477                    "
478INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id)
479SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
480                    ",
481                )?;
482                let mut del_location_stmt =
483                    transaction.prepare("DELETE FROM locations WHERE independent=1 AND id=?")?;
484                for (msg_id, chat_id, viewtype, location_id) in rows {
485                    del_msg_stmt.execute((msg_id, DC_CHAT_ID_TRASH))?;
486                    if location_id > 0 {
487                        del_location_stmt.execute((location_id,))?;
488                    }
489
490                    msgs_changed.push((chat_id, msg_id));
491                    if viewtype == Viewtype::Webxdc {
492                        webxdc_deleted.push(msg_id)
493                    }
494                }
495                Ok((msgs_changed, webxdc_deleted))
496            })
497            .await?;
498
499        let mut modified_chat_ids = BTreeSet::new();
500
501        for (chat_id, msg_id) in msgs_changed {
502            context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
503            modified_chat_ids.insert(chat_id);
504        }
505
506        for modified_chat_id in modified_chat_ids {
507            context.emit_msgs_changed_without_msg_id(modified_chat_id);
508        }
509
510        for msg_id in webxdc_deleted {
511            context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
512        }
513    }
514
515    Ok(())
516}
517
518/// Calculates the next timestamp when a message will be deleted due to
519/// `delete_device_after` setting being set.
520async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<i64>> {
521    if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
522        let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
523            .await?
524            .map(|c| c.id)
525            .unwrap_or_default();
526        let device_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE)
527            .await?
528            .map(|c| c.id)
529            .unwrap_or_default();
530
531        let oldest_message_timestamp: Option<i64> = context
532            .sql
533            .query_get_value(
534                r#"
535                SELECT min(max(timestamp, timestamp_rcvd))
536                FROM msgs
537                WHERE chat_id > ?
538                  AND chat_id != ?
539                  AND chat_id != ?
540                HAVING count(*) > 0
541                "#,
542                (DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
543            )
544            .await?;
545
546        Ok(oldest_message_timestamp.map(|x| x.saturating_add(delete_device_after)))
547    } else {
548        Ok(None)
549    }
550}
551
552/// Calculates next timestamp when expiration of some message will happen.
553///
554/// Expiration can happen either because user has set `delete_device_after` setting or because the
555/// message itself has an ephemeral timer.
556async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
557    let ephemeral_timestamp: Option<i64> = match context
558        .sql
559        .query_get_value(
560            r#"
561            SELECT min(ephemeral_timestamp)
562            FROM msgs
563            WHERE ephemeral_timestamp != 0
564              AND chat_id != ?
565            HAVING count(*) > 0
566            "#,
567            (DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
568        )
569        .await
570    {
571        Err(err) => {
572            warn!(context, "Can't calculate next ephemeral timeout: {}", err);
573            None
574        }
575        Ok(ephemeral_timestamp) => ephemeral_timestamp,
576    };
577
578    let delete_device_after_timestamp: Option<i64> =
579        match next_delete_device_after_timestamp(context).await {
580            Err(err) => {
581                warn!(
582                    context,
583                    "Can't calculate timestamp of the next message expiration: {}", err
584                );
585                None
586            }
587            Ok(timestamp) => timestamp,
588        };
589
590    ephemeral_timestamp
591        .into_iter()
592        .chain(delete_device_after_timestamp)
593        .min()
594}
595
596pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiver<()>) {
597    loop {
598        let ephemeral_timestamp = next_expiration_timestamp(context).await;
599
600        let now = SystemTime::now();
601        let until = if let Some(ephemeral_timestamp) = ephemeral_timestamp {
602            UNIX_EPOCH
603                + Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
604                + Duration::from_secs(1)
605        } else {
606            // no messages to be deleted for now, wait long for one to occur
607            now + Duration::from_secs(86400) // 1 day
608        };
609
610        if let Ok(duration) = until.duration_since(now) {
611            info!(
612                context,
613                "Ephemeral loop waiting for deletion in {} or interrupt",
614                duration_to_str(duration)
615            );
616            match timeout(duration, interrupt_receiver.recv()).await {
617                Ok(Ok(())) => {
618                    // received an interruption signal, recompute waiting time (if any)
619                    continue;
620                }
621                Ok(Err(err)) => {
622                    warn!(
623                        context,
624                        "Interrupt channel closed, ephemeral loop exits now: {err:#}."
625                    );
626                    return;
627                }
628                Err(_err) => {
629                    // Timeout.
630                }
631            }
632        }
633
634        // Make sure that the statistics stay correct by updating them _before_ deleting messages:
635        stats::maybe_update_message_stats(context)
636            .await
637            .log_err(context)
638            .ok();
639
640        delete_expired_messages(context, time())
641            .await
642            .log_err(context)
643            .ok();
644
645        location::delete_expired(context, time())
646            .await
647            .log_err(context)
648            .ok();
649    }
650}
651
652/// Schedules expired IMAP messages for deletion.
653pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
654    let now = time();
655
656    let (threshold_timestamp, threshold_timestamp_extended) =
657        match context.get_config_delete_server_after().await? {
658            None => (0, 0),
659            Some(delete_server_after) => (
660                match delete_server_after {
661                    // Guarantee immediate deletion.
662                    0 => i64::MAX,
663                    _ => now - delete_server_after,
664                },
665                now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
666            ),
667        };
668    let target = context.get_delete_msgs_target().await?;
669
670    context
671        .sql
672        .execute(
673            "UPDATE imap
674             SET target=?
675             WHERE rfc724_mid IN (
676               SELECT rfc724_mid FROM msgs
677               WHERE ((download_state = 0 AND timestamp < ?) OR
678                      (download_state != 0 AND timestamp < ?) OR
679                      (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
680             )",
681            (
682                &target,
683                threshold_timestamp,
684                threshold_timestamp_extended,
685                now,
686            ),
687        )
688        .await?;
689
690    Ok(())
691}
692
693/// Start ephemeral timers for seen messages if they are not started
694/// yet.
695///
696/// It is possible that timers are not started due to a missing or
697/// failed `MsgId.start_ephemeral_timer()` call, either in the current
698/// or previous version of Delta Chat.
699///
700/// This function is supposed to be called in the background,
701/// e.g. from housekeeping task.
702pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
703    context
704        .sql
705        .execute(
706            "UPDATE msgs \
707    SET ephemeral_timestamp = ? + ephemeral_timer \
708    WHERE ephemeral_timer > 0 \
709    AND ephemeral_timestamp = 0 \
710    AND state NOT IN (?, ?, ?)",
711            (
712                time(),
713                MessageState::InFresh,
714                MessageState::InNoticed,
715                MessageState::OutDraft,
716            ),
717        )
718        .await?;
719
720    Ok(())
721}
722
723#[cfg(test)]
724mod ephemeral_tests;