Tidy up "personal" testing in line with RFCs 2369, 2929, 3834.
[users/heiko/exim.git] / src / exim_monitor / em_queue.c
1 /* $Cambridge: exim/src/exim_monitor/em_queue.c,v 1.2 2005/01/04 10:00:42 ph10 Exp $ */
2
3 /*************************************************
4 *                 Exim Monitor                   *
5 *************************************************/
6
7 /* Copyright (c) University of Cambridge 1995 - 2005 */
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
15 queue and displaying the data therefrom. */
16
17
18 /* If we are anonymizing for screen shots, define a function to anonymize
19 addresses. Otherwise, define a macro that does nothing. */
20
21 #ifdef ANONYMIZE
22 static uschar *anon(uschar *s)
23 {
24 static uschar anon_result[256];
25 uschar *ss = anon_result;
26 for (; *s != 0; s++) *ss++ = (*s == '@' || *s == '.')? *s : 'x';
27 *ss = 0;
28 return anon_result;
29 }
30 #else
31 #define anon(x) x
32 #endif
33
34
35 /*************************************************
36 *                 Static variables               *
37 *************************************************/
38
39 static int queue_total = 0;   /* number of items in queue */
40
41 /* Table for turning base-62 numbers into binary */
42
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 */
50           59,60,61};                            /* x-z */
51
52 /* Index for quickly finding things in the ordered queue. */
53
54 static 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;
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. */
67
68 dest_item *find_dest(queue_item *q, uschar *name, int action, BOOL caseless)
69 {
70 dest_item *dd;
71 dest_item **d = &(q->destinations);
72
73 while (*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
97 if (action != dest_add) return NULL;
98
99 dd = (dest_item *)store_malloc(sizeof(dest_item) + Ustrlen(name));
100 Ustrcpy(dd->address, name);
101 dd->next = NULL;
102 dd->parent = NULL;
103 *d = dd;
104 return dd;
105 }
106
107
108
109 /*************************************************
110 *            Clean up a dead queue item          *
111 *************************************************/
112
113 static void clean_up(queue_item *p)
114 {
115 dest_item *dd = p->destinations;
116 while (dd != NULL)
117   {
118   dest_item *next = dd->next;
119   store_free(dd);
120   dd = next;
121   }
122 if (p->sender != NULL) store_free(p->sender);
123 store_free(p);
124 }
125
126
127 /*************************************************
128 *             Set up new queue item              *
129 *************************************************/
130
131 static queue_item *set_up(uschar *name, int dir_char)
132 {
133 int i, rc, save_errno;
134 struct stat statdata;
135 void *reset_point;
136 uschar *p;
137 queue_item *q = (queue_item *)store_malloc(sizeof(queue_item));
138 uschar buffer[256];
139
140 /* Initialize the block */
141
142 q->next = q->prev = NULL;
143 q->destinations = NULL;
144 Ustrcpy(q->name, name);
145 q->seen = TRUE;
146 q->frozen = FALSE;
147 q->dir_char = dir_char;
148 q->sender = NULL;
149 q->size = 0;
150
151 /* Read the header file from the spool; if there is a failure it might mean
152 inaccessibility as a result of protections. A successful read will have caused
153 sender_address to get set and the recipients fields to be initialized. If
154 there's a format error in the headers, we can still display info from the
155 envelope.
156
157 Before reading the header remember the position in the dynamic store so that
158 we can recover the store into which the header is read. All data read by
159 spool_read_header that is to be preserved is copied into malloc store. */
160
161 reset_point = store_get(0);
162 message_size = 0;
163 message_subdir[0] = dir_char;
164 sprintf(CS buffer, "%s-H", name);
165 rc =  spool_read_header(buffer, FALSE, TRUE);
166 save_errno = errno;
167
168 /* If we failed to read the envelope, compute the input time by
169 interpreting the id as a base-62 number. */
170
171 if (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,
179 if it's there. */
180
181 else
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
189 message. If the envelope was read, this appears as a first recipient;
190 otherwise it sets set up in the sender field. */
191
192 if (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
221 q->frozen = deliver_freeze;
222
223 if (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   }
237 else
238   {
239   q->sender = store_malloc(Ustrlen(sender_address) + 1);
240   Ustrcpy(q->sender, sender_address);
241   }
242
243 sender_address = NULL;
244
245 sprintf(CS buffer, "%s/input/%s/%s-D", spool_directory, message_subdir, name);
246 if (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
250 been delivered, and removing visible names. */
251
252 if (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
268 store_reset(reset_point);
269 return 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,
279 to speed up searches, an index into the list is used. This is maintained
280 by the scan_spool_input function when it goes down the list throwing
281 out entries that are no longer needed. When the action is "add" and
282 we don't need to add, mark the found item as seen. */
283
284
285 #ifdef never
286 static void debug_queue(void)
287 {
288 int i;
289 int count = 0;
290 queue_item *p;
291 printf("\nqueue_total=%d\n", queue_total);
292
293 for (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
297 printf("Queue is:\n");
298 p = queue_index[0];
299 while (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
314 queue_item *find_queue(uschar *name, int action, int dir_char)
315 {
316 int first = 0;
317 int last = queue_index_size - 1;
318 int middle = (first + last)/2;
319 queue_item *p, *q, *qq;
320
321 /* Handle the empty queue as a special case. */
322
323 if (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
337 as special cases. */
338
339 if (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
353 if (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
368 when the name is somewhere in the middle, if present. */
369
370 while (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
378 lie if it exists. Both end points are inclusive - though in fact
379 the bottom one can only be = if it is the original bottom. */
380
381 p = queue_index[first];
382 q = queue_index[last];
383
384 for (;;)
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
438 code knows to look for them. We count the entries to set the value for the
439 queue stripchart, and set up data for the queue display window if the "full"
440 option is given. */
441
442 void scan_spool_input(int full)
443 {
444 int i;
445 int subptr;
446 int subdir_max = 1;
447 int count = 0;
448 int indexptr = 1;
449 queue_item *p;
450 struct dirent *ent;
451 DIR *dd;
452 uschar input_dir[256];
453 uschar subdirs[64];
454
455 subdirs[0] = 0;
456 stripchart_total[0] = 0;
457
458 sprintf(CS input_dir, "%s/input", spool_directory);
459 subptr = Ustrlen(input_dir);
460 input_dir[subptr+2] = 0;               /* terminator for lengthened name */
461
462 /* Loop for each spool file on the queue - searching any subdirectories that
463 may exist. When initializing eximon, every file will have to be read. To show
464 there is progress, output a dot for each one to the standard output. */
465
466 for (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
512 items in the in-store queue. */
513
514 if (!full || queue_total == 0) return;
515
516 /* Now scan the queue and remove any items that were not in the directory. At
517 the same time, set up the index pointers into the queue. Because we are
518 removing items, the total that we are comparing against isn't actually correct,
519 but in a long queue it won't make much difference, and in a short queue it
520 doesn't matter anyway!*/
521
522 p = queue_index[0];
523 while (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
554 have got the index all filled in yet. Make sure all the pointers
555 are legal. */
556
557 while (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,
571 or if there is a journal file in existence. */
572
573 /* First, a local subroutine to scan the non-recipients tree and
574 remove any of them from the address list */
575
576 static void
577 scan_tree(queue_item *p, tree_node *tn)
578 {
579 if (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
589 static void update_recipients(queue_item *p)
590 {
591 int i;
592 FILE *jread;
593 void *reset_point;
594 struct stat statdata;
595 uschar buffer[1024];
596
597 message_subdir[0] = p->dir_char;
598
599 sprintf(CS buffer, "%s/input/%s/%s-J", spool_directory, message_subdir, p->name);
600 jread = fopen(CS buffer, "r");
601 if (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.
609 Arrange to recover the dynamic store afterwards. */
610
611 reset_point = store_get(0);
612 sprintf(CS buffer, "%s-H", p->name);
613 if (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
622 if (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
634 been delivered, and removing visible names. In the nonrecipients tree,
635 domains are lower cased. */
636
637 if (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
665 contain child addresses that are not in the recipients list, but
666 which may have got onto the address list as a result of eximon
667 noticing an == line in the log. Then remember the update time,
668 recover the dynamic store, and we are done. */
669
670 scan_tree(p, tree_nonrecipients);
671 p->update_time = statdata.st_mtime;
672 store_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
682 time. Take some care to keep the scrolled position as it previously was, but,
683 if it was at the bottom, keep it at the bottom. Take note of any hide list, and
684 time out the entries as appropriate. */
685
686 void
687 queue_display(void)
688 {
689 int now = (int)time(NULL);
690 queue_item *p = queue_index[0];
691
692 if (menu_is_up) return;            /* Avoid nasty interactions */
693
694 text_empty(queue_widget);
695
696 while (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 */