Além da excelente resposta da @Thomas Ward AWS ter uma maneira "preferida", que é muito semelhante, a única diferença é que ela usa ferramentas internas da AWS para realizar a tarefa em vez de um script python externo.
Há uma diferença fundamental entre essa abordagem e a outra, essa abordagem fará a varredura de vírus / malware e as verificações de DKIM e SPF que você pode testar e ver se eles PASS
.
Então, eu segui o README
do repositório do GitHub aqui: link
Tudo é por causa desse script. Você o coloca no AWS Lambda e processará os emails das regras do SES.
Aqui está uma cópia da parte de configuração do README
:
Nota : altere as coisas como S3-BUCKET-NAME
.
Modify the values in the config
object at the top of index.js
to specify the S3 bucket and object prefix for locating emails stored
by SES. Also provide the email forwarding mapping from original
destinations to new destination.
-
In AWS Lambda, add a new function and skip selecting a blueprint.
Name the function "SesForwarder" and optionally give it a description. Ensure Runtime is set to Node.js 4.3 or 6.10.
For the Lambda function code, either copy and paste the contents of index.js
into the inline code editor or zip the contents of the
repository and upload them directly or via S3.
Ensure Handler is set to index.handler
.
For Role, choose "Basic Execution Role" under Create New Role. In the popup, give the role a name (e.g., LambdaSesForwarder). Configure
the role policy to the following: {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": "ses:SendRawEmail",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::S3-BUCKET-NAME/*"
}
] }
Memory can be left at 128 MB, but set Timeout to 10 seconds to be safe. The task usually takes about 30 MB and a few seconds. After
testing the task, you may be able to reduce the Timeout limit.
In AWS SES, verify the domains for which you want to receive and forward email. Also configure the DNS MX record for these domains to
point to the email receiving (or inbound) SES endpoint. See SES
documentation
for the email receiving endpoints in each region.
If you have the sandbox level of access to SES, then also verify any email addresses to which you want to forward email that are not on
verified domains.
If you have not configured inbound email handling, create a new Rule Set. Otherwise, you can use an existing one.
-
Create a rule for handling email forwarding functionality.
On the Recipients configuration page, add any email addresses from which you want to forward email.
On the Actions configuration page, add an S3 action first and then an Lambda action.
For the S3 action: Create or choose an existing S3 bucket. Optionally, add an object key prefix. Leave Encrypt Message unchecked
and SNS Topic set to [none].
For the Lambda action: Choose the SesForwarder Lambda function. Leave Invocation Type set to Event and SNS Topic set to [none].
Finish by naming the rule, ensuring it's enabled and that spam and virus checking are used.
If you get an error like "Could not write to bucket", follow step 7 before completing this one
If you are asked for SES to attempt to add permissions to access lambda:InvokeFunction, agree to it.
The S3 bucket policy needs to be configured so that your IAM user has read and write access to the S3 bucket. When you set up the S3
action in SES, it may add a bucket policy statement that denies all
users other than root access to get objects. This causes access issues
from the Lambda script, so you will likely need to adjust the bucket
policy statement with one like this: {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GiveSESPermissionToWriteEmail",
"Effect": "Allow",
"Principal": {
"Service": "ses.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::S3-BUCKET-NAME/*",
"Condition": {
"StringEquals": {
"aws:Referer": "AWS-ACCOUNT-ID"
}
}
}
] }
Optionally set the S3 lifecycle for this bucket to delete/expire objects after a few days to clean up the saved emails.
Estou postando uma versão do script no momento da criação desta resposta com uma ou duas alterações.
Percebi que os e-mails que foram roteados duas vezes pelos domínios verificados foram alterados por esse script, por isso, para uma aparência incrível, corrigi-o
"use strict";
var AWS = require('aws-sdk');
console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.2.0");
// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
//
// Expected keys/values:
//
// - fromEmail: Forwarded emails will come from this verified address
//
// - subjectPrefix: Forwarded emails subject will contain this prefix
//
// - emailBucket: S3 bucket name where SES stores emails.
//
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
// trailing slash.
//
// - forwardMapping: Object where the key is the lowercase email address from
// which to forward and the value is an array of email addresses to which to
// send the message.
//
// To match all email addresses on a domain, use a key without the name part
// of an email address before the "at" symbol (i.e. '@example.com').
//
// To match a mailbox name on all domains, use a key without the "at" symbol
// and domain part of an email address (i.e. 'info').
var defaultConfig = {
fromEmail: "",
subjectPrefix: "",
emailBucket: "ses-sammaye",
emailKeyPrefix: "email/",
forwardMapping: {
"@vvv.com": [
"[email protected]"
],
"@fff.com": [
"[email protected]"
],
"@ggg.com": [
"[email protected]"
],
},
verifiedDomains: [
'vvv.com',
'fff.com',
'ggg.com'
]
};
/**
* Parses the SES event record provided for the 'mail' and 'receipients' data.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.parseEvent = function(data) {
// Validate characteristics of a SES event record.
if (!data.event ||
!data.event.hasOwnProperty('Records') ||
data.event.Records.length !== 1 ||
!data.event.Records[0].hasOwnProperty('eventSource') ||
data.event.Records[0].eventSource !== 'aws:ses' ||
data.event.Records[0].eventVersion !== '1.0') {
data.log({message: "parseEvent() received invalid SES message:",
level: "error", event: JSON.stringify(data.event)});
return Promise.reject(new Error('Error: Received invalid SES message.'));
}
data.email = data.event.Records[0].ses.mail;
data.recipients = data.event.Records[0].ses.receipt.recipients;
return Promise.resolve(data);
};
/**
* Transforms the original recipients to the desired forwarded destinations.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.transformRecipients = function(data) {
var newRecipients = [];
data.originalRecipients = data.recipients;
data.recipients.forEach(function(origEmail) {
var origEmailKey = origEmail.toLowerCase();
if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailKey]);
data.originalRecipient = origEmail;
} else {
var origEmailDomain;
var origEmailUser;
var pos = origEmailKey.lastIndexOf("@");
if (pos === -1) {
origEmailUser = origEmailKey;
} else {
origEmailDomain = origEmailKey.slice(pos);
origEmailUser = origEmailKey.slice(0, pos);
}
if (origEmailDomain &&
data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailDomain]);
data.originalRecipient = origEmail;
} else if (origEmailUser &&
data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailUser]);
data.originalRecipient = origEmail;
}
}
});
if (!newRecipients.length) {
data.log({message: "Finishing process. No new recipients found for " +
"original destinations: " + data.originalRecipients.join(", "),
level: "info"});
return data.callback();
}
data.recipients = newRecipients;
return Promise.resolve(data);
};
/**
* Fetches the message data from S3.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.fetchMessage = function(data) {
// Copying email object to ensure read permission
data.log({level: "info", message: "Fetching email at s3://" +
data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId});
return new Promise(function(resolve, reject) {
data.s3.copyObject({
Bucket: data.config.emailBucket,
CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId,
Key: data.config.emailKeyPrefix + data.email.messageId,
ACL: 'private',
ContentType: 'text/plain',
StorageClass: 'STANDARD'
}, function(err) {
if (err) {
data.log({level: "error", message: "copyObject() returned error:",
error: err, stack: err.stack});
return reject(
new Error("Error: Could not make readable copy of email."));
}
// Load the raw email from S3
data.s3.getObject({
Bucket: data.config.emailBucket,
Key: data.config.emailKeyPrefix + data.email.messageId
}, function(err, result) {
if (err) {
data.log({level: "error", message: "getObject() returned error:",
error: err, stack: err.stack});
return reject(
new Error("Error: Failed to load message body from S3."));
}
data.emailData = result.Body.toString();
return resolve(data);
});
});
});
};
/**
* Processes the message data, making updates to recipients and other headers
* before forwarding message.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.processMessage = function(data) {
var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
var header = match && match[1] ? match[1] : data.emailData;
var body = match && match[2] ? match[2] : '';
// Add "Reply-To:" with the "From" address if it doesn't already exists
if (!/^Reply-To: /mi.test(header)) {
match = header.match(/^From: (.*(?:\r?\n\s+.*)*\r?\n)/m);
var from = match && match[1] ? match[1] : '';
if (from) {
header = header + 'Reply-To: ' + from;
data.log({level: "info", message: "Added Reply-To address of: " + from});
} else {
data.log({level: "info", message: "Reply-To address not added because " +
"From address was not properly extracted."});
}
}
// SES does not allow sending messages from an unverified address,
// so replace the message's "From:" header with the original
// recipient (which is a verified domain)
header = header.replace(
/^From: (.*(?:\r?\n\s+.*)*)/mg,
function(match, from) {
var fromText;
var fromEmailDomain = from.replace(/(.*)</, '').replace(/.*@/, "").replace('>', '').trim();
if (data.config.verifiedDomains.indexOf(fromEmailDomain) === -1) {
if (data.config.fromEmail) {
fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
' <' + data.config.fromEmail + '>';
} else {
fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
' <' + data.originalRecipient + '>';
}
} else {
fromText = 'From: ' + from;
}
return fromText;
});
// Add a prefix to the Subject
if (data.config.subjectPrefix) {
header = header.replace(
/^Subject: (.*)/mg,
function(match, subject) {
return 'Subject: ' + data.config.subjectPrefix + subject;
});
}
// Replace original 'To' header with a manually defined one
if (data.config.toEmail) {
header = header.replace(/^To: (.*)/mg, () => 'To: ' + data.config.toEmail);
}
// Remove the Return-Path header.
header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');
// Remove Sender header.
header = header.replace(/^Sender: (.*)\r?\n/mg, '');
// Remove Message-ID header.
header = header.replace(/^Message-ID: (.*)\r?\n/mig, '');
// Remove all DKIM-Signature headers to prevent triggering an
// "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
// These signatures will likely be invalid anyways, since the From
// header was modified.
header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');
data.emailData = header + body;
return Promise.resolve(data);
};
/**
* Send email using the SES sendRawEmail command.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.sendMessage = function(data) {
var params = {
Destinations: data.recipients,
Source: data.originalRecipient,
RawMessage: {
Data: data.emailData
}
};
data.log({level: "info", message: "sendMessage: Sending email via SES. " +
"Original recipients: " + data.originalRecipients.join(", ") +
". Transformed recipients: " + data.recipients.join(", ") + "."});
return new Promise(function(resolve, reject) {
data.ses.sendRawEmail(params, function(err, result) {
if (err) {
data.log({level: "error", message: "sendRawEmail() returned error.",
error: err, stack: err.stack});
return reject(new Error('Error: Email sending failed.'));
}
data.log({level: "info", message: "sendRawEmail() successful.",
result: result});
resolve(data);
});
});
};
/**
* Handler function to be invoked by AWS Lambda with an inbound SES email as
* the event.
*
* @param {object} event - Lambda event from inbound email received by AWS SES.
* @param {object} context - Lambda context object.
* @param {object} callback - Lambda callback object.
* @param {object} overrides - Overrides for the default data, including the
* configuration, SES object, and S3 object.
*/
exports.handler = function(event, context, callback, overrides) {
var steps = overrides && overrides.steps ? overrides.steps :
[
exports.parseEvent,
exports.transformRecipients,
exports.fetchMessage,
exports.processMessage,
exports.sendMessage
];
var data = {
event: event,
callback: callback,
context: context,
config: overrides && overrides.config ? overrides.config : defaultConfig,
log: overrides && overrides.log ? overrides.log : console.log,
ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(),
s3: overrides && overrides.s3 ?
overrides.s3 : new AWS.S3({signatureVersion: 'v4'})
};
Promise.series(steps, data)
.then(function(data) {
data.log({level: "info", message: "Process finished successfully."});
return data.callback();
})
.catch(function(err) {
data.log({level: "error", message: "Step returned error: " + err.message,
error: err, stack: err.stack});
return data.callback(new Error("Error: Step returned error."));
});
};
Promise.series = function(promises, initValue) {
return promises.reduce(function(chain, promise) {
if (typeof promise !== 'function') {
return Promise.reject(new Error("Error: Invalid promise item: " +
promise));
}
return chain.then(promise);
}, Promise.resolve(initValue));
};