]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/LeiDedupe.pm
lei convert: mail format conversion sub-command
[public-inbox.git] / lib / PublicInbox / LeiDedupe.pm
1 # Copyright (C) 2020-2021 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 package PublicInbox::LeiDedupe;
4 use strict;
5 use v5.10.1;
6 use PublicInbox::ContentHash qw(content_hash);
7 use Digest::SHA ();
8
9 # n.b. mutt sets most of these headers not sure about Bytes
10 our @OID_IGNORE = qw(Status X-Status Content-Length Lines Bytes);
11
12 # best-effort regeneration of OID when augmenting existing results
13 sub _regen_oid ($) {
14         my ($eml) = @_;
15         my @stash; # stash away headers we shouldn't have in git
16         for my $k (@OID_IGNORE) {
17                 my @v = $eml->header_raw($k) or next;
18                 push @stash, [ $k, \@v ];
19                 $eml->header_set($k); # restore below
20         }
21         my $dig = Digest::SHA->new(1); # XXX SHA256 later
22         my $buf = $eml->as_string;
23         $dig->add('blob '.length($buf)."\0");
24         $dig->add($buf);
25         undef $buf;
26
27         for my $kv (@stash) { # restore stashed headers
28                 my ($k, @v) = @$kv;
29                 $eml->header_set($k, @v);
30         }
31         $dig->digest;
32 }
33
34 sub _oidbin ($) { defined($_[0]) ? pack('H*', $_[0]) : undef }
35
36 sub smsg_hash ($) {
37         my ($smsg) = @_;
38         my $dig = Digest::SHA->new(256);
39         my $x = join("\0", @$smsg{qw(from to cc ds subject references mid)});
40         utf8::encode($x);
41         $dig->add($x);
42         $dig->digest;
43 }
44
45 # the paranoid option
46 sub dedupe_oid ($) {
47         my ($skv) = @_;
48         (sub { # may be called in a child process
49                 my ($eml, $oid) = @_;
50                 $skv->set_maybe(_oidbin($oid) // _regen_oid($eml), '');
51         }, sub {
52                 my ($smsg) = @_;
53                 $skv->set_maybe(_oidbin($smsg->{blob}), '');
54         });
55 }
56
57 # dangerous if there's duplicate messages with different Message-IDs
58 sub dedupe_mid ($) {
59         my ($skv) = @_;
60         (sub { # may be called in a child process
61                 my ($eml, $oid) = @_;
62                 # TODO: lei will support non-public messages w/o Message-ID
63                 my $mid = $eml->header_raw('Message-ID') // _oidbin($oid) //
64                         content_hash($eml);
65                 $skv->set_maybe($mid, '');
66         }, sub {
67                 my ($smsg) = @_;
68                 my $mid = $smsg->{mid};
69                 $mid = undef if $mid eq '';
70                 $mid //= smsg_hash($smsg) // _oidbin($smsg->{blob});
71                 $skv->set_maybe($mid, '');
72         });
73 }
74
75 # our default deduplication strategy (used by v2, also)
76 sub dedupe_content ($) {
77         my ($skv) = @_;
78         (sub { # may be called in a child process
79                 my ($eml) = @_; # oid = $_[1], ignored
80                 $skv->set_maybe(content_hash($eml), '');
81         }, sub {
82                 my ($smsg) = @_;
83                 $skv->set_maybe(smsg_hash($smsg), '');
84         });
85 }
86
87 # no deduplication at all
88 sub true { 1 }
89 sub dedupe_none ($) { (\&true, \&true) }
90
91 sub new {
92         my ($cls, $lei) = @_;
93         my $dd = $lei->{opt}->{dedupe} // 'content';
94         my $dst = $lei->{ovv}->{dst};
95
96         # allow "none" to bypass Eml->new if writing to directory:
97         return if ($dd eq 'none' && substr($dst // '', -1) eq '/');
98         my $m = "dedupe_$dd";
99         $cls->can($m) or die "unsupported dedupe strategy: $dd\n";
100         my $skv;
101         if ($dd ne 'none') {
102                 require PublicInbox::SharedKV;
103                 $skv = PublicInbox::SharedKV->new;
104         }
105         # [ $skv, $eml_cb, $smsg_cb, "dedupe_$dd" ]
106         bless [ $skv, undef, undef, $m ], $cls;
107 }
108
109 # returns true on seen messages according to the deduplication strategy,
110 # returns false if unseen
111 sub is_dup {
112         my ($self, $eml, $oid) = @_;
113         !$self->[1]->($eml, $oid);
114 }
115
116 sub is_smsg_dup {
117         my ($self, $smsg) = @_;
118         !$self->[2]->($smsg);
119 }
120
121 sub prepare_dedupe {
122         my ($self) = @_;
123         my $skv = $self->[0];
124         $self->[1] or @$self[1,2] = $self->can($self->[3])->($skv);
125         $skv ? $skv->dbh : undef;
126 }
127
128 sub pause_dedupe {
129         my ($self) = @_;
130         my $skv = $self->[0] or return;
131         $skv->dbh_release;
132         delete($skv->{dbh}) if $skv;
133 }
134
135 1;