Coverage Report

Created: 2023-09-25 07:17

/src/neomutt/muttlib.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * @file
3
 * Some miscellaneous functions
4
 *
5
 * @authors
6
 * Copyright (C) 1996-2000,2007,2010,2013 Michael R. Elkins <me@mutt.org>
7
 * Copyright (C) 1999-2008 Thomas Roessler <roessler@does-not-exist.org>
8
 * Copyright (C) 2019 Pietro Cerutti <gahr@gahr.ch>
9
 *
10
 * @copyright
11
 * This program is free software: you can redistribute it and/or modify it under
12
 * the terms of the GNU General Public License as published by the Free Software
13
 * Foundation, either version 2 of the License, or (at your option) any later
14
 * version.
15
 *
16
 * This program is distributed in the hope that it will be useful, but WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19
 * details.
20
 *
21
 * You should have received a copy of the GNU General Public License along with
22
 * this program.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
/**
26
 * @page neo_muttlib Some miscellaneous functions
27
 *
28
 * Some miscellaneous functions
29
 */
30
31
#include "config.h"
32
#include <ctype.h>
33
#include <errno.h>
34
#include <limits.h>
35
#include <pwd.h>
36
#include <stdbool.h>
37
#include <stdint.h>
38
#include <stdio.h>
39
#include <stdlib.h>
40
#include <string.h>
41
#include <sys/stat.h>
42
#include <unistd.h>
43
#include "mutt/lib.h"
44
#include "address/lib.h"
45
#include "config/lib.h"
46
#include "email/lib.h"
47
#include "core/lib.h"
48
#include "alias/lib.h"
49
#include "gui/lib.h"
50
#include "mutt.h"
51
#include "muttlib.h"
52
#include "browser/lib.h"
53
#include "editor/lib.h"
54
#include "history/lib.h"
55
#include "ncrypt/lib.h"
56
#include "parse/lib.h"
57
#include "question/lib.h"
58
#include "format_flags.h"
59
#include "globals.h" // IWYU pragma: keep
60
#include "hook.h"
61
#include "mx.h"
62
#include "protos.h"
63
#ifdef USE_IMAP
64
#include "imap/lib.h"
65
#endif
66
67
/// Accepted XDG environment variables
68
static const char *XdgEnvVars[] = {
69
  [XDG_CONFIG_HOME] = "XDG_CONFIG_HOME",
70
  [XDG_CONFIG_DIRS] = "XDG_CONFIG_DIRS",
71
};
72
73
/// XDG default locations
74
static const char *XdgDefaults[] = {
75
  [XDG_CONFIG_HOME] = "~/.config",
76
  [XDG_CONFIG_DIRS] = "/etc/xdg",
77
};
78
79
/**
80
 * mutt_adv_mktemp - Create a temporary file
81
 * @param buf Buffer for the name
82
 *
83
 * Accept a "suggestion" for file name.  If that file exists, then
84
 * construct one with unique name but keep any extension.
85
 * This might fail, I guess.
86
 */
87
void mutt_adv_mktemp(struct Buffer *buf)
88
0
{
89
0
  if (!(buf->data && (buf->data[0] != '\0')))
90
0
  {
91
0
    buf_mktemp(buf);
92
0
  }
93
0
  else
94
0
  {
95
0
    struct Buffer *prefix = buf_pool_get();
96
0
    buf_strcpy(prefix, buf->data);
97
0
    mutt_file_sanitize_filename(prefix->data, true);
98
0
    const char *const c_tmp_dir = cs_subset_path(NeoMutt->sub, "tmp_dir");
99
0
    buf_printf(buf, "%s/%s", NONULL(c_tmp_dir), buf_string(prefix));
100
101
0
    struct stat st = { 0 };
102
0
    if ((lstat(buf_string(buf), &st) == -1) && (errno == ENOENT))
103
0
      goto out;
104
105
0
    char *suffix = strchr(prefix->data, '.');
106
0
    if (suffix)
107
0
    {
108
0
      *suffix = '\0';
109
0
      suffix++;
110
0
    }
111
0
    buf_mktemp_pfx_sfx(buf, prefix->data, suffix);
112
113
0
  out:
114
0
    buf_pool_release(&prefix);
115
0
  }
116
0
}
117
118
/**
119
 * mutt_expand_path - Create the canonical path
120
 * @param buf    Buffer with path
121
 * @param buflen Length of buffer
122
 * @retval ptr The expanded string
123
 *
124
 * @note The path is expanded in-place
125
 */
126
char *mutt_expand_path(char *buf, size_t buflen)
127
0
{
128
0
  return mutt_expand_path_regex(buf, buflen, false);
129
0
}
130
131
/**
132
 * buf_expand_path_regex - Create the canonical path (with regex char escaping)
133
 * @param buf     Buffer with path
134
 * @param regex If true, escape any regex characters
135
 *
136
 * @note The path is expanded in-place
137
 */
138
void buf_expand_path_regex(struct Buffer *buf, bool regex)
139
0
{
140
0
  const char *s = NULL;
141
0
  const char *tail = "";
142
143
0
  bool recurse = false;
144
145
0
  struct Buffer *p = buf_pool_get();
146
0
  struct Buffer *q = buf_pool_get();
147
0
  struct Buffer *tmp = buf_pool_get();
148
149
0
  do
150
0
  {
151
0
    recurse = false;
152
0
    s = buf_string(buf);
153
154
0
    switch (*s)
155
0
    {
156
0
      case '~':
157
0
      {
158
0
        if ((s[1] == '/') || (s[1] == '\0'))
159
0
        {
160
0
          buf_strcpy(p, HomeDir);
161
0
          tail = s + 1;
162
0
        }
163
0
        else
164
0
        {
165
0
          char *t = strchr(s + 1, '/');
166
0
          if (t)
167
0
            *t = '\0';
168
169
0
          struct passwd *pw = getpwnam(s + 1);
170
0
          if (pw)
171
0
          {
172
0
            buf_strcpy(p, pw->pw_dir);
173
0
            if (t)
174
0
            {
175
0
              *t = '/';
176
0
              tail = t;
177
0
            }
178
0
            else
179
0
            {
180
0
              tail = "";
181
0
            }
182
0
          }
183
0
          else
184
0
          {
185
            /* user not found! */
186
0
            if (t)
187
0
              *t = '/';
188
0
            buf_reset(p);
189
0
            tail = s;
190
0
          }
191
0
        }
192
0
        break;
193
0
      }
194
195
0
      case '=':
196
0
      case '+':
197
0
      {
198
0
        const char *const c_folder = cs_subset_string(NeoMutt->sub, "folder");
199
0
        enum MailboxType mb_type = mx_path_probe(c_folder);
200
201
        /* if folder = {host} or imap[s]://host/: don't append slash */
202
0
        if ((mb_type == MUTT_IMAP) && ((c_folder[strlen(c_folder) - 1] == '}') ||
203
0
                                       (c_folder[strlen(c_folder) - 1] == '/')))
204
0
        {
205
0
          buf_strcpy(p, NONULL(c_folder));
206
0
        }
207
0
        else if (mb_type == MUTT_NOTMUCH)
208
0
        {
209
0
          buf_strcpy(p, NONULL(c_folder));
210
0
        }
211
0
        else if (c_folder && (c_folder[strlen(c_folder) - 1] == '/'))
212
0
        {
213
0
          buf_strcpy(p, NONULL(c_folder));
214
0
        }
215
0
        else
216
0
        {
217
0
          buf_printf(p, "%s/", NONULL(c_folder));
218
0
        }
219
220
0
        tail = s + 1;
221
0
        break;
222
0
      }
223
224
        /* elm compatibility, @ expands alias to user name */
225
226
0
      case '@':
227
0
      {
228
0
        struct AddressList *al = alias_lookup(s + 1);
229
0
        if (al && !TAILQ_EMPTY(al))
230
0
        {
231
0
          struct Email *e = email_new();
232
0
          e->env = mutt_env_new();
233
0
          mutt_addrlist_copy(&e->env->from, al, false);
234
0
          mutt_addrlist_copy(&e->env->to, al, false);
235
236
          /* TODO: fix mutt_default_save() to use Buffer */
237
0
          buf_alloc(p, PATH_MAX);
238
0
          mutt_default_save(p->data, p->dsize, e);
239
0
          buf_fix_dptr(p);
240
241
0
          email_free(&e);
242
          /* Avoid infinite recursion if the resulting folder starts with '@' */
243
0
          if (*p->data != '@')
244
0
            recurse = true;
245
246
0
          tail = "";
247
0
        }
248
0
        break;
249
0
      }
250
251
0
      case '>':
252
0
      {
253
0
        const char *const c_mbox = cs_subset_string(NeoMutt->sub, "mbox");
254
0
        buf_strcpy(p, c_mbox);
255
0
        tail = s + 1;
256
0
        break;
257
0
      }
258
259
0
      case '<':
260
0
      {
261
0
        const char *const c_record = cs_subset_string(NeoMutt->sub, "record");
262
0
        buf_strcpy(p, c_record);
263
0
        tail = s + 1;
264
0
        break;
265
0
      }
266
267
0
      case '!':
268
0
      {
269
0
        if (s[1] == '!')
270
0
        {
271
0
          buf_strcpy(p, LastFolder);
272
0
          tail = s + 2;
273
0
        }
274
0
        else
275
0
        {
276
0
          const char *const c_spool_file = cs_subset_string(NeoMutt->sub, "spool_file");
277
0
          buf_strcpy(p, c_spool_file);
278
0
          tail = s + 1;
279
0
        }
280
0
        break;
281
0
      }
282
283
0
      case '-':
284
0
      {
285
0
        buf_strcpy(p, LastFolder);
286
0
        tail = s + 1;
287
0
        break;
288
0
      }
289
290
0
      case '^':
291
0
      {
292
0
        buf_strcpy(p, CurrentFolder);
293
0
        tail = s + 1;
294
0
        break;
295
0
      }
296
297
0
      default:
298
0
      {
299
0
        buf_reset(p);
300
0
        tail = s;
301
0
      }
302
0
    }
303
304
0
    if (regex && *(buf_string(p)) && !recurse)
305
0
    {
306
0
      mutt_file_sanitize_regex(q, buf_string(p));
307
0
      buf_printf(tmp, "%s%s", buf_string(q), tail);
308
0
    }
309
0
    else
310
0
    {
311
0
      buf_printf(tmp, "%s%s", buf_string(p), tail);
312
0
    }
313
314
0
    buf_copy(buf, tmp);
315
0
  } while (recurse);
316
317
0
  buf_pool_release(&p);
318
0
  buf_pool_release(&q);
319
0
  buf_pool_release(&tmp);
320
321
0
#ifdef USE_IMAP
322
  /* Rewrite IMAP path in canonical form - aids in string comparisons of
323
   * folders. May possibly fail, in which case buf should be the same. */
324
0
  if (imap_path_probe(buf_string(buf), NULL) == MUTT_IMAP)
325
0
    imap_expand_path(buf);
326
0
#endif
327
0
}
328
329
/**
330
 * buf_expand_path - Create the canonical path
331
 * @param buf     Buffer with path
332
 *
333
 * @note The path is expanded in-place
334
 */
