Index: lib/Mail/SpamAssassin/Plugin/VBounce.pm =================================================================== --- lib/Mail/SpamAssassin/Plugin/VBounce.pm (revision 555444) +++ lib/Mail/SpamAssassin/Plugin/VBounce.pm (working copy) @@ -29,6 +29,7 @@ use Mail::SpamAssassin::Plugin; use Mail::SpamAssassin::Logger; + use strict; use warnings; @@ -44,9 +45,17 @@ $self->register_eval_rule("have_any_bounce_relays"); $self->register_eval_rule("check_whitelist_bounce_relays"); + $self->register_eval_rule("check_bounced_message_legitimacy"); + $self->register_eval_rule("check_bounced_message_illegitimacy"); + $self->register_eval_rule("check_bounced_message_has_valid_dkim_signature"); + $self->register_eval_rule("check_bounced_message_has_our_dkim_signature"); + $self->register_eval_rule("check_bounced_message_has_no_dkim_signature"); + $self->register_eval_rule("check_bounced_message_has_valid_x_header"); $self->set_config($mailsaobject->{conf}); + $self->{'dkim_module_loaded'} = 0; + return $self; } @@ -85,6 +94,128 @@ type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST }); +=item use_dkim_b3d (0|1) (default: 0) + +Whether to use DKIM DNS entries for backscatter detection. + +This method is intended for sites that use DKIM signatures in their outgoing +emails. If a legitimate, and thus DKIM-signed message causes a bounce, the +DKIM signature should be found in the copy of the original message enclosed in +the bounce. + +If enabled, bounce messages will be scanned for a copy of the orginial message +that caused the bounce. If found, the original message is extracted, and +checked for a DKIM signature. The original message's headers will then be +verified using the signature. + +The outcome of the test (signature found? signature valid?) allows conclusions +regarding the validity of the bounce message. Note that the result for +non-bounce-messages is unspecified. + +This method may also be used in combination with an alternate method based +on special headers that are included in outgoing emails +(see C). + +Using this method requires C perl module to be installed. + +=cut + push(@cmds, { + setting => 'use_dkim_b3d', + default => 0, + type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL, + }); + +=item b3d_dkim_domain domain (default: *) + +Domains to check DKIM signature against. Only DKIM signatures made under +one of these domains are considered legitimate. + +Multiple domains may be given seperated by spaces, and multiple +C entries may be used. C<*> is accepted as wildcard. + +Only used if the DKIM-method is activated (see C). + +=cut + push(@cmds, { + setting => 'b3d_dkim_domain', + type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST, + }); + +=item use_xheader_b3d (0|1) (default: 0) + +Whether to use a special header for b3d backscatter detection. + +This method relies on a special header to be inserted into all of a site's +outgoing mails. If one of those emails is bounced, the special header should +be found in the bounce. + +If enabled, incoming bounces will be scanned for an enclosed copy of the +original email. This copy will then be checked for the existence of the +special header, and whether the header contains the correct value. + +The outcome of the test (header found? header valid?) allows conclusions +regarding the validity of the bounce message. Note that the result for +non-bounce-messages is unspecified. + +This method may also be used in combination with an alternate method based +on special headers that are included in outgoing emails +(see C). + +=cut + push(@cmds, { + setting => 'use_xheader_b3d', + default => 0, + type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL, + }); + +=item b3d_x_header header-name (default: X-B3D-Key) + +Name of the header containing the secret key used for validating bounce-mails +when using the header method. + +Only used if the special-header-method is activated (see C). + +=cut + push(@cmds, { + setting => 'b3d_x_header', + default => 'X-B3B-Key', + type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, + }); + +=item b3d_x_header_value secret-key + +Secret key to use for validation of DSN-mails. Multiple keys may be given, +separated by spaces. + +The keys given under C are considered valid keys for all +incoming bounces. + +Only used if the special-header-method is activated (see C). + +=cut + push(@cmds, { + setting => 'b3d_x_header_value', + type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, + }); + +=item b3d_x_header_domain_value domain secret-key + +Secret key to use for validation of bounces that are sent to recipients in a +given domain. + +The specified domain-value is matched against the end of the recipient email of the DSN mail. +Multiple domain/secret-key pairs may be specified in multiple configuration lines. + +If a secret key is given for a domain, mails sent to this domain will be tested for this key +and for all the keys given in C. +Only used if the special-header-method is activated (see C). + +=cut + push(@cmds, { + setting => 'b3d_x_header_domain_value', + type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE, + }); + $conf->{parser}->register_commands(\@cmds); } @@ -141,7 +272,7 @@ my ($self, $pms, $relay) = @_; return 1 if $self->_relay_is_in_list( $pms->{conf}->{whitelist_bounce_relays}, $pms, $relay); - dbg("rules: relay $relay doesn't match any whitelist"); + dbg("vbounce: [rules] relay $relay doesn't match any whitelist"); } sub _relay_is_in_list { @@ -152,7 +283,7 @@ foreach my $regexp (values %{$list}) { if ($relay =~ qr/$regexp/i) { - dbg("rules: relay $relay matches regexp: $regexp"); + dbg("vbounce: [rules] relay $relay matches regexp: $regexp"); return 1; } } @@ -160,6 +291,526 @@ return 0; } + ############################# + # # + # Anti-BackScatter-Code B3D # + # # + ############################# + +# DKIM test return codes, see _check_bounced_message_legitimacy_by_dkim for details. +my $DKIM_RESULT_VALID = 4; +my $DKIM_RESULT_ALTERED = 3; +my $DKIM_RESULT_INVALID = 2; +my $DKIM_RESULT_OTHER = 1; +my $DKIM_RESULT_NONE = 0; +my $DKIM_RESULT_UNKNOWN = -1; +my $DKIM_RESULT_NOCONF = -2; + +# X-Header test return codes, see _check_bounced_message_legitimacy_by_xheader for details. +my $XHEADER_RESULT_SUCCESS = 1; +my $XHEADER_RESULT_FAILURE = 0; +my $XHEADER_RESULT_UNKNOWN = -1; +my $XHEADER_RESULT_NOCONF = -2; + +# TODO: Should the message-id check pushed up to the rules-level? +# I.e. only check for DKIM/Secret-header here, and do the interpretation through rules? + +# Check whether a bounced message contains a legitimate original message, +# i.e. one containing either a valid DKIM signature or a predefined header. +# +# The function returns 1 if bounce legitimacy checking is enabled and +# if the bounced message is legitimate with high certainty, 0 otherwise. +sub check_bounced_message_legitimacy { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{use_dkim_b3d} || $pms->{conf}->{use_xheader_b3d}) { + dbg("vbounce: [check_bounced_message_legitimacy] Skipping B3D checks, not activated"); + return 0; + } + + dbg("vbounce: [check_bounced_message_legitimacy] Started X"); + + if ($pms->{conf}->{use_dkim_b3d}) { + $self->_perform_bounced_message_legitimacy_dkim_tests($pms); + dbg("vbounce: [check_bounced_message_legitimacy] DKIM test result is ".$pms->{b3d_dkim_checks_result}); + + if ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_INVALID) { + # Success if a DKIM signature is found and if it is completely valid, valid for the headers only, + # or at least contains our domain as signing domain. + dbg("vbounce: [check_bounced_message_legitimacy] DKIM test succeeded, returning 1"); + return 1; + } + } else { + dbg("vbounce: [check_bounced_message_legitimacy] Skipping DKIM check, not activated"); + } + + if ($pms->{conf}->{use_xheader_b3d}) { + $self->_perform_bounced_message_legitimacy_xheader_tests($pms); + dbg("vbounce: [check_bounced_message_legitimacy] X-Header test result is ".$pms->{b3d_xheader_checks_result}.", returning this value"); + return ($pms->{b3d_xheader_checks_result} == $XHEADER_RESULT_SUCCESS ? 1 : 0); + } else { + dbg("vbounce: [check_bounced_message_legitimacy] Skipping X-Header check, not activated"); + } + + dbg("vbounce: [check_bounced_message_legitimacy] Returning 0"); + return 0; +} + +# Check whether a bounced message does not contain a legitimate original message, +# i.e. one containing either a valid DKIM signature or a predefined header. +# +# The function returns 1 if bounce legitimacy checking is enabled and +# the bounced message is illegitimate with high certainty, 0 otherwise. +sub check_bounced_message_illegitimacy { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{use_dkim_b3d} || $pms->{conf}->{use_xheader_b3d}) { + dbg("vbounce: [check_bounced_message_illegitimacy] Skipping B3D checks, not activated"); + return 0; + } + + dbg("vbounce: [check_bounced_message_illegitimacy] Started"); + + my $res = 0; + + if ($pms->{conf}->{use_dkim_b3d}) { + $self->_perform_bounced_message_legitimacy_dkim_tests($pms); + dbg("vbounce: [check_bounced_message_illegitimacy] DKIM test result is ".$pms->{b3d_dkim_checks_result}); + + if ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_INVALID) { + # Success if a DKIM signature is found and if it is completely valid, valid for the headers only, + # or at least contains our domain as signing domain. + dbg("vbounce: [check_bounced_message_illegitimacy] DKIM test succeeded, thus no illegitimacy, returning 0"); + return 0; + } elsif ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_UNKNOWN) { + # Maybe the reporting MTA did not include the headers, etc. + $res = 0; + } else { + # No DKIM signature was found even though original headers were included. + $res = 1; + } + } else { + dbg("vbounce: [check_bounced_message_illegitimacy] Skipping DKIM check, not activated"); + } + + if ($pms->{conf}->{use_xheader_b3d}) { + $self->_perform_bounced_message_legitimacy_xheader_tests($pms); + dbg("vbounce: [check_bounced_message_illegitimacy] X-Header test result is ".$pms->{b3d_xheader_checks_result}.", returning this value"); + return ($pms->{b3d_xheader_checks_result} == $XHEADER_RESULT_FAILURE ? 1 : 0); + } else { + dbg("vbounce: [check_bounced_message_illegitimacy] Skipping X-Header check, not activated"); + } + + dbg("vbounce: [check_bounced_message_illegitimacy] Returning $res"); + return $res; +} + +# Check whether a bounced message contains a valid DKIM signature. +# +# The function returns 1 if bounce legitimacy checking through DKIM is enabled and +# the bounced message contains a valid DKIM signature for its headers, 0 otherwise. +sub check_bounced_message_has_valid_dkim_signature { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{use_dkim_b3d}) { + dbg("vbounce: [check_bounced_message_has_valid_dkim_signature] Skipping check, not activated"); + return 0; + } + + dbg("vbounce: [check_bounced_message_has_valid_dkim_signature] Started"); + + $self->_perform_bounced_message_legitimacy_dkim_tests($pms); + + # Success if a DKIM signature is found and if it is completely valid or valid for the headers only + my $res = ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED + ? 1 : 0); + + dbg("vbounce: [check_bounced_message_has_valid_dkim_signature] Returning $res"); + return $res; +} + +# Check whether a bounced message contains a DKIM signature made using our domain. +# +# The function returns 1 if bounce legitimacy checking through DKIM is enabled and +# the bounced message contains our DKIM signature, 0 otherwise. +sub check_bounced_message_has_our_dkim_signature { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{use_dkim_b3d}) { + dbg("vbounce: [check_bounced_message_has_our_dkim_signature] Skipping B3D checks, not activated"); + return 0; + } + + dbg("vbounce: [check_bounced_message_has_our_dkim_signature] Started"); + + $self->_perform_bounced_message_legitimacy_dkim_tests($pms); + + # Success if a DKIM signature is found and if it is completely valid, valid for the headers only, + # or at least contains our domain as signing domain. + my $res = ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_VALID + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_ALTERED + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_INVALID + ? 1 : 0); + + dbg("vbounce: [check_bounced_message_has_our_dkim_signature] Returning $res"); + return $res; +} + +# Check whether no DKIM signature made using our domain can be found in a bounced message. +# +# The function returns 1 if bounce legitimacy checking through DKIM is enabled and +# the bounced message seems not to contain a DKIM signature, 0 otherwise. +sub check_bounced_message_has_no_dkim_signature { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{use_dkim_b3d}) { + dbg("vbounce: [check_bounced_message_has_no_dkim_signature] Skipping B3D checks, not activated"); + return 0; + } + + dbg("vbounce: [check_bounced_message_has_no_dkim_signature] Started"); + + $self->_perform_bounced_message_legitimacy_dkim_tests($pms); + + # Success if no DKIM signature was found + my $res = ($pms->{b3d_dkim_checks_result} == $DKIM_RESULT_NONE + || $pms->{b3d_dkim_checks_result} == $DKIM_RESULT_UNKNOWN + ? 1 : 0); + + dbg("vbounce: [check_bounced_message_has_no_dkim_signature] Returning $res"); + return $res; +} + +# Check whether a bounced message contains a special header/value combination. +# +# The function returns 1 if header based bounce legitimacy checking is enabled and +# the bounced message contains the header with the specified value, 0 otherwise. +sub check_bounced_message_has_valid_x_header { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{use_xheader_b3d}) { + dbg("vbounce: [check_bounced_message_has_valid_x_header] Skipping B3D checks, not activated"); + return 0; + } + + dbg("vbounce: [check_bounced_message_has_valid_x_header] Started"); + + $self->_perform_bounced_message_legitimacy_xheader_tests($pms); + + my $res = ($pms->{b3d_xheader_checks_result} ? 1 : 0); + + dbg("vbounce: [check_bounced_message_has_valid_x_header] Returning $res"); + return $res; +} + +sub _perform_bounced_message_legitimate_tests { + my ($self, $pms) = @_; + + $self->_perform_bounced_message_legitimacy_dkim_tests($pms); + $self->_perform_bounced_message_legitimacy_xheader_tests($pms); +} + +sub _perform_bounced_message_legitimacy_dkim_tests { + my ($self, $pms) = @_; + + if ($pms->{conf}->{use_dkim_b3d} && ! $self->{'dkim_module_loaded'}) { + if (eval { require Mail::DKIM::Verifier; }) { + $self->{'dkim_module_loaded'} = 1; + } else { + $pms->{conf}->{use_dkim_b3d} = 0; + dbg("vbounce: DKIM module not found, DKIM check is disabled!"); + } + } + + if ($pms->{conf}->{use_dkim_b3d}) { + if (! $pms->{b3d_dkim_checks_run}) { + $pms->{b3d_dkim_checks_run} = 1; + $pms->{b3d_dkim_checks_result} = $self->_check_bounced_message_legitimacy_by_dkim($pms); + dbg("vbounce: [_perform_bounced_message_legitimacy_dkim_tests] DKIM test performed, resulted in ".$pms->{b3d_dkim_checks_result}); + } else { + dbg("vbounce: [_perform_bounced_message_legitimacy_dkim_tests] DKIM test already performed, skipping"); + } + } else { + dbg("vbounce: [_perform_bounced_message_legitimacy_dkim_tests] DKIM test not enabled, skipping"); + } +} + +sub _perform_bounced_message_legitimacy_xheader_tests { + my ($self, $pms) = @_; + + if ($pms->{conf}->{use_xheader_b3d}) { + if (! $pms->{b3d_xheader_checks_run}) { + $pms->{b3d_xheader_checks_run} = 1; + $pms->{b3d_xheader_checks_result} = $self->_check_bounced_message_legitimacy_by_xheader($pms); + dbg("vbounce: [_perform_bounced_message_legitimacy_xheader_tests] X-header test performed, resulted in ".$pms->{b3d_xheader_checks_result}); + } else { + dbg("vbounce: [_perform_bounced_message_legitimacy_xheader_tests] X-header test already performed, skipping"); + } + } else { + dbg("vbounce: [_perform_bounced_message_legitimacy_xheader_tests] X-header test not enabled, skipping"); + } +} + +# Check a message's legitimacy by reconstruction the original message +# and checking it for a valid DKIM signature. +# +# Returns $DKIM_RESULT_VALID if a valid DKIM signature is found, +# $DKIM_RESULT_ALTERED if a valid DKIM signature is found for the headers, +# $DKIM_RESULT_INVALID if an invalid DKIM signature is found that matches the sender's domain, +# $DKIM_RESULT_OTHER if any is found that does not match the sender's domain, +# $DKIM_RESULT_NONE if no DKIM signature is found, but a message-id header is found, +# $DKIM_RESULT_UNKNOWN otherwise. +sub _check_bounced_message_legitimacy_by_dkim { + my ($self, $pms) = @_; + +### Allow no config value as *-pattern ### +# unless ($pms->{conf}->{b3d_dkim_domain}) { +# warn("vbounce: Missing 'b3d_dkim_domain' configuration parameter, aborting DKIM test"); +# return $DKIM_RESULT_UNKNOWN; +# } + + my $res = $DKIM_RESULT_UNKNOWN; + + my $dkim = Mail::DKIM::Verifier->new_object(); + my $original = $self->_extract_original_message($pms->get_message()); + + # If we cannot get the original message, return "undecided". + unless ($original) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Could not get original message, returning -1"); + return $DKIM_RESULT_UNKNOWN; + } + + my $m = 0; + my $h = 0; + + foreach my $line (split('(?:\015|\012|\015\012)', $original)) { + if ($line =~ qr/DKIM-Signature:/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Found DKIM signature"); + $h = 1; + last; + } + if ($line =~ qr/Message-Id:/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Found Message-ID header"); + $m = 1; + } + } + + # Consider bounce illegitimate if the original message contains no DKIM-signature, but a message-id header. + if ($m && ! $h) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Message-ID but no DKIM signature, returning 0"); + return $DKIM_RESULT_NONE; + } + + # Consider bounce undecidable if the original message contains no DKIM-signature and no message-id header. + if (! $h) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] No DKIM signature and no Message-ID, returning -1"); + return $DKIM_RESULT_UNKNOWN; + } + + # Reformat the message to meet the DKIM module's expectancies, + # and pass the message to the module line by line. + foreach (split('(?:\015|\012|\015\012)', $original)) { +# chomp; + $dkim->PRINT("$_\015\012"); + } + + $dkim->CLOSE(); + + # Check the signature. + my $sigmatch = 1; + if ($dkim->result() ne 'none') { #$dkim->signature() + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Found ".($#{$dkim->{signatures}}+1)." DKIM signature(s)"); + my $signature = ${$dkim->{signatures}}[0]; + $sigmatch = 0; + + if (scalar keys %{$pms->{conf}->{b3d_dkim_domain}} > 0) { + foreach my $dom (values %{$pms->{conf}->{b3d_dkim_domain}}) { + my $domain = $dom.'$'; + + if ($signature->domain() =~ qr/$domain/) { + $sigmatch = 1; last; + } + } + } else { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] No domains specified, accepting any signature"); + } + + if ($sigmatch) { #$dkim->signature + if ($dkim->result() eq 'pass') { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM signature is valid"); + $res = $DKIM_RESULT_VALID; + } elsif ($dkim->result() eq 'fail' && $dkim->result_detail() =~ /body has been altered/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM signature is valid for headers"); + $res = $DKIM_RESULT_ALTERED; + } else { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM signature is not valid (".$dkim->result_detail().")"); + $res = $DKIM_RESULT_INVALID; + } + } else { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] DKIM domain (".$signature->domain(). + ") does not match any of the user domains (".join(', ', values %{$pms->{conf}->{b3d_dkim_domain}}).")"); + $res = $DKIM_RESULT_OTHER; + } + } else { + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] No DKIM signature found"); + $res = $DKIM_RESULT_NONE; + } + + dbg("vbounce: [_check_bounced_message_legitimacy_by_dkim] Result of DKIM check is $res"); + return $res; +} + +# Extract the original message (or just it headers) from the bounce, if possible. +# Returns an empty string if the original message cannot be extracted. +sub _extract_original_message { + my ($self, $msg) = @_; + + my $ctype = $msg->get_header('Content-Type'); + my $body = ''; + + # If we have a multipart message, return the first rfc822-headers part + if ($ctype && $ctype =~ /^multipart\//i) { + dbg("vbounce: [_extract_original_message] Multipart message"); + my @parts = $msg->find_parts('message/rfc822'); + @parts = $msg->find_parts('text/rfc822-headers') if $#parts == -1; + + if ($#parts >= 0) { + my $part = $parts[0]; + dbg("vbounce: [_extract_original_message] Found ".($#parts + 1)." text/rfc822-headers part(s)"); +# dbg("vbounce: [_extract_original_message] Body parts: ".(join(", ", keys %{${$part->{body_parts}}[0]}))."\n"); + $body = ${$part->{body_parts}}[0]->{pristine_headers}; + } + } + + # Otherwise, return everything that looks like a header + if (! $body) { + $body = $msg->get_body(); + my @b = (); + my $h = 0; + + for (my $i = 0; $i <= $#{$body}; $i++) { + if ($body->[$i] =~ /^([\w-]+):\s+/) { + push(@b, $body->[$i]); + $h = 1; + } elsif ($h && $body->[$i] =~ /^\s+\S/) { + push(@b, $body->[$i]); + } else { + $h = 0; + } + } + + $body = join("\n", @b); + dbg("vbounce: [_extract_original_message] Extracted $#b header lines from bounce body"); + } + + dbg("vbounce: [_extract_original_message] Returned body is not empty: ".($body ne '')); + return $body; +} + +# This function's code is mainly copied from check_whitelist_bounce_relays. +# +# The function checks whether the original message's headers contain a specific legitimacy-header +# (see b3d_uses_x_header, b3d_x_header_value and b3d_x_header). +# It returns $XHEADER_RESULT_SUCCESS if the legitimacy-header is found, +# $XHEADER_RESULT_FAILURE if a message-id header is found, but the legitimacy-header is not found, +# $XHEADER_RESULT_UNKNOWN otherwise. +sub _check_bounced_message_legitimacy_by_xheader { + my ($self, $pms) = @_; + + unless ($pms->{conf}->{b3d_x_header}) { + warn("vbounce: Missing 'b3d_x_header' configuration parameter, aborting header test"); + return $XHEADER_RESULT_UNKNOWN; + } + + unless ($pms->{conf}->{b3d_x_header_value} || scalar keys %{$pms->{conf}->{b3d_x_header_domain_value}} >= 0) { + warn("vbounce: Missing 'b3d_x_header_value' and 'b3d_x_header_domain_value' configuration parameter, aborting header test"); + return $XHEADER_RESULT_UNKNOWN; + } + + my $body = $pms->get_decoded_stripped_body_text_array(); + my $recipient = $pms->get('ToCc:addr'); + my @keys = (); + + foreach my $dom (keys %{$pms->{conf}->{b3d_x_header_domain_value}}) { + my $domptrn = $dom.'$'; + next unless $recipient =~ qr/$domptrn/; + push(@keys, $pms->{conf}->{b3d_x_header}.':\s+'.$pms->{conf}->{b3d_x_header_domain_value}->{$dom}); + } + + push(@keys, map { $pms->{conf}->{b3d_x_header}.':\s+'.$_ } split('\s+', $pms->{conf}->{b3d_x_header_value})); + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Testing for one of the following keys: ".join(', ', @keys)); + + my $m = 0; + my $line = ''; + my $key = ''; + + # Check the plain-text body for our header, first. + foreach $line (@{$body}) { + # It's a good bounce if it has our header. + foreach $key (@keys) { + if ($line =~ qr/$key/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found pattern '$key', returning $XHEADER_RESULT_SUCCESS"); + return $XHEADER_RESULT_SUCCESS; + } + } + + if ($line =~ qr/Message-Id:/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found Message-ID during first scan"); + $m = 1; + } + } + + # If we found a message id header, but not the legitimacy header, consider the bounce illegitimate. + if ($m) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found Message-ID but not special header, returning $XHEADER_RESULT_FAILURE"); + return $XHEADER_RESULT_FAILURE; + } + + # now check any "message/anything" attachment MIME parts, too. + # don't use the more efficient find_parts() method until bug 5331 is + # fixed, otherwise we'll miss some messages due to their MIME structure + + my $pristine = $pms->{msg}->get_pristine(); + # skip past the headers + my $foundnlnl = 0; + foreach $line ($pristine =~ /^(.*)$/gm) { + if ($line =~ /^$/) { + $foundnlnl = 1; + } + + # and now through the pristine body + if ($foundnlnl) { + # It's a good bounce if it has our header.<> + foreach $key (@keys) { + if ($line =~ qr/$key/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found pattern '$key', returning $XHEADER_RESULT_SUCCESS"); + return $XHEADER_RESULT_SUCCESS; + } + } + + if ($line =~ qr/^.{0,3}Message-Id:/i) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Found Message-ID during second scan"); + $m = 1; + } + } + } + + # If we can't find the end of the headers, return "undecided". + unless ($foundnlnl) { + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Could not find original message body, returning $XHEADER_RESULT_UNKNOWN"); + return $XHEADER_RESULT_UNKNOWN; + } + + # If we found a message id header, but not our header, consider the bounce illegitimate. + dbg("vbounce: [_check_bounced_message_legitimacy_by_xheader] Returning ".($m ? $XHEADER_RESULT_FAILURE : $XHEADER_RESULT_UNKNOWN)); + return ($m ? $XHEADER_RESULT_FAILURE : $XHEADER_RESULT_UNKNOWN); +} + 1; __DATA__