Coverage Report

Created: 2023-06-07 06:15

/src/neomutt/imap/browse.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * @file
3
 * GUI select an IMAP mailbox from a list
4
 *
5
 * @authors
6
 * Copyright (C) 1996-1999 Brandon Long <blong@fiction.net>
7
 * Copyright (C) 1999-2008 Brendan Cully <brendan@kublai.com>
8
 *
9
 * @copyright
10
 * This program is free software: you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License as published by the Free Software
12
 * Foundation, either version 2 of the License, or (at your option) any later
13
 * version.
14
 *
15
 * This program is distributed in the hope that it will be useful, but WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18
 * details.
19
 *
20
 * You should have received a copy of the GNU General Public License along with
21
 * this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
/**
25
 * @page imap_browse Mailbox browser
26
 *
27
 * GUI select an IMAP mailbox from a list
28
 */
29
30
#include "config.h"
31
#include <limits.h>
32
#include <stdbool.h>
33
#include <stdio.h>
34
#include <string.h>
35
#include "private.h"
36
#include "mutt/lib.h"
37
#include "config/lib.h"
38
#include "email/lib.h"
39
#include "core/lib.h"
40
#include "conn/lib.h"
41
#include "mutt.h"
42
#include "lib.h"
43
#include "browser/lib.h"
44
#include "enter/lib.h"
45
#include "adata.h"
46
#include "mdata.h"
47
#include "mutt_logging.h"
48
#include "muttlib.h"
49
50
/**
51
 * add_folder - Format and add an IMAP folder to the browser
52
 * @param delim       Path delimiter
53
 * @param folder      Name of the folder
54
 * @param noselect    true if item isn't selectable
55
 * @param noinferiors true if item has no children
56
 * @param state       Browser state to add to
57
 * @param isparent    true if item represents the parent folder
58
 *
59
 * The folder parameter should already be 'unmunged' via
60
 * imap_unmunge_mbox_name().
61
 */
62
static void add_folder(char delim, char *folder, bool noselect, bool noinferiors,
63
                       struct BrowserState *state, bool isparent)
64
0
{
65
0
  char tmp[PATH_MAX] = { 0 };
66
0
  char relpath[PATH_MAX] = { 0 };
67
0
  struct ConnAccount cac = { { 0 } };
68
0
  char mailbox[1024] = { 0 };
69
0
  struct FolderFile ff = { 0 };
70
71
0
  if (imap_parse_path(state->folder, &cac, mailbox, sizeof(mailbox)))
72
0
    return;
73
74
0
  if (isparent)
75
0
  {
76
    /* render superiors as unix-standard ".." */
77
0
    mutt_str_copy(relpath, "../", sizeof(relpath));
78
0
  }
79
0
  else if (mutt_str_startswith(folder, mailbox))
80
0
  {
81
    /* strip current folder from target, to render a relative path */
82
0
    mutt_str_copy(relpath, folder + mutt_str_len(mailbox), sizeof(relpath));
83
0
  }
84
0
  else
85
0
  {
86
0
    mutt_str_copy(relpath, folder, sizeof(relpath));
87
0
  }
88
89
  /* apply filemask filter. This should really be done at menu setup rather
90
   * than at scan, since it's so expensive to scan. But that's big changes
91
   * to browser.c */
92
0
  const struct Regex *c_mask = cs_subset_regex(NeoMutt->sub, "mask");
93
0
  if (!mutt_regex_match(c_mask, relpath))
94
0
  {
95
0
    return;
96
0
  }
97
98
0
  imap_qualify_path(tmp, sizeof(tmp), &cac, folder);
99
0
  ff.name = mutt_str_dup(tmp);
100
101
  /* mark desc with delim in browser if it can have subfolders */
102
0
  if (!isparent && !noinferiors && (strlen(relpath) < sizeof(relpath) - 1))
103
0
  {
104
0
    relpath[strlen(relpath) + 1] = '\0';
105
0
    relpath[strlen(relpath)] = delim;
106
0
  }
107
108
0
  ff.desc = mutt_str_dup(relpath);
109
0
  ff.imap = true;
110
111
  /* delimiter at the root is useless. */
112
0
  if (folder[0] == '\0')
113
0
    delim = '\0';
114
0
  ff.delim = delim;
115
0
  ff.selectable = !noselect;
116
0
  ff.inferiors = !noinferiors;
117
118
0
  struct MailboxList ml = STAILQ_HEAD_INITIALIZER(ml);
119
0
  neomutt_mailboxlist_get_all(&ml, NeoMutt, MUTT_MAILBOX_ANY);
120
0
  struct MailboxNode *np = NULL;
121
0
  STAILQ_FOREACH(np, &ml, entries)
122
0
  {
123
0
    if (mutt_str_equal(tmp, mailbox_path(np->mailbox)))
124
0
      break;
125
0
  }
126
127
0
  if (np)
128
0
  {
129
0
    ff.has_mailbox = true;
130
0
    ff.has_new_mail = np->mailbox->has_new;
131
0
    ff.msg_count = np->mailbox->msg_count;
132
0
    ff.msg_unread = np->mailbox->msg_unread;
133
0
  }
134
0
  neomutt_mailboxlist_clear(&ml);
135
136
0
  ARRAY_ADD(&state->entry, ff);
137
0
}
138
139
/**
140
 * browse_add_list_result - Add entries to the folder browser
141
 * @param adata    Imap Account data
142
 * @param cmd      Command string from server
143
 * @param bstate   Browser state to add to
144
 * @param isparent Is this a shortcut for the parent directory?
145
 * @retval  0 Success
146
 * @retval -1 Failure
147
 */
