PHP: Sending e-mail in Laravel using SMTP and Amazon SES

A shared hosting wins over VPS with its simplicity but is also has pretty serious limitations and doesn't scale up very well. On the other hand managing your own server raises a variety of responsibilities, one in particular is crucial to your business - managing an e-mail server. It is a difficult and absorbing task - not only it requires advanced knowledge in terms of configuration (IMAP, SMTP, Agents) but it also forces you to deal with spam, variety of malware and a range of other cases that are even hard to predict. Perhaps it would be better to delegate this responsibility to a 3rd party provider and not reinvent the wheel? Let me show you how to use Amazon SES (Simple Email Service) and how to implement a complete solution in a sample Laravel / PHP application.

In this article we're going to build a very basic contact form that will allow your visitors to send an enquiry e-mails right into your mailbox. The remaining part of this article assumes that you have your basic Laravel application up and running and that you have an AWS account with full access to your dashboard.

Agenda
  • AWS SES setup
  • Laravel setup
  • Creation of a contact form
AWS SES setup

The very first thing that has to be done is a domain verification. You're going to verify the ownership of the domain from which you're going to send your e-mails.

Wait, what? Sending e-mails? Didn't we suppose to create a contact form where I was going to receive e-mails instead of sending them? I'm confused…

So was I. Bear with me and suddenly everything will be clear. Go to your AWS dashboard and under services search for SES. Next verify your domain - to be more precise - you're going to verify the domain that is associated with the e-mail address that people will be sending messages to (contact form).

Verify new domain, remember to tick "Generate DKIM Settings" checkbox.

A modal window with DNS records will appear. Assuming the domain you're verifying is example.com, you should get:

1 x TXT domain verification record (_amazonses.example.com)

3 x CNAME DKIM records (xxx._domainkey.example.com)

1 x MX record (10 inbound-smtp.us-east-1.amazonaws.com - this step is optional, follow instructions from AWS to find out more)

Pending verification is the initial status for newly submited domain. Add new DNS entries in your domain provider's dashboard and click verify.

Now it is time to verify an existing e-mail you're going to use in your contact form. Follow the steps below and perform the validation by clicks on a link you receive from AWS.

This e-mail will be used to send all the messages in your Laravel application.

Now it's time to create new User so that we can obtain specific keys. Under Services, search for IAM.

Don't forget to tick the checkbox next to Programmatic access.

In the next step you're going to attach AmazonSESFullAccess policy.

Summary of the 5-step-process is presented below.

Laravel setup

First thing you have to do is to install AWS PHP SDK. Open your terminal and in the root of your Laravel project type:

composer require aws/aws-sdk-php

Now open config/services.php and ensure that the key called ses contains below sub-array:

'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
],

In your .env file provide values for below environmental variables:

AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_DEFAULT_REGION=eu-central-1

Ensure that changes are applied by clearing the application's cache. Simply type php artisan cache:clear to purge any cached data.

Now open config/mail.php and under Global "From" Address section paste below code:

'from' => [
    'address' => env('MAIL_FROM_ADDRESS', null),
    'name' => env('MAIL_FROM_NAME', null),
],

'contact' => [
    'address' => env('MAIL_CONTACT_ADDRESS', null),
],

Finally open .env file to set all mail-related environmental varibles:

MAIL_DRIVER=smtp
MAIL_HOST="email-smtp.eu-central-1.amazonaws.com"
MAIL_PORT=587
MAIL_USERNAME=OBTAIN_IT_FROM_AWS_SES
MAIL_PASSWORD=OBTAIN_IT_FROM_AWS_SES
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="Your app"
[email protected]

You can obtain MAIL_USERNAME and MAIL_PASSWORD directly from your AWS Console. Under Amazon SES click on SMTP Settings and follow the brief wizard. Remember that you'll have just a single chance to see (copy) your credentials.

Aaaa
Follow this simple wizard to create and obtain your AWS SMTP username and password.

Preview of your credentials:

Use these credentials in your .env file.

At this point we have everything we need in terms of configuration and credentials. Time to do a bit of coding.

Creation of a contact form

It's time to test if everything works correctly. Let's create a simple contact form page as a meaningful example to give you a broader picture.

First of all you have to create a new controller class called ContactController. In terminal go to the root of your project and type php artisan make:controller ContactController. Next, define two routes, one for displaying the contact form (method getContactForm()) and the second one for sending messages (method postContactForm()).

Route::get('/contact', [ContactController::class, 'getContactForm'])->name('contact.get');
Route::post('/contact', [ContactController::class, 'postContactForm'])->name('contact.post');

The getContactForm() method is responsible only for displaying a simple contact form page.

public function getContactForm()
{
    return view('frontend.pages.contact');
}

The fields we care about in this form are:

  • two text inputs for name and e-mail
  • one textarea for message content

The shape of the form itself (the view layer) is presented below - adjust it to your needs:

