# AWS SES configureren voor uitgaande, bounce en inkomende e-mail

**URL:** https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604
**Category:** Sysadmins
**Tags:** email, how-to
**Created:** [2 mei 2023 om 22:18 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604 "2023-05-02T22:18:25Z")
**Posts on this page:** 14
**Page:** 1

<div class="post-metadata">

### Author: ![dlambert](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dlambert/32/295355_2.png) [@dlambert](https://meta.discourse.org/u/dlambert)
#### Post date: [2 mei 2023 om 22:18 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/1 "2023-05-02T22:18:25Z")

</div>

I thought I’d share the configuration I came up with to use AWS SES for outgoing, bounce, and _incoming_ email. There’s definitely some nuance to the SES service, and it took a good deal of trial and error to understand exactly how it works. This is more of a brain-dump than step-by-step-follow-the-dotted-line. It should be unnecessary, but use at your own risk. And by all means _ **always** _ read through and understand any code written by others you implement.

##### Background:

I’m working to deploy Discourse in AWS and utilize all their services I can to ensure reliability and redundancy. As a developer I’m more comfortable with the command line and code, and wanted to use [IaC](https://en.wikipedia.org/wiki/Infrastructure_as_code) automation. My whole environment is being deployed with [Terraform](https://www.terraform.io), but I’ve tried to click through the web console and line things up as best I can. IAM and policy documents are beyond the scope of this, but I believe I’ve called out where things are needed.

Running a Postfix instance seems like over kill for a single application. Using a POP3 mailbox is so very 90’s. So down the AWS rabbit hole I went.

I did find some extremely useful posts which aided my quest

- [AWS SES / AWS Lambda mail receiver endpoint code?](https://meta.discourse.org/t/aws-ses-aws-lambda-mail-receiver-endpoint-code/214391)
- [How to use Amazon SES for sending emails to users?](https://meta.discourse.org/t/how-to-use-amazon-ses-for-sending-emails-to-users/41922)
- [Configure VERP to handle bouncing e-mails](https://meta.discourse.org/t/configure-verp-to-handle-bouncing-e-mails/45343)

The _mail-receiver_ container also helped me understand how Discourse digests messages

- [Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver](https://meta.discourse.org/t/configure-direct-delivery-incoming-email-for-self-hosted-sites/49487)
- [Update mail-receiver to the release version](https://meta.discourse.org/t/update-mail-receiver-to-the-release-version/133491)

Initially I expected the AWS webhook endpoint to handle incoming messages, but after going though the code realized it wouldn’t. I based my lambda receiver code on the [excellent example](https://meta.discourse.org/t/aws-ses-aws-lambda-mail-receiver-endpoint-code/214391/2) by @dltj. I opted to use SNS for message delivery instead of S3.

# Prereqs

- AWS Account
- Working knowledge of DNS and the email related record types
- A domain (or subdomain) in which you can make changes

## Notes

- Everything documented must be created in the same AWS region
- Bold italicized text _ **like this** _ are your implementation specific values
- _Italicized text_ are names of variables, fixed values, or UI elements

### Steps

1. Create a Simple Email Service (SES) domain identity, _ **your.domain** _, in one of the AWS regions supporting email receiving

2. Verify domain identity

3. Create a Simple Notification Service (SNS) topic, _ **feedback-sns-topic** _, for feedback notifications  
a. Add the ARN of the _ **feedback-sns-topic** _ topic to your `aws_sns_topic_arn_allowlist` setting.

4. Configure the _ **your.domain** _ domain identity  
a. Enable email feedback forwarding  
b. Configure bounce and complaint (not delivery) feedback notifications to use SNS _ **feedback-sns-topic** _ topic

5. Create a subscription on the SNS _ **feedback-sns-topic** _ topic  
a. Protocol is HTTPS (you’re not still using HTTP are you?)  
b. Set endpoint to _https:// **your.domain** /webhooks/aws_ (see [VERP post](https://meta.discourse.org/t/configure-verp-to-handle-bouncing-e-mails/45343))  
c. Ensure that **raw message delivery** is **disabled**

6. Create another SNS topic, _ **incoming-sns-topic** _, for incoming email

7. Create an SES email receiving rule set, _ **inbound-mail-set** _, if there isn’t an existing active one. If so use that as there can only be one active rule set

8. Create a receipt rule in the _ **inbound-mail-set** _ receiving rule set  
a. Set recipient condition to _ **your.domain** _  
b. Add action to publish to SNS topic _ **incoming-sns-topic** _, encoding _Base64_

9. Create API key in your Discourse instance for user _system_, granting _receive email_ action on the _email_ resource

10. Create a secret in Secret Manager, _ **email-handler-secret** _, with the following keys and their respective values:

11. Create a Lambda layer, _ **lambda-receiver-layer** _, for the _python3.10_ runtime containing the _requests_ and _aws-lambda-powertools_ libraries

12. Create a lambda function, _ **email-receiver-lambda** _, for the _python3.10_ runtime with the receiver code:

```python
# Copyright (c) 2023 Derek J. Lambert
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import json
import os
from typing import TypedDict

import requests
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities import parameters
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.sns_event import SNSEvent, SNSEventRecord
from aws_lambda_powertools.utilities.typing import LambdaContext

class Secret(TypedDict):
    api_endpoint: str
    api_username: str
    api_key: str

service = os.getenv('AWS_LAMBDA_FUNCTION_NAME')
logger = Logger(log_uncaught_exceptions=True, service=service)

try:
    SECRET_NAME = os.environ['SECRET_NAME']
except KeyError as e:
    raise RuntimeError(f'Missing {e} environment variable')

AWS_EXTENSION_PORT = os.getenv('PARAMETERS_SECRETS_EXTENSION_HTTP_PORT', 2773)
EXTENSION_ENDPOINT = f'http://localhost:{AWS_EXTENSION_PORT}/secretsmanager/get?secretId={SECRET_NAME}'

def get_secret() -> Secret:
    return parameters.get_secret(SECRET_NAME, transform='json')

def handle_record(record: SNSEventRecord):
    sns = record.sns
    sns_message = json.loads(sns.message)

    try:
        message_type = sns_message['notificationType']
        message_mail = sns_message['mail']
        message_content = sns_message['content']
        message_receipt = sns_message['receipt']
    except KeyError as exc:
        raise RuntimeError(f'Key {exc} missing from message')

    try:
        receipt_action = message_receipt['action']
    except KeyError as exc:
        raise RuntimeError(f'Key {exc} missing from receipt')

    try:
        action_encoding = receipt_action['encoding']
    except KeyError as exc:
        raise RuntimeError(f'Key {exc} missing from action')

    try:
        mail_source = message_mail['source']
        mail_destination = ','.join(message_mail['destination'])
    except KeyError as exc:
        raise RuntimeError(f'Key {exc} missing from mail')

    logger.info(f'Processing SNS {message_type} {sns.get_type} record with MessageId {sns.message_id} from {mail_source} to {mail_destination}')

    # 'email' is deprecated, but just in case something is configured incorrectly
    body_key = 'email_encoded' if action_encoding == 'BASE64' else 'email'

    request_body = {
        body_key: message_content
    }

    secret = get_secret()
    headers = {
        'Api-Username': secret['api_username'],
        'Api-Key': secret['api_key'],
    }

    response = requests.post(url=secret['api_endpoint'], headers=headers, json=request_body)

    logger.info(response.text)
    response.raise_for_status()

@event_source(data_class=SNSEvent)
@logger.inject_lambda_context
def lambda_handler(event: SNSEvent, context: LambdaContext):
    for record in event.records:
        handle_record(record)

```

1. Configure _ **email-receiver-lambda** _ lambda function:  
a. Add layer _ **lambda-receiver-layer** _  
b. Add region-specific layer for [AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html)  
c. Add environment variable _SECRET\_NAME_ with the value _ **email-handler-secret** _  
d. If you’d like additional details logged, add environment variable _POWERTOOLS\_LOGGER\_LOG\_EVENT_ with value _true_

2. Grant lambda function _ **email-receiver-lambda** _ IAM permission _secretsmanager:GetSecretValue_ for secret _ **email-handler-secret** _

3. Create a subscription on the SNS topic _ **incoming-sns-topic** _  
a. Protocol is AWS Lambda  
b. Set endpoint to ARN of _ **email-receiver-lambda** _

4. IAM permissions will be needed for the SNS subscription on _ **incoming-sns-topic** _ topic to invoke _ **email-receiver-lambda** _, but I believe this will be done automatically when configured through the console

For debugging purposes, or general self annoyance, you can add an email subscription to either of the SNS topics to monitor the notifications.

I put this down in a couple sittings, but I think it’s everything. I can try and answer general questions as time permits.

---

<div class="post-metadata">

### Author: ![dlambert](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/dlambert/32/295355_2.png) [@dlambert](https://meta.discourse.org/u/dlambert)
#### Post date: [7 juni 2023 om 18:54 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/2 "2023-06-07T18:54:04Z")

</div>

### Updates to original post

- I recently discovered the Lambda Powertools have support for the spectacular [Pydantic](https://docs.pydantic.dev/latest/) library, and updated the script to use it. In the Lambda layer, _ **lambda-receiver-layer** _, the _aws-lambda-powertools_ will need the _parser_ extra included (ie. `aws-lambda-powertools[parser]`)

- I also noticed I’m not actually using the [AWS Parameters and Secrets Lambda Extension](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html) to retrieve the credentials, but functionality from Powertools (which doesn’t cache between invocations).

- For the time being if the version of the requests library in the Lambda layer, _ **lambda-receiver-layer** _, is greater than 2.29.0 you’ll need to pin the _urllib3_ library to version 1.x (ie. `urllib3<2`). [Later versions of _requests_ will install version 2 of _urllib3_ which currently conflicts with the _boto3_ library](https://github.com/psf/requests/issues/6443).

### Version 2

```python
# Copyright (c) 2023 Derek J. Lambert
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
from enum import Enum
from typing import Literal, Optional

import requests
from aws_lambda_powertools import Logger
from aws_lambda_powertools.logging import utils
from aws_lambda_powertools.utilities.parser import BaseModel, event_parser
from aws_lambda_powertools.utilities.parser.models import SnsModel, SesMessage, SnsRecordModel, SesMail, SesReceipt, SesMailCommonHeaders
from aws_lambda_powertools.utilities.typing import LambdaContext

class Secret(BaseModel):
    api_endpoint: str
    api_username: str
    api_key: str

class SnsSesActionEncoding(str, Enum):
    BASE64 = 'BASE64'
    UTF8 = 'UTF8'

class SnsSesReceiptAction(BaseModel):
    type: Literal['SNS']
    encoding: SnsSesActionEncoding
    topicArn: str

class SnsSesReceipt(SesReceipt):
    action: SnsSesReceiptAction

class SnsSesMailCommonHeaders(SesMailCommonHeaders):
    returnPath: Optional[str]

class SnsSesMail(SesMail):
    commonHeaders: SnsSesMailCommonHeaders

class SnsSesMessage(SesMessage):
    notificationType: str # TODO: Are there other values besides 'Received'?
    content: str
    mail: SnsSesMail
    receipt: SnsSesReceipt

try:
    SECRET_NAME = os.environ['SECRET_NAME']
    AWS_SESSION_TOKEN = os.environ['AWS_SESSION_TOKEN']
except KeyError as e:
    raise RuntimeError(f'Missing {e} environment variable')

AWS_EXTENSION_PORT = os.getenv('PARAMETERS_SECRETS_EXTENSION_HTTP_PORT', 2773)

logger = Logger(service=os.getenv('AWS_LAMBDA_FUNCTION_NAME'), log_uncaught_exceptions=True, use_rfc3339=True)

utils.copy_config_to_registered_loggers(source_logger=logger)

def get_secret() -> Secret:
    # AWS Parameters and Secrets Lambda Extension
    # https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html

    response = requests.get(
        url=f'http://localhost:{AWS_EXTENSION_PORT}/secretsmanager/get?secretId={SECRET_NAME}',
        headers={
            'X-Aws-Parameters-Secrets-Token': AWS_SESSION_TOKEN
        }
    )

    try:
        response.raise_for_status()
    except Exception:
        logger.critical(response.text)
        raise

    return Secret.parse_raw(response.json()['SecretString'])

def handle_record(record: SnsRecordModel):
    sns_record = record.Sns
    sns_ses_message = SnsSesMessage.parse_raw(record.Sns.Message)
    mail_destination = ','.join(sns_ses_message.mail.destination)

    logger.info(f'Processing SNS {sns_ses_message.notificationType} notification record with MessageId {sns_record.MessageId} from {sns_ses_message.mail.source} to {mail_destination}')

    # 'email' is deprecated, but just in case something is configured incorrectly
    body_key = 'email_encoded' if sns_ses_message.receipt.action.encoding is SnsSesActionEncoding.BASE64 else 'email'
    secret = get_secret()

    response = requests.post(
        url=secret.api_endpoint,
        headers={
            'Api-Username': secret.api_username,
            'Api-Key': secret.api_key,
        },
        json={
            body_key: sns_ses_message.content
        }
    )

    try:
        response.raise_for_status()
    except Exception:
        logger.critical(response.text)
        raise

    logger.info(f'Endpoint response: {response.text}')

@event_parser(model=SnsModel)
@logger.inject_lambda_context
def lambda_handler(event: SnsModel, context: LambdaContext):
    for record in event.Records:
        handle_record(record)

```

---

<div class="post-metadata">

### Author: ![Richie](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/richie/32/115110_2.png) [@Richie](https://meta.discourse.org/u/Richie)
#### Post date: [18 november 2023 om 15:42 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/3 "2023-11-18T15:42:43Z")

</div>

Thanks for this guide @dlambert😃

I was doing great, until I got to step 11:

> [@dlambert](#):
>
> 1. Create a Lambda layer, _ **lambda-receiver-layer** _, for the _python3.10_ runtime containing the _requests_ and _aws-lambda-powertools_ libraries

Where / how do I create this? 🤔

---

<div class="post-metadata">

### Author: ![kynic](https://avatars.discourse-cdn.com/v4/letter/k/b487fb/32.png) [@kynic](https://meta.discourse.org/u/kynic)
#### Post date: [27 november 2023 om 20:51 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/4 "2023-11-27T20:51:06Z")

</div>

> [@Richie](#):
>
> Where / how do I create this?

Do you get it working?

I also get stuck at step 11. don’t know what to do next. anyone could help?

 ![Screenshot 2023-11-28 021900](https://global.discourse-cdn.com/meta/original/4X/3/e/b/3eb468c6d836f402d79bca8cc12a325b980dac3f.png)

---

<div class="post-metadata">

### Author: ![Richie](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/richie/32/115110_2.png) [@Richie](https://meta.discourse.org/u/Richie)
#### Post date: [27 november 2023 om 21:55 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/5 "2023-11-27T21:55:28Z")

</div>

No, sorry, I gave up and we disabled all reply-by-email functionality, using SES for simple outbound email only 😢

---

<div class="post-metadata">

### Author: ![kynic](https://avatars.discourse-cdn.com/v4/letter/k/b487fb/32.png) [@kynic](https://meta.discourse.org/u/kynic)
#### Post date: [28 november 2023 om 02:06 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/6 "2023-11-28T02:06:55Z")

</div>

I tried to follow all the steps to setup but in the end, I am getting this error in Cloudwatch can anyone help with this?

```plaintext
[ERROR] HTTPError: 403 Client Error: Forbidden for url: https://forum.siteurl.com/admin/email/handle_mail
Traceback (most recent call last):
  File "/opt/python/aws_lambda_powertools/middleware_factory/factory.py", line 135, in wrapper
    response = middleware()
  File "/opt/python/aws_lambda_powertools/utilities/data_classes/event_source.py", line 39, in event_source
    return handler(data_class(event), context)
  File "/opt/python/aws_lambda_powertools/logging/logger.py", line 453, in decorate
    return lambda_handler(event, context, *args, **kwargs)
  File "/var/task/lambda_function.py", line 107, in lambda_handler
    handle_record(record)
  File "/var/task/lambda_function.py", line 100, in handle_record
    response.raise_for_status()
  File "/opt/python/requests/models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)

```

---

<div class="post-metadata">

### Author: ![kynic](https://avatars.discourse-cdn.com/v4/letter/k/b487fb/32.png) [@kynic](https://meta.discourse.org/u/kynic)
#### Post date: [28 november 2023 om 03:32 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/7 "2023-11-28T03:32:38Z")

</div>

Okay, it was because of Cloudflare disabling resolved the issue. maybe later on I’ll write here how I made it work following all the steps. 🙂

---

<div class="post-metadata">

### Author: ![kynic](https://avatars.discourse-cdn.com/v4/letter/k/b487fb/32.png) [@kynic](https://meta.discourse.org/u/kynic)
#### Post date: [28 november 2023 om 23:37 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/8 "2023-11-28T23:37:02Z")

</div>

This is what I did.

Installed Python 3.10 on my PC, after step 10.

Then run these commands.

`mkdir lambda-receiver-layer`

`cd lambda-receiver-layer`

`mkdir python`

`pip install requests aws-lambda-powertools -t ./python`

`touch ./python/ __init__.py`

As I had issues with `urllib3`

Here are additional steps so you don’t get that error.

In your `lambda-receiver-layer` directory create this file `requirements.txt`

add the following line in this file `requirements.txt`:

`urllib3<2`

Then Run the following command

`pip install -r requirements.txt -t layer`

Now another folder will be created inside `lambda-receiver-layer` directory named `layer`

Copy all the contents of `layer` to` python` folder

Now, right-click on the Python folder and click ‘Compress to ZIP’ rename this zip to `lambda-receiver-layer`

Now, Go back to the AWS Management Console, go to the Lambda service, and navigate to “Layers.” Click on “Create Layer,” put this in the name `lambda-receiver-layer` and upload the zip archive you created. In runtime add` Python 3.10` then click create.

Now follow back from step 12 from the original post.

---

<div class="post-metadata">

### Author: ![Mr.X\_Mr.X](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/mr.x_mr.x/32/126610_2.png) [@Mr.X\_Mr.X](https://meta.discourse.org/u/Mr.X_Mr.X)
#### Post date: [19 maart 2024 om 07:39 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/9 "2024-03-19T07:39:28Z")

</div>

> [@dlambert](#):
>
> Create a Lambda layer, _ **lambda-receiver-layer** _, for the _python3.10_ runtime containing the _requests_ and _aws-lambda-powertools_ libraries

I’m getting stuck at step 11, where do I paste the python code?

---

<div class="post-metadata">

### Author: ![Mr.X\_Mr.X](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/mr.x_mr.x/32/126610_2.png) [@Mr.X\_Mr.X](https://meta.discourse.org/u/Mr.X_Mr.X)
#### Post date: [9 april 2024 om 11:22 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/10 "2024-04-09T11:22:43Z")

</div>

I need urgent help to fix my SMTP Bounces in several instances, I’ve posted a #Marketplace job [Fix AWS SNS Bounce](https://meta.discourse.org/t/fix-aws-sns-bounce/303015)

---

<div class="post-metadata">

### Author: ![tumbano](https://avatars.discourse-cdn.com/v4/letter/t/7ea924/32.png) [@tumbano](https://meta.discourse.org/u/tumbano)
#### Post date: [19 april 2024 om 15:35 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/11 "2024-04-19T15:35:52Z")

</div>

I’m stuck at point 14, anybody can clarify what I’ve to do?

---

<div class="post-metadata">

### Author: ![RichardNooooh](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/richardnooooh/32/511419_2.png) [@RichardNooooh](https://meta.discourse.org/u/RichardNooooh)
#### Post date: [24 september 2025 om 20:16 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/12 "2025-09-24T20:16:34Z")

</div>

If anyone in 2025 is wondering if version 2 still works, I can confirm that it does.

A few hiccups that you might come across:

- Make sure you are configuring the rulesets in **Configuration \> Email receiving** in the console, not the rulesets in **Mail Manager \> Rule sets**. The Mail Manager stuff costs a lot of money, especially with those `ingress endpoints`.
- You need an MX record in your DNS to receive reply emails to send to AWS SES. If you already have an MX record for your root domain for general email stuff (i.e., using Google Workspace emails for general business stuff for an address like `contact@example.com`), you will want to use a subdomain for your replies. In my case, I made an MX record on `reply.example.com` to send the replies to `inbound-smtp.<REGION>.amazonaws.com`. Look at [this documentation](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-setting-up.html) for more details.
- You can use CloudWatch to see how things are working. If you see an error where a certain library/module isn’t loading, you likely misconfigured your Lambda Layer or didn’t connect it to the function. Check that the ZIP file that you upload has the correct directory structure that looks like `python/lib/python3.10/site-packages/`; see [this documentation](https://docs.aws.amazon.com/lambda/latest/dg/packaging-layers.html?utm_source=chatgpt.com). I recommend just looking up some online tutorials on creating a Lambda Layer.

The code still works with ARM64 - you just need to configure your Lambda layer with the correct architecture by downloading the ARM-based Python libraries.

When all is said and done, you should see the received emails in your admin logs.

---

<div class="post-metadata">

### Author: ![jesse\_c](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jesse_c/32/545099_2.png) [@jesse\_c](https://meta.discourse.org/u/jesse_c)
#### Post date: [26 februari 2026 om 03:11 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/13 "2026-02-26T03:11:21Z")

</div>

I followed this guide using v2 on a new deployment today and it worked just fine! Thanks!

And I used python 3.14, not 3.10, mostly without issue. Just needed to add one more library.

For step 11, my command looks like this to build the libraries layer:

```plaintext
LAYER_NAME=lambda-receiver-layer
PYVER=3.14
mkdir -p layer/python

docker run --rm -v "$PWD":/var/task public.ecr.aws/sam/build-python${PYVER}:latest \
  /bin/bash -lc "pip install -U pip && pip install -t layer/python \
  requests aws-lambda-powertools 'urllib3<2' pydantic"

# Zip it in the required structure: zip must contain top-level 'python/' folder
cd layer
zip -r ../${LAYER_NAME}.zip python
cd ..
echo "Created: ${LAYER_NAME}.zip"

# Deploy to AWS Lambda:
aws lambda publish-layer-version \
  --layer-name lambda-receiver-layer \
  --zip-file fileb://lambda-receiver-layer.zip \
  --compatible-runtimes python3.14 \
  --compatible-architectures arm64

```

---

<div class="post-metadata">

### Author: ![simonk](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/simonk/32/247950_2.png) [@simonk](https://meta.discourse.org/u/simonk)
#### Post date: [4 september 2026 om 15:30 UTC](https://meta.discourse.org/t/configuring-aws-ses-for-outgoing-bounce-and-incoming-email/263604/14 "2026-09-04T15:30:14Z")

</div>

> [@dlambert](#):
>
> 1. Create a subscription on the SNS _ **feedback-sns-topic** _ topic  
> a. Protocol is HTTPS (you’re not still using HTTP are you?)  
> b. Set endpoint to _https:// **your.domain** /webhooks/aws_ (see [VERP post](https://meta.discourse.org/t/configure-verp-to-handle-bouncing-e-mails/45343))  
> c. **Select enable raw message delivery**

I just set this up, and I believe that **raw message delivery** must be **DISABLED** , not **ENABLED**.

With raw message delivery enabled, the SNS bounce notifications don’t include the SNS metadata that discourse requires in order to validate the message. My access logs contained entries like this:

`"POST /webhooks/aws HTTP/1.1" "Amazon Simple Notification Service Agent" "-" 406 414 "-" 0.008 0.008 "-" "-" "-" "-" "-" "-" "-"`

ie. HTTP status 406, “Not Acceptable”.

After disabling raw message delivery, the access logs showed:

`"POST /webhooks/aws HTTP/1.1" "Amazon Simple Notification Service Agent" "-" 200 402 "-" 0.022 0.022 "-" "-" "-" "-" "-" "-" "-"`

ie. HTTP status 200, “OK”.
