In short

A mail service accepting a notification proves nothing about the team receiving it. Route notifications to an inbox people read, never silently skip an empty setting, and record on every lead who was told, where, and why not, so a misrouted notification stops looking like one that worked.

Most contact forms end the same way. The visitor presses Send, the page says something like “Thanks, we’ll be in touch”, and everybody involved assumes that sentence is true. The visitor believes it because the website said so. The business believes it because the form has never complained. Nobody checks, because there is nothing obvious to check. A contact form not sending email to the people who should read it looks exactly like one that works.

That sentence is written by software, not by your team. It appears the moment the enquiry is saved, before anyone at your business knows the enquiry exists. Whether a person ever hears about it depends on a chain of settings, addresses and services that almost nobody tests after launch day. If one link in that chain is quietly wrong, the form keeps saying thank you, the enquirer keeps waiting, and you find out from the customer who gave up, if you find out at all.

I found exactly this on a system I built and run: the website and admin for Lampstand Edu Consult, an education consultancy that helps Nigerian students secure places at universities abroad. Nothing had crashed. Every component did what it was told. An enquiry could still arrive and nobody on the team would hear a word about it.

The thank-you message is shown before anyone has been told

It helps to see the order things happen in. On Lampstand’s site, and on most sites I have looked at, a submission goes roughly like this:

  1. The visitor fills in the form and presses Send.
  2. The server checks the required fields and the email address.
  3. The enquiry is saved to the database.
  4. A notification email is sent to the team.
  5. A confirmation email is sent to the visitor.
  6. The page tells the visitor their enquiry was received.

Only step four involves your team, and it is the step with the least visible result. If saving fails, the visitor sees an error. If the confirmation fails, the visitor might notice the missing email. If the team notification goes somewhere nobody looks, nothing anywhere changes. The visitor still gets their thank-you. The database still has the row. The only evidence is an absence, and absences do not raise alarms.

Lampstand’s form replies “Enquiry received. We’ll be in touch within one business day.” The contact page promises a reply within 24 hours. Those are commitments made to a family deciding where a son or daughter will study, often comparing several consultancies at once. An enquiry that sits unanswered for three days is not a delay. It is a decision made for them, in favour of whoever replied first.

Every step reported success, and the team still heard nothing

The notification address was a single mailbox, set once during the build and never revisited. It was a perfectly valid address. It just was not one anybody on the team opened.

So every notification was sent. The mail service accepted every one. The system logged each as sent. The enquirer received their confirmation. By every measure the software had, the enquiry path worked, and it would have gone on working, invisibly, for as long as nobody asked.

It was found by doing the thing I now tell everyone to do: filling in the contact form by hand and waiting for it to reach the team. It did not.

When I went looking for why, the code that decided who gets told had this shape:

$to = getSettingValue('lead_notification_email');
if ($to === '') return;

Two lines, and they hide two failures. If the setting is empty, the function quietly returns: nobody is told, and nothing records that nobody was told. If the setting holds the wrong address, the send succeeds and looks, from the inside, exactly like the right address. The software had no way to tell a notification that reached the team from one that reached an unread box, because it never wrote down anything that would distinguish them.

That is the real defect. The wrong address was a configuration mistake, and configuration mistakes happen. The expensive part was that the system made the mistake indistinguishable from success.

A blank settings field should not be able to silence the team

The first change was about where notifications go. They now go to the shared inbox the team actually works from, and the setting accepts a list, because a team is more than one person. People paste colleagues’ addresses separated by commas, semicolons, spaces or new lines, so all of those are accepted. An entry that is not a valid address is dropped and logged rather than failing the whole send: one typo in a list of four should not stop the other three hearing about an enquiry.

The second change was to remove the silent return. If the notification setting is empty, the system falls back to the site’s public contact address and logs that it did so:

function bf_lead_notification_recipients(): array
{
    $configured = bf_email_recipients(getSettingValue('lead_notification_email'));
    if ($configured) return $configured;

    $fallback = bf_email_recipients(getSettingValue('contact_email'));
    if ($fallback) {
        logEvent('email', 'No lead notification address is set; using the public contact address instead.');
        return $fallback;
    }
    return [];
}

The reasoning is simple. An enquiry that reaches the wrong desk can be forwarded. An enquiry that reaches no desk is gone. Arranging the second should take a deliberate act, not an empty text box.

While I was in there, I fixed where replies go, because that is part of the same promise. Notifications are sent from a machine address that nobody reads. Without care, pressing Reply on a notification would answer the machine, not the student. So the team’s notification carries the student’s address as its Reply-To, and pressing Reply starts a conversation with the person who asked. The confirmation the student receives works the other way round: its Reply-To is the team’s inbox, because the email invites a reply and a reply has to reach a person.

Each enquiry now records whether the team was told

Routing fixes the problem Lampstand had. It does not stop the next misconfiguration, whether that is a mistyped address, an expired service key or a transport having a bad afternoon. For that, the system needed to write down what happened.

Every lead now carries three extra fields: when the team was notified, which addresses the notification went to, and, if it failed, the error the mail service returned in its own words. Keeping the service’s own wording matters more than it sounds. “It never arrived” is only answerable if something recorded what the service objected to.

try {
    getDB()->prepare(
        'UPDATE leads SET notified_at = ?, notified_to = ?, notify_error = ? WHERE id = ?'
    )->execute([
        $sent ? date('Y-m-d H:i:s') : null,
        implode(', ', $recipients),
        $error,
        $id,
    ]);
} catch (\Throwable $e) {
    // Recording the outcome must never be what breaks accepting a lead.
    logEvent('email', 'Could not record the notification outcome: ' . $e->getMessage());
}

