Coverage Report

Created: 2025-04-22 06:17

/src/neomutt/alias/dlg_alias.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * @file
3
 * Address book
4
 *
5
 * @authors
6
 * Copyright (C) 2017-2024 Richard Russon <rich@flatcap.org>
7
 * Copyright (C) 2020 Romeu Vieira <romeu.bizz@gmail.com>
8
 * Copyright (C) 2020-2023 Pietro Cerutti <gahr@gahr.ch>
9
 * Copyright (C) 2023 Anna Figueiredo Gomes <navi@vlhl.dev>
10
 * Copyright (C) 2023 Dennis Schön <mail@dennis-schoen.de>
11
 * Copyright (C) 2023-2024 Tóth János <gomba007@gmail.com>
12
 *
13
 * @copyright
14
 * This program is free software: you can redistribute it and/or modify it under
15
 * the terms of the GNU General Public License as published by the Free Software
16
 * Foundation, either version 2 of the License, or (at your option) any later
17
 * version.
18
 *
19
 * This program is distributed in the hope that it will be useful, but WITHOUT
20
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
21
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
22
 * details.
23
 *
24
 * You should have received a copy of the GNU General Public License along with
25
 * this program.  If not, see <http://www.gnu.org/licenses/>.
26
 */
27
28
/**
29
 * @page alias_dlg_alias Address Book Dialog
30
 *
31
 * The Address Book Dialog allows the user to select, add or delete aliases.
32
 *
33
 * This is a @ref gui_simple
34
 *
35
 * @note New aliases will be saved to `$alias_file`.
36
 *       Deleted aliases are deleted from memory only.
37
 *
38
 * ## Windows
39
 *
40
 * | Name                | Type         | See Also    |
41
 * | :------------------ | :----------- | :---------- |
42
 * | Address Book Dialog | WT_DLG_ALIAS | dlg_alias() |
43
 *
44
 * **Parent**
45
 * - @ref gui_dialog
46
 *
47
 * **Children**
48
 * - See: @ref gui_simple
49
 *
50
 * ## Data
51
 * - #Menu
52
 * - #Menu::mdata
53
 * - #AliasMenuData
54
 *
55
 * The @ref gui_simple holds a Menu.  The Address Book Dialog stores
56
 * its data (#AliasMenuData) in Menu::mdata.
57
 *
58
 * ## Events
59
 *
60
 * Once constructed, it is controlled by the following events:
61
 *
62
 * | Event Type            | Handler                    |
63
 * | :-------------------- | :------------------------- |
64
 * | #NT_ALIAS             | alias_alias_observer()     |
65
 * | #NT_CONFIG            | alias_config_observer()    |
66
 * | #NT_WINDOW            | alias_window_observer()    |
67
 * | MuttWindow::recalc()  | alias_recalc()             |
68
 *
69
 * The Address Book Dialog doesn't have any specific colours, so it doesn't
70
 * need to support #NT_COLOR.
71
 *
72
 * MuttWindow::recalc() is handled to support custom sorting.
73
 *
74
 * Some other events are handled by the @ref gui_simple.
75
 */
76
77
#include "config.h"
78
#include <stdbool.h>
79
#include <stdio.h>
80
#include "mutt/lib.h"
81
#include "address/lib.h"
82
#include "config/lib.h"
83
#include "email/lib.h"
84
#include "core/lib.h"
85
#include "gui/lib.h"
86
#include "lib.h"
87
#include "expando/lib.h"
88
#include "key/lib.h"
89
#include "menu/lib.h"
90
#include "pattern/lib.h"
91
#include "send/lib.h"
92
#include "alias.h"
93
#include "expando.h"
94
#include "functions.h"
95
#include "gui.h"
96
#include "mutt_logging.h"
97
98
/// Help Bar for the Alias dialog (address book)
99
static const struct Mapping AliasHelp[] = {
100
  // clang-format off
101
  { N_("Exit"),     OP_EXIT },
102
  { N_("Del"),      OP_DELETE },
103
  { N_("Undel"),    OP_UNDELETE },
104
  { N_("Sort"),     OP_SORT },
105
  { N_("Rev-Sort"), OP_SORT_REVERSE },
106
  { N_("Select"),   OP_GENERIC_SELECT_ENTRY },
107
  { N_("Help"),     OP_HELP },
108
  { NULL, 0 },
109
  // clang-format on
110
};
111
112
/**
113
 * alias_make_entry - Format an Alias for the Menu - Implements Menu::make_entry() - @ingroup menu_make_entry
114
 *
115
 * @sa $alias_format
116
 */
