feat: update email logic to subject not user +

This commit is contained in:
Jet 2026-03-25 23:25:49 -07:00
parent ede986080a
commit 6a652ed4f3
No known key found for this signature in database
5 changed files with 136 additions and 27 deletions

View file

@ -101,9 +101,16 @@ pub async fn post_question(
let notify_email = state.notify_email.clone();
let mail_domain = state.mail_domain.clone();
let qa_reply_domain = state.qa_reply_domain.clone();
let question_text = body.question.clone();
tokio::task::spawn_blocking(move || {
if let Err(e) = email::send_notification(id, &question_text, &notify_email, &mail_domain) {
if let Err(e) = email::send_notification(
id,
&question_text,
&notify_email,
&mail_domain,
&qa_reply_domain,
) {
eprintln!("Failed to send notification: {e}");
}
});
@ -163,10 +170,20 @@ impl Recipient {
#[derive(Deserialize, Default)]
pub struct MtaHookBody {
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub headers: MessageHeaders,
#[serde(default)]
pub contents: String,
}
#[derive(Deserialize, Default)]
pub struct MessageHeaders {
#[serde(default)]
pub subject: Option<String>,
}
#[derive(Serialize)]
pub struct MtaHookResponse {
pub action: &'static str,
@ -209,11 +226,13 @@ fn webhook_secret_matches(headers: &HeaderMap, expected_secret: &str) -> bool {
password == expected_secret
}
fn extract_qa_reply(payload: &MtaHookPayload) -> Option<(i64, String)> {
fn extract_qa_reply(payload: &MtaHookPayload, expected_domain: &str) -> Option<(i64, String)> {
if !payload.messages.is_empty() {
for message in &payload.messages {
if let Some(reply) = extract_qa_reply_from_message(
&message.envelope.to,
expected_domain,
message.message.subject.as_deref().or(message.message.headers.subject.as_deref()),
if message.message.contents.is_empty() {
&message.contents
} else {
@ -226,20 +245,41 @@ fn extract_qa_reply(payload: &MtaHookPayload) -> Option<(i64, String)> {
return None;
}
extract_qa_reply_from_message(&payload.envelope.to, &payload.message.contents)
extract_qa_reply_from_message(
&payload.envelope.to,
expected_domain,
payload
.message
.subject
.as_deref()
.or(payload.message.headers.subject.as_deref()),
&payload.message.contents,
)
}
fn extract_qa_reply_from_message(
recipients: &[Recipient],
expected_domain: &str,
subject: Option<&str>,
contents: &str,
) -> Option<(i64, String)> {
let qa_recipient = recipients.iter().find(|recipient| {
let local = recipient.address().split('@').next().unwrap_or("");
local.starts_with("qa+")
let _qa_recipient = recipients.iter().find(|recipient| {
let address = recipient.address();
let Some((local, domain)) = address.rsplit_once('@') else {
return false;
};
local.eq_ignore_ascii_case("qa") && domain.eq_ignore_ascii_case(expected_domain)
})?;
let id = email::extract_id_from_address(qa_recipient.address()).ok()?;
let body = email::strip_quoted_text(contents);
let subject = subject.map(ToOwned::to_owned).or_else(|| {
contents
.replace("\r\n", "\n")
.lines()
.find_map(|line| line.strip_prefix("Subject: ").map(ToOwned::to_owned))
})?;
let id = email::extract_id_from_subject(&subject).ok()?;
let body = email::extract_plain_text_body(contents);
if body.is_empty() {
return None;
}
@ -257,7 +297,7 @@ pub async fn webhook(
return Err((StatusCode::UNAUTHORIZED, "invalid secret".to_string()));
}
if let Some((id, body)) = extract_qa_reply(&payload) {
if let Some((id, body)) = extract_qa_reply(&payload, &state.qa_reply_domain) {
let db = state
.db
.lock()
@ -290,11 +330,12 @@ mod tests {
"envelope": {
"to": [
{
"address": "qa+42@extremist.software"
"address": "qa@extremist.software"
}
]
},
"message": {
"subject": "Re: 42 - hello",
"contents": "This is the answer.\n\nOn earlier mail wrote:\n> quoted"
}
}"#,
@ -302,7 +343,7 @@ mod tests {
.unwrap();
assert_eq!(
extract_qa_reply(&payload),
extract_qa_reply(&payload, "extremist.software"),
Some((42, "This is the answer.".to_string()))
);
}
@ -314,7 +355,10 @@ mod tests {
"messages": [
{
"envelope": {
"to": ["qa+7@extremist.software"]
"to": ["qa@extremist.software"]
},
"message": {
"subject": "Re: 7 - legacy"
},
"contents": "Legacy answer"
}
@ -324,7 +368,7 @@ mod tests {
.unwrap();
assert_eq!(
extract_qa_reply(&payload),
extract_qa_reply(&payload, "extremist.software"),
Some((7, "Legacy answer".to_string()))
);
}