Complete Guide: Setting Up a Statamic Contact Form with AWS SES Email

David Childs

Learn how to build a professional contact form in Statamic with AWS SES email delivery, including validation, spam protection, and custom templates.

Setting up a reliable contact form is crucial for any website. In this comprehensive guide, I'll walk you through creating a professional contact form in Statamic that sends emails via AWS Simple Email Service (SES), complete with custom templates and proper formatting.

Why AWS SES for Email Delivery?

Before diving into the implementation, let's understand why AWS SES is an excellent choice for transactional emails:

  • Reliability: 99.9% uptime SLA with AWS's infrastructure
  • Cost-effective: $0.10 per 1,000 emails (significantly cheaper than alternatives)
  • Scalability: Handles everything from small sites to enterprise applications
  • Deliverability: Built-in reputation management and compliance tools
  • Integration: Works seamlessly with other AWS services

Prerequisites

Before starting, ensure you have:

  1. A Statamic installation (v4.x or higher)
  2. An AWS account with SES configured
  3. Verified domain or email addresses in SES
  4. Laravel's mail configuration access

Step 1: Configure AWS SES

First, set up your AWS SES credentials. Log into the AWS Console and navigate to SES:

  1. Verify your domain: Go to Verified identities → Create identity → Choose Domain
  2. Generate SMTP credentials: Account dashboard → SMTP settings → Create SMTP credentials
  3. Note your SMTP endpoint: Usually email-smtp.[region].amazonaws.com
  4. Move out of sandbox: For production, request production access

Step 2: Configure Laravel Mail Settings

Update your .env file with AWS SES credentials:

# AWS SES SMTP Configuration
MAIL_MAILER=smtp
MAIL_HOST=email-smtp.us-east-1.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=your-ses-smtp-username
MAIL_PASSWORD=your-ses-smtp-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_TO_ADDRESS="contact@yourdomain.com"

Update config/mail.php to include a custom recipient:

return [
    // ... existing configuration
    
    'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
        'name' => env('MAIL_FROM_NAME', 'Example'),
    ],
    
    // Add custom to address for contact forms
    'to_address' => env('MAIL_TO_ADDRESS', 'contact@example.com'),
];

Step 3: Create the Contact Form Blueprint

Create a form YAML configuration at resources/forms/contact.yaml:

title: 'Contact Form'
handle: contact
honeypot: winnie
fields:
  -
    handle: name
    field:
      display: Name
      type: text
      required: true
      validate:
        - required
  -
    handle: email
    field:
      display: Email
      type: text
      input_type: email
      required: true
      validate:
        - required
        - email
  -
    handle: subject
    field:
      display: Subject
      type: text
      required: true
      validate:
        - required
  -
    handle: message
    field:
      display: Message
      type: textarea
      required: true
      validate:
        - required
        - min:10
      character_limit: 5000
      rows: 8
  -
    handle: phone
    field:
      display: 'Phone Number (Optional)'
      type: text
      required: false
store: true
email:
  -
    to: '{{ config:mail.to_address }}'
    from: '{{ config:mail.from_address }}'
    reply_to: '{{ email }}'
    subject: 'Contact Form Submission: {{ subject }}'
    html: emails/contact
    text: emails/contact_text

Key features of this configuration:

  • Honeypot field: winnie acts as spam protection
  • Field validation: Required fields with appropriate rules
  • Email configuration: Dynamic values from config
  • Template paths: Separate HTML and text templates

Step 4: Create the Contact Form Template

Create the form view at resources/views/contact.antlers.html:

<section class="bg-white py-16">
    <div class="container max-w-3xl mx-auto px-4">
        <h1 class="text-4xl font-bold mb-8">Contact Us</h1>
        
        {{ form:contact }}
            {{ if success }}
                <div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded mb-6">
                    Your message has been sent successfully!
                </div>
            {{ else }}
                {{ if errors }}
                    <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded mb-6">
                        <ul class="list-disc list-inside">
                            {{ errors }}
                                <li>{{ value }}</li>
                            {{ /errors }}
                        </ul>
                    </div>
                {{ /if }}
                
                <div class="space-y-6">
                    <div class="grid md:grid-cols-2 gap-6">
                        <div>
                            <label for="name" class="block text-sm font-medium mb-2">
                                Name <span class="text-red-500">*</span>
                            </label>
                            <input type="text" name="name" id="name" value="{{ old:name }}"
                                class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                                required>
                        </div>
                        
                        <div>
                            <label for="email" class="block text-sm font-medium mb-2">
                                Email <span class="text-red-500">*</span>
                            </label>
                            <input type="email" name="email" id="email" value="{{ old:email }}"
                                class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                                required>
                        </div>
                    </div>
                    
                    <div class="grid md:grid-cols-2 gap-6">
                        <div>
                            <label for="subject" class="block text-sm font-medium mb-2">
                                Subject <span class="text-red-500">*</span>
                            </label>
                            <input type="text" name="subject" id="subject" value="{{ old:subject }}"
                                class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                                required>
                        </div>
                        
                        <div>
                            <label for="phone" class="block text-sm font-medium mb-2">
                                Phone Number <span class="text-gray-500">(Optional)</span>
                            </label>
                            <input type="tel" name="phone" id="phone" value="{{ old:phone }}"
                                class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
                        </div>
                    </div>
                    
                    <div>
                        <label for="message" class="block text-sm font-medium mb-2">
                            Message <span class="text-red-500">*</span>
                        </label>
                        <textarea name="message" id="message" rows="8"
                            class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                            required>{{ old:message }}</textarea>
                        <p class="text-sm text-gray-500 mt-1">Minimum 10 characters</p>
                    </div>
                    
                    <!-- Honeypot field (hidden from users) -->
                    <div class="hidden">
                        <input type="text" name="{{ honeypot }}" tabindex="-1">
                    </div>
                    
                    <button type="submit" 
                        class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
                        Send Message
                    </button>
                </div>
            {{ /if }}
        {{ /form:contact }}
    </div>
