Re: Train Spamassassin

Reindl Harald <[email protected]> Thu, 5 Jan 2017 14:47:54 +0100
Newsgroups gmane.mail.imap.dbmail
Organization the lounge interactive design
Message-ID <[email protected]>

Am 05.01.2017 um 12:56 schrieb Claas Kähler:
> Okay, i did an export from my Junk folder with a Thunderbird Plugin, it
> worked quiet well.
>
> But at least that is not a very nice way! Does someone know a good
> script that pulls a folder from imap in a sa-learn compatible format?

a plugin?
just "save as" - the eml files *are* sa-learn compatible by definition 
because they are nothing else than a raw-message in mbox format

> Thoses 2 do not work, sa-learn could not encode them:
> https://github.com/rtucker/imap2maildir
> https://github.com/rcarmo/imapbackup

see attachment

the trainig-scripts itself are not included, it just pulls samples and 
talks to spamd to ignore junk which is already classified as BAYES_99

> Am 05.01.17 um 04:50 schrieb Reindl Harald:
>>
>>
>> Am 05.01.2017 um 03:10 schrieb Ryan Butler:
>>> There's also a dbmail-export command isn't there to turn it into mbox
>>> files?
>>
>> yes, but you need to fix your picture how to train SA *properly*
>>
>> blowing each and every email there without 100% classification is the
>> wrong way - every single missclassified message does that much harm
>> that you need 10-20 correct ones fixing the damage
>>
>>> On Wed, Jan 4, 2017 at 5:21 PM, Reindl Harald <[email protected]
>>> <mailto:[email protected]>> wrote:
>>>
>>>
>>>
>>>     Am 04.01.2017 um 22:32 schrieb Claas Kähler:
>>>
>>>         Okay this part is easy, but how could i extract samples from
>>>         dbmail to
>>>         put them into a folder?
>>>
>>>
>>>     by IMAP - you are not supposed to directyl access email via mysql
>>>     since you underestimate the complexity of the strcuture and must not
>>>     rely on internal aka non-public API's
>>>
>>>         Am 04.01.17 um 22:26 schrieb Reindl Harald:
>>>
>>>
>>>
>>>             Am 04.01.2017 um 20:50 schrieb Claas Kähler:
>>>
>>>                 Hallo everybody,
>>>
>>>                 I have a serious Spam-Problem and I want to feed
>>>                 sa-learn with those
>>>                 Spam-mails to get rid of it.
>>>
>>>                 Has someone found a solution to train Spamassassin with
>>>                 data from
>>>                 DBMail?
>>>
>>>
>>>             just put your samples in folders and use "sa-learn" with the
>>>             correct user
>>>
>>>
>>>             [root@mail-gw:~]$ bayes-stats.sh
>>>             0      81476    SPAM
>>>             0      25659    HAM
>>>             0    3227434    TOKEN
>>>
>>>             insgesamt 376M
>>>              24K -rw-r----- 1 sa-milt sa-milt  24K 2017-01-04 16:29
>>>             bayes_seen
>>>              65M -rw-r----- 1 sa-milt sa-milt  81M 2017-01-04 16:29
>>>             bayes_toks
>>>             312M -rw-r----- 1 sa-milt sa-milt 312M 2017-01-04 16:29
>>>             wordlist.db
>>>
>>>             BAYES_00         1682   58.99 %
>>>             BAYES_05           84    2.94 %
>>>             BAYES_20           81    2.84 %
>>>             BAYES_40          104    3.64 %
>>>             BAYES_50          394   13.81 %
>>>             BAYES_60           56    1.96 %     9.75 % (OF TOTAL
>>> BLOCKED)
>>>             BAYES_80           60    2.10 %    10.45 % (OF TOTAL
>>> BLOCKED)
>>>             BAYES_95           38    1.33 %     6.62 % (OF TOTAL
>>> BLOCKED)
>>>             BAYES_99          352   12.34 %    61.32 % (OF TOTAL
>>> BLOCKED)
>>>             BAYES_999         285    9.99 %    49.65 % (OF TOTAL
>>> BLOCKED)
>>>
>>>             DELIVERED        5104   91.91 %
>>>             DNSWL            4984   89.75 %
>>>             SPF              4088   73.61 %
>>>             SPF/DKIM WL      2250   40.51 %
>>>             SHORTCIRCUIT     2690   48.44 %
>>>
>>>             BLOCKED           574   10.33 %
>>>             SPAMMY            506    9.11 %    88.15 % (OF TOTAL
>>> BLOCKED)
>> _______________________________________________
>> DBmail mailing list
>> [email protected]
>> http://lists.nfg.nl/mailman/listinfo/dbmail
>
> _______________________________________________
> DBmail mailing list
> [email protected]
> http://lists.nfg.nl/mailman/listinfo/dbmail