148
static int browse_add_list_result(struct ImapAccountData *adata, const char *cmd,
149
                                  struct BrowserState *bstate, bool isparent)
150
0
{
151
0
  struct ImapList list = { 0 };
152
0
  int rc;
153
0
  struct Url *url = url_parse(bstate->folder);
154
0
  if (!url)
155
0
    return -1;
156
157
0
  imap_cmd_start(adata, cmd);
158
0
  adata->cmdresult = &list;
159
0
  do
160
0
  {
161
0
    list.name = NULL;
162
0
    rc = imap_cmd_step(adata);
163
164
0
    if ((rc == IMAP_RES_CONTINUE) && list.name)
165
0
    {
166
      /* Let a parent folder never be selectable for navigation */
167
0
      if (isparent)
168
0
        list.noselect = true;
169
      /* prune current folder from output */
170
0
      if (isparent || !mutt_str_startswith(url->path, list.name))
171
0
        add_folder(list.delim, list.name, list.noselect, list.noinferiors, bstate, isparent);
172
0
    }
173
0
  } while (rc == IMAP_RES_CONTINUE);
174
0
  adata->cmdresult = NULL;
175
176
0
  url_free(&url);
177
178
0
  return (rc == IMAP_RES_OK) ? 0 : -1;
179
0
}
180
181
/**
182
 * imap_browse - IMAP hook into the folder browser
183
 * @param path  Current folder
184
 * @param state BrowserState to populate
185
 * @retval  0 Success
186
 * @retval -1 Failure
187
 *
188
 * Fill out browser_state, given a current folder to browse
189
 */
