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