335
void buf_expand_path(struct Buffer *buf)
336
0
{
337
0
  buf_expand_path_regex(buf, false);
338
0
}
339
340
/**
341
 * mutt_expand_path_regex - Create the canonical path (with regex char escaping)
342
 * @param buf     Buffer with path
343
 * @param buflen  Length of buffer
344
 * @param regex If true, escape any regex characters
345
 * @retval ptr The expanded string
346
 *
347
 * @note The path is expanded in-place
348
 */
349
char *mutt_expand_path_regex(char *buf, size_t buflen, bool regex)
350
0
{
351
0
  struct Buffer *tmp = buf_pool_get();
352
353
0
  buf_addstr(tmp, NONULL(buf));
354
0
  buf_expand_path_regex(tmp, regex);
355
0
  mutt_str_copy(buf, buf_string(tmp), buflen);
356
357
0
  buf_pool_release(&tmp);
358
359
0
  return buf;
360
0
}
361
362
/**
363
 * mutt_gecos_name - Lookup a user's real name in /etc/passwd
364
 * @param dest    Buffer for the result
365
 * @param destlen Length of buffer
366
 * @param pw      Passwd entry
367
 * @retval ptr Result buffer on success
368
 *
369
 * Extract the real name from /etc/passwd's GECOS field.  When set, honor the
370
 * regular expression in `$gecos_mask`, otherwise assume that the GECOS field is a
371
 * comma-separated list.
372
 * Replace "&" by a capitalized version of the user's login name.
373
 */
374
char *mutt_gecos_name(char *dest, size_t destlen, struct passwd *pw)
375
0
{
376
0
  regmatch_t pat_match[1];
377
0
  size_t pwnl;
378
0
  char *p = NULL;
379
380
0
  if (!pw || !pw->pw_gecos)
381
0
    return NULL;
382
383
0
  memset(dest, 0, destlen);
384
385
0
  const struct Regex *c_gecos_mask = cs_subset_regex(NeoMutt->sub, "gecos_mask");
386
0
  if (mutt_regex_capture(c_gecos_mask, pw->pw_gecos, 1, pat_match))
387
0
  {
388
0
    mutt_str_copy(dest, pw->pw_gecos + pat_match[0].rm_so,
389
0
                  MIN(pat_match[0].rm_eo - pat_match[0].rm_so + 1, destlen));
390
0
  }
391
0
  else if ((p = strchr(pw->pw_gecos, ',')))
392
0
  {
393
0
    mutt_str_copy(dest, pw->pw_gecos, MIN(destlen, p - pw->pw_gecos + 1));
394
0
  }
395
0
  else
396
0
  {
397
0
    mutt_str_copy(dest, pw->pw_gecos, destlen);
398
0
  }
399
400
0
  pwnl = strlen(pw->pw_name);
401
402
0
  for (int idx = 0; dest[idx]; idx++)
403
0
  {
404
0
    if (dest[idx] == '&')
405
0
    {
406
0
      memmove(&dest[idx + pwnl], &dest[idx + 1],
407
0
              MAX((ssize_t) (destlen - idx - pwnl - 1), 0));
408
0
      memcpy(&dest[idx], pw->pw_name, MIN(destlen - idx - 1, pwnl));
409
0
      dest[idx] = toupper((unsigned char) dest[idx]);
410
0
    }
411
0
  }
412
413
0
  return dest;
414
0
}
415
416
/**
417
 * mutt_needs_mailcap - Does this type need a mailcap entry do display
418
 * @param m Attachment body to be displayed
419
 * @retval true  NeoMutt requires a mailcap entry to display
420
 * @retval false otherwise
421
 */
422
bool mutt_needs_mailcap(struct Body *m)
423
0
{
424
0
  switch (m->type)
425
0
  {
426
0
    case TYPE_TEXT:
427
0
      if (mutt_istr_equal("plain", m->subtype))
428
0
        return false;
429
0
      break;
430
0
    case TYPE_APPLICATION:
431
0
      if (((WithCrypto & APPLICATION_PGP) != 0) && mutt_is_application_pgp(m))
432
0
        return false;
433
0
      if (((WithCrypto & APPLICATION_SMIME) != 0) && mutt_is_application_smime(m))
434
0
        return false;
435
0
      break;
436
437
0
    case TYPE_MULTIPART:
438
0
    case TYPE_MESSAGE:
439
0
      return false;
440
0
  }
441
442
0
  return true;
443
0
}
444
445
/**
446
 * mutt_is_text_part - Is this part of an email in plain text?
447
 * @param b Part of an email
448
 * @retval true Part is in plain text
449
 */
450
bool mutt_is_text_part(struct Body *b)
451
0
{
452
0
  int t = b->type;
453
0
  char *s = b->subtype;
454
455
0
  if (((WithCrypto & APPLICATION_PGP) != 0) && mutt_is_application_pgp(b))
456
0
    return false;
457
458
0
  if (t == TYPE_TEXT)
459
0
    return true;
460
461
0
  if (t == TYPE_MESSAGE)
462
0
  {
463
0
    if (mutt_istr_equal("delivery-status", s))
464
0
      return true;
465
0
  }
466
467
0
  if (((WithCrypto & APPLICATION_PGP) != 0) && (t == TYPE_APPLICATION))
468
0
  {
469
0
    if (mutt_istr_equal("pgp-keys", s))
470
0
      return true;
471
0
  }
472
473
0
  return false;
474
0
}
475
476
/**
477
 * mutt_pretty_mailbox - Shorten a mailbox path using '~' or '='
478
 * @param buf    Buffer containing string to shorten
479
 * @param buflen Length of buffer
480
 *
481
 * Collapse the pathname using ~ or = when possible
482
 */
