cratefield_module_owlpost/
lib.rs1#![forbid(unsafe_code)]
20
21mod handlers;
22
23pub use crate::handlers::{EVENT_INBOUND, EVENTS};
24
25use cratefield_core::{
26 AnyError, BoxFuture, Config, ConfigError, Migrations, Module, ModuleConfig, ModuleContext,
27 PersonalDataSet, Port, SignatureVerification, SqlMigration,
28};
29use std::sync::Arc;
30
31pub const MODULE_NAME: &str = "owlpost";
34
35pub const SECRET_KEY: &str = "OWLPOST_WEBHOOK_SECRET";
38
39const SECRET_KEY_SUFFIX: &str = "WEBHOOK_SECRET";
43
44const MIGRATION_INIT: SqlMigration = SqlMigration::new(
46 "0001",
47 "init",
48 include_str!("../migrations/sqlite/0001_init.sql"),
49);
50
51pub type InboundHook =
55 Arc<dyn Fn(InboundMail) -> BoxFuture<'static, Result<(), AnyError>> + Send + Sync>;
56
57#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62pub struct InboundMail {
63 pub message_id: String,
65 #[serde(default)]
67 pub from: String,
68 #[serde(default)]
70 pub to: Vec<String>,
71 #[serde(default)]
73 pub subject: Option<String>,
74 #[serde(default)]
76 pub held: bool,
77 #[serde(default)]
79 pub reason: Option<String>,
80}
81
82impl InboundMail {
83 pub(crate) fn received(data: &cratefield_adapter_owlpost::webhook::InboundData) -> Self {
84 Self {
85 message_id: data.message_id.clone(),
86 from: data.from.clone(),
87 to: data.to.clone(),
88 subject: data.subject.clone(),
89 held: false,
90 reason: None,
91 }
92 }
93
94 pub(crate) fn held(data: &cratefield_adapter_owlpost::webhook::HeldData) -> Self {
97 Self {
98 message_id: data.message_id.clone(),
99 from: String::new(),
100 to: Vec::new(),
101 subject: None,
102 held: true,
103 reason: data.reason.clone(),
104 }
105 }
106}
107
108#[derive(Clone, Default)]
110pub struct OwlpostEvents {
111 inbound: Option<InboundHook>,
112}
113
114impl std::fmt::Debug for OwlpostEvents {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("OwlpostEvents")
119 .field("on_inbound", &self.inbound.is_some())
120 .finish()
121 }
122}
123
124impl OwlpostEvents {
125 #[must_use]
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 #[must_use]
134 pub fn on_inbound(
135 mut self,
136 hook: impl Fn(InboundMail) -> BoxFuture<'static, Result<(), AnyError>> + Send + Sync + 'static,
137 ) -> Self {
138 self.inbound = Some(Arc::new(hook));
139 self
140 }
141}
142
143#[derive(Debug, Default, Clone)]
145pub struct Owlpost {
146 events: OwlpostEvents,
147}
148
149impl Owlpost {
150 #[must_use]
153 pub fn new(events: OwlpostEvents) -> Self {
154 Self { events }
155 }
156}
157
158impl Module for Owlpost {
159 fn name(&self) -> &'static str {
160 MODULE_NAME
161 }
162
163 fn version(&self) -> &'static str {
164 env!("CARGO_PKG_VERSION")
165 }
166
167 fn requires(&self) -> &'static [Port] {
168 &[Port::Db, Port::Clock]
170 }
171
172 fn tables(&self) -> &'static [&'static str] {
173 &["owlpost_inbox"]
174 }
175
176 fn personal_data(&self) -> &'static [PersonalDataSet] {
177 const SETS: &[PersonalDataSet] = &[PersonalDataSet::none(
178 "owlpost_inbox",
179 "The ids of the mail events this venture has already processed — a dedup \
180 ledger, so a provider that delivers twice is processed once. It holds event \
181 ids and timestamps and nothing about a person.",
182 )];
183 SETS
184 }
185
186 fn emits(&self) -> &'static [&'static str] {
187 handlers::EVENTS
188 }
189
190 fn signature_verification(&self) -> SignatureVerification {
192 SignatureVerification::Hmac {
193 secret: SECRET_KEY_SUFFIX,
194 }
195 }
196
197 fn migrations(&self) -> Migrations {
198 const MIGRATIONS: [SqlMigration; 1] = [MIGRATION_INIT];
199 const _: () = cratefield_core::assert_migration_set(&MIGRATIONS);
200 Migrations {
201 sqlite: &MIGRATIONS,
202 postgres: &[],
203 }
204 }
205
206 fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError> {
207 let module = ModuleConfig::new(MODULE_NAME, cfg);
211 let mut errors = ConfigError::default();
212 if let Some(secret) = cfg.get(&module.key(SECRET_KEY_SUFFIX))
213 && secret.trim().is_empty()
214 {
215 errors.push(format!(
216 "owlpost: {SECRET_KEY} is set but blank; unset it or give the real signing \
217 secret"
218 ));
219 }
220 errors.into_result()
221 }
222
223 fn router(&self, ctx: ModuleContext) -> axum::Router {
224 handlers::router(Arc::new(ctx), self.events.clone())
225 }
226
227 fn surface(&self) -> cratefield_core::Surface {
228 handlers::surface()
229 }
230}