Relay: the InbuxaRelay route type, naming the relays to send through #264
No files matched your search
@@ -80,8 +80,9 @@ pub enum RoutingStrategy {
|
||||
Local,
|
||||
Mx(MxConfig),
|
||||
Relay(RelayConfig),
|
||||
// inbuxa: handed to an inbuxa Relay over its link (relay spec, RL-24)
|
||||
InbuxaRelay,
|
||||
// inbuxa: handed to an inbuxa Relay over its link (relay spec, RL-24):
|
||||
// one of the relays named, or any relay when none are
|
||||
InbuxaRelay(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -414,6 +415,13 @@ impl QueueConfig {
|
||||
.routing_strategy
|
||||
.insert(route.name, RoutingStrategy::Local);
|
||||
}
|
||||
// inbuxa: relay spec, RL-24
|
||||
MtaRoute::InbuxaRelay(route) => {
|
||||
queue.routing_strategy.insert(
|
||||
route.name,
|
||||
RoutingStrategy::InbuxaRelay(route.relays.into_iter().collect()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use super::{
|
||||
use inbuxa_relay_proto::types::Envelope;
|
||||
use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
collections::{BTreeMap, HashMap},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
@@ -74,8 +74,9 @@ struct State {
|
||||
known: bool,
|
||||
/// An enabled relay has `sendThrough` on (RL-24).
|
||||
send_through: bool,
|
||||
/// Enabled relays not marked `inboundOnly` (RL-20).
|
||||
may_send: HashSet<u32>,
|
||||
/// Enabled relays not marked `inboundOnly` (RL-20), by id, with their
|
||||
/// names as they are now: a rename doesn't restart the link.
|
||||
may_send: HashMap<u32, String>,
|
||||
linked: BTreeMap<u32, Arc<Linked>>,
|
||||
}
|
||||
|
||||
@@ -88,7 +89,7 @@ impl Senders {
|
||||
state.may_send = relays
|
||||
.iter()
|
||||
.filter(|r| r.enabled && !r.inbound_only)
|
||||
.map(|r| r.id)
|
||||
.map(|r| (r.id, r.name.trim().to_string()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -133,14 +134,19 @@ impl Senders {
|
||||
}
|
||||
|
||||
/// The relays to try for one message, in order: those this node is
|
||||
/// linked to and that may send, each message starting with the next
|
||||
/// one in turn (RL-20).
|
||||
pub fn candidates(&self) -> Vec<Arc<Linked>> {
|
||||
/// linked to and that may send, of those `named` when a route names
|
||||
/// some, each message starting with the next one in turn (RL-20, RL-24).
|
||||
pub fn candidates(&self, named: &[String]) -> Vec<Arc<Linked>> {
|
||||
let state = self.state.lock().expect("senders");
|
||||
let mut linked: Vec<Arc<Linked>> = state
|
||||
.linked
|
||||
.values()
|
||||
.filter(|l| state.may_send.contains(&l.relay_id))
|
||||
.filter(|l| {
|
||||
state
|
||||
.may_send
|
||||
.get(&l.relay_id)
|
||||
.is_some_and(|name| named.is_empty() || named.iter().any(|n| n.trim() == name))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if !linked.is_empty() {
|
||||
@@ -275,13 +281,13 @@ mod tests {
|
||||
]);
|
||||
assert!(senders.sending());
|
||||
// Nothing linked: nothing to try, and the mail waits
|
||||
assert!(senders.candidates().is_empty());
|
||||
assert!(senders.candidates(&[]).is_empty());
|
||||
for id in [1, 2, 3] {
|
||||
senders.link_up(linked(id));
|
||||
}
|
||||
let mut firsts = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let ids: Vec<u32> = senders.candidates().iter().map(|l| l.relay_id).collect();
|
||||
let ids: Vec<u32> = senders.candidates(&[]).iter().map(|l| l.relay_id).collect();
|
||||
assert_eq!(ids.len(), 2, "{ids:?}");
|
||||
assert!(!ids.contains(&2), "inboundOnly: {ids:?}");
|
||||
firsts.push(ids[0]);
|
||||
@@ -298,8 +304,37 @@ mod tests {
|
||||
senders.link_up(old.clone());
|
||||
senders.link_up(new.clone());
|
||||
senders.link_down(&old);
|
||||
assert_eq!(senders.candidates().len(), 1);
|
||||
assert_eq!(senders.candidates(&[]).len(), 1);
|
||||
senders.link_down(&new);
|
||||
assert!(senders.candidates().is_empty());
|
||||
assert!(senders.candidates(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_names_its_relays() {
|
||||
let senders = Senders::default();
|
||||
let mut renamed = relay(3, false, false);
|
||||
renamed.name = "east".into();
|
||||
senders.set_relays(&[relay(1, false, false), relay(2, false, true), renamed]);
|
||||
for id in [1, 2, 3] {
|
||||
senders.link_up(linked(id));
|
||||
}
|
||||
let ids = |named: &[&str]| -> Vec<u32> {
|
||||
let named: Vec<String> = named.iter().map(|n| n.to_string()).collect();
|
||||
let mut ids: Vec<u32> = senders
|
||||
.candidates(&named)
|
||||
.iter()
|
||||
.map(|l| l.relay_id)
|
||||
.collect();
|
||||
ids.sort();
|
||||
ids
|
||||
};
|
||||
assert_eq!(ids(&[]), [1, 3]);
|
||||
assert_eq!(ids(&["r1"]), [1]);
|
||||
// By the name it has now, not the one it was linked under
|
||||
assert_eq!(ids(&["east"]), [3]);
|
||||
assert!(ids(&["r3"]).is_empty());
|
||||
// inboundOnly sends nothing, named or not
|
||||
assert!(ids(&["r2"]).is_empty());
|
||||
assert_eq!(ids(&["r1", "east", "nowhere"]), [1, 3]);
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,14 @@
|
||||
use crate::{
|
||||
pickle::{Pickle, PickledStream},
|
||||
schema::{
|
||||
enums::OidcOpaqueTokens,
|
||||
enums::{MtaRouteType, OidcOpaqueTokens},
|
||||
prelude::{ObjectImpl, ObjectInner, ObjectType},
|
||||
structs::{Bootstrap, Directory, DirectoryBootstrap, OidcDirectory},
|
||||
structs::{
|
||||
Bootstrap, Directory, DirectoryBootstrap, MtaRoute, MtaRouteCommon,
|
||||
MtaRouteInbuxaRelay, OidcDirectory,
|
||||
},
|
||||
},
|
||||
types::{EnumImpl, map::Map},
|
||||
};
|
||||
|
||||
/// An OIDC directory as it was stored before the opaque-token fields.
|
||||
@@ -108,3 +112,39 @@ fn oidc_directories_saved_before_the_opaque_token_policy_keep_userinfo() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Relay spec, RL-24: the inbuxaRelay route is a fourth route type, stored
|
||||
// after the three there were, which read as before
|
||||
#[test]
|
||||
fn inbuxa_relay_routes_are_stored_after_the_others() {
|
||||
let route = MtaRoute::InbuxaRelay(MtaRouteInbuxaRelay {
|
||||
relays: Map::new(vec!["east".to_string(), "west".to_string()]),
|
||||
name: "relays".to_string(),
|
||||
description: Some("Through the east and west relays".to_string()),
|
||||
});
|
||||
let bytes = route.to_pickled_vec();
|
||||
assert_eq!(
|
||||
read(ObjectType::MtaRoute, &bytes),
|
||||
ObjectInner::MtaRoute(route)
|
||||
);
|
||||
|
||||
let local = MtaRoute::Local(MtaRouteCommon {
|
||||
name: "local".to_string(),
|
||||
description: None,
|
||||
});
|
||||
let mut bytes = vec![0u8];
|
||||
2u16.pickle(&mut bytes);
|
||||
"local".to_string().pickle(&mut bytes);
|
||||
None::<String>.pickle(&mut bytes);
|
||||
assert_eq!(local.to_pickled_vec(), bytes);
|
||||
assert_eq!(
|
||||
read(ObjectType::MtaRoute, &bytes),
|
||||
ObjectInner::MtaRoute(local)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MtaRouteType::parse("InbuxaRelay"),
|
||||
Some(MtaRouteType::InbuxaRelay)
|
||||
);
|
||||
assert_eq!(MtaRouteType::from_id(3), Some(MtaRouteType::InbuxaRelay));
|
||||
}
|
||||
@@ -1448,6 +1448,8 @@ pub enum MtaRouteType {
|
||||
Mx = 0,
|
||||
Relay = 1,
|
||||
Local = 2,
|
||||
// inbuxa: relay spec, RL-24
|
||||
InbuxaRelay = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -6508,6 +6508,7 @@ impl EnumImpl for MtaRouteType {
|
||||
b"Mx" => MtaRouteType::Mx,
|
||||
b"Relay" => MtaRouteType::Relay,
|
||||
b"Local" => MtaRouteType::Local,
|
||||
b"InbuxaRelay" => MtaRouteType::InbuxaRelay,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6516,6 +6517,7 @@ impl EnumImpl for MtaRouteType {
|
||||
MtaRouteType::Mx => "Mx",
|
||||
MtaRouteType::Relay => "Relay",
|
||||
MtaRouteType::Local => "Local",
|
||||
MtaRouteType::InbuxaRelay => "InbuxaRelay",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6528,11 +6530,12 @@ impl EnumImpl for MtaRouteType {
|
||||
0 => Some(MtaRouteType::Mx),
|
||||
1 => Some(MtaRouteType::Relay),
|
||||
2 => Some(MtaRouteType::Local),
|
||||
3 => Some(MtaRouteType::InbuxaRelay),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const COUNT: usize = 3;
|
||||
const COUNT: usize = 4;
|
||||
}
|
||||
|
||||
impl serde::Serialize for MtaRouteType {
|
||||
|
||||
@@ -989,6 +989,8 @@ pub enum Property {
|
||||
IntrospectionUrl = 936,
|
||||
IntrospectionClientId = 937,
|
||||
IntrospectionClientSecret = 938,
|
||||
// inbuxa: relay spec, RL-24
|
||||
Relays = 939,
|
||||
RequireClientRegistration = 615,
|
||||
RequireScopes = 608,
|
||||
RequireTls = 525,
|
||||
|
||||
@@ -1141,6 +1141,7 @@ impl EnumImpl for Property {
|
||||
b"introspectionUrl" => Property::IntrospectionUrl,
|
||||
b"introspectionClientId" => Property::IntrospectionClientId,
|
||||
b"introspectionClientSecret" => Property::IntrospectionClientSecret,
|
||||
b"relays" => Property::Relays,
|
||||
b"requireClientRegistration" => Property::RequireClientRegistration,
|
||||
b"requireScopes" => Property::RequireScopes,
|
||||
b"requireTls" => Property::RequireTls,
|
||||
@@ -2085,6 +2086,7 @@ impl EnumImpl for Property {
|
||||
Property::IntrospectionUrl => "introspectionUrl",
|
||||
Property::IntrospectionClientId => "introspectionClientId",
|
||||
Property::IntrospectionClientSecret => "introspectionClientSecret",
|
||||
Property::Relays => "relays",
|
||||
Property::RequireClientRegistration => "requireClientRegistration",
|
||||
Property::RequireScopes => "requireScopes",
|
||||
Property::RequireTls => "requireTls",
|
||||
@@ -3033,6 +3035,7 @@ impl EnumImpl for Property {
|
||||
936 => Some(Property::IntrospectionUrl),
|
||||
937 => Some(Property::IntrospectionClientId),
|
||||
938 => Some(Property::IntrospectionClientSecret),
|
||||
939 => Some(Property::Relays),
|
||||
615 => Some(Property::RequireClientRegistration),
|
||||
608 => Some(Property::RequireScopes),
|
||||
525 => Some(Property::RequireTls),
|
||||
@@ -3244,7 +3247,7 @@ impl EnumImpl for Property {
|
||||
}
|
||||
}
|
||||
|
||||
const COUNT: usize = 939;
|
||||
const COUNT: usize = 940;
|
||||
}
|
||||
|
||||
impl serde::Serialize for Property {
|
||||
|
||||
@@ -3796,6 +3796,8 @@ pub enum MtaRoute {
|
||||
Mx(MtaRouteMx),
|
||||
Relay(MtaRouteRelay),
|
||||
Local(MtaRouteCommon),
|
||||
// inbuxa: relay spec, RL-24
|
||||
InbuxaRelay(MtaRouteInbuxaRelay),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -3807,6 +3809,18 @@ pub struct MtaRouteCommon {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
// inbuxa: through inbuxa Relays (relay spec, RL-24)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct MtaRouteInbuxaRelay {
|
||||
#[serde(rename = "relays")]
|
||||
pub relays: Map<String>,
|
||||
#[serde(rename = "name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "description")]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct MtaRouteMx {
|
||||
|
||||
@@ -27860,6 +27860,7 @@ impl ObjectImpl for MtaRoute {
|
||||
MtaRoute::Mx(inner) => inner.validate(errors),
|
||||
MtaRoute::Relay(inner) => inner.validate(errors),
|
||||
MtaRoute::Local(inner) => inner.validate(errors),
|
||||
MtaRoute::InbuxaRelay(inner) => inner.validate(errors),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27874,6 +27875,9 @@ impl ObjectImpl for MtaRoute {
|
||||
MtaRoute::Local(object) => {
|
||||
object.index(i);
|
||||
}
|
||||
MtaRoute::InbuxaRelay(object) => {
|
||||
object.index(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27899,6 +27903,10 @@ impl Pickle for MtaRoute {
|
||||
2u16.pickle(out);
|
||||
inner.pickle(out);
|
||||
}
|
||||
MtaRoute::InbuxaRelay(inner) => {
|
||||
3u16.pickle(out);
|
||||
inner.pickle(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27907,6 +27915,7 @@ impl Pickle for MtaRoute {
|
||||
0 => Pickle::unpickle(stream).map(MtaRoute::Mx),
|
||||
1 => Pickle::unpickle(stream).map(MtaRoute::Relay),
|
||||
2 => Pickle::unpickle(stream).map(MtaRoute::Local),
|
||||
3 => Pickle::unpickle(stream).map(MtaRoute::InbuxaRelay),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -27936,6 +27945,13 @@ impl IntoValue for MtaRoute {
|
||||
.insert_unchecked(Property::Type, JmapValue::Str("Local".into()));
|
||||
obj
|
||||
}
|
||||
MtaRoute::InbuxaRelay(obj) => {
|
||||
let mut obj = obj.into_value();
|
||||
obj.as_object_mut()
|
||||
.unwrap()
|
||||
.insert_unchecked(Property::Type, JmapValue::Str("InbuxaRelay".into()));
|
||||
obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27951,12 +27967,14 @@ impl RegistryJsonPatch for MtaRoute {
|
||||
MtaRouteType::Mx => *self = MtaRoute::Mx(Default::default()),
|
||||
MtaRouteType::Relay => *self = MtaRoute::Relay(Default::default()),
|
||||
MtaRouteType::Local => *self = MtaRoute::Local(Default::default()),
|
||||
MtaRouteType::InbuxaRelay => *self = MtaRoute::InbuxaRelay(Default::default()),
|
||||
}
|
||||
}
|
||||
match self {
|
||||
MtaRoute::Mx(inner) => inner.patch(pointer, value),
|
||||
MtaRoute::Relay(inner) => inner.patch(pointer, value),
|
||||
MtaRoute::Local(inner) => inner.patch(pointer, value),
|
||||
MtaRoute::InbuxaRelay(inner) => inner.patch(pointer, value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27967,6 +27985,7 @@ impl MtaRoute {
|
||||
MtaRoute::Mx(_) => MtaRouteType::Mx,
|
||||
MtaRoute::Relay(_) => MtaRouteType::Relay,
|
||||
MtaRoute::Local(_) => MtaRouteType::Local,
|
||||
MtaRoute::InbuxaRelay(_) => MtaRouteType::InbuxaRelay,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28041,6 +28060,90 @@ impl RegistryJsonPropertyPatch for MtaRouteCommon {
|
||||
}
|
||||
}
|
||||
|
||||
// inbuxa: relay spec, RL-24
|
||||
impl MtaRouteInbuxaRelay {
|
||||
fn validate(&self, errors: &mut Vec<ValidationError>) -> bool {
|
||||
let neb = errors.len();
|
||||
let value = &self.relays;
|
||||
for value in value.iter() {
|
||||
if value.is_empty() {
|
||||
errors.push(ValidationError::required(Property::Relays));
|
||||
}
|
||||
}
|
||||
let value = &self.name;
|
||||
if value.is_empty() {
|
||||
errors.push(ValidationError::required(Property::Name));
|
||||
}
|
||||
if let Some(value) = &self.description {
|
||||
if value.is_empty() {
|
||||
errors.push(ValidationError::required(Property::Description));
|
||||
}
|
||||
}
|
||||
errors.len() == neb
|
||||
}
|
||||
|
||||
fn index<'x>(&'x self, i: &mut IndexBuilder<'x>) {
|
||||
i.unique(Property::Name, &self.name);
|
||||
}
|
||||
}
|
||||
|
||||
impl Pickle for MtaRouteInbuxaRelay {
|
||||
fn pickle(&self, out: &mut Vec<u8>) {
|
||||
self.relays.pickle(out);
|
||||
self.name.pickle(out);
|
||||
self.description.pickle(out);
|
||||
}
|
||||
|
||||
fn unpickle(stream: &mut crate::pickle::PickledStream<'_>) -> Option<Self> {
|
||||
let mut this = Self::default();
|
||||
this.relays = Pickle::unpickle(stream)?;
|
||||
this.name = Pickle::unpickle(stream)?;
|
||||
this.description = Pickle::unpickle(stream)?;
|
||||
Some(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MtaRouteInbuxaRelay {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relays: Default::default(),
|
||||
name: Default::default(),
|
||||
description: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for MtaRouteInbuxaRelay {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(4);
|
||||
map.insert_unchecked(Property::Relays, self.relays.into_value());
|
||||
map.insert_unchecked(Property::Name, self.name.into_value());
|
||||
map.insert_unchecked(Property::Description, self.description.into_value());
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryJsonPropertyPatch for MtaRouteInbuxaRelay {
|
||||
fn patch_property<'x>(
|
||||
&mut self,
|
||||
mut pointer: JsonPointerPatch<'_>,
|
||||
value: JmapValue<'x>,
|
||||
) -> PatchResult<'x> {
|
||||
match pointer.next_property() {
|
||||
Some(Property::Relays) => self
|
||||
.relays
|
||||
.patch(pointer.with_validators(&[StringValidator::Trim]), value),
|
||||
Some(Property::Name) => self.name.patch(pointer.assert_read_only()?, value),
|
||||
Some(Property::Description) => self.description.patch(pointer, value),
|
||||
Some(Property::Type) => Ok(MaybeUnpatched::Unpatched {
|
||||
property: Property::Type,
|
||||
value,
|
||||
}),
|
||||
_ => Err(PatchError::new(pointer, "Invalid property")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MtaRouteMx {
|
||||
fn validate(&self, errors: &mut Vec<ValidationError>) -> bool {
|
||||
let neb = errors.len();
|
||||
|
||||
@@ -239,8 +239,9 @@ impl QueuedMessage {
|
||||
}
|
||||
}
|
||||
// inbuxa: with a relay's sendThrough on, every non-local recipient
|
||||
// goes through a relay (relay spec, RL-24)
|
||||
static THROUGH_RELAY: RoutingStrategy = RoutingStrategy::InbuxaRelay;
|
||||
// goes through a relay, any of them unless its route names some
|
||||
// (relay spec, RL-24)
|
||||
static THROUGH_RELAY: RoutingStrategy = RoutingStrategy::InbuxaRelay(Vec::new());
|
||||
let senders = &server.inner.data.relay_senders;
|
||||
if let Err(err) = senders.load(server.store()).await {
|
||||
trc::error!(err.details("Failed to read the relays").span_id(span_id));
|
||||
@@ -267,7 +268,11 @@ impl QueuedMessage {
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
message.span_id,
|
||||
);
|
||||
let route = if through_relay && !matches!(route, RoutingStrategy::Local) {
|
||||
let route = if through_relay
|
||||
&& !matches!(
|
||||
route,
|
||||
RoutingStrategy::Local | RoutingStrategy::InbuxaRelay(_)
|
||||
) {
|
||||
&THROUGH_RELAY
|
||||
} else {
|
||||
route
|
||||
@@ -334,9 +339,15 @@ impl QueuedMessage {
|
||||
continue 'next_route;
|
||||
}
|
||||
// inbuxa: handed to an inbuxa Relay (relay spec, RL-24)
|
||||
RoutingStrategy::InbuxaRelay => {
|
||||
RoutingStrategy::InbuxaRelay(named) => {
|
||||
message
|
||||
.hand_to_relay(&rcpt_idxs, rcpt_headers, &mut delivery_results, &server)
|
||||
.hand_to_relay(
|
||||
named,
|
||||
&rcpt_idxs,
|
||||
rcpt_headers,
|
||||
&mut delivery_results,
|
||||
&server,
|
||||
)
|
||||
.await;
|
||||
continue 'next_route;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
//! RL-20, RL-24).
|
||||
//!
|
||||
//! With a relay's `sendThrough` on, every non-local recipient is handed to
|
||||
//! one of the relays this node is linked to, in turn. The recipients stay
|
||||
//! one of the relays this node is linked to, in turn; a recipient whose
|
||||
//! route is an `InbuxaRelay` route goes to one of the relays it names. The recipients stay
|
||||
//! in this server's queue, marked [`RCPT_RELAYED`] and shown as handed to
|
||||
//! the relay, until the link service reports the relay's final status for
|
||||
//! them through [`apply_relayed`]. The usual DSNs follow from there. With
|
||||
@@ -47,19 +48,29 @@ use types::id::Id;
|
||||
pub const RELAYED_RECHECK: u64 = 3_600;
|
||||
|
||||
impl MessageWrapper {
|
||||
/// Hands one domain's recipients to the first relay that takes them.
|
||||
/// Hands one domain's recipients to the first relay that takes them,
|
||||
/// of those `named`, or of all when none are.
|
||||
pub(super) async fn hand_to_relay(
|
||||
&self,
|
||||
named: &[String],
|
||||
rcpt_idxs: &[usize],
|
||||
rcpt_headers: Option<&[u8]>,
|
||||
results: &mut Vec<DeliveryResult>,
|
||||
server: &Server,
|
||||
) {
|
||||
let candidates = server.inner.data.relay_senders.candidates();
|
||||
let candidates = server.inner.data.relay_senders.candidates(named);
|
||||
if candidates.is_empty() {
|
||||
let reason = if named.is_empty() {
|
||||
"No relay is linked from this node.".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"No relay named {} is linked from this node and sending.",
|
||||
named.join(" or ")
|
||||
)
|
||||
};
|
||||
let status = Status::TemporaryFailure(ErrorDetails {
|
||||
entity: "relay".into(),
|
||||
details: Error::ConnectionError("No relay is linked from this node.".into()),
|
||||
details: Error::ConnectionError(reason.into()),
|
||||
});
|
||||
results.push(DeliveryResult::domain(status, rcpt_idxs.to_vec()));
|
||||
return;
|
||||
|
||||
@@ -552,9 +552,15 @@ allowing the edge's IP and disabling SPF, DMARC and reverse IP for it.
|
||||
|
||||
### Outbound routing
|
||||
|
||||
- **RL-24.** A new route type, `inbuxaRelay`, alongside the existing route
|
||||
types, names one or more `inbuxa:Relay`s. With `sendThrough` on, it is the
|
||||
route for every non-local recipient.
|
||||
- **RL-24.** A new route type, `InbuxaRelay`, alongside the existing route
|
||||
types, names one or more `inbuxa:Relay`s in `relays`, by name (every
|
||||
relay with that name), or none, for any relay. The queue's `route`
|
||||
expression picks it per recipient like any other route, so some domains
|
||||
can go through relays while the rest go direct. Its recipients go through
|
||||
the relays named, chosen as below; a relay renamed stops matching. With
|
||||
`sendThrough` on, every non-local recipient is routed through relays, to
|
||||
the relays its route names if it is an `InbuxaRelay` route, to any
|
||||
otherwise.
|
||||
- `sendThrough` on any enabled relay sends every non-local recipient
|
||||
through relays: the enabled ones not marked `inboundOnly`, that the node
|
||||
delivering is linked to, each message starting with the next in turn.
|
||||
@@ -841,7 +847,11 @@ In progress. Done so far:
|
||||
`outbound.tls` into a session in this server's TLS-RPT reports, with the
|
||||
policy, result, MX host, address and reason (tested with a relay's
|
||||
STARTTLS failure for REQUIRETLS mail).
|
||||
- Still to come in step 4: the named `inbuxaRelay` route.
|
||||
- The `InbuxaRelay` route type (`x:MtaRoute`, stored as its fourth
|
||||
variant, so routes saved before read as they were), in the served schema
|
||||
for the console's route form. Tested end to end with `sendThrough` off:
|
||||
a domain routed to a route naming the relay goes through it, and one
|
||||
routed to a route naming no linked relay waits, saying so.
|
||||
- `crates/outbound-tls` (RL-30): the TLSA and MTA-STS types, the DANE
|
||||
certificate check, MTA-STS policy parsing and matching, and the DNSSEC
|
||||
lookups (with the bogus-answer recheck), moved out of `crates/smtp` and
|
||||
@@ -859,7 +869,7 @@ Order:
|
||||
`inbuxa:Relay`, ingest with session context (RL-23).
|
||||
3. Live mode (RL-12, RL-13). Steps 2 and 3 ship together: held mode alone
|
||||
can't refuse spam at the door, which the settings-only edge already can.
|
||||
4. Outbound: `Outbound`, the `inbuxaRelay` route, status and DSNs, TLS-RPT
|
||||
4. Outbound: `Outbound`, the `InbuxaRelay` route, status and DSNs, TLS-RPT
|
||||
events.
|
||||
5. inbuxa-admin's Relays page, certificates (RL-27), DNS (RL-25), and
|
||||
upgrades (RL-29).
|
||||
|
||||
@@ -1595,6 +1595,9 @@ default = "none"
|
||||
[object."x:MtaRouteCommon"]
|
||||
default = "none"
|
||||
|
||||
[object."x:MtaRouteInbuxaRelay"]
|
||||
default = "none"
|
||||
|
||||
[object."x:MtaRouteMx"]
|
||||
default = "none"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
D6Ad4OELCzUv1LCDwL_v0XRNhdN-2jaO4HuYVRXzxu4
|
||||
0ySrEn9k_JryaFkuzjsJUtJK_uK9OJvr-n11zkpboUo
|
||||
+111
-5
@@ -26,12 +26,16 @@ use crate::utils::{
|
||||
};
|
||||
use inbuxa_relay_proto::{time, token::JoinToken};
|
||||
use mail_auth::{common::parse::TxtRecordParser, mta_sts::TlsRpt};
|
||||
use registry::schema::{
|
||||
prelude::{ObjectType, Property},
|
||||
structs::{
|
||||
CertificateManagement, DkimManagement, DnsManagement, Domain, Tenant, TlsInternalReport,
|
||||
UserRoles,
|
||||
use registry::{
|
||||
schema::{
|
||||
prelude::{ObjectType, Property},
|
||||
structs::{
|
||||
CertificateManagement, DkimManagement, DnsManagement, Domain, Expression,
|
||||
ExpressionMatch, MtaOutboundStrategy, MtaRoute, MtaRouteInbuxaRelay, Tenant,
|
||||
TlsInternalReport, UserRoles,
|
||||
},
|
||||
},
|
||||
types::map::Map,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::str::FromStr;
|
||||
@@ -1021,6 +1025,108 @@ pub async fn link(test: &mut TestServer) {
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
// A route naming relays (RL-24): with sendThrough off, only the
|
||||
// domains routed to it go through a relay, and only the relays named.
|
||||
// One named relay that isn't there: the mail waits, saying so
|
||||
call(
|
||||
admin,
|
||||
"inbuxa:Relay/set",
|
||||
json!({"update": {&id: {"sendThrough": false}}}),
|
||||
)
|
||||
.await;
|
||||
for (name, relay) in [("through-edge", "Edge"), ("through-nowhere", "Nowhere")] {
|
||||
admin
|
||||
.registry_create_object(MtaRoute::InbuxaRelay(MtaRouteInbuxaRelay {
|
||||
relays: Map::new(vec![relay.to_string()]),
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
admin
|
||||
.registry_update_setting(
|
||||
MtaOutboundStrategy {
|
||||
route: Expression {
|
||||
match_: [
|
||||
ExpressionMatch {
|
||||
if_: "is_local_domain(rcpt_domain)".into(),
|
||||
then: "'local'".into(),
|
||||
},
|
||||
ExpressionMatch {
|
||||
if_: "rcpt_domain == 'named.test'".into(),
|
||||
then: "'through-edge'".into(),
|
||||
},
|
||||
ExpressionMatch {
|
||||
if_: "rcpt_domain == 'elsewhere.test'".into(),
|
||||
then: "'through-nowhere'".into(),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
else_: "'mx'".into(),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&[Property::Route],
|
||||
)
|
||||
.await;
|
||||
admin.reload_settings().await;
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
while test.server.inner.data.relay_senders.sending() {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"sendThrough off reached the queue"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
submit(
|
||||
&jane,
|
||||
&identity,
|
||||
&drafts,
|
||||
&["[email protected]"],
|
||||
"Through the named relay",
|
||||
)
|
||||
.await;
|
||||
submit(
|
||||
&jane,
|
||||
&identity,
|
||||
&drafts,
|
||||
&["[email protected]"],
|
||||
"Through no relay",
|
||||
)
|
||||
.await;
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
let (_, queued) = call(admin, "x:QueuedMessage/get", json!({"ids": null})).await;
|
||||
let text = queued.to_string();
|
||||
let sent = mx_seen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.contains("Subject: Through the named relay"));
|
||||
if sent
|
||||
&& !text.contains("[email protected]")
|
||||
&& text.contains("[email protected]")
|
||||
&& text.contains("No relay named Nowhere")
|
||||
{
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"sent through Edge: {sent}; queued: {queued}"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
assert!(
|
||||
!mx_seen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.contains("Subject: Through no relay")),
|
||||
"sent through a relay the route doesn't name"
|
||||
);
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
assert_eq!(
|
||||
subjects(&jane)
|
||||
|
||||
Reference in new issue
Block a user