1 /**
2 * @file
3 * Address book
4 *
5 * @authors
6 * Copyright (C) 1996-2000,2007 Michael R. Elkins <me@mutt.org>
7 * Copyright (C) 2019 Pietro Cerutti <gahr@gahr.ch>
8 * Copyright (C) 2020 Richard Russon <rich@flatcap.org>
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 alias_dlgalias Address Book Dialog
27 *
28 * The Address Book Dialog allows the user to select, add or delete aliases.
29 *
30 * This is a @ref gui_simple
31 *
32 * @note New aliases will be saved to `$alias_file`.
33 * Deleted aliases are deleted from memory only.
34 *
35 * ## Windows
36 *
37 * | Name | Type | See Also |
38 * | :------------------ | :----------- | :----------------- |
39 * | Address Book Dialog | WT_DLG_ALIAS | dlg_select_alias() |
40 *
41 * **Parent**
42 * - @ref gui_dialog
43 *
44 * **Children**
45 * - See: @ref gui_simple
46 *
47 * ## Data
48 * - #Menu
49 * - #Menu::mdata
50 * - #AliasMenuData
51 *
52 * The @ref gui_simple holds a Menu. The Address Book Dialog stores
53 * its data (#AliasMenuData) in Menu::mdata.
54 *
55 * ## Events
56 *
57 * Once constructed, it is controlled by the following events:
58 *
59 * | Event Type | Handler |
60 * | :-------------------- | :------------------------- |
61 * | #NT_ALIAS | alias_alias_observer() |
62 * | #NT_CONFIG | alias_config_observer() |
63 * | #NT_WINDOW | alias_window_observer() |
64 * | MuttWindow::recalc() | alias_recalc() |
65 *
66 * The Address Book Dialog doesn't have any specific colours, so it doesn't
67 * need to support #NT_COLOR.
68 *
69 * MuttWindow::recalc() is handled to support custom sorting.
70 *
71 * Some other events are handled by the @ref gui_simple.
72 */
73
74 #include "config.h"
75 #include <stdbool.h>
76 #include <stdint.h>
77 #include <stdio.h>
78 #include <string.h>
79 #include "mutt/lib.h"
80 #include "address/lib.h"
81 #include "config/lib.h"
82 #include "core/lib.h"
83 #include "gui/lib.h"
84 #include "lib.h"
85 #include "menu/lib.h"
86 #include "pattern/lib.h"
87 #include "question/lib.h"
88 #include "alias.h"
89 #include "format_flags.h"
90 #include "gui.h"
91 #include "muttlib.h"
92 #include "opcodes.h"
93
94 /// Help Bar for the Alias dialog (address book)
95 static const struct Mapping AliasHelp[] = {
96 // clang-format off
97 { N_("Exit"), OP_EXIT },
98 { N_("Del"), OP_DELETE },
99 { N_("Undel"), OP_UNDELETE },
100 { N_("Sort"), OP_SORT },
101 { N_("Rev-Sort"), OP_SORT_REVERSE },
102 { N_("Select"), OP_GENERIC_SELECT_ENTRY },
103 { N_("Help"), OP_HELP },
104 { NULL, 0 },
105 // clang-format on
106 };
107
108 /**
109 * alias_format_str - Format a string for the alias list - Implements ::format_t - @ingroup expando_api
110 *
111 * | Expando | Description
112 * |:--------|:--------------------------------------------------------
113 * | \%a | Alias name
114 * | \%c | Comments
115 * | \%f | Flags - currently, a 'd' for an alias marked for deletion
116 * | \%n | Index number
117 * | \%r | Address which alias expands to
118 * | \%t | Character which indicates if the alias is tagged for inclusion
119 */
alias_format_str(char * buf,size_t buflen,size_t col,int cols,char op,const char * src,const char * prec,const char * if_str,const char * else_str,intptr_t data,MuttFormatFlags flags)120 static const char *alias_format_str(char *buf, size_t buflen, size_t col, int cols,
121 char op, const char *src, const char *prec,
122 const char *if_str, const char *else_str,
123 intptr_t data, MuttFormatFlags flags)
124 {
125 char fmt[128], addr[1024];
126 struct AliasView *av = (struct AliasView *) data;
127 struct Alias *alias = av->alias;
128
129 switch (op)
130 {
131 case 'a':
132 mutt_format_s(buf, buflen, prec, alias->name);
133 break;
134 case 'c':
135 mutt_format_s(buf, buflen, prec, alias->comment);
136 break;
137 case 'f':
138 snprintf(fmt, sizeof(fmt), "%%%ss", prec);
139 snprintf(buf, buflen, fmt, av->is_deleted ? "D" : " ");
140 break;
141 case 'n':
142 snprintf(fmt, sizeof(fmt), "%%%sd", prec);
143 snprintf(buf, buflen, fmt, av->num + 1);
144 break;
145 case 'r':
146 addr[0] = '\0';
147 mutt_addrlist_write(&alias->addr, addr, sizeof(addr), true);
148 mutt_format_s(buf, buflen, prec, addr);
149 break;
150 case 't':
151 buf[0] = av->is_tagged ? '*' : ' ';
152 buf[1] = '\0';
153 break;
154 }
155
156 return src;
157 }
158
159 /**
160 * alias_make_entry - Format a menu item for the alias list - Implements Menu::make_entry() - @ingroup menu_make_entry
161 */
alias_make_entry(struct Menu * menu,char * buf,size_t buflen,int line)162 static void alias_make_entry(struct Menu *menu, char *buf, size_t buflen, int line)
163 {
164 const struct AliasMenuData *mdata = menu->mdata;
165 const struct AliasViewArray *ava = &mdata->ava;
166 const struct AliasView *av = ARRAY_GET(ava, line);
167
168 const char *const alias_format = cs_subset_string(mdata->sub, "alias_format");
169
170 mutt_expando_format(buf, buflen, 0, menu->win->state.cols, NONULL(alias_format),
171 alias_format_str, (intptr_t) av, MUTT_FORMAT_ARROWCURSOR);
172 }
173
174 /**
175 * alias_tag - Tag some aliases - Implements Menu::tag() - @ingroup menu_tag
176 */
alias_tag(struct Menu * menu,int sel,int act)177 static int alias_tag(struct Menu *menu, int sel, int act)
178 {
179 const struct AliasMenuData *mdata = menu->mdata;
180 const struct AliasViewArray *ava = &mdata->ava;
181 struct AliasView *av = ARRAY_GET(ava, sel);
182
183 bool ot = av->is_tagged;
184
185 av->is_tagged = ((act >= 0) ? act : !av->is_tagged);
186
187 return av->is_tagged - ot;
188 }
189
190 /**
191 * alias_alias_observer - Notification that an Alias has changed - Implements ::observer_t - @ingroup observer_api
192 */
alias_alias_observer(struct NotifyCallback * nc)193 static int alias_alias_observer(struct NotifyCallback *nc)
194 {
195 if ((nc->event_type != NT_ALIAS) || !nc->global_data || !nc->event_data)
196 return -1;
197
198 struct EventAlias *ev_a = nc->event_data;
199 struct Menu *menu = nc->global_data;
200 struct AliasMenuData *mdata = menu->mdata;
201 struct Alias *alias = ev_a->alias;
202
203 if (nc->event_subtype == NT_ALIAS_ADD)
204 {
205 alias_array_alias_add(&mdata->ava, alias);
206
207 if (alias_array_count_visible(&mdata->ava) != ARRAY_SIZE(&mdata->ava))
208 {
209 mutt_pattern_alias_func(NULL, mdata, menu);
210 }
211 }
212 else if (nc->event_subtype == NT_ALIAS_DELETE)
213 {
214 alias_array_alias_delete(&mdata->ava, alias);
215
216 int vcount = alias_array_count_visible(&mdata->ava);
217 int index = menu_get_index(menu);
218 if ((index > (vcount - 1)) && (index > 0))
219 menu_set_index(menu, index - 1);
220 }
221
222 alias_array_sort(&mdata->ava, mdata->sub);
223
224 menu->max = alias_array_count_visible(&mdata->ava);
225 menu_queue_redraw(menu, MENU_REDRAW_FULL);
226 mutt_debug(LL_DEBUG5, "alias done, request WA_RECALC, MENU_REDRAW_FULL\n");
227
228 return 0;
229 }
230
231 /**
232 * alias_window_observer - Notification that a Window has changed - Implements ::observer_t - @ingroup observer_api
233 *
234 * This function is triggered by changes to the windows.
235 *
236 * - Delete (this window): clean up the resources held by the Help Bar
237 */
alias_window_observer(struct NotifyCallback * nc)238 int alias_window_observer(struct NotifyCallback *nc)
239 {
240 if ((nc->event_type != NT_WINDOW) || !nc->global_data || !nc->event_data)
241 return -1;
242
243 if (nc->event_subtype != NT_WINDOW_DELETE)
244 return 0;
245
246 struct MuttWindow *win_menu = nc->global_data;
247 struct EventWindow *ev_w = nc->event_data;
248 if (ev_w->win != win_menu)
249 return 0;
250
251 struct Menu *menu = win_menu->wdata;
252
253 notify_observer_remove(NeoMutt->notify, alias_alias_observer, menu);
254 notify_observer_remove(NeoMutt->notify, alias_config_observer, menu);
255 notify_observer_remove(win_menu->notify, alias_window_observer, win_menu);
256
257 mutt_debug(LL_DEBUG5, "window delete done\n");
258 return 0;
259 }
260
261 /**
262 * alias_dialog_new - Create an Alias Selection Dialog
263 * @param mdata Menu data holding Aliases
264 * @retval ptr New Dialog
265 */
alias_dialog_new(struct AliasMenuData * mdata)266 struct MuttWindow *alias_dialog_new(struct AliasMenuData *mdata)
267 {
268 struct MuttWindow *dlg = simple_dialog_new(MENU_ALIAS, WT_DLG_ALIAS, AliasHelp);
269
270 struct Menu *menu = dlg->wdata;
271 menu->make_entry = alias_make_entry;
272 menu->custom_search = true;
273 menu->tag = alias_tag;
274 menu->max = alias_array_count_visible(&mdata->ava);
275 menu->mdata = mdata;
276
277 struct MuttWindow *win_menu = menu->win;
278
279 // Override the Simple Dialog's recalc()
280 win_menu->recalc = alias_recalc;
281
282 struct MuttWindow *sbar = window_find_child(dlg, WT_STATUS_BAR);
283
284 alias_set_title(sbar, _("Aliases"), mdata->str);
285
286 // NT_COLOR is handled by the SimpleDialog
287 notify_observer_add(NeoMutt->notify, NT_ALIAS, alias_alias_observer, menu);
288 notify_observer_add(NeoMutt->notify, NT_CONFIG, alias_config_observer, menu);
289 notify_observer_add(win_menu->notify, NT_WINDOW, alias_window_observer, win_menu);
290
291 return dlg;
292 }
293
294 /**
295 * dlg_select_alias - Display a menu of Aliases
296 * @param buf Buffer for expanded aliases
297 * @param buflen Length of buffer
298 * @param mdata Menu data holding Aliases
299 */
dlg_select_alias(char * buf,size_t buflen,struct AliasMenuData * mdata)300 static void dlg_select_alias(char *buf, size_t buflen, struct AliasMenuData *mdata)
301 {
302 if (ARRAY_EMPTY(&mdata->ava))
303 {
304 mutt_warning(_("You have no aliases"));
305 return;
306 }
307
308 struct MuttWindow *dlg = alias_dialog_new(mdata);
309 struct Menu *menu = dlg->wdata;
310 struct MuttWindow *sbar = window_find_child(dlg, WT_STATUS_BAR);
311
312 int t = -1;
313 bool done = false;
314
315 alias_array_sort(&mdata->ava, mdata->sub);
316
317 struct AliasView *avp = NULL;
318 ARRAY_FOREACH(avp, &mdata->ava)
319 {
320 avp->num = ARRAY_FOREACH_IDX;
321 }
322
323 while (!done)
324 {
325 int op = menu_loop(menu);
326 switch (op)
327 {
328 case OP_DELETE:
329 case OP_UNDELETE:
330 if (menu->tagprefix)
331 {
332 ARRAY_FOREACH(avp, &mdata->ava)
333 {
334 if (avp->is_tagged)
335 avp->is_deleted = (op == OP_DELETE);
336 }
337 menu_queue_redraw(menu, MENU_REDRAW_INDEX);
338 }
339 else
340 {
341 int index = menu_get_index(menu);
342 ARRAY_GET(&mdata->ava, index)->is_deleted = (op == OP_DELETE);
343 menu_queue_redraw(menu, MENU_REDRAW_CURRENT);
344 const bool c_resolve = cs_subset_bool(NeoMutt->sub, "resolve");
345 if (c_resolve && (index < (menu->max - 1)))
346 {
347 menu_set_index(menu, index + 1);
348 menu_queue_redraw(menu, MENU_REDRAW_INDEX);
349 }
350 }
351 break;
352 case OP_SORT:
353 case OP_SORT_REVERSE:
354 {
355 int sort = cs_subset_sort(mdata->sub, "sort_alias");
356 bool resort = true;
357 bool reverse = (op == OP_SORT_REVERSE);
358
359 switch (mutt_multi_choice(
360 reverse ?
361 /* L10N: The highlighted letters must match the "Sort" options */
362 _("Rev-Sort (a)lias, a(d)dress or (u)nsorted?") :
363 /* L10N: The highlighted letters must match the "Rev-Sort" options */
364 _("Sort (a)lias, a(d)dress or (u)nsorted?"),
365 /* L10N: These must match the highlighted letters from "Sort" and "Rev-Sort" */
366 _("adu")))
367 {
368 case -1: /* abort */
369 resort = false;
370 break;
371
372 case 1: /* (a)lias */
373 sort = SORT_ALIAS;
374 break;
375
376 case 2: /* a(d)dress */
377 sort = SORT_ADDRESS;
378 break;
379
380 case 3: /* (u)nsorted */
381 sort = SORT_ORDER;
382 break;
383 }
384
385 if (resort)
386 {
387 sort |= reverse ? SORT_REVERSE : 0;
388
389 // This will trigger a WA_RECALC
390 cs_subset_str_native_set(mdata->sub, "sort_alias", sort, NULL);
391 }
392
393 break;
394 }
395 case OP_SEARCH_REVERSE:
396 case OP_SEARCH_NEXT:
397 case OP_SEARCH_OPPOSITE:
398 case OP_SEARCH:
399 {
400 int index = mutt_search_alias_command(menu, menu_get_index(menu), op);
401 if (index == -1)
402 break;
403
404 menu_set_index(menu, index);
405 break;
406 }
407
408 case OP_MAIN_LIMIT:
409 {
410 int rc = mutt_pattern_alias_func(_("Limit to addresses matching: "), mdata, menu);
411 if (rc == 0)
412 {
413 alias_set_title(sbar, _("Aliases"), mdata->str);
414 menu_queue_redraw(menu, MENU_REDRAW_FULL);
415 window_redraw(NULL);
416 }
417
418 break;
419 }
420
421 case OP_GENERIC_SELECT_ENTRY:
422 t = menu_get_index(menu);
423 if (t >= ARRAY_SIZE(&mdata->ava))
424 t = -1;
425 done = true;
426 break;
427
428 case OP_EXIT:
429 done = true;
430 break;
431 }
432 }
433
434 ARRAY_FOREACH(avp, &mdata->ava)
435 {
436 if (avp->is_tagged)
437 {
438 mutt_addrlist_write(&avp->alias->addr, buf, buflen, true);
439 t = -1;
440 }
441 }
442
443 if (t != -1)
444 {
445 mutt_addrlist_write(&ARRAY_GET(&mdata->ava, t)->alias->addr, buf, buflen, true);
446 }
447
448 simple_dialog_free(&dlg);
449 }
450
451 /**
452 * alias_complete - Alias completion routine
453 * @param buf Partial Alias to complete
454 * @param buflen Length of buffer
455 * @param sub Config items
456 * @retval 1 Success
457 * @retval 0 Error
458 *
459 * Given a partial alias, this routine attempts to fill in the alias
460 * from the alias list as much as possible. if given empty search string
461 * or found nothing, present all aliases
462 */
alias_complete(char * buf,size_t buflen,struct ConfigSubset * sub)463 int alias_complete(char *buf, size_t buflen, struct ConfigSubset *sub)
464 {
465 struct Alias *np = NULL;
466 char bestname[8192] = { 0 };
467
468 struct AliasMenuData mdata = { NULL, ARRAY_HEAD_INITIALIZER, sub };
469 mdata.str = mutt_str_dup(buf);
470
471 if (buf[0] != '\0')
472 {
473 TAILQ_FOREACH(np, &Aliases, entries)
474 {
475 if (np->name && mutt_strn_equal(np->name, buf, strlen(buf)))
476 {
477 if (bestname[0] == '\0') /* init */
478 {
479 mutt_str_copy(bestname, np->name,
480 MIN(mutt_str_len(np->name) + 1, sizeof(bestname)));
481 }
482 else
483 {
484 int i;
485 for (i = 0; np->name[i] && (np->name[i] == bestname[i]); i++)
486 ; // do nothing
487
488 bestname[i] = '\0';
489 }
490 }
491 }
492
493 if (bestname[0] != '\0')
494 {
495 /* fake the pattern for menu title */
496 char *mtitle = NULL;
497 mutt_str_asprintf(&mtitle, "~f ^%s", buf);
498 FREE(&mdata.str);
499 mdata.str = mtitle;
500
501 if (!mutt_str_equal(bestname, buf))
502 {
503 /* we are adding something to the completion */
504 mutt_str_copy(buf, bestname, mutt_str_len(bestname) + 1);
505 FREE(&mdata.str);
506 return 1;
507 }
508
509 /* build alias list and show it */
510 TAILQ_FOREACH(np, &Aliases, entries)
511 {
512 int aasize = alias_array_alias_add(&mdata.ava, np);
513
514 struct AliasView *av = ARRAY_GET(&mdata.ava, aasize - 1);
515
516 if (np->name && !mutt_strn_equal(np->name, buf, strlen(buf)))
517 {
518 av->is_visible = false;
519 }
520 }
521 }
522 }
523
524 if (ARRAY_EMPTY(&mdata.ava))
525 {
526 TAILQ_FOREACH(np, &Aliases, entries)
527 {
528 alias_array_alias_add(&mdata.ava, np);
529 }
530
531 mutt_pattern_alias_func(NULL, &mdata, NULL);
532 }
533
534 alias_array_sort(&mdata.ava, mdata.sub);
535
536 bestname[0] = '\0';
537 dlg_select_alias(bestname, sizeof(bestname), &mdata);
538 if (bestname[0] != '\0')
539 mutt_str_copy(buf, bestname, buflen);
540
541 struct AliasView *avp = NULL;
542 ARRAY_FOREACH(avp, &mdata.ava)
543 {
544 if (!avp->is_deleted)
545 continue;
546
547 TAILQ_REMOVE(&Aliases, avp->alias, entries);
548 alias_free(&avp->alias);
549 }
550
551 ARRAY_FREE(&mdata.ava);
552 FREE(&mdata.str);
553
554 return 0;
555 }
556