/// Message-level header analysis.
///
/// Provides high-level helpers for working with parsed Message values:
/// - Message-ID, References, and In-Reply-To handling
/// - Received header chain analysis
/// - Structured access to From/To/Cc/Subject headers
/// - Subject decoding (RFC 2047)
///|
/// Parse a Message-ID or a space-separated list of Message-IDs.
/// Handles both "" and " " formats.
pub fn parse_message_ids(input : String) -> Array[MessageId] {
let results : Array[MessageId] = []
let s = input.trim(chars=" \t\r\n").to_owned()
if s.length() == 0 {
return results
}
// Find all <...> tokens by scanning for each '<' and its matching '>'
let len = s.length()
let mut i = 0
while i < len {
if s.get_char(i).unwrap() == '<' {
// Find the closing '>' after this position
let mut j = i + 1
while j < len && s.get_char(j).unwrap() != '>' {
j = j + 1
}
if j < len {
let id_str = s.unsafe_substring(start=i, end=j + 1)
let mid = parse_message_id(id_str)
match mid {
Some(m) => results.push(m)
None => ()
}
i = j + 1
continue
}
}
i = i + 1
}
results
}
///|
/// Parse a Received header value into structured ReceivedInfo.
///
/// Format: from a by b with c for d; date
/// Each clause is optional; date follows the ';'.
pub fn parse_received_header(value : String) -> ReceivedInfo {
let s = value.trim(chars=" \t\r\n").to_owned()
let mut from_str = ""
let mut by_str = ""
let mut with_str = ""
let mut for_str = ""
let mut date_str = ""
// Split at the first ';' to separate clauses from the date
let semi = s.find(";")
let (clause_part, date_part) = match semi {
None => (s, "")
Some(sp) =>
(
s.unsafe_substring(start=0, end=sp),
s
.unsafe_substring(start=sp + 1, end=s.length())
.trim(chars=" \t")
.to_owned(),
)
}
// Parse the date portion
let date_parsed = parse_email_date(date_part)
match date_parsed {
None => ()
Some(d) => date_str = email_date_to_string(d)
}
// Tokenize the clause part and extract known keywords
let tokens = split_ws(clause_part)
let mut i = 0
while i < tokens.length() {
let t = tokens[i]
if t == "from" && i + 1 < tokens.length() {
from_str = tokens[i + 1]
i = i + 2
} else if t == "by" && i + 1 < tokens.length() {
by_str = tokens[i + 1]
i = i + 2
} else if t == "with" && i + 1 < tokens.length() {
with_str = tokens[i + 1]
i = i + 2
} else if t == "for" && i + 1 < tokens.length() {
for_str = tokens[i + 1]
i = i + 2
} else {
i = i + 1
}
}
received_info(
from_=from_str,
by=by_str,
with_=with_str,
for_=for_str,
date=date_str,
)
}
///|
/// Split a string on whitespace into tokens.
fn split_ws(input : String) -> Array[String] {
let tokens : Array[String] = []
let mut sb = StringBuilder::new()
let mut in_token = false
for c in input {
if c == ' ' || c == '\t' {
if in_token {
tokens.push(sb.to_string())
sb = StringBuilder::new()
in_token = false
}
} else {
sb.write_char(c)
in_token = true
}
}
if in_token {
tokens.push(sb.to_string())
}
tokens
}
///|
/// Get the parsed From addresses of a message as Mailboxes.
pub fn message_from_mailboxes(msg : Message) -> Array[Mailbox] {
let value = get_header(msg, "From")
match value {
None => []
Some(v) => parse_mailbox_list(v)
}
}
///|
/// Get the parsed To addresses of a message as Mailboxes.
pub fn message_to_mailboxes(msg : Message) -> Array[Mailbox] {
let value = get_header(msg, "To")
match value {
None => []
Some(v) => parse_mailbox_list(v)
}
}
///|
/// Get the parsed Cc addresses of a message as Mailboxes.
pub fn message_cc_mailboxes(msg : Message) -> Array[Mailbox] {
let value = get_header(msg, "Cc")
match value {
None => []
Some(v) => parse_mailbox_list(v)
}
}
///|
/// Get the decoded Subject of a message.
/// RFC 2047 encoded-words are decoded to plain text.
pub fn message_subject(msg : Message) -> String {
let value = get_header(msg, "Subject")
match value {
None => ""
Some(v) => decode_header_value(v)
}
}
///|
/// Get the Message-ID of a message, if present.
pub fn message_message_id(msg : Message) -> MessageId? {
let value = get_header(msg, "Message-ID")
match value {
None => None
Some(v) => parse_message_id(v)
}
}
///|
/// Get the References header of a message as a list of MessageIds.
pub fn message_references(msg : Message) -> Array[MessageId] {
let value = get_header(msg, "References")
match value {
None => []
Some(v) => parse_message_ids(v)
}
}
///|
/// Get the In-Reply-To header of a message as a list of MessageIds.
pub fn message_in_reply_to(msg : Message) -> Array[MessageId] {
let value = get_header(msg, "In-Reply-To")
match value {
None => []
Some(v) => parse_message_ids(v)
}
}
///|
/// Get the Received chain of a message (topmost first).
pub fn message_received_chain(msg : Message) -> Array[ReceivedInfo] {
let results : Array[ReceivedInfo] = []
let values = get_headers(msg, "Received")
// Received headers are listed newest first; reverse for chronological order
let mut i = values.length() - 1
while i >= 0 {
let info = parse_received_header(values[i])
results.push(info)
i = i - 1
}
results
}
///|
/// Get the Reply-To addresses of a message, falling back to From.
pub fn message_reply_to(msg : Message) -> Array[Mailbox] {
let rt = get_header(msg, "Reply-To")
match rt {
Some(v) => parse_mailbox_list(v)
None => message_from_mailboxes(msg)
}
}
///|
/// Check whether the message is a reply (has In-Reply-To or References).
pub fn is_reply(msg : Message) -> Bool {
has_header(msg, "In-Reply-To") || has_header(msg, "References")
}
///|
/// Check whether the message is a bounce/undeliverable notice.
pub fn is_bounce(msg : Message) -> Bool {
has_header(msg, "Return-Path") ||
has_header(msg, "X-Failed-Recipients") ||
has_header(msg, "Diagnostic-Code")
}
///|
/// Get the Return-Path address, if present.
pub fn message_return_path(msg : Message) -> EmailAddress? {
let value = get_header(msg, "Return-Path")
match value {
None => None
Some(v) => {
let s = v.trim(chars=" \t\r\n").to_owned()
if s == "<>" {
None
} else {
parse_address(s)
}
}
}
}
///|
/// Get the Content-Type of a message as a decoded ContentType.
/// RFC 2231 extended parameters are merged.
pub fn message_decoded_content_type(msg : Message) -> ContentType? {
let value = get_header(msg, "Content-Type")
match value {
None => None
Some(v) => decode_content_type(v)
}
}
///|
/// Get the Content-Disposition of a message as a decoded ContentDisposition.
pub fn message_decoded_disposition(msg : Message) -> ContentDisposition? {
let value = get_header(msg, "Content-Disposition")
match value {
None => None
Some(v) => decode_content_disposition(v)
}
}
///|
/// Extract all email addresses from the From, To, Cc, and Bcc headers.
pub fn message_all_addresses(msg : Message) -> Array[Mailbox] {
let results : Array[Mailbox] = []
let from = message_from_mailboxes(msg)
for mb in from {
results.push(mb)
}
let to = message_to_mailboxes(msg)
for mb in to {
results.push(mb)
}
let cc = message_cc_mailboxes(msg)
for mb in cc {
results.push(mb)
}
let bcc = get_header(msg, "Bcc")
match bcc {
None => ()
Some(v) => {
let b = parse_mailbox_list(v)
for mb in b {
results.push(mb)
}
}
}
results
}
///|
/// Get the sender address (Sender header or From).
pub fn message_sender(msg : Message) -> Array[Mailbox] {
let value = get_header(msg, "Sender")
match value {
Some(v) => parse_mailbox_list(v)
None => message_from_mailboxes(msg)
}
}
///|
/// Generate a unique Message-ID for a new message.
/// Format:
/// The timestamp is derived from a caller-provided value so the function
/// stays pure and deterministic for testing.
pub fn generate_message_id(domain : String, timestamp? : Int = 0) -> MessageId {
let ts = timestamp
let rand = ts * 7919 + 104729
let id = "mbt." + ts.to_string() + "." + rand.to_string()
message_id(id~, domain~)
}
///|
/// Get the List-Unsubscribe header, if present.
pub fn message_list_unsubscribe(msg : Message) -> String? {
get_header(msg, "List-Unsubscribe")
}
///|
/// Check whether a message has a priority header.
pub fn message_priority(msg : Message) -> String? {
let pri = get_header(msg, "X-Priority")
match pri {
Some(p) => Some(p)
None => get_header(msg, "Priority")
}
}
///|
/// Get the number of headers in a message.
pub fn message_header_count(msg : Message) -> Int {
msg.headers.length()
}
///|
/// Summarize a message: Subject, From, and Date in one string.
pub fn summarize_message(msg : Message) -> String {
let subject = message_subject(msg)
let from = match message_from_mailboxes(msg) {
[] => "(unknown)"
mbs => mailbox_list_to_string(mbs)
}
let date = match message_date(msg) {
None => "(no date)"
Some(d) => email_date_to_iso(d)
}
"From: " + from + " | Subject: " + subject + " | Date: " + date
}