• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 22049757745

15 Feb 2026 05:32PM UTC coverage: 42.487% (+0.3%) from 42.162%
22049757745

push

github

flatcap
merge: modules: encapsulate global data 2

 * mod: send init/cleanup/data
 * cmd: move my-header to send
 * send: encapsulate globals
 * mod: alias init/cleanup/data
 * cmd: move alternates,alias to alias
 * alias: encapsulate globals
 * cmd: move tag-formats,-transforms to email
 * email: encapsulate globals

102 of 138 new or added lines in 9 files covered. (73.91%)

2339 existing lines in 12 files now uncovered.

12020 of 28291 relevant lines covered (42.49%)

2766.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/imap/command.c
1
/**
2
 * @file
3
 * Send/receive commands to/from an IMAP server
4
 *
5
 * @authors
6
 * Copyright (C) 1996-2012 Michael R. Elkins <me@mutt.org>
7
 * Copyright (C) 1996-1999 Brandon Long <blong@fiction.net>
8
 * Copyright (C) 1999-2011 Brendan Cully <brendan@kublai.com>
9
 * Copyright (C) 2017-2025 Richard Russon <rich@flatcap.org>
10
 * Copyright (C) 2018 Mehdi Abaakouk <sileht@sileht.net>
11
 * Copyright (C) 2018-2023 Pietro Cerutti <gahr@gahr.ch>
12
 * Copyright (C) 2019 Fabian Groffen <grobian@gentoo.org>
13
 * Copyright (C) 2019 Federico Kircheis <federico.kircheis@gmail.com>
14
 * Copyright (C) 2019 Ian Zimmerman <itz@no-use.mooo.com>
15
 * Copyright (C) 2025 Thomas Adam <thomas@xteddy.org>
16
 * Copyright (C) 2025 Thomas Klausner <wiz@gatalith.at>
17
 *
18
 * @copyright
19
 * This program is free software: you can redistribute it and/or modify it under
20
 * the terms of the GNU General Public License as published by the Free Software
21
 * Foundation, either version 2 of the License, or (at your option) any later
22
 * version.
23
 *
24
 * This program is distributed in the hope that it will be useful, but WITHOUT
25
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
26
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
27
 * details.
28
 *
29
 * You should have received a copy of the GNU General Public License along with
30
 * this program.  If not, see <http://www.gnu.org/licenses/>.
31
 */
32

33
/**
34
 * @page imap_command Send/receive commands
35
 *
36
 * Send/receive commands to/from an IMAP server
37
 */
38

39
#include "config.h"
40
#include <errno.h>
41
#include <limits.h>
42
#include <stdbool.h>
43
#include <stdint.h>
44
#include <stdio.h>
45
#include <stdlib.h>
46
#include <string.h>
47
#include <time.h>
48
#include <unistd.h>
49
#include "private.h"
50
#include "mutt/lib.h"
51
#include "config/lib.h"
52
#include "email/lib.h"
53
#include "core/lib.h"
54
#include "conn/lib.h"
55
#include "commands/lib.h"
56
#include "adata.h"
57
#include "edata.h"
58
#include "mdata.h"
59
#include "msn.h"
60
#include "mx.h"
61

62
/// Default buffer size for IMAP commands
63
#define IMAP_CMD_BUFSIZE 512
64

65
/// Maximum number of reconnection attempts before giving up
66
#define IMAP_MAX_RETRIES 5
67

68
/// Maximum backoff delay in seconds between reconnection attempts
69
#define IMAP_MAX_BACKOFF 30
70

71
/// Threshold in seconds after which to consider a connection potentially stale
72
#define IMAP_CONN_STALE_THRESHOLD 300
73

74
/**
75
 * Capabilities - Server capabilities strings that we understand
76
 *
77
 * @note This must be kept in the same order as ImapCaps.
78
 */
79
static const char *const Capabilities[] = {
80
  "IMAP4",
81
  "IMAP4rev1",
82
  "STATUS",
83
  "ACL",
84
  "NAMESPACE",
85
  "AUTH=CRAM-MD5",
86
  "AUTH=GSSAPI",
87
  "AUTH=ANONYMOUS",
88
  "AUTH=OAUTHBEARER",
89
  "AUTH=XOAUTH2",
90
  "STARTTLS",
91
  "LOGINDISABLED",
92
  "IDLE",
93
  "SASL-IR",
94
  "ENABLE",
95
  "CONDSTORE",
96
  "QRESYNC",
97
  "LIST-EXTENDED",
98
  "COMPRESS=DEFLATE",
99
  "X-GM-EXT-1",
100
  "ID",
101
  NULL,
102
};
103

104
/**
105
 * cmd_queue_full - Is the IMAP command queue full?
106
 * @param adata Imap Account data
107
 * @retval true Queue is full
108
 */
109
static bool cmd_queue_full(struct ImapAccountData *adata)
110
{
UNCOV
111
  if (((adata->nextcmd + 1) % adata->cmdslots) == adata->lastcmd)
×
112
    return true;
113

114
  return false;
115
}
116

117
/**
118
 * cmd_new - Create and queue a new command control block
119
 * @param adata Imap Account data
120
 * @retval NULL The pipeline is full
121
 * @retval ptr New command
122
 */
123
static struct ImapCommand *cmd_new(struct ImapAccountData *adata)
×
124
{
125
  struct ImapCommand *cmd = NULL;
126

127
  if (cmd_queue_full(adata))
128
  {
UNCOV
129
    mutt_debug(LL_DEBUG3, "IMAP command queue full\n");
×
130
    return NULL;
×
131
  }
132

UNCOV
133
  cmd = adata->cmds + adata->nextcmd;
×
UNCOV
134
  adata->nextcmd = (adata->nextcmd + 1) % adata->cmdslots;
×
135

UNCOV
136
  snprintf(cmd->seq, sizeof(cmd->seq), "%c%04u", adata->seqid, adata->seqno++);
×
UNCOV
137
  if (adata->seqno > 9999)
×
UNCOV
138
    adata->seqno = 0;
×
139

UNCOV
140
  cmd->state = IMAP_RES_NEW;
×
141

UNCOV
142
  return cmd;
×
143
}
144

145
/**
146
 * cmd_queue - Add a IMAP command to the queue
147
 * @param adata Imap Account data
148
 * @param cmdstr Command string
149
 * @param flags  Server flags, see #ImapCmdFlags
150
 * @retval  0 Success
151
 * @retval <0 Failure, e.g. #IMAP_RES_BAD
152
 *
153
 * If the queue is full, attempts to drain it.
154
 */
UNCOV
155
static int cmd_queue(struct ImapAccountData *adata, const char *cmdstr, ImapCmdFlags flags)
×
156
{
157
  if (cmd_queue_full(adata))
158
  {
UNCOV
159
    mutt_debug(LL_DEBUG3, "Draining IMAP command pipeline\n");
×
160

161
    const int rc = imap_exec(adata, NULL, flags & IMAP_CMD_POLL);
×
162

UNCOV
163
    if (rc == IMAP_EXEC_ERROR)
×
164
      return IMAP_RES_BAD;
165
  }
166

UNCOV
167
  struct ImapCommand *cmd = cmd_new(adata);
×
UNCOV
168
  if (!cmd)
×
169
    return IMAP_RES_BAD;
170

171
  if (buf_add_printf(&adata->cmdbuf, "%s %s\r\n", cmd->seq, cmdstr) < 0)
×
172
    return IMAP_RES_BAD;
173

174
  return 0;
175
}
176

177
/**
178
 * cmd_handle_fatal - When ImapAccountData is in fatal state, do what we can
179
 * @param adata Imap Account data
180
 *
181
 * Attempts to reconnect using exponential backoff (1s, 2s, 4s, 8s... max 30s).
182
 * Gives up after IMAP_MAX_RETRIES consecutive failures.
183
 */
