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