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.",
293 description
=> "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
302 mailfrom
=> { optional
=> 1 },
303 hostname
=> { optional
=> 1 },
304 lifetime
=> { optional
=> 1 },
305 authmode
=> { optional
=> 1 },
306 reportstyle
=> { optional
=> 1 },
307 viewimages
=> { optional
=> 1 },
308 allowhrefs
=> { optional
=> 1 },
309 port
=> { optional
=> 1 },
310 protocol
=> { optional
=> 1 },
311 quarantinelink
=> { optional
=> 1 },
315 package PMG
::Config
::VirusQuarantine
;
320 use base
qw(PMG::Config::Base);
332 lifetime
=> { optional
=> 1 },
333 viewimages
=> { optional
=> 1 },
334 allowhrefs
=> { optional
=> 1 },
338 package PMG
::Config
::ClamAV
;
343 use base
qw(PMG::Config::Base);
352 description
=> "ClamAV database mirror server.",
354 default => 'database.clamav.net',
356 archiveblockencrypted
=> {
357 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'.",
362 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.",
368 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.",
374 description
=> "Files larger than this limit (in bytes) won't be scanned.",
380 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
383 default => 100000000,
386 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
392 description
=> "Enables support for Google Safe Browsing.",
397 description
=> "Enables ScriptedUpdates (incremental download of signatures)",
406 archiveblockencrypted
=> { optional
=> 1 },
407 archivemaxrec
=> { optional
=> 1 },
408 archivemaxfiles
=> { optional
=> 1 },
409 archivemaxsize
=> { optional
=> 1 },
410 maxscansize
=> { optional
=> 1 },
411 dbmirror
=> { optional
=> 1 },
412 maxcccount
=> { optional
=> 1 },
413 safebrowsing
=> { optional
=> 1 },
414 scriptedupdates
=> { optional
=> 1},
418 package PMG
::Config
::Mail
;
423 use PVE
::ProcFSTools
;
425 use base
qw(PMG::Config::Base);
432 sub physical_memory
{
434 return $physicalmem if $physicalmem;
436 my $info = PVE
::ProcFSTools
::read_meminfo
();
437 my $total = int($info->{memtotal
} / (1024*1024));
442 sub get_max_filters
{
443 # estimate optimal number of filter servers
447 my $memory = physical_memory
();
448 my $add_servers = int(($memory - 512)/$servermem);
449 $max_servers += $add_servers if $add_servers > 0;
450 $max_servers = 40 if $max_servers > 40;
452 return $max_servers - 2;
456 # estimate optimal number of smtpd daemons
458 my $max_servers = 25;
460 my $memory = physical_memory
();
461 my $add_servers = int(($memory - 512)/$servermem);
462 $max_servers += $add_servers if $add_servers > 0;
463 $max_servers = 100 if $max_servers > 100;
468 # estimate optimal number of proxpolicy servers
470 my $memory = physical_memory
();
471 $max_servers = 5 if $memory >= 500;
478 description
=> "SMTP port number for outgoing mail (trusted).",
485 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
492 description
=> "The default mail delivery transport (incoming mails).",
493 type
=> 'string', format
=> 'address',
496 description
=> "Transport protocol for relay host.",
498 enum
=> [qw(smtp lmtp)],
502 description
=> "SMTP/LMTP port number for relay host.",
509 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
514 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
515 type
=> 'string', format
=> 'address',
518 description
=> "SMTP port number for smarthost.",
525 description
=> "ESMTP banner.",
528 default => 'ESMTP Proxmox',
531 description
=> "Maximum number of pmg-smtp-filter processes.",
535 default => get_max_filters
(),
538 description
=> "Maximum number of pmgpolicy processes.",
542 default => get_max_policy
(),
545 description
=> "Maximum number of SMTP daemon processes (in).",
549 default => get_max_smtpd
(),
552 description
=> "Maximum number of SMTP daemon processes (out).",
556 default => get_max_smtpd
(),
558 conn_count_limit
=> {
559 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
565 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.",
570 message_rate_limit
=> {
571 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.",
577 description
=> "Hide received header in outgoing mails.",
582 description
=> "Maximum email size. Larger mails are rejected.",
585 default => 1024*1024*10,
588 description
=> "SMTP delay warning time (in hours).",
594 description
=> "Enable TLS.",
599 description
=> "Enable TLS Logging.",
604 description
=> "Add TLS received header.",
609 description
=> "Use Sender Policy Framework.",
614 description
=> "Use Greylisting for IPv4.",
619 description
=> "Netmask to apply for greylisting IPv4 hosts",
626 description
=> "Use Greylisting for IPv6.",
631 description
=> "Netmask to apply for greylisting IPv6 hosts",
638 description
=> "Use SMTP HELO tests.",
643 description
=> "Reject unknown clients.",
647 rejectunknownsender
=> {
648 description
=> "Reject unknown senders.",
653 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
655 enum
=> ['450', '550'],
658 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
659 type
=> 'string', format
=> 'dnsbl-entry-list',
662 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
667 before_queue_filtering
=> {
668 description
=> "Enable before queue filtering by pmg-smtp-filter",
673 description
=> "Send out NDR when mail gets blocked",
682 int_port
=> { optional
=> 1 },
683 ext_port
=> { optional
=> 1 },
684 smarthost
=> { optional
=> 1 },
685 smarthostport
=> { optional
=> 1 },
686 relay
=> { optional
=> 1 },
687 relayprotocol
=> { optional
=> 1 },
688 relayport
=> { optional
=> 1 },
689 relaynomx
=> { optional
=> 1 },
690 dwarning
=> { optional
=> 1 },
691 max_smtpd_in
=> { optional
=> 1 },
692 max_smtpd_out
=> { optional
=> 1 },
693 greylist
=> { optional
=> 1 },
694 greylistmask4
=> { optional
=> 1 },
695 greylist6
=> { optional
=> 1 },
696 greylistmask6
=> { optional
=> 1 },
697 helotests
=> { optional
=> 1 },
698 tls
=> { optional
=> 1 },
699 tlslog
=> { optional
=> 1 },
700 tlsheader
=> { optional
=> 1 },
701 spf
=> { optional
=> 1 },
702 maxsize
=> { optional
=> 1 },
703 banner
=> { optional
=> 1 },
704 max_filters
=> { optional
=> 1 },
705 max_policy
=> { optional
=> 1 },
706 hide_received
=> { optional
=> 1 },
707 rejectunknown
=> { optional
=> 1 },
708 rejectunknownsender
=> { optional
=> 1 },
709 conn_count_limit
=> { optional
=> 1 },
710 conn_rate_limit
=> { optional
=> 1 },
711 message_rate_limit
=> { optional
=> 1 },
712 verifyreceivers
=> { optional
=> 1 },
713 dnsbl_sites
=> { optional
=> 1 },
714 dnsbl_threshold
=> { optional
=> 1 },
715 before_queue_filtering
=> { optional
=> 1 },
716 ndr_on_block
=> { optional
=> 1 },
729 use PVE
::Tools
qw($IPV4RE $IPV6RE);
736 PMG
::Config
::Admin-
>register();
737 PMG
::Config
::Mail-
>register();
738 PMG
::Config
::SpamQuarantine-
>register();
739 PMG
::Config
::VirusQuarantine-
>register();
740 PMG
::Config
::Spam-
>register();
741 PMG
::Config
::ClamAV-
>register();
743 # initialize all plugins
744 PMG
::Config
::Base-
>init();
746 PVE
::JSONSchema
::register_format
(
747 'transport-domain', \
&pmg_verify_transport_domain
);
749 sub pmg_verify_transport_domain
{
750 my ($name, $noerr) = @_;
752 # like dns-name, but can contain leading dot
753 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
755 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
756 return undef if $noerr;
757 die "value does not look like a valid transport domain\n";
762 PVE
::JSONSchema
::register_format
(
763 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
765 sub pmg_verify_transport_domain_or_email
{
766 my ($name, $noerr) = @_;
768 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
771 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
775 # like dns-name, but can contain leading dot
776 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
777 return undef if $noerr;
778 die "value does not look like a valid transport domain or email address\n";
783 PVE
::JSONSchema
::register_format
(
784 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
786 sub pmg_verify_dnsbl_entry
{
787 my ($name, $noerr) = @_;
789 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
790 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
791 # we don't implement the ';' separated numbers in pattern, because this
792 # breaks at PVE::JSONSchema::split_list
793 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
795 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
796 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
797 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
798 return undef if $noerr;
799 die "value '$name' does not look like a valid dnsbl entry\n";
807 my $class = ref($type) || $type;
809 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
811 return bless $cfg, $class;
817 PVE
::INotify
::write_file
("pmg.conf", $self);
820 my $lockfile = "/var/lock/pmgconfig.lck";
823 my ($code, $errmsg) = @_;
825 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
827 $errmsg ?
die "$errmsg: $err" : die $err;
833 my ($self, $section, $key, $value) = @_;
835 my $pdata = PMG
::Config
::Base-
>private();
837 my $plugin = $pdata->{plugins
}->{$section};
838 die "no such section '$section'" if !$plugin;
840 if (defined($value)) {
841 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
842 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
843 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
845 if (defined($self->{ids
}->{$section})) {
846 delete $self->{ids
}->{$section}->{$key};
853 # get section value or default
855 my ($self, $section, $key, $nodefault) = @_;
857 my $pdata = PMG
::Config
::Base-
>private();
858 my $pdesc = $pdata->{propertyList
}->{$key};
859 die "no such property '$section/$key'\n"
860 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
861 defined($pdata->{options
}->{$section}->{$key}));
863 if (defined($self->{ids
}->{$section}) &&
864 defined(my $value = $self->{ids
}->{$section}->{$key})) {
868 return undef if $nodefault;
870 return $pdesc->{default};
873 # get a whole section with default value
875 my ($self, $section) = @_;
877 my $pdata = PMG
::Config
::Base-
>private();
878 return undef if !defined($pdata->{options
}->{$section});
882 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
884 my $pdesc = $pdata->{propertyList
}->{$key};
886 if (defined($self->{ids
}->{$section}) &&
887 defined(my $value = $self->{ids
}->{$section}->{$key})) {
888 $res->{$key} = $value;
891 $res->{$key} = $pdesc->{default};
897 # get a whole config with default values
901 my $pdata = PMG
::Config
::Base-
>private();
905 foreach my $type (keys %{$pdata->{plugins
}}) {
906 my $plugin = $pdata->{plugins
}->{$type};
907 $res->{$type} = $self->get_section($type);
914 my ($filename, $fh) = @_;
916 local $/ = undef; # slurp mode
918 my $raw = <$fh> if defined($fh);
920 return PMG
::Config
::Base-
>parse_config($filename, $raw);
924 my ($filename, $fh, $cfg) = @_;
926 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
928 PVE
::Tools
::safe_print
($filename, $fh, $raw);
931 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
934 undef, always_call_parser
=> 1);
936 # parsers/writers for other files
938 my $domainsfilename = "/etc/pmg/domains";
940 sub postmap_pmg_domains
{
941 PMG
::Utils
::run_postmap
($domainsfilename);
944 sub read_pmg_domains
{
945 my ($filename, $fh) = @_;
951 while (defined(my $line = <$fh>)) {
953 next if $line =~ m/^\s*$/;
954 if ($line =~ m/^#(.*)\s*$/) {
958 if ($line =~ m/^(\S+)\s.*$/) {
960 $domains->{$domain} = {
961 domain
=> $domain, comment
=> $comment };
964 warn "parse error in '$filename': $line\n";
973 sub write_pmg_domains
{
974 my ($filename, $fh, $domains) = @_;
976 foreach my $domain (sort keys %$domains) {
977 my $comment = $domains->{$domain}->{comment
};
978 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
979 if defined($comment) && $comment !~ m/^\s*$/;
981 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
985 PVE
::INotify
::register_file
('domains', $domainsfilename,
988 undef, always_call_parser
=> 1);
990 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
992 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
995 undef, always_call_parser
=> 1);
997 my $mynetworks_filename = "/etc/pmg/mynetworks";
999 sub read_pmg_mynetworks
{
1000 my ($filename, $fh) = @_;
1002 my $mynetworks = {};
1006 while (defined(my $line = <$fh>)) {
1008 next if $line =~ m/^\s*$/;
1009 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1010 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1011 my $cidr = "$network/${prefix_size}";
1012 $mynetworks->{$cidr} = {
1014 network_address
=> $network,
1015 prefix_size
=> $prefix_size,
1016 comment
=> $comment // '',
1019 warn "parse error in '$filename': $line\n";
1027 sub write_pmg_mynetworks
{
1028 my ($filename, $fh, $mynetworks) = @_;
1030 foreach my $cidr (sort keys %$mynetworks) {
1031 my $data = $mynetworks->{$cidr};
1032 my $comment = $data->{comment
} // '*';
1033 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1037 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1038 \
&read_pmg_mynetworks
,
1039 \
&write_pmg_mynetworks
,
1040 undef, always_call_parser
=> 1);
1042 PVE
::JSONSchema
::register_format
(
1043 'tls-policy', \
&pmg_verify_tls_policy
);
1045 # TODO: extend to parse attributes of the policy
1046 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1047 sub pmg_verify_tls_policy
{
1048 my ($policy, $noerr) = @_;
1050 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1051 return undef if $noerr;
1052 die "value '$policy' does not look like a valid tls policy\n";
1057 PVE
::JSONSchema
::register_format
(
1058 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1060 sub pmg_verify_tls_policy_strict
{
1061 my ($policy, $noerr) = @_;
1063 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1064 return undef if $noerr;
1065 die "value '$policy' does not look like a valid tls policy\n";
1070 PVE
::JSONSchema
::register_format
(
1071 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1073 sub pmg_verify_transport_domain_or_nexthop
{
1074 my ($name, $noerr) = @_;
1076 if (pmg_verify_transport_domain
($name, 1)) {
1078 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1080 if ($nexthop =~ m/^\[(.*)\]$/) {
1083 return $name if pmg_verify_transport_address
($nexthop, 1);
1085 return undef if $noerr;
1086 die "value does not look like a valid domain or next-hop\n";
1090 sub read_tls_policy
{
1091 my ($filename, $fh) = @_;
1093 return {} if !defined($fh);
1095 my $tls_policy = {};
1097 while (defined(my $line = <$fh>)) {
1099 next if $line =~ m/^\s*$/;
1100 next if $line =~ m/^#(.*)\s*$/;
1102 my $parse_error = sub {
1104 die "parse error in '$filename': $line - $err";
1107 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1108 my ($destination, $policy) = ($1, $2);
1111 pmg_verify_transport_domain_or_nexthop
($destination);
1112 pmg_verify_tls_policy
($policy);
1115 $parse_error->($err);
1119 $tls_policy->{$destination} = {
1120 destination
=> $destination,
1124 $parse_error->('wrong format');
1131 sub write_tls_policy
{
1132 my ($filename, $fh, $tls_policy) = @_;
1134 return if !$tls_policy;
1136 foreach my $destination (sort keys %$tls_policy) {
1137 my $entry = $tls_policy->{$destination};
1138 PVE
::Tools
::safe_print
(
1139 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1143 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1144 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1147 undef, always_call_parser
=> 1);
1149 sub postmap_tls_policy
{
1150 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1153 my $transport_map_filename = "/etc/pmg/transport";
1155 sub postmap_pmg_transport
{
1156 PMG
::Utils
::run_postmap
($transport_map_filename);
1159 PVE
::JSONSchema
::register_format
(
1160 'transport-address', \
&pmg_verify_transport_address
);
1162 sub pmg_verify_transport_address
{
1163 my ($name, $noerr) = @_;
1165 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1167 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1170 return undef if $noerr;
1171 die "value does not look like a valid address\n";
1175 sub read_transport_map
{
1176 my ($filename, $fh) = @_;
1178 return [] if !defined($fh);
1184 while (defined(my $line = <$fh>)) {
1186 next if $line =~ m/^\s*$/;
1187 if ($line =~ m/^#(.*)\s*$/) {
1192 my $parse_error = sub {
1194 warn "parse error in '$filename': $line - $err";
1198 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1199 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1201 eval { pmg_verify_transport_domain_or_email
($domain); };
1203 $parse_error->($err);
1207 if ($host =~ m/^\[(.*)\]$/) {
1211 $use_mx = 0 if ($protocol eq "lmtp");
1213 eval { pmg_verify_transport_address
($host); };
1215 $parse_error->($err);
1221 protocol
=> $protocol,
1225 comment
=> $comment,
1227 $res->{$domain} = $data;
1230 $parse_error->('wrong format');
1237 sub write_transport_map
{
1238 my ($filename, $fh, $tmap) = @_;
1242 foreach my $domain (sort keys %$tmap) {
1243 my $data = $tmap->{$domain};
1245 my $comment = $data->{comment
};
1246 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1247 if defined($comment) && $comment !~ m/^\s*$/;
1249 my $bracket_host = !$data->{use_mx
};
1251 if ($data->{protocol
} eq 'lmtp') {
1253 $data->{protocol
} .= ":inet";
1255 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1256 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1258 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1262 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1263 \
&read_transport_map
,
1264 \
&write_transport_map
,
1265 undef, always_call_parser
=> 1);
1267 # config file generation using templates
1269 sub get_host_dns_info
{
1273 my $nodename = PVE
::INotify
::nodename
();
1275 $dnsinfo->{hostname
} = $nodename;
1276 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1278 my $domain = $resolv->{search
} // 'localdomain';
1279 $dnsinfo->{domain
} = $domain;
1281 $dnsinfo->{fqdn
} = "$nodename.$domain";
1286 sub get_template_vars
{
1289 my $vars = { pmg
=> $self->get_config() };
1291 my $dnsinfo = get_host_dns_info
();
1292 $vars->{dns
} = $dnsinfo;
1293 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1294 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1296 my $transportnets = [];
1298 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1299 foreach my $domain (sort keys %$tmap) {
1300 my $data = $tmap->{$domain};
1301 my $host = $data->{host
};
1302 if ($host =~ m/^$IPV4RE$/) {
1303 push @$transportnets, "$host/32";
1304 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1305 push @$transportnets, "[$1]/128";
1310 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1312 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1314 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1315 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1316 push @$mynetworks, "[$1]/$2";
1318 push @$mynetworks, $int_net_cidr;
1321 if ($int_ip =~ m/^$IPV6RE$/) {
1322 push @$mynetworks, "[$int_ip]/128";
1324 push @$mynetworks, "$int_ip/32";
1328 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1329 foreach my $cidr (sort keys %$netlist) {
1330 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1331 push @$mynetworks, "[$1]/$2";
1333 push @$mynetworks, $cidr;
1337 push @$mynetworks, @$transportnets;
1339 # add default relay to mynetworks
1340 if (my $relay = $self->get('mail', 'relay')) {
1341 if ($relay =~ m/^$IPV4RE$/) {
1342 push @$mynetworks, "$relay/32";
1343 } elsif ($relay =~ m/^$IPV6RE$/) {
1344 push @$mynetworks, "[$relay]/128";
1346 # DNS name - do nothing ?
1350 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1352 # normalize dnsbl_sites
1353 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1354 if (scalar(@dnsbl_sites)) {
1355 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1358 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1361 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1362 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1363 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1365 if ($int_ip =~ m/^$IPV6RE$/) {
1366 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1368 $vars->{postfix
}->{int_ip
} = $int_ip;
1371 my $wlbr = $dnsinfo->{fqdn
};
1372 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1375 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1377 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1379 my $uri = URI-
>new($proxy);
1380 my $host = $uri->host;
1381 my $port = $uri->port // 8080;
1383 my $data = { host
=> $host, port
=> $port };
1384 if (my $ui = $uri->userinfo) {
1385 my ($username, $pw) = split(/:/, $ui, 2);
1386 $data->{username
} = $username;
1387 $data->{password
} = $pw if defined($pw);
1389 $vars->{proxy
} = $data;
1392 warn "parse http_proxy failed - $@" if $@;
1394 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1399 # use one global TT cache
1400 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1402 my $template_toolkit;
1404 sub get_template_toolkit
{
1406 return $template_toolkit if $template_toolkit;
1408 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1410 return $template_toolkit;
1413 # rewrite file from template
1414 # return true if file has changed
1415 sub rewrite_config_file
{
1416 my ($self, $tmplname, $dstfn) = @_;
1418 my $demo = $self->get('admin', 'demo');
1421 my $demosrc = "$tmplname.demo";
1422 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1425 my ($perm, $uid, $gid);
1427 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1428 # needed if file contains a HTTPProxyPasswort
1430 $uid = getpwnam('clamav');
1431 $gid = getgrnam('adm');
1435 my $tt = get_template_toolkit
();
1437 my $vars = $self->get_template_vars();
1441 $tt->process($tmplname, $vars, \
$output) ||
1442 die $tt->error() . "\n";
1444 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1446 return 0 if defined($old) && ($old eq $output); # no change
1448 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1450 if (defined($uid) && defined($gid)) {
1451 chown($uid, $gid, $dstfn);
1457 # rewrite spam configuration
1458 sub rewrite_config_spam
{
1461 my $use_awl = $self->get('spam', 'use_awl');
1462 my $use_bayes = $self->get('spam', 'use_bayes');
1463 my $use_razor = $self->get('spam', 'use_razor');
1467 # delete AW and bayes databases if those features are disabled
1469 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1473 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1474 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1475 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1478 # make sure we have the custom SA files (else cluster sync fails)
1479 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1480 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1482 $changes = 1 if $self->rewrite_config_file(
1483 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1485 $changes = 1 if $self->rewrite_config_file(
1486 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1488 $changes = 1 if $self->rewrite_config_file(
1489 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1491 $changes = 1 if $self->rewrite_config_file(
1492 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1495 mkdir "/root/.razor";
1497 $changes = 1 if $self->rewrite_config_file(
1498 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1500 if (! -e
'/root/.razor/identity') {
1503 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1504 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1507 syslog
('info', "registering razor failed: $err") if $err;
1514 # rewrite ClamAV configuration
1515 sub rewrite_config_clam
{
1518 return $self->rewrite_config_file(
1519 'clamd.conf.in', '/etc/clamav/clamd.conf');
1522 sub rewrite_config_freshclam
{
1525 return $self->rewrite_config_file(
1526 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1529 sub rewrite_config_postgres
{
1532 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1533 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1537 $changes = 1 if $self->rewrite_config_file(
1538 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1540 $changes = 1 if $self->rewrite_config_file(
1541 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1546 # rewrite /root/.forward
1547 sub rewrite_dot_forward
{
1550 my $dstfn = '/root/.forward';
1552 my $email = $self->get('admin', 'email');
1555 if ($email && $email =~ m/\s*(\S+)\s*/) {
1558 # empty .forward does not forward mails (see man local)
1561 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1563 return 0 if defined($old) && ($old eq $output); # no change
1565 PVE
::Tools
::file_set_contents
($dstfn, $output);
1570 my $write_smtp_whitelist = sub {
1571 my ($filename, $data, $action) = @_;
1573 $action = 'OK' if !$action;
1575 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1579 foreach my $k (sort keys %$data) {
1580 $new .= "$k $action\n";
1583 return 0 if defined($old) && ($old eq $new); # no change
1585 PVE
::Tools
::file_set_contents
($filename, $new);
1587 PMG
::Utils
::run_postmap
($filename);
1592 sub rewrite_postfix_whitelist
{
1593 my ($rulecache) = @_;
1595 # see man page for regexp_table for postfix regex table format
1597 # we use a hash to avoid duplicate entries in regex tables
1600 my $clientlist = {};
1602 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1603 my $oclass = ref($obj);
1604 if ($oclass eq 'PMG::RuleDB::Receiver') {
1605 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1606 $tolist->{"/^$addr\$/"} = 1;
1607 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1608 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1609 $tolist->{"/^.+\@$addr\$/"} = 1;
1610 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1611 my $addr = $obj->{address
};
1613 $tolist->{"/^$addr\$/"} = 1;
1617 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1618 my $oclass = ref($obj);
1619 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1620 if ($oclass eq 'PMG::RuleDB::EMail') {
1621 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1622 $fromlist->{"/^$addr\$/"} = 1;
1623 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1624 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1625 $fromlist->{"/^.+\@$addr\$/"} = 1;
1626 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1627 my $addr = $obj->{address
};
1629 $fromlist->{"/^$addr\$/"} = 1;
1630 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1631 $clientlist->{$obj->{address
}} = 1;
1632 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1633 $clientlist->{$obj->{address
}} = 1;
1637 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1638 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1639 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1640 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1643 # rewrite /etc/postfix/*
1644 sub rewrite_config_postfix
{
1645 my ($self, $rulecache) = @_;
1647 # make sure we have required files (else postfix start fails)
1648 IO
::File-
>new($transport_map_filename, 'a', 0644);
1652 if ($self->get('mail', 'tls')) {
1654 PMG
::Utils
::gen_proxmox_tls_cert
();
1656 syslog
('info', "generating certificate failed: $@") if $@;
1659 $changes = 1 if $self->rewrite_config_file(
1660 'main.cf.in', '/etc/postfix/main.cf');
1662 $changes = 1 if $self->rewrite_config_file(
1663 'master.cf.in', '/etc/postfix/master.cf');
1665 # make sure we have required files (else postfix start fails)
1666 # Note: postmap need a valid /etc/postfix/main.cf configuration
1667 postmap_pmg_domains
();
1668 postmap_pmg_transport
();
1669 postmap_tls_policy
();
1671 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1673 # make sure aliases.db is up to date
1674 system('/usr/bin/newaliases');
1679 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1680 my $pmg_service_params = {
1688 dkim_sign_all_mail
=> 1,
1692 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1693 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1695 sub dump_smtp_filter_config
{
1700 foreach my $sec (sort keys %$pmg_service_params) {
1701 my $conf_sec = $self->{ids
}->{$sec} // {};
1702 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1703 $val = $conf_sec->{$key};
1704 $conf .= "$sec.$key:$val\n" if defined($val);
1711 sub compare_smtp_filter_config
{
1717 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1721 syslog
('warning', "reloading pmg-smtp-filter: $err");
1724 my $new = $self->dump_smtp_filter_config();
1725 $ret = 1 if $old ne $new;
1728 $self->write_smtp_filter_config() if $ret;
1733 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1735 sub write_smtp_filter_config
{
1738 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1739 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1740 $self->dump_smtp_filter_config());
1746 sub rewrite_config
{
1747 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1749 $force_restart = {} if ! $force_restart;
1751 my $log_restart = sub {
1752 syslog
('info', "configuration change detected for '$_[0]', restarting");
1755 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1756 $force_restart->{postfix
}) {
1757 $log_restart->('postfix');
1758 PMG
::Utils
::service_cmd
('postfix', 'reload');
1761 if ($self->rewrite_dot_forward() && $restart_services) {
1762 # no need to restart anything
1765 if ($self->rewrite_config_postgres() && $restart_services) {
1766 # do nothing (too many side effects)?
1767 # does not happen anyways, because config does not change.
1770 if (($self->rewrite_config_spam() && $restart_services) ||
1771 $force_restart->{spam
}) {
1772 $log_restart->('pmg-smtp-filter');
1773 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1776 if (($self->rewrite_config_clam() && $restart_services) ||
1777 $force_restart->{clam
}) {
1778 $log_restart->('clamav-daemon');
1779 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1782 if (($self->rewrite_config_freshclam() && $restart_services) ||
1783 $force_restart->{freshclam
}) {
1784 $log_restart->('clamav-freshclam');
1785 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1788 if (($self->compare_smtp_filter_config() && $restart_services) ||
1789 $force_restart->{spam
}) {
1790 syslog
('info', "scheduled reload for pmg-smtp-filter");
1791 PMG
::Utils
::reload_smtp_filter
();