# -*- 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);
	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::HeaderSpamStatus);
	$is_autolearn_ham  = (defined($header) && $header =~ /autolearn=ham/m);
	$is_autolearn_spam = (defined($header) && $header =~ /autolearn=spam/m);

	my $subject_prefix = '';

	# 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();
	}

	# 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) {
		if ($MPSConfig->{'convert_html_to_text'}) {
			MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Converted HTML message to plain text');
		} else {
			MailPlusServer::Log::DangerousContentLog($Sender, \@Recipients, $RelayAddr, $Subject, $entity, 'Detected HTML-specific exploits and disarmed them');
		}
		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'}) {
				MailPlusServer::Util::replace_body_by_template($entity, $MPSConfig->{'anti_virus_delete_template'}, $Sender);
				assign_new_msg_id();
				if ($subject_prefix ne '') {
					action_change_header('Subject', $subject_prefix . $Subject);
				}
				return action_rebuild();
			} 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'}) {
				MailPlusServer::Util::replace_body_by_template($entity, $MPSConfig->{'anti_virus_quarantine_template'}, $Sender);
				assign_new_msg_id();
				if ($subject_prefix ne '') {
					action_change_header('Subject', $subject_prefix . $Subject);
				}
				return action_rebuild();
			} else {
				return action_discard();
			}
		}
		# keep going when action eq 'deliver'
	}

	# Spam processing
	if ($is_spam && $MPSConfig->{'spam_enable'}) {
		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 = $subject_prefix . $Subject;
		action_change_header('Subject', $Subject);
	}

	# 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, $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);

	# 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;

