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 sub read_transport_map
{
1106 my ($filename, $fh) = @_;
1108 return [] if !defined($fh);
1114 while (defined(my $line = <$fh>)) {
1116 next if $line =~ m/^\s*$/;
1117 if ($line =~ m/^#(.*)\s*$/) {
1122 my $parse_error = sub {
1124 warn "parse error in '$filename': $line - $err";
1128 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1129 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1131 eval { pmg_verify_transport_domain_or_email
($domain); };
1133 $parse_error->($err);
1137 if ($host =~ m/^\[(.*)\]$/) {
1141 $use_mx = 0 if ($protocol eq "lmtp");
1143 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1145 $parse_error->($err);
1151 protocol
=> $protocol,
1155 comment
=> $comment,
1157 $res->{$domain} = $data;
1160 $parse_error->('wrong format');
1167 sub write_transport_map
{
1168 my ($filename, $fh, $tmap) = @_;
1172 foreach my $domain (sort keys %$tmap) {
1173 my $data = $tmap->{$domain};
1175 my $comment = $data->{comment
};
1176 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1177 if defined($comment) && $comment !~ m/^\s*$/;
1179 my $use_mx = $data->{use_mx
};
1180 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1182 if ($data->{protocol
} eq 'lmtp') {
1184 $data->{protocol
} .= ":inet";
1188 PVE
::Tools
::safe_print
(
1189 $filename, $fh, "$data->{domain} $data->{protocol}:$data->{host}:$data->{port}\n");
1191 PVE
::Tools
::safe_print
(
1192 $filename, $fh, "$data->{domain} $data->{protocol}:[$data->{host}]:$data->{port}\n");
1197 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1198 \
&read_transport_map
,
1199 \
&write_transport_map
,
1200 undef, always_call_parser
=> 1);
1202 # config file generation using templates
1204 sub get_host_dns_info
{
1208 my $nodename = PVE
::INotify
::nodename
();
1210 $dnsinfo->{hostname
} = $nodename;
1211 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1213 my $domain = $resolv->{search
} // 'localdomain';
1214 $dnsinfo->{domain
} = $domain;
1216 $dnsinfo->{fqdn
} = "$nodename.$domain";
1221 sub get_template_vars
{
1224 my $vars = { pmg
=> $self->get_config() };
1226 my $dnsinfo = get_host_dns_info
();
1227 $vars->{dns
} = $dnsinfo;
1228 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1229 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1231 my $transportnets = [];
1233 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1234 foreach my $domain (sort keys %$tmap) {
1235 my $data = $tmap->{$domain};
1236 my $host = $data->{host
};
1237 if ($host =~ m/^$IPV4RE$/) {
1238 push @$transportnets, "$host/32";
1239 } elsif ($host =~ m/^$IPV6RE$/) {
1240 push @$transportnets, "[$host]/128";
1245 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1247 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1249 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1250 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1251 push @$mynetworks, "[$1]/$2";
1253 push @$mynetworks, $int_net_cidr;
1256 if ($int_ip =~ m/^$IPV6RE$/) {
1257 push @$mynetworks, "[$int_ip]/128";
1259 push @$mynetworks, "$int_ip/32";
1263 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1264 foreach my $cidr (sort keys %$netlist) {
1265 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1266 push @$mynetworks, "[$1]/$2";
1268 push @$mynetworks, $cidr;
1272 push @$mynetworks, @$transportnets;
1274 # add default relay to mynetworks
1275 if (my $relay = $self->get('mail', 'relay')) {
1276 if ($relay =~ m/^$IPV4RE$/) {
1277 push @$mynetworks, "$relay/32";
1278 } elsif ($relay =~ m/^$IPV6RE$/) {
1279 push @$mynetworks, "[$relay]/128";
1281 # DNS name - do nothing ?
1285 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1287 # normalize dnsbl_sites
1288 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1289 if (scalar(@dnsbl_sites)) {
1290 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1293 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1296 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1297 $self->get('mail', 'spf');
1298 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1300 if ($int_ip =~ m/^$IPV6RE$/) {
1301 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1303 $vars->{postfix
}->{int_ip
} = $int_ip;
1306 my $wlbr = $dnsinfo->{fqdn
};
1307 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1310 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1312 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1314 my $uri = URI-
>new($proxy);
1315 my $host = $uri->host;
1316 my $port = $uri->port // 8080;
1318 my $data = { host
=> $host, port
=> $port };
1319 if (my $ui = $uri->userinfo) {
1320 my ($username, $pw) = split(/:/, $ui, 2);
1321 $data->{username
} = $username;
1322 $data->{password
} = $pw if defined($pw);
1324 $vars->{proxy
} = $data;
1327 warn "parse http_proxy failed - $@" if $@;
1329 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1334 # use one global TT cache
1335 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1337 my $template_toolkit;
1339 sub get_template_toolkit
{
1341 return $template_toolkit if $template_toolkit;
1343 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1345 return $template_toolkit;
1348 # rewrite file from template
1349 # return true if file has changed
1350 sub rewrite_config_file
{
1351 my ($self, $tmplname, $dstfn) = @_;
1353 my $demo = $self->get('admin', 'demo');
1356 my $demosrc = "$tmplname.demo";
1357 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1360 my ($perm, $uid, $gid);
1362 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1363 # needed if file contains a HTTPProxyPasswort
1365 $uid = getpwnam('clamav');
1366 $gid = getgrnam('adm');
1370 my $tt = get_template_toolkit
();
1372 my $vars = $self->get_template_vars();
1376 $tt->process($tmplname, $vars, \
$output) ||
1377 die $tt->error() . "\n";
1379 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1381 return 0 if defined($old) && ($old eq $output); # no change
1383 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1385 if (defined($uid) && defined($gid)) {
1386 chown($uid, $gid, $dstfn);
1392 # rewrite spam configuration
1393 sub rewrite_config_spam
{
1396 my $use_awl = $self->get('spam', 'use_awl');
1397 my $use_bayes = $self->get('spam', 'use_bayes');
1398 my $use_razor = $self->get('spam', 'use_razor');
1402 # delete AW and bayes databases if those features are disabled
1404 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1408 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1409 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1410 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1413 # make sure we have the custom SA files (else cluster sync fails)
1414 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1415 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1417 $changes = 1 if $self->rewrite_config_file(
1418 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1420 $changes = 1 if $self->rewrite_config_file(
1421 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1423 $changes = 1 if $self->rewrite_config_file(
1424 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1426 $changes = 1 if $self->rewrite_config_file(
1427 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1430 mkdir "/root/.razor";
1432 $changes = 1 if $self->rewrite_config_file(
1433 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1435 if (! -e
'/root/.razor/identity') {
1438 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1439 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1442 syslog
('info', "registering razor failed: $err") if $err;
1449 # rewrite ClamAV configuration
1450 sub rewrite_config_clam
{
1453 return $self->rewrite_config_file(
1454 'clamd.conf.in', '/etc/clamav/clamd.conf');
1457 sub rewrite_config_freshclam
{
1460 return $self->rewrite_config_file(
1461 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1464 sub rewrite_config_postgres
{
1467 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1468 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1472 $changes = 1 if $self->rewrite_config_file(
1473 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1475 $changes = 1 if $self->rewrite_config_file(
1476 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1481 # rewrite /root/.forward
1482 sub rewrite_dot_forward
{
1485 my $dstfn = '/root/.forward';
1487 my $email = $self->get('admin', 'email');
1490 if ($email && $email =~ m/\s*(\S+)\s*/) {
1493 # empty .forward does not forward mails (see man local)
1496 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1498 return 0 if defined($old) && ($old eq $output); # no change
1500 PVE
::Tools
::file_set_contents
($dstfn, $output);
1505 my $write_smtp_whitelist = sub {
1506 my ($filename, $data, $action) = @_;
1508 $action = 'OK' if !$action;
1510 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1514 foreach my $k (sort keys %$data) {
1515 $new .= "$k $action\n";
1518 return 0 if defined($old) && ($old eq $new); # no change
1520 PVE
::Tools
::file_set_contents
($filename, $new);
1522 PMG
::Utils
::run_postmap
($filename);
1527 sub rewrite_postfix_whitelist
{
1528 my ($rulecache) = @_;
1530 # see man page for regexp_table for postfix regex table format
1532 # we use a hash to avoid duplicate entries in regex tables
1535 my $clientlist = {};
1537 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1538 my $oclass = ref($obj);
1539 if ($oclass eq 'PMG::RuleDB::Receiver') {
1540 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1541 $tolist->{"/^$addr\$/"} = 1;
1542 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1543 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1544 $tolist->{"/^.+\@$addr\$/"} = 1;
1545 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1546 my $addr = $obj->{address
};
1548 $tolist->{"/^$addr\$/"} = 1;
1552 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1553 my $oclass = ref($obj);
1554 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1555 if ($oclass eq 'PMG::RuleDB::EMail') {
1556 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1557 $fromlist->{"/^$addr\$/"} = 1;
1558 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1559 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1560 $fromlist->{"/^.+\@$addr\$/"} = 1;
1561 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1562 my $addr = $obj->{address
};
1564 $fromlist->{"/^$addr\$/"} = 1;
1565 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1566 $clientlist->{$obj->{address
}} = 1;
1567 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1568 $clientlist->{$obj->{address
}} = 1;
1572 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1573 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1574 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1575 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1578 # rewrite /etc/postfix/*
1579 sub rewrite_config_postfix
{
1580 my ($self, $rulecache) = @_;
1582 # make sure we have required files (else postfix start fails)
1583 IO
::File-
>new($transport_map_filename, 'a', 0644);
1587 if ($self->get('mail', 'tls')) {
1589 PMG
::Utils
::gen_proxmox_tls_cert
();
1591 syslog
('info', "generating certificate failed: $@") if $@;
1594 $changes = 1 if $self->rewrite_config_file(
1595 'main.cf.in', '/etc/postfix/main.cf');
1597 $changes = 1 if $self->rewrite_config_file(
1598 'master.cf.in', '/etc/postfix/master.cf');
1600 # make sure we have required files (else postfix start fails)
1601 # Note: postmap need a valid /etc/postfix/main.cf configuration
1602 postmap_pmg_domains
();
1603 postmap_pmg_transport
();
1604 postmap_tls_policy
();
1606 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1608 # make sure aliases.db is up to date
1609 system('/usr/bin/newaliases');
1614 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1615 my $pmg_service_params = {
1623 dkim_sign_all_mail
=> 1,
1627 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1628 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1630 sub dump_smtp_filter_config
{
1635 foreach my $sec (sort keys %$pmg_service_params) {
1636 my $conf_sec = $self->{ids
}->{$sec} // {};
1637 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1638 $val = $conf_sec->{$key};
1639 $conf .= "$sec.$key:$val\n" if defined($val);
1646 sub compare_smtp_filter_config
{
1652 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1656 syslog
('warning', "reloading pmg-smtp-filter: $err");
1659 my $new = $self->dump_smtp_filter_config();
1660 $ret = 1 if $old ne $new;
1663 $self->write_smtp_filter_config() if $ret;
1668 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1670 sub write_smtp_filter_config
{
1673 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1674 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1675 $self->dump_smtp_filter_config());
1681 sub rewrite_config
{
1682 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1684 $force_restart = {} if ! $force_restart;
1686 my $log_restart = sub {
1687 syslog
('info', "configuration change detected for '$_[0]', restarting");
1690 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1691 $force_restart->{postfix
}) {
1692 $log_restart->('postfix');
1693 PMG
::Utils
::service_cmd
('postfix', 'reload');
1696 if ($self->rewrite_dot_forward() && $restart_services) {
1697 # no need to restart anything
1700 if ($self->rewrite_config_postgres() && $restart_services) {
1701 # do nothing (too many side effects)?
1702 # does not happen anyways, because config does not change.
1705 if (($self->rewrite_config_spam() && $restart_services) ||
1706 $force_restart->{spam
}) {
1707 $log_restart->('pmg-smtp-filter');
1708 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1711 if (($self->rewrite_config_clam() && $restart_services) ||
1712 $force_restart->{clam
}) {
1713 $log_restart->('clamav-daemon');
1714 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1717 if (($self->rewrite_config_freshclam() && $restart_services) ||
1718 $force_restart->{freshclam
}) {
1719 $log_restart->('clamav-freshclam');
1720 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1723 if (($self->compare_smtp_filter_config() && $restart_services) ||
1724 $force_restart->{spam
}) {
1725 syslog
('info', "scheduled reload for pmg-smtp-filter");
1726 PMG
::Utils
::reload_smtp_filter
();