use core::str;
use alloc::{string::ToString, vec::Vec};
use log::trace;
use thiserror::Error;
use url::Url;
use crate::{
coroutine::{DiscoveryCoroutine, DiscoveryCoroutineState, DiscoveryYield},
shared::dns::{DiscoveryDnsTxt, DiscoveryDnsTxtError},
};
#[derive(Debug, Error)]
pub enum DiscoveryMailconfError {
#[error(transparent)]
Dns(#[from] DiscoveryDnsTxtError),
#[error("no `mailconf=` TXT record found for the queried domain")]
NoMailconfRecord,
}
pub struct DiscoveryMailconf {
txt: DiscoveryDnsTxt,
}
impl DiscoveryMailconf {
pub fn new(domain: impl ToString, resolver: Url) -> Self {
Self {
txt: DiscoveryDnsTxt::new(domain, resolver),
}
}
}
impl DiscoveryCoroutine for DiscoveryMailconf {
type Yield = DiscoveryYield;
type Return = Result<Url, DiscoveryMailconfError>;
fn resume(&mut self, arg: Option<&[u8]>) -> DiscoveryCoroutineState<Self::Yield, Self::Return> {
match self.txt.resume(arg) {
DiscoveryCoroutineState::Yielded(y) => DiscoveryCoroutineState::Yielded(y),
DiscoveryCoroutineState::Complete(Err(err)) => {
DiscoveryCoroutineState::Complete(Err(err.into()))
}
DiscoveryCoroutineState::Complete(Ok(records)) => {
for record in records {
let mut joined = Vec::new();
for cs in record.rdata.iter() {
joined.extend_from_slice(&cs.octets);
}
let Some(value) = joined.strip_prefix(b"mailconf=") else {
trace!("no `mailconf=` prefix in TXT record, skip");
continue;
};
let Ok(url_str) = str::from_utf8(value) else {
trace!("`mailconf=` TXT value is not valid UTF-8, skip");
continue;
};
let Ok(url) = Url::parse(url_str.trim()) else {
trace!("`mailconf=` TXT value `{url_str}` is not a valid URL, skip");
continue;
};
return DiscoveryCoroutineState::Complete(Ok(url));
}
DiscoveryCoroutineState::Complete(Err(DiscoveryMailconfError::NoMailconfRecord))
}
}
}
}