Organise contact emails and confirmations reliably
Tie confirmation to persisted receipt and retry decisions to the transmission outcome actually observed.

Form receipt and email transmission are different states
A contact enquiry should not disappear because a mail server is temporarily unavailable. Equally, visible confirmation must not claim transmission before it happens. Treat receipt, persisted mail jobs and actual SMTP results separately. This makes the workflow understandable and helps investigate a missing message without immediately concluding that the original enquiry was lost or the responsible person already received it.
Our original fictional PHP module saves an enquiry together with its mail jobs in SQLite. A separate worker attempts transmission, while PHPMailer prepares the messages. Actual sending tests terminate exclusively at our own local SMTP fixture using synthetic addresses with non-deliverable demonstration domains. No enquiry is sent to a customer, employee or unrelated recipient, and no production mailbox is accessed.
The experiment has 56 conditions, twelve real local SMTP connections and seven fully captured message payloads. It checks acceptance, temporary rejection, permanent rejection and uncertainty after connection loss. It establishes neither real inbox delivery nor reading. These boundaries determine the status model below and prevent a technical intermediate result from becoming an unsupported statement about what another person has seen or done.
Confirmation should name what actually happened
After successful storage, the interface may say the enquiry was recorded. It should not automatically claim the responsible person received a message or began handling it. Those later claims need different evidence. Our acceptance result is therefore called recorded. It is returned only after the database transaction commits, not immediately after the program reads values from a submitted form.
The database stores the enquiry and corresponding mail jobs together. Failure produces no successful receipt state. A real HTTP caller needs suitable error handling and a useful correction route that preserves appropriate entered content. The displayed module supplies the technical core, not a complete visitor interface, authentication flow or form-action protection mechanism already accepted for a publicly deployed site.
Business handling should additionally have a reliable route to persisted enquiries. A transport message should not be the only representation of an incoming request. This small experiment implements no administrative inbox, but establishes the stored state on which one could be developed. The existence of a database row is not presented as an already organised customer-service operation or a staffed processing guarantee.
Keep sender identity separate from reply routing
The application uses a fixed sender address. The visitor-supplied address appears in Reply-To on the internal message, rather than becoming the application’s technical sender. That fixed identity belongs to the configured integration and must not be replaced by an arbitrary form value. Reply routing and sender authority are related operational details but do not mean the same thing.
This bounded contract accepts suitable ASCII email addresses only. Controls and whitespace in the address are rejected before PHP’s address filter is applied. This deliberately excludes internationalised addresses and is not a universal email-address implementation. Products requiring broader address support need it developed together with the real transport, rather than enabling a parser option and assuming the sending service will support everything it accepts.
Syntactic suitability proves neither mailbox existence nor ownership by the submitter. Example addresses intentionally use invalid demonstration domains accepted only by the local fixture. Production release additionally needs the actual authorised sender, suitable transport and observed delivery behaviour. This experiment changes no DNS records and connects no live mail credentials, so its findings should not be interpreted as a real domain-deliverability assessment.
Free message text remains content, not headers
The application fixes the subject instead of constructing it directly from unrestricted input. Name and enquiry belong in the body. PHPMailer generates MIME with plain-text and HTML alternatives, and user characters are escaped for the HTML context. An executed fixture contains script-like text, which remains text in the HTML part rather than becoming an active script element in the prepared message.
Line breaks in a reply address have different significance from line breaks in message content, so the address contract rejects them. Body lines remain content. The SMTP fixture also tests a line beginning with a dot and observes it correctly after transmission. The established library handles the transfer encoding and SMTP-specific dot handling rather than requiring an improvised header-and-body string assembled from raw user input.
These particular cases do not establish every future template change. An added unescaped link or copied header could introduce another problem. Review templates using actual input characters and generated MIME parts. Friendly visual design does not replace context separation or a readable text alternative, and a successful local capture does not prove identical rendering across every external mail client or filtering system.
Create requested confirmation as a separate job
By default, acceptance creates only the internal message job. A second message requires the explicitly true boolean confirmation option. A string such as yes is not casually treated as the same decision, and the tests check that distinction. An ordinary enquiry does not thereby acquire arbitrary additional communication purposes, and a technical convenience should not silently change what the visitor actually requested.
Confirmation has its own recipient, message identifier and fixed template. It says only that the enquiry was recorded, not that someone has handled it, and does not reproduce the entire unchecked free-text request. Internal delivery and visitor confirmation can have different transport outcomes. They are therefore persisted separately instead of collapsed into one indistinguishable success flag that cannot explain which message encountered a problem.
Production confirmation also needs protection against abuse using someone else’s address and excessive submissions. A visible option alone does not provide that protection. A requested receipt message is likewise not blanket enrolment for unrelated future marketing. This local experiment sends only fictional content into its own fixture and offers no legal approval of a real communication concept, audience or retention policy.
Persist the prepared message before transmission
Every job receives a fixed Message-ID at creation. The prepared MIME message is stored with it, so retries use the same message instead of generating a new apparent first attempt each time. An actual SMTP attempt after temporary rejection verifies that the captured header retains the saved identifier. This supports traceability between the queue record, attempts and the particular message prepared for that enquiry.
An identifier is not a universal guarantee against duplicate delivery. A server may already accept content while its confirmation is lost. The same Message-ID does not force every destination to discard every repetition. The worker therefore needs an explicitly uncertain state rather than translating any connection failure into another automatic attempt. Operational uncertainty remains real even when message identity is stable and carefully recorded.
Stored MIME contains the enquiry itself and needs appropriate protection. It belongs neither in a public directory nor a general diagnostic log. Real retention and editing rights follow the application’s actual requirements. This fixture contains only original artificial text and synthetic contact details. Persisting a useful message record does not authorise keeping real customer correspondence indefinitely or duplicating it into unrelated project artifacts.
Read the complete PHP acceptance module and worker
The code below uses PDO with SQLite and PHPMailer for message preparation. The application must load the library before using it; the executed version was PHPMailer 7.2.0. A real integration can use its established package-management process. This module does not modify an existing production mail route or borrow credentials from another application merely to make the example run.
The worker claims one due job inside an immediate database transaction, then calls transport outside it. Competing workers cannot both claim the same pending record. The test actually starts two independent PHP processes: one claims the job and exactly one local SMTP session occurs for that case. This establishes the specific queue claim boundary, not atomicity between the database and an independent mail server.
<?php
declare(strict_types=1);
use PHPMailer\PHPMailer\PHPMailer;
function contactDatabase(string $path): PDO {
$db=new PDO('sqlite:'.$path,null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$db->exec('PRAGMA foreign_keys=ON');$db->exec('PRAGMA busy_timeout=5000');
$db->exec('CREATE TABLE IF NOT EXISTS submissions(id TEXT PRIMARY KEY,name TEXT NOT NULL,email TEXT NOT NULL,body TEXT NOT NULL,created_at INTEGER NOT NULL)');
$db->exec("CREATE TABLE IF NOT EXISTS mail_jobs(id TEXT PRIMARY KEY,submission_id TEXT NOT NULL REFERENCES submissions(id),kind TEXT NOT NULL,recipient TEXT NOT NULL,state TEXT NOT NULL,attempts INTEGER NOT NULL DEFAULT 0,next_at INTEGER NOT NULL,message_id TEXT NOT NULL,eml TEXT NOT NULL,error_code INTEGER,UNIQUE(submission_id,kind))");
return $db;
}
function contactMessage(string $id,string $submissionId,string $kind,array $data,int $now): array {
$mail=new PHPMailer(true);$mail->CharSet='UTF-8';$mail->Encoding='quoted-printable';
$mail->setFrom('noreply@demo.invalid','Kontaktbeispiel');
$recipient=$kind==='operator'?'anfragen@demo.invalid':$data['email'];
$mail->addAddress($recipient);
$mail->addReplyTo($kind==='operator'?$data['email']:'anfragen@demo.invalid');
$mail->Subject=$kind==='operator'?'Neue Kontaktanfrage':'Ihre Kontaktanfrage wurde erfasst';
$mail->MessageID='<'.$id.'@demo.invalid>';
$mail->MessageDate=gmdate('D, d M Y H:i:s +0000',$now);
$mail->isHTML(true);
$plain=$kind==='operator'?"Name: ".$data['name']."\nAntwortadresse: ".$data['email']."\n\n".$data['body']
:"Ihre Anfrage wurde erfasst.\nVorgang: ".$submissionId."\nDies ist keine Zusage einer bereits erfolgten Bearbeitung.";
$mail->AltBody=$plain;
$mail->Body='<h1>'.htmlspecialchars($mail->Subject,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8').'</h1><p>'.nl2br(htmlspecialchars($plain,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8'),false).'</p>';
$mail->preSend();
return ['recipient'=>$recipient,'message_id'=>$mail->MessageID,'eml'=>$mail->getSentMIMEMessage()];
}
function recordContact(PDO $db,array $input,int $now): array {
foreach(['name','email','body'] as $field)if(!isset($input[$field])||!is_string($input[$field]))throw new InvalidArgumentException('Ungültige Eingabe.');
$data=['name'=>trim($input['name']),'email'=>trim($input['email']),'body'=>trim($input['body'])];
$confirmation=$input['confirmation']??false;
if(!is_bool($confirmation)||$now<0||strlen($data['name'])<1||strlen($data['name'])>240
||strlen($data['body'])<20||strlen($data['body'])>8000||strlen($data['email'])>254
||preg_match('/[\x00-\x20\x7f-\xff]/',$data['email'])||!filter_var($data['email'],FILTER_VALIDATE_EMAIL))
throw new InvalidArgumentException('Ungültige Kontaktangaben.');
foreach($data as $value)if(!preg_match('//u',$value))throw new InvalidArgumentException('Ungültiges UTF-8.');
$submissionId=bin2hex(random_bytes(16));$prepared=[];
foreach($confirmation?['operator','confirmation']:['operator'] as $kind) {
$id=bin2hex(random_bytes(16));$prepared[]=['id'=>$id,'kind'=>$kind]+contactMessage($id,$submissionId,$kind,$data,$now);
}
$db->exec('BEGIN IMMEDIATE');
try {
$db->prepare('INSERT INTO submissions VALUES(?,?,?,?,?)')->execute([$submissionId,$data['name'],$data['email'],$data['body'],$now]);
$insert=$db->prepare("INSERT INTO mail_jobs(id,submission_id,kind,recipient,state,next_at,message_id,eml) VALUES(?,?,?,?,'pending',?,?,?)");
foreach($prepared as $job)$insert->execute([$job['id'],$submissionId,$job['kind'],$job['recipient'],$now,$job['message_id'],$job['eml']]);
$db->exec('COMMIT');
} catch(Throwable $error){$db->exec('ROLLBACK');throw $error;}
return ['submission_id'=>$submissionId,'status'=>'recorded','mail_jobs'=>count($prepared)];
}
function runContactJob(PDO $db,callable $transport,int $now): ?array {
$db->exec('BEGIN IMMEDIATE');
try {
$query=$db->prepare("SELECT * FROM mail_jobs WHERE state='pending' AND next_at<=? ORDER BY rowid LIMIT 1");$query->execute([$now]);$job=$query->fetch(PDO::FETCH_ASSOC);
if(!$job){$db->exec('COMMIT');return null;}
$db->prepare("UPDATE mail_jobs SET state='sending',attempts=attempts+1 WHERE id=?")->execute([$job['id']]);$db->exec('COMMIT');
}catch(Throwable $error){$db->exec('ROLLBACK');throw $error;}
try{$result=$transport($job);}catch(Throwable $error){$result=['outcome'=>'uncertain','code'=>0];}
$outcome=$result['outcome']??'uncertain';$attempts=(int)$job['attempts']+1;
$state=match($outcome){'accepted'=>'accepted_by_smtp','failed'=>'failed','retryable'=>$attempts>=3?'failed':'pending',default=>'uncertain'};
$next=$state==='pending'?$now+60*$attempts:$now;
$code=is_int($result['code']??null)?$result['code']:0;
$db->prepare('UPDATE mail_jobs SET state=?,next_at=?,error_code=? WHERE id=?')->execute([$state,$next,$code,$job['id']]);
return ['job_id'=>$job['id'],'state'=>$state,'attempts'=>$attempts,'code'=>$code];
}
SMTP acceptance does not mean a read message
After the positive SMTP result for supplied message data, the worker records accepted_by_smtp. The wording is deliberate: it identifies acceptance by that counterpart, not reading by a human. Later delivery problems or other processing can still occur. The state must not automatically complete the contact enquiry’s business handling, and it should not be displayed as proof that the intended person has seen the content.
The local fixture provides the positive response and captures the actual message. Checks inspect sender, recipient, Reply-To, Message-ID and both content alternatives. These observations establish neither rendering in every mail program nor filtering decisions at a genuine provider. Such questions need appropriate accounts and authorised integration tests separately, including the intended sender configuration and actual route to the destination.
Visitor feedback should preserve those distinctions. The site can confirm persisted receipt. A separately requested message may later be accepted by the SMTP service. Whether someone notices it is another question. Clear boundaries avoid false promises and support useful investigation: an absent inbox item does not automatically imply a lost form submission, just as a recorded enquiry does not prove mail transmission.
Handle temporary and permanent rejection differently
In one controlled case, the fixture temporarily rejects the recipient. The worker leaves the job pending and schedules another attempt. It transmits nothing before that time. Afterwards the same saved message succeeds. Attempts are counted, and the captured retry retains the original Message-ID. This provides an observable example of bounded retry behaviour rather than a hidden loop that immediately repeats until something appears successful.
Permanent rejection instead produces failed with no automatic endless loop. Repeated temporary rejection is also bounded to three attempts. These intervals and the limit are demonstration product rules, not requirements for every mail provider. A real integration chooses its strategy according to the actual transport contract and operational need, including how unresolved cases become visible to the responsible role.
An operational view should make these cases discoverable through meaningful identifiers and bounded reasons rather than copying every payload into every alert. The persisted enquiry remains independent of transport failure. The actual application still needs a way for staff to begin manual handling. This experiment implements neither a complete support dashboard nor automatic escalation merely because its queue records distinguish failures.
Do not guess away uncertainty after DATA
A critical counterexample captures the complete message at the local SMTP fixture, then closes the connection before positive completion. PHP transport can no longer confidently establish the counterpart’s acknowledged acceptance. The job becomes uncertain, and a later ordinary worker invocation does not automatically send it again. The test directly observes both captured content and the absence of a blind additional attempt.
This avoids equating missing confirmation with a definitely absent effect. It does not claim the experiment already knows real inbox delivery. The fixture proves only that its side had the payload. Operations need an appropriate investigation route, identifiers and, where necessary, a deliberately authorised next action. Uncertainty is a real state to resolve, not an inconvenience to relabel as success or definite failure.
Another gap occurs after positive SMTP acceptance but before the final queue-state update. The test deliberately causes a database failure there. The job remains sending and is not blindly reclaimed either. Production needs a defined recovery procedure for that situation. A timeout alone must not automatically transform a possibly accepted message into a supposedly safe new transmission that could create duplicates.
Treat transport as a separate integration
The complete local transport function below is explicitly a test adapter. It connects only to the loopback receiver and uses no credentials. Production needs its actual configured SMTP or API transport with appropriate security and error rules. The example is not an instruction to send unencrypted public mail through arbitrary servers or to treat a local fixture connection as evidence that an external provider is correctly configured.
PHPMailer handles both message preparation and concrete SMTP communication in the test. Responses are interpreted at the relevant protocol step. Failure before data transmission differs from a lost completion acknowledgement afterwards. Another adapter must retain that distinction; a single undifferentiated mail failed string would be too vague to support the retry decisions demonstrated here. Meaningful classification belongs alongside the real transport behaviour.
Production acceptance includes the real domain, authorised sender, encrypted transport, provider configuration and observed responses. This article supplies no complete DNS setup or delivery percentage. It demonstrates the controlled decision path. The library version and bounded arrangement are documented so the evidence is not transferred without review to a differently configured production mail service with different acceptance, queueing or retry semantics.
<?php
declare(strict_types=1);
use PHPMailer\PHPMailer\SMTP;
// Isolated SMTP fixture only. Real deployments need their actual TLS/auth transport.
function deliverToLoopback(array $job,int $port): array {
if($port<1||$port>65535)throw new InvalidArgumentException('Ungültiger Testport.');
$smtp=new SMTP();$smtp->Timeout=3;$smtp->Timelimit=3;$phase='connect';
try {
if(!$smtp->connect('127.0.0.1',$port,3)||!$smtp->hello('demo.invalid'))return ['outcome'=>'retryable','code'=>(int)($smtp->getError()['smtp_code']??0)];
$phase='envelope';
if(!$smtp->mail('noreply@demo.invalid')||!$smtp->recipient($job['recipient'])) {
$code=(int)($smtp->getError()['smtp_code']??0);return ['outcome'=>$code>=500?'failed':'retryable','code'=>$code];
}
$phase='data';
if($smtp->data($job['eml']))return ['outcome'=>'accepted','code'=>250];
$code=(int)($smtp->getError()['smtp_code']??0);
return ['outcome'=>$code>=500?'failed':($code>=400?'retryable':'uncertain'),'code'=>$code];
}catch(Throwable $error){return ['outcome'=>$phase==='data'?'uncertain':'retryable','code'=>0];}
finally{$smtp->close();}
}

