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