Aimhigh HTML Form 2 Mail
A simple plugin to handle submission of wordpress custom html forms and sending to a designated email address, using wordpress inbuilt email functionality.
by Eric Aimhigh · github.com/ericaimhigh1/aimhigh-html-form-2-mail · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/ericaimhigh1/aimhigh-html-form-2-mail/archive/refs/heads/master.zipAimhigh HTML Form 2 Mail (AHF2M)
AHF2M is a WordPress plugin that exposes a configurable public URL where visitors can POST a plain HTML form. Submissions are sanitized, never stored in the database, and sent to a recipient as an HTML email using your template (admin HTML and/or files under templates/).
Use cases include Elementor “HTML” widgets, block editor Custom HTML, or any theme template where you control the form markup.
Requirements
- WordPress 6.0 or higher (see plugin header).
- PHP 7.4 or higher (see plugin header).
- Pretty permalinks (or equivalent) so the custom rewrite endpoint resolves. After install or endpoint changes, flush permalinks once (see Installation).
Installation
- Copy the
ahf2mfolder intowp-content/plugins/. - In Plugins, activate Custom Form 2 Mail.
- Go to Settings → Permalinks and click Save Changes (no need to edit anything). This registers the form endpoint with WordPress.
- Open Settings → AHF2M and configure the endpoint, recipient, and optional custom HTML template.
Quick start (site owners)
- Settings → AHF2M
Note the URL shown at the top (e.g.https://example.com/ahf2m/). That is your form action. - Build a form with
method="post"andactionpointing to that URL (trailing slash is fine). - Use
nameattributes on inputs; those names drive{{placeholder}}replacement in email templates. - Add the nonce and honeypot fields (recommended; see Special form fields).
- Submit the form. The browser receives a JSON response (see HTTP response).
Settings (Settings → AHF2M)
| Setting | Option key | Description |
|---|---|---|
| Form endpoint path | ahf2m_endpoint |
URL slug only (no slashes), e.g. ahf2m or submit. Allowed characters: letters, numbers, _, -. Default: ahf2m. Changing this triggers a rewrite flush. |
| Recipient email | ahf2m_recipient_email |
Address that receives submissions. If empty or invalid, the WordPress admin email is used. |
| Custom email HTML | ahf2m_email_template |
Optional full HTML for the email body. Sanitized with wp_kses_post. See Template resolution and Placeholders. |
The settings page also shows the exact form action URL for your site.
Special form fields
These name values are reserved; they are not included in the emailed field list as regular data (except where noted).
| Field name | Purpose |
|---|---|
ahf2m_nonce |
CSRF token from [ahf2m_nonce_field] shortcode (or equivalent wp_nonce_field). If present, it must be valid. If omitted, the request is still accepted (weaker security). |
ahf2m_hp |
Honeypot: must stay empty. If filled, the request is rejected as spam-like. |
template_name |
Chooses a file template when custom HTML in settings is empty: loads templates/{template_name}.html, then falls back to default / Default.html. |
Shortcode for nonce (in a post/page):
[ahf2m_nonce_field]
Renders the hidden fields WordPress needs for ahf2m_submit_form.
Honeypot example (hide from users, keep empty):
<input
type="text"
name="ahf2m_hp"
value=""
style="display:none !important;"
tabindex="-1"
autocomplete="off"
/>
Placeholders
Placeholders are resolved in email templates only (custom HTML in settings and/or templates/*.html). The plain HTML table fallback does not interpret these tokens (except it does not use templates at all).
1. Form field placeholders — {{placeholder}}
Syntax: double curly braces, optional spaces inside braces.
- Pattern:
{{field_name}} - Meaning: Replaced with the sanitized value of the submitted field whose
namematches, after WordPresssanitize_key()rules (lowercase, onlya-z,0-9,_,-).
Examples:
- Form:
<input name="first_name" />→ use{{first_name}} - Form:
<input name="availability-date" />→ use{{availability-date}}(hyphen allowed in placeholder token)
If the field was not submitted or is empty after sanitization, the placeholder is replaced with N/A.
Important: HTML in field values is not interpreted as HTML in the email; values are output with esc_html() for safety.
2. Built-in “merge” placeholders — same {{ }} syntax
These are not taken from the form; they are injected by the plugin:
| Placeholder | Replaced with |
|---|---|
{{siteurl}} |
Site home URL (trailing slash), via home_url('/'). |
{{logourl}} |
Logo URL from theme/custom logo, fallbacks, or https://yoursite.com/logo.png if nothing else is found. |
3. Static literals — [timestamp] and [ip]
These use square brackets (not curly braces):
| Token | Replaced with |
|---|---|
[timestamp] |
Current date/time using Settings → General date format, time format, and timezone (wp_date()). |
[ip] |
Client IP from HTTP_CLIENT_IP, X-Forwarded-For (first hop), or REMOTE_ADDR. If unknown: N/A. |
Replaced after {{ }} processing; values are escaped for HTML.
Template resolution order
The email body is built in this order:
-
Custom email HTML (Settings → AHF2M)
If the textarea is not empty (after trim), it is used for every submission. File templates are ignored in this case. -
File templates (only when custom HTML is empty)
wp-content/plugins/ahf2m/templates/{template_name}.html
wheretemplate_namecomes from the POST fieldtemplate_name(defaultdefault).- If that file is missing,
templates/default.html, thentemplates/Default.html(for case-sensitive servers).
-
Fallback
If no template file exists, a simple HTML table of all submitted fields (keys and values) is sent.
Example HTML form
<form method="post" action="https://example.com/ahf2m/">
<!-- If you use the shortcode on a page, paste the rendered nonce fields here instead -->
<input type="hidden" name="ahf2m_nonce" value="..." />
<input type="hidden" name="_wp_http_referer" value="..." />
<input
type="text"
name="ahf2m_hp"
value=""
style="display:none !important;"
tabindex="-1"
autocomplete="off"
/>
<input type="hidden" name="template_name" value="default" />
<label>First name <input type="text" name="first_name" required /></label>
<label>Email <input type="email" name="email" required /></label>
<button type="submit">Send</button>
</form>
Point action at your real endpoint from Settings → AHF2M.
HTTP response
Successful and error responses use wp_send_json(): JSON body and appropriate HTTP status code.
Typical shape:
{
"success": true,
"message": "Form submitted successfully."
}
Common status codes: 200 success, 400 honeypot/invalid, 403 origin or nonce failure, 405 not POST, 429 rate limit, 500 mail failure.
Note: Normal HTML form navigation will show JSON in the browser. For a smooth UX, submit via JavaScript (fetch / XMLHttpRequest) and handle the JSON, or add your own redirect flow later.
Security behavior (summary)
- POST only at the endpoint.
- Same-origin check using
Origin/Refererhost vs site host when those headers are present; if both are missing, the request may still be allowed (defense in depth elsewhere). - Optional nonce when
ahf2m_nonceis posted. - Honeypot
ahf2m_hp. - Per-IP rate limit: 5 submissions per minute (transient-based).
- Input sanitization by field name heuristics (
email,url/website,message/comment, etc.) andsanitize_keyfor names. - No submission rows written to the WordPress database.
Email delivery
- Transport: WordPress
wp_mail()(respects SMTP plugins if configured). - Format: HTML (
Content-Type: text/html; charset=UTF-8). - Subject: Localized string
[AHF2M] New form submission(filterable in code if you extend the plugin).
Troubleshooting
| Issue | What to try |
|---|---|
| 404 on form URL | Settings → Permalinks → Save. Re-save Settings → AHF2M after changing the slug. |
| JSON instead of a “thank you” page | Expected for a raw form POST; use AJAX or a separate thank-you page pattern. |
{{my_field}} always N/A |
Check the input name matches after sanitize_key (lowercase; odd characters stripped). |
| Email not received | Check spam folder, wp_mail / SMTP plugins, and server mail logs. |
403 Origin check failed |
Form must be submitted from the same site (or headers must allow the check). |
For contributors
Repository layout
ahf2m/
├── ahf2m.php # Main plugin bootstrap + AHF2M_Plugin class
├── templates/ # Optional .html file templates (e.g. default.html)
└── README.md # This file
Architecture
- Single class:
AHF2M_Plugin(final) inahf2m.php. - Rewrite:
add_rewrite_rule()maps^{endpoint}/?$→index.php?ahf2m_action=submit. - Query var:
ahf2m_actionregistered viaquery_vars; valuesubmittriggers the handler. - Handler:
template_redirect→maybe_handle_submission(). - Admin:
options.phpgroupahf2m_settings; menu under Settings → AHF2M.
Design choices worth preserving
- No DB writes for form payloads—privacy and simplicity.
- Strict file reads under
templates/usingrealpath()and basename allowlists. - Template rendering separates
{{ }}(escaped values) from[timestamp]/[ip](literal tokens). - Options: use
register_setting+ sanitize callbacks; endpoint changes hookupdate_option_ahf2m_endpointto flush rewrites.
How to extend safely
- Prefer
wp_mailfilters (e.g.wp_mail,wp_mail_from) for from-address or headers rather than editing core flow unless necessary. - New placeholders: extend
normalize_template_data()and/orapply_builtin_template_tokens(); document them in this README. - i18n: strings use
__()/esc_html__()with text domainahf2m; add alanguages/catalog if you ship translations.
Coding standards
- Follow WordPress PHP Coding Standards where practical.
- Keep changes focused; avoid unrelated refactors in the same PR as a feature fix.
Pull requests
- Describe behavior change and testing steps (WP version, PHP version).
- Confirm permalinks / endpoint still work after your change.
- If you add settings, update this README and the settings screen help text.
License
This plugin is licensed under the GPL-2.0-or-later (see plugin header and License URI in ahf2m.php).
Credits
Maintained by Eric Aimhigh and contributors. Issues and PRs welcome on the plugin’s GitHub repository (see Plugin URI in ahf2m.php).