Reach

Grow

Manage

Automate

Reach

Grow

Manage

Automate

构建电子邮件归档系统:存储电子邮件正文

2019年3月4日

电子邮件

1 min read

构建电子邮件归档系统:存储电子邮件正文

2019年3月4日

电子邮件

1 min read

构建电子邮件归档系统:存储电子邮件正文

在这篇博客中,我将描述我如何将电子邮件的主体存储到 S3(亚马逊的简单存储服务)以及将辅助数据存储到 MySQL 表中,以便于交叉引用。

在这篇博文中,我将描述我如何将电子邮件的正文存储到S3(亚马逊的Simple Store Service)以及辅助数据存储到MySQL表中,以便于交叉引用。最终,这将是代码库的起点,该代码库将包括一个应用程序,允许轻松搜索存档的邮件,然后显示这些邮件以及事件(日志)数据。这个项目的代码可以在以下GitHub代码库中找到:https://github.com/jeff-goldstein/PHPArchivePlatform

虽然我将在这个项目中利用S3和MySQL,但绝不是仅有的可以用于构建存档平台的技术,鉴于它们的普及性,我认为它们是这个项目的不错选择。在一个全规模高容量系统中,我会使用比MySQL性能更高的数据库,但对于这个示例项目,MySQL非常合适。对于考虑选择PostgreSQL作为存档数据库的组织,实施适当的备份和还原程序对于维护生产系统中的数据完整性是至关重要的。

在这个项目的第一阶段中,我详细介绍了我采取的步骤:

  1. 创建用于存档的重复电子邮件

  2. 使用SparkPost的Archiving和Inbound Relay功能,将原始电子邮件的副本发送回SparkPost进行处理,转换为JSON结构,然后发送到webhook收集器(应用程序)

  3. 拆解JSON结构以获取必要的组件

  4. 将电子邮件正文发送到S3进行存储

  5. 在MySQL中为每封电子邮件记录一个条目以供交叉引用

创建电子邮件的副本

在 SparkPost 中,存档电子邮件的最佳方法是创建一封专为存档目的设计的相同副本。这是通过使用 SparkPost 的 Archive 功能来完成的。SparkPost 的 Archive 功能使发件人能够将电子邮件的副本发送到一个或多个电子邮件地址。 这个副本使用与原始邮件相同的跟踪和打开链接。SparkPost 文档如下定义了 Archive 功能:

存档列表中的收件人将收到发送到 RCPT TO 地址的邮件的精确副本。具体来说,任何为 RCPT TO 收件人编码的链接在存档信息中将是相同的。

此存档副本与原始 RCPT TO 电子邮件之间的唯一区别在于由于存档电子邮件的目标地址不同,一些头信息将会不同,但电子邮件的正文将是精确的副本!

如果您想要更深入的解释,这里有一个 链接 到关于创建电子邮件的副本(或存档)的 SparkPost 文档。此项目的示例 X-MSYS-API 头信息稍后在此博客中展示。

这种方法有一个警告;虽然原始电子邮件中的所有事件信息都通过一个 transmission_id 和一个 message_id 关联在一起,但在入站中继事件(获取和传播存档电子邮件的机制)中,没有信息将副本电子邮件关联回这两个 id 之一从而关联到原始电子邮件的信息。这意味着我们需要在原始电子邮件的正文和头中放置数据,以将所有的 SparkPost 数据从原始和存档电子邮件联系在一起。

为了在电子邮件正文中创建插入的代码,我在电子邮件创建应用程序中使用了以下过程。

  1. 在电子邮件正文中的某个位置,我放置了以下输入条目:<input name="ArchiveCode" type="hidden" value="<<UID>>">

  2. 然后,我创建了一个唯一代码并替换了 <<UID>> 字段:$uid = md5(uniqid(rand(), true)); $emailBody = str_replace(“<<UID>>,$uid,$emailBody);

    这是一个示例输出:

    <input name="ArchiveCode" type="hidden" value="00006365263145">

  3. 接着,我确保将 $UID 添加到 X-MSYS-API 头信息的 meta_data 模块中。此步骤确保 UID 被嵌入到原始电子邮件的每个事件输出中:

X-MSYS-API: {
  "campaign_id": "<my_campaign>",
  "metadata": {
    "UID": "<UID>"
  },
  "archive": [
    {
      "email": "archive@geekwithapersonality.com"
    }
  ],
  "options": {
    "open_tracking": false,
    "click_tracking": false,
    "transactional": false,
    "ip_pool": "<my_ip_pool>"
  }
}

现在我们有了一种方法来将原始电子邮件的所有数据联系到存档的电子邮件正文上。

获取 Archive 版本

为了获得归档电子邮件的副本,您需要执行以下步骤:

  1. 创建一个子域名,您将把所有归档(重复)电子邮件发送到该子域名

  2. 设置适当的DNS记录,将所有发送到该子域名的电子邮件转发至SparkPost

  3. 在SparkPost中创建一个入站域

  4. 在SparkPost中创建一个入站webhook

  5. 创建一个应用程序(收集器)以接收SparkPost webhook的数据流