<form action="{{ route('frontend.contact.post') }}"
      method="post">
    {{ csrf_field() }}

    <div class="columns small-8 medium-8 large-6">
        <label>
            Your Name {!! $errors->has('name') ? '<p class="validation-error-message">' . $errors->first('name') . '</p>' : '' !!}

            <input name="name"
                   @if($errors->has('name')) class="form-item-failed-validation" @endif
                   type="text"
                   placeholder="Your Name"
                   value="{{ old('name') }}">
        </label>
    </div>

    <div class="columns small-8 medium-8 large-6">
        <label>
            Your E-mail {!! $errors->has('email') ? '<p class="validation-error-message">' . $errors->first('email') . '</p>' : '' !!}

            <input name="email"
                   @if($errors->has('email')) class="form-item-failed-validation" @endif
                   type="text"
                   placeholder="Your E-mail"
                   value="{{ old('email') }}">
        </label>
    </div>

    <div class="columns small-12">
        <label>
            Your Message {!! $errors->has('message') ? '<p class="validation-error-message">' . $errors->first('message') . '</p>' : '' !!}

            <textarea name="message"
                      @if($errors->has('message')) class="form-item-failed-validation" @endif
                      placeholder="Your Message">{{ old('message') }}</textarea>
        </label>
    </div>

    <div class="columns small-12">
        <input type="submit" class="button" value="Submit">
    </div>
</form>

The postContactForm() method is a bit more complicated but it's still far away from a rocket science.

public function postContactForm(ContactFormRequest $request)
{
    try {
        $this->mailer->to(config('mail.contact.address'))->send(new ContactForm());

        return redirect()
            ->route('frontend.contact.get')
            ->with('flash_success', sprintf('Thank you. Your message has been sent successfully.'));
    } catch (Exception $e) {
        return redirect()
            ->route('frontend.contact.get')
            ->with('flash_alert', $e->getMessage());
    }
}

Where $this->mailer comes from?

I'm glad you're asking. I don't like the Facade syntax hence I prefer to inject classess through a constructor when possible. In your constructor inject the Mailer class (namespace is use Illuminate\Mail\Mailer) - this is much nicer IMHO than the Mail::to syntax.

private $mailer;

public function __construct(
    Mailer $mailer
) {
    $this->mailer = $mailer;
}

Go to your terminal and in the root of your project type:

  • php artisan make:request ContactFormRequest to create a new FormRequest class in \App\Http\Requests namespace.
  • php artisan make:mail ContactForm to create a new Mailable class in \App\Mail namespace.

Validation of any incoming data is crucial, contact form is no excuse. Here's the potential content of the FormRequest class.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ContactFormRequest extends FormRequest
{
    const MIN_MESSAGE_LENGTH = 10;
    const MAX_MESSAGE_LENGTH = 1000;

    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name' => [
                'required',
                'string',
            ],

            'email' => [
                'required',
                'string',
                'email',
            ],

            'message' => [
                'required',
                'string',
                'min:' . static::MIN_MESSAGE_LENGTH,
                'max:' . static::MAX_MESSAGE_LENGTH,
            ],
        ];
    }

    public function messages()
    {
        return [
            'name.required' => 'Please provide your name.',
            'email.required' => 'Please provide your e-mail address.',
            'email.email' => 'Please provide a valid e-mail address, otherwise I won\'t be able to respond.',
            'message.required' => 'Please provide your message.',
            'message.min' => sprintf(
                'Provided message is too short. Type at least %d character%s.',
                static::MIN_MESSAGE_LENGTH,
                static::MIN_MESSAGE_LENGTH === 1 ? '' : 's'
            ),
            'message.max' => sprintf(
                'Provided message is too long. Type not more than %d character%s.',
                static::MAX_MESSAGE_LENGTH,
                static::MAX_MESSAGE_LENGTH === 1 ? '' : 's'
            ),
        ];
    }
}

Now the Mailable class.

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Http\Request;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ContactForm extends Mailable
{
    use Queueable, SerializesModels;

    public function build(Request $request)
    {
        $senderEmail = $request->get('email');
        $senderMessage = strip_tags($request->get('message'));
        $senderName = $request->get('name');

        $data = [
            'senderEmail' => $senderEmail,
            'senderMessage' => $senderMessage,
            'senderName' => $senderName,
        ];

        return $this
            ->from(config('mail.contact.address'))
            ->replyTo($senderEmail, $senderName)
            ->view('email.contact-form')
            ->with($data);
    }
}

Keep in mind that nothing stops user from sending you a rich text content. Using strip_tags function can help you get rid of any clutter content as well as any malicious scripts.

Finally let's preview the email.contact-form template for your e-mails.

<div style="font-family: Arial, Helvetica, sans-serif; font-size: 12px;">
    <h3>New contact enquiry from {{ config('app.name') }}</h3>

    <hr />

    <p><strong>Sender Name:</strong> {{ $senderName }}</p>
    <p><strong>Sender E-mail:</strong> <a href="mailto:{{ $senderEmail }}">{{ $senderEmail }}</a></p>

    <hr />

    <p><strong>Message:</strong></p>
    <p>{!! nl2br($senderMessage) !!}</p>
</div>

This is it. This tutorial was pretty detailed but at this point you should have a broad knowledge on setting up a basic contact form logic with the ability to send e-mails through your Amazon SES service with SMTP.


About the author
Matt Komarnicki

Software Architect who crafts highly scalable, multi-tenant, SaaS and cloud-based applications. Matt is specialising in Software Architecture, multi-tenancy, domain-driven-design and custom e-commerce solutions.

Find out more about Matt →

Explore all remaining Articles ↗


Last revision 2020-09-26 20:01:58