转发电子邮件但更改发件人地址

转发电子邮件但更改发件人地址

我有一个在 EC2 实例上运行的 postfix 服务器。我想通过 SES 将所有电子邮件转发到我的个人收件箱。

问题:AWS 只允许在 AWS 控制台中验证的 FROM 地址,并且本例中的 FROM 地址可以是任何地址,例如:twitter.com。我无法将我的服务器的 IP 列入白名单并说:“无论发件人如何,都接受来自此位置的所有电子邮件”(无论如何这都是一个坏主意)

因此,我需要找到一种方法来转发带有经过验证的地址的电子邮件,但我不想丢失原始发件人的地址。

有办法做到这一点吗?

答案1

根据我们在聊天中的讨论,我将向您提供我的黑客定制解决方案,该解决方案将按照我们的预期更改“发件人”地址,然后交付到原始目的地点,但添加“回复”标头。

这是一种非常hackish的方法,但是应该在通过 PostFix 实际将消息发送到需要发送的位置之前,按照您的预期操作消息。

首先,PostFix端口需要更改。我们需要将 Postfix SMTP 端口更改为其他端口,25以便我们要设置的 python SMTP 处理程序能够在该端口上工作。

编辑/etc/postfix/master.cf。您将寻找这样的行:

smtp      inet  n       -       y       -       -       smtpd

注释掉这一行,并在该行下方使用以下内容:

10025      inet  n       -       y       -       -       smtpd

这告诉 Postfix 我们不希望它监听标准 SMTP 端口。完成此步骤后,重新启动 postfix 服务。


接下来,Python SMTP 处理程序我上面提到过。这将处理传入的消息,操纵它们,并将它们重新发送到系统上的 PostFix。当然,假设所有邮件都在端口 25 上提交,即使是在本地。

该代码存在于GitHub 上的 GIST是基于我在某个地方找到的通用 Python SMTP 服务器代码示例(但抱歉不记得从哪里来的!),并且已经进行了操作。

代码也在这里,如果您好奇的话,它是用 Python 3 编写的,并且是用 Python 3 作为目标 Python 版本编写的:

#!/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,或任何您想要的名称。首先,以 root 身份运行以下命令(通过sudo或进入root用户提示符),以确保它按我们的预期工作:

python3 /opt/PythonAutoForwarderSMTP.py

确认运行后,通过服务器发送电子邮件。它应该被拾取并为您提供来自该脚本的日志数据,表明消息已被接收和处理。然后,您还应该在 Postfix 日志上看到一个连接,并且该连接是在 Postfix 之后传送到某个地方的。如果所有这些看起来都正常,并且您正确处理邮件并在邮件最终结束的地方使用不同的“发件人”地址查看它,那么我们现在就可以让它自动启动! (在继续之前,您只需按Ctrl+即可C关闭 python 进程)。

假设我们希望它在启动时启动,那么我们需要对其进行设置。

作为root,运行crontab -e,并将以下内容添加到rootcrontab:

@reboot /usr/bin/python3 /opt/PythonAutoForwarderSMTP.py 2>&1 >> /var/log/PythonSMTP.log &

保存 crontab 文件。如果您不想重新启动服务器,请执行刚刚添加的命令行(减去该@reboot部分)来运行 Python SMTP 处理程序。

无论是否运行cron,加载 Python 的进程最终都会分叉到后台,并且还将所有数据输出(Python 控制台中的错误或其他情况)以/var/log/PythonSMTP.log追加模式放入日志文件中。这样,您随时可以根据需要获取日志。

如果一切按预期工作,这将正确添加 Reply-To 标头,并将消息中的“From”标头调整为我们期望的内容。 如果邮件已签名,我不能保证这将正常用于 SPF 和 DKIM 检查,但我可以说这将在使用 Postfix 将邮件转发到其他地方之前正确地“预处理”邮件。


