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