doc tweaks
[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
a2da3176
JH
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
a2da3176
JH
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 {
a2da3176
JH
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
659if (recipients_list != NULL)
660 {
661 for (i = 0; i < recipients_count; i++)
662 {
663 uschar *pp;
664 uschar *r = recipients_list[i].address;
665 tree_node *node = tree_search(tree_nonrecipients, r);
666
667 if (node == NULL)
668 {
669 uschar temp[256];
670 uschar *rr = temp;
671 Ustrcpy(temp, r);
672 while (*rr != 0 && *rr != '@') rr++;
673 while (*rr != 0) { *rr = tolower(*rr); rr++; }
674 node = tree_search(tree_nonrecipients, temp);
675 }
676
677 if ((pp = strstric(r+1, qualify_domain, FALSE)) != NULL &&
678 *(--pp) == '@') *pp = 0;
679 if (node == NULL)
680 (void)find_dest(p, r, dest_add, FALSE);
681 else
682 (void)find_dest(p, r, dest_remove, FALSE);
683 }
684 }
685
686/* We also need to scan the tree of non-recipients, which might
687contain child addresses that are not in the recipients list, but
688which may have got onto the address list as a result of eximon
689noticing an == line in the log. Then remember the update time,
690recover the dynamic store, and we are done. */
691
692scan_tree(p, tree_nonrecipients);
693p->update_time = statdata.st_mtime;
694store_reset(reset_point);
695}
696
697
698
699/*************************************************
700* Display queue data *
701*************************************************/
702
703/* The present implementation simple re-writes the entire information each
704time. Take some care to keep the scrolled position as it previously was, but,
705if it was at the bottom, keep it at the bottom. Take note of any hide list, and
706time out the entries as appropriate. */
707
708void
709queue_display(void)
710{
711int now = (int)time(NULL);
712queue_item *p = queue_index[0];
713
714if (menu_is_up) return; /* Avoid nasty interactions */
715
716text_empty(queue_widget);
717
718while (p != NULL)
719 {
720 int count = 1;
721 dest_item *dd, *ddd;
722 uschar u = 'm';
723 int t = (now - p->input_time)/60; /* minutes on queue */
724
725 if (t > 90)
726 {
727 u = 'h';
728 t = (t + 30)/60;
729 if (t > 72)
730 {
731 u = 'd';
732 t = (t + 12)/24;
733 if (t > 99) /* someone had > 99 days */
734 {
735 u = 'w';
736 t = (t + 3)/7;
737 if (t > 99) /* so, just in case */
738 {
739 u = 'y';
740 t = (t + 26)/52;
741 }
742 }
743 }
744 }
745
746 update_recipients(p); /* update destinations */
747
748 /* Can't set this earlier, as header data may change things. */
749
750 dd = p->destinations;
751
752 /* Check to see if this message is on the hide list; if any hide
753 item has timed out, remove it from the list. Hide if all destinations
754 are on the hide list. */
755
756 for (ddd = dd; ddd != NULL; ddd = ddd->next)
757 {
758 skip_item *sk;
759 skip_item **skp;
760 int len_address;
761
762 if (ddd->address[0] == '*') break;
763 len_address = Ustrlen(ddd->address);
764
765 for (skp = &queue_skip; ; skp = &(sk->next))
766 {
767 int len_skip;
768
769 sk = *skp;
770 while (sk != NULL && now >= sk->reveal)
771 {
772 *skp = sk->next;
773 store_free(sk);
774 sk = *skp;
775 if (queue_skip == NULL)
776 {
777 XtDestroyWidget(unhide_widget);
778 unhide_widget = NULL;
779 }
780 }
781 if (sk == NULL) break;
782
783 /* If this address matches the skip item, break (sk != NULL) */
784
785 len_skip = Ustrlen(sk->text);
786 if (len_skip <= len_address &&
787 Ustrcmp(ddd->address + len_address - len_skip, sk->text) == 0)
788 break;
789 }
790
791 if (sk == NULL) break;
792 }
793
794 /* Don't use more than one call of anon() in one statement - it uses
795 a fixed static buffer. */
796
797 if (ddd != NULL || dd == NULL)
798 {
799 text_showf(queue_widget, "%c%2d%c %s %s %-8s ",
800 (p->frozen)? '*' : ' ',
801 t, u,
802 string_format_size(p->size, big_buffer),
803 p->name,
804 (p->sender == NULL)? US" " :
805 (p->sender[0] == 0)? US"<> " : anon(p->sender));
806
807 text_showf(queue_widget, "%s%s%s",
808 (dd == NULL || dd->address[0] == '*')? "" : "<",
809 (dd == NULL)? US"" : anon(dd->address),
810 (dd == NULL || dd->address[0] == '*')? "" : ">");
811
812 if (dd != NULL && dd->parent != NULL && dd->parent->address[0] != '*')
813 text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
814
815 text_show(queue_widget, US"\n");
816
817 if (dd != NULL) dd = dd->next;
818 while (dd != NULL && count++ < queue_max_addresses)
819 {
820 text_showf(queue_widget, " <%s>",
821 anon(dd->address));
822 if (dd->parent != NULL && dd->parent->address[0] != '*')
823 text_showf(queue_widget, " parent <%s>", anon(dd->parent->address));
824 text_show(queue_widget, US"\n");
825 dd = dd->next;
826 }
827 if (dd != NULL)
828 text_showf(queue_widget, " ...\n");
829 }
830
831 p = p->next;
832 }
833}
834
835/* End of em_queue.c */