以下两个链接可用于帮助指导您完成此过程:

  1. SparkPost技术文档:启用入站电子邮件中继和中继Webhooks

  2. 还有我去年写的博客,归档邮件:跟踪已发送邮件的指南 将引导您完成SparkPost中的入站中继设置

* 注意:截至2018年10月,归档功能仅在使用SMTP连接发送邮件至SparkPost时有效,RESTful API不支持此功能。这可能不成问题,因为大多数需要此级别审计控制的邮件往往是由后端应用程序在邮件发送前完全构建的个性化邮件。

为了获得归档电子邮件的副本,您需要执行以下步骤:

  1. 创建一个子域名,您将把所有归档(重复)电子邮件发送到该子域名

  2. 设置适当的DNS记录,将所有发送到该子域名的电子邮件转发至SparkPost

  3. 在SparkPost中创建一个入站域

  4. 在SparkPost中创建一个入站webhook

  5. 创建一个应用程序(收集器)以接收SparkPost webhook的数据流

以下两个链接可用于帮助指导您完成此过程:

  1. SparkPost技术文档:启用入站电子邮件中继和中继Webhooks

  2. 还有我去年写的博客,归档邮件:跟踪已发送邮件的指南 将引导您完成SparkPost中的入站中继设置

* 注意:截至2018年10月,归档功能仅在使用SMTP连接发送邮件至SparkPost时有效,RESTful API不支持此功能。这可能不成问题,因为大多数需要此级别审计控制的邮件往往是由后端应用程序在邮件发送前完全构建的个性化邮件。

为了获得归档电子邮件的副本,您需要执行以下步骤:

  1. 创建一个子域名,您将把所有归档(重复)电子邮件发送到该子域名

  2. 设置适当的DNS记录,将所有发送到该子域名的电子邮件转发至SparkPost

  3. 在SparkPost中创建一个入站域

  4. 在SparkPost中创建一个入站webhook

  5. 创建一个应用程序(收集器)以接收SparkPost webhook的数据流

以下两个链接可用于帮助指导您完成此过程:

  1. SparkPost技术文档:启用入站电子邮件中继和中继Webhooks

  2. 还有我去年写的博客,归档邮件:跟踪已发送邮件的指南 将引导您完成SparkPost中的入站中继设置

* 注意:截至2018年10月,归档功能仅在使用SMTP连接发送邮件至SparkPost时有效,RESTful API不支持此功能。这可能不成问题,因为大多数需要此级别审计控制的邮件往往是由后端应用程序在邮件发送前完全构建的个性化邮件。

获取重复的电子邮件在一个JSON结构中

在此项目的第一阶段,我所存储的只是 S3 中的 rfc822 邮件格式和一些用于搜索的 SQL 表中的高级描述字段。 由于 SparkPost 会通过 webhook 数据流以 JSON 结构将电子邮件数据发送到我的归档平台,我构建了一个应用程序(通常称为collector)来接受Relay_Webhook数据流。

来自 SparkPost Relay_Webhook 的每个包将包含一封重复邮件的信息,因此将 JSON 结构分解为本项目的目标组件相当简单。 在我的 PHP 代码中,获取 rfc822 格式的电子邮件就像以下几行代码一样简单:

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $body = file_get_contents("php://input");
    $fields = json_decode($body, true);
    $rfc822body = $fields[0]['msys']['relay_message']['content']['email_rfc822'];
    $htmlbody = $fields[0]['msys']['relay_message']['content']['html'];
    $headers  = $fields[0]['msys']['relay_message']['content']['headers'];
}

我想存储到 SQL 表的一些信息位于一个头字段数组中。 因此,我编写了一个小函数,接受头数组并循环遍历该数组以获取我感兴趣存储的数据:

function get_important_headers($headers, &$original_to, &$headerDate, &$subject, &$from) {
    foreach ($headers as $headerGroup) {
        foreach ($headerGroup as $key => $value) {
            if ($key === 'To') {
                $original_to = $value;
            } elseif ($key === 'Date') {
                $headerDate = $value;
            } elseif ($key === 'Subject') {
                $subject = $value;
            } elseif ($key === 'From') {
                $from = $value;
            }
        }
    }
}

现在我有了数据,我准备将正文存储到 S3。

在 S3 中存储重复的 email

很抱歉让您失望,但我不会提供关于创建用于存储电子邮件的S3 bucket的逐步教程,也不会描述如何为您的应用创建必要的访问密钥以将内容上传到您的bucket;在这个主题上,有比我能写出的更好的教程。 这里有几篇可能有帮助的文章:

https://docs.aws.amazon.com/quickstarts/latest/s3backup/step-1-create-bucket.html
https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/

我要做的是指出一些我选择的,与这样的项目相关的设置。

  1. 访问控制。 您不仅需要为bucket设置安全性,还需要为项目本身设置权限。在我的项目中,我使用了一种非常开放的public-read策略,因为样本数据并不涉及个人隐私,我希望轻松访问数据。您可能需要一个更严格的ACL策略集。这里有一篇关于ACL设置的不错文章:https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html

  2. 归档归档。 在S3中,有一种叫做生命周期管理的东西。这允许您将数据从一种类型的S3存储类移到另一种。不同的存储类代表了您对存储数据需要的访问量,费用较低的存储类与您最少访问的存储相关。在AWS指南中,一篇关于不同存储类及其过渡的好文章叫做,Transitioning Objects。就我而言,我选择创建一个生命周期,在一年后将每个对象从标准类移至Glacier。Glacier的访问费用比标准S3归档便宜,因此会为我节省存储成本。

一旦我创建了S3 bucket并设置好了我的配置,S3就准备好让我上传从SparkPost Relay Webhook数据流中获得的rfc822兼容电子邮件。但是,在将rfc822邮件有效载荷上传到S3之前,我需要创建一个我将用来存储该邮件的唯一文件名。

对于唯一的文件名,我会在邮件正文中搜索发送应用放置的隐藏ID并使用该ID作为文件名。还有更优雅的方法可以从html正文中提取connectorId,但为了简单和清晰,我将使用以下代码:

$start = strpos($htmlbody, $inputField);
if ($start !== false) {
    $start = strpos($htmlbody, 'value="', $start);
    if ($start !== false) {
        $start += 7; // Move past 'value="'
        $end = strpos($htmlbody, '"', $start);
        if ($end !== false) {
            $length = $end - $start;
            $UID = substr($htmlbody, $start, $length);
        }
    }
}

* 我们假设$inputField持有值“ArchiveCode”,并在我的config.php文件中找到了它。

使用UID,我们可以制作将在S3中使用的文件名:

$fileName = $ArchiveDirectory . '/' . $UID . '.eml';

现在我可以打开我的S3连接并上传文件。如果您查看GitHub库中的s3.php文件,您会看到上传文件所需的代码非常少。

我的最后一步是将此条目记录到MYSQL表中。

在 MySQL 中存储 Meta Data

我们在前一步中已经获取了所有必要的数据,所以存储这一步很简单。 在这个第一阶段,我选择建立一个包含以下字段的表:

  • 用于日期/时间的自动字段输入

  • 目标电子邮件地址 (RCPT_TO)

  • 电子邮件 DATE 头的时间戳

  • SUBJECT 头

  • FROM 电子邮件地址头

  • S3 存储桶中使用的目录

  • 存档邮件的 S3 文件名

在 upload.php 应用文件中的名为 MySQLLog 的函数进行必要步骤,包括打开 MySQL 链接、插入新行、测试结果并关闭链接。我确实添加了另一项步骤,以记录这些数据到一个文本文件。是否应该多做一些错误日志记录?是的。但我希望保持这段代码的轻量,以便它可以非常快速运行。有时这段代码每分钟会被调用数百次,因此需要尽可能高效。在未来的更新中,我会添加辅助代码来处理故障并将这些故障通过电子邮件发送给管理员进行监控。

总结

因此,通过几个相当简单的步骤,我们能够完成构建强健的电子邮件归档系统的第一阶段,该系统在S3中保存电子邮件副本,并在MySQL表中进行数据交叉引用。这将为未来几篇帖子中将要解决的项目其余部分奠定基础。

在该项目的未来修订中,我希望能够:

  1. 存储原始电子邮件的所有日志事件

  2. 当上传或记录失败时,向管理员发送存储错误通知

  3. 尽量简化收集器的复杂性。

  4. 添加一个用于查看所有数据的用户界面

  5. 支持重新发送电子邮件的功能

同时,我希望这个项目对您来说是有趣且有帮助的;祝您发送愉快。

让我们为您联系Bird专家。
在30分钟内见证Bird的全部威力。

通过提交,您同意 Bird 可能会就我们的产品和服务与您联系。

您可以随时取消订阅。查看Bird的隐私声明以获取有关数据处理的详细信息。

Newsletter

通过每周更新到您的收件箱,随时了解 Bird 的最新动态。

让我们为您联系Bird专家。
在30分钟内见证Bird的全部威力。

通过提交,您同意 Bird 可能会就我们的产品和服务与您联系。

您可以随时取消订阅。查看Bird的隐私声明以获取有关数据处理的详细信息。

Newsletter

通过每周更新到您的收件箱,随时了解 Bird 的最新动态。

让我们为您联系Bird专家。
在30分钟内见证Bird的全部威力。

通过提交,您同意 Bird 可能会就我们的产品和服务与您联系。

您可以随时取消订阅。查看Bird的隐私声明以获取有关数据处理的详细信息。

R

Reach

G

Grow

M

Manage

A

Automate

Newsletter

通过每周更新到您的收件箱,随时了解 Bird 的最新动态。