Sentinel 1.7b
-------------

    This milter I wrote mostly for myself as it relieves my sysadmin's
live considerably. If somebody else wants to use it, go ahead. But just
keep in mind this program is WITHOUT ANY WARRANTY, use this at your own
risk. Though this milter is stable enough on sparc Solaris7/8. I've never
had a chance to try it somewhere else under more or less solid loading.
It can be compiled in FreeBSD and Linux as well. So if somebody wants to
try it on any platforms different from Solaris7/8 just let me know about
results. Although "rbl" feature in this release doesn't work for FreeBSD.
I'm still looking for reentrant gethostbyname() implementation for FreeBSD.
If somebody has some ideas, just let me know. Also I didn't have enough
time to shape it as a package. Probably I'll do it in the future.


    I think whoever is going to use it knows how to integrate Milter with
sendmail. It is documented very well on http://www.sendmail.org
So this README just explains how to operate Sentinel.

Usage: sentinel -p port [-c config] [-v[1-3]] -d
  or
       sentinel -p port [-c config] [-v[1-3]]
  or
       sentinel -t [-c config] [-f message] [-v[1-3]]

where
    -p port - a port description according to libmilter specification
	      unix:/var/run/smmsp/sentinel.socket for example

    -c config - a configuration file location (/etc/mail/sentinel.cf by default)

    -d - run as a daemon

    -t - test a configuration file and return

    -f message - a message file location for testing your configuration on
       a real message. This option will simulate all SMFI calls like for a real
       message with complete debug/log output on STDOUT. Three additional headers
       are supported for a message in order to simulate mlfi_connect/mlfi_envfrom/mlfi_rcpt
       calls. Just place Connection/EnvelopeFrom/EnvelopeRcpt headers at the begining
       of a message in order to simulate "connection" and  "mail from"/"rcpt to" smtp
       commands like below:
         ---
         Connection: 211.239.127.9
         EnvelopeFrom: foo@bar-list.com
         EnvelopeRcpt: person1@company.com
         EnvelopeRcpt: person2@company.com
         ...
         EnvelopeRcpt: personn@company.com
         Received: from relay.bar-list.com (relay.bar-list.com [192.112.214.7])
	           by mx1.mycompany.com (....)
         ....
         ---
       This option might help you to debug your regular expressions and understand
       your configuration better ;) Also it'll help you to understand how Sentinel
       is working.

    -v - verbose or debug level. Global parameter "debug" need to be specified
         to have appropriate destination for debug records. Syslog is by default.

Use signal USR1 to reload a configuration in fly.

The simple configuration file might look as described below:

sentinel.cf
############################################################################
## global chapter ##
[global]
tmp:/var/tmp
log:/var/log/sentinel.log
debug:/var/log/sentinel.debug
user:smmsp
group:smmsp
max_nofiles:512
max_soconn:64

## hosts chapter ##
[hosts]
smtp1:smtp1:reject:/^64\.32\.18\.3[4-9]/
smtp2:smtp2:reject:/somewhere\.net/i

## headers chapter ##
[headers]
From:	from1:from1:spam:/Vladimir\.Lenin@gorki.com/i
	from2:from2:spam:/Alexandr\.Trotsky@zimney.com/i

EnvelopeRcpt:	rcpt1:rcpt1:accept:/autoparser@mycompany\.com/i

EnvelopeFrom:	efrom1:efrom1:discard:/somebody@tryToSpamUs\.com/i

To:	to1:to1,rcvd1:spam:/undisclosed\.recipients@mycompany\.com/i
	to2:to2,rcvd1:spam:/user@mycompany\.com/i

Received:	rcvd1:rcvd1:null:/by[ ]+mx0[1-9]+\.mycompany\.com/i
		rcvd2:rcvd2:spam:/\.nas[0-9]+\.dialup.somewhere1\.in\.the\.inet\.net/i
		rcvd3:rcvd3:spam:/\.nas[0-9]+\.dialup.somewhere2\.in\.the\.inet\.net/i

Subject:	subj1:subj1:spam:/^adv:|save money|lose weight|save up to/i
		subj2:subj2:spam:/adult|pussy|pussies|sex/i

