Coverage Report

Created: 2025-03-11 06:49

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