8
\$\begingroup\$

I'm going to put this script into production in a mail server /etc/aliases file: we have a system that receives email but the subject line must be limited to a certain size. The proposed usage in the aliases file:

alias_name: "| email_filter.sh -s 200 actual.recipient@example.com"

It will be deployed on a gnu/linux system. I'd appreciate any feedback.

#!/bin/bash
shopt -s extglob

usage="$(basename $BASH_SOURCE) [-h] [-s n] recipient"
where="where: -s n == truncate the subject to n characters"
subject_length=''

while getopts :hs: opt; do
    case $opt in
        h) echo "$usage"; echo "$where"; exit ;;
        s) subject_length=$OPTARG ;;
        *) echo "Error: $usage" >&2; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

# validation
if [[ "$#" -eq 1 ]]; then
    recipient=$1
else
    echo "Error: $usage" >&2
    exit 1
fi
if [[ -n $subject_length ]] && [[ $subject_length != +([0-9]) ]]; then
    echo "Error: subject length must be a whole number"
    exit 1
fi

sed_filters=()
if [[ -n $subject_length ]]; then
    sed_filters+=( -e "s/^(Subject: .{1,$subject_length}).*/\1/" )
fi
# other filters can go here

if [[ ${#sed_filters[@]} > 0 ]]; then
    cmd=( sed -E "${sed_filters[@]}" )
else
    # no command line filters given
    cmd=( cat )
fi

# now, filter the incoming email (on stdin) and pass to sendmail
"${cmd[@]}" | /usr/sbin/sendmail -oi "$recipient"
\$\endgroup\$
9
\$\begingroup\$

Be sure to read the relevant RFCs that govern e-mail headers! Specifically:

  • RFC 2822, Section 1.2.2: Header names are case-insensitive.
  • RFC 2822, Section 2.2.3: Header fields may be line-folded:

    2.2.3. Long Header Fields

    Each header field is logically a single line of characters comprising the field name, the colon, and the field body. For convenience however, and to deal with the 998/78 character limitations per line, the field body portion of a header field can be split into a multiple line representation; this is called "folding". The general rule is that wherever this standard allows for folding white space (not simply WSP characters), a CRLF may be inserted before any WSP. For example, the header field:

    Subject: This is a test
    

    can be represented as:

    Subject: This
     is a test
    

    Since your sed operates on the raw representation of the header, you will miss headers that are logically longer than subject_length characters long, but start with a physically short line.

    What is your rationale for developing this filter? Is the application that processes the incoming messages unable to handle long subject texts, or is it unable to handle long physical lines? If it's the latter, maybe all you need is a filter that performs line folding, rather than truncation.

  • RFC 2047: Encoding mechanisms for non-ASCII headers. A logical subject line

    Subject: this is some text
    

    … could also be represented physically as

    Subject: =?iso-8859-1?q?this=20is=20some=20text?=
    

    … or by many other representations. Is your limit based on the number of bytes in the raw representation, the number of bytes in the UTF-8 representation, the number of Unicode characters, or something else? You didn't specify clearly. If you are truncating the raw representation, you might truncate a MIME-encoded header at a point that makes it syntactically invalid.

\$\endgroup\$
6
\$\begingroup\$

Generally good code - plus points for good use of stdout/stderr and exit status.

Shellcheck reported some issues:

shellcheck -f gcc  214327.sh
214327.sh:4:19: warning: Expanding an array without an index only gives the first element. [SC2128]
214327.sh:4:19: note: Double quote to prevent globbing and word splitting. [SC2086]
214327.sh:31:61: note: Backslash is literal in "\1". Prefer explicit escaping: "\\1". [SC1117]
214327.sh:35:26: error: > is for string comparisons. Use -gt instead. [SC2071]

Taking the first two, I'd simply use $0 to reproduce the program name as it was invoked, rather than messing about with basename to modify it. The other two appear to be mere typos in the code, and the fixes are obvious.

We might want to perform some sanity checks on $recipient; in any case, it's wise to indicate that it's an argument and not an option when invoking sendmail, by using -- as a separator.

The repeated tests for [[ -n $subject_length ]] could be combined into a single block:

sed_filters=()

if [[ -n $subject_length ]]
then
    if [[ $subject_length != +([0-9]) ]]
    then
        echo "Error: subject length must be a whole number"
        exit 1
    fi

    sed_filters+=( -e "s/^(Subject: .{1,$subject_length}).*/\\1/" )
fi

# other filters can go here

Instead of choosing between sed and cat, we could simplify by unconditionally using sed, even if we do no filtering, by priming the filters list with an empty command:

sed_filters=(-e '')

# conditionally add to sed_filters

# now, filter the incoming email (on stdin) and pass to sendmail
sed -E "${sed_filters[@]}" | /usr/sbin/sendmail -oi -- "$recipient"

sed with an empty program acts as cat.

The sed line may match body text as well as headers; we probably want to replace only the latter. We can do that by adding an address prefix:

1,/^$/s/^(Subject: .{1,$subject_length}).*/\1/i

(Note that RFC-822 headers are specified case-insensitively, so let's take that into account, using /i).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.