QmailtoasterMain Page | About | Help | FAQ | Special pages | Log in

Printable version | Disclaimers | Privacy policy

How to integrate qms-analog for nicely log stats

From Qmailtoaster

Tried a while ago qmailrocks and liked qms-analog option and sometimes would have saved me from long log debugging on qmailtoaster. now I have some time and decided to refresh qms-analog script to new qmail-scanner.

BEWARE this patch might contain some errors please use it only in testing environment !

Install Dependencies

  cpan Time::HiRes DB_File Sys::Syslog MIME::Base64
  yum install sharutils unzip perl-suidperl

Download qmailanalog

  wget http://cr.yp.to/software/qmailanalog-0.70.tar.gz
  

Download errno.patch

  wget http://www.qmailrocks.org/downloads/patches/0.70-errno.patch
  tar xzf qmailanalog-0.70.tar.gz
  cd qmailanalog-0.70
  patch < ../0.70-errno.patch
  make && make setup check
  cd ..

Download qlogtools

  wget http://untroubled.org/qlogtools/qlogtools-3.1.tar.gz

Download errno patch

  wget http://www.qmailrocks.org/downloads/patches/qlogtools_errno.patch
  tar xzf qlogtools-3.1.tar.gz
  cd qlogtools-3.1
  patch < ../qlogtools_errno.patch
  make
  mkdir /usr/local/man
  ./installer
  cd ..

Download and install qms-analog from http://www.qms-analog.teel.ws/

  tar xzf qms-analog-0.4.4.tar.gz
  cd qms-analog-0.4.4
  make all
  make install

Edit qmailstats, replace qmail-send, qmail-smtpd qmail-pop3d with correct daemons and qmailscand with qscan, then copy it to qmail bin folder

  cp qmailstats /var/qmail/bin/qmailstats
  chmod 750 /var/qmail/bin/qmailstats

Add an entry to crontab

  crontab -e
  0 3 * * * /var/qmail/bin/qmailstats 1>/dev/null 2>/dev/null

Download qmail-scanner-2.01 with st patch

  wget http://toribio.apollinare.org/qmail-scanner/download/q-s-2.01st-20070204.tgz

Copy this patch to some file, for ex qmail-scanner-2.01-st-qms.patch (sorry no link available) without START STOP

START

diff -Naur qmail-scanner-2.01st/configure qmail-scanner-2.01st-qms/configure --- qmail-scanner-2.01st/configure 2007-03-01 13:23:26.000000000 +0200 +++ qmail-scanner-2.01st-qms/configure 2007-09-09 09:49:00.000000000 +0300 @@ -10,8 +10,8 @@

umask 007

-OLD_LANG="$LANG" -LANG=C +OLD_LANG="$LANG" +LANG=C

export LANG OLD_LANG

JH_VERSION=`grep 'my $VERSION' qmail-scanner-queue.template|cut -d= -f2|sed -e 's/\"//g' -e 's/\;//g'`

@@ -22,7 +22,7 @@

export QS_VERSION

echo

-echo " Building Qmail-Scanner $QS_VERSION..." +echo " Building Qmail-Scanner-QMS $QS_VERSION..."

if [ "`id |grep root`" = "" ]; then
    cat<<EOF

@@ -95,9 +95,14 @@

ARCHIVEDIR="archives"
REDUNDANT="yes"
FIX_MIME="2"

-DISABLE_EOL_CHECK="0" +DISABLE_EOL_CHECK="1"

DEBUG_LEVEL="0"

+QMS_LOG="1" +QMS_MONITOR="no" +QMS_MON_ACCOUNTS="" +QMS_MON_DESTINATIONS=""

FORCE_UNZIP="0"

+QUARANTINE_PASSWORD_PROTECTED="0"

DESCRIPTIVE_HEADERS="0"
ADMIN_DESCRIPTION="System Anti-Virus Administrator"
NOTIFY_ADDRESSES="psender,nmlvadm"

@@ -119,6 +124,7 @@

# st patch options
QS_GROUP=""
MINI_DEBUG="1"

+ADMIN_FROMNAME="System Anti-Virus Administrator"

DESCR_HEADERS_TEXT="X-Qmail-Scanner"
SETTINGS_P_D="0"
VIRUS_DELETE="0"

@@ -181,10 +187,16 @@

	--max-scan-size) if [ "$2" != "" ]; then shift ; fi ; MAX_SCAN_SIZE="$1" ;;
	--lang) if [ "$2" != "" ]; then shift ; fi ; QSLANG="$1" ;;
	--debug)  if [ "$2" != "" ] ; then  shift ; fi ; DEBUG_LEVEL="$1" ;;

+ --qms-log) if [ "$2" != "" ] ; then shift ; fi ; QMS_LOG="$1" ;; + --qms-monitor) if [ "$2" != "" ] ; then shift ; fi ; QMS_MONITOR="$1" ;; + --qms-monitor-accts) if [ "$2" != "" ] ; then shift ; fi ; QMS_MON_ACCOUNTS="$1" ;; + --qms-monitor-dests) if [ "$2" != "" ] ; then shift ; fi ; QMS_MON_DESTINATIONS="$1" ;;

	--unzip)
	    boolean_opt $2 $1 ; FORCE_UNZIP="$RET_VAL" ; if [ "`echo $2 | egrep -i '^\-'`" = "" ] ; then shift ; fi ;;
	--max-zip-size) if [ "$2" != "" -a "`echo $2|grep '\-'`" = "" -a "`echo $2|egrep '^[0-9]+$'`" != "" ] ; then shift ; fi ; MAX_ZIP_SIZE="$1" ;;
	--max-unpacked-files) if [ "$2" != "" -a "`echo $2|grep '\-'`" = "" -a "`echo $2|egrep '^[0-9]+$'`" != "" ] ; then shift ; fi ; MAX_UNPACKED_FILES="$1" ;;

+ --block-password-protected) + boolean_opt $2 $1 ; QUARANTINE_PASSWORD_PROTECTED="$RET_VAL" ; if [ "`echo $2 | egrep -i '^\-'`" = "" ] ; then shift ; fi ;;

	--add-dscr-hdrs) if [ "$2" != "" ] ; then  shift ; fi ; DESCRIPTIVE_HEADERS="$1" ;;
	--scanners) if [ "$2" != "" -a "`echo $2|grep '\-'`" = "" ] ; then shift ; fi ; FIND_SCANNERS="$1" ;;
	--skip-text-msgs)

@@ -358,6 +370,12 @@

  --max-unpacked-files <number-files>   (defaults to 10000 files)

+ --block-password-protected [yes|no] (defaults to "no") + Setting this to "yes" allows you to quarantine any + incoming zip files that are password protected. + This is primarily to stop viruses such as Bagle which + arrive within a password-protected zip file. +

  --max-scan-size <number-bytes>        (defaults to 100 Mbytes)
                   Email messages (raw size) larger than this 
                   number (in bytes) will skip all AV and Spam 

@@ -380,7 +398,7 @@

                   Defaults to "2" enables a bunch of extra MIME checks that
                   have proven to be very useful.

- --ignore-eol-check [yes|no] (defaults to "no") + --ignore-eol-check [yes|no] (defaults to "yes")

                   Making this "yes" stops Qmail-Scanner
                   from treating "\r" or "\0" chars in the headers of 
                   MIME mail messages as being suspicious enough to quarantine

@@ -411,6 +429,33 @@

                   Logs only important information, mail headers, blocks,
                   errors and elapsed time. If set to 2, it will log the
                   parent pid (ppid) and the message size.

+ --qms-log [yes|no] (default: yes) + Whether or not event logging is turned on. On (yes) + by default. Useful for qmail-scanner statistics. + + --qms-monitor [yes|no] (default: no) + Whether or not qms-monitor Account Monitoring is turned on. + + --qms-monitor-accts ["acct1@domain2.com,acct2@domain3.com"] + List of email accounts to be monitored. + + --qms-monitor-dests ["monitor.domain.com/acct1.domain2/Maildir/new, + monitor.domain.com/acct2.domain3/Maildir/new"] + List of destination paths for monitored email messages. + Note 1: locations here will be saved underneath + .../qmailscan/qms-monitor; a cron job can later + copy from that location to an alternate email + domain used for account monitoring. + Note 2: each entry in this array corresponds to the email + address in the same location of the + qms-monitor-accts list above - i.e., + qms-monitor-accts[2] msgs get stored at + qms-monitor-dests[2] - thus, ORDER DOES MATTER. + Note 3: DO NOT include a leading "/" on these paths - + they will typically be entries that ultimately + belong in /home/vpopmail/domains - i.e., starting + with the domain name. +

  --batch [yes|no]                 (default: no = ask for confirm)
                   Do not confirm configure information (mainly for scripting)

@@ -1679,6 +1724,39 @@

     LOCAL_DOMAINS_ARRAY="`echo $LDA|sed 's/^,//g'`"
fi

+## qms-monitor +if [ "`echo $QMS_MONITOR|egrep -i '^no|^0'`" != "" ]; then + QMS_MONITOR="0" + echo "qms-monitor = no" +else + QMS_MONITOR="1" + echo "qms-monitor = yes" + + # clean up the lists a bit + QMS_MON_ACCOUNTS="`echo $QMS_MON_ACCOUNTS|sed -e 's/\"//g' -e 's/ //g'`" + if [ "$QMS_MON_ACCOUNTS" ]; then + LDA="" + for dom in `echo $QMS_MON_ACCOUNTS|sed 's/,/ /g'` + do + dom="`echo $dom|sed -e 's/@/qms_at_sign/g'`" + LDA="$LDA,'$dom'" + done + QMS_MON_ACCOUNTS="`echo $LDA|sed 's/^,//g'`" + fi + + QMS_MON_DESTINATIONS="`echo $QMS_MON_DESTINATIONS|sed -e 's/\"//g' -e 's/ //g'`" + if [ "$QMS_MON_DESTINATIONS" ]; then + LDA="" + for dom in `echo $QMS_MON_DESTINATIONS|sed 's/,/ /g'` + do + LDA="$LDA,'$dom'" + done + QMS_MON_DESTINATIONS="`echo $LDA|sed 's/^,//g'`" + fi + +fi + +

if [ "$MIME_UNPACKER" = "reformime" ]; then
if [ "$UNMIME_BINARY" = "" ]
then

@@ -1930,6 +2008,14 @@

    fi
fi

+if [ "`echo $QMS_LOG|egrep -i '^no|^0'`" != "" ]; then + QMS_LOG="0" + echo "qms-log=no" +else + QMS_LOG="1" + echo "qms-log=yes" +fi +

if [ "$LOG_DETAILS" != "" ]; then
    echo "log-details=$LOG_DETAILS"
fi

@@ -1952,6 +2038,10 @@

    echo "redundant-scanning=$REDUNDANT" 
fi

+if [ "$QUARANTINE_PASSWORD_PROTECTED" != "" ]; then + echo "block-password-protected=$QUARANTINE_PASSWORD_PROTECTED" +fi +

if [ "$ARCHIVEIT" != "0" ]; then
    if [ "$ARCHIVEIT" = "1" ]; then 
     ASTRING="everything"

@@ -2201,6 +2291,12 @@

s?HOST_RELEASE?$HOST_RELEASE?g;
s?HOST_HARDWARE?$HOST_HARDWARE?g;
s?DEBUG_LEVEL?$DEBUG_LEVEL?g;

+s?QMS_LOG?$QMS_LOG?g; +s?QMS_MONITOR?$QMS_MONITOR?g; +s?QMS_MON_ACCOUNTS?$QMS_MON_ACCOUNTS?g; +s?QMS_MON_DESTINATIONS?$QMS_MON_DESTINATIONS?g; +s?QUARANTINE_PASSWORD_PROTECTED?$QUARANTINE_PASSWORD_PROTECTED?g; +s?ADMIN_FROMNAME?$ADMIN_FROMNAME?g;

s?DESCRIPTIVE_HEADERS?$DESCRIPTIVE_HEADERS?g;
s?CMDLINE?$CMDLINE?g;
s?PERL5?$PERL5?g;

@@ -2309,8 +2405,10 @@

s?SA_DEBUG?$SA_DEBUG?g;
s?SA_HDR_REPORT?$SA_HDR_REPORT?g;
s?SPAMD_SOCKET?$SPAMD_SOCKET?g;" qmail-scanner-queue.template > qmail-scanner-queue.pl-1

-perl -pe 's/%%/\$/g' qmail-scanner-queue.pl-1 > qmail-scanner-queue.pl +perl -pe 's/%%/\$/g' qmail-scanner-queue.pl-1 > qmail-scanner-queue.pl-2

rm -f qmail-scanner-queue.pl-1

+perl -pe 's/qms_at_sign/@/g' qmail-scanner-queue.pl-2 > qmail-scanner-queue.pl +rm -f qmail-scanner-queue.pl-2

cat sub-attachments.pl >> qmail-scanner-queue.pl