184
static void cmd_handle_fatal(struct ImapAccountData *adata)
×
185
{
UNCOV
186
  adata->status = IMAP_FATAL;
×
187

UNCOV
188
  mutt_debug(LL_DEBUG1, "state=%d, status=%d, recovering=%d, retries=%d\n",
×
189
             adata->state, adata->status, adata->recovering, adata->retry_count);
190
  mutt_debug(LL_DEBUG1, "Connection: fd=%d, host=%s\n",
×
191
             adata->conn ? adata->conn->fd : -1,
192
             adata->conn ? adata->conn->account.host : "NULL");
193

194
  if (!adata->mailbox)
×
195
    return;
196

UNCOV
197
  struct ImapMboxData *mdata = adata->mailbox->mdata;
×
UNCOV
198
  if (!mdata)
×
199
    return;
200

UNCOV
201
  if ((adata->state >= IMAP_SELECTED) && (mdata->reopen & IMAP_REOPEN_ALLOW))
×
202
  {
UNCOV
203
    mx_fastclose_mailbox(adata->mailbox, true);
×
UNCOV
204
    mutt_error(_("Mailbox %s@%s closed"), adata->conn->account.user,
×
205
               adata->conn->account.host);
206
  }
207

UNCOV
208
  imap_close_connection(adata);
×
UNCOV
209
  if (!adata->recovering)
×
210
  {
211
    adata->recovering = true;
×
212

213
    /* Exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s */
214
    if (adata->retry_count > 0)
×
215
    {
216
      unsigned int delay = (adata->retry_count < 32) ?
217
                               (1U << (adata->retry_count - 1)) :
×
218
                               IMAP_MAX_BACKOFF;
UNCOV
219
      if (delay > IMAP_MAX_BACKOFF)
×
220
        delay = IMAP_MAX_BACKOFF;
UNCOV
221
      mutt_message(_("Connection lost. Retrying in %u seconds..."), delay);
×
UNCOV
222
      sleep(delay);
×
223
    }
224

UNCOV
225
    mutt_debug(LL_DEBUG1, "Attempting to reconnect to %s (attempt %d)\n",
×
226
               adata->conn ? adata->conn->account.host : "NULL", adata->retry_count + 1);
227

228
    if (imap_login(adata))
×
229
    {
UNCOV
230
      mutt_message(_("Reconnected to %s"), adata->conn->account.host);
×
231
      adata->retry_count = 0; // Reset on success
×
232
    }
233
    else
234
    {
UNCOV
235
      adata->retry_count++;
×
UNCOV
236
      mutt_debug(LL_DEBUG1, "Reconnection failed (attempt %d/%d)\n",
×
237
                 adata->retry_count, IMAP_MAX_RETRIES);
238

UNCOV
239
      if (adata->retry_count >= IMAP_MAX_RETRIES)
×
240
      {
UNCOV
241
        mutt_error(_("Failed to reconnect to %s after %d attempts"),
×
242
                   adata->conn->account.host, adata->retry_count);
243
        adata->retry_count = 0; // Reset for future attempts
×
244
      }
245
      else
246
      {
247
        mutt_error(_("Reconnection to %s failed. Will retry automatically."),
×
248
                   adata->conn->account.host);
249
      }
250
    }
UNCOV
251
    adata->recovering = false;
×
252
  }
253
}
254

255
/**
256
 * cmd_start - Start a new IMAP command
257
 * @param adata Imap Account data
258
 * @param cmdstr Command string
259
 * @param flags  Command flags, see #ImapCmdFlags
260
 * @retval  0 Success
261
 * @retval <0 Failure, e.g. #IMAP_RES_BAD
262
 */
263
static int cmd_start(struct ImapAccountData *adata, const char *cmdstr, ImapCmdFlags flags)
×
264
{
265
  int rc;
266

UNCOV
267
  if (adata->status == IMAP_FATAL)
×
268
  {
UNCOV
269
    cmd_handle_fatal(adata);
×
270
    return -1;
×
271
  }
272

UNCOV
273
  if (cmdstr && ((rc = cmd_queue(adata, cmdstr, flags)) < 0))
×
274
    return rc;
275

UNCOV
276
  if (flags & IMAP_CMD_QUEUE)
×
277
    return 0;
278

UNCOV
279
  if (buf_is_empty(&adata->cmdbuf))
×
280
    return IMAP_RES_BAD;
281

UNCOV
282
  rc = mutt_socket_send_d(adata->conn, adata->cmdbuf.data,
×
283
                          (flags & IMAP_CMD_PASS) ? IMAP_LOG_PASS : IMAP_LOG_CMD);
UNCOV
284
  buf_reset(&adata->cmdbuf);
×
285

286
  /* unidle when command queue is flushed */
UNCOV
287
  if (adata->state == IMAP_IDLE)
×
288
    adata->state = IMAP_SELECTED;
×
289

290
  return (rc < 0) ? IMAP_RES_BAD : 0;
×
291
}
292

293
/**
294
 * cmd_status - Parse response line for tagged OK/NO/BAD
295
 * @param s Status string from server
296
 * @retval  0 Success
297
 * @retval <0 Failure, e.g. #IMAP_RES_BAD
298
 */
UNCOV
299
static int cmd_status(const char *s)
×
300
{
301
  s = imap_next_word((char *) s);
×
302

UNCOV
303
  if (mutt_istr_startswith(s, "OK"))
×
304
    return IMAP_RES_OK;
UNCOV
305
  if (mutt_istr_startswith(s, "NO"))
×
306
    return IMAP_RES_NO;
×
307

308
  return IMAP_RES_BAD;
309
}
310

311
/**
312
 * cmd_parse_expunge - Parse expunge command
313
 * @param adata Imap Account data
314
 * @param s     String containing MSN of message to expunge
315
 *
316
 * cmd_parse_expunge: mark headers with new sequence ID and mark adata to be
317
 * reopened at our earliest convenience
318
 */
319
static void cmd_parse_expunge(struct ImapAccountData *adata, const char *s)
×
320
{
UNCOV
321
  unsigned int exp_msn = 0;
×
322
  struct Email *e = NULL;
323

UNCOV
324
  mutt_debug(LL_DEBUG2, "Handling EXPUNGE\n");
×
325

326
  struct ImapMboxData *mdata = adata->mailbox->mdata;
×
327
  if (!mdata)
×
UNCOV
328
    return;
×
329

UNCOV
330
  if (!mutt_str_atoui(s, &exp_msn) || (exp_msn < 1) ||
×
331
      (exp_msn > imap_msn_highest(&mdata->msn)))
×
332
  {
UNCOV
333
    return;
×
334
  }
335

336
  e = imap_msn_get(&mdata->msn, exp_msn - 1);
×
UNCOV
337
  if (e)
×
338
  {
339
    /* imap_expunge_mailbox() will rewrite e->index.
340
     * It needs to resort using EMAIL_SORT_UNSORTED anyway, so setting to INT_MAX
341
     * makes the code simpler and possibly more efficient. */
342
    e->index = INT_MAX;
×
343

UNCOV
344
    struct ImapEmailData *edata = imap_edata_get(e);
×
345
    if (edata)
×
UNCOV
346
      edata->msn = 0;
×
347
  }
348

349
  /* decrement seqno of those above. */
UNCOV
350
  const size_t max_msn = imap_msn_highest(&mdata->msn);
×
351
  for (unsigned int cur = exp_msn; cur < max_msn; cur++)
×
352
  {
UNCOV
353
    e = imap_msn_get(&mdata->msn, cur);
×
UNCOV
354
    if (e)
×
355
    {
UNCOV
356
      struct ImapEmailData *edata = imap_edata_get(e);
×
357
      if (edata)
×
358
        edata->msn--;
×
359
    }
UNCOV
360
    imap_msn_set(&mdata->msn, cur - 1, e);
×
361
  }
UNCOV
362
  imap_msn_shrink(&mdata->msn, 1);
×
363

UNCOV
364
  mdata->reopen |= IMAP_EXPUNGE_PENDING;
×
365
}
366

367
/**
368
 * cmd_parse_vanished - Parse vanished command
369
 * @param adata Imap Account data
370
 * @param s     String containing MSN of message to expunge
371
 *
372
 * Handle VANISHED (RFC7162), which is like expunge, but passes a seqset of UIDs.
373
 * An optional (EARLIER) argument specifies not to decrement subsequent MSNs.
374
 */
