Update version for 4.68 (overlooked, sigh).
[users/jgh/exim.git] / src / exim_monitor / em_queue.c
1 /* $Cambridge: exim/src/exim_monitor/em_queue.c,v 1.6 2007/01/08 10:50:17 ph10 Exp $ */
2
3 /*************************************************
4 *                 Exim Monitor                   *
5 *************************************************/
6
7 /* Copyright (c) University of Cambridge 1995 - 2007 */
8 /* See the file NOTICE for conditions of use and distribution. */
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 *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 {
115 dest_item *dd = p->destinations;
116 while (dd != NULL)
117   {
118   dest_item *next = dd->next;
119   store_free(dd);
120   dd = next;
121   }
122 if (p->sender != NULL) store_free(p->sender);
123 store_free(p);
124 }
125
126
127 /*************************************************
128 *         Set up an ACL variable                 *
129 *************************************************/
130
131 /* The spool_read_header() function calls acl_var_create() when it reads in an
132 ACL variable. We know that in this case, the variable will be new, not re-used,
133 so this is a cut-down version, to save including the whole acl.c module (which
134 would need conditional compilation to cut most of it out). */
135
136 tree_node *
137 acl_var_create(uschar *name)
138 {
139 tree_node *node, **root;
140 root = (name[0] == 'c')? &acl_var_c : &acl_var_m;
141 node = store_get(sizeof(tree_node) + Ustrlen(name));
142 Ustrcpy(node->name, name);
143 node->data.ptr = NULL;
144 (void)tree_insertnode(root, node);
145 return node;
146 }
147
148
149
150 /*************************************************
151 *             Set up new queue item              *
152 *************************************************/
153
154 static queue_item *set_up(uschar *name, int dir_char)
155 {
156 int i, rc, save_errno;
157 struct stat statdata;
158 void *reset_point;
159 uschar *p;
160 queue_item *q = (queue_item *)store_malloc(sizeof(queue_item));
161 uschar buffer[256];
162
163 /* Initialize the block */
164
165 q->next = q->prev = NULL;
166 q->destinations = NULL;
167 Ustrcpy(q->name, name);
168 q->seen = TRUE;
169 q->frozen = FALSE;
170 q->dir_char = dir_char;
171 q->sender = NULL;
172 q->size = 0;
173
174 /* Read the header file from the spool; if there is a failure it might mean
175 inaccessibility as a result of protections. A successful read will have caused
176 sender_address to get set and the recipients fields to be initialized. If
177 there's a format error in the headers, we can still display info from the
178 envelope.
179
180 Before reading the header remember the position in the dynamic store so that
181 we can recover the store into which the header is read. All data read by
182 spool_read_header that is to be preserved is copied into malloc store. */
183
184 reset_point = store_get(0);
185 message_size = 0;
186 message_subdir[0] = dir_char;
187 sprintf(CS buffer, "%s-H", name);
188 rc =  spool_read_header(buffer, FALSE, TRUE);
189 save_errno = errno;
190
191 /* If we failed to read the envelope, compute the input time by
192 interpreting the id as a base-62 number. */
193
194 if (rc != spool_read_OK && rc != spool_read_hdrerror)
195   {
196   int t = 0;
197   for (i = 0; i < 6; i++) t = t * 62 + tab62[name[i] - '0'];
198   q->update_time = q->input_time = t;
199   }
200
201 /* Envelope read; get input time and remove qualify_domain from sender address,
202 if it's there. */
203
204 else
205   {
206   q->update_time = q->input_time = received_time;
207   if ((p = strstric(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 = %d ***",
225         statbuf.st_size);
226     else msg = string_sprintf("*** Format error in spool file ***");
227     }
228   else msg = string_sprintf("*** 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     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 = deliver_freeze;
245
246 if (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 sprintf(CS buffer, "%s/input/%s/%s-D", spool_directory, message_subdir, name);
269 if (Ustat(buffer, &statdata) == 0)
270   q->size = message_size + statdata.st_size - SPOOL_DATA_START_OFFSET + 1;
271
272 /* Scan and process the recipients list, skipping any that have already
273 been delivered, and removing visible names. */
274
275 if (recipients_list != NULL)
276   {
277   for (i = 0; i < recipients_count; i++)
278     {
279     uschar *r = recipients_list[i].address;
280     if (tree_search(tree_nonrecipients, r) == NULL)
281       {
282       if ((p = strstric(r+1, qualify_domain, FALSE)) != NULL &&
283         *(--p) == '@') *p = 0;
284       (void)find_dest(q, r, dest_add, FALSE);
285       }
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 *find_queue(uschar *name, int action, int dir_char)
338 {
339 int first = 0;
340 int last = queue_index_size - 1;
341 int middle = (first + last)/2;
342 queue_item *p, *q, *qq;
343
344 /* Handle the empty queue as a special case. */
345
346 if (queue_total == 0)
347   {
348   if (action != queue_add) return NULL;
349   if ((qq = set_up(name, dir_char)) != NULL)
350     {
351     int i;
352     for (i = 0; i < queue_index_size; i++) queue_index[i] = qq;
353     queue_total++;
354     return qq;
355     }
356   return NULL;
357   }
358
359 /* Also handle insertion at the start or end of the queue
360 as special cases. */
361
362 if (Ustrcmp(name, (queue_index[0])->name) < 0)
363   {
364   if (action != queue_add) return NULL;
365   if ((qq = set_up(name, dir_char)) != NULL)
366     {
367     qq->next = queue_index[0];
368     (queue_index[0])->prev = qq;
369     queue_index[0] = qq;
370     queue_total++;
371     return qq;
372     }
373   return NULL;
374   }
375
376 if (Ustrcmp(name, (queue_index[queue_index_size-1])->name) > 0)
377   {
378   if (action != queue_add) return NULL;
379   if ((qq = set_up(name, dir_char)) != NULL)
380     {
381     qq->prev = queue_index[queue_index_size-1];
382     (queue_index[queue_index_size-1])->next = qq;
383     queue_index[queue_index_size-1] = qq;
384     queue_total++;
385     return qq;
386     }
387   return NULL;
388   }
389
390 /* Use binary chopping on the index to get a range of the queue to search
391 when the name is somewhere in the middle, if present. */
392
393 while (middle > first)
394   {
395   if (Ustrcmp(name, (queue_index[middle])->name) >= 0) first = middle;
396     else last = middle;
397   middle = (first + last)/2;
398   }
399
400 /* Now search down the part of the queue in which the item must
401 lie if it exists. Both end points are inclusive - though in fact
402 the bottom one can only be = if it is the original bottom. */
403
404 p = queue_index[first];
405 q = queue_index[last];
406
407 for (;;)
408   {
409   int c = Ustrcmp(name, p->name);
410
411   /* Already on queue; mark seen if required. */
412
413   if (c == 0)
414     {
415     if (action == queue_add) p->seen = TRUE;
416     return p;
417     }
418
419   /* Not on the queue; add an entry if required. Note that set-up might
420   fail (the file might vanish under our feet). Note also that we know
421   there is always a previous item to p because the end points are
422   inclusive. */
423
424   else if (c < 0)
425     {
426     if (action == queue_add)
427       {
428       if ((qq = set_up(name, dir_char)) != NULL)
429         {
430         qq->next = p;
431         qq->prev = p->prev;
432         p->prev->next = qq;
433         p->prev = qq;
434         queue_total++;
435         return qq;
436         }
437       }
438     return NULL;
439     }
440
441   /* Control should not reach here if p == q, because the name
442   is supposed to be <= the name of the bottom item. */
443
444   if (p == q) return NULL;
445
446   /* Else might be further down the queue; continue */
447
448   p = p->next;
449   }
450
451 /* Control should never reach here. */
452 }
453
454
455
456 /*************************************************
457 *        Scan the exim spool directory           *
458 *************************************************/
459
460 /* If we discover that there are subdirectories, set a flag so that the menu
461 code knows to look for them. We count the entries to set the value for the
462 queue stripchart, and set up data for the queue display window if the "full"
463 option is given. */
464
465 void scan_spool_input(int full)
466 {
467 int i;
468 int subptr;
469 int subdir_max = 1;
470 int count = 0;
471 int indexptr = 1;
472 queue_item *p;
473 struct dirent *ent;
474 DIR *dd;
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   if (subdirchar != 0)
493     {
494     input_dir[subptr] = '/';
495     input_dir[subptr+1] = subdirchar;
496     }
497
498   dd = opendir(CS input_dir);
499   if (dd == NULL) continue;
500
501   while ((ent = readdir(dd)) != NULL)
502     {
503     uschar *name = US ent->d_name;
504     int len = Ustrlen(name);
505
506     /* If we find a single alphameric sub-directory on the first
507     pass, add it to the list for subsequent scans, and remember that
508     we are dealing with a split directory. */
509
510     if (i == 0 && len == 1 && isalnum(*name))
511       {
512       subdirs[subdir_max++] = *name;
513       spool_is_split = TRUE;
514       continue;
515       }
516
517     /* Otherwise, if it is a header spool file, add it to the list */
518
519     if (len == SPOOL_NAME_LENGTH &&
520         name[SPOOL_NAME_LENGTH - 2] == '-' &&
521         name[SPOOL_NAME_LENGTH - 1] == 'H')
522       {
523       uschar basename[SPOOL_NAME_LENGTH + 1];
524       stripchart_total[0]++;
525       if (!eximon_initialized) { printf("."); fflush(stdout); }
526       Ustrcpy(basename, name);
527       basename[SPOOL_NAME_LENGTH - 2] = 0;
528       if (full) find_queue(basename, queue_add, subdirchar);
529       }
530     }
531   closedir(dd);
532   }
533
534 /* If simply counting the number, we are done; same if there are no
535 items in the in-store queue. */
536
537 if (!full || queue_total == 0) return;
538
539 /* Now scan the queue and remove any items that were not in the directory. At
540 the same time, set up the index pointers into the queue. Because we are
541 removing items, the total that we are comparing against isn't actually correct,
542 but in a long queue it won't make much difference, and in a short queue it
543 doesn't matter anyway!*/
544
545 p = queue_index[0];
546 while (p != NULL)
547   {
548   if (!p->seen)
549     {
550     queue_item *next = p->next;
551     if (p->prev == NULL) queue_index[0] = next;
552       else p->prev->next = next;
553     if (next == NULL)
554       {
555       int i;
556       queue_item *q = queue_index[queue_index_size-1];
557       for (i = queue_index_size - 1; i >= 0; i--)
558         if (queue_index[i] == q) queue_index[i] = p->prev;
559       }
560     else next->prev = p->prev;
561     clean_up(p);
562     queue_total--;
563     p = next;
564     }
565   else
566     {
567     if (++count > (queue_total * indexptr)/(queue_index_size-1))
568       {
569       queue_index[indexptr++] = p;
570       }
571     p->seen = FALSE;  /* for next time */
572     p = p->next;
573     }
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   {
582   queue_index[indexptr++] = queue_index[queue_index_size-1];
583   }
584 }
585
586
587
588
589 /*************************************************
590 *    Update the recipients list for a message    *
591 *************************************************/
592
593 /* We read the spool file only if its update time differs from last time,
594 or if there is a journal file in existence. */
595
596 /* First, a local subroutine to scan the non-recipients tree and
597 remove any of them from the address list */
598
599 static void
600 scan_tree(queue_item *p, tree_node *tn)
601 {
602 if (tn != NULL)
603   {
604   if (tn->left != NULL) scan_tree(p, tn->left);
605   if (tn->right != NULL) scan_tree(p, tn->right);
606   (void)find_dest(p, tn->name, dest_remove, FALSE);
607   }
608 }
609
610 /* The main function */
611
612 static void update_recipients(queue_item *p)
613 {
614 int i;
615 FILE *jread;
616 void *reset_point;
617 struct stat statdata;
618 uschar buffer[1024];
619
620 message_subdir[0] = p->dir_char;
621
622 sprintf(CS buffer, "%s/input/%s/%s-J", spool_directory, message_subdir, p->name);
623 jread = fopen(CS buffer, "r");
624 if (jread == NULL)
625   {
626   sprintf(CS buffer, "%s/input/%s/%s-H", spool_directory, 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_get(0);
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 != NULL)
661   {
662   for (i = 0; i < recipients_count; i++)
663     {
664     uschar *pp;
665     uschar *r = recipients_list[i].address;
666     tree_node *node = tree_search(tree_nonrecipients, r);
667
668     if (node == NULL)
669       {
670       uschar temp[256];
671       uschar *rr = temp;
672       Ustrcpy(temp, r);
673       while (*rr != 0 && *rr != '@') rr++;
674       while (*rr != 0) { *rr = tolower(*rr); rr++; }
675       node = tree_search(tree_nonrecipients, temp);
676       }
677
678     if ((pp = strstric(r+1, qualify_domain, FALSE)) != NULL &&
679       *(--pp) == '@') *pp = 0;
680     if (node == NULL)
681       (void)find_dest(p, r, dest_add, FALSE);
682     else
683       (void)find_dest(p, r, dest_remove, FALSE);
684     }
685   }
686
687 /* We also need to scan the tree of non-recipients, which might
688 contain child addresses that are not in the recipients list, but
689 which may have got onto the address list as a result of eximon
690 noticing an == line in the log. Then remember the update time,
691 recover the dynamic store, and we are done. */
692
693 scan_tree(p, tree_nonrecipients);
694 p->update_time = statdata.st_mtime;
695 store_reset(reset_point);
696 }
697
698
699
700 /*************************************************
701 *              Display queue data                *
702 *************************************************/
703
704 /* The present implementation simple re-writes the entire information each
705 time. Take some care to keep the scrolled position as it previously was, but,
706 if it was at the bottom, keep it at the bottom. Take note of any hide list, and
707 time out the entries as appropriate. */
708
709 void
710 queue_display(void)
711 {
712 int now = (int)time(NULL);
713 queue_item *p = queue_index[0];
714
715 if (menu_is_up) return;            /* Avoid nasty interactions */
716
717 text_empty(queue_widget);
718
719 while (p != NULL)
720   {
721   int count = 1;
722   dest_item *dd, *ddd;
723   uschar u = 'm';
724   int t = (now - p->input_time)/60;  /* minutes on queue */
725
726   if (t > 90)
727     {
728     u = 'h';
729     t = (t + 30)/60;
730     if (t > 72)
731       {
732       u = 'd';
733       t = (t + 12)/24;
734       if (t > 99)                    /* someone had > 99 days */
735         {
736         u = 'w';
737         t = (t + 3)/7;
738         if (t > 99)                  /* so, just in case */
739           {
740           u = 'y';
741           t = (t + 26)/52;
742           }
743         }
744       }
745     }
746
747   update_recipients(p);                   /* update destinations */
748
749   /* Can't set this earlier, as header data may change things. */
750
751   dd = p->destinations;
752
753   /* Check to see if this message is on the hide list; if any hide
754   item has timed out, remove it from the list. Hide if all destinations
755   are on the hide list. */
756
757   for (ddd = dd; ddd != NULL; ddd = ddd->next)
758     {
759     skip_item *sk;
760     skip_item **skp;
761     int len_address;
762
763     if (ddd->address[0] == '*') break;
764     len_address = Ustrlen(ddd->address);
765
766     for (skp = &queue_skip; ; skp = &(sk->next))
767       {
768       int len_skip;
769
770       sk = *skp;
771       while (sk != NULL && now >= sk->reveal)
772         {
773         *skp = sk->next;
774         store_free(sk);
775         sk = *skp;
776         if (queue_skip == NULL)
777           {
778           XtDestroyWidget(unhide_widget);
779           unhide_widget = NULL;
780           }
781         }
782       if (sk == NULL) break;
783
784       /* If this address matches the skip item, break (sk != NULL) */
785
786       len_skip = Ustrlen(sk->text);
787       if (len_skip <= len_address &&
788           Ustrcmp(ddd->address + len_address - len_skip, sk->text) == 0)
789         break;
790       }
791
792     if (sk == NULL) break;
793     }
794
795   /* Don't use more than one call of anon() in one statement - it uses
796   a fixed static buffer. */
797
798   if (ddd != NULL || dd == NULL)
799     {
800     text_showf(queue_widget, "%c%2d%c %s %s %-8s ",
801       (p->frozen)? '*' : ' ',
802       t, u,
803       string_format_size(p->size, big_buffer),
804       p->name,
805       (p->sender == NULL)? US"       " :
806         (p->sender[0] == 0)? US"<>     " : anon(p->sender));
807
808     text_showf(queue_widget, "%s%s%s",
809       (dd == NULL || dd->address[0] == '*')? "" : "<",
810       (dd == NULL)? US"" : anon(dd->address),
811       (dd == NULL || dd->address[0] == '*')? "" : ">");
812
813     if (dd != NULL && dd->parent != NULL && dd->parent->address[0] != '*')
814       text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
815
816     text_show(queue_widget, US"\n");
817
818     if (dd != NULL) dd = dd->next;
819     while (dd != NULL && count++ < queue_max_addresses)
820       {
821       text_showf(queue_widget, "                                     <%s>",
822         anon(dd->address));
823       if (dd->parent != NULL && dd->parent->address[0] != '*')
824         text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
825       text_show(queue_widget, US"\n");
826       dd = dd->next;
827       }
828     if (dd != NULL)
829       text_showf(queue_widget, "                                     ...\n");
830     }
831
832   p = p->next;
833   }
834 }
835
836 /* End of em_queue.c */