190
int imap_browse(const char *path, struct BrowserState *state)
191
0
{
192
0
  struct ImapAccountData *adata = NULL;
193
0
  struct ImapList list = { 0 };
194
0
  struct ConnAccount cac = { { 0 } };
195
0
  char buf[PATH_MAX + 16];
196
0
  char mbox[PATH_MAX] = { 0 };
197
0
  char munged_mbox[PATH_MAX] = { 0 };
198
0
  const char *list_cmd = NULL;
199
0
  int len;
200
0
  int n;
201
0
  char ctmp;
202
0
  bool showparents = false;
203
204
0
  if (imap_parse_path(path, &cac, buf, sizeof(buf)))
205
0
  {
206
0
    mutt_error(_("%s is an invalid IMAP path"), path);
207
0
    return -1;
208
0
  }
209
210
0
  const bool c_imap_check_subscribed = cs_subset_bool(NeoMutt->sub, "imap_check_subscribed");
211
0
  cs_subset_str_native_set(NeoMutt->sub, "imap_check_subscribed", false, NULL);
212
213
  // Pick first mailbox connected to the same server
214
0
  struct MailboxList ml = STAILQ_HEAD_INITIALIZER(ml);
215
0
  neomutt_mailboxlist_get_all(&ml, NeoMutt, MUTT_IMAP);
216
0
  struct MailboxNode *np = NULL;
217
0
  STAILQ_FOREACH(np, &ml, entries)
218
0
  {
219
0
    adata = imap_adata_get(np->mailbox);
220
    // Pick first mailbox connected on the same server
221
0
    if (imap_account_match(&adata->conn->account, &cac))
222
0
      break;
223
0
    adata = NULL;
224
0
  }
225
0
  neomutt_mailboxlist_clear(&ml);
226
0
  if (!adata)
227
0
    goto fail;
228
229
0
  const bool c_imap_list_subscribed = cs_subset_bool(NeoMutt->sub, "imap_list_subscribed");
230
0
  if (c_imap_list_subscribed)
231
0
  {
232
    /* RFC3348 section 3 states LSUB is unreliable for hierarchy information.
233
     * The newer LIST extensions are designed for this.  */
234
0
    if (adata->capabilities & IMAP_CAP_LIST_EXTENDED)
235
0
      list_cmd = "LIST (SUBSCRIBED RECURSIVEMATCH)";
236
0
    else
237
0
      list_cmd = "LSUB";
238
0
  }
239
0
  else
240
0
  {
241
0
    list_cmd = "LIST";
242
0
  }
243
244
0
  mutt_message(_("Getting folder list..."));
245
246
  /* skip check for parents when at the root */
247
0
  if (buf[0] == '\0')
248
0
  {
249
0
    mbox[0] = '\0';
250
0
    n = 0;
251
0
  }
252
0
  else
253
0
  {
254
0
    imap_fix_path(adata->delim, buf, mbox, sizeof(mbox));
255
0
    n = mutt_str_len(mbox);
256
0
  }
257
258
0
  if (n)
259
0
  {
260
0
    int rc;
261
0
    mutt_debug(LL_DEBUG3, "mbox: %s\n", mbox);
262
263
    /* if our target exists and has inferiors, enter it if we
264
     * aren't already going to */
265
0
    imap_munge_mbox_name(adata->unicode, munged_mbox, sizeof(munged_mbox), mbox);
266
0
    len = snprintf(buf, sizeof(buf), "%s \"\" %s", list_cmd, munged_mbox);
267
0
    if (adata->capabilities & IMAP_CAP_LIST_EXTENDED)
268
0
      snprintf(buf + len, sizeof(buf) - len, " RETURN (CHILDREN)");
269
0
    imap_cmd_start(adata, buf);
270
0
    adata->cmdresult = &list;
271
0
    do
272
0
    {
273
0
      list.name = 0;
274
0
      rc = imap_cmd_step(adata);
275
0
      if ((rc == IMAP_RES_CONTINUE) && list.name)
276
0
      {
277
0
        if (!list.noinferiors && list.name[0] &&
278
0
            (imap_mxcmp(list.name, mbox) == 0) && (n < sizeof(mbox) - 1))
279
0
        {
280
0
          mbox[n++] = list.delim;
281
0
          mbox[n] = '\0';
282
0
        }
283
0
      }
284
0
    } while (rc == IMAP_RES_CONTINUE);
285
0
    adata->cmdresult = NULL;
286
287
    /* if we're descending a folder, mark it as current in browser_state */
288
0
    if (mbox[n - 1] == list.delim)
289
0
    {
290
0
      showparents = true;
291
0
      imap_qualify_path(buf, sizeof(buf), &cac, mbox);
292
0
      state->folder = mutt_str_dup(buf);
293
0
      n--;
294
0
    }
295
296
    /* Find superiors to list
297
     * Note: UW-IMAP servers return folder + delimiter when asked to list
298
     *  folder + delimiter. Cyrus servers don't. So we ask for folder,
299
     *  and tack on delimiter ourselves.
300
     * Further note: UW-IMAP servers return nothing when asked for
301
     *  NAMESPACES without delimiters at the end. Argh! */
302
0
    for (n--; n >= 0 && mbox[n] != list.delim; n--)
303
0
      ; // do nothing
304
305
0
    if (n > 0) /* "aaaa/bbbb/" -> "aaaa" */
306
0
    {
307
      /* forget the check, it is too delicate (see above). Have we ever
308
       * had the parent not exist? */
309
0
      ctmp = mbox[n];
310
0
      mbox[n] = '\0';
311
312
0
      if (showparents)
313
0
      {
314
0
        mutt_debug(LL_DEBUG3, "adding parent %s\n", mbox);
315
0
        add_folder(list.delim, mbox, true, false, state, true);
316
0
      }
317
318
      /* if our target isn't a folder, we are in our superior */
319
0
      if (!state->folder)
320
0
      {
321
        /* store folder with delimiter */
322
0
        mbox[n++] = ctmp;
323
0
        ctmp = mbox[n];
324
0
        mbox[n] = '\0';
325
0
        imap_qualify_path(buf, sizeof(buf), &cac, mbox);
326
0
        state->folder = mutt_str_dup(buf);
327
0
      }
328
0
      mbox[n] = ctmp;
329
0
    }
330
0
    else
331
0
    {
332
      /* "/bbbb/" -> add  "/", "aaaa/" -> add "" */
333
0
      char relpath[2] = { 0 };
334
      /* folder may be "/" */
335
0
      snprintf(relpath, sizeof(relpath), "%c", (n < 0) ? '\0' : adata->delim);
336
0
      if (showparents)
337
0
        add_folder(adata->delim, relpath, true, false, state, true);
338
0
      if (!state->folder)
339
0
      {
340
0
        imap_qualify_path(buf, sizeof(buf), &cac, relpath);
341
0
        state->folder = mutt_str_dup(buf);
342
0
      }
343
0
    }
344
0
  }
345
346
  /* no namespace, no folder: set folder to host only */
347
0
  if (!state->folder)
348
0
  {
349
0
    imap_qualify_path(buf, sizeof(buf), &cac, NULL);
350
0
    state->folder = mutt_str_dup(buf);
351
0
  }
352
353
0
  mutt_debug(LL_DEBUG3, "Quoting mailbox scan: %s -> ", mbox);
354
0
  snprintf(buf, sizeof(buf), "%s%%", mbox);
355
0
  imap_munge_mbox_name(adata->unicode, munged_mbox, sizeof(munged_mbox), buf);
356
0
  mutt_debug(LL_DEBUG3, "%s\n", munged_mbox);
357
0
  len = snprintf(buf, sizeof(buf), "%s \"\" %s", list_cmd, munged_mbox);
358
0
  if (adata->capabilities & IMAP_CAP_LIST_EXTENDED)
359
0
    snprintf(buf + len, sizeof(buf) - len, " RETURN (CHILDREN)");
360
0
  if (browse_add_list_result(adata, buf, state, false))
361
0
    goto fail;
362
363
0
  if (ARRAY_EMPTY(&state->entry))
364
0
  {
365
    // L10N: (%s) is the name / path of the folder we were trying to browse
366
0
    mutt_error(_("No such folder: %s"), path);
367
0
    goto fail;
368
0
  }
369
370
0
  mutt_clear_error();
371
372
0
  cs_subset_str_native_set(NeoMutt->sub, "imap_check_subscribed",
373
0
                           c_imap_check_subscribed, NULL);
374
0
  return 0;
375
376
0
fail:
377
0
  cs_subset_str_native_set(NeoMutt->sub, "imap_check_subscribed",
378
0
                           c_imap_check_subscribed, NULL);
379
0
  return -1;
380
0
}
381
382
/**
383
 * imap_mailbox_create - Create a new IMAP mailbox
384
 * @param path Mailbox to create
385
 * @retval  0 Success
386
 * @retval -1 Failure
387
 *
388
 * Prompt for a new mailbox name, and try to create it
389
 */
