daaki_imap/connection/append.rs
1#![allow(clippy::wildcard_imports)]
2use super::*;
3
4impl ImapConnection {
5 // -----------------------------------------------------------------------
6 // Append
7 // -----------------------------------------------------------------------
8
9 /// APPEND a message to a mailbox (RFC 3501 Section 6.3.11).
10 ///
11 /// Handles literal synchronization: sends header with `{count}\r\n`,
12 /// waits for `+` continuation, then sends literal data.
13 /// Uses LITERAL+ (RFC 7888 Section 4) `{count+}` when the
14 /// server advertises it.
15 ///
16 /// Returns `Some((uid_validity, uid))` when the server supports UIDPLUS
17 /// (RFC 4315) and includes an `[APPENDUID]` response code, otherwise `None`.
18 pub async fn append(
19 &self,
20 mailbox: &str,
21 flags: &[Flag],
22 date: Option<&str>,
23 message: &[u8],
24 timeout: Duration,
25 ) -> Result<Option<(u32, u32)>, Error> {
26 use super::dispatch::AppendConsumer;
27
28 self.check_utf8_only_enforced()?;
29 // RFC 3501 Section 6.3.11: APPEND is valid in Authenticated and Selected states.
30 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
31
32 // Check APPENDLIMIT (RFC 7889) — reject early if the message is too large.
33 // Compare in u64 space to avoid truncating the limit on 32-bit
34 // platforms where usize is 32 bits (RFC 7889 §5: number64).
35 {
36 let snap = self.state_rx.borrow();
37 for cap in &snap.capabilities {
38 if let Capability::AppendLimit(Some(limit)) = cap {
39 if (message.len() as u64) > *limit {
40 return Err(Error::AppendLimit {
41 size: message.len() as u64,
42 limit: *limit,
43 });
44 }
45 break;
46 }
47 }
48 }
49
50 let utf8_enabled = self.utf8_enabled();
51 let literal_kind = self.append_literal_kind(message)?;
52 let effective_non_sync = self.append_literal_is_non_sync(literal_kind, message.len());
53 // RFC 7888 Sections 4-5: determine the literal mode for the encoder.
54 let mode = self.literal_mode();
55
56 // Build the complete wire bytes as a single buffer.
57 // The driver will send them with literal synchronization handling.
58 let tag = self.next_prebuilt_tag();
59 // RFC 3501 Section 5.1.3 / RFC 9051 Section 5.1: encode mailbox name
60 // with INBOX normalization and MUTF-7 when not in UTF-8 mode.
61 let wire_mailbox = crate::codec::encode::encode_mailbox_str(mailbox, utf8_enabled);
62 let mut buf = BytesMut::new();
63 buf.extend_from_slice(tag.as_bytes());
64 buf.extend_from_slice(b" APPEND ");
65 // RFC 6855 Section 3: when UTF8=ACCEPT is active, the server MUST accept
66 // UTF-8 in quoted strings, so non-ASCII mailbox names can use quoted form
67 // instead of falling back to a synchronizing literal.
68 // RFC 7888 Sections 4-5: use non-synchronizing literal when available.
69 encode_quoted_or_literal_utf8(&mut buf, wire_mailbox.as_bytes(), utf8_enabled, mode);
70
71 // RFC 3501 Section 6.3.11 / RFC 9051 Section 6.3.12: \Recent is
72 // server-only and \* is not valid in APPEND flag lists. Filter them
73 // out just like encode_multi_append_header does.
74 let filtered_flags: Vec<&Flag> = flags
75 .iter()
76 .filter(|f| !matches!(f, Flag::Recent | Flag::Wildcard))
77 .collect();
78 // Validate custom flag keywords contain only ATOM-CHARs (RFC 3501 Section 9).
79 for flag in &filtered_flags {
80 if let Flag::Custom(s) = flag {
81 crate::codec::encode::validate_flag_keyword(s)?;
82 }
83 }
84 if !filtered_flags.is_empty() {
85 buf.extend_from_slice(b" (");
86 for (i, flag) in filtered_flags.iter().enumerate() {
87 if i > 0 {
88 buf.extend_from_slice(b" ");
89 }
90 buf.extend_from_slice(flag.as_imap_str().as_bytes());
91 }
92 buf.extend_from_slice(b")");
93 }
94
95 if let Some(d) = date {
96 // Validate against the date-time production (RFC 3501 Section 9).
97 crate::codec::encode::validate_append_datetime(d)?;
98 // Date-time is a quoted string (RFC 3501 Section 9).
99 buf.extend_from_slice(b" ");
100 // RFC 7888 Sections 4-5: use non-synchronizing literal when available.
101 encode_quoted_or_literal(&mut buf, d.as_bytes(), mode);
102 }
103
104 // Literal header.
105 // RFC 6855 Section 4: when UTF8=ACCEPT is enabled, use the UTF8
106 // APPEND data extension: `UTF8 (~{size}\r\n<message>)`.
107 // RFC 3516 Section 4.4: APPEND data containing NUL octets must use
108 // the `literal8` prefix `~`, not classic `literal` syntax.
109 // RFC 7888 Section 6: non-synchronizing literal8 (`~{N+}\r\n`) is
110 // only valid when BOTH BINARY and LITERAL+/LITERAL- permit it.
111 match literal_kind {
112 AppendLiteralKind::Utf8Literal8 => buf.extend_from_slice(b" UTF8 (~{"),
113 AppendLiteralKind::Literal8 => buf.extend_from_slice(b" ~{"),
114 AppendLiteralKind::Literal => buf.extend_from_slice(b" {"),
115 }
116 buf.extend_from_slice(message.len().to_string().as_bytes());
117 if effective_non_sync {
118 buf.extend_from_slice(b"+");
119 }
120 buf.extend_from_slice(b"}\r\n");
121
122 // Literal data + closing delimiter.
123 buf.extend_from_slice(message);
124 if utf8_enabled {
125 // RFC 6855 Section 4: close the UTF8 data extension group.
126 buf.extend_from_slice(b")\r\n");
127 } else {
128 buf.extend_from_slice(b"\r\n");
129 }
130
131 // Submit the pre-built bytes to the driver task.
132 tokio::time::timeout(
133 timeout,
134 self.submit_prebuilt(
135 buf,
136 tag,
137 crate::types::CommandKind::Append,
138 None,
139 AppendConsumer::default(),
140 ),
141 )
142 .await
143 .map_err(|_| Error::Timeout)?
144 }
145
146 /// MULTIAPPEND — append multiple messages in a single APPEND command (RFC 3502).
147 ///
148 /// Sends all messages as consecutive literals in one APPEND command.
149 /// Each message carries its own flags and optional internal date.
150 /// The first message includes the mailbox name; subsequent messages
151 /// follow immediately with their own flag/date/literal (RFC 3502 Section 3).
152 ///
153 /// Checks APPENDLIMIT (RFC 7889) per message and uses LITERAL+
154 /// (RFC 7888 Section 4) when the server advertises it.
155 ///
156 /// Returns a `Vec<(uid_validity, uid)>` extracted from `[APPENDUID]` response
157 /// codes (RFC 4315 UIDPLUS). The vec may be empty if the server does not
158 /// support UIDPLUS.
159 #[allow(clippy::too_many_lines)]
160 pub async fn multi_append(
161 &self,
162 mailbox: &str,
163 messages: &[AppendMessage],
164 timeout: Duration,
165 ) -> Result<Vec<(u32, u32)>, Error> {
166 use super::dispatch::MultiAppendConsumer;
167
168 self.check_utf8_only_enforced()?;
169 // RFC 3502 Section 3: MULTIAPPEND is valid in Authenticated and Selected states.
170 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
171
172 // Require MULTIAPPEND capability (RFC 3502 Section 3).
173 // Also snapshot capabilities for APPENDLIMIT + BINARY checks.
174 let (has_multiappend, append_limit, allow_literal8) = {
175 let snap = self.state_rx.borrow();
176 let has_multiappend = snap.capabilities.contains(&Capability::MultiAppend);
177 let append_limit: Option<u64> = snap.capabilities.iter().find_map(|cap| {
178 if let Capability::AppendLimit(Some(limit)) = cap {
179 Some(*limit)
180 } else {
181 None
182 }
183 });
184 // RFC 7888 Section 6 / RFC 9051 Section 9: literal8 may use
185 // non-synchronizing `+` only when BINARY is advertised AND the
186 // connection is NOT pure IMAP4rev2 (rev2 literal8 is always
187 // synchronizing).
188 let allow_literal8 = snap.capabilities.contains(&Capability::Binary)
189 && !super::auth::is_rev2_from_snapshot(&snap);
190 drop(snap);
191 (has_multiappend, append_limit, allow_literal8)
192 };
193
194 if !has_multiappend {
195 return Err(Error::MissingCapability("MULTIAPPEND".into()));
196 }
197
198 if messages.is_empty() {
199 return Err(Error::Protocol(
200 "MULTIAPPEND requires at least one message".into(),
201 ));
202 }
203
204 // Validate all message sizes up front.
205 // Compare in u64 space to avoid truncating the limit on 32-bit
206 // platforms where usize is 32 bits (RFC 7889 §5: number64).
207 if let Some(limit) = append_limit {
208 for msg in messages {
209 if (msg.data.len() as u64) > limit {
210 return Err(Error::AppendLimit {
211 size: msg.data.len() as u64,
212 limit,
213 });
214 }
215 }
216 }
217
218 let literal_kinds: Vec<AppendLiteralKind> = messages
219 .iter()
220 .map(|msg| self.append_literal_kind(&msg.data))
221 .collect::<Result<_, _>>()?;
222
223 let utf8_enabled = self.utf8_enabled();
224 let tag = self.next_prebuilt_tag();
225 // RFC 3501 Section 5.1.3 / RFC 9051 Section 5.1: encode mailbox name
226 // with INBOX normalization and MUTF-7 when not in UTF-8 mode.
227 let wire_mailbox = crate::codec::encode::encode_mailbox_str(mailbox, utf8_enabled);
228
229 // RFC 7888 Sections 4-5: determine the literal mode for the encoder.
230 let mode = self.literal_mode();
231
232 // Build the complete wire bytes for all messages.
233 let mut buf = BytesMut::new();
234
235 for (i, (msg, literal_kind)) in messages.iter().zip(literal_kinds.iter()).enumerate() {
236 // Build the header for this message (RFC 3502 Section 3).
237 let header_start = buf.len();
238 encode_multi_append_header_with_literal8(
239 &mut buf,
240 &tag,
241 &wire_mailbox,
242 &msg.flags,
243 msg.date.as_deref(),
244 msg.data.len(),
245 i == 0,
246 mode,
247 matches!(literal_kind, AppendLiteralKind::Utf8Literal8),
248 matches!(
249 literal_kind,
250 AppendLiteralKind::Literal8 | AppendLiteralKind::Utf8Literal8
251 ),
252 )?;
253
254 // RFC 7888 Section 6: when both BINARY and a literal extension are
255 // active, literal8 may use the non-synchronizing `+` modifier. The
256 // encoder conservatively emits synchronizing literal8, so patch the
257 // header before appending the literal data.
258 let header_bytes = buf.split_off(header_start);
259 let patched_header = match mode {
260 LiteralMode::LiteralPlus => {
261 patch_literals_to_plus_with_binary(&header_bytes, allow_literal8)
262 }
263 LiteralMode::LiteralMinus => {
264 patch_small_literals_to_plus_with_binary(&header_bytes, allow_literal8)
265 }
266 LiteralMode::Synchronizing => header_bytes,
267 };
268 buf.extend_from_slice(&patched_header);
269
270 // If the literal is synchronizing, the driver's
271 // send_with_literal_sync will detect the {N}\r\n boundary
272 // and wait for the server's `+` continuation before sending
273 // the literal data. Non-sync markers ({N+}\r\n) are sent
274 // without waiting.
275
276 // Literal data.
277 buf.extend_from_slice(&msg.data);
278 if utf8_enabled {
279 // RFC 6855 Section 4: close the UTF8 data extension group.
280 buf.extend_from_slice(b")");
281 }
282 if i == messages.len() - 1 {
283 // Final message — terminate the command with CRLF.
284 buf.extend_from_slice(b"\r\n");
285 }
286 }
287
288 // Submit the pre-built bytes to the driver task.
289 tokio::time::timeout(
290 timeout,
291 self.submit_prebuilt(
292 buf,
293 tag,
294 crate::types::CommandKind::Append,
295 None,
296 MultiAppendConsumer::default(),
297 ),
298 )
299 .await
300 .map_err(|_| Error::Timeout)?
301 }
302}