Server IP : 85.214.239.14 / Your IP : 3.142.54.83 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /usr/share/perl5/Amavis/SpamControl/ |
Upload File : |
# SPDX-License-Identifier: GPL-2.0-or-later package Amavis::SpamControl::RspamdClient; use strict; use re 'taint'; use warnings; use warnings FATAL => qw(utf8 void); no warnings 'uninitialized'; =pod =head1 Amavis extension module to use Rspamd as a spam checker Copyright (c) 2019 Ralph Seichter, partially based on the SpamdClient extension. Released under GNU General Public License; see Amavis LICENSE file for details. =head2 Example configuration #1 (local Rspamd) # Rspamd running on the same machine as Amavis. Default URL # is http://127.0.0.1:11333/checkv2 , matching Rspamd's # "normal" worker defaults. @spam_scanners = ( [ 'Local Rspamd', 'Amavis::SpamControl::RspamdClient', # Adjust scores according to Rspamd's "required score" # setting (defaults to 15). Scores reported by Rspamd # will be multiplied with this factor. The following # adjusts Rspamd scores to SpamAssassin scores. While # this setting is technically optional, not adjusting # scores is prone to cause headaches. score_factor => $sa_tag2_level_deflt / 15.0, # MTA name is used to assess validity of existing # Authentication-Results headers, e.g. if DKIM/DMARC # validation has already happened. mta_name => 'mail.example.com', ] ); =head2 Example configuration #2 (remote Rspamd) # Rspamd running behind HTTPS-capable proxy using basic # authentication to control access. @spam_scanners = ( [ 'Remote Rspamd', 'Amavis::SpamControl::RspamdClient', url => 'https://rspamd-proxy.example.com/checkv2', # Response timeout in seconds. Default is 60, matching # Rspamd's standard config for the "normal" worker. timeout => 42, # SSL-options and -credentials passed to LWP::UserAgent, # see https://metacpan.org/pod/LWP::UserAgent . Default: # ssl_opts => { verify_hostname => 1 }, credentials => { # The following <host>:<port> must match the 'url' # defined above or credentials won't be transmitted. netloc => 'rspamd-proxy.example.com:443', # Remote authentication realm realm => 'Rspamd restricted access', username => 'Marco', password => 'Polo', }, # Don't scan messages remotely if the body size extends # the following limit (optional setting). mail_body_size_limit => 32 * 1024, score_factor => $sa_tag2_level_deflt / 15.0, mta_name => 'mail.example.com', ] ); =head2 Requirements In addition to Amavis' core requirements, this extension needs the following additional Perl modules: JSON HTTP::Message LWP::UserAgent LWP::Protocol::https Net::SSLeay Should your host OS not provide the necessary packages, these modules can be obtained via https://www.cpan.org . =cut BEGIN { require Exporter; use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); $VERSION = '2.412'; @ISA = qw(Exporter); } use JSON qw(decode_json); use LWP::UserAgent; use Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local); use Amavis::Timing qw(section_time); use Amavis::Util qw(do_log min prolong_timer); sub new { my ($class, $scanner_name, $module, @args) = @_; my (%options) = @args; bless { scanner_name => $scanner_name, options => \%options }, $class; } # Pass meta information using Rspamd's non-standard HTTP headers. sub pass_meta { my ($request, $name, $value) = @_; if (defined $value && $value ne '') { $request->header($name => $value); } } # Invoked by Amavis to spam-check one message. sub check { my ($self, $msginfo) = @_; my ($which_section, $spam_level, $rspamd_action, $rspamd_rscore, $rspamd_skipped, $rspamd_tests, $rspamd_verdict, $size_limit); my $scanner_name = $self->{scanner_name}; my $mbsl = $self->{options}->{'mail_body_size_limit'}; if (defined $mbsl) { $size_limit = min(32 * 1024, $msginfo->orig_header_size) + min($mbsl, $msginfo->orig_body_size); # Allow slightly oversized messages to pass in full. undef $size_limit if $msginfo->msg_size < $size_limit + 5 * 1024; } my $per_recip_data = $msginfo->per_recip_data; $per_recip_data = [] if !$per_recip_data; my $msg = $msginfo->mail_text; my $msg_str_ref = $msginfo->mail_text_str; # In-memory copy available? $msg = $msg_str_ref if ref $msg_str_ref; eval { if (!defined $msg) { do_log(3, "Empty message"); } elsif (ref $msg eq 'SCALAR') { $which_section = 'rspamd_connect'; my $timeout = $self->{options}->{'timeout'}; $timeout = 60 unless defined $timeout; my $url = $self->{options}->{'url'}; $url = 'http://127.0.0.1:11333/checkv2' unless defined $url; do_log(3, "connecting to rspamd %s (timeout %s)", $url, $timeout); my $request = HTTP::Request->new(POST => $url); $request->content_type('application/octet-stream'); $request->content(defined $size_limit ? substr($$msg, 0, $size_limit) : $$msg); pass_meta($request, 'Helo', $msginfo->client_helo); pass_meta($request, 'Hostname', $msginfo->client_name); pass_meta($request, 'IP', $msginfo->client_addr); pass_meta($request, 'MTA-Name', $self->{options}->{'mta_name'}); pass_meta($request, 'From', $msginfo->sender_smtp); pass_meta($request, 'Queue-Id', $msginfo->queue_id); for my $rcpt (qquote_rfc2821_local(@{$msginfo->recips})) { pass_meta($request, 'Rcpt', $rcpt); } $which_section = 'rspamd_tx'; my $ssl_opts = $self->{options}->{'ssl_opts'}; $ssl_opts = { verify_hostname => 1 } unless defined $ssl_opts; my $user_agent = LWP::UserAgent->new( protocols_allowed => [ 'http', 'https' ], ssl_opts => $ssl_opts ); my $credentials = $self->{options}->{'credentials'}; if (defined $credentials) { $user_agent->credentials( $credentials->{'netloc'}, $credentials->{'realm'}, $credentials->{'username'}, $credentials->{'password'}, ) } $user_agent->agent('amavis/' . $VERSION); $user_agent->timeout($timeout); prolong_timer($which_section, undef, undef, $timeout); my $response = $user_agent->request($request); $response->is_success or die "Error calling rspamd: " . $response->status_line . ", stopped"; my $content = $response->content; defined $content or die "Missing rspamd response, stopped"; do_log(5, "Rspamd response: " . $content); my $rspamd = decode_json $content; $rspamd_skipped = $rspamd->{is_skipped}; $spam_level = $rspamd->{score}; $rspamd_rscore = $rspamd->{required_score}; $rspamd_action = $rspamd->{action}; my $rspamd_symbols = $rspamd->{symbols}; if (defined $rspamd_symbols) { my @tests; while (my ($ignored, $symbol) = each %$rspamd_symbols) { my $symbol_name = $symbol->{name}; $symbol_name =~ tr/=,/__/; my $t = sprintf("%s=%s", $symbol_name, $symbol->{score}); push(@tests, $t); } $rspamd_tests = join(',', @tests); } # Map Rspamd action to Amavis verdict my %action2verdict = ( 'add header' => 'Spam', 'no action' => 'Ham', 'reject' => 'Spam', 'rewrite subject' => 'Spam', # Rspamd 1.9 and later 'discard' => 'Spam', 'quarantine' => 'Spam', ); $rspamd_verdict = exists $action2verdict{$rspamd_action} ? $action2verdict{$rspamd_action} : 'Unknown'; } else { do_log(2, "%s skipping message type %s", $scanner_name, ref $msg); $rspamd_action = 'N/A'; $rspamd_verdict = 'Unknown'; $rspamd_skipped = 1; $rspamd_rscore = 0; $spam_level = 0; } 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; do_log(-1, "%s client failed: %s", $scanner_name, $eval_stat); }; section_time($which_section); my $score_factor = $self->{options}->{'score_factor'}; if (defined $spam_level && defined $score_factor) { $spam_level *= $score_factor; $rspamd_rscore *= $score_factor; } do_log(2, "%s rspamd %sscore %.2f/%.2f (%s) %s", $scanner_name, $rspamd_skipped ? 'skipped/' : '', $spam_level, $rspamd_rscore, $rspamd_action, $rspamd_tests); $msginfo->supplementary_info('SCORE-' . $scanner_name, $spam_level); $msginfo->supplementary_info('VERDICT-' . $scanner_name, $rspamd_verdict); for my $r (@$per_recip_data) { $r->spam_level(($r->spam_level || 0) + $spam_level); if (!$r->spam_tests) { $r->spam_tests([ \$rspamd_tests ]); } else { push(@{$r->spam_tests}, \$rspamd_tests); } } } 1;