117
static int alias_make_entry(struct Menu *menu, int line, int max_cols, struct Buffer *buf)
118
0
{
119
0
  const struct AliasMenuData *mdata = menu->mdata;
120
0
  const struct AliasViewArray *ava = &mdata->ava;
121
0
  struct AliasView *av = ARRAY_GET(ava, line);
122
123
0
  const bool c_arrow_cursor = cs_subset_bool(menu->sub, "arrow_cursor");
124
0
  if (c_arrow_cursor)
125
0
  {
126
0
    const char *const c_arrow_string = cs_subset_string(menu->sub, "arrow_string");
127
0
    if (max_cols > 0)
128
0
      max_cols -= (mutt_strwidth(c_arrow_string) + 1);
129
0
  }
130
131
0
  const struct Expando *c_alias_format = cs_subset_expando(mdata->sub, "alias_format");
132
0
  return expando_filter(c_alias_format, AliasRenderCallbacks, av,
133
0
                        MUTT_FORMAT_ARROWCURSOR, max_cols, NeoMutt->env, buf);
134
0
}
135
136
/**
137
 * alias_tag - Tag some aliases - Implements Menu::tag() - @ingroup menu_tag
138
 */
139
static int alias_tag(struct Menu *menu, int sel, int act)
140
0
{
141
0
  const struct AliasMenuData *mdata = menu->mdata;
142
0
  const struct AliasViewArray *ava = &mdata->ava;
143
0
  struct AliasView *av = ARRAY_GET(ava, sel);
144
145
0
  bool ot = av->is_tagged;
146
147
0
  av->is_tagged = ((act >= 0) ? act : !av->is_tagged);
148
149
0
  return av->is_tagged - ot;
150
0
}
151
152
/**
153
 * alias_alias_observer - Notification that an Alias has changed - Implements ::observer_t - @ingroup observer_api
154
 */
155
static int alias_alias_observer(struct NotifyCallback *nc)
156
0
{
157
0
  if (nc->event_type != NT_ALIAS)
158
0
    return 0;
159
0
  if (!nc->global_data || !nc->event_data)
160
0
    return -1;
161
162
0
  struct EventAlias *ev_a = nc->event_data;
163
0
  struct Menu *menu = nc->global_data;
164
0
  struct AliasMenuData *mdata = menu->mdata;
165
0
  struct Alias *alias = ev_a->alias;
166
167
0
  if (nc->event_subtype == NT_ALIAS_ADD)
168
0
  {
169
0
    alias_array_alias_add(&mdata->ava, alias);
170
171
0
    if (alias_array_count_visible(&mdata->ava) != ARRAY_SIZE(&mdata->ava))
172
0
    {
173
0
      mutt_pattern_alias_func(NULL, mdata, PAA_VISIBLE, menu);
174
0
    }
175
0
  }
176
0
  else if (nc->event_subtype == NT_ALIAS_DELETE)
177
0
  {
178
0
    alias_array_alias_delete(&mdata->ava, alias);
179
180
0
    int vcount = alias_array_count_visible(&mdata->ava);
181
0
    int index = menu_get_index(menu);
182
0
    if ((index > (vcount - 1)) && (index > 0))
183
0
      menu_set_index(menu, index - 1);
184
0
  }
185
186
0
  alias_array_sort(&mdata->ava, mdata->sub);
187
188
0
  menu->max = alias_array_count_visible(&mdata->ava);
189
0
  menu_queue_redraw(menu, MENU_REDRAW_FULL);
190
0
  mutt_debug(LL_DEBUG5, "alias done, request WA_RECALC, MENU_REDRAW_FULL\n");
191
192
0
  return 0;
193
0
}
194
195
/**
196
 * alias_window_observer - Notification that a Window has changed - Implements ::observer_t - @ingroup observer_api
197
 *
198
 * This function is triggered by changes to the windows.
199
 *
200
 * - Delete (this window): clean up the resources held by the Help Bar
201
 */
