Oregami
Repositories/oxedyne/fe2o3

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
16use oxedyne_fe2o3_core::prelude::*;
17
18use 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)]
24pub struct FolderName(pub String);
25
26impl 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)]
39pub 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)]
44pub 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
53impl 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)]
103pub 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)]
111pub 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)]
123pub 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
132impl 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.
152pub 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}