Zimbra Content Filter

This is an updated howto on getting a content filter working with Zimbra 6 Only and above; 5.x versions are not considered in this guide.

Credit should go to CyberNerd who created the original howto.


The plan is to use postfix after queue content filter to run a script that extracts email from the queue, process the email, then injects it back into the queue for delivery. We will be using grep to check the email against a wordlist of regular expressions. We will avoid email loops by using putmail.py to re-inject the email into the mail queue.

This version also only runs for people in a given list (students), which is to be populated via a cronjob. This stops the filter irritating staff users, but is entirely optional - adjust the badwordfilter script to suit your requirements.


* = Can be installed via your package manager

For convenience, copies of some of the files can be downloaded at this URL: http://stuff.m0php.net/zcf/

Editing files

Nano is the recommended text editor in this guide.

  • To save a file: Ctrl + O, Enter
  • To exit: Ctrl + X
  • View info (line number): Ctrl + C
  • Search: Ctrl + W
  • Paste: Shift + Insert


All commands given should be ran as root unless otherwise stated.

In Ubuntu, gain root access from the admin account (set at install) by using this method, and enter your password when prompted:

sudo -s

Create Inspection Directory

A directory must be created to spool the mail to whilst it is being processed. Within the Zimbra directory is a good place to put it.

The Zimbra user account needs read and write access to this directory.

mkdir /opt/zimbra/postfix/spool/filter
chown zimbra:zimbra /opt/zimbra/postfix/spool/filter
chmod 700 /opt/zimbra/postfix/spool/filter


Download & Copy

Download putmail.py, then copy it to /usr/local/sbin. Ensure the Zimbra user can access it:

cp putmail.py /usr/local/sbin
chown zimbra /usr/local/sbin/putmail.py
chmod 500 /usr/local/sbin/putmail.py

Modify (if required)

On Ubuntu, Putmail consistently complains that the HOME environment variable is not set (error seen in log files and returned dropped messages). The solution is to make the required changes below.

You do not need to do this if you use the copy of putmail.py from here.

About line 48, add the HOME_DIR entry

HOME_DIR = '/opt/zimbra/'

About line 139, comment-out the lines to stop the error detection

### Check for HOME present (needed later, checking now saves a lot of work) ###
#if not os.environ.has_key(HOME_EV):
        # Note: I still can't use exit_forcing_print() at this point, the log
        # filename is not set.
        #sys.exit(ERROR_HOME_UNSET + "\n")
</code python>
**Modify line 145 to load the log file from our new HOME_DIR variable**
<code python>
### Build the log filename now ###
theLogFile = os.path.join(HOME_DIR, CONFIG_DIRECTORY, LOG_FILE)

Modify line 256 to load the config file from our new HOME_DIR variable

configPath = os.path.join(HOME_DIR, CONFIG_DIRECTORY) # temporally

Create configuration

Create the putmail configuration home directory and set owner:

mkdir /opt/zimbra/.putmail
chown zimbra:zimbra /opt/zimbra/.putmail


Use a text editor (nano) to create the text file:

nano /opt/zimbra/.putmail/putmailrc

Copy the contents below, changing the admin email address to one of your own. The default port value of 10026 should be OK, but it should match the value set later in master.cf.in.

server = localhost
email = admin@example.com
port = 10026

Edit program

Putmail needs to be configured so it knows where its configuration file is. Edit the program using a text editor:

nano /usr/local/sbin/putmail.py

make sure this line points to the configuration home directory:

HOME_DIR = '/opt/zimbra/'

Bad words

The centrepiece of this whole feature is a list of bad words that will be detected in messages. It is stored in a simple plain-text file, with the following format:


Have a look here for some good words to give you a head-start: http://www.noswearing.com/list.php