483
void mutt_pretty_mailbox(char *buf, size_t buflen)
484
0
{
485
0
  if (!buf)
486
0
    return;
487
488
0
  char *p = buf, *q = buf;
489
0
  size_t len;
490
0
  enum UrlScheme scheme;
491
0
  char tmp[PATH_MAX] = { 0 };
492
493
0
  scheme = url_check_scheme(buf);
494
495
0
  const char *const c_folder = cs_subset_string(NeoMutt->sub, "folder");
496
0
  if ((scheme == U_IMAP) || (scheme == U_IMAPS))
497
0
  {
498
0
    imap_pretty_mailbox(buf, buflen, c_folder);
499
0
    return;
500
0
  }
501
502
0
  if (scheme == U_NOTMUCH)
503
0
    return;
504
505
  /* if buf is an url, only collapse path component */
506
0
  if (scheme != U_UNKNOWN)
507
0
  {
508
0
    p = strchr(buf, ':') + 1;
509
0
    if (mutt_strn_equal(p, "//", 2))
510
0
      q = strchr(p + 2, '/');
511
0
    if (!q)
512
0
      q = strchr(p, '\0');
513
0
    p = q;
514
0
  }
515
516
  /* cleanup path */
517
0
  if (strstr(p, "//") || strstr(p, "/./"))
518
0
  {
519
    /* first attempt to collapse the pathname, this is more
520
     * lightweight than realpath() and doesn't resolve links */
521
0
    while (*p)
522
0
    {
523
0
      if ((p[0] == '/') && (p[1] == '/'))
524
0
      {
525
0
        *q++ = '/';
526
0
        p += 2;
527
0
      }
528
0
      else if ((p[0] == '/') && (p[1] == '.') && (p[2] == '/'))
529
0
      {
530
0
        *q++ = '/';
531
0
        p += 3;
532
0
      }
533
0
      else
534
0
      {
535
0
        *q++ = *p++;
536
0
      }
537
0
    }
538
0
    *q = '\0';
539
0
  }
540
0
  else if (strstr(p, "..") && ((scheme == U_UNKNOWN) || (scheme == U_FILE)) &&
541
0
           realpath(p, tmp))
542
0
  {
543
0
    mutt_str_copy(p, tmp, buflen - (p - buf));
544
0
  }
545
546
0
  if ((len = mutt_str_startswith(buf, c_folder)) && (buf[len] == '/'))
547
0
  {
548
0
    *buf++ = '=';
549
0
    memmove(buf, buf + len, mutt_str_len(buf + len) + 1);
550
0
  }
551
0
  else if ((len = mutt_str_startswith(buf, HomeDir)) && (buf[len] == '/'))
552
0
  {
553
0
    *buf++ = '~';
554
0
    memmove(buf, buf + len - 1, mutt_str_len(buf + len - 1) + 1);
555
0
  }
556
0
}
557
558
/**
559
 * buf_pretty_mailbox - Shorten a mailbox path using '~' or '='
560
 * @param buf Buffer containing Mailbox name
561
 */
562
void buf_pretty_mailbox(struct Buffer *buf)
563
0
{
564
0
  if (!buf || !buf->data)
565
0
    return;
566
  /* This reduces the size of the Buffer, so we can pass it through.
567
   * We adjust the size just to make sure buf->data is not NULL though */
568
0
  buf_alloc(buf, PATH_MAX);
569
0
  mutt_pretty_mailbox(buf->data, buf->dsize);
570
0
  buf_fix_dptr(buf);
571
0
}
572
573
/**
574
 * mutt_check_overwrite - Ask the user if overwriting is necessary
575
 * @param[in]  attname   Attachment name
576
 * @param[in]  path      Path to save the file
577
 * @param[out] fname     Buffer for filename
578
 * @param[out] opt       Save option, see #SaveAttach
579
 * @param[out] directory Directory to save under (OPTIONAL)
580
 * @retval  0 Success
581
 * @retval -1 Abort
582
 * @retval  1 Error
583
 */
584
int mutt_check_overwrite(const char *attname, const char *path, struct Buffer *fname,
585
                         enum SaveAttach *opt, char **directory)
586
0
{
587
0
  struct stat st = { 0 };
588
589
0
  buf_strcpy(fname, path);
590
0
  if (access(buf_string(fname), F_OK) != 0)
591
0
    return 0;
592
0
  if (stat(buf_string(fname), &st) != 0)
593
0
    return -1;
594
0
  if (S_ISDIR(st.st_mode))
595
0
  {
596
0
    enum QuadOption ans = MUTT_NO;
597
0
    if (directory)
598
0
    {
599
0
      switch (mw_multi_choice
600
              /* L10N: Means "The path you specified as the destination file is a directory."
601
                 See the msgid "Save to file: " (alias.c, recvattach.c)
602
                 These three letters correspond to the choices in the string.  */
603
0
              (_("File is a directory, save under it: (y)es, (n)o, (a)ll?"), _("yna")))
604
0
      {
605
0
        case 3: /* all */
606
0
          mutt_str_replace(directory, buf_string(fname));
607
0
          break;
608
0
        case 1: /* yes */
609
0
          FREE(directory);
610
0
          break;
611
0
        case -1: /* abort */
612
0
          FREE(directory);
613
0
          return -1;
614
0
        case 2: /* no */
615
0
          FREE(directory);
616
0
          return 1;
617
0
      }
618
0
    }
619
    /* L10N: Means "The path you specified as the destination file is a directory."
620
       See the msgid "Save to file: " (alias.c, recvattach.c) */
621
0
    else if ((ans = query_yesorno(_("File is a directory, save under it?"), MUTT_YES)) != MUTT_YES)
622
0
      return (ans == MUTT_NO) ? 1 : -1;
623
624
0
    struct Buffer *tmp = buf_pool_get();
625
0
    buf_strcpy(tmp, mutt_path_basename(NONULL(attname)));
626
0
    struct FileCompletionData cdata = { false, NULL, NULL, NULL };
627
0
    if ((mw_get_field(_("File under directory: "), tmp, MUTT_COMP_CLEAR,
628
0
                      HC_FILE, &CompleteMailboxOps, &cdata) != 0) ||
629
0
        buf_is_empty(tmp))
630
0
    {
631
0
      buf_pool_release(&tmp);
632
0
      return (-1);
633
0
    }
634
0
    buf_concat_path(fname, path, buf_string(tmp));
635
0
    buf_pool_release(&tmp);
636
0
  }
637
638
0
  if ((*opt == MUTT_SAVE_NO_FLAGS) && (access(buf_string(fname), F_OK) == 0))
639
0
  {
640
0
    char buf[4096] = { 0 };
641
0
    snprintf(buf, sizeof(buf), "%s - %s", buf_string(fname),
642
             // L10N: Options for: File %s exists, (o)verwrite, (a)ppend, or (c)ancel?
643
0
             _("File exists, (o)verwrite, (a)ppend, or (c)ancel?"));
644
0
    switch (mw_multi_choice(buf, _("oac")))
645
0
    {
646
0
      case -1: /* abort */
647
0
        return -1;
648
0
      case 3: /* cancel */
649
0
        return 1;
650
651
0
      case 2: /* append */
652
0
        *opt = MUTT_SAVE_APPEND;
653
0
        break;
654
0
      case 1: /* overwrite */
655
0
        *opt = MUTT_SAVE_OVERWRITE;
656
0
        break;
657
0
    }
658
0
  }
659
0
  return 0;
660
0
}
661
662
/**
663
 * mutt_save_path - Turn an email address into a filename (for saving)
664
 * @param buf    Buffer for the result
665
 * @param buflen Length of buffer
666
 * @param addr   Email address to use
667
 *
668
 * If the user hasn't set `$save_address` the name will be truncated to the '@'
669
 * character.
670
 */
671
void mutt_save_path(char *buf, size_t buflen, const struct Address *addr)
672
0
{
673
0
  if (addr && addr->mailbox)
674
0
  {
675
0
    mutt_str_copy(buf, buf_string(addr->mailbox), buflen);
676
0
    const bool c_save_address = cs_subset_bool(NeoMutt->sub, "save_address");
677
0
    if (!c_save_address)
678
0
    {
679
0
      char *p = strpbrk(buf, "%@");
680
0
      if (p)
681
0
        *p = '\0';
682
0
    }
683
0
    mutt_str_lower(buf);
684
0
  }
685
0
  else
686
0
  {
687
0
    *buf = '\0';
688
0
  }
689
0
}
690
691
/**
692
 * buf_save_path - Make a safe filename from an email address
693
 * @param dest Buffer for the result
694
 * @param a    Address to use
695
 */
