Starting Point — The mail() Version & Why We Upgrade It
If you already have a working contact form using PHP's built-in mail() function, this section is for you. Your form likely looks something like this:
<?php if ($_SERVER["REQUEST_METHOD"] == "POST") { $first_name = $_POST['first-name']; $last_name = $_POST['last-name']; $email = $_POST['email']; $message = $_POST['message']; $to = "you@yourdomain.com"; $subject = "New Contact Form Submission"; $body = "From: {$first_name} {$last_name}\nEmail: {$email}\n\n{$message}"; $headers = "From: {$email}\nReply-To: {$email}"; if (mail($to, $subject, $body, $headers)) { echo "<p>Thank you! We will get back to you soon.</p>"; } else { echo "<p>Sorry, something went wrong.</p>"; } }
- cPanel shared hosting (most providers configure Sendmail for you)
- Servers with Postfix or Sendmail properly set up
- Low-volume forms (a few submissions per day)
- Forms where landing in spam occasionally is acceptable
- VPS or dedicated servers without a configured MTA
- Email consistently landing in spam / junk folders
- Hosting providers that block outbound port 25
- When you need delivery confirmation or error details
- High volume — ISPs rate-limit unverified mail servers
mail() is a perfectly valid solution when it works. PHPMailer + SMTP relay is an alternative for when mail() is not working or emails are going to junk. Either way your reCAPTCHA verification, field collection, honeypot check, and email body code are all identical — only the actual send block at the very bottom changes.
Why PHP's mail() Fails & What to Use Instead
PHP's built-in mail() function has been the default contact form solution for decades — and it fails silently on most modern hosting environments.
The Problem with mail()
- Requires a local MTA —
mail()hands the message to the server's local mail transfer agent. Most shared hosting providers either do not have one configured or have it heavily restricted. - Lands in spam — Even when it works, email sent via
mail()has no SPF, DKIM, or DMARC authentication. - No error reporting —
mail()returnstrueeven when the message was silently dropped. - Header injection risk — Passing user-submitted data directly into headers opens your form to email header injection attacks.
The Solution — PHPMailer + SMTP Relay
PHPMailer connects directly to an authenticated SMTP relay — Gmail, Outlook, SendGrid, or any other provider — using your credentials over an encrypted TLS connection.
process_form.php. Section 3 covers storing credentials safely in a config file outside the web root.
Installing PHPMailer
Option A — Install via Composer (Recommended)
curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer sudo chmod +x /usr/local/bin/composer composer --version
cd /var/www/example.com/public_html composer require phpmailer/phpmailer
.htaccess to block direct browser access to the vendor folder:
<IfModule mod_rewrite.c> RewriteEngine On RewriteRule ^vendor/ - [F,L] </IfModule>
Option B — Manual Download (No Composer)
- Go to github.com/PHPMailer/PHPMailer and download the latest release ZIP.
- Extract it. You only need:
src/PHPMailer.php,src/SMTP.php, andsrc/Exception.php. - Upload these three files to a
PHPMailer/subfolder inside your website directory. - In your
process_form.php, require them manually instead of the Composer autoloader.
Hashed Credentials Config File
The safest way to store SMTP credentials is in a separate config file that lives outside your web root and stores sensitive values as encoded strings rather than plaintext.
/var/www/example.com/ ├── config/ │ └── mail_config.php ← credentials live here (NOT public) └── public_html/ ← web root — publicly accessible ├── index.php ├── contact.html ├── process_form.php └── vendor/
sudo mkdir -p /var/www/example.com/config sudo chown www-data:www-data /var/www/example.com/config sudo chmod 750 /var/www/example.com/config
php -r "echo base64_encode('yourapppasswordhere') . PHP_EOL;"
<?php // Which provider to use: 'gmail' | 'outlook' | 'sendgrid' define('MAIL_PROVIDER', 'gmail'); define('GMAIL_USER', 'youraddress@gmail.com'); define('GMAIL_PASS_ENC', 'eW91cmFwcHBhc3N3b3JkaGVyZQ=='); // base64 encoded define('OUTLOOK_USER', 'youraddress@outlook.com'); define('OUTLOOK_PASS_ENC','eW91cnBhc3N3b3JkaGVyZQ=='); define('SENDGRID_API_ENC','U0cueW91cmFwaWtleWhlcmU='); define('SENDGRID_FROM', 'youraddress@yourdomain.com'); define('MAIL_TO', 'youraddress@gmail.com'); define('MAIL_TO_NAME', 'Your Name'); define('RECAPTCHA_SECRET', 'your_recaptcha_secret_key_here'); function decode_credential($encoded) { return base64_decode($encoded); }
config/mail_config.php to your .gitignore before your first commit. If you accidentally commit credentials, rotate them immediately by generating a new app password or API key.
Gmail SMTP Setup — App Password ✓ Updated March 2026
Step 1 — Enable 2-Factor Authentication
- Go to myaccount.google.com and sign in.
- Click Security in the left navigation — or use the search bar at the top of the page and type
2-Step Verificationto jump straight there. - Under "How you sign in to Google", click 2-Step Verification and follow the prompts to enable it.
- Google Workspace users only: Your admin must first enable App Passwords in the Admin Console under Security → Authentication → 2-Step Verification → Allow users to generate app passwords. Without this step, the App Passwords option will not appear even after enabling personal 2FA — contact your Workspace admin if you don't see it.
Step 2 — Generate an App Password
- Go directly to myaccount.google.com/apppasswords (2FA must be enabled or this page will not exist — Google may prompt you to re-authenticate before continuing).
- In the App name field, type a descriptive label like
PHP Contact Form. Note: the old two-dropdown interface (separate "App" and "Device" selectors) no longer exists — there is now just a single name field. - Click Create. Google shows you a 16-character password like
abcd efgh ijkl mnop. - Copy it immediately — Google will never show it again. Remove the spaces before encoding it.
- Base64-encode it and store it in
mail_config.phpasGMAIL_PASS_ENC.
GMAIL_PASS_ENC in mail_config.php — otherwise your contact form will silently stop sending email.
Gmail SMTP Settings
Host: smtp.gmail.com
Port: 587 (STARTTLS — recommended)
465 (SSL — alternative)
Encryption: STARTTLS (port 587) or SSL/TLS (port 465)
Username: your full Gmail address
Password: App Password (16 characters, no spaces)
PHPMailer Configuration for Gmail
$mail->isSMTP(); $mail->Host = 'smtp.gmail.com'; $mail->SMTPAuth = true; $mail->Username = GMAIL_USER; $mail->Password = decode_credential(GMAIL_PASS_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587;
you@yourbusiness.com, the App Password process is identical — same SMTP host and port. Your username is your full Workspace email address.
Outlook / Microsoft 365 SMTP Setup
Enable SMTP AUTH for Your Account
- For Outlook.com: Go to outlook.live.com → Settings → Mail → Sync email and ensure "POP and IMAP" is enabled.
- For Microsoft 365 Business: An admin must go to the Microsoft 365 Admin Center → Users → Active users, select the mailbox, go to Mail → Manage email apps, and enable Authenticated SMTP.
Host: smtp.office365.com (Microsoft 365 / Outlook.com) Port: 587 Encryption: STARTTLS Username: your full email address Password: your account password (or App Password if MFA enabled)
$mail->isSMTP(); $mail->Host = 'smtp.office365.com'; $mail->SMTPAuth = true; $mail->Username = OUTLOOK_USER; $mail->Password = decode_credential(OUTLOOK_PASS_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587;
SendGrid API Setup
SendGrid is a dedicated transactional email service — the best choice for high-volume forms and professional deliverability. The free tier allows 100 emails per day permanently.
- Go to sendgrid.com and create a free account.
- In the dashboard, go to Settings → API Keys → Create API Key. Set permission to Restricted Access → Mail Send → Full Access. Click Create & View.
- Copy the API key immediately — SendGrid shows it only once. Base64-encode it and store it in
mail_config.php. - Go to Settings → Sender Authentication and verify your sender email address or domain.
Host: smtp.sendgrid.net Port: 587 (STARTTLS — recommended) Username: apikey ← literally the word "apikey", not your email Password: your SendGrid API key
$mail->isSMTP(); $mail->Host = 'smtp.sendgrid.net'; $mail->SMTPAuth = true; $mail->Username = 'apikey'; // Always literally 'apikey' $mail->Password = decode_credential(SENDGRID_API_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587;
Honeypot Spam Trap
A honeypot is a hidden form field that human visitors never see or fill in — but spam bots do. When the honeypot field is populated on submission, silently reject it.
<div style="position:absolute;left:-9999px;top:-9999px;opacity:0;height:0;width:0;overflow:hidden;" aria-hidden="true"> <input type="text" name="website" tabindex="-1" autocomplete="off"> </div>
if (!empty($_POST['website'])) { // Silent rejection — return fake success so bot doesn't retry echo '<p>Thank you! We will be in touch soon.</p>'; exit; }
display:none or visibility:hidden — some bots detect and skip those. The off-screen CSS pushes it out of view for humans while keeping it technically present in the DOM.
Google reCAPTCHA v2
reCAPTCHA v2 adds the "I'm not a robot" checkbox. It's a second layer on top of the honeypot — the honeypot catches automated bots, reCAPTCHA catches more sophisticated ones.
Step 1 — Register Your Site
- Go to google.com/recaptcha/admin/create and sign in.
- Select reCAPTCHA v2 → "I'm not a robot" Checkbox.
- Add your domain (e.g.
anfamily.cloud). Also addlocalhostfor local testing. - Click Submit. You receive two keys: Site Key (goes in HTML — public) and Secret Key (goes in
mail_config.phpasRECAPTCHA_SECRET— never expose).
Add reCAPTCHA to Your HTML Form
<!-- Place just before your submit button --> <div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY_HERE"></div> <button type="submit">Submit</button> <!-- Load reCAPTCHA script at bottom of page --> <script src="https://www.google.com/recaptcha/api.js" async defer></script>
Verify reCAPTCHA in PHP
$recaptcha_response = trim($_POST['g-recaptcha-response'] ?? ''); if (empty($recaptcha_response)) { json_error('Please complete the reCAPTCHA.'); } $rv = json_decode(file_get_contents( 'https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode(RECAPTCHA_SECRET) . '&response=' . urlencode($recaptcha_response) . '&remoteip=' . urlencode($_SERVER['REMOTE_ADDR']) ), true); if (empty($rv['success'])) { json_error('reCAPTCHA failed. Please try again.'); }
The Complete process_form.php
MAIL_PROVIDER from your config file and uses the correct SMTP settings automatically. To switch from Gmail to SendGrid, change one line in mail_config.php.
<?php error_reporting(0); ini_set('display_errors', 0); // Load config (outside web root) $config_path = dirname(__DIR__) . '/config/mail_config.php'; if (!file_exists($config_path)) { http_response_code(500); die('Server configuration error.'); } require_once $config_path; // Load PHPMailer — Option A: Composer autoloader (recommended) require_once __DIR__ . '/vendor/autoload.php'; // Option B: Manual include (no Composer) // require_once __DIR__ . '/PHPMailer/PHPMailer.php'; // require_once __DIR__ . '/PHPMailer/SMTP.php'; // require_once __DIR__ . '/PHPMailer/Exception.php'; use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); die('Method not allowed.'); } function json_error($msg, $code = 400) { http_response_code($code); header('Content-Type: application/json'); echo json_encode(['success' => false, 'message' => $msg]); exit; } function clean($val) { return htmlspecialchars(strip_tags(trim($val)), ENT_QUOTES, 'UTF-8'); } // 1. Honeypot check if (!empty($_POST['website'])) { header('Content-Type: application/json'); echo json_encode(['success' => true, 'message' => 'Thank you! We will be in touch.']); exit; } // 2. reCAPTCHA verification $recaptcha_response = trim($_POST['g-recaptcha-response'] ?? ''); if (empty($recaptcha_response)) { json_error('Please complete the reCAPTCHA.'); } $rv = json_decode(file_get_contents( 'https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode(RECAPTCHA_SECRET) . '&response=' . urlencode($recaptcha_response) . '&remoteip=' . urlencode($_SERVER['REMOTE_ADDR']) ), true); if (empty($rv['success'])) { json_error('reCAPTCHA failed. Please try again.'); } // 3. Collect and sanitize fields $first_name = clean($_POST['first-name'] ?? ''); $last_name = clean($_POST['last-name'] ?? ''); $phone = clean($_POST['phone'] ?? ''); $email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL); $message = clean($_POST['message'] ?? ''); if (empty($first_name) || empty($last_name) || empty($email) || empty($message)) { json_error('Please fill in all required fields.'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { json_error('Please enter a valid email address.'); } $services = ''; if (!empty($_POST['services']) && is_array($_POST['services'])) { $services = implode(', ', array_map('htmlspecialchars', $_POST['services'])); } // 4. Build HTML email body $body = "<html><body style='font-family:Arial,sans-serif;max-width:600px;'> <h2 style='color:#3b1f7a;border-bottom:2px solid #ede9fe;padding-bottom:8px;'>New Contact Form Submission</h2> <table style='width:100%;border-collapse:collapse;'> <tr><td style='padding:8px;font-weight:bold;color:#4b5563;width:140px;'>Name</td> <td style='padding:8px;'>{$first_name} {$last_name}</td></tr> <tr style='background:#f3f0ff;'> <td style='padding:8px;font-weight:bold;color:#4b5563;'>Email</td> <td style='padding:8px;'><a href='mailto:{$email}'>{$email}</a></td></tr> <tr><td style='padding:8px;font-weight:bold;color:#4b5563;'>Phone</td> <td style='padding:8px;'>{$phone}</td></tr>"; if (!empty($services)) { $body .= "<tr style='background:#f3f0ff;'><td style='padding:8px;font-weight:bold;color:#4b5563;'>Services</td><td style='padding:8px;'>{$services}</td></tr>"; } $body .= "</table><h3 style='color:#3b1f7a;margin-top:20px;'>Message</h3> <div style='background:#f3f0ff;padding:16px;border-radius:6px;border-left:4px solid #8b5cf6;white-space:pre-wrap;'>{$message}</div> <p style='color:#9ca3af;font-size:12px;margin-top:20px;'>Submitted: " . date('Y-m-d H:i:s T') . " · IP: " . htmlspecialchars($_SERVER['REMOTE_ADDR']) . "</p></body></html>"; // 5. Send via PHPMailer try { $mail = new PHPMailer(true); switch (MAIL_PROVIDER) { case 'gmail': $mail->isSMTP(); $mail->Host = 'smtp.gmail.com'; $mail->SMTPAuth = true; $mail->Username = GMAIL_USER; $mail->Password = decode_credential(GMAIL_PASS_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; $from_addr = GMAIL_USER; break; case 'outlook': $mail->isSMTP(); $mail->Host = 'smtp.office365.com'; $mail->SMTPAuth = true; $mail->Username = OUTLOOK_USER; $mail->Password = decode_credential(OUTLOOK_PASS_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; $from_addr = OUTLOOK_USER; break; case 'sendgrid': default: $mail->isSMTP(); $mail->Host = 'smtp.sendgrid.net'; $mail->SMTPAuth = true; $mail->Username = 'apikey'; $mail->Password = decode_credential(SENDGRID_API_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; $from_addr = SENDGRID_FROM; break; } $mail->setFrom($from_addr, 'Contact Form'); $mail->addAddress(MAIL_TO, MAIL_TO_NAME); $mail->addReplyTo($email, "{$first_name} {$last_name}"); $mail->isHTML(true); $mail->Subject = "New Contact: {$first_name} {$last_name}"; $mail->Body = $body; $mail->AltBody = "Name: {$first_name} {$last_name}\nEmail: {$email}\nPhone: {$phone}\n\nMessage:\n{$message}"; $mail->send(); header('Content-Type: application/json'); echo json_encode(['success' => true, 'message' => 'Thank you! We will be in touch soon.']); } catch (Exception $e) { error_log('PHPMailer error: ' . $mail->ErrorInfo); json_error('Message could not be sent. Please try again later.', 500); }
The Complete HTML Contact Form
Both form variations — checkbox (multiple selections) and dropdown (single selection) — include the honeypot, reCAPTCHA, and AJAX submit.
- The user might need more than one option at the same time
- Example: a notary form where someone needs Standard Notarization AND Mobile AND Certified Copies
- The user picks exactly one option — mutually exclusive choices
- Example: "How did you hear about us?" or a single service category
Checkbox Version — Multiple Selections
<form id="contactForm" action="process_form.php" method="POST"> <!-- Honeypot --> <div style="position:absolute;left:-9999px;opacity:0;height:0;overflow:hidden;" aria-hidden="true"> <input type="text" name="website" tabindex="-1" autocomplete="off"> </div> <div class="form-row"> <div class="form-group"> <label for="first-name">First Name <span class="req">*</span></label> <input type="text" id="first-name" name="first-name" autocomplete="given-name" required> </div> <div class="form-group"> <label for="last-name">Last Name <span class="req">*</span></label> <input type="text" id="last-name" name="last-name" autocomplete="family-name" required> </div> </div> <div class="form-group"> <label for="email">Email Address <span class="req">*</span></label> <input type="email" id="email" name="email" autocomplete="email" required> </div> <div class="form-group"> <label for="phone">Phone</label> <input type="tel" id="phone" name="phone" autocomplete="tel"> </div> <div class="form-group"> <label>Services Needed <span class="req">*</span></label> <!-- Add your checkboxes here with name="services[]" --> <label><input type="checkbox" name="services[]" value="Option A"> Option A</label> <label><input type="checkbox" name="services[]" value="Option B"> Option B</label> <label><input type="checkbox" name="services[]" value="Option C"> Option C</label> </div> <div class="form-group"> <label for="message">Message <span class="req">*</span></label> <textarea id="message" name="message" rows="5" required></textarea> </div> <div class="g-recaptcha" data-sitekey="YOUR_RECAPTCHA_SITE_KEY" style="margin-bottom:1rem;"></div> <div id="form-status" style="display:none;padding:0.75rem 1rem;border-radius:6px;margin-bottom:1rem;"></div> <button type="submit" id="submitBtn">Send Message</button> </form> <script src="https://www.google.com/recaptcha/api.js" async defer></script> <script> document.getElementById('contactForm').addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('submitBtn'); const status = document.getElementById('form-status'); btn.disabled = true; btn.textContent = 'Sending…'; status.style.display = 'none'; try { const res = await fetch('process_form.php', { method: 'POST', body: new FormData(e.target) }); const data = await res.json(); status.style.display = 'block'; if (data.success) { status.style.cssText = 'display:block;padding:.75rem 1rem;border-radius:6px;background:#f0fdf4;border:1px solid #86efac;color:#14532d;'; status.textContent = data.message; e.target.reset(); if (window.grecaptcha) grecaptcha.reset(); } else { status.style.cssText = 'display:block;padding:.75rem 1rem;border-radius:6px;background:#fef2f2;border:1px solid #fca5a5;color:#7f1d1d;'; status.textContent = data.message; if (window.grecaptcha) grecaptcha.reset(); } } catch (err) { status.style.cssText = 'display:block;padding:.75rem 1rem;border-radius:6px;background:#fef2f2;color:#7f1d1d;'; status.textContent = 'Network error. Please try again.'; } finally { btn.disabled = false; btn.textContent = 'Send Message'; } }); </script>
Dropdown Version — Single Selection
<div class="form-group"> <label for="service">Interested In <span class="req">*</span></label> <select id="service" name="service" required> <option value="">Select an option...</option> <option value="Option A">Option A</option> <option value="Option B">Option B</option> <option value="Option C">Option C</option> <option value="Other">Other</option> </select> </div> <!-- PHP: collect as $service = clean($_POST['service'] ?? ''); -->
implode(", ", $_POST['services']). Dropdowns send a single string: use clean($_POST['service'] ?? '') directly. That's the only difference in your PHP handler.
Deploying & Testing
Deployment Checklist
- Upload
process_form.phpand your HTML form to/var/www/example.com/public_html/ - Upload
mail_config.phpto/var/www/example.com/config/— confirm it is outside the public web root - Run
composer installin your public_html folder to install PHPMailer - Set correct permissions:File PermissionsBASH
sudo chmod 640 /var/www/example.com/config/mail_config.php sudo chown www-data:www-data /var/www/example.com/config/mail_config.php sudo chmod 644 /var/www/example.com/public_html/process_form.php
- Verify
mail_config.phpis NOT accessible from the browser —https://example.com/config/mail_config.phpshould return 403 or 404 - Submit a test form and check your inbox
Testing SMTP Without a Form
<?php error_reporting(E_ALL); ini_set('display_errors', 1); require_once dirname(__DIR__) . '/config/mail_config.php'; require_once __DIR__ . '/vendor/autoload.php'; use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception; $mail = new PHPMailer(true); $mail->SMTPDebug = 2; // Verbose output — shows full SMTP conversation $mail->isSMTP(); $mail->Host = 'smtp.gmail.com'; $mail->SMTPAuth = true; $mail->Username = GMAIL_USER; $mail->Password = decode_credential(GMAIL_PASS_ENC); $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; $mail->setFrom(GMAIL_USER, 'SMTP Test'); $mail->addAddress(MAIL_TO); $mail->Subject = 'SMTP Test'; $mail->Body = 'If you received this, SMTP is working.'; try { $mail->send(); echo 'Test email sent successfully.'; } catch (Exception $e) { echo 'Error: ' . $mail->ErrorInfo; } // DELETE THIS FILE immediately after testing
display_errors enabled and outputs full SMTP debug info. Delete it as soon as you confirm email is working.
Common Errors and Fixes
SMTP Error: Could not authenticate → Wrong username or App Password / spaces not removed from App Password → Gmail: make sure 2FA is on and you generated an App Password, not your login password Connection refused / Could not connect to host → Port 587 blocked by hosting provider — try port 465 → Check UFW: sudo ufw allow out 587/tcp 535 Authentication credentials invalid (SendGrid) → Username must be literally 'apikey' — not your email address stream_socket_enable_crypto(): SSL operation failed → OpenSSL PHP extension not installed → sudo apt install php8.3-curl php8.3-openssl mail_config.php not found → dirname(__DIR__) path incorrect — verify with: → php -r "echo dirname(__DIR__);" run from your public_html folder App Password stopped working (no config changes) → Your Google account password was changed — all App Passwords auto-revoke → Generate a new App Password at myaccount.google.com/apppasswords
Fixing the Portfolio Contact Form
Step 1 — Add the Form Action and Honeypot to portfolio.php
<!-- Change this: --> <form> <!-- To this: --> <form id="contactForm" action="process_form.php" method="POST"> <div style="position:absolute;left:-9999px;opacity:0;height:0;overflow:hidden;" aria-hidden="true"> <input type="text" name="website" tabindex="-1" autocomplete="off"> </div>
Step 2 — Add reCAPTCHA and Status Before Submit Button
<div class="form-group"> <div class="g-recaptcha" data-sitekey="YOUR_RECAPTCHA_SITE_KEY"></div> </div> <div id="form-status" style="display:none;padding:0.75rem 1rem;border-radius:6px;margin-bottom:1rem;"></div> <button type="submit" id="submitBtn" class="form-submit">Send Message →</button> <script src="https://www.google.com/recaptcha/api.js" async defer></script>
Step 3 — Add the AJAX Submit Script Before </body>
<script> const cf = document.getElementById('contactForm'); if (cf) { cf.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('submitBtn'); const status = document.getElementById('form-status'); btn.disabled = true; btn.textContent = 'Sending…'; status.style.display = 'none'; try { const res = await fetch('process_form.php', { method: 'POST', body: new FormData(e.target) }); const data = await res.json(); status.style.display = 'block'; if (data.success) { status.style.cssText = 'display:block;padding:.75rem 1rem;border-radius:6px;background:#f0fdf4;border:1px solid #86efac;color:#14532d;'; status.textContent = data.message; e.target.reset(); if (window.grecaptcha) grecaptcha.reset(); } else { status.style.cssText = 'display:block;padding:.75rem 1rem;border-radius:6px;background:#fef2f2;border:1px solid #fca5a5;color:#7f1d1d;'; status.textContent = data.message; if (window.grecaptcha) grecaptcha.reset(); } } catch(err) { status.style.cssText = 'display:block;padding:.75rem 1rem;border-radius:6px;background:#fef2f2;color:#7f1d1d;'; status.textContent = 'Network error. Please try again.'; } finally { btn.disabled = false; btn.textContent = 'Send Message →'; } }); } </script>
Step 4 — Create process_form.php with Portfolio Field Names
// Use these field names to match portfolio.php form inputs $first_name = clean($_POST['fname'] ?? ''); $last_name = clean($_POST['lname'] ?? ''); $email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL); $service = clean($_POST['service'] ?? ''); $message = clean($_POST['message'] ?? '');
Step 5 — Deploy All Files
- Upload
process_form.phpto your web root alongsideportfolio.php - Create
/var/www/anfamily.cloud/config/mail_config.phpwith your Gmail App Password encoded - Run
composer require phpmailer/phpmailerin your web root - Upload the updated
portfolio.phpwith form action, honeypot, reCAPTCHA widget, status div, and AJAX script - Run
smtp_test.phpto confirm email delivery, then delete it immediately - Submit a live test through the portfolio form