It's Bertha and for just a second, imagine this…

You know what I'd like to imagine, Bertha? A world where you can't find your way into my inbox.

This site is powered by Statamic and my contact form is a regular Statamic form.

I like a regular old-school form. There are no captchas, no math problems, and nothing else that makes it hard for a regular person to say hi.

Along with messages from real people, I get a decent amount of low-effort spam like this opening paragraph from a submission this morning:

Awesome website! My name’s Garrett, and I discovered your site - mead.io - when browsing the net looking for seo opportunities. Your site appeared up at the top of the search engine results, so I checked you out. Looks like what you’re doing is pretty cool.

Trash.

What I want is a way to silently discard specific form submissions while allowing real form submissions through.

Let's get to it.

Determining if a form submission is spam

I took a look through my recent form submissions and found some obvious patterns.

I was getting three types of spam:

SEO spam - These always contain "seo"

General web services spam - These always contain "mead.io"

Cryptocurrency spam - These always contain either "crypto" or "cryptocurrency"

Discarding messages with those words would eliminate 95% of my spam while impacting 0% of my real messages.

Perfect.

Diving into the source

It took me a few minutes of source diving to figure out how to tap into the form submission process.

The original annoyance was the email notifications I was getting, so I thought about modifying the sending of form submission emails by subclassing SendEmail.php.

That felt a bit hacky. It would also technically still accept the submission, meaning that spam submissions would get tracked in the database.

I needed to go further up the chain.

The next goal was to find where emails are sent. This got me to a line in the submit method on FormController.php.

1SendEmails::dispatch($submission, $site);

Just above that, I saw these three lines (after several minutes of poking around):

1// If any event listeners return false, we'll do a silent failure.
2// If they want to add validation errors, they can throw an exception.
3throw_if(FormSubmitted::dispatch($submission) === false, new SilentFormFailureException);

There it is. An official way to tap into this process, allowing me to control which submissions get discarded and which are allowed.

The documentation for the FormSubmitted event confirmed this behavior:

You can return false to prevent the submission, but appear to the user as though it succeeded.

Amazing.

Implementing FormSubmitted

The hard part was figuring out what submissions to ignore and how to ignore them. The actual work of ignoring them wasn't bad.

Here's how you can do it.

First up, make a new event listener for FormSubmitted.

1php artisan make:listener DiscardSpamFormSubmissions --event=FormSubmitted

This will create a new file in app/Listeners/. In my case, it was app/Listeners/DiscardSpamFormSubmissions.php.

From there, you want to modify the handle method for your new listener. Return true to allow a submission. Return false to silently discard a submission.

1// app/Listeners/DiscardSpamFormSubmissions.php
2 
3namespace App\Listeners;
4 
5use Statamic\Events\FormSubmitted;
6 
7class DiscardSpamFormSubmissions
8{
9 // Return true to allow submission
10 // Return false to silently discard submission
11 public function handle(FormSubmitted $event): bool
12 {
13 $spam_words = [
14 'seo',
15 'mead.io',
16 'crypto',
17 'cryptocurrency',
18 'money-back',
19 'instagram'
20 ];
21 $message = $event->submission->data()->get('the_message');
22 
23 foreach ($spam_words as $phrase) {
24 $phrase = preg_quote($phrase);
25 $pattern = "/\b{$phrase}\b/i";
26 
27 if (preg_match($pattern, $message)) {
28 return false;
29 }
30 }
31 
32 return true;
33 }

In the code above, I created a list of spam words and used a regular expression to try and match these words against the message content. If a match is found, the submission is discarded.

I used \b before and after the spam word to match word boundaries. This allowed me to exclude the word "seo" while still allowing words that contained those letters such as "gaseous".

If you want to contact me about something gaseous, such as Jupiter, feel free.

Last up, you need to register the event listener by adding a key-value pair to your EventServiceProvider.

1// app/Providers/EventServiceProvider.php
2 
3namespace App\Providers;
4 
5use Statamic\Events\FormSubmitted;
6use App\Listeners\DiscardSpamFormSubmissions;
7// ...
8 
9class EventServiceProvider extends ServiceProvider
10{
11 protected $listen = [
12 FormSubmitted::class => [
13 DiscardSpamFormSubmissions::class
14 ]
15 // ...
16 ];
17 
18 // ...
19}

And that's it.

When a form is submitted, DiscardSpamFormSubmissions will run and determine which form submission to trash, giving you complete control of who can find their way into your inbox.

Now if I could only do something similar for political texts...