File: /volume1/@appstore/MailPlus-Server/etc/mimedefang/mimedefang-filter
# -*- Perl -*-
#***********************************************************************
#
# mimedefang-filter
#
# Suggested minimum-protection filter for Microsoft Windows clients, plus
# SpamAssassin checks if SpamAssassin is installed.
#
# Copyright (C) 2002 Roaring Penguin Software Inc.
#
# This program may be distributed under the terms of the GNU General
# Public License, Version 2, or (at your option) any later version.
#
# $Id$
#***********************************************************************
#***********************************************************************
# Set administrator's e-mail address here. The administrator receives
# quarantine messages and is listed as the contact for site-wide
# MIMEDefang policy. A good example would be 'defang-admin@mydomain.com'
#***********************************************************************
$AdminAddress = 'postmaster@localhost';
$AdminName = "MIMEDefang Administrator's Full Name";
#***********************************************************************
# Set the e-mail address from which MIMEDefang quarantine warnings and
# user notifications appear to come. A good example would be
# 'mimedefang@mydomain.com'. Make sure to have an alias for this
# address if you want replies to it to work.
#***********************************************************************
$DaemonAddress = 'mimedefang@localhost';
#***********************************************************************
# If you set $AddWarningsInline to 1, then MIMEDefang tries *very* hard
# to add warnings directly in the message body (text or html) rather
# than adding a separate "WARNING.TXT" MIME part. If the message
# has no text or html part, then a separate MIME part is still used.
#***********************************************************************
$AddWarningsInline = 0;
#***********************************************************************
# To enable syslogging of virus and spam activity, add the following
# to the filter:
# md_graphdefang_log_enable();
# You may optionally provide a syslogging facility by passing an
# argument such as: md_graphdefang_log_enable('local4'); If you do this, be
# sure to setup the new syslog facility (probably in /etc/syslog.conf).
# An optional second argument causes a line of output to be produced
# for each recipient (if it is 1), or only a single summary line
# for all recipients (if it is 0.) The default is 1.
# Comment this line out to disable logging.
#***********************************************************************
md_graphdefang_log_enable('mail', 1);
#***********************************************************************
# Uncomment this to block messages with more than 50 parts. This will
# *NOT* work unless you're using Roaring Penguin's patched version
# of MIME tools, version MIME-tools-5.411a-RP-Patched-02 or later.
#
# WARNING: DO NOT SET THIS VARIABLE unless you're using at least
# MIME-tools-5.411a-RP-Patched-02; otherwise, your filter will fail.
#***********************************************************************
# $MaxMIMEParts = 50;
#***********************************************************************
# Set various stupid things your mail client does below.
#***********************************************************************
# Set the next one if your mail client cannot handle multiple "inline"
# parts.
$Stupidity{"NoMultipleInlines"} = 0;
# Detect and load Perl modules
detect_and_load_perl_modules();
#***********************************************************************
#
# Synology block
#
#***********************************************************************
use Encode;
use MailPlusServer::Log;
use MailPlusServer::Util;
use MailPlusServer::Disclaimer;
use MailPlusServer::ReportMailFilter;
use MailPlusServer::ContentScanner;
use utf8;
my $MPSConfigFile = '/var/packages/MailPlus-Server/target/etc/mimedefang/mimedefang.cf';
my $MPSConfig = MailPlusServer::Util::read_json_config($MPSConfigFile);
my $MPSDisclaimer = MailPlusServer::Disclaimer->new($MPSConfig);
my $MPSReportMailFilter = MailPlusServer::ReportMailFilter->new($MPSConfig);
my $MPSContentScanner = MailPlusServer::ContentScanner->new($MPSConfig);
# Set mimedefang global variables
$NotifyNoPreamble = 1;
sub assign_new_msg_id {
my $msg_id_header = gen_msgid_header();
chomp($msg_id_header);
$msg_id_header =~ s/^Message-ID: //;
action_change_header('Message-ID', $msg_id_header);
}
sub action_bounce_dangerous_content {
my $reason = shift;
my $prefix_to_bypass_dup_log = 'dangerous content: ';
$reason = $prefix_to_bypass_dup_log . $reason;
action_bounce($reason);
}
$DaemonAddress = 'MAILER-DAEMON@' . $MPSConfig->{'smtp_main_domain'};
$DaemonName = 'Mail Delivery System';
#***********************************************************************
# %PROCEDURE: filter_begin
# %ARGUMENTS:
# $entity -- the parsed MIME::Entity
# %RETURNS:
# Nothing
# %DESCRIPTION:
# Called just before e-mail parts are processed
#***********************************************************************
sub filter_begin {
my($entity) = @_;
}
#***********************************************************************
# %PROCEDURE: filter
# %ARGUMENTS:
# entity -- a Mime::Entity object (see MIME-tools documentation for details)
# fname -- the suggested filename, taken from the MIME Content-Disposition:
# header. If no filename was suggested, then fname is ""
# ext -- the file extension (everything from the last period in the name
# to the end of the name, including the period.)
# type -- the MIME type, taken from the Content-Type: header.
#
# NOTE: There are two likely and one unlikely place for a filename to
# appear in a MIME message: In Content-Disposition: filename, in
# Content-Type: name, and in Content-Description. If you are paranoid,
# you will use the re_match and re_match_ext functions, which return true
# if ANY of these possibilities match. re_match checks the whole name;
# re_match_ext checks the extension. See the sample filter below for usage.
# %RETURNS:
# Nothing
# %DESCRIPTION:
# This function is called once for each part of a MIME message.
# There are many action_*() routines which can decide the fate
# of each part; see the mimedefang-filter man page.
#***********************************************************************
sub filter {
my($entity, $fname, $ext, $type) = @_;
return if message_rejected(); # Avoid unnecessary work
my ($result, $report) = $MPSContentScanner->scan_mimetype($entity);
if ($result == $MailPlusServer::Util::REJECT) {
if ($report =~ /partial/) {
MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Detected and rejected fragmented message section');
} elsif ($report =~ /external/) {
MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Detected and rejected external message body');
}
action_bounce_dangerous_content($report);
return action_discard();
}
return action_accept();
}
#***********************************************************************
# %PROCEDURE: filter_multipart
# %ARGUMENTS:
# entity -- a Mime::Entity object (see MIME-tools documentation for details)
# fname -- the suggested filename, taken from the MIME Content-Disposition:
# header. If no filename was suggested, then fname is ""
# ext -- the file extension (everything from the last period in the name
# to the end of the name, including the period.)
# type -- the MIME type, taken from the Content-Type: header.
# %RETURNS:
# Nothing
# %DESCRIPTION:
# This is called for multipart "container" parts such as message/rfc822.
# You cannot replace the body (because multipart parts have no body),
# but you should check for bad filenames.
#***********************************************************************
sub filter_multipart {
my($entity, $fname, $ext, $type) = @_;
return if message_rejected(); # Avoid unnecessary work
my ($result, $report) = $MPSContentScanner->scan_mimetype($entity);
if ($result == $MailPlusServer::Util::REJECT) {
if ($report =~ /partial/) {
MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Detected and rejected fragmented message section');
} elsif ($report =~ /external/) {
MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Detected and rejected external message body');
}
action_bounce_dangerous_content($report);
return action_discard();
}
return action_accept();
}
#***********************************************************************
# %PROCEDURE: defang_warning
# %ARGUMENTS:
# oldfname -- the old file name of an attachment
# fname -- the new "defanged" name
# %RETURNS:
# A warning message
# %DESCRIPTION:
# This function customizes the warning message when an attachment
# is defanged.
#***********************************************************************
sub defang_warning {
my($oldfname, $fname) = @_;
return
"An attachment named '$oldfname' was converted to '$fname'.\n" .
"To recover the file, right-click on the attachment and Save As\n" .
"'$oldfname'\n";
}
sub filter_end {
my($entity) = @_;
my ($is_spam, $is_virus, $is_mcp, $is_dkim, $is_dmarc);
my ($is_autolearn_ham, $is_autolearn_spam);
my $header = '';
# No sense doing any extra work
return if message_rejected();
# directly accept mail released from quarantine
$header = $entity->head->get($MailPlusServer::Util::HeaderRelease);
if (defined($header) && $header =~ /^yes/m) {
action_delete_all_headers($MailPlusServer::Util::HeaderRelease);
return action_accept();
}
# check headers added by rspamd
$header = $entity->head->get($MailPlusServer::Util::HeaderVirusStatus);
$is_virus = (defined($header) && $header =~ /^yes/m);
$header = $entity->head->get($MailPlusServer::Util::HeaderMCPStatus);
$is_mcp = (defined($header) && $header =~ /^yes/m);
$header = $entity->head->get($MailPlusServer::Util::HeaderSpamFlag);
$is_spam = (defined($header) && $header =~ /^yes/m);
$header = $entity->head->get($MailPlusServer::Util::HeaderDKIM);
$is_dkim = defined($header);
$header = $entity->head->get($MailPlusServer::Util::HeaderDMARC);
$is_dmarc = defined($header);
$header = $entity->head->get($MailPlusServer::Util::HeaderSpamStatus);
$is_autolearn_ham = (defined($header) && $header =~ /autolearn=ham/m);
$is_autolearn_spam = (defined($header) && $header =~ /autolearn=spam/m);
my $subject_prefix = '';
# Attachment filter
foreach my $part ($entity->parts) {
my $header = $part->head();
if ($header->recommended_filename) {
$filename = Encode::decode('MIME-Header', $header->recommended_filename);
if ($filename =~ /$MPSConfig->{'attachment_filter_rule'}/) {
# postfix will send security log
MailPlusServer::Log::ErrorLog('Attachment filename "%s" not allowed', $filename);
# NOTE: The reply 'Attachment type not allowed' is detected in postfix to send security log
return action_bounce('Attachment type not allowed', 550, '5.7.1');
}
}
}
# Check whether it is a report mail
if ($MPSReportMailFilter->collect_report_mails(\@Recipients, $entity)) {
# attached reports were delivered, discard the original mail
return action_discard();
}
# DKIM
if ($is_dkim && $MPSConfig->{'enable_dkim'}) {
MailPlusServer::Log::DKIMLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity);
}
# DMARC
if ($is_dmarc && $MPSConfig->{'enable_dmarc'}) {
MailPlusServer::Log::DMARCLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity);
}
# Content scan
my ($content_scan_action, $content_scan_report) = $MPSContentScanner->scan($entity);
if ($content_scan_action == $MailPlusServer::Util::REJECT) {
MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Detected HTML-specific exploits');
action_bounce_dangerous_content($content_scan_report);
return action_discard();
} elsif ($content_scan_action == $MailPlusServer::Util::ACCEPT_REBUILD) {
my $log_content = '';
if ($MPSConfig->{'convert_html_to_text'}) {
$log_content = 'Converted HTML message to plain text';
} else {
$log_content = 'Detected HTML-specific exploits and disarmed them';
}
if ($content_scan_report ne '') {
$log_content .= ': ' . $content_scan_report;
}
MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, $log_content);
action_rebuild();
}
# Virus processing
if ($is_virus && $MPSConfig->{'anti_virus_enable'}) {
MailPlusServer::Log::VirusLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity);
if ($MPSConfig->{'anti_virus_enable_rewrite'}) {
$subject_prefix .= $MPSConfig->{'anti_virus_rewrite_subject'} . ' ';
}
if ($MPSConfig->{'anti_virus_action'} eq 'delete') {
if ($MPSConfig->{'anti_virus_notify_recipient'}) {
my $admin_addr = ($MPSConfig->{'anti_virus_delete_admin_addr'} eq '') ? $DaemonAddress : $MPSConfig->{'anti_virus_delete_admin_addr'};
MailPlusServer::Util::notify_virus_recipients(
$Sender,
$DaemonName,
$admin_addr,
$entity,
\@Recipients,
$MPSConfig->{'anti_virus_delete_subject'},
$MPSConfig->{'anti_virus_delete_template'}
);
return action_discard();
} else {
return action_discard();
}
} elsif ($MPSConfig->{'anti_virus_action'} eq 'quarantine') {
MailPlusServer::Util::quarantine_virus($entity, $Sender, \@Recipients, $Subject);
if ($MPSConfig->{'anti_virus_notify_recipient'}) {
my $admin_addr = ($MPSConfig->{'anti_virus_quarantine_admin_addr'} eq '') ? $DaemonAddress : $MPSConfig->{'anti_virus_quarantine_admin_addr'};
MailPlusServer::Util::notify_virus_recipients(
$Sender,
$DaemonName,
$admin_addr,
$entity,
\@Recipients,
$MPSConfig->{'anti_virus_quarantine_subject'},
$MPSConfig->{'anti_virus_quarantine_template'}
);
return action_discard();
} else {
return action_discard();
}
}
# keep going when action eq 'deliver'
}
# MCP processing
$header = $entity->head->get($MailPlusServer::Util::HeaderMCPChecked);
my $is_mcp_forwarded_msg = (defined($header) && $header =~ /^yes$/m);
if ($is_mcp && $MPSConfig->{'mcp_enable'} && !$is_mcp_forwarded_msg) {
MailPlusServer::Log::MCPLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, $MPSConfig->{mcp_rules});
if ($MPSConfig->{'mcp_enable_bounce'} && $Sender ne '<>') {
my $admin_addr = ($MPSConfig->{'mcp_bounce_admin_addr'} eq '') ? $DaemonAddress : $MPSConfig->{'mcp_bounce_admin_addr'};
my $msg = MailPlusServer::Util::gen_mcp_bounce_msg(
$DaemonName,
$admin_addr,
$entity,
$Sender,
$MPSConfig->{'mcp_bounce_subject'},
$MPSConfig->{'mcp_bounce_content'}
);
send_mail($admin_addr, $DaemonName, $Sender, $msg);
}
if ($MPSConfig->{'mcp_enable_forward'} && $MPSConfig->{'mcp_forward_to'} ne '') {
# add internal mcp forward header to avoid forward loop
resend_message_one_recipient($MPSConfig->{'mcp_forward_to'}, undef, $MailPlusServer::Util::HeaderMCPChecked . ": yes");
}
if ($MPSConfig->{'mcp_enable_store'}) {
MailPlusServer::Util::quarantine_mcp($entity, $Sender, \@Recipients, MailPlusServer::Util::concate_subject($subject_prefix, $Subject), $MPSConfig->{mcp_rules});
return action_discard();
} elsif ($MPSConfig->{'mcp_enable_delete'}) {
return action_discard();
}
# keep going when action eq 'deliver'
}
# remove internal mcp forward header before the message goes out
action_delete_all_headers($MailPlusServer::Util::HeaderMCPChecked) if defined $entity->head->get($MailPlusServer::Util::HeaderMCPChecked);
# Spam processing
if ($is_spam && $MPSConfig->{'spam_enable'}) {
if (!$is_dkim && !$is_dmarc) {
MailPlusServer::Log::SpamLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity);
}
if (($is_autolearn_ham || $is_autolearn_spam) && $MPSConfig->{'spam_auto_learn'}) {
# autolearn: copy mail to virtual account
my $mail = MailPlusServer::Util::entity_print($entity);
MailPlusServer::Util::lda_send_mail('', $MailPlusServer::Util::AutoLearnAccount, $mail);
}
if ($MPSConfig->{'spam_enable_rewrite'}) {
$subject_prefix .= $MPSConfig->{'spam_rewrite_subject'} . ' ';
}
if ($MPSConfig->{'spam_report_machanism'} == 1) {
# encapsulate spam as attachment
MailPlusServer::Util::encapsulate_spam($entity, $Sender, $Subject);
action_rebuild();
} elsif ($MPSConfig->{'spam_report_machanism'} == 2) {
# convert spam to plaintext
if ($MPSContentScanner->strip_html($entity) > 0) {
action_rebuild();
}
}
}
# Rewrite subject for virus or spam
if ($subject_prefix ne '') {
$Subject = MailPlusServer::Util::concate_subject($subject_prefix, $Subject);
action_change_header('Subject', $Subject);
}
# Check disclaimer rule
my ($has_disclaimer, $disclaimer_txt, $disclaimer_html) = $MPSDisclaimer->get_disclaimer($Sender, \@Recipients);
if ($has_disclaimer) {
append_text_boilerplate($entity, $disclaimer_txt, 0);
append_html_boilerplate($entity, $disclaimer_html, 0);
}
}
# DO NOT delete the next line, or Perl will complain.
1;