## attachments chapter ##
[attachments]
## reject all unsafe attachments from outside, some of them from inside as well
## all attachments below are according to microsoft
## http://support.microsoft.com/support/kb/articles/Q262/6/17.ASP
att01:att01,rcvd1:virus:/\.ade$/i
att02:att02,rcvd1:virus:/\.adp$/i
att03:att03,rcvd1:virus:/\.bas$/i
att04:att04,rcvd1:virus:/\.bat$/i
att05:att05,rcvd1:virus:/\.chm$/i
att06:att06,rcvd1:virus:/\.cmd$/i
att07:att07,rcvd1:virus:/\.com$/i
att08:att08,rcvd1:virus:/\.cpl$/i
att09:att09,rcvd1:virus:/\.crt$/i
att10:att10,rcvd1:virus:/\.exe$/i
att11:att11,rcvd1:virus:/\.hlp$/i
att12:att12,rcvd1:virus:/\.hta$/i
att13:att13,rcvd1:virus:/\.inf$/i
att14:att14,rcvd1:virus:/\.ins$/i
att15:att15,rcvd1:virus:/\.isp$/i
att16:att16,rcvd1:virus:/\.js$/i
att17:att17,rcvd1:virus:/\.jse$/i
att18:att18,rcvd1:virus:/\.lnk$/i
att19:att19,rcvd1:virus:/\.mdb$/i
att20:att20,rcvd1:virus:/\.mde$/i
att21:att21,rcvd1:virus:/\.msc$/i
att22:att22,rcvd1:virus:/\.msi$/i
att23:att23,rcvd1:virus:/\.msp$/i
att24:att24,rcvd1:virus:/\.mst$/i
att25:att25,rcvd1:virus:/\.pcd$/i
att26:att26:virus:/\.pif$/i
att27:att27,rcvd1:virus:/\.reg$/i
att28:att28:virus:/\.scr$/i
att29:att29,rcvd1:virus:/\.sct$/i
att30:att30,rcvd1:virus:/\.shs$/i
att31:att31,rcvd1:virus:/\.shb$/i
att32:att32,rcvd1:virus:/\.url$/i
att33:att33:virus:/\.vb$/i
att34:att34:virus:/\.vbe$/i
att35:att35:virus:/\.vbs$/i
att36:att36,rcvd1:virus:/\.wsc$/i
att37:att37,rcvd1:virus:/\.wsf$/i
att38:att38,rcvd1:virus:/\.wsh$/i

## mime types chapter ##
[mime types]
mime1:mime1:virus:/^application/x-javascript$/i

## body chapter ##
[body]
body1:body2:offense:/fuck|shit/i

## action chapter ##
[actions]
## all actions are here ##
spam:reject:The message was classified as SPAM.
reject:reject:This host is blacklisted in our company
virus:reject:The attachment of the message was classified as unsafe.
offense:reject:The content of the message consists an offensive words.
discard:discard
accept:accept
null:null

####################################################################

The format of a configuration file is next:

[chapter 1]
statement 1
statement 2
.
.
.
statement n

[chapter 2]
statement 1
statement 2
.
.
.
statement n
.
.
.
[chapter n]
statement 1
statement 2
.
.
.
statement n

"statement n" might be either parameter/event/action statement for a particular chapter
or directive "INCLUDE". Directive "INCLUDE" has next syntax:

INCLUDE full_path_to_included_file

For example:

INCLUDE /etc/mail/sentinel/attachments.cf

All "INCLUDE" files just continue a configuration right from "INCLUDE" point.
Nested "INCLUDE"'s, when included has also "INCLUDE" directive, are allowed.

Below are next chapter names that available in current version:

[global]
[hosts]
[headers]
[attachments]
[mime types]
[body]
[actions]

All chapters but "global" and "actions" have next format:

event:events:actions:regex

