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