e-mail autoresponder using procmail

It may have taken way too many hours, and may have descended into a maze of twisty little passages, all alike, but after fighting various permissions issues and procmail‘s general intransigence (I still can’t work out what determines whether procmail will perform variable-interpolation on shell commands, or why it doesn’t like even multi-escaped square brackets in shell invocations…) here it is in all it’s glory: A procmail-based auto-reply system.

Why use procmail when sieve has a vacation module which is much simpler? The sieve module lacks the flexibility of the procmail solution and the setup below allows a single email to remotely turn the auto-response on and off. Compared to the original script, much more feedback is given when this happens.

Setup

One important email account which I use provides only POP3 mail access – which given the plethora of email-enabled devices around nowadays, isn’t helpful (POP3 doesn’t allow for any form of shared-status, so a read or deleted message must be read or deleted on every device individually, and sent mail can only be tracked locally). I therefore have fetchmail configured to retrieve new messages from this account and import it into my own IMAP server. In order to allow the filtering of these messages, both Postfix and fetchmail are set to use procmail as their mta or mailbox_command respectively. The script below is then invoked (as the fetchmail user) through procmail, which is SetUserID/SetGroupID – and is able to connect to the root-owned Dovecot authentication socket without further changes. Indeed, one of the advantages of this approach is that everything works with only a single permissions change – adding fetchmail to the mail group – which greatly reduces the chance of failure in the event of a future package update.

/etc/procmailrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
LOGFILE="/var/log/procmail.log"
VERBOSE=on
LOGABSTRACT=yes

# Script from http://oclug.on.ca/archives/oclug/2003-June/031832.html to perform
# automatic Out-Of-Office message sending.  Switch state by sending an email
# with a subject of "away message on" or "away message off" to yourself!

PATH="/usr/bin:/bin:/usr/sbin"
UMASK=007
COMSAT=no

XLOOP="$LOGNAME@$HOST"
AWAY_MESSAGE="/var/spool/mail/$LOGNAME.away.msg"
AWAY_CACHE="/var/spool/mail/$LOGNAME.away.cache"
AWAY_LOCK="/var/spool/mail/$LOGNAME.away.lock"

NL="
"


# Alias should set NAME and ADDRESS, if necessary...
NAME=""
ALIAS="$LOGNAME@"
INCLUDERC=$HOME/.alias

# Test whether the message is To and From the same address and whether the
# subject is "away message on" or "away message off"
:0
# ... addressed to a system user?
*$ ^TO_$\ALIAS
# ... and is From that same user?
*$ ^From:.*$\ALIAS
# ... and has Subject containing only "away message (on|off)" (allowing
# variable spacing)
*  ^Subject: *away message *\/o(ff|n) *$
*   MATCH ?? ()\/o(ff|n)
{
    ACTION=$MATCH
    LOG="ACTION: $ACTION, USER: $LOGNAME (ALIAS: $ALIAS)$NL"

    STATUS1=""
    STATUS2=""
    STATUS3="\
To re-enable the Away message, send an email From your account To the same\
account with the desired auto-responder message as the body text.$NL"


    :0

    *   ACTION ?? off
    {
        # Remove away message message and cache, then exit
        :0 cW : $AWAY_LOCK
        *$? /usr/bin/test -e $AWAY_MESSAGE
        | rm -f $AWAY_MESSAGE
        :0 a
        {
            STATUS1="Deleted file "$AWAY_MESSAGE" $NL"
        }
        :0 cW
        *$? /usr/bin/test -e $AWAY_CACHE
        | rm -f $AWAY_CACHE
        :0 a
        {
            STATUS2="Deleted file "$AWAY_CACHE" $NL"
        }
        :0 hWfi
        | formail -ci 'Subject: {PROCMAIL} Away message successfully disabled'
        :0 abWfi
        | echo "$STATUS1$STATUS2$NL$STATUS3"
    }
    # ... else ...
    :0
    *   ACTION ?? on
    {
        :0 bcWi : $AWAY_LOCK
        # Create Away Message message from existing body ...
        | tee $AWAY_MESSAGE
        # FIXME: Extra newline in output; no support for attachements?

        :0 ahWfi
        # Change the subject line ...
        | formail -ci 'Subject: {PROCMAIL} Away message successfully enabled'
        :0 abWfi
        # ... and the body
        | xargs -0 echo -e 'Away message now reads:\n'
        :0 EhWfi
        # It's all gone wrong!
        | formail -ci 'Subject: {PROCMAIL} Away message alteration FAILED'
        :0 abWfi
        | echo 'Saving the new Away message failed'
    }

    # As confirmation that the away message has worked the user will receive
    # the auto-reply from the next recipe...
}

:0
# Is there an away message for this user?
*$? /usr/bin/test -s $AWAY_MESSAGE
# Don't reply to lists...
*! ^Precedence: (bulk|list)$
{
    # From procmailex(5)
    :0 hcW : $AWAY_LOCK
    # Perform a quick check to see if the mail was addressed (To|Cc) us
    *$ ^TO_$\ALIAS
    # Don't reply to daemons and mailinglists
    *  !^FROM_DAEMON
    # Mail loops are evil!
    *$ !^X-Loop: $\XLOOP
    | formail -rD 8192 $AWAY_CACHE

    # If the Sender's name was not in the cache ...
    :0 ehc : $AWAY_LOCK
    | /usr/local/bin/procmail-deliver-alternate.sh $AWAY_MESSAGE
}

# ... and send everything else to Dovecot's 'deliver' for further filtering
# with sieve
:0
| /usr/libexec/dovecot/deliver -d "$LOGNAME"

(Why is ‘test‘ invoked with a full path? Because, for reasons I’m entirely unable to fathom, it fails and returns 1 (which is the totality of the feedback procmail provides) if invokes without a path. An issue with a shell built-in, perhaps?)

To override the GECOS name for a given user and specify a differnt From: address or to specify an external email address, create a ~/.alias file defining NAME and/or ALIAS.

The following file is only needed because procmail didn’t seem to want to interpolate variables in a pipeline… possibly a $SHELLMETA issue? In any case, this script is more flexible…

/usr/local/bin/procmail-deliver-alternate.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

MESSAGE="$1"
[[ -n "$MESSAGE" ]] || exit 2

# We should inherit procmail(1)'s environment...
[[ -n "$LOGNAME" ]] || exit 3
[[ -n "$HOST" ]] || exit 4
[[ -n "$XLOOP" ]] || XLOOP="$LOGNAME@$HOST"


[[ -r "$MESSAGE" ]] || exit 10
[[ -s "$MESSAGE" ]] || exit 11

[[ -x "$SENDMAIL" ]] || exit 20

perl -MMIME::QuotedPrint -e '' >/dev/null 2>&1 || exit 30

SCRIPT="$( basename "$0" | sed -r 's/\..{2,3}$//' )"

cat - | formail -rI"Precedence: junk" -I"From: $NAME < $ALIAS>" -A"X-Loop: $XLOOP" | cat - "$MESSAGE" | perl -MMIME::QuotedPrint -pe '$_ = MIME::QuotedPrint::decode( $_ );' | tee -a "/var/log/$SCRIPT.log" | $SENDMAIL -oi -t

exit $?

The strange exit-codes are to aid debugging as, even in verbose mode, procmail gives very little information about exec‘d programs.

Now, simply send an email from your account to the same account with a subject of ‘away message on‘ with the auto-reply message as the body, or with a subject of ‘away message off‘ – and you’ll now see a status message in response.