-- 

Reindl Harald
the lounge interactive design GmbH
A-1060 Vienna, Hofmühlgasse 17
CTO / CISO / Software-Development
m: +43 676 40 221 40
p: +43 1 595 3999 33
http://www.thelounge.net/

_______________________________________________
DBmail mailing list
[email protected]
http://lists.nfg.nl/mailman/listinfo/dbmail
spamfilter-fetch-samples.php (application/x-php, 11.1 KB)
#!/usr/bin/php
<?php
/** Konfiguration */
$imap_host     = '192.168.196.1';
$imap_user     = 'spamfilter';
$imap_pwd      = '****';
$imap_spam     = 'TRAIN-SPAM';
$imap_ham      = 'TRAIN-HAM';
$spamd_socket  = '/run/spamd-debug/spamassassin.sock';
$corpus_folder = '/var/lib/spamass-milter/training';
$train_script  = '/usr/bin/bash /scripts/sa-learn.sh';
$stats_script  = '/usr/bin/php /scripts/spamfilter/corpus-stats.php today';
$imap_delete   = true;
$training      = false;

/** Sicherstellen dass Extension geladen ist */
if(!function_exists('imap_fetch_overview'))
{
 exit("ERROR: Extension 'php-imap' nicht geladen\n");
}

/** Wir merken uns Hashes duchgelaufener Dateien um keine 1:1 Duplikate abzulegen */
$hash_list = [];

