2020-01-21 21:48:34 +00:00
|
|
|
diff --git a/MANIFEST b/MANIFEST
|
2020-11-09 18:09:46 +00:00
|
|
|
index a988fa1..c4aca1b 100644
|
2020-01-21 21:48:34 +00:00
|
|
|
--- a/MANIFEST
|
|
|
|
+++ b/MANIFEST
|
2020-11-09 18:09:46 +00:00
|
|
|
@@ -118,7 +118,6 @@ lib/Mail/SpamAssassin/Plugin/VBounce.pm
|
2020-01-21 21:48:34 +00:00
|
|
|
lib/Mail/SpamAssassin/Plugin/WLBLEval.pm
|
|
|
|
lib/Mail/SpamAssassin/Plugin/WhiteListSubject.pm
|
|
|
|
lib/Mail/SpamAssassin/PluginHandler.pm
|
|
|
|
-lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
|
|
|
|
lib/Mail/SpamAssassin/RegistryBoundaries.pm
|
|
|
|
lib/Mail/SpamAssassin/Reporter.pm
|
|
|
|
lib/Mail/SpamAssassin/SQLBasedAddrList.pm
|
|
|
|
diff --git a/lib/Mail/SpamAssassin/Plugin/RelayCountry.pm b/lib/Mail/SpamAssassin/Plugin/RelayCountry.pm
|
|
|
|
deleted file mode 100644
|
2020-11-09 18:09:46 +00:00
|
|
|
index 38ec1e3..0000000
|
2020-01-21 21:48:34 +00:00
|
|
|
--- a/lib/Mail/SpamAssassin/Plugin/RelayCountry.pm
|
|
|
|
+++ /dev/null
|
2020-11-09 18:09:46 +00:00
|
|
|
@@ -1,407 +0,0 @@
|
2020-01-21 21:48:34 +00:00
|
|
|
-# <@LICENSE>
|
|
|
|
-# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
|
|
-# contributor license agreements. See the NOTICE file distributed with
|
|
|
|
-# this work for additional information regarding copyright ownership.
|
|
|
|
-# The ASF licenses this file to you under the Apache License, Version 2.0
|
|
|
|
-# (the "License"); you may not use this file except in compliance with
|
|
|
|
-# the License. You may obtain a copy of the License at:
|
|
|
|
-#
|
|
|
|
-# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
-#
|
|
|
|
-# Unless required by applicable law or agreed to in writing, software
|
|
|
|
-# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
-# See the License for the specific language governing permissions and
|
|
|
|
-# limitations under the License.
|
|
|
|
-# </@LICENSE>
|
|
|
|
-
|
|
|
|
-=head1 NAME
|
|
|
|
-
|
|
|
|
-RelayCountry - add message metadata indicating the country code of each relay
|
|
|
|
-
|
|
|
|
-=head1 SYNOPSIS
|
|
|
|
-
|
|
|
|
- loadplugin Mail::SpamAssassin::Plugin::RelayCountry
|
|
|
|
-
|
|
|
|
-=head1 DESCRIPTION
|
|
|
|
-
|
|
|
|
-The RelayCountry plugin attempts to determine the domain country codes
|
|
|
|
-of each relay used in the delivery path of messages and add that information
|
2020-11-09 18:09:46 +00:00
|
|
|
-to the message metadata.
|
|
|
|
-
|
|
|
|
-Following metadata headers and tags are added:
|
|
|
|
-
|
|
|
|
- X-Relay-Countries _RELAYCOUNTRY_
|
|
|
|
- All untrusted relays. Contains all relays starting from the
|
|
|
|
- trusted_networks border. This method has been used by default since
|
|
|
|
- early SA versions.
|
|
|
|
-
|
|
|
|
- X-Relay-Countries-External _RELAYCOUNTRYEXT_
|
|
|
|
- All external relays. Contains all relays starting from the
|
|
|
|
- internal_networks border. Could be useful in some cases when
|
|
|
|
- trusted/msa_networks extend beyond the internal border and those
|
|
|
|
- need to be checked too.
|
|
|
|
-
|
|
|
|
- X-Relay-Countries-All _RELAYCOUNTRYALL_
|
|
|
|
- All possible relays (internal + external).
|
|
|
|
-
|
|
|
|
- X-Relay-Countries-Auth _RELAYCOUNTRYAUTH_
|
|
|
|
- Auth will contain all relays starting from the first relay that used
|
|
|
|
- authentication. For example, this could be used to check for hacked
|
|
|
|
- local users coming in from unexpected countries. If there are no
|
|
|
|
- authenticated relays, this will be empty.
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
-=head1 REQUIREMENT
|
|
|
|
-
|
|
|
|
-This plugin requires the GeoIP2, Geo::IP, IP::Country::DB_File or
|
|
|
|
-IP::Country::Fast module from CPAN.
|
|
|
|
-For backward compatibility IP::Country::Fast is used as fallback if no db_type
|
|
|
|
-is specified in the config file.
|
|
|
|
-
|
|
|
|
-=cut
|
|
|
|
-
|
|
|
|
-package Mail::SpamAssassin::Plugin::RelayCountry;
|
|
|
|
-
|
|
|
|
-use Mail::SpamAssassin::Plugin;
|
|
|
|
-use Mail::SpamAssassin::Logger;
|
|
|
|
-use Mail::SpamAssassin::Constants qw(:ip);
|
|
|
|
-use strict;
|
|
|
|
-use warnings;
|
|
|
|
-# use bytes;
|
|
|
|
-use re 'taint';
|
|
|
|
-
|
|
|
|
-our @ISA = qw(Mail::SpamAssassin::Plugin);
|
|
|
|
-
|
|
|
|
-# constructor: register the eval rule
|
|
|
|
-sub new {
|
|
|
|
- my $class = shift;
|
|
|
|
- my $mailsaobject = shift;
|
|
|
|
-
|
|
|
|
- # some boilerplate...
|
|
|
|
- $class = ref($class) || $class;
|
|
|
|
- my $self = $class->SUPER::new($mailsaobject);
|
|
|
|
- bless ($self, $class);
|
|
|
|
-
|
|
|
|
- $self->set_config($mailsaobject->{conf});
|
|
|
|
- return $self;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-sub set_config {
|
|
|
|
- my ($self, $conf) = @_;
|
|
|
|
- my @cmds;
|
|
|
|
-
|
|
|
|
-=head1 USER PREFERENCES
|
|
|
|
-
|
|
|
|
-The following options can be used in both site-wide (C<local.cf>) and
|
|
|
|
-user-specific (C<user_prefs>) configuration files to customize how
|
|
|
|
-SpamAssassin handles incoming email messages.
|
|
|
|
-
|
|
|
|
-=over 4
|
|
|
|
-
|
|
|
|
-=item country_db_type STRING
|
|
|
|
-
|
|
|
|
-This option tells SpamAssassin which type of Geo database to use.
|
|
|
|
-Valid database types are GeoIP, GeoIP2, DB_File and Fast.
|
|
|
|
-
|
|
|
|
-=back
|
|
|
|
-
|
|
|
|
-=cut
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'country_db_type',
|
|
|
|
- default => "GeoIP",
|
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
2020-11-09 18:09:46 +00:00
|
|
|
- if ($value !~ /^(?:GeoIP|GeoIP2|DB_File|Fast)$/) {
|
2020-01-21 21:48:34 +00:00
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- $self->{country_db_type} = $value;
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
-=over 4
|
|
|
|
-
|
|
|
|
-=item country_db_path STRING
|
|
|
|
-
|
|
|
|
-This option tells SpamAssassin where to find MaxMind GeoIP2 or IP::Country::DB_File database.
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
-If not defined, GeoIP2 default search includes:
|
|
|
|
- /usr/local/share/GeoIP/GeoIP2-Country.mmdb
|
|
|
|
- /usr/share/GeoIP/GeoIP2-Country.mmdb
|
|
|
|
- /var/lib/GeoIP/GeoIP2-Country.mmdb
|
|
|
|
- /usr/local/share/GeoIP/GeoLite2-Country.mmdb
|
|
|
|
- /usr/share/GeoIP/GeoLite2-Country.mmdb
|
|
|
|
- /var/lib/GeoIP/GeoLite2-Country.mmdb
|
|
|
|
- (and same paths again for -City.mmdb, which also has country functionality)
|
|
|
|
-
|
2020-01-21 21:48:34 +00:00
|
|
|
-=back
|
|
|
|
-
|
|
|
|
-=cut
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'country_db_path',
|
|
|
|
- default => "",
|
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
- if (!defined $value || !length $value) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- }
|
2020-11-09 18:09:46 +00:00
|
|
|
- if (!-e $value) {
|
2020-01-21 21:48:34 +00:00
|
|
|
- info("config: country_db_path \"$value\" is not accessible");
|
|
|
|
- $self->{country_db_path} = $value;
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- $self->{country_db_path} = $value;
|
|
|
|
- }
|
|
|
|
- });
|
2020-11-09 18:09:46 +00:00
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'geoip2_default_db_path',
|
|
|
|
- default => [
|
|
|
|
- '/usr/local/share/GeoIP/GeoIP2-Country.mmdb',
|
|
|
|
- '/usr/share/GeoIP/GeoIP2-Country.mmdb',
|
|
|
|
- '/var/lib/GeoIP/GeoIP2-Country.mmdb',
|
|
|
|
- '/usr/local/share/GeoIP/GeoLite2-Country.mmdb',
|
|
|
|
- '/usr/share/GeoIP/GeoLite2-Country.mmdb',
|
|
|
|
- '/var/lib/GeoIP/GeoLite2-Country.mmdb',
|
|
|
|
- '/usr/local/share/GeoIP/GeoIP2-City.mmdb',
|
|
|
|
- '/usr/share/GeoIP/GeoIP2-City.mmdb',
|
|
|
|
- '/var/lib/GeoIP/GeoIP2-City.mmdb',
|
|
|
|
- '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
|
|
|
|
- '/usr/share/GeoIP/GeoLite2-City.mmdb',
|
|
|
|
- '/var/lib/GeoIP/GeoLite2-City.mmdb',
|
|
|
|
- ],
|
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRINGLIST,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
- if ($value eq '') {
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- }
|
|
|
|
- push(@{$self->{geoip2_default_db_path}}, split(/\s+/, $value));
|
|
|
|
- }
|
|
|
|
- });
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
- $conf->{parser}->register_commands(\@cmds);
|
|
|
|
-}
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
-sub get_country {
|
|
|
|
- my ($self, $ip, $db, $dbv6, $country_db_type) = @_;
|
|
|
|
- my $cc;
|
|
|
|
- my $IP_PRIVATE = IP_PRIVATE;
|
|
|
|
- my $IPV4_ADDRESS = IPV4_ADDRESS;
|
|
|
|
-
|
|
|
|
- # Private IPs will always be returned as '**'
|
|
|
|
- if ($ip =~ /^$IP_PRIVATE$/o) {
|
|
|
|
- $cc = "**";
|
|
|
|
- }
|
|
|
|
- elsif ($country_db_type eq "GeoIP") {
|
|
|
|
- if ($ip =~ /^$IPV4_ADDRESS$/o) {
|
|
|
|
- $cc = $db->country_code_by_addr($ip);
|
|
|
|
- } elsif (defined $dbv6) {
|
|
|
|
- $cc = $dbv6->country_code_by_addr_v6($ip);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- elsif ($country_db_type eq "GeoIP2") {
|
|
|
|
- my ($country, $country_rec);
|
|
|
|
- eval {
|
|
|
|
- if (index($db->metadata()->description()->{en}, 'City') != -1) {
|
|
|
|
- $country = $db->city( ip => $ip );
|
|
|
|
- } else {
|
|
|
|
- $country = $db->country( ip => $ip );
|
|
|
|
- }
|
|
|
|
- $country_rec = $country->country();
|
|
|
|
- $cc = $country_rec->iso_code();
|
|
|
|
- 1;
|
|
|
|
- } or do {
|
|
|
|
- $@ =~ s/\s+Trace begun.*//s;
|
|
|
|
- dbg("metadata: RelayCountry: GeoIP2 failed: $@");
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- elsif ($country_db_type eq "DB_File") {
|
|
|
|
- if ($ip =~ /^$IPV4_ADDRESS$/o ) {
|
|
|
|
- $cc = $db->inet_atocc($ip);
|
|
|
|
- } else {
|
|
|
|
- $cc = $db->inet6_atocc($ip);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- elsif ($country_db_type eq "Fast") {
|
|
|
|
- $cc = $db->inet_atocc($ip);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $cc ||= 'XX';
|
|
|
|
-
|
|
|
|
- return $cc;
|
|
|
|
-}
|
|
|
|
-
|
2020-01-21 21:48:34 +00:00
|
|
|
-sub extract_metadata {
|
|
|
|
- my ($self, $opts) = @_;
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $pms = $opts->{permsgstatus};
|
|
|
|
-
|
|
|
|
- my $db;
|
|
|
|
- my $dbv6;
|
|
|
|
- my $db_info; # will hold database info
|
|
|
|
- my $db_type; # will hold database type
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $country_db_type = $opts->{conf}->{country_db_type};
|
|
|
|
- my $country_db_path = $opts->{conf}->{country_db_path};
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- if ($country_db_type eq "GeoIP") {
|
2020-01-21 21:48:34 +00:00
|
|
|
- eval {
|
|
|
|
- require Geo::IP;
|
|
|
|
- $db = Geo::IP->open_type(Geo::IP->GEOIP_COUNTRY_EDITION, Geo::IP->GEOIP_STANDARD);
|
|
|
|
- die "GeoIP.dat not found" unless $db;
|
|
|
|
- # IPv6 requires version Geo::IP 1.39+ with GeoIP C API 1.4.7+
|
|
|
|
- if (Geo::IP->VERSION >= 1.39 && Geo::IP->api eq 'CAPI') {
|
2020-11-09 18:09:46 +00:00
|
|
|
- $dbv6 = Geo::IP->open_type(Geo::IP->GEOIP_COUNTRY_EDITION_V6, Geo::IP->GEOIP_STANDARD);
|
|
|
|
- if (!$dbv6) {
|
|
|
|
- dbg("metadata: RelayCountry: GeoIP: IPv6 support not enabled, GeoIPv6.dat not found");
|
|
|
|
- }
|
2020-01-21 21:48:34 +00:00
|
|
|
- } else {
|
2020-11-09 18:09:46 +00:00
|
|
|
- dbg("metadata: RelayCountry: GeoIP: IPv6 support not enabled, versions Geo::IP 1.39, GeoIP C API 1.4.7 required");
|
|
|
|
- }
|
|
|
|
- $db_info = sub { return "Geo::IP IPv4: " . ($db->database_info || '?')." / IPv6: ".($dbv6 ? $dbv6->database_info || '?' : '?') };
|
|
|
|
- 1;
|
|
|
|
- } or do {
|
|
|
|
- # Fallback to IP::Country::Fast
|
|
|
|
- dbg("metadata: RelayCountry: GeoIP: GeoIP.dat not found, trying IP::Country::Fast as fallback");
|
|
|
|
- $country_db_type = "Fast";
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- elsif ($country_db_type eq "GeoIP2") {
|
|
|
|
- if (!$country_db_path) {
|
|
|
|
- # Try some default locations
|
|
|
|
- foreach (@{$opts->{conf}->{geoip2_default_db_path}}) {
|
|
|
|
- if (-f $_) {
|
|
|
|
- $country_db_path = $_;
|
|
|
|
- last;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- if (-f $country_db_path) {
|
|
|
|
- eval {
|
|
|
|
- require GeoIP2::Database::Reader;
|
|
|
|
- $db = GeoIP2::Database::Reader->new(
|
|
|
|
- file => $country_db_path,
|
|
|
|
- locales => [ 'en' ]
|
|
|
|
- );
|
|
|
|
- die "unknown error" unless $db;
|
|
|
|
- $db_info = sub {
|
|
|
|
- my $m = $db->metadata();
|
|
|
|
- return "GeoIP2 ".$m->description()->{en}." / ".localtime($m->build_epoch());
|
|
|
|
- };
|
|
|
|
- 1;
|
|
|
|
- } or do {
|
|
|
|
- # Fallback to IP::Country::Fast
|
|
|
|
- $@ =~ s/\s+Trace begun.*//s;
|
|
|
|
- dbg("metadata: RelayCountry: GeoIP2: ${country_db_path} load failed: $@, trying IP::Country::Fast as fallback");
|
|
|
|
- $country_db_type = "Fast";
|
|
|
|
- }
|
|
|
|
- } else {
|
|
|
|
- # Fallback to IP::Country::Fast
|
|
|
|
- my $err = $country_db_path ?
|
|
|
|
- "$country_db_path not found" : "database not found from default locations";
|
|
|
|
- dbg("metadata: RelayCountry: GeoIP2: $err, trying IP::Country::Fast as fallback");
|
|
|
|
- $country_db_type = "Fast";
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- elsif ($country_db_type eq "DB_File") {
|
|
|
|
- if (-f $country_db_path) {
|
|
|
|
- eval {
|
|
|
|
- require IP::Country::DB_File;
|
|
|
|
- $db = IP::Country::DB_File->new($country_db_path);
|
|
|
|
- die "unknown error" unless $db;
|
|
|
|
- $db_info = sub { return "IP::Country::DB_File ".localtime($db->db_time()); };
|
|
|
|
- 1;
|
|
|
|
- } or do {
|
|
|
|
- # Fallback to IP::Country::Fast
|
|
|
|
- dbg("metadata: RelayCountry: DB_File: ${country_db_path} load failed: $@, trying IP::Country::Fast as fallback");
|
|
|
|
- $country_db_type = "Fast";
|
2020-01-21 21:48:34 +00:00
|
|
|
- }
|
|
|
|
- } else {
|
|
|
|
- # Fallback to IP::Country::Fast
|
2020-11-09 18:09:46 +00:00
|
|
|
- dbg("metadata: RelayCountry: DB_File: ${country_db_path} not found, trying IP::Country::Fast as fallback");
|
|
|
|
- $country_db_type = "Fast";
|
2020-01-21 21:48:34 +00:00
|
|
|
- }
|
|
|
|
- }
|
2020-11-09 18:09:46 +00:00
|
|
|
-
|
|
|
|
- if ($country_db_type eq "Fast") {
|
2020-01-21 21:48:34 +00:00
|
|
|
- my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
|
|
|
|
- eval {
|
|
|
|
- require IP::Country::Fast;
|
|
|
|
- $db = IP::Country::Fast->new();
|
|
|
|
- $db_info = sub { return "IP::Country::Fast ".localtime($db->db_time()); };
|
|
|
|
- 1;
|
|
|
|
- } or do {
|
|
|
|
- my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
|
|
|
|
- dbg("metadata: RelayCountry: failed to load 'IP::Country::Fast', skipping: $eval_stat");
|
|
|
|
- return 1;
|
2020-11-09 18:09:46 +00:00
|
|
|
- }
|
|
|
|
- }
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- if (!$db) {
|
|
|
|
- return 1;
|
|
|
|
- }
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
- dbg("metadata: RelayCountry: Using database: ".$db_info->());
|
|
|
|
- my $msg = $opts->{msg};
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- my @cc_untrusted;
|
2020-01-21 21:48:34 +00:00
|
|
|
- foreach my $relay (@{$msg->{metadata}->{relays_untrusted}}) {
|
|
|
|
- my $ip = $relay->{ip};
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
|
|
|
|
- push @cc_untrusted, $cc;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- my @cc_external;
|
|
|
|
- foreach my $relay (@{$msg->{metadata}->{relays_external}}) {
|
|
|
|
- my $ip = $relay->{ip};
|
|
|
|
- my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
|
|
|
|
- push @cc_external, $cc;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- my @cc_auth;
|
|
|
|
- my $found_auth;
|
|
|
|
- foreach my $relay (@{$msg->{metadata}->{relays_trusted}}) {
|
|
|
|
- if ($relay->{auth}) {
|
|
|
|
- $found_auth = 1;
|
|
|
|
- }
|
|
|
|
- if ($found_auth) {
|
|
|
|
- my $ip = $relay->{ip};
|
|
|
|
- my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
|
|
|
|
- push @cc_auth, $cc;
|
2020-01-21 21:48:34 +00:00
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- my @cc_all;
|
|
|
|
- foreach my $relay (@{$msg->{metadata}->{relays_internal}}, @{$msg->{metadata}->{relays_external}}) {
|
|
|
|
- my $ip = $relay->{ip};
|
|
|
|
- my $cc = $self->get_country($ip, $db, $dbv6, $country_db_type);
|
|
|
|
- push @cc_all, $cc;
|
|
|
|
- }
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $ccstr = join(' ', @cc_untrusted);
|
|
|
|
- $msg->put_metadata("X-Relay-Countries", $ccstr);
|
|
|
|
- dbg("metadata: X-Relay-Countries: $ccstr");
|
|
|
|
- $pms->set_tag("RELAYCOUNTRY", @cc_untrusted == 1 ? $cc_untrusted[0] : \@cc_untrusted);
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- $ccstr = join(' ', @cc_external);
|
|
|
|
- $msg->put_metadata("X-Relay-Countries-External", $ccstr);
|
|
|
|
- dbg("metadata: X-Relay-Countries-External: $ccstr");
|
|
|
|
- $pms->set_tag("RELAYCOUNTRYEXT", @cc_external == 1 ? $cc_external[0] : \@cc_external);
|
|
|
|
-
|
|
|
|
- $ccstr = join(' ', @cc_auth);
|
|
|
|
- $msg->put_metadata("X-Relay-Countries-Auth", $ccstr);
|
|
|
|
- dbg("metadata: X-Relay-Countries-Auth: $ccstr");
|
|
|
|
- $pms->set_tag("RELAYCOUNTRYAUTH", @cc_auth == 1 ? $cc_auth[0] : \@cc_auth);
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- $ccstr = join(' ', @cc_all);
|
|
|
|
- $msg->put_metadata("X-Relay-Countries-All", $ccstr);
|
|
|
|
- dbg("metadata: X-Relay-Countries-All: $ccstr");
|
|
|
|
- $pms->set_tag("RELAYCOUNTRYALL", @cc_all == 1 ? $cc_all[0] : \@cc_all);
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
- return 1;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-1;
|
|
|
|
diff --git a/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm b/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
|
|
|
|
deleted file mode 100644
|
2020-11-09 18:09:46 +00:00
|
|
|
index 4fbbcb7..0000000
|
2020-01-21 21:48:34 +00:00
|
|
|
--- a/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
|
|
|
|
+++ /dev/null
|
2020-11-09 18:09:46 +00:00
|
|
|
@@ -1,705 +0,0 @@
|
2020-01-21 21:48:34 +00:00
|
|
|
-# <@LICENSE>
|
|
|
|
-# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
|
|
-# contributor license agreements. See the NOTICE file distributed with
|
|
|
|
-# this work for additional information regarding copyright ownership.
|
|
|
|
-# The ASF licenses this file to you under the Apache License, Version 2.0
|
|
|
|
-# (the "License"); you may not use this file except in compliance with
|
|
|
|
-# the License. You may obtain a copy of the License at:
|
|
|
|
-#
|
|
|
|
-# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
-#
|
|
|
|
-# Unless required by applicable law or agreed to in writing, software
|
|
|
|
-# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
-# See the License for the specific language governing permissions and
|
|
|
|
-# limitations under the License.
|
|
|
|
-# </@LICENSE>
|
|
|
|
-
|
|
|
|
-=head1 NAME
|
|
|
|
-
|
|
|
|
-URILocalBL - blacklist URIs using local information (ISP names, address lists, and country codes)
|
|
|
|
-
|
|
|
|
-=head1 SYNOPSIS
|
|
|
|
-
|
|
|
|
-This plugin creates some new rule test types, such as "uri_block_cc",
|
|
|
|
-"uri_block_cidr", and "uri_block_isp". These rules apply to the URIs
|
|
|
|
-found in the HTML portion of a message, i.e. <a href=...> markup.
|
|
|
|
-
|
|
|
|
- loadplugin Mail::SpamAssassin::Plugin::URILocalBL
|
|
|
|
-
|
|
|
|
-Why local blacklisting? There are a few excellent, effective, and
|
|
|
|
-well-maintained DNSBL's out there. But they have several drawbacks:
|
|
|
|
-
|
|
|
|
-=over 2
|
|
|
|
-
|
|
|
|
-=item * blacklists can cover tens of thousands of entries, and you can't select which ones you use;
|
|
|
|
-
|
|
|
|
-=item * verifying that it's correctly configured can be non-trivial;
|
|
|
|
-
|
|
|
|
-=item * new blacklisting entries may take a while to be detected and entered, so it's not instantaneous.
|
|
|
|
-
|
|
|
|
-=back
|
|
|
|
-
|
|
|
|
-Sometimes all you want is a quick, easy, and very surgical blacklisting of
|
|
|
|
-a particular site or a particular ISP. This plugin is defined for that
|
|
|
|
-exact usage case.
|
|
|
|
-
|
|
|
|
-=head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS
|
|
|
|
-
|
|
|
|
-The format for defining a rule is as follows:
|
|
|
|
-
|
|
|
|
- uri_block_cc SYMBOLIC_TEST_NAME cc1 cc2 cc3 cc4
|
|
|
|
-
|
|
|
|
-or:
|
|
|
|
-
|
|
|
|
- uri_block_cont SYMBOLIC_TEST_NAME co1 co2 co3 co4
|
|
|
|
-
|
|
|
|
-or:
|
|
|
|
-
|
|
|
|
- uri_block_cidr SYMBOLIC_TEST_NAME a.a.a.a b.b.b.b/cc d.d.d.d-e.e.e.e
|
|
|
|
-
|
|
|
|
-or:
|
|
|
|
-
|
|
|
|
- uri_block_isp SYMBOLIC_TEST_NAME "DataRancid" "McCarrier" "Phishers-r-Us"
|
|
|
|
-
|
|
|
|
-Example rule for matching a URI in China:
|
|
|
|
-
|
|
|
|
- uri_block_cc TEST1 cn
|
|
|
|
-
|
|
|
|
-This would block the URL http://www.baidu.com/index.htm. Similarly, to
|
|
|
|
-match a Spam-haven netblock:
|
|
|
|
-
|
|
|
|
- uri_block_cidr TEST2 65.181.64.0/18
|
|
|
|
-
|
|
|
|
-would match a netblock where several phishing sites were recently hosted.
|
|
|
|
-
|
|
|
|
-And to block all CIDR blocks registered to an ISP, one might use:
|
|
|
|
-
|
|
|
|
- uri_block_isp TEST3 "ColoCrossing"
|
|
|
|
-
|
|
|
|
-if one didn't trust URL's pointing to that organization's clients. Lastly,
|
|
|
|
-if there's a country that you want to block but there's an explicit host
|
|
|
|
-you wish to exempt from that blacklist, you can use:
|
|
|
|
-
|
|
|
|
- uri_block_exclude TEST1 www.baidu.com
|
|
|
|
-
|
|
|
|
-if you wish to exempt URL's referring to this host. The same syntax is
|
|
|
|
-applicable to CIDR and ISP blocks as well.
|
|
|
|
-
|
|
|
|
-=head1 DEPENDENCIES
|
|
|
|
-
|
|
|
|
-The Country-Code based filtering requires the Geo::IP or GeoIP2 module,
|
|
|
|
-which uses either the fremium GeoLiteCountry database, or the commercial
|
|
|
|
-version of it called GeoIP from MaxMind.com.
|
|
|
|
-
|
|
|
|
-The ISP based filtering requires the same module, plus the GeoIPISP database.
|
|
|
|
-There is no fremium version of this database, so commercial licensing is
|
|
|
|
-required.
|
|
|
|
-
|
|
|
|
-=cut
|
|
|
|
-
|
|
|
|
-package Mail::SpamAssassin::Plugin::URILocalBL;
|
|
|
|
-use Mail::SpamAssassin::Plugin;
|
|
|
|
-use Mail::SpamAssassin::Logger;
|
2020-11-09 18:09:46 +00:00
|
|
|
-use Mail::SpamAssassin::Constants qw(:ip);
|
2020-01-21 21:48:34 +00:00
|
|
|
-use Mail::SpamAssassin::Util qw(untaint_var);
|
|
|
|
-
|
|
|
|
-use Socket;
|
|
|
|
-
|
|
|
|
-use strict;
|
|
|
|
-use warnings;
|
|
|
|
-# use bytes;
|
|
|
|
-use re 'taint';
|
|
|
|
-use version;
|
|
|
|
-
|
|
|
|
-our @ISA = qw(Mail::SpamAssassin::Plugin);
|
|
|
|
-
|
|
|
|
-use constant HAS_GEOIP => eval { require Geo::IP; };
|
|
|
|
-use constant HAS_GEOIP2 => eval { require GeoIP2::Database::Reader; };
|
2020-11-09 18:09:46 +00:00
|
|
|
-use constant HAS_CIDR => eval { require Net::CIDR::Lite; };
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
-# constructor
|
|
|
|
-sub new {
|
|
|
|
- my $class = shift;
|
|
|
|
- my $mailsaobject = shift;
|
|
|
|
-
|
|
|
|
- # some boilerplate...
|
|
|
|
- $class = ref($class) || $class;
|
|
|
|
- my $self = $class->SUPER::new($mailsaobject);
|
|
|
|
- bless ($self, $class);
|
|
|
|
-
|
|
|
|
- # how to handle failure to get the database handle?
|
|
|
|
- # and we don't really have a valid return value...
|
|
|
|
- # can we defer getting this handle until we actually see
|
|
|
|
- # a uri_block_cc rule?
|
|
|
|
-
|
|
|
|
- $self->register_eval_rule("check_uri_local_bl");
|
|
|
|
-
|
|
|
|
- $self->set_config($mailsaobject->{conf});
|
|
|
|
-
|
|
|
|
- return $self;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-sub set_config {
|
|
|
|
- my ($self, $conf) = @_;
|
|
|
|
- my @cmds;
|
|
|
|
-
|
|
|
|
- my $pluginobj = $self; # allow use inside the closure below
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_block_cc',
|
2020-11-09 18:09:46 +00:00
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
|
2020-01-21 21:48:34 +00:00
|
|
|
- is_priv => 1,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
-
|
|
|
|
- if ($value !~ /^(\S+)\s+(.+)$/) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- my $name = $1;
|
|
|
|
- my $def = $2;
|
|
|
|
- my $added_criteria = 0;
|
|
|
|
-
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{countries} = {};
|
|
|
|
-
|
|
|
|
- # this should match all country codes including satellite providers
|
|
|
|
- while ($def =~ m/^\s*([a-z][a-z0-9])(\s+(.*)|)$/) {
|
|
|
|
- my $cc = $1;
|
|
|
|
- my $rest = $2;
|
|
|
|
-
|
|
|
|
- #dbg("config: uri_block_cc adding %s to %s\n", $cc, $name);
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{countries}->{uc($cc)} = 1;
|
|
|
|
- $added_criteria = 1;
|
|
|
|
-
|
|
|
|
- $def = $rest;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if ($added_criteria == 0) {
|
|
|
|
- warn "config: no arguments";
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- } elsif ($def ne '') {
|
|
|
|
- warn "config: failed to add invalid rule $name";
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- dbg("config: uri_block_cc added %s\n", $name);
|
|
|
|
-
|
|
|
|
- $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_block_cont',
|
2020-11-09 18:09:46 +00:00
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
|
2020-01-21 21:48:34 +00:00
|
|
|
- is_priv => 1,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
-
|
|
|
|
- if ($value !~ /^(\S+)\s+(.+)$/) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- my $name = $1;
|
|
|
|
- my $def = $2;
|
|
|
|
- my $added_criteria = 0;
|
|
|
|
-
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{continents} = {};
|
|
|
|
-
|
|
|
|
- # this should match all continent codes
|
|
|
|
- while ($def =~ m/^\s*([a-z]{2})(\s+(.*)|)$/) {
|
|
|
|
- my $cont = $1;
|
|
|
|
- my $rest = $2;
|
|
|
|
-
|
|
|
|
- # dbg("config: uri_block_cont adding %s to %s\n", $cont, $name);
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{continents}->{uc($cont)} = 1;
|
|
|
|
- $added_criteria = 1;
|
|
|
|
-
|
|
|
|
- $def = $rest;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if ($added_criteria == 0) {
|
|
|
|
- warn "config: no arguments";
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- } elsif ($def ne '') {
|
|
|
|
- warn "config: failed to add invalid rule $name";
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- dbg("config: uri_block_cont added %s\n", $name);
|
|
|
|
-
|
|
|
|
- $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_block_isp',
|
2020-11-09 18:09:46 +00:00
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
|
2020-01-21 21:48:34 +00:00
|
|
|
- is_priv => 1,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
-
|
|
|
|
- if ($value !~ /^(\S+)\s+(.+)$/) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- my $name = $1;
|
|
|
|
- my $def = $2;
|
|
|
|
- my $added_criteria = 0;
|
|
|
|
-
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{isps} = {};
|
|
|
|
-
|
|
|
|
- # gather up quoted strings
|
|
|
|
- while ($def =~ m/^\s*"([^"]*)"(\s+(.*)|)$/) {
|
|
|
|
- my $isp = $1;
|
|
|
|
- my $rest = $2;
|
|
|
|
-
|
|
|
|
- dbg("config: uri_block_isp adding \"%s\" to %s\n", $isp, $name);
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{isps}->{$isp} = 1;
|
|
|
|
- $added_criteria = 1;
|
|
|
|
-
|
|
|
|
- $def = $rest;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if ($added_criteria == 0) {
|
|
|
|
- warn "config: no arguments";
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- } elsif ($def ne '') {
|
|
|
|
- warn "config: failed to add invalid rule $name";
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_block_cidr',
|
2020-11-09 18:09:46 +00:00
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
|
2020-01-21 21:48:34 +00:00
|
|
|
- is_priv => 1,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- if (!HAS_CIDR) {
|
|
|
|
- warn "config: uri_block_cidr not supported, required module Net::CIDR::Lite missing\n";
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
2020-01-21 21:48:34 +00:00
|
|
|
- if ($value !~ /^(\S+)\s+(.+)$/) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- my $name = $1;
|
|
|
|
- my $def = $2;
|
|
|
|
- my $added_criteria = 0;
|
|
|
|
-
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr} = new Net::CIDR::Lite;
|
|
|
|
-
|
|
|
|
- # match individual IP's, subnets, and ranges
|
|
|
|
- while ($def =~ m/^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2}|-\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?)(\s+(.*)|)$/) {
|
|
|
|
- my $addr = $1;
|
|
|
|
- my $rest = $3;
|
|
|
|
-
|
|
|
|
- dbg("config: uri_block_cidr adding %s to %s\n", $addr, $name);
|
|
|
|
-
|
|
|
|
- eval { $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr}->add_any($addr) };
|
|
|
|
- last if ($@);
|
|
|
|
-
|
|
|
|
- $added_criteria = 1;
|
|
|
|
-
|
|
|
|
- $def = $rest;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if ($added_criteria == 0) {
|
|
|
|
- warn "config: no arguments";
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- } elsif ($def ne '') {
|
|
|
|
- warn "config: failed to add invalid rule $name";
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- # optimize the ranges
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{cidr}->clean();
|
|
|
|
-
|
|
|
|
- $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_block_exclude',
|
2020-11-09 18:09:46 +00:00
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
|
2020-01-21 21:48:34 +00:00
|
|
|
- is_priv => 1,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
-
|
|
|
|
- if ($value !~ /^(\S+)\s+(.+)$/) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
- my $name = $1;
|
|
|
|
- my $def = $2;
|
|
|
|
- my $added_criteria = 0;
|
|
|
|
-
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{exclusions} = {};
|
|
|
|
-
|
|
|
|
- # match individual IP's, or domain names
|
|
|
|
- while ($def =~ m/^\s*((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(([a-z0-9][-a-z0-9]*[a-z0-9](\.[a-z0-9][-a-z0-9]*[a-z0-9]){1,})))(\s+(.*)|)$/) {
|
|
|
|
- my $addr = $1;
|
|
|
|
- my $rest = $6;
|
|
|
|
-
|
|
|
|
- dbg("config: uri_block_exclude adding %s to %s\n", $addr, $name);
|
|
|
|
-
|
|
|
|
- $conf->{parser}->{conf}->{uri_local_bl}->{$name}->{exclusions}->{$addr} = 1;
|
|
|
|
-
|
|
|
|
- $added_criteria = 1;
|
|
|
|
-
|
|
|
|
- $def = $rest;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if ($added_criteria == 0) {
|
|
|
|
- warn "config: no arguments";
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- } elsif ($def ne '') {
|
|
|
|
- warn "config: failed to add invalid rule $name";
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $conf->{parser}->add_test($name, 'check_uri_local_bl()', $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
-=over 2
|
|
|
|
-
|
|
|
|
-=item uri_country_db_path STRING
|
|
|
|
-
|
|
|
|
-This option tells SpamAssassin where to find the MaxMind country GeoIP2
|
2020-11-09 18:09:46 +00:00
|
|
|
-database. Country or City database are both supported.
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
-=back
|
|
|
|
-
|
|
|
|
-=cut
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_country_db_path',
|
|
|
|
- is_priv => 1,
|
|
|
|
- default => undef,
|
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
- if (!defined $value || !length $value) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- }
|
|
|
|
- if (!-f $value) {
|
|
|
|
- info("config: uri_country_db_path \"$value\" is not accessible");
|
|
|
|
- $self->{uri_country_db_path} = $value;
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $self->{uri_country_db_path} = $value;
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
-=over 2
|
|
|
|
-
|
|
|
|
-=item uri_country_db_isp_path STRING
|
|
|
|
-
|
|
|
|
-This option tells SpamAssassin where to find the MaxMind isp GeoIP2 database.
|
|
|
|
-
|
|
|
|
-=back
|
|
|
|
-
|
|
|
|
-=cut
|
|
|
|
-
|
|
|
|
- push (@cmds, {
|
|
|
|
- setting => 'uri_country_db_isp_path',
|
|
|
|
- is_priv => 1,
|
|
|
|
- default => undef,
|
|
|
|
- type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
|
|
|
|
- code => sub {
|
|
|
|
- my ($self, $key, $value, $line) = @_;
|
|
|
|
- if (!defined $value || !length $value) {
|
|
|
|
- return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
|
|
- }
|
|
|
|
- if (!-f $value) {
|
|
|
|
- info("config: uri_country_db_isp_path \"$value\" is not accessible");
|
|
|
|
- $self->{uri_country_db_isp_path} = $value;
|
|
|
|
- return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $self->{uri_country_db_isp_path} = $value;
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- $conf->{parser}->register_commands(\@cmds);
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-sub check_uri_local_bl {
|
|
|
|
- my ($self, $permsg) = @_;
|
|
|
|
-
|
|
|
|
- my $cc;
|
|
|
|
- my $cont;
|
|
|
|
- my $db_info;
|
|
|
|
- my $isp;
|
|
|
|
-
|
|
|
|
- my $conf_country_db_path = $self->{'main'}{'resolver'}{'conf'}->{uri_country_db_path};
|
|
|
|
- my $conf_country_db_isp_path = $self->{'main'}{'resolver'}{'conf'}->{uri_country_db_isp_path};
|
|
|
|
- # If country_db_path is set I am using GeoIP2 api
|
|
|
|
- if ( HAS_GEOIP2 and ( ( defined $conf_country_db_path ) or ( defined $conf_country_db_isp_path ) ) ) {
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- eval {
|
2020-01-21 21:48:34 +00:00
|
|
|
- $self->{geoip} = GeoIP2::Database::Reader->new(
|
|
|
|
- file => $conf_country_db_path,
|
|
|
|
- locales => [ 'en' ]
|
|
|
|
- ) if (( defined $conf_country_db_path ) && ( -f $conf_country_db_path));
|
|
|
|
- if ( defined ($conf_country_db_path) ) {
|
|
|
|
- $db_info = sub { return "GeoIP2 " . ($self->{geoip}->metadata()->description()->{en} || '?') };
|
|
|
|
- warn "$conf_country_db_path not found" unless $self->{geoip};
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $self->{geoisp} = GeoIP2::Database::Reader->new(
|
|
|
|
- file => $conf_country_db_isp_path,
|
|
|
|
- locales => [ 'en' ]
|
|
|
|
- ) if (( defined $conf_country_db_isp_path ) && ( -f $conf_country_db_isp_path));
|
|
|
|
- if ( defined ($conf_country_db_isp_path) ) {
|
|
|
|
- warn "$conf_country_db_isp_path not found" unless $self->{geoisp};
|
|
|
|
- }
|
|
|
|
- $self->{use_geoip2} = 1;
|
2020-11-09 18:09:46 +00:00
|
|
|
- };
|
|
|
|
- if ($@ || !($self->{geoip} || $self->{geoisp})) {
|
|
|
|
- $@ =~ s/\s+Trace begun.*//s;
|
|
|
|
- warn "URILocalBL: GeoIP2 load failed: $@\n";
|
|
|
|
- return 0;
|
|
|
|
- }
|
|
|
|
-
|
2020-01-21 21:48:34 +00:00
|
|
|
- } elsif ( HAS_GEOIP ) {
|
|
|
|
- BEGIN {
|
|
|
|
- Geo::IP->import( qw(GEOIP_MEMORY_CACHE GEOIP_CHECK_CACHE GEOIP_ISP_EDITION) );
|
|
|
|
- }
|
|
|
|
- $self->{use_geoip2} = 0;
|
|
|
|
- # need GeoIP C library 1.6.3 and GeoIP perl API 1.4.4 or later to avoid messages leaking - Bug 7153
|
|
|
|
- my $gic_wanted = version->parse('v1.6.3');
|
|
|
|
- my $gic_have = version->parse(Geo::IP->lib_version());
|
|
|
|
- my $gip_wanted = version->parse('v1.4.4');
|
|
|
|
- my $gip_have = version->parse($Geo::IP::VERSION);
|
|
|
|
-
|
|
|
|
- # this code burps an ugly message if it fails, but that's redirected elsewhere
|
|
|
|
- my $flags = 0;
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $flag_isp = 0;
|
|
|
|
- my $flag_silent = 0;
|
|
|
|
- eval '$flags = GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE' if ($gip_wanted >= $gip_have);
|
|
|
|
- eval '$flag_silent = Geo::IP::GEOIP_SILENCE' if ($gip_wanted >= $gip_have);
|
|
|
|
- eval '$flag_isp = GEOIP_ISP_EDITION' if ($gip_wanted >= $gip_have);
|
|
|
|
-
|
|
|
|
- eval {
|
|
|
|
- if ($flag_silent && $gic_wanted >= $gic_have) {
|
|
|
|
- $self->{geoip} = Geo::IP->new($flags | $flag_silent);
|
|
|
|
- $self->{geoisp} = Geo::IP->open_type($flag_isp | $flag_silent | $flags);
|
2020-01-21 21:48:34 +00:00
|
|
|
- } else {
|
|
|
|
- open(OLDERR, ">&STDERR");
|
|
|
|
- open(STDERR, ">", "/dev/null");
|
2020-11-09 18:09:46 +00:00
|
|
|
- $self->{geoip} = Geo::IP->new($flags);
|
|
|
|
- $self->{geoisp} = Geo::IP->open_type($flag_isp);
|
2020-01-21 21:48:34 +00:00
|
|
|
- open(STDERR, ">&OLDERR");
|
|
|
|
- close(OLDERR);
|
|
|
|
- }
|
2020-11-09 18:09:46 +00:00
|
|
|
- };
|
|
|
|
- if ($@ || !($self->{geoip} || $self->{geoisp})) {
|
|
|
|
- $@ =~ s/\s+Trace begun.*//s;
|
|
|
|
- warn "URILocalBL: GeoIP load failed: $@\n";
|
|
|
|
- return 0;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $db_info = sub { return "Geo::IP " . ($self->{geoip}->database_info || '?') };
|
2020-01-21 21:48:34 +00:00
|
|
|
- } else {
|
|
|
|
- dbg("No GeoIP module available");
|
|
|
|
- return 0;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- my %uri_detail = %{ $permsg->get_uri_detail_list() };
|
|
|
|
- my $test = $permsg->{current_rule_name};
|
|
|
|
- my $rule = $permsg->{conf}->{uri_local_bl}->{$test};
|
|
|
|
-
|
|
|
|
- my %hit_tests;
|
|
|
|
- my $got_hit = 0;
|
2020-11-09 18:09:46 +00:00
|
|
|
- my @addrs;
|
|
|
|
- my $IP_ADDRESS = IP_ADDRESS;
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
- if ( defined $self->{geoip} ) {
|
|
|
|
- dbg("check: uri_local_bl evaluating rule %s using database %s\n", $test, $db_info->());
|
|
|
|
- } else {
|
|
|
|
- dbg("check: uri_local_bl evaluating rule %s\n", $test);
|
|
|
|
- }
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $dns_available = $permsg->is_dns_available();
|
|
|
|
-
|
2020-01-21 21:48:34 +00:00
|
|
|
- while (my ($raw, $info) = each %uri_detail) {
|
|
|
|
-
|
|
|
|
- next unless $info->{hosts};
|
|
|
|
-
|
|
|
|
- # look for W3 links only
|
2020-11-09 18:09:46 +00:00
|
|
|
- next unless (defined $info->{types}->{a} || defined $info->{types}->{parsed});
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
- while (my($host, $domain) = each %{$info->{hosts}}) {
|
|
|
|
-
|
|
|
|
- # skip if the domain name was matched
|
|
|
|
- if (exists $rule->{exclusions} && exists $rule->{exclusions}->{$domain}) {
|
|
|
|
- dbg("check: uri_local_bl excludes %s as *.%s\n", $host, $domain);
|
|
|
|
- next;
|
|
|
|
- }
|
|
|
|
-
|
2020-11-09 18:09:46 +00:00
|
|
|
- if($host !~ /^$IP_ADDRESS$/) {
|
|
|
|
- if (!$dns_available) {
|
|
|
|
- dbg("check: uri_local_bl skipping $host, dns not available");
|
|
|
|
- next;
|
|
|
|
- }
|
|
|
|
- # this would be best cached from prior lookups
|
|
|
|
- @addrs = gethostbyname($host);
|
|
|
|
- # convert to string values address list
|
|
|
|
- @addrs = map { inet_ntoa($_); } @addrs[4..$#addrs];
|
|
|
|
- } else {
|
|
|
|
- @addrs = ($host);
|
|
|
|
- }
|
2020-01-21 21:48:34 +00:00
|
|
|
-
|
|
|
|
- dbg("check: uri_local_bl %s addrs %s\n", $host, join(', ', @addrs));
|
|
|
|
-
|
|
|
|
- for my $ip (@addrs) {
|
|
|
|
- # skip if the address was matched
|
|
|
|
- if (exists $rule->{exclusions} && exists $rule->{exclusions}->{$ip}) {
|
|
|
|
- dbg("check: uri_local_bl excludes %s(%s)\n", $host, $ip);
|
|
|
|
- next;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (exists $rule->{countries}) {
|
|
|
|
- dbg("check: uri_local_bl countries %s\n", join(' ', sort keys %{$rule->{countries}}));
|
|
|
|
-
|
|
|
|
- if ( $self->{use_geoip2} == 1 ) {
|
2020-11-09 18:09:46 +00:00
|
|
|
- my $country;
|
|
|
|
- if (index($self->{geoip}->metadata()->description()->{en}, 'City') != -1) {
|
|
|
|
- $country = $self->{geoip}->city( ip => $ip );
|
|
|
|
- } else {
|
|
|
|
- $country = $self->{geoip}->country( ip => $ip );
|
|
|
|
- }
|
2020-01-21 21:48:34 +00:00
|
|
|
- my $country_rec = $country->country();
|
|
|
|
- $cc = $country_rec->iso_code();
|
|
|
|
- } else {
|
|
|
|
- $cc = $self->{geoip}->country_code_by_addr($ip);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- dbg("check: uri_local_bl host %s(%s) maps to %s\n", $host, $ip, (defined $cc ? $cc : "(undef)"));
|
|
|
|
-
|
|
|
|
- # handle there being no associated country (yes, there are holes in
|
|
|
|
- # the database).
|
|
|
|
- next unless defined $cc;
|
|
|
|
-
|
|
|
|
- # not in blacklist
|
|
|
|
- next unless (exists $rule->{countries}->{$cc});
|
|
|
|
-
|
|
|
|
- dbg("check: uri_block_cc host %s(%s) matched\n", $host, $ip);
|
|
|
|
-
|
|
|
|
- if (would_log('dbg', 'rules') > 1) {
|
|
|
|
- dbg("check: uri_block_cc criteria for $test met");
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $permsg->test_log("Host: $host in $cc");
|
|
|
|
- $hit_tests{$test} = 1;
|
|
|
|
-
|
|
|
|
- # reset hash
|
|
|
|
- keys %uri_detail;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (exists $rule->{continents}) {
|
|
|
|
- dbg("check: uri_local_bl continents %s\n", join(' ', sort keys %{$rule->{continents}}));
|
|
|
|
-
|
|
|
|
- if ( $self->{use_geoip2} == 1 ) {
|
|
|
|
- my $country = $self->{geoip}->country( ip => $ip );
|
|
|
|
- my $cont_rec = $country->continent();
|
|
|
|
- $cont = $cont_rec->{code};
|
|
|
|
- } else {
|
|
|
|
- $cc = $self->{geoip}->country_code_by_addr($ip);
|
|
|
|
- $cont = $self->{geoip}->continent_code_by_country_code($cc);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- dbg("check: uri_local_bl host %s(%s) maps to %s\n", $host, $ip, (defined $cont ? $cont : "(undef)"));
|
|
|
|
-
|
|
|
|
- # handle there being no associated continent (yes, there are holes in
|
|
|
|
- # the database).
|
|
|
|
- next unless defined $cont;
|
|
|
|
-
|
|
|
|
- # not in blacklist
|
|
|
|
- next unless (exists $rule->{continents}->{$cont});
|
|
|
|
-
|
|
|
|
- dbg("check: uri_block_cont host %s(%s) matched\n", $host, $ip);
|
|
|
|
-
|
|
|
|
- if (would_log('dbg', 'rules') > 1) {
|
|
|
|
- dbg("check: uri_block_cont criteria for $test met");
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $permsg->test_log("Host: $host in $cont");
|
|
|
|
- $hit_tests{$test} = 1;
|
|
|
|
-
|
|
|
|
- # reset hash
|
|
|
|
- keys %uri_detail;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (exists $rule->{isps}) {
|
|
|
|
- dbg("check: uri_local_bl isps %s\n", join(' ', map { '"' . $_ . '"'; } sort keys %{$rule->{isps}}));
|
|
|
|
-
|
|
|
|
- if ( $self->{use_geoip2} == 1 ) {
|
|
|
|
- $isp = $self->{geoisp}->isp(ip => $ip);
|
|
|
|
- } else {
|
|
|
|
- $isp = $self->{geoisp}->isp_by_name($ip);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- dbg("check: uri_local_bl isp %s(%s) maps to %s\n", $host, $ip, (defined $isp ? '"' . $isp . '"' : "(undef)"));
|
|
|
|
-
|
|
|
|
- # handle there being no associated country
|
|
|
|
- next unless defined $isp;
|
|
|
|
-
|
|
|
|
- # not in blacklist
|
|
|
|
- next unless (exists $rule->{isps}->{$isp});
|
|
|
|
-
|
|
|
|
- dbg("check: uri_block_isp host %s(%s) matched\n", $host, $ip);
|
|
|
|
-
|
|
|
|
- if (would_log('dbg', 'rules') > 1) {
|
|
|
|
- dbg("check: uri_block_isp criteria for $test met");
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $permsg->test_log("Host: $host in \"$isp\"");
|
|
|
|
- $hit_tests{$test} = 1;
|
|
|
|
-
|
|
|
|
- # reset hash
|
|
|
|
- keys %uri_detail;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (exists $rule->{cidr}) {
|
|
|
|
- dbg("check: uri_block_cidr list %s\n", join(' ', $rule->{cidr}->list_range()));
|
|
|
|
-
|
|
|
|
- next unless ($rule->{cidr}->find($ip));
|
|
|
|
-
|
|
|
|
- dbg("check: uri_block_cidr host %s(%s) matched\n", $host, $ip);
|
|
|
|
-
|
|
|
|
- if (would_log('dbg', 'rules') > 1) {
|
|
|
|
- dbg("check: uri_block_cidr criteria for $test met");
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- $permsg->test_log("Host: $host as $ip");
|
|
|
|
- $hit_tests{$test} = 1;
|
|
|
|
-
|
|
|
|
- # reset hash
|
|
|
|
- keys %uri_detail;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- # cycle through all tests hitted by the uri
|
|
|
|
- while((my $test_ok) = each %hit_tests) {
|
|
|
|
- $permsg->got_hit($test_ok);
|
|
|
|
- $got_hit = 1;
|
|
|
|
- }
|
|
|
|
- if($got_hit == 1) {
|
|
|
|
- return 1;
|
|
|
|
- } else {
|
|
|
|
- keys %hit_tests;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- dbg("check: uri_local_bl %s no match\n", $test);
|
|
|
|
-
|
|
|
|
- return 0;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-1;
|
|
|
|
-
|
|
|
|
diff --git a/lib/Mail/SpamAssassin/Util/DependencyInfo.pm b/lib/Mail/SpamAssassin/Util/DependencyInfo.pm
|
2020-11-09 18:09:46 +00:00
|
|
|
index 2f8aa65..eca12e1 100644
|
2020-01-21 21:48:34 +00:00
|
|
|
--- a/lib/Mail/SpamAssassin/Util/DependencyInfo.pm
|
|
|
|
+++ b/lib/Mail/SpamAssassin/Util/DependencyInfo.pm
|
2020-11-09 18:09:46 +00:00
|
|
|
@@ -124,46 +124,6 @@ our @OPTIONAL_MODULES = (
|
2020-01-21 21:48:34 +00:00
|
|
|
desc => 'Used to check DNS Sender Policy Framework (SPF) records to fight email
|
|
|
|
address forgery and make it easier to identify spams.',
|
|
|
|
},
|
|
|
|
-{
|
2020-11-09 18:09:46 +00:00
|
|
|
- module => 'GeoIP2::Database::Reader',
|
|
|
|
- version => 0,
|
|
|
|
- desc => 'Used by the RelayCountry plugin (not enabled by default) to
|
|
|
|
- determine the domain country codes of each relay in the path of an email.
|
|
|
|
- Also used by the URILocalBL plugin (not enabled by default) to provide ISP
|
|
|
|
- and Country code based filtering.',
|
|
|
|
-},
|
|
|
|
-{
|
2020-01-21 21:48:34 +00:00
|
|
|
- module => 'Geo::IP',
|
|
|
|
- version => 0,
|
|
|
|
- desc => 'Used by the RelayCountry plugin (not enabled by default) to determine
|
|
|
|
- the domain country codes of each relay in the path of an email. Also used by
|
|
|
|
- the URILocalBL plugin to provide ISP and Country code based filtering.',
|
|
|
|
-},
|
|
|
|
-{
|
2020-11-09 18:09:46 +00:00
|
|
|
- module => 'IP::Country::DB_File',
|
|
|
|
- version => 0,
|
|
|
|
- desc => 'Used by the RelayCountry plugin (not enabled by default) to
|
|
|
|
- determine the domain country codes of each relay in the path of an email.
|
|
|
|
- Also used by the URILocalBL plugin (not enabled by default) to provide
|
|
|
|
- Country code based filtering.',
|
|
|
|
-},
|
|
|
|
-{
|
2020-01-21 21:48:34 +00:00
|
|
|
- module => 'Net::CIDR::Lite',
|
|
|
|
- version => 0,
|
|
|
|
- desc => 'Used by the URILocalBL plugin to process IP address ranges.',
|
|
|
|
-},
|
|
|
|
-{
|
|
|
|
- module => 'Razor2::Client::Agent',
|
|
|
|
- alt_name => 'Razor2',
|
|
|
|
- version => '2.61',
|
|
|
|
- desc => 'Used to check message signatures against Vipul\'s Razor collaborative
|
|
|
|
- filtering network. Razor has a large number of dependencies on CPAN
|
|
|
|
- modules. Feel free to skip installing it, if this makes you nervous;
|
|
|
|
- SpamAssassin will still work well without it.
|
|
|
|
-
|
|
|
|
- More info on installing and using Razor can be found
|
|
|
|
- at http://wiki.apache.org/spamassassin/InstallingRazor .',
|
|
|
|
-},
|
|
|
|
#{
|
|
|
|
# module => 'Net::Ident',
|
|
|
|
# version => 0,
|
|
|
|
diff --git a/rules/init.pre b/rules/init.pre
|
2020-11-09 18:09:46 +00:00
|
|
|
index f9ee06a..0539b29 100644
|
2020-01-21 21:48:34 +00:00
|
|
|
--- a/rules/init.pre
|
|
|
|
+++ b/rules/init.pre
|
|
|
|
@@ -14,13 +14,6 @@
|
|
|
|
# added to new files, named according to the release they're added in.
|
|
|
|
###########################################################################
|
|
|
|
|
|
|
|
-# RelayCountry - add metadata for Bayes learning, marking the countries
|
|
|
|
-# a message was relayed through
|
|
|
|
-#
|
|
|
|
-# Note: This requires the Geo::IP Perl module
|
|
|
|
-#
|
|
|
|
-# loadplugin Mail::SpamAssassin::Plugin::RelayCountry
|
|
|
|
-
|
|
|
|
# URIDNSBL - look up URLs found in the message against several DNS
|
|
|
|
# blocklists.
|
|
|
|
#
|
|
|
|
diff --git a/rules/v341.pre b/rules/v341.pre
|
|
|
|
index 489dd4c..7ff8e84 100644
|
|
|
|
--- a/rules/v341.pre
|
|
|
|
+++ b/rules/v341.pre
|
|
|
|
@@ -19,10 +19,5 @@
|
|
|
|
# TxRep - Reputation database that replaces AWL
|
|
|
|
# loadplugin Mail::SpamAssassin::Plugin::TxRep
|
|
|
|
|
|
|
|
-# URILocalBL - Provides ISP and Country code based filtering as well as
|
|
|
|
-# quick IP based blocks without a full RBL implementation - Bug 7060
|
|
|
|
-
|
|
|
|
-# loadplugin Mail::SpamAssassin::Plugin::URILocalBL
|
|
|
|
-
|
|
|
|
# PDFInfo - Use several methods to detect a PDF file's ham/spam traits
|
|
|
|
# loadplugin Mail::SpamAssassin::Plugin::PDFInfo
|
|
|
|
diff --git a/spamassassin.raw b/spamassassin.raw
|
2020-11-09 18:09:46 +00:00
|
|
|
index 4b52ef9..959297a 100755
|
2020-01-21 21:48:34 +00:00
|
|
|
--- a/spamassassin.raw
|
|
|
|
+++ b/spamassassin.raw
|
2020-11-09 18:09:46 +00:00
|
|
|
@@ -872,9 +872,6 @@ from the SpamAssassin distribution.
|
2020-01-21 21:48:34 +00:00
|
|
|
Mail::SpamAssassin::Plugin::Hashcash
|
|
|
|
perform hashcash verification tests
|
|
|
|
|
|
|
|
- Mail::SpamAssassin::Plugin::RelayCountry
|
|
|
|
- add message metadata indicating the country code of each relay
|
|
|
|
-
|
|
|
|
Mail::SpamAssassin::Plugin::SPF
|
|
|
|
perform SPF verification tests
|
|
|
|
|