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