1use ring::hmac;
2use serde_json::Value;
3use sofie_core::encoding::{base64, hex};
4use sofie_core::{Error, Result};
5use sofie_db::{Read, Resolve, Writes};
6use sofie_http::{Delivery, Routes, Verify, Web};
7use sofie_values::Secret;
8
9use super::Suppressed;
10use crate::suppression::{self, Reason, Scope};
11
12const FRESH_SECONDS: i64 = 15 * 60;
14const UNVERIFIED: &str = "the report could not be verified";
15
16#[derive(Default)]
17pub struct Events {
18 pub postmark: Option<Secret<String>>,
20 pub mailgun: Option<Secret<String>>,
21 pub signed: Option<Secret<String>>,
24}
25
26impl Events {
27 pub fn from_environment() -> Result<Self> {
30 let read = |name| Ok::<_, Error>(sofie_core::environment_variable(name)?.map(Secret::new));
31 Ok(Self {
32 postmark: read("SOFIE_MAIL_POSTMARK_WEBHOOK_AUTH")?,
33 mailgun: read("SOFIE_MAIL_MAILGUN_SIGNING_KEY")?,
34 signed: read("SOFIE_MAIL_WEBHOOK_SECRET")?,
35 })
36 }
37
38 pub(super) fn routes(&self, routes: &mut Routes<Web>) {
39 if let Some(auth) = &self.postmark {
40 routes
41 .webhook("/mail/events/postmark", postmark)
42 .verify(Postmark(sofie_auth::digest(&[auth.expose().as_bytes()])));
43 }
44 if let Some(key) = &self.mailgun {
45 routes
46 .webhook("/mail/events/mailgun", mailgun)
47 .verify(Mailgun(hmac_key(key)));
48 }
49 if let Some(key) = &self.signed {
50 routes
51 .webhook("/mail/events/signed", signed)
52 .verify(Signed(hmac_key(key)));
53 }
54 }
55}
56
57fn hmac_key(key: &Secret<String>) -> hmac::Key {
58 hmac::Key::new(hmac::HMAC_SHA256, key.expose().as_bytes())
59}
60
61fn json(body: &[u8]) -> Result<Value> {
62 serde_json::from_slice(body).map_err(|_| Error::BadRequest("the report is not JSON".to_owned()))
63}
64
65fn unverified() -> Error {
66 Error::BadRequest(UNVERIFIED.to_owned())
67}
68
69struct Postmark(String);
71
72impl Verify for Postmark {
73 fn verify(&self, delivery: &Delivery<'_>) -> Result<String> {
74 let given = delivery
75 .header("authorization")
76 .and_then(|value| value.strip_prefix("Basic "))
77 .and_then(|encoded| base64::decode(&base64::STANDARD, encoded))
78 .ok_or_else(unverified)?;
79 if sofie_auth::digest(&[&given]) != self.0 {
80 return Err(unverified());
81 }
82 report_id(delivery.body(), "ID")
83 }
84}
85
86struct Mailgun(hmac::Key);
87
88impl Verify for Mailgun {
89 fn verify(&self, delivery: &Delivery<'_>) -> Result<String> {
90 let report = json(delivery.body())?;
91 let signature = &report["signature"];
92 let (Some(timestamp), Some(token), Some(signed)) = (
93 signature["timestamp"].as_str(),
94 signature["token"].as_str(),
95 signature["signature"].as_str().and_then(hex::decode),
96 ) else {
97 return Err(unverified());
98 };
99 let fresh = timestamp
100 .parse::<i64>()
101 .is_ok_and(|at| (delivery.now().as_second() - at).abs() <= FRESH_SECONDS);
102 let message = format!("{timestamp}{token}");
103 if !fresh || hmac::verify(&self.0, message.as_bytes(), &signed).is_err() {
104 return Err(unverified());
105 }
106 Ok(token.to_owned())
107 }
108}
109
110struct Signed(hmac::Key);
111
112impl Verify for Signed {
113 fn verify(&self, delivery: &Delivery<'_>) -> Result<String> {
114 let signed = delivery
115 .header("x-sofie-signature")
116 .and_then(hex::decode)
117 .ok_or_else(unverified)?;
118 hmac::verify(&self.0, delivery.body(), &signed).map_err(|_| unverified())?;
119 report_id(delivery.body(), "id")
120 }
121}
122
123fn report_id(body: &[u8], key: &str) -> Result<String> {
124 match &json(body)?[key] {
125 Value::String(id) if !id.is_empty() => Ok(id.clone()),
126 Value::Number(id) => Ok(id.to_string()),
127 _ => Err(Error::BadRequest(format!("the report has no `{key}`"))),
128 }
129}
130
131pub(crate) struct Report(Value);
132
133impl<C: Writes> Resolve<C> for Report {
134 fn resolve(context: &mut C) -> impl Future<Output = Result<Self>> + Send {
135 std::future::ready(context.input().map(Self))
136 }
137}
138
139async fn suppressed(
140 read: &Read,
141 address: Option<&str>,
142 scope: Option<(Scope, Reason)>,
143) -> Result<Suppressed> {
144 let (Some(address), Some((scope, reason))) = (address, scope) else {
145 return Ok(Suppressed(None));
146 };
147 suppression::suppress(read, address, scope, reason)
148 .await
149 .map(Suppressed)
150}
151
152async fn postmark(report: Report, read: Read) -> Result<Suppressed> {
154 let report = report.0;
155 let scope = match (report["RecordType"].as_str(), report["Type"].as_str()) {
156 (Some("Bounce"), Some("HardBounce" | "BadEmailAddress")) => {
157 Some((Scope::All, Reason::Bounce))
158 }
159 (Some("SpamComplaint"), _) => Some((Scope::Bulk, Reason::Complaint)),
160 _ => None,
161 };
162 suppressed(&read, report["Email"].as_str(), scope).await
163}
164
165async fn mailgun(report: Report, read: Read) -> Result<Suppressed> {
166 let event = &report.0["event-data"];
167 let scope = match (event["event"].as_str(), event["severity"].as_str()) {
168 (Some("failed"), Some("permanent")) => Some((Scope::All, Reason::Bounce)),
169 (Some("complained"), _) => Some((Scope::Bulk, Reason::Complaint)),
170 _ => None,
171 };
172 suppressed(&read, event["recipient"].as_str(), scope).await
173}
174
175async fn signed(report: Report, read: Read) -> Result<Suppressed> {
176 let report = report.0;
177 let scope = match report["event"].as_str() {
178 Some("bounce") => Some((Scope::All, Reason::Bounce)),
179 Some("complaint") => Some((Scope::Bulk, Reason::Complaint)),
180 _ => None,
181 };
182 suppressed(&read, report["email"].as_str(), scope).await
183}