1 /*************************************************
3 *************************************************/
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainers 2020 - 2023 */
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, uschar *name, int action, BOOL caseless)
72 dest_item **d = &(q->destinations);
76 if ((caseless? strcmpic(name,(*d)->address) : Ustrcmp(name,(*d)->address))
81 if (action != dest_remove) return *d;
86 /* Unset any parent pointers that were to this address */
88 for (ddd = q->destinations; ddd != NULL; ddd = ddd->next)
90 if (ddd->parent == dd) ddd->parent = NULL;
98 if (action != dest_add) return NULL;
100 dd = (dest_item *)store_malloc(sizeof(dest_item) + Ustrlen(name));
101 Ustrcpy(dd->address, name);
110 /*************************************************
111 * Clean up a dead queue item *
112 *************************************************/
115 clean_up(queue_item *p)
117 dest_item *dd = p->destinations;
120 dest_item *next = dd->next;
124 if (p->sender != NULL) store_free(p->sender);
129 /*************************************************
130 * Set up an ACL variable *
131 *************************************************/
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). */
139 acl_var_create(uschar *name)
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);
152 /*************************************************
153 * Set up new queue item *
154 *************************************************/
157 set_up(uschar * name, int dir_char)
159 int i, rc, save_errno;
160 struct stat statdata;
163 queue_item *q = (queue_item *)store_malloc(sizeof(queue_item));
166 /* Initialize the block */
168 q->next = q->prev = NULL;
169 q->destinations = NULL;
170 Ustrncpy(q->name, name, sizeof(q->name));
173 q->dir_char = dir_char;
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
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. */
187 reset_point = store_mark();
189 message_subdir[0] = dir_char;
190 sprintf(CS buffer, "%s-H", name);
191 rc = spool_read_header(buffer, FALSE, TRUE);
194 /* If we failed to read the envelope, compute the input time by
195 interpreting the id as a base-62 number. */
197 if (rc != spool_read_OK && rc != spool_read_hdrerror)
200 for (i = 0; i < 6; i++) t = t * 62 + tab62[name[i] - '0'];
201 q->update_time = q->input_time = t;
204 /* Envelope read; get input time and remove qualify_domain from sender address,
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;
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. */
218 if (rc != spool_read_OK)
222 if (save_errno == ERRNO_SPOOLFORMAT)
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 " ***",
229 else msg = US"*** Format error in spool file ***";
231 else msg = US"*** Cannot read spool file ***";
233 if (rc == spool_read_hdrerror)
235 (void)find_dest(q, msg, dest_add, FALSE);
239 f.deliver_freeze = FALSE;
240 sender_address = msg;
241 recipients_count = 0;
245 /* Now set up the remaining data. */
247 q->frozen = f.deliver_freeze;
249 if (f.sender_set_untrusted)
251 if (sender_address[0] == 0)
253 q->sender = store_malloc(Ustrlen(originator_login) + 6);
254 sprintf(CS q->sender, "<> (%s)", originator_login);
258 q->sender = store_malloc(Ustrlen(sender_address) +
259 Ustrlen(originator_login) + 4);
260 sprintf(CS q->sender, "%s (%s)", sender_address, originator_login);
265 q->sender = store_malloc(Ustrlen(sender_address) + 1);
266 Ustrcpy(q->sender, sender_address);
269 sender_address = NULL;
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(name) + 1;
276 /* Scan and process the recipients list, skipping any that have already
277 been delivered, and removing visible names. */
280 for (i = 0; i < recipients_count; i++)
282 uschar * r = recipients_list[i].address;
283 if (tree_search(tree_nonrecipients, r) == NULL)
285 if ((p = strstric(r+1, qualify_domain, FALSE)) != NULL &&
286 *(--p) == '@') *p = 0;
287 (void)find_dest(q, r, dest_add, FALSE);
291 /* Recover the dynamic store used by spool_read_header(). */
293 store_reset(reset_point);
299 /*************************************************
300 * Find/Create a queue item *
301 *************************************************/
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. */
311 static void debug_queue(void)
316 printf("\nqueue_total=%d\n", queue_total);
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);
322 printf("Queue is:\n");
327 for (i = 0; i < queue_index_size; i++)
329 if (queue_index[i] == p) printf("count=%d index=%d\n", count, (int)p);
331 printf("%d %d %d %s\n", (int)p, (int)p->next, (int)p->prev, p->name);
340 find_queue(uschar *name, int action, int dir_char)
343 int last = queue_index_size - 1;
344 int middle = (first + last)/2;
345 queue_item *p, *q, *qq;
347 /* Handle the empty queue as a special case. */
349 if (queue_total == 0)
351 if (action != queue_add) return NULL;
352 if ((qq = set_up(name, dir_char)) != NULL)
355 for (i = 0; i < queue_index_size; i++) queue_index[i] = qq;
362 /* Also handle insertion at the start or end of the queue
365 if (Ustrcmp(name, (queue_index[0])->name) < 0)
367 if (action != queue_add) return NULL;
368 if ((qq = set_up(name, dir_char)) != NULL)
370 qq->next = queue_index[0];
371 (queue_index[0])->prev = qq;
379 if (Ustrcmp(name, (queue_index[queue_index_size-1])->name) > 0)
381 if (action != queue_add) return NULL;
382 if ((qq = set_up(name, dir_char)) != NULL)
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;
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. */
396 while (middle > first)
398 if (Ustrcmp(name, (queue_index[middle])->name) >= 0) first = middle;
400 middle = (first + last)/2;
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. */
407 p = queue_index[first];
408 q = queue_index[last];
412 int c = Ustrcmp(name, p->name);
414 /* Already on queue; mark seen if required. */
418 if (action == queue_add) p->seen = TRUE;
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
429 if (action == queue_add)
431 if ((qq = set_up(name, dir_char)) != NULL)
444 /* Control should not reach here if p == q, because the name
445 is supposed to be <= the name of the bottom item. */
447 if (p == q) return NULL;
449 /* Else might be further down the queue; continue */
454 /* Control should never reach here. */
459 /*************************************************
460 * Scan the exim spool directory *
461 *************************************************/
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"
469 scan_spool_input(int full)
477 uschar input_dir[256];
481 stripchart_total[0] = 0;
483 sprintf(CS input_dir, "%s/input", spool_directory);
484 subptr = Ustrlen(input_dir);
485 input_dir[subptr+2] = 0; /* terminator for lengthened name */
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. */
491 for (i = 0; i < subdir_max; i++)
493 int subdirchar = subdirs[i]; /* 0 for main directory */
499 input_dir[subptr] = '/';
500 input_dir[subptr+1] = subdirchar;
503 if (!(dd = exim_opendir(input_dir))) continue;
505 while ((ent = readdir(dd)))
507 uschar *name = US ent->d_name;
508 int len = Ustrlen(name);
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. */
514 if (i == 0 && len == 1 && isalnum(*name))
516 subdirs[subdir_max++] = *name;
517 spool_is_split = TRUE;
521 /* Otherwise, if it is a header spool file, add it to the list */
523 if (len == SPOOL_NAME_LENGTH &&
524 name[SPOOL_NAME_LENGTH - 2] == '-' &&
525 name[SPOOL_NAME_LENGTH - 1] == 'H')
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);
538 /* If simply counting the number, we are done; same if there are no
539 items in the in-store queue. */
541 if (!full || queue_total == 0) return;
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!*/
549 for (p = queue_index[0]; p; )
552 queue_item * next = p->next;
554 p->prev->next = next;
556 queue_index[0] = next;
558 next->prev = p->prev;
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;
572 if (++count > (queue_total * indexptr)/(queue_index_size-1))
573 queue_index[indexptr++] = p;
574 p->seen = FALSE; /* for next time */
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
582 while (indexptr < queue_index_size - 1)
583 queue_index[indexptr++] = queue_index[queue_index_size-1];
589 /*************************************************
590 * Update the recipients list for a message *
591 *************************************************/
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. */
596 /* First, a local subroutine to scan the non-recipients tree and
597 remove any of them from the address list */
600 scan_tree(queue_item *p, tree_node *tn)
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);
610 /* The main function */
612 static void update_recipients(queue_item *p)
617 struct stat statdata;
620 message_subdir[0] = p->dir_char;
622 snprintf(CS buffer, sizeof(buffer), "%s/input/%s/%s/%s-J",
623 spool_directory, queue_name, message_subdir, p->name);
625 if (!(jread = fopen(CS buffer, "r")))
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)
633 /* Get the contents of the header file; if any problem, just give up.
634 Arrange to recover the dynamic store afterwards. */
636 reset_point = store_mark();
637 sprintf(CS buffer, "%s-H", p->name);
638 if (spool_read_header(buffer, FALSE, TRUE) != spool_read_OK)
640 store_reset(reset_point);
641 if (jread != NULL) fclose(jread);
645 /* If there's a journal file, add its contents to the non-recipients tree */
649 while (Ufgets(big_buffer, big_buffer_size, jread) != NULL)
651 int n = Ustrlen(big_buffer);
653 tree_add_nonrecipient(big_buffer);
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. */
663 for (i = 0; i < recipients_count; i++)
666 uschar * r = recipients_list[i].address;
669 if (!(node = tree_search(tree_nonrecipients, r)))
670 node = tree_search(tree_nonrecipients, string_copylc(r));
672 if ((pp = strstric(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 */