× Startseite
PHP mail() Kontaktformular für den statischen Webseiten Generator Hugo

Hugo - Kontaktformular mit PHP

Spam mail is undesirable

Hugo Website Anleitung für ein multilinguales PHP mail() Kontaktformular. Mit Honeypot Input Feldern, Eingabeüberprüfung und Fehlerausgabe. Viel Aufwand für ein einfaches, aber sicheres Kontaktformular.

Es kommt wie so oft auf den Einsatzzweck und das Umfeld der Mail-Komponente an. In meinem Fall benötige ich nur ein einfaches Kontaktformular mit Textfeld und Benutzer E-Mail Adressfeld. Mein Webspace Provider stellt automatisch eine Mail-Server Umgebung zur Verfügung. Aus diesem Grund benötige ich die bekannte PHPMailer Library nicht. Jede zusätzliche Software Komponente muss gepflegt und upgedated werden.

Das Einbinden von fremdem Sourcecode erzeugt Arbeit (Updates) und ist Sicherheitsrelevant. Aus diesem Grund bin ich für Blogs von WordPress zu Hugo gewechselt. Ich möchte programmieren und nicht dauernd irgendwelchen Sicherheitsupdates und gut gemeinten Plug-In “Verbesserungen” hinterherlaufen.

Den eigenen Provider testen ob er das mail() Umfeld zur Verfügung stellt

Bevor Sie sich viel Arbeit machen, sollten Sie testen ob Ihr Provider die PHP mail() Funktion auf Ihrem Webspace oder Server erlaubt und konfiguriert hat. In meinem Beitrag - PHP in Hugo benutzen - habe ich erklärt wie PHP in Hugo integriert werden kann. Deshalb setze ich dieses Wissen voraus. Kopieren Sie den folgenden Test Sourcecode in Ihre 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}}

Die E-Mailadressen in $from und $to ersetzen Sie bitte mit Ihren eigenen Adressen. Danach erstellen Sie die Markdown Dateien. Wenn Sie keine multilinguale Website haben reicht die index.md:

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

Die Datei 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.  

Danach muss das public Verzeichnis von Hugo neu generiert werden. Kopieren Sie den Inhalt von public per sFTP auf den eigenen Webspace. Wenn Sie jetzt die Kontakt Webseite aufrufen, sollte der Hinweis “Message sent” angezeigt und die E-Mail in Ihrem Mail-Postfach eingegangen sein. Funktioniert dies nicht, sprechen Sie bitte mit Ihrem Provider.

Sourcecode für das Hugo PHP Kontaktformular

Die index.md und index.en.md sind erstellt. In diesem Abschnitt zeige ich den Sourcecode des Templates single.php, das Partial File contactForm.html und die PHP Datei contact.php mit Kommentare an.

Hugo Template single.php

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

  {{ partial "contactForm" . }}

</section>
{{end}}

Unterhalb des Titels wird der Content von index.md ausgegeben. Mit der GO Funktion readFile wird die contact.php eingebunden und wartet auf den Klick des Absenden-Button. Danach wird das Partial contactForm.html mit dem eigentlichen Kontaktformular integriert.

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>

Auf den ersten Blick ist nichts ungewöhnlich an diesem HTML Form. Was auffällt ist zum Beispiel {{ T "IhreNachricht" }}. Dies ist eine Hugo spezifische Funktion für mehrsprachige Websites. T steht für Translate. Dann kommt ein Platzhalter, der Platzhalter ist in verschiedene Sprachen übersetzt. Es würde hier zu weit gehen dies alles zu erklären. Für multilinguale Webseiten werde ich zusätzliche Blog Artikel schreiben.

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

HTML 5 validiert Input Felder und gibt einen entsprechenden Hinweis aus. Zum Beispiel wenn ein required Feld bei Klick auf den Absenden-Button nicht ausgefüllt wurde. Die Validierung ist sehr rudimentär und entspricht nicht meinen Ansprüchen. Außerdem verhindert es meine required Honeypot Felder. Um die Browser Validierung auszuschalten, muss novalidate angegeben werden.

Die einzelnen Input-Felder haben unter anderem den Parameter name. Das erste Eingabefeld, die Textarea, hat zum Beispiel den name-Parameterinhalt message. Bei Klick auf den Absenden-Button, wird der Feldinhalt auf dem Server gespeichert und ist im PHP Programm über $_POST[‘message’] lesbar.

Die Felder name und website sind Honeypot-Felder die über CSS mit display: none; ausgeblendet werden. Das required Attribut der Felder soll es für Spam-Robots schmackhafter machen diese auch auszufüllen. Die novailidate Anweisung verhindert das automatische Blockieren durch den Browser. Wenn Sie einen Spam-Robot simulieren möchten, schalten Sie über den Browser das display: none; bei diesen Feldern aus. Dadurch werden die Felder im Formular wieder sichtbar.

Da ich das Bootstrap CSS-Framework benutze werden die Input-Felder dadurch gestylt. An der Stelle müssen Sie den CSS-Code ohne das CSS-Framework selber erweitern.

PHP Datei contact.php

An dieser Stelle zeige ich den kompletten Sourcecode von contact.php an. Weiter unten erkläre ich den Code dann in Abschnitten.

<?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 - Allgemein

Übergeben Sie den Variablen $from und $to Ihre E-Mailadressen. In der $header Variablen wird unter anderem definiert, dass eine HTML-E-Mail verschickt wird.

Ich habe für meine multilinguale Website die Hugo Variante mit der Länderabkürzung im URL-Pfad gewählt. Die englische Version der Kontaktseite hat folgende URI: /en/contact/index.php. Wenn das en in der URI vorhanden ist, setze ich die Variable $langDE auf FALSE. Bei der Initialisierung ist die Variable auf TRUE eingestellt. Dadurch kann ich die Ausgabe der Statusmeldungen in der jeweiligen Sprache anzeigen.

contact.php - Honeypot

Es interessiert mich wie oft ein abgefangener Spam-Robot versucht mein Kontaktformular für seinen Müll zu missbrauchen. Aus diesem Grund schicke ich mir selber eine E-Mail mit dem Mülltext.

// 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);
}

Sobald etwas in den versteckten Input-Feldern Name und Website erfasst wurde, wird die normale E-Mail Verarbeitung umgangen und eine spezielle E-Mail verschickt. Wenn Sie diese Kontrolle nicht möchten - einfach den else Zweig löschen.

Example Spam Mail
Eine BAD ROBOT E-Mail sieht dann beim Empfang so aus.

Die REMOTE_ADDR ist in dem Beispiel ::1 da die Beispiel E-Mail über meinen lokalen Webserver verarbeitet wurde. Siehe Beschreibung in meinem Blog Post - MAMP Webserver lokal mit Hugo benutzen . Ansonsten steht dort die IP-Adresse des Versenders. Ob das die richtige IP-Adresse ist oder ob diese manipuliert wurde ist unklar. Die REMOTE_HOST Angabe wird in den meisten Fällen leer sein. Spamer sind mit ihren eigenen Daten sehr zurückhaltend.

<Update - 21. Jul. 2021>
Am 19. Jul. 2021 habe ich die Website veröffentlicht und Google darüber durch eine sitemap.xml informiert. Am 20.Jul. 2021 war die Site noch nicht komplett im Index. Am 21. Jul. 2021 habe ich als erstes eine Spam-Mail erhalten. Zu dem Zeitpunkt war bisher noch kein externer Besucher auf der Website. Der Honeypot ist wirklich notwendig.
</Update>

contact.php - Benutzer Eingabefehler

Die Fehleingaben können in unterschiedlichen Kombinationen auftreten. Aus diesem Grund addiere ich jeweils eine andere Zahl hinzu. Es gibt maximal 5 unterschiedliche Fehlerkombinationen.

  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;
    }

Durch if($_SERVER[“REQUEST_METHOD”] == “POST”) wird auf den Klick des Absenden-Button gewartet. Der Inhalt des TextArea-Feldes (message) wird geprüft, ob überhaupt etwas eingegeben wurde. Wenn ja, wird die Länge auf maximal 16384 Bytes begrenzt. Dadurch wird ein bewusstes Sprengen der Speicherkapazität unterbunden. Es werden HTML-Tags entfernt und Zeilenwechsel in ein HTML <br> Tag konvertiert.

Die eingegebene E-Mailadresse benutze ich in dem PHP-Code nur im Text. Mit FILTER_VALIDATE_EMAIL wird überprüft ob wirklich eine E-Mailadresse eingegeben wurde.

Wenn ein Fehler festgestellt wurde, wird in einer case Abfrage die entsprechende Fehlermeldung in der jeweiligen Sprache ausgegeben.

contact.php - der eigentliche Mail-Versand

Hier wird jetzt nur in unterschiedlichen Sprachen der E-Mailtext zusammengestellt.

// 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;
    }

Nach dem Versand wird eine Statusmeldung ausgegeben.

SCSS für das Kontaktformular

Wie oben schon beschrieben benutze ich das Bootstrap Framework und muss deshalb nicht viele Anpassungen für das Design erstellen.

.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%;
  }
}

Warum brauche ich ein PHP Kontaktformular für mein Hugo Projekt?

Ein einfacher mailTo-Link im Impressum würde ja eigentlich ausreichen. Aber - in Deutschland gibt es sehr restriktive, rechtliche Vorgaben für Domain-Inhaber. Jeder muss ein Impressum zur Verfügung stellen. Neben der Adresse des Domain-Inhabers muss eine E-Mailadresse und eine Telefonnummer des Inhabers angegeben werden. Da ich meine Telefonnummer nicht im Internet veröffentlichen möchte, muss ich stattdessen ein Kontaktformular zur Verfügung stellen. Der Gesetzgeber geht davon aus, dass nicht jeder einen Mail-Provider hat und deshalb das Kontaktformular als Telefonnummer Ersatz zur Verfügung gestellt werden muss.

Hugo ist ein statischer Website Generator. Die Dynamik für ein Kontaktformular erhalte ich durch PHP. Es gibt externe Dienste die Kontaktformulare zur Integration anbieten. Ich möchte aber nicht auf externe Dienstleister, die Kosten spielen auch eine Rolle, angewiesen sein. Die rechtlichen Vorgaben der EU, für externe Dienstleistungen außerhalb der EU, sind sehr umfangreich bis unmöglich zu erfüllen.

Fazit

Ich programmiere normalerweise nicht in PHP. Sollte Ihnen ein sicherheitsrelevanter Fehler auffallen, schreiben Sie bitte einen Kommentar oder benutzen Sie das Kontaktformular um mich zu informieren. Durch die mail()-Funktion von PHP kann ich auf externe Libraries verzichten. Dadurch habe ich weniger Update Aufwand und kann mich um Dinge kümmern die mir wichtig sind.

Das könnte Sie auch interessieren

- Update 02. Aug. 2021 |
13 Minuten Lesezeit
0
Dieser Beitrag wurde mit der Hugo-Version 0.87.0 erstellt.

Kommentare werden bei deutscher Spracheinstellung nicht in der englischen Variante der Webseite angezeigt und umgekehrt.

© 2021 - Frank Kunert  -  Ich über mich
Ein Service von webdienste-kunert.de