Mailing List Archive

svn commit: r487146 - in /spamassassin/trunk: lib/Mail/SpamAssassin/Message/Metadata/Received.pm lib/Mail/SpamAssassin/Message/Node.pm lib/Mail/SpamAssassin/PerMsgStatus.pm lib/Mail/SpamAssassin/Plugin/SPF.pm t/data/nice/spf3-received-spf t/spf.t
Author: dos
Date: Thu Dec 14 01:21:45 2006
New Revision: 487146

URL: http://svn.apache.org/viewvc?view=rev&rev=487146
Log:
bug 5239: use results from Received-SPF header where possible

- implements the ability to get a range of headers from get_all_headers()
using the received headers as index points

- adds "ALL-TRUSTED", "ALL-INTERNAL", "ALL-UNTRUSTED" and "ALL-EXTERNAL"
pseudo-headers, like the current "ALL" pseudo-header; these pseudo-headers
return all of the headers that were added by either trusted or internal
relays and all of the headers that might have been added by either
untrusted or external relays (most headers will have been added by
untrusted or external relays but there's nothing stopping the trusted or
internal relays from adding headers to the bottom of the message);
these should be handy for header rules... almost everything that currently
uses "ALL" should probably use "ALL-EXTERNAL" or "ALL-UNTRUSTED"

- implements support for using the results found in "Received-SPF" headers
found in the message; only headers that could have been added by internal
relays are used so this is not susceptible to header forgery;
"Received-SPF" header results are used by default, there's an option to
disable their use; support for both the new and old style of
"Received-SPF" headers is included; the new headers support both "mfrom"
and "helo" results, the old ones only support "mfrom" AFAIK

- added tests to t/spf.t and a new test message to test that the
"Received-SPF" headers are only used when they should be (when they're
found to be added by an internal relay) and that they're used correctly

- slight improvement to the pattern that skips helo's that are IPs and not
domain names


Added:
spamassassin/trunk/t/data/nice/spf3-received-spf
- copied, changed from r485934, spamassassin/trunk/t/data/nice/spf3
Modified:
spamassassin/trunk/lib/Mail/SpamAssassin/Message/Metadata/Received.pm
spamassassin/trunk/lib/Mail/SpamAssassin/Message/Node.pm
spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm
spamassassin/trunk/t/spf.t

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Message/Metadata/Received.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Message/Metadata/Received.pm?view=diff&rev=487146&r1=487145&r2=487146
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Message/Metadata/Received.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Message/Metadata/Received.pm Thu Dec 14 01:21:45 2006
@@ -73,6 +73,9 @@

$self->{num_relays_unparseable} = 0;

+ $self->{last_trusted_relay_index} = -1; # last counting from the top,
+ $self->{last_internal_relay_index} = -1; # first in time
+
# now figure out what relays are trusted...
my $trusted = $permsgstatus->{main}->{conf}->{trusted_networks};
my $internal = $permsgstatus->{main}->{conf}->{internal_networks};
@@ -117,7 +120,11 @@
}

# undefined or 0 means there's no result, so goto the next header
- next unless $relay;
+ unless ($relay) {
+ $self->{last_trusted_relay_index}++ if $in_trusted;
+ $self->{last_internal_relay_index}++ if $in_internal;
+ next;
+ }

