oxedyne/fe2o3/fe2o3_net/src/mail/store.rs
8.2 KiB, 108 runs
created by r1870400018:9850, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Mailbox storage trait. |
| 2 | //! |
| 3 | //! `MailStore` is the abstraction the SMTP and IMAP servers use to persist |
| 4 | //! and retrieve messages. The trait deliberately operates on raw RFC 5322 |
| 5 | //! message bytes rather than a parsed `EmailMessage`: IMAP `FETCH BODY[]` |
| 6 | //! must return the original bytes byte-for-byte, and SMTP `DATA` already |
| 7 | //! delivers a fully-formed message blob. |
| 8 | //! |
| 9 | //! Implementations are expected to be cheap to clone (typically via an |
| 10 | //! internal `Arc`) so a long-running server can hand a store to every |
| 11 | //! connection task without contention. |
| 12 | //! |
| 13 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 14 | //! Anthropic Claude |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | |
| 18 | use std::time::SystemTime; |
| 19 | |
| 20 | |
| 21 | /// Always the plain UTF-8, user-facing form. Folder names travel the wire in |
| 22 | /// IMAP modified UTF-7, and the conversion happens at the wire layer. |
| 23 | #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| 24 | pub struct FolderName(pub String); |
| 25 | |
| 26 | impl FolderName { |
| 27 | pub fn new<S: Into<String>>(s: S) -> Self { |
| 28 | Self(s.into()) |
| 29 | } |
| 30 | |
| 31 | pub fn as_str(&self) -> &str { |
| 32 | &self.0 |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | /// IMAP requires these to increase monotonically within a folder (RFC 3501 |
| 37 | /// §2.3.1.1); each `MailStore` implementation honours that itself. |
| 38 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| 39 | pub struct MessageUid(pub u32); |
| 40 | |
| 41 | /// The small set Thunderbird uses on a steady-state session. Custom keywords |
| 42 | /// are out of scope for the MVP. |
| 43 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] |
| 44 | pub struct MessageFlags { |
| 45 | pub seen: bool, |
| 46 | pub answered: bool, |
| 47 | pub flagged: bool, |
| 48 | pub deleted: bool, // marked for EXPUNGE |
| 49 | pub draft: bool, |
| 50 | pub recent: bool, // cleared by the next session opening the folder R/W |
| 51 | } |
| 52 | |
| 53 | impl MessageFlags { |
| 54 | /// The space-separated atom list only, e.g. `\Seen \Flagged`; the caller |
| 55 | /// supplies the surrounding parentheses. |
| 56 | pub fn to_imap_list(&self) -> String { |
| 57 | let mut out = String::new(); |
| 58 | let mut push = |s: &str| { |
| 59 | if !out.is_empty() { out.push(' '); } |
| 60 | out.push_str(s); |
| 61 | }; |
| 62 | if self.seen { push("\\Seen"); } |
| 63 | if self.answered { push("\\Answered"); } |
| 64 | if self.flagged { push("\\Flagged"); } |
| 65 | if self.deleted { push("\\Deleted"); } |
| 66 | if self.draft { push("\\Draft"); } |
| 67 | if self.recent { push("\\Recent"); } |
| 68 | out |
| 69 | } |
| 70 | |
| 71 | /// Is `flag` set? Named with or without its leading backslash; anything |
| 72 | /// outside the known set reads as clear. |
| 73 | pub fn has(&self, flag: &str) -> bool { |
| 74 | match flag { |
| 75 | "\\Seen" | "Seen" => self.seen, |
| 76 | "\\Answered" | "Answered" => self.answered, |
| 77 | "\\Flagged" | "Flagged" => self.flagged, |
| 78 | "\\Deleted" | "Deleted" => self.deleted, |
| 79 | "\\Draft" | "Draft" => self.draft, |
| 80 | "\\Recent" | "Recent" => self.recent, |
| 81 | _ => false, |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /// A name outside the known set is silently ignored, since a client may |
| 86 | /// `STORE` a custom keyword the MVP does not carry. |
| 87 | pub fn set(&mut self, flag: &str, on: bool) { |
| 88 | match flag { |
| 89 | "\\Seen" | "Seen" => self.seen = on, |
| 90 | "\\Answered" | "Answered" => self.answered = on, |
| 91 | "\\Flagged" | "Flagged" => self.flagged = on, |
| 92 | "\\Deleted" | "Deleted" => self.deleted = on, |
| 93 | "\\Draft" | "Draft" => self.draft = on, |
| 94 | "\\Recent" | "Recent" => self.recent = on, |
| 95 | _ => (), |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /// Answers FETCH FLAGS, INTERNALDATE, RFC822.SIZE and UID without re-reading |
| 101 | /// the raw message bytes. |
| 102 | #[derive(Clone, Debug)] |
| 103 | pub struct MessageMeta { |
| 104 | pub uid: MessageUid, |
| 105 | pub size: u64, // raw message, bytes |
| 106 | pub internal: SystemTime, // when the server stored it, RFC 3501 §2.3.3 |
| 107 | pub flags: MessageFlags, |
| 108 | } |
| 109 | |
| 110 | #[derive(Clone, Debug, Default)] |
| 111 | pub struct FolderStatus { |
| 112 | pub exists: u32, // messages present, post-expunge |
| 113 | pub recent: u32, // messages flagged \Recent |
| 114 | pub unseen: u32, // messages without \Seen |
| 115 | pub uid_validity: u32, // changes whenever the UID space is reset |
| 116 | pub uid_next: u32, // UID the next appended message will receive |
| 117 | } |
| 118 | |
| 119 | /// The result of `UserStore::authenticate` (see [`crate::mail::user`]). Which |
| 120 | /// field a backend keys off is its own business: a Maildir store needs only |
| 121 | /// the delivery key, an Ozone-backed store would key off the user id. |
| 122 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 123 | pub struct MailUser { |
| 124 | pub local: String, // left of `@`, as authenticated |
| 125 | pub domain: String, // right of `@` |
| 126 | pub delivery_key: String, // mailbox root: a path or opaque key, set by the UserStore |
| 127 | // Addresses besides its own that the account may send as: the identities a mail client |
| 128 | // sends through this one login. Empty for most accounts. |
| 129 | pub send_as: Vec<String>, |
| 130 | } |
| 131 | |
| 132 | impl MailUser { |
| 133 | pub fn address(&self) -> String { |
| 134 | fmt!("{}@{}", self.local, self.domain) |
| 135 | } |
| 136 | |
| 137 | /// May this account send mail as `address`? Its own address and those listed in |
| 138 | /// `send_as`, compared without regard to case. |
| 139 | pub fn may_send_as(&self, address: &str) -> bool { |
| 140 | let a = address.trim().to_lowercase(); |
| 141 | a == self.address().to_lowercase() |
| 142 | || self.send_as.iter().any(|s| s.trim().to_lowercase() == a) |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | /// Every method takes a `MailUser`, so one store hosts many accounts. |
| 147 | /// |
| 148 | /// The trait is deliberately synchronous: the IMAP and SMTP servers wrap each |
| 149 | /// call in `tokio::task::spawn_blocking` so the underlying I/O does not block |
| 150 | /// the runtime. Async would force every implementation through |
| 151 | /// `Pin<Box<dyn Future>>` for no practical gain on a single-host mailbox. |
| 152 | pub trait MailStore: Clone + Send + Sync + 'static { |
| 153 | /// Creates the folders, INBOX included. Idempotent. |
| 154 | fn ensure_user(&self, user: &MailUser) -> Outcome<()>; |
| 155 | |
| 156 | /// `bytes` must be a fully-formed RFC 5322 message; the UID returned is |
| 157 | /// allocated monotonically within the folder. |
| 158 | fn append( |
| 159 | &self, |
| 160 | user: &MailUser, |
| 161 | folder: &FolderName, |
| 162 | bytes: &[u8], |
| 163 | flags: MessageFlags, |
| 164 | internal: Option<SystemTime>, |
| 165 | ) |
| 166 | -> Outcome<MessageUid>; |
| 167 | |
| 168 | /// Recursively. |
| 169 | fn list_folders(&self, user: &MailUser) -> Outcome<Vec<FolderName>>; |
| 170 | |
| 171 | fn folder_status( |
| 172 | &self, |
| 173 | user: &MailUser, |
| 174 | folder: &FolderName, |
| 175 | ) |
| 176 | -> Outcome<FolderStatus>; |
| 177 | |
| 178 | /// In UID order. `read_only` withholds the clearing of `\Recent` on the |
| 179 | /// messages returned (RFC 3501 §6.3.1: SELECT clears, EXAMINE does not). |
| 180 | fn list_messages( |
| 181 | &self, |
| 182 | user: &MailUser, |
| 183 | folder: &FolderName, |
| 184 | read_only: bool, |
| 185 | ) |
| 186 | -> Outcome<Vec<MessageMeta>>; |
| 187 | |
| 188 | /// The stored bytes exactly, since `FETCH BODY[]` must reproduce them. |
| 189 | fn fetch_bytes( |
| 190 | &self, |
| 191 | user: &MailUser, |
| 192 | folder: &FolderName, |
| 193 | uid: MessageUid, |
| 194 | ) |
| 195 | -> Outcome<Vec<u8>>; |
| 196 | |
| 197 | /// The flag set returned may differ from the one given, where the |
| 198 | /// implementation enforces an invariant such as always-clear `\Recent`. |
| 199 | fn set_flags( |
| 200 | &self, |
| 201 | user: &MailUser, |
| 202 | folder: &FolderName, |
| 203 | uid: MessageUid, |
| 204 | flags: MessageFlags, |
| 205 | ) |
| 206 | -> Outcome<MessageFlags>; |
| 207 | |
| 208 | /// Removes every message flagged `\Deleted`, and returns their UIDs in |
| 209 | /// removal order, since IMAP wants one untagged `EXPUNGE` per UID in that |
| 210 | /// same order. |
| 211 | fn expunge( |
| 212 | &self, |
| 213 | user: &MailUser, |
| 214 | folder: &FolderName, |
| 215 | ) |
| 216 | -> Outcome<Vec<MessageUid>>; |
| 217 | |
| 218 | /// Idempotent. |
| 219 | fn create_folder( |
| 220 | &self, |
| 221 | user: &MailUser, |
| 222 | folder: &FolderName, |
| 223 | ) |
| 224 | -> Outcome<()>; |
| 225 | |
| 226 | /// Bookkeeping only, but it has to persist: Thunderbird issues `LSUB` and |
| 227 | /// expects back what it subscribed to earlier. |
| 228 | fn subscribe( |
| 229 | &self, |
| 230 | user: &MailUser, |
| 231 | folder: &FolderName, |
| 232 | ) |
| 233 | -> Outcome<()>; |
| 234 | |
| 235 | fn list_subscribed(&self, user: &MailUser) -> Outcome<Vec<FolderName>>; |
| 236 | } |