696
void buf_save_path(struct Buffer *dest, const struct Address *a)
697
0
{
698
0
  if (a && a->mailbox)
699
0
  {
700
0
    buf_copy(dest, a->mailbox);
701
0
    const bool c_save_address = cs_subset_bool(NeoMutt->sub, "save_address");
702
0
    if (!c_save_address)
703
0
    {
704
0
      char *p = strpbrk(dest->data, "%@");
705
0
      if (p)
706
0
      {
707
0
        *p = '\0';
708
0
        buf_fix_dptr(dest);
709
0
      }
710
0
    }
711
0
    mutt_str_lower(dest->data);
712
0
  }
713
0
  else
714
0
  {
715
0
    buf_reset(dest);
716
0
  }
717
0
}
718
719
/**
720
 * mutt_safe_path - Make a safe filename from an email address
721
 * @param dest Buffer for the result
722
 * @param a    Address to use
723
 *
724
 * The filename will be stripped of '/', space, etc to make it safe.
725
 */
726
void mutt_safe_path(struct Buffer *dest, const struct Address *a)
727
0
{
728
0
  buf_save_path(dest, a);
729
0
  for (char *p = dest->data; *p; p++)
730
0
    if ((*p == '/') || isspace(*p) || !IsPrint((unsigned char) *p))
731
0
      *p = '_';
732
0
}
733
734
/**
735
 * mutt_expando_format - Expand expandos (%x) in a string - @ingroup expando_api
736
 * @param[out] buf      Buffer in which to save string
737
 * @param[in]  buflen   Buffer length
738
 * @param[in]  col      Starting column
739
 * @param[in]  cols     Number of screen columns
740
 * @param[in]  src      Printf-like format string
741
 * @param[in]  callback Callback - Implements ::format_t
742
 * @param[in]  data     Callback data
743
 * @param[in]  flags    Callback flags
744
 */
745
void mutt_expando_format(char *buf, size_t buflen, size_t col, int cols, const char *src,
746
                         format_t callback, intptr_t data, MuttFormatFlags flags)