# hack for qmail-scanner, as described above; add in the saved
# metadata
@@ -216,6 +223,7 @@
if ($in_trusted) {
push (@{$self->{relays_trusted}}, $relay);
$self->{allow_fetchmail_markers} = 1;
+ $self->{last_trusted_relay_index}++;
} else {
push (@{$self->{relays_untrusted}}, $relay);
$self->{allow_fetchmail_markers} = 0;
@@ -223,6 +231,7 @@

if ($in_internal) {
push (@{$self->{relays_internal}}, $relay);
+ $self->{last_internal_relay_index}++;
} else {
push (@{$self->{relays_external}}, $relay);
}

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Message/Node.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Message/Node.pm?view=diff&rev=487146&r1=487145&r2=487146
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Message/Node.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Message/Node.pm Thu Dec 14 01:21:45 2006
@@ -652,6 +652,14 @@
return the raw headers, and the second parameter (optional) is whether
or not to include the mbox separator.

+The third and fourth parameters (optional) define the first and last
+values, respectively, of an index range of Received headers. Both of
+the Received headers specified by the indexes and the headers found
+between these Received headers will be returned, in order. Use undef
+as the first index value to start at the first header (possibly before
+the first Received header). Use undef as the last index value to end
+at the last header (probably after the last received header).
+
If get_all_header() is called in an array context, an array will be
returned with each header entry in a different element. In a scalar
context, the headers are returned in a single scalar.
@@ -660,17 +668,30 @@

# build it and it will not bomb
sub get_all_headers {
- my ($self, $raw, $include_mbox) = @_;
+ my ($self, $raw, $include_mbox, $start_rcvd_index, $end_rcvd_index) = @_;
$raw ||= 0;
$include_mbox ||= 0;
+ $start_rcvd_index = -1 unless defined $start_rcvd_index;

my @lines = ();

# precalculate destination positions based on order of appearance
my $i = 0;
+ my $lines_skipped = 0;
+ my $cur_rcvd_index = -1;
my %locations;
+
for my $k (@{$self->{header_order}}) {
- push(@{$locations{lc($k)}}, $i++);
+ last if (defined $end_rcvd_index && $end_rcvd_index <= $cur_rcvd_index);
+ my $name = lc($k);
+ $cur_rcvd_index++ if ($name eq 'received');
+ if ($cur_rcvd_index < $start_rcvd_index) {
+ push(@{$locations{$name}}, -1); # indicate we skipped the header
+ $lines_skipped++;
+ $i++;
+ next;
+ }
+ push(@{$locations{$name}}, $i++);
}

# process headers in order of first appearance
@@ -681,8 +702,10 @@
{
# get all same-name headers and poke into correct position
my $positions = $locations{$name};
- for my $contents ($self->get_header($name, $raw)) {
+ INSTANCE: for my $contents ($self->get_header($name, $raw)) {
my $position = shift @{$positions};
+ last INSTANCE unless defined $position;
+ next if $position == -1; # any headers we skipped above
$size += length($name) + length($contents) + 2;
if ($size > MAX_HEADER_LENGTH) {
$self->{'truncated_header'} = 1;
@@ -691,6 +714,9 @@
$lines[$position] = $self->{header_order}->[$position] . ": $contents";
}
}
+
+ # remove these, they'll be undefined if we skipped over them
+ splice @lines, 0, $lines_skipped if $lines_skipped;

# skip undefined lines if we truncated
@lines = grep { defined $_ } @lines if $self->{'truncated_header'};

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm?view=diff&rev=487146&r1=487145&r2=487146
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm Thu Dec 14 01:21:45 2006
@@ -1324,7 +1324,8 @@
relays_untrusted relays_untrusted_str num_relays_untrusted
relays_internal relays_internal_str num_relays_internal
relays_external relays_external_str num_relays_external
- num_relays_unparseable
+ num_relays_unparseable last_trusted_relay_index
+ last_internal_relay_index
))
{
$self->{$item} = $self->{msg}->{metadata}->{$item};
@@ -1416,6 +1417,21 @@

=item C<ALL> can be used to mean the text of all the message's headers.

+=item C<ALL-TRUSTED> can be used to mean the text of all the message's headers
+that could only have been added by trusted relays.
+
+=item C<ALL-INTERNAL> can be used to mean the text of all the message's headers
+that could only have been added by internal relays.
+
+=item C<ALL-UNTRUSTED> can be used to mean the text of all the message's
+headers that may have been added by untrusted relays. To make this
+pseudo-header more useful for header rules the 'Received' header that was added
+by the last trusted relay is included, even though it can be trusted.
+
+=item C<ALL-EXTERNAL> can be used to mean the text of all the message's headers
+that may have been added by external relays. Like C<ALL-UNTRUSTED> the
+'Received' header added by the last internal relay is included.
+
=item C<ToCc> can be used to mean the contents of both the 'To' and 'Cc'
headers.

@@ -1457,6 +1473,36 @@
# ALL: entire raw headers
if ($request eq 'ALL') {
$result = $self->{msg}->get_all_headers(1);
+ }
+ # ALL-TRUSTED: entire trusted raw headers
+ elsif ($request eq 'ALL-TRUSTED') {
+ # if we didn't find any trusted relays, none of the headers are trusted
+ return if $self->{last_trusted_relay_index} == -1;
+ $result = $self->{msg}->get_all_headers(1, 0, undef,
+ $self->{last_trusted_relay_index}+1);
+ }
+ # ALL-INTERNAL: entire internal raw headers
+ elsif ($request eq 'ALL-INTERNAL') {
+ # if we didn't find any internal relays, none of the headers are internal
+ return if $self->{last_internal_relay_index} == -1;
+ $result = $self->{msg}->get_all_headers(1, 0, undef,
+ $self->{last_internal_relay_index}+1);
+ }
+ # ALL-UNTRUSTED: entire untrusted raw headers
+ elsif ($request eq 'ALL-UNTRUSTED') {
+ # if we didn't find any trusted relays get all the headers, otherwise get
+ # all the headers after the last trusted relay
+ my $start_index = ($self->{last_trusted_relay_index} == -1 ? undef :
+ $self->{last_trusted_relay_index} + 1);
+ $result = $self->{msg}->get_all_headers(1, 0, $start_index);
+ }
+ # ALL-EXTERNAL: entire external raw headers
+ elsif ($request eq 'ALL-EXTERNAL') {
+ # if we didn't find any internal relays get all the headers, otherwise get
+ # all the headers after the last internal relay
+ my $start_index = ($self->{last_internal_relay_index} == -1 ? undef :
+ $self->{last_internal_relay_index} + 1);
+ $result = $self->{msg}->get_all_headers(1, 0, $start_index);
}
# EnvelopeFrom: the SMTP MAIL FROM: address
elsif ($request eq 'EnvelopeFrom') {

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm?view=diff&rev=487146&r1=487145&r2=487146
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm Thu Dec 14 01:21:45 2006
@@ -169,6 +169,51 @@
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
});

+=item ignore_received_spf_header (0|1) (default: 0)
+
+By default, to avoid unnecessary DNS lookups, the plugin will try to use the
+SPF results found in any C<Received-SPF> headers it finds in the message that
+could only have been added by an internal relay.
+
+Set this option to 1 to ignore any C<Received-SPF> headers present and to have
+the plugin perform the SPF check itself.
+
+Note that unless the plugin finds an C<identity=helo>, or some unsupported
+identity, it will assume that the result is a mfrom SPF check result. The
+only identities supported are C<mfrom>, C<mailfrom> and C<helo>.
+
+=cut
+
+ push(@cmds, {
+ setting => 'ignore_received_spf_header',
+ is_admin => 1,
+ default => 0,
+ type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+ });
+
+=item use_newest_received_spf_header (0|1) (default: 0)
+
+By default, when using C<Received-SPF> headers, the plugin will attempt to use
+the oldest (bottom most) C<Received-SPF> headers, that were added by internal
+relays, that it can parse results from since they are the most likely to be
+accurate. This is done so that if you have an incoming mail setup where one
+of your primary MXes doesn't know about a secondary MX (or your MXes don't
+know about some sort of forwarding relay that SA considers trusted+internal)
+but SA is aware of the actual domain boundary (internal_networks setting) SA
+will use the results that are most accurate.
+
+Use this option to start with the newest (top most) C<Received-SPF> headers,
+working downwards until results are successfully parsed.
+
+=cut
+
+ push(@cmds, {
+ setting => 'use_newest_received_spf_header',
+ is_admin => 1,
+ default => 0,
+ type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+ });
+
$conf->{parser}->register_commands(\@cmds);
}

