# 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.2;

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.2 (early Beta)
Very much a work in progress.
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, if you would like the hack file we are redeveloping at the moment, it will follow this onto CPAN within a few weeks, our current version is no longer available.

    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

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.

=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

 IO::File       file creation
 IPC::Open3     to start "sendmail -bt -Ctest.cf"
 File::Copy     to copy "tee" file to "file" in sendmails "hack" directory.
 English
 Data::Dumper   debuging this! used by our exported method "debug"

=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.

=back

    Expected/Allowed values allways as a (hash value pairing).

    hack_dir    
            SCALAR with default value of "/usr/share/sendmail/hack",
    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!

    sendmail
            SCALAR with default value of "/usr/sbin/sendmail"
    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'
    cf      SCALAR "test.cf file name" to build for testing purposes.
            if "install" is specified 
                if "cf" is not specified, 
                    will assume "test.cf" within current directory
                else 
                    if "cf" is "sendmail.cf" will "die"!
            else
                will assume the main "sendmail.cf" is being tested. 

    html    HASH REF, default is 0

    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".
    install SCALAR
            SU "root" permissions are required.
            Copy "tee" file to "file", (sendmail hack directory file).
            Create "cf" file.
            NOTE: ignored if also "html".
    test    SCALAR
            Will "build"|"install" before "test" if specified.

    silent  SCALAR
            STOPS all output! AND character translation!!
            It is assumed that you are going to do something with the compiled
            rules.
    error   ARRAY REF   only when also "silent" has contents of "moan"
            "whoops" will allways simply exit.
            
    UNKNOWN ARRAY REF remaining unknown arguments supplied.

    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!
    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.
    testing SCALAR automatic info, set when "test" starts, changes the way 
            both "ok" and "echo" operate.

    SU      SCALAR automatic info, is user "root":"root".
    time    SCALAR automatic info, "time" script started.

    macro   SCALAR automatic variable, incremented on MACRO statements
    rules   ARRAY REF automatic list of read in "S" macro rules
    rule    HASH  REF automatic keyed by "rules" 
            Format

                rule { 
                    Stest_macro => { 
                        S => []         contains complete "S" macro coding
                        H => []         HINT's as to use
                        O => []         keys for "T" in order of specification
                        T => {          TEST tests for coding
                            n => {      n = numeric count of test   
                                see "rule":"TEST" for details>
                            } 
                        } 
                        M => []         contains list of SUB macros
                                        TOP Level S only!
                        F => SCALAR     only defined if FORCE is defined
                    }

    magic   SCALAR  special value used by this program, do not use.

=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;
}

#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,
        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   => [],
    );
}
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));
        }
    }
    $setup{"test"} and $setup{"tee"} and $setup{"log"} = "$setup{'tee'}.log";
    $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?

=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  $_" } (@_),
            );
    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

    NOTE:   NOT for HTML! or when "silent"
            ALLWAYS does nothing, just returns 1 or 0 if "testing".

=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".
        <STDIN> "reply" "y" or "CR"    returns 1  OK!
        anything else                  returns 0  NOT OK!

