implemtent RFE 4255, "Switching 'Thread view' on/off discards message selection"
[claws.git] / tools / thunderbird-filters-convertor.pl
blobd49cfea7afc6baf9888805122345319704d38786
1 #!/usr/bin/perl -w
3 use strict;
4 use Getopt::Long;
5 use URI::Escape;
7 # * This file is free software; you can redistribute it and/or modify it
8 # * under the terms of the GNU General Public License as published by
9 # * the Free Software Foundation; either version 3 of the License, or
10 # * (at your option) any later version.
11 # *
12 # * This program is distributed in the hope that it will be useful, but
13 # * WITHOUT ANY WARRANTY; without even the implied warranty of
14 # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # * General Public License for more details.
16 # *
17 # * You should have received a copy of the GNU General Public License
18 # * along with this program; if not, write to the Free Software
19 # * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 # *
21 # * Copyright 2007 Paul Mangan <paul@claws-mail.org>
22 # *
25 # Convert Thunderbird filtering rules to Claws Mail filtering rules
29 # TABLE OF EQUIVALENTS
31 # thunderbird : Claws Mail
32 #------------------------------------------------------
34 # name="NAME" : rulename "NAME"
36 # enabled="yes" : enabled / disabled
38 # CONDITION LIST
39 # --------------
41 # OR : |
42 # AND : &
44 # subject : subject
45 # from : from
46 # to : to
47 # cc : cc
48 # to or cc : to_or_cc
49 # body : body-part
50 # date : ****
51 # priority : ****
52 # status : ****
53 # age in days : age_greater/age_lower
54 # size : size_greater/size_smaller
55 # [custom header] : header
57 # 2nd level conditions
58 # --------------------
60 # contains : [nothing]
61 # doesn't contain : [append with ~]
62 # is : regexpcase
63 # isn't : regexpcase
64 # ends with : regexpcase
65 # begins with : regexpcase
66 # is in ab : found_in_addressbook
67 # isn't in ab : ~found_in_addressbook
70 # status 2nd and 3rd level conditions
71 # -----------------------------------
73 # [is|isn't] replied
74 # [is|isn't] read
75 # [is|isn't] new
76 # [is|isn't] forwarded
77 # [is|isn't] flagged
80 # Date header 2nd level condition
81 # --------------------------------
83 # is
84 # isn't
85 # is before
86 # is after
89 # Priority header 2nd and 3rd level conditions
90 # --------------------------------------------
91 # is [Lowest|Low|Normal|High|Highest]
92 # is higher than [Lowest|Low|Normal|High|Highest]
93 # is lower than [Lowest|Low|Normal|High|Highest]
96 # ACTION LIST
97 # -----------
99 # Move to folder : move
100 # Copy to folder : copy
101 # Forward : ****
102 # Reply : ****
103 # Mark read : mark_as_read
104 # Mark flagged : mark
105 # Label : ****
106 # Change priority : ****
107 # JunkScore 100 [mark as spam] : ****
108 # JunkScore 0 [mark as ham] : ****
109 # Delete : delete
110 # Delete from Pop3 server : delete
111 # Fetch body from Pop3Server : ****
114 my $script = "thunderbird-filters-convertor.pl";
115 my ($tbirdfile, $account, $mailbox, $iNeedHelp) = 0;
117 GetOptions("tbird-file=s" => \$tbirdfile,
118 "account-name=s" => \$account,
119 "mailbox-name=s" => \$mailbox,
120 "help|h" => \$iNeedHelp);
122 if ($iNeedHelp) {
123 help_me();
126 if (!$tbirdfile) {
127 print "ERROR: No filename given\n";
128 print "Use $script -h for help\n";
129 exit;
132 unless (-e $tbirdfile) {
133 print "ERROR: $tbirdfile NOT FOUND!!\n";
134 exit;
137 if (!$mailbox) {
138 print "ERROR: No mailbox name given\n";
139 print "Use $script -h for help\n";
140 exit;
143 my $config_dir = `claws-mail --config-dir` or die("ERROR:
144 You don't appear to have Claws Mail installed\n");
145 chomp $config_dir;
147 chdir($ENV{HOME} . "/$config_dir") or die("ERROR:
148 Claws Mail config directory not found [~/$config_dir]
149 You need to run Claws Mail once, quit it, and then re-run this script\n");
151 my $acrc = "accountrc";
152 my $acc_number;
154 if ($account) {
155 $acc_number = find_account_number();
157 if ($account && !$acc_number) {
158 print "ERROR: Account '$account' NOT FOUND!\n";
159 exit;
162 my @claws_filters = ();
164 ## check if matcherrc already exists
165 if (-e "matcherrc") {
166 print "matcherrc exists!\n";
167 read_current_filters();
168 } else {
169 push(@claws_filters, "[preglobal]\n\n[postglobal]\n\n[filtering]\n")
172 my ($rule_count,@thunderbird_filters) = read_thunderbird_filters();
174 my ($conv_rule,$ignored_rule,$ignore_list) = convert_filters($rule_count,@thunderbird_filters);
176 if (@claws_filters) {
177 system("mv matcherrc matcherrc-safecopy");
178 print "Moved ". $ENV{HOME}. "/$config_dir/matcherrc to "
179 . $ENV{HOME}. "/$config_dir/matcherrc-safecopy\n";
181 # write new config
182 open(MATCHERRC, ">>matcherrc");
183 print MATCHERRC @claws_filters;
184 close(MATCHERRC);
186 print "We're done!\n";
187 print "-------------\n";
188 print "Converted $conv_rule rules";
189 if (defined($ignored_rule)) {
190 print ", ignored $ignored_rule rules";
192 print "\n-------------\n";
193 print "$ignore_list";
195 exit;
197 sub help_me {
198 print<<'EOH';
199 Usage:
200 thunderbird-filters-convertor.pl [options]
201 Options:
202 --help -h Show this screen.
203 --tbird-file=PATH TO FILE The full path to the file to be converted
204 --mailbox-name=NAME The name of the Claws Mail mailbox
205 --account-name=NAME The name of the account to be used (optional)
207 exit;
210 sub find_account_number {
211 my $cur_acc_numb;
212 my $cur_acc_name;
214 open (ACCOUNTRC, "<$acrc") ||
215 die("Can't open the Accounts file [$acrc]\n");
216 my @acrclines = <ACCOUNTRC>;
217 close ACCOUNTRC;
219 foreach my $line (@acrclines) {
220 unless ($line =~ m/^\[Account/ ||
221 $line =~ m/^account_name/) { next; }
222 chomp($line);
224 if ($line =~ s/^\[Account: //) {
225 $line =~ s/]$//;
226 $cur_acc_numb = $line;
228 if ($line =~ s/^account_name=//) {
229 $cur_acc_name = $line;
231 if (defined($cur_acc_name) && $cur_acc_name eq $account) {
232 return($cur_acc_numb);
237 sub read_current_filters {
238 print "Reading current filters\n";
240 open (CFILTERS, "<matcherrc") ||
241 die("Can't open " . $ENV{HOME} . "/$config_dir/matcherrc");
242 @claws_filters = <CFILTERS>;
243 close CFILTERS;
245 remove_last_empty_lines();
248 sub remove_last_empty_lines {
249 my $line = pop(@claws_filters);
250 if ($line =~ m/^$/) {
251 remove_last_empty_lines();
252 } else {
253 push(@claws_filters, $line);
257 sub read_thunderbird_filters {
258 my @outer_array = ();
259 my @inner_array = ();
260 my $count = 0;
262 open (TBIRDFILE, "<$tbirdfile") ||
263 die("Can't open the tbird file [$tbirdfile]\n");
264 my @tbirdlines = <TBIRDFILE>;
265 close TBIRDFILE;
267 foreach my $line (@tbirdlines) {
268 if ($line =~ m/^version/ || $line =~ m/^logging/) { next; }
270 chomp($line);
272 push(@inner_array, "$line") unless $line eq "";
273 if ($line =~ m/^condition/) {
274 push(@outer_array, [@inner_array]);
275 @inner_array = ();
276 $count++;
279 return($count-1,@outer_array);
282 sub convert_filters {
283 my ($rule_count,@thunderbird_filters) = @_;
285 my $tbird_action_no_value = qr/^(?:"Mark read"|"Mark flagged"|"Delete"|"Delete from Pop3 server"|"Fetch body from Pop3Server")$/;
286 my $tbird_action_ignore = qr/^(?:"Label"|"Change priority"|"JunkScore"|"Fetch body from Pop3Server"|"Delete from Pop3 server"|"Reply")$/;
287 my $exact_matches = qr/^(?:subject|from|to|cc)$/;
288 my $ignore_matches = qr/^(?:date|priority|status)$/;
290 my $conv_rules = my $ignored_rules = 0;
291 my $ignored_list = "";
292 for (my $outerloop = 0; $outerloop <= $rule_count; $outerloop++) {
293 my $part_one = my $part_two = my $part_three = my $part_four = "";
294 my $ignore_rule = my $move_rule = my $copy_rule = my $cond_count = 0;
295 my %ignore_hash;
296 my $bool = my $claws_condition = my $cur_name = "";
297 for (my $innerloop = 0; exists($thunderbird_filters[$outerloop][$innerloop]); $innerloop++) {
298 my $entry = $thunderbird_filters[$outerloop][$innerloop];
299 if ($entry =~ s/^name=//) {
300 $cur_name = $entry;
301 $part_one = "rulename $entry ";
302 } elsif ($entry =~ s/^enabled=//) {
303 if ($entry eq "\"yes\"") {
304 $part_one = "enabled $part_one";
305 } else {
306 $part_one = "disabled $part_one";
308 if (defined($acc_number)) {
309 $part_one .= "account $acc_number ";
311 } elsif ($entry =~ s/^type=//) {
312 # do nothing : what does 'type' mean??
313 } elsif ($entry =~ s/^action=//) {
314 if ($entry =~ m/$tbird_action_ignore/ && !$ignore_rule) {
315 $ignore_rule = 1;
316 unless ($ignore_hash{$cur_name}) {
317 $ignored_list .= "Ignored $cur_name because it contains $entry\n";
318 $ignored_rules++;
320 $ignore_hash{$cur_name}++;
321 $part_one = "";
322 next;
323 } elsif ($entry =~ m/Move to folder/) {
324 $part_four = "move ";
325 $move_rule = 1;
326 } elsif ($entry =~ m/Copy to folder/) {
327 $part_three .= "copy";
328 $copy_rule = 1;
329 } elsif ($entry =~ m/Mark read/) {
330 $part_three .= "mark_as_read ";
331 } elsif ($entry =~ m/Mark flagged/) {
332 $part_three .= "mark";
333 } elsif ($entry =~ m/Delete/) {
334 $part_three .= "delete";
336 } elsif ($entry =~ s/^actionValue=//) {
337 if ($ignore_rule) {
338 $ignore_rule = 0;
339 next;
340 } elsif ($move_rule) {
341 $entry = rewrite_mailbox_name($entry);
342 $part_four .= uri_unescape($entry);
343 $move_rule = 0;
344 } elsif ($copy_rule) {
345 $entry = rewrite_mailbox_name($entry);
346 $part_three .= " " . uri_unescape($entry);
347 $copy_rule = 0;
349 } elsif ($entry =~ s/^condition=//) {
350 if ($entry =~ s/^\"AND//) {
351 $bool= "&";
352 } elsif ($entry =~ s/^\"OR//) {
353 $bool = "|";
355 my @tbird_conditions = split(/ \(/, $entry);
356 foreach my $cond (@tbird_conditions) {
357 my $exact = my $endswith = my $beginswith = my $addrbook = 0;
358 my $age_condition = my $size_condition = my $exact_age = 0;
359 $cond =~ s/\) OR$//;
360 $cond =~ s/\) AND$//;
361 $cond =~ s/\)"$//;
362 $cond =~ s/\\"/"/g;
363 my ($cpart_one, $cpart_two, $cpart_thr) = split(/,/, $cond, 3);
364 if ($cond) {
365 if ($cpart_one =~ m/$exact_matches/) {
366 $claws_condition .= "$cpart_one";
367 } elsif ($cpart_one eq "to or cc") {
368 $claws_condition .= "to_or_cc";
369 } elsif ($cpart_one eq "body") {
370 $claws_condition .= "body-part";
371 } elsif ($cpart_one eq "age in days") {
372 $age_condition = 1;
373 } elsif ($cpart_one eq "size") {
374 $size_condition = 1;
375 } elsif ($cpart_one =~ m/$ignore_matches/) {
376 $part_one = $claws_condition = $part_three = $part_four = "";
377 next;
378 } else {
379 $claws_condition = "header $cpart_one";
382 if ($cpart_two eq "doesn't contain") {
383 $claws_condition = "~$claws_condition matchcase";
384 } elsif ($cpart_two eq "contains") {
385 $claws_condition = "$claws_condition matchcase";
386 } elsif ($cpart_two eq "isn't") {
387 $exact = 1;
388 $claws_condition = "~$claws_condition regexpcase";
389 } elsif ($cpart_two eq "is") {
390 if ($size_condition) {
391 $claws_condition .= "size_equal";
392 } elsif ($age_condition) {
393 if ($bool ne "&") {
394 $part_one = $claws_condition = $part_three = $part_four = "";
395 if (!$ignored_list) {
396 $ignored_list .= "Ignored $cur_name because it matches an exact age and is an OR match\n";
398 next;
399 } else {
400 $ignored_rules--;
401 $exact_age = 1;
403 } else {
404 $exact = 1;
405 $claws_condition = "$claws_condition regexpcase";
407 } elsif ($cpart_two eq "ends with") {
408 $endswith = 1;
409 $claws_condition = "$claws_condition regexpcase";
410 } elsif ($cpart_two eq "begins with") {
411 $beginswith = 1;
412 $claws_condition = "$claws_condition regexpcase";
413 } elsif ($cpart_two eq "is in ab") {
414 $addrbook = 1;
415 $claws_condition = "found_in_addressbook \"$claws_condition\" in \"Any\" ";
416 } elsif ($cpart_two eq "isn't in ab") {
417 $addrbook = 1;
418 $claws_condition = "~found_in_addressbook \"$claws_condition\" in \"Any\" ";
419 } elsif ($cpart_two eq "is greater than") {
420 if ($size_condition) {
421 $claws_condition .= "size_greater";
423 if ($age_condition) {
424 $claws_condition .= "age_greater";
426 } elsif ($cpart_two eq "is less than") {
427 if ($size_condition) {
428 $claws_condition .= "size_smaller";
430 if ($age_condition) {
431 $claws_condition .= "age_lower";
435 if ($exact || $beginswith || $endswith) {
436 $cpart_thr = escape_regex($cpart_thr);
438 if ($exact) {
439 $cpart_thr = "^$cpart_thr\$";
440 } elsif ($beginswith) {
441 $cpart_thr = "^$cpart_thr";
442 } elsif ($endswith) {
443 $cpart_thr = "$cpart_thr\$";
445 unless ($addrbook) {
446 if ($exact_age) {
447 my $lower_limit = $cpart_thr-1;
448 my $upper_limit = $cpart_thr+1;
449 $lower_limit =~ s/^\"//;
450 $lower_limit =~ s/\"$//;
451 $upper_limit =~ s/^\"//;
452 $upper_limit =~ s/\"$//;
453 $claws_condition = "$claws_condition"."age_lower"
454 . " $upper_limit $bool "
455 . "age_greater $lower_limit ";
456 } elsif ($size_condition || $age_condition) {
457 $claws_condition = "$claws_condition $cpart_thr ";
458 } else {
459 $claws_condition = "$claws_condition \"$cpart_thr\" ";
463 if ($tbird_conditions[1] && $cond_count < $#tbird_conditions) {
464 $claws_condition = "$claws_condition$bool ";
467 $cond_count++;
469 if ($part_one) {
470 $conv_rules++;
471 push(@claws_filters, "$part_one$claws_condition$part_three$part_four\n");
476 push(@claws_filters, "\n");
477 return($conv_rules,$ignored_rules,$ignored_list);
480 sub rewrite_mailbox_name {
481 my ($path) = @_;
483 my $new_path;
485 my ($front,$back) = split(/\/\//, $path, 2);
487 if ($front =~ m/^"mailbox/) {
488 $new_path = "\"#mh/$mailbox/";
489 } else {
490 $new_path = "\"#imap/$mailbox/";
493 my ($box,$name) = split(/\//, $back, 2);
495 if ($new_path =~ m/^"#mh/) {
496 $name =~ s/^Inbox/inbox/;
497 $name =~ s/^Sent/sent/;
498 $name =~ s/^Drafts/draft/;
499 $name =~ s/^Trash/trash/;
501 $new_path = $new_path.$name;
503 return($new_path);
506 sub escape_regex {
507 my ($string) = @_;
509 my $escstr = "";
510 my $symbols = qr/^(?:\[|\]|\{|\}|\(|\)|\||\+|\*|\.|\-|\$|\^)$/;
511 my @chars = split(//, $string);
513 foreach my $char (@chars) {
514 if ($char =~ m/$symbols/) { $char = "\\\\$char"; }
515 $escstr .= $char;
518 return($escstr);