FormMail.pl: Noch ein Formularmailer

Abstract

Zielsetzungen bei der Entwicklung dieses Formularmailers-Skripts waren: Aus diesen Anforderungen resultierte folgender Ansatz:

Übersicht

Der Ablauf gestaltet sich wie folgt:
  1. Das Skript wird per GET-Request aufgerufen; der Parameter FormURL muß die URL des Formulars enthalten:
     http://server/cgi-path/FormMail.pl?FormURL=relative_form_URL
  2. Die Formular-Seite wird per HTTP geholt.
  3. Die Formular-Seite wird geparst; es werden folgende Änderungen eingearbeitet:
  4. Das so veränderte Formular wird ausgegeben.
  5. Der Benutzer füllt das Formular aus, und drückt den SUBMIT-Button.
  6. Das Skript wird per POST-Request aufgerufen; die Parameter enthalten die Formular-Eingaben.
  7. Die Formular-Seite wird per HTTP geholt.
  8. Die Formular-Seite wird geparst; es wird getestet, ob alle benötigten Felder ausgefüllt wurden. Ist alles in Ordnung, werden die Namen/Werte-Paare der Eingabefelder dem Postprocessing-Skript übergeben.
  9. Fehlen benötigte Felder, oder schlägt das Postprocessing fehl, so wird das Formular (mit den Eingaben des Benutzers) erneut ausgegeben. Zurück zu 5.
  10. Die E-Mail wird verschickt.
  11. Ist beim Verschicken der E-Mail alles glattgegangen, gibt das Skript eine Umleitung auf eine Erfolgs-Seite aus; sonst eine Umleitung auf eine Fehlschlags-Seite.

Implementation

Schritte 1 und 6

Das Modul CGI vereinfacht u.a. den Zugriff auf die Parameter, mit denen das Skript aufgerufen wurde:
  use CGI;
  my $cgi = new CGI;
  my $form_url = $cgi->param('FormURL');
Im Skript selbst wird der Export-Set ':html' benutzt, um HTML-Ausgaben über die Funktionsschnittstelle von CGI zu machen. Die Objektschnittstelle wird benutzt, da für Debug-Zwecke die Funktionalität new(\*FILEHANDLE) genutzt wird.

Da der Parameter FormURL nur dem Skript selbst dient, würde er bei der weiteren Verarbeitung nur stören. Daher wird er aus der Liste der Parameter gelöscht:

  $cgi->delete('FormURL');
In Schritt 6 werden alle Eingabe-Werte geholt und gespeichert, wobei Mehrfachwerte zu einem String mit mehreren Zeilen zusammengefaßt werden:
  my %values;
  foreach ($cgi->param)
  {
    my @val = $cgi->param($_);
    $values{$_} = join "\n", @val;
  }

Schritte 2 und 7

Auch für diesen Vorgang gibt es Module, die die Handhabung vereinfachen: URI::URL, LWP::UserAgent, und HTTP::Request. Diese sind Teil der libwww-perl (LWP).
  use URI::URL;
  use LWP::UserAgent;
  use HTTP::Request;

  sub abs_url
  {
    # Erweitern einer URL zu einer absolute URL
    url( url( $_[0] )->path, $base_url )->abs->as_string;
  }

  $serverName = $cgi->server_name;
  if ($serverName =~ /^(?:\d+\.)+\d+$/)
  {
    use Socket;

    # Der Servername liegt als IP-Adresse vor.
    # Im folgenden wird er - wenn möglich - in einen Hostnamen verwandelt.
    my $s = gethostbyaddr inet_aton($serverName), AF_INET;
    $serverName = $s if defined $s;
  }
  $base_url = "http://$serverName/";

  my $url = abs_url( $form_url );
  # Abrufen des Inhalts
  my $ua = new LWP::UserAgent;
  $ua->agent( "$0/$VERSION" );
  my $request = HTTP::Request->new( GET => $url );
  my $response = $ua->simple_request( $request );
$response->is_success gibt Auskunft darüber, ob die Abfrage erfolgreich war, $response->content enthält den Inhalt der Formular-Seite.

Der Umweg, die Formular-Seite per HTTP zu laden, wurde gewählt, um die Verwendung von Webserver-Konstrukten (SSI, ASP u.ä.) zu ermöglichen, und um möglichen Sicherheitslücken vorzubeugen.

Schritte 3 und 8.1

