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.",
395 archiveblockencrypted
=> { optional
=> 1 },
396 archivemaxrec
=> { optional
=> 1 },
397 archivemaxfiles
=> { optional
=> 1 },
398 archivemaxsize
=> { optional
=> 1 },
399 maxscansize
=> { optional
=> 1 },
400 dbmirror
=> { optional
=> 1 },
401 maxcccount
=> { optional
=> 1 },
402 safebrowsing
=> { optional
=> 1 },
406 package PMG
::Config
::Mail
;
411 use PVE
::ProcFSTools
;
413 use base
qw(PMG::Config::Base);
420 sub physical_memory
{
422 return $physicalmem if $physicalmem;
424 my $info = PVE
::ProcFSTools
::read_meminfo
();
425 my $total = int($info->{memtotal
} / (1024*1024));
430 sub get_max_filters
{
431 # estimate optimal number of filter servers
435 my $memory = physical_memory
();
436 my $add_servers = int(($memory - 512)/$servermem);
437 $max_servers += $add_servers if $add_servers > 0;
438 $max_servers = 40 if $max_servers > 40;
440 return $max_servers - 2;
444 # estimate optimal number of smtpd daemons
446 my $max_servers = 25;
448 my $memory = physical_memory
();
449 my $add_servers = int(($memory - 512)/$servermem);
450 $max_servers += $add_servers if $add_servers > 0;
451 $max_servers = 100 if $max_servers > 100;
456 # estimate optimal number of proxpolicy servers
458 my $memory = physical_memory
();
459 $max_servers = 5 if $memory >= 500;
466 description
=> "SMTP port number for outgoing mail (trusted).",
473 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
480 description
=> "The default mail delivery transport (incoming mails).",
481 type
=> 'string', format
=> 'address',
484 description
=> "Transport protocol for relay host.",
486 enum
=> [qw(smtp lmtp)],
490 description
=> "SMTP/LMTP port number for relay host.",
497 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
502 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
503 type
=> 'string', format
=> 'address',
506 description
=> "SMTP port number for smarthost.",
513 description
=> "ESMTP banner.",
516 default => 'ESMTP Proxmox',
519 description
=> "Maximum number of pmg-smtp-filter processes.",
523 default => get_max_filters
(),
526 description
=> "Maximum number of pmgpolicy processes.",
530 default => get_max_policy
(),
533 description
=> "Maximum number of SMTP daemon processes (in).",
537 default => get_max_smtpd
(),
540 description
=> "Maximum number of SMTP daemon processes (out).",
544 default => get_max_smtpd
(),
546 conn_count_limit
=> {
547 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
553 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.",
558 message_rate_limit
=> {
559 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.",
565 description
=> "Hide received header in outgoing mails.",
570 description
=> "Maximum email size. Larger mails are rejected.",
573 default => 1024*1024*10,
576 description
=> "SMTP delay warning time (in hours).",
582 description
=> "Enable TLS.",
587 description
=> "Enable TLS Logging.",
592 description
=> "Add TLS received header.",
597 description
=> "Use Sender Policy Framework.",
602 description
=> "Use Greylisting.",
607 description
=> "Use SMTP HELO tests.",
612 description
=> "Reject unknown clients.",
616 rejectunknownsender
=> {
617 description
=> "Reject unknown senders.",
622 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
624 enum
=> ['450', '550'],
627 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
628 type
=> 'string', format
=> 'dnsbl-entry-list',
631 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
636 before_queue_filtering
=> {
637 description
=> "Enable before queue filtering by pmg-smtp-filter",
642 description
=> "Send out NDR when mail gets blocked",
651 int_port
=> { optional
=> 1 },
652 ext_port
=> { optional
=> 1 },
653 smarthost
=> { optional
=> 1 },
654 smarthostport
=> { optional
=> 1 },
655 relay
=> { optional
=> 1 },
656 relayprotocol
=> { optional
=> 1 },
657 relayport
=> { optional
=> 1 },
658 relaynomx
=> { optional
=> 1 },
659 dwarning
=> { optional
=> 1 },
660 max_smtpd_in
=> { optional
=> 1 },
661 max_smtpd_out
=> { optional
=> 1 },
662 greylist
=> { optional
=> 1 },
663 helotests
=> { optional
=> 1 },
664 tls
=> { optional
=> 1 },
665 tlslog
=> { optional
=> 1 },
666 tlsheader
=> { optional
=> 1 },
667 spf
=> { optional
=> 1 },
668 maxsize
=> { optional
=> 1 },
669 banner
=> { optional
=> 1 },
670 max_filters
=> { optional
=> 1 },
671 max_policy
=> { optional
=> 1 },
672 hide_received
=> { optional
=> 1 },
673 rejectunknown
=> { optional
=> 1 },
674 rejectunknownsender
=> { optional
=> 1 },
675 conn_count_limit
=> { optional
=> 1 },
676 conn_rate_limit
=> { optional
=> 1 },
677 message_rate_limit
=> { optional
=> 1 },
678 verifyreceivers
=> { optional
=> 1 },
679 dnsbl_sites
=> { optional
=> 1 },
680 dnsbl_threshold
=> { optional
=> 1 },
681 before_queue_filtering
=> { optional
=> 1 },
682 ndr_on_block
=> { optional
=> 1 },
695 use PVE
::Tools
qw($IPV4RE $IPV6RE);
702 PMG
::Config
::Admin-
>register();
703 PMG
::Config
::Mail-
>register();
704 PMG
::Config
::SpamQuarantine-
>register();
705 PMG
::Config
::VirusQuarantine-
>register();
706 PMG
::Config
::Spam-
>register();
707 PMG
::Config
::ClamAV-
>register();
709 # initialize all plugins
710 PMG
::Config
::Base-
>init();
712 PVE
::JSONSchema
::register_format
(
713 'transport-domain', \
&pmg_verify_transport_domain
);
715 sub pmg_verify_transport_domain
{
716 my ($name, $noerr) = @_;
718 # like dns-name, but can contain leading dot
719 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
721 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
722 return undef if $noerr;
723 die "value does not look like a valid transport domain\n";
728 PVE
::JSONSchema
::register_format
(
729 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
731 sub pmg_verify_transport_domain_or_email
{
732 my ($name, $noerr) = @_;
734 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
737 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
741 # like dns-name, but can contain leading dot
742 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
743 return undef if $noerr;
744 die "value does not look like a valid transport domain or email address\n";
749 PVE
::JSONSchema
::register_format
(
750 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
752 sub pmg_verify_dnsbl_entry
{
753 my ($name, $noerr) = @_;
755 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
756 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
757 # we don't implement the ';' separated numbers in pattern, because this
758 # breaks at PVE::JSONSchema::split_list
759 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
761 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
762 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
763 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
764 return undef if $noerr;
765 die "value '$name' does not look like a valid dnsbl entry\n";
773 my $class = ref($type) || $type;
775 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
777 return bless $cfg, $class;
783 PVE
::INotify
::write_file
("pmg.conf", $self);
786 my $lockfile = "/var/lock/pmgconfig.lck";
789 my ($code, $errmsg) = @_;
791 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
793 $errmsg ?
die "$errmsg: $err" : die $err;
799 my ($self, $section, $key, $value) = @_;
801 my $pdata = PMG
::Config
::Base-
>private();
803 my $plugin = $pdata->{plugins
}->{$section};
804 die "no such section '$section'" if !$plugin;
806 if (defined($value)) {
807 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
808 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
809 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
811 if (defined($self->{ids
}->{$section})) {
812 delete $self->{ids
}->{$section}->{$key};
819 # get section value or default
821 my ($self, $section, $key, $nodefault) = @_;
823 my $pdata = PMG
::Config
::Base-
>private();
824 my $pdesc = $pdata->{propertyList
}->{$key};
825 die "no such property '$section/$key'\n"
826 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
827 defined($pdata->{options
}->{$section}->{$key}));
829 if (defined($self->{ids
}->{$section}) &&
830 defined(my $value = $self->{ids
}->{$section}->{$key})) {
834 return undef if $nodefault;
836 return $pdesc->{default};
839 # get a whole section with default value
841 my ($self, $section) = @_;
843 my $pdata = PMG
::Config
::Base-
>private();
844 return undef if !defined($pdata->{options
}->{$section});
848 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
850 my $pdesc = $pdata->{propertyList
}->{$key};
852 if (defined($self->{ids
}->{$section}) &&
853 defined(my $value = $self->{ids
}->{$section}->{$key})) {
854 $res->{$key} = $value;
857 $res->{$key} = $pdesc->{default};
863 # get a whole config with default values
867 my $pdata = PMG
::Config
::Base-
>private();
871 foreach my $type (keys %{$pdata->{plugins
}}) {
872 my $plugin = $pdata->{plugins
}->{$type};
873 $res->{$type} = $self->get_section($type);
880 my ($filename, $fh) = @_;
882 local $/ = undef; # slurp mode
884 my $raw = <$fh> if defined($fh);
886 return PMG
::Config
::Base-
>parse_config($filename, $raw);
890 my ($filename, $fh, $cfg) = @_;
892 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
894 PVE
::Tools
::safe_print
($filename, $fh, $raw);
897 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
900 undef, always_call_parser
=> 1);
902 # parsers/writers for other files
904 my $domainsfilename = "/etc/pmg/domains";
906 sub postmap_pmg_domains
{
907 PMG
::Utils
::run_postmap
($domainsfilename);
910 sub read_pmg_domains
{
911 my ($filename, $fh) = @_;
917 while (defined(my $line = <$fh>)) {
919 next if $line =~ m/^\s*$/;
920 if ($line =~ m/^#(.*)\s*$/) {
924 if ($line =~ m/^(\S+)\s.*$/) {
926 $domains->{$domain} = {
927 domain
=> $domain, comment
=> $comment };
930 warn "parse error in '$filename': $line\n";
939 sub write_pmg_domains
{
940 my ($filename, $fh, $domains) = @_;
942 foreach my $domain (sort keys %$domains) {
943 my $comment = $domains->{$domain}->{comment
};
944 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
945 if defined($comment) && $comment !~ m/^\s*$/;
947 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
951 PVE
::INotify
::register_file
('domains', $domainsfilename,
954 undef, always_call_parser
=> 1);
956 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
958 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
961 undef, always_call_parser
=> 1);
963 my $mynetworks_filename = "/etc/pmg/mynetworks";
965 sub read_pmg_mynetworks
{
966 my ($filename, $fh) = @_;
972 while (defined(my $line = <$fh>)) {
974 next if $line =~ m/^\s*$/;
975 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
976 my ($network, $prefix_size, $comment) = ($1, $2, $3);
977 my $cidr = "$network/${prefix_size}";
978 $mynetworks->{$cidr} = {
980 network_address
=> $network,
981 prefix_size
=> $prefix_size,
982 comment
=> $comment // '',
985 warn "parse error in '$filename': $line\n";
993 sub write_pmg_mynetworks
{
994 my ($filename, $fh, $mynetworks) = @_;
996 foreach my $cidr (sort keys %$mynetworks) {
997 my $data = $mynetworks->{$cidr};
998 my $comment = $data->{comment
} // '*';
999 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1003 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1004 \
&read_pmg_mynetworks
,
1005 \
&write_pmg_mynetworks
,
1006 undef, always_call_parser
=> 1);
1008 PVE
::JSONSchema
::register_format
(
1009 'tls-policy', \
&pmg_verify_tls_policy
);
1011 # TODO: extend to parse attributes of the policy
1012 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1013 sub pmg_verify_tls_policy
{
1014 my ($policy, $noerr) = @_;
1016 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1017 return undef if $noerr;
1018 die "value '$policy' does not look like a valid tls policy\n";
1023 PVE
::JSONSchema
::register_format
(
1024 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1026 sub pmg_verify_tls_policy_strict
{
1027 my ($policy, $noerr) = @_;
1029 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1030 return undef if $noerr;
1031 die "value '$policy' does not look like a valid tls policy\n";
1036 sub read_tls_policy
{
1037 my ($filename, $fh) = @_;
1039 return {} if !defined($fh);
1041 my $tls_policy = {};
1043 while (defined(my $line = <$fh>)) {
1045 next if $line =~ m/^\s*$/;
1046 next if $line =~ m/^#(.*)\s*$/;
1048 my $parse_error = sub {
1050 die "parse error in '$filename': $line - $err";
1053 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1054 my ($domain, $policy) = ($1, $2);
1057 pmg_verify_transport_domain
($domain);
1058 pmg_verify_tls_policy
($policy);
1061 $parse_error->($err);
1065 $tls_policy->{$domain} = {
1070 $parse_error->('wrong format');
1077 sub write_tls_policy
{
1078 my ($filename, $fh, $tls_policy) = @_;
1080 return if !$tls_policy;
1082 foreach my $domain (sort keys %$tls_policy) {
1083 my $entry = $tls_policy->{$domain};
1084 PVE
::Tools
::safe_print
(
1085 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1089 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1090 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1093 undef, always_call_parser
=> 1);
1095 sub postmap_tls_policy
{
1096 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1099 my $transport_map_filename = "/etc/pmg/transport";
1101 sub postmap_pmg_transport
{
1102 PMG
::Utils
::run_postmap
($transport_map_filename);
1105 PVE
::JSONSchema
::register_format
(
1106 'transport-address', \
&pmg_verify_transport_address
);
1108 sub pmg_verify_transport_address
{
1109 my ($name, $noerr) = @_;
1111 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1113 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1116 return undef if $noerr;
1117 die "value does not look like a valid address\n";
1121 sub read_transport_map
{
1122 my ($filename, $fh) = @_;
1124 return [] if !defined($fh);
1130 while (defined(my $line = <$fh>)) {
1132 next if $line =~ m/^\s*$/;
1133 if ($line =~ m/^#(.*)\s*$/) {
1138 my $parse_error = sub {
1140 warn "parse error in '$filename': $line - $err";
1144 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1145 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1147 eval { pmg_verify_transport_domain_or_email
($domain); };
1149 $parse_error->($err);
1153 if ($host =~ m/^\[(.*)\]$/) {
1157 $use_mx = 0 if ($protocol eq "lmtp");
1159 eval { pmg_verify_transport_address
($host); };
1161 $parse_error->($err);
1167 protocol
=> $protocol,
1171 comment
=> $comment,
1173 $res->{$domain} = $data;
1176 $parse_error->('wrong format');
1183 sub write_transport_map
{
1184 my ($filename, $fh, $tmap) = @_;
1188 foreach my $domain (sort keys %$tmap) {
1189 my $data = $tmap->{$domain};
1191 my $comment = $data->{comment
};
1192 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1193 if defined($comment) && $comment !~ m/^\s*$/;
1195 my $bracket_host = !$data->{use_mx
};
1197 if ($data->{protocol
} eq 'lmtp') {
1199 $data->{protocol
} .= ":inet";
1201 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1202 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1204 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1208 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1209 \
&read_transport_map
,
1210 \
&write_transport_map
,
1211 undef, always_call_parser
=> 1);
1213 # config file generation using templates
1215 sub get_host_dns_info
{
1219 my $nodename = PVE
::INotify
::nodename
();
1221 $dnsinfo->{hostname
} = $nodename;
1222 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1224 my $domain = $resolv->{search
} // 'localdomain';
1225 $dnsinfo->{domain
} = $domain;
1227 $dnsinfo->{fqdn
} = "$nodename.$domain";
1232 sub get_template_vars
{
1235 my $vars = { pmg
=> $self->get_config() };
1237 my $dnsinfo = get_host_dns_info
();
1238 $vars->{dns
} = $dnsinfo;
1239 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1240 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1242 my $transportnets = [];
1244 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1245 foreach my $domain (sort keys %$tmap) {
1246 my $data = $tmap->{$domain};
1247 my $host = $data->{host
};
1248 if ($host =~ m/^$IPV4RE$/) {
1249 push @$transportnets, "$host/32";
1250 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1251 push @$transportnets, "[$1]/128";
1256 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1258 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1260 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1261 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1262 push @$mynetworks, "[$1]/$2";
1264 push @$mynetworks, $int_net_cidr;
1267 if ($int_ip =~ m/^$IPV6RE$/) {
1268 push @$mynetworks, "[$int_ip]/128";
1270 push @$mynetworks, "$int_ip/32";
1274 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1275 foreach my $cidr (sort keys %$netlist) {
1276 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1277 push @$mynetworks, "[$1]/$2";
1279 push @$mynetworks, $cidr;
1283 push @$mynetworks, @$transportnets;
1285 # add default relay to mynetworks
1286 if (my $relay = $self->get('mail', 'relay')) {
1287 if ($relay =~ m/^$IPV4RE$/) {
1288 push @$mynetworks, "$relay/32";
1289 } elsif ($relay =~ m/^$IPV6RE$/) {
1290 push @$mynetworks, "[$relay]/128";
1292 # DNS name - do nothing ?
1296 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1298 # normalize dnsbl_sites
1299 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1300 if (scalar(@dnsbl_sites)) {
1301 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1304 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1307 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1308 $self->get('mail', 'spf');
1309 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1311 if ($int_ip =~ m/^$IPV6RE$/) {
1312 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1314 $vars->{postfix
}->{int_ip
} = $int_ip;
1317 my $wlbr = $dnsinfo->{fqdn
};
1318 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1321 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1323 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1325 my $uri = URI-
>new($proxy);
1326 my $host = $uri->host;
1327 my $port = $uri->port // 8080;
1329 my $data = { host
=> $host, port
=> $port };
1330 if (my $ui = $uri->userinfo) {
1331 my ($username, $pw) = split(/:/, $ui, 2);
1332 $data->{username
} = $username;
1333 $data->{password
} = $pw if defined($pw);
1335 $vars->{proxy
} = $data;
1338 warn "parse http_proxy failed - $@" if $@;
1340 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1345 # use one global TT cache
1346 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1348 my $template_toolkit;
1350 sub get_template_toolkit
{
1352 return $template_toolkit if $template_toolkit;
1354 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1356 return $template_toolkit;
1359 # rewrite file from template
1360 # return true if file has changed
1361 sub rewrite_config_file
{
1362 my ($self, $tmplname, $dstfn) = @_;
1364 my $demo = $self->get('admin', 'demo');
1367 my $demosrc = "$tmplname.demo";
1368 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1371 my ($perm, $uid, $gid);
1373 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1374 # needed if file contains a HTTPProxyPasswort
1376 $uid = getpwnam('clamav');
1377 $gid = getgrnam('adm');
1381 my $tt = get_template_toolkit
();
1383 my $vars = $self->get_template_vars();
1387 $tt->process($tmplname, $vars, \
$output) ||
1388 die $tt->error() . "\n";
1390 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1392 return 0 if defined($old) && ($old eq $output); # no change
1394 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1396 if (defined($uid) && defined($gid)) {
1397 chown($uid, $gid, $dstfn);
1403 # rewrite spam configuration
1404 sub rewrite_config_spam
{
1407 my $use_awl = $self->get('spam', 'use_awl');
1408 my $use_bayes = $self->get('spam', 'use_bayes');
1409 my $use_razor = $self->get('spam', 'use_razor');
1413 # delete AW and bayes databases if those features are disabled
1415 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1419 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1420 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1421 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1424 # make sure we have the custom SA files (else cluster sync fails)
1425 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1426 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1428 $changes = 1 if $self->rewrite_config_file(
1429 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1431 $changes = 1 if $self->rewrite_config_file(
1432 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1434 $changes = 1 if $self->rewrite_config_file(
1435 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1437 $changes = 1 if $self->rewrite_config_file(
1438 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1441 mkdir "/root/.razor";
1443 $changes = 1 if $self->rewrite_config_file(
1444 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1446 if (! -e
'/root/.razor/identity') {
1449 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1450 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1453 syslog
('info', "registering razor failed: $err") if $err;
1460 # rewrite ClamAV configuration
1461 sub rewrite_config_clam
{
1464 return $self->rewrite_config_file(
1465 'clamd.conf.in', '/etc/clamav/clamd.conf');
1468 sub rewrite_config_freshclam
{
1471 return $self->rewrite_config_file(
1472 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1475 sub rewrite_config_postgres
{
1478 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1479 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1483 $changes = 1 if $self->rewrite_config_file(
1484 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1486 $changes = 1 if $self->rewrite_config_file(
1487 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1492 # rewrite /root/.forward
1493 sub rewrite_dot_forward
{
1496 my $dstfn = '/root/.forward';
1498 my $email = $self->get('admin', 'email');
1501 if ($email && $email =~ m/\s*(\S+)\s*/) {
1504 # empty .forward does not forward mails (see man local)
1507 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1509 return 0 if defined($old) && ($old eq $output); # no change
1511 PVE
::Tools
::file_set_contents
($dstfn, $output);
1516 my $write_smtp_whitelist = sub {
1517 my ($filename, $data, $action) = @_;
1519 $action = 'OK' if !$action;
1521 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1525 foreach my $k (sort keys %$data) {
1526 $new .= "$k $action\n";
1529 return 0 if defined($old) && ($old eq $new); # no change
1531 PVE
::Tools
::file_set_contents
($filename, $new);
1533 PMG
::Utils
::run_postmap
($filename);
1538 sub rewrite_postfix_whitelist
{
1539 my ($rulecache) = @_;
1541 # see man page for regexp_table for postfix regex table format
1543 # we use a hash to avoid duplicate entries in regex tables
1546 my $clientlist = {};
1548 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1549 my $oclass = ref($obj);
1550 if ($oclass eq 'PMG::RuleDB::Receiver') {
1551 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1552 $tolist->{"/^$addr\$/"} = 1;
1553 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1554 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1555 $tolist->{"/^.+\@$addr\$/"} = 1;
1556 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1557 my $addr = $obj->{address
};
1559 $tolist->{"/^$addr\$/"} = 1;
1563 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1564 my $oclass = ref($obj);
1565 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1566 if ($oclass eq 'PMG::RuleDB::EMail') {
1567 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1568 $fromlist->{"/^$addr\$/"} = 1;
1569 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1570 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1571 $fromlist->{"/^.+\@$addr\$/"} = 1;
1572 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1573 my $addr = $obj->{address
};
1575 $fromlist->{"/^$addr\$/"} = 1;
1576 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1577 $clientlist->{$obj->{address
}} = 1;
1578 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1579 $clientlist->{$obj->{address
}} = 1;
1583 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1584 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1585 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1586 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1589 # rewrite /etc/postfix/*
1590 sub rewrite_config_postfix
{
1591 my ($self, $rulecache) = @_;
1593 # make sure we have required files (else postfix start fails)
1594 IO
::File-
>new($transport_map_filename, 'a', 0644);
1598 if ($self->get('mail', 'tls')) {
1600 PMG
::Utils
::gen_proxmox_tls_cert
();
1602 syslog
('info', "generating certificate failed: $@") if $@;
1605 $changes = 1 if $self->rewrite_config_file(
1606 'main.cf.in', '/etc/postfix/main.cf');
1608 $changes = 1 if $self->rewrite_config_file(
1609 'master.cf.in', '/etc/postfix/master.cf');
1611 # make sure we have required files (else postfix start fails)
1612 # Note: postmap need a valid /etc/postfix/main.cf configuration
1613 postmap_pmg_domains
();
1614 postmap_pmg_transport
();
1615 postmap_tls_policy
();
1617 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1619 # make sure aliases.db is up to date
1620 system('/usr/bin/newaliases');
1625 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1626 my $pmg_service_params = {
1634 dkim_sign_all_mail
=> 1,
1638 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1639 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1641 sub dump_smtp_filter_config
{
1646 foreach my $sec (sort keys %$pmg_service_params) {
1647 my $conf_sec = $self->{ids
}->{$sec} // {};
1648 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1649 $val = $conf_sec->{$key};
1650 $conf .= "$sec.$key:$val\n" if defined($val);
1657 sub compare_smtp_filter_config
{
1663 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1667 syslog
('warning', "reloading pmg-smtp-filter: $err");
1670 my $new = $self->dump_smtp_filter_config();
1671 $ret = 1 if $old ne $new;
1674 $self->write_smtp_filter_config() if $ret;
1679 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1681 sub write_smtp_filter_config
{
1684 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1685 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1686 $self->dump_smtp_filter_config());
1692 sub rewrite_config
{
1693 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1695 $force_restart = {} if ! $force_restart;
1697 my $log_restart = sub {
1698 syslog
('info', "configuration change detected for '$_[0]', restarting");
1701 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1702 $force_restart->{postfix
}) {
1703 $log_restart->('postfix');
1704 PMG
::Utils
::service_cmd
('postfix', 'reload');
1707 if ($self->rewrite_dot_forward() && $restart_services) {
1708 # no need to restart anything
1711 if ($self->rewrite_config_postgres() && $restart_services) {
1712 # do nothing (too many side effects)?
1713 # does not happen anyways, because config does not change.
1716 if (($self->rewrite_config_spam() && $restart_services) ||
1717 $force_restart->{spam
}) {
1718 $log_restart->('pmg-smtp-filter');
1719 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1722 if (($self->rewrite_config_clam() && $restart_services) ||
1723 $force_restart->{clam
}) {
1724 $log_restart->('clamav-daemon');
1725 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1728 if (($self->rewrite_config_freshclam() && $restart_services) ||
1729 $force_restart->{freshclam
}) {
1730 $log_restart->('clamav-freshclam');
1731 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1734 if (($self->compare_smtp_filter_config() && $restart_services) ||
1735 $force_restart->{spam
}) {
1736 syslog
('info', "scheduled reload for pmg-smtp-filter");
1737 PMG
::Utils
::reload_smtp_filter
();