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