Um ein HTML-Dokument in seine Einzelteile zu zerlegen, gibt es das Modul HTML::Parser. Es stellt eine Basisklasse zur Verfügung, von der man ableiten kann. Da das Skript nicht unnötig auf mehrere Dateien verstreut werden sollte, wurde die abgeleitete Klasse FormMail in das Skript integriert:
  {
    package FormMail;

    use vars qw(@ISA $AUTOLOAD);
    use HTML::Parser;

    @ISA = 'HTML::Parser';

    # ...
  }

  my $cgi_url = abs_url( $cgi->url );

  my $fmp = FormMail->new( $cgi_url, $form_url );
  $fmp->set_values( \%values );
  $fmp->parse( $response->content );
  $fmp->eof;
Die Methode new der Klasse FormMail initialisiert die Variablen des Objekts und blesst die Objekt-Variable:
  sub new
  {
    my ($class, $myurl, $formurl) = @_;
    my $self = $class->SUPER::new;

    $self->{FMsender}       = '';
    $self->{FMsubject}      = '';
    $self->{FMsuccess_url}  = '';
    $self->{FMfailure_url}  = '';
    $self->{FMprocess}      = '';
    $self->{FMpre_results}  = '';
    $self->{FMpost_results} = '';

    $self->{FMrecipient}    = '';
    $self->{FMmy_url}       = URI::URL::url( $myurl )->path;
    $self->{FMform_url}     = URI::URL::url( $formurl )->path;

    $self->{FMelements}     = [];
    $self->{FMfields}       = [];
    $self->{FMvalues}       = \0;

    bless $self, $class;
  }
Außerdem wird eine Methode AUTOLOAD definiert, um lesenden Zugriff auf diese Variablen zu gestatten:
  sub AUTOLOAD
  {
    my ($self) = @_;

    return if $AUTOLOAD =~ /::DESTROY$/;
    $AUTOLOAD =~ s/^FormMail::/FM/;

    die "No such variable '$AUTOLOAD'"
      if !exists $self->{$AUTOLOAD};

    return $self->{$AUTOLOAD};
  }
Die Methode set_values übergibt die Eingabe-Parameter an die Klasse:
  sub set_values
  {
    my ($self, $values) = @_;
    $self->{FMvalues} = $values;
  }
Die Methoden parse und eof werden von HTML::Parser gererbt.

Während des Parsens werden von HTML::Parser Methoden aufgerufen, welche die abgeleitete Klasse überladen kann (und sollte, um etwas Sinnvolles zu tun - in der Basisklasse sind diese Methoden leer). Sie stehen für die verschiedenen Typen von Elementen, die in einem HTML-Dokument vorkommen können:

declaration
Markup-Deklaration
start
öffnendes HTML-Tag
end
schließendes HTML-Tag
text
Text, d.h. alles außerhalb von HTML-Markup
comment
Kommentar
Es wird in der Variablen FMelements ein Array aus anonymen Arrays aufgebaut. Deren erstes Element enthält den Namen des HTML-Elements bzw. Typ des Nicht-HTML-Elements; alle weiteren Elemente enthalten Zusatzinformationen, z.B. für Markup-Deklarationen:
  sub declaration
  {
    my ($self, $decl) = @_;
    push @{$self->{FMelements}}, ['DECL', $decl];
  }
(Da die Namen von HTML-Tags den Methoden start und end stets in Kleinbuchstaben übergeben werden, wurde beschlossen, zur besseren Unterscheidung alle vom Skript generierten Namen in Großbuchstaben auszuführen.)

Bei einem öffenden HTML-Tag ist zu prüfen, ob es sich um das Tag FORM handelt, um die unter Schritt 3 genannten Änderungen am Kopf des Formulars und in demselben vornehmen zu können. Die Hash-Referenz $attr enthält die Attribute des Tags und deren Werte.

  sub start
  {
    my ($self, $tag, $attr) = @_;

    if ($tag eq 'form')
    {
      $attr->{action} =~ /^mailto:(\S+)/i
        or die "FormMail form requires a 'mailto' action";
      
      $self->{FMrecipient} = $1;

      # METHOD auf POST umschreiben
      $attr->{method} = 'POST';

      # ACTION auf uns selbst umschreiben
      $attr->{action} = $self->{FMmy_url};

      push @{$self->{FMelements}}, [$tag, $attr];

      # Verstecktes FormURL-Feld einfügen
      push @{$self->{FMelements}},
        [
          'INPUT',
          {
            type  => 'hidden',
            name  => 'FormURL',
            value => $self->{FMform_url}
          }
        ];
    }
    else
    {
      push @{$self->{FMelements}}, [$tag, $attr];

      if (  (($tag eq 'input' && $attr->{type} !~ /^(?:submit|reset)$/i)
             || $tag =~ /^(?:textarea|select)$/)
          && defined $attr->{name})
      {
        push @{$self->{FMfields}}, $attr->{name};
      }
    }
  }
