diff --git a/bin/parse-mail b/bin/parse-mail new file mode 100755 index 0000000..5bcf370 --- /dev/null +++ b/bin/parse-mail @@ -0,0 +1,45 @@ +#!/usr/bin/php + + * @author Sebastien Palma + * @version 0.1 + */ + +require_once('../config/config.php'); +require_once('../lib/parser.php'); +require_once('../lib/storage.php'); + +$args = $_SERVER['argv']; + +$mailbox = $args[1]; + +$stdin = fopen('php://stdin', 'r'); +$content = ''; +while($line = fread($stdin, 1024)) { + $content .= $line; +} + +$bounce = new bounceParser($content); +$bounce->parse(); + +if ($bounce->getErrorCode()) { + $dbstore = new databaseStorage(); + $dbstore->store_in_db($bounce, $mailbox); +} + + +//if ($bounce->getErrorCode()) print($bounce->getErrorCode().'|'.$bounce->getBouncedEmail().'|'.$bounce->getBounceReason()."|".$bounce->getServerAnswer()."\n"); +//print($bounce->getErrorCode().'|'.$bounce->getBouncedEmail().'|'.$bounce->getBounceReason()."|".$bounce->getServerAnswer()."\n"); + +//exit($bounce->getStatus()); + +?> diff --git a/bin/parse-maildir b/bin/parse-maildir new file mode 100755 index 0000000..b8f8730 --- /dev/null +++ b/bin/parse-maildir @@ -0,0 +1,67 @@ +#!/bin/bash + +# +# parse-maildir +# +# Copyright (c) 2009 Evolix - Tous droits reserves +# +# $Id: index.php 310 2009-10-19 16:04:34Z tmartin $ +# vim: expandtab softtabstop=4 tabstop=4 shiftwidth=4 showtabline=2 +# +# @author Thomas Martin +# @author Sebastien Palma +# @version 0.1 +# + +SCRIPT_START=`date '+%Y-%m-%d %H:%M:%S'` + +TOTAL_BOUNCES=0 + +# Test the parameters presence +if [ "$#" -lt 1 ]; then + echo "Usage of $0:" + echo "$0 {maildir} {database}" + exit 9; +fi + + +MAILDIR=`echo $1 | cut -d"/" -f5` + +STOCK_DIR=`date '+%Y%m%d'` + + +if [ ! -d "$1/.$STOCK_DIR" ]; then + mkdir -p $1/.$STOCK_DIR/{tmp,cur,new} +fi + + +for mail in `ls $1/new/` +do + cat $1/new/$mail | ./parse-mail $MAILDIR + mv $1/new/$mail $1/.$STOCK_DIR/new/ + ((TOTAL_BOUNCES=$TOTAL_BOUNCES+1)) +done + + +for mail in `ls $1/cur/` +do + cat $1/cur/$mail | ./parse-mail $MAILDIR + mv $1/cur/$mail $1/.$STOCK_DIR/cur/ + ((TOTAL_BOUNCES=$TOTAL_BOUNCES+1)) +done + +SCRIPT_END=`date '+%Y-%m-%d %H:%M:%S'` + +#if [ "$1" = "/home/vmail/example.com/no-reply" ]; then + +cat < + * @author Sebastien Palma + * @version 0.1 + */ + +define('BOUNCE_HARD', 1); +define('BOUNCE_SOFT', 2); +define('BOUNCE_UNKN', 9); + +require_once("../config/database.php"); + +// Maildir path +$config = array ( + $search_maildir = '/home/vmail/example.com/no-reply', +); + +$bounce_regex = array( + 1 => array( // Utilisateur n'existe pas + 'User unknown', // wanadoo, orange, 9online, neuf, voila, bluewin.ch + 'Unknown user', // tlb.sympatico.ca + 'This account has been disabled or discontinued', // yahoo + 'The email account .* does not exist', // google + 'mailbox unavailable', // hotmail, live, msn + 'blocked due to inactivity', // free, aliceadsl + 'Recipient address rejected.*User unknown', // laposte + 'skynet\.be.*quota exceeded', // Skynet + 'MAILBOX NOT FOUND', // AOL + 'IS NOT ACCEPTING ANY MAIL', // AOL + 'unknown or illegal alias', // Videotron + 'Inactive MailBox', // Numericable + 'Mailbox has moved' // divers serveurs + ), + 2 => array( // Erreurs de domaines (mal formulés) + 'Operation timed out', + 'Connection refused', + 'Connection timed out', + 'Host not found' + ) +); + + + +?> diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..10993af --- /dev/null +++ b/config/database.php @@ -0,0 +1,7 @@ +A definir : + * define('SERVEUR','localhost'); + * define('SERVEURPORT',3306); + * define('BASE','example'); + * define('NOM', 'root'); + * define('PASSE', 'nopass'); + * + * @author Gregory Colpart + * @author Alexandre Anriot + * @copyright Copyright (c) 2004,2005,2006 Evolix + * @version 1.0 + */ + +class Mysql +{ + + /* + * Connexion a une base MySQL + * les constantes SERVEUR, NOM, PASSE et BASE devront etre definies + * il convient de les definir dans un fichier connect.php + */ + + function MyConnect() + { + $connexion = mysql_connect(SERVEUR, NOM, PASSE); + + if (! $connexion) + { + echo "Une erreur s'est produite : ".mysql_error()."\n"; + exit; + } + + if (! mysql_select_db(BASE, $connexion)) + { + echo "Une erreur s'est produite : ".mysql_error()."\n"; + exit; + } + + return $connexion; + } + + + /** + * Executer une requete SQL quelconque + * + * A noter que cette fonction peut s'ecrire en une ligne + * mais reste sous cette forme pour plus de clarté + * + * @param resource $connexion + * @param string $req + * @return mixed (resource or boolean) + */ + function MyReq($connexion,$req) { + + if ($query = mysql_query($req,$connexion)) { + return $query; + } else { + // print "erreur requete SQL"; + return FALSE; + } + } + + + /* + * Requete MySQL optimale renvoyant une seule variable + */ + + function MyExecReq($connexion,$req) { + if ($r = mysql_fetch_row(mysql_query($req,$connexion))) { + return current($r); + } else { + return FALSE; + } + } + + /* + * Requete MySQL renvoyant un objet + * + * Exemple d'utilisation : + * $req = 'SELECT * FROM main'; + * $obj = Mysql::MyObjectReq($con,$req); + * print("result = $obj->title"); + */ + + function MyObjectReq($connexion,$req) { + if ($query = mysql_query($req,$connexion)) { + return mysql_fetch_object($query); + } else { + return FALSE; + } + } + + /* + * Requete MySQL renvoyant un tableau associatif + * + * Exemple d'utilisation : + * $req = 'SELECT * FROM main'; + * $assoc = Mysql::MyAssocReq($con,$req); + * print($assoc['title']); + */ + + function MyAssocReq($connexion,$req) { + if ($query = mysql_query($req,$connexion)) { + return mysql_fetch_assoc($query); + } else { + return FALSE; + } + } + + /** + * Requete insertion MySQL (INSERT ou UPDATE) + * renvoie 1 si insertion correcte, 0 sinon + */ + function MyInsertReq($connexion,$req) { + return (mysql_query(Html::sqlclean($req),$connexion)) ? TRUE : FALSE; + } + + /** + * Executer une requete SQL renvoyant UNE réponse sur plusieurs lignes + * + * @param resource $connexion + * @param string $req + * @return array + */ + function MyGetArray($connexion,$req) { + + $result = array(); + $query = Mysql::MyReq($connexion,$req); + while ($res = mysql_fetch_row($query)) { + array_push($result,current($res)); + } + return $result; + } + + /** + * Executer une requete SQL renvoyant DEUX réponses sur plusieurs lignes + * + * @param resource $connexion + * @param string $req + * @return array + */ + function MyGetHash($connexion,$req) { + + $result = array(); + $query = Mysql::MyReq($connexion,$req); + while ($res = mysql_fetch_row($query)) { + $result[current($res)] = next($res); + } + return $result; + } + + /** + * Parcourir les differentes lignes pour les requetes complexes + * (plusieurs réponses et plusieurs lignes) + * + * Exemple d'utilisation : + * while ($data = Mysql::MyFetchObject($query)) { ... + * + * @param resource + * @return object + */ + function MyFetchObject($query) { + return mysql_fetch_object($query); + } + + + +} + +?> diff --git a/lib/parser.php b/lib/parser.php new file mode 100644 index 0000000..2b430e2 --- /dev/null +++ b/lib/parser.php @@ -0,0 +1,257 @@ + + * @author Sebastien Palma + * @version 0.1 + */ + + +class bounceParser { + private $content = NULL; + private $bounced_mail = NULL; + private $smtp_error_code = NULL; + private $status = BOUNCE_UNKN; + private $boundary = NULL; + private $boundaries = array(); + private $headers = array(); + private $mailer = NULL; + private $bounce_server_answer = NULL; + private $bounce_reason = NULL; + private $message_id = NULL; + + + private $bounce_official_reasons = array ( + '00' => 'Not Applicable', + + '10' => 'Other address status', + '11' => 'Bad destination mailbox address', + '12' => 'Bad destination system address', + '13' => 'Bad destination mailbox address syntax', + '14' => 'Destination mailbox address ambiguous', + '15' => 'Destination mailbox address valid', + '16' => 'Mailbox has moved', + '17' => 'Bad sender\'s mailbox address syntax', + '18' => 'Bad sender\'s system address', + + '20' => 'Other or undefined mailbox status', + '21' => 'Mailbox disabled, not accepting messages', + '22' => 'Mailbox full', + '23' => 'Message length exceeds administrative limit.', + '24' => 'Mailing list expansion problem', + + '30' => 'Other or undefined mail system status', + '31' => 'Mail system full', + '32' => 'System not accepting network messages', + '33' => 'System not capable of selected features', + '34' => 'Message too big for system', + + '40' => 'Other or undefined network or routing status', + '41' => 'No answer from host', + '42' => 'Bad connection', + '43' => 'Routing server failure', + '44' => 'Unable to route', + '45' => 'Network congestion', + '46' => 'Routing loop detected', + '47' => 'Delivery time expired', + + '50' => 'Other or undefined protocol status', + '51' => 'Invalid command', + '52' => 'Syntax error', + '53' => 'Too many recipients', + '54' => 'Invalid command arguments', + '55' => 'Wrong protocol version', + + '60' => 'Other or undefined media error', + '61' => 'Media not supported', + '62' => 'Conversion required and prohibited', + '63' => 'Conversion required but not supported', + '64' => 'Conversion with loss performed', + '65' => 'Conversion failed', + + '70' => 'Other or undefined security status', + '71' => 'Delivery not authorized, message refused', + '72' => 'Mailing list expansion prohibited', + '73' => 'Security conversion required but not possible', + '74' => 'Security features not supported', + '75' => 'Cryptographic failure', + '76' => 'Cryptographic algorithm not supported', + '77' => 'Message integrity failure', + ); + + public function __construct($content) { + $this->content = $content; + } + + public function parse() { + $this->sanitize(); + + $this->message_id = $this->findMessageID(); + + $this->status = $this->findStatus(); + } + + + private function sanitize() { + // Sépare les parties de mail par boundary + $this->boundary = '--'.$this->findBoundary(); + + // Nettoie les parties en mettant tout sur une ligne + // pour améliorer la rapidité des regex + $this->boundaries = $this->splitAndCleanBoundaries($this->boundary); + + // Décodage des textes + // TODO + } + + + private function findMessageID() { + if (preg_match('/.*Message-Id: <([^>]*)>.*$/', $this->boundaries[0], $bounce_infos) == 1) { + $message_id = $bounce_infos[1]; + } + + return $message_id; + } + + private function findBoundary() { + + $boundary=''; + + // Recherche du délimiteur + preg_match('/boundary="(.*?)"/', $this->content, $preg_results); + + // Si on le trouve, on le renvoie + if ($preg_results[1] != '') { + $boundary = $preg_results[1]; + } else { + // TODO: Gestion des erreurs ? + } + return $boundary; + } + + private function splitAndCleanBoundaries($boundary_delimiter) { + + // Decoupage du mail par boundary + $boundaries = array(); + $boundaries_orig = explode($boundary_delimiter, $this->content); + + // Nettoyage du boundary + foreach($boundaries_orig as $boundary_part) { + $boundary = $boundary_part; + + // On met tout le boundary sur une seule ligne + $boundary = preg_replace('/\n/m', ' ', $boundary); + + // On remplace toutes les tabulations par un espace + $boundary = preg_replace('/\t/', ' ', $boundary); + + // Suppression des doubles espaces + $boundary = preg_replace('/\s{2,}/', ' ', $boundary); + + // "Trim" + $boundary = preg_replace('/^\s*/', '', $boundary); + $boundary = preg_replace('/\s*$/', '', $boundary); + + // Et on le stocke dans un tableau + $boundaries[] = $boundary; + } + + return $boundaries; + } + + + private function findStatus () { + // Analyse du bounce + global $bounce_regex; + + // Pour le moment, on se sait pas si c'est un bounce hard|soft|spam + $score = 9; + + // Mail de bounce classique de Postfix + if (preg_match('/.*-Recipient: rfc822;\s?([^\s]*).*Status: ([\d\.]*).*Diagnostic-Code: (.*)$/', $this->boundaries[2], $bounce_infos) == 1) { + + $this->mailer = "Postfix"; + + if (array_key_exists(1, $bounce_infos)) $this->bounced_mail = $bounce_infos[1]; + if (array_key_exists(2, $bounce_infos)) $this->smtp_error_code = $bounce_infos[2]; + if (array_key_exists(3, $bounce_infos)) $this->bounce_server_answer = $bounce_infos[3]; + + + // Construction de la regexp qui nous assure que c'est un hardbounce + foreach ($bounce_regex as $severity_id => $severity) { + $bounce_pattern=''; + foreach ($severity as $count => $pattern) { + if ($bounce_pattern!="") $bounce_pattern.='|'; + $bounce_pattern .= $pattern; + } + + $bounce_pattern = "/^.*($bounce_pattern).*/"; + //print "Pattern : $bounce_pattern\n\n"; + + if (preg_match($bounce_pattern, $this->boundaries[2])==1) { + //print "FOUND!!!!\n\n"; + $this->smtp_error_code="6.0.".$severity_id; + $this->bounce_reason="HardBounce detected by Bounce-Parser"; + $score=6; + } + } + + if ($this->smtp_error_code != NULL && $this->smtp_error_code != '' && $score==9) { + $score = substr($this->smtp_error_code, 0, 1); + $this->bounce_reason = $this->bounce_official_reasons[substr(str_replace('.', '', $this->smtp_error_code),1,2)]; + } + + // Test de reconnaissance d'un Out of Office + } elseif (false) { + + + // Le reste... Bah on sait pas ce que c'est (SPAM?) + } else { + + } + + return $score; + } + + public function getStatus() { + return $this->status; + } + + public function getMessageID() { + return $this->message_id; + } + + public function getErrorCode() { + return $this->smtp_error_code; + } + + public function getBouncedEmail() { + return $this->bounced_mail; + } + + public function getBounceReason() { + return $this->bounce_reason; + } + + public function getServerAnswer() { + return $this->bounce_server_answer; + } + + public function getBoundary() { + return $this->boundary; + } + + public function getBoundaries($boundary_id) { + return $this->boundaries[$boundary_id]; + } + +} + +?> diff --git a/lib/storage.php b/lib/storage.php new file mode 100644 index 0000000..aaed280 --- /dev/null +++ b/lib/storage.php @@ -0,0 +1,53 @@ + + * @author Sebastien Palma + * @version 0.1 + */ + +require_once('../lib/Mysql.php'); + + +class databaseStorage { + private $mysql=NULL; + private $connexion=NULL; + + public function __construct() { + $this->mysql = new Mysql(); + $this->connexion = $this->mysql->MyConnect(); + } + + public function store_in_db($bounce, $mailbox='') { + $sql = "INSERT INTO `bounces"; + + if ($mailbox!='') $sql .= "_$mailbox"; + + $sql.= "` VALUES(NULL,"; + $sql.= "'".$bounce->getStatus()."',"; + $sql.= "'".$bounce->getMessageID()."',"; + $sql.= "'".$bounce->getErrorCode()."',"; + $sql.= "'".$bounce->getBounceReason()."',"; + $sql.= "'".$bounce->getServerAnswer()."',"; + $sql.= "'".date('Y-m-d H:i:s')."',"; + $sql.= "'".$bounce->getBouncedEmail()."',"; + + $domain=''; + $email_parts = explode('@', $bounce->getBouncedEmail()); + if (is_array($email_parts) && array_key_exists(1, $email_parts)) $domain=$email_parts[1]; + $sql.= "'".$domain."'"; + $sql.= ")"; + + //echo "$sql\n"; + $this->mysql->MyReq($this->connexion, $sql); + } + +} +