/**
 * SPAM-TRAINING
 *
 * IMAP-Verbindung aufbauen
 * Nachrichten abfragen
 * RAW-Messages in Temp-Files schreiben
 * Check gegen SpamAssassin
 * Wenn nicht mindestens BAYES_99 in Trainingsfolder verschieben
 * Wenn neue Samples im Trainings-Folder unser zentrales Learning-Skript aufrufen
*/
$new_spam_samples = [];
$result_types = ['BAYES_00', 'BAYES_05', 'BAYES_20', 'BAYES_40', 'BAYES_50', 'BAYES_60', 'BAYES_80', 'BAYES_95', 'BAYES_99', 'BAYES_999'];
$imap_connection = imap_open('{' . $imap_host . ':143/imap/novalidate-cert}' . $imap_spam, $imap_user, $imap_pwd, OP_SILENT, 5);
if($imap_connection)
{
 $imap_info = @imap_check($imap_connection);
 if($imap_info->Nmsgs > 0)
 {
  foreach(@imap_fetch_overview($imap_connection,"1:{$imap_info->Nmsgs}",0) as $msg)
  {
   if(!$msg->deleted)
   {
    $raw_headers = @imap_fetchheader($imap_connection, $msg->msgno);
    $raw_body    = @imap_body($imap_connection, $msg->msgno);
    if(!empty($raw_headers) && !empty($raw_body))
    {
     $raw_message = cleanup_sample(cleanup_mozilla_removed_attachments($raw_headers . $raw_body));
     unset($raw_headers, $raw_body);
     $tmp_file = '/tmp/' . strftime('%Y-%m-%d-%H-%M-%S') . '-' . hash('MD4', $raw_message) . '.eml';
     file_put_contents($tmp_file, $raw_message);
     $spamd_result = spamc_report(/**$socket_path*/$spamd_socket, /**$eml_path*/$tmp_file, /**$user*/'sa-milt');
     $bayes = '';
     foreach($result_types as $result_type)
     {
      if(strpos($spamd_result, ' ' . $result_type . ' ') !== false)
      {
       $bayes = $result_type;
      }
     }
     /** Bereits zweifelsfrei erkannte Samples ignorieren */
     if($bayes == 'BAYES_99' || $bayes == 'BAYES_999')
     {
      echo 'SPAM-IGNORE: ' .  str_replace('BAYES_999', 'BAYES_99', $bayes) . ' - ' . $tmp_file . "\n";
     }
     else
     {
      /** Sicherstellen dass zumindest in den aktuellen Samples keine echten Doubletten sind */
      $current_hash = hash_file('MD4', $tmp_file);
      if(!isset($hash_list[$current_hash]))
      {
       /** Terminal-Ausgabe */
       echo 'SPAM-TRAIN:  ' .  $bayes . ' - ' . $tmp_file . "\n";
       $current_basename = basename($tmp_file);
       /** Sample in Trainings-Ordner verschieben */
       rename($tmp_file, $corpus_folder . '/spam/' . $current_basename);
       $new_spam_samples[] = $corpus_folder . '/spam/' . $current_basename;
       $training = true;
       $hash_list[$current_hash] = 1;
      }
      /** Doublette loeschen */
      else
      {
       @unlink($tmp_file);
       echo 'SPAM-IGNORE: DUPLICATE - ' . $tmp_file . "\n";
      }
     }
    }
    /** Sample vom IMAP-Server loeschen */
    if($imap_delete)
    {
     @imap_delete($imap_connection, $msg->msgno);
    }
    @unlink($tmp_file);
   }
  }
  /** Sample vom IMAP-Server loeschen */
  if($imap_delete)
  {
   @imap_expunge($imap_connection);
  }
 }
 imap_close($imap_connection);
}

/**
 * HAM-TRAINING
 *
 * IMAP-Verbindung aufbauen
 * Nachrichten abfragen
 * RAW-Messages in Temp-Files schreiben
 * Wenn neue Samples im Trainings-Folder unser zentrales Learning-Skript aufrufen
 * Bei Ham-Samples trainieren wir ohne auf den aktuellen Bayes-Wert zu schauen, rein informell
*/
$new_ham_samples = [];
$imap_connection = imap_open('{' . $imap_host . ':143/imap/novalidate-cert}' . $imap_ham, $imap_user, $imap_pwd, OP_SILENT, 5);
if($imap_connection)
{
 $imap_info = @imap_check($imap_connection);
 if($imap_info->Nmsgs > 0)
 {
  foreach(@imap_fetch_overview($imap_connection,"1:{$imap_info->Nmsgs}",0) as $msg)
  {
   if(!$msg->deleted)
   {
    $raw_headers = @imap_fetchheader($imap_connection, $msg->msgno);
    $raw_body    = @imap_body($imap_connection, $msg->msgno);
    if(!empty($raw_headers) && !empty($raw_body))
    {
     $raw_message = cleanup_sample(cleanup_mozilla_removed_attachments($raw_headers . $raw_body));
     unset($raw_headers, $raw_body);
     $tmp_file = '/tmp/' . strftime('%Y-%m-%d-%H-%M-%S') . '-' . hash('MD4', $raw_message) . '.eml';
     file_put_contents($tmp_file, $raw_message);
     $spamd_result = spamc_report(/**$socket_path*/$spamd_socket, /**$eml_path*/$tmp_file, /**$user*/'sa-milt');
     $bayes = '';
     foreach($result_types as $result_type)
     {
      if(strpos($spamd_result, ' ' . $result_type . ' ') !== false)
      {
       $bayes = $result_type;
       break;
      }
     }
     $current_hash = hash_file('MD4', $tmp_file);
     if(!isset($hash_list[$current_hash]))
     {
      /** Terminal-Ausgabe */
      echo 'HAM-TRAIN:   ' .  $bayes . ' - ' . $tmp_file . "\n";
      $current_basename = basename($tmp_file);
      /** Sample in Trainings-Ordner verschieben */
      rename($tmp_file, $corpus_folder . '/ham/' . $current_basename);
      $new_ham_samples[] = $corpus_folder . '/ham/' . $current_basename;
      $training = true;
      $hash_list[$current_hash] = 1;
     }
     else
     {
      @unlink($tmp_file);
      echo 'HAM-IGNORE:  DUPLICATE - ' . $tmp_file . "\n";
     }
    }
    if($imap_delete)
    {
     @imap_delete($imap_connection, $msg->msgno);
    }
    @unlink($tmp_file);
   }
  }
  if($imap_delete)
  {
   @imap_expunge($imap_connection);
  }
 }
 imap_close($imap_connection);
}