UNCOV
375
static void cmd_parse_vanished(struct ImapAccountData *adata, char *s)
×
376
{
377
  bool earlier = false;
378
  int rc;
UNCOV
379
  unsigned int uid = 0;
×
380

UNCOV
381
  struct ImapMboxData *mdata = adata->mailbox->mdata;
×
382
  if (!mdata)
×
UNCOV
383
    return;
×
384

385
  mutt_debug(LL_DEBUG2, "Handling VANISHED\n");
×
386

UNCOV
387
  if (mutt_istr_startswith(s, "(EARLIER)"))
×
388
  {
389
    /* The RFC says we should not decrement msns with the VANISHED EARLIER tag.
390
     * My experimentation says that's crap. */
391
    earlier = true;
UNCOV
392
    s = imap_next_word(s);
×
393
  }
394

395
  char *end_of_seqset = s;
UNCOV
396
  while (*end_of_seqset)
×
397
  {
398
    if (!strchr("0123456789:,", *end_of_seqset))
×
399
      *end_of_seqset = '\0';
×
400
    else
401
      end_of_seqset++;
×
402
  }
403

UNCOV
404
  struct SeqsetIterator *iter = mutt_seqset_iterator_new(s);
×
UNCOV
405
  if (!iter)
×
406
  {
UNCOV
407
    mutt_debug(LL_DEBUG2, "VANISHED: empty seqset [%s]?\n", s);
×
UNCOV
408
    return;
×
409
  }
410

UNCOV
411
  while ((rc = mutt_seqset_iterator_next(iter, &uid)) == 0)
×
412
  {
UNCOV
413
    struct Email *e = mutt_hash_int_find(mdata->uid_hash, uid);
×
UNCOV
414
    if (!e)
×
415
      continue;
×
416

UNCOV
417
    unsigned int exp_msn = imap_edata_get(e)->msn;
×
418

419
    /* imap_expunge_mailbox() will rewrite e->index.
420
     * It needs to resort using EMAIL_SORT_UNSORTED anyway, so setting to INT_MAX
421
     * makes the code simpler and possibly more efficient. */
UNCOV
422
    e->index = INT_MAX;
×
423
    imap_edata_get(e)->msn = 0;
×
424

425
    if ((exp_msn < 1) || (exp_msn > imap_msn_highest(&mdata->msn)))
×
426
    {
427
      mutt_debug(LL_DEBUG1, "VANISHED: msn for UID %u is incorrect\n", uid);
×
UNCOV
428
      continue;
×
429
    }
UNCOV
430
    if (imap_msn_get(&mdata->msn, exp_msn - 1) != e)
×
431
    {
432
      mutt_debug(LL_DEBUG1, "VANISHED: msn_index for UID %u is incorrect\n", uid);
×
UNCOV
433
      continue;
×
434
    }
435

UNCOV
436
    imap_msn_remove(&mdata->msn, exp_msn - 1);
×
437

438
    if (!earlier)
×
439
    {
440
      /* decrement seqno of those above. */
441
      const size_t max_msn = imap_msn_highest(&mdata->msn);
×
442
      for (unsigned int cur = exp_msn; cur < max_msn; cur++)
×
443
      {
444
        e = imap_msn_get(&mdata->msn, cur);
×
445
        if (e)
×
UNCOV
446
          imap_edata_get(e)->msn--;
×
UNCOV
447
        imap_msn_set(&mdata->msn, cur - 1, e);
×
448
      }
449

UNCOV
450
      imap_msn_shrink(&mdata->msn, 1);
×
451
    }
452
  }
453

UNCOV
454
  if (rc < 0)
×
455
    mutt_debug(LL_DEBUG1, "VANISHED: illegal seqset %s\n", s);
×
456

UNCOV
457
  mdata->reopen |= IMAP_EXPUNGE_PENDING;
×
458

UNCOV
459
  mutt_seqset_iterator_free(&iter);
×
460
}
461

462
/**
463
 * cmd_parse_fetch - Load fetch response into ImapAccountData
464
 * @param adata Imap Account data
465
 * @param s     String containing MSN of message to fetch
466
 *
467
 * Currently only handles unanticipated FETCH responses, and only FLAGS data.
468
 * We get these if another client has changed flags for a mailbox we've
469
 * selected.  Of course, a lot of code here duplicates code in message.c.
470
 */
UNCOV
471
static void cmd_parse_fetch(struct ImapAccountData *adata, char *s)
×
472
{
473
  unsigned int msn, uid;
474
  struct Email *e = NULL;
475
  char *flags = NULL;
476
  int uid_checked = 0;
477
  bool server_changes = false;
×
478

479
  struct ImapMboxData *mdata = imap_mdata_get(adata->mailbox);
×
480
  if (!mdata)
×
UNCOV
481
    return;
×
482

483
  mutt_debug(LL_DEBUG3, "Handling FETCH\n");
×
484

485
  if (!mutt_str_atoui(s, &msn))
×
486
  {
487
    mutt_debug(LL_DEBUG3, "Skipping FETCH response - illegal MSN\n");
×
UNCOV
488
    return;
×
489
  }
490

491
  if ((msn < 1) || (msn > imap_msn_highest(&mdata->msn)))
×
492
  {
UNCOV
493
    mutt_debug(LL_DEBUG3, "Skipping FETCH response - MSN %u out of range\n", msn);
×
UNCOV
494
    return;
×
495
  }
496

497
  e = imap_msn_get(&mdata->msn, msn - 1);
×
498
  if (!e || !e->active)
×
499
  {
UNCOV
500
    mutt_debug(LL_DEBUG3, "Skipping FETCH response - MSN %u not in msn_index\n", msn);
×
501
    return;
×
502
  }
503

504
  struct ImapEmailData *edata = imap_edata_get(e);
×
UNCOV
505
  if (!edata)
×
506
  {
507
    mutt_debug(LL_DEBUG3, "Skipping FETCH response - MSN %u missing edata\n", msn);
×
UNCOV
508
    return;
×
509
  }
510
  /* skip FETCH */
UNCOV
511
  s = imap_next_word(s);
×
512
  s = imap_next_word(s);
×
513

514
  if (*s != '(')
×
515
  {
516
    mutt_debug(LL_DEBUG1, "Malformed FETCH response\n");
×
517
    return;
×
518
  }
UNCOV
519
  s++;
×
520

521
  while (*s)
×
522
  {
523
    SKIPWS(s);
×
524
    size_t plen = mutt_istr_startswith(s, "FLAGS");
×
525
    if (plen != 0)
×
526
    {
527
      flags = s;
528
      if (uid_checked)
×
529
        break;
530

UNCOV
531
      s += plen;
×
532
      SKIPWS(s);
×
533
      if (*s != '(')
×
534
      {
UNCOV
535
        mutt_debug(LL_DEBUG1, "bogus FLAGS response: %s\n", s);
×
536
        return;
×
537
      }
UNCOV
538
      s++;
×
UNCOV
539
      while (*s && (*s != ')'))
×
540
        s++;
×
UNCOV
541
      if (*s == ')')
×
542
      {
543
        s++;
×
544
      }
545
      else
546
      {
547
        mutt_debug(LL_DEBUG1, "Unterminated FLAGS response: %s\n", s);
×
UNCOV
548
        return;
×
549
      }
550
    }
UNCOV
551
    else if ((plen = mutt_istr_startswith(s, "UID")))
×
552
    {
553
      s += plen;
×
554
      SKIPWS(s);
×
UNCOV
555
      if (!mutt_str_atoui(s, &uid))
×
556
      {
UNCOV
557
        mutt_debug(LL_DEBUG1, "Illegal UID.  Skipping update\n");
×
UNCOV
558
        return;
×
559
      }
UNCOV
560
      if (uid != edata->uid)
×
561
      {
UNCOV
562
        mutt_debug(LL_DEBUG1, "UID vs MSN mismatch.  Skipping update\n");
×
UNCOV
563
        return;
×
564
      }
565
      uid_checked = 1;
566
      if (flags)
×
567
        break;
568
      s = imap_next_word(s);
×
569
    }
570
    else if ((plen = mutt_istr_startswith(s, "MODSEQ")))
×
571
    {
572
      s += plen;
×
573
      SKIPWS(s);
×
574
      if (*s != '(')
×
575
      {
576
        mutt_debug(LL_DEBUG1, "bogus MODSEQ response: %s\n", s);
×
UNCOV
577
        return;
×
578
      }
UNCOV
579
      s++;
×
580
      while (*s && (*s != ')'))
×
UNCOV
581
        s++;
×
582
      if (*s == ')')
×
583
      {
UNCOV
584
        s++;
×
585
      }
586
      else
587
      {
UNCOV
588
        mutt_debug(LL_DEBUG1, "Unterminated MODSEQ response: %s\n", s);
×
UNCOV
589
        return;
×
590
      }
591
    }
592
    else if (*s == ')')
×
593
    {
594
      break; /* end of request */
595
    }
UNCOV
596
    else if (*s)
×
597
    {
UNCOV
598
      mutt_debug(LL_DEBUG2, "Only handle FLAGS updates\n");
×
599
      break;
×
600
    }
601
  }
602

UNCOV
603
  if (flags)
×
604
  {
605
    imap_set_flags(adata->mailbox, e, flags, &server_changes);
×
UNCOV
606
    if (server_changes)
×
607
    {
608
      /* If server flags could conflict with NeoMutt's flags, reopen the mailbox. */
UNCOV
609
      if (e->changed)
×
UNCOV
610
        mdata->reopen |= IMAP_EXPUNGE_PENDING;
×
611
      else
UNCOV
612
        mdata->check_status |= IMAP_FLAGS_PENDING;
×
613
    }
614
  }
615
}
616

617
/**
618
 * cmd_parse_capability - Set capability bits according to CAPABILITY response
619
 * @param adata Imap Account data
620
 * @param s     Command string with capabilities
621
 */
