use regex::Regex;
use serde_json::{json, Value};
use std::sync::OnceLock;
pub const IMAP4_SSL_PORT: u16 = 993;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(non_camel_case_types)]
pub struct _IMAPKey {
pub username: String,
pub password: String,
pub server: String,
pub port: u16,
pub folder: String,
pub use_ssl: bool,
}
pub struct EmailIMAPSegment;
impl EmailIMAPSegment {
#[allow(non_upper_case_globals)]
pub const interval: f64 = 60.0;
#[allow(clippy::too_many_arguments)]
pub fn key(
mut username: String,
mut password: String,
mut server: String,
mut port: u16,
username_variable: Option<&str>,
password_variable: Option<&str>,
server_variable: Option<&str>,
port_variable: Option<&str>,
folder: String,
use_ssl: Option<bool>,
) -> _IMAPKey {
let use_ssl = use_ssl.unwrap_or(port == IMAP4_SSL_PORT);
if let Some(var) = username_variable {
if let Ok(v) = std::env::var(var) {
username = v;
}
}
if let Some(var) = password_variable {
if let Ok(v) = std::env::var(var) {
password = v;
}
}
if let Some(var) = server_variable {
if let Ok(v) = std::env::var(var) {
server = v;
}
}
if let Some(var) = port_variable {
if let Ok(v) = std::env::var(var) {
if let Ok(p) = v.parse() {
port = p;
}
}
}
_IMAPKey {
username,
password,
server,
port,
folder,
use_ssl,
}
}
fn unseen_re() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"UNSEEN (\d+)").unwrap())
}
pub fn compute_state(key: &_IMAPKey) -> Option<i64> {
if key.username.is_empty() || key.password.is_empty() {
return None;
}
None
}
pub fn parse_unseen_count(status_line: &str) -> Option<i64> {
Self::unseen_re()
.captures(status_line)
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().parse().ok())
}
pub fn render_one(unread_count: Option<i64>, max_msgs: Option<i64>) -> Option<Vec<Value>> {
match unread_count {
None | Some(0) => None,
Some(n) if max_msgs.is_none() => Some(vec![json!({
"contents": n.to_string(),
"highlight_groups": ["email_alert"],
})]),
Some(n) => {
let max = max_msgs.unwrap();
let gradient = ((n as f64 * 100.0) / max as f64).min(100.0);
Some(vec![json!({
"contents": n.to_string(),
"highlight_groups": ["email_alert_gradient", "email_alert"],
"gradient_level": gradient,
})])
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn imap4_ssl_port_matches_upstream() {
assert_eq!(IMAP4_SSL_PORT, 993);
}
#[test]
fn interval_matches_upstream() {
assert_eq!(EmailIMAPSegment::interval, 60.0);
}
#[test]
fn key_use_ssl_defaults_to_port_eq_ssl_port() {
let k = EmailIMAPSegment::key(
"u".into(),
"p".into(),
"s".into(),
993,
None,
None,
None,
None,
"INBOX".into(),
None,
);
assert!(k.use_ssl);
let k2 = EmailIMAPSegment::key(
"u".into(),
"p".into(),
"s".into(),
143,
None,
None,
None,
None,
"INBOX".into(),
None,
);
assert!(!k2.use_ssl);
}
#[test]
fn key_use_ssl_explicit_overrides_default() {
let k = EmailIMAPSegment::key(
"u".into(),
"p".into(),
"s".into(),
993,
None,
None,
None,
None,
"INBOX".into(),
Some(false),
);
assert!(!k.use_ssl);
}
#[test]
fn parse_unseen_count_extracts_n() {
assert_eq!(EmailIMAPSegment::parse_unseen_count("UNSEEN 7"), Some(7));
assert_eq!(
EmailIMAPSegment::parse_unseen_count(r#"INBOX (MESSAGES 100 UNSEEN 3)"#),
Some(3)
);
assert_eq!(EmailIMAPSegment::parse_unseen_count("no match here"), None);
}
#[test]
fn render_one_none_returns_none() {
assert!(EmailIMAPSegment::render_one(None, None).is_none());
}
#[test]
fn render_one_zero_returns_none() {
assert!(EmailIMAPSegment::render_one(Some(0), None).is_none());
}
#[test]
fn render_one_no_max_emits_email_alert() {
let result = EmailIMAPSegment::render_one(Some(5), None).unwrap();
assert_eq!(result[0]["contents"], "5");
assert_eq!(
result[0]["highlight_groups"],
serde_json::json!(["email_alert"])
);
assert!(result[0].get("gradient_level").is_none());
}
#[test]
fn render_one_with_max_includes_gradient() {
let result = EmailIMAPSegment::render_one(Some(25), Some(100)).unwrap();
assert_eq!(result[0]["contents"], "25");
assert_eq!(
result[0]["highlight_groups"],
serde_json::json!(["email_alert_gradient", "email_alert"])
);
assert_eq!(result[0]["gradient_level"], 25.0);
}
#[test]
fn render_one_clamps_gradient_at_100() {
let result = EmailIMAPSegment::render_one(Some(150), Some(100)).unwrap();
assert_eq!(result[0]["gradient_level"], 100.0);
}
#[test]
fn compute_state_returns_none_without_credentials() {
let key = _IMAPKey {
username: "".to_string(),
password: "".to_string(),
server: "x".to_string(),
port: 993,
folder: "INBOX".to_string(),
use_ssl: true,
};
assert!(EmailIMAPSegment::compute_state(&key).is_none());
}
}