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