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