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

neomutt / neomutt / 17366932035

31 Aug 2025 12:06PM UTC coverage: 50.099% (+0.05%) from 50.049%
17366932035

push

github

web-flow
tweak observer event types (#4023)

9132 of 18228 relevant lines covered (50.1%)

272.36 hits per line

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

0.0
/imap/imap.c
1
/**
2
 * @file
3
 * IMAP network mailbox
4
 *
5
 * @authors
6
 * Copyright (C) 1996-1998,2012 Michael R. Elkins <me@mutt.org>
7
 * Copyright (C) 1996-1999 Brandon Long <blong@fiction.net>
8
 * Copyright (C) 1999-2009,2012,2017 Brendan Cully <brendan@kublai.com>
9
 * Copyright (C) 2017-2023 Richard Russon <rich@flatcap.org>
10
 * Copyright (C) 2018 Mehdi Abaakouk <sileht@sileht.net>
11
 * Copyright (C) 2018-2022 Pietro Cerutti <gahr@gahr.ch>
12
 * Copyright (C) 2019 Federico Kircheis <federico.kircheis@gmail.com>
13
 * Copyright (C) 2019 Ian Zimmerman <itz@no-use.mooo.com>
14
 * Copyright (C) 2019 Sergey Alirzaev <zl29ah@gmail.com>
15
 * Copyright (C) 2020 Reto Brunner <reto@slightlybroken.com>
16
 * Copyright (C) 2023 Anna Figueiredo Gomes <navi@vlhl.dev>
17
 * Copyright (C) 2023-2024 Dennis Schön <mail@dennis-schoen.de>
18
 *
19
 * @copyright
20
 * This program is free software: you can redistribute it and/or modify it under
21
 * the terms of the GNU General Public License as published by the Free Software
22
 * Foundation, either version 2 of the License, or (at your option) any later
23
 * version.
24
 *
25
 * This program is distributed in the hope that it will be useful, but WITHOUT
26
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
27
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
28
 * details.
29
 *
30
 * You should have received a copy of the GNU General Public License along with
31
 * this program.  If not, see <http://www.gnu.org/licenses/>.
32
 */
33

34
/**
35
 * @page imap_imap IMAP network mailbox
36
 *
37
 * Support for IMAP4rev1, with the occasional nod to IMAP 4.
38
 *
39
 * Implementation: #MxImapOps
40
 */
41

42
#include "config.h"
43
#include <limits.h>
44
#include <stdbool.h>
45
#include <stdint.h>
46
#include <stdio.h>
47
#include <string.h>
48
#include "private.h"
49
#include "mutt/lib.h"
50
#include "config/lib.h"
51
#include "email/lib.h"
52
#include "core/lib.h"
53
#include "conn/lib.h"
54
#include "mutt.h"
55
#include "lib.h"
56
#include "editor/lib.h"
57
#include "history/lib.h"
58
#include "parse/lib.h"
59
#include "progress/lib.h"
60
#include "question/lib.h"
61
#include "adata.h"
62
#include "auth.h"
63
#include "commands.h"
64
#include "edata.h"
65
#include "external.h"
66
#include "hook.h"
67
#include "mdata.h"
68
#include "msg_set.h"
69
#include "msn.h"
70
#include "mutt_logging.h"
71
#include "mutt_socket.h"
72
#include "muttlib.h"
73
#include "mx.h"
74
#ifdef ENABLE_NLS
75
#include <libintl.h>
76
#endif
77

78
struct Progress;
79
struct stat;
80

81
/**
82
 * ImapCommands - Imap Commands
83
 */
84
static const struct Command ImapCommands[] = {
85
  // clang-format off
86
  { "subscribe-to",     parse_subscribe_to,     0 },
87
  { "unsubscribe-from", parse_unsubscribe_from, 0 },
88
  { NULL, NULL, 0 },
89
  // clang-format on
90
};
91

92
/**
93
 * imap_init - Setup feature commands
94
 */
95
void imap_init(void)
×
96
{
97
  commands_register(&NeoMutt->commands, ImapCommands);
×
98
}
×
99

100
/**
101
 * check_capabilities - Make sure we can log in to this server
102
 * @param adata Imap Account data
103
 * @retval  0 Success
104
 * @retval -1 Failure
105
 */
106
static int check_capabilities(struct ImapAccountData *adata)
×
107
{
108
  if (imap_exec(adata, "CAPABILITY", IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
109
  {
110
    imap_error("check_capabilities", adata->buf);
×
111
    return -1;
×
112
  }
113

114
  if (!((adata->capabilities & IMAP_CAP_IMAP4) || (adata->capabilities & IMAP_CAP_IMAP4REV1)))
×
115
  {
116
    mutt_error(_("This IMAP server is ancient. NeoMutt does not work with it."));
×
117
    return -1;
×
118
  }
119

120
  return 0;
121
}
122

123
/**
124
 * get_flags - Make a simple list out of a FLAGS response
125
 * @param hflags List to store flags
126
 * @param s      String containing flags
127
 * @retval ptr  End of the flags
128
 * @retval NULL Failure
129
 *
130
 * return stream following FLAGS response
131
 */
132
static char *get_flags(struct ListHead *hflags, char *s)
×
133
{
134
  /* sanity-check string */
135
  const size_t plen = mutt_istr_startswith(s, "FLAGS");
×
136
  if (plen == 0)
×
137
  {
138
    mutt_debug(LL_DEBUG1, "not a FLAGS response: %s\n", s);
×
139
    return NULL;
×
140
  }
141
  s += plen;
×
142
  SKIPWS(s);
×
143
  if (*s != '(')
×
144
  {
145
    mutt_debug(LL_DEBUG1, "bogus FLAGS response: %s\n", s);
×
146
    return NULL;
×
147
  }
148

149
  /* update caller's flags handle */
150
  while (*s && (*s != ')'))
×
151
  {
152
    s++;
×
153
    SKIPWS(s);
×
154
    const char *flag_word = s;
155
    while (*s && (*s != ')') && !mutt_isspace(*s))
×
156
      s++;
×
157
    const char ctmp = *s;
×
158
    *s = '\0';
×
159
    if (*flag_word)
×
160
      mutt_list_insert_tail(hflags, mutt_str_dup(flag_word));
×
161
    *s = ctmp;
×
162
  }
163

164
  /* note bad flags response */
165
  if (*s != ')')
×
166
  {
167
    mutt_debug(LL_DEBUG1, "Unterminated FLAGS response: %s\n", s);
×
168
    mutt_list_free(hflags);
×
169

170
    return NULL;
×
171
  }
172

173
  s++;
×
174

175
  return s;
×
176
}
177

178
/**
179
 * set_flag - Append str to flags if we currently have permission according to aclflag
180
 * @param[in]  m       Selected Imap Mailbox
181
 * @param[in]  aclflag Permissions, see #AclFlags
182
 * @param[in]  flag    Does the email have the flag set?
183
 * @param[in]  str     Server flag name
184
 * @param[out] flags   Buffer for server command
185
 */
186
static void set_flag(struct Mailbox *m, AclFlags aclflag, bool flag,
×
187
                     const char *str, struct Buffer *flags)
188
{
189
  if (m->rights & aclflag)
×
190
    if (flag && imap_has_flag(&imap_mdata_get(m)->flags, str))
×
191
      buf_addstr(flags, str);
×
192
}
×
193

194
/**
195
 * compare_flags_for_copy - Compare local flags against the server
196
 * @param e Email
197
 * @retval true  Flags have changed
198
 * @retval false Flags match cached server flags
199
 *
200
 * The comparison of flags EXCLUDES the deleted flag.
201
 */
202
static bool compare_flags_for_copy(struct Email *e)
×
203
{
204
  struct ImapEmailData *edata = e->edata;
×
205

206
  if (e->read != edata->read)
×
207
    return true;
208
  if (e->old != edata->old)
×
209
    return true;
210
  if (e->flagged != edata->flagged)
×
211
    return true;
212
  if (e->replied != edata->replied)
×
213
    return true;
×
214

215
  return false;
216
}
217

218
/**
219
 * select_email_uids - Create a list of Email UIDs by type
220
 * @param emails     Array of Emails
221
 * @param num_emails Number of Emails in the array
222
 * @param flag       Flag type on which to filter, e.g. #MUTT_REPLIED
223
 * @param changed    Include only changed messages in message set
224
 * @param invert     Invert sense of flag, eg #MUTT_READ matches unread messages
225
 * @param uida       Array to fill with UIDs
226
 * @retval num Number of UIDs added
227
 * @retval  -1 Error
228
 */
229
static int select_email_uids(struct Email **emails, int num_emails, enum MessageType flag,
×
230
                             bool changed, bool invert, struct UidArray *uida)
231
{
232
  if (!emails || !uida)
×
233
    return -1;
234

235
  for (int i = 0; i < num_emails; i++)
×
236
  {
237
    struct Email *e = emails[i];
×
238
    if (changed && !e->changed)
×
239
      continue;
×
240

241
    /* don't include pending expunged messages.
242
     *
243
     * TODO: can we unset active in cmd_parse_expunge() and
244
     * cmd_parse_vanished() instead of checking for index != INT_MAX. */
245
    if (!e || !e->active || (e->index == INT_MAX))
×
246
      continue;
×
247

248
    struct ImapEmailData *edata = imap_edata_get(e);
×
249

250
    bool match = false;
251
    switch (flag)
×
252
    {
253
      case MUTT_DELETED:
×
254
        if (e->deleted != edata->deleted)
×
255
          match = invert ^ e->deleted;
×
256
        break;
257
      case MUTT_FLAG:
×
258
        if (e->flagged != edata->flagged)
×
259
          match = invert ^ e->flagged;
×
260
        break;
261
      case MUTT_OLD:
×
262
        if (e->old != edata->old)
×
263
          match = invert ^ e->old;
×
264
        break;
265
      case MUTT_READ:
×
266
        if (e->read != edata->read)
×
267
          match = invert ^ e->read;
×
268
        break;
269
      case MUTT_REPLIED:
×
270
        if (e->replied != edata->replied)
×
271
          match = invert ^ e->replied;
×
272
        break;
273
      case MUTT_TRASH:
×
274
        if (e->deleted && !e->purge)
×
275
          match = true;
276
        break;
277
      default:
278
        break;
279
    }
280

281
    if (match)
×
282
      ARRAY_ADD(uida, edata->uid);
×
283
  }
284

285
  return ARRAY_SIZE(uida);
×
286
}
287

288
/**
289
 * sync_helper - Sync flag changes to the server
290
 * @param m          Selected Imap Mailbox
291
 * @param emails     Array of Emails
292
 * @param num_emails Number of Emails in the array
293
 * @param right      ACL, see #AclFlags
294
 * @param flag       NeoMutt flag, e.g. #MUTT_DELETED
295
 * @param name       Name of server flag
296
 * @retval >=0 Success, number of messages
297
 * @retval  -1 Failure
298
 */
299
static int sync_helper(struct Mailbox *m, struct Email **emails, int num_emails,
×
300
                       AclFlags right, enum MessageType flag, const char *name)
301
{
302
  struct ImapAccountData *adata = imap_adata_get(m);
×
303
  if (!adata)
×
304
    return -1;
305

306
  if ((m->rights & right) == 0)
×
307
    return 0;
308

309
  if ((right == MUTT_ACL_WRITE) && !imap_has_flag(&imap_mdata_get(m)->flags, name))
×
310
    return 0;
311

312
  int count = 0;
313
  char buf[1024] = { 0 };
×
314

315
  struct UidArray uida = ARRAY_HEAD_INITIALIZER;
×
316

317
  // Set the flag (+FLAGS) on matching emails
318
  select_email_uids(emails, num_emails, flag, true, false, &uida);
×
319
  snprintf(buf, sizeof(buf), "+FLAGS.SILENT (%s)", name);
320
  int rc = imap_exec_msg_set(adata, "UID STORE", buf, &uida);
×
321
  if (rc < 0)
×
322
    return rc;
323
  count += rc;
324
  ARRAY_FREE(&uida);
×
325

326
  // Clear the flag (-FLAGS) on non-matching emails
327
  select_email_uids(emails, num_emails, flag, true, true, &uida);
×
328
  buf[0] = '-';
×
329
  rc = imap_exec_msg_set(adata, "UID STORE", buf, &uida);
×
330
  if (rc < 0)
×
331
    return rc;
332
  count += rc;
×
333
  ARRAY_FREE(&uida);
×
334

335
  return count;
×
336
}
337

338
/**
339
 * longest_common_prefix - Find longest prefix common to two strings
340
 * @param buf   Destination buffer
341
 * @param src   Source buffer
342
 * @param start Starting offset into string
343
 * @retval num Length of the common string
344
 *
345
 * Trim dest to the length of the longest prefix it shares with src.
346
 */
347
static size_t longest_common_prefix(struct Buffer *buf, const char *src, size_t start)
×
348
{
349
  size_t pos = start;
350

351
  size_t len = buf_len(buf);
×
352
  while ((pos < len) && buf->data[pos] && (buf->data[pos] == src[pos]))
×
353
    pos++;
×
354
  buf->data[pos] = '\0';
×
355

356
  buf_fix_dptr(buf);
×
357

358
  return pos;
×
359
}
360

361
/**
362
 * complete_hosts - Look for completion matches for mailboxes
363
 * @param buf Partial mailbox name to complete
364
 * @retval  0 Success
365
 * @retval -1 Failure
366
 *
367
 * look for IMAP URLs to complete from defined mailboxes. Could be extended to
368
 * complete over open connections and account/folder hooks too.
369
 */
370
static int complete_hosts(struct Buffer *buf)
×
371
{
372
  int rc = -1;
373
  size_t matchlen;
374

375
  matchlen = buf_len(buf);
×
376
  struct MailboxList ml = STAILQ_HEAD_INITIALIZER(ml);
×
377
  neomutt_mailboxlist_get_all(&ml, NeoMutt, MUTT_MAILBOX_ANY);
×
378
  struct MailboxNode *np = NULL;
379
  STAILQ_FOREACH(np, &ml, entries)
×
380
  {
381
    if (!mutt_str_startswith(mailbox_path(np->mailbox), buf_string(buf)))
×
382
      continue;
×
383

384
    if (rc)
×
385
    {
386
      buf_strcpy(buf, mailbox_path(np->mailbox));
×
387
      rc = 0;
388
    }
389
    else
390
    {
391
      longest_common_prefix(buf, mailbox_path(np->mailbox), matchlen);
×
392
    }
393
  }
394
  neomutt_mailboxlist_clear(&ml);
×
395

396
#if 0
397
  TAILQ_FOREACH(conn, mutt_socket_head(), entries)
398
  {
399
    struct Url url = { 0 };
400
    char urlstr[1024] = { 0 };
401

402
    if (conn->account.type != MUTT_ACCT_TYPE_IMAP)
403
      continue;
404

405
    account_to_url(&conn->account, &url);
406
    /* FIXME: how to handle multiple users on the same host? */
407
    url.user = NULL;
408
    url.path = NULL;
409
    url_tostring(&url, urlstr, sizeof(urlstr), U_NO_FLAGS);
410
    if (mutt_strn_equal(buf, urlstr, matchlen))
411
    {
412
      if (rc)
413
      {
414
        mutt_str_copy(buf, urlstr, buflen);
415
        rc = 0;
416
      }
417
      else
418
      {
419
        longest_common_prefix(buf, urlstr, matchlen);
420
      }
421
    }
422
  }
423
#endif
424

425
  return rc;
×
426
}
427

428
/**
429
 * imap_create_mailbox - Create a new mailbox
430
 * @param adata Imap Account data
431
 * @param mailbox Mailbox to create
432
 * @retval  0 Success
433
 * @retval -1 Failure
434
 */
435
int imap_create_mailbox(struct ImapAccountData *adata, const char *mailbox)
×
436
{
437
  char buf[2048] = { 0 };
×
438
  char mbox[1024] = { 0 };
×
439

440
  imap_munge_mbox_name(adata->unicode, mbox, sizeof(mbox), mailbox);
×
441
  snprintf(buf, sizeof(buf), "CREATE %s", mbox);
442

443
  if (imap_exec(adata, buf, IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
444
  {
445
    mutt_error(_("CREATE failed: %s"), imap_cmd_trailer(adata));
×
446
    return -1;
×
447
  }
448

449
  return 0;
450
}
451

452
/**
453
 * imap_access - Check permissions on an IMAP mailbox with a new connection
454
 * @param path Mailbox path
455
 * @retval  0 Success
456
 * @retval <0 Failure
457
 *
458
 * TODO: ACL checks. Right now we assume if it exists we can mess with it.
459
 * TODO: This method should take a Mailbox as parameter to be able to reuse the
460
 * existing connection.
461
 */
462
int imap_access(const char *path)
×
463
{
464
  if (imap_path_status(path, false) >= 0)
×
465
    return 0;
×
466
  return -1;
467
}
468

469
/**
470
 * imap_rename_mailbox - Rename a mailbox
471
 * @param adata Imap Account data
472
 * @param oldname Existing mailbox
473
 * @param newname New name for mailbox
474
 * @retval  0 Success
475
 * @retval -1 Failure
476
 */
477
int imap_rename_mailbox(struct ImapAccountData *adata, char *oldname, const char *newname)
×
478
{
479
  char oldmbox[1024] = { 0 };
×
480
  char newmbox[1024] = { 0 };
×
481
  int rc = 0;
482

483
  imap_munge_mbox_name(adata->unicode, oldmbox, sizeof(oldmbox), oldname);
×
484
  imap_munge_mbox_name(adata->unicode, newmbox, sizeof(newmbox), newname);
×
485

486
  struct Buffer *buf = buf_pool_get();
×
487
  buf_printf(buf, "RENAME %s %s", oldmbox, newmbox);
×
488

489
  if (imap_exec(adata, buf_string(buf), IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
490
    rc = -1;
491

492
  buf_pool_release(&buf);
×
493

494
  return rc;
×
495
}
496

497
/**
498
 * imap_delete_mailbox - Delete a mailbox
499
 * @param m  Mailbox
500
 * @param path  name of the mailbox to delete
501
 * @retval  0 Success
502
 * @retval -1 Failure
503
 */
504
int imap_delete_mailbox(struct Mailbox *m, char *path)
×
505
{
506
  char buf[PATH_MAX + 7];
507
  char mbox[PATH_MAX] = { 0 };
×
508
  struct Url *url = url_parse(path);
×
509
  if (!url)
×
510
    return -1;
511

512
  struct ImapAccountData *adata = imap_adata_get(m);
×
513
  imap_munge_mbox_name(adata->unicode, mbox, sizeof(mbox), url->path);
×
514
  url_free(&url);
×
515
  snprintf(buf, sizeof(buf), "DELETE %s", mbox);
516
  if (imap_exec(m->account->adata, buf, IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
517
    return -1;
518

519
  return 0;
520
}
521

522
/**
523
 * imap_logout - Gracefully log out of server
524
 * @param adata Imap Account data
525
 */
526
static void imap_logout(struct ImapAccountData *adata)
×
527
{
528
  if (adata->status != IMAP_FATAL)
×
529
  {
530
    /* we set status here to let imap_handle_untagged know we _expect_ to
531
     * receive a bye response (so it doesn't freak out and close the conn) */
532
    if (adata->state == IMAP_DISCONNECTED)
×
533
    {
534
      return;
535
    }
536

537
    adata->status = IMAP_BYE;
×
538
    imap_cmd_start(adata, "LOGOUT");
×
539
    const short c_imap_poll_timeout = cs_subset_number(NeoMutt->sub, "imap_poll_timeout");
×
540
    if ((c_imap_poll_timeout <= 0) ||
×
541
        (mutt_socket_poll(adata->conn, c_imap_poll_timeout) != 0))
×
542
    {
543
      while (imap_cmd_step(adata) == IMAP_RES_CONTINUE)
×
544
        ; // do nothing
545
    }
546
  }
547
  mutt_socket_close(adata->conn);
×
548
  adata->state = IMAP_DISCONNECTED;
×
549
}
550

551
/**
552
 * imap_logout_all - Close all open connections
553
 *
554
 * Quick and dirty until we can make sure we've got all the context we need.
555
 */
556
void imap_logout_all(void)
×
557
{
558
  struct Account *np = NULL;
559
  TAILQ_FOREACH(np, &NeoMutt->accounts, entries)
×
560
  {
561
    if (np->type != MUTT_IMAP)
×
562
      continue;
×
563

564
    struct ImapAccountData *adata = np->adata;
×
565
    if (!adata)
×
566
      continue;
×
567

568
    struct Connection *conn = adata->conn;
×
569
    if (!conn || (conn->fd < 0))
×
570
      continue;
×
571

572
    mutt_message(_("Closing connection to %s..."), conn->account.host);
×
573
    imap_logout(np->adata);
×
574
    mutt_clear_error();
×
575
  }
576
}
×
577

578
/**
579
 * imap_read_literal - Read bytes bytes from server into file
580
 * @param fp       File handle for email file
581
 * @param adata    Imap Account data
582
 * @param bytes    Number of bytes to read
583
 * @param progress Progress bar
584
 * @retval  0 Success
585
 * @retval -1 Failure
586
 *
587
 * Not explicitly buffered, relies on FILE buffering.
588
 *
589
 * @note Strips `\r` from `\r\n`.
590
 *       Apparently even literals use `\r\n`-terminated strings ?!
591
 */
592
int imap_read_literal(FILE *fp, struct ImapAccountData *adata,
×
593
                      unsigned long bytes, struct Progress *progress)
594
{
595
  char c;
596
  bool r = false;
597
  struct Buffer buf = { 0 }; // Do not allocate, maybe it won't be used
×
598

599
  const short c_debug_level = cs_subset_number(NeoMutt->sub, "debug_level");
×
600
  if (c_debug_level >= IMAP_LOG_LTRL)
×
601
    buf_alloc(&buf, bytes + 1);
×
602

603
  mutt_debug(LL_DEBUG2, "reading %lu bytes\n", bytes);
×
604

605
  for (unsigned long pos = 0; pos < bytes; pos++)
×
606
  {
607
    if (mutt_socket_readchar(adata->conn, &c) != 1)
×
608
    {
609
      mutt_debug(LL_DEBUG1, "error during read, %lu bytes read\n", pos);
×
610
      adata->status = IMAP_FATAL;
×
611

612
      buf_dealloc(&buf);
×
613
      return -1;
×
614
    }
615

616
    if (r && (c != '\n'))
×
617
      fputc('\r', fp);
×
618

619
    if (c == '\r')
×
620
    {
621
      r = true;
622
      continue;
×
623
    }
624
    else
625
    {
626
      r = false;
627
    }
628

629
    fputc(c, fp);
×
630

631
    if ((pos % 1024) == 0)
×
632
      progress_update(progress, pos, -1);
×
633
    if (c_debug_level >= IMAP_LOG_LTRL)
×
634
      buf_addch(&buf, c);
×
635
  }
636

637
  if (c_debug_level >= IMAP_LOG_LTRL)
×
638
  {
639
    mutt_debug(IMAP_LOG_LTRL, "\n%s", buf.data);
×
640
    buf_dealloc(&buf);
×
641
  }
642
  return 0;
643
}
644

645
/**
646
 * imap_notify_delete_email - Inform IMAP that an Email has been deleted
647
 * @param m Mailbox
648
 * @param e Email
649
 */
650
void imap_notify_delete_email(struct Mailbox *m, struct Email *e)
×
651
{
652
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
653
  struct ImapEmailData *edata = imap_edata_get(e);
×
654

655
  if (!mdata || !edata)
×
656
    return;
657

658
  imap_msn_remove(&mdata->msn, edata->msn - 1);
×
659
  edata->msn = 0;
×
660
}
661

662
/**
663
 * imap_expunge_mailbox - Purge messages from the server
664
 * @param m Mailbox
665
 * @param resort Trigger a resort?
666
 *
667
 * Purge IMAP portion of expunged messages from the context. Must not be done
668
 * while something has a handle on any headers (eg inside pager or editor).
669
 * That is, check #IMAP_REOPEN_ALLOW.
670
 */
671
void imap_expunge_mailbox(struct Mailbox *m, bool resort)
×
672
{
673
  struct ImapAccountData *adata = imap_adata_get(m);
×
674
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
675
  if (!adata || !mdata)
×
676
    return;
677

678
  struct Email *e = NULL;
679

680
#ifdef USE_HCACHE
681
  imap_hcache_open(adata, mdata, false);
×
682
#endif
683

684
  for (int i = 0; i < m->msg_count; i++)
×
685
  {
686
    e = m->emails[i];
×
687
    if (!e)
×
688
      break;
689

690
    if (e->index == INT_MAX)
×
691
    {
692
      mutt_debug(LL_DEBUG2, "Expunging message UID %u\n", imap_edata_get(e)->uid);
×
693

694
      e->deleted = true;
×
695

696
      imap_cache_del(m, e);
×
697
#ifdef USE_HCACHE
698
      imap_hcache_del(mdata, imap_edata_get(e)->uid);
×
699
#endif
700

701
      mutt_hash_int_delete(mdata->uid_hash, imap_edata_get(e)->uid, e);
×
702

703
      imap_edata_free((void **) &e->edata);
×
704
    }
705
    else
706
    {
707
      /* NeoMutt has several places where it turns off e->active as a
708
       * hack.  For example to avoid FLAG updates, or to exclude from
709
       * imap_exec_msg_set.
710
       *
711
       * Unfortunately, when a reopen is allowed and the IMAP_EXPUNGE_PENDING
712
       * flag becomes set (e.g. a flag update to a modified header),
713
       * this function will be called by imap_cmd_finish().
714
       *
715
       * The ctx_update_tables() will free and remove these "inactive" headers,
716
       * despite that an EXPUNGE was not received for them.
717
       * This would result in memory leaks and segfaults due to dangling
718
       * pointers in the msn_index and uid_hash.
719
       *
720
       * So this is another hack to work around the hacks.  We don't want to
721
       * remove the messages, so make sure active is on.  */
722
      e->active = true;
×
723
    }
724
  }
725

726
#ifdef USE_HCACHE
727
  imap_hcache_close(mdata);
×
728
#endif
729

730
  mailbox_changed(m, NT_MAILBOX_UPDATE);
×
731
  if (resort)
×
732
  {
733
    mailbox_changed(m, NT_MAILBOX_RESORT);
×
734
  }
735
}
736

737
/**
738
 * imap_open_connection - Open an IMAP connection
739
 * @param adata Imap Account data
740
 * @retval  0 Success
741
 * @retval -1 Failure
742
 */
743
int imap_open_connection(struct ImapAccountData *adata)
×
744
{
745
  if (mutt_socket_open(adata->conn) < 0)
×
746
    return -1;
747

748
  adata->state = IMAP_CONNECTED;
×
749

750
  if (imap_cmd_step(adata) != IMAP_RES_OK)
×
751
  {
752
    imap_close_connection(adata);
×
753
    return -1;
×
754
  }
755

756
  if (mutt_istr_startswith(adata->buf, "* OK"))
×
757
  {
758
    if (!mutt_istr_startswith(adata->buf, "* OK [CAPABILITY") && check_capabilities(adata))
×
759
    {
760
      goto bail;
×
761
    }
762
#ifdef USE_SSL
763
    /* Attempt STARTTLS if available and desired. */
764
    const bool c_ssl_force_tls = cs_subset_bool(NeoMutt->sub, "ssl_force_tls");
×
765
    if ((adata->conn->ssf == 0) &&
×
766
        (c_ssl_force_tls || (adata->capabilities & IMAP_CAP_STARTTLS)))
×
767
    {
768
      enum QuadOption ans;
769

770
      if (c_ssl_force_tls)
771
      {
772
        ans = MUTT_YES;
773
      }
774
      else if ((ans = query_quadoption(_("Secure connection with TLS?"),
×
775
                                       NeoMutt->sub, "ssl_starttls")) == MUTT_ABORT)
×
776
      {
777
        goto bail;
×
778
      }
779
      if (ans == MUTT_YES)
×
780
      {
781
        enum ImapExecResult rc = imap_exec(adata, "STARTTLS", IMAP_CMD_SINGLE);
×
782
        // Clear any data after the STARTTLS acknowledgement
783
        mutt_socket_empty(adata->conn);
×
784

785
        if (rc == IMAP_EXEC_FATAL)
×
786
          goto bail;
×
787
        if (rc != IMAP_EXEC_ERROR)
×
788
        {
789
          if (mutt_ssl_starttls(adata->conn))
×
790
          {
791
            mutt_error(_("Could not negotiate TLS connection"));
×
792
            goto bail;
×
793
          }
794
          else
795
          {
796
            /* RFC2595 demands we recheck CAPABILITY after TLS completes. */
797
            if (imap_exec(adata, "CAPABILITY", IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
798
              goto bail;
×
799
          }
800
        }
801
      }
802
    }
803

804
    if (c_ssl_force_tls && (adata->conn->ssf == 0))
×
805
    {
806
      mutt_error(_("Encrypted connection unavailable"));
×
807
      goto bail;
×
808
    }
809
#endif
810
  }
811
  else if (mutt_istr_startswith(adata->buf, "* PREAUTH"))
×
812
  {
813
#ifdef USE_SSL
814
    /* Unless using a secure $tunnel, an unencrypted PREAUTH response may be a
815
     * MITM attack.  The only way to stop "STARTTLS" MITM attacks is via
816
     * $ssl_force_tls: an attacker can easily spoof "* OK" and strip the
817
     * STARTTLS capability.  So consult $ssl_force_tls, not $ssl_starttls, to
818
     * decide whether to abort. Note that if using $tunnel and
819
     * $tunnel_is_secure, adata->conn->ssf will be set to 1. */
820
    const bool c_ssl_force_tls = cs_subset_bool(NeoMutt->sub, "ssl_force_tls");
×
821
    if ((adata->conn->ssf == 0) && c_ssl_force_tls)
×
822
    {
823
      mutt_error(_("Encrypted connection unavailable"));
×
824
      goto bail;
×
825
    }
826
#endif
827

828
    adata->state = IMAP_AUTHENTICATED;
×
829
    if (check_capabilities(adata) != 0)
×
830
      goto bail;
×
831
    FREE(&adata->capstr);
×
832
  }
833
  else
834
  {
835
    imap_error("imap_open_connection()", adata->buf);
×
836
    goto bail;
×
837
  }
838

839
  return 0;
840

841
bail:
×
842
  imap_close_connection(adata);
×
843
  FREE(&adata->capstr);
×
844
  return -1;
×
845
}
846

847
/**
848
 * imap_close_connection - Close an IMAP connection
849
 * @param adata Imap Account data
850
 */
851
void imap_close_connection(struct ImapAccountData *adata)
×
852
{
853
  if (adata->state != IMAP_DISCONNECTED)
×
854
  {
855
    mutt_socket_close(adata->conn);
×
856
    adata->state = IMAP_DISCONNECTED;
×
857
  }
858
  adata->seqno = 0;
×
859
  adata->nextcmd = 0;
×
860
  adata->lastcmd = 0;
×
861
  adata->status = 0;
×
862
  memset(adata->cmds, 0, sizeof(struct ImapCommand) * adata->cmdslots);
×
863
}
×
864

865
/**
866
 * imap_has_flag - Does the flag exist in the list
867
 * @param flag_list List of server flags
868
 * @param flag      Flag to find
869
 * @retval true Flag exists
870
 *
871
 * Do a caseless comparison of the flag against a flag list, return true if
872
 * found or flag list has '\*'. Note that "flag" might contain additional
873
 * whitespace at the end, so we really need to compare up to the length of each
874
 * element in "flag_list".
875
 */
876
bool imap_has_flag(struct ListHead *flag_list, const char *flag)
×
877
{
878
  if (STAILQ_EMPTY(flag_list))
×
879
    return false;
880

881
  const size_t flaglen = mutt_str_len(flag);
×
882
  struct ListNode *np = NULL;
883
  STAILQ_FOREACH(np, flag_list, entries)
×
884
  {
885
    const size_t nplen = strlen(np->data);
×
886
    if ((flaglen >= nplen) && ((flag[nplen] == '\0') || (flag[nplen] == ' ')) &&
×
887
        mutt_istrn_equal(np->data, flag, nplen))
×
888
    {
889
      return true;
890
    }
891

892
    if (mutt_str_equal(np->data, "\\*"))
×
893
      return true;
894
  }
895

896
  return false;
897
}
898

899
/**
900
 * imap_sort_email_uid - Compare two Emails by UID - Implements ::sort_t - @ingroup sort_api
901
 */
902
static int imap_sort_email_uid(const void *a, const void *b, void *sdata)
×
903
{
904
  const struct Email *ea = *(struct Email const *const *) a;
×
905
  const struct Email *eb = *(struct Email const *const *) b;
×
906

907
  const unsigned int ua = imap_edata_get((struct Email *) ea)->uid;
×
908
  const unsigned int ub = imap_edata_get((struct Email *) eb)->uid;
×
909

910
  return mutt_numeric_cmp(ua, ub);
×
911
}
912

913
/**
914
 * imap_sync_message_for_copy - Update server to reflect the flags of a single message
915
 * @param[in]  m            Mailbox
916
 * @param[in]  e            Email
917
 * @param[in]  cmd          Buffer for the command string
918
 * @param[out] err_continue Did the user force a continue?
919
 * @retval  0 Success
920
 * @retval -1 Failure
921
 *
922
 * Update the IMAP server to reflect the flags for a single message before
923
 * performing a "UID COPY".
924
 *
925
 * @note This does not sync the "deleted" flag state, because it is not
926
 *       desirable to propagate that flag into the copy.
927
 */
928
int imap_sync_message_for_copy(struct Mailbox *m, struct Email *e,
×
929
                               struct Buffer *cmd, enum QuadOption *err_continue)
930
{
931
  struct ImapAccountData *adata = imap_adata_get(m);
×
932
  struct ImapEmailData *edata = imap_edata_get(e);
×
933

934
  if (!adata || (adata->mailbox != m) || !e)
×
935
    return -1;
936

937
  if (!compare_flags_for_copy(e))
×
938
  {
939
    if (e->deleted == edata->deleted)
×
940
      e->changed = false;
×
941
    return 0;
×
942
  }
943

944
  buf_printf(cmd, "UID STORE %u", edata->uid);
×
945

946
  struct Buffer *flags = buf_pool_get();
×
947

948
  set_flag(m, MUTT_ACL_SEEN, e->read, "\\Seen ", flags);
×
949
  set_flag(m, MUTT_ACL_WRITE, e->old, "Old ", flags);
×
950
  set_flag(m, MUTT_ACL_WRITE, e->flagged, "\\Flagged ", flags);
×
951
  set_flag(m, MUTT_ACL_WRITE, e->replied, "\\Answered ", flags);
×
952
  set_flag(m, MUTT_ACL_DELETE, edata->deleted, "\\Deleted ", flags);
×
953

954
  if (m->rights & MUTT_ACL_WRITE)
×
955
  {
956
    /* restore system flags */
957
    if (edata->flags_system)
×
958
      buf_addstr(flags, edata->flags_system);
×
959

960
    /* set custom flags */
961
    struct Buffer *tags = buf_pool_get();
×
962
    driver_tags_get_with_hidden(&e->tags, tags);
×
963
    if (!buf_is_empty(tags))
×
964
      buf_addstr(flags, buf_string(tags));
×
965
    buf_pool_release(&tags);
×
966
  }
967

968
  mutt_str_remove_trailing_ws(flags->data);
×
969
  buf_fix_dptr(flags);
×
970

971
  /* UW-IMAP is OK with null flags, Cyrus isn't. The only solution is to
972
   * explicitly revoke all system flags (if we have permission) */
973
  if (buf_is_empty(flags))
×
974
  {
975
    set_flag(m, MUTT_ACL_SEEN, true, "\\Seen ", flags);
×
976
    set_flag(m, MUTT_ACL_WRITE, true, "Old ", flags);
×
977
    set_flag(m, MUTT_ACL_WRITE, true, "\\Flagged ", flags);
×
978
    set_flag(m, MUTT_ACL_WRITE, true, "\\Answered ", flags);
×
979
    set_flag(m, MUTT_ACL_DELETE, !edata->deleted, "\\Deleted ", flags);
×
980

981
    /* erase custom flags */
982
    if ((m->rights & MUTT_ACL_WRITE) && edata->flags_remote)
×
983
      buf_addstr(flags, edata->flags_remote);
×
984

985
    mutt_str_remove_trailing_ws(flags->data);
×
986
    buf_fix_dptr(flags);
×
987

988
    buf_addstr(cmd, " -FLAGS.SILENT (");
×
989
  }
990
  else
991
  {
992
    buf_addstr(cmd, " FLAGS.SILENT (");
×
993
  }
994

995
  buf_addstr(cmd, buf_string(flags));
×
996
  buf_addstr(cmd, ")");
×
997

998
  int rc = -1;
999

1000
  /* after all this it's still possible to have no flags, if you
1001
   * have no ACL rights */
1002
  if (!buf_is_empty(flags) &&
×
1003
      (imap_exec(adata, cmd->data, IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS) &&
×
1004
      err_continue && (*err_continue != MUTT_YES))
×
1005
  {
1006
    *err_continue = imap_continue("imap_sync_message: STORE failed", adata->buf);
×
1007
    if (*err_continue != MUTT_YES)
×
1008
      goto done;
×
1009
  }
1010

1011
  /* server have now the updated flags */
1012
  FREE(&edata->flags_remote);
×
1013
  struct Buffer *flags_remote = buf_pool_get();
×
1014
  driver_tags_get_with_hidden(&e->tags, flags_remote);
×
1015
  edata->flags_remote = buf_strdup(flags_remote);
×
1016
  buf_pool_release(&flags_remote);
×
1017

1018
  if (e->deleted == edata->deleted)
×
1019
    e->changed = false;
×
1020

1021
  rc = 0;
1022

1023
done:
×
1024
  buf_pool_release(&flags);
×
1025
  return rc;
×
1026
}
1027

1028
/**
1029
 * imap_check_mailbox - Use the NOOP or IDLE command to poll for new mail
1030
 * @param m     Mailbox
1031
 * @param force Don't wait
1032
 * @retval num MxStatus
1033
 */
1034
enum MxStatus imap_check_mailbox(struct Mailbox *m, bool force)
×
1035
{
1036
  if (!m || !m->account)
×
1037
    return MX_STATUS_ERROR;
1038

1039
  struct ImapAccountData *adata = imap_adata_get(m);
×
1040
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
1041

1042
  /* overload keyboard timeout to avoid many mailbox checks in a row.
1043
   * Most users don't like having to wait exactly when they press a key. */
1044
  int rc = 0;
1045

1046
  /* try IDLE first, unless force is set */
1047
  const bool c_imap_idle = cs_subset_bool(NeoMutt->sub, "imap_idle");
×
1048
  const short c_imap_keep_alive = cs_subset_number(NeoMutt->sub, "imap_keep_alive");
×
1049
  if (!force && c_imap_idle && (adata->capabilities & IMAP_CAP_IDLE) &&
×
1050
      ((adata->state != IMAP_IDLE) || (mutt_date_now() >= adata->lastread + c_imap_keep_alive)))
×
1051
  {
1052
    if (imap_cmd_idle(adata) < 0)
×
1053
      return MX_STATUS_ERROR;
1054
  }
1055
  if (adata->state == IMAP_IDLE)
×
1056
  {
1057
    while ((rc = mutt_socket_poll(adata->conn, 0)) > 0)
×
1058
    {
1059
      if (imap_cmd_step(adata) != IMAP_RES_CONTINUE)
×
1060
      {
1061
        mutt_debug(LL_DEBUG1, "Error reading IDLE response\n");
×
1062
        return MX_STATUS_ERROR;
×
1063
      }
1064
    }
1065
    if (rc < 0)
×
1066
    {
1067
      mutt_debug(LL_DEBUG1, "Poll failed, disabling IDLE\n");
×
1068
      adata->capabilities &= ~IMAP_CAP_IDLE; // Clear the flag
×
1069
    }
1070
  }
1071

1072
  const short c_timeout = cs_subset_number(NeoMutt->sub, "timeout");
×
1073
  if ((force || ((adata->state != IMAP_IDLE) && (mutt_date_now() >= adata->lastread + c_timeout))) &&
×
1074
      (imap_exec(adata, "NOOP", IMAP_CMD_POLL) != IMAP_EXEC_SUCCESS))
×
1075
  {
1076
    return MX_STATUS_ERROR;
1077
  }
1078

1079
  /* We call this even when we haven't run NOOP in case we have pending
1080
   * changes to process, since we can reopen here. */
1081
  imap_cmd_finish(adata);
×
1082

1083
  enum MxStatus check = MX_STATUS_OK;
1084
  if (mdata->check_status & IMAP_EXPUNGE_PENDING)
×
1085
    check = MX_STATUS_REOPENED;
1086
  else if (mdata->check_status & IMAP_NEWMAIL_PENDING)
×
1087
    check = MX_STATUS_NEW_MAIL;
1088
  else if (mdata->check_status & IMAP_FLAGS_PENDING)
×
1089
    check = MX_STATUS_FLAGS;
1090
  else if (rc < 0)
×
1091
    check = MX_STATUS_ERROR;
1092

1093
  mdata->check_status = IMAP_OPEN_NO_FLAGS;
×
1094

1095
  if (force)
×
1096
    m->last_checked = 0; // force a check on the next mx_mbox_check() call
×
1097
  return check;
1098
}
1099

1100
/**
1101
 * imap_status - Refresh the number of total and new messages
1102
 * @param adata  IMAP Account data
1103
 * @param mdata  IMAP Mailbox data
1104
 * @param queue  Queue the STATUS command
1105
 * @retval num   Total number of messages
1106
 */
1107
static int imap_status(struct ImapAccountData *adata, struct ImapMboxData *mdata, bool queue)
×
1108
{
1109
  char *uidvalidity_flag = NULL;
1110
  char cmd[2048] = { 0 };
×
1111

1112
  if (!adata || !mdata)
×
1113
    return -1;
1114

1115
  /* Don't issue STATUS on the selected mailbox, it will be NOOPed or
1116
   * IDLEd elsewhere.
1117
   * adata->mailbox may be NULL for connections other than the current
1118
   * mailbox's. */
1119
  if (adata->mailbox && (adata->mailbox->mdata == mdata))
×
1120
  {
1121
    adata->mailbox->has_new = false;
×
1122
    return mdata->messages;
×
1123
  }
1124

1125
  if (adata->mailbox && !adata->mailbox->poll_new_mail)
×
1126
    return mdata->messages;
×
1127

1128
  if (adata->capabilities & IMAP_CAP_IMAP4REV1)
×
1129
  {
1130
    uidvalidity_flag = "UIDVALIDITY";
1131
  }
1132
  else if (adata->capabilities & IMAP_CAP_STATUS)
×
1133
  {
1134
    uidvalidity_flag = "UID-VALIDITY";
1135
  }
1136
  else
1137
  {
1138
    mutt_debug(LL_DEBUG2, "Server doesn't support STATUS\n");
×
1139
    return -1;
×
1140
  }
1141

1142
  snprintf(cmd, sizeof(cmd), "STATUS %s (UIDNEXT %s UNSEEN RECENT MESSAGES)",
×
1143
           mdata->munge_name, uidvalidity_flag);
1144

1145
  int rc = imap_exec(adata, cmd, queue ? IMAP_CMD_QUEUE : IMAP_CMD_POLL);
×
1146
  if (rc != IMAP_EXEC_SUCCESS)
×
1147
  {
1148
    mutt_debug(LL_DEBUG1, "Error queueing command\n");
×
1149
    return rc;
×
1150
  }
1151
  return mdata->messages;
×
1152
}
1153

1154
/**
1155
 * imap_mbox_check_stats - Check the Mailbox statistics - Implements MxOps::mbox_check_stats() - @ingroup mx_mbox_check_stats
1156
 */
1157
static enum MxStatus imap_mbox_check_stats(struct Mailbox *m, uint8_t flags)
×
1158
{
1159
  const bool queue = (flags & MUTT_MAILBOX_CHECK_IMMEDIATE) == 0;
×
1160
  const int new_msgs = imap_mailbox_status(m, queue);
×
1161
  if (new_msgs == -1)
×
1162
    return MX_STATUS_ERROR;
1163
  if (new_msgs == 0)
×
1164
    return MX_STATUS_OK;
×
1165
  return MX_STATUS_NEW_MAIL;
1166
}
1167

1168
/**
1169
 * imap_path_status - Refresh the number of total and new messages
1170
 * @param path   Mailbox path
1171
 * @param queue  Queue the STATUS command
1172
 * @retval num   Total number of messages
1173
 */
1174
int imap_path_status(const char *path, bool queue)
×
1175
{
1176
  struct Mailbox *m = mx_mbox_find2(path);
×
1177

1178
  const bool is_temp = !m;
1179
  if (is_temp)
×
1180
  {
1181
    m = mx_path_resolve(path);
×
1182
    if (!mx_mbox_ac_link(m))
×
1183
    {
1184
      mailbox_free(&m);
×
1185
      return 0;
×
1186
    }
1187
  }
1188

1189
  int rc = imap_mailbox_status(m, queue);
×
1190

1191
  if (is_temp)
×
1192
  {
1193
    mx_ac_remove(m, false);
×
1194
    mailbox_free(&m);
×
1195
  }
1196

1197
  return rc;
1198
}
1199

1200
/**
1201
 * imap_mailbox_status - Refresh the number of total and new messages
1202
 * @param m      Mailbox
1203
 * @param queue  Queue the STATUS command
1204
 * @retval num Total number of messages
1205
 * @retval -1  Error
1206
 *
1207
 * @note Prepare the mailbox if we are not connected
1208
 */
1209
int imap_mailbox_status(struct Mailbox *m, bool queue)
×
1210
{
1211
  struct ImapAccountData *adata = imap_adata_get(m);
×
1212
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
1213
  if (!adata || !mdata)
×
1214
    return -1;
1215
  return imap_status(adata, mdata, queue);
×
1216
}
1217

1218
/**
1219
 * imap_subscribe - Subscribe to a mailbox
1220
 * @param path      Mailbox path
1221
 * @param subscribe True: subscribe, false: unsubscribe
1222
 * @retval  0 Success
1223
 * @retval -1 Failure
1224
 */
1225
int imap_subscribe(const char *path, bool subscribe)
×
1226
{
1227
  struct ImapAccountData *adata = NULL;
×
1228
  struct ImapMboxData *mdata = NULL;
×
1229

1230
  if (imap_adata_find(path, &adata, &mdata) < 0)
×
1231
    return -1;
1232

1233
  if (subscribe)
×
1234
    mutt_message(_("Subscribing to %s..."), mdata->name);
×
1235
  else
1236
    mutt_message(_("Unsubscribing from %s..."), mdata->name);
×
1237

1238
  char buf[2048] = { 0 };
×
1239
  snprintf(buf, sizeof(buf), "%sSUBSCRIBE %s", subscribe ? "" : "UN", mdata->munge_name);
×
1240

1241
  if (imap_exec(adata, buf, IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
1242
  {
1243
    imap_mdata_free((void *) &mdata);
×
1244
    return -1;
×
1245
  }
1246

1247
  const bool c_imap_check_subscribed = cs_subset_bool(NeoMutt->sub, "imap_check_subscribed");
×
1248
  if (c_imap_check_subscribed)
×
1249
  {
1250
    char mbox[1024] = { 0 };
×
1251
    size_t len = snprintf(mbox, sizeof(mbox), "%smailboxes ", subscribe ? "" : "un");
×
1252
    imap_quote_string(mbox + len, sizeof(mbox) - len, path, true);
×
1253
    struct Buffer *err = buf_pool_get();
×
1254
    if (parse_rc_line(mbox, err))
×
1255
      mutt_debug(LL_DEBUG1, "Error adding subscribed mailbox: %s\n", buf_string(err));
×
1256
    buf_pool_release(&err);
×
1257
  }
1258

1259
  if (subscribe)
×
1260
    mutt_message(_("Subscribed to %s"), mdata->name);
×
1261
  else
1262
    mutt_message(_("Unsubscribed from %s"), mdata->name);
×
1263
  imap_mdata_free((void *) &mdata);
×
1264
  return 0;
×
1265
}
1266

1267
/**
1268
 * imap_complete - Try to complete an IMAP folder path
1269
 * @param buf  Buffer for result
1270
 * @param path Partial mailbox name to complete
1271
 * @retval  0 Success
1272
 * @retval -1 Failure
1273
 *
1274
 * Given a partial IMAP folder path, return a string which adds as much to the
1275
 * path as is unique
1276
 */
1277
int imap_complete(struct Buffer *buf, const char *path)
×
1278
{
1279
  struct ImapAccountData *adata = NULL;
×
1280
  struct ImapMboxData *mdata = NULL;
×
1281
  char tmp[2048] = { 0 };
×
1282
  struct ImapList listresp = { 0 };
×
1283
  struct Buffer *completion_buf = NULL;
×
1284
  size_t clen;
1285
  int completions = 0;
1286
  int rc;
1287

1288
  if (imap_adata_find(path, &adata, &mdata) < 0)
×
1289
  {
1290
    buf_strcpy(buf, path);
×
1291
    return complete_hosts(buf);
×
1292
  }
1293

1294
  /* fire off command */
1295
  const bool c_imap_list_subscribed = cs_subset_bool(NeoMutt->sub, "imap_list_subscribed");
×
1296
  snprintf(tmp, sizeof(tmp), "%s \"\" \"%s%%\"",
×
1297
           c_imap_list_subscribed ? "LSUB" : "LIST", mdata->real_name);
×
1298

1299
  imap_cmd_start(adata, tmp);
×
1300

1301
  /* and see what the results are */
1302
  completion_buf = buf_pool_get();
×
1303
  buf_strcpy(completion_buf, mdata->name);
×
1304
  imap_mdata_free((void *) &mdata);
×
1305

1306
  adata->cmdresult = &listresp;
×
1307
  do
1308
  {
1309
    listresp.name = NULL;
×
1310
    rc = imap_cmd_step(adata);
×
1311

1312
    if ((rc == IMAP_RES_CONTINUE) && listresp.name)
×
1313
    {
1314
      /* if the folder isn't selectable, append delimiter to force browse
1315
       * to enter it on second tab. */
1316
      if (listresp.noselect)
×
1317
      {
1318
        clen = strlen(listresp.name);
×
1319
        listresp.name[clen++] = listresp.delim;
×
1320
        listresp.name[clen] = '\0';
×
1321
      }
1322
      /* copy in first word */
1323
      if (!completions)
×
1324
      {
1325
        buf_strcpy(completion_buf, listresp.name);
×
1326
        completions++;
1327
        continue;
×
1328
      }
1329

1330
      longest_common_prefix(completion_buf, listresp.name, 0);
×
1331
      completions++;
×
1332
    }
1333
  } while (rc == IMAP_RES_CONTINUE);
×
1334
  adata->cmdresult = NULL;
×
1335

1336
  if (completions)
×
1337
  {
1338
    /* reformat output */
1339
    imap_buf_qualify_path(buf, &adata->conn->account, completion_buf->data);
×
1340
    buf_pretty_mailbox(buf);
×
1341
    buf_fix_dptr(buf);
×
1342
    buf_pool_release(&completion_buf);
×
1343
    return 0;
×
1344
  }
1345

1346
  buf_pool_release(&completion_buf);
×
1347
  return -1;
×
1348
}
1349

1350
/**
1351
 * imap_fast_trash - Use server COPY command to copy deleted messages to trash
1352
 * @param m    Mailbox
1353
 * @param dest Mailbox to move to
1354
 * @retval -1 Error
1355
 * @retval  0 Success
1356
 * @retval  1 Non-fatal error - try fetch/append
1357
 */
1358
int imap_fast_trash(struct Mailbox *m, const char *dest)
×
1359
{
1360
  char prompt[1024] = { 0 };
×
1361
  int rc = -1;
1362
  bool triedcreate = false;
1363
  enum QuadOption err_continue = MUTT_NO;
×
1364

1365
  struct ImapAccountData *adata = imap_adata_get(m);
×
1366
  struct ImapAccountData *dest_adata = NULL;
×
1367
  struct ImapMboxData *dest_mdata = NULL;
×
1368

1369
  if (imap_adata_find(dest, &dest_adata, &dest_mdata) < 0)
×
1370
    return -1;
1371

1372
  struct Buffer *sync_cmd = buf_pool_get();
×
1373

1374
  /* check that the save-to folder is in the same account */
1375
  if (!imap_account_match(&(adata->conn->account), &(dest_adata->conn->account)))
×
1376
  {
1377
    mutt_debug(LL_DEBUG3, "%s not same server as %s\n", dest, mailbox_path(m));
×
1378
    goto out;
×
1379
  }
1380

1381
  for (int i = 0; i < m->msg_count; i++)
×
1382
  {
1383
    struct Email *e = m->emails[i];
×
1384
    if (!e)
×
1385
      break;
1386
    if (e->active && e->changed && e->deleted && !e->purge)
×
1387
    {
1388
      rc = imap_sync_message_for_copy(m, e, sync_cmd, &err_continue);
×
1389
      if (rc < 0)
×
1390
      {
1391
        mutt_debug(LL_DEBUG1, "could not sync\n");
×
1392
        goto out;
×
1393
      }
1394
    }
1395
  }
1396

1397
  /* loop in case of TRYCREATE */
1398
  do
1399
  {
1400
    struct UidArray uida = ARRAY_HEAD_INITIALIZER;
×
1401
    select_email_uids(m->emails, m->msg_count, MUTT_TRASH, false, false, &uida);
×
1402
    ARRAY_SORT(&uida, imap_sort_uid, NULL);
×
1403
    rc = imap_exec_msg_set(adata, "UID COPY", dest_mdata->munge_name, &uida);
×
1404
    if (rc == 0)
×
1405
    {
1406
      mutt_debug(LL_DEBUG1, "No messages to trash\n");
×
1407
      rc = -1;
1408
      goto out;
×
1409
    }
1410
    else if (rc < 0)
×
1411
    {
1412
      mutt_debug(LL_DEBUG1, "could not queue copy\n");
×
1413
      goto out;
×
1414
    }
1415
    else if (m->verbose)
×
1416
    {
1417
      mutt_message(ngettext("Copying %d message to %s...", "Copying %d messages to %s...", rc),
×
1418
                   rc, dest_mdata->name);
1419
    }
1420
    ARRAY_FREE(&uida);
×
1421

1422
    /* let's get it on */
1423
    rc = imap_exec(adata, NULL, IMAP_CMD_NO_FLAGS);
×
1424
    if (rc == IMAP_EXEC_ERROR)
×
1425
    {
1426
      if (triedcreate)
×
1427
      {
1428
        mutt_debug(LL_DEBUG1, "Already tried to create mailbox %s\n", dest_mdata->name);
×
1429
        break;
×
1430
      }
1431
      /* bail out if command failed for reasons other than nonexistent target */
1432
      if (!mutt_istr_startswith(imap_get_qualifier(adata->buf), "[TRYCREATE]"))
×
1433
        break;
1434
      mutt_debug(LL_DEBUG3, "server suggests TRYCREATE\n");
×
1435
      snprintf(prompt, sizeof(prompt), _("Create %s?"), dest_mdata->name);
×
1436
      const bool c_confirm_create = cs_subset_bool(NeoMutt->sub, "confirm_create");
×
1437
      if (c_confirm_create &&
×
1438
          (query_yesorno_help(prompt, MUTT_YES, NeoMutt->sub, "confirm_create") != MUTT_YES))
×
1439
      {
1440
        mutt_clear_error();
×
1441
        goto out;
×
1442
      }
1443
      if (imap_create_mailbox(adata, dest_mdata->name) < 0)
×
1444
        break;
1445
      triedcreate = true;
1446
    }
1447
  } while (rc == IMAP_EXEC_ERROR);
×
1448

1449
  if (rc != IMAP_EXEC_SUCCESS)
×
1450
  {
1451
    imap_error("imap_fast_trash", adata->buf);
×
1452
    goto out;
×
1453
  }
1454

1455
  rc = IMAP_EXEC_SUCCESS;
1456

1457
out:
×
1458
  buf_pool_release(&sync_cmd);
×
1459
  imap_mdata_free((void *) &dest_mdata);
×
1460

1461
  return ((rc == IMAP_EXEC_SUCCESS) ? 0 : -1);
×
1462
}
1463

1464
/**
1465
 * imap_sync_mailbox - Sync all the changes to the server
1466
 * @param m       Mailbox
1467
 * @param expunge if true do expunge
1468
 * @param close   if true we move imap state to CLOSE
1469
 * @retval enum #MxStatus
1470
 *
1471
 * @note The flag retvals come from a call to imap_check_mailbox()
1472
 */
1473
enum MxStatus imap_sync_mailbox(struct Mailbox *m, bool expunge, bool close)
×
1474
{
1475
  if (!m)
×
1476
    return -1;
1477

1478
  struct Email **emails = NULL;
×
1479
  int rc;
1480

1481
  struct ImapAccountData *adata = imap_adata_get(m);
×
1482
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
1483
  if (!adata || !mdata)
×
1484
    return MX_STATUS_ERROR;
1485

1486
  if (adata->state < IMAP_SELECTED)
×
1487
  {
1488
    mutt_debug(LL_DEBUG2, "no mailbox selected\n");
×
1489
    return -1;
×
1490
  }
1491

1492
  /* This function is only called when the calling code expects the context
1493
   * to be changed. */
1494
  imap_allow_reopen(m);
×
1495

1496
  enum MxStatus check = imap_check_mailbox(m, false);
×
1497
  if (check == MX_STATUS_ERROR)
×
1498
    return check;
1499

1500
  /* if we are expunging anyway, we can do deleted messages very quickly... */
1501
  if (expunge && (m->rights & MUTT_ACL_DELETE))
×
1502
  {
1503
    struct UidArray uida = ARRAY_HEAD_INITIALIZER;
×
1504
    select_email_uids(m->emails, m->msg_count, MUTT_DELETED, true, false, &uida);
×
1505
    ARRAY_SORT(&uida, imap_sort_uid, NULL);
×
1506
    rc = imap_exec_msg_set(adata, "UID STORE", "+FLAGS.SILENT (\\Deleted)", &uida);
×
1507
    ARRAY_FREE(&uida);
×
1508
    if (rc < 0)
×
1509
    {
1510
      mutt_error(_("Expunge failed"));
×
1511
      return rc;
×
1512
    }
1513

1514
    if (rc > 0)
×
1515
    {
1516
      /* mark these messages as unchanged so second pass ignores them. Done
1517
       * here so BOGUS UW-IMAP 4.7 SILENT FLAGS updates are ignored. */
1518
      for (int i = 0; i < m->msg_count; i++)
×
1519
      {
1520
        struct Email *e = m->emails[i];
×
1521
        if (!e)
×
1522
          break;
1523
        if (e->deleted && e->changed)
×
1524
          e->active = false;
×
1525
      }
1526
      if (m->verbose)
×
1527
      {
1528
        mutt_message(ngettext("Marking %d message deleted...",
×
1529
                              "Marking %d messages deleted...", rc),
1530
                     rc);
1531
      }
1532
    }
1533
  }
1534

1535
#ifdef USE_HCACHE
1536
  imap_hcache_open(adata, mdata, true);
×
1537
#endif
1538

1539
  /* save messages with real (non-flag) changes */
1540
  for (int i = 0; i < m->msg_count; i++)
×
1541
  {
1542
    struct Email *e = m->emails[i];
×
1543
    if (!e)
×
1544
      break;
1545

1546
    if (e->deleted)
×
1547
    {
1548
      imap_cache_del(m, e);
×
1549
#ifdef USE_HCACHE
1550
      imap_hcache_del(mdata, imap_edata_get(e)->uid);
×
1551
#endif
1552
    }
1553

1554
    if (e->active && e->changed)
×
1555
    {
1556
#ifdef USE_HCACHE
1557
      imap_hcache_put(mdata, e);
×
1558
#endif
1559
      /* if the message has been rethreaded or attachments have been deleted
1560
       * we delete the message and reupload it.
1561
       * This works better if we're expunging, of course. */
1562
      if (e->env->changed || e->attach_del)
×
1563
      {
1564
        /* L10N: The plural is chosen by the last %d, i.e. the total number */
1565
        if (m->verbose)
×
1566
        {
1567
          mutt_message(ngettext("Saving changed message... [%d/%d]",
×
1568
                                "Saving changed messages... [%d/%d]", m->msg_count),
1569
                       i + 1, m->msg_count);
1570
        }
1571
        bool save_append = m->append;
×
1572
        m->append = true;
×
1573
        mutt_save_message_mbox(m, e, SAVE_MOVE, TRANSFORM_NONE, m);
×
1574
        m->append = save_append;
×
1575
        e->env->changed = false;
×
1576
      }
1577
    }
1578
  }
1579

1580
#ifdef USE_HCACHE
1581
  imap_hcache_close(mdata);
×
1582
#endif
1583

1584
  /* presort here to avoid doing 10 resorts in imap_exec_msg_set */
1585
  emails = MUTT_MEM_MALLOC(m->msg_count, struct Email *);
×
1586
  memcpy(emails, m->emails, m->msg_count * sizeof(struct Email *));
×
1587
  mutt_qsort_r(emails, m->msg_count, sizeof(struct Email *), imap_sort_email_uid, NULL);
×
1588

1589
  rc = sync_helper(m, emails, m->msg_count, MUTT_ACL_DELETE, MUTT_DELETED, "\\Deleted");
×
1590
  if (rc >= 0)
×
1591
    rc |= sync_helper(m, emails, m->msg_count, MUTT_ACL_WRITE, MUTT_FLAG, "\\Flagged");
×
1592
  if (rc >= 0)
×
1593
    rc |= sync_helper(m, emails, m->msg_count, MUTT_ACL_WRITE, MUTT_OLD, "Old");
×
1594
  if (rc >= 0)
×
1595
    rc |= sync_helper(m, emails, m->msg_count, MUTT_ACL_SEEN, MUTT_READ, "\\Seen");
×
1596
  if (rc >= 0)
×
1597
    rc |= sync_helper(m, emails, m->msg_count, MUTT_ACL_WRITE, MUTT_REPLIED, "\\Answered");
×
1598

1599
  FREE(&emails);
×
1600

1601
  /* Flush the queued flags if any were changed in sync_helper. */
1602
  if (rc > 0)
×
1603
    if (imap_exec(adata, NULL, IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
1604
      rc = -1;
1605

1606
  if (rc < 0)
×
1607
  {
1608
    if (close)
×
1609
    {
1610
      if (query_yesorno(_("Error saving flags. Close anyway?"), MUTT_NO) == MUTT_YES)
×
1611
      {
1612
        adata->state = IMAP_AUTHENTICATED;
×
1613
        return 0;
×
1614
      }
1615
    }
1616
    else
1617
    {
1618
      mutt_error(_("Error saving flags"));
×
1619
    }
1620
    return -1;
×
1621
  }
1622

1623
  /* Update local record of server state to reflect the synchronization just
1624
   * completed.  imap_read_headers always overwrites hcache-origin flags, so
1625
   * there is no need to mutate the hcache after flag-only changes. */
1626
  for (int i = 0; i < m->msg_count; i++)
×
1627
  {
1628
    struct Email *e = m->emails[i];
×
1629
    if (!e)
×
1630
      break;
1631
    struct ImapEmailData *edata = imap_edata_get(e);
×
1632
    edata->deleted = e->deleted;
×
1633
    edata->flagged = e->flagged;
×
1634
    edata->old = e->old;
×
1635
    edata->read = e->read;
×
1636
    edata->replied = e->replied;
×
1637
    e->changed = false;
×
1638
  }
1639
  m->changed = false;
×
1640

1641
  /* We must send an EXPUNGE command if we're not closing. */
1642
  if (expunge && !close && (m->rights & MUTT_ACL_DELETE))
×
1643
  {
1644
    if (m->verbose)
×
1645
      mutt_message(_("Expunging messages from server..."));
×
1646
    /* Set expunge bit so we don't get spurious reopened messages */
1647
    mdata->reopen |= IMAP_EXPUNGE_EXPECTED;
×
1648
    if (imap_exec(adata, "EXPUNGE", IMAP_CMD_NO_FLAGS) != IMAP_EXEC_SUCCESS)
×
1649
    {
1650
      mdata->reopen &= ~IMAP_EXPUNGE_EXPECTED;
×
1651
      imap_error(_("imap_sync_mailbox: EXPUNGE failed"), adata->buf);
×
1652
      return -1;
×
1653
    }
1654
    mdata->reopen &= ~IMAP_EXPUNGE_EXPECTED;
×
1655
  }
1656

1657
  if (expunge && close)
×
1658
  {
1659
    adata->closing = true;
×
1660
    imap_exec(adata, "CLOSE", IMAP_CMD_NO_FLAGS);
×
1661
    adata->state = IMAP_AUTHENTICATED;
×
1662
  }
1663

1664
  const bool c_message_cache_clean = cs_subset_bool(NeoMutt->sub, "message_cache_clean");
×
1665
  if (c_message_cache_clean)
×
1666
    imap_cache_clean(m);
×
1667

1668
  return check;
1669
}
1670

1671
/**
1672
 * imap_ac_owns_path - Check whether an Account owns a Mailbox path - Implements MxOps::ac_owns_path() - @ingroup mx_ac_owns_path
1673
 */
1674
static bool imap_ac_owns_path(struct Account *a, const char *path)
×
1675
{
1676
  struct Url *url = url_parse(path);
×
1677
  if (!url)
×
1678
    return false;
1679

1680
  struct ImapAccountData *adata = a->adata;
×
1681
  struct ConnAccount *cac = &adata->conn->account;
×
1682

1683
  const bool rc = mutt_istr_equal(url->host, cac->host) &&
×
1684
                  (!url->user || mutt_istr_equal(url->user, cac->user));
×
1685
  url_free(&url);
×
1686
  return rc;
×
1687
}
1688

1689
/**
1690
 * imap_ac_add - Add a Mailbox to an Account - Implements MxOps::ac_add() - @ingroup mx_ac_add
1691
 */
1692
static bool imap_ac_add(struct Account *a, struct Mailbox *m)
×
1693
{
1694
  struct ImapAccountData *adata = a->adata;
×
1695

1696
  if (!adata)
×
1697
  {
1698
    struct ConnAccount cac = { { 0 } };
×
1699
    char mailbox[PATH_MAX] = { 0 };
×
1700

1701
    if (imap_parse_path(mailbox_path(m), &cac, mailbox, sizeof(mailbox)) < 0)
×
1702
      return false;
×
1703

1704
    adata = imap_adata_new(a);
×
1705
    adata->conn = mutt_conn_new(&cac);
×
1706
    if (!adata->conn)
×
1707
    {
1708
      imap_adata_free((void **) &adata);
×
1709
      return false;
×
1710
    }
1711

1712
    mutt_account_hook(m->realpath);
×
1713

1714
    if (imap_login(adata) < 0)
×
1715
    {
1716
      imap_adata_free((void **) &adata);
×
1717
      return false;
×
1718
    }
1719

1720
    a->adata = adata;
×
1721
    a->adata_free = imap_adata_free;
×
1722
  }
1723

1724
  if (!m->mdata)
×
1725
  {
1726
    struct Url *url = url_parse(mailbox_path(m));
×
1727
    if (!url)
×
1728
      return false;
×
1729
    struct ImapMboxData *mdata = imap_mdata_new(adata, url->path);
×
1730

1731
    /* fixup path and realpath, mainly to replace / by /INBOX */
1732
    char buf[1024] = { 0 };
×
1733
    imap_qualify_path(buf, sizeof(buf), &adata->conn->account, mdata->name);
×
1734
    buf_strcpy(&m->pathbuf, buf);
×
1735
    mutt_str_replace(&m->realpath, mailbox_path(m));
×
1736

1737
    m->mdata = mdata;
×
1738
    m->mdata_free = imap_mdata_free;
×
1739
    url_free(&url);
×
1740
  }
1741
  return true;
1742
}
1743

1744
/**
1745
 * imap_mbox_select - Select a Mailbox
1746
 * @param m Mailbox
1747
 */
1748
static void imap_mbox_select(struct Mailbox *m)
×
1749
{
1750
  struct ImapAccountData *adata = imap_adata_get(m);
×
1751
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
1752
  if (!adata || !mdata)
×
1753
    return;
×
1754

1755
  const char *condstore = NULL;
1756
#ifdef USE_HCACHE
1757
  const bool c_imap_condstore = cs_subset_bool(NeoMutt->sub, "imap_condstore");
×
1758
  if ((adata->capabilities & IMAP_CAP_CONDSTORE) && c_imap_condstore)
×
1759
    condstore = " (CONDSTORE)";
1760
  else
1761
#endif
1762
    condstore = "";
1763

1764
  char buf[PATH_MAX] = { 0 };
×
1765
  snprintf(buf, sizeof(buf), "%s %s%s", m->readonly ? "EXAMINE" : "SELECT",
×
1766
           mdata->munge_name, condstore);
1767

1768
  adata->state = IMAP_SELECTED;
×
1769

1770
  imap_cmd_start(adata, buf);
×
1771
}
1772

1773
/**
1774
 * imap_login - Open an IMAP connection
1775
 * @param adata Imap Account data
1776
 * @retval  0 Success
1777
 * @retval -1 Failure
1778
 *
1779
 * Ensure ImapAccountData is connected and logged into the imap server.
1780
 */
1781
int imap_login(struct ImapAccountData *adata)
×
1782
{
1783
  if (!adata)
×
1784
    return -1;
1785

1786
  if (adata->state == IMAP_DISCONNECTED)
×
1787
  {
1788
    buf_reset(&adata->cmdbuf); // purge outstanding queued commands
×
1789
    imap_open_connection(adata);
×
1790
  }
1791
  if (adata->state == IMAP_CONNECTED)
×
1792
  {
1793
    if (imap_authenticate(adata) == IMAP_AUTH_SUCCESS)
×
1794
    {
1795
      adata->state = IMAP_AUTHENTICATED;
×
1796
      FREE(&adata->capstr);
×
1797
      if (adata->conn->ssf != 0)
×
1798
      {
1799
        mutt_debug(LL_DEBUG2, "Communication encrypted at %d bits\n",
×
1800
                   adata->conn->ssf);
1801
      }
1802
    }
1803
    else
1804
    {
1805
      mutt_account_unsetpass(&adata->conn->account);
×
1806
    }
1807
  }
1808
  if (adata->state == IMAP_AUTHENTICATED)
×
1809
  {
1810
    /* capabilities may have changed */
1811
    imap_exec(adata, "CAPABILITY", IMAP_CMD_PASS);
×
1812

1813
#ifdef USE_ZLIB
1814
    /* RFC4978 */
1815
    const bool c_imap_deflate = cs_subset_bool(NeoMutt->sub, "imap_deflate");
×
1816
    if ((adata->capabilities & IMAP_CAP_COMPRESS) && c_imap_deflate &&
×
1817
        (imap_exec(adata, "COMPRESS DEFLATE", IMAP_CMD_PASS) == IMAP_EXEC_SUCCESS))
×
1818
    {
1819
      mutt_debug(LL_DEBUG2, "IMAP compression is enabled on connection to %s\n",
×
1820
                 adata->conn->account.host);
1821
      mutt_zstrm_wrap_conn(adata->conn);
×
1822
    }
1823
#endif
1824

1825
    /* enable RFC2971, if the server supports that */
1826
    const bool c_imap_send_id = cs_subset_bool(NeoMutt->sub, "imap_send_id");
×
1827
    if (c_imap_send_id && (adata->capabilities & IMAP_CAP_ID))
×
1828
    {
1829
      imap_exec(adata, "ID (\"name\" \"NeoMutt\" \"version\" \"" PACKAGE_VERSION "\")",
×
1830
                IMAP_CMD_QUEUE);
1831
    }
1832

1833
    /* enable RFC6855, if the server supports that */
1834
    const bool c_imap_rfc5161 = cs_subset_bool(NeoMutt->sub, "imap_rfc5161");
×
1835
    if (c_imap_rfc5161 && (adata->capabilities & IMAP_CAP_ENABLE))
×
1836
      imap_exec(adata, "ENABLE UTF8=ACCEPT", IMAP_CMD_QUEUE);
×
1837

1838
    /* enable QRESYNC.  Advertising QRESYNC also means CONDSTORE
1839
     * is supported (even if not advertised), so flip that bit. */
1840
    if (adata->capabilities & IMAP_CAP_QRESYNC)
×
1841
    {
1842
      adata->capabilities |= IMAP_CAP_CONDSTORE;
×
1843
      const bool c_imap_qresync = cs_subset_bool(NeoMutt->sub, "imap_qresync");
×
1844
      if (c_imap_rfc5161 && c_imap_qresync)
×
1845
        imap_exec(adata, "ENABLE QRESYNC", IMAP_CMD_QUEUE);
×
1846
    }
1847

1848
    /* get root delimiter, '/' as default */
1849
    adata->delim = '/';
×
1850
    imap_exec(adata, "LIST \"\" \"\"", IMAP_CMD_QUEUE);
×
1851

1852
    /* we may need the root delimiter before we open a mailbox */
1853
    imap_exec(adata, NULL, IMAP_CMD_NO_FLAGS);
×
1854

1855
    /* select the mailbox that used to be open before disconnect */
1856
    if (adata->mailbox)
×
1857
    {
1858
      imap_mbox_select(adata->mailbox);
×
1859
    }
1860
  }
1861

1862
  if (adata->state < IMAP_AUTHENTICATED)
×
1863
    return -1;
1864

1865
  return 0;
1866
}
1867

1868
/**
1869
 * imap_mbox_open - Open a mailbox - Implements MxOps::mbox_open() - @ingroup mx_mbox_open
1870
 */
1871
static enum MxOpenReturns imap_mbox_open(struct Mailbox *m)
×
1872
{
1873
  if (!m->account || !m->mdata)
×
1874
    return MX_OPEN_ERROR;
1875

1876
  char buf[PATH_MAX] = { 0 };
×
1877
  int count = 0;
1878
  int rc;
1879

1880
  struct ImapAccountData *adata = imap_adata_get(m);
×
1881
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
1882

1883
  mutt_debug(LL_DEBUG3, "opening %s, saving %s\n", m->pathbuf.data,
×
1884
             (adata->mailbox ? adata->mailbox->pathbuf.data : "(none)"));
1885
  adata->prev_mailbox = adata->mailbox;
×
1886
  adata->mailbox = m;
×
1887

1888
  /* clear mailbox status */
1889
  adata->status = 0;
×
1890
  m->rights = 0;
×
1891
  mdata->new_mail_count = 0;
×
1892

1893
  if (m->verbose)
×
1894
    mutt_message(_("Selecting %s..."), mdata->name);
×
1895

1896
  /* pipeline ACL test */
1897
  if (adata->capabilities & IMAP_CAP_ACL)
×
1898
  {
1899
    snprintf(buf, sizeof(buf), "MYRIGHTS %s", mdata->munge_name);
×
1900
    imap_exec(adata, buf, IMAP_CMD_QUEUE);
×
1901
  }
1902
  else
1903
  {
1904
    /* assume we have all rights if ACL is unavailable */
1905
    m->rights |= MUTT_ACL_LOOKUP | MUTT_ACL_READ | MUTT_ACL_SEEN | MUTT_ACL_WRITE |
×
1906
                 MUTT_ACL_INSERT | MUTT_ACL_POST | MUTT_ACL_CREATE | MUTT_ACL_DELETE;
1907
  }
1908

1909
  /* pipeline the postponed count if possible */
1910
  const char *const c_postponed = cs_subset_string(NeoMutt->sub, "postponed");
×
1911
  struct Mailbox *m_postponed = mx_mbox_find2(c_postponed);
×
1912
  struct ImapAccountData *postponed_adata = imap_adata_get(m_postponed);
×
1913
  if (postponed_adata &&
×
1914
      imap_account_match(&postponed_adata->conn->account, &adata->conn->account))
×
1915
  {
1916
    imap_mailbox_status(m_postponed, true);
×
1917
  }
1918

1919
  const bool c_imap_check_subscribed = cs_subset_bool(NeoMutt->sub, "imap_check_subscribed");
×
1920
  if (c_imap_check_subscribed)
×
1921
    imap_exec(adata, "LSUB \"\" \"*\"", IMAP_CMD_QUEUE);
×
1922

1923
  imap_mbox_select(m);
×
1924

1925
  do
1926
  {
1927
    char *pc = NULL;
1928

1929
    rc = imap_cmd_step(adata);
×
1930
    if (rc != IMAP_RES_CONTINUE)
×
1931
      break;
1932

1933
    if (!mutt_strn_equal(adata->buf, "* ", 2))
×
1934
      continue;
×
1935
    pc = imap_next_word(adata->buf);
×
1936

1937
    /* Obtain list of available flags here, may be overridden by a
1938
     * PERMANENTFLAGS tag in the OK response */
1939
    if (mutt_istr_startswith(pc, "FLAGS"))
×
1940
    {
1941
      /* don't override PERMANENTFLAGS */
1942
      if (STAILQ_EMPTY(&mdata->flags))
×
1943
      {
1944
        mutt_debug(LL_DEBUG3, "Getting mailbox FLAGS\n");
×
1945
        pc = get_flags(&mdata->flags, pc);
×
1946
        if (!pc)
×
1947
          goto fail;
×
1948
      }
1949
    }
1950
    else if (mutt_istr_startswith(pc, "OK [PERMANENTFLAGS"))
×
1951
    {
1952
      /* PERMANENTFLAGS are massaged to look like FLAGS, then override FLAGS */
1953
      mutt_debug(LL_DEBUG3, "Getting mailbox PERMANENTFLAGS\n");
×
1954
      /* safe to call on NULL */
1955
      mutt_list_free(&mdata->flags);
×
1956
      /* skip "OK [PERMANENT" so syntax is the same as FLAGS */
1957
      pc += 13;
×
1958
      pc = get_flags(&(mdata->flags), pc);
×
1959
      if (!pc)
×
1960
        goto fail;
×
1961
    }
1962
    else if (mutt_istr_startswith(pc, "OK [UIDVALIDITY"))
×
1963
    {
1964
      /* save UIDVALIDITY for the header cache */
1965
      mutt_debug(LL_DEBUG3, "Getting mailbox UIDVALIDITY\n");
×
1966
      pc += 3;
×
1967
      pc = imap_next_word(pc);
×
1968
      if (!mutt_str_atoui(pc, &mdata->uidvalidity))
×
1969
        goto fail;
×
1970
    }
1971
    else if (mutt_istr_startswith(pc, "OK [UIDNEXT"))
×
1972
    {
1973
      mutt_debug(LL_DEBUG3, "Getting mailbox UIDNEXT\n");
×
1974
      pc += 3;
×
1975
      pc = imap_next_word(pc);
×
1976
      if (!mutt_str_atoui(pc, &mdata->uid_next))
×
1977
        goto fail;
×
1978
    }
1979
    else if (mutt_istr_startswith(pc, "OK [HIGHESTMODSEQ"))
×
1980
    {
1981
      mutt_debug(LL_DEBUG3, "Getting mailbox HIGHESTMODSEQ\n");
×
1982
      pc += 3;
×
1983
      pc = imap_next_word(pc);
×
1984
      if (!mutt_str_atoull(pc, &mdata->modseq))
×
1985
        goto fail;
×
1986
    }
1987
    else if (mutt_istr_startswith(pc, "OK [NOMODSEQ"))
×
1988
    {
1989
      mutt_debug(LL_DEBUG3, "Mailbox has NOMODSEQ set\n");
×
1990
      mdata->modseq = 0;
×
1991
    }
1992
    else
1993
    {
1994
      pc = imap_next_word(pc);
×
1995
      if (mutt_istr_startswith(pc, "EXISTS"))
×
1996
      {
1997
        count = mdata->new_mail_count;
×
1998
        mdata->new_mail_count = 0;
×
1999
      }
2000
    }
2001
  } while (rc == IMAP_RES_CONTINUE);
2002

2003
  if (rc == IMAP_RES_NO)
×
2004
  {
2005
    char *s = imap_next_word(adata->buf); /* skip seq */
×
2006
    s = imap_next_word(s);                /* Skip response */
×
2007
    mutt_error("%s", s);
×
2008
    goto fail;
×
2009
  }
2010

2011
  if (rc != IMAP_RES_OK)
×
2012
    goto fail;
×
2013

2014
  /* check for READ-ONLY notification */
2015
  if (mutt_istr_startswith(imap_get_qualifier(adata->buf), "[READ-ONLY]") &&
×
2016
      !(adata->capabilities & IMAP_CAP_ACL))
×
2017
  {
2018
    mutt_debug(LL_DEBUG2, "Mailbox is read-only\n");
×
2019
    m->readonly = true;
×
2020
  }
2021

2022
  /* dump the mailbox flags we've found */
2023
  const short c_debug_level = cs_subset_number(NeoMutt->sub, "debug_level");
×
2024
  if (c_debug_level > LL_DEBUG2)
×
2025
  {
2026
    if (STAILQ_EMPTY(&mdata->flags))
×
2027
    {
2028
      mutt_debug(LL_DEBUG3, "No folder flags found\n");
×
2029
    }
2030
    else
2031
    {
2032
      struct ListNode *np = NULL;
2033
      struct Buffer *flag_buffer = buf_pool_get();
×
2034
      buf_printf(flag_buffer, "Mailbox flags: ");
×
2035
      STAILQ_FOREACH(np, &mdata->flags, entries)
×
2036
      {
2037
        buf_add_printf(flag_buffer, "[%s] ", np->data);
×
2038
      }
2039
      mutt_debug(LL_DEBUG3, "%s\n", buf_string(flag_buffer));
×
2040
      buf_pool_release(&flag_buffer);
×
2041
    }
2042
  }
2043

2044
  if (!((m->rights & MUTT_ACL_DELETE) || (m->rights & MUTT_ACL_SEEN) ||
×
2045
        (m->rights & MUTT_ACL_WRITE) || (m->rights & MUTT_ACL_INSERT)))
2046
  {
2047
    m->readonly = true;
×
2048
  }
2049

2050
  mx_alloc_memory(m, count);
×
2051

2052
  m->msg_count = 0;
×
2053
  m->msg_unread = 0;
×
2054
  m->msg_flagged = 0;
×
2055
  m->msg_new = 0;
×
2056
  m->msg_deleted = 0;
×
2057
  m->size = 0;
×
2058
  m->vcount = 0;
×
2059

2060
  if ((count > 0) && (imap_read_headers(m, 1, count, true) < 0))
×
2061
  {
2062
    mutt_error(_("Error opening mailbox"));
×
2063
    goto fail;
×
2064
  }
2065

2066
  mutt_debug(LL_DEBUG2, "msg_count is %d\n", m->msg_count);
×
2067
  return MX_OPEN_OK;
×
2068

2069
fail:
×
2070
  if (adata->state == IMAP_SELECTED)
×
2071
    adata->state = IMAP_AUTHENTICATED;
×
2072
  return MX_OPEN_ERROR;
2073
}
2074

2075
/**
2076
 * imap_mbox_open_append - Open a Mailbox for appending - Implements MxOps::mbox_open_append() - @ingroup mx_mbox_open_append
2077
 */
2078
static bool imap_mbox_open_append(struct Mailbox *m, OpenMailboxFlags flags)
×
2079
{
2080
  if (!m->account)
×
2081
    return false;
2082

2083
  /* in APPEND mode, we appear to hijack an existing IMAP connection -
2084
   * mailbox is brand new and mostly empty */
2085
  struct ImapAccountData *adata = imap_adata_get(m);
×
2086
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
2087

2088
  int rc = imap_mailbox_status(m, false);
×
2089
  if (rc >= 0)
×
2090
    return true;
2091
  if (rc == -1)
×
2092
    return false;
2093

2094
  char buf[PATH_MAX + 64];
2095
  snprintf(buf, sizeof(buf), _("Create %s?"), mdata->name);
×
2096
  const bool c_confirm_create = cs_subset_bool(NeoMutt->sub, "confirm_create");
×
2097
  if (c_confirm_create &&
×
2098
      (query_yesorno_help(buf, MUTT_YES, NeoMutt->sub, "confirm_create") != MUTT_YES))
×
2099
    return false;
2100

2101
  if (imap_create_mailbox(adata, mdata->name) < 0)
×
2102
    return false;
2103

2104
  return true;
2105
}
2106

2107
/**
2108
 * imap_mbox_check - Check for new mail - Implements MxOps::mbox_check() - @ingroup mx_mbox_check
2109
 * @param m Mailbox
2110
 * @retval >0 Success, e.g. #MX_STATUS_REOPENED
2111
 * @retval -1 Failure
2112
 */
2113
static enum MxStatus imap_mbox_check(struct Mailbox *m)
×
2114
{
2115
  imap_allow_reopen(m);
×
2116
  enum MxStatus rc = imap_check_mailbox(m, false);
×
2117
  /* NOTE - mv might have been changed at this point. In particular,
2118
   * m could be NULL. Beware. */
2119
  imap_disallow_reopen(m);
×
2120

2121
  return rc;
×
2122
}
2123

2124
/**
2125
 * imap_mbox_close - Close a Mailbox - Implements MxOps::mbox_close() - @ingroup mx_mbox_close
2126
 */
2127
static enum MxStatus imap_mbox_close(struct Mailbox *m)
×
2128
{
2129
  struct ImapAccountData *adata = imap_adata_get(m);
×
2130
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
2131

2132
  /* Check to see if the mailbox is actually open */
2133
  if (!adata || !mdata)
×
2134
    return MX_STATUS_OK;
2135

2136
  /* imap_mbox_open_append() borrows the struct ImapAccountData temporarily,
2137
   * just for the connection.
2138
   *
2139
   * So when these are equal, it means we are actually closing the
2140
   * mailbox and should clean up adata.  Otherwise, we don't want to
2141
   * touch adata - it's still being used.  */
2142
  if (m == adata->mailbox)
×
2143
  {
2144
    if ((adata->status != IMAP_FATAL) && (adata->state >= IMAP_SELECTED))
×
2145
    {
2146
      /* mx_mbox_close won't sync if there are no deleted messages
2147
       * and the mailbox is unchanged, so we may have to close here */
2148
      if (m->msg_deleted == 0)
×
2149
      {
2150
        adata->closing = true;
×
2151
        imap_exec(adata, "CLOSE", IMAP_CMD_NO_FLAGS);
×
2152
      }
2153
      adata->state = IMAP_AUTHENTICATED;
×
2154
    }
2155

2156
    mutt_debug(LL_DEBUG3, "closing %s, restoring %s\n", m->pathbuf.data,
×
2157
               (adata->prev_mailbox ? adata->prev_mailbox->pathbuf.data : "(none)"));
2158
    adata->mailbox = adata->prev_mailbox;
×
2159
    imap_mbox_select(adata->prev_mailbox);
×
2160
    imap_mdata_cache_reset(m->mdata);
×
2161
  }
2162

2163
  return MX_STATUS_OK;
2164
}
2165

2166
/**
2167
 * imap_msg_open_new - Open a new message in a Mailbox - Implements MxOps::msg_open_new() - @ingroup mx_msg_open_new
2168
 */
2169
static bool imap_msg_open_new(struct Mailbox *m, struct Message *msg, const struct Email *e)
×
2170
{
2171
  bool success = false;
2172

2173
  struct Buffer *tempfile = buf_pool_get();
×
2174
  buf_mktemp(tempfile);
×
2175

2176
  msg->fp = mutt_file_fopen(buf_string(tempfile), "w");
×
2177
  if (!msg->fp)
×
2178
  {
2179
    mutt_perror("%s", buf_string(tempfile));
×
2180
    goto cleanup;
×
2181
  }
2182

2183
  msg->path = buf_strdup(tempfile);
×
2184
  success = true;
2185

2186
cleanup:
×
2187
  buf_pool_release(&tempfile);
×
2188
  return success;
×
2189
}
2190

2191
/**
2192
 * imap_tags_edit - Prompt and validate new messages tags - Implements MxOps::tags_edit() - @ingroup mx_tags_edit
2193
 */
2194
static int imap_tags_edit(struct Mailbox *m, const char *tags, struct Buffer *buf)
×
2195
{
2196
  struct ImapMboxData *mdata = imap_mdata_get(m);
×
2197
  if (!mdata)
×
2198
    return -1;
2199

2200
  char *new_tag = NULL;
2201
  char *checker = NULL;
2202

2203
  /* Check for \* flags capability */
2204
  if (!imap_has_flag(&mdata->flags, NULL))
×
2205
  {
2206
    mutt_error(_("IMAP server doesn't support custom flags"));
×
2207
    return -1;
×
2208
  }
2209

2210
  buf_reset(buf);
×
2211
  if (tags)
×
2212
    buf_strcpy(buf, tags);
×
2213

2214
  if (mw_get_field("Tags: ", buf, MUTT_COMP_NO_FLAGS, HC_OTHER, NULL, NULL) != 0)
×
2215
    return -1;
2216

2217
  /* each keyword must be atom defined by rfc822 as:
2218
   *
2219
   * atom           = 1*<any CHAR except specials, SPACE and CTLs>
2220
   * CHAR           = ( 0.-127. )
2221
   * specials       = "(" / ")" / "<" / ">" / "@"
2222
   *                  / "," / ";" / ":" / "\" / <">
2223
   *                  / "." / "[" / "]"
2224
   * SPACE          = ( 32. )
2225
   * CTLS           = ( 0.-31., 127.)
2226
   *
2227
   * And must be separated by one space.
2228
   */
2229

2230
  new_tag = buf->data;
×
2231
  checker = buf->data;
2232
  SKIPWS(checker);
×
2233
  while (*checker != '\0')
×
2234
  {
2235
    if ((*checker < 32) || (*checker >= 127) || // We allow space because it's the separator
×
2236
        (*checker == 40) ||                     // (
2237
        (*checker == 41) ||                     // )
2238
        (*checker == 60) ||                     // <
2239
        (*checker == 62) ||                     // >
2240
        (*checker == 64) ||                     // @
2241
        (*checker == 44) ||                     // ,
2242
        (*checker == 59) ||                     // ;
2243
        (*checker == 58) ||                     // :
2244
        (*checker == 92) ||                     // backslash
2245
        (*checker == 34) ||                     // "
2246
        (*checker == 46) ||                     // .
2247
        (*checker == 91) ||                     // [
2248
        (*checker == 93))                       // ]
2249
    {
2250
      mutt_error(_("Invalid IMAP flags"));
×
2251
      return 0;
×
2252
    }
2253

2254
    /* Skip duplicate space */
2255
    while ((checker[0] == ' ') && (checker[1] == ' '))
×
2256
      checker++;
×
2257

2258
    /* copy char to new_tag and go the next one */
2259
    *new_tag++ = *checker++;
×
2260
  }
2261
  *new_tag = '\0';
×
2262
  new_tag = buf->data; /* rewind */
×
2263
  mutt_str_remove_trailing_ws(new_tag);
×
2264

2265
  return !mutt_str_equal(tags, buf_string(buf));
×
2266
}
2267

2268
/**
2269
 * imap_tags_commit - Save the tags to a message - Implements MxOps::tags_commit() - @ingroup mx_tags_commit
2270
 *
2271
 * This method update the server flags on the server by
2272
 * removing the last know custom flags of a header
2273
 * and adds the local flags
2274
 *
2275
 * If everything success we push the local flags to the
2276
 * last know custom flags (flags_remote).
2277
 *
2278
 * Also this method check that each flags is support by the server
2279
 * first and remove unsupported one.
2280
 */
2281
static int imap_tags_commit(struct Mailbox *m, struct Email *e, const char *buf)
×
2282
{
2283
  char uid[11] = { 0 };
×
2284

2285
  struct ImapAccountData *adata = imap_adata_get(m);
×
2286

2287
  if (*buf == '\0')
×
2288
    buf = NULL;
2289

2290
  if (!(adata->mailbox->rights & MUTT_ACL_WRITE))
×
2291
    return 0;
2292

2293
  snprintf(uid, sizeof(uid), "%u", imap_edata_get(e)->uid);
×
2294

2295
  /* Remove old custom flags */
2296
  if (imap_edata_get(e)->flags_remote)
×
2297
  {
2298
    struct Buffer *cmd = buf_pool_get();
×
2299
    buf_addstr(cmd, "UID STORE ");
×
2300
    buf_addstr(cmd, uid);
×
2301
    buf_addstr(cmd, " -FLAGS.SILENT (");
×
2302
    buf_addstr(cmd, imap_edata_get(e)->flags_remote);
×
2303
    buf_addstr(cmd, ")");
×
2304

2305
    /* Should we return here, or we are fine and we could
2306
     * continue to add new flags */
2307
    int rc = imap_exec(adata, buf_string(cmd), IMAP_CMD_NO_FLAGS);
×
2308
    buf_pool_release(&cmd);
×
2309
    if (rc != IMAP_EXEC_SUCCESS)
×
2310
    {
2311
      return -1;
×
2312
    }
2313
  }
2314

2315
  /* Add new custom flags */
2316
  if (buf)
×
2317
  {
2318
    struct Buffer *cmd = buf_pool_get();
×
2319
    buf_addstr(cmd, "UID STORE ");
×
2320
    buf_addstr(cmd, uid);
×
2321
    buf_addstr(cmd, " +FLAGS.SILENT (");
×
2322
    buf_addstr(cmd, buf);
×
2323
    buf_addstr(cmd, ")");
×
2324

2325
    int rc = imap_exec(adata, buf_string(cmd), IMAP_CMD_NO_FLAGS);
×
2326
    buf_pool_release(&cmd);
×
2327
    if (rc != IMAP_EXEC_SUCCESS)
×
2328
    {
2329
      mutt_debug(LL_DEBUG1, "fail to add new flags\n");
×
2330
      return -1;
×
2331
    }
2332
  }
2333

2334
  /* We are good sync them */
2335
  mutt_debug(LL_DEBUG1, "NEW TAGS: %s\n", buf);
×
2336
  driver_tags_replace(&e->tags, buf);
×
2337
  FREE(&imap_edata_get(e)->flags_remote);
×
2338
  struct Buffer *flags_remote = buf_pool_get();
×
2339
  driver_tags_get_with_hidden(&e->tags, flags_remote);
×
2340
  imap_edata_get(e)->flags_remote = buf_strdup(flags_remote);
×
2341
  buf_pool_release(&flags_remote);
×
2342
  imap_msg_save_hcache(m, e);
×
2343
  return 0;
×
2344
}
2345

2346
/**
2347
 * imap_path_probe - Is this an IMAP Mailbox? - Implements MxOps::path_probe() - @ingroup mx_path_probe
2348
 */
2349
enum MailboxType imap_path_probe(const char *path, const struct stat *st)
×
2350
{
2351
  if (mutt_istr_startswith(path, "imap://"))
×
2352
    return MUTT_IMAP;
2353

2354
  if (mutt_istr_startswith(path, "imaps://"))
×
2355
    return MUTT_IMAP;
2356

2357
  return MUTT_UNKNOWN;
2358
}
2359

2360
/**
2361
 * imap_path_canon - Canonicalise a Mailbox path - Implements MxOps::path_canon() - @ingroup mx_path_canon
2362
 */
2363
int imap_path_canon(struct Buffer *path)
×
2364
{
2365
  struct Url *url = url_parse(buf_string(path));
×
2366
  if (!url)
×
2367
    return 0;
2368

2369
  char tmp[PATH_MAX] = { 0 };
×
2370
  char tmp2[PATH_MAX] = { 0 };
×
2371
  if (url->path)
×
2372
  {
2373
    struct ImapAccountData *adata = NULL;
×
2374
    if (imap_adata_find(buf_string(path), &adata, NULL) == 0)
×
2375
    {
2376
      imap_fix_path_with_delim(adata->delim, url->path, tmp, sizeof(tmp));
×
2377
    }
2378
    else
2379
    {
2380
      imap_fix_path(url->path, tmp, sizeof(tmp));
×
2381
    }
2382
    url->path = tmp;
×
2383
  }
2384
  url_tostring(url, tmp2, sizeof(tmp2), U_NO_FLAGS);
×
2385
  buf_strcpy(path, tmp2);
×
2386
  url_free(&url);
×
2387

2388
  return 0;
×
2389
}
2390

2391
/**
2392
 * imap_path_is_empty - Is the mailbox empty - Implements MxOps::path_is_empty() - @ingroup mx_path_is_empty
2393
 */
2394
static int imap_path_is_empty(struct Buffer *path)
×
2395
{
2396
  int rc = imap_path_status(buf_string(path), false);
×
2397
  if (rc < 0)
×
2398
    return -1;
2399
  if (rc == 0)
×
2400
    return 1;
×
2401
  return 0;
2402
}
2403

2404
/**
2405
 * MxImapOps - IMAP Mailbox - Implements ::MxOps - @ingroup mx_api
2406
 */
2407
const struct MxOps MxImapOps = {
2408
  // clang-format off
2409
  .type            = MUTT_IMAP,
2410
  .name             = "imap",
2411
  .is_local         = false,
2412
  .ac_owns_path     = imap_ac_owns_path,
2413
  .ac_add           = imap_ac_add,
2414
  .mbox_open        = imap_mbox_open,
2415
  .mbox_open_append = imap_mbox_open_append,
2416
  .mbox_check       = imap_mbox_check,
2417
  .mbox_check_stats = imap_mbox_check_stats,
2418
  .mbox_sync        = NULL, /* imap syncing is handled by imap_sync_mailbox */
2419
  .mbox_close       = imap_mbox_close,
2420
  .msg_open         = imap_msg_open,
2421
  .msg_open_new     = imap_msg_open_new,
2422
  .msg_commit       = imap_msg_commit,
2423
  .msg_close        = imap_msg_close,
2424
  .msg_padding_size = NULL,
2425
  .msg_save_hcache  = imap_msg_save_hcache,
2426
  .tags_edit        = imap_tags_edit,
2427
  .tags_commit      = imap_tags_commit,
2428
  .path_probe       = imap_path_probe,
2429
  .path_canon       = imap_path_canon,
2430
  .path_is_empty    = imap_path_is_empty,
2431
  // clang-format on
2432
};
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