svn commit: r1932298 - in spamassassin/trunk: . lib/Mail/SpamAssassin/Header lib/Mail/SpamAssassin/Plugin t t/data/nice

[email protected]
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <177347609111.2587576.9200729887819580524@svn02-us-east.apache.org>
Author: fkento
Date: Sat Mar 14 08:14:50 2026
New Revision: 1932298

Log:
Rewrite AuthRes plugin to use new Header::AuthenticationResults parser

  The AuthRes plugin previously hand-rolled a fragile A-R header parser that
  died (discarding the entire header) on unknown methods or properties.
  Real-world headers from servers like Mail::Milter::Authentication include
  extended properties (policy.published-domain-policy) and non-standard
  methods (x-tls) that caused valid results to be skipped.

  Create Mail::SpamAssassin::Header::AuthenticationResults, inheriting from
  ParameterHeader, which handles the structural parsing (comment stripping,
  semicolon tokenization, quoted strings, line unfolding). The subclass adds
  A-R-specific secondary parsing of method results and ptype.property=value
  pairs.

  Simplify the AuthRes plugin to use the new class. Remove the hand-rolled
  parser (regex constants, skip_cfws, the old parse_authres), the method/
  property whitelists, and ARC handling (handled in other plugins). All
  parsed methods and properties now flow through to $pms->{authres_parsed}
  regardless of whether they appear in a whitelist, so check_authres_result
  works for any method present in the header.

  The $pms->{authres_parsed} and $pms->{authres_result} structures are
  preserved for compatibility with other plugins.

Added:
   spamassassin/trunk/lib/Mail/SpamAssassin/Header/AuthenticationResults.pm
   spamassassin/trunk/t/authentication_results.t
   spamassassin/trunk/t/authres_parser.t
   spamassassin/trunk/t/data/nice/authres_basic
   spamassassin/trunk/t/data/nice/authres_comments
   spamassassin/trunk/t/data/nice/authres_errors
   spamassassin/trunk/t/data/nice/authres_none
   spamassassin/trunk/t/data/nice/authres_props
   spamassassin/trunk/t/data/nice/authres_quoted
   spamassassin/trunk/t/data/nice/authres_quoted_eq
   spamassassin/trunk/t/data/nice/authres_realworld
   spamassassin/trunk/t/data/nice/authres_reason
   spamassassin/trunk/t/data/nice/authres_version
Modified:
   spamassassin/trunk/MANIFEST
   spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AuthRes.pm
   spamassassin/trunk/t/authres.t

Modified: spamassassin/trunk/MANIFEST
==============================================================================
--- spamassassin/trunk/MANIFEST	Sat Mar 14 04:20:03 2026	(r1932297)
+++ spamassassin/trunk/MANIFEST	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -51,6 +51,7 @@ lib/Mail/SpamAssassin/Dns.pm
 lib/Mail/SpamAssassin/DnsResolver.pm
 lib/Mail/SpamAssassin/GeoDB.pm
 lib/Mail/SpamAssassin/Header.pm
+lib/Mail/SpamAssassin/Header/AuthenticationResults.pm
 lib/Mail/SpamAssassin/Header/ParameterHeader.pm
 lib/Mail/SpamAssassin/HTML.pm
 lib/Mail/SpamAssassin/HTML/Color.pm
@@ -260,7 +261,9 @@ t/SATest.pl
 t/SATest.pm
 t/all_modules.t
 t/askdns.t
+t/authentication_results.t
 t/authres.t
+t/authres_parser.t
 t/autolearn.t
 t/autolearn_force.t
 t/autolearn_force_fail.t
@@ -370,6 +373,16 @@ t/data/nice/014
 t/data/nice/015
 t/data/nice/016
 t/data/nice/authres
+t/data/nice/authres_basic
+t/data/nice/authres_comments
+t/data/nice/authres_errors
+t/data/nice/authres_none
+t/data/nice/authres_props
+t/data/nice/authres_quoted
+t/data/nice/authres_quoted_eq
+t/data/nice/authres_realworld
+t/data/nice/authres_reason
+t/data/nice/authres_version
 t/data/nice/base64.txt
 t/data/nice/crlf-endings
 t/data/nice/dkim/AddedVtag_07

