imap: fix custom FETCH commands to handle literal responses

Custom IMAP commands using -X (e.g. 'FETCH 123 BODY[1]') were only
returning the first line of responses containing literals, instead of
the full multi-line body data.

The issue was that custom commands route through imap_perform_list()
and imap_state_listsearch_resp(), which didn't detect or handle IMAP
literal syntax {size}.

This commit adds literal detection to imap_state_listsearch_resp():
- Detects literal syntax {size} in untagged responses
- Writes the response header line containing the literal marker
- Handles any literal body data already in the pingpong buffer
- Sets up transfer layer to read remaining literal data from socket
- Configures maxdownload and transfer size to include header + body
- Initializes pp->overflow to 0 when no buffered data present
- Modifies imap_done() to transition to FETCH_FINAL for custom
  commands that set up downloads

Test 841 and 3206 verify.

Fixes #18847
Reported-by: BohwaZ
Bug: https://github.com/curl/curl/issues/18847
Closes #19246
This commit is contained in:
TheBitBrine 2025-10-26 04:39:02 +00:00 committed by Daniel Stenberg
parent 25aee8648a
commit e64c28e243
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 352 additions and 7 deletions

View file

@ -1197,8 +1197,97 @@ static CURLcode imap_state_listsearch_resp(struct Curl_easy *data,
(void)instate;
if(imapcode == '*')
result = Curl_client_write(data, CLIENTWRITE_BODY, line, len);
if(imapcode == '*') {
/* Check if this response contains a literal (e.g. FETCH responses with
body data). Literal syntax is {size}\r\n */
const char *cr = memchr(line, '\r', len);
size_t line_len = cr ? (size_t)(cr - line) : len;
const char *ptr = memchr(line, '{', line_len);
if(ptr) {
curl_off_t size = 0;
bool parsed = FALSE;
ptr++;
if(!curlx_str_number(&ptr, &size, CURL_OFF_T_MAX) &&
!curlx_str_single(&ptr, '}'))
parsed = TRUE;
if(parsed) {
struct pingpong *pp = &imapc->pp;
size_t buffer_len = curlx_dyn_len(&pp->recvbuf);
size_t after_header = buffer_len - pp->nfinal;
/* This is a literal response, setup to receive the body data */
infof(data, "Found %" FMT_OFF_T " bytes to download", size);
/* Progress size includes both header line and literal body */
Curl_pgrsSetDownloadSize(data, size + len);
/* First write the header line */
result = Curl_client_write(data, CLIENTWRITE_BODY, line, len);
if(result)
return result;
/* Handle data already in buffer after the header line */
if(after_header > 0) {
/* There is already data in the buffer that is part of the literal
body or subsequent responses */
size_t chunk = after_header;
/* Keep only the data after the header line */
curlx_dyn_tail(&pp->recvbuf, chunk);
pp->nfinal = 0; /* done */
/* Limit chunk to the literal size */
if(chunk > (size_t)size)
chunk = (size_t)size;
if(chunk) {
/* Write the literal body data */
result = Curl_client_write(data, CLIENTWRITE_BODY,
curlx_dyn_ptr(&pp->recvbuf), chunk);
if(result)
return result;
}
/* Handle remaining data in buffer (either more literal data or
subsequent responses) */
if(after_header > chunk) {
/* Keep the data after the literal body */
pp->overflow = after_header - chunk;
curlx_dyn_tail(&pp->recvbuf, pp->overflow);
}
else {
pp->overflow = 0;
curlx_dyn_reset(&pp->recvbuf);
}
}
else {
/* No data in buffer yet, reset overflow */
pp->overflow = 0;
}
if(data->req.bytecount == size + (curl_off_t)len)
/* All data already transferred (header + literal body) */
Curl_xfer_setup_nop(data);
else {
/* Setup to receive the literal body data.
maxdownload and transfer size include both header line and
literal body */
data->req.maxdownload = size + len;
Curl_xfer_setup_recv(data, FIRSTSOCKET, size + len);
}
/* End of DO phase */
imap_state(data, imapc, IMAP_STOP);
}
else {
/* Failed to parse literal, just write the line */
result = Curl_client_write(data, CLIENTWRITE_BODY, line, len);
}
}
else {
/* No literal, just write the line as-is */
result = Curl_client_write(data, CLIENTWRITE_BODY, line, len);
}
}
else if(imapcode != IMAP_RESP_OK)
result = CURLE_QUOTE_ERROR;
else
@ -1631,10 +1720,13 @@ static CURLcode imap_done(struct Curl_easy *data, CURLcode status,
connclose(conn, "IMAP done with bad status"); /* marked for closure */
result = status; /* use the already set error code */
}
else if(!data->set.connect_only && !imap->custom &&
(imap->uid || imap->mindex || data->state.upload ||
IS_MIME_POST(data))) {
/* Handle responses after FETCH or APPEND transfer has finished */
else if(!data->set.connect_only &&
((!imap->custom && (imap->uid || imap->mindex)) ||
(imap->custom && data->req.maxdownload > 0) ||
data->state.upload || IS_MIME_POST(data))) {
/* Handle responses after FETCH or APPEND transfer has finished.
For custom commands, check if we set up a download which indicates
a FETCH-like command with literal data. */
if(!data->state.upload && !IS_MIME_POST(data))
imap_state(data, imapc, IMAP_FETCH_FINAL);