Static PHP Contact form provides a self-contained, stand-alone mini-framework allowing to put HTML contact forms on static websites (generated with CMS like Hugo, Jekyll, Gatsby, etc.). The front-end part (HTML form) will need to be added in a page on the static website, while the back-end part (PHP scripts and libs) will need to be installed on any old-school LAMP server you control, and is designed for basic shared hostings where limited admin options are offered (no access to sudo
, apt install
, Docker containers, etc.).
- User-agent detection (OS, browser, public IP, local/private IP),
- GeoIP sender detection through Maxmind (API key needed),
- Bots blocking, (honeypot and mandatory JS support),
Spam detection(needed ?)- Send emails through SMTPS (mandatory) using PHPMailer, for proper email authentication through SPF and DKIM (improve deliverability, limit spam detection),
- Self-hosted with reduced overhead (KISS):
- The PHP framework can be installed on any PHP server (separate from email server and website server),
- The contact form can be displayed on any web page where you can add
<iframes>
, - Update code through Git,
- Auto-update GeoIP databases (with Cron job).
- You can use several email templates (extendable), plain-text and HTML.
Many companies provide the same kind of back-end service (POST endpoint for the HTML form, with SMTP servers). The great thing is they handle everything for you. The not-so-great thing is emails privacy and costs. Truth be told, if you have any kind of (cheap) LAMP-based hosting, you don't need more. Remains that you will need to configure and maintain everything yourself. We are here to make that as painless as possible.
- a dedicated sending email address (like
[email protected]
) accessible through SMTPS,[^1] - PHP 8.1 minimum, and :
- A MaxMind GeoIP2 Lite license key (free) (get yours),
It is recommended to have SPF and DKIM configured on your domain, to maximize email deliverability (how to do it on CPanel hostings).
An HTML contact form is actually much simpler than what people used to CMS imagine. You need an HTML form on some static HTML page:
<form action="https://some-domain.com/send-email.php" method="post">
<input name="email">
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
When you click the "Send" button, an HTTP POST request is sent to https://some-domain.com/send-email.php
by your browser, with email
and message
as parameters. You don't need a CMS or even AJAX calls on your actual website to handle anything dynamically, actually the tech needed to achieve that existed already in 2002.
All we need is the actual send-email.php
end-point to catch that POST request and do something with its content (namely, post it to some mailbox). This end-point doesn't need to be part of our website. It doesn't need to be hosted on the same server, or even domain, as our website.
The scope of this project is to provide this end-point, self-hostable on any PHP server, so static websites only have to display the form fields.
You will need Git installed on your computer. In a terminal, do:
$ git clone --recurse-submodules --shallow-submodules https://github.com/aurelienpierreeng/Static-PHP-contact-form.git
$ cd Static-PHP-contact-form
Copy and rename (or rename) src/config_example.php
to src/config.php
. Then start updating its content, following the instructions in comments. In the following, src/config.php
will be called CONFIG
The first thing to configure is your MaxMind license key and the admin secret key:
'admin-secret' => 'abcdefghijkl',
'maxmind' => array(
'account' => "123456", // Account ID number : 6 digits
'password' => "xxxxxxxxxxxxxxxxxxxxxxxxx", // secret key
),
Once this is done, we will need to download the MaxMind GeoIP databases…
You need to install PHP locally on your computer. Then, to start a local testing server, from the root of the sourcecode, do:
php -S localhost:8000
The root of the sourcecode folder will then be available in your browser at http://localhost:8000. Change the port number if it is already used.
The first thing we will need to do is to download MaxMind GeoIP databases. Once the test server is launched (on directly on the production server), and you set your account ID/license key in CONFIG
, do:
wget localhost:8000/update-geoip.php?key=abcdefghijkl
where abcdefghijkl
is your admin-secret
CONFIG
value. A new download is triggered everytime you hit update-geoip.php
through the network with the proper admin-secret
key, so the obvious purpose of the authentification key is to avoid blasting I/O everytime a bot hits that page (and possibly getting throttled by MaxMind).
After you started your local server, hit http://localhost:8000/demo/contact.html in your browser to get the HTML form. This file is a fully-working example that you can re-use as-is, or adapt. In this setup, you will need to set your CONFIG['mailer']['host']
key to the distant address of your email server (that is, not localhost
). Try sending an email to yourself (don't forget to properly set the CONFIG
file first) and check that everything works.
The simplest way is to create an archive with your whole local sourcecode folder (including GeoIP databases and the PHPMailer submodule), then send it to your server, and decompress it there, for example in your-domain.com/backend
. Otherwise, you can redo the above steps (git clone
then wget your-domain.com/backend//update-geoip.php?key=abcdefghijkl
) on your server.
If the server hosting your PHP scripts also hosts your email account, you can set CONFIG['mailer']['host']
to localhost
, to access the SMTP server from within (and possibly faster).
You may want to delete or deny access to the ./demo
folder if you don't use the HTML files in iframe
.
We are going to assume here that you deployed Static PHP Contact form to your-domain.com/backend
and this server is accessible through https
.
You can simply add to your HTML/Markdown page:
<iframe src="https://your-domain.com/backend/demo/contact.html"
width="100%" height="900px" />
This will display the demo contact form within the target page. However, the height should be set fixed, which is not responsive to display dimensions. A minimal example of iframe
embedding is given in ./demo/iframe-demo.html
.
You can copy and paste the content of ./src/demo/contact.html
to your static website generator templates, and then modify it further. All the <input>
fields found in the demo need to be in the HTML form because they are required by the PHP POST endpoint ./send-email.php
, but you can set most of them to type=hidden
.
If you go this way, don't forget to load the Javascript validation script. Here is a minimal example:
<form action="https://your-domain.com/backend/send-email.php"
method="post" id="contact-form">
<!-- all inputs go there -->
<!-- only our JS validation will enable this button: -->
<button type="submit" disabled>Send</button>
</form>
<!-- Load the validation script -->
<script src="https://your-domain.com/backend/js/user-agent.min.js"></script>
<!-- Call the server-side user-agent identification -->
<script>
window.addEventListener('DOMContentLoaded', () => {
// need to wait for the script above to be loaded before calling the function:
validate_contact("https://your-domain.com/backend/user-agent.php");
});
</script>
Import ./demo/hugo_contact.html
into your Hugo theme layouts/shortcodes
directory. Then, from within your Markdown pages, add the shortcode:
{{< hugo_contact "https://your-domain.com/backend" template="html" >}}
The first parameter is the URL of your Static PHP Contact form installation (without trailing /). The template parameter is optional and will default to default
(aka ./templates/default.php
).
When hitting the Submit
button of the HTML <form>
, all the input fields are sent as parameters of the POST HTTPS request to the ./send-email.php
endpoint. To prevent abuses, the endpoint accepts form submissions only from pre-allowed domains, defined in the origins
key in the CONFIG
file. Any page where you display a contact form should have its domain in the origins
list, even if it is on the same server/domain as the ./send-email.php
endpoint.
Each email template gets:
- its own list of receipients,
- its own body formatting,
- its own language,
- its own confirmation message.
All email templates share the same sending SMTP server.
Templates can be used to send different kind of customized emails, for example you could have one template per language, or one per receipient service (so some contact forms would be directed to customer support, some other to pre-sales, etc.).
To create a new template, for example named custom
:
- in
CONFIG
file, in thetemplates
array, add a newcustom
entry and fill its mandatory fields (see the example in./src/config_example.php
), likereceipients
,lang
- in
./templates
directory, add a newcustom.php
file and write itsemail_body()
function. See./templates/default.php
for a plain-text example, and./templates/html.php
for an HTML body example. - in your HTML contact form, set the
template
input field tocustom
, like:
<input name="template" value="custom" type="hidden" readonly="readonly">
Any additional <input name="xxx">
tag you add in your HTML <form>
will have its value transfered in the global variable $_POST['xxx']
that can be used directly from within the template email_body()
function. You can also define custom keys in CONFIG
file and access them from email_body()
.
You could let visitors chose the template themselves in the form, for example if each template is mapped to a different receipient service. In that case, replace <input name="template">
with:
<label for="template">Service</label>
<select name="template" required>
<option value="">--Please choose an option--</option>
<option value="default">General inquiries</option>
<option value="custom">User support</option>
</select>
WARNING: do NOT change the content of the factory-provided email templates (default.php
and html.php
), as they would be overwritten by later updates. Instead, copy and rename them to anything but default
and html
.
From within the source code directory, use Git to update everything:
git pull --recurse-submodules
Your ./src/config.php
file and custom templates will not be overwritten (provided you did not change the factory-provided templates).
Because PHPMailer is a mild security concern (logging into servers through SSL layers), it is a good idea to keep it reasonably up-to-date.
The databases mapping IP to geographic location have an expiration date because the IP of residential ISP accounts changes every month or so. It might be a good idea to update those databases at least once a month.
To do so, you only have to hit https://your-domain.com/backend/update-geoip.php?key=abcdefghijkl
with key
set as your CONFIG
admin-secret
value. You can do so from a web browser, manually, or you can simply set up a weekly Cron job with:
wget --delete-after https://your-domain.com/backend/update-geoip.php?key=abcdefghijkl > /dev/null
In your config.toml
or hugo.toml
file, define:
[Params]
contact = "/contact"
Then you can create links to your contact page using the ?utm_source
parameter to keep track of the source page, using the Hugo template tag {{ .Permalink }}
to define the URL to contact page, like so:
<a title="Contact" href="{{ with .Site.GetPage .Site.Params.contact }}{{ .Permalink }}{{ end }}?utm_source={{ (replaceRE "https?://" "" .Permalink) }}">
If you click on the contact link from the page your-domain.com/portfolio
, you would then produce the URL:
https://your-domain.com/portfolio.com/contact/?utm_source=your-domain.com/portfolio
The utm_source
parameter is read by our ./js/user-agent.js
script and added to the hidden form field utm
. In your email PHP templates, you can reuse it as $_POST['utm']
. Aside from tracking purposes, it can give some context to understand the content of some cryptic emails if you know where the person comes from.
The form fields validation is done by the browser native features (required
fields and email
type format).
Our mandatory ./js/user-agent.js
script hits the ./user-agent.php
endpoint, which returns a JSON response used by the script to fill the user-agent related HTML form fields. The local IP address is resolved client-side through WebRTC API.
Those user-agent fields are sent back in the POST request to the ./send-email.php
endpoint, which will compare them to the internal ones from ./user-agent.php
and refuse the connection if they don't match. This effectively rejects all user-agents not supporting Javascript, which should keep away most spamming bots.
The <input name="address">
is a honeypot field. It should be in the form but should stay empty, which means it should be hidden from the form GUI. Bots will usually try to stuff it with random content.
./send-email.php
refuses form submissions :
- from empty user agents,
- from domains not in the
CONFIG
origins
key (HTTP_REFER
orHTTP_ORIGIN
), - if the target email domain :
- does not exist (DNS can't reach it),
- has no MX entry (no email server advertised),
- if the
address
POST parameter is not empty (honeypot caught a bot), - if any other POST parameter is empty (see
./demo/contact.html
for mandatory fields)
All internal code subdirectories are protected with a .htaccess
file defining the rules:
deny from all
<Files subdirectory/*>
deny from all
</Files>
On the server, you should set your permissions as follow:
Files:
./src/*.php
: 644,./templates/*.php
: 644,.htaccess
: 444
Folders
./src/
: 555./templates
: 555,./libs/
: 755