Gleichzeitig wird hier das Array FMfields aufgebaut, das die Namen aller Eingabefelder des Formulars enthält (bzw. letztendlich die Namen derjenigen Felder, die in die E-Mail aufgenommen werden).

Bei einem schließenden HTML-Element und bei Text ist nicht mehr zu tun, als es in den Array FMelements aufzunehmen:

  sub end
  {
    my ($self, $tag) = @_;
    push @{$self->{FMelements}}, ['END', $tag];
  }

  sub text
  {
    my ($self, $text) = @_;
    push @{$self->{FMelements}}, ['TEXT', $text];
  }
Befehle an FormMail.pl sind in HTML-Kommentaren enthalten. Die allgemeine Form ist <!--!Kommando!Parameter-->. Die implementierten Kommandos sind:
  1. Kommandos, die mit dem Benötigtwerden von Eingabefeldern zu tun haben:
    required
    die Eingabefelder müssen ausgefüllt werden
    not_required
    die Eingabefelder müssen nicht ausgefüllt werden
    required_if
    Parameter ist ein boolscher Ausdruck der Form name eq/ne wert. Die Eingabefelder müssen ausgefüllt werden, wenn der boolsche Ausdruck wahr ist
    not_required_if
    Parameter ist ein boolscher Ausdruck der Form name eq/ne wert. Die Eingabefelder müssen nicht ausgefüllt werden, wenn der boolsche Ausdruck wahr ist
    Alle diese Kommandos gelten für alle Eingabefelder bis zum nächsten solchen Kommando.

  2. Kommandos, die Werte für die zu versendende E-Mail setzen:
    sender
    From:-Feld
    subject
    Subject:-Feld
    pre_results
    Parameter ist ein Text, der vor der Liste der Name/Wert-Paare in die Mail eingefügt wird
    post_results
    Parameter ist ein Text, der hinter der Liste der Name/Wert-Paare in die Mail eingefügt wird
    Das To:-Feld wird aus der FORM ACTION des Formulars bestimmt.

  3. Diverse Kommandos:
    process
    Parameter ist ein Dateiname eines Perl-Skripts, das sich im selben Verzeichnis wie Skript befinden muß. Nach der Prüfung auf Vorhandensein aller benötigen Eingabefelder wird die Routine FormMail::process dieses Skripts aufgerufen, und eine Referenz auf FMfields und FMvalues übergeben (die sie auch verändern kann). Liefert die Routine einen falschen Wert zurück, wird das Formular erneut ausgegeben.
    success_url
    Parameter ist die URL der Seite, auf die bei Erfolg umgeleitet wird
    failure_url
    Parameter ist die URL der Seite, auf die bei Fehlschlag umgeleitet wird
    only_if_first_time
    Parameter ist ein Text, der nur beim ersten Ausgeben des Formulars (Schritt 4) ausgegeben wird
    only_if_missing_required
    Parameter ist ein Text, der nur beim erneuten Ausgeben des Formulars (Schritt 9) wegen nicht ausgefüllter Eingabefelder ausgegeben wird
    only_if_process_fails
    Parameter ist ein Text, der nur beim erneuten Ausgeben des Formulars (Schritt 9) wegen fehlgeschlagenem Postprocessing ausgegeben wird
Der Code, um diese Kommandos zu erkennen und zu verarbeiten, sieht wie folgt aus:
  sub comment
  {
    my ($self, $comment) = @_;

    my ($command, $parameter) = ($comment =~ /^!(\w+)!(.*)$/s);
    if ($command)
    {
      $command = lc $command;
      if ($command =~
        /^(?:sender|
             subject|
             success_url|
             failure_url|
             process|
             pre_results|
             post_results
        )$/x)
      {
        for ($parameter) { s/^\s+//; s/\s+$//; }
        $self->{"FM$command"} = $parameter;
      }
      elsif ($command =~ /^(?:not_)?required$/)
      {
        push @{$self->{FMelements}}, ['CMD', $command];
      }
      elsif ($command =~ /^(?:not_)?required_if$/)
      {
        for ($parameter) { s/^\s+//; s/\s+$//; }
        push @{$self->{FMelements}}, ['CMD', $command, $parameter];
      }
      elsif ($command =~
        /^(?:only_if_missing_required|
             only_if_first_time|
             only_if_process_fails)
        $/x)
      {
        push @{$self->{FMelements}}, ['CMD', $command, $parameter];
      }
      else
      {
        die "Error: '$command' is not a valid FormMail command";
      }
    }
    else
    {
      push @{$self->{FMelements}}, ['CMNT', $comment];
    }
  }

Schritt 8.2

Die Aufgabe, festzustellen, ob alle benötigten Eingabefelder ausgefüllt wurden, reduziert sich nun auf ein Durchlaufen des Arrays FMelements, und a) das Umschalten eines Flags, wenn ein Kommando (not_)?required(_if)? angetroffen wird, und b) das Prüfen dieses Flags und des Werts, wenn ein Eingabe-Element angetroffen wird:
  sub required_ok
  {
    my ($self) = @_;
    my $value = $self->{FMvalues};

    my $is_required = $::default_is_required;
    for my $element (@{$self->{FMelements}})
    {
      my ($tag, $attr, $param) = @$element;
      if ($tag eq 'CMD')
      {
        if ($attr eq 'required_if')
        {
          $is_required = _eval_expression( $param, $value );
        }
        elsif ($attr eq 'not_required_if')
        {
          $is_required = ! _eval_expression( $param, $value );
        }
        elsif ($attr eq 'required')
        {
          $is_required = 1;
        }
        elsif ($attr eq 'not_required')
        {
          $is_required = 0;
        }
      }
      elsif (   $is_required
             && (($tag eq 'input' && $attr->{type} !~ /^(?:submit|reset)$/i)
                 || $tag =~ /^(?:textarea|select)$/)
             && defined $attr->{name}
             && !$value->{$attr->{name}})
      {
        return 0
      }
    }
    return 1;
  }
Den Evaluation-Engine traue ich mich kaum einzufügen, so trivial ist er bis jetzt &smiley;
  sub _eval_expression
  {
    my ($expression, $value) = @_;

    my ($var, $op, $val) = split /\s+/, $expression, 3;
    my $result = eval "defined \$value->{\$var} && (\$value->{\$var} $op '\$val')";
    die "Error in expression \"$expression\": $@\n" if $@;
    return $result;
  }

Schritt 8.3

Das Einbinden einer externen Prozedur für das Post-Processing ist nicht schwer:
  sub process_ok ($)
  {
    use FindBin;

    my ($self) = @_;

    return 1 if !$self->{FMprocess};

    my $path = $FindBin::Bin;
    $path = VMS::Filespec::unixify( $path ) if $^O eq 'VMS';

    my $process = $path . '/' . $self->{FMprocess};
    return 0 if !(-f $process && -r _ && require $process);
    return FormMail::process( \@{$self->{FMfields}}, $self->{FMvalues} )
  }
Die Variable $FindBin::Bin enthält das Verzeichnis, in dem sich das Skript befindet. Das Modul FindBin ist Bestandteil der Standard-Distribution.

Schritt 4 und 9

Es wird eine Methode as_HTML implementiert, welche die HTML-Seite als String zurückliefert (die Namensgebung ist an das Modul HTML::Element angelehnt). Diese tut nichts anderes, als den Array FMelements durchzugehen, und entsprechend dem ersten Element der Array-Elemente HTML-Teile an den String anzufügen.

In Schritt 9 wird außerdem berücksichtigt, ob die als benötigt gekennzeichneten Eingabe-Felder ausgefüllt wurden, und ob das Post-Processing erfolgreich war. Unterschieden werden die Schritte 9 und 4 hier an der Tatsache, ob diese boolschen Variablen übergeben werden oder nicht.

  sub as_HTML
  {
    my ($self, $required_ok, $process_ok) = @_;
    my $is_first_time = !defined $required_ok || !defined $process_ok;
    $required_ok = 1 if !defined $required_ok;
    $process_ok  = 1 if !defined $process_ok;

    my $value = undef;
    $value = $self->{FMvalues} if !$is_first_time;

    my $HTML = '';
    my $selectName = '';
    my $textareaName = '';

    # as this procedure relies on an empty key for recognizing
    # select and textarea sections...
    delete $value->{''} if exists $value->{''};

    ELEMENT:
    for my $element (@{$self->{FMelements}})
    {
      my ($tag, $attr) = @$element;

      ($tag eq 'CMD') && do
      {
        if (   ($attr eq 'only_if_first_time'       && $is_first_time)
            || ($attr eq 'only_if_missing_required' && !$required_ok )
            || ($attr eq 'only_if_process_fails'    && $required_ok && !$process_ok  ))
        {
          $HTML .= $element->[2];  # Insert CMD parameter as TEXT
        }

        next ELEMENT;
      };

      ($tag eq 'DECL') && do { $HTML .= "<!$attr>";     next ELEMENT; };
      ($tag eq 'CMNT') && do { $HTML .= "<!--$attr-->"; next ELEMENT; };

      ($tag eq 'TEXT') && do
      {
        # supress if text is a textarea default value,
        # and the user has entered something into that textarea
        $HTML .= $attr
          unless $value->{$textareaName};

        next ELEMENT;
      };

      ($tag eq 'END') && do
      {
        $HTML .= "</$attr>";

        # reset select/textarea special mode
        $selectName   = '' if $attr eq 'select';
        $textareaName = '' if $attr eq 'textarea';

        next ELEMENT;
      };

      # Default case:
      #   $tag is a HTML start tag,
      #   $attr is a hash reference of attributes

      my $attrname = $attr->{name}; # the most important attribute

      # fill in user's response
      if (   $tag eq 'input'
          && defined $attrname
          && defined $value->{$attrname})
      {
        if ($attr->{type} =~ /^text$/i)
        {
          $attr->{value} = $value->{$attrname};
        }
        elsif ($attr->{type} =~ /^(?:radio|checkbox)$/i)
        {
          # toggle the CHECKED attribute
          if ($value->{$attrname} =~ /^$attr->{value}$/m)
          {
            $attr->{checked} = undef;
          }
          else
          {
            delete $attr->{checked};
          }
        }
      }
      elsif ($tag eq 'option' && defined $value->{$selectName})
      {
        # toggle the SELECTED attribute
        if ($value->{$selectName} =~ /^$attr->{value}$/m)
        {
          $attr->{selected} = undef;
        }
        else
        {
          delete $attr->{selected};
        }
      }
      elsif ($tag eq 'select' && defined $attrname)
      {
        $selectName = $attrname; # save for 'option' case
      }
      elsif ($tag eq 'textarea' && defined $attrname)
      {
        $textareaName = $attrname; # save for default value
      }

      # output tag and attributes
      $HTML .= "<$tag";
      for my $a (keys %$attr)
      {
        $HTML .= " $a";
        $HTML .= "=\"$attr->{$a}\"" if defined $attr->{$a};
      }
      $HTML .= '>';

      # textarea is special in that its value is output as text
      if (   $tag eq 'textarea'
          && defined $attrname
          && defined $value->{$attrname})
      {
        $HTML .= $value->{$attrname};
      }
    }

    return $HTML;
  }
Eine spezielle Behandlung genießen hier SELECT- und TEXTAREA-Elemente, um (in Schritt 9) sicherzustellen, daß nicht Defaultwerte, sondern diejenigen Werte übernommen werden, die der Benutzer eingegeben hat.

Schritt 10

Da die Hauptplattformen, auf denen dieses Skript laufen soll, Windows NT und OpenVMS sind, wurde eine Mail-Verschickung ohne Gebrauch eines lokalen MTA (Mail Transfer Agent) wie z.B. sendmail implementiert. Dies setzt allerdings voraus, daß ein Mailhost existiert, der die Mail entgegennimmt und weiterbefördert. Dabei kommt das Modul Net::SMTP zum Einsatz, das Teil des libnet-Pakets ist.
  sub send_mail
  {
    use Net::SMTP;
  
    my ($self, $value) = @_;
    my $value = $self->{FMvalues};
  
    # check that all required variables have been filled in
    $self->{FMsender} ne '' && $self->{FMrecipient} ne ''
      or die "Required mail parameter is missing\n";

    # needed on architectures where Net::Domain::_hostname doesn't work
    my %options = ( Hello => $::serverName );
    $options{Debug} = 1 if $::debug;
  
    my @data = (
      "From: $self->{FMsender}\n",
      "To: $self->{FMrecipient}\n",
      "Subject: $self->{FMsubject}\n",
      # This MIME assumption may of course be wrong...
      "Mime-Version: 1.0\n",
      "Content-Type: text/plain; charset=iso-8859-1\n",
      "Content-Transfer-Encoding: 8bit\n",
      #
      "\n"
    );
    push @data, $self->{FMpre_results};
    my %seen;
    for my $e (@{$self->{FMfields}})
    {
      next if $seen{$e}++ || !exists $value->{$e};
      my $blanks = ' ' x length($e);
      my @val = split "\n", $value->{$e};
      push @data, sprintf "%s: %s\n", $e     , (shift @val || '');
      push @data, sprintf "%s  %s\n", $blanks,  shift @val
        while @val != 0;
    }
    push @data, $self->{FMpost_results};
  
    my $smtp = Net::SMTP->new( $::mailHost, %options );
    my $success = 0;
    if ($smtp)
    {
      $success   = $smtp->mail( $self->{FMsender} );
      $success &&= $smtp->recipient( $self->{FMrecipient} );
      $success &&= $smtp->data( \@data );
      $smtp->quit;
    }

    warn "Failed to send mail\n" if !$success;
    if (!$success && $::failFile)
    {
      if (open FAILFILE, $::failFile)
      {
        print FAILFILE '-' x 80, "\n";
        print FAILFILE join( '', @data ), "\n";
        close FAILFILE;
      }
      else
      {
        warn "Failed to save mail either: $!\n";
      }
    }

    return $success;
  }

Der Rest des Hauptprogramms

Der Gesamtablauf, so wie er in der Übersicht beschrieben wurde, macht den Rest des Hauptprogramms aus:
  my $request_method = $cgi->request_method();
  if ($request_method eq 'GET')
  {
    # Send the form

    print
      $cgi->header(-Pragma=>'No-Cache'),
      $fmp->as_HTML;

    exit;
  }

  die "REQUEST_METHOD is neither GET nor POST"
    unless $request_method eq 'POST';

  my $required_ok = $fmp->required_ok();
  my $process_ok  = $required_ok && $fmp->process_ok();

  unless ($required_ok && $process_ok)
  {
    # Resend the form

    print
      $cgi->header(-Pragma=>'No-Cache'),
      $fmp->as_HTML($required_ok, $process_ok);

    exit;
  }

  unless ($fmp->send_mail())
  {
    my $furl = $fmp->failure_url;
    if ($furl)
    {
      $furl = abs_url( $furl ) if $furl !~ m!^(?:http|ftp)://!;
      print $cgi->redirect( $furl );
    }
    else
    {
      # ...Fehlermeldung ausgeben
    }
  
    exit;
  }
  
  # everything went well, tell the user
  
  my $surl = $fmp->success_url;
  if ($surl)
  {
    $surl = abs_url( $surl ) if $surl !~ m!^(?:http|ftp)://!;
    print $cgi->redirect( $surl );
  }
  else
  {
    # ...Erfolgsmeldung ausgeben
  }
  

Resumee

Das Skript hat sich im täglichen Betrieb unter WindowsNT/IIS4 und OpenVMS/OSU-httpd bewährt. Auch die einfache Verwendbarkeit durch den Web-Designer der Firma und einen Kunden wurde schon getestet.

Über den Autor

Martin Vorländer begann seine Programmierer-Laufbahn mit 16 auf einem TI-59 (dieser wurde abgelöst von einem TI-99/4A, C=64, Atari ST, DOS und OS/2). Nach 8 kalten Wintersemestern in Clausthal-Zellerfeld brach er sein Studium der Mathematik (Nebenfach Informatik) an der dortigen TU ab und zog ins 300 Meter tiefer gelegene Goslar um. Dort hatte er seine erste Begegnung mit OpenVMS. Er ist der Meinung, daß die Meldungen über den Tod dieses seines Lieblings-Betriebssystems maßlos übertrieben sind, und beschäftigt sich nur widerwillig mit der eNTartung aus Redmond. Seit er Ende 1997 Perl entdeckte, hat er auch eine Lieblings-Programmiersprache.
Das komplette Skript (das Fehlerbehandlung, Debug-Möglichkeiten und POD auf Englisch beinhaltet) sowie ein Beispielformular und -Postprocessing-Skript stehen unter http://vms.pdv-systeme.de/users/martinv/FormMail.zip zur Verfügung.