//! Email account domain types. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use strum_macros::EnumString; use crate::id_types::{EmailAccountId, UserId}; use super::shared::DbValue; // ============ Email Account ============ /// Authentication method for email accounts. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)] pub enum EmailAuthType { /// Traditional password-based IMAP/SMTP authentication. #[strum(serialize = "password")] #[default] Password, /// OAuth2 with Fastmail JMAP API. #[strum(serialize = "oauth2_fastmail")] OAuth2Fastmail, /// OAuth2 with Google/Gmail (IMAP + XOAUTH2). #[strum(serialize = "oauth2_google")] OAuth2Google, /// OAuth2 with Microsoft/Outlook (IMAP + XOAUTH2). #[strum(serialize = "oauth2_microsoft")] OAuth2Microsoft, /// OAuth2 with Yahoo Mail (IMAP + XOAUTH2). #[strum(serialize = "oauth2_yahoo")] OAuth2Yahoo, } impl EmailAuthType { /// Returns a human-readable display string. pub fn display_name(&self) -> &'static str { match self { EmailAuthType::Password => "Password", EmailAuthType::OAuth2Fastmail => "Fastmail", EmailAuthType::OAuth2Google => "Google", EmailAuthType::OAuth2Microsoft => "Microsoft", EmailAuthType::OAuth2Yahoo => "Yahoo", } } /// Returns the database/serialization string value. pub fn as_str(&self) -> &'static str { match self { EmailAuthType::Password => "password", EmailAuthType::OAuth2Fastmail => "oauth2_fastmail", EmailAuthType::OAuth2Google => "oauth2_google", EmailAuthType::OAuth2Microsoft => "oauth2_microsoft", EmailAuthType::OAuth2Yahoo => "oauth2_yahoo", } } /// Returns true if this auth type uses OAuth2. pub fn is_oauth(&self) -> bool { !matches!(self, EmailAuthType::Password) } /// Returns the provider ID for OAuth providers. pub fn provider_id(&self) -> Option<&'static str> { match self { EmailAuthType::Password => None, EmailAuthType::OAuth2Fastmail => Some("fastmail"), EmailAuthType::OAuth2Google => Some("google"), EmailAuthType::OAuth2Microsoft => Some("microsoft"), EmailAuthType::OAuth2Yahoo => Some("yahoo"), } } /// Creates an EmailAuthType from a provider ID. pub fn from_provider_id(id: &str) -> Option { match id { "fastmail" => Some(EmailAuthType::OAuth2Fastmail), "google" => Some(EmailAuthType::OAuth2Google), "microsoft" => Some(EmailAuthType::OAuth2Microsoft), "yahoo" => Some(EmailAuthType::OAuth2Yahoo), _ => None, } } /// Returns true if this auth type uses JMAP (vs IMAP). pub fn uses_jmap(&self) -> bool { matches!(self, EmailAuthType::OAuth2Fastmail) } /// Parses a string into an EmailAuthType, falling back to `Password` on invalid input. #[allow(clippy::should_implement_trait)] pub fn from_str_or_default(s: &str) -> Self { match s { "oauth2_fastmail" | "OAuth2Fastmail" => EmailAuthType::OAuth2Fastmail, "oauth2_google" | "OAuth2Google" => EmailAuthType::OAuth2Google, "oauth2_microsoft" | "OAuth2Microsoft" => EmailAuthType::OAuth2Microsoft, "oauth2_yahoo" | "OAuth2Yahoo" => EmailAuthType::OAuth2Yahoo, _ => EmailAuthType::Password, } } } impl DbValue for EmailAuthType { fn db_value(&self) -> &'static str { match self { EmailAuthType::Password => "password", EmailAuthType::OAuth2Fastmail => "oauth2_fastmail", EmailAuthType::OAuth2Google => "oauth2_google", EmailAuthType::OAuth2Microsoft => "oauth2_microsoft", EmailAuthType::OAuth2Yahoo => "oauth2_yahoo", } } } /// IMAP/SMTP or OAuth2 email account configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EmailAccount { /// Unique identifier. pub id: EmailAccountId, /// Owner user ID. pub user_id: UserId, /// Display name for the account. pub account_name: String, /// Email address. pub email_address: String, /// IMAP server hostname (password auth only). pub imap_server: String, /// IMAP server port (password auth only). pub imap_port: i32, /// SMTP server hostname (password auth only). pub smtp_server: String, /// SMTP server port (password auth only). pub smtp_port: i32, /// Login username (password auth only). pub username: String, /// Login password (never serialized, password auth only). #[serde(skip_serializing)] pub password: String, /// Whether to use TLS (password auth only). pub use_tls: bool, /// Last successful sync. pub last_sync_at: Option>, /// Account creation timestamp. pub created_at: DateTime, /// IMAP/JMAP folder name for archived emails. pub archive_folder_name: Option, /// Authentication type (password or OAuth2). pub auth_type: EmailAuthType, /// OAuth2 access token (never serialized). #[serde(skip_serializing)] pub oauth2_access_token: Option, /// OAuth2 refresh token (never serialized). #[serde(skip_serializing)] pub oauth2_refresh_token: Option, /// OAuth2 token expiration time. pub oauth2_token_expires_at: Option>, /// JMAP session URL (cached from discovery). pub jmap_session_url: Option, /// JMAP account ID (from session). pub jmap_account_id: Option, /// Auto-sync interval in minutes (None = disabled). pub sync_interval_minutes: Option, /// Plain text email signature, appended to outbound emails. pub email_signature: Option, /// Whether to show a system notification when new emails arrive (default: false). pub notify_new_emails: bool, } /// Per-folder IMAP sync state for incremental UID-based fetching. #[derive(Debug, Clone)] pub struct FolderSyncState { pub uid_validity: u32, pub last_seen_uid: u32, } impl EmailAccount { /// Returns true if this account uses OAuth2 authentication. pub fn is_oauth(&self) -> bool { self.auth_type != EmailAuthType::Password } /// Returns true if the OAuth2 token needs refresh (expired or expiring within 5 minutes). pub fn needs_token_refresh(&self) -> bool { match self.oauth2_token_expires_at { Some(expires_at) => { let buffer = chrono::Duration::minutes(5); Utc::now() + buffer >= expires_at } None => self.is_oauth(), // If no expiry set but is OAuth, assume needs refresh } } } #[cfg(test)] mod tests { use super::*; #[test] fn display_name_all_variants() { assert_eq!(EmailAuthType::Password.display_name(), "Password"); assert_eq!(EmailAuthType::OAuth2Fastmail.display_name(), "Fastmail"); assert_eq!(EmailAuthType::OAuth2Google.display_name(), "Google"); assert_eq!(EmailAuthType::OAuth2Microsoft.display_name(), "Microsoft"); assert_eq!(EmailAuthType::OAuth2Yahoo.display_name(), "Yahoo"); } #[test] fn as_str_all_variants() { assert_eq!(EmailAuthType::Password.as_str(), "password"); assert_eq!(EmailAuthType::OAuth2Fastmail.as_str(), "oauth2_fastmail"); assert_eq!(EmailAuthType::OAuth2Google.as_str(), "oauth2_google"); assert_eq!(EmailAuthType::OAuth2Microsoft.as_str(), "oauth2_microsoft"); assert_eq!(EmailAuthType::OAuth2Yahoo.as_str(), "oauth2_yahoo"); } #[test] fn is_oauth_password_is_false() { assert!(!EmailAuthType::Password.is_oauth()); } #[test] fn is_oauth_all_oauth_variants_are_true() { assert!(EmailAuthType::OAuth2Fastmail.is_oauth()); assert!(EmailAuthType::OAuth2Google.is_oauth()); assert!(EmailAuthType::OAuth2Microsoft.is_oauth()); assert!(EmailAuthType::OAuth2Yahoo.is_oauth()); } #[test] fn provider_id_password_is_none() { assert!(EmailAuthType::Password.provider_id().is_none()); } #[test] fn provider_id_oauth_variants() { assert_eq!(EmailAuthType::OAuth2Fastmail.provider_id(), Some("fastmail")); assert_eq!(EmailAuthType::OAuth2Google.provider_id(), Some("google")); assert_eq!(EmailAuthType::OAuth2Microsoft.provider_id(), Some("microsoft")); assert_eq!(EmailAuthType::OAuth2Yahoo.provider_id(), Some("yahoo")); } #[test] fn from_provider_id_roundtrip() { for variant in [ EmailAuthType::OAuth2Fastmail, EmailAuthType::OAuth2Google, EmailAuthType::OAuth2Microsoft, EmailAuthType::OAuth2Yahoo, ] { let id = variant.provider_id().unwrap(); assert_eq!(EmailAuthType::from_provider_id(id), Some(variant)); } } #[test] fn from_provider_id_unknown_returns_none() { assert!(EmailAuthType::from_provider_id("unknown").is_none()); assert!(EmailAuthType::from_provider_id("").is_none()); } #[test] fn uses_jmap_only_fastmail() { assert!(EmailAuthType::OAuth2Fastmail.uses_jmap()); assert!(!EmailAuthType::Password.uses_jmap()); assert!(!EmailAuthType::OAuth2Google.uses_jmap()); assert!(!EmailAuthType::OAuth2Microsoft.uses_jmap()); assert!(!EmailAuthType::OAuth2Yahoo.uses_jmap()); } #[test] fn from_str_or_default_valid_inputs() { assert_eq!( EmailAuthType::from_str_or_default("oauth2_fastmail"), EmailAuthType::OAuth2Fastmail ); assert_eq!( EmailAuthType::from_str_or_default("OAuth2Google"), EmailAuthType::OAuth2Google ); } #[test] fn from_str_or_default_invalid_falls_back() { assert_eq!( EmailAuthType::from_str_or_default("invalid"), EmailAuthType::Password ); assert_eq!( EmailAuthType::from_str_or_default(""), EmailAuthType::Password ); } #[test] fn db_value_matches_as_str() { for variant in [ EmailAuthType::Password, EmailAuthType::OAuth2Fastmail, EmailAuthType::OAuth2Google, EmailAuthType::OAuth2Microsoft, EmailAuthType::OAuth2Yahoo, ] { assert_eq!(variant.db_value(), variant.as_str()); } } #[test] fn default_is_password() { assert_eq!(EmailAuthType::default(), EmailAuthType::Password); } }