622
static void cmd_parse_capability(struct ImapAccountData *adata, char *s)
×
623
{
624
  mutt_debug(LL_DEBUG3, "Handling CAPABILITY\n");
×
625

626
  s = imap_next_word(s);
×
627
  char *bracket = strchr(s, ']');
×
628
  if (bracket)
×
629
    *bracket = '\0';
×
UNCOV
630
  FREE(&adata->capstr);
×
631
  adata->capstr = mutt_str_dup(s);
×
632
  adata->capabilities = 0;
×
633

UNCOV
634
  while (*s)
×
635
  {
UNCOV
636
    for (size_t i = 0; Capabilities[i]; i++)
×
637
    {
UNCOV
638
      size_t len = mutt_istr_startswith(s, Capabilities[i]);
×
639
      if (len != 0 && ((s[len] == '\0') || mutt_isspace(s[len])))
×
640
      {
641
        adata->capabilities |= (1 << i);
×
642
        mutt_debug(LL_DEBUG3, " Found capability \"%s\": %zu\n", Capabilities[i], i);
×
UNCOV
643
        break;
×
644
      }
645
    }
646
    s = imap_next_word(s);
×
647
  }
648
}
×
649

650
/**
651
 * cmd_parse_list - Parse a server LIST command (list mailboxes)
652
 * @param adata Imap Account data
653
 * @param s     Command string with folder list
654
 */
UNCOV
655
static void cmd_parse_list(struct ImapAccountData *adata, char *s)
×
656
{
657
  struct ImapList *list = NULL;
658
  struct ImapList lb = { 0 };
×
659
  unsigned int litlen;
660

UNCOV
661
  if (adata->cmdresult)
×
662
    list = adata->cmdresult;
663
  else
664
    list = &lb;
665

666
  memset(list, 0, sizeof(struct ImapList));
667

668
  /* flags */
UNCOV
669
  s = imap_next_word(s);
×
UNCOV
670
  if (*s != '(')
×
671
  {
UNCOV
672
    mutt_debug(LL_DEBUG1, "Bad LIST response\n");
×
673
    return;
×
674
  }
675
  s++;
×
676
  while (*s)
×
677
  {
678
    if (mutt_istr_startswith(s, "\\NoSelect"))
×
UNCOV
679
      list->noselect = true;
×
UNCOV
680
    else if (mutt_istr_startswith(s, "\\NonExistent")) /* rfc5258 */
×
681
      list->noselect = true;
×
UNCOV
682
    else if (mutt_istr_startswith(s, "\\NoInferiors"))
×
683
      list->noinferiors = true;
×
684
    else if (mutt_istr_startswith(s, "\\HasNoChildren")) /* rfc5258*/
×
UNCOV
685
      list->noinferiors = true;
×
686

UNCOV
687
    s = imap_next_word(s);
×
UNCOV
688
    if (*(s - 2) == ')')
×
689
      break;
690
  }
691

692
  /* Delimiter */
693
  if (!mutt_istr_startswith(s, "NIL"))
×
694
  {
695
    char delimbuf[5] = { 0 }; // worst case: "\\"\0
×
696
    snprintf(delimbuf, sizeof(delimbuf), "%s", s);
UNCOV
697
    imap_unquote_string(delimbuf);
×
698
    list->delim = delimbuf[0];
×
699
  }
700

701
  /* Name */
702
  s = imap_next_word(s);
×
703
  /* Notes often responds with literals here. We need a real tokenizer. */
UNCOV
704
  if (imap_get_literal_count(s, &litlen) == 0)
×
705
  {
706
    if (imap_cmd_step(adata) != IMAP_RES_CONTINUE)
×
707
    {
708
      adata->status = IMAP_FATAL;
×
709
      return;
×
710
    }
711

712
    if (strlen(adata->buf) < litlen)
×
713
    {
UNCOV
714
      mutt_debug(LL_DEBUG1, "Error parsing LIST mailbox\n");
×
715
      return;
×
716
    }
717

718
    list->name = adata->buf;
×
719
    s = list->name + litlen;
×
UNCOV
720
    if (s[0] != '\0')
×
721
    {
722
      s[0] = '\0';
×
UNCOV
723
      s++;
×
724
      SKIPWS(s);
×
725
    }
726
  }
727
  else
728
  {
729
    list->name = s;
×
730
    /* Exclude rfc5258 RECURSIVEMATCH CHILDINFO suffix */
UNCOV
731
    s = imap_next_word(s);
×
732
    if (s[0] != '\0')
×
733
      s[-1] = '\0';
×
UNCOV
734
    imap_unmunge_mbox_name(adata->unicode, list->name);
×
735
  }
736

UNCOV
737
  if (list->name[0] == '\0')
×
738
  {
UNCOV
739
    adata->delim = list->delim;
×
UNCOV
740
    mutt_debug(LL_DEBUG3, "Root delimiter: %c\n", adata->delim);
×
741
  }
742
}
743

744
/**
745
 * cmd_parse_lsub - Parse a server LSUB (list subscribed mailboxes)
746
 * @param adata Imap Account data
747
 * @param s     Command string with folder list
748
 */
749
static void cmd_parse_lsub(struct ImapAccountData *adata, char *s)
×
750
{
751
  if (adata->cmdresult)
×
752
  {
753
    /* caller will handle response itself */
UNCOV
754
    cmd_parse_list(adata, s);
×
755
    return;
×
756
  }
757

758
  const bool c_imap_check_subscribed = cs_subset_bool(NeoMutt->sub, "imap_check_subscribed");
×
759
  if (!c_imap_check_subscribed)
×
760
    return;
761

762
  struct ImapList list = { 0 };
×
763

764
  adata->cmdresult = &list;
×
765
  cmd_parse_list(adata, s);
×
766
  adata->cmdresult = NULL;
×
767
  /* noselect is for a gmail quirk */
768
  if (!list.name || list.noselect)
×
769
    return;
770

771
  mutt_debug(LL_DEBUG3, "Subscribing to %s\n", list.name);
×
772

773
  struct Buffer *buf = buf_pool_get();
×
774
  struct Buffer *err = buf_pool_get();
×
775
  struct Url url = { 0 };
×
776

777
  account_to_url(&adata->conn->account, &url);
×
778
  url.path = list.name;
×
779

780
  const char *const c_imap_user = cs_subset_string(NeoMutt->sub, "imap_user");
×
781
  if (mutt_str_equal(url.user, c_imap_user))
×
782
    url.user = NULL;
×
783
  url_tobuffer(&url, buf, U_NO_FLAGS);
×
784

785
  if (!mailbox_add_simple(buf_string(buf), err))
×
786
    mutt_debug(LL_DEBUG1, "Error adding subscribed mailbox: %s\n", buf_string(err));
×
787

UNCOV
788
  buf_pool_release(&buf);
×
UNCOV
789
  buf_pool_release(&err);
×
790
}
791

792
/**
793
 * cmd_parse_myrights - Set rights bits according to MYRIGHTS response
794
 * @param adata Imap Account data
795
 * @param s     Command string with rights info
796
 */
797
static void cmd_parse_myrights(struct ImapAccountData *adata, const char *s)
×
798
{
799
  mutt_debug(LL_DEBUG2, "Handling MYRIGHTS\n");
×
800

801
  s = imap_next_word((char *) s);
×
UNCOV
802
  s = imap_next_word((char *) s);
×
803

804
  /* zero out current rights set */
UNCOV
805
  adata->mailbox->rights = 0;
×
806

UNCOV
807
  while (*s && !mutt_isspace(*s))
×
808
  {
809
    switch (*s)
×
810
    {
811
      case 'a':
×
UNCOV
812
        adata->mailbox->rights |= MUTT_ACL_ADMIN;
×
UNCOV
813
        break;
×
UNCOV
814
      case 'e':
×
815
        adata->mailbox->rights |= MUTT_ACL_EXPUNGE;
×
UNCOV
816
        break;
×
817
      case 'i':
×
UNCOV
818
        adata->mailbox->rights |= MUTT_ACL_INSERT;
×
819
        break;
×
820
      case 'k':
×
821
        adata->mailbox->rights |= MUTT_ACL_CREATE;
×
UNCOV
822
        break;
×
UNCOV
823
      case 'l':
×
UNCOV
824
        adata->mailbox->rights |= MUTT_ACL_LOOKUP;
×
UNCOV
825
        break;
×
UNCOV
826
      case 'p':
×
UNCOV
827
        adata->mailbox->rights |= MUTT_ACL_POST;
×
UNCOV
828
        break;
×
UNCOV
829
      case 'r':
×
UNCOV
830
        adata->mailbox->rights |= MUTT_ACL_READ;
×
UNCOV
831
        break;
×
UNCOV
832
      case 's':
×
UNCOV
833
        adata->mailbox->rights |= MUTT_ACL_SEEN;
×
UNCOV
834
        break;
×
835
      case 't':
×
UNCOV
836
        adata->mailbox->rights |= MUTT_ACL_DELETE;
×
837
        break;
×
UNCOV
838
      case 'w':
×
839
        adata->mailbox->rights |= MUTT_ACL_WRITE;
×
UNCOV
840
        break;
×
UNCOV
841
      case 'x':
×
842
        adata->mailbox->rights |= MUTT_ACL_DELMX;
×
UNCOV
843
        break;
×
844

845
      /* obsolete rights */
846
      case 'c':
×
847
        adata->mailbox->rights |= MUTT_ACL_CREATE | MUTT_ACL_DELMX;
×
UNCOV
848
        break;
×
UNCOV
849
      case 'd':
×
850
        adata->mailbox->rights |= MUTT_ACL_DELETE | MUTT_ACL_EXPUNGE;
×
UNCOV
851
        break;
×
852
      default:
×
853
        mutt_debug(LL_DEBUG1, "Unknown right: %c\n", *s);
×
854
    }
UNCOV
855
    s++;
×
856
  }
857
}
×
858