强制性安全问题和功能变更通知:

  • 发件人 DKIM 验证可能会失败。只要签名的邮件被操纵,DKIM 签名验证就会失败,这意味着您可能破坏了发件人的 DKIM 签名。这意味着事情可能由于签名验证失败而被视为垃圾邮件。该脚本可能可以自定义为“正常工作”,但我编写此脚本不是为了进行 DKIM/SPF 检查。
  • 我们必须运行这个 Python SMTP 服务器root。这是必要的,因为在 Linux 中默认情况下我们不能绑定到 1024 以下的端口,除非我们是超级用户;这就是为什么 Postfix 有一个主“root”拥有的进程,并且执行不以 root 用户身份运行很长时间的子进程,只是为了端口绑定。
  • 端口 25 上的所有邮件最终都会通过此 Python SMTP 服务器。如果 Postfix 也处理从外部到内部的邮件,那么 Python SMTP 服务器将取代它。这可能会带来一些弊端,但最终会达到您的要求。
  • 这是一个脆弱的解决方案。虽然它不像其他一些解决方案那么脆弱,但如果 Python 进程终止,它不会自动恢复,因此您必须根据具体情况处理错误,有时如果发生错误,则必须使 Python 进程恢复生机。完全消失。
  • 此中没有 StartTLS 或 SSL/TLS 处理程序。所以一切都是纯文本(这是不安全的!)

与往常一样,除非您知道自己在做什么,否则不应以 root 身份运行任何内容。在这种情况下,我以普通视图提供了此代码,以便您可以自己辨别该脚本的作用,以及您是否想以 root 身份运行它,如果您像我一样以安全为中心且偏执(我是IT 安全专业人员以及系统管理员,因此请原谅这些强制性通知)

答案2

除了 @Thomas Ward 的精彩回答之外,AWS 确实有一种“首选”方式,非常相似,唯一的区别是它使用 AWS 内部工具来完成任务,而不是外部 python 脚本。

此方法与另一种方法之间有一个关键区别,此方法将执行病毒/恶意软件扫描以及 DKIM 和 SPF 检查,您可以实际测试并查看它们是否有效PASS

所以,我在README这里关注了这个 GitHub 存储库:https://github.com/arithmetric/aws-lambda-ses-forwarder

一切都是因为这个剧本。您将其放置在 AWS Lambda 上,它将根据 SES 规则对电子邮件进行后处理。

这是设置部分的副本README

笔记: 改变一些事情,比如S3-BUCKET-NAME.

  1. config修改顶部对象中的值index.js以指定用于查找 SES 存储的电子邮件的 S3 存储桶和对象前缀。还提供从原始目的地到新目的地的电子邮件转发映射。

  2. 在 AWS Lambda 中,添加新函数并跳过选择蓝图。

    • 将函数命名为“SesForwarder”并可选择为其提供描述。确保运行时设置为 Node.js 4.3 或 6.10。

    • 对于 Lambda 函数代码,请将内容复制并粘贴 index.js到内联代码编辑器中,或者压缩存储库的内容并直接或通过 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/*" } ] }

    • 内存可以保留为 128 MB,但为了安全起见,将超时设置为 10 秒。该任务通常需要大约 30 MB 和几秒钟的时间。测试任务后,您也许可以减少超时限制。

  3. 在 AWS SES 中,验证您要接收和转发电子邮件的域。还要配置这些域的 DNS MX 记录以指向电子邮件接收(或入站)SES 端点。看SES 文档 对于每个区域中的电子邮件接收端点。

  4. 如果您具有对 SES 的沙箱级别访问权限,则还要验证您要将电子邮件转发到的、不在已验证域中的任何电子邮件地址。

  5. 如果您尚未配置入站电子邮件处理,请创建新的规则集。否则,您可以使用现有的。

  6. 创建处理电子邮件转发功能的规则。

    • 在收件人配置页面上,添加您要从中转发电子邮件的任何电子邮件地址。

    • 在操作配置页面上,先添加 S3 操作,然后添加 Lambda 操作。

    • 对于 S3 操作:创建或选择现有 S3 存储桶。 (可选)添加对象键前缀。不选中加密消息并将 SNS 主题设置为 [无]。

    • 对于 Lambda 操作:选择 SesForwarder Lambda 函数。将“调用类型”设置为“事件”,将 SNS 主题设置为“[无]”。

    • 最后命名规则,确保其已启用并使用垃圾邮件和病毒检查。

    • 如果您收到“无法写入存储桶”之类的错误,请在完成此步骤之前按照步骤 7 操作

    • 如果 SES 要求您尝试添加访问 lambda:InvokeFunction 的权限,请同意。

  7. 需要配置 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" } } } ] }

  8. (可选)为此存储桶设置 S3 生命周期,以在几天后删除/过期对象,以清理保存的电子邮件。

我将发布此答案创建时的脚本版本,并进行一两处更改。

我注意到通过经过验证的域路由两次的电子邮件已被此脚本更改,因此为了美观,我修复了该问题

"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));
};

相关内容