/**
 * Unser globales Trainings-Skript am Ende aufrufen
 * Dieses ist auch fuer Permissions, Clenaups, Header-Stripping etc. verantwortlich
 * Wir spielen hier einfach nur Samples aus der Mailbox ein
 *
 * In den Arrays $new_spam_samples und $new_ham_samples liegen an dieser
 * Stelle Listen mit Dateinamen die wir am Ende gerne als non-root
 * auch durch 'bogofilter' jagen wurden nach dem das SA-Training
 * abgeschlossen und im Vorfeld Permissions und Cleanups sichergestellt
 * wurden
*/
if($training)
{
 echo "\n";
 passthru($train_script);
 if(!empty($stats_script))
 {
  echo "\n";
  echo "------------------------------------------------------------------------------\n\n";
  echo "BAYES-TEST AUF TAGESAKTUELLE SAMPLES:\n\n";
  passthru($stats_script);
  /** Bogofilter am Ende ebenfalls trainieren - Drop privileges */
  $sa_milt_user  = 189;
  $sa_milt_group = 188;
  if(!posix_initgroups('sa-milt', $sa_milt_group) || !posix_setgid($sa_milt_group) || !posix_setuid($sa_milt_user))
  {
   exit('BOGOFILTER - DROP PRIVILEGES FAILED' . "\n");
  }
  if(!empty($new_spam_samples))
  {
   passthru('/usr/bin/bogofilter -s -B ' . implode(' ', $new_spam_samples));
  }
  if(!empty($new_ham_samples))
  {
   passthru('/usr/bin/bogofilter -n -B ' . implode(' ', $new_ham_samples));
  }
 }
}

/**
 * Wenn per Thunderbird Attachments geloescht werden diese idiotischen
 * Pseudo-Header entfernen so dass am Ende die originale Mime-Struktur
 * einem leeren Content bestehen bleibt und die Attachments in
 * einem Mail-Client mit den korrekten Namen aber 0 Bytes
 * dargestellt werden
 *
 * @param  string $content
 * @return string
 * @access public
*/
function cleanup_mozilla_removed_attachments($content)
{
 /** Suche nach Vorkommen */
 $start_string = 'Content-Type: text/x-moz-deleted';
 $end_string   = 'Content-Type: ';
 $junk_hits = [];
 $offset = 0;
 $total_count = substr_count($content, $start_string);
 for($count=1; $count<=$total_count; $count++)
 {
  $start = strpos($content, $start_string, $offset);
  if($start !== false && $start > 1)
  {
   $end = stripos($content, $end_string, ($start+strlen($start_string)));
   if($end > $start)
   {
    $junk = trim(substr($content, $start, ($end-$start)));
    if(!empty($junk))
    {
     $junk_hits[] = $junk;
    }
   }
   $offset = $end + strlen($end_string);
  }
 }
 /** Entfernen */
 if(!empty($junk_hits))
 {
  foreach($junk_hits as $junk)
  {
   $content = str_replace($junk, '', $content);
  }
  $content = str_replace("\n\nContent-Type:", "\nContent-Type:", $content);
  $content = str_replace("\r\n\r\nContent-Type:", "\r\nContent-Type:", $content);
 }
 /** Ggf. bereinigte Version zurueck geben */
 return $content;
}

