EC2インスタンスでPostfixサーバーを実行しています。すべてのメールをSES経由で個人の受信トレイに転送したいと思います。
問題:AWSはAWSコンソールで確認されたFROMアドレスのみを許可し、この場合のFROMアドレスはTwitter.comのように何でもかまいません。サーバーのIPをホワイトリストに登録して、「送信者に関係なく、この場所からのすべての電子メールを受け入れる」と言うことはできません(とにかく悪い考えです)
そのため、確認済みのアドレスでメールを転送する方法を見つける必要がありますが、元の送信者のアドレスを失いたくありません。
これを行う方法はありますか?
チャットでの議論に基づいて、「FROM」アドレスを期待どおりに変更し、元の宛先ポイントに配信するが「Reply-To」を追加する、ハックのカスタマイズされたソリューションを提供します。ヘッダ。
これは非常にハックなアプローチですが、shouldメッセージを期待どおりに操作してから、PostFixを介してメッセージを必要な場所に実際に送信します。
まず、PostFixポートを変更する必要があります。 PostfixSMTPポートを25
以外に変更して、設定するpythonSMTPハンドラーが代わりにそのポートで機能するようにする必要があります。
/etc/postfix/master.cf
を編集します。次のような行を探すことになります。
smtp inet n - y - - smtpd
この行をコメントアウトし、その行の下で、代わりにこれを使用します。
10025 inet n - y - - smtpd
これはPostfixに標準のSMTPポートでリッスンさせたくないことを伝えます。この手順が完了したら、Postfixサービスを再起動します。
次に、前述のPythonSMTPハンドラー。これにより、着信メッセージが処理され、操作されて、システムのPostFixに再送信されます。もちろん、すべてのメールがローカルであってもポート25で送信されると仮定します。
このコードは GitHub Gist に存在し、どこかで入手した一般的なPython SMTPサーバーのコード例に基づいています(ただし、どこから申し訳ないか覚えていません!)。操作されました。
コードもここにあります。興味がある場合はPython 3にあり、ターゲットPythonバージョンとしてPython3で記述されています。
#!/usr/bin/env python3
# Libraries
import smtplib
import smtpd
import asyncore
import email
import sys
from datetime import datetime
print('Starting custom mail handling server...')
# We need to know where the SMTP server is heh.
SMTP_OUTBOUND = 'localhost'
# We also need to know what we want the "FROM" address to be
FROM_ADDR = "[email protected]"
DESTINATION_ADDRESS = "[email protected]"
#############
#############
# SMTP SERVER
#############
#############
# noinspection PyMissingTypeHints,PyBroadException
class AutoForwardHandlerSMTP(smtpd.SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
print('MESSAGE RECEIVED - [%s]' % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
print('Receiving message from:', peer)
print('Message addressed from:', mailfrom)
print('Message addressed to :', rcpttos)
print('Message length :', len(data))
print(data)
# Flush the output buffered (handy with the Nohup launch)
sys.stdout.flush()
# Analyze and extract data headers
msg = email.message_from_string(data)
orig_from = ''
try:
orig_from = msg['From']
msg['Reply-To'] = orig_from
# We have to use 'replace header' methods to overwrite existing headers.
msg.replace_header("From", FROM_ADDR)
except:
print("Error manipulating headers:", sys.exc_info()[0])
conn = smtplib.SMTP(SMTP_OUTBOUND, 10025)
conn.sendmail(FROM_ADDR, msg["To"], msg.as_string())
# Flush the output buffered (handy with the Nohup launch)
print("\n\n")
sys.stdout.flush()
return
# Listen to port 25 ( 0.0.0.0 can be replaced by the ip of your server but that will work with 0.0.0.0 )
server = AutoForwardHandlerSMTP(('0.0.0.0', 25), None)
# Wait for incoming emails
asyncore.loop()
これを/opt/PythonAutoForwarderSMTP.py
、またはあなたがそれを呼びたいものとして保存してください。以下をルートとして(Sudo
を介して、またはroot
ユーザープロンプトにドロップして)実行し、期待どおりに機能することを確認します。
python3 /opt/PythonAutoForwarderSMTP.py
実行が確認されたら、サーバーを介してメールを送信します。これを取得して、メッセージが受信および処理されたというこのスクリプトからのログデータを提供する必要があります。また、Postfixのログに接続が表示され、これはPostfixの後のどこかに配信されます。これがすべて問題ないように見え、メッセージを適切に処理し、メールメッセージが最終的に終了する場所に異なる「差出人」アドレスで表示された場合は、今すぐ自動起動するように作業できます。 (あなたは単に打つことができます Ctrl + C 続行する前に、pythonプロセスを終了します)。
起動時に開始する場合は、そのように設定する必要があります。
root
として、crontab -e
を実行し、以下をroot
crontabに追加します。
@reboot /usr/bin/python3 /opt/PythonAutoForwarderSMTP.py 2>&1 >> /var/log/PythonSMTP.log &
Crontabファイルを保存します。サーバーを再起動したくない場合は、追加したコマンドラインから@reboot
部分を除いたものを実行して、PythonSMTPハンドラーを実行します。
cron
によって実行されるかどうかに関係なく、Pythonをロードするプロセスは最終的にバックグラウンドにフォークされ、すべてのデータ出力(エラーまたはその他の方法でPythonに配置されます) _ console)を追加モードの/var/log/PythonSMTP.log
のログファイルに追加します。そうすれば、必要に応じていつでもログを取得できます。
すべてが期待どおりに機能する場合、これによりReply-Toヘッダーが適切に追加され、メッセージの「From」ヘッダーが期待どおりになるように調整されます。これを保証することはできません。メッセージが署名されている場合、SPFおよびDKIMチェックでは正しく機能しますが、Postfixを使用してメッセージを他の場所に中継する前に、メッセージを適切に「前処理」すると言えます。
義務的なセキュリティ上の懸念と機能変更の通知:
- 送信者のDKIM検証が失敗する可能性があります。署名されたメッセージが操作されるたびにDKIM署名の検証が失敗します。つまり、送信者からのDKIM署名が壊れている可能性があります。つまり、mightは、署名の検証に失敗したためにスパムとして取り上げられます。このスクリプトはおそらく「正しく機能する」ようにカスタマイズできますが、DKIM/SPFチェックを行うためにこれを作成したわけではありません。
- このPythonSMTPサーバーを
root
として実行する必要があります。 Linuxでは、デフォルトでは、スーパーユーザーでない限り1024未満のポートにバインドできないため、これが必要です。これが、Postfixがマスター「root」所有のプロセスを持ち、ポートバインドするためだけに非常に長い間rootユーザーとして実行されないサブプロセスを実行する理由です。- ポート25のすべてのメールは、最終的にこのPythonSMTPサーバーを通過します。 Postfixがoutside-> inからのメールも処理する場合、PythonSMTPサーバーが代わりになります。これはいくつかの悪をもたらす可能性がありますが、最終的にはあなたが求めていることを行います。
- これは壊れやすいソリューションです。他のいくつかのソリューションほど壊れやすいわけではありませんが、Pythonプロセスが停止すると、自動的に復帰しないため、エラーを処理する必要があります。ケースバイケースで、完全に停止した場合はPythonプロセスを復活させることがあります。
- これにはStartTLSまたはSSL/TLSハンドラーはありませんがあります。したがって、すべてがプレーンテキストです(これは安全ではありません!)
いつものように、何をしているのかわからない限り、rootとして何も実行しないでください。この場合、このスクリプトが何をするのかを自分で識別できるように、このコードをプレーンビューで提供します。そして、rootとして実行するかどうかにかかわらず、セキュリティ中心で、私のように偏執的である場合(私は、ITセキュリティの専門家であり、システム管理者でもあるため、これらの義務的な通知を許してください)
@Thomas Wardによる優れた回答と同様に、AWSには「優先」方法があります。これは非常によく似ていますが、唯一の違いは、外部のpythonスクリプトの代わりにAWS内部ツールを使用してタスクを実行することです。 。
このアプローチと他のアプローチには重要な違いが1つあります。このアプローチでは、ウイルス/マルウェアのスキャンとDKIMおよびSPFのチェックが行われ、実際にテストしてPASS
かどうかを確認できます。
だから、私はこのGitHubリポジトリのREADME
をここでフォローしました: https://github.com/arithmetric/aws-lambda-ses-forwarder
すべてがこのスクリプトによるものです。 AWS Lambdaに配置すると、SESルールのメールが後処理されます。
README
のセットアップ部分のコピーは次のとおりです。
注:S3-BUCKET-NAME
などを変更します。
index.js
の上部にあるconfig
オブジェクトの値を変更して、SESによって保存されたメールを検索するためのS3バケットとオブジェクトプレフィックスを指定します。また、元の宛先から新しい宛先への電子メール転送マッピングを提供します。AWS Lambdaで、新しい関数を追加し、ブループリントの選択をスキップします。
関数に「SesForwarder」という名前を付け、オプションで説明を付けます。ランタイムがNode.js4.3または6.10に設定されていることを確認します。
Lambda関数コードの場合、
index.js
の内容をコピーしてインラインコードエディターに貼り付けるか、リポジトリの内容をZipして直接、またはS3経由でアップロードします。ハンドラーが
index.handler
に設定されていることを確認します。[役割]で、[新しい役割の作成]の下の[基本実行役割]を選択します。ポップアップで、役割に名前を付けます(例:LambdaSesForwarder)。役割ポリシーを次のように構成します。
{ "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/*" } ] }
メモリは128MBのままにしておくことができますが、安全のためにタイムアウトを10秒に設定してください。このタスクには通常、約30MBと数秒かかります。タスクをテストした後、タイムアウト制限を減らすことができる場合があります。
AWS SESで、メールを受信して転送するドメインを確認します。また、これらのドメインのDNS MXレコードを、電子メール受信(または受信)SESエンドポイントを指すように構成します。各地域の電子メール受信エンドポイントについては、 SESドキュメント を参照してください。
SESへのサンドボックスレベルのアクセス権がある場合は、確認済みのドメインにないメールの転送先のメールアドレスも確認してください。
受信メールの処理を構成していない場合は、新しいルールセットを作成します。それ以外の場合は、既存のものを使用できます。
メール転送機能を処理するためのルールを作成します。
[受信者の構成]ページで、電子メールの転送元の電子メールアドレスを追加します。
[アクション設定]ページで、最初にS3アクションを追加し、次にLambdaアクションを追加します。
S3アクションの場合:既存のS3バケットを作成または選択します。必要に応じて、オブジェクトキープレフィックスを追加します。 [メッセージの暗号化]をオフのままにし、SNSトピックを[なし]に設定します。
Lambdaアクションの場合:SesForwarderLambda関数を選択します。 Invocation TypeをEventに設定し、SNSTopicを[none]に設定したままにします。
ルールに名前を付けて終了し、ルールが有効になっていて、スパムとウイルスのチェックが使用されていることを確認します。
「バケットに書き込めませんでした」などのエラーが発生した場合は、手順7に従ってから完了してください。
SESにlambda:InvokeFunctionにアクセスするためのアクセス許可を追加するように求められた場合は、それに同意します。
S3バケットポリシーは、IAMユーザーがS3バケットへの読み取りおよび書き込みアクセス権を持つように設定する必要があります。 SESでS3アクションを設定すると、rootアクセス以外のすべてのユーザーがオブジェクトを取得することを拒否するバケットポリシーステートメントが追加される場合があります。これにより、Lambdaスクリプトからのアクセスの問題が発生するため、バケットポリシーステートメントを次のように調整する必要があります。
{ "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" } } } ] }
オプションで、このバケットのS3ライフサイクルを設定して、保存されたメールをクリーンアップするために数日後にオブジェクトを削除/期限切れにします。
この回答の作成時から、1つまたは2つの変更を加えたバージョンのスクリプトを投稿しています。
確認済みのドメインを介して2回ルーティングされたメールがこのスクリプトによって変更されていることに気づいたので、見栄えを良くするために修正しました
"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));
};