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