where:
  - event - an event name that will be registered for a message in case
            a regular expression is matched (context dependable).
  - events - comma separated list of all events that must be registered
             (or must not in case "negative" event) for a message in order
	     to process "actions". Symbol "!" is a sign of "negative event".

          Sample:
	     --
             addr2:addr2,!addr1:ordb(rejectordb):/^(\d+)\.(\d+)\.(\d+)\.(\d+)/
	     --
	     that means "run an action ordb(rejectordb) only if event addr2
	     is matched and event addr1 isn't matched". In other words:
             --
             if (addr2 == true && addr1 == false)
             then
                 run ordb(rejectordb)
             endif
             --
             Multiple negative events are allowed in a list.

        Sample:
             --
             addr2:addr2,!addr1,!addr3:ordb(rejectordb):/^(\d+)\.(\d+)\.(\d+)\.(\d+)/
             --
	     that means:
             if (addr2 == true && addr1 == false && addr3 == false)
             then
                 run ordb(rejectordb)
             endif
             --
  - actions - comma separated list of all action that will be processed
              in case all "events" are registered and all "negative events"
	      are not registered.
  - regex - regular expression for current event. This version supports
            extended regular expressions. Two options are available for regex:
	      i - case insensitive
	      n - negative, that means NOMATCH must return MATCH result
	      s - check against html content, which has stripped html tags.
	      d - decode html numeric entities, like "&#nnn;"
	    An option "s" may be helpful against such tricks in html contens as
	    "viagra". If you defined your regex like "/viagra|valium|xanax/is",
	    then it'll be running against stripped html content (html content without
	    html tags). It means all html tags will be taken out from html content
	    before to run appropriate regular expression. Of course, it's working for
	    "text/html" only. This optiion will be ignored in case of "text/plain" content".
	    An option "d" is also helpful in case spammers try to hide real content in sequences
	    "&#nnn;". For example, if you defined your regex like "/viagra|valium|xanax/d",
	    then it'll be running against decoded html content (html content without numeric entities).
	    Therefore, the sequences "&#nnn;" will be decoded before to run either regex or body ACL's.
	    You can combine this option with option "s". Of course, it's working for "text/html" only.
	    This option will be ignored in case of "text/plain" content. Below is an example how to run
	    ACL against HTML content with stripped tags and decoded numeric entities:
            --
            [body]
            body1:body1:tabooBody:/.*/ds
            ...
            [actions]
            tabooBody:acl:/etc/mail/milter/tabooBody.lst
            --
            where tabooBody.lst is something like below:
            --
            viagra
            Online Pharmacy
            xanox
            phentermine
            prozac
            valium
            Vicodin
            ...
            --
	    An expression must be delimited by // if you want to use these
	    options. There are some perl-like extentions that are actualy
	    "Alpha Regex Metasymbols":
		\w - match any "word" character (alphanumerics plus "_");
		\d - match any digit character;
		\s - match any whitespace character;
		\n - match the newline character;
		\r - match the return character;
		\t - match the tab character.
	    I don't see any necessity to implement "\W", "\D" and "\S"
	    metasymbols as well. They might be easy defined in your regular
	    expressions as "[^\w]", "[^\d]" and "[^\s]" accordingly.

	Sample:
	    subj1:rblpfx,rcvd1,ordb:/^RBL:/in

	    "[global]" chapter.

The syntax for a chapter "[global]" is next:
:
Currently next parameters can be defined here:

tmp - where is a temporary files location. Very important if you are going
      to use "program" action. In this case there should be enough space
      for temporary files in order to accommodate decoded attachments.

log - where is a logfile location in order to log all actions by sentinel.
      Must have write permissions for an user "user" (see below). You can either
      specify "syslog" there or just omit this parameter if you want all log
      info was recorded by syslog. Syslog is by default. Look for "mail.info"
      destination in your syslog.conf

debug - where is a debugfile location in order to record all debug info there.
      Must have write permissions for an user "user" (see below). You can either
      specify "syslog" there or just omit this parameter if you want all debug
      info was recorded by syslog. Syslog is by default. Look for "mail.debug"
      destination in your syslog.conf

user - milter will be running under this account perm.

group - milter will be running under this group perm.

max_nofiles - max number of file descriptors. Important for heavy loaded systems.
              Can be used as the fix for next possible problems:
	         - "libthread panic: cannot create new lwp";
		 - "too many open files".

max_soconn - backlog value for smfi_sebacklog(). It's using as backlog parameter
             for listen(). Don't use it if you don't understand what you are
	     doing. The default value in libmilter is 20 for most of systems.
	     Could be useful for heavy loaded systems.

max_parse_txt - this parameter tells sentinel not to process "body" rules for
                "text/plain" or "text/html" message part if content length for
		this part is greater than "max_parse_txt". This parameter must
		be specified in bytes. The deafult value is 1M.


	    "[actions]" chapter.

Actually all actions that are described here can be available for events in
chapters "[hosts]", "[headers]", "[attachments]", "[mime types]", "[body]".
The syntax for chapter "[actions]" is next:

::

