* test/runtest, test/tests/inc/test-deb359167,
[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  * $Id$
8  *
9  * This code is Copyright (c) 2008, by the authors of nmh.  See the
10  * COPYRIGHT file in the root directory of the nmh distribution for
11  * complete copyright information.
12  *
13  * Inspired by Luke Mewburn's new: http://www.mewburn.net/luke/src/new
14  */
15
16 #include <sys/types.h>
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21
22 #include <h/mh.h>
23 #include <h/crawl_folders.h>
24 #include <h/utils.h>
25
26 static struct swit switches[] = {
27 #define MODESW 0
28     { "mode", 1 },
29 #define FOLDERSSW 1
30     { "folders", 1 },
31 #define VERSIONSW 2
32     { "version", 1 },
33 #define HELPSW 3
34     { "help", 1 },
35     { NULL, 0 }
36 };
37
38 static enum { NEW, FNEXT, FPREV, UNSEEN } run_mode = NEW;
39
40 /* check_folders uses this to maintain state with both .folders list of
41  * folders and with crawl_folders. */
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 = getcpy(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     free(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 /* Return the string list of message numbers from the sequences file, or NULL
92  * if none. */
93 static char *
94 get_msgnums(char *folder, char *sequences[])
95 {
96     char *seqfile = concat(m_maildir(folder), "/", mh_seq, (void *)NULL);
97     FILE *fp = fopen(seqfile, "r");
98     int state;
99     char name[NAMESZ], field[BUFSIZ];
100     char *cp;
101     char *msgnums = NULL, *this_msgnums, *old_msgnums;
102
103     /* no sequences file -> no messages */
104     if (fp == NULL) {
105         return NULL;
106     }
107
108     /* copied from seq_read.c:seq_public */
109     for (state = FLD;;) {
110         switch (state = m_getfld (state, name, field, sizeof(field), fp)) {
111             case FLD:
112             case FLDPLUS:
113             case FLDEOF:
114                 if (state == FLDPLUS) {
115                     cp = getcpy (field);
116                     while (state == FLDPLUS) {
117                         state = m_getfld (state, name, field,
118                                           sizeof(field), fp);
119                         cp = add (field, cp);
120                     }
121
122                     /* Here's where we differ from seq_public: if it's in a
123                      * sequence we want, save the list of messages. */
124                     if (seq_in_list(name, sequences)) {
125                         this_msgnums = trimcpy(cp);
126                         if (msgnums == NULL) {
127                             msgnums = this_msgnums;
128                         } else {
129                             old_msgnums = msgnums;
130                             msgnums = concat(old_msgnums, " ",
131                                              this_msgnums, (void *)NULL);
132                             free(old_msgnums);
133                             free(this_msgnums);
134                         }
135                     }
136                     free (cp);
137                 } else {
138                     /* and here */
139                     if (seq_in_list(name, sequences)) {
140                         this_msgnums = trimcpy(field);
141                         if (msgnums == NULL) {
142                             msgnums = this_msgnums;
143                         } else {
144                             old_msgnums = msgnums;
145                             msgnums = concat(old_msgnums, " ",
146                                              this_msgnums, (void *)NULL);
147                             free(old_msgnums);
148                             free(this_msgnums);
149                         }
150                     }
151                 }
152
153                 if (state == FLDEOF)
154                     break;
155                 continue;
156
157             case BODY:
158             case BODYEOF:
159                 adios (NULL, "no blank lines are permitted in %s", seqfile);
160                 /* fall */
161
162             case FILEEOF:
163                 break;
164
165             default:
166                 adios (NULL, "%s is poorly formatted", seqfile);
167         }
168         break;  /* break from for loop */
169     }
170
171     fclose(fp);
172
173     return msgnums;
174 }
175
176 /* Check `folder' (of length `len') for interesting messages, filling in the
177  * list in `b'. */
178 static void
179 check_folder(char *folder, size_t len, struct list_state *b)
180 {
181     char *msgnums = get_msgnums(folder, b->sequences);
182     int is_cur = strcmp(folder, b->cur) == 0;
183
184     if (is_cur || msgnums != NULL) {
185         if (*b->first == NULL) {
186             *b->first = b->node = mh_xmalloc(sizeof(*b->node));
187         } else {
188             b->node->n_next = mh_xmalloc(sizeof(*b->node));
189             b->node = b->node->n_next;
190         }
191         b->node->n_name = folder;
192         b->node->n_field = msgnums;
193
194         if (*b->maxlen < len) {
195             *b->maxlen = len;
196         }
197     }
198
199     /* Save the node for the current folder, so we can fall back to it. */
200     if (is_cur) {
201         *b->cur_node = b->node;
202     }
203 }
204
205 static boolean
206 crawl_callback(char *folder, void *baton)
207 {
208     check_folder(folder, strlen(folder), baton);
209     return TRUE;
210 }
211
212 /* Scan folders, returning:
213  * first        -- list of nodes for all folders which have desired messages;
214  *                 if the current folder is listed in .folders, it is also in
215  *                 the list regardless of whether it has any desired messages
216  * last         -- last node in list
217  * cur_node     -- node of current folder, if listed in .folders
218  * maxlen       -- length of longest folder name
219  *
220  * `cur' points to the name of the current folder, `folders' points to the
221  * name of a .folder (if NULL, crawl all folders), and `sequences' points to
222  * the array of sequences for which to look.
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 = *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, *sequences_s;
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->n_next == NULL) {
314             /* We have only one node; any desired messages in it? */
315             if (first->n_field == NULL) {
316                 return NULL;
317             } else {
318                 return first;
319             }
320         } else if (cur_node == NULL) {
321             /* Current folder is not listed in .folders, return first. */
322             return first;
323         }
324     } else if (run_mode == UNSEEN) {
325         sequences_s = join_sequences(sequences);
326     }
327
328     for (node = first, prev = NULL;
329          node != NULL;
330          prev = node, node = node->n_next) {
331         if (run_mode == FNEXT) {
332             /* If we have a previous node and it is the current
333              * folder, return this node. */
334             if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
335                 return node;
336             }
337         } else if (run_mode == FPREV) {
338             if (strcmp(node->n_name, cur) == 0) {
339                 /* Found current folder in fprev mode; if we have a
340                  * previous node in the list, return it; else return
341                  * the last node. */
342                 if (prev == NULL) {
343                     return last;
344                 }
345                 return prev;
346             }
347         } else if (run_mode == UNSEEN) {
348             if (node->n_field == NULL) {
349                 continue;
350             }
351
352             printf("\n%d %s messages in %s",
353                    count_messages(node->n_field),
354                    sequences_s,
355                    node->n_name);
356             if (strcmp(node->n_name, cur) == 0) {
357                 puts(" (*: current folder)");
358             } else {
359                 puts("");
360             }
361             fflush(stdout);
362
363             /* TODO: Split enough of scan.c out so that we can call it here. */
364             command = concat("scan +", node->n_name, " ", sequences_s,
365                              (void *)NULL);
366             system(command);
367             free(command);
368         } else {
369             if (node->n_field == NULL) {
370                 continue;
371             }
372
373             count = count_messages(node->n_field);
374             total += count;
375
376             printf("%-*s %6d.%c %s\n",
377                    folder_len, node->n_name,
378                    count,
379                    (strcmp(node->n_name, cur) == 0 ? '*' : ' '),
380                    node->n_field);
381         }
382     }
383
384     /* If we're fnext, we haven't checked the last node yet.  If it's the
385      * current folder, return the first node. */
386     if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
387         return first;
388     }
389
390     if (run_mode == NEW) {
391         printf("%-*s %6d.\n", folder_len, " total", total);
392     }
393
394     return cur_node;
395 }
396
397 int
398 main(int argc, char **argv)
399 {
400     char **ap, *cp, **argp, **arguments;
401     char help[BUFSIZ];
402     char *folders = NULL;
403     char *sequences[NUMATTRS + 1];
404     int i = 0;
405     char *unseen;
406     struct node *folder;
407
408 #ifdef LOCALE
409     setlocale(LC_ALL, "");
410 #endif
411     invo_name = r1bindex(argv[0], '/');
412
413     /* read user profile/context */
414     context_read();
415
416     arguments = getarguments (invo_name, argc, argv, 1);
417     argp = arguments;
418
419     /*
420      * Parse arguments
421      */
422     while ((cp = *argp++)) {
423         if (*cp == '-') {
424             switch (smatch (++cp, switches)) {
425             case AMBIGSW:
426                 ambigsw (cp, switches);
427                 done (1);
428             case UNKWNSW:
429                 adios (NULL, "-%s unknown", cp);
430
431             case HELPSW:
432                 snprintf (help, sizeof(help), "%s [switches] [sequences]",
433                           invo_name);
434                 print_help (help, switches, 1);
435                 done (1);
436             case VERSIONSW:
437                 print_version(invo_name);
438                 done (1);
439
440             case FOLDERSSW:
441                 if (!(folders = *argp++) || *folders == '-')
442                     adios(NULL, "missing argument to %s", argp[-2]);
443                 continue;
444             case MODESW:
445                 if (!(invo_name = *argp++) || *invo_name == '-')
446                     adios(NULL, "missing argument to %s", argp[-2]);
447                 invo_name = r1bindex(invo_name, '/');
448                 continue;
449             }
450         }
451         /* have a sequence argument */
452         if (!seq_in_list(cp, sequences)) {
453             sequences[i++] = cp;
454         }
455     }
456
457     if (strcmp(invo_name, "fnext") == 0) {
458         run_mode = FNEXT;
459     } else if (strcmp(invo_name, "fprev") == 0) {
460         run_mode = FPREV;
461     } else if (strcmp(invo_name, "unseen") == 0) {
462         run_mode = UNSEEN;
463     }
464
465     if (folders == NULL) {
466         /* will flists */
467     } else {
468         if (folders[0] != '/') {
469             folders = m_maildir(folders);
470         }
471     }
472
473     if (i == 0) {
474         /* no sequence arguments; use unseen */
475         unseen = context_find(usequence);
476         if (unseen == NULL || unseen[0] == '\0') {
477             adios(NULL, "must specify sequences or set %s", usequence);
478         }
479         for (ap = brkstring(unseen, " ", "\n"); *ap; ap++) {
480             sequences[i++] = *ap;
481         }
482     }
483     sequences[i] = NULL;
484
485     folder = doit(context_find(pfolder), folders, sequences);
486     if (folder == NULL) {
487         done(0);
488         return 1;
489     }
490
491     if (run_mode == UNSEEN) {
492         /* All the scan(1)s it runs change the current folder, so we
493          * need to put it back.  Unfortunately, context_replace lamely
494          * ignores the new value you give it if it is the same one it
495          * has in memory.  So, we'll be lame, too.  I'm not sure if i
496          * should just change context_replace... */
497         context_replace(pfolder, "defeat_context_replace_optimization");
498     }
499
500     /* update current folder */
501     context_replace(pfolder, folder->n_name);
502
503     if (run_mode == FNEXT || run_mode == FPREV) {
504         printf("%s  %s\n", folder->n_name, folder->n_field);
505     }
506
507     context_save();
508
509     done (0);
510     return 1;
511 }