/**
 * Mail-Sample so aufraumen dass die Header-Anordnung weitgehend dem Original-Empfang
 * entspricht nachdem das meiste das wir importieren aus Milter-Rejects stammt
 * und in weiten Bereichen auch Subject-Tags enthaelt
 *
 * Die internen Received-Header veraendern oft das Bayes-Scoring und wuerde zwar beim Training
 * vorher Cleanup/Procmail-Scripts durchlaufen, wir wollen hier allerding bereits eine saubere
 * Entscheidung BAYES_99-Samples zu ignorieren und gar nicht erst zu trainieren
 *
 * @param  string $content
 * @return string
 * @access public
*/
function cleanup_sample($content)
{
 $content = str_replace('Subject: [SPAM] ', 'Subject: ', $content);
 $content = str_replace('Subject: [SPAM]', 'Subject: ', $content);
 $content = str_replace('esx1.thelounge.net', 'example.com', $content);
 $local_header_start = strpos($content, 'Received: by mail-gw.thelounge.net (THELOUNGE GATEWAY, from userid 189)');
 if($local_header_start > 0)
 {
  $local_header_stop = strpos($content, 'Received:', ($local_header_start+1));
  if($local_header_stop > $local_header_start)
  {
   $content = substr($content, $local_header_stop);
  }
 }
 $local_header_start = strpos($content, 'Received: from mail-gw.thelounge.net (mail-gw.thelounge.net [10.0.0.19])');
 if($local_header_start !== false)
 {
  $local_header_stop = strpos($content, 'X-AT-Envelope-From', ($local_header_start+1));
  if($local_header_stop > $local_header_start)
  {
   $content = substr($content, $local_header_stop);
  }
 }
 $local_header_start = strpos($content, 'Received: from caladan.thelounge.net (caladan.thelounge.net [10.0.0.17])');
 if($local_header_start !== false)
 {
  $local_header_stop = strpos($content, 'X-AT-Envelope-From', ($local_header_start+1));
  if($local_header_stop > $local_header_start)
  {
   $content = substr($content, $local_header_stop);
  }
 }
 return trim($content) . "\n";
}

/**
 * Scan RAW-Message with spamd
 *
 * @param  string $socket_path spamd-socket
 * @param  string $eml_path    sample-file
 * @param  string $user        client-uid
 * @return string
 * @access public
*/
function spamc_report($socket_path, $eml_path, $user='sa-milt')
{
 $socket = false;
 while(!$socket)
 {
  $socket = @fsockopen('unix://' . $socket_path);
 }
 $raw_eml = trim(file_get_contents($eml_path)) . "\r\n";
 fwrite($socket, 'REPORT SPAMC/1.5' . "\r\n");
 fwrite($socket, 'Content-length: ' . strlen($raw_eml) . "\r\n");
 fwrite($socket, 'User: ' . $user . "\r\n");
 fwrite($socket, "\r\n");
 fwrite($socket, $raw_eml);
 fwrite($socket, "\r\n");
 $response = '';
 while(true)
 {
  $buffer = fgets($socket, 128);
  $response .= $buffer;
  if(feof($socket))
  {
   break;
  }
 }
 fclose($socket);
 return substr($response, strpos($response, 'Content analysis details'));
}