Automatically extract zipped attachments received on postfix mail account

Posted in |
The desire here is to call a Drupal path when email is received on a specific email account. The email in question contains a zipped archive of images that we want to import into Drupal.

Server Configuration

Configuration is Fedora 8 VPS, Drupal 6.6 and Postfix 2.4.5.

We want to email zipped images for processing by Drupal. Using a system account is a reasonable approach so we will eschew virtual email accounts.

see http://www.postfix.org/VIRTUAL_README.html

Postfix Config

Merely append the domain to receive email on to mydestination in /etc/postfix/main.cf

In this case the host is not the main domain mail server so we define a define a DNS MX record for the subdomain.

mydestination = $myhostname, localhost.$mydomain, localhost, subdom1.example.com, subdom2.example.com

This approach results in shared domains and UNIX system accounts. In other words email will be delivered to any system account in any of the domains listed in mydestination.

So if we create a system account called mypics then we can send email to mypics@$myhostname, mypics@subdom1.example.com and mypics@subdom2.example.com. All mail to these addresses will be delivered to the same myopic system account user.

Other much more sophisticated approaches can be provided for but this will suffice for our needs.

In order to have incoming messages directed to /home/mypics/Maildir/new we set configure home_mailbox in /etc/postfix/main.cf

# DELIVERY TO MAILBOX
#
# The home_mailbox parameter specifies the optional pathname of a  
# mailbox file relative to a user's home directory. The default 
# mailbox file is /var/spool/mail/user or /var/mail/user.  Specify
# "Maildir/" for qmail-style delivery (the / is required).
# 
#home_mailbox = Mailbox
home_mailbox = Maildir/

In this case we use the qmail-style delivery. Each message is stored in its own file. A message sent to mypics@subdom1.example.com should appear as a distinct file in /home/mypics/Maildir/new

Debug message reception using the Postfix log

tail -f /var/log/maillog

It should be noted that when Postfix local creates directories or files within /home/mypics it will do so using the local user system account rights i.e.: mypics. If we have placed restricted access rights on home/mypics then Postfix may fail to create the necessary directories of files. This failure will be logged to the Postfix log.

A consequence of this is that all messages written to /home/mypics/Maildir/new have file permissions of 600. It does not seem possible to umask or otherwise request Postfix that non owner access be granted to these resources.

Accessing Received Mail

Alias command usage: see page 39 and chapter 14 of Dent's - Postfix the Definitive Guide.

See Postfix aliases man page.

See Postfix local man page.

Mail is now being delivered into our myopic user home directory. One approach to processing the messages with Drupal might be to check the users mail dir whenever the cron runs. This sounds like it could work but we have to remember that only the mypics user has access to the message store.

A better solution is to use the optional Postfix command maps facility detailed here. Using this approach we can tell the Postfix local agent to hand mailbox processing off to another command on a per user basis. This command will execute with the permissions of the target user, hence it can access the message store directly if required. Otherwise it direct that the email be saved to another location or be directed to another process.

Add a mailbox_command_maps configuration parameter to main.cf

# apply alternative mailbox commands on a per user basis
mailbox_command_maps = hash:/etc/postfix/mailbox_commands
Edit /etc/postfix/mailbox_commands to run a command script whenever mail arrives for user mypics.
#
# mailbox command mappings
#
mypics /home/mypics/scripts/doimages.sh "$USER" "imageimport"
Build the mapping database.
postmap mailbox_commands
Compose /home/mypics/scripts/doimages.sh as below. Tail the Postfix log for issues. Message attachments are saved into msgdir_root. Any zip archives present are unzipped.
#!/bin/sh
#
# arguments:
#  $1 message recipient
#   $2 folder to save attachments to
#
# test with:
# doimages.sh username folder < message
#

#
# script scope vars
#
mydebug=1
mydelete=1
mydivider="*******************************************"

#
# This script will be called by postfix using the receiving user rights.
# This has an effective umask of 0177.
# We need group access so umask accordingly.
#
umask 0002

# message log
msglog="/home/websys/postfix-messages.log"

#
# function mylog
# 
# $1 log text
# $2 exit status
#
mylog()
{
	echo "$(date) ${1:-missing}" >> $msglog

	# exit with given status code
	if [ $# -ge "2" ]; then
		echo "quitting" >> $msglog
		exit $2
	fi
}

#log session
mylog "$mydivider"
 
# redirect stdout and std err to append to file (debug only)
if [ $mydebug -eq 0 ]; then
	exec >> $msglog 2>&1
fi

# validate parameters
if [ $# -ne "2" ]; then
	mylog "invalid parameter count: ${#}" 0
fi

mylog "message rxd for user $1"

# form message path
msgpath_root="/home/websys"
mylog "root path is ${msgpath_root}"
targetdir=$2
mylog "target folder is $targetdir"
msgpath="$msgpath_root/$targetdir"

#change to root folder
if ! cd $msgpath_root ; then mylog "cannot cd to ${msgpath_root}" 0 ; fi

#make target dir and intermediates if required
if ! mkdir -p $targetdir ; then mylog "cannot make dir ${targetdir}" 0 ; fi
if ! cd $targetdir ; then mylog "cannot access ${targetdir}" 0 ; fi

# make message dir using user name ($1), process id and unix time in seconds since 1970
# make intermediate directories if required
msgdir="postfix-msg-$1-$$-$(date +%s)"
if ! mkdir $msgdir ; then mylog "cannot make ${msgdir}" 0 ; fi
if ! cd $msgdir ; then mylog "cannot cd to ${msgdir}" 0 ; fi

#extend path
msgpath="$msgpath/$msgdir"
delpath=$msgpath	#delete path here

# read message from std in and write to file
msgfile="${msgpath}/message"
cat > $msgfile

# check	exit status of last command
if [ "$?" -ne "0" ]; then
    	mylog "could not read message from stdin to ${msgfile}" 0
fi

# decode all attachments encoded within the message
uudeview -i $msgfile 2>>$msglog

# check exit status of last command
if [ "$?" -ne "0" ]; then
    	mylog "error decoding attachments in ${msgfile}" 0
fi

# split on linebreak so that filenames with white space get handled correctly
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")

# iterate over dir and unzip any archives
for f in *.zip
do 
	mylog "unzipping ${f}" 
	unzip $f
	
	# check exit status of last command
	if [ "$?" -ne "0" ]; then
    	mylog "error unzipping attachments in ${msgfile}" 0
	fi
done
IFS=$SAVEIFS

# access image import URL to process images in message dir.
# the path may be accessed only by authenticated users hence the login script
# rather than calling the path directly 
importURL="http://www.example.com/login.php?user=admin&passwd=admin&path=import"
wget $importURL 2>>$msglog

# check	exit status of last command
if [ "$?" -ne "0" ]; then
    mylog "error occurred calling ${importURL}" 0
fi

# flag delete the message directory if required
#mydelete=0

# remove our message directory
if [ "$mydelete" -eq "0" ]; then

	cd $msgpath_root
	rm -r $delpath

	# check	exit status of last command
	if [ "$?" -ne "0" ]; then
    	mylog "dir not removed $delpath"
    else
    	mylog "dir removed $delpath"
	fi
fi

mylog "message import complete" 

# tell postfix all OK
exit 0
Postfix debugging is detailed here.
Submitted by Jonathan Mitchell on Fri, 03/26/2010 - 09:46

Post new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
Let us know you are human.
teleph_nist: