HEX
Server: Apache/2.2.34 (Unix) mod_fastcgi/mod_fastcgi-SNAP-0910052141
System: Linux Kou-Etsu-Dou 4.4.59+ #25556 SMP PREEMPT Thu Mar 4 18:03:46 CST 2021 x86_64
User: hosam (1026)
PHP: 7.2.29
Disabled: NONE
Upload Files
File: /volume1/@appstore/MailPlus-Server/lib/MIMEDefang/MailPlusServer/Util.pm
package MailPlusServer::Util;

use strict;

use JSON;
use Encode qw(encode decode is_utf8);
use MIME::Entity;
use MIME::Words;
use MailPlusServer::Log;
use Net::IDN::Encode qw(domain_to_unicode);
use Mail::Address;

# Action priority: DISCARD > REJECT > ACCEPT_REBUILD > ACCEPT
$MailPlusServer::Util::ACCEPT          = 0;
$MailPlusServer::Util::ACCEPT_REBUILD  = 1;
$MailPlusServer::Util::REJECT          = 2;
$MailPlusServer::Util::DISCARD         = 3;

# Internal accounts
$MailPlusServer::Util::AutoLearnAccount     = 'e14dd245-5d25-41b1-95da-2d1ee815cb89';
$MailPlusServer::Util::MCPAccount           = 'mcp-quarantine-46e2b153-dc70-448b-9ee2-16f0a3b0feb7';
$MailPlusServer::Util::VirusAccount         = 'virus-quarantine-6582509c-61f9-11e7-907b-a6006ad3dba0';

# Internal headers
$MailPlusServer::Util::HeaderSender         = 'X-Synology-Sender';
$MailPlusServer::Util::HeaderRecipients     = 'X-Synology-Recipients';
$MailPlusServer::Util::HeaderVirusReport    = 'X-Synology-VirusReport';
$MailPlusServer::Util::HeaderMCPScore       = 'X-Synology-MCPScore';
$MailPlusServer::Util::HeaderMCPRequired    = 'X-Synology-MCPRequired';
$MailPlusServer::Util::HeaderMCPReport      = 'X-Synology-MCPReport';
@MailPlusServer::Util::QuarantineTmpHeaders = (
	$MailPlusServer::Util::HeaderSender,
	$MailPlusServer::Util::HeaderRecipients,
	$MailPlusServer::Util::HeaderVirusReport,
	$MailPlusServer::Util::HeaderMCPScore,
	$MailPlusServer::Util::HeaderMCPRequired,
	$MailPlusServer::Util::HeaderMCPReport
);
$MailPlusServer::Util::HeaderRelease        = 'X-Synology-Release';
$MailPlusServer::Util::HeaderMCPChecked     = 'X-Synology-MCPChecked';

# Headers added by rspamd
$MailPlusServer::Util::HeaderSpamFlag       = 'X-Synology-Spam-Flag';
$MailPlusServer::Util::HeaderSpamStatus     = 'X-Synology-Spam-Status';
$MailPlusServer::Util::HeaderVirusStatus    = 'X-Synology-Virus-Status';
$MailPlusServer::Util::HeaderMCPStatus      = 'X-Synology-MCP-Status';
$MailPlusServer::Util::HeaderDKIM           = 'X-Synology-DKIM';
$MailPlusServer::Util::HeaderDMARC          = 'X-Synology-DMARC';

sub trim_angle_bracket {
	my($text) = @_;

	$text =~ s/^<//;
	$text =~ s/>$//;

	return $text;
}

sub split_mail_address_to_utf8 {
	my ($addr) = @_;
	my ($local_part, $domain_part) = split('@', $addr);
	$local_part   = '' if not defined($local_part);
	$domain_part = '' if not defined($domain_part);

	$local_part = decode('utf8', $local_part) if !is_utf8($local_part);
	$domain_part = decode('utf8', $domain_part) if !is_utf8($domain_part);

	return ($local_part, $domain_part);
}