Added: spamassassin/trunk/lib/Mail/SpamAssassin/Header/AuthenticationResults.pm
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Header/AuthenticationResults.pm	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,211 @@
+# <@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>
+
+package Mail::SpamAssassin::Header::AuthenticationResults;
+use strict;
+use warnings FATAL => 'all';
+
+use Mail::SpamAssassin::Header::ParameterHeader;
+
+use parent qw(Mail::SpamAssassin::Header::ParameterHeader);
+
+my $QUOTED_STRING = qr/"((?:[^"\\]++|\\.)*+)"?/;
+
+=head1 NAME
+
+Mail::SpamAssassin::Header::AuthenticationResults - parser for Authentication-Results headers
+
+=head1 SYNOPSIS
+
+    my $ar = Mail::SpamAssassin::Header::AuthenticationResults->new($hdr_value);
+    print $ar->authserv_id();    # 'mx.example.com'
+    print $ar->version();        # 1
+    my @names = $ar->methods();  # ('spf', 'dkim', ...)
+    my @results = $ar->method('dkim'); # list of result hashes
+
+=head1 DESCRIPTION
+
+This class inherits from ParameterHeader to parse Authentication-Results
+header fields per RFC 8601.  ParameterHeader handles comment stripping,
+semicolon tokenization, quoted strings, and line unfolding.  This class
+adds A-R-specific secondary parsing of each method's value string.
+
+=head1 METHODS
+
+=over 4
+
+=item new($value)
+
+Creates a new instance, parsing the given raw header value.
+
+=cut
+
+sub new {
+    my ($class, $value) = @_;
+    my $self = $class->SUPER::new($value);
+    bless $self, $class;
+    $self->_parse_authserv();
+    $self->_parse_methods();
+    return $self;
+}
+
+=item authserv_id()
+
+Returns the authserv-id (first token of the header value), lowercased.
+
+=cut
+
+sub authserv_id { $_[0]->{authserv_id} }
+
+=item version()
+
+Returns the version number (default 1).
+
+=cut
+
+sub version { $_[0]->{version} }
+
+=item methods()
+
+Returns a list of method names that have results.
+
+=cut
+
+sub methods { keys %{$_[0]->{methods}} }
+
+=item method($name)
+
+Returns the result(s) for the given method name. Each result is a hash
+containing C<result>, C<reason>, and C<properties>.  In list context,
+returns all results; in scalar context, returns the first.
+
+=cut
+
+sub method {
+    my ($self, $name) = @_;
+    my $results = $self->{methods}{lc $name};
+    return unless $results;
+    return wantarray ? @$results : $results->[0];
+}
+
+sub _parse_authserv {
+    my ($self) = @_;
+    my $val = $self->value();
+    $val =~ s/^\s+|\s+$//g;
+    # Extract authserv-id: first token, optionally followed by version number
+    if ($val =~ /^(\S+)(?:\s+(\d+))?\s*$/) {
+        $self->{authserv_id} = lc($1);
+        $self->{version} = defined $2 ? $2 + 0 : 1;
+    } elsif (length $val) {
+        # Take just the first token
+        ($self->{authserv_id}) = $val =~ /^(\S+)/;
+        $self->{authserv_id} = lc($self->{authserv_id}) if defined $self->{authserv_id};
+        $self->{version} = 1;
+    } else {
+        $self->{authserv_id} = '';
+        $self->{version} = 1;
+    }
+}
+
+sub _parse_methods {
+    my ($self) = @_;
+    my %methods;
+
+    foreach my $param_name ($self->parameters()) {
+        my @values = $self->parameter($param_name);
+        foreach my $val (@values) {
+            my ($method, $parsed) = $self->_parse_method_value($param_name, $val);
+            push @{$methods{$method}}, $parsed if $parsed;
+        }
+    }
+
+    $self->{methods} = \%methods;
+}
+
+sub _parse_method_value {
+    my ($self, $name, $val) = @_;
+
+    # Clean up method name: strip version suffix ("dkim / 1" -> "dkim", "dkim/1" -> "dkim")
+    my $method = $name;
+    $method =~ s{\s*/\s*\d+\s*$}{};
+    $method = lc($method);
+
+    $val =~ s/^\s+|\s+$//g;
+    return () unless length $val;
+
+    local $_ = $val;
+
+    # Strip optional method version prefix ("/ 1 " at the start of value)
+    s{^\s*/\s*\d+\s+}{};
+
+    # Extract result (first word)
+    my $result;
+    if (s/^(\S+)\s*//) {
+        $result = lc($1);
+    } else {
+        return ();
+    }
+
+    my $reason = '';
+
+    # Extract optional reason="quoted" or reason=token
+    if (s/^reason\s*=\s*$QUOTED_STRING\s*//i) {
+        $reason = $1;
+    } elsif (s/^reason\s*=\s*(\S+)\s*//i) {
+        $reason = $1;
+    }
+
+    # Consume optional action=value
+    if (s/^action\s*=\s*$QUOTED_STRING\s*//i) {
+        # consumed
+    } elsif (s/^action\s*=\s*(\S+)\s*//i) {
+        # consumed
+    }
+
+    # Extract ptype.property=value pairs
+    my $properties = {};
+    while (length $_) {
+        # ptype.property=value
+        if (s/^([\w-]+)\.([\w-]+)\s*=\s*//) {
+            my ($ptype, $property) = (lc($1), lc($2));
+            my $pval;
+            if (s/^$QUOTED_STRING\s*//) {
+                $pval = $1;
+            } elsif (s/^(\S+)\s*//) {
+                $pval = $1;
+            } else {
+                next;
+            }
+            $properties->{$ptype}->{$property} = $pval;
+        } else {
+            # Skip unrecognized token
+            last unless s/^\S+\s*//;
+        }
+    }
+
+    return ($method, {
+        result     => $result,
+        reason     => $reason,
+        properties => $properties,
+    });
+}
+
+=back
+
+=cut
+
+1;

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AuthRes.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AuthRes.pm	Sat Mar 14 04:20:03 2026	(r1932297)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AuthRes.pm	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -40,6 +40,7 @@ package Mail::SpamAssassin::Plugin::Auth
 
 use Mail::SpamAssassin::Plugin;
 use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Header::AuthenticationResults;
 use strict;
 use warnings;
 # use bytes;
