1
use chrono::{TimeZone, Utc};
2
use std::path::{Path, PathBuf};
3

            
4
use crate::proto::finger_protocol::MailStatus;
5

            
6
1
pub fn detect_new_mail_for_user(
7
1
    username: &str,
8
1
    homedir: &Path,
9
1
) -> anyhow::Result<Option<MailStatus>> {
10
1
    let mail_storage_paths: Vec<PathBuf> = vec![
11
1
        homedir.join("Mail"),
12
1
        homedir.join("Mailbox"),
13
1
        homedir.join("Maildir"),
14
1
        PathBuf::from("/var/mail").join(username),
15
1
        PathBuf::from("/var/spool/mail").join(username),
16
1
        PathBuf::from("/var/maildir").join(username),
17
1
        PathBuf::from("/var/spool/maildir").join(username),
18
    ];
19

            
20
7
    for path in mail_storage_paths {
21
7
        tracing::trace!("Checking for mail at path: {}", path.display());
22
7
        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
7
        } 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
7
        }
33
    }
34

            
35
1
    Ok(None)
36
1
}
37

            
38
2
fn detect_new_mail_by_mailbox(mailbox_path: &Path) -> anyhow::Result<MailStatus> {
39
2
    let stat = nix::sys::stat::stat(mailbox_path)?;
40

            
41
2
    let mail_recv = stat.st_mtime;
42
2
    let mail_read = stat.st_atime;
43

            
44
2
    if mail_recv > mail_read {
45
1
        Ok(MailStatus::NewMailReceived {
46
1
            received_time: Utc
47
1
                .timestamp_opt(mail_recv, 0)
48
1
                .single()
49
1
                .unwrap_or_else(Default::default),
50
1
            unread_since: Utc
51
1
                .timestamp_opt(mail_read, 0)
52
1
                .single()
53
1
                .unwrap_or_else(Default::default),
54
1
        })
55
    } else {
56
1
        Ok(MailStatus::MailLastRead(
57
1
            Utc.timestamp_opt(mail_read, 0)
58
1
                .single()
59
1
                .unwrap_or_else(Default::default),
60
1
        ))
61
    }
62
2
}
63

            
64
3
fn detect_new_mail_by_maildir(maildir_path: &Path) -> anyhow::Result<MailStatus> {
65
3
    let new_mail_path = maildir_path.join("new");
66
3
    let cur_mail_path = maildir_path.join("cur");
67

            
68
3
    let mail_recv = nix::sys::stat::stat(&new_mail_path)?.st_mtime;
69
3
    let mail_read = nix::sys::stat::stat(&cur_mail_path)?.st_mtime;
70

            
71
3
    if new_mail_path.read_dir()?.next().is_none() && cur_mail_path.read_dir()?.next().is_none() {
72
1
        Ok(MailStatus::NoMail)
73
2
    } else if mail_recv > mail_read {
74
1
        Ok(MailStatus::NewMailReceived {
75
1
            received_time: Utc
76
1
                .timestamp_opt(mail_recv, 0)
77
1
                .single()
78
1
                .unwrap_or_else(Default::default),
79
1
            unread_since: Utc
80
1
                .timestamp_opt(mail_read, 0)
81
1
                .single()
82
1
                .unwrap_or_else(Default::default),
83
1
        })
84
    } else {
85
1
        Ok(MailStatus::MailLastRead(
86
1
            Utc.timestamp_opt(mail_read, 0)
87
1
                .single()
88
1
                .unwrap_or_else(Default::default),
89
1
        ))
90
    }
91
3
}
92

            
93
#[cfg(test)]
94
mod 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
6
    fn set_file_times(path: &Path, atime: i64, mtime: i64) {
104
6
        nix::sys::stat::utimensat(
105
            AT_FDCWD,
106
6
            path,
107
6
            &TimeSpec::new(atime, 0),
108
6
            &TimeSpec::new(mtime, 0),
109
6
            UtimensatFlags::FollowSymlink,
110
        )
111
6
        .unwrap();
112
6
    }
113

            
114
4
    fn set_dir_mtime(path: &Path, mtime: i64) {
115
4
        let metadata = fs::metadata(path).unwrap();
116
4
        set_file_times(path, metadata.atime(), mtime);
117
4
    }
118

            
119
    #[test]
