Testsuite: sort output of retry DB dumps
[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 sprintf(CS buffer, "%s/input/%s/%s-D", spool_directory, message_subdir, name);
267 if (Ustat(buffer, &statdata) == 0)
268   q->size = message_size + statdata.st_size - SPOOL_DATA_START_OFFSET + 1;
269
270 /* Scan and process the recipients list, skipping any that have already
271 been delivered, and removing visible names. */
272
273 if (recipients_list != NULL)
274   {
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
287 /* Recover the dynamic store used by spool_read_header(). */
288
289 store_reset(reset_point);
290 return q;
291 }
292
293
294
295 /*************************************************
296 *             Find/Create a queue item           *
297 *************************************************/
298
299 /* The queue is kept as a doubly-linked list, sorted by name. However,
300 to speed up searches, an index into the list is used. This is maintained
301 by the scan_spool_input function when it goes down the list throwing
302 out entries that are no longer needed. When the action is "add" and
303 we don't need to add, mark the found item as seen. */
304
305
306 #ifdef never
307 static void debug_queue(void)
308 {
309 int i;
310 int count = 0;
311 queue_item *p;
312 printf("\nqueue_total=%d\n", queue_total);
313
314 for (i = 0; i < queue_index_size; i++)
315   printf("index %d = %d %s\n", i, (int)(queue_index[i]),
316     (queue_index[i])->name);
317
318 printf("Queue is:\n");
319 p = queue_index[0];
320 while (p != NULL)
321   {
322   count++;
323   for (i = 0; i < queue_index_size; i++)
324     {
325     if (queue_index[i] == p) printf("count=%d index=%d\n", count, (int)p);
326     }
327   printf("%d %d %d %s\n", (int)p, (int)p->next, (int)p->prev, p->name);
328   p = p->next;
329   }
330 }
331 #endif
332
333
334
335 queue_item *find_queue(uschar *name, int action, int dir_char)
336 {
337 int first = 0;
338 int last = queue_index_size - 1;
339 int middle = (first + last)/2;
340 queue_item *p, *q, *qq;
341
342 /* Handle the empty queue as a special case. */
343
344 if (queue_total == 0)
345   {
346   if (action != queue_add) return NULL;
347   if ((qq = set_up(name, dir_char)) != NULL)
348     {
349     int i;
350     for (i = 0; i < queue_index_size; i++) queue_index[i] = qq;
351     queue_total++;
352     return qq;
353     }
354   return NULL;
355   }
356
357 /* Also handle insertion at the start or end of the queue
358 as special cases. */
359
360 if (Ustrcmp(name, (queue_index[0])->name) < 0)
361   {
362   if (action != queue_add) return NULL;
363   if ((qq = set_up(name, dir_char)) != NULL)
364     {
365     qq->next = queue_index[0];
366     (queue_index[0])->prev = qq;
367     queue_index[0] = qq;
368     queue_total++;
369     return qq;
370     }
371   return NULL;
372   }
373
374 if (Ustrcmp(name, (queue_index[queue_index_size-1])->name) > 0)
375   {
376   if (action != queue_add) return NULL;
377   if ((qq = set_up(name, dir_char)) != NULL)
378     {
379     qq->prev = queue_index[queue_index_size-1];
380     (queue_index[queue_index_size-1])->next = qq;
381     queue_index[queue_index_size-1] = qq;
382     queue_total++;
383     return qq;
384     }
385   return NULL;
386   }
387
388 /* Use binary chopping on the index to get a range of the queue to search
389 when the name is somewhere in the middle, if present. */
390
391 while (middle > first)
392   {
393   if (Ustrcmp(name, (queue_index[middle])->name) >= 0) first = middle;
394     else last = middle;
395   middle = (first + last)/2;
396   }
397
398 /* Now search down the part of the queue in which the item must
399 lie if it exists. Both end points are inclusive - though in fact
400 the bottom one can only be = if it is the original bottom. */
401
402 p = queue_index[first];
403 q = queue_index[last];
404
405 for (;;)
406   {
407   int c = Ustrcmp(name, p->name);
408
409   /* Already on queue; mark seen if required. */
410
411   if (c == 0)
412     {
413     if (action == queue_add) p->seen = TRUE;
414     return p;
415     }
416
417   /* Not on the queue; add an entry if required. Note that set-up might
418   fail (the file might vanish under our feet). Note also that we know
419   there is always a previous item to p because the end points are
420   inclusive. */
421
422   else if (c < 0)
423     {
424     if (action == queue_add)
425       {
426       if ((qq = set_up(name, dir_char)) != NULL)
427         {
428         qq->next = p;
429         qq->prev = p->prev;
430         p->prev->next = qq;
431         p->prev = qq;
432         queue_total++;
433         return qq;
434         }
435       }
436     return NULL;
437     }
438
439   /* Control should not reach here if p == q, because the name
440   is supposed to be <= the name of the bottom item. */
441
442   if (p == q) return NULL;
443
444   /* Else might be further down the queue; continue */
445
446   p = p->next;
447   }
448
449 /* Control should never reach here. */
450 }
451
452
453
454 /*************************************************
455 *        Scan the exim spool directory           *
456 *************************************************/
457
458 /* If we discover that there are subdirectories, set a flag so that the menu
459 code knows to look for them. We count the entries to set the value for the
460 queue stripchart, and set up data for the queue display window if the "full"
461 option is given. */
462
463 void scan_spool_input(int full)
464 {
465 int i;
466 int subptr;
467 int subdir_max = 1;
468 int count = 0;
469 int indexptr = 1;
470 queue_item *p;
471 struct dirent *ent;
472 DIR *dd;
473 uschar input_dir[256];
474 uschar subdirs[64];
475
476 subdirs[0] = 0;
477 stripchart_total[0] = 0;
478
479 sprintf(CS input_dir, "%s/input", spool_directory);
480 subptr = Ustrlen(input_dir);
481 input_dir[subptr+2] = 0;               /* terminator for lengthened name */
482
483 /* Loop for each spool file on the queue - searching any subdirectories that
484 may exist. When initializing eximon, every file will have to be read. To show
485 there is progress, output a dot for each one to the standard output. */
486
487 for (i = 0; i < subdir_max; i++)
488   {
489   int subdirchar = subdirs[i];      /* 0 for main directory */
490   if (subdirchar != 0)
491     {
492     input_dir[subptr] = '/';
493     input_dir[subptr+1] = subdirchar;
494     }
495
496   dd = opendir(CS input_dir);
497   if (dd == NULL) continue;
498
499   while ((ent = readdir(dd)) != NULL)
500     {
501     uschar *name = US ent->d_name;
502     int len = Ustrlen(name);
503
504     /* If we find a single alphameric sub-directory on the first
505     pass, add it to the list for subsequent scans, and remember that
506     we are dealing with a split directory. */
507
508     if (i == 0 && len == 1 && isalnum(*name))
509       {
510       subdirs[subdir_max++] = *name;
511       spool_is_split = TRUE;
512       continue;
513       }
514
515     /* Otherwise, if it is a header spool file, add it to the list */
516
517     if (len == SPOOL_NAME_LENGTH &&
518         name[SPOOL_NAME_LENGTH - 2] == '-' &&
519         name[SPOOL_NAME_LENGTH - 1] == 'H')
520       {
521       uschar basename[SPOOL_NAME_LENGTH + 1];
522       stripchart_total[0]++;
523       if (!eximon_initialized) { printf("."); fflush(stdout); }
524       Ustrcpy(basename, name);
525       basename[SPOOL_NAME_LENGTH - 2] = 0;
526       if (full) find_queue(basename, queue_add, subdirchar);
527       }
528     }
529   closedir(dd);
530   }
531
532 /* If simply counting the number, we are done; same if there are no
533 items in the in-store queue. */
534
535 if (!full || queue_total == 0) return;
536
537 /* Now scan the queue and remove any items that were not in the directory. At
538 the same time, set up the index pointers into the queue. Because we are
539 removing items, the total that we are comparing against isn't actually correct,
540 but in a long queue it won't make much difference, and in a short queue it
541 doesn't matter anyway!*/
542
543 p = queue_index[0];
544 while (p != NULL)
545   {
546   if (!p->seen)
547     {
548     queue_item *next = p->next;
549     if (p->prev == NULL) queue_index[0] = next;
550       else p->prev->next = next;
551     if (next == NULL)
552       {
553       int i;
554       queue_item *q = queue_index[queue_index_size-1];
555       for (i = queue_index_size - 1; i >= 0; i--)
556         if (queue_index[i] == q) queue_index[i] = p->prev;
557       }
558     else next->prev = p->prev;
559     clean_up(p);
560     queue_total--;
561     p = next;
562     }
563   else
564     {
565     if (++count > (queue_total * indexptr)/(queue_index_size-1))
566       {
567       queue_index[indexptr++] = p;
568       }
569     p->seen = FALSE;  /* for next time */
570     p = p->next;
571     }
572   }
573
574 /* If a lot of messages have been removed at the bottom, we may not
575 have got the index all filled in yet. Make sure all the pointers
576 are legal. */
577
578 while (indexptr < queue_index_size - 1)
579   {
580   queue_index[indexptr++] = queue_index[queue_index_size-1];
581   }
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 void *reset_point;
615 struct stat statdata;
616 uschar buffer[1024];
617
618 message_subdir[0] = p->dir_char;
619
620 sprintf(CS buffer, "%s/input/%s/%s-J", spool_directory, message_subdir, p->name);
621 jread = fopen(CS buffer, "r");
622 if (jread == NULL)
623   {
624   sprintf(CS buffer, "%s/input/%s/%s-H", spool_directory, message_subdir, p->name);
625   if (Ustat(buffer, &statdata) < 0 || p->update_time == statdata.st_mtime)
626     return;
627   }
628
629 /* Get the contents of the header file; if any problem, just give up.
630 Arrange to recover the dynamic store afterwards. */
631
632 reset_point = store_get(0);
633 sprintf(CS buffer, "%s-H", p->name);
634 if (spool_read_header(buffer, FALSE, TRUE) != spool_read_OK)
635   {
636   store_reset(reset_point);
637   if (jread != NULL) fclose(jread);
638   return;
639   }
640
641 /* If there's a journal file, add its contents to the non-recipients tree */
642
643 if (jread != NULL)
644   {
645   while (Ufgets(big_buffer, big_buffer_size, jread) != NULL)
646     {
647     int n = Ustrlen(big_buffer);
648     big_buffer[n-1] = 0;
649     tree_add_nonrecipient(big_buffer);
650     }
651   fclose(jread);
652   }
653
654 /* Scan and process the recipients list, removing any that have already
655 been delivered, and removing visible names. In the nonrecipients tree,
656 domains are lower cased. */
657
658 if (recipients_list != NULL)
659   {
660   for (i = 0; i < recipients_count; i++)
661     {
662     uschar *pp;
663     uschar *r = recipients_list[i].address;
664     tree_node *node = tree_search(tree_nonrecipients, r);
665
666     if (node == NULL)
667       {
668       uschar temp[256];
669       uschar *rr = temp;
670       Ustrcpy(temp, r);
671       while (*rr != 0 && *rr != '@') rr++;
672       while (*rr != 0) { *rr = tolower(*rr); rr++; }
673       node = tree_search(tree_nonrecipients, temp);
674       }
675
676     if ((pp = strstric(r+1, qualify_domain, FALSE)) != NULL &&
677       *(--pp) == '@') *pp = 0;
678     if (node == NULL)
679       (void)find_dest(p, r, dest_add, FALSE);
680     else
681       (void)find_dest(p, r, dest_remove, FALSE);
682     }
683   }
684
685 /* We also need to scan the tree of non-recipients, which might
686 contain child addresses that are not in the recipients list, but
687 which may have got onto the address list as a result of eximon
688 noticing an == line in the log. Then remember the update time,
689 recover the dynamic store, and we are done. */
690
691 scan_tree(p, tree_nonrecipients);
692 p->update_time = statdata.st_mtime;
693 store_reset(reset_point);
694 }
695
696
697
698 /*************************************************
699 *              Display queue data                *
700 *************************************************/
701
702 /* The present implementation simple re-writes the entire information each
703 time. Take some care to keep the scrolled position as it previously was, but,
704 if it was at the bottom, keep it at the bottom. Take note of any hide list, and
705 time out the entries as appropriate. */
706
707 void
708 queue_display(void)
709 {
710 int now = (int)time(NULL);
711 queue_item *p = queue_index[0];
712
713 if (menu_is_up) return;            /* Avoid nasty interactions */
714
715 text_empty(queue_widget);
716
717 while (p != NULL)
718   {
719   int count = 1;
720   dest_item *dd, *ddd;
721   uschar u = 'm';
722   int t = (now - p->input_time)/60;  /* minutes on queue */
723
724   if (t > 90)
725     {
726     u = 'h';
727     t = (t + 30)/60;
728     if (t > 72)
729       {
730       u = 'd';
731       t = (t + 12)/24;
732       if (t > 99)                    /* someone had > 99 days */
733         {
734         u = 'w';
735         t = (t + 3)/7;
736         if (t > 99)                  /* so, just in case */
737           {
738           u = 'y';
739           t = (t + 26)/52;
740           }
741         }
742       }
743     }
744
745   update_recipients(p);                   /* update destinations */
746
747   /* Can't set this earlier, as header data may change things. */
748
749   dd = p->destinations;
750
751   /* Check to see if this message is on the hide list; if any hide
752   item has timed out, remove it from the list. Hide if all destinations
753   are on the hide list. */
754
755   for (ddd = dd; ddd != NULL; ddd = ddd->next)
756     {
757     skip_item *sk;
758     skip_item **skp;
759     int len_address;
760
761     if (ddd->address[0] == '*') break;
762     len_address = Ustrlen(ddd->address);
763
764     for (skp = &queue_skip; ; skp = &(sk->next))
765       {
766       int len_skip;
767
768       sk = *skp;
769       while (sk != NULL && now >= sk->reveal)
770         {
771         *skp = sk->next;
772         store_free(sk);
773         sk = *skp;
774         if (queue_skip == NULL)
775           {
776           XtDestroyWidget(unhide_widget);
777           unhide_widget = NULL;
778           }
779         }
780       if (sk == NULL) break;
781
782       /* If this address matches the skip item, break (sk != NULL) */
783
784       len_skip = Ustrlen(sk->text);
785       if (len_skip <= len_address &&
786           Ustrcmp(ddd->address + len_address - len_skip, sk->text) == 0)
787         break;
788       }
789
790     if (sk == NULL) break;
791     }
792
793   /* Don't use more than one call of anon() in one statement - it uses
794   a fixed static buffer. */
795
796   if (ddd != NULL || dd == NULL)
797     {
798     text_showf(queue_widget, "%c%2d%c %s %s %-8s ",
799       (p->frozen)? '*' : ' ',
800       t, u,
801       string_format_size(p->size, big_buffer),
802       p->name,
803       (p->sender == NULL)? US"       " :
804         (p->sender[0] == 0)? US"<>     " : anon(p->sender));
805
806     text_showf(queue_widget, "%s%s%s",
807       (dd == NULL || dd->address[0] == '*')? "" : "<",
808       (dd == NULL)? US"" : anon(dd->address),
809       (dd == NULL || dd->address[0] == '*')? "" : ">");
810
811     if (dd != NULL && dd->parent != NULL && dd->parent->address[0] != '*')
812       text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
813
814     text_show(queue_widget, US"\n");
815
816     if (dd != NULL) dd = dd->next;
817     while (dd != NULL && count++ < queue_max_addresses)
818       {
819       text_showf(queue_widget, "                                     <%s>",
820         anon(dd->address));
821       if (dd->parent != NULL && dd->parent->address[0] != '*')
822         text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
823       text_show(queue_widget, US"\n");
824       dd = dd->next;
825       }
826     if (dd != NULL)
827       text_showf(queue_widget, "                                     ...\n");
828     }
829
830   p = p->next;
831   }
832 }
833
834 /* End of em_queue.c */