Note the order. The enquiry is saved first. The notification is attempted second. The outcome is recorded third, inside its own safety net. A failure at any later step never costs the enquiry itself. Accept the lead, record what went wrong, carry on.

Recording it is only half the job. The other half is putting it where the team already looks. In Lampstand’s admin, a lead whose notification failed gets a marker on its row in the Leads list. If any have failed, a banner above the list says how many enquiries did not reach the team by email and shows the reason. Opening a lead shows where its notification went, and there is a button to send it again. Before this, the only remedy for a failed notification was to ask me to run something on the server, which is not a remedy a business owner can use on a Saturday.

The question “did anyone see this enquiry?” is now one the team answers in its own admin, in a few seconds, without a developer.

Old enquiries are marked unknown, because a false alarm teaches people to ignore the real one

There was a design choice here that I think is worth copying. When the new fields were added, every enquiry already in the database had them empty. The easy reading of an empty “notified at” is “not notified”, which would have lit up every historical lead as a failure on the first day.

That would have been wrong on the facts, since some of those notifications may well have been read, and wrong in effect. An alarm that fires about everything on day one is an alarm nobody reads by day ten. So there are three states, not two:

'notifyState' => !empty($row['notified_at'])
    ? 'sent'
    : ((!empty($row['notified_to']) || !empty($row['notify_error'])) ? 'failed' : 'unknown'),

Sent means the mail service accepted it and we know where it went. Failed means an attempt was made and something objected, or nobody was set to receive it. Unknown means the enquiry predates the record, and the honest answer is that we cannot say. Only failures raise the banner.

Accepted by the mail service is not the same as read by a person

I want to be precise about what this does and does not prove, because overclaiming here is how the original problem happened.

When a mail service accepts a message, it means the service took it. It does not mean the mailbox exists, that the message avoided a spam folder, or that a human opened it. Bounces are invisible unless something subscribes to them, usually through a webhook the mail service calls back. So “sent” in Lampstand’s admin means “handed over successfully, to these addresses”. That narrows the question from “did the system work?”, which nobody can answer by looking, to “did that inbox receive it?”, which a person on the team can answer in a minute.

The stronger evidence is further down the pipe, and it is human. Lampstand’s Leads screen has a status pipeline: New, Contacted, In progress, Closed won. The reply itself goes out from the team’s own email client, so it looks like a person because it is one. A lead that is still at New a day after it arrived, on a site that promises a reply within 24 hours, is the clearest signal there is that the promise has broken somewhere. It does not matter whether the cause is a notification, a holiday or a busy week. The status column is where the promise is either kept or visibly not kept.

An enquiry path has five places where it can go quiet

Lampstand’s problem sat in the notification step, but it is worth seeing the whole path, because each stage has its own way of failing without saying so.

Stage How it fails quietly What should tell you
The form A field rejects something valid, or a spam filter eats real people A visible error for the visitor, and a count of rejections you actually look at
Storage The save fails under load, or two submissions collide The visitor sees an error instead of a thank-you; on Lampstand, a clash retries rather than dropping the enquiry
Notification The address is wrong, unread, or belongs to someone who left A record on the lead of who was told, and a failure you can see and resend
The pipeline A lead is seen once, then forgotten under newer ones Status that only moves when a person moves it, and a New column that stays short
The reply Replies go to a machine address, or from one that lands in spam A Reply-To that reaches a person, tested by actually replying

That is not every way an enquiry can be lost. It covers the ones I keep meeting. The pattern across all five is the same: the failure is silent because nothing downstream expects to hear anything. The fix is also the same. Make each stage leave a mark that a person will see, in the place they already work.

Send yourself an enquiry from your phone this week

You do not need to be technical to test this, and you do not need to wait for your developer. You need a phone and about ten minutes of attention spread over a day.

Submit your own form from your phone, on mobile data, using a personal address your team will not recognise. Use a plausible enquiry, not “test”. Note the time. Then say nothing and see how long it takes for someone on your team to mention it or reply. That number, not the thank-you message, is your real response time.

Reply to the confirmation email you receive. See where your reply lands. If it disappears into an address nobody reads, every customer who replies to it has been talking to nobody.

Ask who receives the notifications, by name. Not “the office”. Which inbox, and who reads it on a Monday morning. If the answer is one person, ask what happens when they are on holiday. If the answer takes a while to arrive, you have learned something important about your enquiry path already.

Look at your oldest unanswered enquiry. If your system has statuses, find the lead that has been sitting at the first stage longest. If it is older than the reply time your website promises, the promise is already broken, whatever the software says.

Then repeat the first test whenever anything changes: a new staff member, someone leaving, a new email setup, a website update. Those are exactly the moments a notification address goes stale.

Pro tip

Ask whoever looks after your website one question: “If a lead notification failed yesterday, where would I see it?” If the answer involves them reading a log on a server, you would not see it. You would find out from a customer, or not at all.

A promise on the contact page needs machinery that can fail out loud

The lesson I took from Lampstand is not about email. It is about what counts as working. A form that accepts an enquiry, stores it and hands a message to a mail service has done everything its code asks of it, and can still have told nobody. The transport accepting a message is not the team receiving it.

This was found by someone testing the form by hand, which is the right instinct and the wrong detection method. When a person finds a fault, the fix is not finished until something would have caught it without that person. For Lampstand, that something is a record on every lead, a banner when a notification fails, a button to send it again, and a status pipeline that shows when a promise is running late. You can read about the rest of the platform in the Lampstand case study.

If your contact page makes a promise, somebody should be able to prove it is being kept. The thank-you message cannot do that. It is written before anyone knows.