thunderbird-filters-convertor.pl 14 KB

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