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