@@ -47,49 +48,6 @@ use re 'taint';
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-# list of valid methods and values
-# https://www.iana.org/assignments/email-auth/email-auth.xhtml
-# some others not in that list:
-#   dkim-atps=neutral
-#   dmarc=bestguesspass  (some microsoft stuff)
-my %method_result = (
-  'arc' => {'fail'=>1,'none'=>1,'pass'=>1},
-  'auth' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
-  'dkim' => {'fail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'temperror'=>1},
-  'dkim-adsp' => {'discard'=>1,'fail'=>1,'none'=>1,'nxdomain'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1,'unknown'=>1},
-  'dkim-atps' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1,'neutral'=>1},
-  'dmarc' => {'bestguesspass'=>1,'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
-  'dnswl' => {'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
-  'domainkeys' => {'fail'=>1,'neutral'=>1,'none'=>1,'permerror'=>1,'policy'=>1,'pass'=>1,'temperror'=>1},
-  'iprev' => {'fail'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
-  'rrvs' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1,'unknown'=>1},
-  'sender-id' => {'fail'=>1,'hardfail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'softfail'=>1,'temperror'=>1},
-  'smime' => {'fail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'temperror'=>1},
-  'spf' => {'fail'=>1,'hardfail'=>1,'neutral'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'policy'=>1,'softfail'=>1,'temperror'=>1},
-  'vbr' => {'fail'=>1,'none'=>1,'pass'=>1,'permerror'=>1,'temperror'=>1},
-);
-my %method_ptype_prop = (
-  'arc' => {'smtp' => {'remote-ip'=>1}, 'header' => {'oldest-pass'=>1}, 'arc' => {'chain'=>1}},
-  'auth' => {'smtp' => {'auth'=>1,'mailfrom'=>1}},
-  'dkim' => {'header' => {'d'=>1,'i'=>1,'b'=>1,'a'=>1,'s'=>1}},
-  'dkim-adsp' => {'header' => {'from'=>1}},
-  'dkim-atps' => {'header' => {'from'=>1}},
-  'dmarc' => {'header' => {'from'=>1}, 'policy' => {'dmarc'=>1}},
-  'dnswl' => {'dns' => {'zone'=>1,'sec'=>1}, 'policy' => {'ip'=>1,'txt'=>1}},
-  'domainkeys' => {'header' => {'d'=>1,'from'=>1,'sender'=>1}},
-  'iprev' => {'policy' => {'iprev'=>1}},
-  'rrvs' => {'smtp' => {'rcptto'=>1}},
-  'sender-id' => {'header' => {'*'=>1}},
-  'smime' => {'body' => {'smime-part'=>1,'smime-identifer'=>1,'smime-serial'=>1,'smime-issuer'=>1}},
-  'spf' => {'smtp' => {'mailfrom'=>1,'mfrom'=>1,'helo'=>1,'rcpttodomain'=>1}},
-  'vbr' => {'header' => {'md'=>1,'mv'=>1}},
-);
-      
-# Some MIME helpers
-my $QUOTED_STRING = qr/"((?:[^"\\]++|\\.)*+)"?/;
-my $TOKEN = qr/[^\s\x00-\x1f\x80-\xff\(\)\<\>\@\,\;\:\/\[\]\?\=\"]+/;
-my $ATOM = qr/[a-zA-Z0-9\@\!\#\$\%\&\\\'\*\+\-\/\=\?\^\_\`\{\|\}\~]+/;
-
 sub new {
   my ($class, $mailsa) = @_;
 
@@ -127,7 +85,7 @@ completely ignored (affects all module s
  all        = all above + all external
 
 Setting "all" is safe only if your MX servers filter properly all incoming
-A-R headers, and you use authres_trusted_authserv to match your authserv-id. 
+A-R headers, and you use authres_trusted_authserv to match your authserv-id.
 This is suitable for default OpenDKIM for example.  These settings might
 also be required if your filters do not insert A-R header to correct
 position above the internal Received header (some known offenders: OpenDKIM,
@@ -331,8 +289,8 @@ sub parsed_metadata {
   }
 
   foreach my $hdr (split(/^/m, $pms->get($nethdr))) {
-    if ($hdr =~ /^((?:Arc\-)?Authentication-Results):\s*(.+)/i) {
-      push @authres, [$1,$2];
+    if ($hdr =~ /^Authentication-Results:\s*(.+)/i) {
+      push @authres, $1;
     }
   }
 
@@ -342,9 +300,9 @@ sub parsed_metadata {
     return 0;
   }
 
-  foreach (@authres) {
+  foreach my $hdr (@authres) {
     eval {
-      $self->parse_authres($pms, $_->[0], $_->[1]);
+      $self->parse_authres($pms, $hdr);
     } or do {
       dbg("authres: skipping header, $@");
     }
@@ -353,10 +311,8 @@ sub parsed_metadata {
   $pms->{authres_result} = {};
   # Set $pms->{authres_result} info for all found methods
   # 'pass' will always win if multiple results
-  foreach my $method (keys %method_result) {
-    my $parsed = $pms->{authres_parsed}->{$method};
-    next if !$parsed;
-    foreach my $pref (@$parsed) {
+  foreach my $method (keys %{$pms->{authres_parsed}}) {
+    foreach my $pref (@{$pms->{authres_parsed}->{$method}}) {
       if (!$pms->{authres_result}->{$method} ||
             $pref->{result} eq 'pass')
       {
@@ -375,48 +331,16 @@ sub parsed_metadata {
 }
 
 sub parse_authres {
-  my ($self, $pms, $hdrname, $hdr) = @_;
-
-  dbg("authres: parsing $hdrname: $hdr");
+  my ($self, $pms, $hdr) = @_;
 
-  my $authserv;
-  my $version = 1;
-  my @methods;
-  my $arc_index;
+  dbg("authres: parsing Authentication-Results: $hdr");
 
-  local $_ = $hdr;
+  my $ar = Mail::SpamAssassin::Header::AuthenticationResults->new($hdr);
 
-  if ($hdrname =~ /^ARC-/i) {
-    if (!/\Gi\b/gcs) {
-      die("missing arc index: $hdr");
-    }
-    skip_cfws();
-    if (!/\G=/gcs) {
-      die("invalid arc index: ".substr($_, pos())."\n");
-    }
-    skip_cfws();
-    if (!/\G(\d+)/gcs) {
-      die("invalid arc index: ".substr($_, pos())."\n");
-    }
-    $arc_index = $1;
-    if ($arc_index < 1 || $arc_index > 50) {
-      die("invalid arc index: $arc_index\n");
-    }
-    skip_cfws();
-    if (!/\G;/gcs) {
-      die("missing delimiter: ".substr($_, pos())."\n");
-    }
-    skip_cfws();
-  }
-
-  # authserv-id
-  if (!/\G($TOKEN)/gcs) {
-    die("invalid authserv: ".substr($_, pos())."\n");
-  }
-  $authserv = lc($1);
+  my $authserv = $ar->authserv_id();
 
   # some invalid headers start with spf=foo etc, missing authserv-id
-  if (/\G=/gcs) {
+  if (!length($authserv) || $hdr =~ /^\s*\S+=/) {
     die("missing authserv: $hdr\n");
   }
 
@@ -429,193 +353,26 @@ sub parse_authres {
     die("ignored authserv: $authserv\n");
   }
 
-  # skip authserv version
-  skip_cfws();
-  if (/\G\d+/gcs) {
-    skip_cfws();
-  }
-
-  if (!/\G;/gcs) {
-    die("missing delimiter: ".substr($_, pos())."\n");
+  # Detect "none" via raw header
+  if ($hdr =~ /;\s*none\b/i) {
+    die("method none\n");
   }
-  skip_cfws();
-
-  while (pos() < length()) {
-    my ($method, $result);
-    my $reason = '';
-    my $props = {};
-
-    # some silly generators add duplicate authserv-id; here
-    if (/\G\Q${authserv}\E\s*;/gcs) {
-      skip_cfws();
-    }
-
-    # skip none method
-    if (/\Gnone\b/igcs) {
-      die("method none\n");
-    }
-
-    # method / version = result
-    if (!/\G([\w-]+)/gcs) {
-      die("invalid method: ".substr($_, pos())."\n");
-    }
-    $method = lc($1);
-    if (!exists $method_result{$method}) {
-      die("unknown method: $method: $hdr\n");
-    }
-    skip_cfws();
-    if (/\G\//gcs) {
-      skip_cfws();
-      if (!/\G\d+/gcs) {
-        die("invalid $method version: ".substr($_, pos())."\n");
-      }
-      $version = $1;
-      skip_cfws();
-    }
-    if (!/\G=/gcs) {
-      die("missing result for $method: ".substr($_, pos())."\n");
-    }
-    skip_cfws();
-    if (!/\G(\w+)/gcs) {
-      die("invalid result for $method: ".substr($_, pos())."\n");
-    }
-    $result = $1;
-    if (!exists $method_result{$method}{$result}) {
-      die("unknown result for $method: $result\n");
-    }
-    skip_cfws();
-
-    # reason = value
-    if (/\Greason\b/igcs) {
-      skip_cfws();
-      if (!/\G=/gcs) {
-        die("invalid reason: ".substr($_, pos())."\n");
-      }
-      skip_cfws();
-      if (!/\G$QUOTED_STRING|($TOKEN)/gcs) {
-        die("invalid reason: ".substr($_, pos())."\n");
-      }
-      $reason = defined $1 ? $1 : $2;
-      skip_cfws();
-    }
-
-    # action = value (some microsoft ARC stuff?)
-    if (/\Gaction\b/igcs) {
-      skip_cfws();
-      if (!/\G=/gcs) {
-        die("invalid action: ".substr($_, pos())."\n");
-      }
-      skip_cfws();
-      if (!/\G$QUOTED_STRING|$TOKEN/gcs) {
-        die("invalid action: ".substr($_, pos())."\n");
-      }
-      skip_cfws();
-    }
-
-    # ptype.property = value
-    while (pos() < length()) {
-      my ($ptype, $property, $value);
-
-      # no props?
-      if (/\G(?:;|$)/gcs) {
-        skip_cfws();
-        last;
-      }
-
-      # ptype
-      if (!/\G([\w-]+)/gcs) {
-        die("invalid ptype: ".substr($_,pos())."\n");
-      }
-      $ptype = lc($1);
-      if (!exists $method_ptype_prop{$method}{$ptype}) {
-        die("unknown ptype: $method/$ptype\n");
-      }
-      skip_cfws();
-
-      # dot
-      if (!/\G\./gcs) {
-        die("missing property: ".substr($_, pos())."\n");
-      }
-      skip_cfws();
-
-      # property
-      if (!/\G([\w-]+)/gcs) {
-        die("invalid property: ".substr($_, pos())."\n");
-      }
-      $property = lc($1);
-      if (!exists $method_ptype_prop{$method}{$ptype}{$property} &&
-          !exists $method_ptype_prop{$method}{$ptype}{'*'}) {
-        die("unknown property for $method/$ptype: $property\n");
-      }
-      skip_cfws();
-
-      # =
-      if (!/\G=/gcs) {
-        die("missing property value: ".substr($_, pos())."\n");
-      }
-      skip_cfws();
 
-      # value:
-      # The grammar is ( value / [ [ local-part ] "@" ] domain-name )
-      # where value := token / quoted-string
-      # and local-part := dot-atom / quoted-string / obs-local-part
-      if (!/\G$QUOTED_STRING|($ATOM(?:\.$ATOM)*|$TOKEN)(?=(?:[\s;]|$))/gcs) {
-        die("invalid $method/$ptype.$property value: ".substr($_, pos())."\n");
-      }
-      $value = defined $1 ? $1 : $2;
-      skip_cfws();
-
-      $props->{$ptype}->{$property} = $value;
-
-      if (/\G(?:;|$)/gcs) {
-        skip_cfws();
-        last;
-      }
-    }
+  my $version = $ar->version();
 
-    push @methods, [$method, {
+  foreach my $method ($ar->methods()) {
+    foreach my $m ($ar->method($method)) {
+      push @{$pms->{authres_parsed}->{$method}}, {
         'authserv' => $authserv,
         'version' => $version,
-        'result' => $result,
-        'reason' => $reason,
-        'properties' => $props,
-        'arc_index' => $arc_index,
-        }];
-  }
-
-  # paranoid check..
-  if (pos() < length()) {
-    die("parse ended prematurely? ".substr($_, pos())."\n");
-  }
-
-  # Pushed to pms only if header parsed completely
-  foreach my $marr (@methods) {
-    push @{$pms->{authres_parsed}->{$marr->[0]}}, $marr->[1];
+        'result' => $m->{result},
+        'reason' => $m->{reason},
+        'properties' => $m->{properties},
+      };
+    }
   }
 
   return 1;
 }
 
-# skip whitespace and comments
-sub skip_cfws {
-  /\G\s*/gcs;
-  if (/\G\(/gcs) {
-    my $i = 1;
-    while (/\G.*?([()]|\z)/gcs) {
-      $1 eq ')' ? $i-- : $i++;
-      last if !$i;
-    }
-    die("comment not ended\n") if $i;
-    /\G\s*/gcs;
-  }
-}
-
-#sub check_cleanup {
-#  my ($self, $opts) = @_;
-#  my $pms = $opts->{permsgstatus};
-#  use Data::Dumper;
-#  print STDERR Dumper($pms->{authres_parsed});
-#  print STDERR Dumper($pms->{authres_result});
-#}
-
 1;

Added: spamassassin/trunk/t/authentication_results.t
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/authentication_results.t	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,372 @@
+#!/usr/bin/perl -T
+use strict;
+use warnings;
+use lib '.'; use lib 't';
+use SATest; sa_t_init("authentication_results");
+use Test::More;
+use Mail::SpamAssassin::Header::AuthenticationResults;
+
+my @tests = (
+    {
+        name     => 'basic single method',
+        input    => 'example.com; spf=pass [email protected]',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                spf => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { mailfrom => '[email protected]' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'authserv-id with version',
+        input    => 'example.com 1; spf=pass smtp.mailfrom=x',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                spf => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { mailfrom => 'x' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'multi-method',
+        input    => 'example.com; spf=pass smtp.mailfrom=x; dkim=pass header.d=y; dmarc=fail header.from=z',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                spf => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { mailfrom => 'x' } },
+                    },
+                ],
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'y' } },
+                    },
+                ],
+                dmarc => [
+                    {
+                        result     => 'fail',
+                        reason     => '',
+                        properties => { header => { from => 'z' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'multiple same-method',
+        input    => 'example.com; dkim=pass header.d=a; dkim=fail header.d=b',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'a' } },
+                    },
+                    {
+                        result     => 'fail',
+                        reason     => '',
+                        properties => { header => { d => 'b' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'multiple properties',
+        input    => 'example.com; dkim=pass header.d=example.com [email protected] header.b=abcdef header.a=rsa-sha256 header.s=selector1',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'example.com', i => '@example.com', b => 'abcdef', a => 'rsa-sha256', s => 'selector1' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'quoted value',
+        input    => 'example.com; dkim=pass header.i="[email protected]"',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { i => '[email protected]' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'quoted value with equals (SRS address)',
+        input    => 'example.com; spf=pass smtp.mailfrom="[email protected]"',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                spf => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { mailfrom => '[email protected]' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'comments (CFWS)',
+        input    => 'example.com; dkim=pass (good sig) header.d=x; spf=pass (authorized) smtp.mailfrom=y',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'x' } },
+                    },
+                ],
+                spf => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { mailfrom => 'y' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'reason field',
+        input    => 'example.com; spf=fail reason="not authorized" smtp.mailfrom=x',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                spf => [
+                    {
+                        result     => 'fail',
+                        reason     => 'not authorized',
+                        properties => { smtp => { mailfrom => 'x' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'action field (consumed, not stored)',
+        input    => 'example.com; dmarc=pass action=none header.from=x',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dmarc => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { from => 'x' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'method version (dkim/1)',
+        input    => 'example.com; dkim/1=pass header.d=x',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'x' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'none - empty methods',
+        input    => 'example.com; none',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {},
+        },
+    },
+    {
+        name     => 'unknown methods preserved',
+        input    => 'example.com; x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_128_GCM_SHA256',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                'x-tls' => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { version => 'TLSv1.3', cipher => 'TLS_AES_128_GCM_SHA256' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'extended properties preserved',
+        input    => 'example.com; dmarc=pass policy.published-domain-policy=reject policy.applied-disposition=none header.from=x',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dmarc => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => {
+                            policy => { 'published-domain-policy' => 'reject', 'applied-disposition' => 'none' },
+                            header => { from => 'x' },
+                        },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'header from Mail::Milter::Authentication',
+        input    => 'mail.example.net;
+    arc=pass smtp.remote-ip=2a01:0111:f403:c111:0000:0000:0000:0009;
+    dkim=pass header.d=example.org [email protected] header.b=QruneRbB header.a=rsa-sha256 header.s=selector1;
+    dmarc=pass policy.published-domain-policy=reject policy.applied-disposition=none policy.evaluated-disposition=none policy.policy-from=p header.from=example.org;
+    spf=pass [email protected] smtp.helo=DM5PR21CU001.outbound.protection.outlook.com;
+    x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 smtp.bits=256',
+        expected => {
+            authserv_id => 'mail.example.net',
+            version     => 1,
+            methods     => {
+                arc => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { 'remote-ip' => '2a01:0111:f403:c111:0000:0000:0000:0009' } },
+                    },
+                ],
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'example.org', i => '@example.org', b => 'QruneRbB', a => 'rsa-sha256', s => 'selector1' } },
+                    },
+                ],
+                dmarc => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => {
+                            policy => { 'published-domain-policy' => 'reject', 'applied-disposition' => 'none', 'evaluated-disposition' => 'none', 'policy-from' => 'p' },
+                            header => { from => 'example.org' },
+                        },
+                    },
+                ],
+                spf => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { mailfrom => '[email protected]', helo => 'DM5PR21CU001.outbound.protection.outlook.com' } },
+                    },
+                ],
+                'x-tls' => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { smtp => { version => 'TLSv1.3', cipher => 'TLS_AES_256_GCM_SHA384', bits => '256' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'quoted with escaped quotes',
+        input    => 'example.com; dkim=pass header.i=" foo \"bar\"@example.com"',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { i => ' foo \"bar\"@example.com' } },
+                    },
+                ],
+            },
+        },
+    },
+    {
+        name     => 'method version with spaces (dkim / 1)',
+        input    => 'example.com; dkim / 1=pass header.d=x',
+        expected => {
+            authserv_id => 'example.com',
+            version     => 1,
+            methods     => {
+                dkim => [
+                    {
+                        result     => 'pass',
+                        reason     => '',
+                        properties => { header => { d => 'x' } },
+                    },
+                ],
+            },
+        },
+    },
+);
+
+plan tests => scalar @tests;
+
+foreach my $test (@tests) {
+    my $ar = Mail::SpamAssassin::Header::AuthenticationResults->new($test->{input});
+    my %methods;
+    foreach my $method ($ar->methods()) {
+        $methods{$method} = [ $ar->method($method) ];
+    }
+    my $result = {
+        authserv_id => $ar->authserv_id(),
+        version     => $ar->version(),
+        methods     => \%methods,
+    };
+    is_deeply($result, $test->{expected}, $test->{name});
+}

Modified: spamassassin/trunk/t/authres.t
==============================================================================
--- spamassassin/trunk/t/authres.t	Sat Mar 14 04:20:03 2026	(r1932297)
+++ spamassassin/trunk/t/authres.t	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -25,9 +25,9 @@ tstprefs("
 %patterns = (
         'parsing Authentication-Results: authrestest1int', 'hdr1',
         'parsing Authentication-Results: authrestest2int', 'hdr2',
-        'parsing authentication-Results: authrestest3int', 'hdr3',
+        'parsing Authentication-Results: authrestest3int', 'hdr3',
         'parsing Authentication-Results: authrestest4int', 'hdr4',
-        'parsing Authentication-RESULTS: authrestest5int', 'hdr5',
+        'parsing Authentication-Results: authrestest5int', 'hdr5',
         'parsing Authentication-Results: authrestest6int', 'hdr6',
         'authres: results: dkim=pass dmarc=none spf=pass', 'results',
             );
@@ -58,9 +58,9 @@ tstprefs("
 %patterns = (
         'parsing Authentication-Results: authrestest1int', 'hdr1',
         'parsing Authentication-Results: authrestest2int', 'hdr2',
-        'parsing authentication-Results: authrestest3int', 'hdr3',
+        'parsing Authentication-Results: authrestest3int', 'hdr3',
         'parsing Authentication-Results: authrestest4int', 'hdr4',
-        'parsing Authentication-RESULTS: authrestest5int', 'hdr5',
+        'parsing Authentication-Results: authrestest5int', 'hdr5',
         'parsing Authentication-Results: authrestest6int', 'hdr6',
         'parsing Authentication-Results: authrestest7tru', 'hdr7',
         'authres: results: dkim=pass dmarc=none spf=pass', 'results',
@@ -92,9 +92,9 @@ tstprefs("
 %patterns = (
         'parsing Authentication-Results: authrestest1int', 'hdr1',
         'parsing Authentication-Results: authrestest2int', 'hdr2',
-        'parsing authentication-Results: authrestest3int', 'hdr3',
+        'parsing Authentication-Results: authrestest3int', 'hdr3',
         'parsing Authentication-Results: authrestest4int', 'hdr4',
-        'parsing Authentication-RESULTS: authrestest5int', 'hdr5',
+        'parsing Authentication-Results: authrestest5int', 'hdr5',
         'parsing Authentication-Results: authrestest6int', 'hdr6',
         'parsing Authentication-Results: authrestest7tru', 'hdr7',
         'parsing Authentication-Results: authrestest8ext', 'hdr8',

Added: spamassassin/trunk/t/authres_parser.t
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/authres_parser.t	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,139 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("authres_parser");
+
+use Test::More;
+plan tests => 21;
+
+# ---------------------------------------------------------------------------
+
+tstpre ("
+loadplugin Mail::SpamAssassin::Plugin::AuthRes
+");
+
+# common network config for all blocks
+my $networks = "
+  clear_internal_networks
+  clear_trusted_networks
+  internal_networks 212.17.35.15
+  trusted_networks 212.17.35.15
+";
+
+# == 1. Basic parsing + aggregation ==
+
+tstlocalrules ("
+  header AR_SPF_PASS   eval:check_authres_result('spf', 'pass')
+  score  AR_SPF_PASS   1.0
+  header AR_DKIM_PASS  eval:check_authres_result('dkim', 'pass')
+  score  AR_DKIM_PASS  1.0
+  header AR_DMARC_FAIL eval:check_authres_result('dmarc', 'fail')
+  score  AR_DMARC_FAIL 1.0
+  header AR_DKIM_FAIL  eval:check_authres_result('dkim', 'fail')
+  score  AR_DKIM_FAIL  1.0
+");
+
+tstprefs($networks);
+
+%patterns = (
+  q{ 1.0 AR_SPF_PASS },   'basic_spf_pass',
+  q{ 1.0 AR_DKIM_PASS },  'basic_dkim_pass',
+  q{ 1.0 AR_DMARC_FAIL }, 'basic_dmarc_fail',
+  'authres: results: dkim=pass dmarc=fail spf=pass', 'basic_results',
+);
+%anti_patterns = (
+  q{ 1.0 AR_DKIM_FAIL },  'basic_dkim_fail_suppressed',
+);
+
+ok sarun ("-D authres -L -t < data/nice/authres_basic 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# == 2. "none" method ==
+
+tstlocalrules ("
+  header AR_SPF_MISSING eval:check_authres_result('spf', 'missing')
+  score  AR_SPF_MISSING 1.0
+");
+
+tstprefs($networks);
+
+%patterns = (
+  'authres: skipping header, method none', 'none_skip',
+  q{ 1.0 AR_SPF_MISSING }, 'none_spf_missing',
+);
+%anti_patterns = (
+  'authres: results:', 'none_no_results',
+);
+
+ok sarun ("-D authres -L -t < data/nice/authres_none 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# == 3. Error handling ==
+
+tstlocalrules ("
+  header AR_SPF_PASS   eval:check_authres_result('spf', 'pass')
+  score  AR_SPF_PASS   1.0
+");
+
+tstprefs($networks);
+
+%patterns = (
+  'authres: skipping header, missing authserv', 'errors_missing_authserv',
+  'authres: results: spf=pass', 'errors_valid_results',
+);
+%anti_patterns = ();
+
+ok sarun ("-D authres -L -t < data/nice/authres_errors 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# == 4. check_authres_result 'missing' ==
+
+tstlocalrules ("
+  header AR_ARC_MISSING   eval:check_authres_result('arc', 'missing')
+  score  AR_ARC_MISSING   1.0
+  header AR_DMARC_MISSING eval:check_authres_result('dmarc', 'missing')
+  score  AR_DMARC_MISSING 1.0
+");
+
+tstprefs($networks);
+
+%patterns = (
+  q{ 1.0 AR_ARC_MISSING }, 'missing_arc',
+);
+%anti_patterns = (
+  q{ 1.0 AR_DMARC_MISSING }, 'missing_dmarc_not_hit',
+);
+
+ok sarun ("-D authres -L -t < data/nice/authres_basic 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+
+# == 5. Real-world header generated by Mail::Milter::Authentication with extended
+# dmarc properties (policy.published-domain-policy etc) and non-standard methods (x-tls)
+# should not cause the entire header to fail.
+
+tstlocalrules ("
+  header AR_SPF_PASS    eval:check_authres_result('spf', 'pass')
+  score  AR_SPF_PASS    1.0
+  header AR_DKIM_PASS   eval:check_authres_result('dkim', 'pass')
+  score  AR_DKIM_PASS   1.0
+  header AR_DMARC_PASS  eval:check_authres_result('dmarc', 'pass')
+  score  AR_DMARC_PASS  1.0
+");
+
+tstprefs($networks);
+
+%patterns = (
+  q{ 1.0 AR_SPF_PASS },   'realworld_spf_pass',
+  q{ 1.0 AR_DKIM_PASS },  'realworld_dkim_pass',
+  q{ 1.0 AR_DMARC_PASS }, 'realworld_dmarc_pass',
+);
+%anti_patterns = (
+  'authres: skipping header', 'realworld_no_skip',
+);
+
+ok sarun ("-D authres -L -t < data/nice/authres_realworld 2>&1", \&patterns_run_cb);
+ok_all_patterns();

Added: spamassassin/trunk/t/data/nice/authres_basic
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_basic	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,21 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    spf=pass smtp.mailfrom=bounce.example.org
+Authentication-Results: authserv.example.com;
+    dkim=pass [email protected];
+    dmarc=fail header.from=example.org
+Authentication-Results: authserv.example.com;
+    dkim=fail [email protected]
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres basic parsing test
+
+This is a test message for basic AuthRes parsing.

Added: spamassassin/trunk/t/data/nice/authres_comments
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_comments	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,18 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    dkim=pass (good sig) header.d=example.com;
+    spf=pass (authorized) [email protected];
+    dmarc=pass (p=reject dis=none) header.from=example.com
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres comments test
+
+This is a test message for AuthRes CFWS comment parsing.

Added: spamassassin/trunk/t/data/nice/authres_errors
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_errors	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,17 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    spf=pass [email protected]
+Authentication-Results: spf=bogus [email protected]
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres error handling test
+
+This is a test message for AuthRes error handling.

Added: spamassassin/trunk/t/data/nice/authres_none
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_none	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,15 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com; none
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres none method test
+
+This is a test message for AuthRes none method parsing.

Added: spamassassin/trunk/t/data/nice/authres_props
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_props	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,17 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    dkim=pass header.d=example.org [email protected] header.b=abcdef header.a=rsa-sha256 header.s=selector1;
+    spf=pass [email protected] smtp.helo=mail.example.org
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres properties parsing test
+
+This is a test message for AuthRes properties parsing.

Added: spamassassin/trunk/t/data/nice/authres_quoted
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_quoted	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,16 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    dkim=pass header.i="[email protected]" header.d=example.com header.b=abcdef
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres quoted string test
+
+This is a test message for AuthRes quoted string parsing.

Added: spamassassin/trunk/t/data/nice/authres_quoted_eq
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_quoted_eq	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,16 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    spf=pass smtp.mailfrom="[email protected]"
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres quoted equals test
+
+This is a test message for AuthRes quoted string with equals sign.

Added: spamassassin/trunk/t/data/nice/authres_realworld
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_realworld	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,20 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: mail.example.net;
+    arc=pass (as.1.microsoft.com=pass, ams.1.microsoft.com=pass) smtp.remote-ip=2a01:0111:f403:c111:0000:0000:0000:0009;
+    dkim=pass (2048-bit rsa key sha256) header.d=example.org [email protected] header.b=QruneRbB header.a=rsa-sha256 header.s=selector1;
+    dmarc=pass policy.published-domain-policy=reject policy.applied-disposition=none policy.evaluated-disposition=none (p=reject,d=none,d.eval=none) policy.policy-from=p header.from=example.org;
+    spf=pass [email protected] smtp.helo=DM5PR21CU001.outbound.protection.outlook.com;
+    x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 smtp.bits=256
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres real-world header test
+
+This is a test message with a real-world Authentication-Results header.

Added: spamassassin/trunk/t/data/nice/authres_reason
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_reason	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,16 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com;
+    spf=fail reason="not authorized" [email protected]
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres reason test
+
+This is a test message for AuthRes reason field parsing.

Added: spamassassin/trunk/t/data/nice/authres_version
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/authres_version	Sat Mar 14 08:14:50 2026	(r1932298)
@@ -0,0 +1,16 @@
+From [email protected]  Wed May 16 00:40:34 2001
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com [212.17.35.15]) by
+    mx.example.com (Postfix) with ESMTP id AAAAAA for
+    <[email protected]>; Tue, 15 May 2001 23:40:33 +0000
+Authentication-Results: authserv.example.com 1;
+    spf=pass [email protected]
+From: Test User <[email protected]>
+To: [email protected]
+Content-Type: text/plain
+Date: 15 May 2001 17:31:22 -0400
+Message-Id: <[email protected]>
+MIME-Version: 1.0
+Subject: authres version test
+
+This is a test message for AuthRes version number parsing.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.