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