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