Wie antwortete der Geschäftsführer auf die Frage, was für eine Software denn da beschafft würde, um die vorhandene eigenentwickelte abzulösen: »Weiß ich auch nicht so genau, aber etwas Modernes.«

Vor einem Vierteljahrhundert hatte ich ehrenamtlich die erste Website für meinen Verein erstellt, natürlich »handgestrickt«, wie damals üblich. Zehn Jahre danach war ich die HTML-Fummelei leid und die Arbeit sollte auch auf weitere Schultern (und Hände), sprich Vereinsmitglieder, verteilt werden. Ich installierte also ein CMS, und zwar nach reiflicher Überlegung und einigen Tests Drupal. Insbesondere überzeugten mich die ausgefeilten Möglichkeiten, Formulare und Arbeitsabläufe mit den Modulen »CCK« und »Views« (mittlerweile im Core) und »Rules« zu gestalten. Damit wurde zum Beispiel die Erstellung eines Online-Veranstaltungsprogrammes einschließlich automatisierter Freigabeverfahren und nachfolgender Übergabe an die Druckerei realisiert.

Mittlerweile gibt es vom Hauptverein die Vorgabe, WordPress einzusetzen. Geliefert werden auch einige vereinsspezifische Plugins und ein Theme für die einheitliche Erscheinung der Ortsvereine, die von einer beauftragten Agentur erstellt wurden. Natürlich ist WordPress mittlerweile nicht mehr ein auf eine Person zentriertes Blogger-CMS, aber bei der Umstellung musste ich schon einige Kröten schlucken. Da landeten einige Jahre Entwicklung einfach in der Tonne; nicht weil das bisherige schlecht funktionierte, sondern weil es mit den Mitteln von WordPress schlicht nicht oder nur unter für einen Verein nicht unerheblichen Kosten umzusetzen war. Um den bisherigen Entwicklungsstand zu halten, müsste man Geld für kostenpflichtige Plugins in die Hand nehmen oder – man glaubt es nicht – sie selbst entwickeln. (Bei obigem Beispiel wären das die Plugins »ACF«, »Content Views« und »Oasis Workflow« mit jährlichen Kosten für die benötigten Pro-Versionen von 49 + 39 + 119 = 207 USD. Langsam verstehe ich, warum Agenturen WordPress lieben.) Kommt mir aus meinem früheren Berufsleben bekannt vor, siehe oben.

Ein Punkt störte mich besonders, und das war das Kontakt-Formular. Bisher hatte ich die E-Mail-Adressen der Mitglieder immer mit Zähnen und Klauen verteidigt. Im CMS waren sowieso nur die funktionalen Vereinsadressen hinterlegt, die Umsetzung auf die privaten Weiterleitungsadressen erfolgte in der Verwaltung des Web-Servers. Das geht natürlich weiterhin. Aber vorgegeben war das Plugin »Contact Form 7« (kurz CF7). Und bei dem sah es auf den ersten Blick für mich so aus, als müsse man erstens alle Adressen dort statisch hinterlegen und dass sie zweitens dadurch außerdem noch im Formular sichtbar werden. Mit dem vom Hauptverein gelieferten Plugin für die Personenverwaltung waren die aber bereits erfasst worden. Und wie pflegte ein Kollege immer zu sagen: »Wer eine Information zum zweiten Mal erfasst, macht etwas falsch.«

Ich habe mir also die Dokumentation von CF7 näher angeschaut, und meine Miene hellte sich wieder auf.

Das erste Manko hat der Ersteller des Plugins erkannt und flugs ein weiteres nachgeschoben: Listo. Das kann über die Option »data« in der CF7-Formulardefinition Daten anliefern; zunächst aber nur vorgegebene. Schaut man sich den Quellcode von listo.php an, sieht man, dass der Entwickler dem Plugin ein Interface spendiert hat:

interface Listo {
	public static function items();
	public static function groups();
}