diff -Naur qmail-scanner-2.01st/qmail-scanner-queue.pl qmail-scanner-2.01st-qms/qmail-scanner-queue.pl --- qmail-scanner-2.01st/qmail-scanner-queue.pl 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qmail-scanner-queue.pl 2007-09-09 09:31:33.000000000 +0300 @@ -0,0 +1,3982 @@ +#!/usr/bin/perl -T +# +# File: qmail-scanner-queue.pl +# Version: 2.01 - st - patch - 20070204 +# +# Author: Jason L. Haar <jhaar - users.sourceforge.net> +# +# Patch by: Salvatore Toribio <toribio - pusc.it> +# +# Patched for Event Logging by: Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.22 - patched: st-qms - 20040530 +# +# Patched for Account Monitoring by Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.22 - patched: st-qms-monitor - 20040919 +# +# Patched for Version 1.24 and merge of qms-monitor functions +# by Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.24 - patched: st-qms - 20041102 +# +# Patched for Version 1.25 +# by Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.25 - patched: st-qms - 20050618 +# +# See the file README-st-patch for information about the patch +# This version deletes/rejects spam based in Chris Hine's patch for v1.16 +# +# Each user could has his own scanners and sa_settings. +# +# This file was auto-generated by: +# +# ./configure --qs-user qscand --admin root --domain mail.netech.ro --admin-description "System Anti-Virus Administrator" --notify psender,nmlvadm --local-domains mail.netech.ro --silent-viruses auto --virus-to-delete 0 --skip-text-msgs 1 --lang en_GB --debug 0 --minidebug 1 --add-dscr-hdrs 0 --dscr-hdrs-text "X-Qmail-Scanner" --normalize yes --archive 0 --settings-per-domain 0 --max-scan-size 100000000 --unzip 0 --max-zip-size 1000000000 --max-unpacked-files 10000 --redundant 0 --log-details syslog --log-crypto 0 --fix-mime 2 --ignore-eol-check 1 --sa-delta 0 --sa-alt 0 --sa-debug 0 --sa-report 0 --sa-quarantine 0 --sa-delete 0 --sa-reject 0 --scanners "auto" --install 1 +# +# Description: This is a replacement/add-on for Qmail 1.0.3's qmail-queue. +# It can call several blocking programs - such as virus scanners - on every +# SMTP-received Email message, checking for viruses and blocked filenames, +# only allowing the message to continue if it passes the tests. +# +# Copyright (C) 1999,2000,2001-2006 the people mentioned above +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 1, or (at your option) +# any later version. See <URL:http://www.gnu.org/copyleft/gpl.html> +# for a copy. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The software is provided as is. Please bear in mind that we have +# done this in my spare time. While it is as accurate as we could +# make it there is a reasonable chance that there are mistakes +# somewhere in here. If you email me and tell me about them, I will +# be happy to fix them but I can't take responsibility for your system. +# Basically use this at your own risk. +# +##################################################################### + +##################################################################### +## +## Required Packages +## +## Qmail-1.03 +## Perl 5.005_03+ +## Maildrop-0.73 +## Bruce Guenter's QMAILQUEUE patch <URL:http://www.qmail.org/qmailqueue-patch> +## Perl module Time::HiRes and DB_File +## +## +## So-far tested Virus scanners: +## Trend's Virus scanner for Linux +## MacAfee's (NAI's) virus scanner for Linux +## Sophos's virus scanner for Linux +## H+BEDV's antivir scanner for Linux +## F-Secure's fsav scanner for Linux +## P-Prot for Linux +## Sophie (daemonized Sophos scanner) +## Trophie (daemonized Trend scanner) +## ...and more - see README for full list +## +##################################################################### + +##################################################################### +## +## Site-specific config +## +##################################################################### + + +delete @ENV{qw(IFS CDPATH ENV BASH_ENV QMAILMFTFILE QMAILINJECT)}; + +use strict 'vars', 'subs'; + +#Set locale to "C" (English). That way any string checks on forked apps +#will tend to be in English - simplifying/standardizing regex matches +my $orig_locale=$ENV{'LC_ALL'}; +$ENV{'LC_ALL'}= $ENV{'LANG'} = $ENV{'LANGUAGE'} = 'C'; +POSIX::setlocale(&POSIX::LC_ALL,'C'); + +use Sys::Syslog qw(:DEFAULT setlogsock); +setlogsock('unix'); + +my $VERSION="2.01"; +my $st_version="20070204"; +$VERSION.='st'; + +#Mail header to add to each scanned message to report stuff in... +#Default is to not generate them ($descriptive_hdrs = 0) - as that +#info is also in the Received: headers... +my $descriptive_hdrs=0; +my $V_HEADER="X-Qmail-Scanner"; +my($qsmsgid); +$qsmsgid=tolower("$V_HEADER-message-id"); + +my($qscan_account)='qscand'; + +#From: line information used when making reports +my $V_FROM='root@mail.netech.ro'; +my $V_FROMNAME='System Anti-Virus Administrator'; + +# Address carbon-copied on any virus reports +my $QUARANTINE_CC='root@mail.netech.ro'; + +#Array of local domains that are checked against for +#deciding whether or not to send recipient alerts to +my @local_domains_array=('mail.netech.ro'); + +# qms: save local domains list string +my $local_domains_string="'mail.netech.ro'"; + + +######## qms-monitor: selective account monitoring/archiving +### +### Description: +### 1) qms-monitor will archive ALL email msgs SENT OR RECEIVED for +### any email address listed below +### 2) Messages are archived to $qms_monitor_home - they can be left +### there for manual examination, or a cron script can be run periodically +### to move them into a "monitor" email domain so that the mail can be +### partitioned into individual monitor domain accounts and read with +### any email client +######## + +my $qms_monitor_enabled='0'; + +### qms_monitor_array: add email addresses of local domains to be monitored +my @qms_monitor_array=(); + +### qms_monitor_dest_array: add destination for email message copies +# Note 1: locations here will be saved underneath $qms_monitor_home; +# a cron job can later copy from that location to an alternate +# email domain used for account monitoring. +# Note 2: each entry in this array corresponds to the email address in the +# same location of the @qms_monitor_array above - i.e., +# @qms_monitor_array[2] msgs get stored at +# @qms_monitor_dest_array[2] - thus, ORDER DOES MATTER. +# Note 3: DO NOT include a leading "/" on these paths - they will typically +# be entries that ultimately belong in /home/vpopmail/domains - +# i.e., starting with the domain name. +### +my @qms_monitor_dest_array=(); + +######## qms-monitor BLOCK END + +# Array of virus that we don't want to inform the sender of. +my @silent_viruses_array=('klez','bugbear','hybris','yaha','braid','nimda','tanatos','sobig','winevar','palyh','fizzer','gibe','cailont','lovelorn','swen','dumaru','sober','hawawi','hawaii','holar-i','mimail','poffer','bagle','worm.galil','mydoom','worm.sco','tanx','novarg','@mm','cissy','cissi','qizy','bugler','dloade','netsky','spam'); + +# st: Virus that will be deleted without notifying anyone, +# you can add other viruses in the form "virus1|virus2|virus3". +# Most of the viruses in the 'silent_viruses_array' could be +# added to this list safely. +# i.e. "mydoom|worm.sco|novarg|tanx|bagle|netsky|somefool|roca|agobot|dumaru|sober|lovgate|klez|rox|zafi|(PIF|SCR|CPL) files|mybot|mabutu" +my $virus_to_delete=""; + +#Array of virtual headers used within perlscanner +my @virtualheaders_array=("MAILFROM","RCPTTO","REMOTEIPADDR","ZIPPASSWORDPROTECTED","ISSENSITIVEANDNOCRYPTO","CRYPTODETAILS","FILELENGTHTOOLONG","FILEDOUBLEBARRELED","FILECLSID"); + +#Addresses that should be alerted of any quarantined Email +my $NOTIFY_ADDRS='psender,nmlvadm'; + +#Try to fix bad MIME messages before passing to MIME unpacker +my $BAD_MIME_CHECKS='2'; + +#Block password protected zip files +#my $BLOCK_PASSWORD_PROTECTED_ARCHIVES='0'; + +#Disable just the EOL char check instead of all of BAD_MIME_CHECKS +my $IGNORE_EOL_CHECK='1'; + +# The full path to qmail programs we'll need. +my $qmailinject = '/var/qmail/bin/qmail-inject'; +my $qmailqueue = '/var/qmail/bin/qmail-queue'; + +# What directory to use for storing temporary files. +my $scandir = '/var/spool/qscan'; + +#Where the Q-S configs are +my $configdir = '/var/spool/qscan'; + +#Where the Q-S logs live +my $logdir = '/var/spool/qscan'; + +#What maildir folder to store working files in +my $wmaildir='working'; + +#What maildir folder to store virus-infected msgs in +my $vmaildir='viruses'; + +#What maildir folder to store policy-blocked msgs in +my $pmaildir='policy'; + +#What maildir folder to store high-scoring SPAM in (instead of passing it on) +#NOTE: this only gets used if 0 set +# st: see below '$smaildir_site' +#my $smaildir='spam'; + +#What maildir folder to archive received Email in instead of deleting +my $archiveit='0'; +my $archivedir='archives'; + +#Name of file in $scandir where debugging output goes +my $debuglog="qmail-queue.log"; + +#Name of file where quarantine reports go (for long-term storage) +my $quarantinelog="quarantine.log"; + +# qms: Name of file where usable logs for analysis are written +my $eventlog="qms-events.log"; + +#Generate nice random filename +my ($sysname, $hostname, $release, $version, $machine) = uname(); +#my $hostname='mail.netech.ro'; #could get via call I suppose... + +#If you trust the virus scanners handling of mbox format itself +#you may want to let it have a go at the "raw" message, and original +#zip files if present +my $redundant_scanning='0'; + +#If you want to log via file/syslog information of all Email +# that passes through your system (from/to/subj/size/attachments) +my $log_details="syslog"; + +#If you'd like Q-S to report which messages are PGP or S/MIME, +#turn this on +my $log_crypto="0"; + +# qms-monitor - the root for temporary storage +my $qms_monitor_home = "$scandir/qms-monitor"; + +#Max size of message allowed to be scanned - 100Mbytes by default +#DO NOT SET LOWER THAN 10Mbytes!!!!! +my $MAX_SCAN_SIZE=100000000; + +#bypass all AV/Spam scanning - but still do perlscan checks +my $SKIP_SCANNING=0; + +# st: If $sa_subject is defined and fast_spamassassin mode is selected, +# a tag will be added to the subject indicating how the message is to +# be considered as spam, in this way: +# LOW: required_hits < score < required_hits + sa_delta +# MEDIUM: required_hits + sa_delta < score < required_hits + 2 * sa_delta +# HIGH: required_hits + 2 * sa_delta < score +# Be aware, 2*sa_delta must be lower than sa_quarantine. +# 'required_hits' is the value set in the SpamAssassin configuration file. +my $sa_delta_site='0'; + +# st: Spam messages with a score higher than +# (required_hits + sa_quarantine) should be quarantined. +# Only relevant if SpamAssassin is used. +# Score of 0 means deliver all messages. Defaults to 0. +my $sa_quarantine_site='0'; + +# st: Some people wants to quarantine spam in a different +# maildir folder than viruses, maybe to run sa-learn. +# The default is: +# my $smaildir_site='spam'; +# You can set it per user/domain in the file 'settings_per_domain.txt' +# WARNING: if $smaildir it is not in the same 'file system' (partition) +# than $wmaildir, you have to change the routine 'sub email_quarantine_report' +# you will find the code commented in that routine. +# (in the official version 2.00 this setting has been added) +my $smaildir_site='spam'; + +# st: address to send a copy of the mails 'quarantined' +# as spam for admin puropose (I thought), almost unmodifyed. +# Enable $sa_fwd_verbose if you want the X-Spam headers in +# the forwarded message. +my $sa_forward_site=; +my $sa_fwd_verbose_site='0'; + +# st: Spam messages with a score higher than +# (required_hits + sa_delete) should be deleted (or rejected). +# Only relevant if SpamAssassin is used. Score of 0 +# means deliver all messages. Defaults to 0. +# If sa-quarantine is set, sa-delete must be greater. +my $sa_delete_site='0'; + +# st: If you enable sa-reject and sa-delete is properly set, +# messages with a score higher than (required_hits + sa_delete) +# will be rejected before the smtp session is closed. +# Otherwise they are just dropped silently. (1/0) +my $sa_reject_site='0'; + +# st: Use the alternative subroutine for spamassassin, it runs +# ALWAYS in *fast_spamassassin* mode and doesn't pass the '-u' option +# to spamc. So if you want to run in *verbose_spamassasin* mode or you +# want to use the sql per user preferences for spamassassin, you have +# to disable this option and run the standard spamassassin routine. +# It also allows to log the spamassassin report. (1/0) +my $sa_alt='0'; + +# st: If sa_alt is enabled an you enable this option, you will +# have a beautiful log with the tests and the scores of +# spamassassin in the file qmail-queue.log, and you +# can add the X-Spam-Report header enabling the +# option below. (1/0) +my $sa_debug='0'; + +# st: If sa_alt and sa_debug are enabled, *qmail-scanner* will +# add the X-Spam-Report header to the messages if you +# enable this option. (1/0) +my $sa_hdr_report_site='0'; + +# st: Enable this option to do not pass to spamassassin messages +# from MAILER-DAEMON, see READMEpatched for details. (1/0) +my $SA_SKIP_MD='0'; + +############################################## +# st: SETTINGS PER DOMAIN +############################################## + +# st: Enable or diasable scanner per domain (1/0) +my $settings_pd='0'; + +# Array of virus scanners used must point to subroutines +my @scanner_array=(); + +# st: @scanners_installed is the array with all scanners installed +# in the computer, if you disable $settings_pd qmail-scanner will fall to +# this array. Don't modify it unless you really know what you do. +my @scanners_installed=("clamdscan_scanner","spamassassin","perlscan_scanner"); + +# st: @scanners_default if $settings_pd is enabled qmail-scanner will +# use this array for the users/domains that don't have a custom +# scanner_array set in the $settings_per_domain.txt file. +# You can set it to "none" to skip all the scanners, even perlscan. +# If you want to skip the scanners only for a particular user/domain +# set his scanners list to "none" in the $settings_per_domain.txt file. +my @scanners_default=("clamdscan_scanner","spamassassin","perlscan_scanner"); + +# st: DB file (without extension) where per domain/user scanners +# are saved, edit $settings_per_domain.txt and run +# "qmail-scanner-queue.pl -p" to generate $settings_per_domain.db +my $settings_per_domain="$scandir/settings_per_domain"; + +# st: if spamassassin has sql user settings, then run spamassassin +# per each recipient. Again verbose_spamassassin is a pain, so sa_alt will +# be run after the first recipient. (1/0) +my $sa_sql='0'; + +# The following variable MUST NOT be modified, qmail-scanner will set +# them by its own for each recipient. +my $domain_returnpath=; +my $domain_one_recip=; +my $sa_rcpt='0'; +my (%found_event); +# +my $sa_subject=; +my $sa_quarantine=; +my $sa_delta=; +my $sa_delete=; +my $sa_reject=; +my $sa_forward=; +my $sa_fwd_verbose=; +my $sa_hdr_report=; +my $smaildir=; + + +############################################## + +#Full path to file in which virus-scanner versioning info is kept +my $versionfile="$logdir/qmail-scanner-queue-version.txt"; + +#DB file (without extension) where bad filenames are kept. +# You edit $db_filename.txt, and "qmail-scanner-queue.pl -g" generates $db_filename.db +my $db_filename="$configdir/quarantine-events"; + +# st: configurable in st-patch +# This rule exists but is never +# expected to trigger normally (defaults 10,000, is stupidly high). +my $MAX_NUM_UNPACKED_FILES='10000'; + +#What locale is used on this system +#$sys_locale="LOCALE"; + +#Full paths to binaries used within this script follow - small performance +#improvement :-) + + +my $mimeunpacker_binary='/usr/bin/reformime '; +my $unzip_binary='/usr/bin/unzip'; +my $unzip_options='-Pxx3160615524xx'; +my $max_zip_size='1000000000'; +my $tnef_binary=; +my $rm_binary='/bin/rm'; +my $grep_binary='/bin/grep'; +my $find_binary='/usr/bin/find'; +my $uudecode_binary='/usr/bin/uudecode'; +my $uudecode_pipe='-o -'; + + +my $uvscan_binary=; +my $csav_binary=; +my $nod32_binary=; +my $nod32upd_binary=; +my $sweep_binary=; +my $sophie_binary=; +my $trophie_binary=; +my $iscan_binary=; +my $hbedv_binary=; +my $hbedv_options=; +my $avp_binary=; +my $avpdaemon_binary=; +my $fprot_binary=; +my $fsecure_binary=; +my $inocucmd_binary=; +my $ravlin_binary=; +my $vexira_binary=; +my $bitdefender_binary=; +my $clamscan_binary='/usr/bin/clamscan'; +my $clamscan_options="-r -m --unzip --unrar --unzoo --lha --disable-summary --max-recursion=10 --max-space=100000"; +my $clamdscan_binary='/usr/bin/clamdscan'; +my $clamdscan_options="--no-summary"; + +# st: I have returned to my own way to set the (1.25st) +my $spamc_binary='/usr/bin/spamc -t 30'; + +# st: whether or not to run spamassassin in fast or verbose mod +# remember that the routine sa_alt always set sa_fast to 1, by her own. +# Please run in fast mode, you can break the verbose mode with your personal +# local.cf, so better run in fast mode (If you like SA REPORT read the docs). +#my $spamc_options='SPAMC_OPTIONS'; +my $sa_fast='1'; + +my $sa_subject_site=""; # st: if fast_spamassassin mode is selected +my $spamassassin_binary='/usr/bin/spamassassin '; + +# st: If somebody is using spamassassin with unix socket... +my $spamd_socket=; +$spamc_binary.=" -U $spamd_socket" if ($spamd_socket ne ""); + +my ($sa_comment,$sa_level); +my $sa_symbol='+'; +my ($tag_score,$tag_sa_score); +my $SNEAKY_WINDOWS_EXTENSIONS="exe|w[pm][szd]|vcf|nws|cmd|bat|pif|sc[rt]|dll|ocx|do[ct]|xl[swt]|p[po]t|pps|vb[se]?|hta|p[lm]|sh[bs]|hlp|chm|eml|ws[cfh]|ad[ep]|jse?|md[abew]|ms[ip]|reg|as[dfx]|cil|cpl"; +my $VALID_WINDOWS_EXTENSIONS="rtf|pdf|sav|htm|html|pst|ost|txt|gif|jpeg|mpeg|jpg|png|mny|wav|tif|$SNEAKY_WINDOWS_EXTENSIONS"; +my $passwd_protected_zip; + +#Little workaround. Apparently people send docs of the form "my.domainname.com.doc" - so you can get false positives +#due to ".com". So don't add ".com" to SNEAKY_WINDOWS_EXTENSIONS until after VALID_WINDOWS_EXTENSIONS is defined +#So now "file.com.tif" won't trigger, but file.tif.com will. +$SNEAKY_WINDOWS_EXTENSIONS="$SNEAKY_WINDOWS_EXTENSIONS|com"; + + +$ENV{'PATH'}='/bin:/usr/bin'; + +my $SCANINFO=; + +my $MAX_FILE_LENGTH=100; +my $MAX_NUM_HDRS=140; +my $QE_LEN=20; + +#Maximum amount of time we allow Q-S to run before returning +# a temp failure. This is so remote SMTP servers don't get confused +# over whether or not they have delivered to a SMTP server +# that's refused to say "OK" for over an hour... +# We'll default to 20 minutes. If the scanner loop takes more than 20 +# minutes to scan the message, then something *must* be wrong with the +# scanner. +my $MAXTIME=20*60; + +#Finally, are you sure your virus scanners can unpack zip files? +#Turn this on to force Qmail-Scanner to unzip for you +my $force_unzip=0; + +#Descriptive string to use in generated Email +my $destring="virus"; + +##################################################################### +## +## End of site-specific settings +## +##################################################################### + + + +#Want debugging? Enable this and read $logdir/qmail-queue.log +my $DEBUG='0'; + +# st: Minimal debug only works if $DEBUG=0 +# If set to 2, the parent pid is written to the logs, and also +# the message size +my $MINIDEBUG='1'; + +# qms: Want meaningful event logs? Enable this and read $scandir/qms-events.log +my $EVENTLOG='1'; + +my @uufile_list = (); +my @attachment_list = (); +my @zipfile_list = (); + +#Want microsec times for debugging +use Time::HiRes qw ( usleep ualarm gettimeofday tv_interval ); +use POSIX; +use DB_File; + +use vars qw/ $opt_v $opt_V $opt_h $opt_g $opt_r $opt_z $opt_p $opt_d $opt_s/; + +use Getopt::Std; + +#my ($opt_v,$opt_h,$opt_g,$opt_r,$opt_z); + +getopts('vVhgrzpds'); + +my ($start_time,$last_time); +$start_time = $last_time = [gettimeofday]; + +(my $prog=$0) =~ s/^.*\///g; + +if ( $opt_h ) { + print " + + $prog $VERSION-$st_version + + -h - This help + -v - show details about this install. + Please include in any bug reports. + -V - show details about this install + and some configuration information. + -z - gather virus scanner/DAT versions + and cleanup old temp files + -g - generate perlscanner database + -r - read from perlscanner database + + -p - generate settings per domain database + -d - display settings per domain database + -s - sort the text file $settings_per_domain.txt\n\n"; + exit; +} + +# st: I need the localtime at this point for the routine read_spd +#Get current timestamp for logs +my ($sec,$min,$hour,$mday,$mon,$year,$nowtime); +($sec,$min,$hour,$mday,$mon,$year) = localtime(time); + +if ( $opt_g || $opt_r) { + &generate_quarantine_db; + exit 0; +} elsif ($opt_p) { + &generate_spd; + exit 0; +} elsif ($opt_d || $opt_s) { + &read_spd; + exit 0; +} elsif ($opt_v || $opt_V) { + &show_version; + exit 0; +} + + +chdir($scandir); +umask(0007); + +if (! -d "$scandir/tmp") { + mkdir("$scandir/tmp",0750) || &error_condition("cannot create $scandir/tmp - $!"); +} + +my ($quarantine_event,$quarantine_event_tmp)=0; +my ($quarantine_DOS,$quarantine_spam)=0; + +my $file_id = &uniq_id(); + +#For security reasons, tighten the follow vars... +$ENV{'SHELL'} = '/bin/sh' if exists $ENV{SHELL}; +$ENV{'TMP'} = $ENV{'TMPDIR'} = "$scandir/tmp/$file_id"; +#$ENV{'QMAILSUSER'} = $ENV{'QMAILSHOST'} = ; + + + +if ($mimeunpacker_binary =~ /reformime/) { + $mimeunpacker_binary .= " -x$ENV{'TMPDIR'}/"; +} elsif ($mimeunpacker_binary =~ /ripmime/) { + $mimeunpacker_binary .= " --unique_names --no-ole --paranoid -i - -d $ENV{'TMPDIR'}/"; +} + +#Get current timestamp for logs +my ($sec,$min,$hour,$mday,$mon,$year,$nowtime); +($sec,$min,$hour,$mday,$mon,$year) = localtime(time); +my ($smtp_sender,$remote_smtp_ip,$remote_smtp_auth,$real_uid,$effective_uid); + +$real_uid=$<; +$effective_uid=$>; + +# st: I will need the process number, and other variables, later +my $nprocess=$$; +my $nppid=getppid; +if ($nppid == 1) { + # The parent pid is dead, maybe a message with BLFs + warn "$V_HEADER-$VERSION: Process $nprocess closed, parent process died\n" if ($MINIDEBUG < 3); + warn "$nprocess QS-$VERSION: Process $nprocess closed, parent process died\n" if ($MINIDEBUG >= 3); + exit 111; +} +$nprocess.="/$nppid" if ($MINIDEBUG >= 2); +my $sa_report=; +my ($sa_hits,$required_hits)=('0','0'); +# st: Flag to delete message +my $del_message='0'; + +if ($DEBUG || $MINIDEBUG ) { + open(LOG,">>$logdir/$debuglog"); + select(LOG);$|=1; + &debug("+++ starting debugging for process $$ (ppid=$nppid) by uid=$real_uid"); + &minidebug("+++ starting debugging for process $$ (ppid=$nppid) by uid=$real_uid"); +} + +# qms: open the event log if enabled +if ($EVENTLOG ) { + open(ELOG,">>$scandir/$eventlog"); + select(ELOG);$|=1; + my $starttime = strftime("%F %H:%M:%S", localtime(time)); + &eventlog("------ START MSG $starttime ------"); +} + +# st: if sa_alt or sa_debug are '0', sa_hdr_report_site must be 0 +$sa_hdr_report_site='0' if ( !$sa_alt || !$sa_debug ); + +# st: if the variable SA_ONLYDELETE_HOST is set in the tcpserver +# don't reject messages coming from those IPs, just delete them +# You should set this variable for your secondary mail server. +if (defined($ENV{'SA_ONLYDELETE_HOST'}) || defined($ENV{'SA_WHITELIST'})) { + $sa_reject="0"; + &debug("WL: The server is a SA_ONLYDELETE_HOST, don't reject"); + &minidebug("WL: The server is a SA_ONLYDELETE_HOST, don't reject"); +} + + +# st: if the variable BMC_WHITELIST is set in the tcpserver +# don't search for 'bad mime characters' in the headers of messages +# coming from those IPs. +# It would be hard to mantain this whitelist... +if (defined($ENV{'BMC_WHITELIST'})) { + $BAD_MIME_CHECKS='0'; + &debug("WL: The server is in the BMC_WHITELIST, don't check BMC"); + &minidebug("WL: The server is in the BMC_WHITELIST, don't check BMC"); +} + + +&debug("setting UID to EUID so subprocesses can access files generated by this script"); +$< = $>; # set real to effective uid +$( = $); # set real to effective gid + +&debug("program name is $prog, version $VERSION"); +if ($opt_z) { + &scan_queue; + exit 0; +} + + +&scanner_info; + +my (%headers , %virtualheader); +my ($CRYPTO_TYPE,$DOMKEYS,$altered_subject, $HEADERS, $env_returnpath, $returnpath, $QS_RELAYCLIENT); +my ($ATTACHMENT, %BOUNDARY,$BOUNDARY_REGEX,$attachment_header,$attachment_value,%attach_hdrs,%content_type); +my ($ct_attachment_filename,$cd_attachment_filename); +my ($env_recips, $recips, $trecips, $recip, $one_recip); +my ($alarm_status,$elapsed_time,$msg_size,$file_desc); +my ($description,$quarantine_description,$illegal_mime); +my $skip_text_msgs=1; +my $plain_text_msg=0; +my $indicates_attachments=0; +my $xstatus=0; +my $attachment_counter=0; + +&working_copy; + + # st: working_copy could be high due to an slow connection + &minidebug("w_c: message size $msg_size bytes") if ($MINIDEBUG >= 2); + my $elapsed_1=tv_interval ($start_time, [gettimeofday]); + &minidebug("w_c: elapsed time from start $elapsed_1 secs"); + +#We will set our own value here as it allows us to unset +#it later without changing how Qmail actually interprets +#RELAYCLIENT +$QS_RELAYCLIENT=1 if (defined($ENV{'RELAYCLIENT'})); + +if ($ENV{'TCPREMOTEIP'}) { + $remote_smtp_ip=$ENV{'TCPREMOTEIP'}; + if ($ENV{'TCPREMOTEINFO'}) { + $remote_smtp_auth=" (".$ENV{'TCPREMOTEINFO'}."\@$remote_smtp_ip)"; + $smtp_sender="via SMTP from $remote_smtp_ip using auth $remote_smtp_auth"; + }else{ + $smtp_sender="via SMTP from $remote_smtp_ip"; + } + $tag_score="RC:1($remote_smtp_ip):" if ($QS_RELAYCLIENT); + &debug("incoming SMTP connection from $smtp_sender"); + &eventlog("CONNECT-SMTP:$ENV{'TCPREMOTEIP'}"); + #system("/usr/bin/printenv > /tmp/qmail-scanner.env"); + # st: do not reject mails from localhost useful for fetchmail + $sa_reject="0" if ($remote_smtp_ip eq "127.0.0.1"); +} else { + $smtp_sender="via local process $$"; + $remote_smtp_ip='127.0.0.1'; + #Set QS_RELAYCLIENT if QS_SPAMASSASSIN isn't set + $QS_RELAYCLIENT=1; + $tag_score="RC:1($remote_smtp_ip):"; #Always would be relayed + &debug("incoming pipe connection from $smtp_sender"); + &eventlog("CONNECT-PIPE:$$"); + # st: do not reject mails from localhost useful for fetchmail + $sa_reject="0"; +} +$tag_score="RC:0($remote_smtp_ip):" if ($tag_score !~ /^RC:1/); + + +#Now alarm this area so that hung networks/virus scanners don't cause +#double-delivery... + +eval { + $SIG{ALRM} = sub { die "Maximum time exceeded. Something cannot handle this message." }; + alarm $MAXTIME; + + &deconstruct_msg; #JLH if (!$quarantine_event); + + + #Now unset env var QMAILQUEUE so any further Email's sent don't + #go through the Qmail-Scanner again + &debug("unsetting QMAILQUEUE env var"); + delete $ENV{'QMAILQUEUE'}; + + #This SMTP session is incomplete until we see dem envelope headers! + &grab_envelope_hdrs; + &debug("from=$headers{'from'},subj=$headers{'subject'}, $qsmsgid=$headers{$qsmsgid} $smtp_sender"); + &minidebug("from='$headers{'from'}', subj='$headers{'subject'}', $smtp_sender"); + &eventlog("HEADER:$headers{'from'}:$headers{'to'}:$headers{'subject'}"); + + ##### st: variables for settings per domain + $returnpath=tolower($returnpath); + $domain_returnpath=$returnpath; + $domain_returnpath=~ s/^(.*)\@(.*)$/$2/; + # + $one_recip=tolower($one_recip); + $domain_one_recip=$one_recip; + $domain_one_recip=~ s/^(.*)\@(.*)$/$2/ if ($one_recip); + ###### + + #Add envelope details to headers array so that they can be matched within + #perlscanner. + #Note how they're uppercase cf the message headers which are all forced + #lowercased. This is to ensure no-one can override them... + + $headers{'MAILFROM'}=$returnpath; + $headers{'RCPTTO'}=$recips; + $headers{'REMOTEIPADDR'}=$remote_smtp_ip; + + if ( ($BAD_MIME_CHECKS > 1 && $headers{'mime-version'} eq "") || ($headers{'mime-version'} ne "" && $headers{'content-type'} =~ /^text\/plain/i)) { + #Hmm, doesn't look nice, but it feels better to make this a separate check for some reason + if ($skip_text_msgs && ($indicates_attachments < 2) && !@uufile_list && !@attachment_list) { + &debug("This is a PLAIN text message (because it's either not mime, or is text/plain), skip virus scanners - but not antispam scanners"); + &minidebug("This is a PLAIN text message, skip virus scanners - but not SA"); + &eventlog("TYPE:PLAIN"); + $plain_text_msg=1; + } + } + if ($headers{'MAILFROM'} eq "" || $headers{'subject'} =~ /Returned mail:|Mail Transaction Failed/) { + &debug("This is a bounce message - better assume there's an attachment in it"); + &eventlog("TYPE:MIXED"); + $plain_text_msg=0; + } + +############################################## +# st: SETTINGS PER DOMAIN +############################################## + + $quarantine_event_tmp=$quarantine_event; + + if ($settings_pd && ( ! -f "$settings_per_domain.db")) { + &debug("s_p_d: $settings_per_domain.db doesn't exist falling to installed scanners"); + &minidebug("s_p_d: $settings_per_domain.db doesn't exist falling to installed scanners"); + $settings_pd='0'; + } + + if ($settings_pd) { + &settings_p_d; + } else { + @scanner_array=@scanners_installed; + &sa_defaults; + &start_scanners($env_returnpath,$env_recips,"$scandir/$wmaildir/new/$file_id"); + } + +############################################## + + alarm 0; +}; + +$alarm_status=$@; +if ($alarm_status and $alarm_status ne "" ) { + if ($alarm_status eq "Maximum time exceeded. Something cannot handle this message.") { + &error_condition("ALARM: taking longer than $MAXTIME secs. Requeuing..."); + } else { + &error_condition("Requeuing: $alarm_status"); + } +} + + +#Msg has been delivered now, so don't want hangs in this part +#to affect delivery + +&log_event; + +&cleanup; + +# st: I don't think that st-patch will reach this point, for a SPAM mail.. +# +# This is commented out as I'm concerned for people running Q-S behind edge gateways. +#Those boxes would then generate a bounce (as they are not the actual spamming SMTP client) +#if ($destring =~ /SPAM/) { +# &debug("exit with permanent error as this is high-scored SPAM"); +# &minidebug("SA: exit with permanent error as this is high-scored SPAM"); +# &close_log; +# exit 111; +#} + +# st: just for the script log-report, add the information of policy block to the log +if ($quarantine_event =~ /^(policy|perlscan)/i && $quarantine_event !~ /gr[ae]ylist/i && $quarantine_description) { + &debug("q_s: Policy BLOCK"); + &minidebug("q_s: Policy BLOCK"); +} + +# st: write to the log the end of the process +&close_log; +&eventlog("SCANTIME:",tv_interval ($start_time, [gettimeofday]),""); +&eventlog("------ STOP MSG ---------------------------"); +exit 0; + +############################################################################ +# Error handling +############################################################################ + +#Generate uniq identifiers +sub uniq_id { + return "$hostname" . time . __LINE__ . $$; +} + + +sub log_event { + if ($log_details) { + $tag_score .= "$tag_sa_score" if ($tag_sa_score); + $tag_score .= "$CRYPTO_TYPE:" if ($log_crypto && $CRYPTO_TYPE ne ""); + $tag_score .= "$DOMKEYS:" if ($log_crypto && $DOMKEYS ne ""); + #$virtualheader{'CRYPTODETAILS'}="$CRYPTO_TYPE:$DOMKEYS"; + $tag_score=":$tag_score" if ($tag_score ne ""); + if ($trecips =~ /\0T/) { + for $recip (split(/\0T/,$trecips)) { + &log_msg("qmail-scanner",($quarantine_event ne "0" ? "$quarantine_event$tag_score" : "Clear$tag_score"),$elapsed_time,$msg_size,$returnpath,$recip,$headers{'subject'},$headers{$qsmsgid},$file_desc) if ($recip ne ""); + } + } else { + #Only one recip + &log_msg("qmail-scanner",($quarantine_event ne "0" ? "$quarantine_event$tag_score" : "Clear$tag_score"),$elapsed_time,$msg_size,$returnpath,$recips,$headers{'subject'},$headers{$qsmsgid},$file_desc); + } + } +} + +# Fail with the given message and a temporary failure code. +sub error_condition { + my ($string,$errcode)=@_; + $errcode=111 if (!$errcode); + eval { + syslog('mail|err',"$V_HEADER-$VERSION:[$file_id] $string"); + }; + if ($@) { + setlogsock('inet'); + syslog('mail|err',"$V_HEADER-$VERSION:[$file_id] $string"); + } + if ($log_details ne "syslog") { + warn "$V_HEADER-$VERSION:[$file_id] $string\n"; + } + #$nowtime = sprintf "%02d/%02d/%02d %02d:%02d:%02d", $mday, $mon+1, $year+1900, $hour, $min, $sec; + &debug("error_condition: $V_HEADER-$VERSION: $string"); + &minidebug("error_condition: $V_HEADER-$VERSION: $string"); + &eventlog("ERROR:$V_HEADER-$VERSION:$string"); + close(ELOG); + &cleanup; + &close_log; + exit $errcode; +} + +sub debug { + my $dnowtime = strftime("%a, %d %b %Y %H:%M:%S %Z", localtime(time)); + print LOG "$dnowtime:$nprocess: ",@_,"\n" if ($DEBUG); +} + +# qms: log events to the file +sub eventlog { + my $enowtime = sprintf "%10d", time; + print ELOG "$enowtime:$$:",@_,"\n" if ($EVENTLOG); +} + +######## qms-monitor BLOCK BEGIN +# qms-monitor: Entry point called prior to requeueing the msg +sub qms_monitor +{ + my($msg) = @_; + my($acct) = ; + my($aindex) = '0'; + + foreach $acct (@qms_monitor_array) + { + # check the sender address first + if ($returnpath =~ /$acct/i) + { + &qms_monitor_save($acct,$msg,"@qms_monitor_dest_array[$aindex]"); + $aindex += 1; + next; + } + + if ($recips =~ /$acct/i) + { + &qms_monitor_save($acct,$msg,"@qms_monitor_dest_array[$aindex]"); + } + + $aindex += 1; + } +} + +# qms-monitor: save the msg to our archive location +sub qms_monitor_save +{ + my($qmsacct,$src,$dest) = @_; + my($finaldest) = "$qms_monitor_home/$dest"; + my($fname) = &qms_monitor_get_filename($qmsacct); + + if (!open(INMSG, "<$src")) + { + &eventlog("--- qms_monitor_save: unable to open src $src"); + &debug ("qms_monitor_save: unable to open src $src\n"); + return; + } + + if (! -d "$finaldest") + { + if (system("mkdir -p $finaldest")) + { + &eventlog("--- qms_monitor_save: unable to mkdir $finaldest"); + &debug ("qms_monitor_save: unable to mkdir $finaldest"); + return; + } + } + + + if (!open(OUTMSG, ">$finaldest/$fname")) + { + &eventlog("--- qms_monitor_save: unable to open dest $finaldest/$fname"); + &debug ("qms_monitor_save: unable to open dest $finaldest/$fname\n"); + return; + } + + while (<INMSG>) + { + print OUTMSG; + } + + close(OUTMSG); + close(INMSG); +} + +# qms-monitor: Generate meaninful file names +sub qms_monitor_get_filename +{ + my($aname) = @_; + my($stime) = strftime("%F_%H:%M:%S", localtime(time)); + + return "$aname" . "_" . "$hostname" . "_" . "$stime" . "_" . $$; +} + +######## qms-monitor BLOCK END + +sub working_copy { + my ($hdr,$last_hdr,$value,$num_of_headers,$last_header,$last_value,$attachment_filename); + select(STDIN); $|=1; + + &debug("w_c: mkdir $ENV{'TMPDIR'}"); + mkdir("$ENV{'TMPDIR'}",0750)||&error_condition("$ENV{'TMPDIR'} exists - try again later..."); + chdir("$ENV{'TMPDIR'}")||&error_condition("cannot chdir to $ENV{'TMPDIR'}/"); + if (-f "$scandir/$wmaildir/tmp/$file_id" || -f "$scandir/$wmaildir/new/$file_id") { + &error_condition("$file_id exists, try again later"); + } + &debug("w_c: start dumping incoming msg into $scandir/$wmaildir/tmp/$file_id [",&deltatime,"]"); + open(TMPFILE,">$scandir/$wmaildir/tmp/$file_id")||&error_condition("cannot write to $scandir/$wmaildir/tmp/$file_id - $!"); + + my $still_headers=1; + my $begin_content=; + my $still_attachment=; + my $first_received=0; + while (<STDIN>) { + if ($still_headers) { + $HEADERS .= $_; + #Catch any naughty illegal header chars here + if ($BAD_MIME_CHECKS && !$IGNORE_EOL_CHECK && /\r|\0/) { + $illegal_mime=1; + &debug("w_c: found CRL/NULL in header - invalid if this is a MIME message"); + &minidebug("w_c: found CRL/NULL in header - invalid if this is a MIME message"); + &eventlog("QMSWC:BAD_HDR_CHARS"); + } + #Put headers into array + if (/^\s+(.*)$/ && $last_hdr) { + #Hmmm, a continuation... + $headers{$last_hdr} .= $1 if (!$illegal_mime); + } elsif (/^([^\s]+)/) { + #This means it's not a continuation header + if (!$quarantine_event && $BAD_MIME_CHECKS && ($headers{'mime-version'} ne "") && !/^([^\s]+):(.*)$/) { + #Wow - a header (not header+value) that goes onto another line - not likely! + $illegal_mime=1; + $destring='problem'; + $quarantine_description="Disallowed breakage found in header name - not valid email"; + $quarantine_event="Policy:Bad_MIME_Break"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &debug("w_c: disallowed breakage found in header name ($_) - not valid email"); + &minidebug("w_c: disallowed breakage found in header name ($_) - not valid email"); + &eventlog("QMSWC:BAD_HDR_BREAKAGE"); + #next; + } else { + /^([^\s]+):(.*)$/; + $hdr=$1; + $last_hdr=tolower($hdr); + $value=$2; + $value =~ s/^\s//; + if (!$quarantine_event && $BAD_MIME_CHECKS && $headers{'mime-version'} ne "" && $hdr =~ /^[^X].*\(/i) { + #Wow - a comment *inside* a standard header name. Only viruses are known to do that + #Should we test for [^0-9a-z\_\-\=\+] instead? + $illegal_mime=1; + $destring='problem'; + $quarantine_description='Disallowed MIME comment found in header name - not valid email'; + $quarantine_event="Policy:Bad_MIME_Comment"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &debug("w_c: $quarantine_description"); + &minidebug("w_c: $quarantine_description"); + &eventlog("QMSWC:BAD_HDR_MIME"); + } + $num_of_headers++; + } + #Don't let this array grow without bounds... + if ($num_of_headers < $MAX_NUM_HDRS) { + if ($hdr =~ /^to|cc/i && $headers{tolower($hdr)}) { + #Special-case the To: and Cc: headers. + #Broken mailers generate messages with multiple + #instances of these, so merge them into one... + $headers{tolower($hdr)} .= ",$value"; + } elsif ($hdr =~ /^(from|x-mail|User-Agent|Organi|Received|Message-ID|Subject)/i && $headers{tolower($hdr)}) { + #Make sure any multiples of these headers are remembered, so that + #perlscanner checks can see all instances - just wrap em up + #into one long line + $headers{tolower($hdr)} .= " $value"; + } elsif ($hdr =~ /^received$/i && !$first_received) { + $first_received=1; + $value=~/\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]\)$/; + if ($1 =~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && !$ENV{'TCPREMOTEIP'}) { + $ENV{'TCPREMOTEIP'}=$1; + &debug("TCPREMOTEIP not set - configuring as $ENV{'TCPREMOTEIP'}"); + }else{ + #&debug("no need to reset TCPREMOTEIP from $value"); + } + } elsif (!$quarantine_event && $BAD_MIME_CHECKS > 1 && (($headers{'mime-version'} ne "" && tolower($hdr) eq "mime-version") || ($headers{'content-type'} ne "" && tolower($hdr) eq "content-type") || ($headers{'content-transfer-encoding'} ne "" && tolower($hdr) eq "content-transfer-encoding") || ($headers{'content-disposition'} ne "" && tolower($hdr) eq "content-disposition"))) { + #Why would a legit message have important MIME headers defined >1 time? It could imply someone is trying to sneak + #something past SMTP scanners... + #Too much parsing needs to be done to do this correctly - stuff 'em - break the sucker ;-/ + &debug("Duplicate MIME headers found [$hdr] - renaming"); + print TMPFILE "$V_HEADER-$VERSION: renamed duplicate MIME headers\n"; + $_="$V_HEADER-Renamed-$_"; + } else { + #All other headers: the last occurance wins! + $headers{tolower($hdr)}=$value; + } + } + } + if (/^(\r|\r\n|\n)$/) { + #headers have finished + $still_headers=0; + #Normalize selected headers + $headers{'subject'}=&normalize_string("Subject:",$headers{'subject'}); + #Try to workaround those nasty broken viruses that produce Content-Type without MIME-Version + #to get around virus scanners + if ($headers{'mime-version'} eq "") { + #Make sure it's a MIME-style Content-type, Sun used to use Content-type for other purposes... + if ($BAD_MIME_CHECKS && $headers{'content-type'} =~ /\//) { + print TMPFILE "$V_HEADER-$VERSION: added fake MIME-Version header\nMIME-Version: 1.0\n"; + $headers{'mime-version'}="1.0"; + &debug("w_c: added fake MIME-Version header"); + } + } elsif ($BAD_MIME_CHECKS > 1 && $headers{'content-type'} eq "") { + #OK, now do the same for Content-Type. RFCs state "if no Content-Type present, then it's text/plain" + #However, Outlook chooses to read the entire message and "figures out" it's mixed/multipart, etc. + #This'll break that - as it should. + #I wonder if I shouldn't just block these instead, the only ones I've seen are either viruses or spam... + print TMPFILE "$V_HEADER-$VERSION: added fake Content-Type header\nContent-Type: text/plain\n"; + $headers{'content-type'}="text/plain"; + &debug("w_c: added fake Content-Type header"); + } + if ( $headers{'mime-version'} ne "" && $headers{'content-type'} =~ /^(\s+|)([^\/\s\(]+)(\s+|)\/(\s+|)([^\/\s\(\;]+)/ ) { + $content_type{$attachment_counter}="$2/$5"; + &debug("w_c: primary Content-Type of $content_type{$attachment_counter} found"); + if ($log_crypto) { + $DOMKEYS="CR:DomKeys(signed)" if ($headers{'domainkey-signature'} ne ""); + if ($content_type{$attachment_counter} =~ /multipart\/signed/i) { + $CRYPTO_TYPE="CR:SMIME(signed)" if ($CRYPTO_TYPE eq "" && $headers{'content-type'} =~ /protocol=\"application\/(x\-|)pkcs/i); + $CRYPTO_TYPE="CR:PGP(signed)" if ($CRYPTO_TYPE eq "" && $headers{'content-type'} =~ /protocol=\"application\/(x\-|)pgp/i); + &debug("found MIME-based crypto ($CRYPTO_TYPE)"); + } elsif ($content_type{$attachment_counter} =~ /multipart\/encrypted/i) { + $CRYPTO_TYPE="CR:PGP(encrypted)" if ($headers{'content-type'} =~ /protocol=\"application\/(x\-|)pgp/i); + &debug("found MIME-based crypto ($CRYPTO_TYPE)"); + }elsif ($content_type{$attachment_counter} =~ /application\/(x\-|)pkcs7/i) { + $CRYPTO_TYPE="CR:SMIME(encrypted)" if ($headers{'content-type'} =~ /application\/(x\-|)pkcs7/i); + &debug("found MIME-based crypto ($CRYPTO_TYPE)"); + } + } + } elsif ($headers{'mime-version'} ne "") { + $destring="problem"; + $illegal_mime=1; + $quarantine_description="Disallowed MIME Content-Type found - not valid email"; + &debug($quarantine_description); + &minidebug("w_c: $quarantine_description"); + $quarantine_event="Policy:Bad_MIME_Type"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &eventlog("QMSWC:BAD_MIME_CONTENT"); + } + #} + #if ( $headers{'content-type'} =~ /boundary(\s*)=(|\s+|\s*\")([^\"\;]+)($|\;|\")/i) { + if ( $headers{'mime-version'} ne "" && $headers{'content-type'} =~ /boundary(\s*)=(|\s+|\s*\")([^\s\"\;]+)($|\;|\")/i) { + $BOUNDARY{$attachment_counter}=$3; + #if (!$quarantine_event && $BAD_MIME_CHECKS > 1 && ($BOUNDARY{$attachment_counter} =~ /\"|\;/ || $BOUNDARY{$attachment_counter} eq "")) { + #&debug("w_c: RFC2046 says boundaries ($BOUNDARY{$attachment_counter}) can't contain such chars [see bcharsnospace]"); + #$destring="problem"; + #$illegal_mime=1; + #$quarantine_description="Disallowed MIME boundary found - potential virus"; + #$quarantine_event="Policy:Bad_MIME_Boundary"; + #$description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + #&eventlog("QMSWC:BAD_MIME_BOUNDARY"); + #} + if (!$quarantine_event && $headers{'mime-version'} ne "" && $BAD_MIME_CHECKS > 1 && ( length($BOUNDARY{$attachment_counter}) == 0 || length($BOUNDARY{$attachment_counter}) > 250)) { + #RFC2046 says boundarys are 1-70 chars - making it 250 is being *real* liberal... + $destring="problem"; + $illegal_mime=1; + $quarantine_description="Disallowed MIME boundary length found (".length($BOUNDARY{$attachment_counter}).") - not valid email"; + &debug($quarantine_description); + $quarantine_event="Policy:Bad_MIME_Length"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &eventlog("QMSWC:BAD_MIME_BOUNDARY"); + } + #Strip off stuff after semicolon, and escape any odd chars + $BOUNDARY{$attachment_counter} =~ s/(\"|\;).*$//g; + #$BOUNDARY{$attachment_counter} =~ s/([^a-z0-9=\_])/\\\1/gi; + $BOUNDARY{$attachment_counter} =~ s/(\W)/\\$1/g; + $BOUNDARY_REGEX=$BOUNDARY{$attachment_counter}; + &debug("w_c: found a top-level boundary definition of $BOUNDARY{$attachment_counter}"); + } + if ( $headers{'content-type'} =~ /name(|\s+)=(|\s+|\s*\")([^\s\"].*)/i) { + $ATTACHMENT=$3; + $attachment_counter++; + #Strip off stuff after semicolon + $ATTACHMENT =~ s/(\"|\;).*$//g; + &debug("w_c: found a top-level file attachment definition of $ATTACHMENT"); + push(@attachment_list, $ATTACHMENT); + } + if ($headers{'message-id'} eq "" && !$headers{$qsmsgid}) { + $headers{$qsmsgid}="<".time . __LINE__ . $$ . "\@$hostname>"; + print TMPFILE "${V_HEADER}-Message-ID: $headers{$qsmsgid}\n"; + } else { + if (!$headers{$qsmsgid}) { + $headers{$qsmsgid}=$headers{'message-id'}; + } + } + } + } + if (/^(\r|\r\n|\n)$/) { + #&debug("w_c: attachment num=$attachment_counter"); + #&debug("w_c: last attachment header: $attachment_header:$attachment_value"); + $attach_hdrs{tolower($attachment_header)}=$attachment_value; + if ($still_attachment ne "") { + $still_attachment=; + $begin_content=$attach_hdrs{'content-transfer-encoding'}; + } else { + $begin_content=; + } + $attachment_header=$attachment_value=; + #Let's see what the last MIME attachment contained + if ($cd_attachment_filename ne "" && $ct_attachment_filename ne "" && $ct_attachment_filename ne $cd_attachment_filename) { + if (!$quarantine_event && $BAD_MIME_CHECKS > 1) { + &debug("w_c: Disallowed MIME filename manipulation - potential virus"); + &minidebug("w_c: Disallowed MIME filename manipulation - potential virus"); + &eventlog("QMSWC:BAD_MIME_FILENAME"); + $illegal_mime=1; + $destring="problem"; + $quarantine_description='Disallowed MIME filename manipulation - not valid email'; + &debug($quarantine_description); + &minidebug("w_c: $quarantine_description"); + $quarantine_event="Policy:Bad_MIME_Manipulation"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message attachment: \"$ct_attachment_filename\" != \"$cd_attachment_filename\""; + } + } + #$ct_attachment_filename=$cd_attachment_filename=; + if ($attach_hdrs{'content-type'} =~ /name(|\s+)=(|\s+|\s*\")([^\s\"].*)/i && $ATTACHMENT eq "") { + $ATTACHMENT=$3; + #Strip off stuff after semicolon + $ATTACHMENT =~ s/(\"|\;).*$//g; + $ATTACHMENT=&normalize_string("Filename:",$ATTACHMENT); + $ATTACHMENT=tolower($ATTACHMENT); + if (!grep(/^\Q$ATTACHMENT\E$/,@attachment_list)) { + &debug("found C-T attachment filename \"$ATTACHMENT\""); + push(@attachment_list, $ATTACHMENT); + } + $ct_attachment_filename=$ATTACHMENT; + $ATTACHMENT=; + #&debug("w_c: found a Content-Type attachment filename of \"$ct_attachment_filename\""); + } + if ($attach_hdrs{'content-disposition'} =~ /name(|\s+)=(|\s+|\s*\")([^\s\"].*)/i && $ATTACHMENT eq "") { + $ATTACHMENT=$3; + #Strip off stuff after semicolon + $ATTACHMENT =~ s/(\"|\;).*$//g; + $ATTACHMENT=&normalize_string("Filename:",$ATTACHMENT); + $ATTACHMENT=tolower($ATTACHMENT); + if (!grep(/^\Q$ATTACHMENT\E$/,@attachment_list)) { + push(@attachment_list, $ATTACHMENT); + &debug("found C-D attachment filename \"$ATTACHMENT\""); + } + $cd_attachment_filename=$ATTACHMENT; + $ATTACHMENT=; + #&debug("w_c: found a Content-Disposition attachment filename of \"$cd_attachment_filename\""); + } + if ($attach_hdrs{'content-type'} =~ /boundary(|\s+)=(|\s+|\s*\")([^\s\"].*)/i) { + $BOUNDARY{$attachment_counter}=$3; + #Strip off delimiters around boundary + $BOUNDARY{$attachment_counter} =~ s/(\"|\;).*$//g; + $BOUNDARY{$attachment_counter} =~ s/(\W)/\\$1/g; + if (!$quarantine_event && $headers{'mime-version'} ne "" && $BAD_MIME_CHECKS > 1 && length($BOUNDARY{$attachment_counter}) > 250) { + #RFC2046 says boundarys are 0-70 chars + $destring="problem"; + $illegal_mime=1; + $quarantine_description="Disallowed MIME boundary length found (".length($BOUNDARY{$attachment_counter}).") - not valid email"; + &debug($quarantine_description); + &minidebug("w_c: $quarantine_description"); + $quarantine_event="Policy:Bad_MIME_Boundary"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &eventlog("QMSWC:BAD_MIME_BOUNDARY"); + } + if ( !$quarantine_event && $headers{'mime-version'} ne "" && $BAD_MIME_CHECKS > 1 && $BOUNDARY{$attachment_counter} =~ /^($BOUNDARY_REGEX)$/i) { + &debug("w_c: hmm, a new boundary defintion that has already being set. Sounds like a trojan"); + &minidebug("w_c: hmm, a new boundary defintion that has already being set. Sounds like a trojan"); + &debug("w_c: broken attachment MIME details - block it!"); + &minidebug("w_c: broken attachment MIME details - block it!"); + $illegal_mime=1; + $destring="problem"; + $quarantine_description='Disallowed MIME boundary found in attachment - not valid email'; + &debug($quarantine_description); + &minidebug("w_c: $quarantine_description"); + $quarantine_event="Policy:Bad_MIME_Boundary"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &eventlog("QMSWC:BAD_MIME_BOUNDARY"); + } + if ($BOUNDARY_REGEX ne "") { + $BOUNDARY_REGEX.="|".$BOUNDARY{$attachment_counter}; + } else { + $BOUNDARY_REGEX=$BOUNDARY{$attachment_counter}; + } + #&debug("w_c: BOUNDARY_REGEX=$BOUNDARY_REGEX"); + } + if ($attach_hdrs{'content-type'} =~ /\//) { + $attachment_filename=; + $attachment_filename=$cd_attachment_filename ne "" ? $cd_attachment_filename : $ct_attachment_filename; + #&debug("w_c: just parsed attachment $attach_hdrs{'content-type'}: filename=$attachment_filename"); + if ( $attach_hdrs{'content-type'} =~ /^(\s+|)([^\/\s\(]+)(\s+|)\/(\s+|)([^\/\s\(\;]+)/ ) { + $content_type{$attachment_counter}="$2/$5"; + &debug("w_c: attachment $attachment_counter: Content-Type of $content_type{$attachment_counter} found"); + if ($attachment_filename =~ /\.(scr|pif|vbs|exe)$/i && $content_type{$attachment_counter} !~ /^(message|text|application|binary)/i) { + $quarantine_description="Disallowed file ($attachment_filename) assosiated with unrelated MIME type ($content_type{$attachment_counter}) - forged attachments blocked"; + &debug("w_c: $quarantine_description"); + &minidebug("w_c: $quarantine_description"); + $illegal_mime=1; + $destring='problem'; + &debug($quarantine_description); + &minidebug("w_c: $quarantine_description"); + &eventlog("QMSWC:BAD_MIME_ASSOCIATION"); + $quarantine_event="Policy:Forged_Attachment"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in attachment $attachment_filename"; + } + } + $attach_hdrs{'content-type'}=$attach_hdrs{'content-disposition'}=; + $ct_attachment_filename=$cd_attachment_filename=; + } + } else { + #&debug("line=$_"); + } + + if ($still_attachment ne "") { + #&debug("w_c: check those attachment headers ($_)"); + if (/^([^\s]+):(|\s+)(.*)$/) { + $last_header=$attachment_header; + $last_value=$attachment_value; + $attachment_header=$1; + $attachment_value=$3; + $attachment_value =~ s/^\s+//; + if ($last_header) { + #&debug("w_c: $last_header:$last_value"); + $attach_hdrs{tolower($last_header)}=$last_value; + } + #&debug("w_c: beginning of $attachment_header, value=$attachment_value"); + } elsif (/^\s(.+)/) { + #&debug("w_c: line :$_: reached"); + $attachment_value.=$1; + } elsif (/^(\r|\r\n|\n|\s+)$/) { + #Yeah - I should block spaces, but too many valid lists send out such junk... + $still_attachment=; + } else { + #This will catch headers that are *correctly* broken over two lines. + #No known mailer does that, but virus writers do, so we block it. + #Note that a lot of mailing-lists (and AV systems...) shove their trailers + #on the bottom of messages irrespective of whether they are MIME or not - so + #we must allow such "hacks" to slip through + if (!$quarantine_event && $BAD_MIME_CHECKS > 1 && ($BOUNDARY_REGEX ne "" && $still_attachment !~ /^\-\-($BOUNDARY_REGEX)\-\-$/) ) { + &debug("w_c: broken attachment MIME details (still_attachment=$still_attachment, but BOUNDARY_REGEX=\"$BOUNDARY_REGEX\")- block it!"); + &minidebug("w_c: broken attachment MIME details (still_attachment=$still_attachment, but BOUNDARY_REGEX=\"$BOUNDARY_REGEX\")- block it!"); + $illegal_mime=1; + $destring="problem"; + $quarantine_description='Disallowed content found in MIME attachment - not valid email'; + &debug($quarantine_description); + &minidebug("w_c: $quarantine_description"); + $quarantine_event="Policy:Bad_MIME_Header"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &eventlog("QMSWC:BAD_MIME_CONTENT"); + } + } + } + if ($begin_content =~ /base64/i && !/^\s/) { + #&debug("w_c: begin=\"$begin_content\",line=$_"); + $begin_content=; + #Only looking for base64 encoded as both QP and binary appear to arrive corrupted under Outlook + if ($_ =~ /^TV(qq|qQ|r1|pQ|pA|py|rm|rh|oF|oI|rQ|o8|ou|oA)/) { + &debug("w_c: base64 looks like a Windows executable, filename=$attachment_filename,type=$content_type{$attachment_counter}"); + if (!$quarantine_event && $BAD_MIME_CHECKS > 1 && $content_type{$attachment_counter} !~ /^(binary|application)/i) { + #As far as I'm aware, a Windows/DOS executable should always be of type "application/<something>" + $illegal_mime=1; + $destring="problem"; + $quarantine_description="Disallowed executable attachment associated with \"$content_type{$attachment_counter}\" MIME type - forged attachment"; + $quarantine_event="Policy:Forged_Attachment"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in attachment \"$attachment_filename\""; + &debug("w_c: $quarantine_description"); + &minidebug("w_c: $quarantine_description"); + &eventlog("QMSWC:BAD_MIME_WINBLOWS"); + } + } + if ($_ =~ /^(UEsDB[AB]|UEswMFBL)/) { + &debug("w_c: base64 looks like a zip file, filename=$attachment_filename,type=$content_type{$attachment_counter}"); + if (!$quarantine_event && $BAD_MIME_CHECKS > 2 && $attachment_filename !~ /\.zip$/i) { + #This is a zip file, and yet the filename doesn't end in .zip - should quarantine it! + $illegal_mime=1; + $destring="problem"; + $quarantine_description="Disallowed zip attachment when not associated with a .zip filename - forged attachment"; + $quarantine_event="Policy:Forged_Attachment"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in attachment \"$attachment_filename\""; + &debug("w_c: $quarantine_description"); + &minidebug("w_c: $quarantine_description"); + &eventlog("QMSWC:BAD_MIME_ZIP"); + } + } + } + if ($BOUNDARY_REGEX ne "" && /^\-\-($BOUNDARY_REGEX)/) { + $still_attachment=$_; + chomp($still_attachment); + if (/^\-\-($BOUNDARY_REGEX)\-\-.$/) { + &debug("w_c: found end of attachment boundary, BOUNDARY_REGEX was \"$BOUNDARY_REGEX\"..."); + my ($delete_bb)=$1; + $delete_bb =~ s/(\W)/\\$1/g; + $BOUNDARY_REGEX =~ s/\Q$delete_bb\E//; + $BOUNDARY_REGEX =~ s/\|\|//; + $BOUNDARY_REGEX =~ s/(^\||\|$)//; + &debug("w_c: now that \"$delete_bb\" has been removed, it's \"$BOUNDARY_REGEX\"..."); + } + $attachment_counter++; + #&debug("w_c: found :$BOUNDARY_REGEX: - must be attachment section $attachment_counter"); + } + if ($CRYPTO_TYPE eq "" && $log_crypto) { + + $CRYPTO_TYPE="CR:PGP(old-signed)" if (/^(\-\-\-\-\-BEGIN PGP SIGNATURE\-\-\-\-\-|LS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS0)/); + $CRYPTO_TYPE="CR:PGP(old-encrypted)" if (/^(\-\-\-\-\-BEGIN PGP MESSAGE\-\-\-\-\-|LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0t)/); + &debug("found old PGP crypto ($CRYPTO_TYPE)") if ($CRYPTO_TYPE ne ""); + &minidebug("w_c: found old PGP crypto ($CRYPTO_TYPE)") if ($CRYPTO_TYPE ne ""); + } + &check_and_grab_attachments; + print TMPFILE ; + } + close(TMPFILE)||&error_condition("cannot close $scandir/$wmaildir/tmp/$file_id - $!"); + + #scanning message has finished + + $HEADERS =~ s/\r|\0//g; + + &debug("w_c: rename new msg from $scandir/$wmaildir/tmp/$file_id to $scandir/$wmaildir/new/$file_id"); + &debug("w_c: total time between DATA command and \".\" was ",&deltatime," secs"); + &debug("w_c: (this is basically the time it took the client to send the message over the network"); + &debug("w_c: resetting timer so as to measure actual Qmail-Scanner processing time"); + &minidebug("w_c: Total time between DATA command and \".\" was ",&deltatime," secs"); + $start_time=[gettimeofday]; + #Not atomic but who cares about the overhead - this is the only app using this area... + link("$scandir/$wmaildir/tmp/$file_id","$scandir/$wmaildir/new/$file_id")||&error_condition("cannot link $scandir/$wmaildir/tmp/$file_id into $scandir/$wmaildir/new/$file_id - $!"); + unlink("$scandir/$wmaildir/tmp/$file_id")||&error_condition("cannot delete $scandir/$wmaildir/tmp/$file_id - $!"); + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$atime,$mtime,$ctime,$blksize,$blocks); + ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$msg_size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$scandir/$wmaildir/new/$file_id"); + if (!$headers{'date'}) { + my (@day, @mon); + $day[0]='Sun';$day[1]='Mon';$day[2]='Tue';$day[3]='Wed';$day[4]='Thu';$day[5]='Fri';$day[6]='Sat'; + $mon[0]='Jan';$mon[1]='Feb';$mon[2]='Mar';$mon[3]='Apr';$mon[4]='May';$mon[5]='Jun';$mon[6]='Jul';$mon[7]='Aug';$mon[8]='Sep';$mon[9]='Oct';$mon[10]='Nov';$mon[11]='Dec'; + my ($tm_sec,$tm_min,$tm_hour,$tm_mday,$tm_mon,$tm_year,$tm_wday,$tm_yday,$tm_isdst); + ($tm_sec,$tm_min,$tm_hour,$tm_mday,$tm_mon,$tm_year,$tm_wday,$tm_yday,$tm_isdst)=localtime; + $tm_year += 1900; + $headers{'date'}=$day[$tm_wday].", $tm_mday ".$mon[$tm_mon]." $tm_year $tm_hour:$tm_min:$tm_sec"; + } +} + +sub grab_envelope_hdrs { + select(STDOUT); $|=1; + + open(SOUT,"<&1")||&error_condition("cannot dup fd 0 - $!"); + while (<SOUT>) { + ($env_returnpath,$env_recips) = split(/\0/,$_,2); + if ( ($returnpath=$env_returnpath) =~ s/^F(.*)$// ) { + $returnpath=$1; + ($recips=$env_recips) =~ s/^T//; + $recips =~ /^(.*)\0+$/; + $recips=$1; + $recips =~ s/\0+$//g; + #Keep a note of the NULL-separated addresses + $trecips=$recips; + $one_recip=$trecips if ($trecips !~ /\0T/); + $recips =~ s/\0T/\,/g; + } + #only meant to be one line! + last; + } + close(SOUT)||&error_condition("cannot close fd 1 - $!"); + if ( ($env_returnpath eq "" && $env_recips eq "") || ($returnpath eq "" && $recips eq "") ) { + #At the very least this is supposed to be $env_returnpath='F' - so + #qmail-smtpd must be officially dropping the incoming message for + #some (valid) reason (including the other end dropping the connection). + &debug("g_e_h: no sender and no recips. Probably due to SMTP client dropping connection. Nothing we can do - cleanup and exit. This is not necessarily an error!"); + &minidebug("g_e_h: no sender and no recips, from $smtp_sender. Dropping, this isn't a QS error."); + &eventlog("SMTP-DROP"); + warn "$$ QS-$VERSION: no sender and no recips, from $smtp_sender\n" if ($MINIDEBUG >= 3); + warn "$V_HEADER-$VERSION: no sender and no recips, from $smtp_sender\n" if ($MINIDEBUG == 2); + &cleanup; + &close_log; + exit; + } + &debug("g_e_h: return-path is \"$returnpath\", recips is \"$recips\""); + &minidebug("g_e_h: return-path='$returnpath', recips='$recips'"); + &eventlog("ENV-HEADER:$local_domains_string:$returnpath:$recips"); +} + + +sub deconstruct_msg { + my ($start_decon_time) = [gettimeofday]; + my $save_filename =; + my ($new_filename,$MAYBETNEF,$tnef_status); + + my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$scandir/$wmaildir/new/$file_id"); + + #override absurd values + $MAX_SCAN_SIZE=10000000 if ($MAX_SCAN_SIZE < 10000000); + if ($size > $MAX_SCAN_SIZE) { + &debug("d_m: msg is $size bytes - too large to scan"); + &minidebug("d_m: msg is $size bytes - too large to scan"); + $SKIP_SCANNING=1; + } + &debug("d_m: starting $mimeunpacker_binary <$scandir/$wmaildir/new/$file_id [",&deltatime,"]"); + open(MIME,"$mimeunpacker_binary <$scandir/$wmaildir/new/$file_id 2>&1|")||&error_condition("cannot call $mimeunpacker_binary - $!"); + while (<MIME>) { + next if (/exists/); + &error_condition("d_m: output spotted from $mimeunpacker_binary ($_) - that shouldn't happen!"); + } + close(MIME)||&error_condition("cannot close $mimeunpacker_binary - $!"); + my $unpacker=; + + opendir(DIR,"$ENV{'TMPDIR'}/")||&error_condition("cannot open dir $ENV{'TMPDIR'}/ - $!"); + my @all_unpacked_files = grep(!/^\.+$/, readdir(DIR)); + closedir(DIR); + &debug("d_m: finished $mimeunpacker_binary [",&deltatime,"]"); + #If you have the tnef app, you'll be able to scan broken M$ attachments + + if ( $tnef_binary ) { + &debug("d_m: Checking all attachments to see if they're MS-TNEF"); + foreach $save_filename (@all_unpacked_files) { + #Clean up $save_filename so as to keep taint happy + $save_filename =~ /^(.*)$/; $save_filename=$1; + ($new_filename=$save_filename) =~ s/([^a-z0-9\.\-\_\+\=\~]+)//gi; + if ($save_filename ne $new_filename) { + $new_filename =~ /(\.[^\.]+)$/; + $new_filename=&uniq_id."$new_filename"; + rename($save_filename,$new_filename); + &debug("d_m: ren $save_filename to $new_filename"); + $save_filename=$new_filename; + } + #Who cares if it is or isn't tnef, just scan it! + if ($tnef_binary) { + $MAYBETNEF=`$tnef_binary --number-backups -d $ENV{'TMPDIR'}/ -f $ENV{'TMPDIR'}/$save_filename 2>&1`; + $tnef_status=$?; + &debug("d_m: is $ENV{'TMPDIR'}/$save_filename is a TNEF file?: $tnef_status [",&deltatime,"]"); + } + } + } + + &debug("d_m: Check for zip files..."); + #Re-initialize directory listing + opendir(DIR,"$ENV{'TMPDIR'}/")||&error_condition("cannot open dir $ENV{'TMPDIR'}/ - $!"); + @all_unpacked_files = grep(!/^\.+$/, readdir(DIR)); + closedir(DIR); + foreach $save_filename (@all_unpacked_files) { + #Clean up $save_filename so as to keep taint happy + $save_filename =~ /^(.*)$/; $save_filename=$1; + ($new_filename=$save_filename) =~ s/([^a-z0-9\.\-\_\+\=\~]+)//gi; + if ($save_filename ne $new_filename) { + $new_filename =~ /(\.[^\.]+)$/; + $new_filename=&uniq_id."$new_filename"; + rename($save_filename,$new_filename); + &debug("d_m: ren $save_filename to $new_filename"); + $save_filename=$new_filename; + } + if ( $save_filename =~ /\.(zip|exe)$/i) { + &unzip_file($save_filename); + } + } + + if (!$redundant_scanning) { + if (-f "$ENV{'TMPDIR'}/$save_filename") { + system $rm_binary,"-f","$ENV{'TMPDIR'}/$save_filename"; + } + } + + my($decon_time)=tv_interval ($start_decon_time, [gettimeofday]); + &debug("d_m: unpacking message took $decon_time seconds"); +} + +sub init_scanners { + my($start_init_scanners_time)=[gettimeofday]; + &debug("ini_sc: start scanning"); + chdir("$ENV{'TMPDIR'}/"); + + #Delete original zip'ped attachment as there's no point + #in the other scanners double-scanning it - unless $redundant scanning + #is set.... + if ($redundant_scanning) { + link("$scandir/$wmaildir/new/$file_id","$ENV{'TMPDIR'}/orig-$file_id"); + } + &debug("ini_sc: recursively scan the directory $ENV{'TMPDIR'}/"); + + #Run AV scanners - even if the message is already going to be quarantined + #due to some Policy: this way you get the definitive answer as to what is + #a virus... The exception to this is if it looks like a DoS attack - then + #don't run the AVs over it - as they may be the ones affected by the DoS... + + # st: JLH has changed this part... let see if I can mantain mine compatible with him. + &scanloop if (!$quarantine_DOS && !$SKIP_SCANNING); + + chdir("$scandir"); + + my($decon_time)=tv_interval ($start_init_scanners_time, [gettimeofday]); + &debug("ini_sc: scanning message took $decon_time seconds"); + &minidebug("ini_sc: finished scan of \"$ENV{'TMPDIR'}\"..."); +} + + +sub perlscan_scanner { + #This is most efficient if called from within deconstruct_msg + + my($start_perlscan_time)=[gettimeofday]; + my (%array,$var,$lfile,$filename,$section,$apptype,$save_filename); + my ($type,$desc,$file,$filepattern,$filepath); + my ($normalized_hdr,$ps_skipfile,$extension); + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks,$fsize); + my ($attachment_list,$perlscan_time); + &debug("p_s: starting scan of directory \"$ENV{'TMPDIR'}\"..."); + + # use DB_File; + + + tie (%array, 'DB_File', "$db_filename.db", O_RDONLY, 0600) || &error_condition("cannot open $db_filename.db - $!"); + + if (!$quarantine_event && $illegal_mime && $headers{'mime-version'} && $BAD_MIME_CHECKS) { + $destring="problem"; + $quarantine_description="Disallowed characters found in MIME headers" if (!$quarantine_description); + $quarantine_event="Policy:Bad_MIME"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description'\n found in message"; + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_MIME_HEADER"); + } + #check out headers against DB... + + foreach $var (sort keys(%array)) { + ($type,$desc)=split(/\t/,$array{$var},2); + &debug("p_s: '$var' = '$type' = '$desc'"); + if ($type !~ /^SIZE=(\-|)[0-9]+$/) { + &debug("p_s: type is a header!"); + $type =~ s/^Policy\-//gi; + $var=~s/^[0-9]+://; + if (!grep(/^$type$/,@virtualheaders_array)) { + #only force lowercase if they are "real" headers + $type=tolower($type); + }else{ + $headers{$type}=$desc if ($headers{$type} eq ""); + } + $virtualheader{$type}=$var; + &debug("p_s: checking for objects containing $type: $var"); + $normalized_hdr=&normalize_string("$type:",$headers{$type}); + + #Check headers against the "virtualheaders" from quarantine-events.txt + if ($headers{$type} ne "" && ($headers{$type} =~ /^$virtualheader{$type}$/ || $normalized_hdr =~ /^$virtualheader{$type}$/i)) { + $quarantine_description="$desc"; + ($quarantine_event=$quarantine_description) =~ s/\s/_/g; + $quarantine_event="Perlscan:".substr($quarantine_event,0,$QE_LEN); + $quarantine_event=~s/_$//g; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_HDR_DB"); + last; + } + } else { + &debug("p_s: type is a size!"); + } + } + + #opendir(DIR,"$ENV{'TMPDIR'}/")||&error_condition("cannot open dir $ENV{'TMPDIR'}/ - $!"); + #@allfiles = grep(!/^\.+$/, readdir(DIR)); + #closedir(DIR); + open(DIR,"$find_binary $ENV{'TMPDIR'}/ -type f |")||&error_condition("cannot open dir $ENV{'TMPDIR'}/ - $!"); + #append any ORIGINAL uuencoded filenames to this directory array + #so that perlscanner can match on uuencoded filenames + my @allfiles=<DIR>; + close(DIR); + + #merge all crypto details + my ($CRYPTO_DETAILS)="$CRYPTO_TYPE:$DOMKEYS"; + + + #Block if "Sensitivity:" header set and yet no sign of encryption used + if ($headers{'sensitivity'} =~ /private|confidential/i) { + if ($virtualheader{'ISSENSITIVEANDNOCRYPTO'} ne "" && $CRYPTO_TYPE !~ /encrypted/) { + $quarantine_description=$headers{'ISSENSITIVEANDNOCRYPTO'}; + ($quarantine_event=$quarantine_description) =~ s/\s/_/g; + $quarantine_event="Policy:Must_Encrypt"; + $quarantine_event=~s/_$//g; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message"; + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_HDR_DB"); + } + $CRYPTO_TYPE=~s/\)$/,private\)/; + } + + if ($virtualheader{'CRYPTODETAILS'} ne "" && !$quarantine_event && $CRYPTO_DETAILS ne "") { + &debug("check crypto characteristics of this message against $virtualheader{'CRYPTODETAILS'}"); + if ($CRYPTO_DETAILS =~ /$virtualheader{'CRYPTODETAILS'}/) { + $destring='problem'; + $quarantine_description=$headers{'CRYPTODETAILS'}; + $quarantine_event="Policy:No_Crypto"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_HDR_DB"); + return; + } + } + if ($#allfiles > $MAX_NUM_UNPACKED_FILES) { + &debug("w_c: more than $MAX_NUM_UNPACKED_FILES files found - quarantine"); + &minidebug("w_c: more than $MAX_NUM_UNPACKED_FILES files found - quarantine"); + $illegal_mime=1; + $destring='problem'; + $quarantine_description="Too many file components found (".$#allfiles.") - potential DoS"; + $quarantine_event="Policy:Many_Files"; + $quarantine_DOS=$quarantine_event; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + $file_desc .= "too_many:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/); + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_ATTACH_LENGTH"); + return; + } + foreach $filepath (@allfiles,@uufile_list,@zipfile_list,@attachment_list) { + chomp($filepath); + ($file=$filepath)=~s/^.*\///g; + #skip files that reformime/ripmime generates. + #This will potentially allow baddies to smuggle files through + #by using filenames like this... Nothing can be done about that:-( + #Reformime generates filenames of the form: + # 967067231.24320-X.host.name (where X is a number) + #Ripmime generates filenames of the form: + # textfileX (where X is a number) + if ($file =~ /^[0-9]+\.[0-9]+\-[0-9]+\.$hostname|^(orig\-|)$file_id|^textfile[0-9]+/) { + &debug("p_s: skipping auto-generated file $file"); + $ps_skipfile=1; + } else { + &debug("p_s: checking $file against perlscanner database..."); + $ps_skipfile=0; + } + + if (!$ps_skipfile && $virtualheader{'FILELENGTHTOOLONG'} ne "" && !$quarantine_event && length($file) > 256 && $BAD_MIME_CHECKS > 1 ) { + &debug("w_c: majorly long attachment filename found - block it"); + &minidebug("w_c: majorly long attachment filename found - block it"); + $quarantine_description=$headers{'FILELENGTHTOOLONG'}; + $illegal_mime=1; + $destring='problem'; + $quarantine_event="Policy:Attach_Length"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + $file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/); + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("QMSWC:BAD_ATTACH_FILENAME"); + return; + } + + #Do the patently obvious filename security checks here + if ( !$ps_skipfile && $BAD_MIME_CHECKS > 1) { + #Not as thorough as I'd like - but I got too many false positives doing it more generically... :-( + #The VALID_WINDOWS_EXTENSIONS is based on double-barrel virii caught in a years worth of Qmail-Scanner + #logs (gotta love those logs!). Notice that I expressly allow "file.exe.exe" through - as the double-extension + #doesn't hide anything [just implies a user made a mistake] + if ($virtualheader{'FILEDOUBLEBARRELED'} ne "" && !$quarantine_event && ($file =~ /(^.*)\.($VALID_WINDOWS_EXTENSIONS)\s*\.($SNEAKY_WINDOWS_EXTENSIONS)$/i) && $file !~ /(\.[a-z0-9]{3})\1$|\.pp.\.pp.$/i) { + $quarantine_description=$headers{'FILEDOUBLEBARRELED'}; + $illegal_mime=1; + $destring='problem'; + $quarantine_event="Policy:Win_Ext"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + $file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/); + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("QMSWC:BAD_ATTACH_FILENAME"); + return; + } + if ($virtualheader{'FILECLSID'} ne "" && !$quarantine_event && $file =~ /\{[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}\}$/i) { + $quarantine_description=$headers{'FILECLSID'}; + $destring='problem'; + $quarantine_event="Policy:Win_CLSID"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + $file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/); + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("QMSWC:BAD_ATTACH_FILENAME"); + return; + } + } + if ($file =~ /(^.*)(\.[^\.]+)\.?$/) { + $extension=tolower($2); + } else { + $extension=""; + } + $lfile = tolower($file); + &debug("p_s: file $file is lowercased to $lfile and has extension $extension") if (!$ps_skipfile); + #Stat'ing attachment names from @attachment_list will fail on filenames that reformime rewrites + #that's OK, as they'll still be picked up via their new filename + ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$filepath"); + #As you stat virtual files as well as real ones, you can't do this check against virtual files... + if ($effective_uid ne "" && $uid ne "" && $uid != $effective_uid) { + $DEBUG=101; + &error_condition("owner of unpacked file \"$filepath\" (uid=$uid) doesn't match UID of Qmail-Scanner (uid=$effective_uid) - can't expect this to work. Fix whatever is creating files with uid=$uid"); + } + if ($ino && $file_desc !~ /\Q$file\E:$size\t/) { + #Sanity check so that the virtual attachments don't get double-counted + $file_desc .= "$file:$size\t"; + } + &debug("p_s: compare $lfile (size $size) against perlscanner database") if (!$ps_skipfile); + if ( $array{$extension} && !$ps_skipfile ) { + $destring="Disallowed attachment type"; + ($fsize,$quarantine_description) = split(/\t/,$array{$extension},2); + $attachment_list.="$file:$size,"; + }else{ + foreach $filepattern (keys %array) { + #&debug("p_s: does \"$filepattern\" match against $lfile?"); + if ( $lfile =~ /^${filepattern}$/i) { + #$destring="Disallowed attachment type"; + ($fsize,$quarantine_description) = split(/\t/,$array{$filepattern},2); + $attachment_list.="$file:$size,"; + } + } + } + $fsize=~s/^SIZE=//; + if (!$ps_skipfile && $quarantine_description && !$quarantine_event && ($size eq $fsize || $fsize =~ /^-1$/i) ) { + ($quarantine_event=$quarantine_description) =~ s/\s/_/g; + if ($quarantine_event=~/gr[ea]ylist/i) { + $quarantine_event="Perlscan:Greylisted"; + }else{ + $quarantine_event="Perlscan:".substr($quarantine_event,0,$QE_LEN); + } + $quarantine_event=~s/_$//g; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file"; + $section=$apptype=$save_filename=$filename=""; + &debug("p_s: something to block! ($quarantine_description)"); + &minidebug("p_s: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_ATTACHMENT_TYPE"); + # return; + } + } + untie %array; + + + if ($CRYPTO_TYPE=~/CR:ZIP/ && $virtualheader{'ZIPPASSWORDPROTECTED'} ne "" && !$quarantine_event) { + $quarantine_description=$headers{'ZIPPASSWORDPROTECTED'}; + &debug("u_f: $quarantine_description"); + &minidebug("u_f: $quarantine_description"); + $destring='problem'; + $quarantine_event="Policy:Encrypted_ZIP"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in zip file"; + $file_desc .= "encrypted_zip:$msg_size\t"; + &debug("u_f: something to block! ($quarantine_description)"); + &minidebug("u_f: something to block! ($quarantine_description)"); + &eventlog("PERLSCAN:BAD_ATTACHMENT_TYPE"); + return; + } + + # st: cosmetic, if the messages is spam don't call it a virus. + if ($quarantine_description =~ /spam/i) { + $destring='problem'; + } + + chdir("$scandir/"); + my($stop_perlscan_time)=[gettimeofday]; + $perlscan_time = tv_interval ($start_perlscan_time, $stop_perlscan_time); + &debug("p_s: finished scan of dir \"$ENV{'TMPDIR'}\" in $perlscan_time secs"); + &minidebug("p_s: finished scan in $perlscan_time secs"); +} + + +sub scanloop { + #my($scanType)=@_; + #&debug("scanloop($scanType): starting scan of directory \"$ENV{'TMPDIR'}\"..."); + &debug("scanloop: starting scan of directory \"$ENV{'TMPDIR'}\"..."); + + my ($scanner); + #Remember any policy blocks that have already occurred, but reset + #$quarantine_event so that if a virus is found, that "wins" + #$quarantine_event_tmp=$quarantine_event; # st: done above. + $quarantine_event='0'; + foreach $scanner (@scanner_array) { + # st: if this recipient has spamassassin in his array we will add the X-Spam headers. + $sa_rcpt='1' if ( $scanner =~ /spam/ ); + + # st: s_p_d, if we have multiples recipients (a lot) run each scanner just once... (except SA) + if (exists $found_event{$scanner}) { + ($destring,$quarantine_event,$quarantine_description,$description)=split(/\t/,$found_event{$scanner}); + $scanner =~ s/^(.*)_scanner$/$1/; + $scanner =~ s/^perlscan$/p_s/; + + # st: spamassassin and multiple recipients... + if ($scanner =~ /spam/i) { + if ($msg_size > 250000) { + &debug("SA: message too big - skip it"); + &minidebug("SA: message too big - skip it"); + next; + } + if ($sa_sql) { + # st: rerun SA, each user could have his own required_hits... + # but we cannot run again verbose_spamassassin, then run sa_alt and add sa_report + # It is better forget verbose_spamassassin for ever... + if (!$sa_fast) { + $sa_alt='1'; + $sa_debug='1'; + $sa_hdr_report='1'; + } + $scanner = "spamassassin_alt" if ($sa_alt); + &{$scanner} (1); + next; + } else { + &check_sa_score ($sa_hits,0,1) if ($sa_hits && ($sa_hits ne "\?")); + if ($sa_hits < $required_hits || ($sa_hits eq "\?")) { + &debug("SA: finished scan for $one_recip - hits=$sa_hits/$required_hits"); + &minidebug("SA: finished scan for $one_recip - hits=$sa_hits/$required_hits"); + } + } + next; + } + + if ($quarantine_description ne "") { + &debug("$scanner: $destring found $quarantine_description"); + &minidebug("$scanner: $destring found $quarantine_description"); + last; + } else { + &debug("$scanner: already checked and clear, skip"); + &minidebug("$scanner: already checked and clear, skip"); + next; + } + } + + #Any scanner errors caused by broken zip files/etc will be ignored + # - not sure how that should be handled... + &debug("scanloop: scanner=$scanner,plain_text_msg=$plain_text_msg"); + + # st: call spamassassin_alt if sa_alt is enabled + $scanner = "spamassassin_alt" if ( $scanner =~ /spam/i && $sa_alt ); + + # st: I am not sure if this is correct + if ($scanner =~ /perl/i) { + $quarantine_event=$quarantine_event_tmp; + } + + #Just run virus scanners over mail that isn't plain text + if ($plain_text_msg) { + #If it's plain text - just run anti-spam checks and perl_scanner + &{$scanner} if ($scanner =~ /spam|perl/i); + } else { + &{$scanner}; + } + + $scanner = "spamassassin" if ($scanner eq "spamassassin_alt"); + if ($quarantine_event) { + #Make sure this is set correctly + $destring="virus" if ($quarantine_event !~ /spam/i && $scanner !~ /perl/i ); + $found_event{$scanner}="$destring\t$quarantine_event\t$quarantine_description\t$description"; + # st: mark the viruses we don't want to quarantine, but delete them + if (($virus_to_delete ne "") && ($quarantine_description=~/($virus_to_delete)/i)) { + $del_message='1'; + &debug("v_t_d: Virus ($quarantine_description), dropping"); + &minidebug("v_t_d: Virus ($quarantine_description), dropping"); + } + #If one scanner finds a virus - why run the rest over it? + last; + } + # st: per user settings... I have to think about... + $found_event{$scanner}="\t\t\t"; + } + &debug("scanloop: finished scan of \"$ENV{'TMPDIR'}\"..."); +} + +sub qmail_requeue { + my($sender,$env_recips,$msg)=@_; + my ($temp,$findate); + + &debug("q_r: fork off child into $qmailqueue..."); + + #($recips=$env_recips) =~ s/^T//; + #$recips =~ s/\0T/\,/g; + #$recips =~ /^(.*)\0+$/; + #$recips = $1; + #$recips =~ s/\0+$//g; + + # Create a pipe through which to send the envelope addresses. + pipe (EOUT, EIN) or &error_condition("Unable to create a pipe. - $!"); + select(EOUT);$|=1; + select(EIN);$|=1; + # Fork qmail-queue. The qmail-queue child will then open fd 0 as + # $message and fd 1 as the reading end of the envelope pipe and exec + # qmail-queue. The parent will read in the addresses and pass them + # through the pipe and then check the exit status. + + $elapsed_time = tv_interval ($start_time, [gettimeofday]); + local $SIG{PIPE} = 'IGNORE'; + my $pid = fork; + + if (not defined $pid) { + &error_condition ("Unable to fork. (#4.3.0) - $!"); + } elsif ($pid == 0) { + # In child. Mutilate our file handles. + close EIN; + + open(STDIN,"<$msg")|| &error_condition ("Unable to reopen fd 0. (#4.3.0) - $!"); + + open (STDOUT, "<&EOUT") || &error_condition ("Unable to reopen fd 1. (#4.3.0) - $!"); + select(STDIN);$|=1; + &debug("q_r: xstatus=$xstatus"); + open (QMQ, "|$qmailqueue")|| &error_condition ("Unable to open pipe to $qmailqueue [$xstatus] (#4.3.0) - $!"); + ($sec,$min,$hour,$mday,$mon,$year) = gmtime(time); + $elapsed_time = tv_interval ($start_time, [gettimeofday]); + $findate = POSIX::strftime( "%d %b ",$sec,$min,$hour,$mday,$mon,$year); + $findate .= sprintf "%02d %02d:%02d:%02d -0000", $year+1900, $hour, $min, $sec; + print QMQ "Received: from $remote_smtp_ip$remote_smtp_auth by $hostname (envelope-from <$returnpath>, uid $real_uid) with qmail-scanner-$VERSION \n"; + if ($scanner_array[0] ne "none") { + print QMQ " ($SCANINFO \n Clear:$tag_score$tag_sa_score. \n"; + print QMQ " Processed in $elapsed_time secs); $findate\n"; + if ($sa_comment ne "" && $sa_rcpt) { + print QMQ "X-Spam-Status: $sa_comment\n"; + print QMQ "X-Spam-Level: $sa_level\n" if ($sa_level ne ""); + print QMQ "X-Spam-Report: SA TESTS\n$sa_report\n" if ($sa_report && $sa_hdr_report); + } + #Only add these headers for Internet-incoming + if ( $descriptive_hdrs && !$QS_RELAYCLIENT) { + print QMQ "${V_HEADER}-Mail-From: $returnpath via $hostname\n"; + print QMQ "${V_HEADER}-Rcpt-To: $recips\n" if ($descriptive_hdrs eq "2"); + print QMQ "$V_HEADER: $VERSION (Clear:$tag_score$tag_sa_score. Processed in $elapsed_time secs Process $nprocess)\n"; + } + } + my $still_headers=1; + my $seen_env=0; + while (<STDIN>) { + if ($still_headers && $sa_fast) { + #break any X-Spam-Status/Level IFF we've set a SA value ourselves. Easier than removing - and it leaves + #them around for diagnosis... + if ($sa_comment ne "" && $sa_rcpt && /^(X-Spam-Status|X-Spam-Flag|X-Spam-Level|X-Spam-Report):/i) { + s/^(X-Spam-Status|X-Spam-Flag|X-Spam-Level|X-Spam-Report):/${V_HEADER}-MOVED-$1:/i; + } + if ($sa_comment =~ /^yes/i && $sa_subject ne "" && !/^Subject: \Q$sa_subject\E/i && /^(Subject):(\s?)([^\n]+)\n/i && $sa_rcpt) { + $altered_subject="$1: $sa_subject $3"; + if ($altered_subject !~ /^: \Q$sa_subject\E/) { + &debug("altering subject line to $altered_subject"); + print QMQ "$altered_subject\n"; + next; + } + } + $still_headers=0 if (/^(\r|\r\n|\n)$/); + #Insert Subject: line if e-mail dosn't contain one but must be tagged + print QMQ "Subject: $sa_subject\n" if ((!$still_headers) && ($sa_comment =~ /^yes/i) && (!$altered_subject) && $sa_subject ne "" && $sa_rcpt); + + } + print QMQ; + } + close(QMQ); #||&error_condition("Unable to close pipe to $qmailqueue (#4.3.0) - $!"); + $xstatus = ( $? >> 8 ); + if ( $xstatus > 10 && $xstatus < 41 ) { + &error_condition("mail server permanently rejected message. (#5.3.0) - $!",$xstatus); + } elsif ($xstatus > 0) { + &error_condition("Unable to open pipe to $qmailqueue [$xstatus] (#4.3.0) - $!",$xstatus); + } + #This child is finished - exit + exit; + } else { + # In parent. + close EOUT; + + # Feed the envelope addresses to qmail-queue. + print EIN "$sender\0$env_recips"; + close EIN || &error_condition ("Write error to envelope pipe. (#4.3.0) - $!"); +} + + # We should now have queued the message. Let's find out the exit status + # of qmail-queue. + waitpid ($pid, 0); + $xstatus =($? >> 8); + if ( $xstatus > 10 && $xstatus < 41 ) { + &error_condition("mail server permanently rejected message. (#5.3.0) - $!",$xstatus); + } elsif ($xstatus > 0) { + &error_condition("Unable to close pipe to $qmailqueue [$xstatus] (#4.3.0) - $!",$xstatus); + } +} + + +sub valid_virus_to_report { + my ($virus_type)=@_; + my ($virus)=; + # This subroutine is used to determine if the virus found during the scan + # is reportable. i.e. do we want to send a message to this user or not as is + # the case with the KLEZ virus. + #&debug("v_v_t_r: called with $virus_type"); + foreach $virus (@silent_viruses_array) { + #&debug("v_v_t_r: does $virus_type contain $virus?"); + if ($virus_type =~ /$virus/i) { + &debug("v_v_t_r: $virus_type contain $virus - so don't notify the sender"); + &minidebug("v_v_t_r: Description contain \"$virus\" - so don't notify the sender"); + return 0; + } + } + return 1; +} + +sub automated_msg { + if ($headers{'x-loop'} || $headers{'auto-submitted'} !~ /^(|no)$/i || $headers{'x-listname'} || $headers{'x-listmember'} || $headers{'mailing-list'} || $headers{'x-mailing-list'} || $headers{'precedence'} =~ /^(bulk|list|junk)$/i || $returnpath =~ /^$|^\#\@\[\]$|anonymous|nobody|daemon|request|bounce|mailer|postm|owner|list|words|majordom|experts|\-(return|error)/i) { + return 1; + } else { + return 0; + } +} + +sub bounce_msg { + if ($returnpath =~ /^$|^\#\@\[\]$|(daemon|bounce|mailer|postm)/i) { + return 1; + } else { + return 0; + } +} + +sub is_unreplyable_email { + my ($addr_type)=@_; + my ($dom,$is_local)=; + #This subroutine is used to see if the sender of this message + #was a mailing-list/postmaster/etc, or the recipient is a local user. + #If it is we don't want to send a reply. + #&debug("i_u_e: called with $addr_type"); + + if ($addr_type eq "recips") { + foreach $dom (@local_domains_array) { + #&debug("i_u_e: does $recips contain $dom?"); + if ($recips =~ /$dom$/i) { + #&debug("i_u_e: yes it does!"); + $is_local++; + } + } + } else { + $is_local="99"; + if (&automated_msg ) { + #&debug("i_u_e: $addr_type is a mailing-list"); + return 1; + } + } + # + #Only reply if it is a local address + if (!$is_local) { + #&debug("i_u_e: is_local=$is_local"); + return 1; + } else { + #&debug("i_u_e: is_local=$is_local"); + return 0; + } +} + +sub email_quarantine_report { + my($start_email_time)=[gettimeofday]; + if ($quarantine_spam) { + # st: now spam is quarantined in a separated directory, but also it is + # possible to set a directory per user, so I must check the directory... + if (! -d "$scandir/quarantine/$smaildir") { + mkdir("$scandir/quarantine/$smaildir",0750) || &error_condition("cannot create $scandir/quarantine/$smaildir - $!"); + mkdir("$scandir/quarantine/$smaildir/new",0750) || &error_condition("cannot create $scandir/quarantine/$smaildir/new - $!"); + mkdir("$scandir/quarantine/$smaildir/cur",0750) || &error_condition("cannot create $scandir/quarantine/$smaildir/cur - $!"); + mkdir("$scandir/quarantine/$smaildir/tmp",0750) || &error_condition("cannot create $scandir/quarantine/$smaildir/tmp - $!"); + } + #Use a different maildir for SPAM + $vmaildir=$smaildir; + $quarantine_event=$quarantine_spam; + }elsif ($quarantine_event =~ /^(Policy|Perlscan)/) { + $destring="policy-violation"; + #Use a different maildir for Policy-blocks + $vmaildir=$pmaildir; + } + + # st: if we have multiple recipient quarantine the file once, unless we have differents smaildir... + return if ( -f "$scandir/quarantine/$vmaildir/new/$file_id"); + + if ($vmaildir ne "none") { + &debug("e_v_r: quarantine msg to $scandir/quarantine/$vmaildir/new/$file_id"); + ### st: if your '$smaildir' resides in a different file system (partition) than + ### '$wmaildir' comment the next line and uncomment the two following lines. + link("$scandir/$wmaildir/new/$file_id","$scandir/quarantine/$vmaildir/new/$file_id")||&error_condition("cannot link $scandir/$wmaildir/new/$file_id into $scandir/quarantine/$vmaildir/new/ - $!"); + # use File::Copy; + # copy("$scandir/$wmaildir/new/$file_id","$scandir/quarantine/$vmaildir/new/$file_id")||&error_condition("cannot copy $scandir/$wmaildir/new/$file_id into $scandir/quarantine/$vmaildir/new/ - $!"); + } + + open(QTINE,">>$scandir/quarantine/$vmaildir/new/$file_id"); + print QTINE "\n*** Qmail-Scanner Quarantine Envelope Details Begin ***\n"; + print QTINE "${V_HEADER}-Mail-From: \"$returnpath\" via $hostname\n"; + print QTINE "${V_HEADER}-Rcpt-To: \"$recips\"\n"; + print QTINE "$V_HEADER: $VERSION ($SCANINFO $destring Found. Processed in ",tv_interval($start_time,[gettimeofday])," secs) process $nprocess \n"; + print QTINE "Quarantine-Description: $quarantine_description\n"; + if (($quarantine_description =~ /spam/i) && $sa_report) { + print QTINE "SA_REPORT hits = $sa_hits/$required_hits\n$sa_report\n"; + } + print QTINE "*** Qmail-Scanner Envelope Details End ***\n"; + close QTINE; + + &email_sender("admin"); + if (!$quarantine_spam) { + &email_sender("sender") if (&valid_virus_to_report($quarantine_description)); + if ($trecips =~ /\0T/) { + for $recip (split(/\0T/,$trecips)) { + &email_recips($recip); + } + } else { + &email_recips($recips); + } + #This is almost 100% certainly SPAM - no point in notifying anyone + } + &write_quarantine_report; + $elapsed_time = tv_interval ($start_time, [gettimeofday]); + &debug("e_v_r: email_quarantine_report took ".tv_interval ($start_email_time, [gettimeofday])." seconds to execute"); +} + +sub cleanup { + closelog(); + chdir("$scandir/"); + if ($archiveit !~ /^(1|yes)$/i) { + #This will only archive mail where the sender or recipient matches the regex that is $archiveit + if ($headers{'MAILFROM'} !~ /$archiveit/i && $headers{'RCPTTO'} !~ /$archiveit/i) { + $archiveit=0; + } + } + if (!$archiveit) { + &debug("cleanup: $rm_binary -rf $ENV{'TMPDIR'}/ $scandir/$wmaildir/new/$file_id") ; + } else { + # check if $archivedir exists + if (!-d "$scandir/$archivedir") { + mkdir("$scandir/$archivedir",0750) || &error_condition("cannot create $scandir/$archivedir - $!"); + mkdir("$scandir/$archivedir/new",0750) || &error_condition("cannot create $scandir/$archivedir/new - $!"); + mkdir("$scandir/$archivedir/cur",0750) || &error_condition("cannot create $scandir/$archivedir/cur - $!"); + mkdir("$scandir/$archivedir/tmp",0750) || &error_condition("cannot create $scandir/$archivedir/tmp - $!"); + } + if ( -f "$scandir/$wmaildir/new/$file_id" ) { + &debug("cleanup: archiving into $scandir/$archivedir/new/"); + &minidebug("cleanup: archiving into $scandir/$archivedir/new/"); + rename("$scandir/$wmaildir/new/$file_id","$scandir/$archivedir/new/$file_id"); + #This will do for now. Not pretty - but very cheap! + #We need to append this information, otherwise how do you know who this message + #was from or to? + # + open(ARCHIVE,">>$scandir/$archivedir/new/$file_id"); + print ARCHIVE "\n*** Qmail-Scanner Envelope Details Begin ***\n"; + print ARCHIVE "${V_HEADER}-Mail-From: \"$returnpath\" via $hostname\n"; + print ARCHIVE "${V_HEADER}-Rcpt-To: \"$recips\"\n"; + print ARCHIVE "$V_HEADER: $VERSION ($SCANINFO Clear:$tag_score$tag_sa_score. Processed in ",tv_interval($start_time,[gettimeofday])," secs)\n"; + if (($quarantine_description =~ /spam/i) && $sa_report) { + print ARCHIVE "SA_REPORT hits = $sa_hits/$required_hits\n$sa_report\n"; + } + print ARCHIVE "*** Qmail-Scanner Envelope Details End ***\n"; + close ARCHIVE; + } + } + if ($DEBUG < 100 && $file_id ne "") { + chdir("$scandir/"); + system("$rm_binary -rf $ENV{'TMPDIR'}/ $scandir/$wmaildir/new/$file_id"); + } + +} + + +sub scan_queue { + my ($scanner,$SCANINFO,$files,$sweep_eng,$sweep_product,$sophie_eng,$dir); + my $start_scan_time =time; + my ($inocucmd_eng,$inocucmd_product,$spamassassin_eng); + + chdir($scandir); + &debug("s_q: re-create the quarantine version file"); + &minidebug("s_q: re-create the quarantine version file"); + foreach $scanner (@scanners_installed) { + $scanner =~ s/_scanner//; + &debug("s_q: detecting version of $scanner"); + if ($scanner eq "uvscan") { + open(UV,"$uvscan_binary --version|")||die "failed to call $uvscan_binary --version - $!"; + while (<UV>) { + chomp; + if (/^Scan engine (v[0-9\.]+) /) { + $SCANINFO .="uvscan: $1/"; + } elsif (/^Virus data file (v[0-9\.]+) /) { + $SCANINFO .= "$1. "; + } + } + close(UV); + } elsif ($scanner eq "csav") { + open(CS,"$csav_binary -virno|")||die "failed to call $csav_binary -virno - $!"; + while (<CS>) { + chomp; + if (/Command Software AntiVirus for Linux (version [0-9\.]+)/) { + $SCANINFO .="csav: $1/"; + } elsif (/^CSA[V]: (.*)/) { + $SCANINFO .= "$1/"; + } + } + close(CS); + } elsif ($scanner eq "trophie" ) { + open(IS,"$trophie_binary -v 2>&1|")||die "failed to call $trophie_binary -v - $!"; + while (<IS>) { + chomp; + if (/VSAPI version (.*)/) { + $SCANINFO .= "trophie: $1/"; + } elsif (/Pattern version ([0-9]+) \(pattern number ([0-9]+)\)/) { + $SCANINFO .= "$1/$2. "; + } + } + close(IS); + } elsif ($scanner eq "iscan") { + open(IS,"$iscan_binary -v|")||die "failed to call $iscan_binary -v - $!"; + while (<IS>) { + chomp; + if (/Virus Scanner (v[0-9\.]+), VSAPI (v[0-9\.\-]+)/) { + $SCANINFO .="iscan: $1/$2/"; + } elsif (/Pattern version ([0-9\.]+)/) { + $SCANINFO .= "$1/"; + } elsif (/Pattern number ([0-9\.]+)/) { + $SCANINFO .= "$1. "; + } + } + close(IS); + } elsif ($scanner eq "fsecure") { + open(FS,"$fsecure_binary --version|")||die "failed to call $fsecure_binary --version - $!"; + while (<FS>) { + chomp; + if (/^F-Secure.*(Release|version)\s+([0-9\.]+)\s+build\s+([0-9]+)/i) { + $SCANINFO .="fsecure: $2/$3/"; + } elsif (/sign.def version ([0-9\.]+-[0-9\.]+-[0-9\.]+)/) { + $SCANINFO .= "$1/"; + } elsif (/fsmacro.def version ([0-9\.]+-[0-9\.]+-[0-9\.]+)/) { + $SCANINFO .= "$1/"; + } elsif (/sign2.def version ([0-9\.]+-[0-9\.]+-[0-9\.]+)/) { + $SCANINFO .= "$1. "; + } elsif (/F-PROT database version (.*)$/) { + $SCANINFO .= "fprot($1)/"; + # Patch for version F-Secure 4.52 by Jyri + } elsif (/AVP FPI Engine database version (.*)$/) { + $SCANINFO .= "avp($1). "; + } elsif (/Libra database version ([0-9\.]+-[0-9\.]+-[0-9\.]+)/) { + $SCANINFO .= "libra database $1 / "; + } elsif (/Orion database version ([0-9\.]+-[0-9\.]+-[0-9\.]+)/) { + $SCANINFO .= "orion database $1 / "; + } elsif (/AVP FPI Engine database version ([0-9\.]+-[0-9\.]+-[0-9\.]+)/) { + $SCANINFO .= "avp fpi database $1. "; + } + } + close(FS); + $SCANINFO .= ". " if ($SCANINFO !~ /\. $/); + } elsif ($scanner eq "fprot") { + open(FP,"$fprot_binary \?|")||die "failed to call $fprot_binary --version - $!"; + while (<FP>) { + chomp; + if (/(F-PROT|Program version:) ([0-9\.]+)/) { + $SCANINFO .="f-prot: $2/"; + } elsif (/Engine version: ([0-9\.]+)/) { + $SCANINFO .= "$1"; + } + } + $SCANINFO .= ". "; + close(FP); + } elsif ($scanner eq "hbedv") { + open(IS,"$hbedv_binary --version 2>&1 |")||die "failed to call $hbedv_binary --version - $!"; + while (<IS>) { + chomp; + if (/engine version:\s+([0-9\.]+)/) { + $SCANINFO .= "hbedv: $1"; + } elsif (/vdf version:\s+([0-9\.]+)/) { + $SCANINFO .= "/$1. "; + } + } + close(IS); + } elsif ($scanner eq "avp") { + open(AVP,"$avp_binary -Y -VL 2>&1 |")||die "failed to call $avp_binary -Y -VL - $!"; + while (<AVP>) { + chomp; + if (/Version (([0-9\.]+)\s+build ([0-9\.]+)|([0-9\.]+))/) { + if ($2) { + $SCANINFO .= "avp: $1/$2. "; + } else { + $SCANINFO .= "avp: $1. "; + } + } + } + close(AVP); + } elsif ($scanner eq "ravlin") { + open(RAV,"$ravlin_binary --version 2>&1 |")||die "failed to call $ravlin_binary --version - $!"; + while (<RAV>) { + chomp; + if (/^Version: ([0-9\.]+)\./) { + $SCANINFO .= "ravlin: $1. "; + } + } + close(RAV); + } elsif ($scanner eq "vexira") { + open(VEX,"$vexira_binary --version 2>&1 |")||die "failed to call $vexira_binary --version - $!"; + while (<VEX>) { + chomp; + if (/^engine version:\s+([0-9\.]+)/) { + $SCANINFO .= "vexira: $1. "; + } + } + close(RAV); + } elsif ($scanner eq "bitdefender") { + open(BITDEF,"$bitdefender_binary --info 2>&1 |")||die "failed to call $bitdefender_binary --info - $!"; + while(<BITDEF>) { + chomp; + if (/^BDC\/Linux\-Console (.*) \(build ([^\)]+)\)/){ + $SCANINFO .= "bitdefender: $1/$2"; + } + if (/^Engine signatures:\s+([0-9]+)/) { + $SCANINFO .= "/$1. "; + } + } + close(BITDEF); + } elsif ($scanner eq "nod32") { + open(NOD,"$nod32_binary --version 2>&1 |")||die "failed to call $nod32_binary --version - $!"; + while(<NOD>) { + chomp; + if (/\s(\d\S+)\s*$/){ + $SCANINFO .= "nod32: $1 "; + } + } + close(NOD); + } elsif ($scanner eq "sophie") { + open(SOP,"$sophie_binary -v 2>&1|")||die "failed to call $sophie_binary -v - $!"; + while (<SOP>) { + chomp; + if (/Sophos engine version (.*)$/) { + $sweep_eng=$1; + } elsif (/Sophos IDE version ([0-9\.]+)/) { + $sweep_product=$1; + } elsif (/Sophie version\s+:\s+([0-9\.]+)/) { + $sophie_eng=$1; + } + } + $SCANINFO .= "sophie: $sophie_eng/$sweep_eng/$sweep_product. "; + close(SOP); + } elsif ($scanner eq "sweep") { + open(SOP,"$sweep_binary -v|")||die "failed to call $sweep_binary -v - $!"; + while (<SOP>) { + chomp; + if (/Engine version\s+:\s+(.*)$/) { + $sweep_eng=$1; + } elsif (/Product version\s+:\s+(.*)$/) { + $sweep_product=$1; + } + } + $SCANINFO .= "sweep: $sweep_eng/$sweep_product. "; + close(SOP); + } elsif ($scanner eq "inocucmd") { + open(IOP,"$inocucmd_binary -HEL|")||die "failed to call $inocucmd_binary -HEL - $!"; + while (<IOP>) { + chomp; + if (/Engine version:\s+(.*) ([0-9\/]+)$/) { + $inocucmd_eng=$1; + } elsif (/Data version:\s+(.*) ([0-9\/]+)$/) { + $inocucmd_product=$1; + } + } + $SCANINFO .= "inocucmd: $inocucmd_eng/$inocucmd_product. "; + close(IOP); + } elsif ($scanner eq "clamscan") { + open(CLAMS,"$clamscan_binary --stdout -V|")||die "failed to call $clamscan_binary --stdout -V - $!"; + while (<CLAMS>) { + chomp; + if (/ersion ([0-9\.\-a-z]+)/i) { + $SCANINFO .="clamscan: $1. "; + } + } + close(CLAMS); + } elsif ($scanner eq "clamdscan") { + open(CLAMS,"$clamdscan_binary --version 2>&1|")||die "failed to call $clamdscan_binary --version - $!"; + while (<CLAMS>) { + chomp; + if (/ersion ([0-9\.\-a-z]+)/i) { + $SCANINFO .="clamdscan: $1. "; + } elsif (/^ClamAV ([^\/]+)\/([^\/]+)\//) { + $SCANINFO .="clamdscan: $1/$2. "; + } + } + close(CLAMS); + } elsif ($scanner eq "spamassassin") { + #X-Spam-Checker-Version: SpamAssassin 2.01 + open(SPAS,"$spamassassin_binary -V |")||die "failed to call $spamassassin_binary -V - $!"; + $spamassassin_eng="2.x"; + while (<SPAS>) { + chomp; + if (/^SpamAssassin version (.*)$/i) { + $spamassassin_eng=$1; + } + } + close(SPAS); + $SCANINFO .= "spamassassin: $spamassassin_eng. "; + } elsif ($scanner eq "perlscan") { + $SCANINFO .="perlscan: $VERSION. "; + } else { + #Catch-all for other ones + $SCANINFO .= "$scanner: ???. "; + } + } + $SCANINFO =~ s/ \. / /g; + open(VER,">$versionfile.tmp")||die "cannot write to $versionfile.tmp - $!"; + print VER $SCANINFO; + close(VER); + rename("$versionfile.tmp","$versionfile"); + &debug("s_q: cleaning up files older than 2 days via $find_binary $scandir/tmp -mtime +2 -exec $rm_binary -rf {} \;"); + &minidebug("s_q: cleaning up files older than 2 days via $find_binary $scandir/tmp -mtime +2 -exec $rm_binary -rf {} \;"); + my ($OLDFILES)=`$find_binary $scandir/tmp -mtime +2 -exec $rm_binary -rf {} \\; 2>/dev/null`; + &debug("s_q: cleaning up quarantined mail older than 14 days via $find_binary $scandir/quarantine -type f -mtime +14 -exec $rm_binary -rf {} \;"); + &minidebug("s_q: cleaning up quarantined mail older than 14 days via $find_binary $scandir/quarantine -type f -mtime +14 -exec $rm_binary -rf {} \;"); + $OLDFILES=`$find_binary $scandir/quarantine/ -type f -mtime +14 -exec $rm_binary -f {} \\; 2>/dev/null`; +} + +sub write_quarantine_report { + my ($temp,$desc,$report,$subj); + $subj=$headers{'subject'}; + $subj =~ s/\t/ /g; + $desc=$quarantine_description; + $desc =~ s/\n\t/ /g; + $nowtime = strftime("%a, %d %b %Y %H:%M:%S %Z", localtime(time)); + $report = "$nowtime\t$returnpath\t$recips\t$subj\t$desc\t$SCANINFO\n"; + open(QUARANTINELOG,">>$scandir/$quarantinelog"); + print QUARANTINELOG $report; + close QUARANTINELOG; + &debug("w_v_r: writing quarantine log report of: $report"); +} + +sub scanner_info { + open(SC,"<$versionfile")||&error_condition("cannot open $versionfile - did you initialise the system by running \"$prog -z\"? - $!"); + $SCANINFO = <SC>; + $SCANINFO =~ s/\n|\r|\0/ /g; + close(SC); +} + +sub generate_quarantine_db { + # use DB_File; + use vars qw( %h); + my ($line,%array,$count,$match,$type,$descr,$entry,$descrip,$size); + if ($opt_g) { + print "perlscanner: generate new DB file from $db_filename.txt\n"; + unlink("$db_filename.db.tmp"); + tie (%array, 'DB_File', "$db_filename.db.tmp", O_CREAT|O_RDWR, 0640, $DB_HASH ) || &error_condition("cannot open for write $db_filename.db.tmp - $!"); + + open(TXT,"<$db_filename.txt")||&error_condition("cannot read $db_filename.txt - $!"); + + #Remeber: all filenames are lowercased, but headers aren't... + while (<TXT>) { + $line++; + next if (/^\#|^\s+$/); #ignore lines starting with hashes + chomp; + $count++; + ($match,$type,$descr)=split(/\t+/,$_,3); + if ( $match eq "" || ($type !~ /^SIZE=(\-|)[0-9]+$/ && $type !~ /^Policy-[0-9a-z\_\-]+:$/i) ) { + print "ERROR: incorrect format on line \"$line\"\n"; + &error_condition("ERROR: incorrect format on line \"$line\""); + } else { + #Strip off any regex endings + if ($type =~ /^SIZE=(|\-)[0-9]+$/) { + #this is a filename/attachment + if ( $match =~ /^\.dat$/i ) { + + print "ERROR: on line \"$line\".\nCannot block all .dat files. Will block too many normal messages.\n"; + &error_condition("ERROR: on line \"$line\".\nCannot block all .dat files. Will block too many normal messages."); + next; + } + $match = tolower($match); + } else { + #this is for header matches + $match =~ s/^\^|\$$//g; + #Now make unique + $match = "$line:$match"; + $type =~ s/:$//; + } + $array{"$match"}="$type\t$descr"; + } + } + close(TXT); +# $array->sync; + untie %array ; + rename("$db_filename.db.tmp","$db_filename.db"); + print "perlscanner: total of $count entries.\n"; + } else { + print "perlscanner: reading from $db_filename.db\n"; + tie (%array, 'DB_File', "$db_filename.db", O_RDONLY, 0600) || &error_condition("cannot open $db_filename.db - $!"); + foreach $entry (keys %array) { + $count++; + ($type,$descrip)=split(/\t/,$array{$entry},2); + if ( $type =~ /^SIZE=((\-|)[0-9]+)/) { + if ($type eq "SIZE=-1") { + $type="Any"; + } elsif ($type =~ /^SIZE=((\-|)[0-9]+)$/) { + $type="$1 bytes"; + } + print "File: \t$entry\n\t\t\tSize: $type\n\t\t\tDescription: $descrip\n\n"; + } + if ($type =~ /^Policy-(.*)$/i) { + $type=$1; + #Strip off numeric uid... + $entry =~ s/^[0-9]+://; + if (grep(/^$type$/,@virtualheaders_array)) { + print "Virtual Header: \t$type\n\t\t\tContent: ^$entry\$\n\t\t\tDescription: $descrip\n\n"; + } else { + print "Email Header: \t$type\n\t\t\tContent: ^$entry\$\n\t\t\tDescription: $descrip\n\n"; + } + } + } + untie %array; + print "perlscanner: total of $count entries found.\n"; + } +} + + + + +sub show_version { + my ($scanner); + &scanner_info; + print " + +$prog + +Version: $VERSION ($st_version) + +Perl: Summary of my perl5 (revision 5 version 8 subversion 8) configuration:\n\n"; + + if ($settings_pd && $opt_V) { + print "Settings per domain: enabled\n\n"; + } else { + print "Settings per domain: disabled\n\n" if ($opt_V); + } + + print "Scanners installed: "; + foreach $scanner (@scanners_installed) { + print " $scanner, "; + } + + if ($settings_pd && $opt_V) { + print "\n\nScanners default: "; + foreach $scanner (@scanners_default) { + print " $scanner, "; + } + } + + print "\n\nScanner versioning: $SCANINFO\n"; + + if ($spamc_binary =~ /spamc/ && $opt_V) { + print "\nSpamassassin settings:\n"; + if ($sa_fast || $sa_alt) { + print " Mode: fast_spamassassin\n"; + } else { + print " Mode: verbose_spamassassin\n"; + } + if ($sa_alt) { + print " sa_alt: enabled / sa_debug = $sa_debug / sa_hdr_report_site = $sa_hdr_report_site\n"; + } + if ($sa_forward_site) { + print " sa_forward_site = '$sa_forward_site' / sa_fwd_verbose_site = $sa_fwd_verbose_site\n"; + } + print " sa_subject_site = '$sa_subject_site'\n"; + print " sa_delta_site = $sa_delta_site\n"; + print " sa_quarantine_site = $sa_quarantine_site\n"; + print " sa_delete_site = $sa_delete_site / sa_reject_site = $sa_reject_site\n"; + } + + print " +Operating System: $sysname, $release +Hardware: $machine"; + print "\n\n\n"; +} + + +sub email_sender { + #Don't e-mail bounced mail messages/etc! + return if (&is_unreplyable_email('sender')); + my($addr_type)=@_; + my ($HDR,$hdr,$tmpsndrs,$tmpsubj,$polstring)=; + my ($tmpmsgid)= &uniq_id() . "-" . $V_FROM; + $polstring='policy' if (&notify_addr('nmlvadm')); + + open(SM,"|$qmailinject -h -f ")||&error_condition("cannot open $qmailinject for sending quarantine report - $!"); + print SM "From: \"$V_FROMNAME\" <$V_FROM>\n"; + if ($addr_type =~ /sender/) { + $addr_type='psender' if ($NOTIFY_ADDRS =~ /psender/); + if ($addr_type eq "sender") { + if (!&is_unreplyable_email('sender') && &notify_addr('sender')) { + &debug("e_s: sending quarantine report via: $qmailinject to sender address ($returnpath)"); + print SM "To: $returnpath\n"; + $tmpsndrs = "$returnpath"; + } else { + &debug("e_s: don't notify sender"); + } + } elsif ($addr_type eq "psender") { + if (!&is_unreplyable_email('sender') && &notify_addr('sender') && ($quarantine_event =~ /^(policy|perlscan)/i && $quarantine_event !~ /(gr[ae]ylist|virus)/i)) { + &debug("e_s: sending policy quarantine report via: $qmailinject to psender address ($returnpath)"); + &minidebug("e_s: sending policy quarantine report via: $qmailinject to psender address ($returnpath)"); + print SM "To: $returnpath\n"; + $tmpsndrs = "$returnpath"; + } else { + &debug("e_s: don't notify psender"); + } + } else { + return; + } + } else { + # st: if the mail is local and is set nmladm or nmlvadm, + # always notify admin (maybe it is not good or a big site) + if ( &notify_addr('admin') || ( &notify_addr('nmladm') && (!&is_unreplyable_email('sender') || $QS_RELAYCLIENT) ) || ( &notify_addr('nmlvadm') && (($quarantine_event =~ /^(policy|perlscan)/i && $quarantine_event !~ /(gr[ae]ylist|virus)/i && !&is_unreplyable_email('sender')) || $QS_RELAYCLIENT) ) ) { + &debug("e_s: sending $polstring quarantine report via: $qmailinject to admin address ($QUARANTINE_CC)"); + print SM "To: $QUARANTINE_CC\n"; + $tmpsndrs .= "$QUARANTINE_CC"; + } else { + &debug("e_s: don't notify admin"); + } + } + $tmpsubj="$destring found in sent message \"$headers{'subject'}\""; + $tmpsubj =~ s/(\r|\0|\n)/ /g; + if ($QS_RELAYCLIENT) { + print SM "Subject: LOCAL USER - $tmpsubj\n"; + } else { + print SM "Subject: $tmpsubj\n"; + } + print SM "Message-ID: <".&uniq_id."\@$hostname>\n"; + print SM "X-Tnz-Problem-Type: 40\n"; + print SM "Auto-Submitted: auto-replied\n"; + if ($headers{'message-id'} ne "") { + print SM "In-Reply-To: ",$headers{'message-id'},"\n"; + print SM "References: ",$headers{'message-id'},"\n"; + } + print SM "MIME-Version: 1.0\n"; + print SM "Content-type: text/plain\n"; + print SM "Content-Transfer-Encoding: 8bit\n"; + #Only add these headers for Internet-incoming + if ( $descriptive_hdrs && !$QS_RELAYCLIENT) { + print SM "${V_HEADER}-Mail-From: $returnpath via $hostname\n"; + print SM "${V_HEADER}-Rcpt-To: $recips\n" if ($descriptive_hdrs eq "2"); + print SM "$V_HEADER: $VERSION ($SCANINFO $destring Found. \n"; + print SM " Processed in ",tv_interval($start_time,[gettimeofday])," secs)\n"; + } + print SM "\n"; + if (&is_unreplyable_email('sender')) { + print SM " +Attention: $V_FROMNAME.\n"; + print SM " +[This warning message is *not* being sent to the apparent originator +of the original message. This address appears to be that of a +mailing list or other automated email system.]\n"; + print SM "\n---------------------------------------\n\n"; + } else { + print SM " +Attention: $returnpath\n"; + } + print SM "\n +A $destring was found in an Email message you sent. +This Email scanner intercepted it and stopped the entire message +reaching its destination. + +The $destring was reported to be: + +$quarantine_description\n"; + if (($addr_type !~ /sender/) && ($quarantine_description =~ /spam/i) && $sa_report) { + print SM "\nSA_REPORT hits = $sa_hits/$required_hits\n$sa_report\n\n"; + } + if ($destring eq "virus") { + print SM "\n +Please update your virus scanner or contact your IT support +personnel as soon as possible as you may have a virus on your system.\n"; + } else { + print SM "\n +Please contact your IT support personnel with any queries regarding this +policy.\n"; + } + print SM "\n +Your message was sent with the following envelope: + +MAIL FROM: $returnpath +RCPT TO: $recips + +... and with the following headers:\n\n"; + print SM "---\n"; + print SM "MAILFROM: $headers{'MAILFROM'}\n"; + print SM "RCPTTO: $headers{'RCPTTO'}\n"; + print SM "IP-Addr: $headers{'REMOTEIPADDR'}\n"; + print SM "$HEADERS\n"; + print SM "---\n"; + if ($addr_type !~ /sender/ ) { + print SM "\n + +The original message is kept in: + + $hostname:$scandir/quarantine/$vmaildir/new/$file_id + +where the $V_FROMNAME can further diagnose it. + +The Email scanner reported the following when it scanned that message: + +--- +$description +---\n"; + } + close(SM); + if ($log_details) { + &log_msg("qmail-scanner","Clear:RC:1(127.0.0.1):",$elapsed_time,1100,$V_FROM,$tmpsndrs,$tmpsubj,$tmpmsgid,"quarantine-event.txt:1000"); + } +} + +sub email_recips { + my($recip)=@_; + return if ($recip eq ""); + #Don't notify precips if this is NOT a "Policy block" + if (&notify_addr('precips')) { + return if ($quarantine_event !~ /^(policy|perlscan)/i); + } else { + #From now on precips is the same as recips + $NOTIFY_ADDRS=~s/precips/recips/; + } + return if (!&notify_addr('recips')); + my($HDR,$hdr,$tmprecips,$tmpsubj)=; + my($tmpmsgid)= &uniq_id() . "-" . $V_FROM; + + open(SM,"|$qmailinject -h -f ")||&error_condition("cannot open $qmailinject for sending quarantine report - $!"); + print SM "From: \"$V_FROMNAME\" <$V_FROM>\n"; + if (!&is_unreplyable_email('recips')) { + &debug("e_r: sending quarantine report via: $qmailinject to recip address ($recip)"); + print SM "To: $recip\n"; + } + $tmpsubj= "$destring found in received message \"$headers{'subject'}\""; + $tmpsubj =~ s/(\r|\0|\n)/ /g; + print SM "Subject: $tmpsubj\n"; + print SM "Message-ID: <".&uniq_id."\@$hostname>\n"; + print SM "X-Tnz-Problem-Type: 40\n"; + if ($headers{'message-id'} ne "") { + print SM "In-Reply-To: ",$headers{'message-id'},"\n"; + print SM "References: ",$headers{'message-id'},"\n"; + } + print SM "Auto-Submitted: auto-replied\n"; + print SM "MIME-Version: 1.0\n"; + print SM "Content-type: text/plain\n"; + if ( $descriptive_hdrs ) { + print SM "${V_HEADER}-Mail-From: $returnpath via $hostname\n"; + print SM "${V_HEADER}-Rcpt-To: $recip\n" if ($descriptive_hdrs eq "2"); + print SM "$V_HEADER: $VERSION ($SCANINFO $destring Found. \n"; + print SM " Processed in ",tv_interval($start_time,[gettimeofday])," secs)\n"; + } + print SM "\n"; + print SM " +Attention: $recip\n"; + if (!&is_unreplyable_email('recips')) { + if (&notify_addr('sender')) { + print SM " +[A message has been sent to the originator, stating there is a virus +in the Email they just sent to you. No further action is required on +your part.]\n"; + } + } else { + print SM " + +[This message was _not_ sent to the originator address, as that appears to +be a mailing-list or some other automated Email message]\n"; + } + print SM "\nA $destring was found in an Email message sent to you. +This Email scanner intercepted it and stopped the entire message +before it reached you. No further action is required on your part.\n"; + print SM "\nThe $destring was reported to be: + +$quarantine_description + +Please contact your IT support personnel with any queries regarding this +policy. + +The message sent to you had the following envelope: + +MAIL FROM: $returnpath +RCPT TO: $recips + +... and with the following headers:\n\n"; + print SM "---\n"; + print SM "MAILFROM: $headers{'MAILFROM'}\n"; + print SM "RCPTTO: $headers{'RCPTTO'}\n"; + print SM "IP-Addr: $headers{'REMOTEIPADDR'}\n"; + print SM "$HEADERS\n"; + print SM "---\n"; + #print SM "\nLxOCALE_recips_quarantine\n"; + close(SM); + if ($log_details) { + &log_msg("qmail-scanner","Clear:$tag_score$tag_sa_score",$elapsed_time,1100,$V_FROM,$recip,$tmpsubj,$tmpmsgid,"quarantine-event.txt:1000"); + } +} + +sub notify_addr { + my($addr_type)=@_; + #&debug("n_a: notify_addr (set to $NOTIFY_ADDRS) called with $addr_type"); + if (($NOTIFY_ADDRS =~ /$addr_type/ || $NOTIFY_ADDRS =~ /all/) && ($NOTIFY_ADDRS !~ /none/)) { + return 1; + } else { + return 0; + } +} + +sub unzip_file { + my($zipfile)=@_; + my ($MAYBEZIP,$ztmp,$zfile,$zline,$zsize,$zip_status); + + &debug("u_f: potential zip archive file found ($zipfile)."); + &debug ("u_f: it is possibly a zip file, run unzip $unzip_options -t $ENV{'TMPDIR'}/$zipfile"); + $MAYBEZIP=`$unzip_binary $unzip_options -t $ENV{'TMPDIR'}/$zipfile 2>&1`; + $zip_status=($? >> 8); + + if ( ($zip_status > 0) && ($zip_status !~ /^(1|2|3|51|81|82)$/) && ($MAYBEZIP !~ /skipping: /) ) { + &debug("u_f: not a recognisable zip file ($MAYBEZIP)"); + } else { + &debug ("u_f: it is a zip file"); + if ($MAYBEZIP =~ /skipping:.*password/) { + &debug ("u_f: it is a password-protected zip file"); + &minidebug ("u_f: it is a password-protected zip file"); + &eventlog("UNZIP:PASSWORD_PROTECTED"); + $CRYPTO_TYPE="CR:ZIP(encrypted)"; + } + if ($force_unzip) { + &debug ("u_f: check size of contents before unzipping to disk"); + my $CHECK_ZIP_SIZE=`$unzip_binary $unzip_options -lv $ENV{'TMPDIR'}/$zipfile 2>&1`; + open(ZIPPED,"$unzip_binary $unzip_options -lv $ENV{'TMPDIR'}/$zipfile 2>&1|")||&error_condition("u_f: cannot open $ENV{'TMPDIR'}/$zipfile - $!"); + my $zip_file_size=0; + while (<ZIPPED>) { + $zip_file_size=$1 if (/^\s+([0-9]+)\s+/); + } + close ZIPPED ; + &debug("u_f: this zip file unpacks to $zip_file_size bytes of content"); + if ($max_zip_size > 0 && $max_zip_size < $zip_file_size) { + $quarantine_description="Disallowed zip file ($zipfile) - content exceeds maximum allowed size"; + &debug("u_f: $quarantine_description"); + &minidebug("u_f: $quarantine_description"); + $destring='problem'; + $quarantine_event="Policy:Oversized_ZIP"; + $quarantine_DOS=$quarantine_event; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$zipfile"; + $file_desc .= "oversized_zip:$msg_size\t"; + return; + } + &debug("u_f: run $unzip_binary $unzip_options $ENV{'TMPDIR'}/$zipfile 2>&1"); + open(ZIPPED,"$unzip_binary $unzip_options $ENV{'TMPDIR'}/$zipfile 2>&1|")||&error_condition("u_f: cannot open $ENV{'TMPDIR'}/$zipfile - $!"); + while (<ZIPPED>) { + if (/^\s+\w+:\s+(.*)$/) { + ($ztmp=$1)=~s/^.*\///g; + #Grrr, I don't know if this'll be exploited, but I have to remove the whitespace... + #$ztmp=~s/\s+$//g; + #if ($ztmp ne "" && !grep(/^${ztmp}$/,@zipfile_list)) { + #&debug("u_f: adding file \"$ztmp\" to list of zipped files"); + #push(@zipfile_list, $ztmp); + #} + } + if (/^\s+skipping:\s(.*)\s+(shrink|encrypted|incorrect password)/) { + $passwd_protected_zip++ if (!/^\s+skipping:\s(.*)\s+shrink/); + #grab these protected filenames for reports anyway. + $zfile = $1; + $zfile =~ s/^.*\///g; + $zfile =~ s/(^\s+|\s+$)//g; + #$file_desc .= "$zfile:$zsize\t"; + } + close(ZIPPED); + $zip_status=($? >> 8); + if ($zip_status > 0 && ($zip_status !~ /^(1|2|3|51|81|82)$/ && !$passwd_protected_zip)) { + &error_condition("u_f: cannot close unzip (error code: $zip_status,$passwd_protected_zip) - $!"); + } + } + } + #Only delete original zip file if it happily unpacked. + if ( $zip_status eq 0 && -f "$ENV{'TMPDIR'}/$zipfile") { + #system $rm_binary,"-f","$ENV{'TMPDIR'}/$zipfile"; + &debug("u_f: $zip_status, and successfully unzipped"); + #It may have been deleted, but you still want to see if + #it matches the perlscanner DB... + #$zipfile=tolower($zipfile); + #push(@zipfile_list, $zipfile) if (!grep(/^$zipfile$/,@zipfile_list)); + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$zsize,$atime,$mtime,$ctime,$blksize,$blocks); + ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$zsize,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$zipfile"); + $file_desc .= "$zipfile:$zsize\t"; + } + } +} + +sub deltatime { + my ($delta,$current_time); + $current_time = [gettimeofday]; + $delta = tv_interval ($last_time, $current_time); + $last_time=$current_time; + return $delta; +} + +sub qmail_parent_check { + my $ppid=getppid; + #&debug("q_s_c: PPID=$ppid"); + if ($ppid == 1) { + &debug("q_s_c: Whoa! parent process is dead! (ppid=$ppid) Better die too..."); + &minidebug("q_s_c: Whoa! parent process is dead! (ppid=$ppid) Better die too..."); + &cleanup; + &close_log; + #Exit with temp error anyway - just to be real anal... + exit 111; + } +} + + +sub check_and_grab_attachments { + #This subroutine find attachments (e.g., MIME/binhex,uuencode) within e-mails + + if (/^MIME-Version:/i || ($headers{'content-type'} ne "" && $headers{'content-type'} !~ /^text\/plain/i)) { + $indicates_attachments++; + &debug("c_a_g: found MIME attachment") if ($indicates_attachments == 2); + } + #This finds MIME messages banged onto the bottom of bounces + if (/^\-+ (Below this line|This) is a copy of the message/) { + $indicates_attachments += 2; + &debug("c_a_g: found hidden MIME attachment") if ($indicates_attachments == 2); + &minidebug("c_a_g: found hidden MIME attachment") if ($indicates_attachments == 2); + } + #This will define any text mail that contains a URL as requiring scanning - otherwise + #some phishing attacks will geet past + if ($indicates_attachments < 2 && /http:\/\/|www\.|[a-z0-9\-]+\.[a-z0-9\-]+\//i) { + $indicates_attachments += 2; + &debug("c_a_g: found URL in message - maybe phishy - better scan it"); + &minidebug("c_a_g: found URL in message - maybe phishy - better scan it"); + } + #This finds BinHex attachments + if (/^\(This file must be converted with BinHex/) { + $indicates_attachments += 2; + &debug("c_a_g: found hidden BinHex attachment") if ($indicates_attachments == 2); + &minidebug("c_a_g: found hidden BinHex attachment") if ($indicates_attachments == 2); + } + my ($begin,$perms,$uufile,$uuextension,$uulength,$uuencoded_attachments,$begin_content); + if (/^(begin) ([0-9][0-9][0-9]) (.*)\n$/) { + &debug("Ooohhhh, a uuencoded attachment!"); + &minidebug("Ooohhhh, a uuencoded attachment!"); + #Better reset this message back to potentially having attachments + $plain_text_msg=0; + $uuencoded_attachments++; + $begin=$1; + $perms=$2; + $uufile=tolower($3); + push(@uufile_list, $uufile) if(!grep(/^$uufile$/,@uufile_list)); + $uufile=~s/[^0-9a-z\_\-\.]/_/gi; + $uufile =~ /\.([^\.]+)$/; + $uuextension=$1; + #Ensure the file extension isn't too long either... + $uuextension=substr($uuextension,0,20); + $uulength=length($uufile); + #Ensure the filelength isn't too large! + if ( $uulength > $MAX_FILE_LENGTH) { + &debug("uudecode output: gah! filename is > $MAX_FILE_LENGTH (actually $uulength), chopping..."); + &minidebug("uudecode output: gah! filename is > $MAX_FILE_LENGTH (actually $uulength), chopping..."); + $uufile=substr($uufile,0,$MAX_FILE_LENGTH).".".$uuextension; + } + return if (!$uudecode_binary); + if (! -f "$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue") { + open(UUIN,">$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue")||&error_condition("cannot open $ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue - $!"); + } else { + &error_condition("$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue already exists! - $!"); + } + print UUIN "$begin 640 $uufile\n"; + print TMPFILE; + $begin_content = "uuencode"; + while (<STDIN>) { + if ($begin_content eq "uuencode" && $_ =~ /^(M35JJ|M35J0|M35KU|M35H\\|M35HN)/i) { + &debug("w_c: looks like a Windows executable, filename=$uufile"); + } + print UUIN; + print TMPFILE; + if (/^end/) { + close(UUIN)||&error_condition("cannot close $ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile - $!"); + #uudecode it - toss away the error code - who cares if it's broken... + &debug("c_a_g_u: $uudecode_binary $uudecode_pipe $ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue"); + if (! -f "$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile") { + system("$uudecode_binary -o $ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile $ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue 2>/dev/null"); + rename("$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile","$ENV{'TMPDIR'}/$uufile") if (!-f "$ENV{'TMPDIR'}/$uufile"); + } else { + &error_condition("$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile already exists! - $!"); + } + #open(UUOUT,"$uudecode_binary $uudecode_pipe $ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue|"); + #open(UUFILE,">$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile"); + #while (<UUOUT>) { + #print UUFILE; + #} + #close UUOUT; + #close UUFILE; + &debug("deleting uuencoded file as we have a decoded version of it now"); + unlink("$ENV{'TMPDIR'}/$file_id-$uuencoded_attachments-$uufile.uue") if ($DEBUG < 100); + last; + } + } + } +} + +sub log_msg { + my($msgtype,$status,$elapsed_time,$msgsize,$frm,$recips,$subj,$msgid,$attachs)=@_; + my ($msg,$file); + + + my $syslogtype='mail|info'; + + if ($log_details eq "syslog") { + + $msgtype =~ s/\s/_/g; + $msgtype .= "[$$]"; + $status =~ s/\s//g; + $syslogtype='mail|warning' if ($status !~ /^Clear/); + $elapsed_time =~ s/\s//g; + $elapsed_time=0.0 if (!$elapsed_time); + $elapsed_time=substr($elapsed_time,0,8); + $frm =~ s/\s/_/g; + $frm='<>' if (!$frm); + $frm=substr($frm,0,100); + $recips =~ s/\s/\|/g; + $recips='<>' if (!$recips); + $recips=substr($recips,0,100); + $subj =~ s/\s/_/g; + $subj='<>' if (!$subj); + $subj=substr($subj,0,80); + $msgid =~ s/\s/_/g; + $msgid = '<>' if (!$msgid); + $msgid=substr($msgid,0,80); + $msgsize =~ s/\s//g; + $attachs =~ s/\s$//g; + #Sub any spaces for underscores then swap tabs for spaces, + #syslog doesn't like tabs, so spaces in filenames have to go... + $attachs =~ s/\ /_/g; + $attachs =~ s/\t/ /g; + if (!$attachs) { + foreach $file (@uufile_list,@zipfile_list,@attachment_list) { + $file =~ s/\s/_/g; + $attachs .= "$file "; + } + $attachs =~ s/\s+$//g; + } + $attachs="$file_id-unpacked:$msg_size" if (!$attachs); + #$attachs=substr($attachs,0,100); + $msg = "$status $elapsed_time $msgsize $frm $recips $subj $msgid $attachs"; + #Do final santity check and remove all low-end chars - like NULL + #I have no idea how some older syslogs would react to such things... + $msg =~s/[\x00-\x09]//g; + $msg =~ s/%/%%/g; + #Now ensure syslog record isn't larger than max syslog size of 1024 chars + $msg=substr($msg,0,1024); + eval { + $SIG{ALRM} = sub { die "Maximum time writing to syslog exceeded. syslog is hung/broken." }; + alarm 10; + eval { + syslog($syslogtype,"$msgtype: $msg"); + }; + if ($@) { + setlogsock('inet'); + syslog('mail|info',"$msgtype: $msg"); + } + }; + #The message is delivered - so no temp failure here - you just have to lose the log entry... + alarm 0; + } else { + #No error checking - inability to write a log report shouldn't + #stop the mail getting through! + + $msgtype =~ s/\t/ /g; + $status =~ s/\s//g; + $elapsed_time =~ s/\s//g; + $elapsed_time=0 if (!$elapsed_time); + $frm =~ s/\t/ /g; + $frm='<>' if (!$frm); + $recips =~ s/\t/ /g; + $recips='<>' if (!$recips); + $subj =~ s/\t/ /g; + $subj='<>' if (!$subj); + $msgid =~ s/\t/ /g; + $msgid = '<>' if (!$msgid); + $msgsize =~ s/\s//g; + $attachs =~ s/\s$//g; + $attachs =~ s/\t/ /g; + $attachs="$file_id-unpacked:$msg_size" if (!$attachs); + $msg = "$status\t$elapsed_time\t$msgsize\t$frm\t$recips\t$subj\t$msgid\t$attachs"; + my $nowtime = strftime("%a, %d %b %Y %H:%M:%S %Z", localtime(time)); + open LOGMSG, ">>$log_details"; + print LOGMSG "$nowtime\t$msg\n"; + close LOGMSG; + } + &debug("$msgtype: $msg"); +} + + +sub normalize_string { + my($type,$raw_string)=@_; + my($bit,$string,$nstring,$encoding,$start_normalize_time,$stop_normalize_time,$normalize_time)=; + $start_normalize_time=[gettimeofday]; + $string=$raw_string; + if ($raw_string =~ /^\=\?[^\?]+\?([bq])\?(.*)\?\=$/i) { + $encoding=$1; + $string=$2; + #&debug("normalize_string: $type \"$string\" is an \"$encoding\" encoded - normalize"); + if ($encoding =~ /^B$/i) { + use MIME::Base64; + foreach $bit (split(/=\?[^\?]+\?B\?/i,$raw_string)) { + $bit=~s/\?=$//; + $nstring .= decode_base64($bit); + } + &debug("normalize_string: $type \"$string\" is decoded to \"$nstring\""); + }elsif ($encoding =~ /^Q$/i) { + use MIME::QuotedPrint; + foreach $bit (split(/=\?[^\?]+\?Q\?/i,$raw_string)) { + $bit=~s/\?=$//; + $nstring .= decode_qp($bit); + } + &debug("normalize_string: $type \"$string\" is decoded to \"$nstring\""); + }else { + &debug("normalize_string: encoded string discovered that isn't Quoted-printable or Base64"); + &minidebug("normalize_string: encoded string discovered that isn't Quoted-printable or Base64"); + $illegal_mime=1; + $destring='LOCALE_destring_problem'; + $quarantine_description="Disallowed MIME encoding - potential virus"; + $quarantine_event="Policy:Bogus_Encoding"; + $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found"; + #$file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E/); + return $string; + } + }else{ + $nstring=$string; + } + $stop_normalize_time=[gettimeofday]; + $normalize_time = tv_interval ($start_normalize_time, $stop_normalize_time); + &debug("normalize_string: finished normalizing in $normalize_time secs") if ($encoding ne ""); + &minidebug("normalize_string: finished normalizing in $normalize_time secs") if ($encoding ne ""); + return $nstring; +} + +############################### +## +## END of standard subroutines +## Virus-scanner specific subroutines automatically added below by setup.sh +## +############################### + +################################################# +# Subroutines added by ST +################################################# + +sub minidebug { + my $dnowtime = strftime("%a, %d %b %Y %H:%M:%S %Z", localtime(time)); + print LOG "$dnowtime:$nprocess: ",@_,"\n" if ($MINIDEBUG && !$DEBUG); +} + +sub close_log { + ($sec,$min,$hour,$mday,$mon,$year) = localtime(time); + + &debug("--- all finished. Total of ",tv_interval ($start_time, [gettimeofday])," secs"); + &minidebug("------ Process $nprocess finished. Total of ",tv_interval ($start_time, [gettimeofday])," secs"); + close(LOG); +} + +sub reject_email { + my ($exit_string,$exit_code)=@_; + $exit_code=111 if (!$exit_code); + + # st: tell qmail-smtpd why the message is rejected, + # so it can be written to the qmail-smtpd log + warn "$V_HEADER-$VERSION: $exit_string\n" if ($MINIDEBUG <= 2); + warn "$nppid QS-$VERSION: $exit_string\n" if ($MINIDEBUG > 2); + + &debug("r_e: $V_HEADER-$VERSION: $exit_string"); + &minidebug("r_e: $V_HEADER-$VERSION: $exit_string") if ($MINIDEBUG <= 2); + &minidebug("r_e: QS-$VERSION: $exit_string") if ($MINIDEBUG > 2); + + &cleanup; + + &close_log; + exit $exit_code; +} + +############################################## +# st: SETTINGS PER DOMAIN routines +############################################## + +sub start_scanners { + my($e_sender,$f_recips,$msg)=@_; + $sa_rcpt='0'; + + # Now, start the scanners! + &init_scanners if ($scanner_array[0] ne "none"); + + # st: if the message is marked to delete skip the mailing routines + if (!$del_message) { + if (($quarantine_event || $quarantine_spam) && ($scanner_array[0] ne "none")) { + &debug("unsetting TCPREMOTEIP env var"); + delete $ENV{'TCPREMOTEIP'}; + #Reset locale back to original + $ENV{'LC_ALL'}=$orig_locale; + + if ($sa_forward ne "" && $quarantine_event =~/spam/i && $description !~/potential virus/i) { + if ($sa_fwd_verbose) { + $sa_hdr_report='1' if ($sa_alt && $sa_debug && $sa_report); + &qmail_parent_check; + &qmail_requeue($e_sender,"T$sa_forward\0\0",$msg); + } else { + open (SF,"$qmailinject -f$returnpath $sa_forward < $msg|")||&error_condition("cannot run $qmailinject -f$returnpath $sa_forward < $msg - $!"); + close SF ; + } + # st: forward the messages just once.. + $sa_rcpt='0'; + $sa_forward=; + } + ## st: This code is from qs-2.00, I have to check... + #is this a greylist event? + if ($quarantine_event=~/gr[ae]ylist/i ) { + #This text will only be seen by those using the "custom-error" + #patch. Others will just get a general "qq" temp failure msg. + &log_event; + print STDERR "Z$quarantine_event"; + &cleanup; + &close_log; + exit 82; + }else{ + &email_quarantine_report; + } + ## + } else { + &qmail_parent_check; + &qmail_requeue($e_sender,$f_recips,$msg); + } + } +} + +sub sa_defaults { + $sa_subject=$sa_subject_site; + $sa_quarantine=$sa_quarantine_site; + $sa_delta=$sa_delta_site; + $sa_delete=$sa_delete_site; + $sa_reject=$sa_reject_site; + $sa_forward=$sa_forward_site; + $sa_fwd_verbose=$sa_fwd_verbose_site; + $sa_hdr_report=$sa_hdr_report_site; + $smaildir=$smaildir_site; +} + +sub settings_pd { + my ($match_hdr,$match_rcpt,$domain_settings)=@_; + my ($scanners_rcpt); + + ($scanners_rcpt,$sa_subject,$sa_quarantine,$sa_delta,$sa_delete,$sa_reject,$sa_forward,$sa_fwd_verbose,$sa_hdr_report,$smaildir)=split(/'/,$domain_settings); + $sa_subject="" if ($sa_subject eq "none"); + $smaildir=untaint($smaildir); # st: suggested by P-O Yliniemi <peo - bsd-guide.net> + $sa_forward=untaint($sa_forward); # st: sa_forward must be untainted too, thanks to Tomas Charvat <tc - excello.cz> + + &debug("s_p_d: $match_hdr match '$match_rcpt', settings '$sa_subject,$sa_quarantine,$sa_delta,$sa_delete,$sa_reject,$sa_forward,$sa_fwd_verbose,$sa_hdr_report,$smaildir'"); + + @scanner_array=split(/,/,$scanners_rcpt); + + &debug("s_p_d: $match_hdr match '$match_rcpt', scanners '$scanners_rcpt'"); + &minidebug("s_p_d: $match_hdr match '$match_rcpt', scanners '$scanners_rcpt'") if ($match_hdr !~ /m_rcpt/); +} + +sub settings_p_d { + my (%domain_settings,%seen,$scanners_array,$scanners_rcpt,$domain_settings); + + &debug("s_p_d: reading from $settings_per_domain.db"); + tie (%domain_settings,'DB_File',"$settings_per_domain.db",O_RDONLY, 0600, $DB_HASH) || &error_condition("cannot open $settings_per_domain.db - $!"); + + # Check if we have a match within the database + # Check order: + # 1) return-path + # 2) domain-return-path + # 3) for each recipient: recipient, domain-recipient + if ((exists $domain_settings{$returnpath}) && $QS_RELAYCLIENT) { + &settings_pd ("return-path",$returnpath,$domain_settings{$returnpath}); + } + elsif ((exists $domain_settings{$domain_returnpath}) && $QS_RELAYCLIENT) { + &settings_pd ("domain-return-path",$domain_returnpath,$domain_settings{$domain_returnpath}); + } + elsif ($one_recip && (exists $domain_settings{$one_recip})) { + &settings_pd ("rcpt",$one_recip,$domain_settings{$one_recip}); + } + elsif ($one_recip && (exists $domain_settings{$domain_one_recip})) { + &settings_pd ("domain_rcpt",$domain_one_recip,$domain_settings{$domain_one_recip}); + } + elsif (!$one_recip) { + &debug("s_p_d: we have multiple recipient, checking each of them"); + &minidebug("s_p_d: we have multiple recipient, checking each of them"); + my @mrecips=split(',',$recips); + my $mrcpt=; + my $domain_mrcpt=; + my %m_rcpt; + foreach $mrcpt(@mrecips) { + $mrcpt=tolower($mrcpt); + $domain_mrcpt=$mrcpt; + $domain_mrcpt=~ s/^(.*)\@(.*)$/$2/; + if (exists $domain_settings{$mrcpt}) { + &settings_pd ("m_rcpt",$mrcpt,$domain_settings{$mrcpt}); + } + elsif (exists $domain_settings{$domain_mrcpt}) { + &settings_pd ("domain-m_rcpt",$domain_mrcpt,$domain_settings{$domain_mrcpt}); + } else { + @scanner_array=@scanners_default; + &sa_defaults; + } + @scanner_array=&check_scanners(@scanner_array); + $scanners_rcpt=join(',',@scanner_array); + $domain_settings="$scanners_rcpt'$sa_subject'$sa_quarantine'$sa_delta'$sa_delete'$sa_reject'$sa_forward'$sa_fwd_verbose'$sa_hdr_report'$smaildir"; + $m_rcpt{$mrcpt}=$domain_settings; + } + untie %domain_settings; + while( ($one_recip,$scanners_array)=each %m_rcpt) { + &settings_pd ("rcpt",$one_recip,$scanners_array); + &start_scanners($env_returnpath,"T$one_recip\0\0","$scandir/$wmaildir/new/$file_id"); + # st: maybe I had to change this if I will ever do 'sa' per user config... + # if an user on a multiples recipients mail has a very low sa_delete... It could + # be rare, but it could be. What to do? + # If sa_hits doesn't exist, the mail has a virus marked to delete, + # but if the mail was rejected this check won't be reached... + last if ($del_message == 1); + } + return; + } else { + @scanner_array=@scanners_default; + &sa_defaults; + &debug("s_p_d: no match, default sa_settings '$sa_quarantine,$sa_delta,$sa_delete,$sa_reject,$sa_forward,$sa_fwd_verbose,$sa_hdr_report,$smaildir'"); + &debug("s_p_d: no match, falling to settings_default"); + &minidebug("s_p_d: no match, falling to settings_default"); + } + # if no multiples recipients + untie %domain_settings; + @scanner_array=&check_scanners(@scanner_array); + &start_scanners($env_returnpath,$env_recips,"$scandir/$wmaildir/new/$file_id"); +} + +sub generate_spd { + my ($line,$count,%domain_settings,$match_rcpt,$scanners_rcpt,@scanners_rcpt_array,%seen); + my ($domain_settings,$sa_subject_ignore); + + print "\n Generating $settings_per_domain.db\n\n"; + + unlink ("$settings_per_domain.db.tmp"); + tie (%domain_settings,'DB_File',"$settings_per_domain.db.tmp",O_CREAT|O_RDWR,0640,$DB_HASH) || &error_condition("cannot open for write $settings_per_domain.db.tmp - $!"); + + open(SPD, "<$settings_per_domain.txt") || &error_condition("cannot read $settings_per_domain.txt - $!"); + + while (<SPD>) { + $line++; + next if (/^\#|^\s.*$/); # Ignore lines starting with # or spaces + next if (!(/:/)); # Ignore lines doesn't contain a ':' + # if (/\;|\!/) { + if (/\;/) { + print "d_w: line $line contains an invalid char, SKIP\n"; + next; + } + chomp; + # sa_subject could has spaces ... (from P-O Yliniemi) + $sa_subject = (split(/'/,$_))[1]; + s/\s|\t//g; + ($match_rcpt,$domain_settings)=split(/:/,$_,2); + $match_rcpt=tolower("$match_rcpt"); + # $domain_settings=tolower("$domain_settings"); + ($scanners_rcpt,$sa_subject_ignore,$sa_quarantine,$sa_delta,$sa_delete,$sa_reject,$sa_forward,$sa_fwd_verbose,$sa_hdr_report,$smaildir)=split(/'/,$domain_settings); + + if (exists $domain_settings{$match_rcpt}) { + print " d_w: duplicated value '$match_rcpt' at line $line, SKIP \n"; + next; + } + + $sa_subject=$sa_subject_site if (!$sa_subject); + $sa_quarantine=$sa_quarantine_site if (!$sa_quarantine && $sa_quarantine ne "0"); + $sa_delta=$sa_delta_site if (!$sa_delta && $sa_delta ne "0"); + $sa_delete=$sa_delete_site if (!$sa_delete && $sa_delete ne "0"); + $sa_reject=$sa_reject_site if (!$sa_reject && $sa_reject ne "0"); + $sa_forward=$sa_forward_site if (!$sa_forward); + $sa_fwd_verbose=$sa_fwd_verbose_site if (!$sa_fwd_verbose && $sa_fwd_verbose ne "0"); + $sa_hdr_report=$sa_hdr_report_site if (!$sa_hdr_report && $sa_hdr_report ne "0"); + $smaildir=$smaildir_site if (!$smaildir); + + # Control the values of sa_delete and sa_quarantine + if ($sa_delete && ($sa_quarantine>$sa_delete)) { + print " d_w: WARNING, sa_delete lower than sa_quarantine, for address '$match_rcpt' at line $line\n"; + print " resetting sa_delete to '0', spam could be quarantined, but not deleted for this address\n"; + $sa_delete='0'; + } + + # Let check if the scanner are really installed, + # change 'sa' and 'ps' for the correct name, and + # add _scanner to the AVs scanners + + @scanners_rcpt_array=split(/,/,$scanners_rcpt); + foreach (@scanners_rcpt_array) { + s/^sa$/spamassassin/; + s/^ps$/perlscan/; + s/^perlscanner$/perlscan/; + s/^(.*)$/$1_scanner/ if((!/spamassassin/) && (!/_scanner/) && (!/^none$/)); + } + + # Check if the scanners are installed + @scanners_rcpt_array=&check_scanners(@scanners_rcpt_array); + + $scanners_rcpt = join(',',@scanners_rcpt_array); + + # Check if at least we have one valid scanner + + if (@scanners_rcpt_array==0) { + print " d_w: There are no valid scanners for address '$match_rcpt' at line $line, SKIP\n"; + next; + } + $count++; + + $domain_settings="$scanners_rcpt'$sa_subject'$sa_quarantine'$sa_delta'$sa_delete'$sa_reject'$sa_forward'$sa_fwd_verbose'$sa_hdr_report'$smaildir"; + + $domain_settings{$match_rcpt}=$domain_settings; + } + close(SPD); + untie %domain_settings; + rename( "$settings_per_domain.db.tmp", "$settings_per_domain.db" ); + print "\n Read $line lines, got $count entries\n\n"; + if (!$settings_pd) { + print "\n WARNING: settings_per_domain is not enabled\n\n The database has been generated but\n"; + print " it won't be used until 'settings_per_domain' will be enabled\n\n"; + } +} + +sub read_spd { + # st: display the database sorted by domains. + + my ($count,%domain_settings,$scanners_rcpt,$TXT,$spd_orig); + my (%sorted,$userpart,$domainpart,$last_domain,$fs); + $count=0; + + if ($opt_d) { + $fs="\t"; + print "\n# Reading from $settings_per_domain.db\n#\n"; + $TXT="STDOUT"; + } else { + $fs="\n#"; + $spd_orig = "$settings_per_domain.txt."; + $spd_orig .= sprintf "%02d%02d%02d.%02d%02d%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec; + print "\n Sorting file '$settings_per_domain.txt'\n\n A copy of the current settings will be saved in:\n"; + print " $spd_orig\n\n"; + if ( (stat("$settings_per_domain.txt"))[9] > (stat("$settings_per_domain.db"))[9] ) { + print "\n WARNING: file '$settings_per_domain.txt' is newer\n"; + print " than database '$settings_per_domain.db'\n"; + print " this could be wrong or not, anyway a copy of the current\n"; + print " 'txt file' has been saved, see above.\n\n"; + } + rename("$settings_per_domain.txt", "$spd_orig"); + open(SPD, ">$settings_per_domain.txt") || &error_condition("cannot open for write $settings_per_domain.txt - $!"); + print SPD "#\n# File settings_per_domain.txt sorted by qmail-scanner-st\n#\n"; + $TXT="SPD"; + } + + print $TXT "# Read the documetation at:\n"; + print $TXT "# http://toribio.apollinare.org/qmail-scanner/settings_per_domain.html\n#\n"; + + if ($opt_s) { + print $TXT "\n######### WIDE SITE SETTINGS\n"; + print $TXT "# scanners_installed = @scanners_installed\n"; + print $TXT "# scanners_default = @scanners_default\n"; + print $TXT "# sa_subject_site = '$sa_subject_site'\n"; + print $TXT "# sa_quarantine_site = $sa_quarantine_site$fs sa_delta_site = $sa_delta_site\n"; + print $TXT "# sa_delete_site = $sa_delete_site$fs sa_reject_site = $sa_reject_site\n"; + print $TXT "# sa_forward_site = '$sa_forward_site'$fs sa_fwd_verbose_site= $sa_fwd_verbose_site\n"; + print $TXT "# sa_hdr_report_site = $sa_hdr_report_site$fs smaildir_site = $smaildir_site\n\n"; + } + + tie (%domain_settings,'DB_File',"$settings_per_domain.db",O_RDONLY, 0600, $DB_HASH) || &error_condition("cannot open for read $settings_per_domain.db - $!");; + + # st: let sort the match_rpt + foreach (keys %domain_settings) { + if ( $_ =~ /\@/) { + ($userpart,$domainpart) = split (/\@/,$_); + $sorted{"$domainpart.$userpart"} = $_; + } else { + $sorted{$_} = $_; + } + } + + foreach(sort keys %sorted) { + $count++; + ($userpart,$domainpart) = split (/\@/,$sorted{$_}); + if ( $sorted{$_} !~ /\@/ ) { + print $TXT "\n######### DOMAIN\t'$sorted{$_}'\n" ; + $last_domain=$domainpart=$sorted{$_}; + } + print $TXT "\n######### DOMAIN\t'$domainpart'\n" if ( $domainpart ne $last_domain ); + $last_domain=$domainpart; + ($scanners_rcpt,$sa_subject,$sa_quarantine,$sa_delta,$sa_delete,$sa_reject,$sa_forward,$sa_fwd_verbose,$sa_hdr_report,$smaildir)=split(/'/,$domain_settings{$sorted{$_}}); + print $TXT "\n## $count. Settings for\t'$sorted{$_}'\n" if ($opt_d) ; + print $TXT "$sorted{$_} : $domain_settings{$sorted{$_}}\n"; + if ($opt_d) { + print $TXT "\n# scanners = $scanners_rcpt\n"; + print $TXT "# sa_subject = '$sa_subject'\n"; + print $TXT "# sa_quarantine = $sa_quarantine\tsa_delta = $sa_delta\n"; + print $TXT "# sa_delete = $sa_delete\tsa_reject = $sa_reject\n"; + print $TXT "# sa_forward = '$sa_forward'\tsa_fwd_verbose = $sa_fwd_verbose\n"; + print $TXT "# sa_hdr_report = $sa_hdr_report\tsmaildir = $smaildir\n\n"; + } + } + + print $TXT "\n######### WIDE SITE SETTINGS\n"; + print $TXT "# scanners_installed = @scanners_installed\n"; + print $TXT "# scanners_default = @scanners_default\n"; + print $TXT "# sa_subject_site = '$sa_subject_site'\n"; + print $TXT "# sa_quarantine_site = $sa_quarantine_site$fs sa_delta_site = $sa_delta_site\n"; + print $TXT "# sa_delete_site = $sa_delete_site$fs sa_reject_site = $sa_reject_site\n"; + print $TXT "# sa_forward_site = '$sa_forward_site'$fs sa_fwd_verbose_site= $sa_fwd_verbose_site\n"; + print $TXT "# sa_hdr_report_site = $sa_hdr_report_site$fs smaildir_site = $smaildir_site\n\n"; + if ($opt_d) { + print $TXT "# Run '/var/qmail/bin/qmail-scanner-queue.pl -p' to generate the db\n"; + print $TXT "# If you have redirect the output of this command to settings_per_domain.txt\n"; + print $TXT "\n# d_w: total of $count entries found\n\n\n"; + } + if (!$settings_pd) { + print "\n WARNING: settings_per_domain is not enabled\n\n The database won't be used\n"; + print " until 'settings_per_domain' will be enabled\n\n"; + } + + close(SPD) if ($opt_s); + untie %domain_settings; +} + + +sub check_scanners { + # Check against the installed scanners + my @scanners_to_check=@_; + return @scanners_to_check if ($scanners_to_check[0] eq "none"); + my %seen=(); + foreach (@scanners_installed) { + $seen{$_}=1; + } + + @scanners_to_check=grep($seen{$_},@scanners_to_check); + return @scanners_to_check; +} + +sub untaint { + # st: suggested by P-O Yliniemi <peo - bsd-guide.net> + my($var) = @_; + if ($var =~ /^(.*)$/) { + $var = $1; + } + return $var; +} + +################################################# +# END of subroutines added by ST +################################################# + + +sub clamdscan_scanner { + #Clamdscan scanner + &debug("clamdscan: starting scan of directory \"$ENV{'TMPDIR'}\"..."); + + my ($start_clamdscan_time)=[gettimeofday]; + my ($DD,$clamdscan_status,$eclamdscan_status,$stop_clamdscan_time,$clamdscan_time); + my ($clamdscan_verbose); + $clamdscan_verbose="-v" if ($DEBUG); + + &debug("run $clamdscan_binary $clamdscan_options $ENV{'TMPDIR'} 2>&1"); + + $DD=`$clamdscan_binary $clamdscan_options $ENV{'TMPDIR'} 2>&1`; + $clamdscan_status=$?; + $eclamdscan_status=($clamdscan_status >> 8); + + &debug("--output of clamdscan was:\n$DD--"); + + if ( $eclamdscan_status > 0) { + if ($eclamdscan_status == 1 && $DD =~ /\:\s(.*)\sFOUND$/m) { + $quarantine_description=$+; + &debug("There be a virus! ($quarantine_description)"); + &minidebug("clamdscan: there be a virus! ($quarantine_description)"); + &eventlog("CLAMAV:$quarantine_description"); + ($quarantine_event=$quarantine_description)=~s/\s/_/g; + $quarantine_event="CLAMDSCAN:".substr($quarantine_event,0,$QE_LEN); + $description .= "\n---clamdscan results ---\n$DD"; + } elsif ($eclamdscan_status == 2 && $DD =~ /module failure/) { + #This is OK, corrupt files are let through + } else { + #This implies a corrupt set of DAT files or resource problems... + &error_condition("clamdscan: corrupt or unknown clamd scanner error or memory/resource/perms problem - exit status $clamdscan_status/$eclamdscan_status"); + } + } else { + if ($DD =~ /Recursion limit exceeded/) { + $quarantine_description="Resource attack - $1"; + &debug("clamdscan: $quarantine_description"); + &minidebug("clamdscan: $quarantine_description"); + $quarantine_event="CLAMDSCAN:Resource_attack"; + &eventlog("CLAMAV:$quarantine_description"); + $description .= "\n---clamdscan results ---\n$DD"; + } elsif ($clamdscan_status > 0) { + #This implies a corrupt set of DAT files or resource problems... + &error_condition("clamdscan: corrupt or unknown clamd scanner error or memory/resource/perms problem - exit status $clamdscan_status/$eclamdscan_status"); + } + } + #Bugs in clamdscan sometimes shows up as zero output. Always error on such conditions + $DD=~s/\n//g; + if ($DD eq "" ) { + &error_condition("clamdscan: corrupt or unknown clamd scanner error or memory/resource/perms problem - exit status $clamdscan_status/$eclamdscan_status, but no output!"); + } + + $stop_clamdscan_time=[gettimeofday]; + $clamdscan_time = tv_interval ($start_clamdscan_time, $stop_clamdscan_time); + &debug("clamdscan: finished scan of dir \"$ENV{'TMPDIR'}\" in $clamdscan_time secs"); + &minidebug("clamdscan: finished scan in $clamdscan_time secs"); +} + +sub spamassassin { + my($scanned)=@_ ; + + $scanned='0' if ( $scanned != 1 ); + + #Only run SA if mail is from a "remote" SMTP client, or QS_SPAMASSASSIN + #is defined via tcpserver... + if ($QS_RELAYCLIENT && !defined($ENV{'QS_SPAMASSASSIN'})) { + &debug("spamassassin: don't scan as RELAYCLIENT implies this was sent by a local user"); + &minidebug("SA: don't scan as RELAYCLIENT implies this was sent by a local user") if (!$scanned); + return; + } + if ( $SA_SKIP_MD ne "0" && $returnpath eq "" && $headers{'from'} =~ /mailer-daemon|postmaster|bounce/i ) { + &debug("SA: skipping message from MAILER-DAEMON"); + &minidebug("SA: skipping message from MAILER-DAEMON") if (!$scanned); + return; + } + + #SpamAssassin client scanner + #my ($spamassassin_found,$spamassassin_status); + my ($spamassassin_status); + my ($start_spamassassin_time)=[gettimeofday]; + my ($sa_tag,$DD,$stop_spamassassin_time,$spamassassin_time,$cmdline_recip,$spamc_options); + my ($sa_status)=0; + my ($sa_score)=0; my ($sa_required_hits)=0; + ($sa_comment,$sa_level)=(,); + + if ($msg_size > 250000) { + &debug("spamassassin: message too big - skip it"); + &minidebug("SA: message too big ($msg_size) - skip it"); + $sa_score=$required_hits="?"; + $tag_sa_score = "SA:0($sa_score/$required_hits):"; + $sa_comment = "No, hits=$sa_score required=$required_hits"; + return; + } + + $spamc_options=' -c ' if ($sa_fast); + + if ($sa_sql) { + #Cleanup $one_recip so it's usable from the commandline... + #any char that isn't supported to changed into an '_' + ($cmdline_recip=$one_recip)=~s/[^0-9a-z\.\_\-\=\+\@]/_/gi; + $cmdline_recip=~/^([0-9a-z\.\_\-\=\+\@]+)$/i; + $cmdline_recip=tolower($1); + $spamc_options="$spamc_options -u \"$cmdline_recip\"" if ($cmdline_recip ne ""); + } + + &debug("SA: run $spamc_binary $spamc_options < $scandir/$wmaildir/new/$file_id"); + open(SIN,"<$scandir/$wmaildir/new/$file_id")||&error_condition("cannot open $scandir/$wmaildir/new/$file_id - $!"); + open(SOUT,"|$spamc_binary $spamc_options > $scandir/$wmaildir/new/$file_id.spamc")||&error_condition("cannot open for write $scandir/$wmaildir/new/$file_id.spamc - $!"); + + print SOUT "X-Envelope-From: $headers{'MAILFROM'}\n"; + while (<SIN>) { + print SOUT; + } + close(SIN)||&error_condition("cannot close $scandir/$wmaildir/new/$file_id - $!"); + close SOUT; + $spamassassin_status=($? >> 8); + $sa_status=$spamassassin_status if ($sa_fast); + open(SA,"<$scandir/$wmaildir/new/$file_id.spamc")||&error_condition("cannot open for read $scandir/$wmaildir/new/$file_id.spamc - $!"); + while (<SA>) { + if ($sa_fast) { + chomp; + ($sa_score,$required_hits)=split(/\//,$_,2); + $sa_tag++; + last; + } else { + #X-Spam-Status: No, score=2.8 required=5.0 + if (/^X-Spam-Status: (Yes|No), (hits|score)=(-?[\d\.]*) required=([\d\.]*)/) { + $sa_tag++; + $sa_status=1 if ($1 eq "Yes"); + $sa_score=$3;$required_hits=$4; + } + } + } + close SA ; + + if (!$sa_fast && -s "$scandir/$wmaildir/new/$file_id.spamc" && $spamassassin_status == 0) { + &debug("SA: overwriting $scandir/$wmaildir/new/$file_id with $scandir/$wmaildir/new/$file_id.spamc"); + rename ("$scandir/$wmaildir/new/$file_id.spamc","$scandir/$wmaildir/new/$file_id"); + } else { + unlink("$scandir/$wmaildir/new/$file_id.spamc"); + } + + # st: new routine to avoid duplicate code, so a shorter code... + &check_sa_score($sa_score,$start_spamassassin_time,$scanned); +} + +################################################# +# Spamassassin subroutine added by ST +################################################# + +sub spamassassin_alt { + # st: Alternative routine for spamassassin, lighter and can logs the report... + my($scanned)=@_ ; + + $scanned='0' if ( $scanned != 1 ); + + #Only run SA if mail is from a "remote" SMTP client, or QS_SPAMASSASSIN + #is defined via tcpserver... + if ($QS_RELAYCLIENT && !defined($ENV{'QS_SPAMASSASSIN'})) { + &debug("spamassassin: don't scan as RELAYCLIENT implies this was sent by a local user"); + &minidebug("SA: don't scan as RELAYCLIENT implies this was sent by a local user") if (!$scanned); + return; + } + if ( $SA_SKIP_MD ne "0" && $returnpath eq "" && $headers{'from'} =~ /mailer-daemon|postmaster|bounce/i ) { + &debug("SA: skipping message from MAILER-DAEMON"); + &minidebug("SA: skipping message from MAILER-DAEMON") if (!$scanned); + return; + } + + #SpamAssassin client scanner + my ($start_spamassassin_time)=[gettimeofday]; + my ($spamc_options,$sa_tag,$spamassassin_status,$sa_score,$stop_spamassassin_time,$spamassassin_time); + my ($sa_status)=0; + ($sa_score,$required_hits)=('0','0'); + ($sa_comment,$sa_level)=(,); + $sa_report=; + $sa_fast=1; + + if ($msg_size > 250000) { + &debug("spamassassin: message too big - skip it"); + &minidebug("SA: message too big - skip it"); + $sa_score=$required_hits="?"; + $tag_sa_score = "SA:0($sa_score/$required_hits):"; + $sa_comment = "No, hits=$sa_score required=$required_hits"; + return; + } + + if ( $sa_debug eq "1" ) { + $spamc_options=" -R "; + } else { + $spamc_options=" -c "; + } + + if ($sa_sql) { + my ($cmdline_recip); + ($cmdline_recip=$one_recip)=~s/[^0-9a-z\.\_\-\=\+\@]/_/gi; + $cmdline_recip=~/^([0-9a-z\.\_\-\=\+\@]+)$/i; + $cmdline_recip=tolower($1); + $spamc_options="$spamc_options -u \"$cmdline_recip\"" if ($cmdline_recip ne ""); + } + + open(SA,"$spamc_binary $spamc_options < $scandir/$wmaildir/new/$file_id|")||&error_condition("cannot run $spamc_binary < $scandir/$wmaildir/new/$file_id - $!"); + while (<SA>) { + if (!$sa_tag) { + chomp; + ($sa_score,$required_hits)=split(/\//,$_,2); + # Clean some invalid returns from SA v.2.5x + $required_hits =~ s/\r//g; + chomp $required_hits; + $sa_tag=1; + next; + } + + if ( $sa_tag<2 ) { + $sa_tag=2 if (/^---- ---------------------- --------------------------------------------------$/); + next; + } + + $sa_report .= " $_" if ( !/^$/ || !/^\s$/ ); + } + + # Clean some invalid returns from SA v.2.5x + $sa_report =~ s/\r/\n/g; + chomp $sa_report; + $sa_report = if ($sa_report =~ /\n\n/ ); + + $spamassassin_status=($? >> 8); + $sa_status=$spamassassin_status if ($sa_fast); + + close SA ; + + # st: new routine to avoid duplicate code, so a shorter code... + &check_sa_score($sa_score,$start_spamassassin_time,$scanned); +} + +sub check_sa_score { + my ($sa_score,$start_spamassassin_time,$scanned)=@_ ; + my ($stop_spamassassin_time,$spamassassin_time); + + # st: if the variable SA_ONLYDELETE_HOST is set in the tcpserver + # don't reject messages coming from those IPs, just delete them + # You should set this variable for your secondary mail server. + if (defined($ENV{'SA_ONLYDELETE_HOST'}) || defined($ENV{'SA_WHITELIST'})) { + $sa_reject="0"; + &debug("WL: The server is a SA_ONLYDELETE_HOST, don't reject"); + &minidebug("WL: The server is a SA_ONLYDELETE_HOST, don't reject"); + } + + $sa_score='?' if (!$sa_score); + $required_hits='?' if (!$required_hits); + $sa_hits=$sa_score; + + &debug("SA: REPORT hits = $sa_score/$required_hits\n$sa_report") if ( $sa_debug && $sa_report ); + &minidebug("SA: REPORT hits = $sa_score/$required_hits\n$sa_report") if ( $sa_debug && $sa_report && !$scanned); + &eventlog("- - -:SCORE:REQ:QRTN:DEL:REJ"); + &eventlog("SPAM-RESULT:$sa_score:$required_hits:$sa_quarantine:$sa_delete:$sa_reject"); + + # st: what about SA sql per user, could be differents $required_hits... + if ($required_hits > $sa_score || ($sa_score == 0) || ($sa_score eq "\?")) { + $tag_sa_score = "SA:0($sa_score/$required_hits):"; + $sa_comment = "No, hits=$sa_score required=$required_hits"; + } else { + $tag_sa_score = "SA:1($sa_score/$required_hits):"; + $sa_comment = "Yes, hits=$sa_score required=$required_hits" if ($sa_fast); + + # If sa_quarantine/sa_delete are set, then compare them to the current score and + # quarantine/delete it if necessary, + # otherwise tag the message as spam. + + # Control the values of sa_delete and sa_quarantine + if ($sa_delete && ($sa_quarantine>$sa_delete)) { + &debug("SA: WARNING, sa_delete is lower than sa_quarantine, spam could be quarantined, but not deleted"); + &minidebug("SA: WARNING, sa_delete is lower than sa_quarantine, spam could be quarantined, but not deleted"); + &eventlog("---- WARN: sa_delete < sa_quarantine => setting sa_delete = 0"); + $sa_delete='0'; + } + + my $sa_threshold='0'; + + if ( $sa_delete && (($sa_delete+$required_hits)<$sa_score)) { + $sa_threshold=$sa_delete+$required_hits; + if ( $sa_reject && (($sa_delete_site+$required_hits)<$sa_score || $one_recip eq $recips )) { + &log_sa_action($scanned,$sa_threshold,"rejected"); + &eventlog("SPAM-DETECT:REJECT"); + $stop_spamassassin_time=[gettimeofday]; + $spamassassin_time = tv_interval ($start_spamassassin_time, $stop_spamassassin_time); + &debug("SA: finished scan of dir \"$ENV{'TMPDIR'}\" in $spamassassin_time secs"); + &minidebug("SA: finished scan in $spamassassin_time secs - hits=$sa_score/$required_hits"); + &reject_email("We have reasons to believe this mail is SPAM",31); + } else { + # st: mark the message to delete it, if it isn't already marked as virus to delete + # actually it is not possible that a marke message reach this point. I think.. + $del_message='2' if ($del_message ne "1"); + # st: maybe these three lines are useful for those who wants the 'log_details'... + # But if the message is rejected nothing remains + $destring="SPAM"; + $quarantine_description="SPAM exceeds \"delete\" threshold - hits=$sa_score/$required_hits"; + $quarantine_event="SA:SPAM-DELETED"; + &log_sa_action($scanned,$sa_threshold,"deleted"); + &eventlog("SPAM-DETECT:DELETE"); + $description .= "\n---spamassassin results ---\n$destring '$quarantine_description'\n found in message $ENV{'TMPDIR'}"; + } + } else { + if ( $sa_quarantine && (($sa_quarantine+$required_hits)<$sa_score)) { + $sa_threshold=$sa_quarantine+$required_hits; + $destring="SPAM"; + $quarantine_description="SPAM exceeds \"quarantine\" threshold - hits=$sa_score/$required_hits"; + $quarantine_event="SA:SPAM-QUARANTINED"; + $quarantine_spam="SA:SPAM-QUARANTINED"; + &log_sa_action($scanned,$sa_threshold,"quarantined"); + &eventlog("SPAM-DETECT:QUARANTINE"); + $description .= "\n---spamassassin results ---\n$destring '$quarantine_description'\n found in message $ENV{'TMPDIR'}"; + } else { + #st: if $spamc_subject and $sa_delta are set, add in the subject the spam-level + if ($sa_subject ne "" && $sa_delta) { + if ($sa_score < ($required_hits+$sa_delta)) { + $sa_subject .= " LOW * "; + } elsif ($sa_score > ($required_hits+(2 * $sa_delta))) { + $sa_subject .= " HIGH * "; + } else { + $sa_subject .= " MEDIUM * "; + } + } + &log_sa_action($scanned,$required_hits,"tagged"); + &eventlog("SPAM-DETECT:MARK"); + } + } + } + + if ($sa_score > 0) { + $sa_score=int($sa_score); + #Keep it RFC compliant + $sa_score=100 if ($sa_score > 100); + my $si=0; + $sa_level=; + if ($sa_fast || $sa_alt) { + while ($si < $sa_score) { + $si++; + $sa_level .= $sa_symbol; + } + } + } + + &debug("SA: required_hits $required_hits / sa_quarantine +$sa_quarantine / sa_delete +$sa_delete") if ($sa_quarantine || $sa_delete); + + if ($start_spamassassin_time) { + $stop_spamassassin_time=[gettimeofday]; + $spamassassin_time = tv_interval ($start_spamassassin_time, $stop_spamassassin_time); + + if ($scanned) { + &debug("SA: finished scan for $one_recip in $spamassassin_time secs - hits=$sa_hits/$required_hits"); + &minidebug("SA: finished scan for $one_recip in $spamassassin_time secs - hits=$sa_hits/$required_hits"); + } else { + &debug("SA: finished scan of dir \"$ENV{'TMPDIR'}\" in $spamassassin_time secs - hits=$sa_hits/$required_hits"); + &minidebug("SA: finished scan in $spamassassin_time secs - hits=$sa_hits/$required_hits"); + } + } +} + +sub log_sa_action { + # st: maybe I will need this routine for multiples recipients + my ($scanned,$sa_threshold,$sa_action)=@_; + if ( $scanned && $sa_action ne "rejected" ) { + &debug("SA: yup, this smells like SPAM - hits=$sa_hits/$required_hits/$sa_threshold - message $sa_action for $one_recip"); + &minidebug("SA: yup, this smells like SPAM - hits=$sa_hits/$required_hits/$sa_threshold - message $sa_action for $one_recip"); + } else { + &debug("SA: yup, this smells like SPAM - hits=$sa_hits/$required_hits/$sa_threshold - message $sa_action ..."); + &minidebug("SA: yup, this smells like SPAM - hits=$sa_hits/$required_hits/$sa_threshold - message $sa_action ..."); + } +} + +################################################# +# END of Spamassassin subroutines added by ST +################################################# + +######################### +## END of scanner definitions +## +######################### diff -Naur qmail-scanner-2.01st/qmail-scanner-queue.template qmail-scanner-2.01st-qms/qmail-scanner-queue.template --- qmail-scanner-2.01st/qmail-scanner-queue.template 2007-02-04 12:55:41.000000000 +0200 +++ qmail-scanner-2.01st-qms/qmail-scanner-queue.template 2007-09-09 09:28:53.000000000 +0300 @@ -7,6 +7,20 @@

