use super::HashAlgorithm;
use crate::ParseError;
use base64::{engine::general_purpose::STANDARD, Engine};
#[derive(Debug, Default, Clone, PartialEq, Eq, strum::EnumString, strum::Display)]
#[strum(serialize_all = "UPPERCASE")]
pub enum Version {
#[default]
Dkim1,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, strum::EnumString, strum::Display)]
#[strum(serialize_all = "lowercase")]
pub enum Type {
#[default]
Rsa,
Ed25519,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, strum::EnumString, strum::Display)]
#[strum(serialize_all = "lowercase")]
pub enum ServiceType {
#[default]
#[strum(serialize = "*")]
Wildcard,
Email,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumString, strum::Display)]
pub enum Flags {
#[strum(serialize = "y")]
Testing,
#[strum(serialize = "s")]
SameDomain,
}
#[derive(Clone, PartialEq, Eq)]
pub struct Record {
pub(super) version: Version,
pub(super) acceptable_hash_algorithms: Vec<HashAlgorithm>,
pub(super) r#type: Type,
pub(super) notes: Option<String>,
pub(super) public_key: Vec<u8>,
pub(super) service_type: Vec<ServiceType>,
pub(super) flags: Vec<Flags>,
}
impl std::fmt::Debug for Record {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Record")
.field("version", &self.version)
.field(
"acceptable_hash_algorithms",
&self.acceptable_hash_algorithms,
)
.field("type", &self.r#type)
.field("notes", &self.notes)
.field("public_key", &STANDARD.encode(&self.public_key))
.field("service_type", &self.service_type)
.field("flags", &self.flags)
.finish()
}
}
impl std::str::FromStr for Record {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut version = Version::default();
let mut acceptable_hash_algorithms = vec![];
let mut r#type = Type::default();
let mut notes = None;
let mut public_key = None;
let mut service_type = vec![ServiceType::default()];
let mut flags = vec![];
for i in s
.split(';')
.map(|tag| tag.split_whitespace().collect::<Vec<_>>().concat())
.take_while(|s| !s.is_empty())
{
match i.split_once('=').ok_or(ParseError::SyntaxError {
reason: "tag syntax is `{tag}={value}`".to_string(),
})? {
("v", p_version) => {
version =
Version::from_str(p_version).map_err(|e| ParseError::SyntaxError {
reason: format!("when parsing `version`, got: `{e}`"),
})?;
}
("h", p_acceptable_hash_algorithms) => {
acceptable_hash_algorithms = p_acceptable_hash_algorithms
.split(':')
.filter_map(|h| HashAlgorithm::from_str(h).ok())
.collect();
}
("k", p_type) => {
r#type = Type::from_str(p_type).unwrap_or_default();
}
("n", p_notes) => notes = Some(p_notes.to_string()),
("p", p_public_key) => {
public_key = Some(STANDARD.decode(p_public_key).map_err(|e| {
ParseError::SyntaxError {
reason: format!("failed to pase `public_key`: got `{e}`"),
}
})?);
}
("s", p_service_type) => {
service_type = p_service_type
.split(':')
.filter_map(|s| ServiceType::from_str(s).ok())
.collect();
}
("t", p_flags) => {
flags = p_flags
.split(':')
.filter_map(|t| Flags::from_str(t).ok())
.collect();
}
_ => continue,
}
}
Ok(Self {
version,
acceptable_hash_algorithms: match r#type {
_ if !acceptable_hash_algorithms.is_empty() => acceptable_hash_algorithms,
#[cfg(feature = "historic")]
Type::Rsa => vec![HashAlgorithm::Sha1, HashAlgorithm::Sha256],
_ => vec![HashAlgorithm::Sha256],
},
r#type,
notes,
public_key: public_key.ok_or(ParseError::MissingRequiredField {
field: "public_key".to_string(),
})?,
service_type,
flags,
})
}
}
#[cfg(test)]
mod try_from {
use super::{HashAlgorithm, Record, ServiceType, Type, Version};
use crate::dkim::{public_key::InnerPublicKey, RSA_MINIMUM_ACCEPTABLE_KEY_SIZE};
impl TryFrom<&InnerPublicKey> for Record {
type Error = ();
fn try_from(inner: &InnerPublicKey) -> Result<Self, Self::Error> {
match inner {
InnerPublicKey::Rsa(rsa) => {
if rsa::PublicKeyParts::size(rsa) * 8 < RSA_MINIMUM_ACCEPTABLE_KEY_SIZE {
return Err(());
}
Ok(Self {
version: Version::Dkim1,
acceptable_hash_algorithms: vec![
#[cfg(feature = "historic")]
HashAlgorithm::Sha1,
HashAlgorithm::Sha256,
],
r#type: Type::Rsa,
notes: None,
public_key: rsa::pkcs8::EncodePublicKey::to_public_key_der(rsa)
.unwrap()
.as_ref()
.to_vec(),
service_type: vec![ServiceType::Wildcard],
flags: vec![],
})
}
InnerPublicKey::Ed25519(ed25519) => Ok(Self {
version: Version::Dkim1,
acceptable_hash_algorithms: vec![HashAlgorithm::Sha256],
r#type: Type::Ed25519,
notes: None,
public_key: ed25519.as_ref().to_vec(),
service_type: vec![ServiceType::Wildcard],
flags: vec![],
}),
}
}
}
}