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