× Home
PHP mail() contact form for the static website generator Hugo

Hugo - Contact form with PHP

Spam mail is undesirable

Hugo website instructions for a multilingual PHP mail() contact form. With honeypot input fields, input validation and error output. A lot of effort for a simple but secure contact form.

As is often the case, it depends on the purpose and the environment of the mail component. In my case, I only need a simple contact form with a text field and a user e-mail address field. My webspace provider automatically provides a mail server environment. For this reason, I do not need the well-known PHPMailer library. Every additional software component has to be maintained and updated.

Integrating foreign source code creates work (updates) and is security relevant. For this reason, I switched from WordPress to Hugo for blogs. I want to program and not constantly run after security updates and well-intentioned plug-in “improvements”.

Test your own provider to see if they provide the mail() environment.

Before you do a lot of work, you should test whether your provider has allowed and configured the PHP mail() function on your web space or server. In my post - Using PHP in Hugo - I explained how PHP can be integrated into Hugo. Therefore, I am assuming this knowledge. Copy the following test source code into your single.php:

{{ define "main" }}
<section class="container">
  <h1>{{ .Title }}</h1>
  {{ .Content }}

  <?php  
  $from = 'hello@example.com';
  $to = 'my@example.com';
  $subject = 'Example Mail';
  $message = 'My first PHP Mail.';
  $headers = 'From: ' . $from;  
  
  if (!mail($to, $subject, $message, $headers))
  {
    echo "Error.";
  }
  else {
    echo "Message sent.";
  }
  ?>

</section>
{{end}}

Please replace the e-mail addresses in $from and $to with your own addresses. Then create the Markdown files. If you do not have a multilingual website, the index.md is sufficient:

content
|__ kontakt
    |__ index.md
    |__ index.en.md

The file index.en.md:

---
title: "Contact"
date: 2021-05-12T18:55:35+02:00
lastupdate: ""
draft: false
slug: contact
translationKey: kontakt
description: "Contact form"
author: "Frank Kunert"
outputs: PHP
---

Have you found an error, would you like to point out something or simply say something nice? Then write me an email. Either via the contact form or directly to kontakt [ at ] tekki-tipps [.] de.  

Afterwards, the public directory must be regenerated by Hugo. Copy the contents of public via sFTP to your own web space. If you now call up the contact website, the message “Message sent” should be displayed and the e-mail should have arrived in your mailbox. If this does not work, please talk to your provider.

Source code for the Hugo PHP contact form

The index.md and index.en.md are created. In this section I show the source code of the template single.php, the partial file contactForm.html and the PHP file contact.php with comments.

Hugo Template single.php

{{ define "main" }}
<section class="container">
  <h1>{{ .Title }}</h1>
  {{ .Content }}
  
  {{ readFile "static/php/contact.php" | safeHTML }}

  {{ partial "contactForm" . }}

</section>
{{end}}

Below the title, the content of index.md is output. With the GO function readFile, the contact.php is integrated and waits for the click of the submit button. Afterwards, the partial contactForm.html is integrated with the actual contact form.

Hugo Partial contactForm.html

<div class="form-wrapper">
  <form class="needs-validation" novalidate method="post">

    <div class="msg-text">
      <label for="messageText" class="form-label">{{ T "IhreNachricht" }}</label>
      <span class="required-field">*</span>
      <textarea class="form-control" id="messageText" rows="3" name="message" required></textarea>
    </div>

    <div class="name">
      <label for="name" class="form-label">Name</label>
      <span class="required-field">*</span>
      <input type="text" class="form-control" id="name" name="name" placeholder="Your Name" required>
    </div>

    <div class="website">
      <label for="website" class="form-label">Website</label>
      <span class="required-field">*</span>
      <input type="text" class="form-control" id="website" name="website" placeholder="Your Website URL" required>
    </div>
    
    <div class="email-adr">
      <label for="email" class="form-label">{{ T "EmailAdresse" }}</label>
      <span class="required-field">*</span>
      <input type="email" class="form-control" id="email" name="email" placeholder="name@example.com" required>
    </div>

    <div class="submit-btn">
      <button class="btn btn-success" type="submit">{{ T "EmailAbsenden" }}</button>
    </div>

  </form>
</div>

At first glance, there is nothing unusual about this HTML form. What stands out is, for example, {{ T "IhreNachricht" }}. This is a Hugo specific function for multilingual websites. T is for Translate. Then comes a placeholder, the placeholder is translated into different languages. It would go too far here to explain all this. For multilingual websites I will write additional blog articles.

<div class="form-wrapper">
  <form class="needs-validation" novalidate method="post">

