mirror of
https://github.com/chatmail/relay.git
synced 2026-09-21 23:00:08 +00:00
feat: Support addresses using domain literals (#42)
If the incoming email comes from address that uses domain literals `[<ipv4>]` or `[IPv6:<ipv6>]`, skip DKIM verification and instead check IP alignment with originating IP from XFORWARD command. Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
This commit is contained in:
committed by
GitHub
parent
a096d0550f
commit
4837754245
+58
-12
@@ -1,4 +1,5 @@
|
||||
use mailparse::MailAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Extracts the first email address found in SMTP command or email header.
|
||||
///
|
||||
@@ -26,14 +27,51 @@ pub fn extract_address(input: &str) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_domain_from_address(address: &str) -> Option<String> {
|
||||
let parts: Vec<&str> = address.split('@').collect();
|
||||
if parts.len() == 2
|
||||
&& let Some(domain) = parts.get(1)
|
||||
{
|
||||
Some(domain.to_string())
|
||||
} else {
|
||||
None
|
||||
/// Domain part of an email address, either a domain-literal (IP address in square brackets with
|
||||
/// optional protocol prefix) or a regular domain name.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AddressDomain {
|
||||
/// Domain literal, e.g.
|
||||
/// - `192.0.2.0` in `test@[192.0.2.0]`,
|
||||
/// - `2001:db8::1` in `test@[IPv6:2001:db8::1]`.
|
||||
Literal(String),
|
||||
/// Regular domain name, e.g. `example.org` in `test@example.org`.
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl FromStr for AddressDomain {
|
||||
type Err = crate::error::Error;
|
||||
|
||||
/// Extracts the domain part from an email address and returns it as an [`AddressDomain`].
|
||||
///
|
||||
/// Returns an [`Error`] if `address` is not a valid email address.
|
||||
///
|
||||
/// [`Error`]: crate::error::Error
|
||||
fn from_str(address: &str) -> Result<Self, Self::Err> {
|
||||
let parts: Vec<&str> = address.split('@').collect();
|
||||
if parts.len() == 2
|
||||
&& let Some(domain) = parts.get(1)
|
||||
{
|
||||
// domain literals
|
||||
if domain.starts_with('[') && domain.ends_with(']') {
|
||||
let mut address_trimmed = domain
|
||||
.get(1..domain.len() - 1)
|
||||
.unwrap_or(domain)
|
||||
.to_lowercase();
|
||||
|
||||
address_trimmed = address_trimmed
|
||||
.strip_prefix("ipv6:")
|
||||
.unwrap_or(&address_trimmed)
|
||||
.to_string();
|
||||
|
||||
return Ok(AddressDomain::Literal(address_trimmed.to_string()));
|
||||
}
|
||||
Ok(AddressDomain::Name(domain.to_string()))
|
||||
} else {
|
||||
Err(crate::error::Error::InvalidEmailAddress(
|
||||
address.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,18 +89,26 @@ mod tests {
|
||||
#[case("mail from:<t4@example.org>", Some("t4@example.org".to_string()))]
|
||||
#[case("Foo Bar <t5@example.org>", Some("t5@example.org".to_string()))]
|
||||
#[case("t6@example.org", Some("t6@example.org".to_string()))]
|
||||
#[case("t7@[192.0.2.0]", Some("t7@[192.0.2.0]".to_string()))]
|
||||
#[case("<t7@[192.0.2.0]>", Some("t7@[192.0.2.0]".to_string()))]
|
||||
// This is a bug in mailparse, it refuses to parse IPv6 without "<>" around.
|
||||
// https://github.com/staktrace/mailparse/issues/137
|
||||
#[case("t8@[IPv6:2001:db8::1]", None)]
|
||||
#[case("<t8@[IPv6:2001:db8::1]>", Some("t8@[ipv6:2001:db8::1]".to_string()))]
|
||||
fn test_extract_address(#[case] input: &str, #[case] expected: Option<String>) {
|
||||
let result = extract_address(input);
|
||||
assert_eq!(result, expected)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("t1@example.org", Some("example.org".to_string()))]
|
||||
#[case("SRS1=HHH=example.com==HHH=TT=example.org=alice@example.net", Some("example.net".to_string()))]
|
||||
#[case("t1@example.org", Some(AddressDomain::Name("example.org".to_string())))]
|
||||
#[case("SRS1=HHH=example.com==HHH=TT=example.org=alice@example.net", Some(AddressDomain::Name("example.net".to_string())))]
|
||||
#[case("t7@[192.0.2.0]", Some(AddressDomain::Literal("192.0.2.0".to_string())))]
|
||||
#[case("t8@[IPv6:2001:db8::1]", Some(AddressDomain::Literal("2001:db8::1".to_string())))]
|
||||
#[case("invalid", None)]
|
||||
#[case("invalid@address@com", None)]
|
||||
fn test_get_domain_from_address(#[case] input: &str, #[case] expected: Option<String>) {
|
||||
let result = get_domain_from_address(input);
|
||||
fn test_get_domain_from_address(#[case] input: &str, #[case] expected: Option<AddressDomain>) {
|
||||
let result = AddressDomain::from_str(input).ok();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user