sub convert_to_eai_addr {
	my ($addr) = @_;
	my ($local_part, $domain_part) = split_mail_address_to_utf8($addr);

	if ($domain_part ne '') {
		$domain_part = domain_to_unicode($domain_part);
		return $local_part . '@' . $domain_part;
	}

	return $local_part;
}
sub read_json_config {
	my($fname) = @_;
	my $fh;
	my $config = {};
	my $json_str;

	eval {
		open $fh, '<', $fname or die "failed to open file $fname: $!";
		$json_str = do { local $/; <$fh> };
		$config = JSON::decode_json($json_str);
	} or do {
		MailPlusServer::Log::ErrorLog("Failed to load config: $@");
	};
	close $fh if $fh;

	return $config;
}

sub entity_print {
	my($entity) = @_;
	my $content = '';
	my $fh;

	open $fh, '>', \$content or MailPlusServer::Log::ErrorLog("failed to open variable: $!");
	$entity->print($fh);
	close($fh);

	return $content;
}

sub entity_print_head {
	my($entity) = @_;
	my $content = '';
	my $fh;

	open $fh, '>', \$content or MailPlusServer::Log::ErrorLog("failed to open variable: $!");
	$entity->print_header($fh);
	close($fh);

	return $content;
}

sub entity_print_body {
	my($entity) = @_;
	my $content = '';
	my $fh;

	open $fh, '>', \$content or MailPlusServer::Log::ErrorLog("failed to open variable: $!");
	$entity->print_body($fh);
	close($fh);

	return $content;
}

sub encapsulate_spam {
	my($entity, $sender, $subject) = @_;

	$sender = MailPlusServer::Util::trim_angle_bracket($sender);
	$sender = MailPlusServer::Util::convert_to_eai_addr($sender);
	$sender = encode('utf8', $sender);
	$subject = encode('utf8', decode('MIME-Header', $subject));

	my $spam_warn_text = <<END_MESSAGE;
MailPlus Server believes that the attachment to this message sent to you

    From: $sender
 Subject: $subject

is Unsolicited Commercial Email (spam). Unless you are sure that this message
is incorrectly thought to be spam, please delete this message without opening
it. Opening spam messages might allow the spammer to verify your email
address.

If you believe that this message has been incorrectly marked as spam, please
contact the server administrator.
END_MESSAGE

	my $report_part = MIME::Entity->build(
		Type        => 'text/plain',
		Disposition => 'inline',
		Encoding    => 'quoted-printable',
		Top         => 0,
		'X-Mailer'  => undef,
		Charset     => 'UTF-8',
		Data        => $spam_warn_text
	);

	my $mail = MailPlusServer::Util::entity_print($entity);
	my $attachment_part = MIME::Entity->build(
		Type        => 'message/rfc822',
		Disposition => 'attachment',
		Top         => 0,
		'X-Mailer'  => undef,
		Data        => $mail,
	);

	$entity->parts([$report_part, $attachment_part]);

	# update headers
	my $mimeboundary = '';
	my $oldboundary = $entity->head->multipart_boundary;
	do {
		$mimeboundary = '======' . $$ . '==' . int(rand(100000)) . '======';
	} while $mimeboundary eq $oldboundary;

	$entity->make_multipart("report");
	$entity->head->mime_attr("Content-type" => "multipart/report");
	$entity->head->mime_attr("Content-type.boundary" => $mimeboundary);
	$entity->head->mime_attr("Content-type.type" => undef);
	$entity->head->mime_attr("Content-type.report-type" => 'spam-notification');
	$entity->preamble(undef);
	$entity->epilogue(undef);
	$entity->head->add('MIME-Version', '1.0') unless $entity->head->get('mime-version');
}

# modified from mimedefang send_mail()
sub lda_send_mail {
	my($fromAddr, $toAccount, $body) = @_;

	my($pid);

	# Fork and exec for safety instead of involving shell
	$pid = open(CHILD, "|-");
	if (!defined($pid)) {
		MailPlusServer::Log::ErrorLog("Cannot fork to run lda");
		return;
	}

	if ($pid) {   # In the parent -- pipe mail message to the child
		print CHILD $body;
		close(CHILD);
		return ($? == 0);
	}

	# In the child -- invoke Sendmail

	# Direct stdout to stderr, or we will screw up communication with
	# the multiplexor..
	open(STDOUT, ">&STDERR");

	my(@cmd);
	if ($fromAddr ne "") {
		push(@cmd, "-f");
		push(@cmd, "$fromAddr");
	} else {
		# push(@cmd, "-f<>");
	}
	push(@cmd, "-d");
	push(@cmd, $toAccount);

	# In curlies to silence Perl warning...
	my $lda = '/var/packages/MailPlus-Server/target/libexec/dovecot/dovecot-lda-setuid';
	{ exec($lda, @cmd); }

	# exec failed!
	MailPlusServer::Log::ErrorLog("Could not exec lda: $!");
	exit(1);
	# NOTREACHED
}