859
/**
860
 * find_mailbox - Find a Mailbox by its name
861
 * @param adata Imap Account data
862
 * @param name  Mailbox to find
863
 * @retval ptr Mailbox
864
 */
865
static struct Mailbox *find_mailbox(struct ImapAccountData *adata, const char *name)
×
866
{
UNCOV
867
  if (!adata || !adata->account || !name)
×
868
    return NULL;
869

870
  struct Mailbox **mp = NULL;
871
  ARRAY_FOREACH(mp, &adata->account->mailboxes)
×
872
  {
873
    struct Mailbox *m = *mp;
×
874

UNCOV
875
    struct ImapMboxData *mdata = imap_mdata_get(m);
×
876
    if (mdata && mutt_str_equal(name, mdata->name))
×
877
      return m;
×
878
  }
879

880
  return NULL;
881
}
882

883
/**
884
 * cmd_parse_status - Parse status from server
885
 * @param adata Imap Account data
886
 * @param s     Command string with status info
887
 *
888
 * first cut: just do mailbox update. Later we may wish to cache all mailbox
889
 * information, even that not desired by mailbox
890
 */
UNCOV
891
static void cmd_parse_status(struct ImapAccountData *adata, char *s)
×
892
{
UNCOV
893
  unsigned int litlen = 0;
×
894

UNCOV
895
  char *mailbox = imap_next_word(s);
×
896

897
  /* We need a real tokenizer. */
UNCOV
898
  if (imap_get_literal_count(mailbox, &litlen) == 0)
×
899
  {
UNCOV
900
    if (imap_cmd_step(adata) != IMAP_RES_CONTINUE)
×
901
    {
UNCOV
902
      adata->status = IMAP_FATAL;
×
UNCOV
903
      return;
×
904
    }
905

906
    if (strlen(adata->buf) < litlen)
×
907
    {
UNCOV
908
      mutt_debug(LL_DEBUG1, "Error parsing STATUS mailbox\n");
×
UNCOV
909
      return;
×
910
    }
911

912
    mailbox = adata->buf;
913
    s = mailbox + litlen;
×
UNCOV
914
    s[0] = '\0';
×
UNCOV
915
    s++;
×
UNCOV
916
    SKIPWS(s);
×
917
  }
918
  else
919
  {
920
    s = imap_next_word(mailbox);
×
921
    s[-1] = '\0';
×
922
    imap_unmunge_mbox_name(adata->unicode, mailbox);
×
923
  }
924

UNCOV
925
  struct Mailbox *m = find_mailbox(adata, mailbox);
×
UNCOV
926
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
UNCOV
927
  if (!mdata)
×
928
  {
929
    mutt_debug(LL_DEBUG3, "Received status for an unexpected mailbox: %s\n", mailbox);
×
930
    return;
×
931
  }
932
  uint32_t olduv = mdata->uidvalidity;
×
UNCOV
933
  unsigned int oldun = mdata->uid_next;
×
934

UNCOV
935
  if (*s++ != '(')
×
936
  {
UNCOV
937
    mutt_debug(LL_DEBUG1, "Error parsing STATUS\n");
×
938
    return;
×
939
  }
UNCOV
940
  while ((s[0] != '\0') && (s[0] != ')'))
×
941
  {
942
    char *value = imap_next_word(s);
×
943

UNCOV
944
    errno = 0;
×
945
    const unsigned long ulcount = strtoul(value, &value, 10);
×
UNCOV
946
    const bool truncated = ((errno == ERANGE) && (ulcount == ULONG_MAX)) ||
×
947
                           ((unsigned int) ulcount != ulcount);
948
    const unsigned int count = (unsigned int) ulcount;
×
949

950
    // we accept truncating a larger value only for UIDVALIDITY, to accommodate
951
    // IMAP servers that use 64-bits for it. This seems to be what Thunderbird
952
    // is also doing, see #3830
953
    if (mutt_str_startswith(s, "UIDVALIDITY"))
×
954
    {
UNCOV
955
      if (truncated)
×
956
      {
957
        mutt_debug(LL_DEBUG1,
×
958
                   "UIDVALIDITY [%lu] exceeds 32 bits, "
959
                   "truncated to [%u]\n",
960
                   ulcount, count);
961
      }
962
      mdata->uidvalidity = count;
×
963
    }
964
    else
965
    {
966
      if (truncated)
×
967
      {
UNCOV
968
        mutt_debug(LL_DEBUG1, "Number in [%s] exceeds 32 bits\n", s);
×
UNCOV
969
        return;
×
970
      }
971
      else
972
      {
973
        if (mutt_str_startswith(s, "MESSAGES"))
×
974
          mdata->messages = count;
×
UNCOV
975
        else if (mutt_str_startswith(s, "RECENT"))
×
UNCOV
976
          mdata->recent = count;
×
UNCOV
977
        else if (mutt_str_startswith(s, "UIDNEXT"))
×
UNCOV
978
          mdata->uid_next = count;
×
UNCOV
979
        else if (mutt_str_startswith(s, "UNSEEN"))
×
UNCOV
980
          mdata->unseen = count;
×
981
      }
982
    }
983

984
    s = value;
×
UNCOV
985
    if ((s[0] != '\0') && (*s != ')'))
×
986
      s = imap_next_word(s);
×
987
  }
988
  mutt_debug(LL_DEBUG3, "%s (UIDVALIDITY: %u, UIDNEXT: %u) %d messages, %d recent, %d unseen\n",
×
989
             mdata->name, mdata->uidvalidity, mdata->uid_next, mdata->messages,
990
             mdata->recent, mdata->unseen);
991

992
  mutt_debug(LL_DEBUG3, "Running default STATUS handler\n");
×
993

UNCOV
994
  mutt_debug(LL_DEBUG3, "Found %s in mailbox list (OV: %u ON: %u U: %d)\n",
×
995
             mailbox, olduv, oldun, mdata->unseen);
996

997
  bool new_mail = false;
UNCOV
998
  const bool c_mail_check_recent = cs_subset_bool(NeoMutt->sub, "mail_check_recent");
×
UNCOV
999
  if (c_mail_check_recent)
×
1000
  {
UNCOV
1001
    if ((olduv != 0) && (olduv == mdata->uidvalidity))
×
1002
    {
UNCOV
1003
      if (oldun < mdata->uid_next)
×
1004
        new_mail = (mdata->unseen > 0);
×
1005
    }
UNCOV
1006
    else if ((olduv == 0) && (oldun == 0))
×
1007
    {
1008
      /* first check per session, use recent. might need a flag for this. */
1009
      new_mail = (mdata->recent > 0);
×
1010
    }
1011
    else
1012
    {
1013
      new_mail = (mdata->unseen > 0);
×
1014
    }
1015
  }
1016
  else
1017
  {
1018
    new_mail = (mdata->unseen > 0);
×
1019
  }
1020

UNCOV
1021
  m->has_new = new_mail;
×
1022
  m->msg_count = mdata->messages;
×
UNCOV
1023
  m->msg_unread = mdata->unseen;
×
1024

1025
  // force back to keep detecting new mail until the mailbox is opened
UNCOV
1026
  if (m->has_new)
×
UNCOV
1027
    mdata->uid_next = oldun;
×
1028

UNCOV
1029
  struct EventMailbox ev_m = { m };
×
UNCOV
1030
  notify_send(m->notify, NT_MAILBOX, NT_MAILBOX_CHANGE, &ev_m);
×
1031
}
1032

1033
/**
1034
 * cmd_parse_enabled - Record what the server has enabled
1035
 * @param adata Imap Account data
1036
 * @param s     Command string containing acceptable encodings
1037
 */
UNCOV
1038
static void cmd_parse_enabled(struct ImapAccountData *adata, const char *s)
×
1039
{
UNCOV
1040
  mutt_debug(LL_DEBUG2, "Handling ENABLED\n");
×
1041

UNCOV
1042
  while ((s = imap_next_word((char *) s)) && (*s != '\0'))
×
1043
  {
1044
    if (mutt_istr_startswith(s, "UTF8=ACCEPT") || mutt_istr_startswith(s, "UTF8=ONLY"))
×
1045
    {
1046
      adata->unicode = true;
×
1047
    }
UNCOV
1048
    if (mutt_istr_startswith(s, "QRESYNC"))
×
1049
      adata->qresync = true;
×
1050
  }
UNCOV
1051
}
×
1052

