Skip to main content

pidge_core/
flags.rs

1//! Cheap facts about a list item that a harness can triage on. No ranking.
2use crate::{FlagStatus, Message};
3
4pub struct UserContext<'a> {
5    pub my_addresses: &'a [String],
6    pub trusted_senders: &'a [String],
7}
8
9#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
10pub struct ItemFlags {
11    pub to_me: bool,
12    pub trusted: bool,
13    pub question: bool,
14    pub attachments: bool,
15    pub flagged: bool,
16    pub unread: bool,
17}
18
19impl ItemFlags {
20    pub fn labels(&self) -> Vec<&'static str> {
21        let mut v = Vec::new();
22        if self.to_me {
23            v.push("to-me");
24        }
25        if self.trusted {
26            v.push("trusted");
27        }
28        if self.question {
29            v.push("question");
30        }
31        if self.attachments {
32            v.push("attachments");
33        }
34        if self.flagged {
35            v.push("flagged");
36        }
37        if self.unread {
38            v.push("unread");
39        }
40        v
41    }
42}
43
44fn eq_ci(a: &str, b: &str) -> bool {
45    a.eq_ignore_ascii_case(b)
46}
47
48pub fn compute_flags(m: &Message, ctx: &UserContext) -> ItemFlags {
49    let to_me =
50        m.to.iter()
51            .any(|r| ctx.my_addresses.iter().any(|mine| eq_ci(&r.address, mine)));
52    let trusted = ctx
53        .trusted_senders
54        .iter()
55        .any(|t| eq_ci(t, &m.from.address));
56    let question = m.preview.contains('?');
57    ItemFlags {
58        to_me,
59        trusted,
60        question,
61        attachments: m.has_attachments,
62        flagged: m.flag_status == FlagStatus::Flagged,
63        unread: !m.is_read,
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::{BodyContentType, FlagStatus, Message, MessageFrom};
71    use chrono::Utc;
72
73    fn msg(from: &str, to: &[&str], cc: &[&str], preview: &str) -> Message {
74        let who = |a: &str| MessageFrom {
75            name: String::new(),
76            address: a.to_string(),
77        };
78        Message {
79            account: "me@example.com".into(),
80            id: "1".into(),
81            conversation_id: String::new(),
82            from: who(from),
83            subject: "s".into(),
84            received_at: Utc::now(),
85            is_read: false,
86            preview: preview.into(),
87            flag_status: FlagStatus::NotFlagged,
88            has_attachments: false,
89            body: String::new(),
90            body_content_type: BodyContentType::Text,
91            to: to.iter().map(|a| who(a)).collect(),
92            cc: cc.iter().map(|a| who(a)).collect(),
93            is_invite: false,
94        }
95    }
96
97    #[test]
98    fn to_me_requires_to_not_cc() {
99        let mine = vec!["me@example.com".to_string()];
100        let ctx = UserContext {
101            my_addresses: &mine,
102            trusted_senders: &[],
103        };
104        assert!(compute_flags(&msg("a@x", &["Me@Example.com"], &[], ""), &ctx).to_me);
105        assert!(!compute_flags(&msg("a@x", &["b@x"], &["me@example.com"], ""), &ctx).to_me);
106    }
107
108    #[test]
109    fn trusted_and_question() {
110        let mine = vec!["me@example.com".to_string()];
111        let trusted = vec!["Anna@Example.com".to_string()];
112        let ctx = UserContext {
113            my_addresses: &mine,
114            trusted_senders: &trusted,
115        };
116        let f = compute_flags(&msg("anna@example.com", &[], &[], "Can you come?"), &ctx);
117        assert!(f.trusted && f.question);
118        assert_eq!(f.labels(), vec!["trusted", "question", "unread"]);
119    }
120}