roowho2_lib/server/fingerd/
local_email.rs1use 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}