747
0
{
748
0
  char prefix[128], tmp[1024];
749
0
  char *cp = NULL, *wptr = buf;
750
0
  char ch;
751
0
  char if_str[128], else_str[128];
752
0
  size_t wlen, count, len, wid;
753
0
  FILE *fp_filter = NULL;
754
0
  char *recycler = NULL;
755
756
0
  char src2[1024];
757
0
  mutt_str_copy(src2, src, mutt_str_len(src) + 1);
758
0
  src = src2;
759
760
0
  const bool c_arrow_cursor = cs_subset_bool(NeoMutt->sub, "arrow_cursor");
761
0
  const char *const c_arrow_string = cs_subset_string(NeoMutt->sub, "arrow_string");
762
0
  const int arrow_width = mutt_strwidth(c_arrow_string);
763
764
0
  prefix[0] = '\0';
765
0
  buflen--; /* save room for the terminal \0 */
766
0
  wlen = ((flags & MUTT_FORMAT_ARROWCURSOR) && c_arrow_cursor) ? arrow_width + 1 : 0;
767
0
  col += wlen;
768
769
0
  if ((flags & MUTT_FORMAT_NOFILTER) == 0)
770
0
  {
771
0
    int off = -1;
772
773
    /* Do not consider filters if no pipe at end */
774
0
    int n = mutt_str_len(src);
775
0
    if ((n > 1) && (src[n - 1] == '|'))
776
0
    {
777
      /* Scan backwards for backslashes */
778
0
      off = n;
779
0
      while ((off > 0) && (src[off - 2] == '\\'))
780
0
        off--;
781
0
    }
782
783
    /* If number of backslashes is even, the pipe is real. */
784
    /* n-off is the number of backslashes. */
785
0
    if ((off > 0) && (((n - off) % 2) == 0))
786
0
    {
787
0
      char srccopy[1024] = { 0 };
788
0
      int i = 0;
789
790
0
      mutt_debug(LL_DEBUG3, "fmtpipe = %s\n", src);
791
792
0
      strncpy(srccopy, src, n);
793
0
      srccopy[n - 1] = '\0';
794
795
      /* prepare Buffers */
796
0
      struct Buffer srcbuf = buf_make(0);
797
0
      buf_addstr(&srcbuf, srccopy);
798
      /* note: we are resetting dptr and *reading* from the buffer, so we don't
799
       * want to use buf_reset(). */
800
0
      buf_seek(&srcbuf, 0);
801
0
      struct Buffer word = buf_make(0);
802
0
      struct Buffer cmd = buf_make(0);
803
804
      /* Iterate expansions across successive arguments */
805
0
      do
806
0
      {
807
        /* Extract the command name and copy to command line */
808
0
        mutt_debug(LL_DEBUG3, "fmtpipe +++: %s\n", srcbuf.dptr);
809
0
        if (word.data)
810
0
          *word.data = '\0';
811
0
        parse_extract_token(&word, &srcbuf, TOKEN_NO_FLAGS);
812
0
        mutt_debug(LL_DEBUG3, "fmtpipe %2d: %s\n", i++, word.data);
813
0
        buf_addch(&cmd, '\'');
814
0
        mutt_expando_format(tmp, sizeof(tmp), 0, cols, word.data, callback,
815
0
                            data, flags | MUTT_FORMAT_NOFILTER);
816
0
        for (char *p = tmp; p && (*p != '\0'); p++)
817
0
        {
818
0
          if (*p == '\'')
819
0
          {
820
            /* shell quoting doesn't permit escaping a single quote within
821
             * single-quoted material.  double-quoting instead will lead
822
             * shell variable expansions, so break out of the single-quoted
823
             * span, insert a double-quoted single quote, and resume. */
824
0
            buf_addstr(&cmd, "'\"'\"'");
825
0
          }
826
0
          else
827
0
          {
828
0
            buf_addch(&cmd, *p);
829
0
          }
830
0
        }
831
0
        buf_addch(&cmd, '\'');
832
0
        buf_addch(&cmd, ' ');
833
0
      } while (MoreArgs(&srcbuf));
834
835
0
      mutt_debug(LL_DEBUG3, "fmtpipe > %s\n", cmd.data);
836
837
0
      col -= wlen; /* reset to passed in value */
838
0
      wptr = buf;  /* reset write ptr */
839
0
      pid_t pid = filter_create(cmd.data, NULL, &fp_filter, NULL, EnvList);
840
0
      if (pid != -1)
841
0
      {
842
0
        int rc;
843
844
0
        n = fread(buf, 1, buflen /* already decremented */, fp_filter);
845
0
        mutt_file_fclose(&fp_filter);
846
0
        rc = filter_wait(pid);
847
0
        if (rc != 0)
848
0
          mutt_debug(LL_DEBUG1, "format pipe cmd exited code %d\n", rc);
849
0
        if (n > 0)
850
0
        {
851
0
          buf[n] = '\0';
852
0
          while ((n > 0) && ((buf[n - 1] == '\n') || (buf[n - 1] == '\r')))
853
0
            buf[--n] = '\0';
854
0
          mutt_debug(LL_DEBUG5, "fmtpipe < %s\n", buf);
855
856
          /* If the result ends with '%', this indicates that the filter
857
           * generated %-tokens that neomutt can expand.  Eliminate the '%'
858
           * marker and recycle the string through mutt_expando_format().
859
           * To literally end with "%", use "%%". */
860
0
          if ((n > 0) && (buf[n - 1] == '%'))
861
0
          {
862
0
            n--;
863
0
            buf[n] = '\0'; /* remove '%' */
864
0
            if ((n > 0) && (buf[n - 1] != '%'))
865
0
            {
866
0
              recycler = mutt_str_dup(buf);
867
0
              if (recycler)
868
0
              {
869
                /* buflen is decremented at the start of this function
870
                 * to save space for the terminal nul char.  We can add
871
                 * it back for the recursive call since the expansion of
872
                 * format pipes does not try to append a nul itself.  */
873
0
                mutt_expando_format(buf, buflen + 1, col, cols, recycler,
874
0
                                    callback, data, flags);
875
0
                FREE(&recycler);
876
0
              }
877
0
            }
878
0
          }
879
0
        }
880
0
        else
881
0
        {
882
          /* read error */
883
0
          mutt_debug(LL_DEBUG1, "error reading from fmtpipe: %s (errno=%d)\n",
884
0
                     strerror(errno), errno);
885
0
          *wptr = '\0';
886
0
        }
887
0
      }
888
0
      else
889
0
      {
890
        /* Filter failed; erase write buffer */
891
0
        *wptr = '\0';
892
0
      }
893
894
0
      buf_dealloc(&cmd);
895
0
      buf_dealloc(&srcbuf);
896
0
      buf_dealloc(&word);
897
0
      return;
898
0
    }
899
0
  }
900
901
0
  while (*src && (wlen < buflen))
902
0
  {
903
0
    if (*src == '%')
904
0
    {
905
0
      if (*++src == '%')
906
0
      {
907
0
        *wptr++ = '%';
908
0
        wlen++;
909
0
        col++;
910
0
        src++;
911
0
        continue;
912
0
      }
913
914
0
      if (*src == '?')
915
0
      {
916
        /* change original %? to new %< notation */
917
        /* %?x?y&z? to %<x?y&z> where y and z are nestable */
918
0
        char *p = (char *) src;
919
0
        *p = '<';
920
        /* skip over "x" */
921
0
        for (; *p && (*p != '?'); p++)
922
0
          ; // do nothing
923
924
        /* nothing */
925
0
        if (*p == '?')
926
0
          p++;
927
        /* fix up the "y&z" section */
928
0
        for (; *p && (*p != '?'); p++)
929
0
        {
930
          /* escape '<' and '>' to work inside nested-if */
931
0
          if ((*p == '<') || (*p == '>'))
932
0
          {
933
0
            memmove(p + 2, p, mutt_str_len(p) + 1);
934
0
            *p++ = '\\';
935
0
            *p++ = '\\';
936
0
          }
937
0
        }
938
0
        if (*p == '?')
939
0
          *p = '>';
940
0
      }
941
942
0
      if (*src == '<')
943
0
      {
944
0
        flags |= MUTT_FORMAT_OPTIONAL;
945
0
        ch = *(++src); /* save the character to switch on */
946
0
        src++;
947
0
        cp = prefix;
948
0
        count = 0;
949
0
        while ((count < (sizeof(prefix) - 1)) && (*src != '\0') && (*src != '?'))
950
0
        {
951
0
          *cp++ = *src++;
952
0
          count++;
953
0
        }
954
0
        *cp = '\0';
955
0
      }
956
0
      else
957
0
      {
958
0
        flags &= ~MUTT_FORMAT_OPTIONAL;
959
960
        /* eat the format string */
961
0
        cp = prefix;
962
0
        count = 0;
963
0
        while ((count < (sizeof(prefix) - 1)) && strchr("0123456789.-=", *src))
964
0
        {
965
0
          *cp++ = *src++;
966
0
          count++;
967
0
        }
968
0
        *cp = '\0';
969
970
0
        if (*src == '\0')
971
0
          break; /* bad format */
972
973
0
        ch = *src++; /* save the character to switch on */
974
0
      }
975
976
0
      if (flags & MUTT_FORMAT_OPTIONAL)
977
0
      {
978
0
        int lrbalance;
979
980
0
        if (*src != '?')
981
0
          break; /* bad format */
982
0
        src++;
983
984
        /* eat the 'if' part of the string */
985
0
        cp = if_str;
986
0
        count = 0;
987
0
        lrbalance = 1;
988
0
        while ((lrbalance > 0) && (count < sizeof(if_str)) && *src)
989
0
        {
990
0
          if ((src[0] == '%') && (src[1] == '>'))
991
0
          {
992
            /* This is a padding expando; copy two chars and carry on */
993
0
            *cp++ = *src++;
994
0
            *cp++ = *src++;
995
0
            count += 2;
996
0
            continue;
997
0
          }
998
999
0
          if (*src == '\\')
1000
0
          {
1001
0
            src++;
1002
0
            *cp++ = *src++;
1003
0
          }
1004
0
          else if ((src[0] == '%') && (src[1] == '<'))
1005
0
          {
1006
0
            lrbalance++;
1007
0
          }
1008
0
          else if (src[0] == '>')
1009
0
          {
1010
0
            lrbalance--;
1011
0
          }
1012
0
          if (lrbalance == 0)
1013
0
            break;
1014
0
          if ((lrbalance == 1) && (src[0] == '&'))
1015
0
            break;
1016
0
          *cp++ = *src++;
1017
0
          count++;
1018
0
        }
1019
0
        *cp = '\0';
1020
1021
        /* eat the 'else' part of the string (optional) */
1022
0
        if (*src == '&')
1023
0
          src++; /* skip the & */
1024
0
        cp = else_str;
1025
0
        count = 0;
1026
0
        while ((lrbalance > 0) && (count < sizeof(else_str)) && (*src != '\0'))
1027
0
        {
1028
0
          if ((src[0] == '%') && (src[1] == '>'))
1029
0
          {
1030
            /* This is a padding expando; copy two chars and carry on */
1031
0
            *cp++ = *src++;
1032
0
            *cp++ = *src++;
1033
0
            count += 2;
1034
0
            continue;
1035
0
          }
1036
1037
0
          if (*src == '\\')
1038
0
          {
1039
0
            src++;
1040
0
            *cp++ = *src++;
1041
0
          }
1042
0
          else if ((src[0] == '%') && (src[1] == '<'))
1043
0
          {
1044
0
            lrbalance++;
1045
0
          }
1046
0
          else if (src[0] == '>')
1047
0
          {
1048
0
            lrbalance--;
1049
0
          }
1050
0
          if (lrbalance == 0)
1051
0
            break;
1052
0
          if ((lrbalance == 1) && (src[0] == '&'))
1053
0
            break;
1054
0
          *cp++ = *src++;
1055
0
          count++;
1056
0
        }
1057
0
        *cp = '\0';
1058
1059
0
        if ((*src == '\0'))
1060
0
          break; /* bad format */
1061
1062
0
        src++; /* move past the trailing '>' (formerly '?') */
1063
0
      }
1064
1065
      /* handle generic cases first */
1066
0
      if ((ch == '>') || (ch == '*'))
1067
0
      {
1068
        /* %>X: right justify to EOL, left takes precedence
1069
         * %*X: right justify to EOL, right takes precedence */
1070
0
        int soft = ch == '*';
1071
0
        int pl, pw;
1072
0
        pl = mutt_mb_charlen(src, &pw);
1073
0
        if (pl <= 0)
1074
0
        {
1075
0
          pl = 1;
1076
0
          pw = 1;
1077
0
        }
1078
1079
        /* see if there's room to add content, else ignore */
1080
0
        if (((col < cols) && (wlen < buflen)) || soft)
1081
0
        {
1082
0
          int pad;
1083
1084
          /* get contents after padding */
1085
0
          mutt_expando_format(tmp, sizeof(tmp), 0, cols, src + pl, callback, data, flags);
1086
0
          len = mutt_str_len(tmp);
1087
0
          wid = mutt_strwidth(tmp);
1088
1089
0
          pad = (cols - col - wid) / pw;
1090
0
          if (pad >= 0)
1091
0
          {
1092
            /* try to consume as many columns as we can, if we don't have
1093
             * memory for that, use as much memory as possible */
1094
0
            if (wlen + (pad * pl) + len > buflen)
1095
0
            {
1096
0
              pad = (buflen > (wlen + len)) ? ((buflen - wlen - len) / pl) : 0;
1097
0
            }
1098
0
            else
1099
0
            {
1100
              /* Add pre-spacing to make multi-column pad characters and
1101
               * the contents after padding line up */
1102
0
              while (((col + (pad * pw) + wid) < cols) && ((wlen + (pad * pl) + len) < buflen))
1103
0
              {
1104
0
                *wptr++ = ' ';
1105
0
                wlen++;
1106
0
                col++;
1107
0
              }
1108
0
            }
1109
0
            while (pad-- > 0)
1110
0
            {
1111
0
              memcpy(wptr, src, pl);
1112
0
              wptr += pl;
1113
0
              wlen += pl;
1114
0
              col += pw;
1115
0
            }
1116
0
          }
1117
0
          else if (soft)
1118
0
          {
1119
0
            int offset = ((flags & MUTT_FORMAT_ARROWCURSOR) && c_arrow_cursor) ?
1120
0
                             arrow_width + 1 :
1121
0
                             0;
1122
0
            int avail_cols = (cols > offset) ? (cols - offset) : 0;
1123
            /* \0-terminate buf for length computation in mutt_wstr_trunc() */
1124
0
            *wptr = '\0';
1125
            /* make sure right part is at most as wide as display */
1126
0
            len = mutt_wstr_trunc(tmp, buflen, avail_cols, &wid);
1127
            /* truncate left so that right part fits completely in */
1128
0
            wlen = mutt_wstr_trunc(buf, buflen - len, avail_cols - wid, &col);
1129
0
            wptr = buf + wlen;
1130
            /* Multi-column characters may be truncated in the middle.
1131
             * Add spacing so the right hand side lines up. */
1132
0
            while (((col + wid) < avail_cols) && ((wlen + len) < buflen))
1133
0
            {
1134
0
              *wptr++ = ' ';
1135
0
              wlen++;
1136
0
              col++;
1137
0
            }
1138
0
          }
1139
0
          if ((len + wlen) > buflen)
1140
0
            len = mutt_wstr_trunc(tmp, buflen - wlen, cols - col, NULL);
1141
0
          memcpy(wptr, tmp, len);
1142
0
          wptr += len;
1143
0
        }
1144
0
        break; /* skip rest of input */
1145
0
      }
1146
0
      else if (ch == '|')
1147
0
      {
1148
        /* pad to EOL */
1149
0
        int pl, pw;
1150
0
        pl = mutt_mb_charlen(src, &pw);
1151
0
        if (pl <= 0)
1152
0
        {
1153
0
          pl = 1;
1154
0
          pw = 1;
1155
0
        }
1156
1157
        /* see if there's room to add content, else ignore */
1158
0
        if ((col < cols) && (wlen < buflen))
1159
0
        {
1160
0
          int c = (cols - col) / pw;
1161
0
          if ((c > 0) && ((wlen + (c * pl)) > buflen))
1162
0
            c = ((signed) (buflen - wlen)) / pl;
1163
0
          while (c > 0)
1164
0
          {
1165
0
            memcpy(wptr, src, pl);
1166
0
            wptr += pl;
1167
0
            wlen += pl;
1168
0
            col += pw;
1169
0
            c--;
1170
0
          }
1171
0
        }
1172
0
        break; /* skip rest of input */
1173
0
      }
1174
0
      else
1175
0
      {
1176
0
        bool to_lower = false;
1177
0
        bool no_dots = false;
1178
1179
0
        while ((ch == '_') || (ch == ':'))
1180
0
        {
1181
0
          if (ch == '_')
1182
0
            to_lower = true;
1183
0
          else if (ch == ':')
1184
0
            no_dots = true;
1185
1186
0
          ch = *src++;
1187
0
        }
1188
1189
        /* use callback function to handle this case */
1190
0
        *tmp = '\0';
1191
0
        src = callback(tmp, sizeof(tmp), col, cols, ch, src, prefix, if_str,
1192
0
                       else_str, data, flags);
1193
1194
0
        if (to_lower)
1195
0
          mutt_str_lower(tmp);
1196
0
        if (no_dots)
1197
0
        {
1198
0
          char *p = tmp;
1199
0
          for (; *p; p++)
1200
0
            if (*p == '.')
1201
0
              *p = '_';
1202
0
        }
1203
1204
0
        len = mutt_str_len(tmp);
1205
0
        if ((len + wlen) > buflen)
1206
0
          len = mutt_wstr_trunc(tmp, buflen - wlen, cols - col, NULL);
1207
1208
0
        memcpy(wptr, tmp, len);
1209
0
        wptr += len;
1210
0
        wlen += len;
1211
0
        col += mutt_strwidth(tmp);
1212
0
      }
1213
0
    }
1214
0
    else if (*src == '\\')
1215
0
    {
1216
0
      if (!*++src)
1217
0
        break;
1218
0
      switch (*src)
1219
0
      {
1220
0
        case 'f':
1221
0
          *wptr = '\f';
1222
0
          break;
1223
0
        case 'n':
1224
0
          *wptr = '\n';
1225
0
          break;
1226
0
        case 'r':
1227
0
          *wptr = '\r';
1228
0
          break;
1229
0
        case 't':
1230
0
          *wptr = '\t';
1231
0
          break;
1232
0
        case 'v':
1233
0
          *wptr = '\v';
1234
0
          break;
1235
0
        default:
1236
0
          *wptr = *src;
1237
0
          break;
1238
0
      }
1239
0
      src++;
1240
0
      wptr++;
1241
0
      wlen++;
1242
0
      col++;
1243
0
    }
1244
0
    else
1245
0
    {
1246
0
      int bytes, width;
1247
      /* in case of error, simply copy byte */
1248
0
      bytes = mutt_mb_charlen(src, &width);
1249
0
      if (bytes < 0)
1250
0
      {
1251
0
        bytes = 1;
1252
0
        width = 1;
1253
0
      }
1254
0
      if ((bytes > 0) && ((wlen + bytes) < buflen))
1255
0
      {
1256
0
        memcpy(wptr, src, bytes);
1257
0
        wptr += bytes;
1258
0
        src += bytes;
1259
0
        wlen += bytes;
1260
0
        col += width;
1261
0
      }
1262
0
      else
1263
0
      {
1264
0
        src += buflen - wlen;
1265
0
        wlen = buflen;
1266
0
      }
1267
0
    }
1268
0
  }
1269
0
  *wptr = '\0';
1270
0
}
1271
1272
/**
1273
 * mutt_open_read - Run a command to read from
1274
 * @param[in]  path   Path to command
1275
 * @param[out] thepid PID of the command
1276
 * @retval ptr File containing output of command
1277
 *
1278
 * This function allows the user to specify a command to read stdout from in
1279
 * place of a normal file.  If the last character in the string is a pipe (|),
1280
 * then we assume it is a command to run instead of a normal file.
1281
 */