1053
/**
1054
 * cmd_parse_exists - Parse EXISTS message from serer
1055
 * @param adata  Imap Account data
1056
 * @param pn     String containing the total number of messages for the selected mailbox
1057
 */
1058
static void cmd_parse_exists(struct ImapAccountData *adata, const char *pn)
×
1059
{
1060
  unsigned int count = 0;
×
1061
  mutt_debug(LL_DEBUG2, "Handling EXISTS\n");
×
1062

UNCOV
1063
  if (!mutt_str_atoui(pn, &count))
×
1064
  {
UNCOV
1065
    mutt_debug(LL_DEBUG1, "Malformed EXISTS: '%s'\n", pn);
×
1066
    return;
×
1067
  }
1068

UNCOV
1069
  struct ImapMboxData *mdata = adata->mailbox->mdata;
×
1070
  if (!mdata)
×
1071
    return;
1072

1073
  /* new mail arrived */
1074
  if (count < imap_msn_highest(&mdata->msn))
×
1075
  {
1076
    /* Notes 6.0.3 has a tendency to report fewer messages exist than
1077
     * it should. */
1078
    mutt_debug(LL_DEBUG1, "Message count is out of sync\n");
×
1079
  }
1080
  else if (count == imap_msn_highest(&mdata->msn))
×
1081
  {
1082
    /* at least the InterChange server sends EXISTS messages freely,
1083
     * even when there is no new mail */
1084
    mutt_debug(LL_DEBUG3, "superfluous EXISTS message\n");
×
1085
  }
1086
  else
1087
  {
1088
    mutt_debug(LL_DEBUG2, "New mail in %s - %d messages total\n", mdata->name, count);
×
UNCOV
1089
    mdata->reopen |= IMAP_NEWMAIL_PENDING;
×
1090
    mdata->new_mail_count = count;
×
1091
  }
1092
}
1093

1094
/**
1095
 * cmd_handle_untagged - Fallback parser for otherwise unhandled messages
1096
 * @param adata Imap Account data
1097
 * @retval  0 Success
1098
 * @retval -1 Failure
1099
 */
1100
static int cmd_handle_untagged(struct ImapAccountData *adata)
×
1101
{
1102
  char *s = imap_next_word(adata->buf);
×
UNCOV
1103
  char *pn = imap_next_word(s);
×
1104

UNCOV
1105
  const bool c_imap_server_noise = cs_subset_bool(NeoMutt->sub, "imap_server_noise");
×
1106
  if ((adata->state >= IMAP_SELECTED) && mutt_isdigit(*s))
×
1107
  {
1108
    /* pn vs. s: need initial seqno */
1109
    pn = s;
UNCOV
1110
    s = imap_next_word(s);
×
1111

1112
    /* EXISTS, EXPUNGE, FETCH are always related to the SELECTED mailbox */
1113
    if (mutt_istr_startswith(s, "EXISTS"))
×
1114
      cmd_parse_exists(adata, pn);
×
1115
    else if (mutt_istr_startswith(s, "EXPUNGE"))
×
1116
      cmd_parse_expunge(adata, pn);
×
UNCOV
1117
    else if (mutt_istr_startswith(s, "FETCH"))
×
1118
      cmd_parse_fetch(adata, pn);
×
1119
  }
1120
  else if ((adata->state >= IMAP_SELECTED) && mutt_istr_startswith(s, "VANISHED"))
×
1121
  {
1122
    cmd_parse_vanished(adata, pn);
×
1123
  }
UNCOV
1124
  else if (mutt_istr_startswith(s, "CAPABILITY"))
×
1125
  {
UNCOV
1126
    cmd_parse_capability(adata, s);
×
1127
  }
UNCOV
1128
  else if (mutt_istr_startswith(s, "OK [CAPABILITY"))
×
1129
  {
UNCOV
1130
    cmd_parse_capability(adata, pn);
×
1131
  }
UNCOV
1132
  else if (mutt_istr_startswith(pn, "OK [CAPABILITY"))
×
1133
  {
UNCOV
1134
    cmd_parse_capability(adata, imap_next_word(pn));
×
1135
  }
UNCOV
1136
  else if (mutt_istr_startswith(s, "LIST"))
×
1137
  {
UNCOV
1138
    cmd_parse_list(adata, s);
×
1139
  }
1140
  else if (mutt_istr_startswith(s, "LSUB"))
×
1141
  {
1142
    cmd_parse_lsub(adata, s);
×
1143
  }
UNCOV
1144
  else if (mutt_istr_startswith(s, "MYRIGHTS"))
×
1145
  {
UNCOV
1146
    cmd_parse_myrights(adata, s);
×
1147
  }
UNCOV
1148
  else if (mutt_istr_startswith(s, "SEARCH"))
×
1149
  {
UNCOV
1150
    cmd_parse_search(adata, s);
×
1151
  }
UNCOV
1152
  else if (mutt_istr_startswith(s, "STATUS"))
×
1153
  {
1154
    cmd_parse_status(adata, s);
×
1155
  }
1156
  else if (mutt_istr_startswith(s, "ENABLED"))
×
1157
  {
UNCOV
1158
    cmd_parse_enabled(adata, s);
×
1159
  }
UNCOV
1160
  else if (mutt_istr_startswith(s, "BYE"))
×
1161
  {
UNCOV
1162
    mutt_debug(LL_DEBUG2, "Handling BYE\n");
×
1163

1164
    /* check if we're logging out */
1165
    if (adata->status == IMAP_BYE)
×
1166
      return 0;
1167

1168
    /* server shut down our connection */
UNCOV
1169
    s += 3;
×
UNCOV
1170
    SKIPWS(s);
×
UNCOV
1171
    mutt_error("%s", s);
×
UNCOV
1172
    cmd_handle_fatal(adata);
×
1173

UNCOV
1174
    return -1;
×
1175
  }
UNCOV
1176
  else if (c_imap_server_noise && mutt_istr_startswith(s, "NO"))
×
1177
  {
1178
    mutt_debug(LL_DEBUG2, "Handling untagged NO\n");
×
1179

1180
    /* Display the warning message from the server */
UNCOV
1181
    mutt_error("%s", s + 2);
×
1182
  }
1183

1184
  return 0;
1185
}
1186

1187
/**
1188
 * imap_cmd_start - Given an IMAP command, send it to the server
1189
 * @param adata Imap Account data
1190
 * @param cmdstr Command string to send
1191
 * @retval  0 Success
1192
 * @retval <0 Failure, e.g. #IMAP_RES_BAD
1193
 *
1194
 * If cmdstr is NULL, sends queued commands.
1195
 */
UNCOV
1196
int imap_cmd_start(struct ImapAccountData *adata, const char *cmdstr)
×
1197
{
1198
  return cmd_start(adata, cmdstr, IMAP_CMD_NO_FLAGS);
×
1199
}
1200

1201
/**
1202
 * imap_cmd_step - Reads server responses from an IMAP command
1203
 * @param adata Imap Account data
1204
 * @retval  0 Success
1205
 * @retval <0 Failure, e.g. #IMAP_RES_BAD
1206
 *
1207
 * detects tagged completion response, handles untagged messages, can read
1208
 * arbitrarily large strings (using malloc, so don't make it _too_ large!).
1209
 */
