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