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