Name this file (e.g. badwords and place it into a configuration directory (such as /etc/)

Copying & Pasting

nano /etc/badwords

Copying from elsewhere

cp badwords /etc/

Filtering script

The script below is what does the actual work of checking if a message contains any of the words listed in the badwords file from above.

This script was modified from the postfix website.

Save it to /usr/local/sbin/badwordfilter.

Variables to set

  • INSPECT_DIR: path to filter directory (/opt/zimbra/postfix/spool/filter)
  • SENDMAIL: path to putmail.py program (/usr/local/sbin/putmail.py)
  • BADWORDS: path to the badwords file (/etc/badwords)
  • MAILADMIN: Email address of the administrator, or whoever should receive copies of the dropped messages

The script

# Simple shell-based filter. It is meant to be invoked as follows:
#       /path/to/script -f sender recipients...
# Localize these. The -G option does nothing before Postfix 2.3.
# Changable variables
# Exit codes from <sysexits.h>
# Debug (make sure to create this file and give Zimbra write permissions to it)
#echo Bad word filter running for $@ >> /var/log/badwordfilter.log
# Clean up when done or when aborting.
trap "rm -f in.$$" 0 1 2 3 15
# Start processing.
cd $INSPECT_DIR || {
	echo $INSPECT_DIR does not exist; exit $EX_TEMPFAIL;
cat >in.$$ || {
	echo Cannot save mail to file; exit $EX_TEMPFAIL;
# Specify your content filter here.
cat in.$$ | grep -Eif /etc/students > /dev/null && {
	# Debug:
	#echo Student: $@ >> /var/log/badwordfilter.log
	cat in.$$ | while read line; do echo $line | grep -v 'Content-Disposition: attachment;' || break; done | grep -if $BADWORDS > /dev/null && {
		echo " Swearing detected - message has NOT been delivered and your actions HAVE been reported.";
$SENDMAIL "$@" <in.$$
exit $?

Set permissions

chown zimbra:zimbra /usr/local/sbin/badwordfilter
chmod 700 /usr/local/sbin/badwordfilter

Student list

The student list will contain a list of student accounts that the script checks. If the message being processed doesn't contain anyone in the list, then it skips the filter and gets delivered. Otherwise, the filter runs as expected.

Create file

Create a file called /etc/students and give the Zimbra user permission to it:

touch /etc/students
chown zimbra:zimbra /etc/students
chmod 700 /etc/students

Command for Cron

zmprov -l gaa -v | grep -e uid: -e zimbraCOSId | grep -B1 MyZimbraStudentCOSId | grep uid: | awk '{print $2}' > /etc/students

Edit the Zimbra crontab:

su - zimbra
crontab -e

Paste the line above into the end of the file, making sure to change the MyZimbraStudentCOSId to the COS ID that your students are assigned to. It should end up looking like this (change the time to suit):

0 6 * * *  zmprov -l gaa -v | grep -e uid: -e zimbraCOSId | grep -B1 d9c4bcbb-d02a-46a0-8e08-bbdfcd5f26d1 | grep uid: | awk '{print $2}' > /etc/students

To find the ID of a COS, run this command as Zimbra (substituting your own name):

zmprov gc studentcos|grep zimbraId:

Zimbra modification (Master.cf.in)

The Postfix part of Zimbra needs to be modified to run the content filter during mail delivery. The file is /opt/zimbra/postfix/conf/master.cf.in.

Backup the original master.cf.in file then edit it.

cd /opt/zimbra/postfix/conf
cp master.cf.in master.cf.in.backup
nano master.cf.in

Add this section to the bit after maildrop - around line 59:

filter    unix  -       n       n       -       -      pipe
        flags=Rq user=zimbra argv=/usr/local/sbin/badwordfilter -f ${sender} -- ${recipient}

Navigate to this line below (hit Ctrl + W, enter 10025, then hit Enter to search) and add the content filter named filter to the option -o content_filter=. Not easy to explain, but it should look like this: inet n  -       n       -       -  smtpd
                  -o content_filter=filter:dummy

Then add the following to the very end of the file: inet n  -       n       -       -  smtpd
        -o content_filter=
        -o smtpd_authorized_xforward_hosts=
        -o local_recipient_maps=
        -o virtual_mailbox_maps=
        -o virtual_alias_maps=
        -o relay_recipient_maps=
        -o smtpd_restriction_classes=
        -o smtpd_delay_reject=no
        -o smtpd_client_restrictions=permit_mynetworks,reject
        -o smtpd_helo_restrictions=
        -o smtpd_sender_restrictions=
        -o smtpd_recipient_restrictions=permit_mynetworks,reject
        -o mynetworks_style=host
        -o mynetworks=
        -o strict_rfc821_envelopes=yes
        -o smtpd_error_sleep_time=0
        -o smtpd_soft_error_limit=1001
        -o smtpd_hard_error_limit=1000
        -o smtpd_client_connection_count_limit=0
        -o smtpd_client_connection_rate_limit=0
        -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_address_mappings

Restart Services

This step must be completed as the Zimbra user. It's usually OK to just restart the MTA.

Restart all Zimbra services

# su - zimbra
$ zmcontrol stop
$ zmcontrol start

Restart just the MTA

# su - zimbra
$ zmmtactl restart

Zimbra Upgrades

After completing a Zimbra upgrade, the master.cf.in file is overwritten. It must be replaced after an upgrade of the content filter will fail. It may also be necessary to re-create the postfix spool directory.

Further reading

howto/zimbracontentfilter.txt · Last modified: 2011/01/17 09:59 (external edit)
www.chimeric.de Creative Commons License Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0