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