1282
FILE *mutt_open_read(const char *path, pid_t *thepid)
1283
0
{
1284
0
  FILE *fp = NULL;
1285
0
  struct stat st = { 0 };
1286
1287
0
  size_t len = mutt_str_len(path);
1288
0
  if (len == 0)
1289
0
  {
1290
0
    return NULL;
1291
0
  }
1292
1293
0
  if (path[len - 1] == '|')
1294
0
  {
1295
    /* read from a pipe */
1296
1297
0
    char *p = mutt_str_dup(path);
1298
1299
0
    p[len - 1] = 0;
1300
0
    mutt_endwin();
1301
0
    *thepid = filter_create(p, NULL, &fp, NULL, EnvList);
1302
0
    FREE(&p);
1303
0
  }
1304
0
  else
1305
0
  {
1306
0
    if (stat(path, &st) < 0)
1307
0
      return NULL;
1308
0
    if (S_ISDIR(st.st_mode))
1309
0
    {
1310
0
      errno = EINVAL;
1311
0
      return NULL;
1312
0
    }
1313
0
    fp = fopen(path, "r");
1314
0
    *thepid = -1;
1315
0
  }
1316
0
  return fp;
1317
0
}
1318
1319
/**
1320
 * mutt_save_confirm - Ask the user to save
1321
 * @param s  Save location
1322
 * @param st Timestamp
1323
 * @retval  0 OK to proceed
1324
 * @retval -1 to abort
1325
 * @retval  1 to retry
1326
 */
