Skip to main content
Version: 1.1

External script

This condition runs a script on the mail server for the message and decides by the script's exit code or its output. Use it for checks the built-in conditions cannot do - a virus scan with ClamAV, a lookup in a CRM or customer list, or a query to another system.

The condition is available in the conditions and exceptions of policy, signature, disclaimer and autoresponder rules. You find it at the top level of the condition menu, as External script.

The scripts directory​

Scripts run only from the scripts directory of the server, never from a path given in a rule, so an account that can edit rules cannot run arbitrary programs. The directory is SCRIPTS_DIR in the configuration; the default is scripts in the installation, /opt/msh-zr/scripts, so external scripts work out of the box. A relative path is resolved against the install directory. Set SCRIPTS_DIR empty to keep the condition from running anything:

/opt/msh-zr/etc/config.cf
SCRIPTS_DIR=

SCRIPTS_MAX_CONCURRENT caps how many scripts run at the same time (5 by default). A script that finds no free slot within its timeout does not run.

The directory ships with two sample scripts and a README.txt. Both samples get the sender, the recipients and the subject as arguments and always end the same way, whatever the message holds: accept.sh exits with 0 and reject.sh with 1. Use them to try the condition before writing your own. An update of the installation replaces the samples and the README; scripts under other names are kept.

To add your own scripts:

  1. Copy them into /opt/msh-zr/scripts.

  2. Run the permissions script. It makes the directory and its scripts owned by root and readable and executable by the application user, which cannot change them:

    sudo sh /opt/msh-zr/lib/fixperms.sh
  3. If you changed SCRIPTS_DIR or SCRIPTS_MAX_CONCURRENT, restart the services so the milter and the administrator panel pick up the new values:

    sudo systemctl restart msh-zr-milter.service msh-zr-web.service

A script is run only when its real path - after resolving .. and symbolic links - is inside the scripts directory, it is a regular executable file, and neither its group nor other users can write to it. Anything else is refused, and the reason is written to the processing trace and to the server log.

A script runs without a shell and with a cleaned environment that holds only the MSH_* variables below, so values from the message cannot be expanded by a shell. Still, every argument and variable resolved from the message - a subject, an address, a header - is chosen by the sender. Prefer the MSH_* variables to arguments and quote them in the script ("$MSH_SUBJECT"); when a value has to be an argument, put -- before it so a subject such as --config=/x is not read as an option; and never pass a value through a shell (eval, sh -c). Control characters in the values are replaced by spaces before the script starts, and an argument that resolves to nothing is passed as an empty string so the arguments after it keep their positions.

Script​

The Script tab of the External script dialog, with the Script and Arguments fields, the Pass the message choice and the timeout

Script: The file to run, relative to the scripts directory, for example check-sender.sh.

Arguments: Arguments passed to the script. Variables such as {message:mail} can be used; each variable stays a single argument even when its value contains spaces. Put a value with spaces in double quotes. {message_file} is replaced with the path of a temporary file holding the message.

Pass the message:

  • Not passed (default) - the script gets the message details from the environment variables and arguments only.
  • As a file (MSH_MESSAGE_FILE) - the message is written to a temporary .eml file, readable only by the application user, and its path is passed in MSH_MESSAGE_FILE. The file is deleted when the script ends.
  • On standard input - the message is passed on the script's standard input.

Stop the script after (seconds): The timeout, from 1 to 300 seconds, 10 by default. A script that runs longer is stopped, together with any processes it started. Above 60 seconds the dialog warns that Postfix waits at most 300 seconds for the milter (milter_content_timeout), so keep scripts well below that.

Environment variables​

Every script gets these environment variables:

VariableValue
MSH_SENDEREnvelope sender
MSH_RECIPIENTSAll recipients (To, Cc and Bcc), comma-separated
MSH_SUBJECTSubject
MSH_MESSAGE_IDMessage-ID header
MSH_MESSAGE_SIZEMessage size in bytes
MSH_QUEUE_IDPostfix queue ID
MSH_MESSAGE_FILEPath of the message as an .eml file, when the message is passed as a file or an argument uses {message_file}

Match​

The Match tab of the External script dialog, with Exit code selected, the comparison and exit code fields, and the greyed out Output options

Choose what decides whether the condition matches.

Exit code (default): Compare the script's exit code.

  • Comparison - equal, not equal, greater than, greater or equal, less than, or less or equal.
  • Exit code - the value to compare with, 0 by default. Equal and not equal take a list of codes and ranges, such as 1, 2, 5 or 10-19; the other comparisons take a single number.

Output: Look for a text in the script's standard output (up to 64 KB is read).

  • Text to look for - the text or pattern.
  • Match using - Contains (default), Wildcards or Regular expression.
  • Look at the first line only (on by default) - check only the first line of the output instead of all of it.

Options​

The Options tab of the External script dialog, with the choice for a script that cannot run, fails or times out, and the Save the result as field

When the script cannot run, fails or times out:

  • Do not match (default) - the condition is not met.
  • Match - the condition is met. Choose this when a failed check should rather block than let a message through.

This also applies when the script is refused, for example because external scripts are not enabled, and when too many scripts are already running and none finishes within the timeout.

Save the result as: A name for the result, for example spam. The actions of the same message can then use it:

VariableValue
{script_output:name}The script's output, trimmed
{script_exit_code:name}The script's exit code

Both are empty when the script did not run for the message, was refused, failed or timed out. The name is not case-sensitive. For example, the Add header action can add X-Spam-Score: {script_output:spam}.

Click OK to save the condition. OK is enabled only when a script is given, the timeout is valid, and the exit code or the text to look for is filled in.

One run per message​

A script runs once per message, however many rules use it: when another condition runs the same script with the same arguments, message input and timeout for the same message, it reuses the first result.

Examples​

Try the condition with the shipped samples - reject.sh always exits with 1:

  • Script reject.sh, arguments -- {message:envelope:sender} {message:envelope:recipients} {message:subject}.
  • Match on Exit code, equal 1.
  • Action: Block message. Every message the rule applies to is now blocked; swap in accept.sh with equal 0 to see the opposite.

Virus scan with ClamAV - a script of your own that runs clamdscan on the message file:

  • Pass the message As a file and run clamdscan --no-summary "$MSH_MESSAGE_FILE" in the script.
  • Match on Exit code, equal 1 - clamdscan exits 0 for a clean message, 1 when a virus was found and 2 on errors.
  • Set When the script cannot run, fails or times out to Match, so a message the scanner could not check is not let through.
  • Action: Block message.

No disclaimer for customers - a script that checks MSH_RECIPIENTS against a list of customer addresses or domains and exits with 0 when one is found:

  • Use it as an exception of a disclaimer rule.
  • Match on Exit code, equal 0.

Testing​

The Rules tester does not run scripts by default. On the Scripts tab of a test message, set the Simulated script exit code and Simulated script output that every External script condition gets instead.

Tick Run the scripts on this server instead to run the scripts for real with the test message. This option is available only when the web app was started with a scripts directory, which is the case unless SCRIPTS_DIR is empty.

Processing trace​

The message processing trace shows the command line, the exit code, the run time and the first 200 characters of the output and error output, for example:

  • Running script clamav-scan.sh.
  • The script finished with exit code 1 in 412 ms.
  • The script was not run: External scripts are not enabled on this server.
  • Script reject.sh already ran for this message, using its result.

A refused, failed or timed out script is also reported in the server log, once a minute per script and problem.

Additional resources​

  • Run executable - run a program as an action, after a rule matched; it runs programs from the same directory under the same rules
  • Variables - variables that can be used in the arguments