# modified from mimedefang send_mail()
sub send_mail {
	my($fromAddr, $recipient, $body) = @_;

	my($pid);

	# Fork and exec for safety instead of involving shell
	$pid = open(CHILD, "|-");
	if (!defined($pid)) {
		MailPlusServer::Log::ErrorLog("Cannot fork to run sendmail");
		return;
	}

	if ($pid) {   # In the parent -- pipe mail message to the child
		print CHILD $body;
		close(CHILD);
		return ($? == 0);
	}

	# In the child -- invoke Sendmail

	# Direct stdout to stderr, or we will screw up communication with
	# the multiplexor..
	open(STDOUT, ">&STDERR");

	my(@cmd);
	if ($fromAddr ne "") {
		push(@cmd, "-f$fromAddr");
	} else {
		push(@cmd, "-f<>");
	}
	push(@cmd, "-oi");
	push(@cmd, "--");
	push(@cmd, $recipient);

	# In curlies to silence Perl warning...
	my $sendmail = '/var/packages/MailPlus-Server/target/sbin/sendmail';
	{ exec($sendmail, @cmd); }

	# exec failed!
	# md_syslog('err', "Could not exec $sm: $!");
	exit(1);
	# NOTREACHED
}

sub replace_body_by_template {
	my($entity, $template, $sender) = @_;

	my $date = $entity->head->get('Date');
	my $subject = $entity->head->get('Subject');

	$date = '' if not defined($date);
	$subject = '' if not defined($subject);

	chomp($date);
	chomp($subject);

	$sender = MailPlusServer::Util::trim_angle_bracket($sender);
	$sender = MailPlusServer::Util::convert_to_eai_addr($sender);
	$sender = encode('utf8', $sender);
	$subject = encode('utf8', decode('MIME-Header', $subject));

	$template .= <<END_MESSAGE;

===============================================================
The following is the original mail information:

    From: $sender
 Subject: $subject
    Date: $date
===============================================================
END_MESSAGE
	$template = encode('UTF-8', $template);

	my $report_part = MIME::Entity->build(
		Type        => 'text/plain',
		Disposition => 'inline',
		Encoding    => 'quoted-printable',
		Top         => 0,
		'X-Mailer'  => undef,
		Charset     => 'UTF-8',
		Data        => $template
	);

	$entity->parts([$report_part]);
}

sub gen_virus_notification_msg_entity {
	my($orig_sender, $sender_name, $sender_addr, $entity, $subject, $template) = @_;

	my $orig_to      = '';
	my $orig_date    = '';
	my $orig_subject = '';
	my $first_address = 1;

	if (defined($entity->head->get('To'))) {
		$orig_to = $entity->head->get('To');
		my @addresses = Mail::Address->parse($orig_to);
		foreach my $one_address (@addresses) {
			my $phrase = $one_address->phrase();
			my $address = convert_to_eai_addr($one_address->address());
			my $item = '';

			if ($phrase eq '') {
				$item = $address;
			} else {
				$phrase = decode('MIME-Header', $phrase);
				$phrase = Encode::decode('utf8', $phrase) if (!Encode::is_utf8($phrase));
				$item = $phrase . ' <' . $address . '>';
			}
			if ($first_address eq 1) {
				$orig_to = $item;
				$first_address = 0;
			} else {
				$orig_to .= ', ' . $item;
			}
		}
	}

	if (defined($entity->head->get('Date'))) {
		$orig_date = $entity->head->get('Date');
	}

	if (defined($entity->head->get('Subject'))) {
		$orig_subject = $entity->head->get('Subject');
		$orig_subject = decode('MIME-Header', $orig_subject);
		$orig_subject = Encode::decode('utf8', $orig_subject) if (!Encode::is_utf8($orig_subject));
	}

	chomp($orig_to);
	chomp($orig_date);
	chomp($orig_subject);
	
	my $encode_subject = encode('MIME-Q', $subject);

	$template .= <<END_MESSAGE;

===============================================================
The following is the original mail information:

    From: $orig_sender
 Subject: $orig_subject
    Date: $orig_date
===============================================================
END_MESSAGE
	$template = encode('UTF-8', $template);

	my $report_part = MIME::Entity->build(
		Type        => 'text/plain',
		Disposition => 'inline',
		Encoding    => 'quoted-printable',
		Top         => 0,
		'X-Mailer'  => undef,
		Charset     => 'UTF-8',
		Data        => $template
	);

	my $e = MIME::Entity->build(
		Type        => 'multipart/report',
		From        => "\"$sender_name\" <$sender_addr>",
		Subject     => $subject,
		Charset     => 'UTF-8',
		'X-Mailer'  => undef,
	);

	my $entity_subject = $e->head->get('Subject');

	$e->parts([$report_part]);
	$e->preamble(undef);
	$e->epilogue(undef);

	return $e;
}

