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
| 1 | use 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")] |
| 11 | use crate::{ |
| 12 | constant, |
| 13 | smtp::msg::SmtpMessage, |
| 14 | }; |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | #[cfg(feature = "async")] |
| 18 | use oxedyne_fe2o3_core::count::ErrorWhen; |
| 19 | |
| 20 | use std::{ |
| 21 | fmt, |
| 22 | }; |
| 23 | |
| 24 | #[cfg(feature = "async")] |
| 25 | use tokio::io::{ |
| 26 | AsyncRead, |
| 27 | AsyncBufRead, |
| 28 | }; |
| 29 | |
| 30 | |
| 31 | #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] |
| 32 | pub 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 | |
| 40 | impl 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")] |
| 54 | impl 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)] |
| 125 | pub 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 | |
| 157 | impl Default for EmailHeader { |
| 158 | fn default() -> Self { |
| 159 | Self::Other(String::new(), String::new()) |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | impl 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 | |
| 199 | impl 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)] |
| 261 | pub enum ContentDisposition { |
| 262 | Inline, |
| 263 | Attachment(Option<String>), |
| 264 | } |
| 265 | |
| 266 | impl 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 | |
| 278 | impl 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 | } |