Files
inbuxa-server/crates/features/src/masked_email/state.rs
T
jcoffey-dev aaca8fe537 Masked email: the fork's subspace and the masked_email module (ME-1, ME-2, ME-3, ME-6a, ME-7, ME-8, ME-13, ME-14, ME-15)
Subspace _ holds the fork's own data, with its own SQL table and RocksDB
column family, and is part of backup. The masked_email module keeps each
mask's state, last mail and pending deadline beside upstream's record, an
index from address to mask with tombstones, and a per-account change log;
and generates addresses in the fork's format.
2026-09-18 16:17:47 -07:00

130 lines
3.9 KiB
Rust

/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! One state per mask, shown by each API in its own terms (ME-1, ME-2,
//! ME-6a).
/// A mask's state. The order of the discriminants is stored, so it never
/// changes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum State {
/// Created by a password manager, not yet used: delivered, and becomes
/// `Enabled` on the first message (ME-7). Removed after 24 hours
/// without mail (ME-8).
Pending = 0,
/// Delivered normally.
Enabled = 1,
/// Accepted, and filed straight to Trash (ME-5).
Disabled = 2,
/// Refused (ME-6).
Deleted = 3,
}
impl State {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(State::Pending),
1 => Some(State::Enabled),
2 => Some(State::Disabled),
3 => Some(State::Deleted),
_ => None,
}
}
/// The state of a mask written before the fork, from upstream's
/// `enabled` flag.
pub fn from_upstream(enabled: bool) -> Self {
if enabled {
State::Enabled
} else {
State::Deleted
}
}
/// Whether mail to the mask is accepted (ME-4).
pub fn is_live(self) -> bool {
self != State::Deleted
}
/// The Fastmail API's name for the state.
pub fn as_fastmail(self) -> &'static str {
match self {
State::Pending => "pending",
State::Enabled => "enabled",
State::Disabled => "disabled",
State::Deleted => "deleted",
}
}
pub fn parse_fastmail(value: &str) -> Option<Self> {
match value {
"pending" => Some(State::Pending),
"enabled" => Some(State::Enabled),
"disabled" => Some(State::Disabled),
"deleted" => Some(State::Deleted),
_ => None,
}
}
/// Upstream's `enabled`: whether mail is accepted. `disabled` masks
/// accept mail (into Trash), so they read `true`. An expired mask reads
/// `false` whatever its state (ME-6a).
pub fn as_upstream_enabled(self, expired: bool) -> bool {
self.is_live() && !expired
}
/// The state an upstream `enabled` write sets (ME-2).
pub fn from_upstream_write(enabled: bool) -> Self {
State::from_upstream(enabled)
}
/// Whether a Fastmail write may move a mask from `self` to `to` (ME-1):
/// `pending` can't be set once a mask has left it.
pub fn can_become(self, to: State) -> bool {
to != State::Pending || self == State::Pending
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn both_apis_read_one_state() {
// The table in "One state, two APIs"
for (state, fastmail, enabled) in [
(State::Pending, "pending", true),
(State::Enabled, "enabled", true),
(State::Disabled, "disabled", true),
(State::Deleted, "deleted", false),
] {
assert_eq!(state.as_fastmail(), fastmail);
assert_eq!(State::parse_fastmail(fastmail), Some(state));
assert_eq!(state.as_upstream_enabled(false), enabled);
assert!(!state.as_upstream_enabled(true), "expired reads false");
assert_eq!(State::from_u8(state as u8), Some(state));
}
}
#[test]
fn upstream_writes() {
// ME-2: enabled false is deleted, enabled true is enabled
assert_eq!(State::from_upstream_write(false), State::Deleted);
assert_eq!(State::from_upstream_write(true), State::Enabled);
}
#[test]
fn pending_is_one_way() {
// ME-1
assert!(State::Pending.can_become(State::Pending));
assert!(State::Pending.can_become(State::Enabled));
assert!(!State::Enabled.can_become(State::Pending));
assert!(!State::Deleted.can_become(State::Pending));
assert!(State::Deleted.can_become(State::Enabled));
}
}