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