]> mj.ucw.cz Git - checkmail.git/blob - cm.c
3a4630e63762b8e4d582882773665d1c71322dde
[checkmail.git] / cm.c
1 /*
2  *      Incoming Mail Checker
3  *
4  *      (c) 2005 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <stdio.h>
8 #include <string.h>
9 #include <stdlib.h>
10 #include <ctype.h>
11 #include <getopt.h>
12 #include <fcntl.h>
13 #include <glob.h>
14 #include <fnmatch.h>
15 #include <sys/stat.h>
16 #include <unistd.h>
17 #include <pwd.h>
18 #include <time.h>
19 #include <curses.h>
20
21 #include "util.h"
22 #include "clists.h"
23
24 static int check_interval = 30;
25 static int force_refresh;
26 static int allow_bells = 1;
27 static int minimum_priority;
28 static time_t last_scan_time;
29 static char *run_cmd = "mutt -f %s";
30
31 struct options {
32   int priority;
33   int hide;
34   int hide_if_empty;
35   int highlight;
36   int beep;
37   int snippets;
38 };
39
40 struct option_node {
41   cnode n;
42   struct options o;
43   char pattern[1];
44 };
45
46 struct pattern_node {
47   cnode n;
48   char *name;
49   char pattern[1];
50 };
51
52 static clist options, patterns;
53 static struct options global_options;
54
55 struct mbox {
56   cnode n;
57   struct options o;
58   char *name;
59   char *path;
60   int index;
61   int scanning;
62   int seen;
63   time_t last_time;
64   int last_size, last_pos;
65   int total, new;
66   int last_total, last_new;
67   int last_beep_new;
68   int force_refresh;
69   char snippet[256];
70 };
71
72 static clist mboxes;
73 static struct mbox **mbox_array;
74 static int num_mboxes, mbox_array_size;
75
76 static void redraw_line(int i);
77 static void rethink_display(void);
78
79 static void
80 add_pattern(char *patt)
81 {
82   struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
83   strcpy(n->pattern, patt);
84   if (patt = strchr(n->pattern, '='))
85     {
86       *patt++ = 0;
87       n->name = patt;
88     }
89   else
90     n->name = NULL;
91   clist_add_tail(&patterns, &n->n);
92 }
93
94 static void
95 add_inbox(void)
96 {
97   struct passwd *p = getpwuid(getuid());
98   if (!p)
99     die("You don't exist, go away!");
100   char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
101   sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
102   add_pattern(buf);
103 }
104
105 static void
106 init_options(struct options *o)
107 {
108   o->priority = -1;
109   o->hide = -1;
110   o->hide_if_empty = -1;
111   o->beep = -1;
112   o->highlight = -1;
113   o->snippets = -1;
114 }
115
116 static void
117 setup_options(struct mbox *b)
118 {
119   b->o = global_options;
120   CLIST_FOR_EACH(struct option_node *, n, options)
121     if (!fnmatch(n->pattern, b->name, 0))
122       {
123         debug("\tApplied options %s\n", n->pattern);
124 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
125         MERGE(priority);
126         MERGE(hide);
127         MERGE(hide_if_empty);
128         MERGE(highlight);
129         MERGE(beep);
130         MERGE(snippets);
131       }
132 }
133
134 static char *
135 mbox_name(char *path)
136 {
137   char *c = strrchr(path, '/');
138   return c ? (c+1) : path;
139 }
140
141 static struct mbox *
142 add_mbox(clist *l, char *path, char *name)
143 {
144   struct mbox *b = xmalloc(sizeof(*b));
145   bzero(b, sizeof(*b));
146   b->path = xstrdup(path);
147   b->name = xstrdup(name);
148
149   if (name)
150     {
151       cnode *prev = l->head.prev;
152       while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
153         prev = prev->prev;
154       clist_insert_after(&b->n, prev);
155     }
156   else
157     clist_add_tail(l, &b->n);
158
159   return b;
160 }
161
162 static void
163 del_mbox(struct mbox *b)
164 {
165   clist_remove(&b->n);
166   free(b->path);
167   free(b->name);
168   free(b);
169 }
170
171 static struct mbox *
172 find_mbox(clist *l, char *path)
173 {
174   CLIST_FOR_EACH(struct mbox *, b, *l)
175     if (!strcmp(b->path, path))
176       return b;
177   return NULL;
178 }
179
180 static inline int
181 mbox_active_p(struct mbox *b)
182 {
183   if (b->o.priority < minimum_priority)
184     return 0;
185   if (b->o.hide)
186     return 0;
187   return 1;
188 }
189
190 static inline int
191 mbox_visible_p(struct mbox *b)
192 {
193   if (!mbox_active_p(b))
194     return 0;
195   if (b->scanning < 0)
196     return 1;
197   if (b->o.hide_if_empty && !b->total)
198     return 0;
199   return 1;
200 }
201
202 static void
203 add_snippet(char **ppos, char *term, unsigned char *add)
204 {
205   char *pos = *ppos;
206   int space = 1;
207   while (*add && pos < term)
208     {
209       if (*add <= ' ')
210         {
211           if (!space)
212             *pos++ = ' ';
213           space = 1;
214         }
215       else if (*add >= 0x7f)
216         {
217           *pos++ = '?';
218           space = 0;
219         }
220       else
221         {
222           *pos++ = *add;
223           space = 0;
224         }
225       add++;
226     }
227   *ppos = pos;
228   *pos = 0;
229 }
230
231 static void
232 prepare_snippet(struct mbox *b, char *sender, char *subject)
233 {
234   while (*sender == ' ' || *sender == '\t')
235     sender++;
236   while (*subject == ' ' || *subject == '\t')
237     subject++;
238
239   char *pos = b->snippet;
240   char *term = b->snippet + sizeof(b->snippet) - 1;
241   if (sender[0])
242     {
243       add_snippet(&pos, term, sender);
244       add_snippet(&pos, term, ": ");
245     }
246   if (subject[0])
247     add_snippet(&pos, term, subject);
248   else
249     add_snippet(&pos, term, "No subject");
250 }
251
252 static int mb_fd, mb_pos;
253 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
254
255 static void
256 mb_reset(int pos)
257 {
258   mb_cc = mb_end = mb_buf;
259   mb_pos = pos;
260 }
261
262 static void
263 mb_seek(uns pos)
264 {
265   lseek(mb_fd, pos, SEEK_SET);
266   mb_reset(pos);
267 }
268
269 static int
270 mb_tell(void)
271 {
272   return mb_pos - (mb_end - mb_cc);
273 }
274
275 static int
276 mb_ll_get(void)
277 {
278   int len = read(mb_fd, mb_buf, sizeof(mb_buf));
279   mb_cc = mb_buf;
280   if (len <= 0)
281     {
282       mb_end = mb_buf;
283       return -1;
284     }
285   else
286     {
287       mb_end = mb_buf + len;
288       mb_pos += len;
289       return *mb_cc++;
290     }
291 }
292
293 static inline int
294 mb_get(void)
295 {
296   return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
297 }
298
299 static void
300 mb_unget(int c)
301 {
302   if (c >= 0)
303     mb_cc--;
304 }
305
306 static int
307 mb_check(const char *p, int len)
308 {
309   while (len--)
310     {
311       if (mb_get() != *p++)
312         return 0;
313     }
314   return 1;
315 }
316
317 static void
318 scan_mbox(struct mbox *b, struct stat *st)
319 {
320   char buf[1024], sender[1024], subject[1024];
321   int c;
322   const char from[] = "\nFrom ";
323
324   if (!st->st_size)
325     {
326       b->total = b->new = 0;
327       b->last_pos = 0;
328       return;
329     }
330
331   /* FIXME: Locking! */
332
333   mb_fd = open(b->path, O_RDONLY);
334   if (mb_fd < 0)
335     {
336       debug("[open failed: %m] ");
337       b->total = b->new = -1;
338       return;
339     }
340   mb_reset(0);
341
342   int incremental = 0;
343   if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
344     {
345       mb_seek(b->last_pos);
346       if (mb_check(from, 6))
347         {
348           debug("[incremental] ");
349           incremental = 1;
350         }
351       else
352         {
353           debug("[incremental failed] ");
354           mb_seek(0);
355         }
356     }
357   if (!incremental)
358     {
359       if (!mb_check(from+1, 5))
360         {
361           debug("[inconsistent] ");
362           b->total = b->new = -1;
363           goto done;
364         }
365       b->total = b->new = 0;
366       b->last_total = b->last_new = 0;
367     }
368   else
369     {
370       b->total = b->last_total;
371       b->new = b->last_new;
372     }
373
374   for(;;)
375     {
376       b->last_pos = mb_tell() - 5;
377       if (b->last_pos)
378         b->last_pos--;          // last_pos should be the previous \n character
379       b->last_total = b->total;
380       b->last_new = b->new;
381       while ((c = mb_get()) >= 0 && c != '\n')
382         ;
383
384       int new = 1;
385       sender[0] = 0;
386       subject[0] = 0;
387       for (;;)
388         {
389           uns i = 0;
390           for (;;)
391             {
392               c = mb_get();
393               if (c < 0)
394                 {
395                   debug("[truncated] ");
396                   goto done;
397                 }
398               if (c == '\n')
399                 {
400                   int fold = -1;
401                   do
402                     {
403                       fold++;
404                       c = mb_get();
405                     }
406                   while (c == ' ' || c == '\t');
407                   mb_unget(c);
408                   if (!fold)
409                     break;
410                   c = ' ';
411                 }
412               if (c == '\r')
413                 continue;
414               if (i < sizeof(buf) - 1)
415                 buf[i++] = c;
416             }
417           buf[i] = 0;
418           if (!buf[0])
419             break;
420           if (!strncasecmp(buf, "Status:", 7))
421             new = 0;
422           else if (!strncasecmp(buf, "From:", 5))
423             strcpy(sender, buf+5);
424           else if (!strncasecmp(buf, "Subject:", 8))
425             strcpy(subject, buf+8);
426         }
427
428       b->total++;
429       if (new)
430         {
431           b->new++;
432           prepare_snippet(b, sender, subject);
433         }
434
435       int ct = 1;
436       while (from[ct])
437         {
438           c = mb_get();
439           if (c < 0)
440             goto done;
441           if (c != from[ct++])
442             ct = (c == '\n');
443         }
444     }
445
446  done:
447   close(mb_fd);
448 }
449
450 static void
451 scan(void)
452 {
453   debug("Searching for mailboxes...\n");
454   last_scan_time = time(NULL);
455   CLIST_FOR_EACH(struct pattern_node *, p, patterns)
456     {
457       debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
458       glob_t g;
459       int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
460       if (err && err != GLOB_NOMATCH)
461         die("Failed to glob %s: %m", p->pattern);
462       for (uns i=0; i<g.gl_pathc; i++)
463         {
464           char *name = g.gl_pathv[i];
465           struct mbox *b = find_mbox(&mboxes, name);
466           if (!b)
467             {
468               b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
469               debug("Discovered mailbox %s (%s)\n", b->name, b->path);
470               setup_options(b);
471               b->scanning = -1;
472             }
473           b->seen = 1;
474         }
475       globfree(&g);
476     }
477
478   struct mbox *tmp;
479   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
480     {
481       if (b->seen)
482         b->seen = 0;
483       else
484         {
485           debug("Lost mailbox %s\n", b->name);
486           del_mbox(b);
487         }
488     }
489
490   rethink_display();
491
492   debug("Scanning mailboxes...\n");
493   CLIST_FOR_EACH(struct mbox *, b, mboxes)
494     {
495       struct stat st;
496       debug("%s: ", b->name);
497       if (!mbox_active_p(b))
498         {
499           debug("inactive\n");
500           continue;
501         }
502       if (force_refresh)
503         b->force_refresh = 1;
504       if (stat(b->path, &st) < 0)
505         {
506           b->total = b->new = -1;
507           debug("%m\n");
508         }
509       else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
510         {
511           b->scanning = 1;
512           redraw_line(b->index);
513           refresh();
514
515           scan_mbox(b, &st);
516           b->last_time = st.st_mtime;
517           b->last_size = st.st_size;
518           debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
519
520           b->scanning = 0;
521           redraw_line(b->index);
522           refresh();
523         }
524       else
525         debug("not changed\n");
526       b->force_refresh = 0;
527     }
528   force_refresh = 0;
529
530   debug("Scan finished\n");
531   last_scan_time = time(NULL);
532   rethink_display();
533 }
534
535 static int cursor_at, cursor_max;
536
537 enum {
538   M_IDLE,
539   M_SCAN,
540   M_NEW,
541   M_BAD,
542   M_MAX
543 };
544 static int attrs[2][2][M_MAX];          // active, hilite, status
545
546 static void
547 redraw_line(int i)
548 {
549   move(i, 0);
550   if (i < cursor_max)
551     {
552       struct mbox *b = mbox_array[i];
553       int cc = (cursor_at == i);
554       int hi = b->o.highlight;
555
556       attrset(attrs[cc][hi][M_IDLE]);
557       if (cc)
558         printw("> ");
559       else
560         printw("  ");
561       if (b->new)
562         attrset(attrs[cc][hi][M_NEW]);
563       printw("%-20s ", b->name);
564       if (b->scanning < 0)
565         ;
566       else if (b->scanning)
567         {
568           attrset(attrs[cc][hi][M_SCAN]);
569           printw("[SCANNING]");
570         }
571       else if (b->total < 0)
572         {
573           attrset(attrs[cc][hi][M_BAD]);
574           printw("BROKEN");
575         }
576       else
577         {
578           attrset(attrs[cc][hi][M_IDLE]);
579           printw("%6d ", b->total);
580           if (b->new)
581             {
582               attrset(attrs[cc][hi][M_NEW]);
583               printw("%6d  ", b->new);
584               attrset(attrs[cc][hi][M_IDLE]);
585               int age = (last_scan_time - b->last_time);
586               if (age < 0)
587                 age = 0;
588               if (age < 3600)
589                 printw("%2d min  ", age/60);
590               else if (age < 86400)
591                 printw("%2d hrs  ", age/3600);
592               else
593                 printw("        ");
594               if (b->o.snippets && b->snippet[0])
595                 {
596                   int xx, yy;
597                   getyx(stdscr, yy, xx);
598                   int remains = COLS-1-xx;
599                   if (remains > 2)
600                     printw("%-.*s", remains, b->snippet);
601                 }
602             }
603         }
604     }
605   attrset(attrs[0][0][M_IDLE]);
606   clrtoeol();
607 }
608
609 static void
610 redraw_all(void)
611 {
612   cursor_max = num_mboxes;
613   if (cursor_max > LINES-1)
614     cursor_max = LINES-1;
615   if (cursor_at >= cursor_max)
616     cursor_at = cursor_max - 1;
617   if (cursor_at < 0)
618     cursor_at = 0;
619
620   for (int i=0; i<cursor_max; i++)
621     redraw_line(i);
622   move(cursor_max, 0);
623   if (!cursor_max)
624     {
625       printw("(no mailboxes found)");
626       clrtoeol();
627       move(1, 0);
628     }
629   clrtobot();
630 }
631
632 static void
633 rethink_display(void)
634 {
635   int i = 0;
636   int changed = 0;
637   int beeeep = 0;
638   CLIST_FOR_EACH(struct mbox *, b, mboxes)
639     if (mbox_visible_p(b))
640       {
641         b->index = i;
642         if (i >= num_mboxes || mbox_array[i] != b)
643           {
644             changed = 1;
645             if (i >= mbox_array_size)
646               {
647                 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
648                 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
649               }
650             mbox_array[i] = b;
651           }
652         if (b->o.beep && b->new > b->last_beep_new)
653           beeeep = 1;
654         b->last_beep_new = b->new;
655         i++;
656       }
657   if (i != num_mboxes)
658     changed = 1;
659   num_mboxes = i;
660
661   if (changed)
662     {
663       redraw_all();
664       refresh();
665     }
666   if (beeeep && allow_bells)
667     beep();
668 }
669
670 static void
671 term_init(void)
672 {
673   initscr();
674   cbreak();
675   noecho();
676   nonl();
677   intrflush(stdscr, FALSE);
678   keypad(stdscr, TRUE);
679   curs_set(0);
680
681   static const int attrs_mono[2][M_MAX] = {
682     [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
683     [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
684   };
685   for (int i=0; i<2; i++)
686     for (int j=0; j<M_MAX; j++)
687       {
688         attrs[0][i][j] = attrs_mono[i][j];
689         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
690       }
691
692   if (has_colors())
693     {
694       start_color();
695       if (COLOR_PAIRS >= 5)
696         {
697           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
698           init_pair(2, COLOR_RED, COLOR_BLACK);
699           init_pair(3, COLOR_WHITE, COLOR_BLUE);
700           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
701           init_pair(5, COLOR_RED, COLOR_BLUE);
702           static const int attrs_color[2][2][M_MAX] = {
703             [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
704             [0][1] = { [M_IDLE] = A_BOLD, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1) | A_BOLD, [M_BAD] = COLOR_PAIR(2) | A_BOLD },
705             [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
706             [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD, [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4) | A_BOLD, [M_BAD] = COLOR_PAIR(5) | A_BOLD },
707           };
708           memcpy(attrs, attrs_color, sizeof(attrs));
709         }
710     }
711 }
712
713 static void
714 term_cleanup(void)
715 {
716   endwin();
717 }
718
719 static void
720 print_status(char *status)
721 {
722   move(LINES-1, 0);
723   if (status)
724     printw("%s", status);
725   clrtoeol();
726   refresh();
727 }
728
729 static void
730 scan_and_redraw(void)
731 {
732   print_status("Busy...");
733   scan();
734   print_status(NULL);
735 }
736
737 static void
738 move_cursor(int i)
739 {
740   if (i >= 0 && i < cursor_max && i != cursor_at)
741     {
742       int old = cursor_at;
743       cursor_at = i;
744       redraw_line(old);
745       redraw_line(i);
746     }
747 }
748
749 static void
750 next_active(int since, int step)
751 {
752   if (!cursor_max)
753     return;
754   since = (since+cursor_max) % cursor_max;
755   step = (step+cursor_max) % cursor_max;
756   int besti = -1;
757   int bestp = -1;
758   int i = since;
759   do
760     {
761       struct mbox *b = mbox_array[i];
762       if (b->new && b->o.priority > bestp)
763         {
764           besti = i;
765           bestp = b->o.priority;
766         }
767       i = (i+step) % cursor_max;
768     }
769   while (i != since);
770   if (besti >= 0)
771     move_cursor(besti);
772 }
773
774 #define STR2(c) #c
775 #define STR(c) STR2(c)
776
777 static void NONRET
778 usage(void)
779 {
780   fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
781 \n\
782 Options:\n\
783 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
784 -d\t\t\tLog debug messages to stderr\n\
785 -i\t\t\tInclude user's INBOX\n\
786 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
787 -o <pattern>=<opts>\tSet mailbox options\n\
788 -o <opts>\t\tSet default options for all mailboxes\n\
789 -p <pri>\t\tSet minimum priority to show\n\
790 \n\
791 Mailbox options (set with `-o', use upper case to negate):\n\
792 0-9\t\t\tSet mailbox priority (0=default)\n\
793 b\t\t\tBeep when a message arrives\n\
794 e\t\t\tHide from display if empty\n\
795 h\t\t\tHide from display\n\
796 s\t\t\tShow message snippets\n\
797 t\t\t\tHighlight the entry\n\
798 \n\
799 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
800 It can be freely distributed and used according to the GNU GPL v2.\n\
801 ");
802   exit(1);
803 }
804
805 static void
806 parse_options(char *c)
807 {
808   struct options *o;
809   char *sep;
810   if (sep = strchr(c, '='))
811     {
812       struct option_node *n = xmalloc(sizeof(*n) + sep-c);
813       memcpy(n->pattern, c, sep-c);
814       n->pattern[sep-c] = 0;
815       clist_add_tail(&options, &n->n);
816       o = &n->o;
817       init_options(o);
818       c = sep+1;
819     }
820   else
821     o = &global_options;
822
823   int x;
824   while (x = *c++)
825     if (x >= '0' && x <= '9')
826       o->priority = x - '0';
827     else
828       {
829         int value = !!islower(x);
830         switch (tolower(x))
831           {
832           case 'b':
833             o->beep = value;
834             break;
835           case 'e':
836             o->hide_if_empty = value;
837             break;
838           case 'h':
839             o->hide = value;
840             break;
841           case 's':
842             o->snippets = value;
843             break;
844           case 't':
845             o->highlight = value;
846             break;
847           default:
848             fprintf(stderr, "Invalid mailbox option `%c'\n", x);
849             usage();
850           }
851       }
852 }
853
854 int
855 main(int argc, char **argv)
856 {
857   clist_init(&mboxes);
858   clist_init(&options);
859   clist_init(&patterns);
860
861   int c;
862   while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
863     switch (c)
864       {
865       case 'c':
866         check_interval = atol(optarg);
867         if (check_interval <= 0)
868           usage();
869         break;
870       case 'd':
871         debug_mode++;
872         break;
873       case 'i':
874         add_inbox();
875         break;
876       case 'm':
877         run_cmd = optarg;
878         break;
879       case 'o':
880         parse_options(optarg);
881         break;
882       case 'p':
883         minimum_priority = atol(optarg);
884         break;
885       default:
886         usage();
887       }
888   while (optind < argc)
889     add_pattern(argv[optind++]);
890
891   term_init();
892   scan_and_redraw();
893   next_active(0, 1);
894
895   int should_exit = 0;
896   while (!should_exit)
897     {
898       time_t now = time(NULL);
899       int remains = last_scan_time + check_interval - now;
900       if (remains <= 0 || force_refresh)
901         scan_and_redraw();
902       else
903         {
904           remains *= 10;
905           halfdelay((remains > 255) ? 255 : remains);
906           int ch = getch();
907           switch (ch)
908             {
909             case 'q':
910               should_exit = 1;
911               break;
912             case 'j':
913             case KEY_DOWN:
914               move_cursor(cursor_at+1);
915               break;
916             case 'k':
917             case KEY_UP:
918               move_cursor(cursor_at-1);
919               break;
920             case '^':
921             case KEY_HOME:
922             case KEY_PPAGE:
923               move_cursor(0);
924               break;
925             case '$':
926             case KEY_END:
927             case KEY_NPAGE:
928               move_cursor(cursor_max-1);
929               break;
930             case '\t':
931               next_active(cursor_at+1, 1);
932               break;
933             case '`':
934               next_active(cursor_at-1, -1);
935               break;
936             case '\r':
937             case '\n':
938               if (cursor_at < cursor_max)
939                 {
940                   struct mbox *b = mbox_array[cursor_at];
941                   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
942                   sprintf(cmd, run_cmd, b->path);
943                   term_cleanup();
944                   system(cmd);
945                   term_init();
946                   redraw_all();
947                   refresh();
948                   b->force_refresh = 1;
949                   scan_and_redraw();
950                 }
951               break;
952             case 'l' & 0x1f:
953               clearok(stdscr, TRUE);
954               redraw_all();
955               refresh();
956               break;
957             case 'r' & 0x1f:
958               force_refresh = 1;
959               break;
960             case 'b':
961               allow_bells = 1;
962               print_status("Bells and whistles are now enabled. Toot!");
963               break;
964             case 'B':
965               allow_bells = 0;
966               print_status("Bells and whistles are now disabled. Pssst!");
967               break;
968             default:
969               if (ch >= '0' && ch <= '9')
970                 {
971                   minimum_priority = ch - '0';
972                   scan_and_redraw();
973                 }
974               else
975                 debug("Pressed unknown key %d\n", ch);
976             }
977           refresh();
978         }
979     }
980
981   term_cleanup();
982   return 0;
983 }