# 
# Patch by: Salvatore Toribio <toribio - pusc.it>
#

+# Patched for Event Logging by: Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.22 - patched: st-qms - 20040530 +# +# Patched for Account Monitoring by Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.22 - patched: st-qms-monitor - 20040919 +# +# Patched for Version 1.24 and merge of qms-monitor functions +# by Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.24 - patched: st-qms - 20041102 +# +# Patched for Version 1.25 +# by Mark S. Teel <mteel@users.sourceforge.net> +# Version: 1.25 - patched: st-qms - 20050618 +#

# See the file README-st-patch for information about the patch
# This version deletes/rejects spam based in Chris Hine's patch for v1.16
#

@@ -112,6 +126,43 @@

#deciding whether or not to send recipient alerts to
my @local_domains_array=(LOCAL_DOMAINS_ARRAY);

+# qms: save local domains list string +my $local_domains_string="LOCAL_DOMAINS_ARRAY"; + + +######## qms-monitor: selective account monitoring/archiving +### +### Description: +### 1) qms-monitor will archive ALL email msgs SENT OR RECEIVED for +### any email address listed below +### 2) Messages are archived to $qms_monitor_home - they can be left +### there for manual examination, or a cron script can be run periodically +### to move them into a "monitor" email domain so that the mail can be +### partitioned into individual monitor domain accounts and read with +### any email client +######## + +my $qms_monitor_enabled='QMS_MONITOR'; + +### qms_monitor_array: add email addresses of local domains to be monitored +my @qms_monitor_array=(QMS_MON_ACCOUNTS); + +### qms_monitor_dest_array: add destination for email message copies +# Note 1: locations here will be saved underneath $qms_monitor_home; +# a cron job can later copy from that location to an alternate +# email domain used for account monitoring. +# Note 2: each entry in this array corresponds to the email address in the +# same location of the @qms_monitor_array above - i.e., +# @qms_monitor_array[2] msgs get stored at +# @qms_monitor_dest_array[2] - thus, ORDER DOES MATTER. +# Note 3: DO NOT include a leading "/" on these paths - they will typically +# be entries that ultimately belong in /home/vpopmail/domains - +# i.e., starting with the domain name. +### +my @qms_monitor_dest_array=(QMS_MON_DESTINATIONS); + +######## qms-monitor BLOCK END +

