1use mail_parser::{MessageParser, MimeHeaders};
8use rand::{Rng, rngs::OsRng};
9
10use crate::{
11 Error, PublicKey, Result, SecretKey,
12 message::{self, Decrypted},
13};
14
15pub const OBSCURED_SUBJECT: &str = "[...]";
17
18pub fn encrypt_message(raw: &[u8], recipients: &[&PublicKey], signer: Option<&SecretKey>) -> Result<Vec<u8>> {
21 let (header, body) = split(raw).ok_or(Error::NotMail)?;
22 let fields = fields(header);
23 let mut inner = Vec::with_capacity(raw.len() + 256);
24 let mut outer = Vec::with_capacity(header.len() + 256);
25 let mut content_type = false;
26 for field in &fields {
27 let name = field_name(field);
28 if name.eq_ignore_ascii_case("content-type") {
29 content_type = true;
30 inner.extend_from_slice(trim_newline(field));
31 inner.extend_from_slice(b"; protected-headers=\"v1\"\r\n");
32 continue;
33 }
34 inner.extend_from_slice(field);
35 if name.to_ascii_lowercase().starts_with("content-") || name.eq_ignore_ascii_case("mime-version") {
36 continue;
37 }
38 if name.eq_ignore_ascii_case("subject") {
39 outer.extend_from_slice(format!("Subject: {OBSCURED_SUBJECT}\r\n").as_bytes());
40 } else {
41 outer.extend_from_slice(field);
42 }
43 }
44 if !content_type {
45 inner.extend_from_slice(b"Content-Type: text/plain; charset=us-ascii; protected-headers=\"v1\"\r\n");
46 }
47 if fields.iter().any(|f| field_name(f).eq_ignore_ascii_case("subject")) {
48 inner.extend_from_slice(format!("HP-Outer: Subject: {OBSCURED_SUBJECT}\r\n").as_bytes());
49 }
50 inner.extend_from_slice(b"\r\n");
51 inner.extend_from_slice(body);
52
53 let armored = crlf(&message::encrypt(&inner, recipients, signer, true)?);
54 let boundary = boundary();
55 outer.extend_from_slice(
56 format!(
57 "MIME-Version: 1.0\r\nContent-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\"; \
58 boundary=\"{boundary}\"\r\n\r\nThis is an OpenPGP/MIME encrypted message (RFC 3156 and 9580).\r\n\
59 --{boundary}\r\nContent-Type: application/pgp-encrypted\r\nContent-Description: PGP/MIME version \
60 identification\r\n\r\nVersion: 1\r\n\r\n--{boundary}\r\nContent-Type: application/octet-stream; \
61 name=\"encrypted.asc\"\r\nContent-Description: OpenPGP encrypted message\r\nContent-Disposition: \
62 inline; filename=\"encrypted.asc\"\r\n\r\n"
63 )
64 .as_bytes(),
65 );
66 outer.extend_from_slice(&armored);
67 if !armored.ends_with(b"\r\n") {
68 outer.extend_from_slice(b"\r\n");
69 }
70 outer.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
71 Ok(outer)
72}
73
74pub fn is_encrypted(raw: &[u8]) -> bool {
76 encrypted_payload(raw).is_some()
77}
78
79pub fn decrypt_message(raw: &[u8], keys: &[&SecretKey], senders: &[&PublicKey]) -> Result<Decrypted> {
81 let payload = encrypted_payload(raw).ok_or(Error::NotEncrypted)?;
82 message::decrypt(&payload, keys, senders)
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct Summary {
89 pub subject: Option<String>,
90 pub preview: String,
91 pub has_attachment: bool,
92}
93
94pub fn summary(raw: &[u8]) -> Result<Summary> {
96 let message = MessageParser::default().parse(raw).ok_or(Error::NotMail)?;
97 let preview = message.body_preview(256).map(|p| p.into_owned()).unwrap_or_default();
98 Ok(Summary {
99 subject: message.subject().map(str::to_owned),
100 preview: preview.split_whitespace().collect::<Vec<_>>().join(" "),
101 has_attachment: message.attachment_count() > 0,
102 })
103}
104
105pub fn encrypted_summary(raw: &[u8], recipients: &[&PublicKey]) -> Result<Vec<u8>> {
107 let json = serde_json::to_vec(&summary(raw)?).map_err(|e| Error::Pgp(e.to_string()))?;
108 message::encrypt(&json, recipients, None, false)
109}
110
111pub fn decrypt_summary(data: &[u8], keys: &[&SecretKey]) -> Result<Summary> {
113 let decrypted = message::decrypt(data, keys, &[])?;
114 serde_json::from_slice(&decrypted.data).map_err(|_| Error::Decrypt)
115}
116
117fn encrypted_payload(raw: &[u8]) -> Option<Vec<u8>> {
120 let message = MessageParser::default().parse(raw)?;
121 let root = message.content_type()?;
122 let encrypted = root.ctype().eq_ignore_ascii_case("multipart")
123 && root.subtype().is_some_and(|s| s.eq_ignore_ascii_case("encrypted"))
124 && root.attribute("protocol").is_some_and(|p| p.eq_ignore_ascii_case("application/pgp-encrypted"));
125 if !encrypted {
126 return None;
127 }
128 let mail_parser::PartType::Multipart(children) = &message.root_part().body else { return None };
129 let part = message.part(*children.get(1)?)?;
130 let contents = part.contents();
131 message::is_armored(contents).then(|| contents.to_vec())
132}
133
134fn split(raw: &[u8]) -> Option<(&[u8], &[u8])> {
136 if let Some(i) = find(raw, b"\r\n\r\n") {
137 return Some((&raw[..i + 2], &raw[i + 4..]));
138 }
139 find(raw, b"\n\n").map(|i| (&raw[..i + 1], &raw[i + 2..]))
140}
141
142fn fields(header: &[u8]) -> Vec<Vec<u8>> {
144 let mut fields: Vec<Vec<u8>> = Vec::new();
145 for line in header.split_inclusive(|b| *b == b'\n') {
146 let mut line = trim_newline(line).to_vec();
147 line.extend_from_slice(b"\r\n");
148 match fields.last_mut() {
149 Some(field) if line.first().is_some_and(|b| *b == b' ' || *b == b'\t') => field.extend_from_slice(&line),
150 _ => fields.push(line),
151 }
152 }
153 fields
154}
155
156fn field_name(field: &[u8]) -> String {
157 let end = field.iter().position(|b| *b == b':').unwrap_or(field.len());
158 String::from_utf8_lossy(&field[..end]).trim().to_owned()
159}
160
161fn trim_newline(line: &[u8]) -> &[u8] {
162 let line = line.strip_suffix(b"\n").unwrap_or(line);
163 line.strip_suffix(b"\r").unwrap_or(line)
164}
165
166fn crlf(data: &[u8]) -> Vec<u8> {
167 let mut out = Vec::with_capacity(data.len() + data.len() / 32);
168 for line in data.split_inclusive(|b| *b == b'\n') {
169 out.extend_from_slice(trim_newline(line));
170 if line.ends_with(b"\n") {
171 out.extend_from_slice(b"\r\n");
172 }
173 }
174 out
175}
176
177fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
178 haystack.windows(needle.len()).position(|w| w == needle)
179}
180
181fn boundary() -> String {
182 let bytes: [u8; 16] = OsRng.r#gen();
183 format!("kernoid-{}", crate::keys::hex(&bytes))
184}