Turning legacy mail protocols off or back on raises security.legacy-protocols-changed (id 643, info level, also in the packaged schema), with the scope (policy = server), the new value, who made the change (accountId), whether listeners closed or reopened (details), which ones (listenerId), and -- only when a listener could not be put back -- which and why (reason). It is raised in Server::set_protocol_policy rather than by the JMAP method, so whatever turns the switch is reported. A /set that changes nothing -- the switch already where it was asked to be, nothing to close or reopen -- is not a change and raises nothing. The event is never an error, but jmap's exhaustive map from security events to HTTP errors has to name it; it joins the other two that can't occur there. rustfmt now also wraps LP-6's two over-long lines in enums_impl.rs, which it flagged along with this change's. tests/e2e/legacy_protocols.py now gives the server a stdout tracer and reads events from the container's log: turning the switch off is exactly one event naming the scope, value, author and listeners closed; setting it off again raises none; turning it on is one event naming the listeners reopened. It also proves LP-6's side: seven refused submission sign-ins are seven auth.legacy-protocol-refused events, and there is no auth.failed or auth.too-many-attempts among them. All checks pass.
264 lines
11 KiB
Rust
264 lines
11 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use crate::blob::UploadResponse;
|
|
use calcard::jscalendar::JSCalendarDateTime;
|
|
use common::ipc::{CalendarAlert, PushNotification};
|
|
use http_proto::{HttpResponse, JsonResponse, ToHttpResponse};
|
|
use hyper::StatusCode;
|
|
use jmap_proto::{
|
|
error::request::{RateLimitPolicy, RateLimitUnit, RequestError, RequestLimitError},
|
|
request::capability::Session,
|
|
response::{Response, status::PushObject},
|
|
types::state::State,
|
|
};
|
|
use types::{id::Id, type_state::DataType};
|
|
use utils::map::vec_map::VecMap;
|
|
|
|
pub mod acl;
|
|
pub mod auth;
|
|
pub mod event_source;
|
|
pub mod query;
|
|
pub mod request;
|
|
pub mod session;
|
|
|
|
impl ToHttpResponse for UploadResponse {
|
|
fn into_http_response(self) -> HttpResponse {
|
|
JsonResponse::new(self).into_http_response()
|
|
}
|
|
}
|
|
|
|
pub trait ToJmapHttpResponse {
|
|
fn into_http_response(self) -> HttpResponse;
|
|
}
|
|
|
|
impl ToJmapHttpResponse for Response<'_> {
|
|
fn into_http_response(self) -> HttpResponse {
|
|
JsonResponse::new(self).into_http_response()
|
|
}
|
|
}
|
|
|
|
impl ToJmapHttpResponse for Session {
|
|
fn into_http_response(self) -> HttpResponse {
|
|
JsonResponse::new(self).into_http_response()
|
|
}
|
|
}
|
|
|
|
impl ToJmapHttpResponse for RequestError<'_> {
|
|
fn into_http_response(self) -> HttpResponse {
|
|
let mut response =
|
|
HttpResponse::new(StatusCode::from_u16(self.status).unwrap_or(StatusCode::BAD_REQUEST));
|
|
if let Some(retry_after) = self.retry_after {
|
|
response = response.with_header("Retry-After", retry_after.to_string());
|
|
}
|
|
if let Some(policy) = self.rate_limit_policy_header() {
|
|
response = response.with_header("RateLimit-Policy", policy);
|
|
}
|
|
if let Some(state) = self.rate_limit_state_header() {
|
|
response = response.with_header("RateLimit", state);
|
|
}
|
|
response
|
|
.with_content_type("application/problem+json")
|
|
.with_text_body(serde_json::to_string(&self).unwrap_or_default())
|
|
}
|
|
}
|
|
|
|
pub trait ToRequestError {
|
|
fn to_request_error(&self) -> RequestError<'_>;
|
|
}
|
|
|
|
impl ToRequestError for trc::Error {
|
|
fn to_request_error(&self) -> RequestError<'_> {
|
|
let details_or_reason = self
|
|
.value(trc::Key::Details)
|
|
.or_else(|| self.value(trc::Key::Reason))
|
|
.and_then(|v| v.as_str());
|
|
let details = details_or_reason.unwrap_or_else(|| self.as_ref().message());
|
|
|
|
match self.as_ref() {
|
|
trc::EventType::Jmap(cause) => match cause {
|
|
trc::JmapEvent::UnknownCapability => RequestError::unknown_capability(details),
|
|
trc::JmapEvent::NotJson => RequestError::not_json(details),
|
|
trc::JmapEvent::NotRequest => RequestError::not_request(details),
|
|
_ => RequestError::invalid_parameters(),
|
|
},
|
|
trc::EventType::Limit(cause) => {
|
|
let reset = self.value(trc::Key::Expires).and_then(|v| v.to_uint());
|
|
let limit = self.value(trc::Key::Limit).and_then(|v| v.to_uint());
|
|
let total = self.value(trc::Key::Total).and_then(|v| v.to_uint());
|
|
let size = self.value(trc::Key::Size).and_then(|v| v.to_uint());
|
|
|
|
match cause {
|
|
trc::LimitEvent::SizeRequest => {
|
|
RequestError::limit(RequestLimitError::SizeRequest)
|
|
}
|
|
trc::LimitEvent::SizeUpload => {
|
|
RequestError::limit(RequestLimitError::SizeUpload)
|
|
}
|
|
trc::LimitEvent::CallsIn => RequestError::limit(RequestLimitError::CallsIn),
|
|
trc::LimitEvent::ConcurrentRequest | trc::LimitEvent::ConcurrentConnection => {
|
|
let mut policy =
|
|
RateLimitPolicy::new("concurrent-requests", limit.unwrap_or(0))
|
|
.with_unit(RateLimitUnit::ConcurrentRequests);
|
|
if let Some(reset) = reset {
|
|
policy = policy.with_reset(reset);
|
|
}
|
|
RequestError::limit(RequestLimitError::ConcurrentRequest)
|
|
.with_rate_limit(policy)
|
|
}
|
|
trc::LimitEvent::ConcurrentUpload => {
|
|
let mut policy =
|
|
RateLimitPolicy::new("concurrent-uploads", limit.unwrap_or(0))
|
|
.with_unit(RateLimitUnit::ConcurrentRequests);
|
|
if let Some(reset) = reset {
|
|
policy = policy.with_reset(reset);
|
|
}
|
|
RequestError::limit(RequestLimitError::ConcurrentUpload)
|
|
.with_rate_limit(policy)
|
|
}
|
|
trc::LimitEvent::Quota => RequestError::over_quota(),
|
|
trc::LimitEvent::TenantQuota => RequestError::tenant_over_quota(),
|
|
trc::LimitEvent::BlobQuota => {
|
|
let mut err = RequestError::over_blob_quota(
|
|
total.unwrap_or(0) as usize,
|
|
size.unwrap_or(0) as usize,
|
|
);
|
|
if let Some(total) = total {
|
|
let mut policy = RateLimitPolicy::new("blob-upload-files", total);
|
|
if let Some(reset) = reset {
|
|
policy = policy.with_reset(reset);
|
|
}
|
|
err = err.with_rate_limit(policy);
|
|
}
|
|
if let Some(size) = size {
|
|
let mut policy = RateLimitPolicy::new("blob-upload-bytes", size)
|
|
.with_unit(RateLimitUnit::ContentBytes);
|
|
if let Some(reset) = reset {
|
|
policy = policy.with_reset(reset);
|
|
}
|
|
err = err.with_rate_limit(policy);
|
|
}
|
|
err
|
|
}
|
|
trc::LimitEvent::TooManyRequests => {
|
|
let mut err = RequestError::too_many_requests();
|
|
if let Some(limit) = limit {
|
|
let mut policy = RateLimitPolicy::new("requests", limit);
|
|
if let Some(reset) = reset {
|
|
policy = policy.with_reset(reset);
|
|
}
|
|
err = err.with_rate_limit(policy);
|
|
} else if let Some(reset) = reset {
|
|
err = err.with_retry_after(reset);
|
|
}
|
|
err
|
|
}
|
|
}
|
|
}
|
|
trc::EventType::Auth(cause) => match cause {
|
|
trc::AuthEvent::MfaRequired => {
|
|
RequestError::blank(402, "MFA code required", self.as_ref().message())
|
|
}
|
|
trc::AuthEvent::TooManyAttempts => {
|
|
let mut err = RequestError::too_many_auth_attempts();
|
|
if let Some(reset) = self.value(trc::Key::Expires).and_then(|v| v.to_uint()) {
|
|
err = err.with_retry_after(reset);
|
|
}
|
|
err
|
|
}
|
|
_ => RequestError::unauthorized(),
|
|
},
|
|
trc::EventType::Security(cause) => match cause {
|
|
trc::SecurityEvent::AuthenticationBan
|
|
| trc::SecurityEvent::ScanBan
|
|
| trc::SecurityEvent::AbuseBan
|
|
| trc::SecurityEvent::LoiterBan
|
|
| trc::SecurityEvent::IpBlocked => {
|
|
let mut err = RequestError::too_many_auth_attempts();
|
|
if let Some(reset) = self.value(trc::Key::Expires).and_then(|v| v.to_uint()) {
|
|
err = err.with_retry_after(reset);
|
|
}
|
|
err
|
|
}
|
|
trc::SecurityEvent::Unauthorized | trc::SecurityEvent::IpUnauthorized => {
|
|
RequestError::forbidden()
|
|
}
|
|
// inbuxa: legacy-protocols LP-8 is an event, never an error
|
|
trc::SecurityEvent::IpBlockExpired
|
|
| trc::SecurityEvent::IpAllowExpired
|
|
| trc::SecurityEvent::LegacyProtocolsChanged => {
|
|
RequestError::internal_server_error()
|
|
}
|
|
},
|
|
trc::EventType::Resource(cause) => match cause {
|
|
trc::ResourceEvent::NotFound => RequestError::not_found(),
|
|
trc::ResourceEvent::BadParameters => RequestError::blank(
|
|
StatusCode::BAD_REQUEST.as_u16(),
|
|
"Invalid parameters",
|
|
details_or_reason.unwrap_or("One or multiple parameters could not be parsed."),
|
|
),
|
|
trc::ResourceEvent::Error => RequestError::internal_server_error(),
|
|
_ => RequestError::internal_server_error(),
|
|
},
|
|
_ => RequestError::internal_server_error(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) trait IntoPushObject {
|
|
fn into_push_object(self) -> PushObject;
|
|
}
|
|
|
|
pub(crate) fn notifications_into_push_objects(
|
|
notifications: Vec<PushNotification>,
|
|
) -> Vec<PushObject> {
|
|
let mut changed: VecMap<Id, VecMap<DataType, State>> = VecMap::new();
|
|
let mut objects = Vec::with_capacity(notifications.len());
|
|
for notification in notifications {
|
|
match notification {
|
|
PushNotification::StateChange(state_change) => {
|
|
for type_state in state_change.types {
|
|
changed
|
|
.get_mut_or_insert(state_change.account_id.into())
|
|
.set(type_state, State::Exact(state_change.change_id));
|
|
}
|
|
}
|
|
PushNotification::CalendarAlert(calendar_alert) => {
|
|
objects.push(calendar_alert.into_push_object());
|
|
}
|
|
PushNotification::EmailPush(email_push) => {
|
|
let state_change = email_push.to_state_change();
|
|
for type_state in state_change.types {
|
|
changed
|
|
.get_mut_or_insert(state_change.account_id.into())
|
|
.set(type_state, State::Exact(state_change.change_id));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !changed.is_empty() {
|
|
objects.push(PushObject::StateChange { changed });
|
|
}
|
|
objects
|
|
}
|
|
|
|
impl IntoPushObject for CalendarAlert {
|
|
fn into_push_object(self) -> PushObject {
|
|
PushObject::CalendarAlert {
|
|
account_id: self.account_id.into(),
|
|
calendar_event_id: self.event_id.into(),
|
|
uid: self.uid,
|
|
recurrence_id: self
|
|
.recurrence_id
|
|
.map(|timestamp| JSCalendarDateTime::new(timestamp, true).to_rfc3339()),
|
|
alert_id: self.alert_id,
|
|
}
|
|
}
|
|
}
|