390
int imap_mailbox_create(const char *path)
391
0
{
392
0
  struct ImapAccountData *adata = NULL;
393
0
  struct ImapMboxData *mdata = NULL;
394
0
  struct Buffer *name = buf_pool_get();
395
0
  int rc = -1;
396
397
0
  if (imap_adata_find(path, &adata, &mdata) < 0)
398
0
  {
399
0
    mutt_debug(LL_DEBUG1, "Couldn't find open connection to %s\n", path);
400
0
    goto done;
401
0
  }
402
403
  /* append a delimiter if necessary */
404
0
  const size_t n = buf_strcpy(name, mdata->real_name);
405
0
  if ((n != 0) && (name->data[n - 1] != adata->delim))
406
0
  {
407
0
    buf_addch(name, adata->delim);
408
0
  }
409
410
0
  if (buf_get_field(_("Create mailbox: "), name, MUTT_COMP_FILE, false, NULL, NULL, NULL) != 0)
411
0
  {
412
0
    goto done;
413
0
  }
414
415
0
  if (buf_is_empty(name))
416
0
  {
417
0
    mutt_error(_("Mailbox must have a name"));
418
0
    goto done;
419
0
  }
420
421
0
  if (imap_create_mailbox(adata, buf_string(name)) < 0)
422
0
    goto done;
423
424
0
  imap_mdata_free((void *) &mdata);
425
0
  mutt_message(_("Mailbox created"));
426
0
  mutt_sleep(0);
427
0
  rc = 0;
428
429
0
done:
430
0
  imap_mdata_free((void *) &mdata);
431
0
  buf_pool_release(&name);
432
0
  return rc;
433
0
}
434
435
/**
436
 * imap_mailbox_rename - Rename a mailbox
437
 * @param path Mailbox to rename
438
 * @retval  0 Success
439
 * @retval -1 Failure
440
 *
441
 * The user will be prompted for a new name.
442
 */