202
static int alias_window_observer(struct NotifyCallback *nc)
203
0
{
204
0
  if (nc->event_type != NT_WINDOW)
205
0
    return 0;
206
0
  if (!nc->global_data || !nc->event_data)
207
0
    return -1;
208
0
  if (nc->event_subtype != NT_WINDOW_DELETE)
209
0
    return 0;
210
211
0
  struct MuttWindow *win_menu = nc->global_data;
212
0
  struct EventWindow *ev_w = nc->event_data;
213
0
  if (ev_w->win != win_menu)
214
0
    return 0;
215
216
0
  struct Menu *menu = win_menu->wdata;
217
218
0
  notify_observer_remove(NeoMutt->notify, alias_alias_observer, menu);
219
0
  notify_observer_remove(NeoMutt->sub->notify, alias_config_observer, menu);
220
0
  notify_observer_remove(win_menu->notify, alias_window_observer, win_menu);
221
222
0
  mutt_debug(LL_DEBUG5, "window delete done\n");
223
0
  return 0;
224
0
}
225
226
/**
227
 * alias_dialog_new - Create an Alias Selection Dialog
228
 * @param mdata Menu data holding Aliases
229
 * @retval obj SimpleDialogWindows Tuple containing Dialog, SimpleBar and Menu pointers
230
 */
231
static struct SimpleDialogWindows alias_dialog_new(struct AliasMenuData *mdata)
232
0
{
233
0
  struct SimpleDialogWindows sdw = simple_dialog_new(MENU_ALIAS, WT_DLG_ALIAS, AliasHelp);
234
235
0
  struct Menu *menu = sdw.menu;
236
237
0
  menu->make_entry = alias_make_entry;
238
0
  menu->tag = alias_tag;
239
0
  menu->max = alias_array_count_visible(&mdata->ava);
240
0
  menu->mdata = mdata;
241
0
  menu->mdata_free = NULL; // Menu doesn't own the data
242
243
0
  struct MuttWindow *win_menu = menu->win;
244
245
  // Override the Simple Dialog's recalc()
246
0
  win_menu->recalc = alias_recalc;
247
248
0
  alias_set_title(sdw.sbar, mdata->title, mdata->limit);
249
250
  // NT_COLOR is handled by the SimpleDialog
251
0
  notify_observer_add(NeoMutt->notify, NT_ALIAS, alias_alias_observer, menu);
252
0
  notify_observer_add(NeoMutt->sub->notify, NT_CONFIG, alias_config_observer, menu);
253
0
  notify_observer_add(win_menu->notify, NT_WINDOW, alias_window_observer, win_menu);
254
255
0
  return sdw;
256
0
}
257
258
/**
259
 * dlg_alias - Display a menu of Aliases - @ingroup gui_dlg
260
 * @param mdata Menu data holding Aliases
261
 * @retval true Selection was made
262
 *
263
 * The Alias Dialog is an Address Book.
264
 * The user can select addresses to add to an Email.
265
 */
