Removed mhn, as it was already replaced by mhlist/mhshow/mhstore.
[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  * An empty list is returned as first=last=NULL.
223  */
224 static void
225 check_folders(struct node **first, struct node **last,
226               struct node **cur_node, size_t *maxlen,
227               char *cur, char *folders, char *sequences[])
228 {
229     struct list_state b;
230     FILE *fp;
231     char *line;
232     size_t len;
233
234     *first = *last = *cur_node = NULL;
235     *maxlen = 0;
236
237     b.first = first;
238     b.cur_node = cur_node;
239     b.maxlen = maxlen;
240     b.cur = cur;
241     b.sequences = sequences;
242
243     if (folders == NULL) {
244         chdir(m_maildir(""));
245         crawl_folders(".", crawl_callback, &b);
246     } else {
247         fp = fopen(folders, "r");
248         if (fp  == NULL) {
249             adios(NULL, "failed to read %s", folders);
250         }
251         while (vfgets(fp, &line) == OK) {
252             len = strlen(line) - 1;
253             line[len] = '\0';
254             check_folder(getcpy(line), len, &b);
255         }
256         fclose(fp);
257     }
258
259     if (*first != NULL) {
260         b.node->n_next = NULL;
261         *last = b.node;
262     }
263 }
264
265 /* Return a single string of the `sequences' joined by a space (' '). */
266 static char *
267 join_sequences(char *sequences[])
268 {
269     int i;
270     size_t len = 0;
271     char *result, *cp;
272
273     for (i = 0; sequences[i] != NULL; i++) {
274         len += strlen(sequences[i]) + 1;
275     }
276     result = mh_xmalloc(len + 1);
277
278     for (i = 0, cp = result; sequences[i] != NULL; i++, cp += len + 1) {
279         len = strlen(sequences[i]);
280         memcpy(cp, sequences[i], len);
281         cp[len] = ' ';
282     }
283     /* -1 to overwrite the last delimiter */
284     *--cp = '\0';
285
286     return result;
287 }
288
289 /* Return a struct node for the folder to change to.  This is the next
290  * (previous, if FPREV mode) folder with desired messages, or the current
291  * folder if no folders have desired.  If NEW or UNSEEN mode, print the
292  * output but don't change folders.
293  *
294  * n_name is the folder to change to, and n_field is the string list of
295  * desired message numbers.
296  */
297 static struct node *
298 doit(char *cur, char *folders, char *sequences[])
299 {
300     struct node *first, *cur_node, *node, *last, *prev;
301     size_t folder_len;
302     int count, total = 0;
303     char *command = NULL, *sequences_s = NULL;
304
305     if (cur == NULL || cur[0] == '\0') {
306         cur = "inbox";
307     }
308
309     check_folders(&first, &last, &cur_node, &folder_len, cur,
310                   folders, sequences);
311
312     if (run_mode == FNEXT || run_mode == FPREV) {
313         if (first == NULL) {
314             /* No folders at all... */
315             return NULL;
316         } else if (first->n_next == NULL) {
317             /* We have only one node; any desired messages in it? */
318             if (first->n_field == NULL) {
319                 return NULL;
320             } else {
321                 return first;
322             }
323         } else if (cur_node == NULL) {
324             /* Current folder is not listed in .folders, return first. */
325             return first;
326         }
327     } else if (run_mode == UNSEEN) {
328         sequences_s = join_sequences(sequences);
329     }
330
331     for (node = first, prev = NULL;
332          node != NULL;
333          prev = node, node = node->n_next) {
334         if (run_mode == FNEXT) {
335             /* If we have a previous node and it is the current
336              * folder, return this node. */
337             if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
338                 return node;
339             }
340         } else if (run_mode == FPREV) {
341             if (strcmp(node->n_name, cur) == 0) {
342                 /* Found current folder in fprev mode; if we have a
343                  * previous node in the list, return it; else return
344                  * the last node. */
345                 if (prev == NULL) {
346                     return last;
347                 }
348                 return prev;
349             }
350         } else if (run_mode == UNSEEN) {
351             if (node->n_field == NULL) {
352                 continue;
353             }
354
355             printf("\n%d %s messages in %s",
356                    count_messages(node->n_field),
357                    sequences_s,
358                    node->n_name);
359             if (strcmp(node->n_name, cur) == 0) {
360                 puts(" (*: current folder)");
361             } else {
362                 puts("");
363             }
364             fflush(stdout);
365
366             /* TODO: Split enough of scan.c out so that we can call it here. */
367             command = concat("scan +", node->n_name, " ", sequences_s,
368                              (void *)NULL);
369             system(command);
370             free(command);
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",
380                    (int) folder_len, node->n_name,
381                    count,
382                    (strcmp(node->n_name, cur) == 0 ? '*' : ' '),
383                    node->n_field);
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     if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
390         return first;
391     }
392
393     if (run_mode == NEW) {
394         printf("%-*s %6d.\n", (int) folder_len, " total", total);
395     }
396
397     return cur_node;
398 }
399
400 int
401 main(int argc, char **argv)
402 {
403     char **ap, *cp, **argp, **arguments;
404     char help[BUFSIZ];
405     char *folders = NULL;
406     char *sequences[NUMATTRS + 1];
407     int i = 0;
408     char *unseen;
409     struct node *folder;
410
411 #ifdef LOCALE
412     setlocale(LC_ALL, "");
413 #endif
414     invo_name = r1bindex(argv[0], '/');
415
416     /* read user profile/context */
417     context_read();
418
419     arguments = getarguments (invo_name, argc, argv, 1);
420     argp = arguments;
421
422     /*
423      * Parse arguments
424      */
425     while ((cp = *argp++)) {
426         if (*cp == '-') {
427             switch (smatch (++cp, switches)) {
428             case AMBIGSW:
429                 ambigsw (cp, switches);
430                 done (1);
431             case UNKWNSW:
432                 adios (NULL, "-%s unknown", cp);
433
434             case HELPSW:
435                 snprintf (help, sizeof(help), "%s [switches] [sequences]",
436                           invo_name);
437                 print_help (help, switches, 1);
438                 done (1);
439             case VERSIONSW:
440                 print_version(invo_name);
441                 done (1);
442
443             case FOLDERSSW:
444                 if (!(folders = *argp++) || *folders == '-')
445                     adios(NULL, "missing argument to %s", argp[-2]);
446                 continue;
447             case MODESW:
448                 if (!(invo_name = *argp++) || *invo_name == '-')
449                     adios(NULL, "missing argument to %s", argp[-2]);
450                 invo_name = r1bindex(invo_name, '/');
451                 continue;
452             }
453         }
454         /* have a sequence argument */
455         if (!seq_in_list(cp, sequences)) {
456             sequences[i++] = cp;
457         }
458     }
459
460     if (strcmp(invo_name, "fnext") == 0) {
461         run_mode = FNEXT;
462     } else if (strcmp(invo_name, "fprev") == 0) {
463         run_mode = FPREV;
464     } else if (strcmp(invo_name, "unseen") == 0) {
465         run_mode = UNSEEN;
466     }
467
468     if (folders == NULL) {
469         /* will flists */
470     } else {
471         if (folders[0] != '/') {
472             folders = m_maildir(folders);
473         }
474     }
475
476     if (i == 0) {
477         /* no sequence arguments; use unseen */
478         unseen = context_find(usequence);
479         if (unseen == NULL || unseen[0] == '\0') {
480             adios(NULL, "must specify sequences or set %s", usequence);
481         }
482         for (ap = brkstring(unseen, " ", "\n"); *ap; ap++) {
483             sequences[i++] = *ap;
484         }
485     }
486     sequences[i] = NULL;
487
488     folder = doit(context_find(pfolder), folders, sequences);
489     if (folder == NULL) {
490         done(0);
491         return 1;
492     }
493
494     if (run_mode == UNSEEN) {
495         /* All the scan(1)s it runs change the current folder, so we
496          * need to put it back.  Unfortunately, context_replace lamely
497          * ignores the new value you give it if it is the same one it
498          * has in memory.  So, we'll be lame, too.  I'm not sure if i
499          * should just change context_replace... */
500         context_replace(pfolder, "defeat_context_replace_optimization");
501     }
502
503     /* update current folder */
504     context_replace(pfolder, folder->n_name);
505
506     if (run_mode == FNEXT || run_mode == FPREV) {
507         printf("%s  %s\n", folder->n_name, folder->n_field);
508     }
509
510     context_save();
511
512     done (0);
513     return 1;
514 }