Send form submissions via SMTP
This recipe configures a contact form that delivers each submission as a
plain-text email over SMTP. Unlike the email output type (which uses Amazon
SES), the smtp output connects directly to any SMTP server — useful when you
already have an SMTP relay, want to send through a transactional email provider’s
SMTP interface, or are running entirely outside AWS.
Prerequisites
- A deployed Webform Relay stack
- An SMTP server reachable from the Lambda function’s VPC (or the public internet if not in a VPC)
- SMTP credentials (host, port, username, password)
TLS behaviour
The relay picks a TLS mode based on the port:
| Port | Behaviour |
|---|---|
465 | Implicit TLS (SMTPS) — TLS on the first byte |
587 (default) | STARTTLS — upgrades if the server advertises it |
| Any other | STARTTLS if the server advertises it; plain otherwise |
Always prefer port 465 or 587 with a server that supports TLS. The relay
enforces a minimum of TLS 1.2.
Config
Upload this to S3 at your CONFIG_BUCKET / CONFIG_KEY path.
cache_ttl: 30s
forms:
contact:
fields:
- name: name
required: true
type: text
- name: email
required: true
type: email
- name: message
required: true
type: textarea
outputs:
- type: smtp
host: smtp.example.com
port: "587"
username: relay@example.com
password: "your-smtp-password"
to: team@example.com
subject: "New contact form submission"
mapping:
from_name: name
from_email: email
body: messageThe email body will look like:
body=Hello, I'd like to ask about your services.
from_email=jane@example.com
from_name=Jane DoeFields appear as key=value lines sorted alphabetically by the mapped output
key. The envelope sender is set to webform-relay@<host>.
Common SMTP providers
Gmail / Google Workspace
Use an App Password — not your regular Google password.
outputs:
- type: smtp
host: smtp.gmail.com
port: "587"
username: youraddress@gmail.com
password: "abcd efgh ijkl mnop" # 16-character App Password (spaces optional)
to: inbox@example.com
subject: "New submission"
mapping:
from: email
message: messageSendGrid
Use port 587 with your API key as the password:
outputs:
- type: smtp
host: smtp.sendgrid.net
port: "587"
username: apikey # literal string "apikey"
password: "SG.xxxxxxx" # your SendGrid API key
to: inbox@example.com
subject: "New submission"
mapping:
from: email
message: messageMailgun
outputs:
- type: smtp
host: smtp.mailgun.org
port: "587"
username: postmaster@mg.yourdomain.com
password: "your-mailgun-smtp-password"
to: inbox@example.com
subject: "New submission"
mapping:
from: email
message: messageImplicit TLS (port 465)
For servers that require SMTPS:
outputs:
- type: smtp
host: mail.privateemail.com
port: "465"
username: relay@yourdomain.com
password: "your-password"
to: inbox@example.com
subject: "New submission"
mapping:
from: email
message: messageSecurity
Credentials are stored in the config YAML in S3. Apply the following controls:
- Restrict bucket access — the Lambda’s IAM role has read-only access to the config bucket. No other principal should have access.
- Enable S3 server-side encryption — use SSE-S3 (
AES256) or SSE-KMS on the config bucket. - Use an app-specific password or API key — never store your primary account password. Most providers offer per-application credentials that can be revoked independently.
- Rotate credentials regularly and update the S3 object. The relay picks up
the new config within
cache_ttlwith no redeployment.
Multiple recipients
The smtp output type delivers to a single to address. To notify multiple
recipients, add multiple smtp outputs:
outputs:
- type: smtp
host: smtp.example.com
port: "587"
username: relay@example.com
password: "secret"
to: alice@example.com
subject: "New submission"
mapping:
message: message
- type: smtp
host: smtp.example.com
port: "587"
username: relay@example.com
password: "secret"
to: bob@example.com
subject: "New submission"
mapping:
message: messageBoth outputs run on every submission regardless of whether the other succeeds.
Test the config before going live
Validate the config locally:
bin/webform-relay --config webform.yaml config validate
# okThen send a test submission:
curl -X POST \
https://<api-id>.execute-api.<region>.amazonaws.com/api/v1/submit/contact \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=Test+User&email=test@example.com&message=Hello"Check CloudWatch for SMTP-level errors if the response is 500:
task relay:logs:tailCommon errors and their causes:
| Log message | Likely cause |
|---|---|
smtp: dial: ...connection refused | Wrong host or port; Lambda can’t reach the server |
smtp: auth: ...authentication failed | Wrong username or password |
smtp: STARTTLS: ... | Server requires STARTTLS but there was a TLS handshake error |
smtp: RCPT TO: ... | Invalid or rejected recipient address |