Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/email/msg.rs

11.0 KiB, 32 runs

created by r1870400018:569, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::{
2 charset::Charset,
3 media::{
4 ContentTypeValue,
5 MediaType,
6 },
7};
8// Only `EmailMessage::read` needs these, and it reads off the async SMTP line
9// reader (`smtp::msg`), which is itself gated -- so the imports follow the fn.
10#[cfg(feature = "async")]
11use crate::{
12 constant,
13 smtp::msg::SmtpMessage,
14};
15
16use oxedyne_fe2o3_core::prelude::*;
17#[cfg(feature = "async")]
18use oxedyne_fe2o3_core::count::ErrorWhen;
19
20use std::{
21 fmt,
22};
23
24#[cfg(feature = "async")]
25use tokio::io::{
26 AsyncRead,
27 AsyncBufRead,
28};
29
30
31#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
32pub struct EmailMessage {
33 pub from: String,
34 pub to: Vec<String>,
35 pub subject: String,
36 pub body: String,
37 pub headers: Vec<EmailHeader>,
38}
39
40impl fmt::Display for EmailMessage {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 ok!(writeln!(f, "From: {}", self.from));
43 ok!(writeln!(f, "To: {}", self.to.join(", ")));
44 ok!(writeln!(f, "Subject: {}", self.subject));
45 for header in &self.headers {
46 ok!(writeln!(f, "{}", header));
47 }
48 ok!(writeln!(f));
49 write!(f, "{}", self.body)
50 }
51}
52
53#[cfg(feature = "async")]
54impl EmailMessage {
55
56 pub async fn read<R: AsyncRead + AsyncBufRead + Unpin>(
57 stream: &mut R,
58 )
59 -> Outcome<EmailMessage>
60 {
61 let mut email = Self::default();
62 let mut in_headers = true;
63 let mut header_line = String::new();
64
65 let mut safety = ErrorWhen::new(constant::READ_LOOP_SAFETY_LIMIT);
66 loop {
67 res!(safety.inc());
68 let mut line = Vec::new();
69 let result = SmtpMessage::read_line(stream, &mut line).await;
70 let byts_read = res!(result);
71
72 if byts_read == 1 {
73 if line[0] == b'.' {
74 break;
75 }
76 }
77 if byts_read == 2 {
78 if line[0] == b'\r' && line[1] == b'\n' {
79 in_headers = false;
80 continue;
81 }
82 }
83
84 let line = String::from_utf8_lossy(&line);
85 debug!("### line={}", line);
86 let line = line.trim_end();
87
88 if line.is_empty() && in_headers {
89 in_headers = false;
90 continue;
91 }
92
93 if in_headers {
94 if line.starts_with(char::is_whitespace) {
95 // Accumulate header lines.
96 header_line.push(' ');
97 header_line.push_str(line.trim_start());
98 } else {
99 if !header_line.is_empty() {
100 match res!(EmailHeader::from_str(&header_line)) {
101 EmailHeader::From(value) => email.from = value,
102 EmailHeader::To(value) => email.to.push(value),
103 EmailHeader::Subject(value) => email.subject = value,
104 header => {
105 email.headers.push(header);
106 }
107 }
108 }
109 header_line = line.to_string();
110 }
111 } else {
112 // Accumulate body lines.
113 email.body.push_str(line);
114 email.body.push('\n');
115 }
116 }
117
118 Ok(email)
119 }
120
121}
122
123/// RFC 5322 Internet Message Format https://datatracker.ietf.org/doc/html/rfc5322
124#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
125pub enum EmailHeader {
126 From(String),
127 To(String),
128 Cc(String),
129 Bcc(String),
130 Subject(String),
131 Date(String),
132 MessageId(String),
133 InReplyTo(String),
134 References(String),
135 ResentFrom(String),
136 ResentTo(String),
137 ResentCc(String),
138 ResentBcc(String),
139 ResentDate(String),
140 ResentMessageId(String),
141 ReplyTo(String),
142 Sender(String),
143 ReturnPath(String),
144 ContentType(ContentTypeValue),
145 ContentTransferEncoding(String),
146 ContentDisposition(ContentDisposition),
147 ContentId(String),
148 MimeVersion(String),
149 XMailer(String),
150 XPriority(String),
151 XMsMailPriority(String),
152 Importance(String),
153 Received(String),
154 Other(String, String),
155}
156
157impl Default for EmailHeader {
158 fn default() -> Self {
159 Self::Other(String::new(), String::new())
160 }
161}
162
163impl fmt::Display for EmailHeader {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 EmailHeader::From(value) => write!(f, "From: {}", value),
167 EmailHeader::To(value) => write!(f, "To: {}", value),
168 EmailHeader::Cc(value) => write!(f, "Cc: {}", value),
169 EmailHeader::Bcc(value) => write!(f, "Bcc: {}", value),
170 EmailHeader::Subject(value) => write!(f, "Subject: {}", value),
171 EmailHeader::Date(value) => write!(f, "Date: {}", value),
172 EmailHeader::MessageId(value) => write!(f, "Message-ID: {}", value),
173 EmailHeader::InReplyTo(value) => write!(f, "In-Reply-To: {}", value),
174 EmailHeader::References(value) => write!(f, "References: {}", value),
175 EmailHeader::ResentFrom(value) => write!(f, "Resent-From: {}", value),
176 EmailHeader::ResentTo(value) => write!(f, "Resent-To: {}", value),
177 EmailHeader::ResentCc(value) => write!(f, "Resent-Cc: {}", value),
178 EmailHeader::ResentBcc(value) => write!(f, "Resent-Bcc: {}", value),
179 EmailHeader::ResentDate(value) => write!(f, "Resent-Date: {}", value),
180 EmailHeader::ResentMessageId(value) => write!(f, "Resent-Message-ID: {}", value),
181 EmailHeader::ReplyTo(value) => write!(f, "Reply-To: {}", value),
182 EmailHeader::Sender(value) => write!(f, "Sender: {}", value),
183 EmailHeader::ReturnPath(value) => write!(f, "Return-Path: {}", value),
184 EmailHeader::ContentType(content_type) => write!(f, "Content-Type: {}", content_type),
185 EmailHeader::ContentTransferEncoding(value) => write!(f, "Content-Transfer-Encoding: {}", value),
186 EmailHeader::ContentDisposition(disposition) => write!(f, "Content-Disposition: {}", disposition),
187 EmailHeader::ContentId(value) => write!(f, "Content-ID: {}", value),
188 EmailHeader::MimeVersion(value) => write!(f, "MIME-Version: {}", value),
189 EmailHeader::XMailer(value) => write!(f, "X-Mailer: {}", value),
190 EmailHeader::XPriority(value) => write!(f, "X-Priority: {}", value),
191 EmailHeader::XMsMailPriority(value) => write!(f, "X-MS-Mail-Priority: {}", value),
192 EmailHeader::Importance(value) => write!(f, "Importance: {}", value),
193 EmailHeader::Received(value) => write!(f, "Received: {}", value),
194 EmailHeader::Other(name, value) => write!(f, "{}: {}", name, value),
195 }
196 }
197}
198
199impl FromStr for EmailHeader {
200 type Err = Error<ErrTag>;
201
202 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 let parts: Vec<&str> = s.splitn(2, ':').map(str::trim).collect();
204 if parts.len() != 2 {
205 return Err(err!("Invalid email header format: {}", s; Invalid, Input));
206 }
207
208 let name = parts[0].to_lowercase();
209 let value = parts[1].to_string();
210
211 match name.as_str() {
212 "from" => Ok(EmailHeader::From(value)),
213 "to" => Ok(EmailHeader::To(value)),
214 "cc" => Ok(EmailHeader::Cc(value)),
215 "bcc" => Ok(EmailHeader::Bcc(value)),
216 "subject" => Ok(EmailHeader::Subject(value)),
217 "date" => Ok(EmailHeader::Date(value)),
218 "message-id" => Ok(EmailHeader::MessageId(value)),
219 "in-reply-to" => Ok(EmailHeader::InReplyTo(value)),
220 "references" => Ok(EmailHeader::References(value)),
221 "resent-from" => Ok(EmailHeader::ResentFrom(value)),
222 "resent-to" => Ok(EmailHeader::ResentTo(value)),
223 "resent-cc" => Ok(EmailHeader::ResentCc(value)),
224 "resent-bcc" => Ok(EmailHeader::ResentBcc(value)),
225 "resent-date" => Ok(EmailHeader::ResentDate(value)),
226 "resent-message-id" => Ok(EmailHeader::ResentMessageId(value)),
227 "reply-to" => Ok(EmailHeader::ReplyTo(value)),
228 "sender" => Ok(EmailHeader::Sender(value)),
229 "return-path" => Ok(EmailHeader::ReturnPath(value)),
230 "content-type" => {
231 let content_type = res!(MediaType::from_str(value.as_str()));
232 let content_type_value = match content_type {
233 MediaType::Text(ref _text) => {
234 ContentTypeValue::MediaType((content_type, Some(Charset::Utf_8)))
235 }
236 MediaType::Multipart(multipart) => {
237 ContentTypeValue::Multipart((multipart, String::new()))
238 }
239 _ => ContentTypeValue::MediaType((content_type, None)),
240 };
241 Ok(EmailHeader::ContentType(content_type_value))
242 },
243 "content-transfer-encoding" => Ok(EmailHeader::ContentTransferEncoding(value)),
244 "content-disposition" => {
245 let disposition = res!(ContentDisposition::from_str(&value));
246 Ok(EmailHeader::ContentDisposition(disposition))
247 },
248 "content-id" => Ok(EmailHeader::ContentId(value)),
249 "mime-version" => Ok(EmailHeader::MimeVersion(value)),
250 "x-mailer" => Ok(EmailHeader::XMailer(value)),
251 "x-priority" => Ok(EmailHeader::XPriority(value)),
252 "x-ms-mail-priority" => Ok(EmailHeader::XMsMailPriority(value)),
253 "importance" => Ok(EmailHeader::Importance(value)),
254 "received" => Ok(EmailHeader::Received(value)),
255 _ => Ok(EmailHeader::Other(name, value)),
256 }
257 }
258}
259
260#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
261pub enum ContentDisposition {
262 Inline,
263 Attachment(Option<String>),
264}
265
266impl fmt::Display for ContentDisposition {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 match self {
269 ContentDisposition::Inline => write!(f, "inline"),
270 ContentDisposition::Attachment(Some(filename)) => {
271 write!(f, "attachment; filename={}", filename)
272 }
273 ContentDisposition::Attachment(None) => write!(f, "attachment"),
274 }
275 }
276}
277
278impl FromStr for ContentDisposition {
279 type Err = Error<ErrTag>;
280
281 fn from_str(s: &str) -> Result<Self, Self::Err> {
282 let parts: Vec<&str> = s.split(';').map(str::trim).collect();
283 match parts[0].to_lowercase().as_str() {
284 "inline" => Ok(ContentDisposition::Inline),
285 "attachment" => {
286 if parts.len() > 1 {
287 let filename = parts[1].trim_start_matches("filename=").to_string();
288 Ok(ContentDisposition::Attachment(Some(filename)))
289 } else {
290 Ok(ContentDisposition::Attachment(None))
291 }
292 }
293 _ => Err(err!(
294 "Invalid Content-Disposition value: {}", s;
295 Invalid, Input)),
296 }
297 }
298}