Skip to main content

max / goingson

10.6 KB · 311 lines History Blame Raw
1 //! Email account domain types.
2
3 use chrono::{DateTime, Utc};
4 use serde::{Deserialize, Serialize};
5 use strum_macros::EnumString;
6 use crate::id_types::{EmailAccountId, UserId};
7
8 use super::shared::DbValue;
9
10 // ============ Email Account ============
11
12 /// Authentication method for email accounts.
13 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, EnumString)]
14 pub enum EmailAuthType {
15 /// Traditional password-based IMAP/SMTP authentication.
16 #[strum(serialize = "password")]
17 #[default]
18 Password,
19 /// OAuth2 with Fastmail JMAP API.
20 #[strum(serialize = "oauth2_fastmail")]
21 OAuth2Fastmail,
22 /// OAuth2 with Google/Gmail (IMAP + XOAUTH2).
23 #[strum(serialize = "oauth2_google")]
24 OAuth2Google,
25 /// OAuth2 with Microsoft/Outlook (IMAP + XOAUTH2).
26 #[strum(serialize = "oauth2_microsoft")]
27 OAuth2Microsoft,
28 /// OAuth2 with Yahoo Mail (IMAP + XOAUTH2).
29 #[strum(serialize = "oauth2_yahoo")]
30 OAuth2Yahoo,
31 }
32
33 impl EmailAuthType {
34 /// Returns a human-readable display string.
35 pub fn display_name(&self) -> &'static str {
36 match self {
37 EmailAuthType::Password => "Password",
38 EmailAuthType::OAuth2Fastmail => "Fastmail",
39 EmailAuthType::OAuth2Google => "Google",
40 EmailAuthType::OAuth2Microsoft => "Microsoft",
41 EmailAuthType::OAuth2Yahoo => "Yahoo",
42 }
43 }
44
45 /// Returns the database/serialization string value.
46 pub fn as_str(&self) -> &'static str {
47 match self {
48 EmailAuthType::Password => "password",
49 EmailAuthType::OAuth2Fastmail => "oauth2_fastmail",
50 EmailAuthType::OAuth2Google => "oauth2_google",
51 EmailAuthType::OAuth2Microsoft => "oauth2_microsoft",
52 EmailAuthType::OAuth2Yahoo => "oauth2_yahoo",
53 }
54 }
55
56 /// Returns true if this auth type uses OAuth2.
57 pub fn is_oauth(&self) -> bool {
58 !matches!(self, EmailAuthType::Password)
59 }
60
61 /// Returns the provider ID for OAuth providers.
62 pub fn provider_id(&self) -> Option<&'static str> {
63 match self {
64 EmailAuthType::Password => None,
65 EmailAuthType::OAuth2Fastmail => Some("fastmail"),
66 EmailAuthType::OAuth2Google => Some("google"),
67 EmailAuthType::OAuth2Microsoft => Some("microsoft"),
68 EmailAuthType::OAuth2Yahoo => Some("yahoo"),
69 }
70 }
71
72 /// Creates an EmailAuthType from a provider ID.
73 pub fn from_provider_id(id: &str) -> Option<Self> {
74 match id {
75 "fastmail" => Some(EmailAuthType::OAuth2Fastmail),
76 "google" => Some(EmailAuthType::OAuth2Google),
77 "microsoft" => Some(EmailAuthType::OAuth2Microsoft),
78 "yahoo" => Some(EmailAuthType::OAuth2Yahoo),
79 _ => None,
80 }
81 }
82
83 /// Returns true if this auth type uses JMAP (vs IMAP).
84 pub fn uses_jmap(&self) -> bool {
85 matches!(self, EmailAuthType::OAuth2Fastmail)
86 }
87
88 /// Parses a string into an EmailAuthType, falling back to `Password` on invalid input.
89 #[allow(clippy::should_implement_trait)]
90 pub fn from_str_or_default(s: &str) -> Self {
91 match s {
92 "oauth2_fastmail" | "OAuth2Fastmail" => EmailAuthType::OAuth2Fastmail,
93 "oauth2_google" | "OAuth2Google" => EmailAuthType::OAuth2Google,
94 "oauth2_microsoft" | "OAuth2Microsoft" => EmailAuthType::OAuth2Microsoft,
95 "oauth2_yahoo" | "OAuth2Yahoo" => EmailAuthType::OAuth2Yahoo,
96 _ => EmailAuthType::Password,
97 }
98 }
99 }
100
101 impl DbValue for EmailAuthType {
102 fn db_value(&self) -> &'static str {
103 match self {
104 EmailAuthType::Password => "password",
105 EmailAuthType::OAuth2Fastmail => "oauth2_fastmail",
106 EmailAuthType::OAuth2Google => "oauth2_google",
107 EmailAuthType::OAuth2Microsoft => "oauth2_microsoft",
108 EmailAuthType::OAuth2Yahoo => "oauth2_yahoo",
109 }
110 }
111 }
112
113 /// IMAP/SMTP or OAuth2 email account configuration.
114 #[derive(Debug, Clone, Serialize, Deserialize)]
115 #[serde(rename_all = "camelCase")]
116 pub struct EmailAccount {
117 /// Unique identifier.
118 pub id: EmailAccountId,
119 /// Owner user ID.
120 pub user_id: UserId,
121 /// Display name for the account.
122 pub account_name: String,
123 /// Email address.
124 pub email_address: String,
125 /// IMAP server hostname (password auth only).
126 pub imap_server: String,
127 /// IMAP server port (password auth only).
128 pub imap_port: i32,
129 /// SMTP server hostname (password auth only).
130 pub smtp_server: String,
131 /// SMTP server port (password auth only).
132 pub smtp_port: i32,
133 /// Login username (password auth only).
134 pub username: String,
135 /// Login password (never serialized, password auth only).
136 #[serde(skip_serializing)]
137 pub password: String,
138 /// Whether to use TLS (password auth only).
139 pub use_tls: bool,
140 /// Last successful sync.
141 pub last_sync_at: Option<DateTime<Utc>>,
142 /// Account creation timestamp.
143 pub created_at: DateTime<Utc>,
144 /// IMAP/JMAP folder name for archived emails.
145 pub archive_folder_name: Option<String>,
146 /// Authentication type (password or OAuth2).
147 pub auth_type: EmailAuthType,
148 /// OAuth2 access token (never serialized).
149 #[serde(skip_serializing)]
150 pub oauth2_access_token: Option<String>,
151 /// OAuth2 refresh token (never serialized).
152 #[serde(skip_serializing)]
153 pub oauth2_refresh_token: Option<String>,
154 /// OAuth2 token expiration time.
155 pub oauth2_token_expires_at: Option<DateTime<Utc>>,
156 /// JMAP session URL (cached from discovery).
157 pub jmap_session_url: Option<String>,
158 /// JMAP account ID (from session).
159 pub jmap_account_id: Option<String>,
160 /// Auto-sync interval in minutes (None = disabled).
161 pub sync_interval_minutes: Option<i32>,
162 /// Plain text email signature, appended to outbound emails.
163 pub email_signature: Option<String>,
164 /// Whether to show a system notification when new emails arrive (default: false).
165 pub notify_new_emails: bool,
166 }
167
168 /// Per-folder IMAP sync state for incremental UID-based fetching.
169 #[derive(Debug, Clone)]
170 pub struct FolderSyncState {
171 pub uid_validity: u32,
172 pub last_seen_uid: u32,
173 }
174
175 impl EmailAccount {
176 /// Returns true if this account uses OAuth2 authentication.
177 pub fn is_oauth(&self) -> bool {
178 self.auth_type != EmailAuthType::Password
179 }
180
181 /// Returns true if the OAuth2 token needs refresh (expired or expiring within 5 minutes).
182 pub fn needs_token_refresh(&self) -> bool {
183 match self.oauth2_token_expires_at {
184 Some(expires_at) => {
185 let buffer = chrono::Duration::minutes(5);
186 Utc::now() + buffer >= expires_at
187 }
188 None => self.is_oauth(), // If no expiry set but is OAuth, assume needs refresh
189 }
190 }
191 }
192
193 #[cfg(test)]
194 mod tests {
195 use super::*;
196
197 #[test]
198 fn display_name_all_variants() {
199 assert_eq!(EmailAuthType::Password.display_name(), "Password");
200 assert_eq!(EmailAuthType::OAuth2Fastmail.display_name(), "Fastmail");
201 assert_eq!(EmailAuthType::OAuth2Google.display_name(), "Google");
202 assert_eq!(EmailAuthType::OAuth2Microsoft.display_name(), "Microsoft");
203 assert_eq!(EmailAuthType::OAuth2Yahoo.display_name(), "Yahoo");
204 }
205
206 #[test]
207 fn as_str_all_variants() {
208 assert_eq!(EmailAuthType::Password.as_str(), "password");
209 assert_eq!(EmailAuthType::OAuth2Fastmail.as_str(), "oauth2_fastmail");
210 assert_eq!(EmailAuthType::OAuth2Google.as_str(), "oauth2_google");
211 assert_eq!(EmailAuthType::OAuth2Microsoft.as_str(), "oauth2_microsoft");
212 assert_eq!(EmailAuthType::OAuth2Yahoo.as_str(), "oauth2_yahoo");
213 }
214
215 #[test]
216 fn is_oauth_password_is_false() {
217 assert!(!EmailAuthType::Password.is_oauth());
218 }
219
220 #[test]
221 fn is_oauth_all_oauth_variants_are_true() {
222 assert!(EmailAuthType::OAuth2Fastmail.is_oauth());
223 assert!(EmailAuthType::OAuth2Google.is_oauth());
224 assert!(EmailAuthType::OAuth2Microsoft.is_oauth());
225 assert!(EmailAuthType::OAuth2Yahoo.is_oauth());
226 }
227
228 #[test]
229 fn provider_id_password_is_none() {
230 assert!(EmailAuthType::Password.provider_id().is_none());
231 }
232
233 #[test]
234 fn provider_id_oauth_variants() {
235 assert_eq!(EmailAuthType::OAuth2Fastmail.provider_id(), Some("fastmail"));
236 assert_eq!(EmailAuthType::OAuth2Google.provider_id(), Some("google"));
237 assert_eq!(EmailAuthType::OAuth2Microsoft.provider_id(), Some("microsoft"));
238 assert_eq!(EmailAuthType::OAuth2Yahoo.provider_id(), Some("yahoo"));
239 }
240
241 #[test]
242 fn from_provider_id_roundtrip() {
243 for variant in [
244 EmailAuthType::OAuth2Fastmail,
245 EmailAuthType::OAuth2Google,
246 EmailAuthType::OAuth2Microsoft,
247 EmailAuthType::OAuth2Yahoo,
248 ] {
249 let id = variant.provider_id().unwrap();
250 assert_eq!(EmailAuthType::from_provider_id(id), Some(variant));
251 }
252 }
253
254 #[test]
255 fn from_provider_id_unknown_returns_none() {
256 assert!(EmailAuthType::from_provider_id("unknown").is_none());
257 assert!(EmailAuthType::from_provider_id("").is_none());
258 }
259
260 #[test]
261 fn uses_jmap_only_fastmail() {
262 assert!(EmailAuthType::OAuth2Fastmail.uses_jmap());
263 assert!(!EmailAuthType::Password.uses_jmap());
264 assert!(!EmailAuthType::OAuth2Google.uses_jmap());
265 assert!(!EmailAuthType::OAuth2Microsoft.uses_jmap());
266 assert!(!EmailAuthType::OAuth2Yahoo.uses_jmap());
267 }
268
269 #[test]
270 fn from_str_or_default_valid_inputs() {
271 assert_eq!(
272 EmailAuthType::from_str_or_default("oauth2_fastmail"),
273 EmailAuthType::OAuth2Fastmail
274 );
275 assert_eq!(
276 EmailAuthType::from_str_or_default("OAuth2Google"),
277 EmailAuthType::OAuth2Google
278 );
279 }
280
281 #[test]
282 fn from_str_or_default_invalid_falls_back() {
283 assert_eq!(
284 EmailAuthType::from_str_or_default("invalid"),
285 EmailAuthType::Password
286 );
287 assert_eq!(
288 EmailAuthType::from_str_or_default(""),
289 EmailAuthType::Password
290 );
291 }
292
293 #[test]
294 fn db_value_matches_as_str() {
295 for variant in [
296 EmailAuthType::Password,
297 EmailAuthType::OAuth2Fastmail,
298 EmailAuthType::OAuth2Google,
299 EmailAuthType::OAuth2Microsoft,
300 EmailAuthType::OAuth2Yahoo,
301 ] {
302 assert_eq!(variant.db_value(), variant.as_str());
303 }
304 }
305
306 #[test]
307 fn default_is_password() {
308 assert_eq!(EmailAuthType::default(), EmailAuthType::Password);
309 }
310 }
311