# Copyright (c) 2007 celmorlauren limited. All rights reserved. # This program is free software; you can redistribute it and/or modify it under # the same terms as Perl itself. package Sendmail::M4::Utils; require Exporter; use vars qw(@ISA @EXPORT $VERSION); use strict; @ISA = qw(Exporter); @EXPORT = (); $VERSION= 0.27; use IO::File; use IO::Select; use IPC::Open3; use File::Copy; use English; #debug use Data::Dumper; =head1 NAME Sendmail::M4::Utils - create and test sendmail M4 hack macro files =head1 STATUS Version 0.27 (Beta) This compiles the M4 sendmail hack used by celmorlauren since version 0.23 HTML coding just STUBS at the moment. =head1 SYNOPSIS Sendmail is arguably the most powerfull and configurable e-mailing system in the world, however it does tend to be intimidating to System Adminstrators without a good foundation in programming. It is a very good idea to look at the "O'Reilly" publications "sendmail 3rd edition +" and their "Sendmail Cookbook", most tasks that need to be done can be solved by having a look at the "CookbooK". Where a solution can not be found in the "Cookbook" or an existing "Hack" you will need to create your own. Creating and testing B can be a tiresome and error prone business, this script has been developed to help, however you will still need to understand sendmail macros to use this. Testing methods are desgined to be used by both the commamd line and via HTML using a web browser. Please note that you will have to hand edit your B file, to include the reference to the B being generated, below is an example taken from our own B file. The line you must include, begins with B the hack file follows, the current development version can be found as L and L, B is the program, B is its module, see their documetation for more. dnl We use the generic m4 macro definition. This defines dnl an extented .forward and redirect mechanism. dnl DOMAIN(`generic')dnl dnl HACK(`mail8-stop-fake-mx')dnl dnl These mailers are available. per default only smtp is used. You have dnl to add entries to /etc/mail/mailertable to enable one of the other dnl mailers. dnl MAILER(`local')dnl MAILER(`smtp')dnl MAILER(`procmail')dnl MAILER(`uucp')dnl MAILER(`bsmtp')dnl MAILER(`fido')dnl dnl dnl Just an other (open)ldap feature is the usage of maill500 as mailer dnl for a given (open)ldap domain (see manual page mail500). dnl dnl MAILER(`mail500', `place_here_your_openldap_domain')dnl dnl dnl This line is required for formating the /etc/sendmail.cf dnl LOCAL_CONFIG The most notable help are. =over 2 =over 2 =item MACRO{ When constructing "macros" is the ability to "nest" called macros within the text block of the calling "macro", below is an example of the development version of our ANTI-SPAM hack. rule <Screen_Local_check_mail_2 $&{CheckHelo} }MACRO R $* $@ $>ScreenMail8blocker ${mail3tt} }MACRO }MACRO RULE Without the "nested" macro structure this could be difficult to keep track of, and indeed it was, thats why we have developed this. =item Inline MACRO The above B also handles INLINE MACROS which enable much used logical statements to be included without the cost of another rule-set, this module includes a selection of these. =item Packed Macro {MashFound#} Most of the included INLINE MACROS use the packed macro {MashFound#}, which are designed to hold 9 long-names each, which of the {MashFound#} macros being refered to is invisable to the developer|user. And during testing the normal macro statement {####} where #### is a macro contained by {MashFound#} may be used, the testing program does all the required conversions. This is required due to the limited number of free to use long-names, B assigns long-names for it-self at run-time. And so working OK during testing does not mean that sendmail will not fail at run-time. It is recommended to keep develeoper long-names to under 16. =item TEST Automated testing, the inclusion of test data within the source program, some of which is highly automated. It is very easy to generate 4000 lines of test results, the B setup has expected replys, so will only stop on the unexpected, so any changes to a script can be checked with ease. =back After using this to generate your HACK M4 files you will never want to it by hand again! =back This module is non OO, and exports the methods descriped under EXPORTS. =head1 AUTHOR Ian McNulty, celmorlauren limited (registered in England & Wales 5418604). email Edevelopment@celmorlauren.comE =head1 USES =over 16 =item IO::File file creation =item IPC::Open3 to start "sendmail -bt -Ctest.cf" =item File::Copy to copy "tee" file to "file" in sendmails "hack" directory. =item English Data::Dumper debuging this! used by our exported method "debug" =back =head1 EXPORTS =cut =head2 HASH REF = setup(@_) returns HASH REF to internal hash %setup =over 4 This configures this module, and is always required first. The %setup hash is enclosed in a BEGIN block, to ensure that all programs and modules that use this get the same settings. Expected/Allowed values allways as a (hash value pairing). =over 16 =item hack_dir SCALAR with default value of "/usr/share/sendmail/hack", =item file SCALAR "hack file name" to generate, with either full path or just the name, no default. NOTE: "build" or "install" must also be specifed. NOTE: if "install" is also defined a backup copy of "file" is made if it already exists! =item sendmail SCALAR with default value of "/usr/sbin/sendmail" =item mc SCALAR with default value of /etc/mail/linux.mc, this is the sendmail m4 source file to be used to build "cf", this is required for 'installation' =item cf SCALAR "test.cf file name" to build for testing purposes. if "install" is specified and "cf" is not specified, will assume "test.cf" within current directory. if "install" is specified and "cf" is is "sendmail.cf" will "die"! otherwise will assume the main "sendmail.cf" is being tested. =item html HASH REF, default is 0 =item build SCALAR Generate|build "tee" file, this does not require root permissions. Enables you to check the "tee", before installing it. NOTE: ignored if also "html". =item install SCALAR SU "root" permissions are required. Copy "tee" file to "file", (sendmail hack directory file). Create "cf" file. NOTE: ignored if also "html". =item test SCALAR Will "build"|"install" before "test" if specified. =item silent SCALAR STOPS all output! AND character translation!! It is assumed that you are going to do something with the compiled rules. =item error ARRAY REF only when also "silent" has contents of "moan", "whoops" will allways simply exit. =item UNKNOWN ARRAY REF remaining unknown arguments supplied. =item tee SCALAR automatic info, name optained from "file", this file does not need "root" SU permissions, and is placed in the current working directory. Installation phase copies this to "file" which will need SU perms! NOTE: if "build" is also defined a backup copy of "tee" is made if it already exists! =item log SCALAR automatic info, as "tee" but appended with ".log". This file is generated during non "html" testing, contains all data entered by yourself and from "sendmail -bt". If "file" is not also defined then this file will not be generated. =item testing SCALAR automatic info, set when "test" starts, changes the way both "ok" and "echo" operate. =item SU SCALAR automatic info, is user "root":"root". =item time SCALAR automatic info, "time" script started. =item macro SCALAR automatic variable, incremented on MACRO statements =item rules ARRAY REF automatic list of read in "S" macro rules =item rule HASH REF automatic keyed by "rules" Format =over 4 rule { =over 2 Stest_macro => { =over 2 =over 12 =item S => [] contains complete "S" macro coding =item H => [] HINT's as to use =item O => [] keys for "T" in order of specification =item T => { TEST tests for coding =over 2 =over 12 =item n => { n = numeric count of test see L for details =back } =back } =item M => [] contains list of SUB macros. TOP Level S only! =item F => SCALAR only defined if FORCE is defined =item N => SCALAR only defined if NOTEST is defined =item G => SCALAR only defined if GLOBAL is defined Top Level S only 1st line after S definition. Reduces number of {macro_names} Limit of 96 ! =back } =back } =back =back =item inline HASH REF automatic, where a rule is to be inlined, rule should start life as a standard rule above, when known to work OK, then inline. No other changes are needed. TEST lines etc are ignored. Format is almost the the same as the the above rule, except most entrys are only here, so as not to break things. Format =over 4 inline { =over 2 Stest_macro => { =over 2 =over 12 =item S => [] contains complete "S" macro coding =item G => SCALAR only defined if GLOBAL is defined =item I => [] contains list of sub inlines =item H => [] exists only for compatability =item O => [] exists only for compatability =item T => {} exists only for compatability =item M => [] exists only for compatability =item F => SCALAR exists only for compatability =item N => SCALAR exists only for compatability =back } =back } =back =back =item sane HASH REF automatic, keyed normally by "rule", however anything may be used as a key. Generated noramally by the method "sane" and refernced during testing by the MACRO TEST sub statement SANE "key". Format =over 4 sane { =over 2 =over 12 =item key => [] sendmail .D statements =back } =back =back =item testing_domains HASH REF automatic, generated by method "testing_domains", used during testing. Format =over 4 testing_domains { =over 2 =over 12 =item OUR [ HELO, DOMAIN, IP, RESOLVE, FROM, RCPT \n ], =item OK [ HELO, DOMAIN, IP, RESOLVE, FROM, RCPT \n ], =item BAD [ HELO, DOMAIN, IP, RESOLVE, FROM, RCPT \n ], =back } =back testing_domains_keys { =over 2 HELO => 0, DOMAIN => 1, IP => 2, RESOLVE => 3, FROM => 4, RCPT => 5, =back } =back Lists|lines of "," delimited values. "OUR" is your domain, "OK" are legal domains and should be ok "BAD" are faked|forged domains and should allways fail. =item FOUND HASH REF automatic, generated by MACRO statements such as FOUND, this uses just ONE "long name" to store as many FOUND statements as needed. Format =over 4 FOUND => { =over 2 =over 12 =item LIST => [] list of FOUND keys =item KEY => {} key is {macro} value is FOUND key =back }, =back =back =item MASH_FOUND HASH REF automatic, used during testing to keep current values for {MashFound} packed components. Format =over 4 MASH_FOUND => { =over 2 macro => value, macro => value, macro => value, macro => value, =back } =back =item magic SCALAR special value used by this program, do not use. =item paranoid SCALAR value used by "Mail8" see its page for meaning. =back =back =cut =head2 debug @_ =over 4 debug prints out B info, and anything supplied to it, and asks for input, nothing and it will simply return, "n" or "no" and it B. Note any refs supplied will parsed by B from package Data::Dumper Included to help to debug this and modules that use it. Also when your code is OK it is easy to find and remove. =back =cut push @EXPORT, "debug"; sub debug { print "----STACK------------\n"; my ($method,@stack) = &caller_ref(); my @m_stack = map { print " $_\n" } (@stack); print "----DUMPER-----------\n"; map { print "$_\n" } map { (ref $_)?(Dumper $_):($_) } ( @_ ); print "==================\nCarry on?>[Y|n]:>"; my $d = &getline(); scalar $d and $d=~/n/i and exit; } # debugging switch my $DEBUG; #global so all can see it my %setup; BEGIN { # Need to know If this is being used by a SU root user my $gid = $GID; $gid =~ s/\s+.+//; my $root = (scalar $UID or scalar $gid )?(0):(1); my $time = localtime; #configure it here %setup = ( magic => 0, paranoid=> 0, hack_dir=> "/usr/share/sendmail/hack", file => 0, sendmail=> "/usr/sbin/sendmail", mc => "/etc/mail/linux.mc", cf => 0, html => 0, build => 0, install => 0, test => 0, testing => 0, silent => 0, tee => 0, log => 0, UNKNOWN => [], SU => $root, time => $time, macro => 0, rule => {}, rules => [], sane => {}, testing_domains => { OUR => [], OK => [], BAD => [], }, testing_domains_keys => { HELO => 0, DOMAIN => 1, IP => 2, RESOLVE => 3, FROM => 4, RCPT => 5, }, FOUND => { LIST => [], KEY => {}, }, MASH_FOUND => {}, ); } push @EXPORT, "setup"; sub setup { while (scalar @_) { my $hash = shift @_ or last; if ( exists $setup{$hash} ) { $setup{$hash} = shift @_; } else { push @{$setup{'UNKNOWN'}}, $hash; last; } } push @{$setup{'UNKNOWN'}}, @_ if scalar @_; if ( $setup{'silent'} ) { map { $setup{$_} = 0 } (qw(file tee log cf html build install test)); } if ( $setup{'html'} ) { map { $setup{$_} = 0 } (qw(file tee log build install)); $setup{'test'} = 1; } # can not install if not root $setup{'install'} = 0 unless $setup{'SU'}; if ( $setup{'build'} or $setup{'install'} ) { if ( my $file = $setup{'file'} ) { my $tee; # ok has file a path? if ( $file =~ /\// ) { my @tee = split "/", $file; $tee = pop @tee; $setup{"hack_dir"} = join "/", @tee; } # ok place in std sendmail hack dir elsif ( my $hack_dir = $setup{'hack_dir'} ) { $setup{'file'} = "$hack_dir/$file"; $tee = $file; } # something wrong else { $tee = 0; } # auto install on callback? magic is needed as otherwise build has precedence if ( $setup{'magic'} and $setup{'install'} and $tee and -f $tee ) { $setup{'tee'} = $tee; &install(); exit; } if ( $setup{'build'} and scalar $tee ) { my $time= $setup{'time'}; if ( -f $tee ) { unless ( rename $tee, "$tee.$time~" ) { &moan("unable to archive existing $tee file"); undef $tee; &ok("STOP RUN") and exit; } } } # auto install on callback? elsif ( $setup{'install'} and scalar $tee and -f $tee ) { $setup{'tee'} = $tee; &install(); exit; } unless ( scalar $tee ) { map { $setup{$_} = 0 } (qw(file tee log build install)); &moan( "err unable to obtain \"tee\" from \"file\"", map { "$_ = $setup{$_}" } (qw(install build file))); } $setup{'tee'} = $tee; } else { map { $setup{$_} = 0 } (qw(file tee log build install)); } } if ( $setup{"test"} and $setup{"tee"} ) { my $log = $setup{"log"} = "$setup{'tee'}.log"; my $time= $setup{'time'}; if ( -f $log ) { unless ( rename $log, "$log.$time~" ) { &moan("unable to archive existing $log file"); &ok("STOP RUN") and exit; } } } $setup{'cf'} = "test.cf" unless scalar $setup{'cf'}; $setup{'cf'} =~ /sendmail\.cf/ and $setup{'install'} and die "install&cf=sendmail.cf"; return \%setup; } =head2 0 = moan(@_) allways returns 0 =over 4 Either prints out to STDERR or to a ItdEEtableE> HTML table depending on use. Expects a list of moaning messages. If setup{silent} places complaints in setup{error} instead of displaying Perhaps this should be in Carp? And just to let you know, our own comment module will be on CPAN soon, just as soon as the requested name space has been OKed, will be B, not uploaded yet due to the module that depends on it not being ready. =back =cut # this is the common code for both moan and whoops sub caller_ref { #0 is ourselves! my $i = 1; my @stack; my $method = "moan"; while((my($pack,$file,$line,$subname,@others) = caller($i++))) { my $stack; #us our package if ( $pack =~ /^Sendmail::M4::Utils$/ ) { $subname =~ /(show_moan|caller_ref)/ and next; $stack = "$subname ($line)"; if ( $subname =~ /sendmail_(moan|whoops)/ ) { pop @stack; } if ( $subname =~ /(moan|whoops)/ ) { my $method = $subname; $method =~ s/^Sendmail::M4::Utils:://; } } #someone using this package else { $stack = "$pack ($line) $subname"; } push @stack, $stack; } return ($method,@stack); } sub show_moan { my ($method,@stack) = caller_ref; my @m_stack = map { "$method $_" } (@stack); #display moan my @moan = ( @m_stack, map { "$method $_" } map { (ref $_)?(Dumper $_):($_) } ( @_ ), ); if ( $setup{'silent'} ) { my $e = $setup{'error'} = []; @$e = @moan; } elsif ( scalar $setup{'html'} ) { print "", "", map { "" } (@moan), "
$_
", ""; } else { my $moan = join "\n", @moan; no strict; print STDERR "$moan\n"; } return 0; } push @EXPORT, "moan"; sub moan { return show_moan @_; } =head2 whoops(@_) allways exits =over 4 Based on B and does much the same except it also exits. Perhaps this should be in Carp? =back =cut push @EXPORT, "whoops"; sub whoops { show_moan @_; exit; } #getline explict readline from STDIN, as this uses strict sub getline { my $line; { no strict; $line = ; } chomp $line; return $line; } =head2 $ok = ok("message") message defaults to "OK" or "TRY: " =cut push @EXPORT, "ok"; sub ok { =pod =over 4 =over 12 =item NOTE: NOT for HTML! or when "silent" ALLWAYS does nothing, just returns 1 or 0 if "testing". =back =cut ($setup{'html'} or $setup{'silent'}) and return ($setup{'testing'})?(0):(1); =pod print "message?" allways apends a ? =cut my ($package, $filename, $line) = caller; my $caller = ($package=~/Sendmail::M4::Utils/)?("($line)"):("$package ($line)"); my $def_msg= ($setup{'testing'})?("TRY: "):("OK"); my $ok_msg = shift @_; scalar $ok_msg or $ok_msg = $def_msg; print "$caller, $ok_msg?"; my $ok = getline; unless ($setup{'testing'}) { =pod Normal usage, when not "testing". =over 4 =over 32 =item "reply" "y" or "CR" returns 1 OK! =item anything else returns 0 NOT OK! =back =back =cut scalar $ok or return 1; return ($ok =~ /y/i)?(1):(0); } else { =pod During "testing" =over 4 =over 32 =item ESTDINE "CR" returns 0 =item anything else returned as is =back =back =back =cut return (scalar $ok)?($ok):(0); } } # tee, output to file, a bit like the shell command sub tee { my $file= ($setup{'testing'})?('log'):('tee'); my $tee = $setup{$file}; if (scalar $tee) { my $TEE; unless ( open $TEE, ">>$tee" ) { whoops "tee: cant open \"$file\" $tee","exit code $?"; undef $setup{$file}; return @_; } if ( scalar @_ ) { map { print $TEE "$_\n"; } (@_); } else { print $TEE "\n"; } close $TEE; } return @_; } =head2 @_ = translate @_ =over 4 Does all the formating for B & B. Currently =over 4 UTF8 ("pound" UKP)|("euro" E) to $ conversion, also converts 3+ spaces to a tab. EURO character works, but breaks Perldoc display for Perl 5.6! So for the B bits EURO character is shown either as B or B. POUND character works, but looks bad on CPAN, will display correctly on Perldoc for 5.8.8, but not on earlier versions, so is shown for these pages as B or B =back =back =cut push @EXPORT, "translate"; sub translate { return map { $_=~s/(£|€)/\$/g; $_=~s/\s{3,}/\t/g;$_ } map{ split "\n",$_ } (@_); } =head2 echo @_ =over 4 This produces output, both to the screen and to the "tee" file, most functions use this to output, this does a simple echo with no other formating other than shown below. During B no formating is done, text is output as is with just a "linefeed" appended. Otherwise. Sendmail expects tabed macro fields, however your "vi" session may be set to use spaces and colours etc, also "$" is used to signify a varity of things and this causes problems for Perl SCALARS. To get round these problems, and to allow for better looking text. =over 2 In your code use at least 3 spaces where sendmail expects a "tab", and use ("B" or "B") where sendmail expects a "$", however if you are not using a keyboard with either of these symbols then you will have to escape \$ as normal. "echo" does UTF8 ("pound" B)|("euro" B) to $ conversion, also converts 3+ spaces to a tab, this is done via B above. =back =over 12 =item NOTE: NOT for HTML! or when "silent" ALLWAYS does nothing, just returns 1 or 0 if "testing". =back =back =cut push @EXPORT, "echo"; sub echo { ($setup{'html'} or $setup{'silent'}) and return 1; if ( $setup{'testing'} ) { scalar @_ and map { print "$_\n"; } tee map{ split "\n",$_} (@_); } elsif ( scalar @_ ) { map { print "$_\n"; } tee translate(@_); } else { print "\n"; tee; } } =head2 dnl @_ =over 4 For sendmail "dnl" comments, wraps supplied args in "dnl". =over 12 =item NOTE: NOT for HTML! or when "silent" ALLWAYS does nothing, just returns 1 or 0 if "testing". =back =back =cut push @EXPORT, "dnl"; sub dnl { ($setup{'html'} or $setup{'silent'}) and return 1; echo map { "dnl $_ dnl" } map {split "\n",$_} (@_); } ###################### # rule ##################### =head2 define_MashFound @_ =over 4 It is safest to define {MashFound} before use, supply it with a list of {macro names} which will be stored within this packed macro, sets up %setup{FOUND} and %setup{MASH_FOUND}. =back =cut push @EXPORT, "define_MashFound"; sub define_MashFound { my $FOUND = $setup{'FOUND'}; my $L_KEY = scalar @{$FOUND->{'LIST'}}; my $FOUND_LIST; unless ( scalar $L_KEY ) { $FOUND_LIST = $FOUND->{'LIST'}->[0] = []; } else { $L_KEY--; $FOUND_LIST = $FOUND->{'LIST'}->[$L_KEY]; } my $KEY = scalar @$FOUND_LIST; my $MASH_FOUND = $setup{'MASH_FOUND'}; foreach my $load_macro (@_) { if ( $KEY > 8 ) { $L_KEY++; $KEY = 0; $FOUND_LIST = $FOUND->{'LIST'}->[$L_KEY] = []; } $KEY = push @$FOUND_LIST, $load_macro; $FOUND->{'KEY'}->{$load_macro} = [$L_KEY, $KEY ]; $MASH_FOUND->{$load_macro} = "none"; } } sub DEFINE_MASHFOUND { my $L_KEY = scalar @{$setup{'FOUND'}->{'LIST'}}; $L_KEY--; my $inits = " £| 0" x 9; my $key = < for the HASH setup{testing_domains}, remaining argument|lines are ("," delimeted (HELO, DOMAIN, IP, RESOLVE, FROM, RCPT) values, which are for use during testing. Referenced during testing by B =over 2 where B is one of (E,F,O,V), B is one of (OUR,OK,BAD), and B is one of (HELO,DOMAIN,IP,RESOLVE,FROM,RCPT) =back Format OUR mail.celmorlauren.com, 0, 80.176.153.184, FAIL, development@celmorlauren.com, ian@daisymoo.com mail.celmorlauren.co.uk, 0, 80.176.153.184, FAIL, development@celmorlauren.co.uk, ian@daisymoo.com mail.daisymoo.com, 0, 80.176.153.184, FAIL, development@daisymoo.com, ian@daisymoo.com BAD this.is.bogus.bogus, 0, 10.0.3.4, FAIL, you@localhost, ian@daisymoo.com So long as there is a blank line, between B then definitions for OUR,OK,BAD can be done together as the sample above shows. This also allows "#" comment lines to be included for clarity. You may notice that our IP does not resolve to a domain, that is a common problem and so B does not care about that, it only cares that the B resolves to the connected B, the B of OK stops a B look-up. =back =cut push @EXPORT, "testing_domains"; sub testing_domains { my ($rule, $rule_set); my @macro_rule = map { split "\n", $_ } (@_); foreach my $in_line (@macro_rule) { unless ( scalar $in_line ) { $rule = undef; next; } my $line = $in_line; $line =~ s/^\s+//; if ( $line =~ /^#/ ) { next; } elsif ( $line =~ /^(OUR|OK|BAD)$/ ) { $rule = $1; $rule_set = $setup{"testing_domains"}->{$rule}; next; } scalar $rule or whoops "testing_domains requires a key of (OUR|OK|BAD)", \@macro_rule; $line =~ s/,\s+/,/g; push @$rule_set, $line; } } =head2 SCALAR inline SCALAR (optional) =over 4 single argument must be either B<0> or <1> or someother B quantity. Always returns current value for B. Argument is purely optional,if not supplied just returns current value. This switches B or B the B statement for Bs and Bs contained within them, enabling inline cabable Bs to be tested as seperate B and then inlined when known to be OK, it should be noted testing is required to ensure the inlining does not cause unwanted side effects. Initial value is B|B<0> =back =cut push @EXPORT, "inline"; my $inline = 0; sub inline { scalar @_ and $inline = shift @_; return $inline; } =head2 sane @_ =over 4 "sane" expects at least two arguments|lines, the first is the B for the HASH setup{sane}, remaining argument|lines are statements to be encoded as B statments, statements are "," delimited. Referenced during testing by B Format Local_check_mail {client_addr}127.0.0.1, {client_name}Localhost, {client_resolve}OK =back =cut push @EXPORT, "sane"; sub sane { my (@macro_rule); my $rule = shift @_; if ( scalar @_ ) { @macro_rule = map { split ",", $_ } map { $_ =~ s/,\s+/,/g; $_ } map { split "\n", $_ } (@_); } else { @macro_rule = map { split ",", $_ } map { $_ =~ s/,\s+/,/g; $_ } (split "\n", $rule); $rule = shift @macro_rule; } my $rule_set = $setup{"sane"}->{$rule} = []; @$rule_set = @macro_rule; } =head2 rule @_ =over 4 "rule" is the main worker, sendmail macros are very powerfull and usefull, you will need to understand the "sendmail" macro programming syntax to use this. =over 2 =over 4 =item 1 1st argument|line is the "S" macro rule, which must start with "S". =item 2 2nd argument|line B were B is the letter to use. B =over 4 GLOBAL is a special argument that is used to reduce the number of B, as B has a limit of B<96>. It works by using the B specified (defaults to Z) to base its naming policy, sub macros are numbered from ZERO. Use it if you have the sendmail error message "B" =back =item 3 2nd or 3rd argument|line B code is intended to be (inlined) =over 4 INLINE is a special argument that is used to reduce the number of B as B has a standard limit of B<100>. Used with the method B this will inline code rather than define them as B, resulting in a lower count of B at the expense of larger file size. Use it if you have the sendmail error message "B". Best policy is to test small sections as "rule sets" and inline when noted to be OK. But remember to ensure all works OK when inlined. =back =back Remaining argumentslines are the Macro, normally starting with "R", or something that make sense as a macro to sendmail. The generated macro code returns the supplied arg by default, unless the code returns first. =back Extensions to the sendmail syntax are =over 2 =cut push @EXPORT, "rule"; sub rule { my (@macro_rule,@macro_rules); my $rule = shift @_; if ( scalar @_ ) { @macro_rule = map { split "\n", $_ } (@_); } else { @macro_rule = split "\n", $rule; $rule = shift @macro_rule; } # init macro list with main S RULE, also only top level has a M list my $rule_set = { S => [], O => [], T => {}, M => [], H => [],}; my $macros = $rule_set->{"M"}; #GLOBAL #sendmail has a limit of 96 {macro_names} including its own! my $global = $macro_rule[0]; if ( $global =~ s/^\s*GLOBAL\s*// ) { shift @macro_rule; $global = "Z" unless scalar $global; $global = uc $global; $rule_set->{'G'} = $global; } #INLINE #sendmail has a standard limit of 100 {named rulesets} including its own my $INLINE = $macro_rule[0]; if ( $INLINE =~ s/^\s*INLINE\s*// ) { #inline also set global, for safty reasons my $use_inline = $inline; $INLINE =~ s/^ALLWAYS\s*// and $use_inline = 1; if ( $use_inline ) { $setup{"inline"}->{$rule} = $rule_set; $macros = $rule_set->{"I"} = []; $global = "Z" unless scalar $global; $rule_set->{'G'} = $global; $INLINE = 1; } else { $setup{"rule"}->{$rule} = $rule_set; $INLINE = 0; } } else { $setup{"rule"}->{$rule} = $rule_set; $INLINE = 0; } #keep backup copy for use later on @macro_rules = @macro_rule; # main rule, and any sub macros have the same properties ¯o($rule, $macros, \@macro_rules); #only standard rulesets have sub macros $macros = $rule_set->{"M"}; # now for output? But not if silent! (scalar $setup{"silent"} or scalar $INLINE) and return; #HTML layout if ( scalar $setup{"html"} ) { #TODO } else #Standard Layout { echo @{$rule_set->{"S"}}; # have we macros? (inline does not, or should not) foreach ( @$macros ) { $_ =~ /^NOSUCH\s+/ and next; echo; echo @{$setup{"rule"}->{$_}->{"S"}}; } echo; } } # MACRO for use within rules # usage where a sub macro is called as below, but we are only using it for IF ELSE reasons # R $* $: $>Screen_bad_relay $&{RelayIP} mail8 DB, spammer relay check # use MACRO # R $* $: MACRO{ $&{RelayIP} #mail8 DB, spammer relay check # R $* $: $>Screen_bad_relay2 $1 mail8 DB, spammer relay check # R $* $: $(SelfMacro {BadRelay} $@ $1 $) $1 # }MACRO # # MACRO code may be nested as deeply as required, also can be indented to improve readability # # # MashFound use a single "long name" instead of several sub MashPack { #TODO my @sane_define; #one day the packed macro {mash_found} may not be needed, but in the mean time to keep testing simple #translate sane and define statemnts into packed form if they have been declared my (@pre_sane, %pre_sane); my $mash_found = 0; foreach my $pre_sane (@_) { if ( $pre_sane =~ /\{/ ) { my $pre_mash = $pre_sane; $pre_mash =~ s/\{//; $pre_mash =~ s/\}.+$//; if ( exists $setup{'MASH_FOUND'}->{$pre_mash} ) { $pre_sane =~ s/\{\w+\}//; $setup{'MASH_FOUND'}->{$pre_mash} = $pre_sane; my ($L_KEY,$KEY) = @{$setup{'FOUND'}->{'KEY'}->{$pre_mash}}; $pre_sane{$L_KEY} = $KEY; $mash_found++; } else { push @sane_define, ".D$pre_sane"; } } else { push @sane_define, ".D$pre_sane"; } } if ( scalar $mash_found ) { foreach my $L_KEY ( keys %pre_sane ) { my $pre_sane = "Translate \$| $L_KEY \$| ".join ' $| ',(map "$setup{'MASH_FOUND'}->{$_}",@{$setup{'FOUND'}->{'LIST'}->[$L_KEY]}); if ( $pre_sane{$L_KEY} < 9 ) { my $diff = 9 - $pre_sane{$L_KEY}; $pre_sane .= " \$| 0" x $diff; } push @sane_define, $pre_sane; } } return @sane_define; } sub MashCalcs { my ($load_macro) = @_; my $FOUND = $setup{'FOUND'}; scalar $FOUND->{'KEY'}->{$load_macro} or define_MashFound $load_macro; my ($L_KEY,$KEY) = @{$FOUND->{'KEY'}->{$load_macro}}; my $FOUND_LIST = @{$FOUND->{'LIST'}->[$L_KEY]}; my $MashFound = "£|£+" x $KEY; my $end_KEY = $KEY - 1; my $MashRewrite = ""; if ( $KEY > 1 ) { $MashRewrite = "£|" . join "£|", (map "£$_", (1..$end_KEY)); } $end_KEY += 2; my $wild_end = ""; if ( $FOUND_LIST < 9 or $KEY < 9 ) { $MashFound .= "£|£+"; $wild_end = "£|£$end_KEY"; } return ( $L_KEY, $KEY, $MashFound, $MashRewrite, $wild_end ); } sub MashStore { my ( $L_KEY, $KEY, $MashFound, $MashRewrite, $wild_end ) = MashCalcs @_; my $key = <{$rule}; =pod INLINE =over 2 B must be the very first line, (after B if used), this B this macro rule instead of producing a real B, this statement only has effect if B has been used, otherwise it only modifies the generated maco not to return the original saved value (so as not to break things when inlined). B supports sub arguments =over 4 B which overrides the global value of $inline, meaning that this code will allways be INLINED, also that this code is expected to allways work correctly and does not require any testing, please refrain from using this youself as it is intended for internal program use. Most if not all internal MACROS are coded this way. Note: ALLWAYS is the 1st sub argument after INLINE, and other sub arguments may follow. Usage: INLINE ALLWAYS MASH INLINE ALLWAYS MASH TempA B which also stops the normal action of saving the original value. Usage: INLINE NOMASH B retores original saved value at the end of this macro rule, so for routines that are much used, they remain more like the original MACRO specification (without INLINE), also a over-ride value for MASH may follow, internal methods use B which results in {MashTempA} Uasage: INLINE MASH INLINE MASH TempA =back If a named B is inlined all its component Bs B also inlined! and so must also be compliant with B usage. Also note it is advised that B has also been specified, otherwise this will assume the default GLOBAL of Z. Note all code within the INLINED macro must be compliant with the usage, use of a RHS $@ will cause this to B complaing about the infrigment of use. Otherwise all the things that a normall macro use may be specified, however when B is in effect all B lines are ignored. May be used in explicitly named rulesets and MACROs, the entire line Bruleset $1> is replaced with the B that the ruleset refers to. =back =cut my $INLINE = $macro_rules->[0]; my $use_inline = $inline; my $allways; my ($NOMASH,$MASH,$OPTION,$TEMP); if ( $INLINE =~ s/^\s*INLINE\s*// ) { $INLINE =~ s/^ALLWAYS\s*// and $use_inline = $allways = 1; $INLINE =~ /^NOMASH\s*/ and $NOMASH = 1; $INLINE =~ s/^MASH\s*// and $MASH = 1 and $TEMP = $INLINE; shift @$macro_rules; if ( $use_inline ) { #are we using the parents macro settings? if ( $macro_inline and ref $macro_inline) { $rule_set = { S => $macro_inline->{'S'}, O => [], T => {}, H => [], G => $macro_inline->{'G'}, }; } else { $rule_hash = $setup{"inline"}; $rule_set = $rule_hash->{$rule}; } $rule_list = []; } $INLINE = 1; } =pod OPTION =over 2 B