where
   - any name you want

   - one of the next functions that available in the current release:

    null - null operator, that means do nothing but just register an event.
          Sample: null:null

    accept - accept a message without any further checking. Don't need any arguments.
             This type is a highest priority type. If you defined some rules with
	     "accept" type, a message will be accepted, no matter what kind of events
	     were triggered before.
          Sample: accept:accept

    reject - reject a message. An argument will be passed to 554 4.7.1 DSN diagnostic.
             An argument will be processed as a pattern relatively to a context from
	     where this action was called (headers, hosts, body reg. expressions).
	     You can use next subexpressions:
	         ${0} - whole string that was submitted for an expression
		        (for example "subject" header)
		 ${1} .. ${nn} - subexpressions according to parenthesized
				 subexpressions (delimited by () regular expressions)
		                 in appropriate regular expression.
		 $file - attached file name in context of "[attachments]" chapter.
          Samples:
	        spam:reject:The message was classified as SPAM.
                rejectordb:reject:Rejected according to ORDB Realtime Blackhole List for IP address ${1}.${2}.${3}.${4} (see http://ordb.org/lookup/?host=${1}.${2}.${3}.${4}). Please contact postmaster@netapp.com in case any business related issues.
		virus:reject:The attachment "$file" of the message was classified as unsafe.

    discard - just discard a message without any notifications.
              Don't need any arguments.
	  Sample:
	        discard:discard

    addrcpt - add an recipient for a message. A recipient must be specified
              as an argument.
	  Sample:
	        addrobot:addrcpt:robot@mycompany.com

    delrcpt - remove an recipient. Don't need any argument. Make sense only
              in "headers" chapter and only for "EnvelopeRcpt" header. Will
	      remove current recipient for an event in "EnvelopeRcpt".
	  Sample:
	        delrcpt:delrcpt

    chgrcpt - change an recipient. A new recipient must be specified as argument.
              Make sense only in "headers" chapter and only for "EnvelopeRcpt"
	      header. Will change a current recipient for an event in "EnvelopeRcpt".
	  Sample:
	        chg_domain:chgrcpt:${1}@newdomain.com

    addhdr - add a header for a message. A header name and value must be specified
             as arguments
	  Sample:
	        xfilter:addhdr:X-Filter: this message was checked by Sentinel

    delhdr - remove a header for a message. Don't need any arguments. Will remove
             a current header for a first matched regular expression in an event
	     list for a header that defined in "headers" chapter.
	  Sample:
	        delhdr:delhdr

    chghdr - change a header. A new value for a header must be specified as an argument.
             Make sense only in "headers" chapter. Will change a current header for a
	     first matched regular expression in an event list for a header that defined
	     in "headers" chapter.
	     Sample:
	        rblpfx:chghdr:RBL: ${0}

    quarantine - save message in quarantine directory. Quarantine directory must be specified
                 as an argument. A message will be saved as a file with name event_name.queue_id,
		 where event_name is an event from where this action was called, and queue_id
		 is sendmail queue ID for a message. Default action after a message will be
		 saved is just to discard if an argument is not specified (see arguments for
		 actions program/rbl/quarantine below). You can use formatted string as a
		 directory specification. Only last subdirectory in the path will be created
		 according to your formatted string. Next date/time formatted symbols can be
		 used for last subdirectory in your directory path specification:
                     %a - locale's abbreviated weekday name;
                     %A - lLocale's full weekday name;
                     %b - Locale's abbreviated month name;
                     %B - Locale's full month name;
                     %c - Locale's appropriate date and time representation;
                     %d - day of month [1,31]; single digits are preceded by 0;
                     %D - date as %m/%d/%y;
                     %e - day of month [1,31]; single digits are preceded by a space;
                     %g - week-based year within century [00,99];
                     %G - week-based year, including the century [0000,9999];
                     %h - locale's abbreviated month name;
                     %H - hour (24-hour clock) [0,23]; single  digits  are  preceded by 0;
                     %I - hour (12-hour clock) [1,12]; single  digits  are  preceded by 0;
                     %j - day number of year [1,366]; single digits are preceded by 0;
                     %k - hour (24-hour clock) [0,23]; single  digits  are  preceded by a blank;
                     %l - hour (12-hour clock) [1,12]; single  digits  are  preceded by a blank;
                     %m - month number [1,12]; single digits are preceded by 0;
                     %M - minute  [00,59]; leading 0 is permitted but not required;
                     %p - locale's equivalent of either a.m. or p.m.;
                     %r - appropriate time representation in 12-hour clock format with %p;
                     %R - time as %H:%M;
                     %S - seconds [00,61];
                     %T - time as %H:%M:%S;
                     %u - weekday as a decimal number [1,7], with 1 representing Monday;
                     %U - week number of year as a decimal number [00,53], with Sunday as the first day of week 1;
                     %w - weekday as a decimal number [0,6], with 0 representing Sunday;
                     %W - week number of year as a decimal number [00,53], with Monday as the first day of week 1;
                     %x - locale's appropriate date representation;
                     %X - locale's appropriate time representation;
                     %y - year within century [00,99];
                     %Y - year, including the century (for example 1993);
	  Samples:
	         qrtDiscard:quarantine:/var/spool/quarantine
		     or
	         qrtDiscard:quarantine:/var/spool/quarantine/%Y%m%d
                     
		 qrtReject:quarantine(spam):/var/log/quarantine
		     or
		 qrtReject:quarantine(spam):/var/log/quarantine/%Y%m%d

    program - call an external program. This action make sense only in "attachments"
              chapter. An argument is a program name with all required parameters.
	      An argument must be specified as a pattern with "$file" subexpression.
	      This action will call an "argument" if it's specified or just reject
	      action if it isn't. An argument or reject action will be called only
	      in case non zero return code.
	Samples:
	         vscan:program:/usr/local/sbin/vscan -b -f $file
	         vscan:program(virus):/usr/local/sbin/vscan -b -f $file
		 
		    where "virus" action was defined as:

		 virus:reject:The attachment "$file" of the message was classified as unsafe.

    strip - strip an attached file. An argument is a diagnostic text message that
            will be instead of attached file. A subexpression "$file" can be specified
	    in argument string.
	Sample:
	    stripatt:strip:The attached file "$file" was stripped.

    rbl - Remote Blackhole List. DNS lookup in RBL database will be done according
          to an argument. Actually this is an argument for gethostbyname call.
	  Next subexpressions may be specified: ${0}, ${1} .. ${nn}. A message
	  will be just rejected in case defined name will be found in RBL.
	  Of course this is just default behavior when no arguments is specified
	  for "rbl" action (see arguments for actions "progarm,rbl,resolv,quarantine" below).
	Samples:
	     ordb:rbl:${4}.${3}.${2}.${1}.relays.ordb.org.
	     ordb(rejectordb):rbl:${4}.${3}.${2}.${1}.relays.ordb.org.

		where "rejectordb" might be defined as:

	     rejectordb:reject:Rejected according to ORDB Realtime Blackhole List for IP address ${1}.${2}.${3}.${4} (see http://ordb.org/lookup/?host=${1}.${2}.${3}.${4}). Please contact postmaster@mycompany.com in case any business related issues.

    resolv - try to resolve name and compare results with second parameter (if defined).
             This action can accept 1 or 2 arguments. If only one argument defined, "resolv"
	     will return "reject" by default or run an action in parentheses in case specified
	     name is not resolved. If second argument is defined, which expects to be an IP
	     address, "resolv" will try to compare it with each with each address of resolved
	     name. It will return "reject" by default or run an action in parentheses if there
	     is no specified IP in results of resolving. First and second arguments must be
	     separated by comma in an action definition (see an example below).
	Samples:
             [headers]
	       Received:
                  rcvd2:rcvd2:helo(quarantine):/^from\s+([^ \t]+)\s+\((unknown\s+)?\[(\d+\.\d+\.\d+\.\d+)\]\)\s+by\s+mx[1-3]\.mycompany\.com/i

             [actions]
               helo(quarantine):resolv:${1}, ${3}
    acl - Access Control List allows to define external (outside of sentinel.cf) lists,
	  which can be utilized as whitelists, blacklists, banned URL's lists, and etc.
	  These external lists are regular text files, which consist one entry per string.
	  Entries may be domains, addresses, url's, and etc. It depends of context from which
	  particular "acl" will be called. For example, if you defined an "acl" to be called
	  from header's context, then such "acl" must refer to a list of domains/addresses.
	  In case you want to define an external list as banned URL's list, you must call
	  appropriate "acl" from "body" context ("body" chapter). Just keep in mind, you
	  can't use regular expression inside such lists. Sentinel will check all entries
	  in such lists as case insensitive string occurrences in appropriate context (headers,
	  text or html message body). It means incomplete domain name like "dailypromo.com"
	  will trigger an event for "From" header like "".
	Samples:
	  Lets say you defined blacklist in /etc/mail/milter/blacklist.lst, which consists:
	  --
	  apslist526.com
	  backpackgirl
	  cupidsclock
	  cdsteals.com
	  DailyDealDepot.com
	  ebanat.com
	  hotbot.com
	  vmadmin.com
	  VIRTUAL0.NET
	  SMTP0.NET
	  IREAYE.NET
	  sepis4556@yahoo.com
	  --
	  Then you need to define "acl" action for this list like below:
	  --
          [actions]
	  accept:accept
	  quarantine:quarantine:/var/spool/quarantine/sentinel/%Y%m%d
          blacklist(quarantine):acl:/etc/mail/milter/blacklist.lst
          whitelist(accept):acl:/etc/mail/milter/whitelist.lst
	  --
	  which can be utilized in "headers" chapter, like below:
	  --
	  [headers]
	  From: from1:from1:blacklist(quarantine):/.*/
	  ...
	  ...
	  EnvelopeFrom: efrom1:efrom1:blacklist(quarantine):/.*/
	                efrom2:efrom2:whitelist(accept):/.*/
	  --
	  In case of "whitelist" an example above shows "whitelist(accept)"
	  action for /etc/mail/milter/whitelist.lst whitelist.
          Or lets say you defined /etc/mail/milter/bannedurl.lst as below:
	  --
	  beefupyourpenis
	  cool-host
	  quickhost.bz
	  verycoolcars
	  secretsexads
	  freelivewebcamteens
	  --
	  In this case you can utilize this list like in example below:
	  --
	  [body]
	  body1:body1:bannedurl(quarantine):/.*/
	  ...
	  ...
          [actions]
	  quarantine:quarantine:/var/spool/quarantine/sentinel/%Y%m%d
          bannedurl(quarantine):acl:/etc/mail/milter/bannedurl.lst
	  --
	  Just keep in mind, if you want to utilize regular expressions, then you have to use
	  them in sentinel.cf file. "Acl"'s won't be much helpful in this case.
          Access control (ACL) can accept one extra parameter in action definition.
          Typically it could back reference to subexpression in regular expression of
          event calling this ACL. Like an example below:
          --
          [headers]
          Received: rcvd1:rcvd1:blackRelays:/^from.*\((.+)\s+\[.*\]( \(may be forged\))?\)\s+by\s+mx[1-9]\.mycompany\.com/i
          ...
          [actions]
          blackRelays:acl:${1}:/etc/mail/milter/blackRelays.lst
          --
          If you omit first parameter ("${1}" for example above), ACL will apply to entire header.
          Like below:
          --
          [actions]
          blackRelays:acl:/etc/mail/milter/blackRelays.lst
          --
          Please note, it does not work for body rules. Will implement it in next release.
          However, "url" parameter can be specified in "body" rules and will be ignored in
	  a header context. This parameter tells sentinel to run ACL rules against URL strings
	  only in a message body. All URL strings will be decoded before to run ACL's.
	  For example:
          --
          [body]
          body1:body1:bannedURLS:/.*/
          ...
          [actions]
          bannedURLs:acl:url:/etc/mail/milter/bannedURLS.lst
          --

Five next actions "program", "rbl", "acl", "resolv", and "quarantine" can be specified with
arguments that delimited in parenthesis. For "program" action an argument action will be called
every time when a program return non zero code. For "rbl" in case a name will be found in
a Remote Blackhole List. ACL's have "null" action by default. It means if you need something else
for your "acl", you have to specify it ias parameter, like "whitelist(accept)". For "resolv"
if a name cannot be resolved or results of resolving are different from a second specified argument
(IP address). For "quarantine" this is just an action that must be called instead of default "discard"
after a message will be saved in quarantine directory. Only one argument is allowed. But an argument
action can be called with an argument as well.
Samples:

      rcvd1:rcvd1,rblpfx:ordb(quarantine(spam)):/^from .*\(.*\[([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\]\).*by mx[1-3]+\.mycompany\.com/i
      rcvd2:rcvd2:helo(quarantine):/^from\s+([^ \t]+)\s+\((unknown\s+)?\[(\d+\.\d+\.\d+\.\d+)\]\)\s+by\s+mx[1-3]+\.mycompany\.com/i
      att10:att10,rcvd1:vscan(quarantine(virus)):/\.exe$/i

  where actions:

      ordb(quarantine(spam)),quarantine(spam),helo(quarantine),spam,vscan(quarantine(virus)),quarantine(virus),virus

  are define below as:

      spam:reject:The message was classified as SPAM.
      virus:reject:The attachment "$file" of the message was classified as unsafe.
      quarantine(virus):quarantine:/var/spool/quarantine
      quarantine(spam):quarantine:/var/spool/quarantine
      ordb(quarantine(spam)):rbl:${4}.${3}.${2}.${1}.relays.ordb.org.
      helo(quarantine):resolv:${1}, ${3}
      vscan(quarantine(virus)):program:/usr/local/sbin/vscan -b -f $file

Four actions rbl, acl, resolv, and program will trigger events with the same names in case
of non zero return code for "program", positive results for "rbl" and "acl", and negative
result for "resolv". For the sample above the event names will be "ordb" and "vscan" - action
name without arguments. You can use these triggered events in any another event lists.
Sample:

      subj3:subj3,ordb:rblpfx:/^RBL:/in

   that means if current subject is not prefixed and ordb action has positive
   result, then add "RBL:" prefix to a subject header by action "rblpfx". For
   this sample "rblpfx" action might looks as:

      rblpfx:chghdr:RBL: ${0}

Very simple configuration below is a real sample how "negative events"
could be used to distinguish internal "outbound" and external "inbound"
smtp connections. It's useless resource consumption to do rbl lookup for
"private network" hosts.
##############################################################################
[global]
tmp:/var/tmp
log:/var/log/sentinel.log
user:smmsp
group:smmsp
max_nofiles:512
max_soconn:30
max_parse_txt:524288

[hosts]
haddr1:haddr1:null:/^(192\.168\.|172\.16\.|10\.)/
# It does not make any sense to do ordb lookup for internal hosts
haddr2:haddr2,!haddr1:ordb(rejectordb):/^(\d+)\.(\d+)\.(\d+)\.(\d+)/

[actions]
ordb(rejectordb):rbl:${4}.${3}.${2}.${1}.relays.ordb.org.
rejectordb:reject:Rejected according to ORDB Realtime Blackhole List for IP address ${1}.${2}.${3}.${4} (see http://ordb.org/lookup/?host=${1}.${2}.${3}.${4}).
null:null

Below is more complex sample.
##############################################################################
[global]
tmp:/var/tmp
log:/var/log/sentinel.log
user:smmsp
group:smmsp
max_nofiles:512
max_soconn:30
max_parse_txt:524288
 
[headers]
X-Mailer:	xmlr1:xmlr1,rcvd1:spam:/WorldMerge|Extractor Pro|Floodgate Pro|Emailer Platinum.*Internet Marketing|BritecastMailer/i

#
# "subj3" event is defined with "negative" option in regex in order to
# eliminate duplicate prefixing.
# The rule for "subj3" is about "Do a subject prefixing by rblpfx action in
# case 'ordb' action returned positive result and a subject wasn't prefixed
# before".
#
Subject:        subj1:subj1,rcvd1:spam:/^adv:|save money|lose weight|save up to/i
		subj2:subj2,rcvd1:spam:/adult|pussy|girl| ass | fuck | sex |fucking/i
		subj3:subj3,ordb:rblpfx:/^RBL:/in

#
# The rules in aol.com, msn.com, and juno.com don't allow to use
# names that have first numeric symbols
#
From:	from1:from1:spam:/^[0-9]+@(aol|msn|juno)\.com/i
	from2:from2:spam:/girls\.net|jobseekernews\.com|jobseekernews\.net|jobfly\.com|supportgate\.com/i

To:	to1:to1,rcvd1:spam:/undisclosed\.recipients|friend@|@public\.com/i

#
# Do RBL checking and add an extra "X-RBL" header for all blackholed sources only
# if a subject header was not prefixed. This case is for Milter that's behind of
# firewall. Otherwise we could use "hosts" chapter in order to catch an IP.
#
Received:	rcvd1:rcvd1,subj3:ordb(addhdrordb):/^from .*\(.*\[([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\]\).*by mx[1-3]+\.mycompany\.com/i
		rcvd2:rcvd2:spam:/\.nas[0-9]+\.dialup\..*\.someISP\.net/i

#
# Next recipient's addresses are "pass through" addresses, no matter from where
# they are and which message content
#
EnvelopeRcpt:	rcpt1:rcpt1:accept:/autoparser@mycompany\.com|postmaster@mycompany\.com/i

[attachments]
#
# reject all unsafe attachments from outside, some of them from inside as well.
# All attachments below are according to the next article for microsoft:
# http://support.microsoft.com/support/kb/articles/Q262/6/17.ASP
# "rcvd1" event in this case allow to distinguish inbound external traffic
# (see "rcvd1" event above)
#
att01:att01,rcvd1:virus:/\.ade$/i
att02:att02,rcvd1:virus:/\.adp$/i
att03:att03,rcvd1:virus:/\.bas$/i
att04:att04,rcvd1:virus:/\.bat$/i
att05:att05,rcvd1:virus:/\.chm$/i
att06:att06,rcvd1:virus:/\.cmd$/i
att07:att07,rcvd1:virus:/\.com$/i
att08:att08,rcvd1:virus:/\.cpl$/i
att09:att09,rcvd1:virus:/\.crt$/i
att10:att10,rcvd1:virus:/\.exe$/i
att11:att11,rcvd1:virus:/\.hlp$/i
att12:att12,rcvd1:virus:/\.hta$/i
att13:att13,rcvd1:virus:/\.inf$/i
att14:att14,rcvd1:virus:/\.ins$/i
att15:att15,rcvd1:virus:/\.isp$/i
att16:att16,rcvd1:virus:/\.js$/i
att17:att17,rcvd1:virus:/\.jse$/i
att18:att18,rcvd1:virus:/\.lnk$/i
att19:att19,rcvd1:virus:/\.mdb$/i
att20:att20,rcvd1:virus:/\.mde$/i
att21:att21,rcvd1:virus:/\.msc$/i
att22:att22,rcvd1:virus:/\.msi$/i
att23:att23,rcvd1:virus:/\.msp$/i
att24:att24,rcvd1:virus:/\.mst$/i
att25:att25,rcvd1:virus:/\.pcd$/i
att26:att26:virus:/\.pif$/i
att27:att27,rcvd1:virus:/\.reg$/i
att28:att28:virus:/\.scr$/i
att29:att29,rcvd1:virus:/\.sct$/i
att30:att30,rcvd1:virus:/\.shs$/i
att31:att31,rcvd1:virus:/\.shb$/i
att32:att32,rcvd1:virus:/\.url$/i
att33:att33:virus:/\.vb$/i
att34:att34:virus:/\.vbe$/i
att35:att35:virus:/\.vbs$/i
att36:att36,rcvd1:virus:/\.wsc$/i
att37:att37,rcvd1:virus:/\.wsf$/i
att38:att38,rcvd1:virus:/\.wsh$/i
att39:att39:confd:/SunPricing2002\.xls/i

[mime types]
#
# Strip all javascript attachments for a message
#
mime1:mime1:stripatt:/^application/x-javascript$/i

[actions]
#
# All actions are defined here 
#
spam:reject:The message was classified as SPAM.
virus:reject:The attachment "$file" of the message was classified as unsafe.
stripatt:strip:Unsafe attachment was stripped.
ordb(rejectordb):rbl:${4}.${3}.${2}.${1}.relays.ordb.org.
ordb(addhdrordb):rbl:${4}.${3}.${2}.${1}.relays.ordb.org.
rejectordb:reject:Rejected according to ORDB Realtime Blackhole List for IP address ${1}.${2}.${3}.${4} (see http://ordb.org/lookup/?host=${1}.${2}.${3}.${4}). Please contact postmaster@netapp.com in case any business related issues.
addhdrordb:addhdr:X-RBL: dubious source ${1}.${2}.${3}.${4} according to ORDB Realtime Blackhole List (see http://ordb.org/lookup/?host=${1}.${2}.${3}.${4})
rblpfx:chghdr:RBL: ${0}
discard:discard
accept:accept
null:null

###############################################################################

What's in my TODO:

   1. more flexible event's list with or/and logic like below:

          event1 | (event2 & event3)

   2. ms-tnef support (don't know if I realy need it)
   
   3. different character set support for headers/body mime decoding / compare

   4. bugs fixing (if somebody will report)

If you have any questions or comments, just send them to smfilter2002@yahoo.com.

----
Igor