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