Sendmail_M/M4/Utils.pm
# 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<sendmail hack macros> 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<sendmail m4 #.mc> file, to include the reference to the B<hack> being generated, below is an example taken from our own B<linux.mc> file.
The line you must include, begins with B<HACK> the hack file follows, the current development version can be found as L<Sendmail::M4::Mail8> and L<Sendmail::M4::mail8>, B<mail8> is the program, B<Mail8> 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 <<RULE;
SScreen_Local_check_rcpt_1
R $*.FOUND $@ MACRO{ $1 # checking for localusers and Trouble Tickets
R $*.mail3 $@ MACRO{ $1 # Trouble Ticket user
R $&{CheckRcpt} $@ MACRO{ $&{CheckRcpt} # Valid TT?
dnl TT must conform to minimal rules
R $* $: $>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<MACRO{> 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<sendmail> 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<TEST> 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 E<lt>development@celmorlauren.comE<gt>
=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</rule::TEST> 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<caller> info, and anything supplied to it, and asks for input, nothing and it will simply return, "n" or "no" and it B<exits>.
Note any refs supplied will parsed by B<Dumper> 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 I<E<lt>tdE<gt>E<lt>tableE<gt>> 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<Carp::Comment>, 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 "<td class = \"m4_error\">",
"<table class = \"m4_error\">",
map { "<tr><td>$_</td></tr>" } (@moan),
"</table>",
"</td>";
}
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<moan> 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 = <STDIN>;
}
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 <STDIN> "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 E<lt>STDINE<gt> "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<echo> & B<build>.
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<pod> bits EURO character is shown either as B<EURO> or B<E>.
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<POUND> or B<UKP>
=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<testing> 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<POUND>" or "B<EURO>") 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<UKP>)|("euro" B<E>) to $ conversion, also converts 3+ spaces to a tab, this is done via B<translate> 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 = <<KEY;
R £* £:$inits
KEY
foreach my $KEY (0..$L_KEY)
{
$key .= <<KEY;
R £* £: £(SelfMacro {MashFound$KEY} £@ £1 £) £1
KEY
}
return $key;
}
=head2 testing_domains @_
=over 4
"testing_domains" expects at least two arguments|lines, the first is the B<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<TEST AUTO(key KEY sub_key1 sub_key2,)>
=over 2
where B<key> is one of (E,F,O,V), B<KEY> is one of (OUR,OK,BAD), and B<sub_key#> 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<keys> 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<Mail8> does not care about that, it only cares that the B<HELO> resolves to the connected B<IP>, the B<RESOLVE> of OK stops a B<DNS> 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<scalar> quantity. Always returns current value for B<inline>.
Argument is purely optional,if not supplied just returns current value.
This switches B<ON> or B<OFF> the B<INLINE> statement for B<rule>s and B<MACRO>s contained within them, enabling inline cabable B<rule>s to be tested as seperate B<macros> 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<OFF>|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<key> for the HASH setup{sane}, remaining argument|lines are statements to be encoded as B<sendmail -bt .D> statments, statements are "," delimited.
Referenced during testing by B<TEST SANE(key)>
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<GLOBAL A> were B<A> is the letter to use. B<OPTIONAL>
=over 4
GLOBAL is a special argument that is used to reduce the number of B<sendmail {macro_names}>, as B<sendmail> has a limit of B<96>. It works by using the B<letter> 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<too many long names>"
=back
=item 3
2nd or 3rd argument|line B<INLINE> code is intended to be (inlined)
=over 4
INLINE is a special argument that is used to reduce the number of B<sendmail "named rulesets"> as B<sendmail> has a standard limit of B<100>. Used with the method B<inline> this will inline code rather than define them as B<rule sets>, resulting in a lower count of B<rule sets> at the expense of larger file size. Use it if you have the sendmail error message "B<too many named rulesets>".
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 = <<KEY;
R £* £: £(SelfMacro {MashTempB} £@ £1 £) £1
R £* £: £&{MashFound$L_KEY}
R $MashFound £: $MashRewrite£|£&{MashTempB}$wild_end
R £* £: £(SelfMacro {MashFound$L_KEY} £@ £1 £) £1
KEY
return $key;
}
sub MashFound
{
my ( $L_KEY, $KEY, $MashFound, $MashRewrite, $wild_end ) = MashCalcs @_;
my $LOAD_MACRO = "SelfMacro$_[0]";
my $key = <<KEY;
R £+.FOUND £: $LOAD_MACRO.£1.FOUND
R $LOAD_MACRO.£+ £: £(SelfMacro {MashTempB} £@ £1 £) £1
R £+.FOUND £: £&{MashFound$L_KEY}
R $MashFound £: $LOAD_MACRO.$MashRewrite£|£&{MashTempB}$wild_end
R $LOAD_MACRO.£+ £: £(SelfMacro {MashFound$L_KEY} £@ £1 £) £1
KEY
return $key;
}
sub MashFind
{
my ( $L_KEY, $KEY, $MashFound, $MashRewrite, $wild_end ) = MashCalcs @_;
my $key = <<KEY;
R £* £: £&{MashFound$L_KEY}
R $MashFound £: £$KEY
KEY
return $key;
}
sub macro
{
my ($rule, $macros, $macro_rules, $macro_inline) = @_;
my $rule_hash = $setup{"rule"};
my $rule_list = $setup{"rules"};
my $rule_set = $rule_hash->{$rule};
=pod
INLINE
=over 2
B<INLINE> must be the very first line, (after B<GLOBAL> if used), this B<inlines> this macro rule instead of producing a real B<named ruleset>, this statement only has effect if B<inline 1> 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<INLINE> supports sub arguments
=over 4
B<ALLWAYS> 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<NOMASH> which also stops the normal action of saving the original value.
Usage:
INLINE NOMASH
B<MASH> 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<TempA> which results in {MashTempA}
Uasage:
INLINE MASH
INLINE MASH TempA
=back
If a named B<rule seet> is inlined all its component B<MACRO>s B<must> also inlined! and so must also be compliant with B<INLINE> usage.
Also note it is advised that B<GLOBAL> 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<whoops> complaing about the infrigment of use.
Otherwise all the things that a normall macro use may be specified, however when B<inline> is in effect all B<TEST> lines are ignored.
May be used in explicitly named rulesets and MACROs, the entire line B<R $* $: $>ruleset $1> is replaced with the B<inlined code> 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<OPTION> must be the very first line, (after B<GLOBAL> if used), and can not be used with B<INLINE>, it supports sub arguments that alter the formatation of normal non INLINE macros.
B<OPTION> supports sub arguments
=over 4
B<NOMASH> which also stops the normal action of saving the original value.
Usage:
OPTION NOMASH
B<MASH> which forces the Macro to use a B<known> value for its mash
Usage:
OPTION MASH 1
Which generates {MashA1} if GLOBAL is A
=back
=back
=cut
elsif ( $INLINE =~ s/^\s*OPTION\s*// )
{
$INLINE =~ /^NOMASH\s*/ and $NOMASH = 1;
$INLINE =~ s/^MASH\s*// and $MASH = $OPTION = $INLINE;
shift @$macro_rules;
$INLINE = undef;
}
else
{
$INLINE = undef;
$MASH = 1;
}
my $macro = $setup{'macro'};
#my $rule_hash = $setup{"rule"};
#my $rule_list = $setup{"rules"};
my $rules = $rule_set->{"S"};
my $test_hash = $rule_set->{"T"};
my $test_list = $rule_set->{"O"};
my $hint_list = $rule_set->{"H"};
my $tests = 0;
my $mash = push @$rule_list, $rule;
#GLOBAL
my $global = $rule_set->{'G'};
if ( scalar $TEMP )
{
$mash = $TEMP;
}
elsif ( scalar $global and scalar $OPTION )
{
$mash = "$global$OPTION";
}
elsif ( scalar $OPTION )
{
$mash = $OPTION;
}
elsif ( scalar $global )
{
my $mashed = scalar @$macros;
$mash = "$global$mashed";
}
#save S argument to return if S does not return first
push @$rules, $rule unless ($INLINE and $use_inline);
push @$rules, "R £* £: £(SelfMacro {Mash$mash} £@ £1 £) £1" unless $NOMASH;
#read through supplied S definition
while ( my $line = shift @$macro_rules )
{
#remove all leading space
scalar $DEBUG and debug $line;
$line =~ s/^\s+//;
=pod
# comment line within Rule to improve readability, otherwise ignored
=cut
$line =~ /^#/ and next;
#=pod
#
#when code is INLINE B<$@> returns are a very bad idea, and will cause all sorts of strange problems, code may work as a MACRO but weird things happen when inlined!
#
#However if B<ALLWAYS> is defined then, the action of the macro will never vary, and so any return can be assumed to be OK
#
#=cut
unless ( scalar $allways )
{
$INLINE and $line =~ /\s{3,}(€|£|\$)@\s*/ and whoops "CODE IS INLINE!", $line, $rule, $rule_set;
}
#does line reference an inline coded "rule set" macro??
my $call_line = $line;
if ( $call_line =~ s/\s{3,}(€|£|\$):\s*(€|£|\$)>/\n/ )
{
my ($pre_line, $maybe_macro) = split "\n", $call_line;
$maybe_macro =~ s/\s+/\n/;
($maybe_macro) = split "\n", $maybe_macro;
if ( my $inline_macro = $setup{'inline'}->{"S$maybe_macro"} )
{
my $inline_code = $inline_macro->{'S'};
if ( scalar $inline_code and scalar @$inline_code )
{
push @$rules, @$inline_code;
}
else
{
whoops "INLINE $maybe_macro has an empty 'S'?", $inline_macro, $line, $rule, $rule_set;
}
next;
}
}
elsif ( $call_line =~ s/\s{1}MACRO\{// and $use_inline )
{
#peek ahead for inlined MACRO code
my $peek_is_GLOBAL = $macro_rules->[0];
my $peek_is_INLINE = $macro_rules->[1];
if ( scalar $peek_is_GLOBAL and $peek_is_GLOBAL =~ /(GLOBAL|INLINE)/ )
{
if ( $1 =~ /GLOBAL/ )
{
unless ( scalar $peek_is_INLINE and $peek_is_INLINE =~ /INLINE/ )
{
$peek_is_INLINE = 0;
}
}
else
{
$peek_is_INLINE = 1;
}
#OK looks like an INLINE call
if ( $peek_is_INLINE )
{
$call_line =~ s/\s+#/ /;
push @$rules, $call_line;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, $macro_rules, $rule_set);
next;
}
}
}
=pod
MACRO MACRO{ }MACRO
=over 2
$: MACRO{ $1 # comment == $: $>Sub_something $1 comment
MACRO{ opens a block, }MACRO terminates the block.
Enables a sub macro that is used only once to be contained within the calling macro stament block, it is however coded in the normal way in the hack file. MACROs may be nested as deeply as required, enabling easy to code and read complex IF|ELSE statment blocks. Example below.
rule <<RULE;
SSome_macro
R $*.FOUND $@ MACRO{ $1 # something.FOUND
R $*.mail3 $@ MACRO{ $1 # something.mail3.FOUND
R $&{CheckRcpt} $@ MACRO{ $&{CheckRcpt} # Valid TT?
dnl TT must conform to minimal rules
R $* $: $>Standard_TT_mail $1
}MACRO
R $* $@ $>SBad_mail $1
}MACRO
}MACRO
RULE
Please do not use the macro named B<SScreen_macro> yourself as it is used by this method appended with numerics
=back
=cut
if ( $line =~ s/MACRO\{\s*/£>Screen_macro_\n/ )
{
$macro = $setup{'macro'}++;
my ($start, $arg_comment) = split "\n", $line;
# get rid of leading space from nested macro?
$start =~ s/^\s+//;
# comment follows HASH, helps keep code readable
if ( scalar $arg_comment )
{
#has been noted that some macros are not supplied with anything
$arg_comment =~ s/\s+#/\t/;
my ($arg,$comment) = split "\t", $arg_comment;
if ( scalar $comment )
{
push @$rules, "$start$macro $arg $comment\n";
}
else
{
push @$rules, "$start$macro $arg\n";
}
}
else
{
push @$rules, "$start$macro\n";
}
# sub macro rule, note $start has other bits
$rule = "SScreen_macro_$macro";
# record this new S rule
$rule_hash->{$rule} = { S => [], O => [], T => {}, H => [] };
# also note Global scheme being used if any
$global and $rule_hash->{$rule}->{'G'} = $global;
push @$macros, $rule;
# nested call to process sub macro
¯o($rule, $macros, $macro_rules);
}
elsif ( $line =~ /\}MACRO/ )
{
last;
}
=pod
DEFINE_MASHFOUND
=over 2
Must be used after the Perl statement B<define_MashFound> and before any M4 macro statements that refer to the packed macro {MashFound}.
This should be placed in the first B<rule> that is used, and before any other capatalised macros, such as B<FIND> B<IS> etc. Failure to do so will cause unpredictable errors elsewhere when running the M4 hack file.
=back
=cut
elsif ( $line =~ s/^DEFINE_MASHFOUND\s*// )
{
my $key = DEFINE_MASHFOUND;
my $found_macro = <<FOUND;
INLINE ALLWAYS MASH TempA
NOTEST AUTO
$key
}MACRO
FOUND
my @found_macro = split "\n", $found_macro;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, \@found_macro, $rule_set);
}
=pod
FOUND
=over 2
expects a single argument, which is the B<{macro}> to be loaded with the $+.FOUND if that is the case,
this is a an inbuilt B<INLINE ALLWAYS> MACRO which generates code to be included in m4 source.
Usage:
FOUND BadRelay
BadRelay will be loaded with $+.FOUND only if B<R $+.FOUND>, current work space is saved and restored.
comments may be used, this will be included as a "dnl" line within the macro
It should be noted that only {MashFound} is used, the {macro} is now a key to an internal array kept by {MashFound}, this compexity is required due to the limited number of "long names" available to the developer, testing does not show up these limitations, it requires sendmail to be run for real and observed while talking to other servers.
=back
=cut
elsif ( $line =~ s/^FOUND\s+// )
{
$line =~ s/\s+/\t/;
my ($load_macro,$comments) = split "\t", $line;
my $comment = (scalar $comments)?($comments):("if FOUND save into $load_macro");
my $LOAD_MACRO = "SelfMacro$load_macro";
my $key = MashFound $load_macro;
my $found_macro = <<FOUND;
INLINE ALLWAYS MASH TempA
NOTEST AUTO
dnl $comment dnl
$key
}MACRO
FOUND
my @found_macro = split "\n", $found_macro;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, \@found_macro, $rule_set);
}
=pod
FIND
=over 2
expects a single argument, which is the B<{MashFound}->{macro}> to be accessed and have its contents placed in the workspace, this is now the only way to access items saved by B<FOUND>.
this is a an inbuilt B<INLINE ALLWAYS> MACRO which generates code to be included in m4 source.
Usage:
FIND BadRelay
=back
=cut
elsif ( $line =~ s/^FIND\s+// )
{
$line =~ s/\s+/\t/;
my ($load_macro,$comments) = split "\t", $line;
my $key = MashFind $load_macro;
my $found_macro = <<FOUND;
INLINE ALLWAYS NOMASH
NOTEST AUTO
$key
}MACRO
FOUND
my @found_macro = split "\n", $found_macro;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, \@found_macro, $rule_set);
}
=pod
STORE
=over 2
expects a single argument, works like FOUND excecpt allways loads value with current work space.
this is a an inbuilt B<INLINE ALLWAYS> MACRO which generates code to be included in m4 source.
Usage:
STORE BadRelay
=back
=cut
elsif ( $line =~ s/^STORE\s+// )
{
$line =~ s/\s+/\t/;
my ($load_macro,$comments) = split "\t", $line;
my $key = MashStore $load_macro;
my $found_macro = <<FOUND;
INLINE ALLWAYS MASH TempA
NOTEST AUTO
$key
}MACRO
FOUND
my @found_macro = split "\n", $found_macro;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, \@found_macro, $rule_set);
}
=pod
IS
=over 2
Expects upto 3 arguments. Number expected depends on the first argument.
=over 4
=cut
elsif ( $line =~ s/^IS\s+// )
{
=pod
B<FOUND> expects 2 sub arguments.
=over 2
=over 4
=item 1
is the B<{macro}> to check for B<.FOUND>, just the name, do not enclose in brackets.
IS FOUND Bounce
=item 2
is the B<action> to do if B<.FOUND>, since the nature of this INLINE ALLWAYS MASH macro never varys the normal form would be
$@ $>SomethingOrOther $1
alternativly if you do not care about the returned value
$: $>SomethingOrOther $1
or even
$#err something
IS FOUND Bounce $# "Bounce not wanted here"
=back
=back
B<THISFOUND> expects 1 argument, the B<action> as B<FOUND>
=over 2
checks current B<work space> for B<.FOUND>
IS THISFOUND $@ $1.FOUND
=back
B<REFUSED> and B<ALREADYREFUSED> expects 1 argument, the B<action> as B<FOUND>
=over 2
Normally the action should be B<#err somthing>
B<REFUSED> and B<ALREADYREFUSED> the checked B<{macro}> is either {Refused} or {AlreadyRefused}, these macro's are used by B<Mail8>, however we feel that these are usefull to other scripts.
=back
AND (REFUSED|ALREADYREFUSED) $#err somthing
=over 4
B<AND> is a special sub macro statement that allows the actions that REFUSED|ALREADYREFUSED does to be enacted also without the cost of another B<rule set>. See below, we are not refering to the "IS REFUSED"!
IS FOUND Bounce AND REFUSED $#err somthing
=back
=back
=back
=cut
if ( $line =~ s/^(THISFOUND|FOUND|REFUSED|ALREADYREFUSED)\s+// )
{
my $load_macro = $line;
my $this_found;
if ( $1 eq "THISFOUND" )
{
$this_found = 1;
$load_macro = "This";
}
elsif ( $1 eq "FOUND" )
{
$load_macro =~ s/\s+.*$//;
$line =~ s/^\w+\s+//;
}
elsif ( $1 eq "REFUSED" )
{
$load_macro = "Refused";
}
else
{
$load_macro = "AlreadyRefused";
}
my $LOAD_MACRO = "SelfMacro$load_macro";
my $found_macro = <<FOUND;
INLINE ALLWAYS MASH TempA
NOTEST AUTO
FOUND
if ( scalar $this_found )
{
$found_macro .= <<FOUND;
R £+.FOUND £: $LOAD_MACRO.£1.FOUND
FOUND
}
else
{
$found_macro .= MashFind $load_macro;
$found_macro .= <<KEY;
R £+.FOUND £: $LOAD_MACRO.£1.FOUND
KEY
}
if ( $line =~ s/^AND\s+//)
{
if ( $line =~ s/^(REFUSED|ALREADYREFUSED)\s*// )
{
if ( $1 eq "REFUSED" )
{
$load_macro = "Refused";
}
else
{
$load_macro = "AlreadyRefused";
}
$found_macro .= MashFound $load_macro;
if ( scalar $line )
{
$found_macro .= <<FOUND;
R £* £: £&{MashSelf}
R £+.FOUND £: $LOAD_MACRO.£1.FOUND
R $LOAD_MACRO.£+ $line
}MACRO
FOUND
}
else
{
$found_macro .= <<FOUND;
}MACRO
FOUND
}
}
else
{
whoops "IS $load_macro, uexpected AND $line";
}
}
else
{
$found_macro .= <<FOUND;
R $LOAD_MACRO.£+ $line
}MACRO
FOUND
}
my @found_macro = split "\n", $found_macro;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, \@found_macro, $rule_set);
}
}
=pod
B<REFUSED> and B<ALREADYREFUSED>
=over 2
These INLINE ALLWAYS MASH macros, load the {client_addr}.FOUND into the {macro} which is either {Refused} or {AlreadyRefused}, a single sub argument is expected, which is the action to do, however if the sub argument is ommited, this will simply store and do nothing else.
Normally
REFUSED $#err something
=back
=cut
elsif ( $line =~ s/^(REFUSED|ALREADYREFUSED)\s*// )
{
my $load_macro = $line;
if ( $1 eq "REFUSED" )
{
$load_macro = "Refused";
}
else
{
$load_macro = "AlreadyRefused";
}
my $LOAD_MACRO = "SelfMacro$load_macro";
my $found_macro = <<FOUND;
INLINE ALLWAYS MASH TempA
NOTEST AUTO
R £* £: £&{client_addr}.FOUND
FOUND
$found_macro .= MashStore $load_macro;
if ( scalar $line )
{
$found_macro .= <<FOUND;
R £* $line
}MACRO
FOUND
}
else
{
$found_macro .= <<FOUND;
}MACRO
FOUND
}
my @found_macro = split "\n", $found_macro;
$macro = $setup{'macro'}++;
push @$macros, "NOSUCH $rule $macro";
# nested call to process sub macro, and tell it to use the same rule_set as this
¯o($rule, $macros, \@found_macro, $rule_set);
}
=pod
{MashSelf}
=over 2
{MashSelf} provides access to the autosaved argument for this rule.
Usage
R $* £: &${MashSelf}
=back
=cut
elsif ( $line =~ s/\{MashSelf\}/\{MashSelf\}\n/g )
{
my @MashStack = split "\n", $line;
$NOMASH and whoops "attempt at using {MashSelf}", @MashStack;
$line = "";
while ( my $next = shift @MashStack )
{
if ( $next =~ s/\{MashSelf\}$// )
{
$line .= "$next"."{Mash$mash}";
}
else
{
$line .= $next;
}
}
push @$rules, $line;
}
=pod
{MashStack}
=over 2
{MashStack} provides a lasy way to keep data, without polluting other data.
Allways append something to the "MashStack", such as "A" as shown in the example.
Usage
R $* $: &${MashStackA}
R $* $: &${MashStackB}
=back
=cut
elsif ( $line =~ s/\{MashStack/\{MashStack\n/g )
{
my @MashStack = split "\n", $line;
$NOMASH and whoops "attempt at using {MashStack}", @MashStack;
$line = "";
while ( my $next = shift @MashStack )
{
if ( $next =~ /\{MashStack$/ )
{
$line .= "$next$mash"."D";
}
else
{
$line .= $next;
}
}
push @$rules, $line;
}
=pod
{MashTemp}
=over 2
{MashTemp} provides a lasy way to keep very temporary data, these values are only dependable within the current Macro, and may be clobbered by contained Macro's. This method exits to reduce further the number of B<sendmail {macro names}>.
Allways append something to the "MashTemp", such as "A" as shown in the example. Remember to use a consistant sub naming policy to minimise the generated names, we recomend using the sequence (A,B,C,D ..) but use as few as possible.
Usage
R $* $: &${MashTempA}
R $* $: &${MashTempB}
=back
=cut
elsif ( $line =~ /\{MashTemp\}/ )
{
push @$rules, $line;
}
=pod
DEBUG switchs on|off debug info during read-in
=over 2
Errors in the macro TEST coding can be difficult to track, so this will display helpfull debuging info, remove when the problem has been sorted.
Usage
DEBUG 1 To switch on
DEBUG 0 To switch off
DEBUG To switch off, however its best to be explicit.
=back
=cut
elsif ( $line =~ s/^DEBUG\s*//)
{
$DEBUG = $line;
}
=pod
TEST
=over 2
TEST macro code, is for testing of the macro, this code does not enter the output file.
TEST lines are converted into a simple HASH as follows
=over 4
{
=over 2
=over 16
=item D => []
list of B<.D> define a Macro statements
=item T => SCALAR
translation macro, to be used before values below are supplied to the B<macro> under test
=item V => []
values to try with B<macro>
=item E => []
values as "V" but must result in "ERR"
=item O => []
values as "V" but must result in "OK"
=item F => []
values as "V" but must result in "FOUND"
=item I# => []
values as "V" but must result in "#"
where "#" is the expected reply.
eg
IREPLY
=item SANE => []
list of $setup{sane} keys that define lists of B<.D> define a Macro statements
=item AUTO
does not have a HASH, but instead creates (V,E,O,F) as required.
=back
=back
}
=back
Encoded with leading definition letter and opening bracket, values "," delimited.
D() D( {client_addr}198.168.2.1, {client_name}dog.bone.com )
T() T(Translate)
V() V(frodo\@hobit.com, frog\@pond.com)
Not all definitions are required, you may use all or just one, in the case where no enclosing "()" brackets are used, this assumes you mean "V()".
B<E> and B<O> will stop|interrupt testing if returned result is unexpected.
B<V> will stop|interrupt testing if result is either "ERR" or "OK"!
Examples below
TEST SANE(std) D({client_addr}198.168.2.1, sdog.bone.com) V(frodo\@hobit.com)
Assumed "V()" values for macro
TEST frodo\@hobit.com, frog\@pond.com
Testing "Local_check_relay" requires "host.name"$|"ip_address", which requires our build "Translate" macro or your own for other uses.
TEST T(Translate) E(bogus.host.domain 12.5.7.89, n.n.bogus 1.2.3.4)
TEST methods are used in order of specification, and effects persist during testing, so things defined for a preceding "Macro" will effect all "Macros" that follow
=over 2
=cut
elsif ( $line =~ s/^TEST\s+// )
{
push @$test_list, $tests;
my $th = $test_hash->{$tests} = {};
#line may have ", " where we only want ","
$line =~ s/,\s*/,/g;
#braketed definintions?
if ( $line =~ s/\s*\)\s*/\n/g )
{
foreach ( split "\n", $line )
{
my $part = $_;
if ( $part =~ s/^T\(\s*// )
{
$th->{'T'} = $part;
}
elsif ( $part =~ s/^(SANE|D|V|E|O|F|I\w+)\(\s*// )
{
my $D = $th->{$1} = [];
@$D = split ",", $part;
}
=pod
AUTO
=over 2
AUTO enables local site checking, without the need to hack the module, or expect module methods to modify the TESTS from their command line, set this up with the method B<testing_domains>, do not use other TEST methods with this apart from B<SANE>, B<T> and (B<D> where B<AUTO D> is not used).
General format for this is (except for D)
=over 2
AUTO(key KEY sub_key1 sub_key2, key KEY sub_key1 sub_key2, ...
=over 2
Where key is one of (E,F,O,V), KEY is one of (OUR,OK,BAD) and sub_key# is one of (HELO,DOMAIN,IP,RESOLVE,FROM,RCPT).
Foreach setup{testing_domains}->{KEY}->[] line, the relevent field is used for testing, and so has the effect of specifying
TEST E(...........) where each "." is the relevent field referenced by sub_key#.
=back
=back
B<D>
=over 2
AUTO(D; KEY; M sub_key1; M sub_key1; M sub_key1, ...
=over 2
Where KEY is one of (OUR,OK,BAD), M is a B<sendmail macro name>, enclosed in {} if that would normally be required, and may be anything that can be defined, sub_key1 as already defined.
Please note the use of ";" to delimit fields, do not forget to place a ";" after the B<D> and the KEY even if you are only defining a single macro.
This is not of any use without other TEST options, being specified. If used B<D> generates a TEST line based on the other TEST options for each setup{testing_domains}->{KEY}->[] line.
And so has the effect of specifying.
TEST D({macro}value,{macro}value) E(...........) V(.......)
TEST D({macro}value,{macro}value) E(...........) V(.......)
TEST D({macro}value,{macro}value) E(...........) V(.......)
TEST D({macro}value,{macro}value) E(...........) V(.......)
......
=cut
elsif ( $part =~ s/^AUTO\(\s*// )
{
my $tdk = $setup{'testing_domains_keys'};
$part =~ s/\s+/ /g;
my %D;
my @P = split ",", $part;
foreach my $P ( @P )
{
if ( $P =~ /^D\s*/i )
{
$P =~ s/;\s*/;/g;
my ($key, $KEY, @D) = split ";", $P;
$D{$KEY} = {};
scalar $key and $key =~ /^D$/ or whoops "TEST AUTO (D \"key\" error, $part";
scalar $KEY and $KEY =~ /^(OUR|OK|BAD)$/ or whoops "TEST AUTO (D \"KEY\" error, $part";
foreach my $D ( @D )
{
my ($M, $sub_key1) = split " ",$D;
scalar $M or whoops "TEST AUTO D \"M\" error, $part";
scalar $sub_key1 and exists $tdk->{$sub_key1} or whoops "TEST AUTO D \"sub_key1\" error, $part";
$D{$KEY}->{$M} = $sub_key1;
}
}
else
{
my ($key,$KEY,$sub_key1,$sub_key2) = split " ", $P;
scalar $key and $key =~ /^(E|F|O|V)$/ or whoops "TEST AUTO \"key\" error, $part";
scalar $KEY and $KEY =~ /^(OUR|OK|BAD)$/ or whoops "TEST AUTO \"KEY\" error, $part";
scalar $sub_key1 and exists $tdk->{$sub_key1} or whoops "TEST AUTO \"sub_key1\" error, $part";
if ( scalar $sub_key2 )
{
exists $tdk->{$sub_key2} or whoops "TEST AUTO \"sub_key2\" error, $part";
}
my $KEYED = $setup{'testing_domains'}->{$KEY};
unless ( scalar @$KEYED )
{
moan "TEST AUTO KEY $KEY is empty";
next;
}
my $D = $th->{$key} = [];
#this can generate a lot of test lines, try to ensure each test is unique
my %tested;
foreach ( @$KEYED )
{
my @keyed = split ",", $_;
my $keyed;
if ( scalar $sub_key1 and scalar $sub_key2 )
{
$keyed = "$keyed[$tdk->{$sub_key1}] $keyed[$tdk->{$sub_key2}]";
}
else
{
$keyed = $keyed[$tdk->{$sub_key1}];
}
if ( scalar $tested{$keyed} )
{
next;
}
else
{
$tested{$keyed} = 1;
}
push @$D, $keyed;
}
}
}
my @D_keys = keys %D;
#have we got AUTO(D specified? D HASH can have 3 keys, OUR,OK,BAD
if ( scalar @D_keys )
{
my $keyed;
my $d_th = $th;
my $tested_lines = 0;
AUTO_D_KEY: foreach my $KEY ( @D_keys )
{
my $KEYED = $setup{'testing_domains'}->{$KEY};
unless ( scalar @$KEYED )
{
moan "TEST AUTO D KEY $KEY is empty";
next AUTO_D_KEY;
}
#this can generate a lot of test lines, try to ensure each test is unique
my %tested;
AUTO_KEYED: foreach my $keyed_line ( @$KEYED )
{
#each will need testing with the D values
my @keyed = split ",", $keyed_line;
my %keyed;
map { $keyed{$_} = "$keyed[$tdk->{$D{$KEY}->{$_}}]" } (keys %{$D{$KEY}});
#but check it has not already been used
my @define = map { "$_$keyed{$_}" } (keys %keyed);
my $define = join ",", @define;
if ( scalar $tested{$define} )
{
next AUTO_KEYED;
}
else
{
$tested{$define} = 1;
}
#ok completly new test? well at least for this KEY
if ( scalar $tested_lines )
{
$tests++;
push @$test_list, $tests;
$th = $test_hash->{$tests} = {};
#copy original test hash to new duplicate
%$th = %$d_th;
}
$tested_lines++;
my $D = $th->{"D"} = [];
@$D = @define;
}
}
}
#TODO
=pod
=back
=back
=back
=cut
}
else
{
moan "unexpected TEST definition $part";
}
}
}
#values for macro without brackets
else
{
my $V = $th->{'V'} = [];
@$V = split ",", $line;
}
$tests++;
}
=pod
HINT
=over 2
HINT is used to supply hints during testing, examples as to expected format etc, use as many as required, or none at all, but it will make your life easier to use them if you do not include TEST code or want to enter data on the fly.
All HINT are stored in the B<H=E<gt>[] ARRAY> for the B<rule>
Example below
TEST D({client_addr}198.168.2.1, sdog.bone.com) V(frodo\@hobit.com)
HINT email address expected, valid or invalid
=back
=cut
elsif ( $line =~ s/^HINT\s+// )
{
push @$hint_list, $line;
}
=pod
FORCE
=over 2
FORCE if specified will allways pause testing and ask you for test data, regardless of wether B<TEST> has been used, has no meaning for "HTML", and omitting B<TEST>s has the same effect. Some sort of hint should follow, which will be shown before asking you for data.
FORCE is stored in the B<F=E<gt>SCALAR> for the B<rule>
Example below
TEST D({client_addr}198.168.2.1, sdog.bone.com) V(frodo\@hobit.com)
FORCE email address expected, valid or invalid
=back
=cut
elsif ( $line =~ s/^FORCE(\s+|$)// )
{
$rule_set->{"F"} = (scalar $line)?($line):("?");
}
=pod
NOTEST
=over 2
NOTEST if specified is the reverse of FORCE, meaning if no B<TEST>s have been defined, this will allways skip testing, and continue. Some sort of hint should follow, explaining why testing is not required.
If B<NOTEST AUTO> is specified then it is assumed that the code is program generated and is tested by a controlling macro, so this will stay quite about it, otherwise this will B<moan> about the lack of testing.
Note if both FORCE and NOTEST are defined, NOTEST takes precedence.
NOTEST is stored in the B<N=E<gt>SCALAR> for the B<rule>
Example below
NOTEST containing rule tests this.
=back
=cut
elsif ( $line =~ s/^NOTEST\s+// )
{
$rule_set->{"N"} = (scalar $line)?($line):("!NOT TESTED!?");
}
# normal line
else
{
push @$rules, $line;
}
}
# restore saved value from begining, BUT do not clobber if inline!
push @$rules, "R £* £: £&{Mash$mash}" if $MASH;
}
=back
=back
=back
=back
=head2 inbuilt_rule @_
=over 4
Enables this to test B<sendmails> own internal rules, instruction format is the same as for the above B<rule>, indeed this uses the same B<%setup HASHs>.
NOTE: This only supports the B<test> methods, even though it uses the same B<macro> parser to its work, nothing is output, and the "B<S>", "B<M>" and"B<N>" componants are removed for safty reasons, and a "B<I>" with the value B<1> is added.
=back
=cut
push @EXPORT, "inbuilt_rule";
sub inbuilt_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"};
$setup{"rule"}->{$rule} = $rule_set;
#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);
map { delete $rule_set->{$_} } (qw(S M N));
$rule_set->{'I'} = 1;
}
=head2 VERSIONID $title
=over 4
Only argument expected is the title|name for this hack to insert in the B<VERSIONID> statement. Output format is.
# version
my ($title) = @_;
my $time = localtime();
echo "VERSIONID(`@(#)$title for Sendmail 8.12 or better $time')";
=back
=cut
push @EXPORT, "VERSIONID";
sub VERSIONID
{
# version
my ($title) = @_;
my $time = localtime();
echo "VERSIONID(`@(#)$title for Sendmail 8.12 or better $time')";
}
=head2 LOCAL_CONFIG
=over 4
Required statement, this inserts required statments into the hack file.
echo <<ECHO;
LOCAL_CONFIG
KSelfMacro macro
ECHO
Currently only the B<SelfMacro macro>, which is used by many of the above methods, feel free to use it yourself but do not use names starting with B<Mash> other than those stated in B<rule> above.
Add your own definitions after this.
=back
=cut
push @EXPORT, "LOCAL_CONFIG";
sub LOCAL_CONFIG
{
echo <<ECHO;
LOCAL_CONFIG
KSelfMacro macro
ECHO
}
=head2 LOCAL_RULESETS
=over 4
Required statement, this inserts required statments into the hack file.
Currently only a B<Translate> macro, which is based on the example in the B<Sendmail 3rd edition> book, section 7.1.1, page 290, however we will assume only 2 tokens are going to be supplied (the program inserts the seperator), this is for the standard macro B<Local_check_relay>
Due to the limited number of "long names", some have had to be recoded as an $| delimited array {MashFound}, which of course makes testing difficult, so as we already have a problem with "rule sets", "Translate" will now also pack {MashFound}, which is re-writen each time this is used.
echo <<ECHO;
LOCAL_RULESETS
STranslate
R $* $$| $* $: $1 $| $2 fake for -bt mode
ECHO
Add your own definitions after this.
=back
=cut
push @EXPORT, "LOCAL_RULESETS";
sub LOCAL_RULESETS
{
my $echo = <<ECHO;
LOCAL_RULESETS
STranslate
R ££| £+ £: £| £1
R £* ££| £* £1 £| £2 fake for -bt mode
R £| £+ £: £| £| £1
ECHO
my $L_KEY = scalar @{$setup{'FOUND'}->{'LIST'}};
$L_KEY = 1 unless scalar $L_KEY;
$L_KEY--;
foreach my $KEY (0..$L_KEY)
{
$echo .= <<ECHO;
R £| £| $KEY £+ £: £(SelfMacro {MashFound$KEY} £@ £1 £) £1
ECHO
}
$echo .= <<ECHO;
ECHO
echo $echo;
}
=head2 build
=over 4
No arguments, this may included in the script after the B<rule>s and just before B<install>, this has no effect unless B<setup{silent}> is in effect, meaning that preceeding B<rule>s have not produced output, or you have built the required B<setup> HASH yourself.
=back
=cut
push @EXPORT, "build";
sub build
{
#is this just a comment?
$setup{'silent'} or return;
#check we have something to do
my @rules_list = @{$setup{'rules'}};
scalar @rules_list or return moan "nothing to test? setup{rules} empty?";
my $rule_hash = $setup{'rule'};
foreach ( @rules_list )
{
tee translate @{$setup{"rule"}->{$_}->{"S"}};
}
}
=head2 install
=over 4
No arguments, this may be included in the script after the B<rule>s or B<build> and just before B<test>, if you are not root this will attempt to B<su -c '"program" install 1'>
Note you may call your program with "install 1" so long as B<setup> processes the program arguments, or at least gets 1st pick. You will have to ensure that B<setup> gets all its requires.
=back
=cut
push @EXPORT, "install";
sub install
{
#normal users will not have "install" rights
map { $setup{$_} or return moan "setup{$_} not defined" } (qw(file hack_dir tee cf mc));
#if not root, try to su to do the install
unless ($setup{"SU"})
{
ok "Next is 'su' login password, this enable us to intall the generated code.\nContinue" or exit;
my $self = ($0 =~ /\//) ? ($0):("./$0");
$setup{"install"} = 1;
#need to install, build takes precedence stopping the install from happening!
$setup{'magic'} = 1;
#essential args for installation
my $args = join " ", map { "$_ \'$setup{$_}\'" } ( qw(
magic
hack_dir
file
sendmail
mc
cf
install
));
system "su -c \'$self $args\'" and exit moan "can not su -c \'$self $args\'";
#clear these to prevent mishaps?
$setup{"install"} = 0;
$setup{'magic'} = 0;
return 1;
}
map { $setup{$_} or return moan "setup{$_} not defined" } (qw(file hack_dir tee cf mc install));
my $tee = $setup{'tee'};
my $file= $setup{'file'};
my $time= $setup{'time'};
my $cf = $setup{'cf'};
my $mc = $setup{'mc'};
#archive existing installation files
foreach (qw(file cf))
{
if ( -f $setup{$_} )
{
rename $setup{$_}, "$setup{$_}.$time~" or
whoops "$!. install \"$_\" \"$setup{$_}\" rename failed";
}
}
#copy hack to its destination
copy($tee, $file) or whoops "$!. install, copy failed";
#compile CF file for testing
system "m4 $mc > $cf" and whoops "\"m4 $mc > $cf\" resulted in $?";
return 1;
}
=head1 Testing methods ============================
Sendmail intialization and chit chat methods, usable directly. But normally used by B<test> specified further down this document.
=cut
=head2 REF HASH setup{senddmail_hash} = sendmail_hash
=over 4
Setup script for B<sendmail> below, call it yourself to get the "setup" that will be used by B<sendmail>, mostly of use to initialize the B<output> methods with something more suitable for your needs, this currently defaults to methods suitable for command line usage.
If used place before B<test> to enable your alternative setup, otherwise omit and use the default settings.
If you use this directly be sure to also use B<sendmail> with no arguments to intialise the connection, sendmail -bt gives a greating message on starting.
NOTE calling it replaces the existing HASH with the default.
B<sendmail> calls this itself if the required HASH does not exist!
sendmail_hash => {
IO => { IO::File objects used by IPC::Open3 open3
r => IO::File object
w => IO::FIle object
e => IO::File object
pid => IPC::Open3 open3 object 'sendmail'
}
select { IO:Select objects which refer to above IO::File objects
r => IO::Select object
w => IO::Select object timeout has 30 seconds added to it
e => IO::Select object
t => SCALAR = 3 timeout seconds for select statment
l => SCALAR last action that caused this to return
one of
r=(read),w=(write),e=(error),t=(timeout)
}
buffer { [] REFs containing data for|from above IO::File objects
r => [] REF contains read in data (push)
w => [] REF contains data waiting to be written (shift)
e => [] REF contains errors (push)
l => [] REF contains last read in data or error
}
error => [] REF general errors, undef if OK
output { what is this supposed to do with 'display' infomation?
silent => SCALAR = 0 1 suppresses all output
echo => SUB REF default is &echo (command line only)
moan => SUB REF default is &moan
(which already understands HTML)
whoops => SEB REF default is &whoops
(based on moan, but also exits)
}
=back
=cut
push @EXPORT, "sendmail_hash";
sub sendmail_hash
{
# main hash, if called clear down existing, and start again
my $s = $setup{'sendmail_hash'} = {
IO => {},
"select"=> {
t => 3,
l => 0,
},
buffer => {
l => [],
},
output => {
silent => 0,
echo => \&echo,
moan => \&moan,
whoops => \&whoops,
},
};
#IO::Select has to be done after a file has been opened for it
foreach (qw(r w e))
{
$s->{"IO"}->{$_} = new IO::File;
$s->{"buffer"}->{$_} = [];
}
#init pipe to sendmail
my $sendmail = "$setup{'sendmail'} -bt";
$setup{'cf'} and $sendmail .= " -C$setup{'cf'}";
#simple refs reguired for open3
my $rh = $s->{'IO'}->{'r'};
my $wh = $s->{'IO'}->{'w'};
my $eh = $s->{'IO'}->{'e'};
$s->{'pid'} = open3($wh, $rh, $eh, $sendmail);
unless ( $s->{'pid'} )
{
#this is the first call to 'sendmail' so do not know for sure what to do
$s->{'error'} = "open3 \"$sendmail\" call failed with: $!";
whoops $s->{'error'};
return undef;
}
#creat select object now we have open file handles
foreach (qw(r w e))
{
$s->{"select"}->{$_} = new IO::Select($s->{"IO"}->{$_});
unless ( $s->{'select'}->{$_}->count() )
{
$s->{'error'} = "unable to create IO::Select object for $_";
whoops $s->{'error'};
return undef;
}
}
return $s;
}
=head2 undef sendmail_whoops @_
=over 4
B<sendmail> methods use this to complain and exit, will be silent if B<sendmail_hash->output->silent>, alternativly uses the relevant B<whoops> method to complain and exit. NOTE will allways B<exit>.
=back
=cut
push @EXPORT, "sendmail_whoops";
sub sendmail_whoops
{
my $s = $setup{'sendmail_hash'};
my $whoops = \&whoops;
if ( scalar $s )
{
if ( scalar $s->{'object'} )
{
if ( scalar $s->{'object'}->{'whoops'} )
{
$whoops = $s->{'object'}->{'whoops'};
}
$s->{'object'}->{'silent'} and exit;
}
}
$whoops->(@_);
exit;
}
=head2 undef sendmail_moan @_
=over 4
B<sendmail> methods use this to complain and to fill out its own sendmail_hash{error}, will be silent if B<sendmail_hash->output->silent>, alternativly uses the relevant B<moan> method to complain.
=back
=cut
push @EXPORT, "sendmail_moan";
sub sendmail_moan
{
my $s = $setup{'sendmail_hash'};
my $moan = \&moan;
if ( scalar $s )
{
my $e = $s->{'error'} = [];
@$e = @_;
if ( scalar $s->{'object'} )
{
if ( scalar $s->{'object'}->{'moan'} )
{
$moan = $s->{'object'}->{'moan'};
}
$s->{'object'}->{'silent'} and return undef;
}
}
return $moan->(@_);
}
=head2 undef sendmail_echo @_
=over 4
B<sendmail> methods use this to display the output of "sendmail -bt" interprocess pipe, will be silent if B<sendmail_hash->output->silent>, alternativly uses the relevant B<echo> method to display.
=back
=cut
push @EXPORT, "sendmail_echo";
sub sendmail_echo
{
my $s = $setup{'sendmail_hash'};
my $echo = \&echo;
if ( scalar $s )
{
if ( scalar $s->{'object'} )
{
if ( scalar $s->{'object'}->{'echo'} )
{
$echo = $s->{'object'}->{'echo'};
}
$s->{'object'}->{'silent'} and return 1;
}
}
return $echo->(@_);
}
=head2 ($code,@buffer) = sendmail(@_)
=over 4
Interface for talking to "sendmail -bt", on first call will set it self up using B<sendmail_hash> if the required HASH does not already exist.
Any arguments are "sendmail instructions" this will allways append newlines.
Returns recieved @buffer, does not return on writes as sendmail will allways reply, however returns B<undef> on timeouts or on read and write fails!
B<sendmail> has its own "sendmail_hash" HASH in setup, which will be setup on first use if not already defined, and enougth other information exists to enable this.
USES
=over 4
B<sendmail_whoops> to complain about errors and exit!
B<sendmail_moan> to complain about errors!
B<sendmail_echo> to display received data
=back
=back
=cut
push @EXPORT, "sendmail";
sub sendmail
{
my $s = $setup{'sendmail_hash'};
unless ( scalar $s )
{
unless ( $s = sendmail_hash())
{
my $e = $setup{'sendmail_hash'}->{'error'};
my $error = ($e)?($e):("sendmail setup failed?");
sendmail_whoops $error;
}
my @ok = &sendmail();
if ( my $ok = scalar @ok )
{
if ( $ok > 4 )
{
ok "Sendmail reported errors, STOP RUN [Y|n]" or exit;
}
return @ok;
}
else
{
return sendmail_whoops "initial sendmail communication failed?";
}
}
#sendmail allways replys, this may have been given a list of work to do
#But we must wait for sendmail to reply before continuing, or we can end up
#in a mess!
# write buffer, should be empty
my $w_buff = $s->{'buffer'}->{'w'};
scalar @_ and push @$w_buff, @_;
if ( scalar @$w_buff )
{
my @ok;
while ( my $write = shift @$w_buff )
{
@ok = &sendmail_comms($write);
scalar @ok or return;
}
#return last recieved block;
return @ok;
}
else
{
return &sendmail_comms();
}
}
sub sendmail_comms
{
#sendmail connection must be open
my $s = $setup{'sendmail_hash'};
scalar $s->{"pid"} or sendmail_whoops "IO connection with sendmail is closed!";
my $timeout = $s->{"select"}->{'t'};
#buffers
my $bufs = $s->{'buffer'};
my $sels = $s->{'select'};
#have we stuff to write?
my $delay = 0;
my $last_write = scalar @_;
$last_write > 1 and sendmail_whoops <<WHOOPS;
This must be supplied with one \"write\" at a time!
supplied with $last_write arguments
WHOOPS
#sendmail may have quite a lot to do, so need a longer timeout
$last_write and $timeout += 30;
while ( 1 )
{
my @selects = (
$sels->{'r'},
#do we still have stuff to write?
((scalar @_)?($sels->{'w'}):(undef)),
$sels->{'e'},
);
my $start_time = time;
my ( $read,$write,$error ) = IO::Select->select(@selects,$timeout);
my $end_time = time;
my $waited = $end_time - $start_time;
$delay += $waited;
#increment buffer and return just read in
if ( (scalar $read and scalar @$read) or (scalar $error and scalar @$error) )
{
my ($HDL,$hdl);
if ( scalar $read )
{
$HDL = shift @$read;
$hdl = "r";
}
else
{
$HDL = shift @$error;
$hdl = "e";
}
$sels->{'l'} = $hdl;
my $buffer = $bufs->{"l"} = [];
my $string;
my $recv = sysread $HDL,$string,1024;
if ( $recv )
{
#normal read operation
if ( $hdl =~ /r/i )
{
if ( scalar $bufs->{"long_string"} )
{
$bufs->{'long_string'} .= $string;
}
else
{
$bufs->{'long_string'} = $string;
}
if ( $string =~ /(^|\n)>\s+$/ )
{
@$buffer = grep {scalar $_} (split "\n", $bufs->{'long_string'});
#clear down read buffer
$bufs->{'long_string'} = undef;
push @{$bufs->{$hdl}}, @$buffer;
sendmail_echo @$buffer;
#dont return if still have something to write
scalar @_ or return @$buffer;
$delay = 0;
}
}
#errors need to be reported, normally we can not continue
else
{
@$buffer = split "\n", $string;
push @{$bufs->{$hdl}}, @$buffer;
return sendmail_moan @$buffer;
}
}
else
{
$s->{"pid"} = 0;
return sendmail_moan "(Read|Write|Error) handle $hdl connection with \"sendmail -bt\" has been closed";
}
}
#writes expect some reply in all cases to sendmail
elsif ( scalar $write and scalar @$write )
{
my $line = shift @_;
my $HDL = shift @$write;
sendmail_echo $line;
my $ok = print $HDL "$line\n";
unless ($ok)
{
$s->{"pid"} = 0;
$sels->{'l'} = "w";
return sendmail_whoops "$! failed to talk to sendmail pipe";
}
$delay = 0;
}
#timeout, however as sendmail may be busy with a slow operating external program
else
{
$sels->{'l'} = "t";
if ( $last_write )
{
sendmail_moan "Timeout waiting \"$delay\" for sendmail to reply";
next if ok "Try again? [n|y] :";
next if $delay < 360 and $setup{"silent"};
}
else
{
sendmail_moan "Timeout waiting \"$delay\" for sendmail";
}
return undef;
}
}
}
=head2 test @_
=over 4
Expects either
=over 4
nothing, in which case all defined rules are tested in turn, if any "rule" does not have "TEST"s defined for it, this will halt on and ask you for a test value, or simply press return to continue, HTML format is still in development.
rule=>test, rule=>test, rule=>test hash value pairs, which are the rule to test and the TEST number to do, or alternativly the word "ALL" to do all "TESTS" for this rule.
=back
This will only "TEST" rules that have been defined, so it is best to place this last in your code.
This uses B<sendmail> to talk to "sendmail -bt" via open3.
sets B<setup{testing}> to inform other methods that are common to both B<build> and B<test> to use B<setup{log}> instead of B<setup{tee}>.
=back
=cut
push @EXPORT, "test";
sub test
{
#flag for methods to understand testing is in progress
$setup{'testing'} = 1;
#if first time this has been used, init sendmail so that we can use its methods
$setup{'sendmail_hash'} or
sendmail() or
sendmail_whoops "test failed to init sendmail!";
#check we have something to do
my @rules_list = @{$setup{'rules'}};
scalar @rules_list or return sendmail_moan "nothing to test? setup{rules} empty?";
my $rule_hash = $setup{'rule'};
# have arguments been supplied?
my $cmd_line = scalar @_;
my @test_list = ($cmd_line)?(@_):(map{ ($_,"ALL") } (@rules_list));
RULE:while ( my $rule = shift @test_list )
{
#although written to file with a leading S, testing requires it to be removed
my $use_rule = $rule;
$use_rule =~ s/^S//;
my $test_ind = shift @test_list or last;
my $rule_def = $rule_hash->{$rule} or
return sendmail_moan "rule{$rule} does not exist!";
#any hints? better show them now
my $hints = $rule_def->{'H'};
scalar $hints and scalar @$hints and sendmail_echo @$hints;
#not all rule's will have tests
my @tests = @{$rule_def->{'O'}};
my $force = $rule_def->{'F'};
my $notest= $rule_def->{'N'};
my ($code,$ok);
#command line is likly to be explicit value to try
if ( $cmd_line )
{
#so long as the word is not all
if ( $test_ind =~/\D+/ and $test_ind !~ /^ALL$/i )
{
sendmail "$use_rule $test_ind" or next RULE;
}
#numeric must exist
elsif ( $test_ind =~ /^\d+$/ )
{
if ( $rule_def->{'T'}->{$test_ind})
{
@tests = ($test_ind);
}
else
{
sendmail_moan "no such $test_ind for $rule";
next RULE;
}
}
}
#no tests for this rule?
elsif ( $notest and not scalar @tests )
{
$notest =~ /AUTO/ or sendmail_moan "$notest for $rule";
next RULE;
}
elsif ( $force or not scalar @tests )
{
my $msg = ($force)?($force):("Rule=:\"$rule\", Enter TEST value to try:> ");
while ( my $test = ok $msg )
{
sendmail "$use_rule $test";
}
}
my $SANE = $setup{'sane'};
foreach ( @tests )
{
my $tests = $rule_def->{'T'}->{$_} or next RULE;
#sane define statements required? remember these persit
my $sane_define;
if ( scalar $tests->{'SANE'} and scalar @{$tests->{'SANE'}})
{
#TODO
#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
$sane_define = [];
my @pre_sane;
my $mash_found = 0;
@$sane_define = MashPack ( map { @{$SANE->{$_}} } grep { $SANE->{$_} } (@{$tests->{'SANE'}}));
#@$sane_define = (map { ".D$_" } map { @{$SANE->{$_}} } grep { $SANE->{$_} } (@{$tests->{'SANE'}}));
#set sane here, as there may be no tests defined, and preserves original action
sendmail @$sane_define;
}
#define statements required? remember these persit
if ( scalar $tests->{'D'} and scalar @{$tests->{'D'}})
{
sendmail MashPack (@{$tests->{'D'}});
}
#translation macro?
my $T = $tests->{'T'};
foreach ( grep /^(V|E|O|F|I\w+)$/, (keys %$tests))
{
scalar $tests->{$_} and scalar @{$tests->{$_}} or next;
my $v = $_;
my @V = @{$tests->{$v}};
foreach ( @V )
{
my $t = $_;
#sane settings reguired for test run?
if ( scalar $sane_define )
{
sendmail @$sane_define;
}
#spaces should not be included in values, but if there is one assume $| magic
$t =~ s/\s/ \$| / if $T;
my $a = ($T)?("$T,$use_rule $t"):("$use_rule $t");
my @R = sendmail $a;
scalar @R or next;
my @Un = grep /^Undefined ruleset/, @R;
if ( scalar @Un )
{
sendmail_moan @Un;
ok "stop run? [y|n]" or exit;
next RULE;
}
my @err = grep /returns:\s+\$#\s*err/i,@R;
my @ok = grep /returns:\s+\$#\s*ok/i, @R;
my @found = grep /\.\s*FOUND/, @R;
my $stop = 0;
#with returns such as #ok and #err, it is very likly to get .FOUND replys also
#1st pop ">" prompt off, last reply may alter things
pop @R;
my $last_return = pop @R;
if ( scalar $last_return )
{
if ( $last_return =~ /returns:\s+\$#\s*err/i )
{
$last_return = "err";
}
elsif ( $last_return =~ /returns:\s+\$#\s*ok/i )
{
$last_return = "ok";
}
else
{
$last_return = "NA";
}
}
else
{
sendmail_moan "expected replys for ($rule,$v,$a)";
$stop = 1;
}
if ( scalar @err and $v =~/(v|o|f)/i )
{
sendmail_moan "unexpected \$# err, for ($rule,$v,$a)", @err;
$stop = 1;
}
elsif ( scalar @ok and $v =~ /(v|e|f)/i )
{
sendmail_moan "unexpected \$# OK, for ($rule,$v,$a)", @ok;
$stop = 1;
}
elsif ( scalar @found and $v =~ /(v|e|o)/i )
{
if ( $v =~ /e/i and $last_return =~ /err/i )
{
sendmail_moan "warning (last=<$last_return>), unexpected .FOUND, for ($rule,$v,$a)", @found;
}
elsif ( $v =~ /o/i and $last_return =~ /ok/i )
{
sendmail_moan "warning (last=<$last_return>), unexpected .FOUND, for ($rule,$v,$a)", @found;
}
else
{
sendmail_moan "unexpected .FOUND, for ($rule,$v,$a)", @found;
$stop = 1;
}
}
elsif ( not scalar @err and $v =~ /e/i )
{
sendmail_moan "expected \$# err, for ($rule,$v,$a)";
$stop = 1;
}
elsif ( not scalar @ok and $v =~ /o/i )
{
sendmail_moan "expected \$# OK, for ($rule,$v,$a)";
$stop = 1;
}
elsif ( not scalar @found and $v =~ /f/i )
{
sendmail_moan "expected .FOUND, for ($rule,$v,$a)";
$stop = 1;
}
elsif ( $v =~ /i\w+/i )
{
sendmail_echo <<SENDMAIL_ECHO;
------TEST results can not be AUTO checked-----
$rule, $v, $a
------VERIFY RESULT BEFORE CONTINUING----------
SENDMAIL_ECHO
ok "Results as expected? [Y|n]" and $stop = 1;
}
else
{
unless ( $v =~ /(v|o|e|f)/i )
{
sendmail_moan "? unmatched $v, program error?";
$stop = 1;
}
}
if ( $stop )
{
ok "stop run? [y|n]" or exit;
}
}
}
}
}
}
#OK end of main program documentation, next is usage
=head1 Example USAGE from a command line driven program
Note this also contains a cut down snippet of the ANTI SPAM hack that caused this to come into existance.
#! /usr/bin/perl -w
use Sendmail::M4::Utils;
setup @ARGV;
# copyright message
dnl <<DNL;
Copyright (c) 2007 celmorlauren Limited England
Author: Ian McNulty <development\@celmorlauren.com>
this should live in /usr/share/sendmail/hack/mail8-stop-fake-mx.m4
some settings that are advised
FEATURE(`access_db', `hash -T<TMPF> -o /etc/mail/access.db')
FEATURE(`greet_pause', `2000')
define(`confPRIVACY_FLAGS', `goaway')
DNL
# version
VERSIONID "ANTI SPAM";
#
dnl <<DNL;
SPAM checking additions --------------------------
'-' added to trap DSL faked domain names
DNL
echo <<ECHO;
define(`confOPERATORS',`.:@!^/[]-')
ECHO
LOCAL_CONFIG
echo <<ECHO;
KRlookup dns -RA -a.FOUND -d5s -r4
ECHO
# we can do some checking with HEADER lines
echo "HReceived: $>+ScreenReceived";
################################################################
################################################################
# end of snippet, this would of course contain your own code
################################################################
################################################################
# this is the start of the real code
LOCAL_RULESETS
echo <<ECHO;
dnl this bit is for mail8, intial contact and flood checking?
dnl bit below checked, see p288
ECHO
#######################################
# CONTACT
# This bit arrived at on first contact, and so permissions based on IP can be set
rule <<RULE;
SLocal_check_relay
TEST T(Translate) V(local 192.168.0.1, bogus.host 1.2.3.4)
R $* $| $* $: $(SelfMacro {RelayName} $@ $1 $) $1 $| $2
R $* $| $* $: $(SelfMacro {RelayIP} $@ $2 $) $1 $| $2
R $* $: $>Screen_bad_relay $&{RelayIP}
RULE
intstall;
test;
################################################################
################################################################
# end of snippet, this would of course contain your own code
################################################################
################################################################
=cut
=head1 HISTORY
B<Versions>
=over 5
=item 0.1
Nov 2006 1st version, pure sendmail M4 hack, using plug-in Perl programs.
=item 0.2
25 Aug 2007, B<this> 1st CPAN test module, developed to test M4 hack scripts, original script split into B<Utils> for creation and testing, and B<Mail8> the B<ANTI SPAM> engine.
B<Amendments to release version>
=over 3
=item 30 Aug 2007
TEST, HINT & FORCE did not nest.
=item 3 Sept 2007
cf file backup now has a tilde ending "~".
%setup{paranoid} added for mail8.
=item 5 Sept 2007
NOTEST, for nested MACROS that are already tested by a containing level, or where additional testing makes no sense.
=item 8 Sept 2007
Testing of a Mail8 component with bugs caused files with wrong permisions to be created, meaning the standard user could not re-create them, and some confusion as to what was happening. Utils will now B<whoops> on these problems giving a clear indication as to the real problem.
{MashStack} failed to work when more than one instance was used on a single line.
NOTEST AUTO will not B<moan> meaning auto generated lines that not be meaningfully tested do not complain about it.
FORCE and absence of TEST's now will continue to ask for input for a rule, until nothing is entered
=item 10 Sept 2007
Testing of Reintergrated Mail8 showed that NESTing still did not work, reason found and fixed, also somethings that where expected were not allways supplied.
GLOBAL added to reduce the number of {macro_names} as Mail8 managed to go over B<sendmail>s limit of B<96>, used at the top level S rule to reset counters.
=item 11 Sept 2007
INLINE added, Mail8 managed to go over the standard B<sendmail> limit of B<100> B<named rulesets>, counted a total of 123 in the test.cf, we know we could re-compile sendmail with a bigger limit. But that is something we can not expect of anyone else.
=item 13 Sept 2007
UTF8 EURO currency "character" added can now be used in rule definitions, where $ would have to be escaped.
=item 14 Sept 2007
FOUND inbuilt MACRO added to load B<SelfMacro {macro}> with "$+.FOUND", intention is to remove another B<rule set> as this B<MACRO> will be coded B<INLINE>.
=item 15 Sept 2007
method B<inbuilt_rule> added to enable testing of B<sendmail>s own rule sets, these use the same methods and control HASHs as B<rule> except generates no code.
=item 16 Sept 2007
B<MACRO{> statements (REFUSED, ALREADYREFUSED, IS (REFUSED, ALREADYREFUSED, FOUND), INLINE ALLWAYS) added to both help with reducing the number of generated B<rule sets> and to improve the layout of B<Mail8>.
=item 17 Sept 2007
B<MACRO{ TEST> sub statement SANE and the method "sane" added to simplify reseting B<sendmail -bt> test session to sensible values.
=item 19 Sept 2007
B<MACRO{ TEST> sub statement AUTO and method "testing_domains" added to enable customers vary the test data to reflect their setup, testing B<Sendmail::M4::Mail8> via B<Sendmail::M4::mail8> with just celmorlauren email setup is not sufficient.
=item 21 Sept 2007
Documentation clean up, noted that EURO character causes problems with Perldoc for version 5.6 Perl, POUND does not work either (but at least does mess up display)
=back
=item 0.21
21 Sept 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=item 22 Sept 2007
Documentation clean up, noted that POUND character does not display correctly on CPAN, hum it would be better if CPAN coped with UTF8 characters!
B<MACRO{> DEBUG statement added to switch on debuging within the TEST line read in phase, to track difficult to see errors.
{MashSelf} failed to work when more than one instance was used on a single line.
=back
=item 0.22
22 Sept 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=item 22 Sept 2007
installed on a test system, started to run ("too many long names" again) AAARGH!
{MashTemp} added a variant of {MashStack}, differnce being the reduced number of names generated, the names only being safe only in the current macro, and can be clobbered by contained macros, that use this. You have been warned!
=item 23 Sept 2007
B<Macro{> OPTION added, this is to enable such things
OPTION NO MASH
OPTION MASH 1 mash nameing policy uses {Mash1}
Also added sub option to INLINE ALLWAYS MASH, which overides the normal macro nameing policy, internal methods now use the mash name {MashTempA} for purposes of saving and returning a value.
=back
=item 0.23
23 Sept 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=item 01 Oct 2007
Live on primary, secondary and test systems. When sending mail to "sendmail.org" (via test), sendmail tried and failed to allocate more names for itself ("too many long names" again) AAARGH! However the send did still work (without md5)
Currently the Sendmail::M4::Mail8.pm version uses 21 "long names", OK for normall sending. But md5 needs more.
=over 4
=item *
MACRO{ statement FOUND & IS FOUND modified, new statements FIND & STORE, now a single {MashFound} macro can be loaded with as many sub names as required.
=item *
Translate rule set modified to pack {MashFound}
=item *
define_MashFoud added, to declare packed components of {MashFound}
Modifications made so that {macro} maybe used for TEST D & SANE statments, but will be packed into {MashFound} if they have been defined.
=back
=back
=item 0.24
22 September 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=item 08 Oct 2007
Error in B<pod> line 960 space between =head 2, as B<Mail8> has been updated with B<Reply-to> header line checking, this little thing can be fixed and uploaded.
=back
=item 0.25
08 October 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=item 12 Oct 2007
FIND did not have NOMASH stated, so used a long-name when it should not have had done.
Mail8 development, dealing with hotmail & yahoo mail addresses and domains, showed that sendmail has a problem with wild-cards higher than $9, use a $10 and sendmail will complain ( too many wildcards ).
define_MashFound ammended along with others that use the packed form of {MashFound}, although sendmail has a limit of 9 wildcards, {MashFound#} where # is numeric, each containing upto 9 elements, the presence of these makes no other difference to the macro coding, FIND STORE etc all work as before.
=item 13 Oct 2007
POD clean up, HISTORY moved to end of document, layout of POD improved, but some bits will be left for later.
Code check. Should not touching again until the socks method is ready.
=back
=item 0.26
13 October 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=item 14 Oct 2007
Mail8 added another component to MashFound# making 9 in one, causing M4 statement not to formated correctly, failure in logic fixed.
=back
=item 0.27
14 October 2007 CPAN Amended version
B<Amendments to release version>
=over 3
=back
=cut
1;