@@ -242,6 +287,100 @@
sub _check_spf {
my ($self, $scanner, $ishelo) = @_;

+ # we can re-use results from any *INTERNAL* Received-SPF header in the message...
+ # we can't use results from trusted but external hosts since (i) spf checks are
+ # supposed to be done "on the domain boundary", (ii) even if an external header
+ # has a result that matches what we would get, the check was probably done on a
+ # different envelope (like the apache.org list servers checking the ORCPT and
+ # then using a new envelope to send the mail from the list) and (iii) if the
+ # checks are being done right and the envelope isn't being changed it's 99%
+ # likely that the trusted+external host really should be defined as part of your
+ # internal network
+ if ($scanner->{conf}->{ignore_received_spf_header}) {
+ dbg("spf: ignoring any Received-SPF headers from internal hosts, by admin setting");
+ } elsif ($scanner->{checked_for_received_spf_header}) {
+ dbg("spf: already checked for Received-SPF headers, proceeding with DNS based checks");
+ } else {
+ $scanner->{checked_for_received_spf_header} = 1;
+ dbg("spf: checking to see if the message has a Received-SPF header that we can use");
+ # if we didn't find any internal relays, none of the headers are internal
+ my @internal_hdrs;
+ unless ($scanner->{last_internal_relay_index} == -1) { # -1 means there are none
+ @internal_hdrs = $scanner->{msg}->get_all_headers(1, 0, undef,
+ $scanner->{last_internal_relay_index}+1);
+ unless ($scanner->{conf}->{use_newest_received_spf_header}) {
+ @internal_hdrs = reverse(@internal_hdrs);
+ } else {
+ dbg("spf: starting with the newest Received-SPF headers first");
+ }
+ }
+ # look for the LAST (earliest in time) header, it'll be the most accurate
+ foreach my $hdr (@internal_hdrs) {
+ if ($hdr =~ /^received-spf: /i) {
+ dbg("spf: found a Received-SPF header added by an internal host: $hdr");
+
+ # old version:
+ # Received-SPF: pass (herse.apache.org: domain of spamassassin@dostech.ca
+ # designates 69.61.78.188 as permitted sender)
+
+ # new version:
+ # Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use
+ # 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))
+ # receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";
+ # helo=smtp.dostech.net; client-ip=69.61.78.188
+
+ # Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca'
+ # in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo;
+ # helo=dostech.ca; client-ip=69.61.78.188
+
+ # http://www.openspf.org/RFC_4408#header-field
+ # wtf - for some reason something is sticking an extra space between the header name and field value
+ if ($hdr =~ /^received-spf:\s+(pass|neutral|(?:soft)?fail|none) (?:.* identity=(helo|m(?:ail)?from); )?/i) {
+ my $result = lc($1);
+
+ my $identity = ''; # we assume it's a mfrom check if we can't tell otherwise
+ if (defined $2) {
+ $identity = lc($2);
+ if ($identity eq 'mfrom' || $identity eq 'mailfrom') {
+ next if $scanner->{spf_checked};
+ $identity = '';
+ } elsif ($identity eq 'helo') {
+ next if $scanner->{spf_helo_checked};
+ $identity = 'helo_';
+ } else {
+ dbg("spf: found unknown identity value, cannot use: $identity");
+ next; # try the next Received-SPF header, if any
+ }
+ } else {
+ next if $scanner->{spf_checked};
+ }
+
+ # we'd set these if we actually did the check
+ $scanner->{"spf_${identity}checked"} = 1;
+ $scanner->{"spf_${identity}pass"} = 0;
+ $scanner->{"spf_${identity}neutral"} = 0;
+ $scanner->{"spf_${identity}fail"} = 0;
+ $scanner->{"spf_${identity}softfail"} = 0;
+ $scanner->{"spf_${identity}failure_comment"} = undef;
+
+ # and the result
+ $scanner->{"spf_${identity}${result}"} = 1;
+ dbg("spf: re-using ".($identity ? 'helo' : 'mfrom')." result from Received-SPF header: $result");
+
+ # if we've got *both* the mfrom and helo results we're done
+ return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
+
+ } else {
+ dbg("spf: could not parse result from existing Received-SPF header");
+ }
+ }
+ }
+ # we can return if we've found the one we're being asked to get
+ return if ( ($ishelo && $scanner->{spf_helo_checked}) ||
+ (!$ishelo && $scanner->{spf_checked}) );
+ }
+
+ # abort if dns or an spf module isn't available
return unless $scanner->is_dns_available();
return if $self->{no_spf_module};

@@ -350,7 +489,7 @@

# this test could probably stand to be more strict, but try to test
# any invalid HELO hostname formats with a header rule
- if ($ishelo && ($helo =~ /^\d+\.\d+\.\d+\.\d+$/ || $helo =~ /^[^.]+$/)) {
+ if ($ishelo && ($helo =~ /^[\[!]?\d+\.\d+\.\d+\.\d+[\]!]?$/ || $helo =~ /^[^.]+$/)) {
dbg("spf: cannot check HELO of '$helo', skipping");
return;
}

Copied: spamassassin/trunk/t/data/nice/spf3-received-spf (from r485934, spamassassin/trunk/t/data/nice/spf3)
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/data/nice/spf3-received-spf?view=diff&rev=487146&p1=spamassassin/trunk/t/data/nice/spf3&r1=485934&p2=spamassassin/trunk/t/data/nice/spf3-received-spf&r2=487146
==============================================================================
--- spamassassin/trunk/t/data/nice/spf3 (original)
+++ spamassassin/trunk/t/data/nice/spf3-received-spf Thu Dec 14 01:21:45 2006
@@ -1,10 +1,16 @@
Return-Path: <newsalerts-noreply@dnsbltest.spamassassin.org>
+X-Comment: Yeah, the Received-SPF headers make no sense, there just there to test that the SPF plugin will parse the results from them... the IPs and comments are bogus
Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.158]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [64.142.3.173]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
+Received-SPF: fail (dostech.ca: 69.61.78.188 is authorized to use 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca"; helo=smtp.dostech.net; client-ip=69.61.78.188
+Received-SPF: softfail (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca' in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo; helo=dostech.ca; client-ip=69.61.78.188
+Received-SPF: neutral (herse.apache.org: domain of spamassassin@dostech.ca designates 69.61.78.188 as permitted sender)
Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.155]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.156]) by dnsbltest.spamassassin.org (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
Received: from dnsbltest.spamassassin.org (dnsbltest.spamassassin.org [65.214.43.157]) by amgod.boxhost.net (Postfix) with SMTP id B9B2931016D for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 18:18:49 +0000 (GMT)
Received: by proxy.google.com with SMTP id so1951389 for <jm-google-news-alerts@jmason.org>; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
+
+
Received: by abbulk2 with SMTP id mr733125; Tue, 10 Feb 2004 10:14:01 -0800 (PST)
Message-ID: <1076436841.67074.8fa05ccdc458abe5.1446041b@persist.google.com>
Date: Tue, 10 Feb 2004 10:14:01 -0800 (PST)

Modified: spamassassin/trunk/t/spf.t
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/spf.t?view=diff&rev=487146&r1=487145&r2=487146
==============================================================================
--- spamassassin/trunk/t/spf.t (original)
+++ spamassassin/trunk/t/spf.t Thu Dec 14 01:21:45 2006
@@ -21,7 +21,7 @@
BEGIN {

# some tests are run once for each SPF module, others are only run once
- plan tests => (DO_RUN ? (HAS_SPFQUERY && HAS_MAILSPF ? 90 : (HAS_SPFQUERY ? 46 : 46)) : 0);
+ plan tests => (DO_RUN ? (HAS_SPFQUERY && HAS_MAILSPF ? 98 : (HAS_SPFQUERY ? 54 : 54)) : 0);

};

@@ -390,5 +390,91 @@
);

