Added -check/-nocheck switches to sortm(1). Also removed obsolete
[mmh] / uip / sortm.c
1
2 /*
3  * sortm.c -- sort messages in a folder by date/time
4  *
5  * This code is Copyright (c) 2002, by the authors of nmh.  See the
6  * COPYRIGHT file in the root directory of the nmh distribution for
7  * complete copyright information.
8  */
9
10 #include <h/mh.h>
11 #include <h/tws.h>
12 #include <h/utils.h>
13
14 static struct swit switches[] = {
15 #define DATESW                 0
16      { "datefield field", 0 },
17 #define TEXTSW                 1
18      { "textfield field", 0 },
19 #define NSUBJSW                2
20      { "notextfield", 0 },
21 #define SUBJSW                 3
22      { "subject", -3 },            /* backward-compatibility */
23 #define LIMSW                  4
24      { "limit days", 0 },
25 #define NLIMSW                 5
26      { "nolimit", 0 },
27 #define VERBSW                 6
28      { "verbose", 0 },
29 #define NVERBSW                7
30      { "noverbose", 0 },
31 #define ALLMSGS                8
32      { "all", 0 },
33 #define NALLMSGS               9
34      { "noall", 0 },
35 #define CHECKSW               10
36      { "check", 0 },
37 #define NCHECKSW              11
38      { "nocheck", 0 },
39 #define VERSIONSW             12
40      { "version", 0 },
41 #define HELPSW                13
42      { "help", 0 },
43      { NULL, 0 }
44 };
45
46 struct smsg {
47     int s_msg;
48     time_t s_clock;
49     char *s_subj;
50 };
51
52 static struct smsg *smsgs;
53 int nmsgs;
54
55 char *subjsort = (char *) 0;    /* sort on subject if != 0 */
56 time_t datelimit = 0;
57 int submajor = 0;               /* if true, sort on subject-major */
58 int verbose;
59 int allmsgs = 1;
60 int check_failed = 0;
61
62 /* This keeps compiler happy on calls to qsort */
63 typedef int (*qsort_comp) (const void *, const void *);
64
65 /*
66  * static prototypes
67  */
68 static int read_hdrs (struct msgs *, char *);
69 static int get_fields (char *, int, struct smsg *);
70 static int dsort (struct smsg **, struct smsg **);
71 static int subsort (struct smsg **, struct smsg **);
72 static int txtsort (struct smsg **, struct smsg **);
73 static void rename_chain (struct msgs *, struct smsg **, int, int);
74 static void rename_msgs (struct msgs *, struct smsg **);
75
76
77 int
78 main (int argc, char **argv)
79 {
80     int i, msgnum;
81     unsigned char *cp;
82     char *maildir, *datesw = NULL;
83     char *folder = NULL, buf[BUFSIZ], **argp;
84     char **arguments;
85     struct msgs_array msgs = { 0, 0, NULL };
86     struct msgs *mp;
87     struct smsg **dlist;
88     int checksw = 0;
89
90 #ifdef LOCALE
91     setlocale(LC_ALL, "");
92 #endif
93     invo_name = r1bindex (argv[0], '/');
94
95     /* read user profile/context */
96     context_read();
97
98     arguments = getarguments (invo_name, argc, argv, 1);
99     argp = arguments;
100
101     /*
102      * Parse arguments
103      */
104     while ((cp = *argp++)) {
105         if (*cp == '-') {
106             switch (smatch (++cp, switches)) {
107             case AMBIGSW:
108                 ambigsw (cp, switches);
109                 done (1);
110             case UNKWNSW:
111                 adios (NULL, "-%s unknown", cp);
112
113             case HELPSW:
114                 snprintf(buf, sizeof(buf), "%s [+folder] [msgs] [switches]",
115                         invo_name);
116                 print_help (buf, switches, 1);
117                 done (0);
118             case VERSIONSW:
119                 print_version(invo_name);
120                 done (0);
121
122             case DATESW:
123                 if (datesw)
124                     adios (NULL, "only one date field at a time");
125                 if (!(datesw = *argp++) || *datesw == '-')
126                     adios (NULL, "missing argument to %s", argp[-2]);
127                 continue;
128
129             case TEXTSW:
130                 if (subjsort)
131                     adios (NULL, "only one text field at a time");
132                 if (!(subjsort = *argp++) || *subjsort == '-')
133                     adios (NULL, "missing argument to %s", argp[-2]);
134                 continue;
135
136             case SUBJSW:
137                 subjsort = "subject";
138                 continue;
139             case NSUBJSW:
140                 subjsort = (char *)0;
141                 continue;
142
143             case LIMSW:
144                 if (!(cp = *argp++) || *cp == '-')
145                         adios (NULL, "missing argument to %s", argp[-2]);
146                 while (*cp == '0')
147                     cp++;               /* skip any leading zeros */
148                 if (!*cp) {             /* hit end of string */
149                     submajor++;         /* sort subject-major */
150                     continue;
151                 }
152                 if (!isdigit(*cp) || !(datelimit = atoi(cp)))
153                     adios (NULL, "impossible limit %s", cp);
154                 datelimit *= 60*60*24;
155                 continue;
156             case NLIMSW:
157                 submajor = 0;   /* use date-major, but */
158                 datelimit = 0;  /* use no limit */
159                 continue;
160
161             case VERBSW:
162                 verbose++;
163                 continue;
164             case NVERBSW:
165                 verbose = 0;
166                 continue;
167
168             case ALLMSGS:
169                 allmsgs = 1;
170                 continue;
171             case NALLMSGS:
172                 allmsgs = 0;
173                 continue;
174
175             case CHECKSW:
176                 checksw = 1;
177                 continue;
178             case NCHECKSW:
179                 checksw = 0;
180                 continue;
181             }
182         }
183         if (*cp == '+' || *cp == '@') {
184             if (folder)
185                 adios (NULL, "only one folder at a time!");
186             else
187                 folder = pluspath (cp);
188         } else
189                 app_msgarg(&msgs, cp);
190     }
191
192     if (!context_find ("path"))
193         free (path ("./", TFOLDER));
194     if (!msgs.size) {
195         if (allmsgs) {
196             app_msgarg(&msgs, "all");
197         } else {
198             adios (NULL, "must specify messages to sort with -noall");
199         }
200     }
201     if (!datesw)
202         datesw = "date";
203     if (!folder)
204         folder = getfolder (1);
205     maildir = m_maildir (folder);
206
207     if (chdir (maildir) == NOTOK)
208         adios (maildir, "unable to change directory to");
209
210     /* read folder and create message structure */
211     if (!(mp = folder_read (folder)))
212         adios (NULL, "unable to read folder %s", folder);
213
214     /* check for empty folder */
215     if (mp->nummsg == 0)
216         adios (NULL, "no messages in %s", folder);
217
218     /* parse all the message ranges/sequences and set SELECTED */
219     for (msgnum = 0; msgnum < msgs.size; msgnum++)
220         if (!m_convert (mp, msgs.msgs[msgnum]))
221             done (1);
222     seq_setprev (mp);   /* set the previous sequence */
223
224     if ((nmsgs = read_hdrs (mp, datesw)) <= 0)
225         adios (NULL, "no messages to sort");
226
227     if (checksw  &&  check_failed) {
228         adios (NULL, "errors found, no messages sorted");
229     }
230
231     /*
232      * sort a list of pointers to our "messages to be sorted".
233      */
234     dlist = (struct smsg **) mh_xmalloc ((nmsgs+1) * sizeof(*dlist));
235     for (i = 0; i < nmsgs; i++)
236         dlist[i] = &smsgs[i];
237     dlist[nmsgs] = 0;
238
239     if (verbose) {      /* announce what we're doing */
240         if (subjsort)
241             if (submajor)
242                 printf ("sorting by %s\n", subjsort);
243             else
244                 printf ("sorting by %s-major %s-minor\n", subjsort, datesw);
245         else
246             printf ("sorting by datefield %s\n", datesw);
247     }
248
249     /* first sort by date, or by subject-major, date-minor */
250     qsort ((char *) dlist, nmsgs, sizeof(*dlist),
251             (qsort_comp) (submajor && subjsort ? txtsort : dsort));
252
253     /*
254      * if we're sorting on subject, we need another list
255      * in subject order, then a merge pass to collate the
256      * two sorts.
257      */
258     if (!submajor && subjsort) {        /* already date sorted */
259         struct smsg **slist, **flist;
260         register struct smsg ***il, **fp, **dp;
261
262         slist = (struct smsg **) mh_xmalloc ((nmsgs+1) * sizeof(*slist));
263         memcpy((char *)slist, (char *)dlist, (nmsgs+1)*sizeof(*slist));
264         qsort((char *)slist, nmsgs, sizeof(*slist), (qsort_comp) subsort);
265
266         /*
267          * make an inversion list so we can quickly find
268          * the collection of messages with the same subj
269          * given a message number.
270          */
271         il = (struct smsg ***) calloc (mp->hghsel+1, sizeof(*il));
272         if (! il)
273             adios (NULL, "couldn't allocate msg list");
274         for (i = 0; i < nmsgs; i++)
275             il[slist[i]->s_msg] = &slist[i];
276         /*
277          * make up the final list, chronological but with
278          * all the same subjects grouped together.
279          */
280         flist = (struct smsg **) mh_xmalloc ((nmsgs+1) * sizeof(*flist));
281         fp = flist;
282         for (dp = dlist; *dp;) {
283             register struct smsg **s = il[(*dp++)->s_msg];
284
285             /* see if we already did this guy */
286             if (! s)
287                 continue;
288
289             *fp++ = *s++;
290             /*
291              * take the next message(s) if there is one,
292              * its subject isn't null and its subject
293              * is the same as this one and it's not too
294              * far away in time.
295              */
296             while (*s && (*s)->s_subj[0] &&
297                    strcmp((*s)->s_subj, s[-1]->s_subj) == 0 &&
298                    (datelimit == 0 ||
299                    (*s)->s_clock - s[-1]->s_clock <= datelimit)) {
300                 il[(*s)->s_msg] = 0;
301                 *fp++ = *s++;
302             }
303         }
304         *fp = 0;
305         free (slist);
306         free (dlist);
307         dlist = flist;
308     }
309
310     /*
311      * At this point, dlist is a sorted array of pointers to smsg structures,
312      * each of which contains a message number.
313      */
314
315     rename_msgs (mp, dlist);
316
317     context_replace (pfolder, folder);  /* update current folder         */
318     seq_save (mp);                      /* synchronize message sequences */
319     context_save ();                    /* save the context file         */
320     folder_free (mp);                   /* free folder/message structure */
321     done (0);
322     return 1;
323 }
324
325 static int
326 read_hdrs (struct msgs *mp, char *datesw)
327 {
328     int msgnum;
329     struct tws tb;
330     register struct smsg *s;
331
332     twscopy (&tb, dlocaltimenow ());
333
334     smsgs = (struct smsg *)
335         calloc ((size_t) (mp->hghsel - mp->lowsel + 2),
336             sizeof(*smsgs));
337     if (smsgs == NULL)
338         adios (NULL, "unable to allocate sort storage");
339
340     s = smsgs;
341     for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
342         if (is_selected(mp, msgnum)) {
343             if (get_fields (datesw, msgnum, s)) {
344                 s->s_msg = msgnum;
345                 s++;
346             }
347         }
348     }
349     s->s_msg = 0;
350     return(s - smsgs);
351 }
352
353
354 /*
355  * Parse the message and get the data or subject field,
356  * if needed.
357  */
358
359 static int
360 get_fields (char *datesw, int msg, struct smsg *smsg)
361 {
362     register int state;
363     int compnum;
364     char *msgnam, buf[BUFSIZ], nam[NAMESZ];
365     register struct tws *tw;
366     register char *datecomp = NULL, *subjcomp = NULL;
367     register FILE *in;
368
369     if ((in = fopen (msgnam = m_name (msg), "r")) == NULL) {
370         admonish (msgnam, "unable to read message");
371         return (0);
372     }
373     for (compnum = 1, state = FLD;;) {
374         switch (state = m_getfld (state, nam, buf, sizeof(buf), in)) {
375         case FLD:
376         case FLDEOF:
377         case FLDPLUS:
378             compnum++;
379             if (!mh_strcasecmp (nam, datesw)) {
380                 datecomp = add (buf, datecomp);
381                 while (state == FLDPLUS) {
382                     state = m_getfld (state, nam, buf, sizeof(buf), in);
383                     datecomp = add (buf, datecomp);
384                 }
385                 if (!subjsort || subjcomp)
386                     break;
387             } else if (subjsort && !mh_strcasecmp (nam, subjsort)) {
388                 subjcomp = add (buf, subjcomp);
389                 while (state == FLDPLUS) {
390                     state = m_getfld (state, nam, buf, sizeof(buf), in);
391                     subjcomp = add (buf, subjcomp);
392                 }
393                 if (datecomp)
394                     break;
395             } else {
396                 /* just flush this guy */
397                 while (state == FLDPLUS)
398                     state = m_getfld (state, nam, buf, sizeof(buf), in);
399             }
400             continue;
401
402         case BODY:
403         case BODYEOF:
404         case FILEEOF:
405             break;
406
407         case LENERR:
408         case FMTERR:
409             if (state == LENERR || state == FMTERR) {
410                 admonish (NULL, "format error in message %d (header #%d)",
411                       msg, compnum);
412                 check_failed = 1;
413             }
414             if (datecomp)
415                 free (datecomp);
416             if (subjcomp)
417                 free (subjcomp);
418             fclose (in);
419             return (0);
420
421         default:
422             adios (NULL, "internal error -- you lose");
423         }
424         break;
425     }
426
427     /*
428      * If no date component, then use the modification
429      * time of the file as its date
430      */
431     if (!datecomp || (tw = dparsetime (datecomp)) == NULL) {
432         struct stat st;
433
434         admonish (NULL, "can't parse %s field in message %d", datesw, msg);
435         fstat (fileno (in), &st);
436         smsg->s_clock = st.st_mtime;
437         check_failed = 1;
438     } else {
439         smsg->s_clock = dmktime (tw);
440     }
441
442     if (subjsort) {
443         if (subjcomp) {
444             /*
445              * try to make the subject "canonical": delete
446              * leading "re:", everything but letters & smash
447              * letters to lower case.
448              */
449             register char  *cp, *cp2;
450             register unsigned char c;
451
452             cp = subjcomp;
453             cp2 = subjcomp;
454             if (strcmp (subjsort, "subject") == 0) {
455                 while ((c = *cp)) {
456                     if (! isspace(c)) {
457                         if(uprf(cp, "re:"))
458                             cp += 2;
459                         else
460                             break;
461                     }
462                     cp++;
463                 }
464             }
465
466             while ((c = *cp++)) {
467                 if (isalnum(c))
468                     *cp2++ = isupper(c) ? tolower(c) : c;
469             }
470
471             *cp2 = '\0';
472         }
473         else
474             subjcomp = "";
475
476         smsg->s_subj = subjcomp;
477     }
478     fclose (in);
479     if (datecomp)
480         free (datecomp);
481
482     return (1);
483 }
484
485 /*
486  * sort on dates.
487  */
488 static int
489 dsort (struct smsg **a, struct smsg **b)
490 {
491     if ((*a)->s_clock < (*b)->s_clock)
492         return (-1);
493     else if ((*a)->s_clock > (*b)->s_clock)
494         return (1);
495     else if ((*a)->s_msg < (*b)->s_msg)
496         return (-1);
497     else
498         return (1);
499 }
500
501 /*
502  * sort on subjects.
503  */
504 static int
505 subsort (struct smsg **a, struct smsg **b)
506 {
507     register int i;
508
509     if ((i = strcmp ((*a)->s_subj, (*b)->s_subj)))
510         return (i);
511
512     return (dsort (a, b));
513 }
514
515 static int
516 txtsort (struct smsg **a, struct smsg **b)
517 {
518     register int i;
519
520     if ((i = strcmp ((*a)->s_subj, (*b)->s_subj)))
521         return (i);
522     else if ((*a)->s_msg < (*b)->s_msg)
523         return (-1);
524     else
525         return (1);
526 }
527
528 static void
529 rename_chain (struct msgs *mp, struct smsg **mlist, int msg, int endmsg)
530 {
531     int nxt, old, new;
532     char *newname, oldname[BUFSIZ];
533     char newbuf[PATH_MAX + 1];
534
535     for (;;) {
536         nxt = mlist[msg] - smsgs;       /* mlist[msg] is a ptr into smsgs */
537         mlist[msg] = (struct smsg *)0;
538         old = smsgs[nxt].s_msg;
539         new = smsgs[msg].s_msg;
540         strncpy (oldname, m_name (old), sizeof(oldname));
541         newname = m_name (new);
542         if (verbose)
543             printf ("message %d becomes message %d\n", old, new);
544
545         (void)snprintf(oldname, sizeof (oldname), "%s/%d", mp->foldpath, old);
546         (void)snprintf(newbuf, sizeof (newbuf), "%s/%d", mp->foldpath, new);
547         ext_hook("ref-hook", oldname, newbuf);
548
549         if (rename (oldname, newname) == NOTOK)
550             adios (newname, "unable to rename %s to", oldname);
551
552         copy_msg_flags (mp, new, old);
553         if (mp->curmsg == old)
554             seq_setcur (mp, new);
555
556         if (nxt == endmsg)
557             break;
558
559         msg = nxt;
560     }
561 /*      if (nxt != endmsg); */
562 /*      rename_chain (mp, mlist, nxt, endmsg); */
563 }
564
565 static void
566 rename_msgs (struct msgs *mp, struct smsg **mlist)
567 {
568     int i, j, old, new;
569     seqset_t tmpset;
570     char f1[BUFSIZ], tmpfil[BUFSIZ];
571     char newbuf[PATH_MAX + 1];
572     struct smsg *sp;
573
574     strncpy (tmpfil, m_name (mp->hghmsg + 1), sizeof(tmpfil));
575
576     for (i = 0; i < nmsgs; i++) {
577         if (! (sp = mlist[i]))
578             continue;   /* did this one */
579
580         j = sp - smsgs;
581         if (j == i)
582             continue;   /* this one doesn't move */
583
584         /*
585          * the guy that was msg j is about to become msg i.
586          * rename 'j' to make a hole, then recursively rename
587          * guys to fill up the hole.
588          */
589         old = smsgs[j].s_msg;
590         new = smsgs[i].s_msg;
591         strncpy (f1, m_name (old), sizeof(f1));
592
593         if (verbose)
594             printf ("renaming message chain from %d to %d\n", old, new);
595
596         /*
597          *      Run the external hook to refile the old message as the
598          *      temporary message number that is off of the end of the
599          *      messages in the folder.
600          */
601
602         (void)snprintf(f1, sizeof (f1), "%s/%d", mp->foldpath, old);
603         (void)snprintf(newbuf, sizeof (newbuf), "%s/%d", mp->foldpath, mp->hghmsg + 1);
604         ext_hook("ref-hook", f1, newbuf);
605
606         if (rename (f1, tmpfil) == NOTOK)
607             adios (tmpfil, "unable to rename %s to ", f1);
608
609         get_msg_flags (mp, &tmpset, old);
610
611         rename_chain (mp, mlist, j, i);
612
613         /*
614          *      Run the external hook to refile the temorary message number
615          *      to the real place.
616          */
617
618         (void)snprintf(f1, sizeof (f1), "%s/%d", mp->foldpath, new);
619         ext_hook("ref-hook", newbuf, f1);
620
621         if (rename (tmpfil, m_name(new)) == NOTOK)
622             adios (m_name(new), "unable to rename %s to", tmpfil);
623
624         set_msg_flags (mp, &tmpset, new);
625         mp->msgflags |= SEQMOD;
626     }
627 }