Remove dead code: FLDEOF and BODYEOF are never set
[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 = 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 /*
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         char *seqfile = concat(toabsdir(folder), "/", mh_seq, (void *)NULL);
99         FILE *fp = fopen(seqfile, "r");
100         int state;
101         char name[NAMESZ], field[BUFSIZ];
102         char *cp;
103         char *msgnums = NULL, *this_msgnums, *old_msgnums;
104
105         /* no sequences file -> no messages */
106         if (fp == NULL) {
107                 return NULL;
108         }
109
110         /* copied from seq_read.c:seq_public */
111         for (state = FLD;;) {
112                 switch (state = m_getfld(state, name, field, sizeof(field),
113                                 fp)) {
114                 case FLD:
115                 case FLDPLUS:
116                         if (state == FLDPLUS) {
117                                 cp = getcpy(field);
118                                 while (state == FLDPLUS) {
119                                         state = m_getfld(state, name, field,
120                                                         sizeof(field), fp);
121                                         cp = add(field, cp);
122                                 }
123
124                                 /*
125                                 ** Here's where we differ from
126                                 ** seq_public: if it's in a
127                                 ** sequence we want, save the list
128                                 ** of messages.
129                                 */
130                                 if (seq_in_list(name, sequences)) {
131                                         this_msgnums = trimcpy(cp);
132                                         if (msgnums == NULL) {
133                                                 msgnums = this_msgnums;
134                                         } else {
135                                                 old_msgnums = msgnums;
136                                                 msgnums = concat(old_msgnums, " ", this_msgnums, (void *)NULL);
137                                                 free(old_msgnums);
138                                                 free(this_msgnums);
139                                         }
140                                 }
141                                 free(cp);
142                         } else {
143                                 /* and here */
144                                 if (seq_in_list(name, sequences)) {
145                                         this_msgnums = trimcpy(field);
146                                         if (msgnums == NULL) {
147                                                 msgnums = this_msgnums;
148                                         } else {
149                                                 old_msgnums = msgnums;
150                                                 msgnums = concat(old_msgnums, " ", this_msgnums, (void *)NULL);
151                                                 free(old_msgnums);
152                                                 free(this_msgnums);
153                                         }
154                                 }
155                         }
156
157                         continue;
158
159                 case BODY:
160                         adios(EX_DATAERR, NULL, "no blank lines are permitted in %s",
161                                         seqfile);
162                         /* fall */
163
164                 case FILEEOF:
165                         break;
166
167                 default:
168                         adios(EX_SOFTWARE, NULL, "%s is poorly formatted", seqfile);
169                 }
170                 break;  /* break from for loop */
171         }
172
173         fclose(fp);
174
175         return msgnums;
176 }
177
178 /*
179 ** Check `folder' (of length `len') for interesting messages,
180 ** filling in the list in `b'.
181 */
182 static void
183 check_folder(char *folder, size_t len, struct list_state *b)
184 {
185         char *msgnums = get_msgnums(folder, b->sequences);
186         int is_cur = strcmp(folder, b->cur) == 0;
187
188         if (is_cur || msgnums != NULL) {
189                 if (*b->first == NULL) {
190                         *b->first = b->node = mh_xmalloc(sizeof(*b->node));
191                 } else {
192                         b->node->n_next = mh_xmalloc(sizeof(*b->node));
193                         b->node = b->node->n_next;
194                 }
195                 b->node->n_name = folder;
196                 b->node->n_field = msgnums;
197
198                 if (*b->maxlen < len) {
199                         *b->maxlen = len;
200                 }
201         }
202
203         /* Save the node for the current folder, so we can fall back to it. */
204         if (is_cur) {
205                 *b->cur_node = b->node;
206         }
207 }
208
209 static boolean
210 crawl_callback(char *folder, void *baton)
211 {
212         check_folder(folder, strlen(folder), baton);
213         return TRUE;
214 }
215
216 /*
217 ** Scan folders, returning:
218 ** first        -- list of nodes for all folders which have desired messages;
219 **                 if the current folder is listed in .folders, it is also in
220 **                 the list regardless of whether it has any desired messages
221 ** last         -- last node in list
222 ** cur_node     -- node of current folder, if listed in .folders
223 ** maxlen       -- length of longest folder name
224 **
225 ** `cur' points to the name of the current folder, `folders' points to the
226 ** name of a .folder (if NULL, crawl all folders), and `sequences' points to
227 ** the array of sequences for which to look.
228 **
229 ** An empty list is returned as first=last=NULL.
230 */
231 static void
232 check_folders(struct node **first, struct node **last,
233         struct node **cur_node, size_t *maxlen,
234         char *cur, char *folders, char *sequences[])
235 {
236         struct list_state b;
237         FILE *fp;
238         char *line;
239         size_t len;
240
241         *first = *last = *cur_node = NULL;
242         *maxlen = 0;
243
244         b.first = first;
245         b.cur_node = cur_node;
246         b.maxlen = maxlen;
247         b.cur = cur;
248         b.sequences = sequences;
249
250         if (folders == NULL) {
251                 chdir(toabsdir("+"));
252                 crawl_folders(".", crawl_callback, &b);
253         } else {
254                 fp = fopen(folders, "r");
255                 if (fp  == NULL) {
256                         adios(EX_IOERR, NULL, "failed to read %s", folders);
257                 }
258                 while (vfgets(fp, &line) == OK) {
259                         len = strlen(line) - 1;
260                         line[len] = '\0';
261                         check_folder(getcpy(line), len, &b);
262                 }
263                 fclose(fp);
264         }
265
266         if (*first != NULL) {
267                 b.node->n_next = NULL;
268                 *last = b.node;
269         }
270 }
271
272 /* Return a single string of the `sequences' joined by a space (' '). */
273 static char *
274 join_sequences(char *sequences[])
275 {
276         int i;
277         size_t len = 0;
278         char *result, *cp;
279
280         for (i = 0; sequences[i] != NULL; i++) {
281                 len += strlen(sequences[i]) + 1;
282         }
283         result = mh_xmalloc(len + 1);
284
285         for (i = 0, cp = result; sequences[i] != NULL; i++, cp += len + 1) {
286                 len = strlen(sequences[i]);
287                 memcpy(cp, sequences[i], len);
288                 cp[len] = ' ';
289         }
290         /* -1 to overwrite the last delimiter */
291         *--cp = '\0';
292
293         return result;
294 }
295
296 /*
297 ** Return a struct node for the folder to change to.  This is the next
298 ** (previous, if FPREV mode) folder with desired messages, or the current
299 ** folder if no folders have desired.  If NEW or UNSEEN mode, print the
300 ** output but don't change folders.
301 **
302 ** n_name is the folder to change to, and n_field is the string list of
303 ** desired message numbers.
304 */
305 static struct node *
306 doit(char *cur, char *folders, char *sequences[])
307 {
308         struct node *first, *cur_node, *node, *last = NULL, *prev;
309         size_t folder_len;
310         int count, total = 0;
311         char *sequences_s = NULL;
312         int argc = 0;
313         char *argv[MAXARGS];
314         char **seqp;
315         char buf[BUFSIZ];
316
317         if (cur == NULL || cur[0] == '\0') {
318                 cur = "inbox";
319         }
320
321         check_folders(&first, &last, &cur_node, &folder_len, cur,
322                         folders, sequences);
323
324         if (run_mode == FNEXT || run_mode == FPREV) {
325                 if (first == NULL) {
326                         /* No folders at all... */
327                         return NULL;
328                 } else if (first->n_next == NULL) {
329                         /*
330                         ** We have only one node; any desired messages in it?
331                         */
332                         if (first->n_field == NULL) {
333                                 return NULL;
334                         } else {
335                                 return first;
336                         }
337                 } else if (cur_node == NULL) {
338                         /*
339                         ** Current folder is not listed in .folders,
340                         ** return first.
341                         */
342                         return first;
343                 }
344         } else if (run_mode == UNSEEN) {
345                 sequences_s = join_sequences(sequences);
346         }
347
348         for (node = first, prev = NULL;
349                  node != NULL;
350                  prev = node, node = node->n_next) {
351                 if (run_mode == FNEXT) {
352                         /*
353                         ** If we have a previous node and it is the current
354                         ** folder, return this node.
355                         */
356                         if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
357                                 return node;
358                         }
359                 } else if (run_mode == FPREV) {
360                         if (strcmp(node->n_name, cur) == 0) {
361                                 /*
362                                 ** Found current folder in fprev mode;
363                                 ** if we have a previous node in the list,
364                                 ** return it; else return the last node.
365                                 */
366                                 if (prev == NULL) {
367                                         return last;
368                                 }
369                                 return prev;
370                         }
371                 } else if (run_mode == UNSEEN) {
372                         if (node->n_field == NULL) {
373                                 continue;
374                         }
375
376                         printf("\n%d %s messages in %s",
377                                    count_messages(node->n_field),
378                                    sequences_s,
379                                    node->n_name);
380                         if (strcmp(node->n_name, cur) == 0) {
381                                 puts(" (*: current folder)");
382                         } else {
383                                 puts("");
384                         }
385                         fflush(stdout);
386
387                         argc = 0;
388                         argv[argc++] = "scan";
389                         snprintf(buf, sizeof buf, "+%s", node->n_name);
390                         argv[argc++] = buf;
391                         for (seqp=sequences; *seqp; seqp++) {
392                                 argv[argc++] = *seqp;
393                         }
394                         argv[argc] = (char *)NULL;
395                         execprog(*argv, argv);
396                 } else {
397                         if (node->n_field == NULL) {
398                                 continue;
399                         }
400
401                         count = count_messages(node->n_field);
402                         total += count;
403
404                         printf("%-*s %6d.%c %s\n", (int) folder_len,
405                                 node->n_name, count,
406                                 (strcmp(node->n_name, cur) == 0 ? '*' : ' '),
407                                 node->n_field);
408                 }
409         }
410
411         /*
412         ** If we're fnext, we haven't checked the last node yet.  If it's the
413         ** current folder, return the first node.
414         */
415         if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
416                 return first;
417         }
418
419         if (run_mode == NEW) {
420                 printf("%-*s %6d.\n", (int) folder_len, " total", total);
421         }
422
423         return cur_node;
424 }
425
426 int
427 main(int argc, char **argv)
428 {
429         char **ap, *cp, **argp, **arguments;
430         char help[BUFSIZ];
431         char *folders = NULL;
432         char *sequences[NUMATTRS + 1];
433         int i = 0;
434         char *unseen;
435         struct node *folder;
436
437         sequences[0] = NULL;
438         sequences[1] = NULL;
439
440         setlocale(LC_ALL, "");
441         invo_name = mhbasename(argv[0]);
442
443         /* read user profile/context */
444         context_read();
445
446         arguments = getarguments(invo_name, argc, argv, 1);
447         argp = arguments;
448
449         /*
450         ** Parse arguments
451         */
452         while ((cp = *argp++)) {
453                 if (*cp == '-') {
454                         switch (smatch(++cp, switches)) {
455                         case AMBIGSW:
456                                 ambigsw(cp, switches);
457                                 exit(EX_USAGE);
458                         case UNKWNSW:
459                                 adios(EX_USAGE, NULL, "-%s unknown", cp);
460
461                         case HELPSW:
462                                 snprintf(help, sizeof(help),
463                                                 "%s [switches] [sequences]",
464                                                 invo_name);
465                                 print_help(help, switches, 1);
466                                 exit(argc == 2 ? EX_OK : EX_USAGE);
467                         case VERSIONSW:
468                                 print_version(invo_name);
469                                 exit(argc == 2 ? EX_OK : EX_USAGE);
470
471                         case FOLDERSSW:
472                                 if (!(folders = *argp++) || *folders == '-')
473                                         adios(EX_USAGE, NULL, "missing argument to %s",
474                                                         argp[-2]);
475                                 continue;
476                         case MODESW:
477                                 if (!(invo_name = *argp++) || *invo_name == '-')
478                                         adios(EX_USAGE, NULL, "missing argument to %s",
479                                                         argp[-2]);
480                                 invo_name = mhbasename(invo_name);
481                                 continue;
482                         }
483                 }
484                 /* have a sequence argument */
485                 if (!seq_in_list(cp, sequences)) {
486                         sequences[i++] = cp;
487                         sequences[i] = NULL;
488                 }
489         }
490
491         if (strcmp(invo_name, "fnext") == 0) {
492                 run_mode = FNEXT;
493         } else if (strcmp(invo_name, "fprev") == 0) {
494                 run_mode = FPREV;
495         } else if (strcmp(invo_name, "unseen") == 0) {
496                 run_mode = UNSEEN;
497         }
498
499         if (folders == NULL) {
500                 /* will flists */
501         } else {
502                 if (folders[0] != '/') {
503                         folders = toabsdir(folders);
504                 }
505         }
506
507         if (i == 0) {
508                 char *dp;
509                 /* no sequence arguments; use unseen */
510                 if ((unseen = context_find(usequence))) {
511                         if (!*unseen) {
512                                 adios(EX_CONFIG, NULL, "profile entry %s set, but empty, and no sequences given", usequence);
513                         }
514                 } else {
515                         unseen = seq_unseen;  /* use default */
516                 }
517                 dp = getcpy(unseen);
518                 for (ap = brkstring(dp, " ", "\n"); *ap; ap++) {
519                         sequences[i++] = *ap;
520                 }
521         }
522         sequences[i] = NULL;
523
524         folder = doit(context_find(curfolder), folders, sequences);
525         if (folder == NULL) {
526                 exit(EX_OK);
527                 return 1;
528         }
529
530         if (run_mode == UNSEEN) {
531                 /*
532                 ** All the scan(1)s it runs change the current folder, so we
533                 ** need to put it back.  Unfortunately, context_replace lamely
534                 ** ignores the new value you give it if it is the same one it
535                 ** has in memory.  So, we'll be lame, too.  I'm not sure if i
536                 ** should just change context_replace...
537                 */
538                 context_replace(curfolder, "defeat_context_replace_optimization");
539         }
540
541         /* update current folder */
542         context_replace(curfolder, folder->n_name);
543
544         if (run_mode == FNEXT || run_mode == FPREV) {
545                 printf("%s  %s\n", folder->n_name, folder->n_field);
546         }
547
548         context_save();
549
550         return 0;
551 }