266
static bool dlg_alias(struct AliasMenuData *mdata)
267
0
{
268
0
  if (ARRAY_EMPTY(&mdata->ava))
269
0
  {
270
0
    mutt_warning(_("You have no aliases"));
271
0
    return false;
272
0
  }
273
274
0
  mdata->title = mutt_str_dup(_("Aliases"));
275
276
0
  struct SimpleDialogWindows sdw = alias_dialog_new(mdata);
277
0
  struct Menu *menu = sdw.menu;
278
0
  mdata->menu = menu;
279
0
  mdata->sbar = sdw.sbar;
280
281
0
  alias_array_sort(&mdata->ava, mdata->sub);
282
283
0
  struct AliasView *avp = NULL;
284
0
  ARRAY_FOREACH(avp, &mdata->ava)
285
0
  {
286
0
    avp->num = ARRAY_FOREACH_IDX_avp;
287
0
  }
288
289
0
  struct MuttWindow *old_focus = window_set_focus(menu->win);
290
  // ---------------------------------------------------------------------------
291
  // Event Loop
292
0
  int rc = 0;
293
0
  int op = OP_NULL;
294
0
  do
295
0
  {
296
0
    menu_tagging_dispatcher(menu->win, op);
297
0
    window_redraw(NULL);
298
299
0
    op = km_dokey(MENU_ALIAS, GETCH_NO_FLAGS);
300
0
    mutt_debug(LL_DEBUG1, "Got op %s (%d)\n", opcodes_get_name(op), op);
301
0
    if (op < 0)
302
0
      continue;
303
0
    if (op == OP_NULL)
304
0
    {
305
0
      km_error_key(MENU_ALIAS);
306
0
      continue;
307
0
    }
308
0
    mutt_clear_error();
309
310
0
    rc = alias_function_dispatcher(sdw.dlg, op);
311
0
    if (rc == FR_UNKNOWN)
312
0
      rc = menu_function_dispatcher(menu->win, op);
313
0
    if (rc == FR_UNKNOWN)
314
0
      rc = global_function_dispatcher(NULL, op);
315
0
  } while ((rc != FR_DONE) && (rc != FR_CONTINUE));
316
  // ---------------------------------------------------------------------------
317
318
0
  window_set_focus(old_focus);
319
0
  simple_dialog_free(&sdw.dlg);
320
0
  window_redraw(NULL);
321
0
  return (rc == FR_CONTINUE); // Was a selection made?
322
0
}
323
324
/**
325
 * alias_complete - Alias completion routine
326
 * @param buf Partial Alias to complete
327
 * @param sub Config items
328
 * @retval 1 Success
329
 * @retval 0 Error
330
 *
331
 * Given a partial alias, this routine attempts to fill in the alias
332
 * from the alias list as much as possible. if given empty search string
333
 * or found nothing, present all aliases
334
 */
335
int alias_complete(struct Buffer *buf, struct ConfigSubset *sub)
336
0
{
337
0
  struct Alias *np = NULL;
338
0
  char bestname[8192] = { 0 };
339
340
0
  struct AliasMenuData mdata = { ARRAY_HEAD_INITIALIZER, NULL, sub };
341
0
  mdata.limit = buf_strdup(buf);
342
0
  mdata.search_state = search_state_new();
343
344
0
  if (buf_at(buf, 0) != '\0')
345
0
  {
346
0
    TAILQ_FOREACH(np, &Aliases, entries)
347
0
    {
348
0
      if (np->name && mutt_strn_equal(np->name, buf_string(buf), buf_len(buf)))
349
0
      {
350
0
        if (bestname[0] == '\0') /* init */
351
0
        {
352
0
          mutt_str_copy(bestname, np->name,
353
0
                        MIN(mutt_str_len(np->name) + 1, sizeof(bestname)));
354
0
        }
355
0
        else
356
0
        {
357
0
          int i;
358
0
          for (i = 0; np->name[i] && (np->name[i] == bestname[i]); i++)
359
0
            ; // do nothing
360
361
0
          bestname[i] = '\0';
362
0
        }
363
0
      }
364
0
    }
365
366
0
    if (bestname[0] == '\0')
367
0
    {
368
      // Create a View Array of all the Aliases
369
0
      FREE(&mdata.limit);
370
0
      TAILQ_FOREACH(np, &Aliases, entries)
371
0
      {
372
0
        alias_array_alias_add(&mdata.ava, np);
373
0
      }
374
0
    }
375
0
    else
376
0
    {
377
      /* fake the pattern for menu title */
378
0
      char *mtitle = NULL;
379
0
      mutt_str_asprintf(&mtitle, "~f ^%s", buf_string(buf));
380
0
      FREE(&mdata.limit);
381
0
      mdata.limit = mtitle;
382
383
0
      if (!mutt_str_equal(bestname, buf_string(buf)))
384
0
      {
385
        /* we are adding something to the completion */
386
0
        buf_strcpy_n(buf, bestname, mutt_str_len(bestname) + 1);
387
0
        FREE(&mdata.limit);
388
0
        search_state_free(&mdata.search_state);
389
0
        return 1;
390
0
      }
391
392
      /* build alias list and show it */
393
0
      TAILQ_FOREACH(np, &Aliases, entries)
394
0
      {
395
0
        int aasize = alias_array_alias_add(&mdata.ava, np);
396
397
0
        struct AliasView *av = ARRAY_GET(&mdata.ava, aasize - 1);
398
399
0
        if (np->name && !mutt_strn_equal(np->name, buf_string(buf), buf_len(buf)))
400
0
        {
401
0
          av->is_visible = false;
402
0
        }
403
0
      }
404
0
    }
405
0
  }
406
407
0
  if (ARRAY_EMPTY(&mdata.ava))
408
0
  {
409
0
    TAILQ_FOREACH(np, &Aliases, entries)
410
0
    {
411
0
      alias_array_alias_add(&mdata.ava, np);
412
0
    }
413
414
0
    mutt_pattern_alias_func(NULL, &mdata, PAA_VISIBLE, NULL);
415
0
  }
416
417
0
  if (!dlg_alias(&mdata))
418
0
    goto done;
419
420
0
  buf_reset(buf);
421
422
  // Extract the selected aliases
423
0
  struct Buffer *tmpbuf = buf_pool_get();
424
0
  struct AliasView *avp = NULL;
425
0
  ARRAY_FOREACH(avp, &mdata.ava)
426
0
  {
427
0
    if (!avp->is_tagged)
428
0
      continue;
429
430
0
    mutt_addrlist_write(&avp->alias->addr, tmpbuf, true);
431
0
    buf_addstr(tmpbuf, ", ");
432
0
  }
433
0
  buf_copy(buf, tmpbuf);
434
0
  buf_pool_release(&tmpbuf);
435
436
0
done:
437
  // Process any deleted aliases
438
0
  ARRAY_FOREACH(avp, &mdata.ava)
439
0
  {
440
0
    if (!avp->is_deleted)
441
0
      continue;
442
443
0
    TAILQ_REMOVE(&Aliases, avp->alias, entries);
444
0
    alias_free(&avp->alias);
445
0
  }
446
447
0
  ARRAY_FREE(&mdata.ava);
448
0
  FREE(&mdata.limit);
449
0
  FREE(&mdata.title);
450
0
  search_state_free(&mdata.search_state);
451
452
0
  return 0;
453
0
}
454
455
/**
456
 * alias_dialog - Open the aliases dialog
457
 * @param m   Mailbox
458
 * @param sub Config item
459
 */
