mod suppression;
pub use suppression::is_hard_bounce;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueueStatus {
Pending,
InFlight,
Delivered,
Failed,
Bounced,
}
impl QueueStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::InFlight => "inflight",
Self::Delivered => "delivered",
Self::Failed => "failed",
Self::Bounced => "bounced",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"pending" => Some(Self::Pending),
"inflight" => Some(Self::InFlight),
"delivered" => Some(Self::Delivered),
"failed" => Some(Self::Failed),
"bounced" => Some(Self::Bounced),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct QueuedMessage {
pub id: i64,
pub sender: String,
pub recipient: String,
pub domain: String,
pub message_data: Vec<u8>,
pub status: QueueStatus,
pub attempts: u32,
pub max_attempts: u32,
pub next_retry: i64,
pub last_error: Option<String>,
pub message_id: Option<String>,
pub created_at: i64,
pub updated_at: i64,
pub is_forwarded: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn queue_status_roundtrip() {
let variants = [
QueueStatus::Pending,
QueueStatus::InFlight,
QueueStatus::Delivered,
QueueStatus::Failed,
QueueStatus::Bounced,
];
for v in &variants {
let s = v.as_str();
let parsed = QueueStatus::parse(s).unwrap();
assert_eq!(&parsed, v, "roundtrip failed for {s}");
}
}
#[test]
fn queue_status_parse_unknown() {
assert_eq!(QueueStatus::parse("unknown"), None);
assert_eq!(QueueStatus::parse(""), None);
assert_eq!(QueueStatus::parse("PENDING"), None);
}
#[test]
fn queue_status_as_str_values() {
assert_eq!(QueueStatus::Pending.as_str(), "pending");
assert_eq!(QueueStatus::InFlight.as_str(), "inflight");
assert_eq!(QueueStatus::Delivered.as_str(), "delivered");
assert_eq!(QueueStatus::Failed.as_str(), "failed");
assert_eq!(QueueStatus::Bounced.as_str(), "bounced");
}
#[test]
fn queue_status_parse_case_sensitive() {
assert_eq!(QueueStatus::parse("Pending"), None);
assert_eq!(QueueStatus::parse("InFlight"), None);
assert_eq!(QueueStatus::parse("DELIVERED"), None);
assert_eq!(QueueStatus::parse("Failed"), None);
assert_eq!(QueueStatus::parse("Bounced"), None);
}
#[test]
fn queue_status_parse_whitespace_rejected() {
assert_eq!(QueueStatus::parse(" pending"), None);
assert_eq!(QueueStatus::parse("pending "), None);
assert_eq!(QueueStatus::parse(" "), None);
}
#[test]
fn queue_status_eq() {
assert_eq!(QueueStatus::Pending, QueueStatus::Pending);
assert_ne!(QueueStatus::Pending, QueueStatus::Delivered);
assert_ne!(QueueStatus::Failed, QueueStatus::Bounced);
}
#[test]
fn queue_status_clone() {
let s = QueueStatus::InFlight;
let c = s.clone();
assert_eq!(s, c);
}
#[test]
fn queued_message_clone_preserves_fields() {
let msg = QueuedMessage {
id: 42,
sender: "s@example.com".into(),
recipient: "r@remote.com".into(),
domain: "remote.com".into(),
message_data: vec![1, 2, 3],
status: QueueStatus::Pending,
attempts: 3,
max_attempts: 8,
next_retry: 1_700_000_000,
last_error: Some("temporary failure".into()),
message_id: Some("msg-id-123".into()),
created_at: 1_699_000_000,
updated_at: 1_699_500_000,
is_forwarded: true,
};
let cloned = msg.clone();
assert_eq!(cloned.id, 42);
assert_eq!(cloned.sender, "s@example.com");
assert_eq!(cloned.recipient, "r@remote.com");
assert_eq!(cloned.domain, "remote.com");
assert_eq!(cloned.message_data, vec![1, 2, 3]);
assert_eq!(cloned.attempts, 3);
assert_eq!(cloned.max_attempts, 8);
assert_eq!(cloned.next_retry, 1_700_000_000);
assert_eq!(cloned.last_error, Some("temporary failure".into()));
assert_eq!(cloned.message_id, Some("msg-id-123".into()));
assert!(cloned.is_forwarded);
}
#[test]
fn queued_message_no_last_error() {
let msg = QueuedMessage {
id: 1,
sender: "s@example.com".into(),
recipient: "r@remote.com".into(),
domain: "remote.com".into(),
message_data: vec![],
status: QueueStatus::Pending,
attempts: 0,
max_attempts: 8,
next_retry: 0,
last_error: None,
message_id: None,
created_at: 0,
updated_at: 0,
is_forwarded: false,
};
assert!(msg.last_error.is_none());
assert!(msg.message_id.is_none());
}
}