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