Skip to main content

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}