Skip to main content

roowho2_lib/server/fingerd/
local_email.rs

1use chrono::{TimeZone, Utc};
2use std::path::{Path, PathBuf};
3
4use crate::proto::finger_protocol::MailStatus;
5
6pub fn detect_new_mail_for_user(
7    username: &str,
8    homedir: &Path,
9) -> anyhow::Result<Option<MailStatus>> {
10    let mail_storage_paths: Vec<PathBuf> = vec![
11        homedir.join("Mail"),
12        homedir.join("Mailbox"),
13        homedir.join("Maildir"),
14        PathBuf::from("/var/mail").join(username),
15        PathBuf::from("/var/spool/mail").join(username),
16        PathBuf::from("/var/maildir").join(username),
17        PathBuf::from("/var/spool/maildir").join(username),
18    ];
19
20    for path in mail_storage_paths {
21        tracing::trace!("Checking for mail at path: {}", path.display());
22        if path.is_dir() && path.join("new").is_dir() && path.join("cur").is_dir() {
23            tracing::trace!("Detected maildir structure at path: {}", path.display());
24            if let Ok(status) = detect_new_mail_by_maildir(&path) {
25                return Ok(Some(status));
26            }
27        } else if path.is_file() {
28            tracing::trace!("Detected mailbox file at path: {}", path.display());
29            if let Ok(status) = detect_new_mail_by_mailbox(&path) {
30                return Ok(Some(status));
31            }
32        }
33    }
34
35    Ok(None)
36}
37
38fn detect_new_mail_by_mailbox(mailbox_path: &Path) -> anyhow::Result<MailStatus> {
39    let stat = nix::sys::stat::stat(mailbox_path)?;
40
41    let mail_recv = stat.st_mtime;
42    let mail_read = stat.st_atime;
43
44    if mail_recv > mail_read {
45        Ok(MailStatus::NewMailReceived {
46            received_time: Utc
47                .timestamp_opt(mail_recv, 0)
48                .single()
49                .unwrap_or_else(Default::default),
50            unread_since: Utc
51                .timestamp_opt(mail_read, 0)
52                .single()
53                .unwrap_or_else(Default::default),
54        })
55    } else {
56        Ok(MailStatus::MailLastRead(
57            Utc.timestamp_opt(mail_read, 0)
58                .single()
59                .unwrap_or_else(Default::default),
60        ))
61    }
62}
63
64fn detect_new_mail_by_maildir(maildir_path: &Path) -> anyhow::Result<MailStatus> {
65    let new_mail_path = maildir_path.join("new");
66    let cur_mail_path = maildir_path.join("cur");
67
68    let mail_recv = nix::sys::stat::stat(&new_mail_path)?.st_mtime;
69    let mail_read = nix::sys::stat::stat(&cur_mail_path)?.st_mtime;
70
71    if new_mail_path.read_dir()?.next().is_none() && cur_mail_path.read_dir()?.next().is_none() {
72        Ok(MailStatus::NoMail)
73    } else if mail_recv > mail_read {
74        Ok(MailStatus::NewMailReceived {
75            received_time: Utc
76                .timestamp_opt(mail_recv, 0)
77                .single()
78                .unwrap_or_else(Default::default),
79            unread_since: Utc
80                .timestamp_opt(mail_read, 0)
81                .single()
82                .unwrap_or_else(Default::default),
83        })
84    } else {
85        Ok(MailStatus::MailLastRead(
86            Utc.timestamp_opt(mail_read, 0)
87                .single()
88                .unwrap_or_else(Default::default),
89        ))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use nix::{
97        fcntl::AT_FDCWD,
98        sys::{stat::UtimensatFlags, time::TimeSpec},
99    };
100    use std::{fs, os::unix::fs::MetadataExt};
101    use tempfile::TempDir;
102
103    fn set_file_times(path: &Path, atime: i64, mtime: i64) {
104        nix::sys::stat::utimensat(
105            AT_FDCWD,
106            path,
107            &TimeSpec::new(atime, 0),
108            &TimeSpec::new(mtime, 0),
109            UtimensatFlags::FollowSymlink,
110        )
111        .unwrap();
112    }
113
114    fn set_dir_mtime(path: &Path, mtime: i64) {
115        let metadata = fs::metadata(path).unwrap();
116        set_file_times(path, metadata.atime(), mtime);
117    }
118
119    #[test]
120    fn test_detect_new_mail_by_mailbox_new_mail_received() {
121        let tempdir = TempDir::new().unwrap();
122        let mailbox = tempdir.path().join("Mailbox");
123        fs::write(&mailbox, b"mail").unwrap();
124        set_file_times(&mailbox, 1_700_000_000, 1_700_000_100);
125
126        let status = detect_new_mail_by_mailbox(&mailbox).unwrap();
127
128        assert_eq!(
129            status,
130            MailStatus::NewMailReceived {
131                received_time: Utc.timestamp_opt(1_700_000_100, 0).single().unwrap(),
132                unread_since: Utc.timestamp_opt(1_700_000_000, 0).single().unwrap(),
133            }
134        );
135    }
136
137    #[test]
138    fn test_detect_new_mail_by_mailbox_mail_last_read() {
139        let tempdir = TempDir::new().unwrap();
140        let mailbox = tempdir.path().join("Mailbox");
141        fs::write(&mailbox, b"mail").unwrap();
142        set_file_times(&mailbox, 1_700_000_100, 1_700_000_000);
143
144        let status = detect_new_mail_by_mailbox(&mailbox).unwrap();
145
146        assert_eq!(
147            status,
148            MailStatus::MailLastRead(Utc.timestamp_opt(1_700_000_100, 0).single().unwrap())
149        );
150    }
151
152    #[test]
153    fn test_detect_new_mail_by_maildir_no_mail() {
154        let tempdir = TempDir::new().unwrap();
155        let maildir = tempdir.path().join("Maildir");
156        let new_dir = maildir.join("new");
157        let cur_dir = maildir.join("cur");
158        fs::create_dir_all(&new_dir).unwrap();
159        fs::create_dir_all(&cur_dir).unwrap();
160
161        let status = detect_new_mail_by_maildir(&maildir).unwrap();
162
163        assert_eq!(status, MailStatus::NoMail);
164    }
165
166    #[test]
167    fn test_detect_new_mail_by_maildir_new_mail_received() {
168        let tempdir = TempDir::new().unwrap();
169        let maildir = tempdir.path().join("Maildir");
170        let new_dir = maildir.join("new");
171        let cur_dir = maildir.join("cur");
172        fs::create_dir_all(&new_dir).unwrap();
173        fs::create_dir_all(&cur_dir).unwrap();
174        fs::write(new_dir.join("msg"), b"mail").unwrap();
175        set_dir_mtime(&cur_dir, 1_700_000_000);
176        set_dir_mtime(&new_dir, 1_700_000_100);
177
178        let status = detect_new_mail_by_maildir(&maildir).unwrap();
179
180        assert_eq!(
181            status,
182            MailStatus::NewMailReceived {
183                received_time: Utc.timestamp_opt(1_700_000_100, 0).single().unwrap(),
184                unread_since: Utc.timestamp_opt(1_700_000_000, 0).single().unwrap(),
185            }
186        );
187    }
188
189    #[test]
190    fn test_detect_new_mail_by_maildir_mail_last_read() {
191        let tempdir = TempDir::new().unwrap();
192        let maildir = tempdir.path().join("Maildir");
193        let new_dir = maildir.join("new");
194        let cur_dir = maildir.join("cur");
195        fs::create_dir_all(&new_dir).unwrap();
196        fs::create_dir_all(&cur_dir).unwrap();
197        fs::write(cur_dir.join("msg"), b"mail").unwrap();
198        set_dir_mtime(&new_dir, 1_700_000_000);
199        set_dir_mtime(&cur_dir, 1_700_000_100);
200
201        let status = detect_new_mail_by_maildir(&maildir).unwrap();
202
203        assert_eq!(
204            status,
205            MailStatus::MailLastRead(Utc.timestamp_opt(1_700_000_100, 0).single().unwrap())
206        );
207    }
208}