443
int imap_mailbox_rename(const char *path)
444
0
{
445
0
  struct ImapAccountData *adata = NULL;
446
0
  struct ImapMboxData *mdata = NULL;
447
0
  struct Buffer *buf = NULL;
448
0
  struct Buffer *newname = NULL;
449
0
  int rc = -1;
450
451
0
  if (imap_adata_find(path, &adata, &mdata) < 0)
452
0
  {
453
0
    mutt_debug(LL_DEBUG1, "Couldn't find open connection to %s\n", path);
454
0
    goto done;
455
0
  }
456
457
0
  if (mdata->real_name[0] == '\0')
458
0
  {
459
0
    mutt_error(_("Can't rename root folder"));
460
0
    goto done;
461
0
  }
462
463
0
  buf = buf_pool_get();
464
0
  newname = buf_pool_get();
465
466
0
  buf_printf(buf, _("Rename mailbox %s to: "), mdata->name);
467
0
  buf_strcpy(newname, mdata->name);
468
469
0
  if (buf_get_field(buf_string(buf), newname, MUTT_COMP_FILE, false, NULL, NULL, NULL) != 0)
470
0
  {
471
0
    goto done;
472
0
  }
473
474
0
  if (buf_is_empty(newname))
475
0
  {
476
0
    mutt_error(_("Mailbox must have a name"));
477
0
    goto done;
478
0
  }
479
480
0
  imap_fix_path(adata->delim, buf_string(newname), buf->data, buf->dsize);
481
482
0
  if (imap_rename_mailbox(adata, mdata->name, buf_string(buf)) < 0)
483
0
  {
484
0
    mutt_error(_("Rename failed: %s"), imap_get_qualifier(adata->buf));
485
0
    goto done;
486
0
  }
487
488
0
  mutt_message(_("Mailbox renamed"));
489
0
  mutt_sleep(0);
490
0
  rc = 0;
491
492
0
done:
493
0
  imap_mdata_free((void *) &mdata);
494
0
  buf_pool_release(&buf);
495
0
  buf_pool_release(&newname);
496
497
0
  return rc;
498
0
}