sarun ("-t < data/nice/spf1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the Received-SPF headers shouldn't be used in this test
+
+tstprefs("
+ clear_trusted_networks
+ clear_internal_networks
+ trusted_networks 65.214.43.158
+ internal_networks 65.214.43.158
+ always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+ q{ SPF_HELO_PASS }, 'helo_pass',
+ q{ SPF_PASS }, 'pass',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the Received-SPF headers shouldn't be used in this test
+
+tstprefs("
+ clear_trusted_networks
+ clear_internal_networks
+ trusted_networks 65.214.43.158 64.142.3.173
+ internal_networks 65.214.43.158 64.142.3.173
+ always_trust_envelope_sender 1
+ ignore_received_spf_header 1
+");
+
+%anti_patterns = ();
+%patterns = (
+ q{ SPF_HELO_FAIL }, 'helo_fail_ignore_header',
+ q{ SPF_FAIL }, 'fail_ignore_header',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the bottom 2 Received-SPF headers should be used in this test
+
+tstprefs("
+ clear_trusted_networks
+ clear_internal_networks
+ trusted_networks 65.214.43.158 64.142.3.173
+ internal_networks 65.214.43.158 64.142.3.173
+ always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+ q{ SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
+ q{ SPF_NEUTRAL }, 'neutral_from_header',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# test usage of Received-SPF headers added by internal relays
+# the top 2 Received-SPF headers should be used in this test
+
+tstprefs("
+ clear_trusted_networks
+ clear_internal_networks
+ trusted_networks 65.214.43.158 64.142.3.173
+ internal_networks 65.214.43.158 64.142.3.173
+ use_newest_received_spf_header 1
+ always_trust_envelope_sender 1
+");
+
+%anti_patterns = ();
+%patterns = (
+ q{ SPF_HELO_SOFTFAIL }, 'helo_softfail_from_header',
+ q{ SPF_FAIL }, 'fail_from_header',
+);
+
+sarun ("-t < data/nice/spf3-received-spf", \&patterns_run_cb);
ok_all_patterns();