sub notify_virus_recipients {
	my($orig_sender, $admin_name, $admin_addr, $entity, $recipients_aref, $subject, $template) = @_;

	my $msg = MailPlusServer::Util::gen_virus_notification_msg_entity(
		$orig_sender,
		$admin_name,
		$admin_addr,
		$entity,
		$subject,
		$template
	);

	my @no_bracket_recipients = map {
		$_ = MailPlusServer::Util::trim_angle_bracket($_);
		$_ = MailPlusServer::Util::convert_to_eai_addr($_);
		$_ = encode('utf8', $_);
	} @{$recipients_aref};

	for my $recipient (@no_bracket_recipients) {
		$msg->head->replace('To', $recipient);
		MailPlusServer::Util::send_mail($admin_addr, $recipient, MailPlusServer::Util::entity_print($msg));
	}
}

sub gen_mcp_bounce_msg {
	my($sender_name, $sender_addr, $entity, $recipient, $subject, $template) = @_;

	my $orig_to      = '';
	my $orig_date    = '';
	my $orig_subject = '';
	my $first_address = 1;

	if (defined($entity->head->get('To'))) {
		$orig_to = $entity->head->get('To');
		my @addresses = Mail::Address->parse($orig_to);
		foreach my $one_address (@addresses) {
			my $phrase = $one_address->phrase();
			my $address = convert_to_eai_addr($one_address->address());
			my $item = '';

			if ($phrase eq '') {
				$item = $address;
			} else {
				$phrase = decode('MIME-Header', $phrase);
				$phrase = Encode::decode('utf8', $phrase) if (!Encode::is_utf8($phrase));
				$item = $phrase . ' <' . $address . '>';
			}
			if ($first_address eq 1) {
				$orig_to = $item;
				$first_address = 0;
			} else {
				$orig_to .= ', ' . $item;
			}
		}
	}

	if (defined($entity->head->get('Date'))) {
		$orig_date = $entity->head->get('Date');
	}

	if (defined($entity->head->get('Subject'))) {
		$orig_subject = $entity->head->get('Subject');
		$orig_subject = decode('MIME-Header', $orig_subject);
		$orig_subject = Encode::decode('utf8', $orig_subject) if (!Encode::is_utf8($orig_subject));
	}

	chomp($orig_to);
	chomp($orig_date);
	chomp($orig_subject);

	$template .= "\n\n" .
		"The following message from you has been banned:\n" .
		"===============================================================\n" .
		"To: $orig_to \n" .
		"Subject: $orig_subject \n" .
		"Date: $orig_date \n" .
		"===============================================================\n\n";
	$template = encode('UTF-8', $template);

	my $report_part = MIME::Entity->build(
		Type        => 'text/plain',
		Disposition => 'inline',
		Encoding    => 'quoted-printable',
		Top         => 0,
		'X-Mailer'  => undef,
		Charset     => 'UTF-8',
		Data        => $template
	);

	my $mail = MailPlusServer::Util::entity_print($entity);
	my $attachment_part = MIME::Entity->build(
		Type        => 'message/rfc822',
		Disposition => 'attachment',
		Top         => 0,
		'X-Mailer'  => undef,
		Data        => $mail,
	);

	my $e = MIME::Entity->build(
		Type        => 'multipart/report',
		From        => "\"$sender_name\" <$sender_addr>",
		To          => "$recipient",
		Subject     => encode('MIME-Q', $subject),
		Charset     => 'UTF-8',
		'X-Mailer'  => undef
	);

	$e->parts([$report_part, $attachment_part]);

	# update headers
	my $mimeboundary = '';
	my $oldboundary = $entity->head->multipart_boundary;
	do {
		$mimeboundary = '======' . $$ . '==' . int(rand(100000)) . '======';
	} while $mimeboundary eq $oldboundary;

	$e->make_multipart("report");
	$e->head->mime_attr("Content-type" => "multipart/report");
	$e->head->mime_attr("Content-type.boundary" => $mimeboundary);
	$e->head->mime_attr("Content-type.type" => undef);
	$e->head->replace($MailPlusServer::Util::HeaderMCPChecked, 'yes');
	$e->preamble(undef);
	$e->epilogue(undef);

	return MailPlusServer::Util::entity_print($e);
}