HTML 5 validates input fields and displays a corresponding message. For example, if a required field was not filled in when clicking on the submit button. The validation is very rudimentary and does not meet my requirements. It also prevents my required honeypot fields. To turn off browser validation, novalidate must be specified.

The individual input fields have, among others, the name parameter. For example, the first input field, the textarea, has the name parameter content message. When the send button is clicked, the field content is saved on the server and can be read in the PHP programme via $_POST[‘message’].

The name and website fields are honeypot fields that are hidden via CSS with display: none;. The required attribute of the fields is intended to make it more palatable for spam robots to fill them in. The novailidate statement prevents automatic blocking by the browser. If you want to simulate a spam robot, switch off the display: none; for these fields via the browser. This will make the fields visible again in the form.

Since I use the Bootstrap CSS framework, the input fields are styled by this. At this point, you have to extend the CSS code without the CSS framework itself.

PHP file contact.php

At this point I show the complete source code of contact.php. Further down I will explain the code in sections.

<?php

$from = 'hello@example.com';
$to = 'my@example.com';
$header  = "MIME-Version: 1.0\r\n";
$header .= "Content-type: text/html; charset=utf-8\r\n";
$header .= "From: $from\r\n";
$header .= "Reply-To: $to\r\n";
$header .= "X-Mailer: tekki-tipps.de\r\n";
$msg = '';
$email = '';
$text = '';
$langDE = TRUE;
if (substr($_SERVER['REQUEST_URI'],1,2) == "en" ) {
  $langDE = FALSE;
}

// this should catch a lot of spam bots
$honeypot = trim($_POST["name"]) . trim($_POST["website"]);

if(empty($honeypot)) {
  if ($_SERVER["REQUEST_METHOD"] == "POST") { 
    // Check user input
    $fielderror = 0;
    if (strlen($_POST["message"]) < 1) {
      $fielderror += 1;
    }
    else {
      $text= substr(nl2br(strip_tags($_POST['message'])), 0, 16384);
    }
    if (strlen($_POST["email"]) < 1) {
      $fielderror += 2;
    }
    elseif (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
      $fielderror += 4;
    }
    else {
      $email = $_POST["email"];
    }
    // User input error
    if ( $fielderror > 0 ) {
      $msg = "<div class='fielderror'>";
      switch ($fielderror) {
        case 1:
          if ($langDE) {
            $msg .= "Bitte das Nachrichtenfeld ausfüllen.";
          }
          else {
            $msg .= "Please fill in the message field.";
          }
          break;
        case 2:
          if ($langDE) {
            $msg .= "Bitte die E-Mailadresse ausfüllen.";
          }
          else {
            $msg .= "Please fill in the email address.";
          }
          break;
        case 3:
          if ($langDE) {
            $msg .= "Bitte das Nachrichtenfeld ausfüllen.<br>" . "Bitte die E-Mailadresse ausfüllen.";
          }
          else {
            $msg .= "Please fill in the message field.<br>" . "Please fill in the email address.";
          }
          break;
        case 4:
          if ($langDE) {
            $msg .= "Die E-Mailadresse ist ungültig.";
          }
          else {
            $msg .= "The email address is invalid.";
          }
          break;
        case 5:
          if ($langDE) {
            $msg .= "Bitte das Nachrichtenfeld ausfüllen.<br>" . "Die E-Mailadresse ist ungültig";
          }
          else {
            $msg .= "Please fill in the message field." . "The email address is invalid.";
          }
          break;
      }
      $msg .= "</div>";
      echo $msg;
    }
    // There was no user input error
    else {
      if ($langDE) {
        $subject =  'tekki-tipps.de - Kontaktformular';
        $message = 	"<h2>tekki-tipps.de - Kontaktformular</h2>" .
										"<p><strong>Von: </strong>" .	$email . "</p><hr>" . 
										"<p>" . $text . "</p>";
      }
      else {
        $subject =  'tekki-tipps.de - Contact Form';
        $message = 	"<h2>tekki-tipps.de - Contact Form</h2>" .
										"<p><strong>From: </strong>" .	$email . "</p><hr>" . 
										"<p>" . $text . "</p>";
      }
      if (!mail($to, $subject, $message, $header))
      {
        if ($langDE) {
          $msg = '<div class="mail-error">Entschuldigung, etwas ist schief gelaufen. Bitte versuchen Sie es später erneut.</div>';
        }
        else {
          $msg = '<div class="mail-error">Sorry, something went wrong. Please try again later.</div>';
        }
      } 
      else {
        if ($langDE) {
          $msg = '<div class="mail-success">Nachricht gesendet! Danke für die Kontaktaufnahme.</div>';
        }
        else {
          $msg = '<div class="mail-success">Message sent! Thanks for getting in contact.</div>';
        }
      }
      echo $msg;
    }
  }
}
else {
  $subject = 'tekki-tipps.de - BAD ROBOT - Contact Form';
  $message =  "<h2>tekki-tipps.de - BAD ROBOT - Contact Form</h2>";
  $text = substr(nl2br(strip_tags($_POST['message'])), 0, 16384);
  $message .= "<p>BAD ROBOT!</p>"
            . "<p>HTTP_USER_AGENT: " . $_SERVER["HTTP_USER_AGENT"] . "<br>"
            . "REMOTE_ADDR: " . $_SERVER["REMOTE_ADDR"] . "<br>"
            . "REMOTE_HOST: " . $_SERVER["REMOTE_HOST"] . "</p><hr>"
            . "<p>" . $text . "</p>";
  mail($to, $subject, $message, $header);
}
?>

