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