# Array of virus that we don't want to inform the sender of.
my @silent_viruses_array=(SILENT_VIRUSES_ARRAY);

@@ -174,6 +225,9 @@

#Name of file where quarantine reports go (for long-term storage)
my $quarantinelog="quarantine.log";

+# qms: Name of file where usable logs for analysis are written +my $eventlog="qms-events.log"; +

#Generate nice random filename
my ($sysname, $hostname, $release, $version, $machine) = uname();
#my $hostname='FQDN'; #could get via call I suppose...

@@ -191,6 +245,9 @@

#turn this on
my $log_crypto="LOG_CRYPTO";

+# qms-monitor - the root for temporary storage +my $qms_monitor_home = "$scandir/qms-monitor"; +

#Max size of message allowed to be scanned - 100Mbytes by default 
#DO NOT SET LOWER THAN 10Mbytes!!!!!
my $MAX_SCAN_SIZE=MAX_MSG_SIZE;

@@ -446,12 +503,15 @@

# the message size
my $MINIDEBUG='MINI_DEBUG';

+# qms: Want meaningful event logs? Enable this and read $scandir/qms-events.log +my $EVENTLOG='QMS_LOG'; +

my @uufile_list = ();
my @attachment_list = ();
my @zipfile_list = ();

#Want microsec times for debugging

