1# <@LICENSE>
2# Licensed to the Apache Software Foundation (ASF) under one or more
3# contributor license agreements.  See the NOTICE file distributed with
4# this work for additional information regarding copyright ownership.
5# The ASF licenses this file to you under the Apache License, Version 2.0
6# (the "License"); you may not use this file except in compliance with
7# the License.  You may obtain a copy of the License at:
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16# </@LICENSE>
17
18=head1 NAME
19
20RelayCountry - add message metadata indicating the country code of each relay
21
22=head1 SYNOPSIS
23
24  loadplugin     Mail::SpamAssassin::Plugin::RelayCountry
25
26=head1 DESCRIPTION
27
28The RelayCountry plugin attempts to determine the domain country codes
29of each relay used in the delivery path of messages and add that information
30to the message metadata.
31
32Following metadata headers and tags are added:
33
34 X-Relay-Countries           _RELAYCOUNTRY_
35   All untrusted relays. Contains all relays starting from the
36   trusted_networks border. This method has been used by default since
37   early SA versions.
38
39 X-Relay-Countries-External  _RELAYCOUNTRYEXT_
40   All external relays. Contains all relays starting from the
41   internal_networks border. Could be useful in some cases when
42   trusted/msa_networks extend beyond the internal border and those
43   need to be checked too.
44
45 X-Relay-Countries-All       _RELAYCOUNTRYALL_
46   All possible relays (internal + external).
47
48 X-Relay-Countries-Auth      _RELAYCOUNTRYAUTH_
49   Auth will contain all relays starting from the first relay that used
50   authentication. For example, this could be used to check for hacked
51   local users coming in from unexpected countries. If there are no
52   authenticated relays, this will be empty.
53
54=head1 REQUIREMENT
55
56This plugin uses Mail::SpamAssassin::GeoDB and requires a module supported
57by it, for example MaxMind::DB::Reader (GeoIP2).
58
59=cut
60
61package Mail::SpamAssassin::Plugin::RelayCountry;
62
63use Mail::SpamAssassin::Plugin;
64use Mail::SpamAssassin::Logger;
65use strict;
66use warnings;
67# use bytes;
68use re 'taint';
69
70our @ISA = qw(Mail::SpamAssassin::Plugin);
71
72my $db;
73my $dbv6;
74my $db_info;  # will hold database info
75my $db_type;  # will hold database type
76
77# constructor: register the eval rule
78sub new {
79  my $class = shift;
80  my $mailsaobject = shift;
81
82  # some boilerplate...
83  $class = ref($class) || $class;
84  my $self = $class->SUPER::new($mailsaobject);
85  bless ($self, $class);
86
87  # we need GeoDB country
88  $self->{main}->{geodb_wanted}->{country} = 1;
89
90  return $self;
91}
92
93sub extract_metadata {
94  my ($self, $opts) = @_;
95  my $pms = $opts->{permsgstatus};
96
97  return if $self->{relaycountry_disabled};
98
99  if (!$self->{main}->{geodb} ||
100        !$self->{main}->{geodb}->can('country')) {
101    dbg("metadata: RelayCountry: plugin disabled, GeoDB country not available");
102    $self->{relaycountry_disabled} = 1;
103    return;
104  }
105
106  my $msg = $opts->{msg};
107  my $geodb = $self->{main}->{geodb};
108
109  my @cc_untrusted;
110  foreach my $relay (@{$msg->{metadata}->{relays_untrusted}}) {
111    my $ip = $relay->{ip};
112    my $cc = $geodb->get_country($ip);
113    push @cc_untrusted, $cc;
114  }
115
116  my @cc_external;
117  foreach my $relay (@{$msg->{metadata}->{relays_external}}) {
118    my $ip = $relay->{ip};
119    my $cc = $geodb->get_country($ip);
120    push @cc_external, $cc;
121  }
122
123  my @cc_auth;
124  my $found_auth;
125  foreach my $relay (@{$msg->{metadata}->{relays_trusted}}) {
126    if ($relay->{auth}) {
127      $found_auth = 1;
128    }
129    if ($found_auth) {
130      my $ip = $relay->{ip};
131      my $cc = $geodb->get_country($ip);
132      push @cc_auth, $cc;
133    }
134  }
135
136  my @cc_all;
137  foreach my $relay (@{$msg->{metadata}->{relays_internal}}, @{$msg->{metadata}->{relays_external}}) {
138    my $ip = $relay->{ip};
139    my $cc = $geodb->get_country($ip);
140    push @cc_all, $cc;
141  }
142
143  my $ccstr = join(' ', @cc_untrusted);
144  $msg->put_metadata("X-Relay-Countries", $ccstr);
145  dbg("metadata: X-Relay-Countries: $ccstr");
146  $pms->set_tag("RELAYCOUNTRY", @cc_untrusted == 1 ? $cc_untrusted[0] : \@cc_untrusted);
147
148  $ccstr = join(' ', @cc_external);
149  $msg->put_metadata("X-Relay-Countries-External", $ccstr);
150  dbg("metadata: X-Relay-Countries-External: $ccstr");
151  $pms->set_tag("RELAYCOUNTRYEXT", @cc_external == 1 ? $cc_external[0] : \@cc_external);
152
153  $ccstr = join(' ', @cc_auth);
154  $msg->put_metadata("X-Relay-Countries-Auth", $ccstr);
155  dbg("metadata: X-Relay-Countries-Auth: $ccstr");
156  $pms->set_tag("RELAYCOUNTRYAUTH", @cc_auth == 1 ? $cc_auth[0] : \@cc_auth);
157
158  $ccstr = join(' ', @cc_all);
159  $msg->put_metadata("X-Relay-Countries-All", $ccstr);
160  dbg("metadata: X-Relay-Countries-All: $ccstr");
161  $pms->set_tag("RELAYCOUNTRYALL", @cc_all == 1 ? $cc_all[0] : \@cc_all);
162}
163
1641;
165