120
1
    fn test_detect_new_mail_by_mailbox_new_mail_received() {
121
1
        let tempdir = TempDir::new().unwrap();
122
1
        let mailbox = tempdir.path().join("Mailbox");
123
1
        fs::write(&mailbox, b"mail").unwrap();
124
1
        set_file_times(&mailbox, 1_700_000_000, 1_700_000_100);
125

            
126
1
        let status = detect_new_mail_by_mailbox(&mailbox).unwrap();
127

            
128
1
        assert_eq!(
129
            status,
130
1
            MailStatus::NewMailReceived {
131
1
                received_time: Utc.timestamp_opt(1_700_000_100, 0).single().unwrap(),
132
1
                unread_since: Utc.timestamp_opt(1_700_000_000, 0).single().unwrap(),
133
1
            }
134
        );
135
1
    }
136

            
137
    #[test]
138
1
    fn test_detect_new_mail_by_mailbox_mail_last_read() {
139
1
        let tempdir = TempDir::new().unwrap();
140
1
        let mailbox = tempdir.path().join("Mailbox");
141
1
        fs::write(&mailbox, b"mail").unwrap();
142
1
        set_file_times(&mailbox, 1_700_000_100, 1_700_000_000);
143

            
144
1
        let status = detect_new_mail_by_mailbox(&mailbox).unwrap();
145

            
146
1
        assert_eq!(
147
            status,
148
1
            MailStatus::MailLastRead(Utc.timestamp_opt(1_700_000_100, 0).single().unwrap())
149
        );
150
1
    }
151

            
152
    #[test]
153
1
    fn test_detect_new_mail_by_maildir_no_mail() {
154
1
        let tempdir = TempDir::new().unwrap();
155
1
        let maildir = tempdir.path().join("Maildir");
156
1
        let new_dir = maildir.join("new");
157
1
        let cur_dir = maildir.join("cur");
158
1
        fs::create_dir_all(&new_dir).unwrap();
159
1
        fs::create_dir_all(&cur_dir).unwrap();
160

            
161
1
        let status = detect_new_mail_by_maildir(&maildir).unwrap();
162

            
163
1
        assert_eq!(status, MailStatus::NoMail);
164
1
    }
165

            
166
    #[test]
167
1
    fn test_detect_new_mail_by_maildir_new_mail_received() {
168
1
        let tempdir = TempDir::new().unwrap();
169
1
        let maildir = tempdir.path().join("Maildir");
170
1
        let new_dir = maildir.join("new");
171
1
        let cur_dir = maildir.join("cur");
172
1
        fs::create_dir_all(&new_dir).unwrap();
173
1
        fs::create_dir_all(&cur_dir).unwrap();
174
1
        fs::write(new_dir.join("msg"), b"mail").unwrap();
175
1
        set_dir_mtime(&cur_dir, 1_700_000_000);
176
1
        set_dir_mtime(&new_dir, 1_700_000_100);
177

            
178
1
        let status = detect_new_mail_by_maildir(&maildir).unwrap();
179

            
180
1
        assert_eq!(
181
            status,
182
1
            MailStatus::NewMailReceived {
183
1
                received_time: Utc.timestamp_opt(1_700_000_100, 0).single().unwrap(),
184
1
                unread_since: Utc.timestamp_opt(1_700_000_000, 0).single().unwrap(),
185
1
            }
186
        );
187
1
    }
188

            
189
    #[test]
190
1
    fn test_detect_new_mail_by_maildir_mail_last_read() {
191
1
        let tempdir = TempDir::new().unwrap();
192
1
        let maildir = tempdir.path().join("Maildir");
193
1
        let new_dir = maildir.join("new");
194
1
        let cur_dir = maildir.join("cur");
195
1
        fs::create_dir_all(&new_dir).unwrap();
196
1
        fs::create_dir_all(&cur_dir).unwrap();
197
1
        fs::write(cur_dir.join("msg"), b"mail").unwrap();
198
1
        set_dir_mtime(&new_dir, 1_700_000_000);
199
1
        set_dir_mtime(&cur_dir, 1_700_000_100);
200

            
201
1
        let status = detect_new_mail_by_maildir(&maildir).unwrap();
202

            
203
1
        assert_eq!(
204
            status,
205
1
            MailStatus::MailLastRead(Utc.timestamp_opt(1_700_000_100, 0).single().unwrap())
206
        );
207
1
    }
208
}