-use Time::HiRes qw( usleep ualarm gettimeofday tv_interval ); +use Time::HiRes qw ( usleep ualarm gettimeofday tv_interval );

use POSIX;
use DB_File;

@@ -534,7 +594,9 @@

   $mimeunpacker_binary .= " --unique_names --no-ole --paranoid -i - -d $ENV{'TMPDIR'}/";
}

- +#Get current timestamp for logs +my ($sec,$min,$hour,$mday,$mon,$year,$nowtime); +($sec,$min,$hour,$mday,$mon,$year) = localtime(time);

my ($smtp_sender,$remote_smtp_ip,$remote_smtp_auth,$real_uid,$effective_uid);

$real_uid=$<;

@@ -562,9 +624,27 @@

  &minidebug("+++ starting debugging for process $$ (ppid=$nppid) by uid=$real_uid");
}

+# qms: open the event log if enabled +if ($EVENTLOG ) { + open(ELOG,">>$scandir/$eventlog"); + select(ELOG);$|=1; + my $starttime = strftime("%F %H:%M:%S", localtime(time)); + &eventlog("------ START MSG $starttime ------"); +} +

# st: if sa_alt or sa_debug are '0', sa_hdr_report_site must be 0
$sa_hdr_report_site='0' if ( !$sa_alt || !$sa_debug );

