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