Add string-hashing interface
[exim.git] / src / exim_monitor / em_menu.c
1 /*************************************************
2 *                  Exim Monitor                  *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainers 2021 */
7 /* See the file NOTICE for conditions of use and distribution. */
8
9
10 #include "em_hdr.h"
11
12 /* This module contains code for handling the popup menus. */
13
14 static Widget menushell;
15 static Widget queue_text_sink;
16 static Widget dialog_shell, dialog_widget;
17
18 static Widget text_create(uschar *, int);
19
20 static int highlighted_start, highlighted_end, highlighted_x, highlighted_y;
21
22
23
24 static Arg queue_get_arg[] = {
25   { "textSink",   (XtArgVal)NULL },
26   { "textSource", (XtArgVal)NULL },
27   { "string",     (XtArgVal)NULL } };
28
29 static Arg dialog_arg[] = {
30   { "label",      (XtArgVal)"dialog" },
31   { "value",      (XtArgVal)"value" } };
32
33 static Arg get_pos_args[] = {
34   {"x",           (XtArgVal)NULL },
35   {"y",           (XtArgVal)NULL } };
36
37 static Arg menushell_arg[] = {
38   { "label",      (XtArgVal)NULL } };
39
40 static Arg button_arg[] = {
41   { XtNfromVert, (XtArgVal) NULL },         /* must be first */
42   { XtNlabel,    (XtArgVal) " Dismiss " },
43   { "left",      XawChainLeft },
44   { "right",     XawChainLeft },
45   { "top",       XawChainBottom },
46   { "bottom",    XawChainBottom } };
47
48 static Arg text_arg[] = {
49   { XtNfromVert, (XtArgVal) NULL },         /* must be first */
50   { "editType",  XawtextEdit },
51   { "string",    (XtArgVal)"" },            /* dummy to get it going */
52   { "scrollVertical", XawtextScrollAlways },
53   { "wrap",      XawtextWrapWord },
54   { "top",       XawChainTop },
55   { "bottom",    XawChainBottom } };
56
57 static Arg item_1_arg[] = {
58   { XtNfromVert,  (XtArgVal)NULL },         /* must be first */
59   { "label",      (XtArgVal)" Message log" } };
60
61 static Arg item_2_arg[] = {
62   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
63   { "label",      (XtArgVal)" Headers" } };
64
65 static Arg item_3_arg[] = {
66   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
67   { "label",      (XtArgVal)" Body" } };
68
69 static Arg item_4_arg[] = {
70   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
71   { "label",      (XtArgVal)" Deliver message" } };
72
73 static Arg item_5_arg[] = {
74   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
75   { "label",      (XtArgVal)" Freeze message" } };
76
77 static Arg item_6_arg[] = {
78   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
79   { "label",      (XtArgVal)" Thaw message" } };
80
81 static Arg item_7_arg[] = {
82   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
83   { "label",      (XtArgVal)" Give up on msg" } };
84
85 static Arg item_8_arg[] = {
86   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
87   { "label",      (XtArgVal)" Remove message" } };
88
89 static Arg item_9_arg[] = {
90   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
91   { "label",      (XtArgVal)"----------------" } };
92
93 static Arg item_10_arg[] = {
94   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
95   { "label",      (XtArgVal)" Add recipient" } };
96
97 static Arg item_11_arg[] = {
98   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
99   { "label",      (XtArgVal)" Mark delivered" } };
100
101 static Arg item_12_arg[] = {
102   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
103   { "label",      (XtArgVal)" Mark all delivered" } };
104
105 static Arg item_13_arg[] = {
106   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
107   { "label",      (XtArgVal)" Edit sender" } };
108
109 static Arg item_99_arg[] = {
110   { XtNfromVert,  (XtArgVal) NULL },        /* must be first */
111   { "label",      (XtArgVal)" " } };
112
113
114
115 /*************************************************
116 *        Destroy the menu when popped down       *
117 *************************************************/
118
119 static void
120 popdownAction(Widget w, XtPointer client_data, XtPointer call_data)
121 {
122 if (highlighted_x >= 0)
123   XawTextSinkDisplayText(queue_text_sink,
124     highlighted_x, highlighted_y,
125     highlighted_start, highlighted_end, 0);
126 XtDestroyWidget(w);
127 menu_is_up = FALSE;
128 }
129
130
131
132 /*************************************************
133 *          Display the message log               *
134 *************************************************/
135
136 static void
137 msglogAction(Widget w, XtPointer client_data, XtPointer call_data)
138 {
139 Widget text = text_create(US client_data, text_depth);
140 uschar * fname = NULL;
141 FILE * f = NULL;
142
143 /* End up with the split version, so message looks right when non-exist */
144
145 for (int i = 0; i < (spool_is_split ? 2:1); i++)
146   {
147   message_subdir[0] = i != 0 ? (US client_data)[5] : 0;
148   fname = spool_fname(US"msglog", message_subdir, US client_data, US"");
149   if ((f = fopen(CS fname, "r")))
150     break;
151   }
152
153 if (!f)
154   text_showf(text, "%s: %s\n", fname, strerror(errno));
155 else
156   {
157   uschar buffer[256];
158   while (Ufgets(buffer, sizeof(buffer), f) != NULL) text_show(text, buffer);
159   fclose(f);
160   }
161 }
162
163
164
165 /*************************************************
166 *          Display the message body               *
167 *************************************************/
168
169 static void
170 bodyAction(Widget w, XtPointer client_data, XtPointer call_data)
171 {
172 Widget text = text_create(US client_data, text_depth);
173 FILE *f = NULL;
174
175 for (int i = 0; i < (spool_is_split? 2:1); i++)
176   {
177   uschar * fname;
178   message_subdir[0] = i != 0 ? (US client_data)[5] : 0;
179   fname = spool_fname(US"input", message_subdir, US client_data, US"-D");
180   if ((f = fopen(CS fname, "r")))
181     break;
182   }
183
184 if (!f)
185   text_showf(text, "Failed to open file: %s\n", strerror(errno));
186 else
187   {
188   uschar buffer[256];
189   int count = 0;
190
191   while (Ufgets(buffer, sizeof(buffer), f) != NULL)
192     {
193     text_show(text, buffer);
194     count += Ustrlen(buffer);
195     if (count > body_max)
196       {
197       text_show(text, US"\n*** Message length exceeds BODY_MAX ***\n");
198       break;
199       }
200     }
201   fclose(f);
202   }
203 }
204
205
206
207 /*************************************************
208 *        Do something to a message               *
209 *************************************************/
210
211 /* The output is not shown in a window for non-delivery actions that succeed,
212 unless action_output is set. We can't, however, tell until we have run
213 the command whether we want the output or not, so the pipe has to be set up in
214 all cases. */
215
216 static void
217 ActOnMessage(uschar *id, uschar *action, uschar *address_arg)
218 {
219 int pid;
220 int pipe_fd[2];
221 int delivery = Ustrcmp(action + Ustrlen(action) - 2, "-M") == 0;
222 uschar *quote = US"";
223 uschar *at = US"";
224 uschar *qualify = US"";
225 uschar buffer[256];
226 queue_item *qq;
227 Widget text = NULL;
228
229 /* If the address arg is not empty and does not contain @ and there is a
230 qualify domain, qualify it. (But don't qualify '<>'.)*/
231
232 if (address_arg[0] != 0)
233   {
234   quote = US"\'";
235   if (Ustrchr(address_arg, '@') == NULL &&
236       Ustrcmp(address_arg, "<>") != 0 &&
237       qualify_domain != NULL &&
238       qualify_domain[0] != 0)
239     {
240     at = US"@";
241     qualify = qualify_domain;
242     }
243   }
244 sprintf(CS buffer, "%s %s %s %s %s %s%s%s%s%s", exim_path,
245   (alternate_config == NULL)? US"" : US"-C",
246   (alternate_config == NULL)? US"" : alternate_config,
247   action, id, quote, address_arg, at, qualify, quote);
248
249 /* If we know we are going to need the window, create it now. */
250
251 if (action_output || delivery)
252   {
253   text = text_create(id, text_depth);
254   text_showf(text, "%s\n", buffer);
255   }
256
257 /* Create the pipe for output. Remember, on most systems pipe[0] is
258 for reading and pipe[1] is for writing! Solaris, with its two-way
259 pipes is a trap! */
260
261 if (pipe(pipe_fd) != 0)
262   {
263   if (text == NULL)
264     {
265     text = text_create(id, text_depth);
266     text_showf(text, "%s\n", buffer);
267     }
268   text_show(text, US"*** Failed to create pipe ***\n");
269   return;
270   }
271
272 if (  fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK)
273    || fcntl(pipe_fd[1], F_SETFL, O_NONBLOCK))
274   {
275   perror("set nonblocking on pipe");
276   exit(1);
277   }
278
279 /* Delivering a message can take some time, and we want to show the
280 output as it goes along. This requires subprocesses and is coded below. For
281 other commands, we can assume an immediate response, and so need not waste
282 resources with subprocesses. If action_output is FALSE, don't show the
283 output at all. */
284
285 if (!delivery)
286   {
287   int count, rc;
288   int save_stdout = dup(1);
289   int save_stderr = dup(2);
290
291   close(1);
292   close(2);
293
294   dup2(pipe_fd[1], 1);
295   dup2(pipe_fd[1], 2);
296   close(pipe_fd[1]);
297
298   rc = system(CS buffer);
299
300   close(1);
301   close(2);
302
303   if (action_output || rc != 0)
304     {
305     if (text == NULL)
306       {
307       text = text_create(id, text_depth);
308       text_showf(text, "%s\n", buffer);
309       }
310     while ((count = read(pipe_fd[0], buffer, 254)) > 0)
311       {
312       buffer[count] = 0;
313       text_show(text, buffer);
314       }
315     }
316
317   close(pipe_fd[0]);
318
319   dup2(save_stdout, 1);
320   dup2(save_stderr, 2);
321   close(save_stdout);
322   close(save_stderr);
323
324   /* If action was to change the sender, and it succeeded, we have to
325   update the in-store data. */
326
327   if (rc == 0 && Ustrcmp(action + Ustrlen(action) - 4, "-Mes") == 0)
328     {
329     queue_item *q = find_queue(id, queue_noop, 0);
330     if (q)
331       {
332       if (q->sender) store_free(q->sender);
333       q->sender = store_malloc(Ustrlen(address_arg) + 1);
334       Ustrcpy(q->sender, address_arg);
335       }
336     }
337
338   /* If configured, cause a display update and return */
339
340   if (action_queue_update) tick_queue_accumulator = 999999;
341   return;
342   }
343
344 /* Message is to be delivered. Ensure that it is marked unfrozen,
345 because nothing will get written to the log to show that this has
346 happened. (Other freezing/unfreezings get logged and picked up from
347 there.) */
348
349 qq = find_queue(id, queue_noop, 0);
350 if (qq != NULL) qq->frozen = FALSE;
351
352 /* New, asynchronous code runs in a subprocess for commands that
353 will take some time. The main process does not wait. There is a
354 SIGCHLD handler in the main program that cleans up any terminating
355 sub processes. */
356
357 if ((pid = fork()) == 0)
358   {
359   close(1);
360   close(2);
361
362   dup2(pipe_fd[1], 1);
363   dup2(pipe_fd[1], 2);
364   close(pipe_fd[1]);
365
366   system(CS buffer);
367
368   close(1);
369   close(2);
370   close(pipe_fd[0]);
371   _exit(0);
372   }
373
374 /* Main process - set up an item for the main ticker to watch. */
375
376 if (pid < 0) text_showf(text, "Failed to fork: %s\n", strerror(errno)); else
377   {
378   pipe_item *p = (pipe_item *)store_malloc(sizeof(pipe_item));
379
380   if (p == NULL)
381     {
382     text_show(text, US"Run out of store\n");
383     return;
384     }
385
386   p->widget = text;
387   p->fd = pipe_fd[0];
388
389   p->next = pipe_chain;
390   pipe_chain = p;
391
392   close(pipe_fd[1]);
393   }
394 }
395
396
397
398
399 /*************************************************
400 *        Cause a message to be delivered         *
401 *************************************************/
402
403 static void
404 deliverAction(Widget w, XtPointer client_data, XtPointer call_data)
405 {
406 ActOnMessage(US client_data, US"-v -M", US"");
407 }
408
409 /*************************************************
410 *        Cause a message to be Frozen            *
411 *************************************************/
412
413 static void
414 freezeAction(Widget w, XtPointer client_data, XtPointer call_data)
415 {
416 ActOnMessage(US client_data, US"-Mf", US"");
417 }
418
419 /*************************************************
420 *        Cause a message to be thawed            *
421 *************************************************/
422
423 static void
424 thawAction(Widget w, XtPointer client_data, XtPointer call_data)
425 {
426 ActOnMessage(US client_data, US"-Mt", US"");
427 }
428
429 /*************************************************
430 *          Take action using dialog data         *
431 *************************************************/
432
433 /* This function is called after a dialog box has been filled
434 in. It is global because it is set up in the action table at
435 start-up time. If the string is empty, do nothing. */
436
437 XtActionProc
438 dialogAction(Widget w, XEvent *event, String *ss, Cardinal *c)
439 {
440 uschar *s = US XawDialogGetValueString(dialog_widget);
441
442 XtPopdown((Widget)dialog_shell);
443 XtDestroyWidget((Widget)dialog_shell);
444 while (isspace(*s)) s++;
445 if (s[0] != 0)
446   if (actioned_message[0] != 0)
447     ActOnMessage(actioned_message, action_required, s);
448   else
449     NonMessageDialogue(s);    /* When called from somewhere else */
450 return NULL;
451 }
452
453
454
455 /*************************************************
456 *              Create a dialog box               *
457 *************************************************/
458
459 /* The focus is grabbed exclusively, so nothing else can
460 be done to the application until the box is filled in. This
461 function is also used by the Hide button handler. */
462
463 void
464 create_dialog(uschar *label, uschar *value)
465 {
466 Arg warg[4];
467 Dimension x, y, xx, yy;
468 XtTranslations pop_trans;
469 Widget text;
470
471 /* Get the position of a reference widget so the dialog box can be put
472 near to it. */
473
474 get_pos_args[0].value = (XtArgVal)(&x);
475 get_pos_args[1].value = (XtArgVal)(&y);
476 XtGetValues(dialog_ref_widget, get_pos_args, 2);
477
478 /* When this is not a message_specific thing, the position of the reference
479 widget is relative to the window. Get the position of the top level widget and
480 add to the position. */
481
482 if (dialog_ref_widget != menushell)
483   {
484   get_pos_args[0].value = (XtArgVal)(&xx);
485   get_pos_args[1].value = (XtArgVal)(&yy);
486   XtGetValues(toplevel_widget, get_pos_args, 2);
487   x += xx;
488   y += yy;
489   }
490
491 /* Create a transient shell for the dialog box. */
492
493 XtSetArg(warg[0], XtNtransientFor, queue_widget);
494 XtSetArg(warg[1], XtNx, x + 50);
495 XtSetArg(warg[2], XtNy, y + 50);
496 XtSetArg(warg[3], XtNallowShellResize, True);
497 dialog_shell = XtCreatePopupShell("forDialog", transientShellWidgetClass,
498    toplevel_widget, warg, 4);
499
500 /* Create the dialog box. */
501
502 dialog_arg[0].value = (XtArgVal)label;
503 dialog_arg[1].value = (XtArgVal)value;
504 dialog_widget = XtCreateManagedWidget("dialog", dialogWidgetClass, dialog_shell,
505   dialog_arg, XtNumber(dialog_arg));
506
507 /* Get the text widget from within the dialog box, give it the keyboard focus,
508 make it wider than the default, and override its translations to make Return
509 call the dialog action function. */
510
511 text = XtNameToWidget(dialog_widget, "value");
512 XawTextSetInsertionPoint(text, Ustrlen(value));
513 XtSetKeyboardFocus(dialog_widget, text);
514 xs_SetValues(text, 1, "width", 200);
515 pop_trans = XtParseTranslationTable(
516   "<Key>Return:         dialogAction()\n");
517 XtOverrideTranslations(text, pop_trans);
518
519 /* Pop the thing up. */
520
521 XtPopup(dialog_shell, XtGrabExclusive);
522 XFlush(X_display);
523 }
524
525
526 /*************************************************
527 *        Cause a recipient to be added           *
528 *************************************************/
529
530 /* This just sets up the dialog box; the action happens when it has been filled
531 in. */
532
533 static void
534 addrecipAction(Widget w, XtPointer client_data, XtPointer call_data)
535 {
536 Ustrncpy(actioned_message, client_data, 24);
537 actioned_message[23] = '\0';
538 action_required = US"-Mar";
539 dialog_ref_widget = menushell;
540 create_dialog(US"Recipient address to add?", US"");
541 }
542
543 /*************************************************
544 *    Cause an address to be marked delivered     *
545 *************************************************/
546
547 static void
548 markdelAction(Widget w, XtPointer client_data, XtPointer call_data)
549 {
550 Ustrncpy(actioned_message, client_data, 24);
551 actioned_message[23] = '\0';
552 action_required = US"-Mmd";
553 dialog_ref_widget = menushell;
554 create_dialog(US"Recipient address to mark delivered?", US"");
555 }
556
557 /*************************************************
558 *   Cause all addresses to be marked delivered   *
559 *************************************************/
560
561 static void
562 markalldelAction(Widget w, XtPointer client_data, XtPointer call_data)
563 {
564 ActOnMessage(US client_data, US"-Mmad", US"");
565 }
566
567 /*************************************************
568 *        Edit the message's sender               *
569 *************************************************/
570
571 static void
572 editsenderAction(Widget w, XtPointer client_data, XtPointer call_data)
573 {
574 queue_item *q;
575 uschar *sender;
576
577 Ustrncpy(actioned_message, client_data, 24);
578 actioned_message[23] = '\0';
579 q = find_queue(actioned_message, queue_noop, 0);
580 sender = !q ? US"" : q->sender[0] == 0 ? US"<>" : q->sender;
581 action_required = US"-Mes";
582 dialog_ref_widget = menushell;
583 create_dialog(US"New sender address?", sender);
584 }
585
586 /*************************************************
587 *    Cause a message to be returned to sender    *
588 *************************************************/
589
590 static void
591 giveupAction(Widget w, XtPointer client_data, XtPointer call_data)
592 {
593 ActOnMessage(US client_data, US"-v -Mg", US"");
594 }
595
596 /*************************************************
597 *      Cause a message to be cancelled           *
598 *************************************************/
599
600 static void
601 removeAction(Widget w, XtPointer client_data, XtPointer call_data)
602 {
603 ActOnMessage(US client_data, US"-Mrm", US"");
604 }
605
606 /*************************************************
607 *             Display a message's headers        *
608 *************************************************/
609
610 static void
611 headersAction(Widget w, XtPointer client_data, XtPointer call_data)
612 {
613 uschar buffer[256];
614 Widget text = text_create(US client_data, text_depth);
615 rmark reset_point;
616
617 /* Remember the point in the dynamic store so we can recover to it afterwards.
618 Then use Exim's function to read the header. */
619
620 reset_point = store_mark();
621
622 sprintf(CS buffer, "%s-H", US client_data);
623 if (spool_read_header(buffer, TRUE, FALSE) != spool_read_OK)
624   {
625   if (errno == ERRNO_SPOOLFORMAT)
626     {
627     struct stat statbuf;
628     sprintf(CS big_buffer, "%s/input/%s", spool_directory, buffer);
629     if (Ustat(big_buffer, &statbuf) == 0)
630       text_showf(text, "Format error in spool file %s: size=%lu\n", buffer,
631         (unsigned long)statbuf.st_size);
632     else text_showf(text, "Format error in spool file %s\n", buffer);
633     }
634   else text_showf(text, "Read error for spool file %s\n", buffer);
635   store_reset(reset_point);
636   return;
637   }
638
639 if (sender_address)
640   text_showf(text, "%s sender: <%s>\n", f.sender_local ? "Local" : "Remote",
641     sender_address);
642
643 if (recipients_list)
644   {
645   text_show(text, US"Recipients:\n");
646   for (int i = 0; i < recipients_count; i++)
647     text_showf(text, "  %s %s\n",
648       tree_search(tree_nonrecipients, recipients_list[i].address)
649         ? "*" : " ",
650       recipients_list[i].address);
651   text_show(text, US"\n");
652   }
653
654 for (header_line * next, * h = header_list; h; h = next)
655   {
656   next = h->next;
657   text_showf(text, "%c ", h->type);   /* Don't push h->text through a %s */
658   text_show(text, h->text);           /* expansion as it may be v large */
659   }
660
661 store_reset(reset_point);
662 }
663
664 /*************************************************
665 *              Dismiss a text window             *
666 *************************************************/
667
668 static void
669 dismissAction(Widget w, XtPointer client_data, XtPointer call_data)
670 {
671 XtPopdown((Widget)client_data);
672 XtDestroyWidget((Widget)client_data);
673
674 /* If this is a text widget for a sub-process, clear it out of
675 the chain so that subsequent data doesn't try to use it. We have
676 to search the parents of the saved widget to see if one of them
677 is what we have just destroyed. */
678
679 for (pipe_item * p = pipe_chain; p; p = p->next)
680   for (Widget pp = p->widget; pp; pp = XtParent(pp))
681     if (pp == (Widget)client_data) { p->widget = NULL; return; }
682 }
683
684
685
686 /*************************************************
687 *             Set up popup text window           *
688 *************************************************/
689
690 static Widget
691 text_create(uschar *name, int height)
692 {
693 Widget textshell, form, text, button;
694
695 /* Create a popup shell widget to display as an additional
696 toplevel window. */
697
698 textshell = XtCreatePopupShell("textshell", topLevelShellWidgetClass,
699   toplevel_widget, NULL, 0);
700 xs_SetValues(textshell, 4,
701   "title",     name,
702   "iconName",  name,
703   "minWidth",  100,
704   "minHeight", 100);
705
706 /* Create a form widget, containing the text widget and the
707 dismiss button widget. */
708
709 form = XtCreateManagedWidget("textform", formWidgetClass,
710   textshell, NULL, 0);
711 xs_SetValues(form, 1, "defaultDistance", 8);
712
713 text = XtCreateManagedWidget("texttext", asciiTextWidgetClass,
714   form, text_arg, XtNumber(text_arg));
715 xs_SetValues(text, 4,
716   "editType",        XawtextAppend,
717   "width",           700,
718   "height",          height,
719   "translations",    text_trans);
720 XawTextDisplayCaret(text, TRUE);
721
722 /* Use the same font as for the queue display */
723
724 if (queue_font != NULL)
725   {
726   XFontStruct *f = XLoadQueryFont(X_display, CS queue_font);
727   if (f != NULL) xs_SetValues(text, 1, "font", f);
728   }
729
730 button_arg[0].value = (XtArgVal)text;
731 button = XtCreateManagedWidget("dismiss", commandWidgetClass,
732   form, button_arg, XtNumber(button_arg));
733 XtAddCallback(button, "callback",  dismissAction, (XtPointer)textshell);
734
735 /* Get the toplevel popup displayed, and yield the text widget so
736 that text can be put into it. */
737
738 XtPopup(textshell, XtGrabNone);
739 return text;
740 }
741
742 /*************************************************
743 *            Set up menu in queue window         *
744 *************************************************/
745
746 /* We have added an action table that causes this function to
747 be called, and set up button 2 in the text widgets to call it. */
748
749 void
750 menu_create(Widget w, XEvent *event, String *actargs, Cardinal *count)
751 {
752 int line;
753 int i;
754 uschar *s;
755 XawTextPosition p;
756 Widget src, menu_line, item_1, item_2, item_3, item_4,
757   item_5, item_6, item_7, item_8, item_9, item_10, item_11,
758   item_12, item_13;
759 XtTranslations menu_trans = XtParseTranslationTable(
760   "<EnterWindow>:   highlight()\n\
761    <LeaveWindow>:   unhighlight()\n\
762    <BtnMotion>:     highlight()\n\
763    <BtnUp>:         MenuPopdown()notify()unhighlight()\n\
764   ");
765
766 /* Get the sink and source and the current text pointer */
767
768 queue_get_arg[0].value = (XtArgVal)(&queue_text_sink);
769 queue_get_arg[1].value = (XtArgVal)(&src);
770 queue_get_arg[2].value = (XtArgVal)(&s);
771 XtGetValues(w, queue_get_arg, 3);
772
773 /* Find the line number of the pointer in the window, and the
774 character offset of the top lefthand of the window. */
775
776 line = (event->xbutton).y / XawTextSinkMaxHeight(queue_text_sink, 1);
777 p = XawTextTopPosition(w);
778
779 /* Find the start of the line on which the button was clicked. */
780
781 i = line;
782 while (i-- > 0)
783   {
784   while (s[p] != 0 && s[p++] != '\n');
785   }
786
787 /* Now pointing either at 0 or 1st uschar after \n, or very 1st uschar.
788 If 0, the click was beyond the end of the data; just set up a dummy
789 menu. (Not easy to ignore as several actions are specified for the
790 mouse click and it expects this one to set up a menu.) If on a
791 continuation line, move back to the main line. */
792
793 if (s[p] == 0)
794   {
795   menushell_arg[0].value = (XtArgVal)"No message selected";
796   menushell = XtCreatePopupShell("menu", simpleMenuWidgetClass,
797     queue_widget, menushell_arg, XtNumber(menushell_arg));
798   XtAddCallback(menushell, "popdownCallback", popdownAction, NULL);
799   xs_SetValues(menushell, 2,
800     "cursor",       XCreateFontCursor(X_display, XC_arrow),
801     "translations", menu_trans);
802
803   /* To keep the widgets in XFree86 happy, we have to create at least one menu
804   item, it seems. (Openwindows doesn't mind a menu with no items.) Otherwise
805   there's a complaint about a zero width menu, and a crash. */
806
807   menu_line = XtCreateManagedWidget("line", smeLineObjectClass, menushell,
808     NULL, 0);
809
810   item_99_arg[0].value = (XtArgVal)menu_line;
811   (void)XtCreateManagedWidget("item99", smeBSBObjectClass, menushell,
812     item_99_arg, XtNumber(item_99_arg));
813
814   highlighted_x = -1;
815   return;
816   }
817
818 while (p > 0 && s[p+11] == ' ')
819   {
820   line--;
821   p--;
822   while (p > 0 && s[p-1] != '\n') p--;
823   }
824
825 /* Now pointing at first character of a main line. */
826
827 Ustrncpy(message_id, s+p+11, MESSAGE_ID_LENGTH);
828 message_id[MESSAGE_ID_LENGTH] = 0;
829
830 /* Highlight the line being menued, and save its parameters so that it
831 can be de-highlighted at popdown. */
832
833 highlighted_start = highlighted_end = p;
834 while (s[highlighted_end] != '\n') highlighted_end++;
835 highlighted_x = 17;
836 highlighted_y = line * XawTextSinkMaxHeight(queue_text_sink, 1) + 2;
837
838 XawTextSinkDisplayText(queue_text_sink,
839   highlighted_x, highlighted_y,
840   highlighted_start, highlighted_end, 1);
841
842 /* Create the popup shell and the other widgets that comprise the menu.
843 Set the translations and pointer shape, and add the callback pointers. */
844
845 menushell_arg[0].value = (XtArgVal)message_id;
846 menushell = XtCreatePopupShell("menu", simpleMenuWidgetClass,
847   queue_widget, menushell_arg, XtNumber(menushell_arg));
848 XtAddCallback(menushell, "popdownCallback", popdownAction, NULL);
849
850 xs_SetValues(menushell, 2,
851   "cursor",       XCreateFontCursor(X_display, XC_arrow),
852   "translations", menu_trans);
853
854 menu_line = XtCreateManagedWidget("line", smeLineObjectClass, menushell,
855   NULL, 0);
856
857 item_1_arg[0].value = (XtArgVal)menu_line;
858 item_1 = XtCreateManagedWidget("item1", smeBSBObjectClass, menushell,
859   item_1_arg, XtNumber(item_1_arg));
860 XtAddCallback(item_1, "callback",  msglogAction, (XtPointer)message_id);
861
862 item_2_arg[0].value = (XtArgVal)item_1;
863 item_2 = XtCreateManagedWidget("item2", smeBSBObjectClass, menushell,
864   item_2_arg, XtNumber(item_2_arg));
865 XtAddCallback(item_2, "callback",  headersAction, (XtPointer)message_id);
866
867 item_3_arg[0].value = (XtArgVal)item_2;
868 item_3 = XtCreateManagedWidget("item3", smeBSBObjectClass, menushell,
869   item_3_arg, XtNumber(item_3_arg));
870 XtAddCallback(item_3, "callback",  bodyAction, (XtPointer)message_id);
871
872 item_4_arg[0].value = (XtArgVal)item_3;
873 item_4 = XtCreateManagedWidget("item4", smeBSBObjectClass, menushell,
874   item_4_arg, XtNumber(item_4_arg));
875 XtAddCallback(item_4, "callback",  deliverAction, (XtPointer)message_id);
876
877 item_5_arg[0].value = (XtArgVal)item_4;
878 item_5 = XtCreateManagedWidget("item5", smeBSBObjectClass, menushell,
879   item_5_arg, XtNumber(item_5_arg));
880 XtAddCallback(item_5, "callback",  freezeAction, (XtPointer)message_id);
881
882 item_6_arg[0].value = (XtArgVal)item_5;
883 item_6 = XtCreateManagedWidget("item6", smeBSBObjectClass, menushell,
884   item_6_arg, XtNumber(item_6_arg));
885 XtAddCallback(item_6, "callback",  thawAction, (XtPointer)message_id);
886
887 item_7_arg[0].value = (XtArgVal)item_6;
888 item_7 = XtCreateManagedWidget("item7", smeBSBObjectClass, menushell,
889   item_7_arg, XtNumber(item_7_arg));
890 XtAddCallback(item_7, "callback",  giveupAction, (XtPointer)message_id);
891
892 item_8_arg[0].value = (XtArgVal)item_7;
893 item_8 = XtCreateManagedWidget("item8", smeBSBObjectClass, menushell,
894   item_8_arg, XtNumber(item_8_arg));
895 XtAddCallback(item_8, "callback",  removeAction, (XtPointer)message_id);
896
897 item_9_arg[0].value = (XtArgVal)item_8;
898 item_9 = XtCreateManagedWidget("item9", smeBSBObjectClass, menushell,
899   item_9_arg, XtNumber(item_9_arg));
900
901 item_10_arg[0].value = (XtArgVal)item_9;
902 item_10 = XtCreateManagedWidget("item10", smeBSBObjectClass, menushell,
903   item_10_arg, XtNumber(item_10_arg));
904 XtAddCallback(item_10, "callback",  addrecipAction, (XtPointer)message_id);
905
906 item_11_arg[0].value = (XtArgVal)item_10;
907 item_11 = XtCreateManagedWidget("item11", smeBSBObjectClass, menushell,
908   item_11_arg, XtNumber(item_11_arg));
909 XtAddCallback(item_11, "callback",  markdelAction, (XtPointer)message_id);
910
911 item_12_arg[0].value = (XtArgVal)item_11;
912 item_12 = XtCreateManagedWidget("item12", smeBSBObjectClass, menushell,
913   item_12_arg, XtNumber(item_12_arg));
914 XtAddCallback(item_12, "callback",  markalldelAction, (XtPointer)message_id);
915
916 item_13_arg[0].value = (XtArgVal)item_12;
917 item_13 = XtCreateManagedWidget("item13", smeBSBObjectClass, menushell,
918   item_13_arg, XtNumber(item_13_arg));
919 XtAddCallback(item_13, "callback",  editsenderAction, (XtPointer)message_id);
920
921 /* Arrange that the menu pops up with the first item selected. */
922
923 xs_SetValues(menushell, 1, "popupOnEntry", item_1);
924
925 /* Flag that the menu is up to suppress queue updates. */
926
927 menu_is_up = TRUE;
928 }
929
930 /* End of em_menu.c */