+# st: if the variable SA_ONLYDELETE_HOST is set in the tcpserver +# don't reject messages coming from those IPs, just delete them +# You should set this variable for your secondary mail server. +if (defined($ENV{'SA_ONLYDELETE_HOST'}) || defined($ENV{'SA_WHITELIST'})) { + $sa_reject="0"; + &debug("WL: The server is a SA_ONLYDELETE_HOST, don't reject"); + &minidebug("WL: The server is a SA_ONLYDELETE_HOST, don't reject"); +} + +

# st: if the variable BMC_WHITELIST is set in the tcpserver
# don't search for 'bad mime characters' in the headers of messages
# coming from those IPs.

@@ -624,6 +704,7 @@

  }
  $tag_score="RC:1($remote_smtp_ip):" if ($QS_RELAYCLIENT);
  &debug("incoming SMTP connection from $smtp_sender");

+ &eventlog("CONNECT-SMTP:$ENV{'TCPREMOTEIP'}");

  #system("/usr/bin/printenv > /tmp/qmail-scanner.env");
  # st: do not reject mails from localhost useful for fetchmail
  $sa_reject="0" if ($remote_smtp_ip eq "127.0.0.1");

@@ -634,6 +715,7 @@

  $QS_RELAYCLIENT=1;
  $tag_score="RC:1($remote_smtp_ip):"; #Always would be relayed
  &debug("incoming pipe connection from $smtp_sender");

