/// Message construction and enhanced serialization.
///
/// Provides helpers for building well-formed RFC 5322 messages and
/// serializing headers with proper folding and encoded-words.
///|
/// Build a Message from header name/value pairs and a body.
pub fn build_message(
headers : Array[(String, String)],
body : String,
) -> Message {
let hs : Array[Header] = []
for hi = 0; hi < headers.length(); hi = hi + 1 {
let (name, value) = headers[hi]
hs.push(Header::{ name, value })
}
let clean_body = body.trim(chars="\r\n").to_owned()
Message::{
headers: hs,
body: if clean_body.length() > 0 {
Some(clean_body)
} else {
None
},
mime_part: None,
}
}
///|
/// Build a plain-text message from common fields.
pub fn make_text_message(
from~ : String,
to~ : String,
subject~ : String,
body~ : String,
cc? : String = "",
) -> Message {
let headers : Array[(String, String)] = [("From", from), ("To", to)]
if cc.length() > 0 {
headers.push(("Cc", cc))
}
headers.push(("Subject", subject))
headers.push(("MIME-Version", "1.0"))
headers.push(("Content-Type", "text/plain; charset=utf-8"))
headers.push(("Content-Transfer-Encoding", "7bit"))
build_message(headers, body)
}
///|
/// Build an HTML message with a text/plain alternative part.
pub fn make_html_message(
from~ : String,
to~ : String,
subject~ : String,
text_body~ : String,
html_body~ : String,
) -> Message {
let boundary = "----=_mbt_alt_01"
let text_part = "Content-Type: text/plain; charset=utf-8\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"\r\n" +
text_body +
"\r\n"
let html_part = "Content-Type: text/html; charset=utf-8\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"\r\n" +
html_body +
"\r\n"
let body = "--" +
boundary +
"\r\n" +
text_part +
"--" +
boundary +
"\r\n" +
html_part +
"--" +
boundary +
"--\r\n"
let headers : Array[(String, String)] = [
("From", from),
("To", to),
("Subject", subject),
("MIME-Version", "1.0"),
("Content-Type", "multipart/alternative; boundary=\"" + boundary + "\""),
]
build_message(headers, body)
}
///|
/// Fold a header value at 78 columns using RFC 5322 folding rules.
/// Continuation lines start with a single space.
pub fn fold_header_value(name : String, value : String) -> String {
let first_line = name + ": " + value
if first_line.length() <= 78 {
return first_line
}
let sb = StringBuilder::new()
let mut col = 0
let mut i = 0
let len = value.length()
while i < len {
let c = value.get_char(i).unwrap()
if col >= 76 {
sb.write_string("\r\n ")
col = 0
}
sb.write_char(c)
col = col + 1
i = i + 1
}
name + ": " + sb.to_string()
}
///|
/// Serialize a header block (all headers + blank line).
pub fn serialize_header_block(headers : Array[Header]) -> String {
let sb = StringBuilder::new()
for h in headers {
let line = fold_header_value(h.name, h.value)
sb.write_string(line)
sb.write_string("\r\n")
}
sb.write_string("\r\n")
sb.to_string()
}
///|
/// Encode a header value for output: non-ASCII text becomes an
/// RFC 2047 encoded-word. ASCII-only values pass through unchanged.
pub fn encode_header_for_output(value : String) -> String {
if is_ascii_only(value) {
value
} else {
encode_encoded_word_b(value)
}
}
///|
/// Serialize a list of mailboxes into a header value string.
pub fn serialize_mailboxes(mbs : Array[Mailbox]) -> String {
mailbox_list_to_string(mbs)
}
///|
/// Render a plain-text message (without MIME) to a string.
pub fn render_text_message(msg : Message) -> String {
message_to_string(msg)
}
///|
/// Render a message with folded headers.
pub fn render_folded_message(msg : Message) -> String {
let sb = StringBuilder::new()
for h in msg.headers {
let line = fold_header_value(h.name, h.value)
sb.write_string(line)
sb.write_string("\r\n")
}
sb.write_string("\r\n")
match msg.body {
Some(b) => {
sb.write_string(b)
if !b.has_suffix("\n") {
sb.write_string("\r\n")
}
}
None => ()
}
match msg.mime_part {
Some(_) => ()
None => ()
}
sb.to_string()
}
///|
/// Render a MIME part tree with proper boundaries and content encodings.
pub fn render_mime_tree(part : MimePart) -> String {
let sb = StringBuilder::new()
render_mime_part_tree(part, sb)
sb.to_string()
}
///|
/// Recursively render a MIME part tree.
fn render_mime_part_tree(part : MimePart, sb : StringBuilder) -> Unit {
// Write the part headers
for h in part.headers {
let line = fold_header_value(h.name, h.value)
sb.write_string(line)
sb.write_string("\r\n")
}
sb.write_string("\r\n")
match part.body {
Body::Text(text) => {
sb.write_string(text)
if !text.has_suffix("\n") {
sb.write_string("\r\n")
}
}
Body::Binary(bytes) => {
// Base64-encode with line wrapping via the codec module
let ints : Array[Int] = []
for i = 0; i < bytes.length(); i = i + 1 {
ints.push(bytes[i].to_int())
}
let encoded = b64_encode_bytes(ints)
sb.write_string(encoded)
if !encoded.has_suffix("\n") {
sb.write_string("\r\n")
}
}
Body::Multipart(parts) => {
let boundary = match find_boundary(part.headers) {
Some(b) => b
None => "----=_mbt_boundary_"
}
for p in parts {
sb.write_string("--")
sb.write_string(boundary)
sb.write_string("\r\n")
render_mime_part_tree(p, sb)
}
sb.write_string("--")
sb.write_string(boundary)
sb.write_string("--\r\n")
}
}
}
///|
/// Create a multipart/mixed message from text and attachment parts.
pub fn make_multipart_message(
from~ : String,
to~ : String,
subject~ : String,
parts~ : Array[MimePart],
) -> Message {
let boundary = "----=_mbt_mixed_01"
let body = StringBuilder::new()
for p in parts {
body.write_string("--")
body.write_string(boundary)
body.write_string("\r\n")
body.write_string(render_mime_tree(p))
}
body.write_string("--")
body.write_string(boundary)
body.write_string("--\r\n")
let headers : Array[(String, String)] = [
("From", from),
("To", to),
("Subject", subject),
("MIME-Version", "1.0"),
("Content-Type", "multipart/mixed; boundary=\"" + boundary + "\""),
]
build_message(headers, body.to_string())
}
///|
/// Serialize headers with RFC 2047 encoding applied to values.
pub fn serialize_encoded_headers(headers : Array[Header]) -> String {
let sb = StringBuilder::new()
for h in headers {
let encoded = encode_header_for_output(h.value)
let line = fold_header_value(h.name, encoded)
sb.write_string(line)
sb.write_string("\r\n")
}
sb.write_string("\r\n")
sb.to_string()
}
///|
/// Convert a ContentType into a header value string.
pub fn content_type_to_string(ct : ContentType) -> String {
let sb = StringBuilder::new()
sb.write_string(ct.media_type)
sb.write_string("/")
sb.write_string(ct.sub_type)
for pi = 0; pi < ct.params.length(); pi = pi + 1 {
let (k, v) = ct.params[pi]
sb.write_string("; ")
sb.write_string(k)
sb.write_string("=")
// Quote values containing special characters
let needs_quote = v.contains(";") ||
v.contains("\"") ||
v.contains(" ") ||
v.contains("(") ||
v.contains(")")
if needs_quote {
sb.write_string("\"")
sb.write_string(v)
sb.write_string("\"")
} else {
sb.write_string(v)
}
}
sb.to_string()
}
///|
/// Convert a ContentDisposition into a header value string.
pub fn disposition_to_string(cd : ContentDisposition) -> String {
let sb = StringBuilder::new()
sb.write_string(cd.disposition)
for pi = 0; pi < cd.params.length(); pi = pi + 1 {
let (k, v) = cd.params[pi]
sb.write_string("; ")
sb.write_string(k)
sb.write_string("=")
sb.write_string("\"")
sb.write_string(v)
sb.write_string("\"")
}
sb.to_string()
}
///|
/// Generate a random boundary string from a seed.
pub fn generate_boundary(seed? : Int = 0) -> String {
"----=_mbt_part_" +
seed.to_string() +
"_" +
(seed * 48271 % 1000000000).to_string()
}