1327
int mutt_save_confirm(const char *s, struct stat *st)
1328
0
{
1329
0
  int rc = 0;
1330
1331
0
  enum MailboxType type = mx_path_probe(s);
1332
1333
0
#ifdef USE_POP
1334
0
  if (type == MUTT_POP)
1335
0
  {
1336
0
    mutt_error(_("Can't save message to POP mailbox"));
1337
0
    return 1;
1338
0
  }
1339
0
#endif
1340
1341
0
  if ((type != MUTT_MAILBOX_ERROR) && (type != MUTT_UNKNOWN) && (mx_access(s, W_OK) == 0))
1342
0
  {
1343
0
    const bool c_confirm_append = cs_subset_bool(NeoMutt->sub, "confirm_append");
1344
0
    if (c_confirm_append)
1345
0
    {
1346
0
      struct Buffer *tmp = buf_pool_get();
1347
0
      buf_printf(tmp, _("Append messages to %s?"), s);
1348
0
      enum QuadOption ans = query_yesorno_help(buf_string(tmp), MUTT_YES,
1349
0
                                               NeoMutt->sub, "confirm_append");
1350
0
      if (ans == MUTT_NO)
1351
0
        rc = 1;
1352
0
      else if (ans == MUTT_ABORT)
1353
0
        rc = -1;
1354
0
      buf_pool_release(&tmp);
1355
0
    }
1356
0
  }
1357
1358
0
#ifdef USE_NNTP
1359
0
  if (type == MUTT_NNTP)
1360
0
  {
1361
0
    mutt_error(_("Can't save message to news server"));
1362
0
    return 0;
1363
0
  }
1364
0
#endif
1365
1366
0
  if (stat(s, st) != -1)
1367
0
  {
1368
0
    if (type == MUTT_MAILBOX_ERROR)
1369
0
    {
1370
0
      mutt_error(_("%s is not a mailbox"), s);
1371
0
      return 1;
1372
0
    }
1373
0
  }
1374
0
  else if (type != MUTT_IMAP)
1375
0
  {
1376
0
    st->st_mtime = 0;
1377
0
    st->st_atime = 0;
1378
1379
    /* pathname does not exist */
1380
0
    if (errno == ENOENT)
1381
0
    {
1382
0
      const bool c_confirm_create = cs_subset_bool(NeoMutt->sub, "confirm_create");
1383
0
      if (c_confirm_create)
1384
0
      {
1385
0
        struct Buffer *tmp = buf_pool_get();
1386
0
        buf_printf(tmp, _("Create %s?"), s);
1387
0
        enum QuadOption ans = query_yesorno_help(buf_string(tmp), MUTT_YES,
1388
0
                                                 NeoMutt->sub, "confirm_create");
1389
0
        if (ans == MUTT_NO)
1390
0
          rc = 1;
1391
0
        else if (ans == MUTT_ABORT)
1392
0
          rc = -1;
1393
0
        buf_pool_release(&tmp);
1394
0
      }
1395
1396
      /* user confirmed with MUTT_YES or set `$confirm_create` */
1397
0
      if (rc == 0)
1398
0
      {
1399
        /* create dir recursively */
1400
0
        char *tmp_path = mutt_path_dirname(s);
1401
0
        if (mutt_file_mkdir(tmp_path, S_IRWXU) == -1)
1402
0
        {
1403
          /* report failure & abort */
1404
0
          mutt_perror("%s", s);
1405
0
          FREE(&tmp_path);
1406
0
          return 1;
1407
0
        }
1408
0
        FREE(&tmp_path);
1409
0
      }
1410
0
    }
1411
0
    else
1412
0
    {
1413
0
      mutt_perror("%s", s);
1414
0
      return 1;
1415
0
    }
1416
0
  }
1417
1418
0
  msgwin_clear_text(NULL);
1419
0
  return rc;
1420
0
}
1421
1422
/**
1423
 * mutt_sleep - Sleep for a while
1424
 * @param s Number of seconds to sleep
1425
 *
1426
 * If the user config '$sleep_time' is larger, sleep that long instead.
1427
 */
1428
void mutt_sleep(short s)
1429
0
{
1430
0
  const short c_sleep_time = cs_subset_number(NeoMutt->sub, "sleep_time");
1431
0
  if (c_sleep_time > s)
1432
0
    sleep(c_sleep_time);
1433
0
  else if (s)
1434
0
    sleep(s);
1435
0
}
1436
1437
/**
1438
 * mutt_make_version - Generate the NeoMutt version string
1439
 * @retval ptr Version string
1440
 *
1441
 * @note This returns a pointer to a static buffer
1442
 */
1443
const char *mutt_make_version(void)
1444
0
{
1445
0
  static char vstring[256];
1446
0
  snprintf(vstring, sizeof(vstring), "NeoMutt %s%s", PACKAGE_VERSION, GitVer);
1447
0
  return vstring;
1448
0
}
1449
1450
/**
1451
 * mutt_encode_path - Convert a path to 'us-ascii'
1452
 * @param buf Buffer for the result
1453
 * @param src Path to convert (OPTIONAL)
1454
 *
1455
 * If `src` is NULL, the path in `buf` will be converted in-place.
1456
 */
1457
void mutt_encode_path(struct Buffer *buf, const char *src)
1458
0
{
1459
0
  char *p = mutt_str_dup(src);
1460
0
  int rc = mutt_ch_convert_string(&p, cc_charset(), "us-ascii", MUTT_ICONV_NO_FLAGS);
1461
0
  size_t len = buf_strcpy(buf, (rc == 0) ? NONULL(p) : NONULL(src));
1462
1463
  /* convert the path to POSIX "Portable Filename Character Set" */
1464
0
  for (size_t i = 0; i < len; i++)
1465
0
  {
1466
0
    if (!isalnum(buf->data[i]) && !strchr("/.-_", buf->data[i]))
1467
0
    {
1468
0
      buf->data[i] = '_';
1469
0
    }
1470
0
  }
1471
0
  FREE(&p);
1472
0
}
1473
1474
/**
1475
 * mutt_set_xdg_path - Find an XDG path or its fallback
1476
 * @param type    Type of XDG variable, e.g. #XDG_CONFIG_HOME
1477
 * @param buf     Buffer to save path
1478
 * @retval 1 An entry was found that actually exists on disk and 0 otherwise
1479
 *
1480
 * Process an XDG environment variable or its fallback.
1481
 */
1482
int mutt_set_xdg_path(enum XdgType type, struct Buffer *buf)
1483
0
{
1484
0
  const char *xdg_env = mutt_str_getenv(XdgEnvVars[type]);
1485
0
  char *xdg = xdg_env ? mutt_str_dup(xdg_env) : mutt_str_dup(XdgDefaults[type]);
1486
0
  char *x = xdg; /* mutt_str_sep() changes xdg, so free x instead later */
1487
0
  char *token = NULL;
1488
0
  int rc = 0;
1489
1490
0
  while ((token = mutt_str_sep(&xdg, ":")))
1491
0
  {
1492
0
    if (buf_printf(buf, "%s/%s/neomuttrc", token, PACKAGE) < 0)
1493
0
      continue;
1494
0
    buf_expand_path(buf);
1495
0
    if (access(buf_string(buf), F_OK) == 0)
1496
0
    {
1497
0
      rc = 1;
1498
0
      break;
1499
0
    }
1500
1501
0
    if (buf_printf(buf, "%s/%s/Muttrc", token, PACKAGE) < 0)
1502
0
      continue;
1503
0
    buf_expand_path(buf);
1504
0
    if (access(buf_string(buf), F_OK) == 0)
1505
0
    {
1506
0
      rc = 1;
1507
0
      break;
1508
0
    }
1509
0
  }
1510
1511
0
  FREE(&x);
1512
0
  return rc;
1513
0
}
1514
1515
/**
1516
 * mutt_get_parent_path - Find the parent of a path (or mailbox)
1517
 * @param path   Path to use
1518
 * @param buf    Buffer for the result
1519
 * @param buflen Length of buffer
1520
 */