+ &eventlog("CONNECT-PIPE:$$");

  # st: do not reject mails from localhost useful for fetchmail
  $sa_reject="0";
}

@@ -659,6 +741,7 @@

  &grab_envelope_hdrs;
  &debug("from=$headers{'from'},subj=$headers{'subject'}, $qsmsgid=$headers{$qsmsgid} $smtp_sender");
  &minidebug("from='$headers{'from'}', subj='$headers{'subject'}', $smtp_sender");

+ &eventlog("HEADER:$headers{'from'}:$headers{'to'}:$headers{'subject'}");

  ##### st: variables for settings per domain
  $returnpath=tolower($returnpath);

@@ -684,11 +767,13 @@

    if ($skip_text_msgs && ($indicates_attachments < 2) && !@uufile_list && !@attachment_list) {
      &debug("This is a PLAIN text message (because it's either not mime, or is text/plain), skip virus scanners - but not antispam scanners");
      &minidebug("This is a PLAIN text message, skip virus scanners - but not SA");

+ &eventlog("TYPE:PLAIN");

      $plain_text_msg=1;
    }
  }
  if ($headers{'MAILFROM'} eq "" || $headers{'subject'} =~ /Returned mail:|Mail Transaction Failed/) {
    &debug("This is a bounce message - better assume there's an attachment in it");

+ &eventlog("TYPE:MIXED");

    $plain_text_msg=0;
  }

@@ -753,6 +838,8 @@

# st: write to the log the end of the process
&close_log;

+&eventlog("SCANTIME:",tv_interval ($start_time, [gettimeofday]),""); +&eventlog("------ STOP MSG ---------------------------");

exit 0;

############################################################################

@@ -800,6 +887,8 @@

  #$nowtime = sprintf "%02d/%02d/%02d %02d:%02d:%02d", $mday, $mon+1, $year+1900, $hour, $min, $sec;
  &debug("error_condition: $V_HEADER-$VERSION: $string");
  &minidebug("error_condition: $V_HEADER-$VERSION: $string");

+ &eventlog("ERROR:$V_HEADER-$VERSION:$string"); + close(ELOG);

  &cleanup;
  &close_log;
  exit $errcode;

@@ -810,6 +899,91 @@

  print LOG "$dnowtime:$nprocess: ",@_,"\n" if ($DEBUG);
}

+# qms: log events to the file +sub eventlog { + my $enowtime = sprintf "%10d", time; + print ELOG "$enowtime:$$:",@_,"\n" if ($EVENTLOG); +} + +######## qms-monitor BLOCK BEGIN +# qms-monitor: Entry point called prior to requeueing the msg +sub qms_monitor +{ + my($msg) = @_; + my($acct) = ; + my($aindex) = '0'; + + foreach $acct (@qms_monitor_array) + { + # check the sender address first + if ($returnpath =~ /$acct/i) + { + &qms_monitor_save($acct,$msg,"@qms_monitor_dest_array[$aindex]"); + $aindex += 1; + next; + } + + if ($recips =~ /$acct/i) + { + &qms_monitor_save($acct,$msg,"@qms_monitor_dest_array[$aindex]"); + } + + $aindex += 1; + } +} + +# qms-monitor: save the msg to our archive location +sub qms_monitor_save +{ + my($qmsacct,$src,$dest) = @_; + my($finaldest) = "$qms_monitor_home/$dest"; + my($fname) = &qms_monitor_get_filename($qmsacct); + + if (!open(INMSG, "<$src")) + { + &eventlog("--- qms_monitor_save: unable to open src $src"); + &debug ("qms_monitor_save: unable to open src $src\n"); + return; + } + + if (! -d "$finaldest") + { + if (system("mkdir -p $finaldest")) + { + &eventlog("--- qms_monitor_save: unable to mkdir $finaldest"); + &debug ("qms_monitor_save: unable to mkdir $finaldest"); + return; + } + } + + + if (!open(OUTMSG, ">$finaldest/$fname")) + { + &eventlog("--- qms_monitor_save: unable to open dest $finaldest/$fname"); + &debug ("qms_monitor_save: unable to open dest $finaldest/$fname\n"); + return; + } + + while (<INMSG>) + { + print OUTMSG; + } + + close(OUTMSG); + close(INMSG); +} + +# qms-monitor: Generate meaninful file names +sub qms_monitor_get_filename +{ + my($aname) = @_; + my($stime) = strftime("%F_%H:%M:%S", localtime(time)); + + return "$aname" . "_" . "$hostname" . "_" . "$stime" . "_" . $$; +} + +######## qms-monitor BLOCK END +

sub working_copy {
  my ($hdr,$last_hdr,$value,$num_of_headers,$last_header,$last_value,$attachment_filename);
  select(STDIN); $|=1;

@@ -835,6 +1009,7 @@

	$illegal_mime=1;
	&debug("w_c: found CRL/NULL in header - invalid if this is a MIME message");
        &minidebug("w_c: found CRL/NULL in header - invalid if this is a MIME message");

+ &eventlog("QMSWC:BAD_HDR_CHARS");

      }
      #Put headers into array
      if (/^\s+(.*)$/ && $last_hdr) {

@@ -851,6 +1026,7 @@

	  $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";
	  &debug("w_c: disallowed breakage found in header name ($_) - not valid email");
	  &minidebug("w_c: disallowed breakage found in header name ($_) - not valid email");

+ &eventlog("QMSWC:BAD_HDR_BREAKAGE");

	  #next;
	} else {
	  /^([^\s]+):(.*)$/;

@@ -868,6 +1044,7 @@

	    $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";
	    &debug("w_c: $quarantine_description");
	    &minidebug("w_c: $quarantine_description");

+ &eventlog("QMSWC:BAD_HDR_MIME");

	  }
	  $num_of_headers++;
	}

@@ -953,6 +1130,7 @@

	    &minidebug("w_c: $quarantine_description");
	    $quarantine_event="Policy:Bad_MIME_Type";
	    $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";

+ &eventlog("QMSWC:BAD_MIME_CONTENT");

	  }
	#}
	#if ( $headers{'content-type'} =~ /boundary(\s*)=(|\s+|\s*\")([^\"\;]+)($|\;|\")/i) {

@@ -965,6 +1143,7 @@

	    #$quarantine_description="Disallowed MIME boundary found - potential virus";
	    #$quarantine_event="Policy:Bad_MIME_Boundary";
	    #$description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";

+ #&eventlog("QMSWC:BAD_MIME_BOUNDARY");

	  #}
	  if (!$quarantine_event && $headers{'mime-version'} ne "" && $BAD_MIME_CHECKS > 1 && ( length($BOUNDARY{$attachment_counter}) == 0 || length($BOUNDARY{$attachment_counter}) > 250)) {
	    #RFC2046 says boundarys are 1-70 chars - making it 250 is being *real* liberal...

@@ -974,6 +1153,7 @@

	    &debug($quarantine_description);
	    $quarantine_event="Policy:Bad_MIME_Length";
	    $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";

+ &eventlog("QMSWC:BAD_MIME_BOUNDARY");

	  }
	  #Strip off stuff after semicolon, and escape any odd chars
	  $BOUNDARY{$attachment_counter} =~ s/(\"|\;).*$//g;

@@ -1016,6 +1196,7 @@

	if (!$quarantine_event && $BAD_MIME_CHECKS > 1) {
	  &debug("w_c: Disallowed MIME filename manipulation - potential virus");
	  &minidebug("w_c: Disallowed MIME filename manipulation - potential virus");

+ &eventlog("QMSWC:BAD_MIME_FILENAME");

	  $illegal_mime=1;
	  $destring="LOCALE_destring_problem";
	  $quarantine_description='Disallowed MIME filename manipulation - not valid email';

@@ -1068,6 +1249,7 @@

	  &minidebug("w_c: $quarantine_description");
	  $quarantine_event="Policy:Bad_MIME_Boundary";
	  $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";

+ &eventlog("QMSWC:BAD_MIME_BOUNDARY");

	}
	if ( !$quarantine_event && $headers{'mime-version'} ne "" && $BAD_MIME_CHECKS > 1 && $BOUNDARY{$attachment_counter} =~ /^($BOUNDARY_REGEX)$/i) {
	  &debug("w_c: hmm, a new boundary defintion that has already being set. Sounds like a trojan");

@@ -1081,6 +1263,7 @@

	  &minidebug("w_c: $quarantine_description");
	  $quarantine_event="Policy:Bad_MIME_Boundary";
	  $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";

+ &eventlog("QMSWC:BAD_MIME_BOUNDARY");

	}
	if ($BOUNDARY_REGEX ne "") {
	  $BOUNDARY_REGEX.="|".$BOUNDARY{$attachment_counter};

@@ -1104,6 +1287,7 @@

	    $destring='LOCALE_destring_problem';
	    &debug($quarantine_description);
	    &minidebug("w_c: $quarantine_description");

+ &eventlog("QMSWC:BAD_MIME_ASSOCIATION");

	    $quarantine_event="Policy:Forged_Attachment";
	    $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in attachment $attachment_filename";
	  }

@@ -1150,6 +1334,7 @@

	  &minidebug("w_c: $quarantine_description");
	  $quarantine_event="Policy:Bad_MIME_Header";
	  $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";

+ &eventlog("QMSWC:BAD_MIME_CONTENT");

	}
      }
    }

@@ -1168,6 +1353,7 @@

	  $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in attachment \"$attachment_filename\"";
	  &debug("w_c: $quarantine_description");
	  &minidebug("w_c: $quarantine_description");

+ &eventlog("QMSWC:BAD_MIME_WINBLOWS");

	}
      }
      if ($_ =~ /^(UEsDB[AB]|UEswMFBL)/) {

@@ -1181,6 +1367,7 @@

	  $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in attachment \"$attachment_filename\"";
	  &debug("w_c: $quarantine_description");
	  &minidebug("w_c: $quarantine_description");

+ &eventlog("QMSWC:BAD_MIME_ZIP");

	}
      }
    }

@@ -1264,6 +1451,7 @@

    #some (valid) reason (including the other end dropping the connection).
    &debug("g_e_h: no sender and no recips. Probably due to SMTP client dropping connection. Nothing we can do - cleanup and exit. This is not necessarily an error!");
    &minidebug("g_e_h: no sender and no recips, from $smtp_sender. Dropping, this isn't a QS error.");

+ &eventlog("SMTP-DROP");

    warn "$$ QS-$VERSION: no sender and no recips, from $smtp_sender\n" if ($MINIDEBUG >= 3);
    warn "$V_HEADER-$VERSION: no sender and no recips, from $smtp_sender\n" if ($MINIDEBUG == 2);
    &cleanup;

@@ -1272,6 +1460,7 @@

  }
  &debug("g_e_h: return-path is \"$returnpath\", recips is \"$recips\"");
  &minidebug("g_e_h: return-path='$returnpath', recips='$recips'");

+ &eventlog("ENV-HEADER:$local_domains_string:$returnpath:$recips");

}


@@ -1409,6 +1598,7 @@

    $description .= "\n---perlscanner results ---\n$destring '$quarantine_description'\n found in message";
    &debug("p_s: something to block! ($quarantine_description)");
    &minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_MIME_HEADER");

  }
  #check out headers against DB...
  

@@ -1438,6 +1628,7 @@

	$description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file";
	&debug("p_s: something to block! ($quarantine_description)");
	&minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_HDR_DB");

	last;
      }
    } else {

@@ -1468,6 +1659,7 @@

      $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in message";
      &debug("p_s: something to block! ($quarantine_description)");
      &minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_HDR_DB");

    }
    $CRYPTO_TYPE=~s/\)$/,private\)/;
  }

@@ -1481,6 +1673,7 @@

      $description .= "\n---perlscanner results ---\n$destring '$quarantine_description' found in file $ENV{'TMPDIR'}/$file";
      &debug("p_s: something to block! ($quarantine_description)");
      &minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_HDR_DB");

      return;
    }
  }

@@ -1496,6 +1689,7 @@

    $file_desc .= "too_many:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/);
    &debug("p_s: something to block! ($quarantine_description)");
    &minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_ATTACH_LENGTH");

    return;
  }
  foreach $filepath (@allfiles,@uufile_list,@zipfile_list,@attachment_list) {

@@ -1527,6 +1721,7 @@

      $file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/);
      &debug("p_s: something to block! ($quarantine_description)");
      &minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("QMSWC:BAD_ATTACH_FILENAME");

      return;
    }

@@ -1545,6 +1740,7 @@

	$file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/);
	&debug("p_s: something to block! ($quarantine_description)");
	&minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("QMSWC:BAD_ATTACH_FILENAME");

	return;
      }
      if ($virtualheader{'FILECLSID'} ne "" && !$quarantine_event && $file =~ /\{[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}\}$/i) {

@@ -1555,6 +1751,7 @@

	$file_desc .= "$file:$msg_size\t" if ($file_desc !~ /\Q$file\E:$size\t/);
	&debug("p_s: something to block! ($quarantine_description)");
	&minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("QMSWC:BAD_ATTACH_FILENAME");

	return;
      }
    }

@@ -1605,6 +1802,7 @@

      $section=$apptype=$save_filename=$filename="";
      &debug("p_s: something to block! ($quarantine_description)");
      &minidebug("p_s: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_ATTACHMENT_TYPE");

      #	return;
    }
  }

@@ -1621,6 +1819,7 @@

    $file_desc .= "encrypted_zip:$msg_size\t";
    &debug("u_f: something to block! ($quarantine_description)");
    &minidebug("u_f: something to block! ($quarantine_description)");

+ &eventlog("PERLSCAN:BAD_ATTACHMENT_TYPE");

    return;
  }

@@ -2482,6 +2681,7 @@

     print SM "Subject: $tmpsubj\n";
  }
  print SM "Message-ID: <".&uniq_id."\@$hostname>\n";

