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 (/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',
120 advfilter
=> { optional
=> 1 },
121 avast
=> { optional
=> 1 },
122 clamav
=> { optional
=> 1 },
123 statlifetime
=> { optional
=> 1 },
124 dailyreport
=> { optional
=> 1 },
125 demo
=> { optional
=> 1 },
126 email
=> { optional
=> 1 },
127 http_proxy
=> { optional
=> 1 },
128 custom_check
=> { optional
=> 1 },
129 custom_check_path
=> { optional
=> 1 },
133 package PMG
::Config
::Spam
;
138 use base
qw(PMG::Config::Base);
147 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
149 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
153 description
=> "Whether to use the naive-Bayesian-style classifier.",
158 description
=> "Use the Auto-Whitelist plugin.",
163 description
=> "Whether to use Razor2, if it is available.",
167 wl_bounce_relays
=> {
168 description
=> "Whitelist legitimate bounce relays.",
171 clamav_heuristic_score
=> {
172 description
=> "Score for ClamAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
179 description
=> "Additional score for bounce mails.",
186 description
=> "Enable real time blacklists (RBL) checks.",
191 description
=> "Maximum size of spam messages in bytes.",
201 use_awl
=> { optional
=> 1 },
202 use_razor
=> { optional
=> 1 },
203 wl_bounce_relays
=> { optional
=> 1 },
204 languages
=> { optional
=> 1 },
205 use_bayes
=> { optional
=> 1 },
206 clamav_heuristic_score
=> { optional
=> 1 },
207 bounce_score
=> { optional
=> 1 },
208 rbl_checks
=> { optional
=> 1 },
209 maxspamsize
=> { optional
=> 1 },
213 package PMG
::Config
::SpamQuarantine
;
218 use base
qw(PMG::Config::Base);
227 description
=> "Quarantine life time (days)",
233 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.",
235 enum
=> [qw(ticket ldap ldapticket)],
239 description
=> "Spam report style.",
241 enum
=> [qw(none short verbose custom)],
242 default => 'verbose',
245 description
=> "Allow to view images.",
250 description
=> "Allow to view hyperlinks.",
255 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
256 type
=> 'string', format
=> 'address',
259 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
266 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
268 enum
=> [qw(http https)],
272 description
=> "Text for 'From' header in daily spam report mails.",
280 mailfrom
=> { optional
=> 1 },
281 hostname
=> { optional
=> 1 },
282 lifetime
=> { optional
=> 1 },
283 authmode
=> { optional
=> 1 },
284 reportstyle
=> { optional
=> 1 },
285 viewimages
=> { optional
=> 1 },
286 allowhrefs
=> { optional
=> 1 },
287 port
=> { optional
=> 1 },
288 protocol
=> { optional
=> 1 },
292 package PMG
::Config
::VirusQuarantine
;
297 use base
qw(PMG::Config::Base);
309 lifetime
=> { optional
=> 1 },
310 viewimages
=> { optional
=> 1 },
311 allowhrefs
=> { optional
=> 1 },
315 package PMG
::Config
::ClamAV
;
320 use base
qw(PMG::Config::Base);
329 description
=> "ClamAV database mirror server.",
331 default => 'database.clamav.net',
333 archiveblockencrypted
=> {
334 description
=> "Whether to block encrypted archives. Mark encrypted archives as viruses.",
339 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.",
345 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.",
351 description
=> "Files larger than this limit won't be scanned.",
357 description
=> "Sets the maximum amount of data to be scanned for each input file.",
360 default => 100000000,
363 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
369 description
=> "Enables support for Google Safe Browsing.",
378 archiveblockencrypted
=> { optional
=> 1 },
379 archivemaxrec
=> { optional
=> 1 },
380 archivemaxfiles
=> { optional
=> 1 },
381 archivemaxsize
=> { optional
=> 1 },
382 maxscansize
=> { optional
=> 1 },
383 dbmirror
=> { optional
=> 1 },
384 maxcccount
=> { optional
=> 1 },
385 safebrowsing
=> { optional
=> 1 },
389 package PMG
::Config
::Mail
;
394 use PVE
::ProcFSTools
;
396 use base
qw(PMG::Config::Base);
403 sub physical_memory
{
405 return $physicalmem if $physicalmem;
407 my $info = PVE
::ProcFSTools
::read_meminfo
();
408 my $total = int($info->{memtotal
} / (1024*1024));
413 sub get_max_filters
{
414 # estimate optimal number of filter servers
418 my $memory = physical_memory
();
419 my $add_servers = int(($memory - 512)/$servermem);
420 $max_servers += $add_servers if $add_servers > 0;
421 $max_servers = 40 if $max_servers > 40;
423 return $max_servers - 2;
427 # estimate optimal number of smtpd daemons
429 my $max_servers = 25;
431 my $memory = physical_memory
();
432 my $add_servers = int(($memory - 512)/$servermem);
433 $max_servers += $add_servers if $add_servers > 0;
434 $max_servers = 100 if $max_servers > 100;
439 # estimate optimal number of proxpolicy servers
441 my $memory = physical_memory
();
442 $max_servers = 5 if $memory >= 500;
449 description
=> "SMTP port number for outgoing mail (trusted).",
456 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
463 description
=> "The default mail delivery transport (incoming mails).",
464 type
=> 'string', format
=> 'address',
467 description
=> "SMTP port number for relay host.",
474 description
=> "Disable MX lookups for default relay.",
479 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
480 type
=> 'string', format
=> 'address',
483 description
=> "SMTP port number for smarthost.",
490 description
=> "ESMTP banner.",
493 default => 'ESMTP Proxmox',
496 description
=> "Maximum number of pmg-smtp-filter processes.",
500 default => get_max_filters
(),
503 description
=> "Maximum number of pmgpolicy processes.",
507 default => get_max_policy
(),
510 description
=> "Maximum number of SMTP daemon processes (in).",
514 default => get_max_smtpd
(),
517 description
=> "Maximum number of SMTP daemon processes (out).",
521 default => get_max_smtpd
(),
523 conn_count_limit
=> {
524 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
530 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.",
535 message_rate_limit
=> {
536 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.",
542 description
=> "Hide received header in outgoing mails.",
547 description
=> "Maximum email size. Larger mails are rejected.",
550 default => 1024*1024*10,
553 description
=> "SMTP delay warning time (in hours).",
559 description
=> "Enable TLS.",
564 description
=> "Enable TLS Logging.",
569 description
=> "Add TLS received header.",
574 description
=> "Use Sender Policy Framework.",
579 description
=> "Use Greylisting.",
584 description
=> "Use SMTP HELO tests.",
589 description
=> "Reject unknown clients.",
593 rejectunknownsender
=> {
594 description
=> "Reject unknown senders.",
599 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
601 enum
=> ['450', '550'],
604 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
605 type
=> 'string', format
=> 'dnsbl-entry-list',
608 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
618 int_port
=> { optional
=> 1 },
619 ext_port
=> { optional
=> 1 },
620 smarthost
=> { optional
=> 1 },
621 smarthostport
=> { optional
=> 1 },
622 relay
=> { optional
=> 1 },
623 relayport
=> { optional
=> 1 },
624 relaynomx
=> { optional
=> 1 },
625 dwarning
=> { optional
=> 1 },
626 max_smtpd_in
=> { optional
=> 1 },
627 max_smtpd_out
=> { optional
=> 1 },
628 greylist
=> { optional
=> 1 },
629 helotests
=> { optional
=> 1 },
630 tls
=> { optional
=> 1 },
631 tlslog
=> { optional
=> 1 },
632 tlsheader
=> { optional
=> 1 },
633 spf
=> { optional
=> 1 },
634 maxsize
=> { optional
=> 1 },
635 banner
=> { optional
=> 1 },
636 max_filters
=> { optional
=> 1 },
637 max_policy
=> { optional
=> 1 },
638 hide_received
=> { optional
=> 1 },
639 rejectunknown
=> { optional
=> 1 },
640 rejectunknownsender
=> { optional
=> 1 },
641 conn_count_limit
=> { optional
=> 1 },
642 conn_rate_limit
=> { optional
=> 1 },
643 message_rate_limit
=> { optional
=> 1 },
644 verifyreceivers
=> { optional
=> 1 },
645 dnsbl_sites
=> { optional
=> 1 },
646 dnsbl_threshold
=> { optional
=> 1 },
659 use PVE
::Tools
qw($IPV4RE $IPV6RE);
665 PMG
::Config
::Admin-
>register();
666 PMG
::Config
::Mail-
>register();
667 PMG
::Config
::SpamQuarantine-
>register();
668 PMG
::Config
::VirusQuarantine-
>register();
669 PMG
::Config
::Spam-
>register();
670 PMG
::Config
::ClamAV-
>register();
672 # initialize all plugins
673 PMG
::Config
::Base-
>init();
675 PVE
::JSONSchema
::register_format
(
676 'transport-domain', \
&pmg_verify_transport_domain
);
678 sub pmg_verify_transport_domain
{
679 my ($name, $noerr) = @_;
681 # like dns-name, but can contain leading dot
682 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
684 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
685 return undef if $noerr;
686 die "value does not look like a valid transport domain\n";
691 PVE
::JSONSchema
::register_format
(
692 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
694 sub pmg_verify_transport_domain_or_email
{
695 my ($name, $noerr) = @_;
697 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
700 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
704 # like dns-name, but can contain leading dot
705 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
706 return undef if $noerr;
707 die "value does not look like a valid transport domain or email address\n";
712 PVE
::JSONSchema
::register_format
(
713 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
715 sub pmg_verify_dnsbl_entry
{
716 my ($name, $noerr) = @_;
718 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
719 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
720 # we don't implement the ';' separated numbers in pattern, because this
721 # breaks at PVE::JSONSchema::split_list
722 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
724 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
725 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
726 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
727 return undef if $noerr;
728 die "value '$name' does not look like a valid dnsbl entry\n";
736 my $class = ref($type) || $type;
738 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
740 return bless $cfg, $class;
746 PVE
::INotify
::write_file
("pmg.conf", $self);
749 my $lockfile = "/var/lock/pmgconfig.lck";
752 my ($code, $errmsg) = @_;
754 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
756 $errmsg ?
die "$errmsg: $err" : die $err;
762 my ($self, $section, $key, $value) = @_;
764 my $pdata = PMG
::Config
::Base-
>private();
766 my $plugin = $pdata->{plugins
}->{$section};
767 die "no such section '$section'" if !$plugin;
769 if (defined($value)) {
770 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
771 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
772 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
774 if (defined($self->{ids
}->{$section})) {
775 delete $self->{ids
}->{$section}->{$key};
782 # get section value or default
784 my ($self, $section, $key, $nodefault) = @_;
786 my $pdata = PMG
::Config
::Base-
>private();
787 my $pdesc = $pdata->{propertyList
}->{$key};
788 die "no such property '$section/$key'\n"
789 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
790 defined($pdata->{options
}->{$section}->{$key}));
792 if (defined($self->{ids
}->{$section}) &&
793 defined(my $value = $self->{ids
}->{$section}->{$key})) {
797 return undef if $nodefault;
799 return $pdesc->{default};
802 # get a whole section with default value
804 my ($self, $section) = @_;
806 my $pdata = PMG
::Config
::Base-
>private();
807 return undef if !defined($pdata->{options
}->{$section});
811 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
813 my $pdesc = $pdata->{propertyList
}->{$key};
815 if (defined($self->{ids
}->{$section}) &&
816 defined(my $value = $self->{ids
}->{$section}->{$key})) {
817 $res->{$key} = $value;
820 $res->{$key} = $pdesc->{default};
826 # get a whole config with default values
830 my $pdata = PMG
::Config
::Base-
>private();
834 foreach my $type (keys %{$pdata->{plugins
}}) {
835 my $plugin = $pdata->{plugins
}->{$type};
836 $res->{$type} = $self->get_section($type);
843 my ($filename, $fh) = @_;
845 local $/ = undef; # slurp mode
847 my $raw = <$fh> if defined($fh);
849 return PMG
::Config
::Base-
>parse_config($filename, $raw);
853 my ($filename, $fh, $cfg) = @_;
855 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
857 PVE
::Tools
::safe_print
($filename, $fh, $raw);
860 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
863 undef, always_call_parser
=> 1);
865 # parsers/writers for other files
867 my $domainsfilename = "/etc/pmg/domains";
869 sub postmap_pmg_domains
{
870 PMG
::Utils
::run_postmap
($domainsfilename);
873 sub read_pmg_domains
{
874 my ($filename, $fh) = @_;
880 while (defined(my $line = <$fh>)) {
882 next if $line =~ m/^\s*$/;
883 if ($line =~ m/^#(.*)\s*$/) {
887 if ($line =~ m/^(\S+)\s.*$/) {
889 $domains->{$domain} = {
890 domain
=> $domain, comment
=> $comment };
893 warn "parse error in '$filename': $line\n";
902 sub write_pmg_domains
{
903 my ($filename, $fh, $domains) = @_;
905 foreach my $domain (sort keys %$domains) {
906 my $comment = $domains->{$domain}->{comment
};
907 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
908 if defined($comment) && $comment !~ m/^\s*$/;
910 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
914 PVE
::INotify
::register_file
('domains', $domainsfilename,
917 undef, always_call_parser
=> 1);
919 my $mynetworks_filename = "/etc/pmg/mynetworks";
921 sub read_pmg_mynetworks
{
922 my ($filename, $fh) = @_;
928 while (defined(my $line = <$fh>)) {
930 next if $line =~ m/^\s*$/;
931 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
932 my ($network, $prefix_size, $comment) = ($1, $2, $3);
933 my $cidr = "$network/${prefix_size}";
934 $mynetworks->{$cidr} = {
936 network_address
=> $network,
937 prefix_size
=> $prefix_size,
938 comment
=> $comment // '',
941 warn "parse error in '$filename': $line\n";
949 sub write_pmg_mynetworks
{
950 my ($filename, $fh, $mynetworks) = @_;
952 foreach my $cidr (sort keys %$mynetworks) {
953 my $data = $mynetworks->{$cidr};
954 my $comment = $data->{comment
} // '*';
955 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
959 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
960 \
&read_pmg_mynetworks
,
961 \
&write_pmg_mynetworks
,
962 undef, always_call_parser
=> 1);
964 PVE
::JSONSchema
::register_format
(
965 'tls-policy', \
&pmg_verify_tls_policy
);
967 # TODO: extend to parse attributes of the policy
968 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
969 sub pmg_verify_tls_policy
{
970 my ($policy, $noerr) = @_;
972 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
973 return undef if $noerr;
974 die "value '$policy' does not look like a valid tls policy\n";
979 PVE
::JSONSchema
::register_format
(
980 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
982 sub pmg_verify_tls_policy_strict
{
983 my ($policy, $noerr) = @_;
985 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
986 return undef if $noerr;
987 die "value '$policy' does not look like a valid tls policy\n";
992 sub read_tls_policy
{
993 my ($filename, $fh) = @_;
995 return {} if !defined($fh);
999 while (defined(my $line = <$fh>)) {
1001 next if $line =~ m/^\s*$/;
1002 next if $line =~ m/^#(.*)\s*$/;
1004 my $parse_error = sub {
1006 die "parse error in '$filename': $line - $err";
1009 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1010 my ($domain, $policy) = ($1, $2);
1013 pmg_verify_transport_domain
($domain);
1014 pmg_verify_tls_policy
($policy);
1017 $parse_error->($err);
1021 $tls_policy->{$domain} = {
1026 $parse_error->('wrong format');
1033 sub write_tls_policy
{
1034 my ($filename, $fh, $tls_policy) = @_;
1036 return if !$tls_policy;
1038 foreach my $domain (sort keys %$tls_policy) {
1039 my $entry = $tls_policy->{$domain};
1040 PVE
::Tools
::safe_print
(
1041 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1045 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1046 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1049 undef, always_call_parser
=> 1);
1051 sub postmap_tls_policy
{
1052 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1055 my $transport_map_filename = "/etc/pmg/transport";
1057 sub postmap_pmg_transport
{
1058 PMG
::Utils
::run_postmap
($transport_map_filename);
1061 sub read_transport_map
{
1062 my ($filename, $fh) = @_;
1064 return [] if !defined($fh);
1070 while (defined(my $line = <$fh>)) {
1072 next if $line =~ m/^\s*$/;
1073 if ($line =~ m/^#(.*)\s*$/) {
1078 my $parse_error = sub {
1080 warn "parse error in '$filename': $line - $err";
1084 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1085 my ($domain, $host, $port) = ($1, $2, $3);
1087 eval { pmg_verify_transport_domain_or_email
($domain); };
1089 $parse_error->($err);
1093 if ($host =~ m/^\[(.*)\]$/) {
1098 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1100 $parse_error->($err);
1109 comment
=> $comment,
1111 $res->{$domain} = $data;
1114 $parse_error->('wrong format');
1121 sub write_transport_map
{
1122 my ($filename, $fh, $tmap) = @_;
1126 foreach my $domain (sort keys %$tmap) {
1127 my $data = $tmap->{$domain};
1129 my $comment = $data->{comment
};
1130 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1131 if defined($comment) && $comment !~ m/^\s*$/;
1133 my $use_mx = $data->{use_mx
};
1134 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1137 PVE
::Tools
::safe_print
(
1138 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1140 PVE
::Tools
::safe_print
(
1141 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1146 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1147 \
&read_transport_map
,
1148 \
&write_transport_map
,
1149 undef, always_call_parser
=> 1);
1151 # config file generation using templates
1153 sub get_template_vars
{
1156 my $vars = { pmg
=> $self->get_config() };
1158 my $nodename = PVE
::INotify
::nodename
();
1159 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1160 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1162 my $transportnets = [];
1164 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1165 foreach my $domain (sort keys %$tmap) {
1166 my $data = $tmap->{$domain};
1167 my $host = $data->{host
};
1168 if ($host =~ m/^$IPV4RE$/) {
1169 push @$transportnets, "$host/32";
1170 } elsif ($host =~ m/^$IPV6RE$/) {
1171 push @$transportnets, "[$host]/128";
1176 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1178 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1180 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1181 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1182 push @$mynetworks, "[$1]/$2";
1184 push @$mynetworks, $int_net_cidr;
1187 if ($int_ip =~ m/^$IPV6RE$/) {
1188 push @$mynetworks, "[$int_ip]/128";
1190 push @$mynetworks, "$int_ip/32";
1194 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1195 foreach my $cidr (keys %$netlist) {
1196 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1197 push @$mynetworks, "[$1]/$2";
1199 push @$mynetworks, $cidr;
1203 push @$mynetworks, @$transportnets;
1205 # add default relay to mynetworks
1206 if (my $relay = $self->get('mail', 'relay')) {
1207 if ($relay =~ m/^$IPV4RE$/) {
1208 push @$mynetworks, "$relay/32";
1209 } elsif ($relay =~ m/^$IPV6RE$/) {
1210 push @$mynetworks, "[$relay]/128";
1212 # DNS name - do nothing ?
1216 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1218 # normalize dnsbl_sites
1219 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1220 if (scalar(@dnsbl_sites)) {
1221 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1224 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1227 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1228 $self->get('mail', 'spf');
1229 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1231 if ($int_ip =~ m/^$IPV6RE$/) {
1232 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1234 $vars->{postfix
}->{int_ip
} = $int_ip;
1237 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1238 $vars->{dns
}->{hostname
} = $nodename;
1240 my $domain = $resolv->{search
} // 'localdomain';
1241 $vars->{dns
}->{domain
} = $domain;
1243 my $wlbr = "$nodename.$domain";
1244 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1247 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1249 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1251 my $uri = URI-
>new($proxy);
1252 my $host = $uri->host;
1253 my $port = $uri->port // 8080;
1255 my $data = { host
=> $host, port
=> $port };
1256 if (my $ui = $uri->userinfo) {
1257 my ($username, $pw) = split(/:/, $ui, 2);
1258 $data->{username
} = $username;
1259 $data->{password
} = $pw if defined($pw);
1261 $vars->{proxy
} = $data;
1264 warn "parse http_proxy failed - $@" if $@;
1270 # use one global TT cache
1271 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1273 my $template_toolkit;
1275 sub get_template_toolkit
{
1277 return $template_toolkit if $template_toolkit;
1279 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1281 return $template_toolkit;
1284 # rewrite file from template
1285 # return true if file has changed
1286 sub rewrite_config_file
{
1287 my ($self, $tmplname, $dstfn) = @_;
1289 my $demo = $self->get('admin', 'demo');
1292 my $demosrc = "$tmplname.demo";
1293 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1296 my ($perm, $uid, $gid);
1298 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1299 # needed if file contains a HTTPProxyPasswort
1301 $uid = getpwnam('clamav');
1302 $gid = getgrnam('adm');
1306 my $tt = get_template_toolkit
();
1308 my $vars = $self->get_template_vars();
1312 $tt->process($tmplname, $vars, \
$output) ||
1313 die $tt->error() . "\n";
1315 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1317 return 0 if defined($old) && ($old eq $output); # no change
1319 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1321 if (defined($uid) && defined($gid)) {
1322 chown($uid, $gid, $dstfn);
1328 # rewrite spam configuration
1329 sub rewrite_config_spam
{
1332 my $use_awl = $self->get('spam', 'use_awl');
1333 my $use_bayes = $self->get('spam', 'use_bayes');
1334 my $use_razor = $self->get('spam', 'use_razor');
1338 # delete AW and bayes databases if those features are disabled
1340 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1344 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1345 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1346 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1349 # make sure we have a custom.cf file (else cluster sync fails)
1350 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1352 $changes = 1 if $self->rewrite_config_file(
1353 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1355 $changes = 1 if $self->rewrite_config_file(
1356 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1358 $changes = 1 if $self->rewrite_config_file(
1359 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1361 $changes = 1 if $self->rewrite_config_file(
1362 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1365 mkdir "/root/.razor";
1367 $changes = 1 if $self->rewrite_config_file(
1368 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1370 if (! -e
'/root/.razor/identity') {
1373 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1374 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1377 syslog
('info', "registering razor failed: $err") if $err;
1384 # rewrite ClamAV configuration
1385 sub rewrite_config_clam
{
1388 return $self->rewrite_config_file(
1389 'clamd.conf.in', '/etc/clamav/clamd.conf');
1392 sub rewrite_config_freshclam
{
1395 return $self->rewrite_config_file(
1396 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1399 sub rewrite_config_postgres
{
1402 my $pgconfdir = "/etc/postgresql/9.6/main";
1406 $changes = 1 if $self->rewrite_config_file(
1407 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1409 $changes = 1 if $self->rewrite_config_file(
1410 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1415 # rewrite /root/.forward
1416 sub rewrite_dot_forward
{
1419 my $dstfn = '/root/.forward';
1421 my $email = $self->get('admin', 'email');
1424 if ($email && $email =~ m/\s*(\S+)\s*/) {
1427 # empty .forward does not forward mails (see man local)
1430 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1432 return 0 if defined($old) && ($old eq $output); # no change
1434 PVE
::Tools
::file_set_contents
($dstfn, $output);
1439 my $write_smtp_whitelist = sub {
1440 my ($filename, $data, $action) = @_;
1442 $action = 'OK' if !$action;
1444 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1448 foreach my $k (sort keys %$data) {
1449 $new .= "$k $action\n";
1452 return 0 if defined($old) && ($old eq $new); # no change
1454 PVE
::Tools
::file_set_contents
($filename, $new);
1456 PMG
::Utils
::run_postmap
($filename);
1461 sub rewrite_postfix_whitelist
{
1462 my ($rulecache) = @_;
1464 # see man page for regexp_table for postfix regex table format
1466 # we use a hash to avoid duplicate entries in regex tables
1469 my $clientlist = {};
1471 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1472 my $oclass = ref($obj);
1473 if ($oclass eq 'PMG::RuleDB::Receiver') {
1474 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1475 $tolist->{"/^$addr\$/"} = 1;
1476 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1477 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1478 $tolist->{"/^.+\@$addr\$/"} = 1;
1479 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1480 my $addr = $obj->{address
};
1482 $tolist->{"/^$addr\$/"} = 1;
1486 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1487 my $oclass = ref($obj);
1488 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1489 if ($oclass eq 'PMG::RuleDB::EMail') {
1490 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1491 $fromlist->{"/^$addr\$/"} = 1;
1492 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1493 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1494 $fromlist->{"/^.+\@$addr\$/"} = 1;
1495 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1496 my $addr = $obj->{address
};
1498 $fromlist->{"/^$addr\$/"} = 1;
1499 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1500 $clientlist->{$obj->{address
}} = 1;
1501 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1502 $clientlist->{$obj->{address
}} = 1;
1506 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1507 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1508 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1509 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1512 # rewrite /etc/postfix/*
1513 sub rewrite_config_postfix
{
1514 my ($self, $rulecache) = @_;
1516 # make sure we have required files (else postfix start fails)
1517 IO
::File-
>new($transport_map_filename, 'a', 0644);
1521 if ($self->get('mail', 'tls')) {
1523 PMG
::Utils
::gen_proxmox_tls_cert
();
1525 syslog
('info', "generating certificate failed: $@") if $@;
1528 $changes = 1 if $self->rewrite_config_file(
1529 'main.cf.in', '/etc/postfix/main.cf');
1531 $changes = 1 if $self->rewrite_config_file(
1532 'master.cf.in', '/etc/postfix/master.cf');
1534 # make sure we have required files (else postfix start fails)
1535 # Note: postmap need a valid /etc/postfix/main.cf configuration
1536 postmap_pmg_domains
();
1537 postmap_pmg_transport
();
1538 postmap_tls_policy
();
1540 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1542 # make sure aliases.db is up to date
1543 system('/usr/bin/newaliases');
1548 sub rewrite_config
{
1549 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1551 $force_restart = {} if ! $force_restart;
1553 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1554 $force_restart->{postfix
}) {
1555 PMG
::Utils
::service_cmd
('postfix', 'restart');
1558 if ($self->rewrite_dot_forward() && $restart_services) {
1559 # no need to restart anything
1562 if ($self->rewrite_config_postgres() && $restart_services) {
1563 # do nothing (too many side effects)?
1564 # does not happen anyways, because config does not change.
1567 if (($self->rewrite_config_spam() && $restart_services) ||
1568 $force_restart->{spam
}) {
1569 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1572 if (($self->rewrite_config_clam() && $restart_services) ||
1573 $force_restart->{clam
}) {
1574 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1577 if (($self->rewrite_config_freshclam() && $restart_services) ||
1578 $force_restart->{freshclam
}) {
1579 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');