UNCOV
1210
int imap_cmd_step(struct ImapAccountData *adata)
×
1211
{
1212
  if (!adata)
×
1213
    return -1;
1214

1215
  size_t len = 0;
1216
  int c;
1217
  int rc;
1218
  int stillrunning = 0;
1219
  struct ImapCommand *cmd = NULL;
1220

UNCOV
1221
  if (adata->status == IMAP_FATAL)
×
1222
  {
UNCOV
1223
    cmd_handle_fatal(adata);
×
UNCOV
1224
    return IMAP_RES_BAD;
×
1225
  }
1226

1227
  /* read into buffer, expanding buffer as necessary until we have a full
1228
   * line */
1229
  do
1230
  {
UNCOV
1231
    if (len == adata->blen)
×
1232
    {
1233
      MUTT_MEM_REALLOC(&adata->buf, adata->blen + IMAP_CMD_BUFSIZE, char);
×
UNCOV
1234
      adata->blen = adata->blen + IMAP_CMD_BUFSIZE;
×
UNCOV
1235
      mutt_debug(LL_DEBUG3, "grew buffer to %zu bytes\n", adata->blen);
×
1236
    }
1237

1238
    /* back up over '\0' */
1239
    if (len)
×
UNCOV
1240
      len--;
×
1241

UNCOV
1242
    mutt_debug(LL_DEBUG3, "reading from socket (fd=%d, state=%d)\n",
×
1243
               adata->conn ? adata->conn->fd : -1, adata->state);
1244
    time_t read_start = mutt_date_now();
×
1245

1246
    c = mutt_socket_readln_d(adata->buf + len, adata->blen - len, adata->conn, MUTT_SOCK_LOG_FULL);
×
1247

1248
    time_t read_duration = mutt_date_now() - read_start;
×
UNCOV
1249
    if (read_duration > 1)
×
1250
    {
UNCOV
1251
      mutt_debug(LL_DEBUG1, "socket read took %ld seconds\n", (long) read_duration);
×
1252
    }
1253

UNCOV
1254
    if (c <= 0)
×
1255
    {
UNCOV
1256
      mutt_debug(LL_DEBUG1, "Error reading server response (rc=%d, errno=%d: %s)\n",
×
1257
                 c, errno, strerror(errno));
UNCOV
1258
      mutt_debug(LL_DEBUG1, "Connection state: fd=%d, state=%d, status=%d\n",
×
1259
                 adata->conn ? adata->conn->fd : -1, adata->state, adata->status);
1260
      cmd_handle_fatal(adata);
×
UNCOV
1261
      return IMAP_RES_BAD;
×
1262
    }
1263

UNCOV
1264
    len += c;
×
1265
  }
1266
  /* if we've read all the way to the end of the buffer, we haven't read a
1267
   * full line (mutt_socket_readln strips the \r, so we always have at least
1268
   * one character free when we've read a full line) */
1269
  while (len == adata->blen);
×
1270

1271
  /* don't let one large string make cmd->buf hog memory forever */
UNCOV
1272
  if ((adata->blen > IMAP_CMD_BUFSIZE) && (len <= IMAP_CMD_BUFSIZE))
×
1273
  {
UNCOV
1274
    MUTT_MEM_REALLOC(&adata->buf, IMAP_CMD_BUFSIZE, char);
×
UNCOV
1275
    adata->blen = IMAP_CMD_BUFSIZE;
×
UNCOV
1276
    mutt_debug(LL_DEBUG3, "shrank buffer to %zu bytes\n", adata->blen);
×
1277
  }
1278

UNCOV
1279
  adata->lastread = mutt_date_now();
×
1280

1281
  /* handle untagged messages. The caller still gets its shot afterwards. */
UNCOV
1282
  if ((mutt_str_startswith(adata->buf, "* ") ||
×
1283
       mutt_str_startswith(imap_next_word(adata->buf), "OK [")) &&
×
UNCOV
1284
      cmd_handle_untagged(adata))
×
1285
  {
1286
    return IMAP_RES_BAD;
1287
  }
1288

1289
  /* server demands a continuation response from us */
UNCOV
1290
  if (adata->buf[0] == '+')
×
1291
    return IMAP_RES_RESPOND;
1292

1293
  /* Look for tagged command completions.
1294
   *
1295
   * Some response handlers can end up recursively calling
1296
   * imap_cmd_step() and end up handling all tagged command
1297
   * completions.
1298
   * (e.g. FETCH->set_flag->set_header_color->~h pattern match.)
1299
   *
1300
   * Other callers don't even create an adata->cmds entry.
1301
   *
1302
   * For both these cases, we default to returning OK */
1303
  rc = IMAP_RES_OK;
1304
  c = adata->lastcmd;
×
1305
  do
1306
  {
1307
    cmd = &adata->cmds[c];
×
1308
    if (cmd->state == IMAP_RES_NEW)
×
1309
    {
UNCOV
1310
      if (mutt_str_startswith(adata->buf, cmd->seq))
×
1311
      {
1312
        if (!stillrunning)
×
1313
        {
1314
          /* first command in queue has finished - move queue pointer up */
UNCOV
1315
          adata->lastcmd = (adata->lastcmd + 1) % adata->cmdslots;
×
1316
        }
UNCOV
1317
        cmd->state = cmd_status(adata->buf);
×
1318
        rc = cmd->state;
UNCOV
1319
        if (cmd->state == IMAP_RES_NO || cmd->state == IMAP_RES_BAD)
×
1320
        {
UNCOV
1321
          mutt_message(_("IMAP command failed: %s"), adata->buf);
×
1322
        }
1323
      }
1324
      else
1325
      {
UNCOV
1326
        stillrunning++;
×
1327
      }
1328
    }
1329

UNCOV
1330
    c = (c + 1) % adata->cmdslots;
×
1331
  } while (c != adata->nextcmd);
×
1332

UNCOV
1333
  if (stillrunning)
×
1334
  {
1335
    rc = IMAP_RES_CONTINUE;
1336
  }
1337
  else
1338
  {
UNCOV
1339
    mutt_debug(LL_DEBUG3, "IMAP queue drained\n");
×
UNCOV
1340
    imap_cmd_finish(adata);
×
1341
  }
1342

1343
  return rc;
1344
}
1345

1346
/**
1347
 * imap_code - Was the command successful
1348
 * @param s IMAP command status
1349
 * @retval 1 Command result was OK
1350
 * @retval 0 NO or BAD
1351
 */
1352
bool imap_code(const char *s)
×
1353
{
UNCOV
1354
  return cmd_status(s) == IMAP_RES_OK;
×
1355
}
1356

1357
/**
1358
 * imap_cmd_trailer - Extra information after tagged command response if any
1359
 * @param adata Imap Account data
1360
 * @retval ptr Extra command information (pointer into adata->buf)
1361
 * @retval ""  Error (static string)
1362
 */
UNCOV
1363
const char *imap_cmd_trailer(struct ImapAccountData *adata)
×
1364
{
1365
  static const char *notrailer = "";
1366
  const char *s = adata->buf;
×
1367

1368
  if (!s)
×
1369
  {
UNCOV
1370
    mutt_debug(LL_DEBUG2, "not a tagged response\n");
×
1371
    return notrailer;
×
1372
  }
1373

UNCOV
1374
  s = imap_next_word((char *) s);
×
1375
  if (!s || (!mutt_istr_startswith(s, "OK") && !mutt_istr_startswith(s, "NO") &&
×
UNCOV
1376
             !mutt_istr_startswith(s, "BAD")))
×
1377
  {
1378
    mutt_debug(LL_DEBUG2, "not a command completion: %s\n", adata->buf);
×
1379
    return notrailer;
×
1380
  }
1381

UNCOV
1382
  s = imap_next_word((char *) s);
×
UNCOV
1383
  if (!s)
×
UNCOV
1384
    return notrailer;
×
1385

1386
  return s;
1387
}
1388

1389
/**
1390
 * imap_exec - Execute a command and wait for the response from the server
1391
 * @param adata Imap Account data
1392
 * @param cmdstr Command to execute
1393
 * @param flags  Flags, see #ImapCmdFlags
1394
 * @retval #IMAP_EXEC_SUCCESS Command successful or queued
1395
 * @retval #IMAP_EXEC_ERROR   Command returned an error
1396
 * @retval #IMAP_EXEC_FATAL   Imap connection failure
1397
 *
1398
 * Also, handle untagged responses.
1399
 */