Review 56 conditions through twelve local SMTP sessions
Execution uses PHP 8.4.27, PHPMailer 7.2.0 and an original local SMTP fixture. Every address is synthetic. Twelve real SMTP sessions yield seven fully captured message payloads. Fifty-six conditions cover fixed sender, separate reply address, optional confirmation, plain text, HTML escaping, dot handling and retained Message-ID on retry. Stored queue state is observed through separate PHP invocations, not assumed from the caller’s memory.
Input cases include injected header line breaks, an address outside the ASCII contract, unsuitable syntax, empty name, short or oversized message and an incorrect option type. Rejected inputs leave no partial enquiry. Separate cases examine temporary and permanent rejection, the maximum attempt count and duplicate-claim prevention across two PHP processes. These distinctions are based on actual outcomes from the executed fixture rather than fabricated delivery metrics.
Post-DATA connection loss and database failure after acceptance expose limitations explicitly. The explanatory image uses actual returns. No external server or real inbox is tested. Evidence concerns persisted receipt, our queue and observed protocol states. A general delivery or handling guarantee would exceed that evidence, particularly where a recipient system, filtering process or human action lies outside the application’s control.
Dependable handling needs more than a polished template
A template should state the occasion clearly and make replies useful. Operations also need visibility into pending, failed and uncertain jobs. A friendly logo cannot supply that capability. Responsibility for an unprocessed enquiry and the technical meaning of a transmission state must remain distinguishable while fitting the real work process. Design, implementation and staff handling all participate in that contract.
Template changes deserve review of both body alternatives and actual generated headers. Transport changes require repeating affected failure cases. Recovery needs enquiry and mail-job records to remain consistent. An old backup must not silently make already transmitted jobs appear new. These transitions are operational responsibilities, not merely formatting concerns that can be settled by one successful message preview or a green success badge.
Contact mail becomes manageable when receipt, transmission and business handling have clear boundaries. This executed module provides a bounded foundation. A real site connects it with form protection, actual transport and a working handling interface. Each confirmation can then remain tied to what the application genuinely persisted or observed, without confusing a useful intermediate acknowledgement with completed human attention or guaranteed inbox delivery.
States in the executed experiment
| Outcome | Meaning |
|---|---|
| recorded | Enquiry and mail jobs persisted |
| accepted_by_smtp | SMTP counterpart accepted, not a read receipt |
| pending after 450 | Bounded later retry planned |
| failed after 550 | No blind automatic retry |
| uncertain after message data | Outcome unclear, review required |
| sending after status-write failure | Investigate possible acceptance before resending |
Contact-mail checklist
- Distinguish recorded receipt, transmission and handling.
- Use an authorised fixed sender and separate reply address.
- Define address syntax and supported characters explicitly.
- Separate headers from free content fields.
- Review plain text and escaped HTML together.
- Create requested confirmation as a separate job.
- Persist message and stable Message-ID before attempting.
- Prevent duplicate worker claims.
- Distinguish temporary, permanent and uncertain outcomes.
- Review real transport, recovery and manual investigation separately.
Sources and review scope
Primary sources reviewed on 9 November 2026. PHP 8.4.27 and PHPMailer 7.2.0, 56 executed conditions, twelve local SMTP sessions and seven captured synthetic payloads. No external recipient contacted and no real inbox delivery measured.







