barrierefreies Webdesign Ingo Turski

zum Inhalt

Ingo

Tips und Tricks:Einfachster, sicherer und universeller Formmailer

Um Formulardaten als E-Mail zu versenden, hat mailto: mangels Browserunterstützung längst ausgedient und kann u.U. noch über mein Javascript eingesetzt werden. Besser ist hingegen ein serverseitiger Versand, der meist über ein PHP-Script erfolgt. Im Internet gibt es Tausende davon, aber die meisten sind leider unsicher und können über Manipulation der Header-Daten als „Spamschleuder“ missbraucht werden und Tausende von Spam-Mails an beliebige Empfänger versenden mit der Folge, dass die Seite vom Provider gesperrt wird oder gar Schadenersatzforderungen von Mitbenutzern des Mailservers aufgrund der Eintragung desselben in eine Spam-Blacklist gestellt werden.

Die PHP-mail()-Funktion erwartet mindestens drei Parameter: 1. Empfängeradresse, 2. Betreff, 3. Mailtext. Über einen zusätzlichen 4. Parameter ist meist noch eine Absenderadresse anzugeben. Die ersten beiden und ggfls. der 4. Parameter werden, durch einen Zeilenumbruch getrennt, als Mailheader gesendet. Vor dem dritten Parameter setzt die Funktion einen doppelten Zeilenumbruch, der die Mailheader vom Mailbody (dem Inhalt) trennt. Wenn nun Formulardaten ungeprüft in die Mailheader übernommen werden und einen Zeilenumbruch enthalten, können weitere Mailheader (und damit auch Empfängeradressen) eingeschmuggelt werden. Ein sicheres Script prüft daher die Formulardaten oder versendet sie ausschließlich im Mailbody und trägt in den anderen Parametern eigene Werte ein.
Hierzu reicht im Prinzip folgendes Script schon aus:

<?php
if(!empty($_POST))
  mail("mail@example.org", "Nachricht",
   "Folgende Daten wurden übermittelt im Formular-".print_r($_POST,true),
   "From: formmailer@example.org");
?>

Dieses Script muss lediglich in die PHP-Seite eingetragen werden, an die die Formulardaten gesandt werden, was auch durchaus dieselbe Seite sein kann, die das Formular enthält. Man spricht in diesem Fall von einem „Affenformular“.
Die E-Mail wird dann an »mail@example.org« mit dem Betreff »Nachricht« und dem Absender »formmailer@example.org« verschickt. Der Inhalt des $_POST-Arrays, das alle übermittelten Formulardaten enthält, wird im Mailbody in dieser Form geschrieben:

Folgende Daten wurden übermittelt im Formular-Array
(
    [Name] => Ingo
    [Nachricht] => Test des Formmailers
)

Die Ausgabe ist zwar ungewohnt, aber übersichtlich und eingetragene Seiten- oder Mailadressen werden vom Mailprogramm meist auch automatisch verlinkt.

Was noch fehlt, sind einige der folgenden Überprüfungen und Rückmeldungen:

Wurde das Formular abgeschickt?

Diese Funktion liefert dann true zurück:

function Formular_abgeschickt() {
  return !empty($_POST);
}

Wurde das Formular (zumindest teilweise) ausgefüllt?

Diese Funktion liefert dann true zurück:

function Formular_Daten() {
  foreach ($_POST as $key => $val) {
    if(trim($val)) return true;
  }
  return false;
}

Magic_Quotes?

Leider wurde in früheren PHP-Versionen ein Automatismus zur Verfügung gestellt, der einfache und doppelte Anführungszeichen in über GET, POST oder COOKIE übertragenen Daten maskiert, um unbedarfte Programmierer vor Manipulationen in Sonderfällen zu bewahren. Da sich diese üble Funktion nicht bei jedem Provider z.B. über den Eintrag php_flag magic_quotes_gpc Off in der Apache-Konfigurationsdatei .htaccess abschalten lässt, sollte ein universelles Script die Daten in diesem Fall bereinigen. Der folgende Code ersetzt außerdem die vorherigen Funktionen und stellt stattdessen Variablen zur Verfügung:

if($Formular_abgeschickt = !empty($_POST)) {
  $Formular_leer = true; ini_set('magic_quotes_runtime',0);
  $_POST = array_map('Formular_Daten', $_POST);
}
function Formular_Daten($val) {
  global $Formular_leer;
  if(is_array($val)) return array_map('Formular_Daten', $val);
  if(ini_get('magic_quotes_gpc')) $val = stripslashes($val);
  if($val = trim($val)) $Formular_leer = false;
  return $val;
}

Wurden evtl. Pflichtfelder nicht ausgefüllt?

Für diese Funktion muss zuvor über $Pflichtfelder=array(); ein Array angelegt sein, in das bei Bedarf die Namen der zu prüfenden Pflichtfelder eingetragen werden. Sie liefert einen Leerstring oder entsprechende Fehlermeldungen zurück:

function Formular_Pflichtfelder() {
  global $Pflichtfelder;
  $Fehler = '';
  foreach ($Pflichtfelder as $Feld) {
    $key = str_replace(' ','_',$Feld);
    if(!(isset($_POST[$key]) && trim($_POST[$key])!=='')) {
      if($Fehler) $Fehler .= '<br />';
      $Fehler .= 'Pflichtfeld "' . $Feld . '" nicht ausgefüllt.';
    }
  }
  return $Fehler;
}

Wurden die Formulardaten (versehentlich) doppelt abgeschickt?

Um dies zu vermeiden, werden oft Sessions eingesetzt oder eine Weiterleitung genutzt, die dem Prinzip des „Affenformulars“ widerspricht, das alle Funktionen zur Formularausgabe, -Auswertung und -Rückmeldung sinnvoll vereinigt. Ich nutze hierzu eine ganz simple Methode, für die dem Verzeichnis allerdings Schreibrechte eingeräumt werden muss.
Diese Funktion liefert bei wiederholtem Versenden false zurück:

function Formular_neu($log='.htPOSTdata.txt') {
  if(file_exists($log) && is_readable($log)
   && file_get_contents($log) == print_r($_POST,true))
  return false;
  if($handle=@fopen($log, 'w')) {
    fwrite($handle, print_r($_POST,true)); fclose($handle);
  }
  return true;
}

Ohne Schreibrechte wird immer true zurück geliefert; um in diesem Fall doppeltes Versenden zu vermeiden, könnte man z.B. auch den Betreff in einer Session speichern und abfragen.
Übrigens keine Sorge, dass Außenstehende die letzte verschickte Mail einsehen können - ein Apache-Server ist standardmäßig so konfiguriert, dass er den Zugriff auf Dateien, deren Name mit ".ht" beginnt (wie insb. ".htaccess") verweigert.

Kombination der Prüfungen und Fehlermeldungen

Diese Funktion nutzt die vorherigen und liefert Fehlermeldungen oder einen Leerstring zurück:

function Formular_Check() {
  global $Formular_leer;
  if($Formular_leer) $Fehler = 'Keine Daten eingetragen.';
  elseif(!$Fehler = Formular_Pflichtfelder()) {
    if(!Formular_neu()) $Fehler = 'Nachricht war bereits verschickt.';
  }
  return $Fehler;
}

Bereits erfolgte Eingaben in das Formular übertragen

Diese Funktion wird mit dem name-Attribut des Feldes im Formular dort aufgerufen, wo eine evtl. erfolgte Eingabe ausgegeben werden soll:

function Formular_Eingabe($Feldname, $def='') {
  if(isset($_POST[$Feldname]) && $_POST[$Feldname]!=='')
    echo htmlspecialchars($_POST[$Feldname],ENT_COMPAT,'ISO-8859-15');
    // ggfls. Zeichenkodierung anpassen (Vorgabe ist seit PHP 5.4 UTF-8!)
  else echo $def; // ggfls. übergebenen Vorgabewert ausgeben
}

Sonderfälle: Checkboxen, Radio-Buttons und Select-Options

Um auch für diese (im einfachen Kontaktformuar nicht vorkommenden) Sonderfälle die Auswahl zu übernehmen, dient diese Funktion. Ihr wird außer dem name-Attribut auch der Wert des Feldes übergeben sowie "checked" für Radio-Buttons und Checkboxen oder "selected" für Options und optional als letzter Parameter true für ein vorselektiertes Element:

function Formular_Auswahl($Feldname,$Wert,$Auswahl,$def=false) {
  if(
    (empty($_POST[$Feldname]) && $def) ||
    (!empty($_POST[$Feldname]) &&         // Achtung: $Wert darf nicht "0" sein!
      ($_POST[$Feldname]==$Wert ||
       (is_array($_POST[$Feldname]) && in_array($Wert,$_POST[$Feldname]))
      )
    )
  ) echo ' ', $Auswahl, '="', $Auswahl, '"';
}

# Beispielaufruf dieser Funktion für eine Checkbox:
<input name="AGB" type="checkbox" value="Ja" <?php
  Formular_Auswahl('AGB','Ja','checked'); ?>
/>
<label for="AGB">Ich akzeptiere die AGB.</label>

# Beispielaufruf dieser Funktion für Radio-Buttons:
<input type="radio" id="Herr" name="Anrede" value="Herr"
  <?php Formular_Auswahl('Anrede','Herr','checked');
  ?>
><label for="Herr">Herr</label> &nbsp;
<input type="radio" id="Frau" name="Anrede" value="Frau"
  <?php Formular_Auswahl('Anrede','Frau','checked',true); // Vorauswahl
  ?>
><label for="Frau">Frau</label>

# Beispielaufruf dieser Funktion für eine Auswahlliste:
<label for="Anrede">Wie darf ich Sie ansprechen?</label>
<select id="Anrede" name="Anrede">
  <option <?php Formular_Auswahl('Anrede','Herr','selected'); ?>>Herr</option>
  <option <?php Formular_Auswahl('Anrede','Frau','selected','true'); ?>>Frau</option>
</select>

Ein komplettes, einfaches aber sicheres Kontaktformular

Diese Funktionen sollten zu einer sicheren Auswertung eines Kontaktformulars ausreichen, aber da sich dieser Tipp auch an Anfänger richtet, die bisher unsichere PHP-Scripte aus dem Internet genutzt haben, biete ich nachfolgend den kompletten Code für ein sicheres Kontaktformular an - in der Hoffnung, die „Spam-Schleudern“ im Internet zu reduzieren.

Um die Regeln der RFC einzuhalten, werden in diesem Script Umlaute und Sonderzeichen ab PHP 5.3.0 maskiert. Wenn eine ältere PHP-Version installiert ist und der Mail-Server nicht RFC-konforme Mails abweisen sollte (was allerdings nur sehr selten vorkommen soll), könnten Sie auf eine Mailer-Klasse wie PHPMailer zurückgreifen, wobei mit der class.phpmailer.php jedoch rund 137 KB PHP-Code eingebunden und Anpassungen vorgenommen werden müssen.

<?php
define ('MAILTO', "mail@example.org"); // Empfänger hier eintragen
define ('MAILFROM', "Kontaktformular@example.org"); // ggfls. Absender hier eintragen
define ('SUBJECT', "Nachricht vom Kontaktformular"); // ohne Sonderzeichen
define ('CHARSET', "ISO-8859-15"); // Zeichenkodierung ggfls. anpassen
$Pflichtfelder = array('Nachricht'); // ggfls. weitere Pflichtfelder angeben
define ('MailAnzeige', true); // Nachricht nach Versand angezeigen
define ('FormularLink', true); // nach Versand Link auf neues Formular ausgeben
define ('FormularAnzeige', false); // keine Formularausgabe nach Versand


define ('LF', chr(13).chr(10)); // Zeilenumbruch
$AddHeader = (MAILFROM) ? 'From: '.MAILFROM.LF : "";
if(function_exists('quoted_printable_encode')) { // ab PHP 5.3.0 für RFC-Konformität
  $AddHeader .= 'MIME-Version: 1.0';
  $AddHeader .= LF.'Content-Type: text/plain; charset='.CHARSET;
  $AddHeader .= LF.'Content-Transfer-Encoding: quoted-printable';
}
else $AddHeader .= 'Content-Type: text/plain; charset='.CHARSET;

if($Formular_abgeschickt = !empty($_POST)) {
  $Formular_leer = true;
  if(ini_get('magic_quotes_runtime')) ini_set('magic_quotes_runtime',0);
  $_POST = array_map('Formular_Daten', $_POST);
}
function Formular_Daten($val) {
  global $Formular_leer;
  if(is_array($val)) return array_map('Formular_Daten', $val);
  if(ini_get('magic_quotes_gpc')) $val = stripslashes($val);
  if($val = trim($val)) $Formular_leer = false;
  return $val;
}

function Formular_Pflichtfelder() {
  global $Pflichtfelder;
  $Fehler = '';
  foreach ($Pflichtfelder as $Feld) {
    $key = str_replace(' ','_',$Feld);
    if(!(isset($_POST[$key]) && trim($_POST[$key])!=='')) {
      if($Fehler) $Fehler .= '<br />';
      $Fehler .= 'Pflichtfeld "' . $Feld . '" nicht ausgefüllt.';
    }
  }
  return $Fehler;
}

function Formular_neu($log='.htPOSTdata.txt') {
  if(file_exists($log) && is_readable($log)
   && file_get_contents($log) == print_r($_POST,true))
  return false;
  if($handle=@fopen($log, 'w')) {
    fwrite($handle, print_r($_POST,true)); fclose($handle);
  }
  return true;
}

function Formular_Check() {
  global $Formular_leer;
  if($Formular_leer) $Fehler = 'Keine Daten eingetragen.';
  elseif(!$Fehler = Formular_Pflichtfelder()) {
    if(!Formular_neu()) $Fehler = 'Nachricht war bereits verschickt.';
  }
  return $Fehler;
}

function Formular_Eingabe($Feldname, $def='') {
  if(isset($_POST[$Feldname]) && $_POST[$Feldname]!=='')
    echo htmlspecialchars($_POST[$Feldname],ENT_COMPAT,CHARSET);
  else echo $def;
}

function Formular_Nachricht($HTML=false) {
  $msg=''; $vor=''; $nach=': ';
  foreach ($_POST as $key => $val) {
    if($key != 'abschicken' && trim($val)) { // if(true) um alle Felder auszugeben
      if($HTML) {
        $msg .= '<strong>'.$vor.$key.$nach.'</strong>'
        .htmlspecialchars($val,ENT_COMPAT,CHARSET).'<br />';
      }
      else {
        if(function_exists('quoted_printable_encode')) {
          $val = quoted_printable_encode($val);
        }
        $msg .= $vor.$key.$nach.$val.LF.LF;
      }
    }
  }
  return $msg;
}

$Meldung = ""; $id = "";
if($Formular_abgeschickt) {
  if($Formular_Fehler = Formular_Check()) {
    $Meldung = $Formular_Fehler; $id = 'Fehler';
  }
  elseif(@mail(MAILTO, SUBJECT, Formular_Nachricht(), $AddHeader)) {
    $Meldung = 'Nachricht verschickt.'; $id = 'OK';
  }
  else {
    $Meldung = 'Server-Fehler !'; $id = 'Fehler';
  }
}
?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de">
<head>
  <title>Kontaktformular</title>
  <meta http-equiv="Content-Type" content="text/html; charset=<?php echo CHARSET; ?>" />
</head>
<body>
<h1 id="Kontakt">Kontakt</h1>

<?php
if($Meldung) echo '<p class="Meldung" id="',$id,'">',$Meldung,'</p>';
if($id=='OK') {
  if(MailAnzeige) echo '<p id="Nachricht">',Formular_Nachricht(true),'</p>';
  if(FormularLink) {
    echo '<p id="FormularLink">
      <a href="'.$_SERVER['SCRIPT_NAME'].'">neues Formular?</a>
    </p>';
  }
}

if(FormularAnzeige || $id != 'OK'): ?>


<form action="<?php echo $_SERVER['SCRIPT_NAME']; ?>#Kontakt" method="post"
 enctype="multipart/form-data" accept-charset="<?php echo CHARSET; ?>">
  <fieldset><legend>Kontaktformular</legend>
    <p>
      <label for="Name">Ihr Name:</label>
      <input name="Name" id="Name" size="66"
       value="<?php Formular_Eingabe('Name'); ?>" />
    </p>
    <p>
      <label for="Nachricht">Nachricht:</label>
      <textarea name="Nachricht" id="Nachricht" rows="5" cols="50"><?php
       Formular_Eingabe('Nachricht'); ?>
</textarea>
    </p>
    <p><input type="submit" value="abschicken" /></p>
  </fieldset>
</form>

<?php endif; ?>

</body>
</html>

Zur besseren Übersicht habe ich die PHP-Codebereiche farblich hervorgehoben. Damit diese ausgeführt und nicht im HTML-Quelltext ausgegeben werden, muss die Datei auf einem PHP-fähigen Webspace liegen und sollte mit der Endung ".php" benannt sein.
Sie können das Script auf zwei Arten in Ihre Seite integrieren:

  1. Sie kopieren den gesamten Code (zunächst) in einen reinen Texteditor wie Notepad++, damit die für die HTML-Anzeige der Code-Einrückungen erforderlichen geschützten Leerzeichen nicht als solche mit dem Sonderzeichen chr(160) übernommen werden.
  2. Sie speichern den Code z.B. unter "Kontakt.php" und ergänzen den HTML-Bereich mit Ihren Seiteninhalten.
  3. Sie ändern ggf. die Endung ".html" Ihrer bestehenden Seite in ".php" und fügen die PHP-Bereiche dort ein.

In beiden Fällen muss zumindest die Zeile »define ('MAILTO', "mail@example.org");« angepasst und ggf. auch die Formularfelder geändert oder weitere hinzugefügt werden.
Wenn Ihr Kontaktormular erst nach Scrollen erreicbar ist, sollte dessen Überschrift wie hier mit einer id versehen und das Ziel des Formulars mit einem Ankersprung hierauf erweitert werden, damit die Rückmeldungen direkt ersichtlich sind.

Weitere Felder und Prüfungen hinzufügen

Wenn Sie z.B. eine E-Mail-Adresse als Pflichtfeld hinzufügen und diese auf grobe Tippfehler prüfen möchten, gehen Sie wie folgt vor:

  1. Erweitern Sie die Definition der Pflichtfelder:
    $Pflichtfelder = array('Nachricht', 'eMail');
  2. Kopieren Sie die PHP-Funktion checkEmail() aus meinem Tipp »E-Mail-Adressen prüfen« zwischen die übrigen Funktionen.
  3. Ergänzen Sie die Formularprüfung wie folgt:
    function Formular_Check() {
      global $Formular_leer;
      if($Formular_leer) $Fehler = 'Keine Daten eingetragen.';
      elseif(!$Fehler = Formular_Pflichtfelder()) {
        if(!checkEmail($_POST['eMail'])) $Fehler = 'E-Mail fehlerhaft.';
        else
    if(!Formular_neu()) $Fehler = 'Nachricht war bereits verschickt.';
      }
      return $Fehler;
    }
  4. Fügen Sie die E-Mail-Abfrage in das Formular ein:
    <p>
      <label for="eMail">Ihre E-Mail:</label>
      <input name="eMail" id="eMail" size="64"
       value="<?php Formular_Eingabe('eMail'); ?>" />
    </p>

Die eingegebene E-Mail als Absender verwenden

Wenn Sie wie oben beschrieben die Abfrage einer E-Mail-Adresse als geprüftes Pflichtfeld hinzugefügt haben und diese als Absender der Mail eintragen möchten, gehen Sie wie folgt vor:

  1. Löschen sie die Absenderangabe:
    define ('MAILFROM', "");
  2. Erweitern sie den mail()-Befehl:
    @mail(MAILTO, SUBJECT, Formular_Nachricht(), 'From: '.$_POST['eMail'].LF.$AddHeader)
  3. Kopieren Sie statt der Funktion checkEmail() die Funktion checkEmailRFC() aus meinem Tipp »E-Mail-Adressen prüfen« zwischen die übrigen Funktionen und tragen diesen Funktionsnamen statt checkEmail in der Funktion Formular_Check() ein.

Der Mailserver könnte fremde Absenderangaben allerdings möglicherweise nicht akzeptieren.

Eine Datei als Anhang versenden

Hierzu müssen Sie zunächst das HTML-Formular erweitern, z.B. so:

<p>
  <label for="Datei">Datei anfügen:</label>
  <input id="Datei" name="Datei" type="file" size="70" />
</p>

Außerdem erweitern Sie den PHP-Code am Anfang:

$AddHeader = 'MIME-Version: 1.0'.LF;
$boundary = strtoupper(md5(uniqid(time())));
$AddHeader .= 'Content-Type: multipart/mixed; boundary='.$boundary.' charset='.CHARSET;

Wird fortgesetzt... das Prinzip für den restlichen Code wird aber z.B. auf
https://www.teialehrbuch.de/Kostenlose-Kurse/PHP/9404-Versenden-einer-E-Mail-mit-Anhang.html
recht gut erklärt.