1use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Attachment {
19 pub content_id: String,
21 pub content_type: String,
23 pub bytes: Vec<u8>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct MtomMessage {
30 pub boundary: String,
32 pub envelope_xml: String,
34 pub attachments: Vec<Attachment>,
36}
37
38#[must_use]
41pub fn encode_mtom_packaging(msg: &MtomMessage) -> String {
42 let mut out = String::new();
43 let b = &msg.boundary;
44 out.push_str(&format!("--{b}\r\n"));
46 out.push_str(
47 "Content-Type: application/xop+xml; charset=UTF-8; type=\"application/soap+xml\"\r\n",
48 );
49 out.push_str("Content-Transfer-Encoding: 8bit\r\n");
50 out.push_str("Content-ID: <root.message@zerodds-soap>\r\n\r\n");
51 out.push_str(&msg.envelope_xml);
52 out.push_str("\r\n");
53 for att in &msg.attachments {
55 out.push_str(&format!("--{b}\r\n"));
56 out.push_str(&format!("Content-Type: {}\r\n", att.content_type));
57 out.push_str("Content-Transfer-Encoding: binary\r\n");
58 out.push_str(&format!("Content-ID: {}\r\n\r\n", att.content_id));
59 out.push_str(&hex_encode(&att.bytes));
63 out.push_str("\r\n");
64 }
65 out.push_str(&format!("--{b}--\r\n"));
66 out
67}
68
69#[must_use]
73pub fn xop_include(content_id: &str) -> String {
74 format!(
75 r#"<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="cid:{}"/>"#,
76 content_id.trim_start_matches('<').trim_end_matches('>')
77 )
78}
79
80fn hex_encode(b: &[u8]) -> String {
81 let mut out = String::with_capacity(b.len() * 2);
82 for byte in b {
83 let hi = (byte >> 4) & 0x0f;
84 let lo = byte & 0x0f;
85 out.push(hex_digit(hi));
86 out.push(hex_digit(lo));
87 }
88 out
89}
90
91fn hex_digit(n: u8) -> char {
92 match n {
93 0..=9 => char::from(b'0' + n),
94 10..=15 => char::from(b'A' + n - 10),
95 _ => '?',
96 }
97}
98
99impl MtomMessage {
100 #[must_use]
102 pub fn new(envelope_xml: String) -> Self {
103 Self {
104 boundary: "MIME_boundary_dds_soap".to_string(),
105 envelope_xml,
106 attachments: Vec::new(),
107 }
108 }
109
110 pub fn attach(&mut self, att: Attachment) {
112 self.attachments.push(att);
113 }
114}
115
116#[cfg(test)]
117#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
118mod tests {
119 use super::*;
120
121 fn sample_attachment() -> Attachment {
122 Attachment {
123 content_id: "<image1@demo>".into(),
124 content_type: "image/png".into(),
125 bytes: alloc::vec![0xCA, 0xFE, 0xBA, 0xBE],
126 }
127 }
128
129 #[test]
130 fn package_starts_with_xop_root_part() {
131 let mut msg = MtomMessage::new("<env/>".into());
132 msg.attach(sample_attachment());
133 let s = encode_mtom_packaging(&msg);
134 assert!(s.contains("application/xop+xml"));
135 assert!(s.contains("Content-ID: <root.message@zerodds-soap>"));
136 }
137
138 #[test]
139 fn package_includes_attachment_part() {
140 let mut msg = MtomMessage::new("<env/>".into());
141 msg.attach(sample_attachment());
142 let s = encode_mtom_packaging(&msg);
143 assert!(s.contains("Content-Type: image/png"));
144 assert!(s.contains("Content-ID: <image1@demo>"));
145 assert!(s.contains("CAFEBABE"));
146 }
147
148 #[test]
149 fn package_terminates_with_closing_boundary() {
150 let msg = MtomMessage::new("<env/>".into());
151 let s = encode_mtom_packaging(&msg);
152 assert!(s.ends_with("--MIME_boundary_dds_soap--\r\n"));
153 }
154
155 #[test]
156 fn xop_include_strips_angle_brackets() {
157 let s = xop_include("<image1@demo>");
158 assert!(s.contains("cid:image1@demo"));
159 assert!(!s.contains("<image1"));
160 }
161
162 #[test]
163 fn xop_include_namespace_is_correct() {
164 let s = xop_include("x");
165 assert!(s.contains("http://www.w3.org/2004/08/xop/include"));
166 }
167
168 #[test]
169 fn empty_attachments_still_produce_root_part() {
170 let msg = MtomMessage::new("<env/>".into());
171 let s = encode_mtom_packaging(&msg);
172 assert!(s.contains("application/xop+xml"));
173 assert!(s.contains("<env/>"));
174 }
175}