Und die anschließend definierte Klasse Listo_Manager kann man natürlich in OO-Manier mit einer eigenen beerben (bei mir heißt sie Listo_Personen), die man in die functions.php des (Child-)Themes einfügt. Die überschreibende Funktion items() muss dann in diesem Falle eine Liste mit Empfängernamen liefern. Woher sie die bekommt, hängt natürlich von den individuellen Gegebenheiten ab. Die Daten können aus einem WP Post Type kommen (wie in meinem Fall), aus einer externen Datenbank, usw.:

class Listo_Personen implements Listo
{
    // Build a dynamic recipients list with functional names (Funktionsbezeichnungen) for Contact Form 7.
    // Implements the "data" option in CF7 for the "Personen" plugin.
    // [select* to data:personen include_blank default:get]
    public static function items()
    {
        $args = array(
            'post_type' => 'personen',
            'numberposts' => -1,
            'meta_key' => 'funktionale_email-bezeichnung', // The key for the ACF field
            'orderby' => 'meta_value',  // Order by functional name
            'order' => 'ASC'
        );
        $personen = get_posts($args);  // Get all personen
        if (!$personen)
            return array();
        // Retrieve the meta data for each post of this post type
        foreach ($personen as $person) {
            $post_id = $person->ID;
            $funktionale_email_bezeichnung = get_post_meta($post_id, 'funktionale_email-bezeichnung', true);
            //$funktionale_email_adresse = get_post_meta($post_id, 'funktionale_email-adresse', true);
            if ($funktionale_email_bezeichnung) {
                // Put it in the "items" array for the CF7 "data" option, 
                // but do not expose the email address in the "select" field of the form. 
                // HTML Select Tag: Option Text = Value
                $items[$funktionale_email_bezeichnung] = $funktionale_email_bezeichnung;
            }
        }
        return $items;
    }
    public static function groups()
    {
    }
}

Das Ganze muss man dem Listo-Plugin natürlich noch bekannt machen:

// Add own list types for the Listo plugin
function add_list_types_to_listo($list_types)
{
    $list_types['personen'] = 'Listo_Personen';
    return $list_types;
}
add_filter('listo_list_types', 'add_list_types_to_listo');

Und warum wird oben in der Funktion Listo_Personen.items() nicht die E-Mail-Adresse, nur die funktionale Bezeichnung zurückgegeben?

Weil es tatsächlich Experten gibt, die vorschlagen, in der Auswahlliste von CF7 das Pipe-Symbol zu verwenden, um E-Mail-Adressen vor SPAM-Bots zu verstecken:

<label> Select Department
[select* department include_blank "Sales|sales@example.com" "Support|support@example.com" "CEO|ceo@Wexample.com"]</label>

Und verkünden allen Ernstes: »That’s it. You have now successfully added a dropdown menu with selectable email addresses on your contact form, protect these email addresses from email harvesting using the pipe character …«

Und tatsächlich, der Browser zeigt:

Dropdown List GUI

Da klappt mir die Kinnlade runter. Haben die nicht in den von CF7 generierten HTML-Code geguckt? Der Harvester tut’s mit Sicherheit:

<label> Select Department
<select>
<option value"sales@example.com">Sales</option>
<option value"support@example.com">Support</option>
<option value"ceo@example.com">CEO</option>
</select>
</label>

Das würde natürlich nicht nur bei statischen, sondern auch bei dynamisch generierten Listen passieren.

Und wenn wir in der items()-Funktion geschrieben hätten

$items[$funktionale_email_bezeichnung] = $funktionale_email_adresse;

wäre genau das passiert.

Die Daten haben wir also. Weiter geht’s!

Als Nächstes brauchen wir eine Funktion, die sich in den Sende-Mechanismus von CF7 einklinkt (Hook) und eine gültige E-Mail-Adresse für den im Formular selektierten Empfänger liefert, also zum Beispiel »Beschwerdestelle« durch »beschwerden-vk3@firma.de« oder auch eine persönliche Adresse ersetzt.

Als Gerüst (wieder in functions.php) könnte das so aussehen:

/** 
 * In das CF7-Plugin einklinken (Hook 'wpcf7_before_send_mail') und die im Formular im select-Feld 'to' ausgewählte 
 * oder per GET übergebene funktionale Bezeichnung durch die E-Mail-Adresse der betreffenden Person ersetzen. 
 * Damit wird keine E-Mail-Adresse preisgegeben, wie das bei der Pipe-Lösung der Fall ist.
 */
function process_cf7($contact_form, &$abort, $submission)
{
    // Getting the post and form IDs
    $post_id = $submission->get_meta('container_post_id');
    $form_id = $contact_form->id();  // $form_id = $contact_form->posted_data['_wpcf7'];
    // The submission object is an instance of the WPCF7_Submission, 
    // and we can use it to retrieve the data submitted to the form.
    $posted_data = $submission->get_posted_data();
    // $posted_data is now an array that contains all of the form information:
    /* Array (
        [to] => Beschwerdestelle
        [your-name] => Emma Muster
        [your-email] => emma-muster@mail.com
        [your-subject] => Test
        [your-message] => Nur ein Test.
    ) */
    switch ($form_id) {
        // contact-form-7 id="6974" title="Ihre Mitteilung an uns"
        case 6974:
            // Get a list of functional email addresses for the 'data' parameter of the cf7 form. 
            // [select* to data:personen include_blank default:get] 
            $args = array(
                'post_type' => 'personen',
                'numberposts' => -1,
                'orderby' => 'title',
                'order' => 'ASC'
            );
            //Send the email to the appropriate recipient. Fallback to webmaster or postmaster.
            $personen = get_posts($args);
            if (!$personen) {
                $recipient_email = 'postmaster@firma.de';
            } else {
                // Also fallback to webmaster or postmaster, if symbolic recipient (funktionale E-Mail-Adresse) 
                // from the cf7 form not found in personen 
                $recipient_email = 'postmaster@firma.de';
                foreach ($personen as $person) {
                    $post_id = $person->ID;
                    $funktionale_email_bezeichnung = get_post_meta($post_id, 'funktionale_e-mail-bezeichnung', true);
                    $funktionale_email_adresse = get_post_meta($post_id, 'funktionale_e-mail-adresse', true);
                    if (strtolower($funktionale_email_bezeichnung) == strtolower($posted_data["to"][0])) {
                        $recipient_email = $funktionale_email_adresse;
                        break;
                    }
                }
            }
            $properties = $contact_form->get_properties();
            $properties['mail']['recipient'] = $recipient_email;
            // update the form properties
            $contact_form->set_properties($properties);
            break;
        default:
            write_log('Contact form not processed: $form_id=' . $form_id);
            return;
    }
    return $contact_form;
}
add_action('wpcf7_before_send_mail', 'process_cf7', 10, 3);

Das CF7-Formular mit der ID 6974 bekommt folgendes Select-Feld:

[select* to data:personen include_blank default:get]

Dann erstellen wir (mindestens) eine Seite (Page), in die der Shortcut des CF7-Formulars eingetragen wird:

[contact-form-7 id="6974"]

Sie bekommt in meinem Fall den Permalink »https://domain.tld/kontakt/«. Wird dieser Link aufgerufen, muss der Besucher eine symbolische Adresse aus der Dropdown-Liste auswählen (wegen include_blank). Wird aber in einer Verlinkung an den URL ?to=Beschwerdestelle angehängt, wird wegen default:get in der Dropdown-Liste die betreffende Zeile ausgewählt. Nützlich für Wendungen, wie »Wenn Sie nicht zufrieden waren, wenden Sie sich bitte an unsere Beschwerdestelle«. (Bei symbolischen Adressen, die Umlaute oder Sonderzeichen enthalten, müssen diese natürlich URL-encodiert werden.)

Geschafft! Das Ganze im Zusammenhang (mylist ist im obigen konkreten Fall personen):

CF7 aufgebohrt Schema

Und, ach ja, die vom Hauptverein beauftragte Entwicklung des Themes und der Plugins wurde mittlerweile eingestellt. Die Agentur hat den Code auf GitHub veröffentlicht. Da kann ich jetzt ganz ungeniert einen Fork anlegen, um da meine Anpassungen reinzuschreiben.

Wobei wir wieder beim Thema der Überschrift wären.

(Bildquelle: JMortonPhoto.com & OtoGodfrey.com, CC BY-SA 4.0, via Wikimedia Commons)