1400
int imap_exec(struct ImapAccountData *adata, const char *cmdstr, ImapCmdFlags flags)
×
1401
{
1402
  if (!adata)
×
1403
    return IMAP_EXEC_ERROR;
1404

1405
  /* Check connection health before executing command */
UNCOV
1406
  if ((adata->state >= IMAP_AUTHENTICATED) && (adata->last_success > 0))
×
1407
  {
UNCOV
1408
    time_t now = mutt_date_now();
×
1409
    time_t idle_time = now - adata->last_success;
×
1410

UNCOV
1411
    if (idle_time > IMAP_CONN_STALE_THRESHOLD)
×
1412
    {
1413
      mutt_debug(LL_DEBUG2, "Connection idle for %ld seconds, sending NOOP to verify\n",
×
1414
                 (long) idle_time);
1415
      /* Connection may be stale - let the command proceed and handle any error */
1416
    }
1417
  }
1418

UNCOV
1419
  if (flags & IMAP_CMD_SINGLE)
×
1420
  {
1421
    // Process any existing commands
1422
    if (adata->nextcmd != adata->lastcmd)
×
1423
      imap_exec(adata, NULL, IMAP_CMD_POLL);
×
1424
  }
1425

1426
  int rc = cmd_start(adata, cmdstr, flags);
×
1427
  if (rc < 0)
×
1428
  {
UNCOV
1429
    cmd_handle_fatal(adata);
×
UNCOV
1430
    return IMAP_EXEC_FATAL;
×
1431
  }
1432

1433
  if (flags & IMAP_CMD_QUEUE)
×
1434
    return IMAP_EXEC_SUCCESS;
1435

1436
  const short c_imap_poll_timeout = cs_subset_number(NeoMutt->sub, "imap_poll_timeout");
×
1437
  if ((flags & IMAP_CMD_POLL) && (c_imap_poll_timeout > 0) &&
×
UNCOV
1438
      ((mutt_socket_poll(adata->conn, c_imap_poll_timeout)) == 0))
×
1439
  {
UNCOV
1440
    mutt_error(_("Connection to %s timed out"), adata->conn->account.host);
×
1441
    cmd_handle_fatal(adata);
×
UNCOV
1442
    return IMAP_EXEC_FATAL;
×
1443
  }
1444

1445
  /* Allow interruptions, particularly useful if there are network problems. */
1446
  mutt_sig_allow_interrupt(true);
×
1447
  do
1448
  {
1449
    rc = imap_cmd_step(adata);
×
1450
    // The queue is empty, so the single command has been processed
UNCOV
1451
    if ((flags & IMAP_CMD_SINGLE) && (adata->nextcmd == adata->lastcmd))
×
1452
      break;
1453
  } while (rc == IMAP_RES_CONTINUE);
×
UNCOV
1454
  mutt_sig_allow_interrupt(false);
×
1455

UNCOV
1456
  if (rc == IMAP_RES_NO)
×
1457
    return IMAP_EXEC_ERROR;
UNCOV
1458
  if (rc != IMAP_RES_OK)
×
1459
  {
UNCOV
1460
    if (adata->status != IMAP_FATAL)
×
1461
      return IMAP_EXEC_ERROR;
1462

UNCOV
1463
    mutt_debug(LL_DEBUG1, "command failed: %s\n", adata->buf);
×
UNCOV
1464
    return IMAP_EXEC_FATAL;
×
1465
  }
1466

1467
  /* Track successful command completion for connection health monitoring */
1468
  adata->last_success = mutt_date_now();
×
1469

UNCOV
1470
  return IMAP_EXEC_SUCCESS;
×
1471
}
1472

1473
/**
1474
 * imap_cmd_finish - Attempt to perform cleanup
1475
 * @param adata Imap Account data
1476
 *
1477
 * If a reopen is allowed, it attempts to perform cleanup (eg fetch new mail if
1478
 * detected, do expunge). Called automatically by imap_cmd_step(), but may be
1479
 * called at any time.
1480
 *
1481
 * mdata->check_status is set and will be used later by imap_check_mailbox().
1482
 */
1483
void imap_cmd_finish(struct ImapAccountData *adata)
×
1484
{
UNCOV
1485
  if (!adata)
×
1486
    return;
1487

UNCOV
1488
  if (adata->status == IMAP_FATAL)
×
1489
  {
UNCOV
1490
    adata->closing = false;
×
1491
    cmd_handle_fatal(adata);
×
UNCOV
1492
    return;
×
1493
  }
1494

UNCOV
1495
  if (!(adata->state >= IMAP_SELECTED) || (adata->mailbox && adata->closing))
×
1496
  {
1497
    adata->closing = false;
×
UNCOV
1498
    return;
×
1499
  }
1500

UNCOV
1501
  adata->closing = false;
×
1502

UNCOV
1503
  struct ImapMboxData *mdata = imap_mdata_get(adata->mailbox);
×
1504

UNCOV
1505
  if (mdata && mdata->reopen & IMAP_REOPEN_ALLOW)
×
1506
  {
1507
    // First remove expunged emails from the msn_index
UNCOV
1508
    if (mdata->reopen & IMAP_EXPUNGE_PENDING)
×
1509
    {
UNCOV
1510
      mutt_debug(LL_DEBUG2, "Expunging mailbox\n");
×
UNCOV
1511
      imap_expunge_mailbox(adata->mailbox, true);
×
1512
      /* Detect whether we've gotten unexpected EXPUNGE messages */
UNCOV
1513
      if (!(mdata->reopen & IMAP_EXPUNGE_EXPECTED))
×
UNCOV
1514
        mdata->check_status |= IMAP_EXPUNGE_PENDING;
×
UNCOV
1515
      mdata->reopen &= ~(IMAP_EXPUNGE_PENDING | IMAP_EXPUNGE_EXPECTED);
×
1516
    }
1517

1518
    // Then add new emails to it
UNCOV
1519
    if (mdata->reopen & IMAP_NEWMAIL_PENDING)
×
1520
    {
UNCOV
1521
      const size_t max_msn = imap_msn_highest(&mdata->msn);
×
UNCOV
1522
      if (mdata->new_mail_count > max_msn)
×
1523
      {
UNCOV
1524
        if (!(mdata->reopen & IMAP_EXPUNGE_PENDING))
×
UNCOV
1525
          mdata->check_status |= IMAP_NEWMAIL_PENDING;
×
1526

UNCOV
1527
        mutt_debug(LL_DEBUG2, "Fetching new mails from %zd to %u\n",
×
1528
                   max_msn + 1, mdata->new_mail_count);
UNCOV
1529
        imap_read_headers(adata->mailbox, max_msn + 1, mdata->new_mail_count, false);
×
1530
      }
1531
    }
1532

1533
    // And to finish inform about MUTT_REOPEN if needed
UNCOV
1534
    if (mdata->reopen & IMAP_EXPUNGE_PENDING && !(mdata->reopen & IMAP_EXPUNGE_EXPECTED))
×
UNCOV
1535
      mdata->check_status |= IMAP_EXPUNGE_PENDING;
×
1536

UNCOV
1537
    if (mdata->reopen & IMAP_EXPUNGE_PENDING)
×
UNCOV
1538
      mdata->reopen &= ~(IMAP_EXPUNGE_PENDING | IMAP_EXPUNGE_EXPECTED);
×
1539
  }
1540

UNCOV
1541
  adata->status = 0;
×
1542
}
1543

1544
/**
1545
 * imap_cmd_idle - Enter the IDLE state
1546
 * @param adata Imap Account data
1547
 * @retval  0 Success
1548
 * @retval <0 Failure, e.g. #IMAP_RES_BAD
1549
 */
UNCOV
1550
int imap_cmd_idle(struct ImapAccountData *adata)
×
1551
{
1552
  int rc;
1553

UNCOV
1554
  mutt_debug(LL_DEBUG2, "Entering IDLE mode for %s\n",
×
1555
             adata->conn ? adata->conn->account.host : "NULL");
1556

UNCOV
1557
  if (cmd_start(adata, "IDLE", IMAP_CMD_POLL) < 0)
×
1558
  {
UNCOV
1559
    mutt_debug(LL_DEBUG1, "Failed to send IDLE command\n");
×
UNCOV
1560
    cmd_handle_fatal(adata);
×
UNCOV
1561
    return -1;
×
1562
  }
1563

UNCOV
1564
  const short c_imap_poll_timeout = cs_subset_number(NeoMutt->sub, "imap_poll_timeout");
×
UNCOV
1565
  mutt_debug(LL_DEBUG2, "Waiting for IDLE continuation (timeout=%d)\n", c_imap_poll_timeout);
×
1566

UNCOV
1567
  if ((c_imap_poll_timeout > 0) &&
×
UNCOV
1568
      ((mutt_socket_poll(adata->conn, c_imap_poll_timeout)) == 0))
×
1569
  {
UNCOV
1570
    mutt_debug(LL_DEBUG1, "IDLE timed out waiting for server continuation response\n");
×
UNCOV
1571
    mutt_error(_("Connection to %s timed out waiting for IDLE response"),
×
1572
               adata->conn->account.host);
UNCOV
1573
    cmd_handle_fatal(adata);
×
UNCOV
1574
    return -1;
×
1575
  }
1576

1577
  do
1578
  {
UNCOV
1579
    rc = imap_cmd_step(adata);
×
UNCOV
1580
  } while (rc == IMAP_RES_CONTINUE);
×
1581

UNCOV
1582
  if (rc == IMAP_RES_RESPOND)
×
1583
  {
1584
    /* successfully entered IDLE state */
UNCOV
1585
    adata->state = IMAP_IDLE;
×
1586
    /* queue automatic exit when next command is issued */
UNCOV
1587
    buf_addstr(&adata->cmdbuf, "DONE\r\n");
×
UNCOV
1588
    mutt_debug(LL_DEBUG2, "Successfully entered IDLE state\n");
×
1589
    rc = IMAP_RES_OK;
1590
  }
UNCOV
1591
  if (rc != IMAP_RES_OK)
×
1592
  {
UNCOV
1593
    mutt_debug(LL_DEBUG1, "IDLE command failed with rc=%d (expected RESPOND=%d)\n",
×
1594
               rc, IMAP_RES_RESPOND);
UNCOV
1595
    mutt_error(_("IDLE command failed for %s"), adata->conn->account.host);
×
UNCOV
1596
    return -1;
×
1597
  }
1598

1599
  return 0;
1600
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc