AWS SES / AWS Lambda 邮件接收器端点代码?

我正在考虑实现邮件接收器的最佳方法(因为我已经在使用 AWS SES),那就是通过 Lambda 函数调用 API,然后调用 API 端点 /admin/email/handle_mail

有一些关于如何将邮件发送到 SES,然后通过 S3 传递给 Lambda 函数的教程。还有一些现成的 Node.js 函数可以获取该邮件并将其转发到邮件服务器。

但与其这样做,不如直接从 Lambda 函数调用 API 并传递邮件。 (我没有维护自己的本地邮件服务器,也不想进行轮询)。

因此,我需要一些帮助,无论是提供更详细的文档(据我所知没有文档页面)关于如何调用该端点以及具体发送什么(这是一个 REST 端点吗?是否有 WebSocket 替代方案?发送内容的格式是什么?)还是提供一些代码示例。我还需要运行这个额外的容器吗?
Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver (该帖子已经有 6 年了)

有人似乎用 Python 做到了这一点 Update mail-receiver to the release version - #7 by dltj? 但没有详细信息,我需要用 Node.js/JavaScript 来完成。

有人有可用的 JavaScript 函数可以分享吗? 谢谢。

2 个赞

@dltj 在此。我无法帮助处理 Node/JavaScript,但我可以提供一些伪代码,形式是我正在使用的 Python 代码:

""" AWS Lambda 接收来自 SES 的消息并将其发布到 Discourse"""
import json
import logging
import boto3
import requests
from base64 import b64decode

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)

# pylint: disable=C0301
ENCRYPTED_DISCOURSE_KEY = "{cyper-text}"
DISCOURSE_KEY = boto3.client("kms").decrypt(
    CiphertextBlob=b64decode(ENCRYPTED_DISCOURSE_KEY)
)["Plaintext"]
DISCOURSE_SITE = "discuss.folio.org"
EMAIL_S3_BUCKET = "openlibraryfoundation-discourse-mail"

DISCOURSE_URL = f"https://{DISCOURSE_SITE}/admin/email/handle_mail"
DISCOURSE_API_HEADERS = {
    "Api-Key": DISCOURSE_KEY,
    "Api-Username": "system",
}


def log_lambda_context(context):
    """记录 Lambda 调用方提供的 context 对象的属性
    """
    LOGGER.info(
        "剩余时间 (毫秒): %s。 日志流: %s。 日志组: %s。 内存限制 (MB): %s",
        context.get_remaining_time_in_millis(),
        context.log_stream_name,
        context.log_group_name,
        context.memory_limit_in_mb,
    )


def lambda_handler(event, context):
    """处理 SES 传递事件
    """
    log_lambda_context(context)
    LOGGER.info(json.dumps(event))

    processed = False

    for record in event["Records"]:
        mail = record["ses"]["mail"]
        mailMessageId = mail["commonHeaders"]["messageId"]
        sesMessageId = mail["messageId"]
        mailSender = mail["source"]

        LOGGER.info(
            "正在处理 SES 消息,mID %s (%s),发件人 %s",
            mailMessageId,
            sesMessageId,
            mailSender,
        )

        deliveryRecipients = mail["destination"]
        for recipientEmailAddress in deliveryRecipients:
            LOGGER.info("传递收件人: %s", recipientEmailAddress)

            s3resource = boto3.resource("s3")
            bucket = s3resource.Bucket(EMAIL_S3_BUCKET)
            obj = bucket.Object(sesMessageId)
            LOGGER.info("正在获取 %s/%s", EMAIL_S3_BUCKET, sesMessageId)
            post_data = {"email": obj.get()["Body"].read()}

            LOGGER.info("正在发布到 %s", DISCOURSE_SITE)
            r = requests.post(
                DISCOURSE_URL, headers=DISCOURSE_API_HEADERS, data=post_data
            )
            if r.status_code == 200:
                LOGGER.info("邮件已由 Discourse 接受: %s", DISCOURSE_SITE)
                obj.delete()
                processed = True
            else:
                LOGGER.error(
                    "邮件被 %s (%s) 拒绝: %s",
                    DISCOURSE_SITE,
                    r.status_code,
                    r.content,
                )

    return processed

