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 for IPv4.",
613 description
=> "Netmask to apply for greylisting IPv4 hosts",
620 description
=> "Use Greylisting for IPv6.",
625 description
=> "Netmask to apply for greylisting IPv6 hosts",
632 description
=> "Use SMTP HELO tests.",
637 description
=> "Reject unknown clients.",
641 rejectunknownsender
=> {
642 description
=> "Reject unknown senders.",
647 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
649 enum
=> ['450', '550'],
652 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
653 type
=> 'string', format
=> 'dnsbl-entry-list',
656 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
661 before_queue_filtering
=> {
662 description
=> "Enable before queue filtering by pmg-smtp-filter",
667 description
=> "Send out NDR when mail gets blocked",
676 int_port
=> { optional
=> 1 },
677 ext_port
=> { optional
=> 1 },
678 smarthost
=> { optional
=> 1 },
679 smarthostport
=> { optional
=> 1 },
680 relay
=> { optional
=> 1 },
681 relayprotocol
=> { optional
=> 1 },
682 relayport
=> { optional
=> 1 },
683 relaynomx
=> { optional
=> 1 },
684 dwarning
=> { optional
=> 1 },
685 max_smtpd_in
=> { optional
=> 1 },
686 max_smtpd_out
=> { optional
=> 1 },
687 greylist
=> { optional
=> 1 },
688 greylistmask4
=> { optional
=> 1 },
689 greylist6
=> { optional
=> 1 },
690 greylistmask6
=> { optional
=> 1 },
691 helotests
=> { optional
=> 1 },
692 tls
=> { optional
=> 1 },
693 tlslog
=> { optional
=> 1 },
694 tlsheader
=> { optional
=> 1 },
695 spf
=> { optional
=> 1 },
696 maxsize
=> { optional
=> 1 },
697 banner
=> { optional
=> 1 },
698 max_filters
=> { optional
=> 1 },
699 max_policy
=> { optional
=> 1 },
700 hide_received
=> { optional
=> 1 },
701 rejectunknown
=> { optional
=> 1 },
702 rejectunknownsender
=> { optional
=> 1 },
703 conn_count_limit
=> { optional
=> 1 },
704 conn_rate_limit
=> { optional
=> 1 },
705 message_rate_limit
=> { optional
=> 1 },
706 verifyreceivers
=> { optional
=> 1 },
707 dnsbl_sites
=> { optional
=> 1 },
708 dnsbl_threshold
=> { optional
=> 1 },
709 before_queue_filtering
=> { optional
=> 1 },
710 ndr_on_block
=> { optional
=> 1 },
723 use PVE
::Tools
qw($IPV4RE $IPV6RE);
730 PMG
::Config
::Admin-
>register();
731 PMG
::Config
::Mail-
>register();
732 PMG
::Config
::SpamQuarantine-
>register();
733 PMG
::Config
::VirusQuarantine-
>register();
734 PMG
::Config
::Spam-
>register();
735 PMG
::Config
::ClamAV-
>register();
737 # initialize all plugins
738 PMG
::Config
::Base-
>init();
740 PVE
::JSONSchema
::register_format
(
741 'transport-domain', \
&pmg_verify_transport_domain
);
743 sub pmg_verify_transport_domain
{
744 my ($name, $noerr) = @_;
746 # like dns-name, but can contain leading dot
747 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
749 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
750 return undef if $noerr;
751 die "value does not look like a valid transport domain\n";
756 PVE
::JSONSchema
::register_format
(
757 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
759 sub pmg_verify_transport_domain_or_email
{
760 my ($name, $noerr) = @_;
762 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
765 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
769 # like dns-name, but can contain leading dot
770 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
771 return undef if $noerr;
772 die "value does not look like a valid transport domain or email address\n";
777 PVE
::JSONSchema
::register_format
(
778 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
780 sub pmg_verify_dnsbl_entry
{
781 my ($name, $noerr) = @_;
783 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
784 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
785 # we don't implement the ';' separated numbers in pattern, because this
786 # breaks at PVE::JSONSchema::split_list
787 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
789 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
790 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
791 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
792 return undef if $noerr;
793 die "value '$name' does not look like a valid dnsbl entry\n";
801 my $class = ref($type) || $type;
803 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
805 return bless $cfg, $class;
811 PVE
::INotify
::write_file
("pmg.conf", $self);
814 my $lockfile = "/var/lock/pmgconfig.lck";
817 my ($code, $errmsg) = @_;
819 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
821 $errmsg ?
die "$errmsg: $err" : die $err;
827 my ($self, $section, $key, $value) = @_;
829 my $pdata = PMG
::Config
::Base-
>private();
831 my $plugin = $pdata->{plugins
}->{$section};
832 die "no such section '$section'" if !$plugin;
834 if (defined($value)) {
835 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
836 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
837 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
839 if (defined($self->{ids
}->{$section})) {
840 delete $self->{ids
}->{$section}->{$key};
847 # get section value or default
849 my ($self, $section, $key, $nodefault) = @_;
851 my $pdata = PMG
::Config
::Base-
>private();
852 my $pdesc = $pdata->{propertyList
}->{$key};
853 die "no such property '$section/$key'\n"
854 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
855 defined($pdata->{options
}->{$section}->{$key}));
857 if (defined($self->{ids
}->{$section}) &&
858 defined(my $value = $self->{ids
}->{$section}->{$key})) {
862 return undef if $nodefault;
864 return $pdesc->{default};
867 # get a whole section with default value
869 my ($self, $section) = @_;
871 my $pdata = PMG
::Config
::Base-
>private();
872 return undef if !defined($pdata->{options
}->{$section});
876 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
878 my $pdesc = $pdata->{propertyList
}->{$key};
880 if (defined($self->{ids
}->{$section}) &&
881 defined(my $value = $self->{ids
}->{$section}->{$key})) {
882 $res->{$key} = $value;
885 $res->{$key} = $pdesc->{default};
891 # get a whole config with default values
895 my $pdata = PMG
::Config
::Base-
>private();
899 foreach my $type (keys %{$pdata->{plugins
}}) {
900 my $plugin = $pdata->{plugins
}->{$type};
901 $res->{$type} = $self->get_section($type);
908 my ($filename, $fh) = @_;
910 local $/ = undef; # slurp mode
912 my $raw = <$fh> if defined($fh);
914 return PMG
::Config
::Base-
>parse_config($filename, $raw);
918 my ($filename, $fh, $cfg) = @_;
920 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
922 PVE
::Tools
::safe_print
($filename, $fh, $raw);
925 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
928 undef, always_call_parser
=> 1);
930 # parsers/writers for other files
932 my $domainsfilename = "/etc/pmg/domains";
934 sub postmap_pmg_domains
{
935 PMG
::Utils
::run_postmap
($domainsfilename);
938 sub read_pmg_domains
{
939 my ($filename, $fh) = @_;
945 while (defined(my $line = <$fh>)) {
947 next if $line =~ m/^\s*$/;
948 if ($line =~ m/^#(.*)\s*$/) {
952 if ($line =~ m/^(\S+)\s.*$/) {
954 $domains->{$domain} = {
955 domain
=> $domain, comment
=> $comment };
958 warn "parse error in '$filename': $line\n";
967 sub write_pmg_domains
{
968 my ($filename, $fh, $domains) = @_;
970 foreach my $domain (sort keys %$domains) {
971 my $comment = $domains->{$domain}->{comment
};
972 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
973 if defined($comment) && $comment !~ m/^\s*$/;
975 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
979 PVE
::INotify
::register_file
('domains', $domainsfilename,
982 undef, always_call_parser
=> 1);
984 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
986 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
989 undef, always_call_parser
=> 1);
991 my $mynetworks_filename = "/etc/pmg/mynetworks";
993 sub read_pmg_mynetworks
{
994 my ($filename, $fh) = @_;
1000 while (defined(my $line = <$fh>)) {
1002 next if $line =~ m/^\s*$/;
1003 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1004 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1005 my $cidr = "$network/${prefix_size}";
1006 $mynetworks->{$cidr} = {
1008 network_address
=> $network,
1009 prefix_size
=> $prefix_size,
1010 comment
=> $comment // '',
1013 warn "parse error in '$filename': $line\n";
1021 sub write_pmg_mynetworks
{
1022 my ($filename, $fh, $mynetworks) = @_;
1024 foreach my $cidr (sort keys %$mynetworks) {
1025 my $data = $mynetworks->{$cidr};
1026 my $comment = $data->{comment
} // '*';
1027 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1031 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1032 \
&read_pmg_mynetworks
,
1033 \
&write_pmg_mynetworks
,
1034 undef, always_call_parser
=> 1);
1036 PVE
::JSONSchema
::register_format
(
1037 'tls-policy', \
&pmg_verify_tls_policy
);
1039 # TODO: extend to parse attributes of the policy
1040 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1041 sub pmg_verify_tls_policy
{
1042 my ($policy, $noerr) = @_;
1044 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1045 return undef if $noerr;
1046 die "value '$policy' does not look like a valid tls policy\n";
1051 PVE
::JSONSchema
::register_format
(
1052 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1054 sub pmg_verify_tls_policy_strict
{
1055 my ($policy, $noerr) = @_;
1057 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1058 return undef if $noerr;
1059 die "value '$policy' does not look like a valid tls policy\n";
1064 PVE
::JSONSchema
::register_format
(
1065 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1067 sub pmg_verify_transport_domain_or_nexthop
{
1068 my ($name, $noerr) = @_;
1070 if (pmg_verify_transport_domain
($name, 1)) {
1072 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1074 if ($nexthop =~ m/^\[(.*)\]$/) {
1077 return $name if pmg_verify_transport_address
($nexthop, 1);
1079 return undef if $noerr;
1080 die "value does not look like a valid domain or next-hop\n";
1084 sub read_tls_policy
{
1085 my ($filename, $fh) = @_;
1087 return {} if !defined($fh);
1089 my $tls_policy = {};
1091 while (defined(my $line = <$fh>)) {
1093 next if $line =~ m/^\s*$/;
1094 next if $line =~ m/^#(.*)\s*$/;
1096 my $parse_error = sub {
1098 die "parse error in '$filename': $line - $err";
1101 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1102 my ($destination, $policy) = ($1, $2);
1105 pmg_verify_transport_domain_or_nexthop
($destination);
1106 pmg_verify_tls_policy
($policy);
1109 $parse_error->($err);
1113 $tls_policy->{$destination} = {
1114 destination
=> $destination,
1118 $parse_error->('wrong format');
1125 sub write_tls_policy
{
1126 my ($filename, $fh, $tls_policy) = @_;
1128 return if !$tls_policy;
1130 foreach my $destination (sort keys %$tls_policy) {
1131 my $entry = $tls_policy->{$destination};
1132 PVE
::Tools
::safe_print
(
1133 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1137 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1138 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1141 undef, always_call_parser
=> 1);
1143 sub postmap_tls_policy
{
1144 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1147 my $transport_map_filename = "/etc/pmg/transport";
1149 sub postmap_pmg_transport
{
1150 PMG
::Utils
::run_postmap
($transport_map_filename);
1153 PVE
::JSONSchema
::register_format
(
1154 'transport-address', \
&pmg_verify_transport_address
);
1156 sub pmg_verify_transport_address
{
1157 my ($name, $noerr) = @_;
1159 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1161 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1164 return undef if $noerr;
1165 die "value does not look like a valid address\n";
1169 sub read_transport_map
{
1170 my ($filename, $fh) = @_;
1172 return [] if !defined($fh);
1178 while (defined(my $line = <$fh>)) {
1180 next if $line =~ m/^\s*$/;
1181 if ($line =~ m/^#(.*)\s*$/) {
1186 my $parse_error = sub {
1188 warn "parse error in '$filename': $line - $err";
1192 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1193 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1195 eval { pmg_verify_transport_domain_or_email
($domain); };
1197 $parse_error->($err);
1201 if ($host =~ m/^\[(.*)\]$/) {
1205 $use_mx = 0 if ($protocol eq "lmtp");
1207 eval { pmg_verify_transport_address
($host); };
1209 $parse_error->($err);
1215 protocol
=> $protocol,
1219 comment
=> $comment,
1221 $res->{$domain} = $data;
1224 $parse_error->('wrong format');
1231 sub write_transport_map
{
1232 my ($filename, $fh, $tmap) = @_;
1236 foreach my $domain (sort keys %$tmap) {
1237 my $data = $tmap->{$domain};
1239 my $comment = $data->{comment
};
1240 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1241 if defined($comment) && $comment !~ m/^\s*$/;
1243 my $bracket_host = !$data->{use_mx
};
1245 if ($data->{protocol
} eq 'lmtp') {
1247 $data->{protocol
} .= ":inet";
1249 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1250 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1252 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1256 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1257 \
&read_transport_map
,
1258 \
&write_transport_map
,
1259 undef, always_call_parser
=> 1);
1261 # config file generation using templates
1263 sub get_host_dns_info
{
1267 my $nodename = PVE
::INotify
::nodename
();
1269 $dnsinfo->{hostname
} = $nodename;
1270 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1272 my $domain = $resolv->{search
} // 'localdomain';
1273 $dnsinfo->{domain
} = $domain;
1275 $dnsinfo->{fqdn
} = "$nodename.$domain";
1280 sub get_template_vars
{
1283 my $vars = { pmg
=> $self->get_config() };
1285 my $dnsinfo = get_host_dns_info
();
1286 $vars->{dns
} = $dnsinfo;
1287 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1288 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1290 my $transportnets = [];
1292 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1293 foreach my $domain (sort keys %$tmap) {
1294 my $data = $tmap->{$domain};
1295 my $host = $data->{host
};
1296 if ($host =~ m/^$IPV4RE$/) {
1297 push @$transportnets, "$host/32";
1298 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1299 push @$transportnets, "[$1]/128";
1304 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1306 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1308 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1309 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1310 push @$mynetworks, "[$1]/$2";
1312 push @$mynetworks, $int_net_cidr;
1315 if ($int_ip =~ m/^$IPV6RE$/) {
1316 push @$mynetworks, "[$int_ip]/128";
1318 push @$mynetworks, "$int_ip/32";
1322 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1323 foreach my $cidr (sort keys %$netlist) {
1324 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1325 push @$mynetworks, "[$1]/$2";
1327 push @$mynetworks, $cidr;
1331 push @$mynetworks, @$transportnets;
1333 # add default relay to mynetworks
1334 if (my $relay = $self->get('mail', 'relay')) {
1335 if ($relay =~ m/^$IPV4RE$/) {
1336 push @$mynetworks, "$relay/32";
1337 } elsif ($relay =~ m/^$IPV6RE$/) {
1338 push @$mynetworks, "[$relay]/128";
1340 # DNS name - do nothing ?
1344 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1346 # normalize dnsbl_sites
1347 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1348 if (scalar(@dnsbl_sites)) {
1349 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1352 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1355 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1356 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1357 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1359 if ($int_ip =~ m/^$IPV6RE$/) {
1360 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1362 $vars->{postfix
}->{int_ip
} = $int_ip;
1365 my $wlbr = $dnsinfo->{fqdn
};
1366 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1369 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1371 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1373 my $uri = URI-
>new($proxy);
1374 my $host = $uri->host;
1375 my $port = $uri->port // 8080;
1377 my $data = { host
=> $host, port
=> $port };
1378 if (my $ui = $uri->userinfo) {
1379 my ($username, $pw) = split(/:/, $ui, 2);
1380 $data->{username
} = $username;
1381 $data->{password
} = $pw if defined($pw);
1383 $vars->{proxy
} = $data;
1386 warn "parse http_proxy failed - $@" if $@;
1388 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1393 # use one global TT cache
1394 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1396 my $template_toolkit;
1398 sub get_template_toolkit
{
1400 return $template_toolkit if $template_toolkit;
1402 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1404 return $template_toolkit;
1407 # rewrite file from template
1408 # return true if file has changed
1409 sub rewrite_config_file
{
1410 my ($self, $tmplname, $dstfn) = @_;
1412 my $demo = $self->get('admin', 'demo');
1415 my $demosrc = "$tmplname.demo";
1416 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1419 my ($perm, $uid, $gid);
1421 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1422 # needed if file contains a HTTPProxyPasswort
1424 $uid = getpwnam('clamav');
1425 $gid = getgrnam('adm');
1429 my $tt = get_template_toolkit
();
1431 my $vars = $self->get_template_vars();
1435 $tt->process($tmplname, $vars, \
$output) ||
1436 die $tt->error() . "\n";
1438 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1440 return 0 if defined($old) && ($old eq $output); # no change
1442 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1444 if (defined($uid) && defined($gid)) {
1445 chown($uid, $gid, $dstfn);
1451 # rewrite spam configuration
1452 sub rewrite_config_spam
{
1455 my $use_awl = $self->get('spam', 'use_awl');
1456 my $use_bayes = $self->get('spam', 'use_bayes');
1457 my $use_razor = $self->get('spam', 'use_razor');
1461 # delete AW and bayes databases if those features are disabled
1463 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1467 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1468 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1469 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1472 # make sure we have the custom SA files (else cluster sync fails)
1473 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1474 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1476 $changes = 1 if $self->rewrite_config_file(
1477 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1479 $changes = 1 if $self->rewrite_config_file(
1480 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1482 $changes = 1 if $self->rewrite_config_file(
1483 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1485 $changes = 1 if $self->rewrite_config_file(
1486 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1489 mkdir "/root/.razor";
1491 $changes = 1 if $self->rewrite_config_file(
1492 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1494 if (! -e
'/root/.razor/identity') {
1497 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1498 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1501 syslog
('info', "registering razor failed: $err") if $err;
1508 # rewrite ClamAV configuration
1509 sub rewrite_config_clam
{
1512 return $self->rewrite_config_file(
1513 'clamd.conf.in', '/etc/clamav/clamd.conf');
1516 sub rewrite_config_freshclam
{
1519 return $self->rewrite_config_file(
1520 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1523 sub rewrite_config_postgres
{
1526 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1527 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1531 $changes = 1 if $self->rewrite_config_file(
1532 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1534 $changes = 1 if $self->rewrite_config_file(
1535 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1540 # rewrite /root/.forward
1541 sub rewrite_dot_forward
{
1544 my $dstfn = '/root/.forward';
1546 my $email = $self->get('admin', 'email');
1549 if ($email && $email =~ m/\s*(\S+)\s*/) {
1552 # empty .forward does not forward mails (see man local)
1555 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1557 return 0 if defined($old) && ($old eq $output); # no change
1559 PVE
::Tools
::file_set_contents
($dstfn, $output);
1564 my $write_smtp_whitelist = sub {
1565 my ($filename, $data, $action) = @_;
1567 $action = 'OK' if !$action;
1569 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1573 foreach my $k (sort keys %$data) {
1574 $new .= "$k $action\n";
1577 return 0 if defined($old) && ($old eq $new); # no change
1579 PVE
::Tools
::file_set_contents
($filename, $new);
1581 PMG
::Utils
::run_postmap
($filename);
1586 sub rewrite_postfix_whitelist
{
1587 my ($rulecache) = @_;
1589 # see man page for regexp_table for postfix regex table format
1591 # we use a hash to avoid duplicate entries in regex tables
1594 my $clientlist = {};
1596 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1597 my $oclass = ref($obj);
1598 if ($oclass eq 'PMG::RuleDB::Receiver') {
1599 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1600 $tolist->{"/^$addr\$/"} = 1;
1601 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1602 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1603 $tolist->{"/^.+\@$addr\$/"} = 1;
1604 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1605 my $addr = $obj->{address
};
1607 $tolist->{"/^$addr\$/"} = 1;
1611 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1612 my $oclass = ref($obj);
1613 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1614 if ($oclass eq 'PMG::RuleDB::EMail') {
1615 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1616 $fromlist->{"/^$addr\$/"} = 1;
1617 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1618 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1619 $fromlist->{"/^.+\@$addr\$/"} = 1;
1620 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1621 my $addr = $obj->{address
};
1623 $fromlist->{"/^$addr\$/"} = 1;
1624 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1625 $clientlist->{$obj->{address
}} = 1;
1626 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1627 $clientlist->{$obj->{address
}} = 1;
1631 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1632 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1633 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1634 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1637 # rewrite /etc/postfix/*
1638 sub rewrite_config_postfix
{
1639 my ($self, $rulecache) = @_;
1641 # make sure we have required files (else postfix start fails)
1642 IO
::File-
>new($transport_map_filename, 'a', 0644);
1646 if ($self->get('mail', 'tls')) {
1648 PMG
::Utils
::gen_proxmox_tls_cert
();
1650 syslog
('info', "generating certificate failed: $@") if $@;
1653 $changes = 1 if $self->rewrite_config_file(
1654 'main.cf.in', '/etc/postfix/main.cf');
1656 $changes = 1 if $self->rewrite_config_file(
1657 'master.cf.in', '/etc/postfix/master.cf');
1659 # make sure we have required files (else postfix start fails)
1660 # Note: postmap need a valid /etc/postfix/main.cf configuration
1661 postmap_pmg_domains
();
1662 postmap_pmg_transport
();
1663 postmap_tls_policy
();
1665 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1667 # make sure aliases.db is up to date
1668 system('/usr/bin/newaliases');
1673 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1674 my $pmg_service_params = {
1682 dkim_sign_all_mail
=> 1,
1686 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1687 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1689 sub dump_smtp_filter_config
{
1694 foreach my $sec (sort keys %$pmg_service_params) {
1695 my $conf_sec = $self->{ids
}->{$sec} // {};
1696 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1697 $val = $conf_sec->{$key};
1698 $conf .= "$sec.$key:$val\n" if defined($val);
1705 sub compare_smtp_filter_config
{
1711 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1715 syslog
('warning', "reloading pmg-smtp-filter: $err");
1718 my $new = $self->dump_smtp_filter_config();
1719 $ret = 1 if $old ne $new;
1722 $self->write_smtp_filter_config() if $ret;
1727 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1729 sub write_smtp_filter_config
{
1732 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1733 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1734 $self->dump_smtp_filter_config());
1740 sub rewrite_config
{
1741 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1743 $force_restart = {} if ! $force_restart;
1745 my $log_restart = sub {
1746 syslog
('info', "configuration change detected for '$_[0]', restarting");
1749 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1750 $force_restart->{postfix
}) {
1751 $log_restart->('postfix');
1752 PMG
::Utils
::service_cmd
('postfix', 'reload');
1755 if ($self->rewrite_dot_forward() && $restart_services) {
1756 # no need to restart anything
1759 if ($self->rewrite_config_postgres() && $restart_services) {
1760 # do nothing (too many side effects)?
1761 # does not happen anyways, because config does not change.
1764 if (($self->rewrite_config_spam() && $restart_services) ||
1765 $force_restart->{spam
}) {
1766 $log_restart->('pmg-smtp-filter');
1767 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1770 if (($self->rewrite_config_clam() && $restart_services) ||
1771 $force_restart->{clam
}) {
1772 $log_restart->('clamav-daemon');
1773 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1776 if (($self->rewrite_config_freshclam() && $restart_services) ||
1777 $force_restart->{freshclam
}) {
1778 $log_restart->('clamav-freshclam');
1779 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1782 if (($self->compare_smtp_filter_config() && $restart_services) ||
1783 $force_restart->{spam
}) {
1784 syslog
('info', "scheduled reload for pmg-smtp-filter");
1785 PMG
::Utils
::reload_smtp_filter
();