Skip to content

Add support for sending email via AWS SES#5910

Open
txase wants to merge 2 commits intodani-garcia:mainfrom
txase:aws-ses
Open

Add support for sending email via AWS SES#5910
txase wants to merge 2 commits intodani-garcia:mainfrom
txase:aws-ses

Conversation

@txase
Copy link
Contributor

@txase txase commented May 29, 2025

This code adds AWS SES (Simple Email Service) as an additional (optionally enabled) email transport. It uses the same default AWS SDK config that the s3 file support uses.

This is the second piece of functionality from the AWS Serverless RFC (#5591), following the recent merge of AWS S3 support for files (#5626). I plan to add docs for using all the AWS features together once each feature has been merged.

Requirements for use

  • An AWS account with SES enabled:
    • An email or domain identity for your sending address must be validated in SES, see SES docs for details
    • The identity must not have a Default Configuration Set (You can delete the default configuration set if you accidentally created it)
  • Build vaultwarden with either the ses or aws feature
  • Run vaultwarden with:
    • USE_AWS_SES=true
    • SMTP_FROM=<sending email address>
    • AWS_PROFILE=<profile name> (If not using a default AWS config profile. You may also use any other standard AWS env vars to configure SDK credentials.)

@septatrix
Copy link

Why does this need a custom implementation? AWS SES also provides an SMTP endpoint which you should already be able to use?

@txase
Copy link
Contributor Author

txase commented Jun 29, 2025

Why does this need a custom implementation? AWS SES also provides an SMTP endpoint which you should already be able to use?

@septatrix The AWS SES SMTP endpoint only supports static credential authentication (doc link). Static credentials add operational overhead (e.g. generating them the first time, keeping them safe, managing rotations, etc.), and are less secure relative to temporary session credentials.

This PR uses the AWS SES SDK that supports temporary session credentials via the SES API instead of the SMTP endpoint.

@txase
Copy link
Contributor Author

txase commented Jun 29, 2025

Note for reviewers: If #5917 is merged, I will update this PR to use the same approach to re-use the reqwest library for SES SDK calls.

@txase txase force-pushed the aws-ses branch 2 times, most recently from 53109d9 to 1fd3cb6 Compare July 4, 2025 15:54
@txase
Copy link
Contributor Author

txase commented Jul 4, 2025

I just pushed up a rebased and updated commit now that #5917 has been merged. This only change is that we now also use the built-in reqwest client implementation for AWS SDK calls, which significantly reduces the packages brought in as dependencies.

@dani-garcia @BlackDex: This PR is once again ready for review and merging, thanks!

src/mail.rs Outdated
Comment on lines +690 to +691
#[cfg(not(ses))]
err!("Failed to send email", "Failed to send email using AWS SES: `ses` feature is not enabled");
Copy link
Collaborator

@BlackDex BlackDex Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to move this check to the config.rs file around line 927 where it starts with if cfg._enable_smtp {

There we also check if sendmail is enabled that the binary exists or not.
Checking if the feature is enabled their would prevent a system running thinking to use SES while that feature isn't enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, good point. I'll fix this up!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that the PR already has a check at line 970-972 of config.rs:

...
        } else if cfg.use_aws_ses {
            #[cfg(not(ses))]
            err!("`USE_AWS_SES` is set, but the `ses` feature is not enabled in this build");
        } else {
...

This particular code is unreachable. I could change it to unreachable!() to make this more clear, or I could simply delete this line of code. What do you think makes more sense?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, i think it can be removed in a way that this should probably never be called at all.
The validation is done during startup and admin config save, and thus should never ever be able to reach it.
And i see no point in having a redundant check here too.

sorry that i missed the config.rs part though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried removing the code, and then realized that you have to return from that else if branch. I then tried to figure out syntax to make that else if branch conditionally configured, but I don't think there's a simple syntax to do so without modifying the structure of the entire send_with_selected_transport() function.

Instead, I changed the err!() call to an unreachable!() call. This will at least make it clear to those reading the code that this branch should never be called upon in this circumstance.

Thoughts?

src/mail.rs Outdated
Comment on lines +690 to +691
#[cfg(not(ses))]
err!("Failed to send email", "Failed to send email using AWS SES: `ses` feature is not enabled");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, i think it can be removed in a way that this should probably never be called at all.
The validation is done during startup and admin config save, and thus should never ever be able to reach it.
And i see no point in having a redundant check here too.

sorry that i missed the config.rs part though.

@MOZGIII
Copy link

MOZGIII commented Jul 24, 2025

Better use something like https://github.com/lezgomatt/gen_mailer (or https://github.com/lettre/lettre with a custom transport)

@septatrix
Copy link

lettre is already used so it seems like to most natural choice. The custom transport trait seems like quite a clean solution

@txase
Copy link
Contributor Author

txase commented Jul 29, 2025

@MOZGIII @septatrix: It sounds like you are proposing an alternative approach to using the AWS SES client directly, as this PR does. At first glance, it looks like lettre itself should be useful for sending an email via SES. There are two possible approaches to do so:

  1. Use lettre's built-in SMTP transport.

    The problem here is that AWS SES's SMTP interface only supports long-lived credentials instead of short-term credentials. See this reply from above on why this is not ideal.

  2. Use a custom lettre transport.

    Custom transports are implemented as a trait. This trait requires the implementation of a single method, send_raw. It is effectively equivalent to the send_with_aws_ses method in this PR. Implementing the functionality as a lettre transport doesn't really provide any value, we'd still need all the same config changes and transport selection logic.

For these reasons I decided to implement the SES transport more straightforwardly as a direct method in mail.rs.

@septatrix
Copy link

While implementing the trait does not offer any additional value it makes the code cleaner and easier to reason about. The only reason why it is currently possible to just implement and ad-hoc method cleanly is because for each transport the errors are handled differently with a giant match statement. If that code were to be refactored sometime in the future it would be significantly easier to do that with the trait approach

@txase
Copy link
Contributor Author

txase commented Jul 29, 2025

I agree that some of the code could be refactored to simplify if all the transports were lettre async transport implementations. In this PR my goal was to disturb as little existing code as possible. If the maintainers would rather a larger refactor to simplify the code (e.g. making the transport a Box around a dynamic implementation of the lettre async transport trait) as part of this PR, then I can look into it. That could also just as easily be done in a future PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants