Fortsetzung von Tutorial: Mail-Autoresponder mit PHP - Teil 2
Es gibt Frühstück mit Cappuccino, frischgepresstem Saft (Orange-Karotte-Ingwer) und Brötchen - und für Euch den nächsten Teil unseres kleinen Autoresponder-Tutorials. Die Sonne scheint, und bevor ich mich auf's Fahrrad schwinge bleibe ich hier noch ein wenig sitzen und tippe fleißig weiter - es ist viiiel zu gemütlich, als dass ich jetzt schon in die Pedale treten wollte
Weil das aus den bisherigen Code-Schnipseln nicht ersichtlich war (und um Fragen vorzubeugen): JA, das ganze wird natürlich in eine hübsche, flexible und erweiterbare Klasse verpackt. Sie MUSS sogar erweitert werden - es gibt nämlich eine ganze Reihe von Anwengungs-Szenarien für einen solchen Autoresponder: ich kann und will euch hier nicht jede einzelne davon vorkauen. Ziel und Zweck dieses Tutorials ist in erster Linie die Vermittlung des nötigen Basis-KnowHows zur Entwicklung eines solchen Dienstes - nicht mehr und nicht weniger.
if (! isset($argv[1])) {
fail("Invalid parameters");
}
switch ($argv[1]) {
case 'pipe':
if (! $argv[2]) {
fail('Sender needs to be specified');
}
$returnPath = $argv[2];
$mail_source = file_get_contents('php://stdin');
break;
case 'file':
$filename = $argv[2];
if (! $argv[2]) {
fail('File needs to be specified');
}
if (! is_readable($filename)) {
fail('Cannot read provided mail file');
}
$mail_source = file_get_contents($filename);
break;
default:
fail('Unknown method specified');
}
Ich hatte euch ja bisher vorenthalten, woher unsere Variable $mail_source
kommt. Obiger Code erlaubt, dass unser kleiner Autoresponder auf zweierlei
Art und Weise aufgerufen werden kann. In der master.cf von Postfix geschieht das z.B. wie folgt:
autoresponder unix - n n - - pipe
flags= user=mailbox argv=/usr/local/bin/autoresponder pipe ${sender}
Man kann das Tool aber auch mit einem Dateinamen als Parameter aufrufen:
/usr/local/bin/autoresponder file testmail.txt
Nach dem das jetzt geklärt wäre, geht es munter mit einer Serie von Sicherheits-Checks weiter:
if ((isset($hdr['auto-submitted'])
&& strtolower(trim($hdr['auto-submitted'])) != 'no')
|| (isset($hdr['precedence'])
&& preg_match('~(?:list|bulk|junk)~i', $hdr['precedence']))
) {
fail(sprintf(
'Bulk or ML mail, sending no reply to mail from %s',
$sender
));
}
Der erste Header der uns interessiert ist Auto-Submitted. Wenn ein solcher Header vorhanden ist, und dort nicht explizit no steht, werden wir nicht antworten. Auch Mails mit list, bulk oder junk im Precedence-Header sind selten solche, welche eine Urlaubsbenachrichtigung zu schätzen wissen.
foreach (array_keys($hdr) as $key) {
if (preg_match('~^list\-~', $key)) {
fail(sprintf(
'Bulk or ML mail, sending no reply to mail from %s',
$sender
));
}
}
Auch wenn ein mit "List-" beginnender Header vorhanden ist, ersparen wir unseren Mitmenschen eine Benachrichtigung! Anschließend betreiben wir noch ein wenig "Heuristik". Wir nehmen RFC5321.MailFrom unter die Lupe und verzichten auf Antworten, wenn der eigentliche Sender einer der üblichen Verdächtigen ist:
function getSystemUsers()
{
return array(
'root',
'www-data',
'wwwrun',
'nobody',
'apache',
'nagios',
'httpd',
'postmaster',
'svn',
'return'
'newsletter'
);
}
if (in_array($returnUserpart, $this->getSystemUsers())) {
fail('Ignore mail, return-path is a system account');
}
So langsam kommen wir der Sache schon näher! Sofern der Empfänger bekannt ist, wird eine Benachrichtigung gesendet - falls nicht bereits geschehen. An diesem Teil werde ich noch ein wenig feilen, habe im Kopf schon eine Variante, die sowohl für Installationen mit virtuellen Benutzern, also auch für die Nutzung in .forward und Ähnlichem funktionieren wird. Nachfolgender Code zeigt aber schon mal, wie so etwas grundsätzlich aussehen könnte:
if ($recipient === null) {
fail(sprintf(
'Sending NO reply to mail from %s to unknown recipients',
$sender
));
} else {
if (replyHasBeenSentTo($sender, $recipient)) {
fail(sprintf(
'Not sending reply as it has already been sent to %s',
$sender
));
}
if (sendNotification($sender, $recipient)) {
logger(sprintf(
'Sending reply to mail from %s to %s',
$sender,
$recipient
));
} else {
logger(sprintf(
'Sending reply to mail from %s to %s FAILED',
$sender,
$recipient
));
}
}
Weil wir schon so richtig schön in Schwung sind, erstellen wir auch gleich die Benachrichtigungsfunktion:
function sendNotification($to, $from)
{
$mime = new Mail_mime("\n");
$subject = getSubject($from);
$txt = getVacationMessage();
$mime->setTXTBody($txt);
$body = $mime->get();
$headers = $mime->headers(array(
'Return-Path' => getConfig('auto_sender'),
'Precedence' => 'bulk',
'Auto-Submitted' => 'auto-replied',
'To' => '<' . $to . '>',
'From' => '<' . $from . '>',
'Date' => date('r'),
'Subject' => $subject
));
$mail = Mail::factory('smtp', getConfig('smtp_transport'));
if (isDryRun()) {
$res = $mail->send(getConfig('dry_to'), $headers, $body);
} else {
$res = $mail->send($to, $headers, $body);
}
if (is_a($return, 'PEAR_Error')) {
return false;
} else {
storeToHistory($to, $from);
return true;
}
}
Die Funktionen getSubject() und getVacationMessage() werden hierbei in der Basis-Klasse ganz simple vordefinierte Texte liefern. Sie können in der jeweiligen Umsetzung zwecks Personalisierung nach Belieben erweitert werden:
function getSubject($sender)
{
return 'Abwesenheitsnotiz / Messaggio di assenza'
. ' / out of office';
}
Die Konfiguration könnte dabei in etwa wie folgt aussehen:
$config = array(
'smtp_transport' => array(
'host' => 'localhost',
'port' => 25,
'localhost' => 'mail.mydomain.tld'
),
'auto_sender' => 'no-reply@autoreply.mydomain.tld',
'dry_run' => false,
'dry_to' => 'thomas@gelf.net'
);
So, das reicht für heute - sonst habe ich ja nichts mehr von diesem Kaiserwetter! Darf heute ohnehin nur die paar Kilometer bis ins Büro treten - dort wartet dann jede Menge Arbeit auf mich In der nächsten Folge werde ich die vollständige Basis-Klasse unseres Autoresponders präsentieren und näher auf die verschiedenen Anwendungs-Szenarien eingehen.
nicht dass Du denkst, den Text liest keiner - im Gegenteil, ich habe ihn in allen drei Teilen gelesen. Ich wollte eigentlich erst nach dem vierten Teil, den Du ja mehr oder weniger am Anfang dieses Teils angekündigt hast (die komplette Klasse "am Stück") kommentieren, aber ich mache es schon mal jetzt: SUPER! Das ist wirklich mal ein Tutorial, das man dank der theoretischen Hinterlegung nachvollziehen kann - nicht einfach nur "wir machen jetzt dies und dann das", sondern auch mal "weil das so ist, machen wir nun das". Von der Sorte hätte ich gern mehr Danke!
Vielen lieben Dank für die netten Worte! So etwas hört man richtig gerne Eine Woche lang war ich jetzt so ziemlich weg vom Fenster, da Tag und Nacht mit einer größeren Migration beschäftigt.
Das Ärgste ist überstanden, und der vierte Teil des Tutorials beinahe fertig. Der Code ist jetzt doch noch ein wenig umfangreicher geworden, darum die Verzögerung. Dazu aber in Kürze mehr!