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',
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);
666 PMG
::Config
::Admin-
>register();
667 PMG
::Config
::Mail-
>register();
668 PMG
::Config
::SpamQuarantine-
>register();
669 PMG
::Config
::VirusQuarantine-
>register();
670 PMG
::Config
::Spam-
>register();
671 PMG
::Config
::ClamAV-
>register();
673 # initialize all plugins
674 PMG
::Config
::Base-
>init();
676 PVE
::JSONSchema
::register_format
(
677 'transport-domain', \
&pmg_verify_transport_domain
);
679 sub pmg_verify_transport_domain
{
680 my ($name, $noerr) = @_;
682 # like dns-name, but can contain leading dot
683 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
685 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
686 return undef if $noerr;
687 die "value does not look like a valid transport domain\n";
692 PVE
::JSONSchema
::register_format
(
693 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
695 sub pmg_verify_transport_domain_or_email
{
696 my ($name, $noerr) = @_;
698 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
701 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
705 # like dns-name, but can contain leading dot
706 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
707 return undef if $noerr;
708 die "value does not look like a valid transport domain or email address\n";
713 PVE
::JSONSchema
::register_format
(
714 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
716 sub pmg_verify_dnsbl_entry
{
717 my ($name, $noerr) = @_;
719 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
720 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
721 # we don't implement the ';' separated numbers in pattern, because this
722 # breaks at PVE::JSONSchema::split_list
723 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
725 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
726 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
727 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
728 return undef if $noerr;
729 die "value '$name' does not look like a valid dnsbl entry\n";
737 my $class = ref($type) || $type;
739 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
741 return bless $cfg, $class;
747 PVE
::INotify
::write_file
("pmg.conf", $self);
750 my $lockfile = "/var/lock/pmgconfig.lck";
753 my ($code, $errmsg) = @_;
755 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
757 $errmsg ?
die "$errmsg: $err" : die $err;
763 my ($self, $section, $key, $value) = @_;
765 my $pdata = PMG
::Config
::Base-
>private();
767 my $plugin = $pdata->{plugins
}->{$section};
768 die "no such section '$section'" if !$plugin;
770 if (defined($value)) {
771 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
772 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
773 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
775 if (defined($self->{ids
}->{$section})) {
776 delete $self->{ids
}->{$section}->{$key};
783 # get section value or default
785 my ($self, $section, $key, $nodefault) = @_;
787 my $pdata = PMG
::Config
::Base-
>private();
788 my $pdesc = $pdata->{propertyList
}->{$key};
789 die "no such property '$section/$key'\n"
790 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
791 defined($pdata->{options
}->{$section}->{$key}));
793 if (defined($self->{ids
}->{$section}) &&
794 defined(my $value = $self->{ids
}->{$section}->{$key})) {
798 return undef if $nodefault;
800 return $pdesc->{default};
803 # get a whole section with default value
805 my ($self, $section) = @_;
807 my $pdata = PMG
::Config
::Base-
>private();
808 return undef if !defined($pdata->{options
}->{$section});
812 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
814 my $pdesc = $pdata->{propertyList
}->{$key};
816 if (defined($self->{ids
}->{$section}) &&
817 defined(my $value = $self->{ids
}->{$section}->{$key})) {
818 $res->{$key} = $value;
821 $res->{$key} = $pdesc->{default};
827 # get a whole config with default values
831 my $pdata = PMG
::Config
::Base-
>private();
835 foreach my $type (keys %{$pdata->{plugins
}}) {
836 my $plugin = $pdata->{plugins
}->{$type};
837 $res->{$type} = $self->get_section($type);
844 my ($filename, $fh) = @_;
846 local $/ = undef; # slurp mode
848 my $raw = <$fh> if defined($fh);
850 return PMG
::Config
::Base-
>parse_config($filename, $raw);
854 my ($filename, $fh, $cfg) = @_;
856 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
858 PVE
::Tools
::safe_print
($filename, $fh, $raw);
861 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
864 undef, always_call_parser
=> 1);
866 # parsers/writers for other files
868 my $domainsfilename = "/etc/pmg/domains";
870 sub postmap_pmg_domains
{
871 PMG
::Utils
::run_postmap
($domainsfilename);
874 sub read_pmg_domains
{
875 my ($filename, $fh) = @_;
881 while (defined(my $line = <$fh>)) {
883 next if $line =~ m/^\s*$/;
884 if ($line =~ m/^#(.*)\s*$/) {
888 if ($line =~ m/^(\S+)\s.*$/) {
890 $domains->{$domain} = {
891 domain
=> $domain, comment
=> $comment };
894 warn "parse error in '$filename': $line\n";
903 sub write_pmg_domains
{
904 my ($filename, $fh, $domains) = @_;
906 foreach my $domain (sort keys %$domains) {
907 my $comment = $domains->{$domain}->{comment
};
908 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
909 if defined($comment) && $comment !~ m/^\s*$/;
911 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
915 PVE
::INotify
::register_file
('domains', $domainsfilename,
918 undef, always_call_parser
=> 1);
920 my $mynetworks_filename = "/etc/pmg/mynetworks";
922 sub read_pmg_mynetworks
{
923 my ($filename, $fh) = @_;
929 while (defined(my $line = <$fh>)) {
931 next if $line =~ m/^\s*$/;
932 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
933 my ($network, $prefix_size, $comment) = ($1, $2, $3);
934 my $cidr = "$network/${prefix_size}";
935 $mynetworks->{$cidr} = {
937 network_address
=> $network,
938 prefix_size
=> $prefix_size,
939 comment
=> $comment // '',
942 warn "parse error in '$filename': $line\n";
950 sub write_pmg_mynetworks
{
951 my ($filename, $fh, $mynetworks) = @_;
953 foreach my $cidr (sort keys %$mynetworks) {
954 my $data = $mynetworks->{$cidr};
955 my $comment = $data->{comment
} // '*';
956 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
960 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
961 \
&read_pmg_mynetworks
,
962 \
&write_pmg_mynetworks
,
963 undef, always_call_parser
=> 1);
965 PVE
::JSONSchema
::register_format
(
966 'tls-policy', \
&pmg_verify_tls_policy
);
968 # TODO: extend to parse attributes of the policy
969 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
970 sub pmg_verify_tls_policy
{
971 my ($policy, $noerr) = @_;
973 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
974 return undef if $noerr;
975 die "value '$policy' does not look like a valid tls policy\n";
980 PVE
::JSONSchema
::register_format
(
981 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
983 sub pmg_verify_tls_policy_strict
{
984 my ($policy, $noerr) = @_;
986 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
987 return undef if $noerr;
988 die "value '$policy' does not look like a valid tls policy\n";
993 sub read_tls_policy
{
994 my ($filename, $fh) = @_;
996 return {} if !defined($fh);
1000 while (defined(my $line = <$fh>)) {
1002 next if $line =~ m/^\s*$/;
1003 next if $line =~ m/^#(.*)\s*$/;
1005 my $parse_error = sub {
1007 die "parse error in '$filename': $line - $err";
1010 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1011 my ($domain, $policy) = ($1, $2);
1014 pmg_verify_transport_domain
($domain);
1015 pmg_verify_tls_policy
($policy);
1018 $parse_error->($err);
1022 $tls_policy->{$domain} = {
1027 $parse_error->('wrong format');
1034 sub write_tls_policy
{
1035 my ($filename, $fh, $tls_policy) = @_;
1037 return if !$tls_policy;
1039 foreach my $domain (sort keys %$tls_policy) {
1040 my $entry = $tls_policy->{$domain};
1041 PVE
::Tools
::safe_print
(
1042 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1046 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1047 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1050 undef, always_call_parser
=> 1);
1052 sub postmap_tls_policy
{
1053 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1056 my $transport_map_filename = "/etc/pmg/transport";
1058 sub postmap_pmg_transport
{
1059 PMG
::Utils
::run_postmap
($transport_map_filename);
1062 sub read_transport_map
{
1063 my ($filename, $fh) = @_;
1065 return [] if !defined($fh);
1071 while (defined(my $line = <$fh>)) {
1073 next if $line =~ m/^\s*$/;
1074 if ($line =~ m/^#(.*)\s*$/) {
1079 my $parse_error = sub {
1081 warn "parse error in '$filename': $line - $err";
1085 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1086 my ($domain, $host, $port) = ($1, $2, $3);
1088 eval { pmg_verify_transport_domain_or_email
($domain); };
1090 $parse_error->($err);
1094 if ($host =~ m/^\[(.*)\]$/) {
1099 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1101 $parse_error->($err);
1110 comment
=> $comment,
1112 $res->{$domain} = $data;
1115 $parse_error->('wrong format');
1122 sub write_transport_map
{
1123 my ($filename, $fh, $tmap) = @_;
1127 foreach my $domain (sort keys %$tmap) {
1128 my $data = $tmap->{$domain};
1130 my $comment = $data->{comment
};
1131 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1132 if defined($comment) && $comment !~ m/^\s*$/;
1134 my $use_mx = $data->{use_mx
};
1135 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1138 PVE
::Tools
::safe_print
(
1139 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1141 PVE
::Tools
::safe_print
(
1142 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1147 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1148 \
&read_transport_map
,
1149 \
&write_transport_map
,
1150 undef, always_call_parser
=> 1);
1152 # config file generation using templates
1154 sub get_template_vars
{
1157 my $vars = { pmg
=> $self->get_config() };
1159 my $nodename = PVE
::INotify
::nodename
();
1160 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1161 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1163 my $transportnets = [];
1165 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1166 foreach my $domain (sort keys %$tmap) {
1167 my $data = $tmap->{$domain};
1168 my $host = $data->{host
};
1169 if ($host =~ m/^$IPV4RE$/) {
1170 push @$transportnets, "$host/32";
1171 } elsif ($host =~ m/^$IPV6RE$/) {
1172 push @$transportnets, "[$host]/128";
1177 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1179 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1181 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1182 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1183 push @$mynetworks, "[$1]/$2";
1185 push @$mynetworks, $int_net_cidr;
1188 if ($int_ip =~ m/^$IPV6RE$/) {
1189 push @$mynetworks, "[$int_ip]/128";
1191 push @$mynetworks, "$int_ip/32";
1195 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1196 foreach my $cidr (sort keys %$netlist) {
1197 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1198 push @$mynetworks, "[$1]/$2";
1200 push @$mynetworks, $cidr;
1204 push @$mynetworks, @$transportnets;
1206 # add default relay to mynetworks
1207 if (my $relay = $self->get('mail', 'relay')) {
1208 if ($relay =~ m/^$IPV4RE$/) {
1209 push @$mynetworks, "$relay/32";
1210 } elsif ($relay =~ m/^$IPV6RE$/) {
1211 push @$mynetworks, "[$relay]/128";
1213 # DNS name - do nothing ?
1217 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1219 # normalize dnsbl_sites
1220 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1221 if (scalar(@dnsbl_sites)) {
1222 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1225 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1228 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1229 $self->get('mail', 'spf');
1230 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1232 if ($int_ip =~ m/^$IPV6RE$/) {
1233 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1235 $vars->{postfix
}->{int_ip
} = $int_ip;
1238 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1239 $vars->{dns
}->{hostname
} = $nodename;
1241 my $domain = $resolv->{search
} // 'localdomain';
1242 $vars->{dns
}->{domain
} = $domain;
1244 my $wlbr = "$nodename.$domain";
1245 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1248 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1250 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1252 my $uri = URI-
>new($proxy);
1253 my $host = $uri->host;
1254 my $port = $uri->port // 8080;
1256 my $data = { host
=> $host, port
=> $port };
1257 if (my $ui = $uri->userinfo) {
1258 my ($username, $pw) = split(/:/, $ui, 2);
1259 $data->{username
} = $username;
1260 $data->{password
} = $pw if defined($pw);
1262 $vars->{proxy
} = $data;
1265 warn "parse http_proxy failed - $@" if $@;
1267 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1272 # use one global TT cache
1273 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1275 my $template_toolkit;
1277 sub get_template_toolkit
{
1279 return $template_toolkit if $template_toolkit;
1281 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1283 return $template_toolkit;
1286 # rewrite file from template
1287 # return true if file has changed
1288 sub rewrite_config_file
{
1289 my ($self, $tmplname, $dstfn) = @_;
1291 my $demo = $self->get('admin', 'demo');
1294 my $demosrc = "$tmplname.demo";
1295 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1298 my ($perm, $uid, $gid);
1300 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1301 # needed if file contains a HTTPProxyPasswort
1303 $uid = getpwnam('clamav');
1304 $gid = getgrnam('adm');
1308 my $tt = get_template_toolkit
();
1310 my $vars = $self->get_template_vars();
1314 $tt->process($tmplname, $vars, \
$output) ||
1315 die $tt->error() . "\n";
1317 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1319 return 0 if defined($old) && ($old eq $output); # no change
1321 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1323 if (defined($uid) && defined($gid)) {
1324 chown($uid, $gid, $dstfn);
1330 # rewrite spam configuration
1331 sub rewrite_config_spam
{
1334 my $use_awl = $self->get('spam', 'use_awl');
1335 my $use_bayes = $self->get('spam', 'use_bayes');
1336 my $use_razor = $self->get('spam', 'use_razor');
1340 # delete AW and bayes databases if those features are disabled
1342 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1346 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1347 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1348 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1351 # make sure we have a custom.cf file (else cluster sync fails)
1352 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1354 $changes = 1 if $self->rewrite_config_file(
1355 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1357 $changes = 1 if $self->rewrite_config_file(
1358 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1360 $changes = 1 if $self->rewrite_config_file(
1361 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1363 $changes = 1 if $self->rewrite_config_file(
1364 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1367 mkdir "/root/.razor";
1369 $changes = 1 if $self->rewrite_config_file(
1370 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1372 if (! -e
'/root/.razor/identity') {
1375 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1376 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1379 syslog
('info', "registering razor failed: $err") if $err;
1386 # rewrite ClamAV configuration
1387 sub rewrite_config_clam
{
1390 return $self->rewrite_config_file(
1391 'clamd.conf.in', '/etc/clamav/clamd.conf');
1394 sub rewrite_config_freshclam
{
1397 return $self->rewrite_config_file(
1398 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1401 sub rewrite_config_postgres
{
1404 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1405 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1409 $changes = 1 if $self->rewrite_config_file(
1410 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1412 $changes = 1 if $self->rewrite_config_file(
1413 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1418 # rewrite /root/.forward
1419 sub rewrite_dot_forward
{
1422 my $dstfn = '/root/.forward';
1424 my $email = $self->get('admin', 'email');
1427 if ($email && $email =~ m/\s*(\S+)\s*/) {
1430 # empty .forward does not forward mails (see man local)
1433 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1435 return 0 if defined($old) && ($old eq $output); # no change
1437 PVE
::Tools
::file_set_contents
($dstfn, $output);
1442 my $write_smtp_whitelist = sub {
1443 my ($filename, $data, $action) = @_;
1445 $action = 'OK' if !$action;
1447 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1451 foreach my $k (sort keys %$data) {
1452 $new .= "$k $action\n";
1455 return 0 if defined($old) && ($old eq $new); # no change
1457 PVE
::Tools
::file_set_contents
($filename, $new);
1459 PMG
::Utils
::run_postmap
($filename);
1464 sub rewrite_postfix_whitelist
{
1465 my ($rulecache) = @_;
1467 # see man page for regexp_table for postfix regex table format
1469 # we use a hash to avoid duplicate entries in regex tables
1472 my $clientlist = {};
1474 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1475 my $oclass = ref($obj);
1476 if ($oclass eq 'PMG::RuleDB::Receiver') {
1477 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1478 $tolist->{"/^$addr\$/"} = 1;
1479 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1480 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1481 $tolist->{"/^.+\@$addr\$/"} = 1;
1482 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1483 my $addr = $obj->{address
};
1485 $tolist->{"/^$addr\$/"} = 1;
1489 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1490 my $oclass = ref($obj);
1491 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1492 if ($oclass eq 'PMG::RuleDB::EMail') {
1493 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1494 $fromlist->{"/^$addr\$/"} = 1;
1495 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1496 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1497 $fromlist->{"/^.+\@$addr\$/"} = 1;
1498 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1499 my $addr = $obj->{address
};
1501 $fromlist->{"/^$addr\$/"} = 1;
1502 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1503 $clientlist->{$obj->{address
}} = 1;
1504 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1505 $clientlist->{$obj->{address
}} = 1;
1509 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1510 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1511 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1512 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1515 # rewrite /etc/postfix/*
1516 sub rewrite_config_postfix
{
1517 my ($self, $rulecache) = @_;
1519 # make sure we have required files (else postfix start fails)
1520 IO
::File-
>new($transport_map_filename, 'a', 0644);
1524 if ($self->get('mail', 'tls')) {
1526 PMG
::Utils
::gen_proxmox_tls_cert
();
1528 syslog
('info', "generating certificate failed: $@") if $@;
1531 $changes = 1 if $self->rewrite_config_file(
1532 'main.cf.in', '/etc/postfix/main.cf');
1534 $changes = 1 if $self->rewrite_config_file(
1535 'master.cf.in', '/etc/postfix/master.cf');
1537 # make sure we have required files (else postfix start fails)
1538 # Note: postmap need a valid /etc/postfix/main.cf configuration
1539 postmap_pmg_domains
();
1540 postmap_pmg_transport
();
1541 postmap_tls_policy
();
1543 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1545 # make sure aliases.db is up to date
1546 system('/usr/bin/newaliases');
1551 sub rewrite_config
{
1552 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1554 $force_restart = {} if ! $force_restart;
1556 my $log_restart = sub {
1557 syslog
('info', "configuration change detected for '$_[0]', restarting");
1560 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1561 $force_restart->{postfix
}) {
1562 $log_restart->('postfix');
1563 PMG
::Utils
::service_cmd
('postfix', 'reload');
1566 if ($self->rewrite_dot_forward() && $restart_services) {
1567 # no need to restart anything
1570 if ($self->rewrite_config_postgres() && $restart_services) {
1571 # do nothing (too many side effects)?
1572 # does not happen anyways, because config does not change.
1575 if (($self->rewrite_config_spam() && $restart_services) ||
1576 $force_restart->{spam
}) {
1577 $log_restart->('pmg-smtp-filter');
1578 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1581 if (($self->rewrite_config_clam() && $restart_services) ||
1582 $force_restart->{clam
}) {
1583 $log_restart->('clamav-daemon');
1584 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1587 if (($self->rewrite_config_freshclam() && $restart_services) ||
1588 $force_restart->{freshclam
}) {
1589 $log_restart->('clamav-freshclam');
1590 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');