How to integrate qms-analog for nicely log stats

From QmailToaster
Jump to navigation Jump to search

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: +

      }
      &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 !