Re: Deduplication ?
[notmuch-archives.git] / 63 / 1be5dca412e2e156d02a19014e9b19f9d13654
1 Return-Path: <Vladimir.Marek@oracle.com>\r
2 X-Original-To: notmuch@notmuchmail.org\r
3 Delivered-To: notmuch@notmuchmail.org\r
4 Received: from localhost (localhost [127.0.0.1])\r
5         by olra.theworths.org (Postfix) with ESMTP id 1D0AB40DAD6\r
6         for <notmuch@notmuchmail.org>; Fri,  6 Jun 2014 03:40:52 -0700 (PDT)\r
7 X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
8 X-Spam-Flag: NO\r
9 X-Spam-Score: -2.299\r
10 X-Spam-Level: \r
11 X-Spam-Status: No, score=-2.299 tagged_above=-999 required=5\r
12         tests=[RCVD_IN_DNSWL_MED=-2.3, UNPARSEABLE_RELAY=0.001]\r
13         autolearn=disabled\r
14 Received: from olra.theworths.org ([127.0.0.1])\r
15         by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
16         with ESMTP id hcLz1mheg5DB for <notmuch@notmuchmail.org>;\r
17         Fri,  6 Jun 2014 03:40:44 -0700 (PDT)\r
18 Received: from aserp1040.oracle.com (aserp1040.oracle.com [141.146.126.69])\r
19         (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))\r
20         (No client certificate requested)\r
21         by olra.theworths.org (Postfix) with ESMTPS id 22E7945499F\r
22         for <notmuch@notmuchmail.org>; Fri,  6 Jun 2014 03:40:44 -0700 (PDT)\r
23 Received: from acsinet21.oracle.com (acsinet21.oracle.com [141.146.126.237])\r
24         by aserp1040.oracle.com (Sentrion-MTA-4.3.2/Sentrion-MTA-4.3.2) with\r
25         ESMTP id s56AeQ9N029868\r
26         (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);\r
27         Fri, 6 Jun 2014 10:40:27 GMT\r
28 Received: from userz7021.oracle.com (userz7021.oracle.com [156.151.31.85])\r
29         by acsinet21.oracle.com (8.14.4+Sun/8.14.4) with ESMTP id\r
30         s56AeMUW013677\r
31         (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=FAIL);\r
32         Fri, 6 Jun 2014 10:40:23 GMT\r
33 Received: from abhmp0010.oracle.com (abhmp0010.oracle.com [141.146.116.16])\r
34         by userz7021.oracle.com (8.14.4+Sun/8.14.4) with ESMTP id\r
35         s56AeLVH007113; Fri, 6 Jun 2014 10:40:21 GMT\r
36 Received: from virt.cz.oracle.com (/10.163.102.127)\r
37         by default (Oracle Beehive Gateway v4.0)\r
38         with ESMTP ; Fri, 06 Jun 2014 03:40:20 -0700\r
39 Date: Fri, 6 Jun 2014 12:40:18 +0200\r
40 From: Vladimir Marek <Vladimir.Marek@oracle.com>\r
41 To: David Edmondson <david.edmondson@oracle.com>\r
42 Subject: Re: Deduplication ?\r
43 Message-ID: <20140606104018.GJ2154@virt.cz.oracle.com>\r
44 References: <20140602123212.GA12639@virt.cz.oracle.com>\r
45         <87d2ers9mi.fsf@qmul.ac.uk> <m2ppirs8ea.fsf@guru.guru-group.fi>\r
46         <87ppirqtfa.fsf@qmul.ac.uk> <87y4xfz1fi.fsf@nikula.org>\r
47         <cunegz71aw9.fsf@gargravarr.hh.sledj.net>\r
48 MIME-Version: 1.0\r
49 Content-Type: multipart/mixed; boundary="mJm6k4Vb/yFcL9ZU"\r
50 Content-Disposition: inline\r
51 In-Reply-To: <cunegz71aw9.fsf@gargravarr.hh.sledj.net>\r
52 User-Agent: Mutt/1.5.22.1-rc1 (2013-10-16)\r
53 X-Source-IP: acsinet21.oracle.com [141.146.126.237]\r
54 Cc: Tomi Ollila <tomi.ollila@iki.fi>, notmuch@notmuchmail.org\r
55 X-BeenThere: notmuch@notmuchmail.org\r
56 X-Mailman-Version: 2.1.13\r
57 Precedence: list\r
58 List-Id: "Use and development of the notmuch mail system."\r
59         <notmuch.notmuchmail.org>\r
60 List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
61         <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
62 List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
63 List-Post: <mailto:notmuch@notmuchmail.org>\r
64 List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
65 List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
66         <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
67 X-List-Received-Date: Fri, 06 Jun 2014 10:40:52 -0000\r
68 \r
69 \r
70 --mJm6k4Vb/yFcL9ZU\r
71 Content-Type: text/plain; charset=utf-8\r
72 Content-Disposition: inline\r
73 \r
74 Hi,\r
75 \r
76 \r
77 So I wrote some code which works for me well. I have erased ~40k\r
78 messages out of 500k. It does not try to be complete solution, it only\r
79 detects and removes the obvious cases. The idea is to help me control\r
80 the number of duplicates when I import big mail archives which surely\r
81 contain many duplicates into my mail database.\r
82 \r
83 > Thinking about this a bit...\r
84 \r
85 > The headers are likely to be different, so you could remove them (get\r
86 > rid of everything up to the first empty line).\r
87 \r
88 Yes, that's what I ended up doing. And I delete the files which have\r
89 less 'Received:' headers.\r
90 \r
91 \r
92 > Various mailing lists add footers, so you would need to remove them (a\r
93 > regular expression based approach would catch most of them easily).\r
94 \r
95 I defined a list of known footers. Then I take the two mails with the\r
96 same message-id, create diff between them and  compare it to the list of\r
97 footers.\r
98 \r
99 \r
100 > The remaining content should be the same for identical messages, so a\r
101 > sensible hash (md5) could be used to compare.\r
102\r
103 > Although, some MTAs modify the body of the message when manipulating\r
104 > encoding. I don't know how to address this.\r
105 \r
106 I'm attaching my perl script if anyone is interested. It's in no way\r
107 complete solution. It is supposed to be used as\r
108 \r
109 notmuch search --output=files --duplicate=2 '*' > dups\r
110 ./dedup # It opens the file 'dups'\r
111 \r
112 The attached version does not remove anyting (the 'unlink' command is\r
113 commented out).\r
114 \r
115 \r
116 Interestingly this does not work (it seems to return all messages):\r
117 notmuch search --output=messages --duplicate=2 '*'\r
118 \r
119 Also I have found that if I run 'notmuch search' and 'notmuch new' at\r
120 the same time, the notmuch search crashes sometimes. That's why I don't\r
121 use\r
122 \r
123 notmuch search ... | ./dedup\r
124 \r
125 Use with care :)\r
126 \r
127 Thank you for your help\r
128 -- \r
129         Vlad\r
130 \r
131 --mJm6k4Vb/yFcL9ZU\r
132 Content-Type: text/plain; charset=utf-8\r
133 Content-Disposition: attachment; filename=dedup\r
134 \r
135 #!/usr/bin/perl\r
136 \r
137 use Data::Dumper;\r
138 use List::Util;\r
139 \r
140 \r
141 @TO_IGNORE= (\r
142 \r
143 <<'EOT'\r
144 > _______________________________________________\r
145 > notmuch mailing list\r
146 > notmuch@notmuchmail.org\r
147 > http://notmuchmail.org/mailman/listinfo/notmuch\r
148 EOT\r
149 \r
150 ,\r
151 \r
152 <<'EOT'\r
153 > _______________________________________________\r
154 > Userland-perl mailing list\r
155 > Userland-perl@userland.us.oracle.com\r
156 > http://userland.us.oracle.com/mailman/listinfo/userland-perl\r
157 EOT\r
158 \r
159 ,\r
160 \r
161 <<'EOT'\r
162 > _______________________________________________\r
163 > Mercurial mailing list\r
164 > Mercurial@selenic.com\r
165 > http://selenic.com/mailman/listinfo/mercurial\r
166 EOT\r
167 \r
168 ,\r
169 \r
170 <<'EOT'\r
171 > --    \r
172 > To unsubscribe from this list go to the following URL and read the\r
173 > instructions:  https://lists.samba.org/mailman/options/samba\r
174 EOT\r
175 \r
176 ,\r
177 \r
178 <<'EOT'\r
179\r
180 EOT\r
181 \r
182 );\r
183 \r
184 sub rm($$) {\r
185         my ($file, $comment) = @_;\r
186         print "-> $file\n";\r
187         print $comment;\r
188         # unlink $file;\r
189 }\r
190 \r
191 sub check_mail_id($) {\r
192         $ID = $_[0];\r
193 \r
194         unless (open ID, "-|", "./notmuch", "search", "--output=files", "id:$ID") {\r
195                 warn "Can not fork: $!";\r
196                 return;\r
197         }\r
198         chomp(@FILES = <ID>);\r
199         close ID;\r
200 \r
201         if (scalar @FILES <= 1) {\r
202                 warn "Not enough files for ID:$ID\n";\r
203                 return;\r
204         }\r
205 \r
206         my ($F1, $F2) = @FILES;\r
207 \r
208         unless (-r $F1) {\r
209                 warn "Can not read $F1 in ID:$ID\n";\r
210                 return;\r
211         }\r
212         unless (-r $F2) {\r
213                 warn "Can not read $F2 in ID:$ID\n";\r
214                 return;\r
215         }\r
216         if ($F1 eq $F2) {\r
217                 warn "Same filename $F1\n in ID:$ID\n";\r
218                 return;\r
219         }\r
220 \r
221         unless (open DIFF_WHOLE, "-|", $diff, $F1, $F2) {\r
222                 warn "Can not fork $diff: $!\n";\r
223                 return;\r
224         }\r
225         $DIFF_WHOLE = join "", <DIFF_WHOLE>;\r
226         close DIFF_WHOLE;\r
227 \r
228         if ( length($DIFF_WHOLE) == 0 ) {\r
229                 rm $F2, "deleting_1\nID:$ID\n\n";\r
230                 return;\r
231         }\r
232 \r
233         # 35a36\r
234         # > Content-Length: 893\r
235         if (\r
236                 $DIFF_WHOLE =~ /^\d+a\d+\n> Content-Length: \d+$/\r
237                 or\r
238                 $DIFF_WHOLE =~ /^\d+d\d+\n< Content-Length: \d+$/\r
239         ) {\r
240                 rm $F2, "deleting_2\nID:$ID\n\n";\r
241                 return;\r
242         }\r
243 \r
244 \r
245 \r
246         # $r="[a-zA-Z0-9 ()[\]\.\+:/=;,\t-]+";\r
247         # if (\r
248         #       $DIFF_WHOLE =~ /1,7d0\n< Received:$r\n< \t$r\n< \t$r\n< Received:$r\n< \t$r\n< \t$r\n< \t$r\n\d+a\d+,\d+\n> Content-Length:$r\n> Lines:$r/\r
249         # ) {\r
250         #       printf "deleting_3\nID:$ID\n$DIFF_WHOLE\n\n";\r
251         #       return;\r
252         # }\r
253 \r
254         unless (open DIFF_BODY, "-|", "bash", "-c", "$diff <(sed -e 1,/^\$/d \"\$1\" ) <(sed -e 1,/^\$/d \"\$2\" )", "", $F1, $F2) {\r
255                 warn "Can not fork $diff (2): $!\n";\r
256                 return;\r
257         }\r
258         $DIFF_BODY = join "", <DIFF_BODY>;\r
259         close DIFF_BODY;\r
260 \r
261         if ( length($DIFF_BODY) == 0 ) {\r
262                 # The bodies are the same - let's find which one has less\r
263                 # Received: headers and delete that\r
264                 unless (open F, $F1) \r
265                 {\r
266                         warn "Can't open F1 '$F1': $!";\r
267                         return;\r
268                 }\r
269                 my $count1 = grep { /^Received: / } <F>;\r
270                 close F;\r
271                 unless (open F, $F2) \r
272                 {\r
273                         warn "Can't open F2 '$F2': $!";\r
274                         return;\r
275                 }\r
276                 my $count2 = grep { /^Received: / } <F>;\r
277                 close F;\r
278 \r
279                 if ($count1 > $count2) {\r
280                         rm $F2, "deleting_4a\nID:$ID\n\n";\r
281                 } else {\r
282                         rm $F1, "deleting_4b\nID:$ID\n\n";\r
283                 }\r
284                 return;\r
285         }\r
286 \r
287 \r
288         for (@TO_IGNORE) {\r
289                 next unless $DIFF_BODY =~ $_;\r
290                 # Remove the first one as the second is adding lines\r
291                 rm $F1, "deleting_5\nID:$ID\n\n";\r
292                 return;\r
293         }\r
294 \r
295         for (@TO_IGNORE_REVERSE) {\r
296                 next unless $DIFF_BODY =~ $_;\r
297                 # Remove the second as it is removing some lines\r
298                 rm $F2, "deleting_6\nID:$ID\n\n";\r
299                 return;\r
300         }\r
301 \r
302         #--------------------------------------------------\r
303         # '2c2\r
304         # < --Boundary_(ID_DK6KMxNlhttcScVv/QSi8A)\r
305         # ---\r
306         # > --Boundary_(ID_dlReFj9tgdNWy+1SUxwTeQ)\r
307         # 39c39\r
308         # < --Boundary_(ID_DK6KMxNlhttcScVv/QSi8A)\r
309         # ---\r
310         # > --Boundary_(ID_dlReFj9tgdNWy+1SUxwTeQ)\r
311         # 55c55\r
312         # < --Boundary_(ID_DK6KMxNlhttcScVv/QSi8A)--\r
313         # ---\r
314         # > --Boundary_(ID_dlReFj9tgdNWy+1SUxwTeQ)--\r
315         #-------------------------------------------------- \r
316         $re = qr/(\d+)c\1\n< --Boundary_\(\S+\)(?:--)?\n---\n> --Boundary_\(\S+\)(?:--)?\n/;\r
317         if ( $DIFF_BODY =~ m/^(?:$re+)$/ ) {\r
318                 # Change in boundary strings\r
319                 rm $F2, "deleting_7\nID:$ID\n\n";\r
320                 return;\r
321         }\r
322 \r
323         print "DIFF_BODY (ID: $ID):\n'$DIFF_BODY'\n\n" if length $DIFF_BODY < 300;\r
324 }\r
325 \r
326 $diff = 'diff';\r
327 $diff = 'gdiff' if -x '/usr/bin/gdiff'; # Solaris\r
328 \r
329 # First create reverse regexps (removing lines from the mail) so that we don't\r
330 # overwrite the original @TO_IGNORE\r
331 @TO_IGNORE_REVERSE = map {\r
332         $x = $_;                       # Make sure we don't change the @TO_IGNORE array\r
333         $x =~ s/^>/</mg;               # Make sure all the lines are adding a text\r
334         qr/^(?:\d+,)?\d+d\d+\n\Q$x\E$/ # 1,2d3 or 2d3\r
335 } @TO_IGNORE;\r
336 \r
337 # Now map the positive regexp (adding lines to the mail)\r
338 @TO_IGNORE = map {\r
339         s/^</>/mg;                      # Make sure all the lines are removing text\r
340         qr/^\d+a\d+?(?:,\d+)?\n\Q$_\E$/ # 115a116,119 or 114a116\r
341 } @TO_IGNORE;\r
342 \r
343 # File 'dups' is created via\r
344 # notmuch search --output=files --duplicate=2 '*' > dups\r
345 \r
346 open INPUT, "dups" or die "Can't open dups: $!\n";\r
347 while (<INPUT>) {\r
348         chomp;\r
349         if (open FILE, $_) {\r
350                 $id =  List::Util::first { s/^message-id:.*<(.*)>\n$/\1/i } <FILE>;\r
351                 close FILE;\r
352                 check_mail_id $id if defined $id;\r
353         } else {\r
354                 print "Can't find '$_\n'";\r
355         }\r
356 }\r
357 close INPUT;\r
358 \r
359 --mJm6k4Vb/yFcL9ZU--\r