1521
void mutt_get_parent_path(const char *path, char *buf, size_t buflen)
1522
0
{
1523
0
  enum MailboxType mb_type = mx_path_probe(path);
1524
1525
0
  const char *const c_folder = cs_subset_string(NeoMutt->sub, "folder");
1526
0
  if (mb_type == MUTT_IMAP)
1527
0
  {
1528
0
    imap_get_parent_path(path, buf, buflen);
1529
0
  }
1530
0
  else if (mb_type == MUTT_NOTMUCH)
1531
0
  {
1532
0
    mutt_str_copy(buf, c_folder, buflen);
1533
0
  }
1534
0
  else
1535
0
  {
1536
0
    mutt_str_copy(buf, path, buflen);
1537
0
    int n = mutt_str_len(buf);
1538
0
    if (n == 0)
1539
0
      return;
1540
1541
    /* remove any final trailing '/' */
1542
0
    if (buf[n - 1] == '/')
1543
0
      buf[n - 1] = '\0';
1544
1545
    /* Remove everything until the next slash */
1546
0
    for (n--; ((n >= 0) && (buf[n] != '/')); n--)
1547
0
      ; // do nothing
1548
1549
0
    if (n > 0)
1550
0
    {
1551
0
      buf[n] = '\0';
1552
0
    }
1553
0
    else
1554
0
    {
1555
0
      buf[0] = '/';
1556
0
      buf[1] = '\0';
1557
0
    }
1558
0
  }
1559
0
}
1560
1561
/**
1562
 * mutt_inbox_cmp - Do two folders share the same path and one is an inbox - @ingroup sort_api
1563
 * @param a First path
1564
 * @param b Second path
1565
 * @retval -1 a is INBOX of b
1566
 * @retval  0 None is INBOX
1567
 * @retval  1 b is INBOX for a
1568
 *
1569
 * This function compares two folder paths. It first looks for the position of
1570
 * the last common '/' character. If a valid position is found and it's not the
1571
 * last character in any of the two paths, the remaining parts of the paths are
1572
 * compared (case insensitively) with the string "INBOX". If one of the two
1573
 * paths matches, it's reported as being less than the other and the function
1574
 * returns -1 (a < b) or 1 (a > b). If no paths match the requirements, the two
1575
 * paths are considered equivalent and this function returns 0.
1576
 *
1577
 * Examples:
1578
 * * mutt_inbox_cmp("/foo/bar",      "/foo/baz") --> 0
1579
 * * mutt_inbox_cmp("/foo/bar/",     "/foo/bar/inbox") --> 0
1580
 * * mutt_inbox_cmp("/foo/bar/sent", "/foo/bar/inbox") --> 1
1581
 * * mutt_inbox_cmp("=INBOX",        "=Drafts") --> -1
1582
 */
1583
int mutt_inbox_cmp(const char *a, const char *b)
1584
0
{
1585
  /* fast-track in case the paths have been mutt_pretty_mailbox'ified */
1586
0
  if ((a[0] == '+') && (b[0] == '+'))
1587
0
  {
1588
0
    return mutt_istr_equal(a + 1, "inbox") ? -1 :
1589
0
           mutt_istr_equal(b + 1, "inbox") ? 1 :
1590
0
                                             0;
1591
0
  }
1592
1593
0
  const char *a_end = strrchr(a, '/');
1594
0
  const char *b_end = strrchr(b, '/');
1595
1596
  /* If one path contains a '/', but not the other */
1597
0
  if ((!a_end) ^ (!b_end))
1598
0
    return 0;
1599
1600
  /* If neither path contains a '/' */
1601
0
  if (!a_end)
1602
0
    return 0;
1603
1604
  /* Compare the subpaths */
1605
0
  size_t a_len = a_end - a;
1606
0
  size_t b_len = b_end - b;
1607
0
  size_t min = MIN(a_len, b_len);
1608
0
  int same = (a[min] == '/') && (b[min] == '/') && (a[min + 1] != '\0') &&
1609
0
             (b[min + 1] != '\0') && mutt_istrn_equal(a, b, min);
1610
1611
0
  if (!same)
1612
0
    return 0;
1613
1614
0
  if (mutt_istr_equal(&a[min + 1], "inbox"))
1615
0
    return -1;
1616
1617
0
  if (mutt_istr_equal(&b[min + 1], "inbox"))
1618
0
    return 1;
1619
1620
0
  return 0;
1621
0
}
1622
1623
/**
1624
 * buf_sanitize_filename - Replace unsafe characters in a filename
1625
 * @param buf   Buffer for the result
1626
 * @param path  Filename to make safe
1627
 * @param slash Replace '/' characters too
1628
 */
1629
void buf_sanitize_filename(struct Buffer *buf, const char *path, short slash)
1630
0
{
1631
0
  if (!buf || !path)
1632
0
    return;
1633
1634
0
  buf_reset(buf);
1635
1636
0
  for (; *path; path++)
1637
0
  {
1638
0
    if ((slash && (*path == '/')) || !strchr(FilenameSafeChars, *path))
1639
0
      buf_addch(buf, '_');
1640
0
    else
1641
0
      buf_addch(buf, *path);
1642
0
  }
1643
0
}
1644
1645
/**
1646
 * mutt_str_pretty_size - Display an abbreviated size, like 3.4K
1647
 * @param buf    Buffer for the result
1648
 * @param buflen Length of the buffer
1649
 * @param num    Number to abbreviate
1650
 */
1651
void mutt_str_pretty_size(char *buf, size_t buflen, size_t num)
1652
0
{
1653
0
  if (!buf || (buflen == 0))
1654
0
    return;
1655
1656
0
  const bool c_size_show_bytes = cs_subset_bool(NeoMutt->sub, "size_show_bytes");
1657
0
  const bool c_size_show_fractions = cs_subset_bool(NeoMutt->sub, "size_show_fractions");
1658
0
  const bool c_size_show_mb = cs_subset_bool(NeoMutt->sub, "size_show_mb");
1659
0
  const bool c_size_units_on_left = cs_subset_bool(NeoMutt->sub, "size_units_on_left");
1660
1661
0
  if (c_size_show_bytes && (num < 1024))
1662
0
  {
1663
0
    snprintf(buf, buflen, "%d", (int) num);
1664
0
  }
1665
0
  else if (num == 0)
1666
0
  {
1667
0
    mutt_str_copy(buf, c_size_units_on_left ? "K0" : "0K", buflen);
1668
0
  }
1669
0
  else if (c_size_show_fractions && (num < 10189)) /* 0.1K - 9.9K */
1670
0
  {
1671
0
    snprintf(buf, buflen, c_size_units_on_left ? "K%3.1f" : "%3.1fK",
1672
0
             (num < 103) ? 0.1 : (num / 1024.0));
1673
0
  }
1674
0
  else if (!c_size_show_mb || (num < 1023949)) /* 10K - 999K */
1675
0
  {
1676
    /* 51 is magic which causes 10189/10240 to be rounded up to 10 */
1677
0
    snprintf(buf, buflen, c_size_units_on_left ? ("K%zu") : ("%zuK"), (num + 51) / 1024);
1678
0
  }
1679
0
  else if (c_size_show_fractions && (num < 10433332)) /* 1.0M - 9.9M */
1680
0
  {
1681
0
    snprintf(buf, buflen, c_size_units_on_left ? "M%3.1f" : "%3.1fM", num / 1048576.0);
1682
0
  }
1683
0
  else /* 10M+ */
1684
0
  {
1685
    /* (10433332 + 52428) / 1048576 = 10 */
1686
0
    snprintf(buf, buflen, c_size_units_on_left ? ("M%zu") : ("%zuM"), (num + 52428) / 1048576);
1687
0
  }
1688
0
}
1689
1690
/**
1691
 * add_to_stailq - Add a string to a list
1692
 * @param head String list
1693
 * @param str  String to add
1694
 *
1695
 * @note Duplicate or empty strings will not be added
1696
 */
1697
void add_to_stailq(struct ListHead *head, const char *str)
1698
0
{
1699
  /* don't add a NULL or empty string to the list */
1700
0
  if (!str || (*str == '\0'))
1701
0
    return;
1702
1703
  /* check to make sure the item is not already on this list */
1704
0
  struct ListNode *np = NULL;
1705
0
  STAILQ_FOREACH(np, head, entries)
1706
0
  {
1707
0
    if (mutt_istr_equal(str, np->data))
1708
0
    {
1709
0
      return;
1710
0
    }
1711
0
  }
1712
0
  mutt_list_insert_tail(head, mutt_str_dup(str));
1713
0
}
1714
1715
/**
1716
 * remove_from_stailq - Remove an item, matching a string, from a List
1717
 * @param head Head of the List
1718
 * @param str  String to match
1719
 *
1720
 * @note The string comparison is case-insensitive
1721
 */
1722
void remove_from_stailq(struct ListHead *head, const char *str)
1723
0
{
1724
0
  if (mutt_str_equal("*", str))
1725
0
  {
1726
0
    mutt_list_free(head); /* "unCMD *" means delete all current entries */
1727
0
  }
1728
0
  else
1729
0
  {
1730
0
    struct ListNode *np = NULL, *tmp = NULL;
1731
0
    STAILQ_FOREACH_SAFE(np, head, entries, tmp)
1732
0
    {
1733
0
      if (mutt_istr_equal(str, np->data))
1734
0
      {
1735
0
        STAILQ_REMOVE(head, np, ListNode, entries);
1736
0
        FREE(&np->data);
1737
0
        FREE(&np);
1738
0
        break;
1739
0
      }
1740
0
    }
1741
0
  }
1742
0
}