=cut
        scalar $ok or return 1;
        return ($ok =~ /y/i)?(1):(0);
    }
    else
    {

=pod

    During "testing"
        <STDIN> "CR"        returns 0
        anything else       returned as is

=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" )
        { 
            moan "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" £ to $ conversion, also converts 3+ spaces to a tab.

=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 "£" where sendmail expects a "$", however if you are not using a UK English keyboard then you will have to escape \$ as normal.

"echo" does UTF8 "pound" £ to $ conversion, also converts 3+ spaces to a tab, this is done via B<translate> above.

=back

=back

    NOTE:   NOT for HTML! or when "silent"    
            allways does nothing, just returns 1.

=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 @_

    For sendmail "dnl" comments, wraps supplied args in "dnl".

    NOTE:   NOT for HTML! or when "silent"    
            allways does nothing, just returns 1.

=cut
push @EXPORT, "dnl";
sub dnl 
{
    ($setup{'html'} or $setup{'silent'}) and return 1;
    echo map { "dnl $_ dnl" } map {split "\n",$_} (@_);
}

######################
# 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

1st argument|line is the "S" macro rule, which must start with "S".

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;
    }
#keep backup copy for use latter on
    @macro_rules = @macro_rule;
# init macro list with main S RULE, also only top level has a M list
    $setup{"rule"}->{$rule} = { S => [], O => [], T => {}, M => [], H => [],};
    my $macros = $setup{"rule"}->{$rule}->{"M"};
# main rule, and any sub macros have the same properties
    &macro($rule, $macros, \@macro_rules);
# now for output? But not if silent!
    scalar $setup{"silent"} and return;
#HTML layout
    if ( scalar $setup{"html"} )
    {
#TODO
    }
    else
#Standard Layout
    {
        echo @{$setup{"rule"}->{$rule}->{"S"}};
# have we macros?
        foreach ( @$macros )
        {
            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
#
#
sub macro
{

    my ($rule, $macros, $macro_rules) = @_;

    my $macro       = $setup{'macro'};
    my $rule_hash   = $setup{"rule"};
    my $rule_list   = $setup{"rules"};
    my $rules       = $rule_hash->{$rule}->{"S"};
    my $test_hash   = $rule_hash->{$rule}->{"T"};
    my $test_list   = $rule_hash->{$rule}->{"O"};
    my $hint_list   = $rule_hash->{$rule}->{"H"};
    my $tests       = 0;

    my $mash = push @$rule_list, $rule;
#save S argument to return if S does not return first
    push @$rules, $rule, "R £*    £: £(SelfMacro {Mash$mash} £@ £1 £) £1";
#read through supplied S definition
    while ( my $line = shift @$macro_rules )
    {

=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
            $arg_comment =~ s/\s+#/\t/; 
            my ($arg,$comment) = split "\t", $arg_comment;
            push @$rules, "$start$macro $arg    $comment\n";
# sub macro rule, note $start has other bits
            $rule = "SScreen_macro_$macro";
# record this new S rule
            $rule_hash->{$rule} = { S => [], O => [], T => {} };
            push @$macros, $rule;
# nested call to process sub macro
            &macro($rule, $macros, $macro_rules);
        }
        elsif ( $line =~ /\}MACRO/ )
        {
            last;
        }

=pod

{MashSelf}

=over 2

{MashSelf} provides access to the autosaved argument for this rule.

Usage
    R £*    £: &£{MashSelf}

=back

=cut
        elsif ( $line =~ s/\{MashSelf\}/\{Mash\n/ )
        {
            my ($start,$rest) = split "\n", $line;
            push @$rules, "$start$mash}$rest";
        }

=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/ )
        {
            my ($start,$rest) = split "\n", $line;
            push @$rules, "$start$mash$rest";
        }

=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
    {   
        D   => [],      list of B<.D> define a Macro statements
        T   => SCALAR   translation macro, to be used before values 
                        below are supplied to the B<macro> under test
        V   => [],      values to try with B<macro>
        E   => [],      values as "V" but must result in "ERR"
        O   => [],      values as "V" but must result in "OK"
        F   => [],      values as "V" but must result in "FOUND"
    }

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 5 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 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


=back

=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/(D|V|E|O|F)\(\s*// )
                    {
                        my $D = $th->{$1} = [];
                        @$D   = split ",", $part;
                    }
                    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=>[] 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=>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_hash->{$rule}->{"F"} = (scalar $line)?($line):("?");
        }
# normal line
        else
        {
            $line =~ s/^\s+//;
            push @$rules, $line;
        }
    }
# restore saved value from begining
    push @$rules, "R £*    £: £&{Mash$mash}";
}
     

=back

=back

=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> 

    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
{
    echo <<ECHO;
LOCAL_RULESETS

STranslate
R £* ££| £*     £: £1 £| £2     fake for -bt mode
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
    map { 
        -f $setup{$_} and rename $setup{$_}, "$setup{$_}.$time" or 
            return moan "$!. install \"$_\" \"$setup{$_}\" rename failed" 
    } (qw(file cf));
#copy hack to its destination
    copy($tee, $file)  or return moan "$!. install, copy failed";
#compile CF file for testing
    system "m4 $mc > $cf" and return moan "\"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: $!";
        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 ( scalar @ok )
        {
            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 "$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_moan "$! 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 ($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 ( $force or not scalar @tests )
        {
            my $msg  = ($force)?($force):("Rule=:\"$rule\", Enter TEST value to try:> ");
            my $test = ok $msg;
            if ( scalar $test )
            {
                sendmail "$use_rule $test";
            }
        }
        foreach ( @tests )
        {
            my $tests = $rule_def->{'T'}->{$_} or next RULE;
#define statements required? remember these persit
            if ( scalar $tests->{'D'} and scalar @{$tests->{'D'}})
            {
                sendmail map { ".D$_" } (@{$tests->{'D'}}); 
            }
#translation macro?
            my $T = $tests->{'T'};
            foreach ( qw(V E O F))
            {
                scalar $tests->{$_} and scalar @{$tests->{$_}} or next;
                my $v = $_;
                my @V = @{$tests->{$v}};
                foreach ( @V )
                {
                    my $t = $_;
#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;
                    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 )
                    {
                        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;
                    }
                    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

