Coverage Report

Created: 2023-06-07 06:15

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