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