</section>

Step 5: Create Email Templates

HTML Email Template

Create resources/views/emails/contact.antlers.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact Form Submission</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            background-color: #f9fafb;
            border-radius: 8px;
            padding: 30px;
        }
        h1 {
            color: #1f2937;
            font-size: 24px;
            margin-bottom: 20px;
            border-bottom: 2px solid #e5e7eb;
            padding-bottom: 10px;
        }
        .field {
            margin-bottom: 15px;
        }
        .label {
            font-weight: 600;
            color: #6b7280;
            display: block;
            margin-bottom: 5px;
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .value {
            color: #1f2937;
            background-color: white;
            padding: 10px 12px;
            border-radius: 6px;
            border: 1px solid #e5e7eb;
        }
        .message-content {
            color: #1f2937;
            line-height: 1.6;
        }
        .footer {
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #e5e7eb;
            font-size: 12px;
            color: #9ca3af;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>New Contact Form Submission</h1>
        
        {{ fields }}
            {{ if value && fieldtype != 'spacer' }}
            <div class="field">
                <span class="label">{{ display }}</span>
                <div class="value">
                    {{ if handle == 'email' }}
                        <a href="mailto:{{ value }}">{{ value }}</a>
                    {{ elseif handle == 'message' }}
                        <div class="message-content">
                            {{ value | nl2br | raw }}
                        </div>
                    {{ else }}
                        {{ value }}
                    {{ /if }}
                </div>
            </div>
            {{ /if }}
        {{ /fields }}
        
        <div class="footer">
            <p>This email was sent from the contact form on {{ config:app.url }}</p>
            <p>Submitted on {{ now format="F j, Y \a\t g:i A" }}</p>
        </div>
    </div>
</body>
</html>

Plain Text Email Template

Create resources/views/emails/contact_text.antlers.html:

NEW CONTACT FORM SUBMISSION
============================

{{ fields }}
{{ if value && fieldtype != 'spacer' }}
{{ if handle != 'message' }}{{ display }}: {{ value }}
{{ /if }}
{{ /if }}
{{ /fields }}

Message:
--------
{{ fields }}
{{ if handle == 'message' }}{{ value }}{{ /if }}
{{ /fields }}

============================
This email was sent from the contact form on {{ config:app.url }}
Submitted on {{ now format="F j, Y \a\t g:i A" }}

Step 6: Test Your Configuration

Test your email configuration using Laravel's tinker:

php artisan tinker
Mail::raw('Test email', function($message) {
    $message->to(config('mail.to_address'))
            ->subject('Test Email from Statamic');
});

If successful, you should receive the test email.

Step 7: Advanced Features

Adding File Attachments

To allow file uploads, add to your form YAML:

-
  handle: attachment
  field:
    display: Attachment
    type: files
    max_files: 1
    max_filesize: 5
    allowed_extensions:
      - pdf
      - doc
      - docx

And update the email configuration:

email:
  -
    # ... other config
    attachments: true

Rate Limiting

Protect against spam by adding rate limiting in your controller:

use Illuminate\Support\Facades\RateLimiter;

public function submit(Request $request)
{
    $key = 'contact-form:' . $request->ip();
    
    if (RateLimiter::tooManyAttempts($key, 3)) {
        return back()->with('error', 'Too many submissions. Please try again later.');
    }
    
    RateLimiter::hit($key, 3600); // 3 attempts per hour
    
    // Process form...
}

Custom Validation Messages

Customize validation messages in your form:

validate:
  - required
  - 'min:10'
messages:
  required: 'The :attribute field is required.'
  min: 'Your message must be at least :min characters.'

Troubleshooting Common Issues

Emails Not Sending

  1. Check credentials: Verify your AWS SES SMTP credentials
  2. Check logs: Review storage/logs/laravel.log
  3. Test with log driver: Set MAIL_MAILER=log temporarily
  4. Verify domain: Ensure your domain is verified in SES

Message Field Formatting Issues

If message paragraphs aren't displaying correctly:

  1. Use nl2br filter: {{ value | nl2br | raw }}
  2. For plain text: Preserve line breaks naturally
  3. Consider using a textarea instead of rich text editor

Duplicate Configuration

Clean up .env file to avoid duplicate mail settings:

# Remove duplicates, keep only one set of MAIL_ variables
MAIL_MAILER=smtp
MAIL_HOST=email-smtp.us-east-1.amazonaws.com
# ... rest of configuration

Security Best Practices

  1. Never commit credentials: Keep .env out of version control
  2. Use environment variables: Don't hardcode credentials
  3. Implement CSRF protection: Statamic handles this automatically
  4. Add honeypot fields: Prevents bot submissions
  5. Rate limit submissions: Prevent abuse
  6. Validate all inputs: Use Statamic's validation rules
  7. Sanitize output: Use proper escaping in templates

Conclusion

You now have a fully functional contact form in Statamic that sends professional emails via AWS SES. This setup provides reliability, scalability, and cost-effectiveness for your transactional emails.

The key takeaways:

  • AWS SES offers excellent deliverability at low cost
  • Statamic's form builder makes configuration straightforward
  • Custom email templates enhance professionalism
  • Proper validation and security measures are essential

Remember to test thoroughly in development before deploying to production, and always monitor your email delivery rates through the AWS SES console.

Additional Resources

Share this article

DC

David Childs

Consulting Systems Engineer with over 10 years of experience building scalable infrastructure and helping organizations optimize their technology stack.

Related Articles