SES Email Forwading 2024
SES Email Forwading 2024
EC2 Menu
EC2 Menu
This is complicated stuff. The example below is in production. I have various versions of this.
SES Email Forwading 2024
As with all technical posts, things change over time. This was written in February 2024.
Amazon sunset Lambda Node16, so we lost functionality around email forwarding. The example here will use Node18.
The content below shows how to forward an email message to let one person know an email has arrived in an S3 bucket from SES.
After crontab processes the email, the same person receives it as an attachment, where the shell script uses “mutt” to send the file. Things like “Sendmail” do not work. This is not foolproof. An email notification may also relate to a junk email.
I developed scripts to deal with various spams, bad domain names, blocking etc. but it became highly complex and at times a little faulty. As a result, I am only providing a simplified version of scripting.
index.mjs Lambda function example
Here is the code to forward a notification: (I use us-west-2 for Oregon. Use your own email address and subject Text message.)
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses"; const ses = new SESClient({ region: "us-west-2" }); export const handler = async(event) => { const command = new SendEmailCommand({ Destination: { ToAddresses: ["YOUR_EMAIL@gmail.com"], }, Message: { Body: { Text: { Data: "Email has arrived for YOU" }, }, Subject: { Data: "New Email SES" }, }, Source: "YOUR_EMAIL@gmail.com", }); try { let response = await ses.send(command); // process data. return response; } catch (error) { // error handling. } finally { // finally. } };
SES default_rule_set
You need to know how to add an SES rule that identifies inbound emails, places them into an S3 bucket, and runs the Lambda function to notify you. I have other articles on this. The bucket needs specific permissions. This also involves use of IAM. The domain name will need to have been verified in SES as well.
An example of bucket permissions (user your own account and bucket details). The bucket has to be set up before adding the SES rule, as the rule will want to add some further permissions to access the bucket.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowSESPuts-1674865967951", "Effect": "Allow", "Principal": { "Service": "ses.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::MY_EMAIL_BUCKET/*", "Condition": { "StringEquals": { "AWS:SourceAccount": "MY_AWS_ACCOUNT_NUMBER" }, "StringLike": { "AWS:SourceArn": "arn:aws:ses:*" } } } ] }
A shell script to process the bucket files
This is a simplified version of my scripting.
Where you see “^M” you must replace with CTRLVM to get the special end of line character. THis was not needed in Linux2, but is needed in Linux2023.
The script looks at configuration files under you own /data directory.
cd /home/ec2-user vi email.sh #!/bin/sh # email-ses.sh FORWARD S3 BUCKET EMAILS via POSTIX and mutt # Crontab root entry example: * * * * * /home/ec2-user/email-ses.sh domain.com.inbox fred domain.com >/dev/null 2>&1 # S3 bucket minimum subfolders: .ses, .archive, .dmarc, .other IFS=$'\n' bucket="$1" user="$2" domain="$3" work="/data/ses/$domain/$user/workdir" out="/data/ses/$domain/$user/outdir" dest_register="/data/ses/$domain/$user/register.csv" adir="/data/ses/$domain/$user/archivedir" dest_block="/data/ses/$domain/$user/register_block.csv" bfile="/data/ses/block.dat" dfile="/data/ses/domain.dat" ffile="/data/ses/$domain/$user/forward.dat" ifile="/data/ses/ip.dat" lfile="/data/ses/language.dat" sfile="/data/ses/spam.dat" cfile="/data/ses/$domain/$user/content.dat" unset IFS pf="" pf=`sudo ps -ef|grep "postfix/master"|awk '{print $9}'|head -1` if [ "$pf" = "-w" ] ; then : else /usr/bin/systemctl start postfix fi for sets in `cat /data/ses/$domain/$user/sets.dat` do content="" content=`grep $sets $cfile` if [ "$content" != "" ] ; then content="y" fi cd $work aws s3 sync s3://$bucket/$sets/ $work if [ -f "$work/AMAZON_SES_SETUP_NOTIFICATION" ] ; then aws s3 rm s3://$bucket/$sets/AMAZON_SES_SETUP_NOTIFICATION rm -f $work/AMAZON_SES_SETUP_NOTIFICATION fi lf="" lf=`ls *` if [ "$lf" != "" ] ; then let fcount=0 for file in `ls *` do let fcount=$fcount+1 earray[$fcount]="$file" size="" time="" day="" sta="" sta=`stat $file | grep "Access:"|tail -1` day=`echo $sta | grep "Access:"|tail -1|awk '{print $2}'|awk -F- '{print $1,$2,$3}'` time=`echo $sta | grep "Access:"|tail -1|awk -F. '{print $1}'|awk '{print $NF}'` day_array[$fcount]="$day" time_array[$fcount]="$time" size=`stat $file | grep "Size:"|head -1|awk '{print $2}'` size_array[$fcount]="$size" chmod 777 $file chgrp ec2-user $file done fi dmarc_function() { m="" m=`grep "Subject: " $work/$efile | grep -i "Report Domain"` if [ "$m" != "" ] ; then aws s3 cp $work/$efile s3://$bucket/.other/$efile.eml dmarc_ret=1 fi return "$dmarc_ret" } fail_function() { flag="" o="" o=`head -15 $work/$efile|grep -i "Received-SPF: pass"| awk '{print $1,$2}'` if [ "$o" = "" ] ; then block_ret=1 fi o="" o=`grep "X-SES-Spam-Verdict:" $work/$efile` p="" p=`grep "X-SES-Virus-Verdict:" $work/$efile` if [ "$o" != "" ] ; then k="" k=`echo $o|grep PASS` if [ "$k" = "" ] ; then block_ret=1 fi fi if [ "$p" != "" ] ; then k="" k=`echo $p|grep PASS` if [ "$k" = "" ] ; then block_ret=1 fi fi return "$block_ret" } block_function() { IFS=$'\n' for x in `cat $bfile` do y="" y=`grep "From: " $work/$efile|grep -v "Date:" | grep -v Message-ID | head -1 | grep -i "$x"` if [ "$y" != "" ] ; then block_ret=2 fi done return "$block_ret" } ip_function() { IFS=$'\n' for ip in `cat $ifile` do ipf="" ipf=`echo $ip|sed 's/\./\\\./g'` ipa="" ipa=`grep "client-ip=" $work/$efile | grep -E $ipf | head -1` if [ "$ipa" != "" ] ; then block_ret=3 fi done return "$block_ret" } domain_function() { IFS=$'\n' y="";yy="" yy=`grep "From: " $work/$efile|grep -v "Date:"|grep -v Message-ID|head -1|awk -F@ '{print $NF}'|awk -F. '{print $NF}'|tr "<" " "|tr ">" " "` y=`echo $yy|awk '{print $1}'` for x in `cat $dfile` do j=`echo $x|awk -F\. '{print $NF}'|awk '{print $1}'` if [ "$y" = "$j" ] ; then block_ret=4 fi done return "$block_ret" } forward_email() { day=""; time=""; size="";from="";to="";subject="" day=${day_array[$email]} time=${time_array[$email]} size=${size_array[$email]} from=`grep "From: " $work/$efile|awk '{print $NF}'|sed 's/^M//g'` to=`grep "To:[ ]" $work/$efile|sed 's/,/-/g'|awk -FTo: '{print $NF}'|sed 's/^M//g'` subject=`grep "Subject: " $work/$efile|sed 's/,/-/g'|awk -F: '{print $NF}'|awk '{$1=$1}1'` echo $day , $time , $size , $efile, $from, $to, $subject >> $dest_register IFS=$'\n' gr="\\s\\"$sets for l in `cat $ffile| grep "$gr"` do if [ "$l" != "" ] ; then forward=`echo $l|awk '{print $1}'` # /usr/sbin/sendmail -f $forward $forward < $work/$efile echo "An e-mail attachment has arrived from Amazon SES for domain $domain" | EMAIL="mail $domain <admin@$domain>" /usr/bin/mutt -s "New e-mail from $domain" $forward -a $out/$efile.eml aws s3 cp $adir/$efile.eml s3://$bucket/.archive/$efile.eml else aws s3 cp $adir/$efile.eml s3://$bucket/.archive/$efile.eml fi done } block_email() { day=""; time=""; size="";from="";to="";subject="" day=${day_array[$email]} time=${time_array[$email]} size=${size_array[$email]} from=`grep "From: " $work/$efile|awk '{print $NF}'|sed 's/^M//g'` to=`grep "To:[ ]" $work/$efile|sed 's/,/-/g'|awk -FTo: '{print $NF}'|sed 's/^M//g'` subject=`grep "Subject: " $work/$efile|sed 's/,/-/g'|awk -F: '{print $NF}'|sed 's/^M//g'` echo $day , $time , $size , $efile, $from, $to, $subject >> $dest_block aws s3 cp $work/$efile s3://$bucket/.other/$efile.eml echo "A BLOCKED e-mail attachment has arrived from Amazon SES for domain $domain" | EMAIL="mail $domain <admin@$domain>" /usr/bin/mutt -s "A BLOCKED e-mail from $domain" $forward -a $out/$efile.eml } content_header() { cat $work/$efile | sed '0,/Subject: /{s//Subject: [content advice] /}' | sed 's/^M//g' > $work/$efile.tmp mv $work/$efile.tmp $work/$efile } archive_header() { cat $work/$efile | sed '0,/Subject: /{s//Subject: [archive] /}' | sed 's/^M//g' > $adir/$efile.tmp mv $adir/$efile.tmp $adir/$efile.eml } click_function() { m="" m=`grep -i "click" $work/$efile|grep href|head -1` if [ "$m" != "" ] ; then content_ret=2 fi return "$content_ret" } language_function() { IFS=$'\n' for v in `cat $lfile` do w="" w=`grep -w -i "$v|$v[[:space:]]|$v[[:space:]]\|$v\|\b$v\|$v\b" $work/$efile |grep -v -i "$v="|grep -v -i "=$v"|grep -v -i "$v/"|grep -v -i "/$v"|grep -v -i "+$v"|grep -v -i "$v+"|grep -v -i '\-$v'|grep -v -i '$v-'` if [ "$w" != "" ] ; then content_ret=1 fi done return "$content_ret" } spam_function() { IFS=$'\n' for x in `cat $sfile` do y="" y=`grep -i "$x" $work/$efile` if [ "$y" != "" ] ; then content_ret=3 fi done return "$content_ret" } reset_function() { dmarc_ret=0 block_ret=0 content_ret=0 } # START PROGRAM dmarc_ret=0 block_ret=0 content_ret=0 # FOR LOOP if [ "$lf" != "" ] ; then let email=0 for efile in ${earray[*]} do let email=$email+1 cp $efile $out/$efile.eml chmod 777 $out/$efile.eml chgrp ec2-user $out/$efile.eml dmarc_function dmarc_ret=$? # uncomment block function to block specific bad externaladdresses - see block.dat # block_function # fail function is for some spf dmarc failures fail_function # ip function does not really work # ip_function # domain function to block .biz, .info and so on domain_function block_ret=$? # If you want to test content and insert [content advice] into the subject line # language_function content_ret=$? # spam_function content_ret=$? # click_function content_ret=$? proc="" if [ "$dmarc_ret" = "0" ] ; then : else proc="dmarc" fi if [ "$block_ret" = "1" ] || [ "$block_ret" = "2" ] || [ "$block_ret" = "3" ] || [ "$block_ret" = "4" ] ; then proc="block" block_email else : fi archive_header if [ "$content_ret" = "1" ] || [ "$content_ret" = "2" ] || [ "$content_ret" = "3" ] ; then if [ "$content" = "y" ] ; then content_header fi fi if [ "$proc" = "" ] ; then forward_email proc="email" fi if [ "$work" != "" ] ; then if [ "$efile" != "" ] ; then if [ -f $work/$efile ] ; then rm -f $work/$efile aws s3 rm s3://$bucket/$sets/$efile fi if [ -f $adir/$efile.eml ] ; then rm -f $adir/$efile.eml fi if [ -f $out/$efile.eml ] ; then rm -f $out/$efile.eml fi fi fi reset_function done # lf not null fi # end sets done # systemctl stop postfix - if this is required as a precaution exit [save and exit]
The script is run with the command:
[crontab run every 30 minutes in this example] 30 * * * * /home/ec2-user/email.sh YOUR_S3_BUCKET YOUR_SUBDIRECTORY_USER YOUR_SUBDIRECTORY >/dev/null 2>&1 [For example:] /home/ec2-user/email.sh mydomain.com.inbox fred mydomain.com
We would then have these directories on Linux: (all your own naming choice)
/data/ses/mydomian.com/fred
The ses directory has some basic configuration files, which for simplification are:
cd / mkdir data chmod 2775 data cd data mkdir ses chmod 2775 ses cd ses mkdir mydomain.com chmod 2775 mydomain.com touch block.dat touch domain.dat touch ip.dat touch language.dat touch spam.dat chmod 777 *.dat [As I have not refined this work, simply create the above files. You can put specific email addresses you want to block in block.dat without any arrows. e.g., you coud block bad.person@baddomain.com] cd mydomain.com mkdir fred chmod 2775 fred cd fred mkdir archivedir workdir outdir vi content.dat .ses [save and exit - this was originally intended to alert to bad content in the email. I still need it to run the script regarldess.] vi sets.dat .ses .set [save and exit] vi forward.dat [If you include .set, it means you have an SES rule to place specific emails such as john@gmail.com or julie@gmail.com to the S3 bucket undr the subfolder .set. They will both receive the emails. If you only want emails to yourself, such as me@, dmarc@, admin@, webmaster@ and so forth, these can be identified by SES and placed into the .ses bucket subfolder. In the follwing forward.dat file, you could have:] me@gmail.com .ses [If you have the .set subfoldre receiving emails, you could send a copy to yourself and others like this:] me@gmail.com .set john@gmail.com .set julie@gmail.com .set [save and exit] [Under your email bucket, such as mydomain.com.inbox, create the subfolders:] .ses .set .archive .other [You can put life cycles on these if you wish. Emails are archived as .eml format under .other or .archive. You can manually download archived files. I keep archives for up to 365 days. Cyberduch can also be used to access.] [We also have a register.csv file to record some details. You can create a softlink from /var/www/html to /data in order to download the file to you PC, using a password protected webpage.] touch register.csv touch register_block.csv chmod 777 * [Your SES rules should catch at least the following for the .ses subfolder:] dmarc@mydomain.com admin@ webmaster@ postmaster@
[and others you like, such as ME@, contact@ etc.]
[and the .set SES rule should catch people you want to forward copies to, such as:]
contact@
mail@
Your Content Goes Here