contact.php - Generally

Pass your e-mail addresses to the $from and $to variables. The $header variable defines, among other things, that an HTML e-mail is sent.

I have chosen the Hugo variant with the country abbreviation in the URL path for my multilingual website. The English version of the contact page has the following URI: /en/contact/index.php. If the en is present in the URI, I set the variable $langDE to FALSE. At initialisation, the variable is set to TRUE. This allows me to display the output of the status messages in the respective language.

contact.php - Honeypot

I am interested in how often an intercepted spam robot tries to misuse my contact form for its rubbish. For this reason I send myself an email with the rubbish text.

// this should catch a lot of spam bots
$honeypot = trim($_POST["name"]) . trim($_POST["website"]);

if(empty($honeypot)) {
  ..
  ..
}
else {
  $subject = 'tekki-tipps.de - BAD ROBOT - Contact Form';
  $message =  "<h2>tekki-tipps.de - BAD ROBOT - Contact Form</h2>";
  $text = substr(nl2br(strip_tags($_POST['message'])), 0, 16384);
  $message .= "<p>BAD ROBOT!</p>" .
              "<p>HTTP_USER_AGENT: " . $_SERVER["HTTP_USER_AGENT"] . "<br>" .
              "REMOTE_ADDR: " . $_SERVER["REMOTE_ADDR"] . "<br>" .
              "REMOTE_HOST: " . $_SERVER["REMOTE_HOST"] . "</p><hr>" .
              "<p>" . $text . "</p>";
  mail($to, $subject, $message, $header);
}

As soon as something is entered in the hidden input fields Name and Website, the normal e-mail processing is bypassed and a special e-mail is sent. If you do not want this control - simply delete the else branch.

Example Spam Mail
A BAD ROBOT e-mail then looks like this when it is received.

The REMOTE_ADDR is ::1 in the example because the example email was processed through my local web server. See description in my blog post - Using MAMP Webserver locally with Hugo . Otherwise, the IP address of the sender is there. Whether this is the correct IP address or whether it has been manipulated is unclear. The REMOTE_HOST specification will be empty in most cases. Spammers are very cautious with their own data.

<Update - 21. July 2021>
On 19 July 2021, I published the site and informed Google about it through a sitemap.xml. On 20 July 2021, the site was not yet completely in the index. On 21 July 2021, the first thing I received was a spam email. At that time, no external visitor had been on the site yet. The honeypot is really necessary.
</Update>

contact.php - User input error