460
void alias_dialog(struct Mailbox *m, struct ConfigSubset *sub)
461
0
{
462
0
  struct Alias *np = NULL;
463
464
0
  struct AliasMenuData mdata = { ARRAY_HEAD_INITIALIZER, NULL, sub };
465
0
  mdata.search_state = search_state_new();
466
467
  // Create a View Array of all the Aliases
468
0
  TAILQ_FOREACH(np, &Aliases, entries)
469
0
  {
470
0
    alias_array_alias_add(&mdata.ava, np);
471
0
  }
472
473
0
  if (!dlg_alias(&mdata))
474
0
    goto done;
475
476
  // Prepare the "To:" field of a new email
477
0
  struct Email *e = email_new();
478
0
  e->env = mutt_env_new();
479
480
0
  struct AliasView *avp = NULL;
481
0
  ARRAY_FOREACH(avp, &mdata.ava)
482
0
  {
483
0
    if (!avp->is_tagged)
484
0
      continue;
485
486
0
    struct AddressList al_copy = TAILQ_HEAD_INITIALIZER(al_copy);
487
0
    if (alias_to_addrlist(&al_copy, avp->alias))
488
0
    {
489
0
      mutt_addrlist_copy(&e->env->to, &al_copy, false);
490
0
      mutt_addrlist_clear(&al_copy);
491
0
    }
492
0
  }
493
494
0
  mutt_send_message(SEND_REVIEW_TO, e, NULL, m, NULL, sub);
495
496
0
done:
497
  // Process any deleted aliases
498
0
  ARRAY_FOREACH(avp, &mdata.ava)
499
0
  {
500
0
    if (avp->is_deleted)
501
0
    {
502
0
      TAILQ_REMOVE(&Aliases, avp->alias, entries);
503
0
      alias_free(&avp->alias);
504
0
    }
505
0
  }
506
507
0
  ARRAY_FREE(&mdata.ava);
508
0
  FREE(&mdata.limit);
509
0
  FREE(&mdata.title);
510
0
  search_state_free(&mdata.search_state);
511
0
}