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