1 package PMG
::Config
::Base
;
9 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::SectionConfig
;
12 use base
qw(PVE::SectionConfig);
16 type
=> { description
=> "Section type." },
18 description
=> "Section ID.",
19 type
=> 'string', format
=> 'pve-configid',
28 sub format_section_header
{
29 my ($class, $type, $sectionId) = @_;
31 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
33 return "section: $type\n";
37 sub parse_section_header
{
38 my ($class, $line) = @_;
40 if ($line =~ m/^section:\s*(\S+)\s*$/) {
42 my $errmsg = undef; # set if you want to skip whole section
43 eval { PVE
::JSONSchema
::pve_verify_configid
($section); };
45 my $config = {}; # to return additional attributes
46 return ($section, $section, $errmsg, $config);
51 package PMG
::Config
::Admin
;
56 use base
qw(PMG::Config::Base);
65 description
=> "Use advanced filters for statistic.",
70 description
=> "Send daily reports.",
75 description
=> "User Statistics Lifetime (days)",
81 description
=> "Demo mode - do not start SMTP filter.",
86 description
=> "Administrator E-Mail address.",
87 type
=> 'string', format
=> 'email',
88 default => 'admin@domain.tld',
91 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
93 pattern
=> "http://.*",
96 description
=> "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
101 description
=> "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
106 description
=> "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
110 custom_check_path
=> {
111 description
=> "Absolute Path to the Custom Check Script",
112 type
=> 'string', pattern
=> '^/([^/\0]+\/)+[^/\0]+$',
113 default => '/usr/local/bin/pmg-custom-check',
116 description
=> "DKIM sign outbound mails with the configured Selector.",
120 dkim_sign_all_mail
=> {
121 description
=> "DKIM sign all outgoing mails irrespective of the Envelope From domain.",
126 description
=> "Default DKIM selector",
127 type
=> 'string', format
=> 'dns-name', #see RFC6376 3.1
134 advfilter
=> { optional
=> 1 },
135 avast
=> { optional
=> 1 },
136 clamav
=> { optional
=> 1 },
137 statlifetime
=> { optional
=> 1 },
138 dailyreport
=> { optional
=> 1 },
139 demo
=> { optional
=> 1 },
140 email
=> { optional
=> 1 },
141 http_proxy
=> { optional
=> 1 },
142 custom_check
=> { optional
=> 1 },
143 custom_check_path
=> { optional
=> 1 },
144 dkim_sign
=> { optional
=> 1 },
145 dkim_sign_all_mail
=> { optional
=> 1 },
146 dkim_selector
=> { optional
=> 1 },
150 package PMG
::Config
::Spam
;
155 use base
qw(PMG::Config::Base);
164 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
166 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
170 description
=> "Whether to use the naive-Bayesian-style classifier.",
175 description
=> "Use the Auto-Whitelist plugin.",
180 description
=> "Whether to use Razor2, if it is available.",
184 wl_bounce_relays
=> {
185 description
=> "Whitelist legitimate bounce relays.",
188 clamav_heuristic_score
=> {
189 description
=> "Score for ClamAV heuristics (Encrypted Archives/Documents, Google Safe Browsing database, PhishingScanURLs, ...).",
196 description
=> "Additional score for bounce mails.",
203 description
=> "Enable real time blacklists (RBL) checks.",
208 description
=> "Maximum size of spam messages in bytes.",
218 use_awl
=> { optional
=> 1 },
219 use_razor
=> { optional
=> 1 },
220 wl_bounce_relays
=> { optional
=> 1 },
221 languages
=> { optional
=> 1 },
222 use_bayes
=> { optional
=> 1 },
223 clamav_heuristic_score
=> { optional
=> 1 },
224 bounce_score
=> { optional
=> 1 },
225 rbl_checks
=> { optional
=> 1 },
226 maxspamsize
=> { optional
=> 1 },
230 package PMG
::Config
::SpamQuarantine
;
235 use base
qw(PMG::Config::Base);
244 description
=> "Quarantine life time (days)",
250 description
=> "Authentication mode to access the quarantine interface. Mode 'ticket' allows login using tickets sent with the daily spam report. Mode 'ldap' requires to login using an LDAP account. Finally, mode 'ldapticket' allows both ways.",
252 enum
=> [qw(ticket ldap ldapticket)],
256 description
=> "Spam report style.",
258 enum
=> [qw(none short verbose custom)],
259 default => 'verbose',
262 description
=> "Allow to view images.",
267 description
=> "Allow to view hyperlinks.",
272 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
273 type
=> 'string', format
=> 'address',
276 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
283 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
285 enum
=> [qw(http https)],
289 description
=> "Text for 'From' header in daily spam report mails.",
297 mailfrom
=> { optional
=> 1 },
298 hostname
=> { optional
=> 1 },
299 lifetime
=> { optional
=> 1 },
300 authmode
=> { optional
=> 1 },
301 reportstyle
=> { optional
=> 1 },
302 viewimages
=> { optional
=> 1 },
303 allowhrefs
=> { optional
=> 1 },
304 port
=> { optional
=> 1 },
305 protocol
=> { optional
=> 1 },
309 package PMG
::Config
::VirusQuarantine
;
314 use base
qw(PMG::Config::Base);
326 lifetime
=> { optional
=> 1 },
327 viewimages
=> { optional
=> 1 },
328 allowhrefs
=> { optional
=> 1 },
332 package PMG
::Config
::ClamAV
;
337 use base
qw(PMG::Config::Base);
346 description
=> "ClamAV database mirror server.",
348 default => 'database.clamav.net',
350 archiveblockencrypted
=> {
351 description
=> "Whether to mark encrypted archives and documents as heuristic virus match. A match does not necessarily result in an immediate block, it just raises the Spam Score by 'clamav_heuristic_score'.",
356 description
=> "Nested archives are scanned recursively, e.g. if a ZIP archive contains a TAR file, all files within it will also be scanned. This options specifies how deeply the process should be continued. Warning: setting this limit too high may result in severe damage to the system.",
362 description
=> "Number of files to be scanned within an archive, a document, or any other kind of container. Warning: disabling this limit or setting it too high may result in severe damage to the system.",
368 description
=> "Files larger than this limit (in bytes) won't be scanned.",
374 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
377 default => 100000000,
380 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
386 description
=> "Enables support for Google Safe Browsing.",
391 description
=> "Enables ScriptedUpdates (incremental download of signatures)",
400 archiveblockencrypted
=> { optional
=> 1 },
401 archivemaxrec
=> { optional
=> 1 },
402 archivemaxfiles
=> { optional
=> 1 },
403 archivemaxsize
=> { optional
=> 1 },
404 maxscansize
=> { optional
=> 1 },
405 dbmirror
=> { optional
=> 1 },
406 maxcccount
=> { optional
=> 1 },
407 safebrowsing
=> { optional
=> 1 },
408 scriptedupdates
=> { optional
=> 1},
412 package PMG
::Config
::Mail
;
417 use PVE
::ProcFSTools
;
419 use base
qw(PMG::Config::Base);
426 sub physical_memory
{
428 return $physicalmem if $physicalmem;
430 my $info = PVE
::ProcFSTools
::read_meminfo
();
431 my $total = int($info->{memtotal
} / (1024*1024));
436 sub get_max_filters
{
437 # estimate optimal number of filter servers
441 my $memory = physical_memory
();
442 my $add_servers = int(($memory - 512)/$servermem);
443 $max_servers += $add_servers if $add_servers > 0;
444 $max_servers = 40 if $max_servers > 40;
446 return $max_servers - 2;
450 # estimate optimal number of smtpd daemons
452 my $max_servers = 25;
454 my $memory = physical_memory
();
455 my $add_servers = int(($memory - 512)/$servermem);
456 $max_servers += $add_servers if $add_servers > 0;
457 $max_servers = 100 if $max_servers > 100;
462 # estimate optimal number of proxpolicy servers
464 my $memory = physical_memory
();
465 $max_servers = 5 if $memory >= 500;
472 description
=> "SMTP port number for outgoing mail (trusted).",
479 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
486 description
=> "The default mail delivery transport (incoming mails).",
487 type
=> 'string', format
=> 'address',
490 description
=> "Transport protocol for relay host.",
492 enum
=> [qw(smtp lmtp)],
496 description
=> "SMTP/LMTP port number for relay host.",
503 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
508 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
509 type
=> 'string', format
=> 'address',
512 description
=> "SMTP port number for smarthost.",
519 description
=> "ESMTP banner.",
522 default => 'ESMTP Proxmox',
525 description
=> "Maximum number of pmg-smtp-filter processes.",
529 default => get_max_filters
(),
532 description
=> "Maximum number of pmgpolicy processes.",
536 default => get_max_policy
(),
539 description
=> "Maximum number of SMTP daemon processes (in).",
543 default => get_max_smtpd
(),
546 description
=> "Maximum number of SMTP daemon processes (out).",
550 default => get_max_smtpd
(),
552 conn_count_limit
=> {
553 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
559 description
=> "The maximal number of connection attempts any client is allowed to make to this service per minute. To disable this feature, specify a limit of 0.",
564 message_rate_limit
=> {
565 description
=> "The maximal number of message delivery requests that any client is allowed to make to this service per minute.To disable this feature, specify a limit of 0.",
571 description
=> "Hide received header in outgoing mails.",
576 description
=> "Maximum email size. Larger mails are rejected.",
579 default => 1024*1024*10,
582 description
=> "SMTP delay warning time (in hours).",
588 description
=> "Enable TLS.",
593 description
=> "Enable TLS Logging.",
598 description
=> "Add TLS received header.",
603 description
=> "Use Sender Policy Framework.",
608 description
=> "Use Greylisting.",
613 description
=> "Use SMTP HELO tests.",
618 description
=> "Reject unknown clients.",
622 rejectunknownsender
=> {
623 description
=> "Reject unknown senders.",
628 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
630 enum
=> ['450', '550'],
633 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
634 type
=> 'string', format
=> 'dnsbl-entry-list',
637 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
642 before_queue_filtering
=> {
643 description
=> "Enable before queue filtering by pmg-smtp-filter",
648 description
=> "Send out NDR when mail gets blocked",
657 int_port
=> { optional
=> 1 },
658 ext_port
=> { optional
=> 1 },
659 smarthost
=> { optional
=> 1 },
660 smarthostport
=> { optional
=> 1 },
661 relay
=> { optional
=> 1 },
662 relayprotocol
=> { optional
=> 1 },
663 relayport
=> { optional
=> 1 },
664 relaynomx
=> { optional
=> 1 },
665 dwarning
=> { optional
=> 1 },
666 max_smtpd_in
=> { optional
=> 1 },
667 max_smtpd_out
=> { optional
=> 1 },
668 greylist
=> { optional
=> 1 },
669 helotests
=> { optional
=> 1 },
670 tls
=> { optional
=> 1 },
671 tlslog
=> { optional
=> 1 },
672 tlsheader
=> { optional
=> 1 },
673 spf
=> { optional
=> 1 },
674 maxsize
=> { optional
=> 1 },
675 banner
=> { optional
=> 1 },
676 max_filters
=> { optional
=> 1 },
677 max_policy
=> { optional
=> 1 },
678 hide_received
=> { optional
=> 1 },
679 rejectunknown
=> { optional
=> 1 },
680 rejectunknownsender
=> { optional
=> 1 },
681 conn_count_limit
=> { optional
=> 1 },
682 conn_rate_limit
=> { optional
=> 1 },
683 message_rate_limit
=> { optional
=> 1 },
684 verifyreceivers
=> { optional
=> 1 },
685 dnsbl_sites
=> { optional
=> 1 },
686 dnsbl_threshold
=> { optional
=> 1 },
687 before_queue_filtering
=> { optional
=> 1 },
688 ndr_on_block
=> { optional
=> 1 },
701 use PVE
::Tools
qw($IPV4RE $IPV6RE);
708 PMG
::Config
::Admin-
>register();
709 PMG
::Config
::Mail-
>register();
710 PMG
::Config
::SpamQuarantine-
>register();
711 PMG
::Config
::VirusQuarantine-
>register();
712 PMG
::Config
::Spam-
>register();
713 PMG
::Config
::ClamAV-
>register();
715 # initialize all plugins
716 PMG
::Config
::Base-
>init();
718 PVE
::JSONSchema
::register_format
(
719 'transport-domain', \
&pmg_verify_transport_domain
);
721 sub pmg_verify_transport_domain
{
722 my ($name, $noerr) = @_;
724 # like dns-name, but can contain leading dot
725 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
727 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
728 return undef if $noerr;
729 die "value does not look like a valid transport domain\n";
734 PVE
::JSONSchema
::register_format
(
735 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
737 sub pmg_verify_transport_domain_or_email
{
738 my ($name, $noerr) = @_;
740 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
743 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
747 # like dns-name, but can contain leading dot
748 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
749 return undef if $noerr;
750 die "value does not look like a valid transport domain or email address\n";
755 PVE
::JSONSchema
::register_format
(
756 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
758 sub pmg_verify_dnsbl_entry
{
759 my ($name, $noerr) = @_;
761 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
762 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
763 # we don't implement the ';' separated numbers in pattern, because this
764 # breaks at PVE::JSONSchema::split_list
765 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
767 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
768 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
769 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
770 return undef if $noerr;
771 die "value '$name' does not look like a valid dnsbl entry\n";
779 my $class = ref($type) || $type;
781 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
783 return bless $cfg, $class;
789 PVE
::INotify
::write_file
("pmg.conf", $self);
792 my $lockfile = "/var/lock/pmgconfig.lck";
795 my ($code, $errmsg) = @_;
797 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
799 $errmsg ?
die "$errmsg: $err" : die $err;
805 my ($self, $section, $key, $value) = @_;
807 my $pdata = PMG
::Config
::Base-
>private();
809 my $plugin = $pdata->{plugins
}->{$section};
810 die "no such section '$section'" if !$plugin;
812 if (defined($value)) {
813 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
814 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
815 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
817 if (defined($self->{ids
}->{$section})) {
818 delete $self->{ids
}->{$section}->{$key};
825 # get section value or default
827 my ($self, $section, $key, $nodefault) = @_;
829 my $pdata = PMG
::Config
::Base-
>private();
830 my $pdesc = $pdata->{propertyList
}->{$key};
831 die "no such property '$section/$key'\n"
832 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
833 defined($pdata->{options
}->{$section}->{$key}));
835 if (defined($self->{ids
}->{$section}) &&
836 defined(my $value = $self->{ids
}->{$section}->{$key})) {
840 return undef if $nodefault;
842 return $pdesc->{default};
845 # get a whole section with default value
847 my ($self, $section) = @_;
849 my $pdata = PMG
::Config
::Base-
>private();
850 return undef if !defined($pdata->{options
}->{$section});
854 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
856 my $pdesc = $pdata->{propertyList
}->{$key};
858 if (defined($self->{ids
}->{$section}) &&
859 defined(my $value = $self->{ids
}->{$section}->{$key})) {
860 $res->{$key} = $value;
863 $res->{$key} = $pdesc->{default};
869 # get a whole config with default values
873 my $pdata = PMG
::Config
::Base-
>private();
877 foreach my $type (keys %{$pdata->{plugins
}}) {
878 my $plugin = $pdata->{plugins
}->{$type};
879 $res->{$type} = $self->get_section($type);
886 my ($filename, $fh) = @_;
888 local $/ = undef; # slurp mode
890 my $raw = <$fh> if defined($fh);
892 return PMG
::Config
::Base-
>parse_config($filename, $raw);
896 my ($filename, $fh, $cfg) = @_;
898 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
900 PVE
::Tools
::safe_print
($filename, $fh, $raw);
903 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
906 undef, always_call_parser
=> 1);
908 # parsers/writers for other files
910 my $domainsfilename = "/etc/pmg/domains";
912 sub postmap_pmg_domains
{
913 PMG
::Utils
::run_postmap
($domainsfilename);
916 sub read_pmg_domains
{
917 my ($filename, $fh) = @_;
923 while (defined(my $line = <$fh>)) {
925 next if $line =~ m/^\s*$/;
926 if ($line =~ m/^#(.*)\s*$/) {
930 if ($line =~ m/^(\S+)\s.*$/) {
932 $domains->{$domain} = {
933 domain
=> $domain, comment
=> $comment };
936 warn "parse error in '$filename': $line\n";
945 sub write_pmg_domains
{
946 my ($filename, $fh, $domains) = @_;
948 foreach my $domain (sort keys %$domains) {
949 my $comment = $domains->{$domain}->{comment
};
950 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
951 if defined($comment) && $comment !~ m/^\s*$/;
953 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
957 PVE
::INotify
::register_file
('domains', $domainsfilename,
960 undef, always_call_parser
=> 1);
962 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
964 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
967 undef, always_call_parser
=> 1);
969 my $mynetworks_filename = "/etc/pmg/mynetworks";
971 sub read_pmg_mynetworks
{
972 my ($filename, $fh) = @_;
978 while (defined(my $line = <$fh>)) {
980 next if $line =~ m/^\s*$/;
981 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
982 my ($network, $prefix_size, $comment) = ($1, $2, $3);
983 my $cidr = "$network/${prefix_size}";
984 $mynetworks->{$cidr} = {
986 network_address
=> $network,
987 prefix_size
=> $prefix_size,
988 comment
=> $comment // '',
991 warn "parse error in '$filename': $line\n";
999 sub write_pmg_mynetworks
{
1000 my ($filename, $fh, $mynetworks) = @_;
1002 foreach my $cidr (sort keys %$mynetworks) {
1003 my $data = $mynetworks->{$cidr};
1004 my $comment = $data->{comment
} // '*';
1005 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1009 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1010 \
&read_pmg_mynetworks
,
1011 \
&write_pmg_mynetworks
,
1012 undef, always_call_parser
=> 1);
1014 PVE
::JSONSchema
::register_format
(
1015 'tls-policy', \
&pmg_verify_tls_policy
);
1017 # TODO: extend to parse attributes of the policy
1018 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1019 sub pmg_verify_tls_policy
{
1020 my ($policy, $noerr) = @_;
1022 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1023 return undef if $noerr;
1024 die "value '$policy' does not look like a valid tls policy\n";
1029 PVE
::JSONSchema
::register_format
(
1030 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1032 sub pmg_verify_tls_policy_strict
{
1033 my ($policy, $noerr) = @_;
1035 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1036 return undef if $noerr;
1037 die "value '$policy' does not look like a valid tls policy\n";
1042 PVE
::JSONSchema
::register_format
(
1043 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1045 sub pmg_verify_transport_domain_or_nexthop
{
1046 my ($name, $noerr) = @_;
1048 if (pmg_verify_transport_domain
($name, 1)) {
1050 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1052 if ($nexthop =~ m/^\[(.*)\]$/) {
1055 return $name if pmg_verify_transport_address
($nexthop, 1);
1057 return undef if $noerr;
1058 die "value does not look like a valid domain or next-hop\n";
1062 sub read_tls_policy
{
1063 my ($filename, $fh) = @_;
1065 return {} if !defined($fh);
1067 my $tls_policy = {};
1069 while (defined(my $line = <$fh>)) {
1071 next if $line =~ m/^\s*$/;
1072 next if $line =~ m/^#(.*)\s*$/;
1074 my $parse_error = sub {
1076 die "parse error in '$filename': $line - $err";
1079 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1080 my ($destination, $policy) = ($1, $2);
1083 pmg_verify_transport_domain_or_nexthop
($destination);
1084 pmg_verify_tls_policy
($policy);
1087 $parse_error->($err);
1091 $tls_policy->{$destination} = {
1092 destination
=> $destination,
1096 $parse_error->('wrong format');
1103 sub write_tls_policy
{
1104 my ($filename, $fh, $tls_policy) = @_;
1106 return if !$tls_policy;
1108 foreach my $destination (sort keys %$tls_policy) {
1109 my $entry = $tls_policy->{$destination};
1110 PVE
::Tools
::safe_print
(
1111 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1115 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1116 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1119 undef, always_call_parser
=> 1);
1121 sub postmap_tls_policy
{
1122 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1125 my $transport_map_filename = "/etc/pmg/transport";
1127 sub postmap_pmg_transport
{
1128 PMG
::Utils
::run_postmap
($transport_map_filename);
1131 PVE
::JSONSchema
::register_format
(
1132 'transport-address', \
&pmg_verify_transport_address
);
1134 sub pmg_verify_transport_address
{
1135 my ($name, $noerr) = @_;
1137 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1139 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1142 return undef if $noerr;
1143 die "value does not look like a valid address\n";
1147 sub read_transport_map
{
1148 my ($filename, $fh) = @_;
1150 return [] if !defined($fh);
1156 while (defined(my $line = <$fh>)) {
1158 next if $line =~ m/^\s*$/;
1159 if ($line =~ m/^#(.*)\s*$/) {
1164 my $parse_error = sub {
1166 warn "parse error in '$filename': $line - $err";
1170 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1171 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1173 eval { pmg_verify_transport_domain_or_email
($domain); };
1175 $parse_error->($err);
1179 if ($host =~ m/^\[(.*)\]$/) {
1183 $use_mx = 0 if ($protocol eq "lmtp");
1185 eval { pmg_verify_transport_address
($host); };
1187 $parse_error->($err);
1193 protocol
=> $protocol,
1197 comment
=> $comment,
1199 $res->{$domain} = $data;
1202 $parse_error->('wrong format');
1209 sub write_transport_map
{
1210 my ($filename, $fh, $tmap) = @_;
1214 foreach my $domain (sort keys %$tmap) {
1215 my $data = $tmap->{$domain};
1217 my $comment = $data->{comment
};
1218 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1219 if defined($comment) && $comment !~ m/^\s*$/;
1221 my $bracket_host = !$data->{use_mx
};
1223 if ($data->{protocol
} eq 'lmtp') {
1225 $data->{protocol
} .= ":inet";
1227 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1228 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1230 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1234 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1235 \
&read_transport_map
,
1236 \
&write_transport_map
,
1237 undef, always_call_parser
=> 1);
1239 # config file generation using templates
1241 sub get_host_dns_info
{
1245 my $nodename = PVE
::INotify
::nodename
();
1247 $dnsinfo->{hostname
} = $nodename;
1248 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1250 my $domain = $resolv->{search
} // 'localdomain';
1251 $dnsinfo->{domain
} = $domain;
1253 $dnsinfo->{fqdn
} = "$nodename.$domain";
1258 sub get_template_vars
{
1261 my $vars = { pmg
=> $self->get_config() };
1263 my $dnsinfo = get_host_dns_info
();
1264 $vars->{dns
} = $dnsinfo;
1265 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1266 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1268 my $transportnets = [];
1270 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1271 foreach my $domain (sort keys %$tmap) {
1272 my $data = $tmap->{$domain};
1273 my $host = $data->{host
};
1274 if ($host =~ m/^$IPV4RE$/) {
1275 push @$transportnets, "$host/32";
1276 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1277 push @$transportnets, "[$1]/128";
1282 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1284 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1286 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1287 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1288 push @$mynetworks, "[$1]/$2";
1290 push @$mynetworks, $int_net_cidr;
1293 if ($int_ip =~ m/^$IPV6RE$/) {
1294 push @$mynetworks, "[$int_ip]/128";
1296 push @$mynetworks, "$int_ip/32";
1300 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1301 foreach my $cidr (sort keys %$netlist) {
1302 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1303 push @$mynetworks, "[$1]/$2";
1305 push @$mynetworks, $cidr;
1309 push @$mynetworks, @$transportnets;
1311 # add default relay to mynetworks
1312 if (my $relay = $self->get('mail', 'relay')) {
1313 if ($relay =~ m/^$IPV4RE$/) {
1314 push @$mynetworks, "$relay/32";
1315 } elsif ($relay =~ m/^$IPV6RE$/) {
1316 push @$mynetworks, "[$relay]/128";
1318 # DNS name - do nothing ?
1322 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1324 # normalize dnsbl_sites
1325 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1326 if (scalar(@dnsbl_sites)) {
1327 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1330 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1333 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1334 $self->get('mail', 'spf');
1335 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1337 if ($int_ip =~ m/^$IPV6RE$/) {
1338 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1340 $vars->{postfix
}->{int_ip
} = $int_ip;
1343 my $wlbr = $dnsinfo->{fqdn
};
1344 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1347 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1349 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1351 my $uri = URI-
>new($proxy);
1352 my $host = $uri->host;
1353 my $port = $uri->port // 8080;
1355 my $data = { host
=> $host, port
=> $port };
1356 if (my $ui = $uri->userinfo) {
1357 my ($username, $pw) = split(/:/, $ui, 2);
1358 $data->{username
} = $username;
1359 $data->{password
} = $pw if defined($pw);
1361 $vars->{proxy
} = $data;
1364 warn "parse http_proxy failed - $@" if $@;
1366 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1371 # use one global TT cache
1372 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1374 my $template_toolkit;
1376 sub get_template_toolkit
{
1378 return $template_toolkit if $template_toolkit;
1380 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1382 return $template_toolkit;
1385 # rewrite file from template
1386 # return true if file has changed
1387 sub rewrite_config_file
{
1388 my ($self, $tmplname, $dstfn) = @_;
1390 my $demo = $self->get('admin', 'demo');
1393 my $demosrc = "$tmplname.demo";
1394 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1397 my ($perm, $uid, $gid);
1399 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1400 # needed if file contains a HTTPProxyPasswort
1402 $uid = getpwnam('clamav');
1403 $gid = getgrnam('adm');
1407 my $tt = get_template_toolkit
();
1409 my $vars = $self->get_template_vars();
1413 $tt->process($tmplname, $vars, \
$output) ||
1414 die $tt->error() . "\n";
1416 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1418 return 0 if defined($old) && ($old eq $output); # no change
1420 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1422 if (defined($uid) && defined($gid)) {
1423 chown($uid, $gid, $dstfn);
1429 # rewrite spam configuration
1430 sub rewrite_config_spam
{
1433 my $use_awl = $self->get('spam', 'use_awl');
1434 my $use_bayes = $self->get('spam', 'use_bayes');
1435 my $use_razor = $self->get('spam', 'use_razor');
1439 # delete AW and bayes databases if those features are disabled
1441 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1445 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1446 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1447 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1450 # make sure we have the custom SA files (else cluster sync fails)
1451 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1452 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1454 $changes = 1 if $self->rewrite_config_file(
1455 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1457 $changes = 1 if $self->rewrite_config_file(
1458 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1460 $changes = 1 if $self->rewrite_config_file(
1461 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1463 $changes = 1 if $self->rewrite_config_file(
1464 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1467 mkdir "/root/.razor";
1469 $changes = 1 if $self->rewrite_config_file(
1470 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1472 if (! -e
'/root/.razor/identity') {
1475 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1476 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1479 syslog
('info', "registering razor failed: $err") if $err;
1486 # rewrite ClamAV configuration
1487 sub rewrite_config_clam
{
1490 return $self->rewrite_config_file(
1491 'clamd.conf.in', '/etc/clamav/clamd.conf');
1494 sub rewrite_config_freshclam
{
1497 return $self->rewrite_config_file(
1498 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1501 sub rewrite_config_postgres
{
1504 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1505 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1509 $changes = 1 if $self->rewrite_config_file(
1510 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1512 $changes = 1 if $self->rewrite_config_file(
1513 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1518 # rewrite /root/.forward
1519 sub rewrite_dot_forward
{
1522 my $dstfn = '/root/.forward';
1524 my $email = $self->get('admin', 'email');
1527 if ($email && $email =~ m/\s*(\S+)\s*/) {
1530 # empty .forward does not forward mails (see man local)
1533 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1535 return 0 if defined($old) && ($old eq $output); # no change
1537 PVE
::Tools
::file_set_contents
($dstfn, $output);
1542 my $write_smtp_whitelist = sub {
1543 my ($filename, $data, $action) = @_;
1545 $action = 'OK' if !$action;
1547 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1551 foreach my $k (sort keys %$data) {
1552 $new .= "$k $action\n";
1555 return 0 if defined($old) && ($old eq $new); # no change
1557 PVE
::Tools
::file_set_contents
($filename, $new);
1559 PMG
::Utils
::run_postmap
($filename);
1564 sub rewrite_postfix_whitelist
{
1565 my ($rulecache) = @_;
1567 # see man page for regexp_table for postfix regex table format
1569 # we use a hash to avoid duplicate entries in regex tables
1572 my $clientlist = {};
1574 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1575 my $oclass = ref($obj);
1576 if ($oclass eq 'PMG::RuleDB::Receiver') {
1577 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1578 $tolist->{"/^$addr\$/"} = 1;
1579 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1580 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1581 $tolist->{"/^.+\@$addr\$/"} = 1;
1582 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1583 my $addr = $obj->{address
};
1585 $tolist->{"/^$addr\$/"} = 1;
1589 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1590 my $oclass = ref($obj);
1591 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1592 if ($oclass eq 'PMG::RuleDB::EMail') {
1593 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1594 $fromlist->{"/^$addr\$/"} = 1;
1595 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1596 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1597 $fromlist->{"/^.+\@$addr\$/"} = 1;
1598 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1599 my $addr = $obj->{address
};
1601 $fromlist->{"/^$addr\$/"} = 1;
1602 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1603 $clientlist->{$obj->{address
}} = 1;
1604 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1605 $clientlist->{$obj->{address
}} = 1;
1609 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1610 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1611 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1612 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1615 # rewrite /etc/postfix/*
1616 sub rewrite_config_postfix
{
1617 my ($self, $rulecache) = @_;
1619 # make sure we have required files (else postfix start fails)
1620 IO
::File-
>new($transport_map_filename, 'a', 0644);
1624 if ($self->get('mail', 'tls')) {
1626 PMG
::Utils
::gen_proxmox_tls_cert
();
1628 syslog
('info', "generating certificate failed: $@") if $@;
1631 $changes = 1 if $self->rewrite_config_file(
1632 'main.cf.in', '/etc/postfix/main.cf');
1634 $changes = 1 if $self->rewrite_config_file(
1635 'master.cf.in', '/etc/postfix/master.cf');
1637 # make sure we have required files (else postfix start fails)
1638 # Note: postmap need a valid /etc/postfix/main.cf configuration
1639 postmap_pmg_domains
();
1640 postmap_pmg_transport
();
1641 postmap_tls_policy
();
1643 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1645 # make sure aliases.db is up to date
1646 system('/usr/bin/newaliases');
1651 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1652 my $pmg_service_params = {
1660 dkim_sign_all_mail
=> 1,
1664 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1665 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1667 sub dump_smtp_filter_config
{
1672 foreach my $sec (sort keys %$pmg_service_params) {
1673 my $conf_sec = $self->{ids
}->{$sec} // {};
1674 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1675 $val = $conf_sec->{$key};
1676 $conf .= "$sec.$key:$val\n" if defined($val);
1683 sub compare_smtp_filter_config
{
1689 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1693 syslog
('warning', "reloading pmg-smtp-filter: $err");
1696 my $new = $self->dump_smtp_filter_config();
1697 $ret = 1 if $old ne $new;
1700 $self->write_smtp_filter_config() if $ret;
1705 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1707 sub write_smtp_filter_config
{
1710 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1711 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1712 $self->dump_smtp_filter_config());
1718 sub rewrite_config
{
1719 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1721 $force_restart = {} if ! $force_restart;
1723 my $log_restart = sub {
1724 syslog
('info', "configuration change detected for '$_[0]', restarting");
1727 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1728 $force_restart->{postfix
}) {
1729 $log_restart->('postfix');
1730 PMG
::Utils
::service_cmd
('postfix', 'reload');
1733 if ($self->rewrite_dot_forward() && $restart_services) {
1734 # no need to restart anything
1737 if ($self->rewrite_config_postgres() && $restart_services) {
1738 # do nothing (too many side effects)?
1739 # does not happen anyways, because config does not change.
1742 if (($self->rewrite_config_spam() && $restart_services) ||
1743 $force_restart->{spam
}) {
1744 $log_restart->('pmg-smtp-filter');
1745 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1748 if (($self->rewrite_config_clam() && $restart_services) ||
1749 $force_restart->{clam
}) {
1750 $log_restart->('clamav-daemon');
1751 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1754 if (($self->rewrite_config_freshclam() && $restart_services) ||
1755 $force_restart->{freshclam
}) {
1756 $log_restart->('clamav-freshclam');
1757 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1760 if (($self->compare_smtp_filter_config() && $restart_services) ||
1761 $force_restart->{spam
}) {
1762 syslog
('info', "scheduled reload for pmg-smtp-filter");
1763 PMG
::Utils
::reload_smtp_filter
();