use serde::{Deserialize, Serialize};
use crate::commands::keyring::email::imap_client::ImapSecurity;
use crate::commands::keyring::email::provider::Provider;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
pub alias: String,
pub email: String,
pub provider: Provider,
pub host: String,
pub port: u16,
pub secret: String,
#[serde(default)]
pub max_imap_connections: Option<u32>,
#[serde(default)]
pub security: Option<ImapSecurity>,
}
impl Identity {
pub fn security(&self) -> ImapSecurity {
self.security
.unwrap_or_else(|| self.provider.default_security(self.port))
}
}
impl crate::core::keyring::KeyringEntry for Identity {
fn alias(&self) -> &str {
&self.alias
}
fn kind(&self) -> &'static str {
"email"
}
fn detail(&self) -> String {
match self.max_imap_connections {
Some(cap) => format!("{} ({}, max {cap} IMAP conns)", self.email, self.provider),
None => format!("{} ({})", self.email, self.provider),
}
}
}
const MAX_SEGMENT_LENGTH: usize = 100;
pub fn sanitize_segment(input: &str) -> String {
let mut segment = String::with_capacity(input.len());
let mut last_was_hyphen = false;
for ch in input.chars() {
if ch.is_ascii_alphanumeric() {
segment.push(ch.to_ascii_lowercase());
last_was_hyphen = false;
} else if !last_was_hyphen && !segment.is_empty() {
segment.push('-');
last_was_hyphen = true;
}
}
segment.truncate(MAX_SEGMENT_LENGTH);
if segment.ends_with('-') {
segment.pop();
}
segment
}
pub fn sanitize_alias(email: &str) -> String {
let local_part = email.split('@').next().unwrap_or(email);
sanitize_segment(local_part)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(provider: Provider, port: u16, security: Option<ImapSecurity>) -> Identity {
Identity {
alias: "a".to_string(),
email: "a@example.com".to_string(),
provider,
host: "127.0.0.1".to_string(),
port,
secret: "s".to_string(),
max_imap_connections: None,
security,
}
}
#[test]
fn security_falls_back_to_the_provider_default_and_honours_an_override() {
assert_eq!(
sample(Provider::Proton, 1143, None).security(),
ImapSecurity::StartTls
);
assert_eq!(
sample(Provider::Gmail, 993, None).security(),
ImapSecurity::Tls
);
assert_eq!(
sample(Provider::Proton, 1143, Some(ImapSecurity::Tls)).security(),
ImapSecurity::Tls
);
}
#[test]
fn an_entry_written_before_cli_0162_has_no_security_and_still_loads() {
let identity: Identity = toml::from_str(
"alias = \"luna\"\nemail = \"luna@proton.me\"\nprovider = \"proton\"\n\
host = \"127.0.0.1\"\nport = 1143\nsecret = \"bridge\"\n",
)
.unwrap();
assert_eq!(identity.security, None);
assert_eq!(identity.security(), ImapSecurity::StartTls);
}
#[test]
fn sanitizes_dotted_local_part() {
assert_eq!(sanitize_alias("first.last@example.com"), "first-last");
}
#[test]
fn sanitizes_plus_addressing() {
assert_eq!(sanitize_alias("jane+work@example.com"), "jane-work");
}
#[test]
fn sanitize_segment_truncates_long_input_with_no_trailing_hyphen() {
let long_subject = "word ".repeat(50); let segment = sanitize_segment(&long_subject);
assert!(segment.len() <= MAX_SEGMENT_LENGTH);
assert!(!segment.ends_with('-'));
}
}