use strict;
use warnings;
+use Cwd;
use DBI;
use Net::Cmd;
use Net::SMTP;
use POSIX qw(strftime);
use File::stat;
use File::Basename;
+use MIME::Entity;
use MIME::Words;
use MIME::Parser;
use Time::HiRes qw (gettimeofday);
no utf8;
use HTML::Entities;
+use JSON;
use PVE::ProcFSTools;
use PVE::Network;
use PMG::AtomicFile;
use PMG::MailQueue;
use PMG::SMTPPrinter;
+use PMG::MIMEUtils;
use base 'Exporter';
PVE::JSONSchema::register_standard_option('pmg-email-address', {
description => "Email Address (allow most characters).",
type => 'string',
- pattern => '(?:[^\s\/\\\@]+\@[^\s\/\\\@]+)',
+ pattern => '(?:[^\s\\\@]+\@[^\s\/\\\@]+)',
maxLength => 512,
minLength => 3,
});
}
sub remove_marks {
- my ($entity, $add_id, $id) = @_;
+ my ($entity, $add_id) = @_;
- $id //= 1;
+ my $id = 1;
- foreach my $tag (grep {/^x-proxmox-tmp/i} $entity->head->tags) {
- $entity->head->delete ($tag);
- }
-
- $entity->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
+ PMG::MIMEUtils::traverse_mime_parts($entity, sub {
+ my ($part) = @_;
+ foreach my $tag (grep {/^x-proxmox-tmp/i} $part->head->tags) {
+ $part->head->delete($tag);
+ }
- foreach my $part ($entity->parts) {
- $id = remove_marks($part, $add_id, $id + 1);
- }
+ $part->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
- return $id;
+ $id++;
+ });
}
sub subst_values {
}
sub reinject_mail {
- my ($entity, $sender, $targets, $xforward, $me, $nodsn) = @_;
+ my ($entity, $sender, $targets, $xforward, $me, $params) = @_;
my $smtp;
my $resid;
my $resmess;
eval {
- my $smtp = Net::SMTP->new('127.0.0.1', Port => 10025, Hello => $me) ||
+ my $smtp = Net::SMTP->new('::FFFF:127.0.0.1', Port => 10025, Hello => $me) ||
die "unable to connect to localhost at port 10025";
if (defined($xforward)) {
$mail_opts .= " SMTPUTF8" if $has_utf8_targets;
}
+ if (defined($params->{mail})) {
+ my $mailparams = $params->{mail};
+ for my $p (keys %$mailparams) {
+ $mail_opts .= " $p=$mailparams->{$p}";
+ }
+ }
+
if (!$smtp->_MAIL("FROM:" . $sender_addr . $mail_opts)) {
syslog('err', "smtp error - got: %s %s", $smtp->code, scalar ($smtp->message));
die "smtp from: ERROR";
}
- my $rcpt_opts = $nodsn ? " NOTIFY=NEVER" : "";
-
foreach my $target (@$targets) {
my $rcpt_addr;
+ my $rcpt_opts = '';
+ if (defined($params->{rcpt}->{$target})) {
+ my $rcptparams = $params->{rcpt}->{$target};
+ for my $p (keys %$rcptparams) {
+ $rcpt_opts .= " $p=$rcptparams->{$p}";
+ }
+ }
+
if (utf8::is_utf8($target)) {
$rcpt_addr = encode('UTF-8', $smtp->_addr($target));
} else {
my $service_aliases = {
'postfix' => 'postfix@-',
- 'postgres' => 'postgresql@11-main',
};
sub lookup_real_service_name {
my $alias = shift;
+ if ($alias eq 'postgres') {
+ my $pg_ver = get_pg_server_version();
+ return "postgresql\@${pg_ver}-main";
+ }
+
return $service_aliases->{$alias} // $alias;
}
}
};
- $service = $service_aliases->{$service} // $service;
+ $service = lookup_real_service_name($service);
PVE::Tools::run_command(['systemctl', 'show', $service], outfunc => $parser);
return $res;
if $cmd !~ m/^(start|stop|restart|reload|reload-or-restart)$/;
if ($service eq 'pmgdaemon' || $service eq 'pmgproxy') {
- die "invalid service cmd '$service $cmd': ERROR" if $cmd eq 'stop';
+ die "invalid service cmd '$service $cmd': refusing to stop essential service!\n"
+ if $cmd eq 'stop';
} elsif ($service eq 'fetchmail') {
# use restart instead of start - else it does not start 'exited' unit
# after setting START_DAEMON=yes in /etc/default/fetchmail
$cmd = 'restart' if $cmd eq 'start';
}
- $service = $service_aliases->{$service} // $service;
+ $service = lookup_real_service_name($service);
PVE::Tools::run_command(['systemctl', $cmd, $service]);
};
-# this is also used to get the IP of the local node
-sub lookup_node_ip {
- my ($nodename, $noerr) = @_;
-
- my ($family, $packed_ip);
-
- eval {
- my @res = PVE::Tools::getaddrinfo_all($nodename);
- $family = $res[0]->{family};
- $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
- };
-
- if ($@) {
- die "hostname lookup failed:\n$@" if !$noerr;
- return undef;
- }
-
- my $ip = Socket::inet_ntop($family, $packed_ip);
- if ($ip =~ m/^127\.|^::1$/) {
- die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
- return undef;
- }
-
- return wantarray ? ($ip, $family) : $ip;
-}
-
sub run_postmap {
my ($filename) = @_;
IO::File->new($filename, 'a', 0644);
my $mtime_src = (CORE::stat($filename))[9] //
- die "unbale to read mtime of $filename\n";
+ die "unable to read mtime of $filename\n";
my $mtime_dst = (CORE::stat("$filename.db"))[9] // 0;
my $header;
my $fh = IO::File->new("<$dbfile");
if (!$fh) {
- warn "cant open ClamAV Database $dbname ($dbfile) - $!\n";
+ warn "can't open ClamAV Database $dbname ($dbfile) - $!\n";
return;
}
$fh->read($header, 512);
$filename = "/var/lib/clamav/bytecode.cvd";
$read_cvd_info->('bytecode', $filename) if -f $filename;
- $filename = "/var/lib/clamav/safebrowsing.cvd";
- $read_cvd_info->('safebrowsing', $filename) if -f $filename;
-
my $ss_dbs_fn = "/var/lib/clamav-unofficial-sigs/configs/ss-include-dbs.txt";
my $ss_dbs_files = {};
if (my $ssfh = IO::File->new("<${ss_dbs_fn}")) {
return if !defined($fh);
while (defined(my $line = <$fh>)) {
- if ($line =~ m/^describe\s+(\S+)\s+(.*)\s*$/) {
+ if ($line =~ m/^(?:\s*)describe\s+(\S+)\s+(.*)\s*$/) {
my ($name, $desc) = ($1, $2);
next if $res->{$name};
$res->{$name}->{desc} = $desc;
$top->print();
return;
}
- # we use an empty envelope sender (we dont want to receive NDRs)
+ # we use an empty envelope sender (we don't want to receive NDRs)
PMG::Utils::reinject_mail ($top, '', [$receiver], undef, $data->{fqdn});
}
my $pregreet_count = 0;
my $parser = sub {
- my $line = shift;
+ my $log = decode_json(shift);
- if ($line =~ m/^--\scursor:\s(\S+)$/) {
- $rbl_scan_last_cursor = $1;
- return;
- }
+ $rbl_scan_last_cursor = $log->{__CURSOR} if defined($log->{__CURSOR});
+
+ my $message = $log->{MESSAGE};
+ return if !defined($message);
- if ($line =~ m/\s$identifier\[\d+\]:\sNOQUEUE:\sreject:.*550 5.7.1 Service unavailable;/) {
+ if ($message =~ m/^NOQUEUE:\sreject:.*550 5.7.1 Service unavailable/) {
$rbl_count++;
- } elsif ($line =~ m/\s$identifier\[\d+\]:\sPREGREET\s\d+\safter\s/) {
+ } elsif ($message =~ m/^PREGREET\s\d+\safter\s/) {
$pregreet_count++;
}
};
# limit to last 5000 lines to avoid long delays
- my $cmd = ['journalctl', '--show-cursor', '-o', 'short-unix', '--no-pager',
- '--identifier', $identifier, '-n', 5000];
+ my $cmd = ['journalctl', '-o', 'json', '--output-fields', '__CURSOR,MESSAGE',
+ '--no-pager', '--identifier', $identifier, '-n', 5000];
if (defined($rbl_scan_last_cursor)) {
push @$cmd, "--after-cursor=${rbl_scan_last_cursor}";
my $save_uid = POSIX::getuid();
my $pg_uid = getpwnam('postgres') || die "getpwnam postgres failed\n";
+ #cd to / to prevent warnings on EPERM (e.g. when running in /root)
+ my $cwd = getcwd() || die "getcwd failed\n";
+ ($cwd) = ($cwd =~ m|^(/.*)$|); #untaint
+ chdir('/') || die "could not chdir to '/'\n";
PVE::Tools::setresuid(-1, $pg_uid, -1) ||
die "setresuid postgres ($pg_uid) failed - $!\n";
PVE::Tools::setresuid(-1, $save_uid, -1) ||
die "setresuid back failed - $!\n";
+
+ chdir("$cwd") || die "could not chdir back to $cwd\n";
}
sub get_pg_server_version {
return $major_ver;
}
+sub reload_smtp_filter {
+
+ my $pid_file = '/run/pmg-smtp-filter.pid';
+ my $pid = PVE::Tools::file_read_firstline($pid_file);
+
+ return 0 if !$pid;
+
+ return 0 if $pid !~ m/^(\d+)$/;
+ $pid = $1; # untaint
+
+ return kill (10, $pid); # send SIGUSR1
+}
+
+sub domain_regex {
+ my ($domains) = @_;
+
+ my @ra;
+ foreach my $d (@$domains) {
+ # skip domains with non-DNS name characters
+ next if $d =~ m/[^A-Za-z0-9\-\.]/;
+ if ($d =~ m/^\.(.*)$/) {
+ my $dom = $1;
+ $dom =~ s/\./\\\./g;
+ push @ra, $dom;
+ push @ra, "\.\*\\.$dom";
+ } else {
+ $d =~ s/\./\\\./g;
+ push @ra, $d;
+ }
+ }
+
+ my $re = join ('|', @ra);
+
+ my $regex = qr/\@($re)$/i;
+
+ return $regex;
+}
+
+sub read_sa_channel {
+ my ($filename) = @_;
+
+ my $content = PVE::Tools::file_get_contents($filename);
+ my $channel = {
+ filename => $filename,
+ };
+
+ ($channel->{keyid}) = ($content =~ /^KEYID=([a-fA-F0-9]+)$/m);
+ die "no KEYID in $filename!\n" if !defined($channel->{keyid});
+ ($channel->{channelurl}) = ($content =~ /^CHANNELURL=(.+)$/m);
+ die "no CHANNELURL in $filename!\n" if !defined($channel->{channelurl});
+ ($channel->{gpgkey}) = ($content =~ /(?:^|\n)(-----BEGIN PGP PUBLIC KEY BLOCK-----.+-----END PGP PUBLIC KEY BLOCK-----)(?:\n|$)/s);
+ die "no GPG public key in $filename!\n" if !defined($channel->{gpgkey});
+
+ return $channel;
+};
+
+sub local_spamassassin_channels {
+
+ my $res = [];
+
+ my $local_channel_dir = '/etc/mail/spamassassin/channel.d/';
+
+ PVE::Tools::dir_glob_foreach($local_channel_dir, '.*\.conf', sub {
+ my ($filename) = @_;
+ my $channel = read_sa_channel($local_channel_dir.$filename);
+ push(@$res, $channel);
+ });
+
+ return $res;
+}
+
+sub update_local_spamassassin_channels {
+ my ($verbose) = @_;
+ # import all configured channel's gpg-keys to sa-update's keyring
+ my $localchannels = PMG::Utils::local_spamassassin_channels();
+ for my $channel (@$localchannels) {
+ my $importcmd = ['sa-update', '--import', $channel->{filename}];
+ push @$importcmd, '-v' if $verbose;
+
+ print "Importing gpg key from $channel->{filename}\n" if $verbose;
+ PVE::Tools::run_command($importcmd);
+ }
+
+ my $fresh_updates = 0;
+
+ for my $channel (@$localchannels) {
+ my $cmd = ['sa-update', '--channel', $channel->{channelurl}, '--gpgkey', $channel->{keyid}];
+ push @$cmd, '-v' if $verbose;
+
+ print "Updating $channel->{channelurl}\n" if $verbose;
+ my $ret = PVE::Tools::run_command($cmd, noerr => 1);
+ die "updating $channel->{channelurl} failed - sa-update exited with $ret\n" if $ret >= 2;
+
+ $fresh_updates = 1 if $ret == 0;
+ }
+
+ return $fresh_updates
+}
+
+sub get_existing_object_id {
+ my ($dbh, $obj_id, $obj_type, $value) = @_;
+
+ my $sth = $dbh->prepare("SELECT id FROM Object WHERE ".
+ "Objectgroup_ID = ? AND ".
+ "ObjectType = ? AND ".
+ "Value = ?"
+ );
+ $sth->execute($obj_id, $obj_type, $value);
+
+ if (my $ref = $sth->fetchrow_hashref()) {
+ return $ref->{id};
+ }
+
+ return;
+}
+
1;