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:

PortBehaviour
465Implicit TLS (SMTPS) — TLS on the first byte
587 (default)STARTTLS — upgrades if the server advertises it
Any otherSTARTTLS 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: message

The email body will look like:

body=Hello, I'd like to ask about your services.
from_email=jane@example.com
from_name=Jane Doe

Fields 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: message

SendGrid

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: message

Mailgun

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: message

Implicit 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: message

Security

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_ttl with 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: message

Both 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
# ok

Then 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:tail

Common errors and their causes:

Log messageLikely cause
smtp: dial: ...connection refusedWrong host or port; Lambda can’t reach the server
smtp: auth: ...authentication failedWrong username or password
smtp: STARTTLS: ...Server requires STARTTLS but there was a TLS handshake error
smtp: RCPT TO: ...Invalid or rejected recipient address