sub quarantine_virus {
	my($entity, $sender, $recipients_aref, $new_subject) = @_;
	my $account = '';

	my $header = '';
	my $e = $entity->dup;
	$e->head->replace($MailPlusServer::Util::HeaderSender, $sender);
	$e->head->replace($MailPlusServer::Util::HeaderRecipients, join(',', @{$recipients_aref}));
	$e->head->replace('Subject', $new_subject);

	$account = $MailPlusServer::Util::VirusAccount;

	$header = $e->head->get($MailPlusServer::Util::HeaderVirusStatus);
	if (defined($header)) {
		$header =~ s/^yes, //g;
		$e->head->replace($MailPlusServer::Util::HeaderVirusReport, $header);
	}

	my $mail = MailPlusServer::Util::entity_print($e);
	MailPlusServer::Util::lda_send_mail('', $account, $mail);
}

sub quarantine_mcp {
	my($entity, $sender, $recipients_aref, $new_subject, $rules_aref) = @_;
	my $account = '';

	my $header = '';
	my $e = $entity->dup;
	$e->head->replace($MailPlusServer::Util::HeaderSender, $sender);
	$e->head->replace($MailPlusServer::Util::HeaderRecipients, join(',', @{$recipients_aref}));
	$e->head->replace('Subject', $new_subject);

	$account = $MailPlusServer::Util::MCPAccount;

	$header = $e->head->get($MailPlusServer::Util::HeaderMCPStatus);
	if (defined($header) && $header =~ /yes, score=(\d*), required[ =](\d*), (.*)/) {
		$e->head->replace($MailPlusServer::Util::HeaderMCPScore, $1);
		$e->head->replace($MailPlusServer::Util::HeaderMCPRequired, $2);
	}

	# save mcp rule names for ui reporting
	my $mcp_report = '';
	# FIXME: need to handle folding header
	my @mcp_rules = split(', ', $3);
	foreach my $mcp_rule (@mcp_rules) {
		my $mcp_rule_name = $mcp_rule;
		if ($mcp_rule =~ /SYNO_MCP_(\d*)/ && $1 < scalar(@{$rules_aref})) {
			$mcp_rule_name = $rules_aref->[$1]->{name};
		}

		if ($mcp_report ne '') {
			$mcp_report .= ', ';
		}
		$mcp_report .= $mcp_rule_name;
	}
	$e->head->replace($MailPlusServer::Util::HeaderMCPReport, encode('MIME-Header', $mcp_report));

	my $mail = MailPlusServer::Util::entity_print($e);
	MailPlusServer::Util::lda_send_mail('', $account, $mail);
}

sub concate_subject {
	my($subject_prefix, $subject) = @_;

	# Encoding of $subject_prefix may not be 'utf8', need to encode to 'utf8' first.
	# MIME encoding to quoted printable is not needed as our mail server & client can handle UTF-8 subject.
	my $utf8_subject_prefix = encode('utf8', $subject_prefix);

	# To leave original $subject as-is, we need to determine if subject prefix need to MIME-encode.
	# Check if $subject is MIME-encoded
	if (MIME::Words::decode_mimewords($subject) eq $subject) {
		# $subject is not MIME-encoded, do not need to MIME-encode subject prefix
		return $utf8_subject_prefix . $subject;
	} else {
		# $subject is MIME-encoded, need to MIME-encode subject prefix
		return MIME::Words::encode_mimewords($utf8_subject_prefix, 'Charset' => 'UTF-8') . $subject;
	}
}

1;