#![forbid(unsafe_code)]
mod handlers;
pub use crate::handlers::{EVENT_INBOUND, EVENTS};
use cratefield_core::{
AnyError, BoxFuture, Config, ConfigError, Migrations, Module, ModuleConfig, ModuleContext,
PersonalDataSet, Port, SignatureVerification, SqlMigration,
};
use std::sync::Arc;
pub const MODULE_NAME: &str = "owlpost";
pub const SECRET_KEY: &str = "OWLPOST_WEBHOOK_SECRET";
const SECRET_KEY_SUFFIX: &str = "WEBHOOK_SECRET";
const MIGRATION_INIT: SqlMigration = SqlMigration::new(
"0001",
"init",
include_str!("../migrations/sqlite/0001_init.sql"),
);
pub type InboundHook =
Arc<dyn Fn(InboundMail) -> BoxFuture<'static, Result<(), AnyError>> + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct InboundMail {
pub message_id: String,
#[serde(default)]
pub from: String,
#[serde(default)]
pub to: Vec<String>,
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub held: bool,
#[serde(default)]
pub reason: Option<String>,
}
impl InboundMail {
pub(crate) fn received(data: &cratefield_adapter_owlpost::webhook::InboundData) -> Self {
Self {
message_id: data.message_id.clone(),
from: data.from.clone(),
to: data.to.clone(),
subject: data.subject.clone(),
held: false,
reason: None,
}
}
pub(crate) fn held(data: &cratefield_adapter_owlpost::webhook::HeldData) -> Self {
Self {
message_id: data.message_id.clone(),
from: String::new(),
to: Vec::new(),
subject: None,
held: true,
reason: data.reason.clone(),
}
}
}
#[derive(Clone, Default)]
pub struct OwlpostEvents {
inbound: Option<InboundHook>,
}
impl std::fmt::Debug for OwlpostEvents {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwlpostEvents")
.field("on_inbound", &self.inbound.is_some())
.finish()
}
}
impl OwlpostEvents {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn on_inbound(
mut self,
hook: impl Fn(InboundMail) -> BoxFuture<'static, Result<(), AnyError>> + Send + Sync + 'static,
) -> Self {
self.inbound = Some(Arc::new(hook));
self
}
}
#[derive(Debug, Default, Clone)]
pub struct Owlpost {
events: OwlpostEvents,
}
impl Owlpost {
#[must_use]
pub fn new(events: OwlpostEvents) -> Self {
Self { events }
}
}
impl Module for Owlpost {
fn name(&self) -> &'static str {
MODULE_NAME
}
fn version(&self) -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn requires(&self) -> &'static [Port] {
&[Port::Db, Port::Clock]
}
fn tables(&self) -> &'static [&'static str] {
&["owlpost_inbox"]
}
fn personal_data(&self) -> &'static [PersonalDataSet] {
const SETS: &[PersonalDataSet] = &[PersonalDataSet::none(
"owlpost_inbox",
"The ids of the mail events this venture has already processed — a dedup \
ledger, so a provider that delivers twice is processed once. It holds event \
ids and timestamps and nothing about a person.",
)];
SETS
}
fn emits(&self) -> &'static [&'static str] {
handlers::EVENTS
}
fn signature_verification(&self) -> SignatureVerification {
SignatureVerification::Hmac {
secret: SECRET_KEY_SUFFIX,
}
}
fn migrations(&self) -> Migrations {
const MIGRATIONS: [SqlMigration; 1] = [MIGRATION_INIT];
const _: () = cratefield_core::assert_migration_set(&MIGRATIONS);
Migrations {
sqlite: &MIGRATIONS,
postgres: &[],
}
}
fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError> {
let module = ModuleConfig::new(MODULE_NAME, cfg);
let mut errors = ConfigError::default();
if let Some(secret) = cfg.get(&module.key(SECRET_KEY_SUFFIX))
&& secret.trim().is_empty()
{
errors.push(format!(
"owlpost: {SECRET_KEY} is set but blank; unset it or give the real signing \
secret"
));
}
errors.into_result()
}
fn router(&self, ctx: ModuleContext) -> axum::Router {
handlers::router(Arc::new(ctx), self.events.clone())
}
fn surface(&self) -> cratefield_core::Surface {
handlers::surface()
}
}