我正在使用 Serverless 来管理 Lambda 的部署和维护。如果您想采用类似的方式,欢迎参考这个 serverless.yml 文件:

service: ses-post-to-discourse

frameworkVersion: "=1.36.1"

provider:
  name: aws
  runtime: python3.7
  onError: arn:aws:sns:us-east-1:{AWS_ACCOUNT}:ses-discourse-DLQ
  region: us-east-1
  iamRoleStatements:
  - Effect: 'Allow'
    Action:
      - 'ses:SendBounce'
    Resource:
      - '*'
  - Effect: 'Allow'
    Action:
      - 's3:*'
    Resource:
      - 'arn:aws:s3:::openlibraryfoundation-discourse-mail/*'
  - Effect: 'Allow'
    Action:
      - 'kms:decrypt'
    Resource:
      - 'arn:aws:kms:us-east-1:{AWS_ACCOUNT}:key/{UUID-of-key}'

package:
  include:
    # - something
  exclude:
    - node_modules/**
    - .venv/**
    - __pycache__
    - secrets.yml

functions:
  emailDispatcher:
    handler: post_ses_delivery_lambda.lambda_handler
    events:
      - sns: ses-discourse-inbound

# CloudFormation 资源模板
resources:
  Resources:
    EmailDispatcherLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        RetentionInDays: "30"

plugins:
  - serverless-python-requirements
custom:
  pythonRequirements:
    pythonBin: .venv/bin/python
    dockerizePip: false

谢谢,即使不是 JS 也是一个好的开始。

  1. 如何创建(加密的)API 密钥?它需要加密吗?
  2. 我是否需要勾选“启用手动轮询”以通过 API 推送电子邮件以获取电子邮件回复,或者它是默认启用的?
  3. 在 AWS 权限或 SES 规则方面有什么需要特别注意的吗?

密钥不需要加密。我们项目的基础设施代码位于共享空间中,因此我不想将密钥硬编码到源代码中。

我的网站上启用了“手动轮询”,尽管我不记得它具体有什么作用。

除了发送和读/写临时存储桶之外,我记不起 AWS 权限有任何异常。

祝你好运!

在 WebUI 中是否有生成 API 密钥的地方?在设置中找不到任何地方。也许您只需在构建时将其设置为您想要的任何内容作为环境变量?无论如何,我不知道如何生成 API 密钥。

Discourse API 密钥?它在管理界面中生成,网址为 https://{discourse_url}/admin/api/keys

1 个赞

是的,我看到了,但认为它不正确,因为“granular”没有 handle_email 权限,我不确定应该选择哪个用户或所有用户。

所以我为用户“system”生成了一个具有全局权限的密钥。你也是这么做的吗?

嗯,我当时想使用我的“postman”应用程序来学习/试用,但没有文档,我不知道要 POST 什么以及使用什么格式,以及如何传递密钥。如果我更熟悉 Python,也许可以通过查看你的代码来理解这一点。

我知道这是一个开源项目,所以可以理解为什么没有关于使用 handle_mail API 的实际文档,只能得到像你这样的好心人的帮助……谢谢!

它并非未被记录,因为它开源,而是因为没有人(或很少有人)询问过。

建议您将编码后的电子邮件 POST 到一个 email_encoded 东西。routes.rb 显示这是一个 POST 请求,路由是 https://discourse.example.com/admin/email/handle_mail。我从邮件接收器示例模板中获得了它(我建议您直接使用它,因为如果您那样做了,您早就完成了),但它也在 discourse/config/routes.rb at main · discourse/discourse · GitHub 中(除非您了解 rals,否则它并不清楚具体发生了什么)。

1 个赞

“邮件接收者示例模板”
它在哪里?在仓库里吗? https://github.com/discourse/discourse?

我几乎从 Python 代码中弄明白了

经过几天的努力,终于成功了!

@dltj 我已经使用 aws-sdk 和 got 模块将您的代码重写为 nodejs。非常感谢。

一路上有几个障碍,从 AWS(ses、IAM、S3、lambda)、搞定 serverless、正确进行 API 调用,到配置好 discourse 设置。

我将写一个详细的主题,介绍我为实现这一目标所做的具体工作,细节足够让其他人无需编码或抓狂就能启用此功能。此方法真正的优势在于,通过使用 ses,您无需为域名设置电子邮件服务器。这样您就可以为您的域名设置一个完全独立的邮件服务器。

如果您对实现方法感兴趣,请稍后回来查看,准备好后我会在此处发布链接(几天后)。

3 个赞

在此基础上,@dltj 我们将很快从 admin/email/handle_mail 路由中移除 email 参数,您需要将电子邮件正文作为 base64 编码字符串发送到此路由的 email_encoded 参数中。

3 个赞

编码是我无法解决的问题之一
我无法让编码后的电子邮件正常工作

  1. @martin 根据您的帖子,email_encoded 不正确。我两者都试过了,email_encoded 抛出错误,但 encoded_email 仍然给出警告。

body: 'warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded encoded_email parameter instead. email has been received and is queued for processing',

  1. 我需要将收到的邮件转换为纯文本(我现在正在这样做)然后转回 base64,还是 AWS Ses 存储在存储桶中的内容已经是 base64?如果我将电子邮件正文按 SES 保存的方式传递,它就不会显示在收到的电子邮件日志中。POST 命令没有返回错误,只是警告。所以基本上我被卡在这里了。
    请推迟删除 email 键,直到我们能够解决这个问题。

即使我解码为纯文本并重新编码为 base64,我仍然会收到警告,并且电子邮件未显示为已收到 :roll_eyes:

      const en_package = {
      headers: DISCOURSE_API_HEADERS,
      json: {encoded_email:Buffer.from(body.toString(),'base64') }
    }
   
   console.log(Buffer.from(body.toString(),'base64'))
   const res = await rest.post(DISCOURSE_URL,en_package)

再次,如果我发送纯文本“emal”,则会收到并处理相同的消息。
你一直说“strict”,那有什么意义吗? Nodejs/JS 只有两种 base64 风格“base64”“base64url”。
为什么即使我编码出错,当我使用 encoded_email 时 API 仍然会发出警告。

1 个赞

我尝试了 curl,它不喜欢 encoded_email,但可以接受 email_encoded……真烦人,所以警告是错误的!

所以,我再次尝试了我的代码,但使用了 email_encoded,这次我使用了 base64 编码模块而不是内置的,它现在可以工作了,呼!

2 个赞

啊!我现在知道了。感谢你的提醒,@martin — 我会把它加到我的待办事项列表里。

2 个赞

哇,这完全是我的错(搞错了警告信息),我会做一个小的 PR 来修复它。

2 个赞

此 PR 将修复它,应于今天晚些时候合并 https://github.com/discourse/discourse/pull/15577。

2 个赞

太棒了,感谢分享。
我们在 Lambda 上使用了已弃用的 base64 前版本三年,一个月前它失效了。

我们无法使用 SMTP :25,因为我们托管在 cloudflared tunnel 后面,而且既然 Amazon SES 能够很好地阻止噪音,为什么还要费力处理 25 端口呢?

总之,我快要崩溃了。
代码在 Postman 和本地计算机上运行的 Python 中都可以正常工作。从 AWS Lambda 发送相同的代码时,似乎有些东西被破坏了。

Forbidden
UTF-8

好了,解决了我的问题——
即使查询被传递了,‘机器人对抗模式’与 Discourse API 也不兼容。可能是因为 base64 编码?

希望这能为某人节省 72 小时生命:
关闭这个:

2 个赞