1 /*************************************************
3 *************************************************/
5 /* Copyright (c) The Exim Maintainers 2020 - 2024 */
6 /* Copyright (c) University of Cambridge 1995 - 2018 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 /* SPDX-License-Identifier: GPL-2.0-or-later */
14 /* This module contains functions to do with scanning exim's
15 queue and displaying the data therefrom. */
18 /* If we are anonymizing for screen shots, define a function to anonymize
19 addresses. Otherwise, define a macro that does nothing. */
22 static uschar *anon(uschar *s)
24 static uschar anon_result[256];
25 uschar *ss = anon_result;
26 for (; *s != 0; s++) *ss++ = (*s == '@' || *s == '.')? *s : 'x';
35 /*************************************************
37 *************************************************/
39 static int queue_total = 0; /* number of items in queue */
41 /* Table for turning base-62 numbers into binary */
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 */
52 /* Index for quickly finding things in the ordered queue. */
54 static queue_item *queue_index[queue_index_size];
58 /*************************************************
59 * Find/Create/Delete a destination *
60 *************************************************/
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. */
69 find_dest(queue_item * q, const uschar * name, int action, BOOL caseless)
72 dest_item ** d = &q->destinations;
76 if ((caseless? strcmpic(name,(*d)->address) : Ustrcmp(name,(*d)->address))
79 if (action != dest_remove) return *d;
84 /* Unset any parent pointers that were to this address */
86 for (dest_item * ddd = q->destinations; ddd; ddd = ddd->next)
87 if (ddd->parent == dd) ddd->parent = NULL;
94 if (action != dest_add) return NULL;
96 dd = (dest_item *)store_malloc(sizeof(dest_item) + Ustrlen(name));
97 Ustrcpy(dd->address, name);
106 /*************************************************
107 * Clean up a dead queue item *
108 *************************************************/
111 clean_up(queue_item *p)
113 dest_item *dd = p->destinations;
116 dest_item *next = dd->next;
120 if (p->sender != NULL) store_free(p->sender);
125 /*************************************************
126 * Set up an ACL variable *
127 *************************************************/
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). */
135 acl_var_create(uschar *name)
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), GET_UNTAINTED);
140 Ustrcpy(node->name, name);
141 node->data.ptr = NULL;
142 (void)tree_insertnode(root, node);
148 /*************************************************
149 * Set up new queue item *
150 *************************************************/
153 set_up(uschar * name, int dir_char)
155 int i, rc, save_errno;
156 struct stat statdata;
159 queue_item *q = (queue_item *)store_malloc(sizeof(queue_item));
162 /* Initialize the block */
164 q->next = q->prev = NULL;
165 q->destinations = NULL;
166 Ustrncpy(q->name, name, sizeof(q->name)-1);
167 q->name[sizeof(q->name)-1] = '\0';
170 q->dir_char = dir_char;
174 /* Read the header file from the spool; if there is a failure it might mean
175 inaccessibility as a result of protections. A successful read will have caused
176 sender_address to get set and the recipients fields to be initialized. If
177 there's a format error in the headers, we can still display info from the
180 Before reading the header remember the position in the dynamic store so that
181 we can recover the store into which the header is read. All data read by
182 spool_read_header that is to be preserved is copied into malloc store. */
184 reset_point = store_mark();
186 message_subdir[0] = dir_char;
187 sprintf(CS buffer, "%s-H", name);
188 rc = spool_read_header(buffer, FALSE, TRUE);
191 /* If we failed to read the envelope, compute the input time by
192 interpreting the id as a base-62 number. */
194 if (rc != spool_read_OK && rc != spool_read_hdrerror)
197 for (i = 0; i < 6; i++) t = t * 62 + tab62[name[i] - '0'];
198 q->update_time = q->input_time = t;
201 /* Envelope read; get input time and remove qualify_domain from sender address,
206 q->update_time = q->input_time = received_time.tv_sec;
207 /* deconst ok; strstric is actually safe */
208 if ((p = strstric(US sender_address+1, qualify_domain, FALSE)) != NULL &&
212 /* If we didn't read the whole header successfully, generate an error
213 message. If the envelope was read, this appears as a first recipient;
214 otherwise it sets set up in the sender field. */
216 if (rc != spool_read_OK)
220 if (save_errno == ERRNO_SPOOLFORMAT)
223 sprintf(CS big_buffer, "%s/input/%s", spool_directory, buffer);
224 if (Ustat(big_buffer, &statbuf) == 0)
225 msg = string_sprintf("*** Format error in spool file: size = " OFF_T_FMT " ***",
227 else msg = US"*** Format error in spool file ***";
229 else msg = US"*** Cannot read spool file ***";
231 if (rc == spool_read_hdrerror)
233 (void)find_dest(q, msg, dest_add, FALSE);
237 f.deliver_freeze = FALSE;
238 sender_address = msg;
239 recipients_count = 0;
243 /* Now set up the remaining data. */
245 q->frozen = f.deliver_freeze;
247 if (f.sender_set_untrusted)
249 if (sender_address[0] == 0)
251 q->sender = store_malloc(Ustrlen(originator_login) + 6);
252 sprintf(CS q->sender, "<> (%s)", originator_login);
256 q->sender = store_malloc(Ustrlen(sender_address) +
257 Ustrlen(originator_login) + 4);
258 sprintf(CS q->sender, "%s (%s)", sender_address, originator_login);
263 q->sender = store_malloc(Ustrlen(sender_address) + 1);
264 Ustrcpy(q->sender, sender_address);
267 sender_address = NULL;
269 snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-D",
270 spool_directory, queue_name, message_subdir, name);
271 if (Ustat(buffer, &statdata) == 0)
272 q->size = message_size + statdata.st_size - spool_data_start_offset(name) + 1;
274 /* Scan and process the recipients list, skipping any that have already
275 been delivered, and removing visible names. */
278 for (i = 0; i < recipients_count; i++)
280 const uschar * r = recipients_list[i].address;
281 if (tree_search(tree_nonrecipients, r) == NULL)
283 /* deconst ok; strstric is actually safe */
284 if ((p = strstric(US r+1, qualify_domain, FALSE)) != NULL &&
285 *(--p) == '@') *p = 0;
286 (void)find_dest(q, r, dest_add, FALSE);
290 /* Recover the dynamic store used by spool_read_header(). */
292 store_reset(reset_point);
298 /*************************************************
299 * Find/Create a queue item *
300 *************************************************/
302 /* The queue is kept as a doubly-linked list, sorted by name. However,
303 to speed up searches, an index into the list is used. This is maintained
304 by the scan_spool_input function when it goes down the list throwing
305 out entries that are no longer needed. When the action is "add" and
306 we don't need to add, mark the found item as seen. */
310 static void debug_queue(void)
315 printf("\nqueue_total=%d\n", queue_total);
317 for (i = 0; i < queue_index_size; i++)
318 printf("index %d = %d %s\n", i, (int)(queue_index[i]),
319 (queue_index[i])->name);
321 printf("Queue is:\n");
326 for (i = 0; i < queue_index_size; i++)
328 if (queue_index[i] == p) printf("count=%d index=%d\n", count, (int)p);
330 printf("%d %d %d %s\n", (int)p, (int)p->next, (int)p->prev, p->name);
339 find_queue(uschar *name, int action, int dir_char)
342 int last = queue_index_size - 1;
343 int middle = (first + last)/2;
344 queue_item *p, *q, *qq;
346 /* Handle the empty queue as a special case. */
348 if (queue_total == 0)
350 if (action != queue_add) return NULL;
351 if ((qq = set_up(name, dir_char)) != NULL)
354 for (i = 0; i < queue_index_size; i++) queue_index[i] = qq;
361 /* Also handle insertion at the start or end of the queue
364 if (Ustrcmp(name, (queue_index[0])->name) < 0)
366 if (action != queue_add) return NULL;
367 if ((qq = set_up(name, dir_char)) != NULL)
369 qq->next = queue_index[0];
370 (queue_index[0])->prev = qq;
378 if (Ustrcmp(name, (queue_index[queue_index_size-1])->name) > 0)
380 if (action != queue_add) return NULL;
381 if ((qq = set_up(name, dir_char)) != NULL)
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;
392 /* Use binary chopping on the index to get a range of the queue to search
393 when the name is somewhere in the middle, if present. */
395 while (middle > first)
397 if (Ustrcmp(name, (queue_index[middle])->name) >= 0) first = middle;
399 middle = (first + last)/2;
402 /* Now search down the part of the queue in which the item must
403 lie if it exists. Both end points are inclusive - though in fact
404 the bottom one can only be = if it is the original bottom. */
406 p = queue_index[first];
407 q = queue_index[last];
411 int c = Ustrcmp(name, p->name);
413 /* Already on queue; mark seen if required. */
417 if (action == queue_add) p->seen = TRUE;
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
428 if (action == queue_add)
430 if ((qq = set_up(name, dir_char)) != NULL)
443 /* Control should not reach here if p == q, because the name
444 is supposed to be <= the name of the bottom item. */
446 if (p == q) return NULL;
448 /* Else might be further down the queue; continue */
453 /* Control should never reach here. */
458 /*************************************************
459 * Scan the exim spool directory *
460 *************************************************/
462 /* If we discover that there are subdirectories, set a flag so that the menu
463 code knows to look for them. We count the entries to set the value for the
464 queue stripchart, and set up data for the queue display window if the "full"
468 scan_spool_input(int full)
476 uschar input_dir[256];
480 stripchart_total[0] = 0;
482 sprintf(CS input_dir, "%s/input", spool_directory);
483 subptr = Ustrlen(input_dir);
484 input_dir[subptr+2] = 0; /* terminator for lengthened name */
486 /* Loop for each spool file on the queue - searching any subdirectories that
487 may exist. When initializing eximon, every file will have to be read. To show
488 there is progress, output a dot for each one to the standard output. */
490 for (i = 0; i < subdir_max; i++)
492 int subdirchar = subdirs[i]; /* 0 for main directory */
498 input_dir[subptr] = '/';
499 input_dir[subptr+1] = subdirchar;
502 if (!(dd = exim_opendir(input_dir))) continue;
504 while ((ent = readdir(dd)))
506 uschar *name = US ent->d_name;
507 int len = Ustrlen(name);
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. */
513 if (i == 0 && len == 1 && isalnum(*name))
515 subdirs[subdir_max++] = *name;
516 spool_is_split = TRUE;
520 /* Otherwise, if it is a header spool file, add it to the list */
522 if (len == SPOOL_NAME_LENGTH &&
523 name[SPOOL_NAME_LENGTH - 2] == '-' &&
524 name[SPOOL_NAME_LENGTH - 1] == 'H')
526 uschar basename[SPOOL_NAME_LENGTH + 1];
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);
537 /* If simply counting the number, we are done; same if there are no
538 items in the in-store queue. */
540 if (!full || queue_total == 0) return;
542 /* Now scan the queue and remove any items that were not in the directory. At
543 the same time, set up the index pointers into the queue. Because we are
544 removing items, the total that we are comparing against isn't actually correct,
545 but in a long queue it won't make much difference, and in a short queue it
546 doesn't matter anyway!*/
548 for (p = queue_index[0]; p; )
551 queue_item * next = p->next;
553 p->prev->next = next;
555 queue_index[0] = next;
557 next->prev = p->prev;
561 queue_item * q = queue_index[queue_index_size-1];
562 for (i = queue_index_size - 1; i >= 0; i--)
563 if (queue_index[i] == q) queue_index[i] = p->prev;
571 if (++count > (queue_total * indexptr)/(queue_index_size-1))
572 queue_index[indexptr++] = p;
573 p->seen = FALSE; /* for next time */
577 /* If a lot of messages have been removed at the bottom, we may not
578 have got the index all filled in yet. Make sure all the pointers
581 while (indexptr < queue_index_size - 1)
582 queue_index[indexptr++] = queue_index[queue_index_size-1];
588 /*************************************************
589 * Update the recipients list for a message *
590 *************************************************/
592 /* We read the spool file only if its update time differs from last time,
593 or if there is a journal file in existence. */
595 /* First, a local subroutine to scan the non-recipients tree and
596 remove any of them from the address list */
599 scan_tree(queue_item *p, tree_node *tn)
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);
609 /* The main function */
611 static void update_recipients(queue_item *p)
616 struct stat statdata;
619 message_subdir[0] = p->dir_char;
621 snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-J",
622 spool_directory, queue_name, message_subdir, p->name);
624 if (!(jread = fopen(CS buffer, "r")))
626 snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-H",
627 spool_directory, queue_name, message_subdir, p->name);
628 if (Ustat(buffer, &statdata) < 0 || p->update_time == statdata.st_mtime)
632 /* Get the contents of the header file; if any problem, just give up.
633 Arrange to recover the dynamic store afterwards. */
635 reset_point = store_mark();
636 sprintf(CS buffer, "%s-H", p->name);
637 if (spool_read_header(buffer, FALSE, TRUE) != spool_read_OK)
639 store_reset(reset_point);
640 if (jread != NULL) fclose(jread);
644 /* If there's a journal file, add its contents to the non-recipients tree */
648 while (Ufgets(big_buffer, big_buffer_size, jread) != NULL)
650 int n = Ustrlen(big_buffer);
652 tree_add_nonrecipient(big_buffer);
657 /* Scan and process the recipients list, removing any that have already
658 been delivered, and removing visible names. In the nonrecipients tree,
659 domains are lower cased. */
662 for (i = 0; i < recipients_count; i++)
665 const uschar * r = recipients_list[i].address;
668 if (!(node = tree_search(tree_nonrecipients, r)))
669 node = tree_search(tree_nonrecipients, string_copylc(r));
671 /* deconst ok; strstric is actually safe */
672 if ((pp = strstric(US r+1, qualify_domain, FALSE)) && *(--pp) == '@')
675 (void)find_dest(p, r, dest_add, FALSE);
677 (void)find_dest(p, r, dest_remove, FALSE);
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. */
686 scan_tree(p, tree_nonrecipients);
687 p->update_time = statdata.st_mtime;
688 store_reset(reset_point);
693 /*************************************************
694 * Display queue data *
695 *************************************************/
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. */
705 int now = (int)time(NULL);
706 queue_item *p = queue_index[0];
708 if (menu_is_up) return; /* Avoid nasty interactions */
710 text_empty(queue_widget);
717 int t = (now - p->input_time)/60; /* minutes on queue */
727 if (t > 99) /* someone had > 99 days */
731 if (t > 99) /* so, just in case */
740 update_recipients(p); /* update destinations */
742 /* Can't set this earlier, as header data may change things. */
744 dd = p->destinations;
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. */
750 for (ddd = dd; ddd != NULL; ddd = ddd->next)
756 if (ddd->address[0] == '*') break;
757 len_address = Ustrlen(ddd->address);
759 for (skp = &queue_skip; ; skp = &(sk->next))
764 while (sk != NULL && now >= sk->reveal)
769 if (queue_skip == NULL)
771 XtDestroyWidget(unhide_widget);
772 unhide_widget = NULL;
775 if (sk == NULL) break;
777 /* If this address matches the skip item, break (sk != NULL) */
779 len_skip = Ustrlen(sk->text);
780 if (len_skip <= len_address &&
781 Ustrcmp(ddd->address + len_address - len_skip, sk->text) == 0)
785 if (sk == NULL) break;
788 /* Don't use more than one call of anon() in one statement - it uses
789 a fixed static buffer. */
791 if (ddd != NULL || dd == NULL)
793 text_showf(queue_widget, "%c%2d%c %s %s %-8s ",
794 (p->frozen)? '*' : ' ',
796 string_format_size(p->size, big_buffer),
798 (p->sender == NULL)? US" " :
799 (p->sender[0] == 0)? US"<> " : anon(p->sender));
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] == '*')? "" : ">");
806 if (dd != NULL && dd->parent != NULL && dd->parent->address[0] != '*')
807 text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
809 text_show(queue_widget, US"\n");
811 if (dd != NULL) dd = dd->next;
812 while (dd != NULL && count++ < queue_max_addresses)
814 text_showf(queue_widget, " <%s>",
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");
822 text_showf(queue_widget, " ...\n");
829 /* End of em_queue.c */