+ print SM "X-Tnz-Problem-Type: 40\n";

  print SM "Auto-Submitted: auto-replied\n";
  if ($headers{'message-id'} ne "") {
    print SM "In-Reply-To: ",$headers{'message-id'},"\n";

@@ -2554,6 +2754,7 @@

  $tmpsubj =~ s/(\r|\0|\n)/ /g;
  print SM "Subject: $tmpsubj\n";
  print SM "Message-ID: <".&uniq_id."\@$hostname>\n";

+ print SM "X-Tnz-Problem-Type: 40\n";

  if ($headers{'message-id'} ne "") {
    print SM "In-Reply-To: ",$headers{'message-id'},"\n";
    print SM "References: ",$headers{'message-id'},"\n";

@@ -2617,6 +2818,7 @@

    if ($MAYBEZIP =~ /skipping:.*password/) {
      &debug ("u_f: it is a password-protected zip file");
      &minidebug ("u_f: it is a password-protected zip file");

+ &eventlog("UNZIP:PASSWORD_PROTECTED");

      $CRYPTO_TYPE="CR:ZIP(encrypted)";
    }
    if ($force_unzip) {

diff -Naur qmail-scanner-2.01st/qms-analog-types.txt qmail-scanner-2.01st-qms/qms-analog-types.txt --- qmail-scanner-2.01st/qms-analog-types.txt 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qms-analog-types.txt 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,64 @@ +qms-analog log files are of the general form: +<time (secs since epoch)>:processID:LOGTYPE:<type-specific parms>... + + +LOGTYPES and associated parameters are listed below: + +*Info* +----------------------------------------------------------------- + +-* - remark line +CONNECT-SMTP:<IP ADRS> - SMTP connect from IP +CONNECT-PIPE:$$ - SMTP connect from process $$ +HEADER:<from adrs>:<to adrs>:subject - SMTP header contents +ENV-HEADER:<returnpath>:<recips> - envelope hdr contents +TYPE:PLAIN - message has no mime type or text/plain +TYPE:MIXED - message has non-text/plain mime type +SCANTIME:<time> - total time in qmail-scanner +SPAM-RESULT:<score>:<thresh>:<delete> - spamassassin score for msg + +ERROR:<VERSION>:<string> - error condition +SMTP-DROP - qmail dropping msg + + +*Flagged Msgs* +----------------------------------------------------------------- + +QMSWC: - QMS working copy detection + BAD_HDR_CHARS - bad chars in hdr + BAD_HDR_BREAKAGE - breakage in header name + BAD_HDR_MIME - Disallowed mime content in hdr name + BAD_MIME_CONTENT - Disallowed mime content type + BAD_MIME_FILENAME - Disallowed MIME filename manipulation + BAD_MIME_BOUNDARY - broken attachment MIME details + BAD_MIME_ASSOCIATION - Disallowed file associated with unrelated MIME type + BAD_MIME_WINBLOWS - Disallowed executable attachment (windows) + BAD_ATTACH_FILENAME - Disallowed attachment filename MIME type + BAD_MIME_ZIP - Disallowed zip attachment when not assoc with a .zip + +PERLSCAN: - PerlScan detection + BAD_MIME_HEADER - Disallowed characters found in MIME headers + BAD_HDR_DB - hdr type in disallowed db + BAD_ATTACH_LENGTH - majorly long attachment filename found + BAD_ATTACHMENT_TYPE - Disallowed attachment type + +UNZIP:PASSWORD_PROTECTED - zip file is password protected + +CLAMAV:<quarantine_description> - clamAV found a virus +AVPAV:<quarantine_description> - AVPLinux AV found a virus +CSAV:<quarantine_description> - Command scanner AV found a virus +FPROTAV:<quarantine_description> - F-prot scanner AV found a virus +FSECUREAV:<quarantine_description> - Fsecure AV found a virus +HBEDVAV:<quarantine_description> - H+BEDV scanner AV found a virus +INNOCAV:<quarantine_description> - Innoculan AV found a virus +ISCANAV:<quarantine_description> - Iscan AV found a virus +RAVLINAV:<quarantine_description> - Ravlin AV found a virus +UVSCANAV:<quarantine_description> - McAfee AV found a virus +VEXIRAV:<quarantine_description> - Vexira AV found a virus + + +SPAM-DETECT: - spamassassin spam detected + DELETE - score > thresh + delete + MARK - thresh < score < thresh + delete + QUARANTINE - thresh + quarantine < score < thresh + delete + REJECT - SMTP reject sent to sender \ No newline at end of file diff -Naur qmail-scanner-2.01st/qms-config qmail-scanner-2.01st-qms/qms-config --- qmail-scanner-2.01st/qms-config 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qms-config 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,81 @@ +#!/bin/sh + +## File: qms-config +## +## Purpose: Provide a file to save personal qmail-scanner configuration +## options. This file should be edited for your server and +## saved somewhere so that it survives qmail-scanner and +## qms-analog upgrades. +## + +# Was the "install" option given? +if [ "$1" != "install" ]; then + INSTALL= +else + INSTALL="--install" +fi + +## Definition of Options +## +## domain - your primary email domain, where your postmaster +## account is located +## admin - your postmaster username, normally "postmaster" +## local-domains - list all of your local email domains for this qmail +## server, separated by commas +## add-dscr-hdrs - enable descriptive email headers +## dscr-hdrs-text - "Header" for the header +## ignore-eol-check - ignore end of line characters in email headers +## sa-quarantine - enable/disable quarantining email identified as spam +## sa-delete - enable/disable deleting email identified as spam; +## if 0, deletion is disabled; if positive, the value +## over "required_hits" to start deletion +## sa-reject - enable/disable rejection of emails identified as spam +## sa-subject - spam-identifying test to be prepended to the subject +## header +## sa-alt - use alternative "fast" Spamassassin processing +## provided in the "st" patch +## sa-debug - turn on default qmail-scanner debugging; very verbose +## and annoying +## notify - comma-separated list parties to notify when a virus +## is quarantined; see the qmail-scanner docs for more +## details +## redundant - enable/disable allowing the scanners to scan any zip +## files and the original "raw" email file +## qms-monitor - [yes|no] enable qms-monitor Account Monitoring +## qms-monitor-accts - list of email accounts to be monitored, separated by +## commas +## Example: "acct1@dom2.com,acct2@dom1.com" +## qms-monitor-dests - list of destination paths for monitored email messages +## Note 1: locations here will be saved underneath +## .../qmailscan/qms-monitor; a cron job can later +## copy from that location to an alternate email +## domain used for account monitoring. +## Note 2: each entry in this array corresponds to the +## email address in the same location of the +## qms-monitor-accts list above - i.e., +## qms-monitor-accts[2] msgs get stored at +## qms-monitor-dests[2] - thus, ORDER DOES MATTER +## Note 3: DO NOT include a leading "/" on these paths - +## they will typically be entries that ultimately +## belong in /home/vpopmail/domains - so start with +## the domain name. +## Example: "mon.dom2.com/acct1/Maildir/new,mon.dom1.com/acct2/Maildir/new" +## + +./configure --domain yourdomain.com \ + --admin postmaster \ + --local-domains "yourdomain.com,yourotherdomain.com" \ + --add-dscr-hdrs yes \ + --dscr-hdrs-text "X-Antivirus-MYDOMAIN" \ + --ignore-eol-check yes \ + --sa-quarantine 0 \ + --sa-delete 0 \ + --sa-reject no \ + --sa-subject ":SPAM:" \ + --sa-alt yes \ + --sa-debug no \ + --notify admin \ + --redundant yes \ + --qms-monitor no \ + "$INSTALL" + diff -Naur qmail-scanner-2.01st/qms-config-cwrapper qmail-scanner-2.01st-qms/qms-config-cwrapper --- qmail-scanner-2.01st/qms-config-cwrapper 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qms-config-cwrapper 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,82 @@ +#!/bin/sh + +## File: qms-config-cwrapper +## +## Purpose: Provide a file to save personal qmail-scanner configuration +## options. This file should be edited for your server and +## saved somewhere so that it survives qmail-scanner and +## qms-analog upgrades. +## + +# Was the "install" option given? +if [ "$1" != "install" ]; then + INSTALL= +else + INSTALL="--install" +fi + +## Definition of Options +## +## domain - your primary email domain, where your postmaster +## account is located +## admin - your postmaster username, normally "postmaster" +## local-domains - list all of your local email domains for this qmail +## server, separated by commas +## add-dscr-hdrs - enable descriptive email headers +## dscr-hdrs-text - "Header" for the header +## ignore-eol-check - ignore end of line characters in email headers +## sa-quarantine - enable/disable quarantining email identified as spam +## sa-delete - enable/disable deleting email identified as spam; +## if 0, deletion is disabled; if positive, the value +## over "required_hits" to start deletion +## sa-reject - enable/disable rejection of emails identified as spam +## sa-subject - spam-identifying test to be prepended to the subject +## header +## sa-alt - use alternative "fast" Spamassassin processing +## provided in the "st" patch +## sa-debug - turn on default qmail-scanner debugging; very verbose +## and annoying +## notify - comma-separated list parties to notify when a virus +## is quarantined; see the qmail-scanner docs for more +## details +## redundant - enable/disable allowing the scanners to scan any zip +## files and the original "raw" email file +## qms-monitor - [yes|no] enable qms-monitor Account Monitoring +## qms-monitor-accts - list of email accounts to be monitored, separated by +## commas +## Example: "acct1@dom2.com,acct2@dom1.com" +## qms-monitor-dests - list of destination paths for monitored email messages +## Note 1: locations here will be saved underneath +## .../qmailscan/qms-monitor; a cron job can later +## copy from that location to an alternate email +## domain used for account monitoring. +## Note 2: each entry in this array corresponds to the +## email address in the same location of the +## qms-monitor-accts list above - i.e., +## qms-monitor-accts[2] msgs get stored at +## qms-monitor-dests[2] - thus, ORDER DOES MATTER +## Note 3: DO NOT include a leading "/" on these paths - +## they will typically be entries that ultimately +## belong in /home/vpopmail/domains - so start with +## the domain name. +## Example: "mon.dom2.com/acct1/Maildir/new,mon.dom1.com/acct2/Maildir/new" +## + +./configure --domain yourdomain.com \ + --admin postmaster \ + --local-domains "yourdomain.com,yourotherdomain.com" \ + --add-dscr-hdrs yes \ + --dscr-hdrs-text "X-Antivirus-MYDOMAIN" \ + --ignore-eol-check yes \ + --sa-quarantine 0 \ + --sa-delete 0 \ + --sa-reject no \ + --sa-subject ":SPAM:" \ + --sa-alt yes \ + --sa-debug no \ + --notify admin \ + --redundant yes \ + --skip-setuid-test \ + --qms-monitor no \ + "$INSTALL" + diff -Naur qmail-scanner-2.01st/qms-config-monitor qmail-scanner-2.01st-qms/qms-config-monitor --- qmail-scanner-2.01st/qms-config-monitor 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qms-config-monitor 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,85 @@ +#!/bin/sh + +## File: qms-config-monitor +## +## Purpose: Provide a file to save personal qmail-scanner configuration +## options. This file should be edited for your server and +## saved somewhere so that it survives qmail-scanner and +## qms-analog upgrades. +## +## Note: This is a special version to enable qms-monitor +## + +# Was the "install" option given? +if [ "$1" != "install" ]; then + INSTALL= +else + INSTALL="--install" +fi + +## Definition of Options +## +## domain - your primary email domain, where your postmaster +## account is located +## admin - your postmaster username, normally "postmaster" +## local-domains - list all of your local email domains for this qmail +## server, separated by commas +## add-dscr-hdrs - enable descriptive email headers +## dscr-hdrs-text - "Header" for the header +## ignore-eol-check - ignore end of line characters in email headers +## sa-quarantine - enable/disable quarantining email identified as spam +## sa-delete - enable/disable deleting email identified as spam; +## if 0, deletion is disabled; if positive, the value +## over "required_hits" to start deletion +## sa-reject - enable/disable rejection of emails identified as spam +## sa-subject - spam-identifying test to be prepended to the subject +## header +## sa-alt - use alternative "fast" Spamassassin processing +## provided in the "st" patch +## sa-debug - turn on default qmail-scanner debugging; very verbose +## and annoying +## notify - comma-separated list parties to notify when a virus +## is quarantined; see the qmail-scanner docs for more +## details +## redundant - enable/disable allowing the scanners to scan any zip +## files and the original "raw" email file +## qms-monitor - [yes|no] enable qms-monitor Account Monitoring +## qms-monitor-accts - list of email accounts to be monitored, separated by +## commas +## Example: "acct1@dom2.com,acct2@dom1.com" +## qms-monitor-dests - list of destination paths for monitored email messages +## Note 1: locations here will be saved underneath +## .../qmailscan/qms-monitor; a cron job can later +## copy from that location to an alternate email +## domain used for account monitoring. +## Note 2: each entry in this array corresponds to the +## email address in the same location of the +## qms-monitor-accts list above - i.e., +## qms-monitor-accts[2] msgs get stored at +## qms-monitor-dests[2] - thus, ORDER DOES MATTER +## Note 3: DO NOT include a leading "/" on these paths - +## they will typically be entries that ultimately +## belong in /home/vpopmail/domains - so start with +## the domain name. +## Example: "mon.dom2.com/acct1/Maildir/new,mon.dom1.com/acct2/Maildir/new" +## + +./configure --domain yourdomain.com \ + --admin postmaster \ + --local-domains "yourdomain.com,yourotherdomain.com" \ + --add-dscr-hdrs yes \ + --dscr-hdrs-text "X-Antivirus-MYDOMAIN" \ + --ignore-eol-check yes \ + --sa-quarantine 0 \ + --sa-delete 0 \ + --sa-reject no \ + --sa-subject ":SPAM:" \ + --sa-alt yes \ + --sa-debug no \ + --notify admin \ + --redundant yes \ + --qms-monitor yes \ + --qms-monitor-accts "acct1@dom2.com,acct2@dom1.com" \ + --qms-monitor-dests "monitor.dom2.com/acct1/Maildir/new,monitor.dom1.com/acct2/Maildir/new" \ + "$INSTALL" + diff -Naur qmail-scanner-2.01st/qms-config-monitor-cwrapper qmail-scanner-2.01st-qms/qms-config-monitor-cwrapper --- qmail-scanner-2.01st/qms-config-monitor-cwrapper 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qms-config-monitor-cwrapper 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,86 @@ +#!/bin/sh + +## File: qms-config-monitor-cwrapper +## +## Purpose: Provide a file to save personal qmail-scanner configuration +## options. This file should be edited for your server and +## saved somewhere so that it survives qmail-scanner and +## qms-analog upgrades. +## +## Note: This is a special version to enable qms-monitor with the +## C-wrapper +## + +# Was the "install" option given? +if [ "$1" != "install" ]; then + INSTALL= +else + INSTALL="--install" +fi + +## Definition of Options +## +## domain - your primary email domain, where your postmaster +## account is located +## admin - your postmaster username, normally "postmaster" +## local-domains - list all of your local email domains for this qmail +## server, separated by commas +## add-dscr-hdrs - enable descriptive email headers +## dscr-hdrs-text - "Header" for the header +## ignore-eol-check - ignore end of line characters in email headers +## sa-quarantine - enable/disable quarantining email identified as spam +## sa-delete - enable/disable deleting email identified as spam; +## if 0, deletion is disabled; if positive, the value +## over "required_hits" to start deletion +## sa-reject - enable/disable rejection of emails identified as spam +## sa-subject - spam-identifying test to be prepended to the subject +## header +## sa-alt - use alternative "fast" Spamassassin processing +## provided in the "st" patch +## sa-debug - turn on default qmail-scanner debugging; very verbose +## and annoying +## notify - comma-separated list parties to notify when a virus +## is quarantined; see the qmail-scanner docs for more +## details +## redundant - enable/disable allowing the scanners to scan any zip +## files and the original "raw" email file +## qms-monitor - [yes|no] enable qms-monitor Account Monitoring +## qms-monitor-accts - list of email accounts to be monitored, separated by +## commas +## Example: "acct1@dom2.com,acct2@dom1.com" +## qms-monitor-dests - list of destination paths for monitored email messages +## Note 1: locations here will be saved underneath +## .../qmailscan/qms-monitor; a cron job can later +## copy from that location to an alternate email +## domain used for account monitoring. +## Note 2: each entry in this array corresponds to the +## email address in the same location of the +## qms-monitor-accts list above - i.e., +## qms-monitor-accts[2] msgs get stored at +## qms-monitor-dests[2] - thus, ORDER DOES MATTER +## Note 3: DO NOT include a leading "/" on these paths - +## they will typically be entries that ultimately +## belong in /home/vpopmail/domains - so start with +## the domain name. +## Example: "mon.dom2.com/acct1/Maildir/new,mon.dom1.com/acct2/Maildir/new" +## + +./configure --domain yourdomain.com \ + --admin postmaster \ + --local-domains "yourdomain.com,yourotherdomain.com" \ + --add-dscr-hdrs yes \ + --dscr-hdrs-text "X-Antivirus-MYDOMAIN" \ + --ignore-eol-check yes \ + --sa-quarantine 0 \ + --sa-delete 0 \ + --sa-reject no \ + --sa-subject ":SPAM:" \ + --sa-alt yes \ + --sa-debug no \ + --notify admin \ + --redundant yes \ + --skip-setuid-test \ + --qms-monitor yes \ + --qms-monitor-accts "acct1@dom2.com,acct2@dom1.com" \ + --qms-monitor-dests "monitor.dom2.com/acct1/Maildir/new,monitor.dom1.com/acct2/Maildir/new" \ + "$INSTALL" diff -Naur qmail-scanner-2.01st/qms-monitor-move.sh qmail-scanner-2.01st-qms/qms-monitor-move.sh --- qmail-scanner-2.01st/qms-monitor-move.sh 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/qms-monitor-move.sh 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,21 @@ +#!/bin/sh + +## example cron script to move qms-monitor messages to their final destination +## +## must be run from the root cron table! +## +## cron entry: 0-59/5 * * * * /var/qmail/bin/qms-monitor-move.sh >/dev/null +## will run the script every 5 minutes, moving emails to the appropriate +## monitor email domains, where they can be read as normal email +## +## Just run crontab -e (as root) and add the line above. +## + +# change ownership so we can retrieve via vpopmail +chown -R vpopmail:vchkpw /var/spool/qmailscan/qms-monitor/* + +# copy from the temp location to vpopmail home +cp -R -p /var/spool/qmailscan/qms-monitor/* /home/vpopmail/domains + +# delete the temp copies +rm -rf /var/spool/qmailscan/qms-monitor/* diff -Naur qmail-scanner-2.01st/README-qms-analog qmail-scanner-2.01st-qms/README-qms-analog --- qmail-scanner-2.01st/README-qms-analog 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/README-qms-analog 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,275 @@ +qms-analog: Qmail-Scanner Log File Analyzer +------------------------------------------- + +Version: 0.4.0, 11/02/2004 + + +Distribution Files +------------------ + +COPYING - The GPL Version 2 License file +Makefile - builds the qms-analog utility +qmail-scanner-1.24-st-qms-20041102.patch + - patch file for clean qmail-scanner-1.24 + distro which contains the qms event logger + and the popular "st" patch +qms-analog-types.txt - defines the event log types provided + by the patch +qms-config - qmail-scanner config script for qms-analog +qms-config-cwrapper - qmail-scanner config script for qms-analog + used when perl doesn't have setuid support +qms-config-monitor - qmail-scanner config script for qms-analog + and qms-monitor +qms-config-monitor-cwrapper - qmail-scanner config script for qms-analog + and qms-monitor used when perl doesn't have + setuid support +README - this file +RELEASE-NOTES - version change log +src/ - source directory for qms-analog + + + +What You Get +------------ + +/var/qmail/bin/qms-analog + The utility which takes /var/spool/qmailscan/qms-events.log + records as input from stdin and generates statistics on stdout. + +/var/qmail/bin/qmail-scanner-queue.pl + The patched version which generates nice logs in + /var/spool/qmailscan/qms-events.log and optionally provides account + monitoring if qms-monitor is enabled + + + +!!!!!!!! ATTENTION: READ THIS FIRST !!!!!!!! +-------------------------------------------- +qms-analog requires a patch be applied to your qmail-scanner-1.24 +distribution in order to generate a new, more legible log file. The +patch file includes the popular "st" patch which adds useful +capabilities to qmail-scanner. + +The following are minimum requirements for qms-analog to work: +1) qmail-scanner version 1.24 (unpatched, clean distro) +2) ClamAV or other qmail-scanner supported AV software. +3) Spamassassin + +If you don't have these, and are unwilling to upgrade or install +them, DO NOT USE qms-analog. I cannot be responsible for what might +happen. + +Disclaimer: +----------- +Generally speaking, it is a quite simple matter to upgrade versions +of qms-analog. Patch a clean distro of qmail-scanner-1.24, configure +it, modify qmail-scanner-queue.pl if using the C wrapper instead of +setuid, then build and install qms-analog. I have done this time and +time again with no adverse effects on my Qmail installation or any +of the ancillary utilities that were installed in the QmailRocks +procedure. Having said that, as with all open-source software, no +guarantee is expressed or implied in any way, and I am not responsible +for mistakes or abnormalities in your particular installation. + + +If you meet these requirements, let's get started... + + + +Note Concerning Where This Fits In the QmailRocks Procedure +----------------------------------------------------------- + +qms-analog is now part of the QmailRocks procedure. See +http://qmailrocks.org for details. + + + +A. Patching the qmail-scanner-1.24 Distribution +----------------------------------------------- + +1) Obtain the unpatched source distribution qmail-scanner-1.24.tgz. + +2) Extract it to the location of your choice. + +3) Make a backup copy of the qmail-scanner-1.24 directory before + patching it: + cp -R qmail-scanner-1.24 qmail-scanner-1.24-orig + +4) Copy qmail-scanner-1.24-st-qms-20041102.patch from the qms-analog + distro to the qmail-scanner-1.24 directory where the tarball was + extracted. + cp qmail-scanner-1.24-st-qms-20041102.patch <path_qm-scanner-1.24> + +5) Change directory to the qmail-scanner-1.24 distribution: + cd <path_qm-scanner-1.24> + +6) Patch qmail-scanner-1.24: + patch -p1 < qmail-scanner-1.24-st-qms-20041102.patch + +7) Configure qmail-scanner-1.24: + + To configure qms-monitor support: CONFFILE = qms-config-monitor + To disable qms-monitor support: CONFFILE = qms-config + + Substitute the appropriate config file name above for the place holder + CONFFILE in the following directions. + + + a) Edit CONFFILE to insert your domain name, postmaster account name, + and local domain list. Also modify other settings of interest including + qms-monitor accounts, etc. + + b) Execute: + ./CONFFILE + + c) if the test configure looks good, install it: + ./CONFFILE install + + Proceed to step 8. + + d) if the script complains about setuid, execute: + ./CONFFILE-cwrapper + + e) if that works, execute: + ./CONFFILE-cwrapper install + to install it and follow the "C Wrapper" instructions + + + This sets up qmail-scanner in a qms-analog friendly way. + + Note: I changed debug to default to disabled in the patch. You can add + "--debug=1" as an option to configure to enable it. It gets very + large and is of no real use anyway to users. The st --minidebug + together with --sa_alt and --sa_debug produce much better debug + output anyway. + +8) After successful configuration , if you are using the C-wrapper instead + of perl's setuid, follow the directions in the + qmail-scanner-1.24/contrib/qmail-scanner-queue.c file - to modify the + permissions of qmail-scanner-queue.pl and the perl tag at the top of + that file (delete the "-T"). + +9) Copy the qmailstats script from the qms-analog distro directory to + /var/qmail/bin: + cp <qms-analog directory>/qmailstats /var/qmail/bin + chmod 0755 /var/qmail/bin/qmailstats + +10) The following log files will need to be rotated or otherwise monitored + so they do not grow too large: + + /var/spool/qmailscan/qmail-queue.log + /var/spool/qmailscan/qms-events.log + + + +B. Building qms-analog and installing it with a new qmailstats script +--------------------------------------------------------------------- + +1) Become root + +2) cd to the qms-analog directory (wherever you extracted it) + +3) make all + + + +C. Testing +---------- + +1) Allow several logs to accumulate in /var/spool/qmailscan/qms-events.log. + +2) Execute: + cat /var/spool/qmailscan/qms-events.log | /var/qmail/bin/qms-analog 0 + + This should dump the qms-analog results to stdout (the shell you ran it + from). + +3) If that looks good, execute: + /var/qmail/bin/qmailstats + + This should generate the nightly email to the postmaster including the + qms-analog stats at the bottom. + +4) If those two tests pass, you are done! + + + +D. Using qms-analog +------------------- + +1) qms-analog reads the log records from stdin. Thus you can pipe the + output of "cat /var/spool/qmailscan/qms-events.log" into qms-analog. + +2) qms-analog writes its results to stdout. This can be redirected to + a file or viewed on the controlling console. + +3) qms-analog requires the hours-of-history argument which specifies the + number of hours of historical stats to compile. + You can also pass a second argument, sort-key. This specifies the order + of the account based statistics. + + usage: qms-analog hours-of-history <sort-key> + + hours-of-history (0 - n) hours of history to collect + 0 => all records + sort-key (optional) sort key for account statistics + msgbw (default) msg bandwidth - successful msgs + alpha alphanumeric by account name + virus number of viruses received + saavg Spamassassin avg score + sadet Spamassassin msgs detected + + Some examples: + "qms-analog 24" - use only records within the last 24 hours, + sort by msg bandwidth + "qms-analog 168" - use only records within the last 7 days, + sort by msg bandwidth + "qms-analog 0" - use all records, sort by msg bandwidth + "qms-analog 0 alpha" - use all records, sort alphabetically + "qms-analog 0 saavg" - use all records, sort by Spam average score + +4) Although qms-analog is installed as part of the "qmailstats" script, + it could easily be invoked from a custom script which could be run + from a shell at any time, or as part of the cron daemon's tasks. See + "qmailstats" for example usage. + +5) Notes on Statistical Output + The headings for the account statistics are described below: + + MsgRx - messages successfully received (not virus, not deleted or + quarantined spam) + MsgTx - messages successfully transmitted (not virus) +  %Total - what percent of total successful messages for the mail server + (MsgRx + MsgTx) comprises + ScanTime - total time in secs that qmail-scanner took to process the + messages + VirusRx - messages received that were intercepted as containing a virus + VirusTx - messages transmited that were intercepted as containing a virus + SA-AVG - average Spamassassin score for all messages for this account + run through Spamassassin + SA-MRK - number of messages for this account marked and delivered by + qmail-scanner based on the Spamassassin score + SA-DEL - number of messages for this account deleted by qmail-scanner + based on the Spamassassin score + SA-REJ - number of messages for this account rejected by qmail-scanner + based on the Spamassassin score + SA-QUA - number of messages for this account quarantined by qmail-scanner + based on the Spamassassin score + + +E. Notes +-------- + +If you have any problems, first re-read the directions and make sure you did +everything as prescribed, and if you are still having a problem, just restore +the original qmail-scanner-1.24 distribution (we backed it up, right?) and +configure it as normal. Also, please report problems or suggestions to the +Sourceforge mailing list or to the appropriate Sourceforge forum at: +http://sourceforge.net/projects/qms-analog/ + +I try to be very responsive. + + +Mark Teel +mteel@users.sourceforge.net + diff -Naur qmail-scanner-2.01st/README-qms-monitor qmail-scanner-2.01st-qms/README-qms-monitor --- qmail-scanner-2.01st/README-qms-monitor 1970-01-01 02:00:00.000000000 +0200 +++ qmail-scanner-2.01st-qms/README-qms-monitor 2007-09-09 07:58:35.000000000 +0300 @@ -0,0 +1,146 @@ +qms-monitor: Local Account Email Activity Monitoring for qmail-scanner +---------------------------------------------------------------------- + + +What Is It? +----------- + +A patch to the qmail-scanner distro which provides per account email +monitoring, incoming and outgoing. For accounts specified in the +qmail-scanner configure script, there will be a corresponding destination +specified for the monitor copies of all email into or out of that account. +The destination is a path that will be placed under +/var/spool/qmailscan/qms-monitor. A cron script is provided to periodically +move messages from this location to alternate vpopmail account locations, +if you want to use a normal email client to retrieve and manage the monitored +mail in unique monitor accounts/domains, or to one unique account/domain. +Otherwise it will collect in that location until you archive it or delete it. + + + +Purpose: +-------- + +Provide a mechanism to specify email addresses and corresponding archive +locations for local accounts so that ALL incoming and outgoing SMTP mail from +those accounts is archived for later review. + + + +Setup (all done as root): +------------------------- + +1) Edit the qms-monitor-config (or qms-monitor-config-cwrapper) script: + + NOTE: Before you can accurately set up destinations for your monitored + email, you must decide on a monitor strategy (see #6 below). + + Pay particular attention to the following in the script: + + domain - your primary email domain, where your postmaster + account is located + admin - your postmaster username, normally "postmaster" + local-domains - list all of your local email domains for this qmail + server, separated by commas + qms-monitor - [yes|no] enable qms-monitor Account Monitoring + qms-monitor-accts - list of email accounts to be monitored, separated by + commas + Example: "acct1@dom2.com,acct2@dom1.com" + qms-monitor-dests - list of destination paths for monitored email messages + Note 1: locations here will be saved underneath + .../qmailscan/qms-monitor; a cron job can later + copy from that location to an alternate email + domain used for account monitoring. + Note 2: each entry in this array corresponds to the + email address in the same location of the + qms-monitor-accts list above - i.e., + qms-monitor-accts[2] msgs get stored at + qms-monitor-dests[2] - thus, ORDER DOES MATTER + Note 3: DO NOT include a leading "/" on these paths - + they will typically be entries that ultimately + belong in /home/vpopmail/domains - so start with + the domain name. + Example: "mon.dom2.com/acct1/Maildir/new,mon.dom1.com/acct2/Maildir/new" + + The example destination paths in the config scripts provided indicate the + proper paths for vpopmail Maildir accounts + (<domain>/<account name>/Maildir/new). The account name here should be the + monitor account and the domain can be the monitor domain if you are using + one, otherwise the existing domain you will create the monitor accounts in. + +2) Configure qmail-scanner: + + For use with setuid: + ./qms-config-monitor + + For use with the qmail-scanner-queue C-wrapper and no setuid: + ./qms-config-monitor-cwrapper + +3) If that does not produce errors, install the new perl script: + + For use with setuid: + ./qms-config-monitor install + + For use with the qmail-scanner-queue C-wrapper and no setuid: + ./qms-config-monitor-cwrapper install + +4) Setup version and database (and C-wrapper): + + For use with setuid: + setuidgid qscand /var/qmail/bin/qmail-scanner-queue.pl -z + setuidgid qscand /var/qmail/bin/qmail-scanner-queue.pl -g + + For use with the qmail-scanner-queue C-wrapper and no setuid: + (Unset the setuid bit on /var/qmail/bin/qmail-scanner-queue.pl) + chmod 0755 /var/qmail/bin/qmail-scanner-queue.pl + + (Remove the "-T" from the perl signature in + /var/qmail/bin/qmail-scanner-queue.pl (first line of the file)) + vi /var/qmail/bin/qmail-scanner-queue.pl + + /var/qmail/bin/qmail-scanner-queue -z + /var/qmail/bin/qmail-scanner-queue -g + + +***************************************************************************** +That's it to start archiving all incoming and outgoing email for the accounts +specified in the config script. But if you want to be able to read/manage the +email with any email client, a cron job and associated script are required as +well as the creation of a new email domain(s) and/or accounts to +store/retrieve/manage the monitored mail. Those optional instructions follow... +***************************************************************************** + +5) Create Monitor Email Domain(s) and/or Account(s): + + I suggest adopting a convention like creating a shadow domain for every + real email domain, for example if you have domain domain.com, your + shadow domain could be monitor.domain.com, if you can control or add + domains to your DNS server of record. If not, you can just shadow at + the account level in your existing domain(s). In this case, if the account + of interest is shady@domain.com, you could create an account called + monitor.shady@domain.com, and monitor the account from that account. + These are just suggestions, bottom line is that you decide on a strategy + BEFORE setting up the monitoring domains and/or accounts. If you adopt + the convention of providing the destinations in the qms-config-monitor + script as starting with the email domain name (no leading '/'), then + the cron script below will work right out of the box if your vpopmail + is located in /home/vpopmail, otherwise some simple editing of the + script is required to get email placed into the proper monitor locations. + + I will leave it to the QMR guide to describe how new domains and + accounts are created... + +6) Copy the cron script to /var/qmail/bin: + + cp ./qms-monitor-move.sh /var/qmail/bin + chmod 700 /var/qmail/bin/qms-monitor-move.sh + +7) Add an entry to the root's cron table so this script will be run (and + the monitored mail moved to the proper vpopmail location: + + crontab -e + + (Add a line such as: + 0-59/5 * * * * /var/qmail/bin/qms-monitor-move.sh >/dev/null + to the root's cron table. Save it and it will start running every + 5 minutes.) diff -Naur qmail-scanner-2.01st/sub-avp.pl qmail-scanner-2.01st-qms/sub-avp.pl --- qmail-scanner-2.01st/sub-avp.pl 2006-02-26 12:34:40.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-avp.pl 2007-09-09 09:11:32.000000000 +0300 @@ -27,6 +27,7 @@

      }
      &debug("There be a $destring! ($quarantine_description)");
      &minidebug("kasp: there be a virus! ($quarantine_description)");

+ &eventlog("AVPAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="AVP:".substr($quarantine_event,0,$QE_LEN);
    } else {

diff -Naur qmail-scanner-2.01st/sub-clamdscan.pl qmail-scanner-2.01st-qms/sub-clamdscan.pl --- qmail-scanner-2.01st/sub-clamdscan.pl 2006-02-26 17:20:25.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-clamdscan.pl 2007-09-09 09:15:18.000000000 +0300 @@ -21,6 +21,7 @@

      $quarantine_description=$+;
      &debug("There be a virus! ($quarantine_description)");
      &minidebug("clamdscan: there be a virus! ($quarantine_description)");

+ &eventlog("CLAMAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="CLAMDSCAN:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---clamdscan results ---\n$DD";

@@ -36,6 +37,7 @@

      &debug("clamdscan: $quarantine_description");
      &minidebug("clamdscan: $quarantine_description");
      $quarantine_event="CLAMDSCAN:Resource_attack";

+ &eventlog("CLAMAV:$quarantine_description");

      $description .= "\n---clamdscan results ---\n$DD";
    } elsif ($clamdscan_status > 0) {
      #This implies a corrupt set of DAT files or resource problems...

diff -Naur qmail-scanner-2.01st/sub-clamscan.pl qmail-scanner-2.01st-qms/sub-clamscan.pl --- qmail-scanner-2.01st/sub-clamscan.pl 2006-02-26 17:24:39.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-clamscan.pl 2007-09-09 09:15:54.000000000 +0300 @@ -20,6 +20,7 @@

      $quarantine_description=$+;
      &debug("There be a virus! ($quarantine_description)");
      &minidebug("clamscan: there be a virus! ($quarantine_description)");

+ &eventlog("CLAMAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="CLAMSCAN:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---clamscan results ---\n$DD";

@@ -32,6 +33,7 @@

      $quarantine_description="Resource attack - $1";
      &debug("clamscan: $quarantine_description");
      &minidebug("clamscan: $quarantine_description");

+ &eventlog("CLAMAV:$quarantine_description");

      $quarantine_event="CLAMSCAN:Resource_attack";
      $description .= "\n---clamscan results ---\n$DD";
    } elsif ($clamscan_status > 0) {

diff -Naur qmail-scanner-2.01st/sub-csav.pl qmail-scanner-2.01st-qms/sub-csav.pl --- qmail-scanner-2.01st/sub-csav.pl 2006-02-26 12:37:50.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-csav.pl 2007-09-09 09:16:24.000000000 +0300 @@ -18,6 +18,7 @@

    $quarantine_description=$1;
    &debug("There be a virus! ($quarantine_description)");
    &minidebug("csav_scanner: there be a virus! ($quarantine_description)");

+ &eventlog("CSAV:$quarantine_description");

    ($quarantine_event=$quarantine_description)=~s/\s/_/g;
    $quarantine_event="CSAV:".substr($quarantine_event,0,$QE_LEN);
  }  

diff -Naur qmail-scanner-2.01st/sub-fprot.pl qmail-scanner-2.01st-qms/sub-fprot.pl --- qmail-scanner-2.01st/sub-fprot.pl 2006-02-26 17:21:15.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-fprot.pl 2007-09-09 09:17:23.000000000 +0300 @@ -21,6 +21,7 @@

      $quarantine_description=~s/^\s+//g;
      &debug("There be a virus! ($quarantine_description)");
      &minidebug("fprot: there be a virus! ($quarantine_description)");

+ &eventlog("FPROTAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="FPROT:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---fprot results ---\n$DD";

diff -Naur qmail-scanner-2.01st/sub-fsecure.pl qmail-scanner-2.01st-qms/sub-fsecure.pl --- qmail-scanner-2.01st/sub-fsecure.pl 2006-02-26 17:21:39.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-fsecure.pl 2007-09-09 09:17:38.000000000 +0300 @@ -24,6 +24,7 @@

	$quarantine_description=~s/^\s+//g;
	&debug("There be a virus! ($quarantine_description)");
	&minidebug("fsecure: there be a virus! ($quarantine_description)");

+ &eventlog("FSECUREAV:$quarantine_description");

	($quarantine_event=$quarantine_description)=~s/\s/_/g;
	$quarantine_event="FSEC:".substr($quarantine_event,0,$QE_LEN);
	$description .= "\n---fsecure results ---\n$DD";

diff -Naur qmail-scanner-2.01st/sub-hbedv.pl qmail-scanner-2.01st-qms/sub-hbedv.pl --- qmail-scanner-2.01st/sub-hbedv.pl 2006-02-26 17:22:11.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-hbedv.pl 2007-09-09 09:17:48.000000000 +0300 @@ -17,6 +17,7 @@

      $quarantine_description=$1;
      &debug("There be a virus! ($quarantine_description)");
      &minidebug("hbedv: there be a virus! ($quarantine_description)");

+ &eventlog("HBEDVAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="HBEDV:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---hbedv results ---\n$DD";

diff -Naur qmail-scanner-2.01st/sub-inocucmd.pl qmail-scanner-2.01st-qms/sub-inocucmd.pl --- qmail-scanner-2.01st/sub-inocucmd.pl 2006-02-26 17:22:35.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-inocucmd.pl 2007-09-09 09:18:02.000000000 +0300 @@ -15,6 +15,7 @@

    $quarantine_description=$1;
    &debug("There be a virus! ($quarantine_description)");
    &minidebug("inocucmd: there be a virus! ($quarantine_description)");

+ &eventlog("INNOCAV:$quarantine_description");

    ($quarantine_event=$quarantine_description)=~s/\s/_/g;
    $quarantine_event="INOC:".substr($quarantine_event,0,$QE_LEN);
    $description .= "\n$DD\n";

diff -Naur qmail-scanner-2.01st/sub-iscan.pl qmail-scanner-2.01st-qms/sub-iscan.pl --- qmail-scanner-2.01st/sub-iscan.pl 2006-02-26 17:22:59.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-iscan.pl 2007-09-09 09:18:13.000000000 +0300 @@ -16,6 +16,7 @@

    $quarantine_description=$1;
    &debug("There be a virus! ($quarantine_description)");
    &minidebug("iscan: there be a virus! ($quarantine_description)");

+ &eventlog("ISCANAV:$quarantine_description");

    ($quarantine_event=$quarantine_description)=~s/\s/_/g;
    $quarantine_event="ISCAN:".substr($quarantine_event,0,$QE_LEN);
    $description .= "\n---iscan results ---\n$DD";

diff -Naur qmail-scanner-2.01st/sub-ravlin.pl qmail-scanner-2.01st-qms/sub-ravlin.pl --- qmail-scanner-2.01st/sub-ravlin.pl 2006-02-26 17:18:35.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-ravlin.pl 2007-09-09 09:19:23.000000000 +0300 @@ -22,6 +22,7 @@

      $quarantine_description=$1;
      &debug("ravlin_scanner: There be a virus! ($quarantine_description)");
      &minidebug("ravlin_scanner: there be a virus! ($quarantine_description)");

+ &eventlog("RAVLINAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="RAV:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---ravlin results ---\n$DD";

diff -Naur qmail-scanner-2.01st/sub-spamassassin.pl qmail-scanner-2.01st-qms/sub-spamassassin.pl --- qmail-scanner-2.01st/sub-spamassassin.pl 2006-12-23 14:00:20.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-spamassassin.pl 2007-09-09 09:22:54.000000000 +0300 @@ -195,6 +195,8 @@

  &debug("SA: REPORT hits = $sa_score/$required_hits\n$sa_report") if ( $sa_debug && $sa_report );
  &minidebug("SA: REPORT hits = $sa_score/$required_hits\n$sa_report") if ( $sa_debug && $sa_report && !$scanned);

+ &eventlog("- - -:SCORE:REQ:QRTN:DEL:REJ"); + &eventlog("SPAM-RESULT:$sa_score:$required_hits:$sa_quarantine:$sa_delete:$sa_reject");

  # st: what about SA sql per user, could be differents $required_hits...
  if ($required_hits > $sa_score || ($sa_score == 0) || ($sa_score eq "\?")) {

@@ -212,6 +214,7 @@

    if ($sa_delete && ($sa_quarantine>$sa_delete)) {
       &debug("SA: WARNING, sa_delete is lower than sa_quarantine, spam could be quarantined, but not deleted");
       &minidebug("SA: WARNING, sa_delete is lower than sa_quarantine, spam could be quarantined, but not deleted");

+ &eventlog("---- WARN: sa_delete < sa_quarantine => setting sa_delete = 0");

       $sa_delete='0';
    }

@@ -221,6 +224,7 @@

       $sa_threshold=$sa_delete+$required_hits;
       if ( $sa_reject && (($sa_delete_site+$required_hits)<$sa_score || $one_recip eq $recips )) {
          &log_sa_action($scanned,$sa_threshold,"rejected");

+ &eventlog("SPAM-DETECT:REJECT");

          $stop_spamassassin_time=[gettimeofday];
          $spamassassin_time = tv_interval ($start_spamassassin_time, $stop_spamassassin_time);
          &debug("SA: finished scan of dir \"$ENV{'TMPDIR'}\" in $spamassassin_time secs");

@@ -236,6 +240,7 @@

          $quarantine_description="SPAM exceeds \"delete\" threshold - hits=$sa_score/$required_hits";
          $quarantine_event="SA:SPAM-DELETED";
          &log_sa_action($scanned,$sa_threshold,"deleted");

+ &eventlog("SPAM-DETECT:DELETE");

          $description .= "\n---spamassassin results ---\n$destring '$quarantine_description'\n found in message $ENV{'TMPDIR'}";
       }
    } else {

@@ -246,6 +251,7 @@

          $quarantine_event="SA:SPAM-QUARANTINED";
          $quarantine_spam="SA:SPAM-QUARANTINED";
          &log_sa_action($scanned,$sa_threshold,"quarantined");

+ &eventlog("SPAM-DETECT:QUARANTINE");

          $description .= "\n---spamassassin results ---\n$destring '$quarantine_description'\n found in message $ENV{'TMPDIR'}";
       } else {
          #st: if $spamc_subject and $sa_delta are set, add in the subject the spam-level

@@ -259,6 +265,7 @@

             }
          }
          &log_sa_action($scanned,$required_hits,"tagged");

+ &eventlog("SPAM-DETECT:MARK");

       }
    }
  }

diff -Naur qmail-scanner-2.01st/sub-uvscan.pl qmail-scanner-2.01st-qms/sub-uvscan.pl --- qmail-scanner-2.01st/sub-uvscan.pl 2006-02-26 12:55:43.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-uvscan.pl 2007-09-09 09:23:24.000000000 +0300 @@ -16,6 +16,7 @@

      $quarantine_description=$1;
      &debug("There be a virus! ($quarantine_description)");
      &minidebug("uvscan: there be a virus! ($quarantine_description)");

+ &eventlog("UVSCANAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="UVSCAN:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---uvscan results ---\n$DD";

diff -Naur qmail-scanner-2.01st/sub-vexira.pl qmail-scanner-2.01st-qms/sub-vexira.pl --- qmail-scanner-2.01st/sub-vexira.pl 2006-02-26 17:19:03.000000000 +0200 +++ qmail-scanner-2.01st-qms/sub-vexira.pl 2007-09-09 09:23:41.000000000 +0300 @@ -18,6 +18,7 @@

      $quarantine_description=$1;
      &debug("vexira_scanner: There be a virus! ($quarantine_description)");
      &minidebug("vexira_scanner: there be a virus! ($quarantine_description)");

+ &eventlog("VEXIRAV:$quarantine_description");

      ($quarantine_event=$quarantine_description)=~s/\s/_/g;
      $quarantine_event="VEX:".substr($quarantine_event,0,$QE_LEN);
      $description .= "\n---vexira results ---\n$DD";	

@@ -30,6 +31,7 @@

    if ($DD =~ /WARNING: archive not completely scanned: (contents exceed \d+ (levels of recursion|bytes))/) {
      $quarantine_description="Resource attack - $1";
      &debug("vexira_scanner: $quarantine_description");

+ &eventlog("VEXIRAV:$quarantine_description");

      $quarantine_event="VEX:Resource_attack";
      $description .= "\n---vexira results ---\n$DD";
    } elsif ($vexira_status > 0) {

STOP


  tar xzf q-s-2.01st-20070204.tgz
  cd qmail-scanner-2.01st
  patch -p1 < ../qmail-scanner-2.01-st-qms.patch

Add qscand new group and user

  groupadd qscand
  useradd -c "Qmail-Scanner Account" -g qscand -s /bin/false qscand

Execute the config file

  ./configure

You shouldn't get any errors, but I have a machine who is complaining about some variables (like opt_d opt_p) it is safe to ignore

Now run configure with --install option

  ./configure --install

You can edit /var/qmail/bin/qmail-scanner-queue.pl to suit your needs You will hawe to replace default queue(simscan) with qmail-scanner Relace in /etc/tcprules.d/tcp.smtp QMAILQUEUE="/var/qmail/bin/simscan" With QMAILQUEUE="/var/qmail/bin/qmail-scanner-queue.pl"

Rebuild cdb

  qmailctl cdb

Stop qmail

  qmailctl stop

Edit clamd to run as qscand in file /var/qmail/supervise/clamd/run

Replace

  exec /usr/bin/setuidgid clamav /usr/sbin/clamd 2>&1

With

  exec /usr/bin/setuidgid qscand /usr/sbin/clamd 2>&1

On some systems it is working only with root user: exec /usr/bin/setuidgid root /usr/sbin/clamd 2>&1

Begin testing.. send you some mails and analize /var/log/maillog, /var/spool/qscan/qms-events.log, qmail-queue.log, quarantine.log the exectue qmailstats and wait for a nicely log !

Retrieved from "http://wiki.qmailtoaster.com/index.php/How_to_integrate_qms-analog_for_nicely_log_stats"

This page has been accessed 12,112 times. This page was last modified on 10 September 2007, at 17:47. Content is available under GNU Free Documentation License 1.2.


Find

Browse
Main page
Community portal
Current events
Recent changes
Random page
Help
Edit
View source
Editing help
This page
Discuss this page
New section
Printable version
Context
Page history
What links here
Related changes
My pages
Log in / create account
Special pages
New pages
File list
Statistics
More...