cm-reparent.pl 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/perl
  2. use 5.14.1;
  3. use warnings;
  4. our $VERSION = "1.05 - 2018-10-08";
  5. our $cmd = $0 =~ s{.*/}{}r;
  6. sub usage {
  7. my $err = shift and select STDERR;
  8. say "usage: $cmd file ...";
  9. exit $err;
  10. } # usage
  11. use Date::Parse;
  12. use Getopt::Long;
  13. GetOptions (
  14. "help|?" => sub { usage (0); },
  15. "V|version" => sub { say "$cmd [$VERSION]"; exit 0; },
  16. ) or usage (1);
  17. my $p;
  18. my %f;
  19. foreach my $fn (@ARGV) {
  20. open my $fh, "<", $fn or die "$fn: $!\n";
  21. my ($hdr, $body) = split m/(?<=\n)(?=\r?\n)/ => do { local $/; <$fh> }, 2;
  22. close $fh;
  23. $hdr && $hdr =~ m/\b(?:Date|Received)\b/ or next;
  24. my ($mid) = $hdr =~ m{^Message-Id: (?:[\x20\t]*\n)?[\x20\t]+ (\S.*)}xmi;
  25. my ($dte) = $hdr =~ m{^Date: (?:[\x20\t]*\n)?[\x20\t]+ (\S.*)}xmi;
  26. my ($rcv) = $hdr =~ m{\nReceived: (?:[\x20\t]*\n)?[\x20\t]+ (\S.*(?:\n\s+.*)*+)}xi;
  27. my ($irt) = $hdr =~ m{^In-Reply-To: (?:[\x20\t]*\n)?[\x20\t]+ (\S.*)}xmi;
  28. my ($ref) = $hdr =~ m{^References: (?:[\x20\t]*\n)?[\x20\t]+ (\S.*)}xmi;
  29. $rcv ||= $dte;
  30. $rcv =~ s/[\s\r\n]+/ /g;
  31. $rcv =~ s/\s+$//;
  32. $rcv =~ s/.*;\s*//;
  33. $rcv =~ s/.* id \S+\s+//i;
  34. my $stamp = str2time ($rcv) or die $rcv;
  35. my $date = $stamp ? do {
  36. my @d = localtime $stamp;
  37. sprintf "%4d-%02d-%02d %02d:%02d:%02d", $d[5] + 1900, ++$d[4], @d[3,2,1,0];
  38. } : "-";
  39. #printf "%12s %-20s %s\n", $stamp // "-", $date, $rcv;
  40. $f{$fn} = {
  41. msg_id => $mid,
  42. refs => $ref,
  43. irt => $irt,
  44. date => $dte,
  45. rcvd => $rcv,
  46. stamp => $stamp,
  47. sdate => $date,
  48. hdr => $hdr,
  49. body => $body,
  50. };
  51. $p //= $fn;
  52. $stamp < $f{$p}{stamp} and $p = $fn;
  53. }
  54. # All but the oldest will refer to the oldest as parent
  55. $p or exit 0;
  56. my $pid = $f{$p}{msg_id} or die "Parent file $p has no Message-Id\n";
  57. foreach my $fn (sort keys %f) {
  58. $fn eq $p and next;
  59. my $c = 0;
  60. my $f = $f{$fn};
  61. if ($f->{refs}) {
  62. unless ($f->{refs} eq $pid) {
  63. $c++;
  64. $f->{hdr} =~ s{^(?=References:)}{References: $pid\nX-}mi;
  65. }
  66. }
  67. else {
  68. $c++;
  69. $f->{hdr} =~ s{^(?=Message-Id:)}{References: $pid\n}mi;
  70. }
  71. if ($f->{irt}) {
  72. unless ($f->{irt} eq $pid) {
  73. $c++;
  74. $f->{hdr} =~ s{^(?=In-Reply-To:)}{In-Reply-To: $pid\nX-}mi;
  75. }
  76. }
  77. else {
  78. $c++;
  79. $f->{hdr} =~ s{^(?=Message-Id:)}{In-Reply-To: $pid\n}mi;
  80. }
  81. $c or next; # No changes required
  82. unless ($f->{msg_id}) {
  83. warn "Child message $fn has no Message-Id, skipped\n";
  84. next;
  85. }
  86. say "$f->{msg_id} => $pid";
  87. my @t = stat $fn;
  88. open my $fh, ">", $fn or die "$fn: $!\n";
  89. print $fh $f->{hdr}, $f->{body};
  90. close $fh or die "$fn: $!\n";
  91. utime $t[8], $t[9], $fn;
  92. }
  93. __END__
  94. =head1 NAME
  95. cm-reparent.pl - fix mail threading
  96. =head1 SYNOPSIS
  97. cm-reparent.pl ~/Mail/inbox/23 ~/Mail/inbox/45 ...
  98. =head1 DESCRIPTION
  99. This script should be called from within Claws-Mail as an action
  100. Define an action as
  101. Menu name: Reparent (fix threading)
  102. Command: cm-reparent.pl %F
  103. Then select from the message list all files that should be re-parented
  104. Then invoke the action
  105. All but the oldest of those mails will be modified (if needed) to
  106. reflect that the oldest mail is the parent of all other mails by
  107. adding or altering the header lines C<In-Reply-To:> and C<References:>
  108. Given 4 files A, B, C, and D like
  109. File Message-Id Date
  110. A 123AC_12 2016-06-01 12:13:14
  111. B aFFde2993 2016-06-01 13:14:15
  112. C 0000_1234 2016-06-02 10:18:04
  113. D foo_bar_12 2016-06-03 04:00:00
  114. The new tree will be like
  115. A 123AC_12 2016-06-01 12:13:14
  116. +- B aFFde2993 2016-06-01 13:14:15
  117. +- C 0000_1234 2016-06-02 10:18:04
  118. +- D foo_bar_12 2016-06-03 04:00:00
  119. and not like
  120. A 123AC_12 2016-06-01 12:13:14
  121. +- B aFFde2993 2016-06-01 13:14:15
  122. +- C 0000_1234 2016-06-02 10:18:04
  123. +- D foo_bar_12 2016-06-03 04:00:00
  124. Existing entries of C<References:> and C<In-Reply-To:> in the header
  125. of any of B, C, or D will be preserved as C<X-References:> or
  126. C<X-In-Reply-To:> respectively.
  127. =head1 SEE ALSO
  128. L<Date::Parse>, L<Claws Mail|http://www.claws-mail.org>
  129. cm-break.pl
  130. =head1 AUTHOR
  131. H.Merijn Brand <h.m.brand@xs4all.nl>
  132. =head1 COPYRIGHT AND LICENSE
  133. Copyright (C) 2016-2018 H.Merijn Brand. All rights reserved.
  134. This library is free software; you can redistribute and/or modify it under
  135. the same terms as Perl itself.
  136. See the L<Artistic license|http://dev.perl.org/licenses/artistic.html>.
  137. =cut