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