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