## MailFront

## SMTP Front Ends

## Mail Rules Specification

# Mail Rules Specification vX

The current mail rules specification is lacking in several areas:

- There is no way to select rules based on the state of environment variables (such as `$RELAYCLIENT` or `$TCPREMOTEIP`.
- There is no way to select rules based on the SMTP authentication state.
- It is difficult to seperate sender processing from recipient processing.
- The dictionary and CDB lookups don't support wildcarding (minor).

This document proposes an improved mail rules specification that addresses these problems.

## Selection

The use of mail rules is controlled by the environment variable `$MAILRULES`. This variable specifies the path to the compiled mail rules file. If `$MAILRULES` is set but the path that it points to cannot be opened, processing will fail with a temporary error. There is no default value -- if it is not set, mail rules processing is disabled.

The rules listed are applied before any other sender or recipient processing is done (such as checking against qmail's badmailfrom file).

## Compiled File Format

The mail rules must be compiled to a binary format. The binary file contains a signature header followed by rules, and terminated with a 32-bit CRC check code of all the data in the file. All numbers are unsigned 32-bit LSB unless otherwise indicated. A string consisists of a length number followed by that many bytes of data.

### Signature Header

1. Unique signature string.
2. Rule count.

### Rule

1. Rule size: total number of bytes in this rule.
2. Rule type: single byte code indicating what type of rule this is and when it should get executed. The format of remainder of the data in the rule is dependant on this value.
   - 0: Connection/Setup
   - 1: Sender validation
   - 2: Recipient validation
3. Conditions: count of the number of conditions (C).
4. (C) conditions containing:
   1. Negation flag: single byte boolean
   2. Comparison type: single byte code:
      - 0: Is defined
      - 1: Exact match
      - 2: Pattern match
      - 3: File lookup, whole string
      - 4: File lookup, domain portion
      - 5: CDB lookup, whole string
      - 6: CDB lookup, domain portion
   3. Variable name: string
   4. Comparison value: string
5. Assignments: count of the number of assignments (A).
6. (A) assignments containing:
   1. Set (1) / unset (0) flag byte
   2. Variable name
   3. Assigned value
7. Action: single byte code indicating the action this rule is to take:
   - 0: NO-OP
   - 1: PASS
   - 2: ACCEPT, followed by message string.
   - 3: DEFER, followed by message string.
   - 4: REJECT, followed by message string.
   - 5: DEFER-ALL, followed by message string.
   - 6: REJECT-ALL, followed by message string.
8. Message: string containing the response message.

## Variables

The following variables are internally defined.

- `authenticated`: Defined if SMTP authentication has succeeded.
- `sender`: The envelope sender address.
- `recipient`: The current envelope recipient address.
- `databytes`: The current maximum message size.

All other variable names are taken from environment variables.

## Variable Substitution

Response messages and assignment values undergo variable substition. All instances of `$NAME` or `${NAME}` in these strings are replaced with the contents of the named variable.

## Pattern Syntax

A pattern is a string of stars and non-stars. It matches any concatenation of strings matched by all the stars and non-stars in the same order. A non-star matches itself. A star before the end of pattern matches any string that does not include the next character in pattern. A star at the end of pattern matches any string. Patterns containing only "`*`" match anything. Note: An empty pattern matches only the empty string.

## Semantics

Each rule is applied in the order they are listed in the rules file until one matches. At that point, the command that triggered the rule search is accepted, deferred, or rejected depending on the rule type. If the sender is not accepted, no recipients can be accepted, as usual. As long as at least one recipient is accepted the message data may be accepted.

## Text File Format

### Syntax

Rules are seperated into sections by one of the following lines:

`[connect]`
  Connection validation
`[sender]`
  Sender validation
`[recipient]`
  Recipient validation

Each rule in the file occupies a series of lines. Empty lines are used to seperate rules within the file. Lines starting with "`#`" are ignored. The rule contains the following sections:

1. Conditions: a list of zero or more conditions. Each condition has a format from the following list. All conditions must be true for the rule to execute. A rule with no conditions always matches.
   - `!CONDITION` True if the condition (one of the below) is false.
   - `VAR=VALUE` True if the variable named `VAR` is defined and matches "`VALUE`" exactly.
   - `VAR~PATTERN` True if the variable `VAR` is defined and matches the pattern "`PATTERN`".
   - `VAR` True if the variable `VAR` is defined (even if it has been assigned an empty string).
   Sender-only rules must contain the condition "`!recipient`".
2. Action: One of the following:
   `:ACCEPT`
     Accept the sender or recipient.
   `:DEFER`
     Reject the sender or recipient with a temporary error code.
   `:REJECT`
     Reject the sender or recipient with a permanent error code.
   `:DEFER-ALL`
     Reject the message with a temporary error (rejects all past and future recipients, and resets the state).
   `:REJECT-ALL`
     Reject the message with a permanent error.
   `:PASS`
     Pass the sender or recipient on to the next processing state (ie `$RELAYCLIENT` or back-end validation).
   The action may be followed by another colon ("`:`") and a response message that will be given to the client. The default for this message depends on the action.
3. Assignments: A list of environment variables to set or unset as a result of matching this rule, one per line. Each assignment is formatted as `NAME=VALUE` (note, no quotes, the value ends at the end of the line). The special names `databytes`, `sender`, and `recipient` are handled specially. Assignment to `recipient` when handling a sender, and vice versa has no effect.

### Escaping

The following escape sequences are recognized in all the fields:

- `\n` newline (replaced with a CR+LF pair on output)
- `\###` character with octal value ### (exactly 3 digits)
- `\\` backslash
- `\:` colon

### Special Patterns

The following patterns are treated specially:

`[[@filename]]`
  Matches the domain portion of the address against the control file named `filename`. Typical uses would be "`recipient~[[@rcpthosts]]`" and "`recipient~[[@morercpthosts.cdb]]`".
`[[filename]]`
  Matches the entire address against the control file named `filename`. A typical use would be "`sender~[[badmailfrom]]`".

If `filename` ends with `.cdb`, the control file is opened as a CDB file. Addresses are translated to lower-case before doing CDB lookups. Otherwise, control files are plain text lists, with one entry per line. Empty lines and lines starting with "`#`" are ignored. Lines starting with "`@`" match only the domain portion of the address. All comparisons are case insensitive. Missing CDB files are silently ignored. Missing text files cause an error message at startup.

### Examples

#### qmail Rules

The following rules provide the functionality available in qmail-smtpd:

    [sender]
    sender~[[/var/qmail/control/badmailfrom]]
    :REJECT:Sorry, your envelope sender is in my badmailfrom list (#5.7.1)

    [recipient]
    $RELAYCLIENT
    :ACCEPT:Accepted
    recipient=${recipient}$RELAYCLIENT

    authenticated
    :ACCEPT:Accepted

    recipient~[[@/var/qmail/control/rcpthosts]]
    :ACCEPT:Accepted

    recipient~[[@/var/qmail/control/morercpthosts.cdb]]
    :ACCEPT:Accepted

    :REJECT:Sorry, that domain isn't in my list of allowed rcpthosts

## Missing Features

The following features are absent from this description, and could be added:

- Extended variable expansion: Borrow the following parameter/variable expansion features from bash:
  `${parameter:-word}`
  `${parameter:+word}`
  `${parameter:offset}`
  `${parameter:offset:length}`
  `${#parameter}`
  `${parameter#word}`
  `${parameter##word}`
  `${parameter%word}`
  `${parameter%%word}`
  `${parameter/pattern/string}`
  `${parameter//pattern/string}`