Introduction
Data integrity and validation are important aspects of web development and data collection. It's always important that we (as web developers) validate data before we store in the database. As an example, imagine that you're building a form for a new website that visitors can use to subscribe to your new email newsletter. I'm sure that you'd want to make sure that anyone submitting the form was using a valid email address and not trying to add any spam addresses. In my ebook, Battle Ready Laravel, I actually cover how you can find and improve other weak spots like this in your Laravel applications.
Sure, there are things that you could do to verify that email, such as sending out a confirmation email for the visitor to confirm that they actually want to subscribe. But you might want to actually validate that the email exists before you even attempt to send that confirmation email.
To do this, you can use the Laravel Mailbox Layer package to get up and running quickly.
Why Not Just Use the Built-in PHP Functions?
As you'll already know, PHP and Laravel come with methods that can be used for validating emails. But these methods are better suited for checking that the structure of an email address is correct.
For example, let's assume that we have the following email address that does not exist: i-do-not-exist@invalid.com. If we were to validate this email using the built-in PHP functions, or the Laravel 'email' validation rule, it would pass the validation. This would be because the address matches the standard structure that you'd expect for an email address. However, seeing as the email address doesn't exist, we would have expected the validation to fail.
For these reasons, we can use the Laravel Mailbox Layer package to validate the email, check if the address actually exists and make sure it's from a reputable source that we want to allow through. For example, we might want to disregard any disposable addresses from services like Mailinator.
Getting an API key
The Laravel Mailbox Layer package is a wrapper for the Mailbox Layer API. So, to get started, you'll need to register on their site and grab a new API key. Once you've got your API key, you can add it to your .env
file like shown below:
1MAILBOX_LAYER_API_KEY=your-api-key-here
Installing the Package
Now that we've got our API key added to the .env
file, we can install the package by running the following command in the root of our Laravel project:
1composer require ashallendesign/laravel-mailboxlayer
Validating the Email Address
We're now ready to start validating emails!
For the following examples, we're going to make the assumption that we have a newsletter subscription form on a website.
So, imagine that we have the controller below. At the moment, it validates the email address using the built-in Laravel 'email' rule and then dispatches a job to finish the visitor's subscription process.
1use App\Jobs\CompleteNewsletterSubscription; 2use Illuminate\Http\JsonResponse; 3use Illuminate\Http\Request; 4 5class NewsletterSubscriptionController extends Controller 6{ 7 /** 8 * Store a new newsletter subscriber. 9 *10 * @param Request $request11 * @return JsonResponse12 */13 public function store(Request $request): JsonResponse14 {15 $request->validate([16 'email_address' => 'required|email'17 ]);18 19 CompleteNewsletterSubscription::dispatch();20 21 return response()->json(['success' => true]);22 }23}
Now, as I mentioned earlier, doing this is okay and will check to make sure that an email address has been sent in the request. But, this isn't going to check if the email address actually exists. All it's doing is checking that the email_address
field is there and that the string follows the email address structure. So, we can use the Laravel Mailbox Layer package here to improve this method and the validation.
In the code below, you can see how we have used the MailboxLayer
facade to get started without needing to create a new object manually or using dependency injection. If you're more comfortable using those approaches though, feel free to use them.
After we have run the Laravel validation, we then run the check()
method which makes a request to the Mailbox Layer API and returns the results in a ValidationResult
object that we can work with.
We can check if the smtpCheck
property is false
. If it is, that means that the address doesn't exist, and so we can return an error to the visitor to let them know that the address is invalid. If the address is real, the smtpCheck
property would be returned as true
.
1use App\Jobs\CompleteNewsletterSubscription; 2use AshAllenDesign\MailboxLayer\Facades\MailboxLayer; 3use Illuminate\Http\JsonResponse; 4use Illuminate\Http\Request; 5 6class NewsletterSubscriptionController extends Controller 7{ 8 /** 9 * Store a new newsletter subscriber.10 *11 * @param Request $request12 * @return JsonResponse13 */14 public function store(Request $request): JsonResponse15 {16 $request->validate([17 'email_address' => 'required|email'18 ]);19 20 $result = MailboxLayer::check($request->email_address);21 22 if (! $result->smtpCheck) {23 abort(422, 'The email address is not valid.');24 }25 26 CompleteNewsletterSubscription::dispatch();27 28 return response()->json(['success' => true]);29 }30}
Denying Disposable Addresses
Depending on the use-case of your project and where you might want to be using this validation, you might want to deny any disposable addresses. For example, it is simple to get an email address for a site such as Mailinator that you can use for spam and disregard when you don't need it. For a little bit more information on disposable email addresses, check out the Beginner’s Guide to Disposable Email Addresses by Envato Tuts.
To extend the code above, we can simply check if $result->disposable
is true
. If it's true
then we know that the address is disposable and that we can deny access. Let's see how it might look in our controller method:
1use App\Jobs\CompleteNewsletterSubscription; 2use AshAllenDesign\MailboxLayer\Facades\MailboxLayer; 3use Illuminate\Http\JsonResponse; 4use Illuminate\Http\Request; 5 6class NewsletterSubscriptionController extends Controller 7{ 8 /** 9 * Store a new newsletter subscriber.10 *11 * @param Request $request12 * @return JsonResponse13 */14 public function store(Request $request): JsonResponse15 {16 $request->validate([17 'email_address' => 'required|email'18 ]);19 20 $result = MailboxLayer::check($request->email_address);21 22 if (! $result->mxFound) {23 abort(422, 'The email address is not valid.');24 }25 26 if ($result->disposable) {27 abort(422, 'The email address is disposable.');28 }29 30 CompleteNewsletterSubscription::dispatch();31 32 return response()->json(['success' => true]);33 }34}
Adding Caching for Performance Improvements
Each time you make a request to the Mailbox Layer API, you get one step closer to hitting your API throttle limit. So, the fewer requests that you make, the better. For this reason, it can be beneficial to enable the caching mechanism that comes built in to the package. By doing this, whenever a request is made, the result is stored in the cache. By doing this, it prevents you from needing to make another request in the future for that same address.
To add the caching to the package, it's as simple as using the shouldCache()
method before we run check()
. This instructs the package to store the result in your cache after it's fetched it from the API. This then means that next time you want to validate that same email address, the package will check your cache first to see if a cached result already exists there. If it does, it will return it. If not, another request will be made.
Here you can see how we extended the controller method code to use caching:
1use App\Jobs\CompleteNewsletterSubscription; 2use AshAllenDesign\MailboxLayer\Facades\MailboxLayer; 3use Illuminate\Http\JsonResponse; 4use Illuminate\Http\Request; 5 6class NewsletterSubscriptionController extends Controller 7{ 8 /** 9 * Store a new newsletter subscriber.10 *11 * @param Request $request12 * @return JsonResponse13 */14 public function store(Request $request): JsonResponse15 {16 $request->validate([17 'email_address' => 'required|email'18 ]);19 20 $result = MailboxLayer::shouldCache()->check($request->email_address);21 22 if (! $result->mxFound) {23 abort(422, 'The email address is not valid.');24 }25 26 if ($result->disposable) {27 abort(422, 'The email address is disposable.');28 }29 30 CompleteNewsletterSubscription::dispatch();31 32 return response()->json(['success' => true]);33 }34}
This particular feature might not be too helpful for something like a newsletter subscription form like mentioned above. This is because it's not likely that someone is going to try to subscribe to the newsletter multiple times. It's more than likely going to be a one-time action.
So, this feature would be more beneficial for something like a CSV import into your Laravel system. To help show the benefits of this feature, let's imagine that you need to import a CSV with 1,000 users into a system and that their email addresses aren't all unique. Let's say that out of the 1,000 users, there's only actually 600 unique addresses. As you'd have guessed, it would be much nicer to only have to make 600 API requests rather than 1,000. It would reduce the chance of hitting the API limits, and it would also provide a performance improvement because fetching from the cache is nice and quick!
What Next?
If you think you might be using this kind of email validation in multiple parts of your application, it might be worth creating your own Laravel validation rule. By creating one, you can then use it in your $request->validate
method to keep the code DRY. That means that you can then remove the additional 'if' statements in your code and be confident that the validation is all handled in your own validation rule. This also means that if you want to validate an email somewhere else in your application, you can drop the rule into your new validator to get up and running quickly.
Drop a comment on the article if you'd like a follow-up tutorial to this one on how to create this kind of email validation rule.
Check It out on GitHub
The code and full documentation for this package can all be found in the package's repository on GitHub. Feel free to head over there and check out the code or make any contributions to improve it.