1 /*************************************************
2 *                 Exim Monitor                   *
3 *************************************************/
4 
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainers 2020 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 
9 
10 #include "em_hdr.h"
11 
12 
13 /* This module contains functions to do with scanning exim's
14 queue and displaying the data therefrom. */
15 
16 
17 /* If we are anonymizing for screen shots, define a function to anonymize
18 addresses. Otherwise, define a macro that does nothing. */
19 
20 #ifdef ANONYMIZE
anon(uschar * s)21 static uschar *anon(uschar *s)
22 {
23 static uschar anon_result[256];
24 uschar *ss = anon_result;
25 for (; *s != 0; s++) *ss++ = (*s == '@' || *s == '.')? *s : 'x';
26 *ss = 0;
27 return anon_result;
28 }
29 #else
30 #define anon(x) x
31 #endif
32 
33 
34 /*************************************************
35 *                 Static variables               *
36 *************************************************/
37 
38 static int queue_total = 0;   /* number of items in queue */
39 
40 /* Table for turning base-62 numbers into binary */
41 
42 static uschar tab62[] =
43           {0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,     /* 0-9 */
44            0,10,11,12,13,14,15,16,17,18,19,20,  /* A-K */
45           21,22,23,24,25,26,27,28,29,30,31,32,  /* L-W */
46           33,34,35, 0, 0, 0, 0, 0,              /* X-Z */
47            0,36,37,38,39,40,41,42,43,44,45,46,  /* a-k */
48           47,48,49,50,51,52,53,54,55,56,57,58,  /* l-w */
49           59,60,61};                            /* x-z */
50 
51 /* Index for quickly finding things in the ordered queue. */
52 
53 static queue_item *queue_index[queue_index_size];
54 
55 
56 
57 /*************************************************
58 *         Find/Create/Delete a destination       *
59 *************************************************/
60 
61 /* If the action is dest_noop, then just return item or NULL;
62 if it is dest_add, then add if not present, and return item;
63 if it is dest_remove, remove if present and return NULL. The
64 address is lowercased to start with, unless it begins with
65 "*", which it does for error messages. */
66 
67 dest_item *
find_dest(queue_item * q,uschar * name,int action,BOOL caseless)68 find_dest(queue_item *q, uschar *name, int action, BOOL caseless)
69 {
70 dest_item *dd;
71 dest_item **d = &(q->destinations);
72 
73 while (*d != NULL)
74   {
75   if ((caseless? strcmpic(name,(*d)->address) : Ustrcmp(name,(*d)->address))
76         == 0)
77     {
78     dest_item *ddd;
79 
80     if (action != dest_remove) return *d;
81     dd = *d;
82     *d = dd->next;
83     store_free(dd);
84 
85     /* Unset any parent pointers that were to this address */
86 
87     for (ddd = q->destinations; ddd != NULL; ddd = ddd->next)
88       {
89       if (ddd->parent == dd) ddd->parent = NULL;
90       }
91 
92     return NULL;
93     }
94   d = &((*d)->next);
95   }
96 
97 if (action != dest_add) return NULL;
98 
99 dd = (dest_item *)store_malloc(sizeof(dest_item) + Ustrlen(name));
100 Ustrcpy(dd->address, name);
101 dd->next = NULL;
102 dd->parent = NULL;
103 *d = dd;
104 return dd;
105 }
106 
107 
108 
109 /*************************************************
110 *            Clean up a dead queue item          *
111 *************************************************/
112 
113 static void
clean_up(queue_item * p)114 clean_up(queue_item *p)
115 {
116 dest_item *dd = p->destinations;
117 while (dd != NULL)
118   {
119   dest_item *next = dd->next;
120   store_free(dd);
121   dd = next;
122   }
123 if (p->sender != NULL) store_free(p->sender);
124 store_free(p);
125 }
126 
127 
128 /*************************************************
129 *         Set up an ACL variable                 *
130 *************************************************/
131 
132 /* The spool_read_header() function calls acl_var_create() when it reads in an
133 ACL variable. We know that in this case, the variable will be new, not re-used,
134 so this is a cut-down version, to save including the whole acl.c module (which
135 would need conditional compilation to cut most of it out). */
136 
137 tree_node *
acl_var_create(uschar * name)138 acl_var_create(uschar *name)
139 {
140 tree_node *node, **root;
141 root = (name[0] == 'c')? &acl_var_c : &acl_var_m;
142 node = store_get(sizeof(tree_node) + Ustrlen(name), FALSE);
143 Ustrcpy(node->name, name);
144 node->data.ptr = NULL;
145 (void)tree_insertnode(root, node);
146 return node;
147 }
148 
149 
150 
151 /*************************************************
152 *             Set up new queue item              *
153 *************************************************/
154 
155 static queue_item *
set_up(uschar * name,int dir_char)156 set_up(uschar *name, int dir_char)
157 {
158 int i, rc, save_errno;
159 struct stat statdata;
160 rmark reset_point;
161 uschar *p;
162 queue_item *q = (queue_item *)store_malloc(sizeof(queue_item));
163 uschar buffer[256];
164 
165 /* Initialize the block */
166 
167 q->next = q->prev = NULL;
168 q->destinations = NULL;
169 Ustrncpy(q->name, name, sizeof(q->name));
170 q->seen = TRUE;
171 q->frozen = FALSE;
172 q->dir_char = dir_char;
173 q->sender = NULL;
174 q->size = 0;
175 
176 /* Read the header file from the spool; if there is a failure it might mean
177 inaccessibility as a result of protections. A successful read will have caused
178 sender_address to get set and the recipients fields to be initialized. If
179 there's a format error in the headers, we can still display info from the
180 envelope.
181 
182 Before reading the header remember the position in the dynamic store so that
183 we can recover the store into which the header is read. All data read by
184 spool_read_header that is to be preserved is copied into malloc store. */
185 
186 reset_point = store_mark();
187 message_size = 0;
188 message_subdir[0] = dir_char;
189 sprintf(CS buffer, "%s-H", name);
190 rc =  spool_read_header(buffer, FALSE, TRUE);
191 save_errno = errno;
192 
193 /* If we failed to read the envelope, compute the input time by
194 interpreting the id as a base-62 number. */
195 
196 if (rc != spool_read_OK && rc != spool_read_hdrerror)
197   {
198   int t = 0;
199   for (i = 0; i < 6; i++) t = t * 62 + tab62[name[i] - '0'];
200   q->update_time = q->input_time = t;
201   }
202 
203 /* Envelope read; get input time and remove qualify_domain from sender address,
204 if it's there. */
205 
206 else
207   {
208   q->update_time = q->input_time = received_time.tv_sec;
209   if ((p = strstric(sender_address+1, qualify_domain, FALSE)) != NULL &&
210     *(--p) == '@') *p = 0;
211   }
212 
213 /* If we didn't read the whole header successfully, generate an error
214 message. If the envelope was read, this appears as a first recipient;
215 otherwise it sets set up in the sender field. */
216 
217 if (rc != spool_read_OK)
218   {
219   uschar *msg;
220 
221   if (save_errno == ERRNO_SPOOLFORMAT)
222     {
223     struct stat statbuf;
224     sprintf(CS big_buffer, "%s/input/%s", spool_directory, buffer);
225     if (Ustat(big_buffer, &statbuf) == 0)
226       msg = string_sprintf("*** Format error in spool file: size = " OFF_T_FMT " ***",
227         statbuf.st_size);
228     else msg = US"*** Format error in spool file ***";
229     }
230   else msg = US"*** Cannot read spool file ***";
231 
232   if (rc == spool_read_hdrerror)
233     {
234     (void)find_dest(q, msg, dest_add, FALSE);
235     }
236   else
237     {
238     f.deliver_freeze = FALSE;
239     sender_address = msg;
240     recipients_count = 0;
241     }
242   }
243 
244 /* Now set up the remaining data. */
245 
246 q->frozen = f.deliver_freeze;
247 
248 if (f.sender_set_untrusted)
249   {
250   if (sender_address[0] == 0)
251     {
252     q->sender = store_malloc(Ustrlen(originator_login) + 6);
253     sprintf(CS q->sender, "<> (%s)", originator_login);
254     }
255   else
256     {
257     q->sender = store_malloc(Ustrlen(sender_address) +
258       Ustrlen(originator_login) + 4);
259     sprintf(CS q->sender, "%s (%s)", sender_address, originator_login);
260     }
261   }
262 else
263   {
264   q->sender = store_malloc(Ustrlen(sender_address) + 1);
265   Ustrcpy(q->sender, sender_address);
266   }
267 
268 sender_address = NULL;
269 
270 snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-D",
271   spool_directory, queue_name, message_subdir, name);
272 if (Ustat(buffer, &statdata) == 0)
273   q->size = message_size + statdata.st_size - SPOOL_DATA_START_OFFSET + 1;
274 
275 /* Scan and process the recipients list, skipping any that have already
276 been delivered, and removing visible names. */
277 
278 if (recipients_list != NULL)
279   for (i = 0; i < recipients_count; i++)
280     {
281     uschar *r = recipients_list[i].address;
282     if (tree_search(tree_nonrecipients, r) == NULL)
283       {
284       if ((p = strstric(r+1, qualify_domain, FALSE)) != NULL &&
285         *(--p) == '@') *p = 0;
286       (void)find_dest(q, r, dest_add, FALSE);
287       }
288     }
289 
290 /* Recover the dynamic store used by spool_read_header(). */
291 
292 store_reset(reset_point);
293 return q;
294 }
295 
296 
297 
298 /*************************************************
299 *             Find/Create a queue item           *
300 *************************************************/
301 
302 /* The queue is kept as a doubly-linked list, sorted by name. However,
303 to speed up searches, an index into the list is used. This is maintained
304 by the scan_spool_input function when it goes down the list throwing
305 out entries that are no longer needed. When the action is "add" and
306 we don't need to add, mark the found item as seen. */
307 
308 
309 #ifdef never
debug_queue(void)310 static void debug_queue(void)
311 {
312 int i;
313 int count = 0;
314 queue_item *p;
315 printf("\nqueue_total=%d\n", queue_total);
316 
317 for (i = 0; i < queue_index_size; i++)
318   printf("index %d = %d %s\n", i, (int)(queue_index[i]),
319     (queue_index[i])->name);
320 
321 printf("Queue is:\n");
322 p = queue_index[0];
323 while (p != NULL)
324   {
325   count++;
326   for (i = 0; i < queue_index_size; i++)
327     {
328     if (queue_index[i] == p) printf("count=%d index=%d\n", count, (int)p);
329     }
330   printf("%d %d %d %s\n", (int)p, (int)p->next, (int)p->prev, p->name);
331   p = p->next;
332   }
333 }
334 #endif
335 
336 
337 
338 queue_item *
find_queue(uschar * name,int action,int dir_char)339 find_queue(uschar *name, int action, int dir_char)
340 {
341 int first = 0;
342 int last = queue_index_size - 1;
343 int middle = (first + last)/2;
344 queue_item *p, *q, *qq;
345 
346 /* Handle the empty queue as a special case. */
347 
348 if (queue_total == 0)
349   {
350   if (action != queue_add) return NULL;
351   if ((qq = set_up(name, dir_char)) != NULL)
352     {
353     int i;
354     for (i = 0; i < queue_index_size; i++) queue_index[i] = qq;
355     queue_total++;
356     return qq;
357     }
358   return NULL;
359   }
360 
361 /* Also handle insertion at the start or end of the queue
362 as special cases. */
363 
364 if (Ustrcmp(name, (queue_index[0])->name) < 0)
365   {
366   if (action != queue_add) return NULL;
367   if ((qq = set_up(name, dir_char)) != NULL)
368     {
369     qq->next = queue_index[0];
370     (queue_index[0])->prev = qq;
371     queue_index[0] = qq;
372     queue_total++;
373     return qq;
374     }
375   return NULL;
376   }
377 
378 if (Ustrcmp(name, (queue_index[queue_index_size-1])->name) > 0)
379   {
380   if (action != queue_add) return NULL;
381   if ((qq = set_up(name, dir_char)) != NULL)
382     {
383     qq->prev = queue_index[queue_index_size-1];
384     (queue_index[queue_index_size-1])->next = qq;
385     queue_index[queue_index_size-1] = qq;
386     queue_total++;
387     return qq;
388     }
389   return NULL;
390   }
391 
392 /* Use binary chopping on the index to get a range of the queue to search
393 when the name is somewhere in the middle, if present. */
394 
395 while (middle > first)
396   {
397   if (Ustrcmp(name, (queue_index[middle])->name) >= 0) first = middle;
398     else last = middle;
399   middle = (first + last)/2;
400   }
401 
402 /* Now search down the part of the queue in which the item must
403 lie if it exists. Both end points are inclusive - though in fact
404 the bottom one can only be = if it is the original bottom. */
405 
406 p = queue_index[first];
407 q = queue_index[last];
408 
409 for (;;)
410   {
411   int c = Ustrcmp(name, p->name);
412 
413   /* Already on queue; mark seen if required. */
414 
415   if (c == 0)
416     {
417     if (action == queue_add) p->seen = TRUE;
418     return p;
419     }
420 
421   /* Not on the queue; add an entry if required. Note that set-up might
422   fail (the file might vanish under our feet). Note also that we know
423   there is always a previous item to p because the end points are
424   inclusive. */
425 
426   else if (c < 0)
427     {
428     if (action == queue_add)
429       {
430       if ((qq = set_up(name, dir_char)) != NULL)
431         {
432         qq->next = p;
433         qq->prev = p->prev;
434         p->prev->next = qq;
435         p->prev = qq;
436         queue_total++;
437         return qq;
438         }
439       }
440     return NULL;
441     }
442 
443   /* Control should not reach here if p == q, because the name
444   is supposed to be <= the name of the bottom item. */
445 
446   if (p == q) return NULL;
447 
448   /* Else might be further down the queue; continue */
449 
450   p = p->next;
451   }
452 
453 /* Control should never reach here. */
454 }
455 
456 
457 
458 /*************************************************
459 *        Scan the exim spool directory           *
460 *************************************************/
461 
462 /* If we discover that there are subdirectories, set a flag so that the menu
463 code knows to look for them. We count the entries to set the value for the
464 queue stripchart, and set up data for the queue display window if the "full"
465 option is given. */
466 
467 void
scan_spool_input(int full)468 scan_spool_input(int full)
469 {
470 int i;
471 int subptr;
472 int subdir_max = 1;
473 int count = 0;
474 int indexptr = 1;
475 queue_item *p;
476 uschar input_dir[256];
477 uschar subdirs[64];
478 
479 subdirs[0] = 0;
480 stripchart_total[0] = 0;
481 
482 sprintf(CS input_dir, "%s/input", spool_directory);
483 subptr = Ustrlen(input_dir);
484 input_dir[subptr+2] = 0;               /* terminator for lengthened name */
485 
486 /* Loop for each spool file on the queue - searching any subdirectories that
487 may exist. When initializing eximon, every file will have to be read. To show
488 there is progress, output a dot for each one to the standard output. */
489 
490 for (i = 0; i < subdir_max; i++)
491   {
492   int subdirchar = subdirs[i];      /* 0 for main directory */
493   DIR *dd;
494   struct dirent *ent;
495 
496   if (subdirchar != 0)
497     {
498     input_dir[subptr] = '/';
499     input_dir[subptr+1] = subdirchar;
500     }
501 
502   if (!(dd = exim_opendir(input_dir))) continue;
503 
504   while ((ent = readdir(dd)))
505     {
506     uschar *name = US ent->d_name;
507     int len = Ustrlen(name);
508 
509     /* If we find a single alphameric sub-directory on the first
510     pass, add it to the list for subsequent scans, and remember that
511     we are dealing with a split directory. */
512 
513     if (i == 0 && len == 1 && isalnum(*name))
514       {
515       subdirs[subdir_max++] = *name;
516       spool_is_split = TRUE;
517       continue;
518       }
519 
520     /* Otherwise, if it is a header spool file, add it to the list */
521 
522     if (len == SPOOL_NAME_LENGTH &&
523         name[SPOOL_NAME_LENGTH - 2] == '-' &&
524         name[SPOOL_NAME_LENGTH - 1] == 'H')
525       {
526       uschar basename[SPOOL_NAME_LENGTH + 1];
527       stripchart_total[0]++;
528       if (!eximon_initialized) { printf("."); fflush(stdout); }
529       Ustrcpy(basename, name);
530       basename[SPOOL_NAME_LENGTH - 2] = 0;
531       if (full) find_queue(basename, queue_add, subdirchar);
532       }
533     }
534   closedir(dd);
535   }
536 
537 /* If simply counting the number, we are done; same if there are no
538 items in the in-store queue. */
539 
540 if (!full || queue_total == 0) return;
541 
542 /* Now scan the queue and remove any items that were not in the directory. At
543 the same time, set up the index pointers into the queue. Because we are
544 removing items, the total that we are comparing against isn't actually correct,
545 but in a long queue it won't make much difference, and in a short queue it
546 doesn't matter anyway!*/
547 
548 for (p = queue_index[0]; p; )
549   if (!p->seen)
550     {
551     queue_item * next = p->next;
552     if (p->prev)
553       p->prev->next = next;
554     else
555       queue_index[0] = next;
556     if (next)
557       next->prev = p->prev;
558     else
559       {
560       int i;
561       queue_item * q = queue_index[queue_index_size-1];
562       for (i = queue_index_size - 1; i >= 0; i--)
563         if (queue_index[i] == q) queue_index[i] = p->prev;
564       }
565     clean_up(p);
566     queue_total--;
567     p = next;
568     }
569   else
570     {
571     if (++count > (queue_total * indexptr)/(queue_index_size-1))
572       queue_index[indexptr++] = p;
573     p->seen = FALSE;  /* for next time */
574     p = p->next;
575     }
576 
577 /* If a lot of messages have been removed at the bottom, we may not
578 have got the index all filled in yet. Make sure all the pointers
579 are legal. */
580 
581 while (indexptr < queue_index_size - 1)
582   queue_index[indexptr++] = queue_index[queue_index_size-1];
583 }
584 
585 
586 
587 
588 /*************************************************
589 *    Update the recipients list for a message    *
590 *************************************************/
591 
592 /* We read the spool file only if its update time differs from last time,
593 or if there is a journal file in existence. */
594 
595 /* First, a local subroutine to scan the non-recipients tree and
596 remove any of them from the address list */
597 
598 static void
scan_tree(queue_item * p,tree_node * tn)599 scan_tree(queue_item *p, tree_node *tn)
600 {
601 if (tn != NULL)
602   {
603   if (tn->left != NULL) scan_tree(p, tn->left);
604   if (tn->right != NULL) scan_tree(p, tn->right);
605   (void)find_dest(p, tn->name, dest_remove, FALSE);
606   }
607 }
608 
609 /* The main function */
610 
update_recipients(queue_item * p)611 static void update_recipients(queue_item *p)
612 {
613 int i;
614 FILE *jread;
615 rmark reset_point;
616 struct stat statdata;
617 uschar buffer[1024];
618 
619 message_subdir[0] = p->dir_char;
620 
621 snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-J",
622   spool_directory, queue_name, message_subdir, p->name);
623 
624 if (!(jread = fopen(CS buffer, "r")))
625   {
626   snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-H",
627     spool_directory, queue_name, message_subdir, p->name);
628   if (Ustat(buffer, &statdata) < 0 || p->update_time == statdata.st_mtime)
629     return;
630   }
631 
632 /* Get the contents of the header file; if any problem, just give up.
633 Arrange to recover the dynamic store afterwards. */
634 
635 reset_point = store_mark();
636 sprintf(CS buffer, "%s-H", p->name);
637 if (spool_read_header(buffer, FALSE, TRUE) != spool_read_OK)
638   {
639   store_reset(reset_point);
640   if (jread != NULL) fclose(jread);
641   return;
642   }
643 
644 /* If there's a journal file, add its contents to the non-recipients tree */
645 
646 if (jread != NULL)
647   {
648   while (Ufgets(big_buffer, big_buffer_size, jread) != NULL)
649     {
650     int n = Ustrlen(big_buffer);
651     big_buffer[n-1] = 0;
652     tree_add_nonrecipient(big_buffer);
653     }
654   fclose(jread);
655   }
656 
657 /* Scan and process the recipients list, removing any that have already
658 been delivered, and removing visible names. In the nonrecipients tree,
659 domains are lower cased. */
660 
661 if (recipients_list)
662   for (i = 0; i < recipients_count; i++)
663     {
664     uschar * pp;
665     uschar * r = recipients_list[i].address;
666     tree_node * node;
667 
668     if (!(node = tree_search(tree_nonrecipients, r)))
669       node = tree_search(tree_nonrecipients, string_copylc(r));
670 
671     if ((pp = strstric(r+1, qualify_domain, FALSE)) && *(--pp) == '@')
672        *pp = 0;
673     if (!node)
674       (void)find_dest(p, r, dest_add, FALSE);
675     else
676       (void)find_dest(p, r, dest_remove, FALSE);
677     }
678 
679 /* We also need to scan the tree of non-recipients, which might
680 contain child addresses that are not in the recipients list, but
681 which may have got onto the address list as a result of eximon
682 noticing an == line in the log. Then remember the update time,
683 recover the dynamic store, and we are done. */
684 
685 scan_tree(p, tree_nonrecipients);
686 p->update_time = statdata.st_mtime;
687 store_reset(reset_point);
688 }
689 
690 
691 
692 /*************************************************
693 *              Display queue data                *
694 *************************************************/
695 
696 /* The present implementation simple re-writes the entire information each
697 time. Take some care to keep the scrolled position as it previously was, but,
698 if it was at the bottom, keep it at the bottom. Take note of any hide list, and
699 time out the entries as appropriate. */
700 
701 void
queue_display(void)702 queue_display(void)
703 {
704 int now = (int)time(NULL);
705 queue_item *p = queue_index[0];
706 
707 if (menu_is_up) return;            /* Avoid nasty interactions */
708 
709 text_empty(queue_widget);
710 
711 while (p != NULL)
712   {
713   int count = 1;
714   dest_item *dd, *ddd;
715   uschar u = 'm';
716   int t = (now - p->input_time)/60;  /* minutes on queue */
717 
718   if (t > 90)
719     {
720     u = 'h';
721     t = (t + 30)/60;
722     if (t > 72)
723       {
724       u = 'd';
725       t = (t + 12)/24;
726       if (t > 99)                    /* someone had > 99 days */
727         {
728         u = 'w';
729         t = (t + 3)/7;
730         if (t > 99)                  /* so, just in case */
731           {
732           u = 'y';
733           t = (t + 26)/52;
734           }
735         }
736       }
737     }
738 
739   update_recipients(p);                   /* update destinations */
740 
741   /* Can't set this earlier, as header data may change things. */
742 
743   dd = p->destinations;
744 
745   /* Check to see if this message is on the hide list; if any hide
746   item has timed out, remove it from the list. Hide if all destinations
747   are on the hide list. */
748 
749   for (ddd = dd; ddd != NULL; ddd = ddd->next)
750     {
751     skip_item *sk;
752     skip_item **skp;
753     int len_address;
754 
755     if (ddd->address[0] == '*') break;
756     len_address = Ustrlen(ddd->address);
757 
758     for (skp = &queue_skip; ; skp = &(sk->next))
759       {
760       int len_skip;
761 
762       sk = *skp;
763       while (sk != NULL && now >= sk->reveal)
764         {
765         *skp = sk->next;
766         store_free(sk);
767         sk = *skp;
768         if (queue_skip == NULL)
769           {
770           XtDestroyWidget(unhide_widget);
771           unhide_widget = NULL;
772           }
773         }
774       if (sk == NULL) break;
775 
776       /* If this address matches the skip item, break (sk != NULL) */
777 
778       len_skip = Ustrlen(sk->text);
779       if (len_skip <= len_address &&
780           Ustrcmp(ddd->address + len_address - len_skip, sk->text) == 0)
781         break;
782       }
783 
784     if (sk == NULL) break;
785     }
786 
787   /* Don't use more than one call of anon() in one statement - it uses
788   a fixed static buffer. */
789 
790   if (ddd != NULL || dd == NULL)
791     {
792     text_showf(queue_widget, "%c%2d%c %s %s %-8s ",
793       (p->frozen)? '*' : ' ',
794       t, u,
795       string_format_size(p->size, big_buffer),
796       p->name,
797       (p->sender == NULL)? US"       " :
798         (p->sender[0] == 0)? US"<>     " : anon(p->sender));
799 
800     text_showf(queue_widget, "%s%s%s",
801       (dd == NULL || dd->address[0] == '*')? "" : "<",
802       (dd == NULL)? US"" : anon(dd->address),
803       (dd == NULL || dd->address[0] == '*')? "" : ">");
804 
805     if (dd != NULL && dd->parent != NULL && dd->parent->address[0] != '*')
806       text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
807 
808     text_show(queue_widget, US"\n");
809 
810     if (dd != NULL) dd = dd->next;
811     while (dd != NULL && count++ < queue_max_addresses)
812       {
813       text_showf(queue_widget, "                                     <%s>",
814         anon(dd->address));
815       if (dd->parent != NULL && dd->parent->address[0] != '*')
816         text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
817       text_show(queue_widget, US"\n");
818       dd = dd->next;
819       }
820     if (dd != NULL)
821       text_showf(queue_widget, "                                     ...\n");
822     }
823 
824   p = p->next;
825   }
826 }
827 
828 /* End of em_queue.c */
829