The incorrect entries can occur in different combinations. For this reason, I add a different number in each case. There is a maximum of 5 different combinations of errors.

  if ($_SERVER["REQUEST_METHOD"] == "POST") { 
    // Check user input
    $fielderror = 0;
    if (strlen($_POST["message"]) < 1) {
      $fielderror += 1;
    }
    else {
      $text= substr(nl2br(strip_tags($_POST['message'])), 0, 16384);
    }
    if (strlen($_POST["email"]) < 1) {
      $fielderror += 2;
    }
    elseif (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
      $fielderror += 4;
    }
    else {
      $email = $_POST["email"];
    }
    // User input error
    if ( $fielderror > 0 ) {
      $msg = "<div class='fielderror'>";
      switch ($fielderror) {
        case 1:
          if ($langDE) {
            $msg .= "Bitte das Nachrichtenfeld ausfüllen.";
          }
          else {
            $msg .= "Please fill in the message field.";
          }
          break;
        case 2:
          if ($langDE) {
            $msg .= "Bitte die E-Mailadresse ausfüllen.";
          }
          else {
            $msg .= "Please fill in the email address.";
          }
          break;
        case 3:
          if ($langDE) {
            $msg .= "Bitte das Nachrichtenfeld ausfüllen.<br>" .
                    "Bitte die E-Mailadresse ausfüllen.";
          }
          else {
            $msg .= "Please fill in the message field.<br>" .
                    "Please fill in the email address.";
          }
          break;
        case 4:
          if ($langDE) {
            $msg .= "Die E-Mailadresse ist ungültig.";
          }
          else {
            $msg .= "The email address is invalid.";
          }
          break;
        case 5:
          if ($langDE) {
            $msg .= "Bitte das Nachrichtenfeld ausfüllen.<br>" .
                    "Die E-Mailadresse ist ungültig";
          }
          else {
            $msg .= "Please fill in the message field.<br>" .
                    "The email address is invalid.";
          }
          break;
      }
      $msg .= "</div>";
      echo $msg;
    }

By if($_SERVER[“REQUEST_METHOD”] == “POST”) the click of the submit button is waited for. The content of the TextArea field (message) is checked to see if anything was entered at all. If so, the length is limited to a maximum of 16384 bytes. This prevents the memory capacity from being deliberately blown up. HTML tags are removed and line breaks are converted into an HTML <br> tag.

I use the entered email address in the PHP code only in the text. With FILTER_VALIDATE_EMAIL it is checked whether an e-mail address was really entered.

If an error is detected, a case query displays the corresponding error message in the respective language.

contact.php - the actual mail dispatch

The e-mail text is now only compiled in different languages.

// There was no user input error
    else {
      if ($langDE) {
        $subject =  'tekki-tipps.de - Kontaktformular';
        $message = 	"<h2>tekki-tipps.de - Kontaktformular</h2>" .
										"<p><strong>Von: </strong>" .	$email . "</p><hr>" .  
										"<p>" . $text . "</p>";
      }
      else {
        $subject =  'tekki-tipps.de - Contact Form';
        $message = 	"<h2>tekki-tipps.de - Contact Form</h2>" .
										"<p><strong>From: </strong>" .	$email . "</p><hr>" .  
										"<p>" . $text . "</p>";
      }
      if (!mail($to, $subject, $message, $header))
      {
        if ($langDE) {
          $msg = '<div class="mail-error">Entschuldigung, etwas ist schief gelaufen. Bitte versuchen Sie es später erneut.</div>';
        }
        else {
          $msg = '<div class="mail-error">Sorry, something went wrong. Please try again later.</div>';
        }
      } 
      else {
        if ($langDE) {
          $msg = '<div class="mail-success">Nachricht gesendet! Danke für die Kontaktaufnahme.</div>';
        }
        else {
          $msg = '<div class="mail-success">Message sent! Thanks for getting in contact.</div>';
        }
      }
      echo $msg;
    }

After sending, a status message is output.

SCSS for the contact form

As described above, I use the Bootstrap framework and therefore do not have to create many customisations for the design.

.fielderror,
.mail-error {
  color: $wk-color-4;
  font-size: $content-fontsize-xl;
  font-weight: 600;
}
.mail-success {
  color: var(--wk-accent-color-3);
  font-size: $content-fontsize-xl;
  font-weight: 600;
}
.form-wrapper {
  margin: 1.0rem 0;
  padding: 2.0rem;
  border: 1px solid var(--wk-accent-border-color);

  .required-field {
    color: $wk-color-4;
    font-weight: 600;
  }
  .name,
  .website {
    display: none;
  }
  .email-adr {
    margin: 1.5rem 0 2.0rem;
  }
  .submit-btn {
    display: flex;
    justify-content: flex-end;
    width: 100%;
  }
}

Why do I need a PHP contact form for my Hugo project?

A simple mailTo link in the imprint would actually suffice. But - in Germany there are very restrictive legal requirements for domain owners. Everyone must provide an imprint. In addition to the address of the domain holder, an e-mail address and a telephone number of the holder must be provided. Since I do not want to publish my telephone number on the internet, I have to provide a contact form instead. The legislator assumes that not everyone has a mail provider and therefore the contact form must be provided as a telephone number substitute.

Hugo is a static website generator. I get the dynamic for a contact form through PHP. There are external services that offer contact forms for integration. But I don’t want to depend on external service providers, the costs also play a role. The legal requirements of the EU, for external services outside the EU, are very extensive to impossible to fulfil.

Conclusion

I do not normally program in PHP. If you notice a security-relevant error, please write a comment or use the contact form to inform me. The mail() function of PHP allows me to do without external libraries. This way I have less update effort and can take care of things that are important to me.

- Update 02. Aug. 2021 |
13 minutes to read
0
This post was created with Hugo version 0.85.0.

With the German language setting, comments are not displayed in the English version of the website and vice versa.

© 2021 - Frank Kunert  -  About me
A service from webdienste-kunert.de