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