use crate::error::ServerError;
use crate::provision::{read_ledger, LedgerEntry, NodeStatus};
use mail4agent_api::{Address, MessageId, ParticipantId, RoomId, SendRequest};
use mail4agent_core::{MailboxEngine, ParticipantPermissions};
use mail4agent_store_sqlite::{migrations, Db, DbConfig, MigrationRunner, SqliteMailStore};
use serde::Serialize;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
pub const OPERATOR: &str = "hatchery.operator";
fn unix_ms() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis() as u64
}
#[derive(Debug, thiserror::Error)]
pub enum MessengerError {
#[error("not permitted")]
Forbidden,
#[error("{0}")]
Bad(String),
#[error("{0}")]
Internal(String),
}
#[derive(Serialize)]
pub struct MailView {
pub message_id: String,
pub from: String,
pub to: String,
pub subject: String,
pub body: String,
pub created_at_ms: u64,
}
pub struct Messenger {
engine: MailboxEngine<SqliteMailStore>,
data_dir: PathBuf,
registered: BTreeSet<String>,
memberships: std::collections::BTreeMap<String, BTreeSet<String>>,
stamp: Option<(std::time::SystemTime, u64)>,
}
fn pid(id: &str) -> Result<ParticipantId, MessengerError> {
ParticipantId::new(id).map_err(|e| MessengerError::Bad(e.to_string()))
}
fn mail_allowed(entry: &LedgerEntry) -> bool {
matches!(entry.status, NodeStatus::Member | NodeStatus::Trusted)
&& entry.grants.mailbox
&& entry.grants.capabilities.iter().any(|c| c == "mail")
}
impl Messenger {
pub fn open(data_dir: &Path) -> Result<Self, ServerError> {
let dir = data_dir.join("messenger");
std::fs::create_dir_all(&dir)?;
let db = Db::open(&DbConfig::new(dir.join("mail.db"))).map_err(|e| ServerError::sub("messenger", format!("open: {e}")))?;
db.run_migrations_blocking(MigrationRunner::new(migrations()))
.map_err(|e| ServerError::sub("messenger", format!("migrate: {e}")))?;
let mut me = Self {
engine: MailboxEngine::new(SqliteMailStore::new(db)),
data_dir: data_dir.to_owned(),
registered: BTreeSet::new(),
memberships: Default::default(),
stamp: None,
};
let operator = pid(OPERATOR).map_err(|e| ServerError::sub("messenger", e.to_string()))?;
let _ = me.engine.register_participant(operator, Some("operator".into()), ParticipantPermissions { may_send: true, may_read: true, operator: true });
Ok(me)
}
pub fn sync(&mut self) -> Result<(), MessengerError> {
let path = self.data_dir.join("provision").join("ledger.json");
let stamp = std::fs::metadata(&path).ok().and_then(|m| m.modified().ok().map(|t| (t, m.len())));
if stamp.is_some() && stamp == self.stamp {
return Ok(());
}
let entries = read_ledger(&self.data_dir).map_err(|e| MessengerError::Internal(e.to_string()))?;
let mut rooms_seen = BTreeSet::new();
for entry in &entries {
let allowed = mail_allowed(entry) && entry.node_id != OPERATOR;
let id = pid(&entry.node_id)?;
if allowed {
if !self.registered.contains(&entry.node_id) {
let _ = self.engine.register_participant(id.clone(), Some(entry.node_id.clone()), ParticipantPermissions { may_send: true, may_read: true, operator: false });
self.registered.insert(entry.node_id.clone());
}
let want: BTreeSet<String> = entry.grants.rooms.iter().cloned().collect();
let have = self.memberships.entry(entry.node_id.clone()).or_default().clone();
for room in want.difference(&have) {
let rid = RoomId::new(room).map_err(|e| MessengerError::Bad(e.to_string()))?;
let _ = self.engine.create_room(rid.clone(), unix_ms());
self.engine.add_room_member(&rid, id.clone()).map_err(|e| MessengerError::Internal(e.to_string()))?;
}
for room in have.difference(&want) {
let rid = RoomId::new(room).map_err(|e| MessengerError::Bad(e.to_string()))?;
let _ = self.engine.remove_room_member(&rid, &id);
}
rooms_seen.extend(want.iter().cloned());
self.memberships.insert(entry.node_id.clone(), want);
} else {
let _ = self.engine.deregister_participant(&id);
self.registered.remove(&entry.node_id);
if let Some(rooms) = self.memberships.remove(&entry.node_id) {
for room in rooms {
if let Ok(rid) = RoomId::new(&room) {
let _ = self.engine.remove_room_member(&rid, &id);
}
}
}
}
}
self.stamp = stamp;
Ok(())
}
fn address(&mut self, who: &str) -> Result<Address, MessengerError> {
self.sync()?;
if who != OPERATOR && !self.registered.contains(who) {
return Err(MessengerError::Forbidden);
}
Ok(Address::Direct { participant: pid(who)? })
}
pub fn send(&mut self, from: &str, to: &str, subject: &str, body: &str, idempotency_key: Option<String>) -> Result<String, MessengerError> {
let sender = self.address(from)?;
let to = if let Some(room) = to.strip_prefix("room:") {
Address::Room { room: RoomId::new(room).map_err(|e| MessengerError::Bad(e.to_string()))? }
} else {
Address::Direct { participant: pid(to)? }
};
let request = SendRequest { to, subject: subject.to_owned(), body: body.to_owned(), reply_to: None, correlation: None, refs: Vec::new(), idempotency_key };
let response = self.engine.send(&sender, request, unix_ms()).map_err(map_mail)?;
Ok(response.message_id.as_str().to_owned())
}
pub fn inbox(&mut self, who: &str, since_ms: u64, limit: u16) -> Result<Vec<MailView>, MessengerError> {
let reader = self.address(who)?;
let page = self.engine.inbox(&reader, since_ms, limit.clamp(1, 256)).map_err(map_mail)?;
Ok(page
.messages
.into_iter()
.map(|m| MailView {
message_id: m.message_id.as_str().to_owned(),
from: addr(&m.from),
to: addr(&m.to),
subject: m.subject,
body: m.body,
created_at_ms: m.created_at_unix_ms,
})
.collect())
}
pub fn thread(&mut self, node: &str, limit: u16) -> Result<Vec<MailView>, MessengerError> {
self.sync()?;
if !self.registered.contains(node) {
return Ok(Vec::new());
}
let operator = Address::Direct { participant: pid(OPERATOR)? };
let target = Address::Direct { participant: pid(node)? };
let to_op = self.engine.inbox(&operator, 0, 256).map_err(map_mail)?;
let to_node = self.engine.inbox_of(&operator, &target, 0, 256).map_err(map_mail)?;
let mut all: Vec<MailView> = to_op
.messages
.into_iter()
.filter(|m| addr(&m.from) == node)
.chain(to_node.messages.into_iter().filter(|m| addr(&m.from) == OPERATOR))
.map(|m| MailView { message_id: m.message_id.as_str().to_owned(), from: addr(&m.from), to: addr(&m.to), subject: m.subject, body: m.body, created_at_ms: m.created_at_unix_ms })
.collect();
all.sort_by_key(|m| (m.created_at_ms, m.message_id.clone()));
let skip = all.len().saturating_sub(limit.clamp(1, 256) as usize);
Ok(all.split_off(skip))
}
pub fn ack(&mut self, who: &str, message_id: &str) -> Result<(), MessengerError> {
let reader = self.address(who)?;
let id = MessageId::new(message_id).map_err(|e| MessengerError::Bad(e.to_string()))?;
self.engine.ack(&reader, &id, unix_ms()).map(|_| ()).map_err(map_mail)
}
pub fn ensure_session_participant(&mut self, nick: &str) -> Result<(), MessengerError> {
if self.registered.contains(nick) {
return Ok(());
}
let id = pid(nick)?;
let _ = self.engine.register_participant(id, Some(nick.to_owned()), ParticipantPermissions { may_send: true, may_read: true, operator: false });
self.registered.insert(nick.to_owned());
Ok(())
}
pub fn is_registered(&mut self, who: &str) -> bool {
self.sync().is_ok() && self.registered.contains(who)
}
}
fn addr(a: &Address) -> String {
match a {
Address::Direct { participant } => participant.as_str().to_owned(),
Address::Session { participant, session } => format!("{}/{}", participant.as_str(), session.as_str()),
Address::Room { room } => format!("room:{}", room.as_str()),
}
}
fn map_mail(error: mail4agent_api::MailError) -> MessengerError {
use mail4agent_api::MailError as E;
match error {
E::PermissionDenied { .. } | E::UnknownParticipant { .. } | E::UnknownRoom { .. } => MessengerError::Forbidden,
other => MessengerError::Bad(other.to_string()),
}
}