#!/usr/bin/perl ################################################################################ # This program is released under a Creative Commons # Attribution-NonCommerical-ShareAlike2.5 License. # # For more information, please see # http://creativecommons.org/licenses/by-nc-sa/2.5/ # # You are free: # # * to copy, distribute, display, and perform the work # * to make derivative works # # Under the following conditions: # Attribution: You must attribute the work in the manner specified by the # author or licensor. # Noncommercial: You may not use this work for commercial purposes. # Share Alike: If you alter, transform, or build upon this work, you may # distribute the resulting work only under a license identical # to this one. # # * For any reuse or distribution, you must make clear to others the license # terms of this work. # * Any of these conditions can be waived if you get permission from the # copyright holder. # # Your fair use and other rights are in no way affected by the above. ################################################################################ use strict; use Parse::RecDescent; ############################################################################### ### Version 0.1 beta (2007-03-08) ### ############################################################################### %::globalvars=( # Root of mail path (i.e. save $mail-root/$dir) 'mail-root' => 'IMAP', # Where errors should go (i.e. if error_message then; save $mail-root/$errors-to; finish; endif) 'errors-to' => 'errors', # Current vacation level 'vacation-level' => 0, # Vacation aliases 'vacation-alias' => '', # Vacation message path 'vacation-msg' => '$home/vacation/message', # Vacation log file 'vacation-log' => '$home/vacation/log', # Vacation memory file 'vacation-once' => '$home/vacation/once', # Vacation repeat rate 'vacation-repeat' => '2d', # Path to use for automatic archiving ($1 = YYYY-MM) 'archive-path' => 'IMAP/archive/$1/' ); %::secmeta=(); %::op_translate=( '==' => 'is', '!=' => 'is not', '=^' => 'begins', '!^' => 'does not begin', '=$' => 'ends', '!$' => 'does not end', '=|' => 'contains', '!|' => 'does not contain', '=~' => 'matches', '!~' => 'does not match', # for the difficult people 'is' => 'is', 'is not' => 'is not', 'begins' => 'begins', 'does not begin' => 'does not begin', 'ends' => 'ends', 'does not end' => 'does not end', 'contains' => 'contains', 'does not contain' => 'does not contain', 'matches' => 'matches', 'does not match' => 'does not match', ); $::indentlvl=0; $::inch=' '; $::indent=''; $::RD_ERRORS = 1; # Make sure the parser dies when it encounters an error $::RD_WARN = 1; # Enable warnings. This will warn on unused rules &c. $::RD_HINT = 1; # Give out hints to help fix problems. sub newHeader($) { %::secmeta = (); } # Automatic pretty-printing indentation sub println($) { my $line=$_[0]; $line=~s/^/$::indent/msg; print "$line\n"; } sub indent() { $::indent=$::inch x (++$::indentlvl); } sub outdent() { $::indent=$::inch x (--$::indentlvl); } sub globalSection() { println 'if error_message then'; indent; println "save $::globalvars{'mail-root'}/$::globalvars{'errors-to'}"; println 'finish'; outdent; println "endif\n"; println "add $::globalvars{'vacation-level'} to n0"; } sub metaSection($\@\@\@) { return 1 if @{$_[1]}; my $dir=$_[0]; my @match_rules=(defined(@{$_[2]}[0])) ? @{@{$_[2]}[0]} : (); my @actions=(defined(@{$_[3]}[0])) ? @{@{$_[3]}[0]} : (); if ($dir eq 'UNDELIVERED') { @match_rules = (!@match_rules) ? ('not delivered') : ('#comp and', 'not delivered', '#(', '#comp or', @match_rules, '#)'); section("###########\n## INBOX ##\n###########\n", [], [[@match_rules]], [[@actions]]); } elsif ($dir eq 'DESTROY') { @actions = (!@actions) ? ('seen') : (@actions, 'seen'); section("#!!!!!!!!!!!!!!!!#\n#! DELETE EMAIL !#\n#!!!!!!!!!!!!!!!!#\n", [], [[@match_rules]], [[@actions]]); } } sub outputMatchRules(\@) { my @match_rules=@{$_[0]}; my $rulecount=0; my $rulecomp='or'; if (@match_rules) { print 'if '; foreach my $rule (@match_rules) { if ($rule =~ /^#/) { if ($rule eq '#(') { # start group println ((($rulecount==0)?'':"$rulecomp ").'('); indent; $rulecount=0; } elsif ($rule eq '#)') { # end group outdent; println ')'; } elsif ($rule =~ /^#comp (.+)$/) { $rulecomp=$1; } } else { println ((($rulecount++==0)?'':"$rulecomp ").$rule); } } println "then"; print "\n"; indent; } } sub section($\@\@\@) { return 1 if @{$_[1]}; my $dir=$_[0]; #my @match_rules=@{@{$_[2]}[0]}; #my @actions=(); my @match_rules=(defined(@{$_[2]}[0])) ? @{@{$_[2]}[0]} : (); my @actions=(defined(@{$_[3]}[0])) ? @{@{$_[3]}[0]} : (); my $special=0; @actions=@{@{$_[3]}[0]} if (defined @{$_[3]}[0]); $special=1 if ($dir =~ /\n/); println <<_ ############################################################ ## Dir: $dir ############################################################ _ unless ($special); println "\n$dir" if ($special); outputMatchRules(@match_rules); println "save $::globalvars{'mail-root'}/$dir\n" unless ($special); if (defined($::secmeta{'ARCHIVE'})) { println 'if $tod_log matches "^(....-..)" then'; indent; println "save $::globalvars{'archive-path'}$::secmeta{'ARCHIVE'}"; outdent; println "endif\n"; } if (defined($::secmeta{'VACATION'})) { $::secmeta{'VACATION'}--; my $vacalias = join(' alias ', (split /\s+/, $::globalvars{'vacation-alias'})); $vacalias = ' alias '.$vacalias if ($vacalias ne ''); println "if personal$vacalias"; println "and \$n0 is above $::secmeta{'VACATION'} then"; indent; println 'mail'; indent; println 'to $reply_address'; println "subject \"Re: \$h_subject:\""; println "expand file $::globalvars{'vacation-msg'}"; println "log $::globalvars{'vacation-log'}"; println "once $::globalvars{'vacation-once'}"; println "once_repeat $::globalvars{'vacation-repeat'}"; outdent; outdent; println "endif\n"; } println $_ foreach (@actions); print "\n" if (@actions); println 'finish' unless ($::secmeta{'NOFINISH'}); if (@match_rules) { outdent; println 'endif'; } } my $grammar = q{ ConfigFile : CommentLine(s?) GlobalSection Section(s) eofile | GlobalSection : GlobalHeader NewLine GlobalLines { &::globalSection(); } | Section : WS MetaHeader WS NewLine CommentLine(s?) MetaDisabled(?) MetaActions(?) MatchRuleBlock(?) ActionBlock(?) { &::metaSection($item[2], $item[6], $item[8], $item[9]); } | WS SectionHeader WS NewLine CommentLine(s?) MetaDisabled(?) MetaActions(?) MatchRuleBlock(?) ActionBlock(?) { &::section($item[2], $item[6], $item[8], $item[9]); } | GlobalLines : GlobalLine GlobalLines | GlobalLine | GlobalLine : WS Comment WS NewLine | WS KeyVal WS NewLine | MatchRuleBlock : WS CompStyle WS NewLine MatchRuleBlock { $return = [ "#comp $item[2]" ]; push @{$return}, @{$item[5]}; } | WS CompStyle WS NewLine { $return = [ "#comp $item[2]" ]; } | WS Group WS NewLine MatchRuleBlock { $return = [ $item[2] ]; push @{$return}, @{$item[5]}; } | WS Group WS NewLine { $return = [ $item[2] ]; } | WS MatchRule WS NewLine MatchRuleBlock { $return = [ $item[2] ]; push @{$return}, @{$item[5]}; } | WS MatchRule WS NewLine { $return = [ $item[2] ]; } | CommentLine MatchRuleBlock { $return = $item[2]; } | CommentLine { $return = []; } | CommentLine : WS Comment WS NewLine MatchRule : MatchHeader | MatchAnyDomain | MatchAnyAddress | MetaActions : WS MetaAction WS NewLine MetaActions | WS MetaAction WS NewLine | CommentLine MetaActions | CommentLine | MetaAction : '@' /[A-Z]+/ RWS Value { $::secmeta{$item[2]}=$item[4]; 1; } | '@' /[A-Z]+/ { $::secmeta{$item[2]}=1; 1; } | ActionBlock : WS Action WS NewLine ActionBlock { $return = [ $item[2] ]; push @{$return}, @{$item[5]}; } | WS Action WS NewLine { $return = [ $item[2] ]; } | CommentLine ActionBlock { $return = $item[2]; } | CommentLine { $return = []; } | WS Custom WS NewLine ActionBlock { $return = [ $item[2] ]; push @{$return}, @{$item[5]}; } | WS Custom WS NewLine { $return = [ $item[2] ]; } | Action : copyTo | saveTo | Comment : /##.*/ { $item[1]; } | /#.*/ { ''; } | MetaHeader : '[!' /[A-Z]+/ '!]' { &::newHeader($item[2]); $item[2]; } | SectionHeader : '[' /[^]]+/ ']' { &::newHeader($item[2]); $item[2]; } | CompStyle : /(and|or)/i { $item[1]; } | Group : /(end)?/i /group/i { ($item[1] eq 'end')?'#)':'#('; } | Custom : 'custom' WS NewLine /.+?(?=endcustom)/ms 'endcustom' { $return = $item[4]; $return=~/^(\s*)/; $return=~s/^$1//msg; } | MatchHeader : /header/i RWS Key RWS Operator RWS Value { "\$h_$item[3]: $::op_translate{$item[5]} \"$item[7]\""; } | MatchAnyDomain : Negator(?) /anydomain/i RWS Value { 'foranyaddress $h_From:,$h_Reply-To:'."\n$::inch".'($thisaddress '.((@{$item[1]})?'does not match':'matches').' "@([^@]+\\.)?'.$item[4].'")'; } | MatchAnyAddress : /anyaddress/i RWS Operator RWS Value { 'foranyaddress $h_From:,$h_To:,$h_Cc:,$h_Reply-To:,$h_Resent-To:,$h_X-Envelope-To:'."\n$::inch".'($thisaddress '."$::op_translate{$item[3]} \"$item[5]\")"; } | MetaDisabled : '@DISABLED' WS NewLine { $item[0]; } | copyTo : /copyto/i RWS Value { "deliver \"$item[3]\""; } | saveTo : /saveto/i RWS Value { "save $item[3]"; } | KeyVal : Key WS '=' WS Value { $::globalvars{$item[1]} = $item[5]; 1; } | Key : /\S+/ | Value : /"?/ /[^\n"]+/ /"?/ { $item[2]; } | GlobalHeader : '[!GLOBAL!]' { &::newHeader(''); } | Negator : /!/ | Operator : /[=!][=~\|\$\^]/ | /is( not)?/i | /(begins|ends|contains|matches)/i | /does not (begin|end|contain|match)/i | NewLine : /[\n\r]+/ | WS : /[ \t]*/ | RWS : /[ \t]+/ | eofile : /^\Z/ }; # All whitespace is significant; don't skip it please. $Parse::RecDescent::skip=''; my $parse = new Parse::RecDescent($grammar); my $configfile = join '',(<>); print "# Exim filter <<== do not edit or remove this line!\n\n"; $parse->ConfigFile($configfile);