better error handling
[mmh] / uip / new.c
1 /*
2 ** new.c -- as new,    list all folders with unseen messages
3 **       -- as fnext,  move to next folder with unseen messages
4 **       -- as fprev,  move to previous folder with unseen messages
5 **       -- as unseen, scan all unseen messages
6 ** This code is Copyright (c) 2008, by the authors of nmh.  See the
7 ** COPYRIGHT file in the root directory of the nmh distribution for
8 ** complete copyright information.
9 **
10 ** Inspired by Luke Mewburn's new: http://www.mewburn.net/luke/src/new
11 */
12
13 #include <h/mh.h>
14 #include <h/crawl_folders.h>
15 #include <h/utils.h>
16 #include <sys/types.h>
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <unistd.h>
21 #include <locale.h>
22 #include <sysexits.h>
23
24 static struct swit switches[] = {
25 #define MODESW 0
26         { "mode", 0 },
27 #define FOLDERSSW 1
28         { "folders", 0 },
29 #define VERSIONSW 2
30         { "Version", 0 },
31 #define HELPSW 3
32         { "help", 0 },
33         { NULL, 0 }
34 };
35
36 static enum { NEW, FNEXT, FPREV, UNSEEN } run_mode = NEW;
37
38 /*
39 ** check_folders uses this to maintain state with both .folders list of
40 ** folders and with crawl_folders.
41 */
42 struct list_state {
43         struct node **first, **cur_node;
44         size_t *maxlen;
45         char *cur;
46         char **sequences;
47         struct node *node;
48 };
49
50 /* Return the number of messages in a string list of message numbers. */
51 static int
52 count_messages(char *field)
53 {
54         int total = 0;
55         int j, k;
56         char *cp, **ap;
57
58         field = mh_xstrdup(field);
59
60         /* copied from seq_read.c:seq_init */
61         for (ap = brkstring(field, " ", "\n"); *ap; ap++) {
62                 if ((cp = strchr(*ap, '-')))
63                         *cp++ = '\0';
64                 if ((j = m_atoi(*ap)) > 0) {
65                         k = cp ? m_atoi(cp) : j;
66
67                         total += k - j + 1;
68                 }
69         }
70
71         mh_free0(&field);
72
73         return total;
74 }
75
76 /* Return TRUE if the sequence 'name' is in 'sequences'. */
77 static boolean
78 seq_in_list(char *name, char *sequences[])
79 {
80         int i;
81
82         for (i = 0; sequences[i] != NULL; i++) {
83                 if (strcmp(name, sequences[i]) == 0) {
84                         return TRUE;
85                 }
86         }
87
88         return FALSE;
89 }
90
91 /*
92 ** Return the string list of message numbers from the sequences file,
93 ** or NULL if none.
94 */
95 static char *
96 get_msgnums(char *folder, char *sequences[])
97 {
98         enum state state;
99         struct field f = {{0}};
100         char *seqfile = concat(toabsdir(folder), "/", mh_seq, (void *)NULL);
101         FILE *fp = fopen(seqfile, "r");
102         char *msgnums = NULL, *this_msgnums, *old_msgnums;
103
104         /* no sequences file -> no messages */
105         if (fp == NULL) {
106                 return NULL;
107         }
108
109         for (state = FLD2;;) {
110                 switch (state = m_getfld2(state, &f, fp)) {
111                 case LENERR2:
112                         state = FLD2;
113                         /* FALL */
114
115                 case FLD2:
116                         /*
117                         ** if it's in a sequence we want,
118                         ** save the list of messages.
119                         */
120                         if (seq_in_list(f.name, sequences)) {
121                                 this_msgnums = trimcpy(f.value);
122                                 if (msgnums == NULL) {
123                                         msgnums = this_msgnums;
124                                 } else {
125                                         old_msgnums = msgnums;
126                                         msgnums = concat(old_msgnums, " ",
127                                                         this_msgnums,
128                                                         NULL);
129                                         mh_free0(&old_msgnums);
130                                         mh_free0(&this_msgnums);
131                                 }
132                         }
133                         continue;
134
135                 case BODY2:
136                         adios(EX_DATAERR, NULL, "no blank lines are permitted in %s", seqfile);
137                         /* FALL */
138
139                 case FILEEOF2:
140                         break;
141
142                 default:
143                         adios(EX_SOFTWARE, NULL, "%s is poorly formatted", seqfile);
144                 }
145                 break;
146         }
147
148         fclose(fp);
149
150         return msgnums;
151 }
152
153 /*
154 ** Check `folder' (of length `len') for interesting messages,
155 ** filling in the list in `b'.
156 */
157 static void
158 check_folder(char *folder, size_t len, struct list_state *b)
159 {
160         char *msgnums = get_msgnums(folder, b->sequences);
161         int is_cur = strcmp(folder, b->cur) == 0;
162
163         if (is_cur || msgnums != NULL) {
164                 if (*b->first == NULL) {
165                         *b->first = b->node = mh_xcalloc(1, sizeof(*b->node));
166                 } else {
167                         b->node->n_next = mh_xcalloc(1, sizeof(*b->node));
168                         b->node = b->node->n_next;
169                 }
170                 b->node->n_name = folder;
171                 b->node->n_field = msgnums;
172
173                 if (*b->maxlen < len) {
174                         *b->maxlen = len;
175                 }
176         }
177
178         /* Save the node for the current folder, so we can fall back to it. */
179         if (is_cur) {
180                 *b->cur_node = b->node;
181         }
182 }
183
184 static boolean
185 crawl_callback(char *folder, void *baton)
186 {
187         check_folder(folder, strlen(folder), baton);
188         return TRUE;
189 }
190
191 /*
192 ** Scan folders, returning:
193 ** first        -- list of nodes for all folders which have desired messages;
194 **                 if the current folder is listed in .folders, it is also in
195 **                 the list regardless of whether it has any desired messages
196 ** last         -- last node in list
197 ** cur_node     -- node of current folder, if listed in .folders
198 ** maxlen       -- length of longest folder name
199 **
200 ** `cur' points to the name of the current folder, `folders' points to the
201 ** name of a .folder (if NULL, crawl all folders), and `sequences' points to
202 ** the array of sequences for which to look.
203 **
204 ** An empty list is returned as first=last=NULL.
205 */
206 static void
207 check_folders(struct node **first, struct node **last,
208         struct node **cur_node, size_t *maxlen,
209         char *cur, char *folders, char *sequences[])
210 {
211         struct list_state b;
212         FILE *fp;
213         char *line;
214         size_t len;
215
216         *first = *last = *cur_node = NULL;
217         *maxlen = 0;
218
219         b.first = first;
220         b.cur_node = cur_node;
221         b.maxlen = maxlen;
222         b.cur = cur;
223         b.sequences = sequences;
224
225         if (folders == NULL) {
226                 chdir(toabsdir("+"));
227                 crawl_folders(".", crawl_callback, &b);
228         } else {
229                 fp = fopen(folders, "r");
230                 if (fp  == NULL) {
231                         adios(EX_IOERR, NULL, "failed to read %s", folders);
232                 }
233                 while (vfgets(fp, &line) == OK) {
234                         len = strlen(line) - 1;
235                         line[len] = '\0';
236                         check_folder(mh_xstrdup(line), len, &b);
237                 }
238                 fclose(fp);
239         }
240
241         if (*first != NULL) {
242                 b.node->n_next = NULL;
243                 *last = b.node;
244         }
245 }
246
247 /* Return a single string of the `sequences' joined by a space (' '). */
248 static char *
249 join_sequences(char *sequences[])
250 {
251         int i;
252         size_t len = 0;
253         char *result, *cp;
254
255         for (i = 0; sequences[i] != NULL; i++) {
256                 len += strlen(sequences[i]) + 1;
257         }
258         result = mh_xcalloc(len + 1, sizeof(char));
259
260         for (i = 0, cp = result; sequences[i] != NULL; i++, cp += len + 1) {
261                 len = strlen(sequences[i]);
262                 memcpy(cp, sequences[i], len);
263                 cp[len] = ' ';
264         }
265         /* -1 to overwrite the last delimiter */
266         *--cp = '\0';
267
268         return result;
269 }
270
271 /*
272 ** Return a struct node for the folder to change to.  This is the next
273 ** (previous, if FPREV mode) folder with desired messages, or the current
274 ** folder if no folders have desired.  If NEW or UNSEEN mode, print the
275 ** output but don't change folders.
276 **
277 ** n_name is the folder to change to, and n_field is the string list of
278 ** desired message numbers.
279 */
280 static struct node *
281 doit(char *cur, char *folders, char *sequences[])
282 {
283         struct node *first, *cur_node, *node, *last = NULL, *prev;
284         size_t folder_len;
285         int count, total = 0;
286         char *sequences_s = NULL;
287         int argc = 0;
288         char *argv[MAXARGS];
289         char **seqp;
290         char buf[BUFSIZ];
291
292         if (cur == NULL || cur[0] == '\0') {
293                 cur = "inbox";
294         }
295
296         check_folders(&first, &last, &cur_node, &folder_len, cur,
297                         folders, sequences);
298
299         if (run_mode == FNEXT || run_mode == FPREV) {
300                 if (first == NULL) {
301                         /* No folders at all... */
302                         return NULL;
303                 } else if (first->n_next == NULL) {
304                         /*
305                         ** We have only one node; any desired messages in it?
306                         */
307                         if (first->n_field == NULL) {
308                                 return NULL;
309                         } else {
310                                 return first;
311                         }
312                 } else if (cur_node == NULL) {
313                         /*
314                         ** Current folder is not listed in .folders,
315                         ** return first.
316                         */
317                         return first;
318                 }
319         } else if (run_mode == UNSEEN) {
320                 sequences_s = join_sequences(sequences);
321         }
322
323         for (node = first, prev = NULL;
324                  node != NULL;
325                  prev = node, node = node->n_next) {
326                 if (run_mode == FNEXT) {
327                         /*
328                         ** If we have a previous node and it is the current
329                         ** folder, return this node.
330                         */
331                         if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
332                                 return node;
333                         }
334                 } else if (run_mode == FPREV) {
335                         if (strcmp(node->n_name, cur) == 0) {
336                                 /*
337                                 ** Found current folder in fprev mode;
338                                 ** if we have a previous node in the list,
339                                 ** return it; else return the last node.
340                                 */
341                                 if (prev == NULL) {
342                                         return last;
343                                 }
344                                 return prev;
345                         }
346                 } else if (run_mode == UNSEEN) {
347                         if (node->n_field == NULL) {
348                                 continue;
349                         }
350
351                         printf("\n%d %s messages in %s",
352                                    count_messages(node->n_field),
353                                    sequences_s,
354                                    node->n_name);
355                         if (strcmp(node->n_name, cur) == 0) {
356                                 puts(" (*: current folder)");
357                         } else {
358                                 puts("");
359                         }
360                         fflush(stdout);
361
362                         argc = 0;
363                         argv[argc++] = "scan";
364                         snprintf(buf, sizeof buf, "+%s", node->n_name);
365                         argv[argc++] = buf;
366                         for (seqp=sequences; *seqp; seqp++) {
367                                 argv[argc++] = *seqp;
368                         }
369                         argv[argc] = (char *)NULL;
370                         execprog(*argv, argv);
371                 } else {
372                         if (node->n_field == NULL) {
373                                 continue;
374                         }
375
376                         count = count_messages(node->n_field);
377                         total += count;
378
379                         printf("%-*s %6d.%c %s\n", (int) folder_len,
380                                 node->n_name, count,
381                                 (strcmp(node->n_name, cur) == 0 ? '*' : ' '),
382                                 node->n_field);
383                 }
384         }
385
386         /*
387         ** If we're fnext, we haven't checked the last node yet.  If it's the
388         ** current folder, return the first node.
389         */
390         if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
391                 return first;
392         }
393
394         if (run_mode == NEW) {
395                 printf("%-*s %6d.\n", (int) folder_len, " total", total);
396         }
397
398         return cur_node;
399 }
400
401 int
402 main(int argc, char **argv)
403 {
404         char **ap, *cp, **argp, **arguments;
405         char help[BUFSIZ];
406         char *folders = NULL;
407         char *sequences[NUMATTRS + 1];
408         int i = 0;
409         char *unseen;
410         struct node *folder;
411
412         sequences[0] = NULL;
413         sequences[1] = NULL;
414
415         setlocale(LC_ALL, "");
416         invo_name = mhbasename(argv[0]);
417
418         /* read user profile/context */
419         context_read();
420
421         arguments = getarguments(invo_name, argc, argv, 1);
422         argp = arguments;
423
424         /*
425         ** Parse arguments
426         */
427         while ((cp = *argp++)) {
428                 if (*cp == '-') {
429                         switch (smatch(++cp, switches)) {
430                         case AMBIGSW:
431                                 ambigsw(cp, switches);
432                                 exit(EX_USAGE);
433                         case UNKWNSW:
434                                 adios(EX_USAGE, NULL, "-%s unknown", cp);
435
436                         case HELPSW:
437                                 snprintf(help, sizeof(help),
438                                                 "%s [switches] [sequences]",
439                                                 invo_name);
440                                 print_help(help, switches, 1);
441                                 exit(argc == 2 ? EX_OK : EX_USAGE);
442                         case VERSIONSW:
443                                 print_version(invo_name);
444                                 exit(argc == 2 ? EX_OK : EX_USAGE);
445
446                         case FOLDERSSW:
447                                 if (!(folders = *argp++) || *folders == '-')
448                                         adios(EX_USAGE, NULL, "missing argument to %s",
449                                                         argp[-2]);
450                                 continue;
451                         case MODESW:
452                                 if (!(invo_name = *argp++) || *invo_name == '-')
453                                         adios(EX_USAGE, NULL, "missing argument to %s",
454                                                         argp[-2]);
455                                 invo_name = mhbasename(invo_name);
456                                 continue;
457                         }
458                 }
459                 /* have a sequence argument */
460                 if (!seq_in_list(cp, sequences)) {
461                         sequences[i++] = cp;
462                         sequences[i] = NULL;
463                 }
464         }
465
466         if (strcmp(invo_name, "fnext") == 0) {
467                 run_mode = FNEXT;
468         } else if (strcmp(invo_name, "fprev") == 0) {
469                 run_mode = FPREV;
470         } else if (strcmp(invo_name, "unseen") == 0) {
471                 run_mode = UNSEEN;
472         }
473
474         if (folders == NULL) {
475                 /* will flists */
476         } else {
477                 if (folders[0] != '/') {
478                         folders = toabsdir(folders);
479                 }
480         }
481
482         if (i == 0) {
483                 char *dp;
484                 /* no sequence arguments; use unseen */
485                 if ((unseen = context_find(usequence))) {
486                         if (!*unseen) {
487                                 adios(EX_CONFIG, NULL, "profile entry %s set, but empty, and no sequences given", usequence);
488                         }
489                 } else {
490                         unseen = seq_unseen;  /* use default */
491                 }
492                 dp = mh_xstrdup(unseen);
493                 for (ap = brkstring(dp, " ", "\n"); *ap; ap++) {
494                         sequences[i++] = *ap;
495                 }
496         }
497         sequences[i] = NULL;
498
499         folder = doit(context_find(curfolder), folders, sequences);
500         if (folder == NULL) {
501                 exit(EX_OK);
502                 return 1;
503         }
504
505         if (run_mode == UNSEEN) {
506                 /*
507                 ** All the scan(1)s it runs change the current folder, so we
508                 ** need to put it back.  Unfortunately, context_replace lamely
509                 ** ignores the new value you give it if it is the same one it
510                 ** has in memory.  So, we'll be lame, too.  I'm not sure if i
511                 ** should just change context_replace...
512                 */
513                 context_replace(curfolder, "defeat_context_replace_optimization");
514         }
515
516         /* update current folder */
517         context_replace(curfolder, folder->n_name);
518
519         if (run_mode == FNEXT || run_mode == FPREV) {
520                 printf("%s  %s\n", folder->n_name, folder->n_field);
521         }
522
523         context_save();
524
525         return 0;
526 }