1 /**
2  * @file
3  * Parse and execute user-defined hooks
4  *
5  * @authors
6  * Copyright (C) 1996-2002,2004,2007 Michael R. Elkins <me@mutt.org>, and others
7  * Copyright (C) 2019 Pietro Cerutti <gahr@gahr.ch>
8  *
9  * @copyright
10  * This program is free software: you can redistribute it and/or modify it under
11  * the terms of the GNU General Public License as published by the Free Software
12  * Foundation, either version 2 of the License, or (at your option) any later
13  * version.
14  *
15  * This program is distributed in the hope that it will be useful, but WITHOUT
16  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18  * details.
19  *
20  * You should have received a copy of the GNU General Public License along with
21  * this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 /**
25  * @page neo_hook Parse and execute user-defined hooks
26  *
27  * Parse and execute user-defined hooks
28  */
29 
30 #include "config.h"
31 #include <limits.h>
32 #include <stdbool.h>
33 #include <stdio.h>
34 #include <string.h>
35 #include <unistd.h>
36 #include "mutt/lib.h"
37 #include "address/lib.h"
38 #include "config/lib.h"
39 #include "email/lib.h"
40 #include "core/lib.h"
41 #include "alias/lib.h"
42 #include "mutt.h"
43 #include "hook.h"
44 #include "attach/lib.h"
45 #include "ncrypt/lib.h"
46 #include "pattern/lib.h"
47 #include "context.h"
48 #include "format_flags.h"
49 #include "hdrline.h"
50 #include "init.h"
51 #include "mutt_globals.h"
52 #include "muttlib.h"
53 #include "mx.h"
54 #ifdef USE_COMP_MBOX
55 #include "compmbox/lib.h"
56 #endif
57 
58 /**
59  * struct Hook - A list of user hooks
60  */
61 struct Hook
62 {
63   HookFlags type;              ///< Hook type
64   struct Regex regex;          ///< Regular expression
65   char *command;               ///< Filename, command or pattern to execute
66   struct PatternList *pattern; ///< Used for fcc,save,send-hook
67   TAILQ_ENTRY(Hook) entries;   ///< Linked list
68 };
69 TAILQ_HEAD(HookList, Hook);
70 
71 static struct HookList Hooks = TAILQ_HEAD_INITIALIZER(Hooks);
72 
73 static struct HashTable *IdxFmtHooks = NULL;
74 static HookFlags current_hook_type = MUTT_HOOK_NO_FLAGS;
75 
76 /**
77  * mutt_parse_hook - Parse the 'hook' family of commands - Implements Command::parse() - @ingroup command_parse
78  *
79  * This is used by 'account-hook', 'append-hook' and many more.
80  */
mutt_parse_hook(struct Buffer * buf,struct Buffer * s,intptr_t data,struct Buffer * err)81 enum CommandResult mutt_parse_hook(struct Buffer *buf, struct Buffer *s,
82                                    intptr_t data, struct Buffer *err)
83 {
84   struct Hook *hook = NULL;
85   int rc = MUTT_CMD_ERROR;
86   bool pat_not = false;
87   bool use_regex = true;
88   regex_t *rx = NULL;
89   struct PatternList *pat = NULL;
90   const bool folder_or_mbox = (data & (MUTT_FOLDER_HOOK | MUTT_MBOX_HOOK));
91 
92   struct Buffer *cmd = mutt_buffer_pool_get();
93   struct Buffer *pattern = mutt_buffer_pool_get();
94 
95   if (~data & MUTT_GLOBAL_HOOK) /* NOT a global hook */
96   {
97     if (*s->dptr == '!')
98     {
99       s->dptr++;
100       SKIPWS(s->dptr);
101       pat_not = true;
102     }
103 
104     mutt_extract_token(pattern, s, MUTT_TOKEN_NO_FLAGS);
105     if (folder_or_mbox &&
106         mutt_str_equal(mutt_buffer_string(pattern), "-noregex"))
107     {
108       use_regex = false;
109       if (!MoreArgs(s))
110       {
111         mutt_buffer_printf(err, _("%s: too few arguments"), buf->data);
112         rc = MUTT_CMD_WARNING;
113         goto cleanup;
114       }
115       mutt_extract_token(pattern, s, MUTT_TOKEN_NO_FLAGS);
116     }
117 
118     if (!MoreArgs(s))
119     {
120       mutt_buffer_printf(err, _("%s: too few arguments"), buf->data);
121       rc = MUTT_CMD_WARNING;
122       goto cleanup;
123     }
124   }
125 
126   mutt_extract_token(cmd, s,
127                      (data & (MUTT_FOLDER_HOOK | MUTT_SEND_HOOK | MUTT_SEND2_HOOK |
128                               MUTT_ACCOUNT_HOOK | MUTT_REPLY_HOOK)) ?
129                          MUTT_TOKEN_SPACE :
130                          MUTT_TOKEN_NO_FLAGS);
131 
132   if (mutt_buffer_is_empty(cmd))
133   {
134     mutt_buffer_printf(err, _("%s: too few arguments"), buf->data);
135     rc = MUTT_CMD_WARNING;
136     goto cleanup;
137   }
138 
139   if (MoreArgs(s))
140   {
141     mutt_buffer_printf(err, _("%s: too many arguments"), buf->data);
142     rc = MUTT_CMD_WARNING;
143     goto cleanup;
144   }
145 
146   const char *const c_default_hook =
147       cs_subset_string(NeoMutt->sub, "default_hook");
148   if (folder_or_mbox)
149   {
150     /* Accidentally using the ^ mailbox shortcut in the .neomuttrc is a
151      * common mistake */
152     if ((pattern->data[0] == '^') && !CurrentFolder)
153     {
154       mutt_buffer_strcpy(err, _("current mailbox shortcut '^' is unset"));
155       goto cleanup;
156     }
157 
158     struct Buffer *tmp = mutt_buffer_pool_get();
159     mutt_buffer_copy(tmp, pattern);
160     mutt_buffer_expand_path_regex(tmp, use_regex);
161 
162     /* Check for other mailbox shortcuts that expand to the empty string.
163      * This is likely a mistake too */
164     if (mutt_buffer_is_empty(tmp) && !mutt_buffer_is_empty(pattern))
165     {
166       mutt_buffer_strcpy(err, _("mailbox shortcut expanded to empty regex"));
167       mutt_buffer_pool_release(&tmp);
168       goto cleanup;
169     }
170 
171     if (use_regex)
172     {
173       mutt_buffer_copy(pattern, tmp);
174     }
175     else
176     {
177       mutt_file_sanitize_regex(pattern, mutt_buffer_string(tmp));
178     }
179     mutt_buffer_pool_release(&tmp);
180   }
181 #ifdef USE_COMP_MBOX
182   else if (data & (MUTT_APPEND_HOOK | MUTT_OPEN_HOOK | MUTT_CLOSE_HOOK))
183   {
184     if (mutt_comp_valid_command(mutt_buffer_string(cmd)) == 0)
185     {
186       mutt_buffer_strcpy(err, _("badly formatted command string"));
187       goto cleanup;
188     }
189   }
190 #endif
191   else if (c_default_hook && (~data & MUTT_GLOBAL_HOOK) &&
192            !(data & (MUTT_CHARSET_HOOK | MUTT_ICONV_HOOK | MUTT_ACCOUNT_HOOK)) &&
193            (!WithCrypto || !(data & MUTT_CRYPT_HOOK)))
194   {
195     /* At this stage remain only message-hooks, reply-hooks, send-hooks,
196      * send2-hooks, save-hooks, and fcc-hooks: All those allowing full
197      * patterns. If given a simple regex, we expand $default_hook.  */
198     mutt_check_simple(pattern, c_default_hook);
199   }
200 
201   if (data & (MUTT_MBOX_HOOK | MUTT_SAVE_HOOK | MUTT_FCC_HOOK))
202   {
203     mutt_buffer_expand_path(cmd);
204   }
205 
206   /* check to make sure that a matching hook doesn't already exist */
207   TAILQ_FOREACH(hook, &Hooks, entries)
208   {
209     if (data & MUTT_GLOBAL_HOOK)
210     {
211       /* Ignore duplicate global hooks */
212       if (mutt_str_equal(hook->command, mutt_buffer_string(cmd)))
213       {
214         rc = MUTT_CMD_SUCCESS;
215         goto cleanup;
216       }
217     }
218     else if ((hook->type == data) && (hook->regex.pat_not == pat_not) &&
219              mutt_str_equal(mutt_buffer_string(pattern), hook->regex.pattern))
220     {
221       if (data & (MUTT_FOLDER_HOOK | MUTT_SEND_HOOK | MUTT_SEND2_HOOK | MUTT_MESSAGE_HOOK |
222                   MUTT_ACCOUNT_HOOK | MUTT_REPLY_HOOK | MUTT_CRYPT_HOOK |
223                   MUTT_TIMEOUT_HOOK | MUTT_STARTUP_HOOK | MUTT_SHUTDOWN_HOOK))
224       {
225         /* these hooks allow multiple commands with the same
226          * pattern, so if we've already seen this pattern/command pair, just
227          * ignore it instead of creating a duplicate */
228         if (mutt_str_equal(hook->command, mutt_buffer_string(cmd)))
229         {
230           rc = MUTT_CMD_SUCCESS;
231           goto cleanup;
232         }
233       }
234       else
235       {
236         /* other hooks only allow one command per pattern, so update the
237          * entry with the new command.  this currently does not change the
238          * order of execution of the hooks, which i think is desirable since
239          * a common action to perform is to change the default (.) entry
240          * based upon some other information. */
241         FREE(&hook->command);
242         hook->command = mutt_buffer_strdup(cmd);
243         rc = MUTT_CMD_SUCCESS;
244         goto cleanup;
245       }
246     }
247   }
248 
249   if (data & (MUTT_CHARSET_HOOK | MUTT_ICONV_HOOK))
250   {
251     /* These are managed separately by the charset code */
252     enum LookupType type = (data & MUTT_CHARSET_HOOK) ? MUTT_LOOKUP_CHARSET : MUTT_LOOKUP_ICONV;
253     if (mutt_ch_lookup_add(type, mutt_buffer_string(pattern), mutt_buffer_string(cmd), err))
254       rc = MUTT_CMD_SUCCESS;
255     goto cleanup;
256   }
257   else if (data & (MUTT_SEND_HOOK | MUTT_SEND2_HOOK | MUTT_SAVE_HOOK |
258                    MUTT_FCC_HOOK | MUTT_MESSAGE_HOOK | MUTT_REPLY_HOOK))
259   {
260     PatternCompFlags comp_flags;
261 
262     if (data & (MUTT_SEND2_HOOK))
263       comp_flags = MUTT_PC_SEND_MODE_SEARCH;
264     else if (data & (MUTT_SEND_HOOK | MUTT_FCC_HOOK))
265       comp_flags = MUTT_PC_NO_FLAGS;
266     else
267       comp_flags = MUTT_PC_FULL_MSG;
268 
269     pat = mutt_pattern_comp(ctx_mailbox(Context), Context ? Context->menu : NULL,
270                             mutt_buffer_string(pattern), comp_flags, err);
271     if (!pat)
272       goto cleanup;
273   }
274   else if (~data & MUTT_GLOBAL_HOOK) /* NOT a global hook */
275   {
276     /* Hooks not allowing full patterns: Check syntax of regex */
277     rx = mutt_mem_malloc(sizeof(regex_t));
278     int rc2 = REG_COMP(rx, NONULL(mutt_buffer_string(pattern)),
279                        ((data & MUTT_CRYPT_HOOK) ? REG_ICASE : 0));
280     if (rc2 != 0)
281     {
282       regerror(rc2, rx, err->data, err->dsize);
283       FREE(&rx);
284       goto cleanup;
285     }
286   }
287 
288   hook = mutt_mem_calloc(1, sizeof(struct Hook));
289   hook->type = data;
290   hook->command = mutt_buffer_strdup(cmd);
291   hook->pattern = pat;
292   hook->regex.pattern = mutt_buffer_strdup(pattern);
293   hook->regex.regex = rx;
294   hook->regex.pat_not = pat_not;
295   TAILQ_INSERT_TAIL(&Hooks, hook, entries);
296   rc = MUTT_CMD_SUCCESS;
297 
298 cleanup:
299   mutt_buffer_pool_release(&cmd);
300   mutt_buffer_pool_release(&pattern);
301   return rc;
302 }
303 
304 /**
305  * delete_hook - Delete a Hook
306  * @param h Hook to delete
307  */
delete_hook(struct Hook * h)308 static void delete_hook(struct Hook *h)
309 {
310   FREE(&h->command);
311   FREE(&h->regex.pattern);
312   if (h->regex.regex)
313   {
314     regfree(h->regex.regex);
315     FREE(&h->regex.regex);
316   }
317   mutt_pattern_free(&h->pattern);
318   FREE(&h);
319 }
320 
321 /**
322  * mutt_delete_hooks - Delete matching hooks
323  * @param type Hook type to delete, see #HookFlags
324  *
325  * If 0 is passed, all the hooks will be deleted.
326  */
mutt_delete_hooks(HookFlags type)327 void mutt_delete_hooks(HookFlags type)
328 {
329   struct Hook *h = NULL;
330   struct Hook *tmp = NULL;
331 
332   TAILQ_FOREACH_SAFE(h, &Hooks, entries, tmp)
333   {
334     if ((type == MUTT_HOOK_NO_FLAGS) || (type == h->type))
335     {
336       TAILQ_REMOVE(&Hooks, h, entries);
337       delete_hook(h);
338     }
339   }
340 }
341 
342 /**
343  * idxfmt_hashelem_free - Delete an index-format-hook from the Hash Table - Implements ::hash_hdata_free_t - @ingroup hash_hdata_free_api
344  */
idxfmt_hashelem_free(int type,void * obj,intptr_t data)345 static void idxfmt_hashelem_free(int type, void *obj, intptr_t data)
346 {
347   struct HookList *hl = obj;
348   struct Hook *h = NULL;
349   struct Hook *tmp = NULL;
350 
351   TAILQ_FOREACH_SAFE(h, hl, entries, tmp)
352   {
353     TAILQ_REMOVE(hl, h, entries);
354     delete_hook(h);
355   }
356 
357   FREE(&hl);
358 }
359 
360 /**
361  * delete_idxfmt_hooks - Delete all the index-format-hooks
362  */
delete_idxfmt_hooks(void)363 static void delete_idxfmt_hooks(void)
364 {
365   mutt_hash_free(&IdxFmtHooks);
366 }
367 
368 /**
369  * mutt_parse_idxfmt_hook - Parse the 'index-format-hook' command - Implements Command::parse() - @ingroup command_parse
370  */
mutt_parse_idxfmt_hook(struct Buffer * buf,struct Buffer * s,intptr_t data,struct Buffer * err)371 enum CommandResult mutt_parse_idxfmt_hook(struct Buffer *buf, struct Buffer *s,
372                                           intptr_t data, struct Buffer *err)
373 {
374   enum CommandResult rc = MUTT_CMD_ERROR;
375   bool pat_not = false;
376 
377   struct Buffer *name = mutt_buffer_pool_get();
378   struct Buffer *pattern = mutt_buffer_pool_get();
379   struct Buffer *fmtstring = mutt_buffer_pool_get();
380 
381   if (!IdxFmtHooks)
382   {
383     IdxFmtHooks = mutt_hash_new(30, MUTT_HASH_STRDUP_KEYS);
384     mutt_hash_set_destructor(IdxFmtHooks, idxfmt_hashelem_free, 0);
385   }
386 
387   if (!MoreArgs(s))
388   {
389     mutt_buffer_printf(err, _("%s: too few arguments"), buf->data);
390     goto out;
391   }
392   mutt_extract_token(name, s, MUTT_TOKEN_NO_FLAGS);
393   struct HookList *hl = mutt_hash_find(IdxFmtHooks, mutt_buffer_string(name));
394 
395   if (*s->dptr == '!')
396   {
397     s->dptr++;
398     SKIPWS(s->dptr);
399     pat_not = true;
400   }
401   mutt_extract_token(pattern, s, MUTT_TOKEN_NO_FLAGS);
402 
403   if (!MoreArgs(s))
404   {
405     mutt_buffer_printf(err, _("%s: too few arguments"), buf->data);
406     goto out;
407   }
408   mutt_extract_token(fmtstring, s, MUTT_TOKEN_NO_FLAGS);
409 
410   if (MoreArgs(s))
411   {
412     mutt_buffer_printf(err, _("%s: too many arguments"), buf->data);
413     goto out;
414   }
415 
416   const char *const c_default_hook =
417       cs_subset_string(NeoMutt->sub, "default_hook");
418   if (c_default_hook)
419     mutt_check_simple(pattern, c_default_hook);
420 
421   /* check to make sure that a matching hook doesn't already exist */
422   struct Hook *hook = NULL;
423   if (hl)
424   {
425     TAILQ_FOREACH(hook, hl, entries)
426     {
427       if ((hook->regex.pat_not == pat_not) &&
428           mutt_str_equal(mutt_buffer_string(pattern), hook->regex.pattern))
429       {
430         mutt_str_replace(&hook->command, mutt_buffer_string(fmtstring));
431         rc = MUTT_CMD_SUCCESS;
432         goto out;
433       }
434     }
435   }
436 
437   /* MUTT_PC_PATTERN_DYNAMIC sets so that date ranges are regenerated during
438    * matching.  This of course is slower, but index-format-hook is commonly
439    * used for date ranges, and they need to be evaluated relative to "now", not
440    * the hook compilation time.  */
441   struct PatternList *pat =
442       mutt_pattern_comp(ctx_mailbox(Context), Context ? Context->menu : NULL,
443                         mutt_buffer_string(pattern),
444                         MUTT_PC_FULL_MSG | MUTT_PC_PATTERN_DYNAMIC, err);
445   if (!pat)
446     goto out;
447 
448   hook = mutt_mem_calloc(1, sizeof(struct Hook));
449   hook->type = MUTT_IDXFMTHOOK;
450   hook->command = mutt_buffer_strdup(fmtstring);
451   hook->pattern = pat;
452   hook->regex.pattern = mutt_buffer_strdup(pattern);
453   hook->regex.regex = NULL;
454   hook->regex.pat_not = pat_not;
455 
456   if (!hl)
457   {
458     hl = mutt_mem_calloc(1, sizeof(*hl));
459     TAILQ_INIT(hl);
460     mutt_hash_insert(IdxFmtHooks, mutt_buffer_string(name), hl);
461   }
462 
463   TAILQ_INSERT_TAIL(hl, hook, entries);
464   rc = MUTT_CMD_SUCCESS;
465 
466 out:
467   mutt_buffer_pool_release(&name);
468   mutt_buffer_pool_release(&pattern);
469   mutt_buffer_pool_release(&fmtstring);
470 
471   return rc;
472 }
473 
474 /**
475  * mutt_parse_unhook - Parse the 'unhook' command - Implements Command::parse() - @ingroup command_parse
476  */
mutt_parse_unhook(struct Buffer * buf,struct Buffer * s,intptr_t data,struct Buffer * err)477 enum CommandResult mutt_parse_unhook(struct Buffer *buf, struct Buffer *s,
478                                      intptr_t data, struct Buffer *err)
479 {
480   while (MoreArgs(s))
481   {
482     mutt_extract_token(buf, s, MUTT_TOKEN_NO_FLAGS);
483     if (mutt_str_equal("*", buf->data))
484     {
485       if (current_hook_type != MUTT_TOKEN_NO_FLAGS)
486       {
487         mutt_buffer_printf(err, "%s", _("unhook: Can't do unhook * from within a hook"));
488         return MUTT_CMD_WARNING;
489       }
490       mutt_delete_hooks(MUTT_HOOK_NO_FLAGS);
491       delete_idxfmt_hooks();
492       mutt_ch_lookup_remove();
493     }
494     else
495     {
496       HookFlags type = mutt_get_hook_type(buf->data);
497 
498       if (type == MUTT_HOOK_NO_FLAGS)
499       {
500         mutt_buffer_printf(err, _("unhook: unknown hook type: %s"), buf->data);
501         return MUTT_CMD_ERROR;
502       }
503       if (type & (MUTT_CHARSET_HOOK | MUTT_ICONV_HOOK))
504       {
505         mutt_ch_lookup_remove();
506         return MUTT_CMD_SUCCESS;
507       }
508       if (current_hook_type == type)
509       {
510         mutt_buffer_printf(err, _("unhook: Can't delete a %s from within a %s"),
511                            buf->data, buf->data);
512         return MUTT_CMD_WARNING;
513       }
514       if (type == MUTT_IDXFMTHOOK)
515         delete_idxfmt_hooks();
516       else
517         mutt_delete_hooks(type);
518     }
519   }
520   return MUTT_CMD_SUCCESS;
521 }
522 
523 /**
524  * mutt_folder_hook - Perform a folder hook
525  * @param path Path to potentially match
526  * @param desc Description to potentially match
527  */
mutt_folder_hook(const char * path,const char * desc)528 void mutt_folder_hook(const char *path, const char *desc)
529 {
530   if (!path && !desc)
531     return;
532 
533   struct Hook *hook = NULL;
534   struct Buffer *err = mutt_buffer_pool_get();
535 
536   current_hook_type = MUTT_FOLDER_HOOK;
537 
538   TAILQ_FOREACH(hook, &Hooks, entries)
539   {
540     if (!hook->command)
541       continue;
542 
543     if (!(hook->type & MUTT_FOLDER_HOOK))
544       continue;
545 
546     const char *match = NULL;
547     if (mutt_regex_match(&hook->regex, path))
548       match = path;
549     else if (mutt_regex_match(&hook->regex, desc))
550       match = desc;
551 
552     if (match)
553     {
554       mutt_debug(LL_DEBUG1, "folder-hook '%s' matches '%s'\n", hook->regex.pattern, match);
555       mutt_debug(LL_DEBUG5, "    %s\n", hook->command);
556       if (mutt_parse_rc_line(hook->command, err) == MUTT_CMD_ERROR)
557       {
558         mutt_error("%s", mutt_buffer_string(err));
559         break;
560       }
561     }
562   }
563   mutt_buffer_pool_release(&err);
564 
565   current_hook_type = MUTT_HOOK_NO_FLAGS;
566 }
567 
568 /**
569  * mutt_find_hook - Find a matching hook
570  * @param type Hook type, see #HookFlags
571  * @param pat  Pattern to match
572  * @retval ptr Command string
573  *
574  * @note The returned string must not be freed.
575  */
mutt_find_hook(HookFlags type,const char * pat)576 char *mutt_find_hook(HookFlags type, const char *pat)
577 {
578   struct Hook *tmp = NULL;
579 
580   TAILQ_FOREACH(tmp, &Hooks, entries)
581   {
582     if (tmp->type & type)
583     {
584       if (mutt_regex_match(&tmp->regex, pat))
585         return tmp->command;
586     }
587   }
588   return NULL;
589 }
590 
591 /**
592  * mutt_message_hook - Perform a message hook
593  * @param m   Mailbox Context
594  * @param e   Email
595  * @param type Hook type, see #HookFlags
596  */
mutt_message_hook(struct Mailbox * m,struct Email * e,HookFlags type)597 void mutt_message_hook(struct Mailbox *m, struct Email *e, HookFlags type)
598 {
599   struct Hook *hook = NULL;
600   struct PatternCache cache = { 0 };
601   struct Buffer *err = mutt_buffer_pool_get();
602 
603   current_hook_type = type;
604 
605   TAILQ_FOREACH(hook, &Hooks, entries)
606   {
607     if (!hook->command)
608       continue;
609 
610     if (hook->type & type)
611     {
612       if ((mutt_pattern_exec(SLIST_FIRST(hook->pattern), 0, m, e, &cache) > 0) ^
613           hook->regex.pat_not)
614       {
615         if (mutt_parse_rc_line(hook->command, err) == MUTT_CMD_ERROR)
616         {
617           mutt_error("%s", mutt_buffer_string(err));
618           current_hook_type = MUTT_HOOK_NO_FLAGS;
619           mutt_buffer_pool_release(&err);
620 
621           return;
622         }
623         /* Executing arbitrary commands could affect the pattern results,
624          * so the cache has to be wiped */
625         memset(&cache, 0, sizeof(cache));
626       }
627     }
628   }
629   mutt_buffer_pool_release(&err);
630 
631   current_hook_type = MUTT_HOOK_NO_FLAGS;
632 }
633 
634 /**
635  * addr_hook - Perform an address hook (get a path)
636  * @param path    Buffer for path
637  * @param pathlen Length of buffer
638  * @param type    Hook type, see #HookFlags
639  * @param m       Mailbox
640  * @param e       Email
641  * @retval  0 Success
642  * @retval -1 Failure
643  */
addr_hook(char * path,size_t pathlen,HookFlags type,struct Mailbox * m,struct Email * e)644 static int addr_hook(char *path, size_t pathlen, HookFlags type,
645                      struct Mailbox *m, struct Email *e)
646 {
647   struct Hook *hook = NULL;
648   struct PatternCache cache = { 0 };
649 
650   /* determine if a matching hook exists */
651   TAILQ_FOREACH(hook, &Hooks, entries)
652   {
653     if (!hook->command)
654       continue;
655 
656     if (hook->type & type)
657     {
658       if ((mutt_pattern_exec(SLIST_FIRST(hook->pattern), 0, m, e, &cache) > 0) ^
659           hook->regex.pat_not)
660       {
661         mutt_make_string(path, pathlen, 0, hook->command, m, -1, e, MUTT_FORMAT_PLAIN, NULL);
662         return 0;
663       }
664     }
665   }
666 
667   return -1;
668 }
669 
670 /**
671  * mutt_default_save - Find the default save path for an email
672  * @param path    Buffer for the path
673  * @param pathlen Length of buffer
674  * @param e       Email
675  */
mutt_default_save(char * path,size_t pathlen,struct Email * e)676 void mutt_default_save(char *path, size_t pathlen, struct Email *e)
677 {
678   *path = '\0';
679   if (addr_hook(path, pathlen, MUTT_SAVE_HOOK, ctx_mailbox(Context), e) == 0)
680     return;
681 
682   struct Envelope *env = e->env;
683   const struct Address *from = TAILQ_FIRST(&env->from);
684   const struct Address *reply_to = TAILQ_FIRST(&env->reply_to);
685   const struct Address *to = TAILQ_FIRST(&env->to);
686   const struct Address *cc = TAILQ_FIRST(&env->cc);
687   const struct Address *addr = NULL;
688   bool from_me = mutt_addr_is_user(from);
689 
690   if (!from_me && reply_to && reply_to->mailbox)
691     addr = reply_to;
692   else if (!from_me && from && from->mailbox)
693     addr = from;
694   else if (to && to->mailbox)
695     addr = to;
696   else if (cc && cc->mailbox)
697     addr = cc;
698   else
699     addr = NULL;
700   if (addr)
701   {
702     struct Buffer *tmp = mutt_buffer_pool_get();
703     mutt_safe_path(tmp, addr);
704     snprintf(path, pathlen, "=%s", mutt_buffer_string(tmp));
705     mutt_buffer_pool_release(&tmp);
706   }
707 }
708 
709 /**
710  * mutt_select_fcc - Select the FCC path for an email
711  * @param path    Buffer for the path
712  * @param e       Email
713  */
mutt_select_fcc(struct Buffer * path,struct Email * e)714 void mutt_select_fcc(struct Buffer *path, struct Email *e)
715 {
716   mutt_buffer_alloc(path, PATH_MAX);
717 
718   if (addr_hook(path->data, path->dsize, MUTT_FCC_HOOK, NULL, e) != 0)
719   {
720     const struct Address *to = TAILQ_FIRST(&e->env->to);
721     const struct Address *cc = TAILQ_FIRST(&e->env->cc);
722     const struct Address *bcc = TAILQ_FIRST(&e->env->bcc);
723     const bool c_save_name = cs_subset_bool(NeoMutt->sub, "save_name");
724     const bool c_force_name = cs_subset_bool(NeoMutt->sub, "force_name");
725     const char *const c_record = cs_subset_string(NeoMutt->sub, "record");
726     if ((c_save_name || c_force_name) && (to || cc || bcc))
727     {
728       const struct Address *addr = to ? to : (cc ? cc : bcc);
729       struct Buffer *buf = mutt_buffer_pool_get();
730       mutt_safe_path(buf, addr);
731       const char *const c_folder = cs_subset_string(NeoMutt->sub, "folder");
732       mutt_buffer_concat_path(path, NONULL(c_folder), mutt_buffer_string(buf));
733       mutt_buffer_pool_release(&buf);
734       if (!c_force_name && (mx_access(mutt_buffer_string(path), W_OK) != 0))
735         mutt_buffer_strcpy(path, c_record);
736     }
737     else
738       mutt_buffer_strcpy(path, c_record);
739   }
740   else
741     mutt_buffer_fix_dptr(path);
742 
743   mutt_buffer_pretty_mailbox(path);
744 }
745 
746 /**
747  * list_hook - Find hook strings matching
748  * @param[out] matches List of hook strings
749  * @param[in]  match   String to match
750  * @param[in]  hook    Hook type, see #HookFlags
751  */
list_hook(struct ListHead * matches,const char * match,HookFlags hook)752 static void list_hook(struct ListHead *matches, const char *match, HookFlags hook)
753 {
754   struct Hook *tmp = NULL;
755 
756   TAILQ_FOREACH(tmp, &Hooks, entries)
757   {
758     if ((tmp->type & hook) && mutt_regex_match(&tmp->regex, match))
759     {
760       mutt_list_insert_tail(matches, mutt_str_dup(tmp->command));
761     }
762   }
763 }
764 
765 /**
766  * mutt_crypt_hook - Find crypto hooks for an Address
767  * @param[out] list List of keys
768  * @param[in]  addr Address to match
769  *
770  * The crypt-hook associates keys with addresses.
771  */
mutt_crypt_hook(struct ListHead * list,struct Address * addr)772 void mutt_crypt_hook(struct ListHead *list, struct Address *addr)
773 {
774   list_hook(list, addr->mailbox, MUTT_CRYPT_HOOK);
775 }
776 
777 /**
778  * mutt_account_hook - Perform an account hook
779  * @param url Account URL to match
780  */
mutt_account_hook(const char * url)781 void mutt_account_hook(const char *url)
782 {
783   /* parsing commands with URLs in an account hook can cause a recursive
784    * call. We just skip processing if this occurs. Typically such commands
785    * belong in a folder-hook -- perhaps we should warn the user. */
786   static bool inhook = false;
787   if (inhook)
788     return;
789 
790   struct Hook *hook = NULL;
791   struct Buffer *err = mutt_buffer_pool_get();
792 
793   TAILQ_FOREACH(hook, &Hooks, entries)
794   {
795     if (!(hook->command && (hook->type & MUTT_ACCOUNT_HOOK)))
796       continue;
797 
798     if (mutt_regex_match(&hook->regex, url))
799     {
800       inhook = true;
801       mutt_debug(LL_DEBUG1, "account-hook '%s' matches '%s'\n", hook->regex.pattern, url);
802       mutt_debug(LL_DEBUG5, "    %s\n", hook->command);
803 
804       if (mutt_parse_rc_line(hook->command, err) == MUTT_CMD_ERROR)
805       {
806         mutt_error("%s", mutt_buffer_string(err));
807         mutt_buffer_pool_release(&err);
808 
809         inhook = false;
810         goto done;
811       }
812 
813       inhook = false;
814     }
815   }
816 done:
817   mutt_buffer_pool_release(&err);
818 }
819 
820 /**
821  * mutt_timeout_hook - Execute any timeout hooks
822  *
823  * The user can configure hooks to be run on timeout.
824  * This function finds all the matching hooks and executes them.
825  */
mutt_timeout_hook(void)826 void mutt_timeout_hook(void)
827 {
828   struct Hook *hook = NULL;
829   struct Buffer err;
830   char buf[256];
831 
832   mutt_buffer_init(&err);
833   err.data = buf;
834   err.dsize = sizeof(buf);
835 
836   TAILQ_FOREACH(hook, &Hooks, entries)
837   {
838     if (!(hook->command && (hook->type & MUTT_TIMEOUT_HOOK)))
839       continue;
840 
841     if (mutt_parse_rc_line(hook->command, &err) == MUTT_CMD_ERROR)
842     {
843       mutt_error("%s", err.data);
844       mutt_buffer_reset(&err);
845 
846       /* The hooks should be independent of each other, so even though this on
847        * failed, we'll carry on with the others. */
848     }
849   }
850 
851   /* Delete temporary attachment files */
852   mutt_unlink_temp_attachments();
853 }
854 
855 /**
856  * mutt_startup_shutdown_hook - Execute any startup/shutdown hooks
857  * @param type Hook type: #MUTT_STARTUP_HOOK or #MUTT_SHUTDOWN_HOOK
858  *
859  * The user can configure hooks to be run on startup/shutdown.
860  * This function finds all the matching hooks and executes them.
861  */
mutt_startup_shutdown_hook(HookFlags type)862 void mutt_startup_shutdown_hook(HookFlags type)
863 {
864   struct Hook *hook = NULL;
865   struct Buffer err = mutt_buffer_make(0);
866   char buf[256];
867 
868   err.data = buf;
869   err.dsize = sizeof(buf);
870 
871   TAILQ_FOREACH(hook, &Hooks, entries)
872   {
873     if (!(hook->command && (hook->type & type)))
874       continue;
875 
876     if (mutt_parse_rc_line(hook->command, &err) == MUTT_CMD_ERROR)
877     {
878       mutt_error("%s", err.data);
879       mutt_buffer_reset(&err);
880     }
881   }
882 }
883 
884 /**
885  * mutt_idxfmt_hook - Get index-format-hook format string
886  * @param name Hook name
887  * @param m    Mailbox
888  * @param e    Email
889  * @retval ptr  printf(3)-like format string
890  * @retval NULL No matching hook
891  */
mutt_idxfmt_hook(const char * name,struct Mailbox * m,struct Email * e)892 const char *mutt_idxfmt_hook(const char *name, struct Mailbox *m, struct Email *e)
893 {
894   if (!IdxFmtHooks)
895     return NULL;
896 
897   struct HookList *hl = mutt_hash_find(IdxFmtHooks, name);
898   if (!hl)
899     return NULL;
900 
901   current_hook_type = MUTT_IDXFMTHOOK;
902 
903   struct PatternCache cache = { 0 };
904   const char *fmtstring = NULL;
905   struct Hook *hook = NULL;
906 
907   TAILQ_FOREACH(hook, hl, entries)
908   {
909     struct Pattern *pat = SLIST_FIRST(hook->pattern);
910     if ((mutt_pattern_exec(pat, 0, m, e, &cache) > 0) ^ hook->regex.pat_not)
911     {
912       fmtstring = hook->command;
913       break;
914     }
915   }
916 
917   current_hook_type = MUTT_HOOK_NO_FLAGS;
918 
919   return fmtstring;
920 }
921