Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::auth::oauth::FormData;
|
||||
use chrono::Utc;
|
||||
use common::{
|
||||
KV_RATE_LIMIT_CONTACT, Server,
|
||||
config::network::{ContactForm, FieldOrDefault},
|
||||
network::ip_to_bytes,
|
||||
psl,
|
||||
};
|
||||
use email::message::delivery::{IngestMessage, IngestRecipient, LocalDeliveryStatus, MailDelivery};
|
||||
use http_proto::*;
|
||||
use hyper::StatusCode;
|
||||
use mail_auth::common::cache::NoCache;
|
||||
use mail_builder::{
|
||||
MessageBuilder,
|
||||
headers::{
|
||||
HeaderType,
|
||||
address::{Address, EmailAddress},
|
||||
},
|
||||
mime::make_boundary,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, fmt::Write, future::Future};
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
|
||||
pub trait FormHandler: Sync + Send {
|
||||
fn handle_contact_form(
|
||||
&self,
|
||||
session: &HttpSessionData,
|
||||
form: &ContactForm,
|
||||
form_data: FormData,
|
||||
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl FormHandler for Server {
|
||||
async fn handle_contact_form(
|
||||
&self,
|
||||
session: &HttpSessionData,
|
||||
form: &ContactForm,
|
||||
form_data: FormData,
|
||||
) -> trc::Result<HttpResponse> {
|
||||
// Validate rate
|
||||
if let Some(rate) = &form.rate
|
||||
&& !session.remote_ip.is_loopback()
|
||||
&& self
|
||||
.in_memory_store()
|
||||
.is_rate_allowed(
|
||||
KV_RATE_LIMIT_CONTACT,
|
||||
&ip_to_bytes(&session.remote_ip),
|
||||
rate,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.is_some()
|
||||
{
|
||||
return Err(trc::LimitEvent::TooManyRequests.into_err());
|
||||
}
|
||||
|
||||
// Validate honeypot
|
||||
if form
|
||||
.field_honey_pot
|
||||
.as_ref()
|
||||
.is_some_and(|field| form_data.has_field(field))
|
||||
{
|
||||
return Err(trc::ResourceEvent::BadParameters
|
||||
.into_err()
|
||||
.details("Honey pot field present"));
|
||||
}
|
||||
|
||||
// Obtain fields
|
||||
let from_email = form_data
|
||||
.get_or_default(&form.from_email)
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
let from_subject = form_data.get_or_default(&form.from_subject).trim();
|
||||
let from_name = form_data.get_or_default(&form.from_name).trim();
|
||||
|
||||
// Validate email
|
||||
let mut failure = None;
|
||||
let mut has_success = false;
|
||||
if form.validate_domain && from_email != form.from_email.default {
|
||||
if let Some(domain) = from_email.rsplit_once('@').and_then(|(local, domain)| {
|
||||
if !local.is_empty()
|
||||
&& domain.contains('.')
|
||||
&& psl::domain(domain.as_bytes()).is_some_and(|d| d.suffix().typ().is_some())
|
||||
{
|
||||
Some(domain)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if self
|
||||
.core
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.mx_lookup(domain, None::<&NoCache<_, _>>)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
failure = Some(format!("No MX records found for domain {domain:?}. Please enter a valid email address.", ).into());
|
||||
}
|
||||
} else {
|
||||
failure = Some(Cow::Borrowed("Please enter a valid email address."));
|
||||
}
|
||||
}
|
||||
|
||||
// Discard empty forms
|
||||
if failure.is_none() && form_data.fields().all(|(_, value)| value.trim().is_empty()) {
|
||||
failure = Some(Cow::Borrowed("Empty form"));
|
||||
}
|
||||
|
||||
if failure.is_none() {
|
||||
// Build body
|
||||
let mut body = String::with_capacity(1024);
|
||||
for (field, value) in form_data.fields() {
|
||||
if !value.is_empty() {
|
||||
body.push_str(field);
|
||||
body.push_str(": ");
|
||||
body.push_str(value);
|
||||
body.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
let _ = write!(
|
||||
&mut body,
|
||||
"Date: {}\r\n",
|
||||
Utc::now().format("%a, %d %b %Y %T %z")
|
||||
);
|
||||
let _ = write!(
|
||||
&mut body,
|
||||
"IP: {}:{}\r\n",
|
||||
session.remote_ip, session.remote_port
|
||||
);
|
||||
|
||||
// Build message
|
||||
let message = MessageBuilder::new()
|
||||
.from((from_name, from_email.as_str()))
|
||||
.header(
|
||||
"To",
|
||||
HeaderType::Address(Address::List(
|
||||
form.rcpt_to
|
||||
.iter()
|
||||
.map(|rcpt| {
|
||||
Address::Address(EmailAddress {
|
||||
name: None,
|
||||
email: rcpt.into(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
)
|
||||
.header("Auto-Submitted", HeaderType::Text("auto-generated".into()))
|
||||
.message_id(format!(
|
||||
"{}@{}",
|
||||
make_boundary("."),
|
||||
self.core.network.server_name
|
||||
))
|
||||
.subject(from_subject)
|
||||
.text_body(body)
|
||||
.write_to_vec()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Reserve and write blob
|
||||
let (message_blob, blob_hold) = self
|
||||
.put_temporary_blob(u32::MAX, &message, 60)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for result in self
|
||||
.deliver_message(IngestMessage {
|
||||
sender_address: from_email,
|
||||
sender_authenticated: false,
|
||||
recipients: form
|
||||
.rcpt_to
|
||||
.iter()
|
||||
.map(|address| IngestRecipient {
|
||||
address: address.clone(),
|
||||
orcpt: None,
|
||||
spam_percentage: None,
|
||||
})
|
||||
.collect(),
|
||||
message_blob,
|
||||
message_size: message.len() as u64,
|
||||
session_id: session.session_id,
|
||||
})
|
||||
.await
|
||||
.status
|
||||
{
|
||||
match result {
|
||||
LocalDeliveryStatus::Success => {
|
||||
has_success = true;
|
||||
}
|
||||
LocalDeliveryStatus::TemporaryFailure { reason }
|
||||
| LocalDeliveryStatus::PermanentFailure { reason, .. } => {
|
||||
failure = Some(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove blob hold
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.clear(blob_hold);
|
||||
self.store()
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Suppress errors if there is at least one success
|
||||
if has_success {
|
||||
failure = None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(JsonResponse::with_status(
|
||||
if has_success {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::BAD_REQUEST
|
||||
},
|
||||
json!({
|
||||
"data": {
|
||||
"success": has_success,
|
||||
"details": failure,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.into_http_response())
|
||||
}
|
||||
}
|
||||
|
||||
impl FormData {
|
||||
pub fn get_or_default<'x>(&'x self, field: &'x FieldOrDefault) -> &'x str {
|
||||
if let Some(field_name) = &field.field {
|
||||
self.get(field_name)
|
||||
.filter(|f| !f.is_empty())
|
||||
.unwrap_or(field.default.as_str())
|
||||
} else {
|
||||
field.default.as_str()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user