Remove RCS keywords, since they no longer work after git migration.
[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->n_next == NULL) {
312             /* We have only one node; any desired messages in it? */
313             if (first->n_field == NULL) {
314                 return NULL;
315             } else {
316                 return first;
317             }
318         } else if (cur_node == NULL) {
319             /* Current folder is not listed in .folders, return first. */
320             return first;
321         }
322     } else if (run_mode == UNSEEN) {
323         sequences_s = join_sequences(sequences);
324     }
325
326     for (node = first, prev = NULL;
327          node != NULL;
328          prev = node, node = node->n_next) {
329         if (run_mode == FNEXT) {
330             /* If we have a previous node and it is the current
331              * folder, return this node. */
332             if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
333                 return node;
334             }
335         } else if (run_mode == FPREV) {
336             if (strcmp(node->n_name, cur) == 0) {
337                 /* Found current folder in fprev mode; if we have a
338                  * previous node in the list, return it; else return
339                  * the last node. */
340                 if (prev == NULL) {
341                     return last;
342                 }
343                 return prev;
344             }
345         } else if (run_mode == UNSEEN) {
346             if (node->n_field == NULL) {
347                 continue;
348             }
349
350             printf("\n%d %s messages in %s",
351                    count_messages(node->n_field),
352                    sequences_s,
353                    node->n_name);
354             if (strcmp(node->n_name, cur) == 0) {
355                 puts(" (*: current folder)");
356             } else {
357                 puts("");
358             }
359             fflush(stdout);
360
361             /* TODO: Split enough of scan.c out so that we can call it here. */
362             command = concat("scan +", node->n_name, " ", sequences_s,
363                              (void *)NULL);
364             system(command);
365             free(command);
366         } else {
367             if (node->n_field == NULL) {
368                 continue;
369             }
370
371             count = count_messages(node->n_field);
372             total += count;
373
374             printf("%-*s %6d.%c %s\n",
375                    (int) folder_len, node->n_name,
376                    count,
377                    (strcmp(node->n_name, cur) == 0 ? '*' : ' '),
378                    node->n_field);
379         }
380     }
381
382     /* If we're fnext, we haven't checked the last node yet.  If it's the
383      * current folder, return the first node. */
384     if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
385         return first;
386     }
387
388     if (run_mode == NEW) {
389         printf("%-*s %6d.\n", (int) folder_len, " total", total);
390     }
391
392     return cur_node;
393 }
394
395 int
396 main(int argc, char **argv)
397 {
398     char **ap, *cp, **argp, **arguments;
399     char help[BUFSIZ];
400     char *folders = NULL;
401     char *sequences[NUMATTRS + 1];
402     int i = 0;
403     char *unseen;
404     struct node *folder;
405
406 #ifdef LOCALE
407     setlocale(LC_ALL, "");
408 #endif
409     invo_name = r1bindex(argv[0], '/');
410
411     /* read user profile/context */
412     context_read();
413
414     arguments = getarguments (invo_name, argc, argv, 1);
415     argp = arguments;
416
417     /*
418      * Parse arguments
419      */
420     while ((cp = *argp++)) {
421         if (*cp == '-') {
422             switch (smatch (++cp, switches)) {
423             case AMBIGSW:
424                 ambigsw (cp, switches);
425                 done (1);
426             case UNKWNSW:
427                 adios (NULL, "-%s unknown", cp);
428
429             case HELPSW:
430                 snprintf (help, sizeof(help), "%s [switches] [sequences]",
431                           invo_name);
432                 print_help (help, switches, 1);
433                 done (1);
434             case VERSIONSW:
435                 print_version(invo_name);
436                 done (1);
437
438             case FOLDERSSW:
439                 if (!(folders = *argp++) || *folders == '-')
440                     adios(NULL, "missing argument to %s", argp[-2]);
441                 continue;
442             case MODESW:
443                 if (!(invo_name = *argp++) || *invo_name == '-')
444                     adios(NULL, "missing argument to %s", argp[-2]);
445                 invo_name = r1bindex(invo_name, '/');
446                 continue;
447             }
448         }
449         /* have a sequence argument */
450         if (!seq_in_list(cp, sequences)) {
451             sequences[i++] = cp;
452         }
453     }
454
455     if (strcmp(invo_name, "fnext") == 0) {
456         run_mode = FNEXT;
457     } else if (strcmp(invo_name, "fprev") == 0) {
458         run_mode = FPREV;
459     } else if (strcmp(invo_name, "unseen") == 0) {
460         run_mode = UNSEEN;
461     }
462
463     if (folders == NULL) {
464         /* will flists */
465     } else {
466         if (folders[0] != '/') {
467             folders = m_maildir(folders);
468         }
469     }
470
471     if (i == 0) {
472         /* no sequence arguments; use unseen */
473         unseen = context_find(usequence);
474         if (unseen == NULL || unseen[0] == '\0') {
475             adios(NULL, "must specify sequences or set %s", usequence);
476         }
477         for (ap = brkstring(unseen, " ", "\n"); *ap; ap++) {
478             sequences[i++] = *ap;
479         }
480     }
481     sequences[i] = NULL;
482
483     folder = doit(context_find(pfolder), folders, sequences);
484     if (folder == NULL) {
485         done(0);
486         return 1;
487     }
488
489     if (run_mode == UNSEEN) {
490         /* All the scan(1)s it runs change the current folder, so we
491          * need to put it back.  Unfortunately, context_replace lamely
492          * ignores the new value you give it if it is the same one it
493          * has in memory.  So, we'll be lame, too.  I'm not sure if i
494          * should just change context_replace... */
495         context_replace(pfolder, "defeat_context_replace_optimization");
496     }
497
498     /* update current folder */
499     context_replace(pfolder, folder->n_name);
500
501     if (run_mode == FNEXT || run_mode == FPREV) {
502         printf("%s  %s\n", folder->n_name, folder->n_field);
503     }
504
505     context_save();
506
507     done (0);
508     return 1;
509 }