Sieve scripts are a great tool to filter incoming email messages before they are stored in your mailbox. The Dovecot IMAP server supports Sieve scripts via the Pigeonhole plugin. However, those scripts are applied to your incoming emails before they are delivered to your inbox.
Sometimes, you may wish to filter messages that are already stored in your mailbox. For instance, when a bug in a Sieve script caused many messages to be delivered incorrectly. As long as the Sieve IMAP filter is experimental and its support limited, the `sieve-filter` tool can come to your aid. This article explains how it is done.
Introduction
Our Dovecot IMAP email service user defined mail filter rules using the Managesieve protocol. His Sieve rules automatically classify the hundreds of emails flooding his inbox every day. They delete unwanted messages, store others in distinct folders, set flags and prioritize them for later reading. A typo in his sieve scripts caused the filters to break. Emails were sent directly to his inbox where now thousands of unclassified newsletters and spam mails linger, making him unable to determine the important communication. We were asked to help him refilter the messages using his fixed Sieve filters.
Example environment
For the sake of this article, we use `user.name@example.com` as email inbox. We advice our IMAP user to create a subfolder `Refilter` in his email inbox and move all unclassified emails into this folder.
Our example Dovecot email server uses `/var/vmail/[domain]/[user]` as storage location. User-defined custom Sieve scripts are located in a `/var/vmail/[domain]/[user]/.sieve` symbolic link that targets the enabled Sieve filter rules file. The inbox folder root is `INBOX`.
Proceedings
In order to re-apply Sieve rules on a Dovecot IMAP server, newer versions of Dovecot Pigeonhole come with the `sieve-filter` tool. It takes the target inbox, the sieve rules to apply, and the inbox folder to apply the rules to as arguments:
By default, the `sieve-filter` command runs in simulation mode. To apply any changes, we have to explicitly enable execution mode by adding the `-e` option, and allow write and delete access to the mailbox by adding the `-W` option. We also want the Sieve scripts to be recompiled to apply any last changes by the user. So we add the `-C` option to the command (a complete reference of available options can be found at the Pigeonhole Dovecot sieve-filter documentation page):
Considering that inboxes can grow rather large, and processing thousands of messages at a time may put a heavy strain on our live mail server, we may want to run the filtering with lower priority, only when the system isn't too busy. Therefore, we additionally use the `ionice` command with `-c2` for best-effort mode and `-n7` for lowest priority:
Wrap it into a Bash script
We don't always want to return to our knowledge base when having to help a mail user to refilter its messages. Therefore, we created a simple Bash script that just requires the mail user name as argument and does the job for us.
It is using the `doveadm` user lookup command to determine the users' Sieve script location and mailbox folders:
Which, for existing mailbox users, returns an answer like this:
uid *****
gid *****
home /var/vmail/example.com/user.name
mail maildir:/var/vmail/example.com/user.name/Maildir
quota_rule *:storage=0B
sieve /var/vmail/example.com/user.name/.sieve
We are using the `maildir` information to check for existing messages to process, as well as the `sieve` information to find the user-defined Sieve rules.
And here is the script as reference for our valued readers `sieve-refilter.sh`:
#------------------------------------------------------------------------------
# Reapply user-defined Sieve filters to a Dovecot mailbox folder.
#------------------------------------------------------------------------------
# REQUIREMENTS:
# awk, cat, doveadm, echo, exit, find, grep, ionice, printf, read, shift,
# sieve-filter, tr, wc, non-POSIX support for `< <()` syntax
#------------------------------------------------------------------------------
# USAGE:
# ./sieve-refilter.sh <user> [<folder>]
#
# Options:
# <user> The Dovecot user to reapply Sieve filters for. E.g.,
# "user.name@example.com".
# <folder> Optional: The mailbox folder to process.
# Defaults to 'INBOX.Refilter'.
#------------------------------------------------------------------------------
# Author: SHORELESS Limited <contact@shoreless.limited>
# See https://shoreless.ltd/kb20224071315
#------------------------------------------------------------------------------
# User option.
EMAIL_USER="$1"
if [[ -z "${EMAIL_USER}" ]]; then
printf "\n\e[1;31mAn argument error occurred.\e[0m\n\n"
(>&2 printf 'Argument error: The user parameter "<user>" is missing.')
printf "\n\nUsage: ./sieve-refilter.sh <user> [<folder>]\n\n"
printf "Options:\n\n"
printf " <user> The Dovecot user to reapply Sieve filters for. E.g.,\n"
printf " \"user.name@example.com\"."
printf " <folder> Optional: The mailbox folder to process.\n"
printf " Defaults to 'INBOX.Refilter'.\n\n"
exit 22
fi
FOLDER="$2"
if [[ -z "${FOLDER}" ]]; then
FOLDER="INBOX.Refilter"
fi
# Executes a command while writing STDOUT and STDERR to variables.
#
# @param STDOUT
# The variable to write STDOUT to.
# @param STDERR
# The variable to write STDERR to.
# @param COMMAND
# The command to run.
# @param [ARG1[ ARG2[ ...[ ARGN]]]]
# Any additional arguments for the command to run.
#
# @return
# The command exit code.
#
# @see https://stackoverflow.com/questions/11027679/#answer-59592881
catch() {
{
IFS=$'\n' read -r -d '' "${1}";
IFS=$'\n' read -r -d '' "${2}";
(IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ shift 2; ${@}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
}
echo "Dovecot Sieve refilter"
echo "----------------------"
echo "Checking provided options and mailbox:"
# Check, whether the user exists in the local Dovecot instance.
printf '\e[0m- Checking, whether the user exists... '
catch STDOUT STDERR doveadm user "${EMAIL_USER}"
if [ ${?} -eq 0 ]; then
printf "\e[1;32m[ok]\e[0m\n"
else
printf "\e[1;31m[error]\e[0m\n"
(>&2 printf '%s\n%s\n\n' " User lookup failed." " ${STDERR}")
exit 22;
fi
# Determine mail folder.
printf '\e[0m- Determine mail folder... '
MAIL_FOLDER=$(echo "${STDOUT}" | grep '^mail' | awk '{print $2}')
MAIL_FOLDER="${MAIL_FOLDER#maildir:}"
if [[ -z "${MAIL_FOLDER}" ]] || [ ! \( -d "${MAIL_FOLDER}" \) ]; then
printf "\e[1;31m[error]\e[0m\n"
(>&2 printf '%s\n%s\n\n' " Dovecot Maildir could not be found." " ${MAIL_FOLDER}")
exit 2;
else
printf "\e[1;32m[ok]\e[0m\n"
printf " ${MAIL_FOLDER}\n"
fi
# Determine sieve script.
printf '\e[0m- Check for Sieve script... '
SIEVE=$(echo "${STDOUT}" | grep '^sieve' | awk '{print $2}')
if [[ -z "${SIEVE}" ]] || [ ! \( -e "${SIEVE}" \) ] || [ ! \( -f "${SIEVE}" \) ] || [ ! \( -r "${SIEVE}" \) ] || [ ! \( -s "${SIEVE}" \) ]; then
printf "\e[1;31m[error]\e[0m\n"
(>&2 printf '%s\n%s\n\n' " Sieve script does not exist or is empty." " ${SIEVE}")
exit 2;
else
printf "\e[1;32m[ok]\e[0m\n"
printf " ${SIEVE}\n"
fi
# Count the number of unprocessed messages.
printf '\e[0m- Check for messages to refilter... '
MAIL_FOLDER="${MAIL_FOLDER}/.${FOLDER}"
if [ ! \( -d "${MAIL_FOLDER}/cur" \) ] || [ ! \( -d "${MAIL_FOLDER}/new" \) ]; then
printf "\e[1;31m[error]\e[0m\n"
(>&2 printf " User inbox doesn't have a '${FOLDER}' folder.\n\n")
exit 2;
fi
# Count the number of files in the target inbox folder.
FILES=`find "${MAIL_FOLDER}/cur/" "${MAIL_FOLDER}/new/" -type f -name '*' | wc -l`
if [ ${FILES} -eq 0 ]; then
printf "\e[1;33m[warning]\e[0m\n"
(>&2 printf " No messages to process. Aborting.\n\n")
exit 61;
fi
printf "\e[1;32m[ok]\e[0m\n"
printf " Found ${FILES} messages to process.\n\n"
echo "Running ${FILES} messages in ${FOLDER} through the Sieve filter."
ionice -c2 -n7 sieve-filter -e -W -C -u "${EMAIL_USER}" "${SIEVE}" "${FOLDER}"
Make the script executable:
Now we can run the script as administrator or vmail shell user:
Conclusion
The Pigeonhole `sieve-filter` tool is great for re-running Sieve filter rules on a Dovecot IMAP inbox folder. We created a simple script to perform this operation for a single user.