You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by he...@apache.org on 2022/05/04 12:51:24 UTC

svn commit: r1900549 - /spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm

Author: hege
Date: Wed May  4 12:51:24 2022
New Revision: 1900549

URL: http://svn.apache.org/viewvc?rev=1900549&view=rev
Log:
Clean up code, properly wait for DKIM results, improve docs

Modified:
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm?rev=1900549&r1=1900548&r2=1900549&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/FromNameSpoof.pm Wed May  4 12:51:24 2022
@@ -17,29 +17,33 @@
 
 =head1 NAME
 
-FromNameSpoof - perform various tests to detect spoof attempts using the From header name section
+FromNameSpoof - perform various tests to detect spoof attempts using the
+From header name section
 
 =head1 SYNOPSIS
 
 loadplugin    Mail::SpamAssassin::Plugin::FromNameSpoof
 
- # Does the From:name look like it contains an email address
- header   __PLUGIN_FROMNAME_EMAIL  eval:check_fromname_contains_email()
+ # From:name and From:addr do not match, matching depends on C<fns_check> setting
+ header  __PLUGIN_FROMNAME_SPOOF  eval:check_fromname_spoof()
+  
+ # From:name and From:addr do not match (same as above rule and C<fns_check 0>)
+ header  __PLUGIN_FROMNAME_DIFFERENT  eval:check_fromname_different()
 
- # Is the From:name different to the From:addr header
- header   __PLUGIN_FROMNAME_DIFFERENT  eval:check_fromname_different()
+ # From:name and From:addr domains differ
+ header  __PLUGIN_FROMNAME_DOMAIN_DIFFER  eval:check_fromname_domain_differ()
 
- # From:name and From:addr owners differ
- header   __PLUGIN_FROMNAME_OWNERS_DIFFER  eval:check_fromname_owners_differ()
+ # From:name looks like it contains an email address (not same as From:addr)
+ header  __PLUGIN_FROMNAME_EMAIL  eval:check_fromname_contains_email()
 
- # From:name domain differs to from header
- header   __PLUGIN_FROMNAME_DOMAIN_DIFFER  eval:check_fromname_domain_differ()
+ # From:name matches any To:addr
+ header  __PLUGIN_FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
 
- # From:name and From:address don't match and owners differ
- header   __PLUGIN_FROMNAME_SPOOF  eval:check_fromname_spoof()
-  
- # From:name address matches To:address
- header __PLUGIN_FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
+ # From:name and From:addr owners differ
+ header  __PLUGIN_FROMNAME_OWNERS_DIFFER  eval:check_fromname_owners_differ()
+
+ # From:name matches Reply-To:addr
+ header  __PLUGIN_FROMNAME_EQUALS_REPLYTO  eval:check_fromname_equals_replyto()
 
 =head1 DESCRIPTION
 
@@ -50,27 +54,31 @@ ensure minimal FPs.
 
 The plugin allows you to skip emails that have been DKIM signed by specific senders:
 
- fns_ignore_dkim googlegroups.com
+  fns_ignore_dkim googlegroups.com
 
 FromNameSpoof allows for a configurable closeness when matching the From:addr and From:name,
 the closeness can be adjusted with:
 
- fns_extrachars 50
+  fns_extrachars 50
 
 B<Note> that FromNameSpoof detects the "owner" of a domain by the following search:
 
- <owner>.<tld>
+  <owner>.<tld>
 
-By default FromNameSpoof will ignore the TLD when testing if From:addr is spoofed.
-Default 1
+By default FromNameSpoof will ignore the TLD when comparing addresses:
 
   fns_check 1
 
 Check levels:
 
- 0 - Strict checking of From:name != From:addr
- 1 - Allow for different tlds
- 2 - Allow for different aliases but same domain
+  0 - Strict checking of From:name != From:addr
+  1 - Allow for different TLDs
+  2 - Allow for different aliases but same domain
+
+"Owner" info can also be mapped as aliases with C<fns_add_addrlist>.  For
+example, to consider "googlemail.com" as "gmail":
+
+  fns_add_addrlist (gmail) *@googlemail.com
 
 =head1 TAGS
 
@@ -93,14 +101,14 @@ use in reports, header fields, other plu
     Actual From:addr domain
 
   _FNSFADDROWNER_
-    Actual From:addr detected owner
+    Actual From:addr owner
 
 =head1 EXAMPLE 
 
-header   __PLUGIN_FROMNAME_SPOOF eval:check_fromname_spoof()
-header   __PLUGIN_FROMNAME_EQUALS_TO eval:check_fromname_equals_to()
+header  __PLUGIN_FROMNAME_SPOOF  eval:check_fromname_spoof()
+header  __PLUGIN_FROMNAME_EQUALS_TO  eval:check_fromname_equals_to()
 
-meta     FROMNAME_SPOOF_EQUALS_TO  (__PLUGIN_FROMNAME_SPOOF && __PLUGIN_FROMNAME_EQUALS_TO)
+meta     FROMNAME_SPOOF_EQUALS_TO (__PLUGIN_FROMNAME_SPOOF && __PLUGIN_FROMNAME_EQUALS_TO)
 describe FROMNAME_SPOOF_EQUALS_TO From:name is spoof to look like To: address
 score    FROMNAME_SPOOF_EQUALS_TO 1.2
 
@@ -117,13 +125,12 @@ use Mail::SpamAssassin::Plugin;
 use vars qw(@ISA);
 @ISA = qw(Mail::SpamAssassin::Plugin);
 
-my $VERSION = 0.9;
+my $VERSION = 1.0;
 
 sub dbg { my $msg = shift; Mail::SpamAssassin::Plugin::dbg("FromNameSpoof: $msg", @_); }
 
 # constructor: register the eval rule
-sub new
-{
+sub new {
   my $class = shift;
   my $mailsaobject = shift;
 
@@ -153,14 +160,13 @@ sub set_config {
     setting => 'fns_add_addrlist',
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
-      my($self, $key, $value, $line) = @_;
+      my ($self, $key, $value, $line) = @_;
       local($1,$2);
-      if ($value !~ /^ \( (.*?) \) \s+ (.*) \z/sx) {
+      if ($value !~ /^ \( (.+?) \) \s+ (.+) \z/sx) {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      my $listname = "FNS_$1";
-      $value = $2;
-      $self->{parser}->add_to_addrlist ($listname, split(/\s+/, lc($value)));
+      my $listname = "FNS_".lc($1);
+      $self->{parser}->add_to_addrlist($listname, split(/\s+/, lc $2));
       $self->{fns_addrlists}{$listname} = 1;
     }
   });
@@ -169,14 +175,13 @@ sub set_config {
     setting => 'fns_remove_addrlist',
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
     code => sub {
-      my($self, $key, $value, $line) = @_;
+      my ($self, $key, $value, $line) = @_;
       local($1,$2);
-      if ($value !~ /^ \( (.*?) \) \s+ (.*) \z/sx) {
+      if ($value !~ /^ \( (.+?) \) \s+ (.+) \z/sx) {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      my $listname = "FNS_$1";
-      $value = $2;
-      $self->{parser}->remove_from_addrlist ($listname, split (/\s+/, $value));
+      my $listname = "FNS_".lc($1);
+      $self->{parser}->remove_from_addrlist($listname, split (/\s+/, lc $2));
     }
   });
 
@@ -195,7 +200,7 @@ sub set_config {
       if ($value eq '') {
         return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
       }
-      $self->{fns_ignore_dkim}->{$_} = 1 foreach (split(/\s+/, lc($value)));
+      $self->{fns_ignore_dkim}->{$_} = 1 foreach (split(/\s+/, lc $value));
     }
   });
 
@@ -216,6 +221,16 @@ sub set_config {
     setting => 'fns_check',
     default => 1,
     type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value eq '') {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      if ($value !~ /^[012]$/) {
+        return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{fns_check} = $value;
+    }
   });
 
   $conf->{parser}->register_commands(\@cmds);
@@ -224,202 +239,274 @@ sub set_config {
 sub parsed_metadata {
   my ($self, $opts) = @_;
   my $pms = $opts->{permsgstatus};
-  $pms->action_depends_on_tags('DKIMDOMAIN',
-      sub { my($pms,@args) = @_;
-        $self->_check_fromnamespoof($pms);
+
+  # If fns_ignore_dkim used, force wait for DKIM results
+  if (%{$pms->{conf}->{fns_ignore_dkim}}) {
+    # Check that DKIM module is loaded
+    if (exists $pms->{conf}->{dkim_timeout}) {
+      if (!$self->{main}->{local_tests_only}) {
+        # Initialize async queue
+        $pms->{fromname_async_queue} = [];
+      } else {
+        dbg("local tests only, ignoring fns_ignore_dkim setting");
       }
-  );
-  1;
+    } else {
+      dbg("DKIM plugin not loaded, ignoring fns_ignore_dkim setting");
+    }
+  }
 }
 
-sub check_fromname_different
-{
-  my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_address_different};
-}
+sub _check_eval {
+  my ($self, $pms, $result) = @_;
+
+  if (exists $pms->{fromname_async_queue}) {
+    my $rulename = $pms->get_current_eval_rule_name();
+    push @{$pms->{fromname_async_queue}}, sub {
+      if ($result->()) {
+        $pms->got_hit($rulename, '', ruletype => 'header');
+      } else {
+        $pms->rule_ready($rulename);
+      }
+    };
+    $pms->rule_pending($rulename); # mark async
+    return;
+  }
 
-sub check_fromname_domain_differ
-{
-  my ($self, $pms) = @_;
   $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_domain_different};
+  return $result->();
 }
 
-sub check_fromname_spoof
-{
+sub check_fromname_spoof {
   my ($self, $pms, $check_lvl) = @_;
-  $self->_check_fromnamespoof($pms);
 
-  $check_lvl //= $pms->{conf}->{fns_check};
+  # Some deprecated eval parameter, was not documented?
+  if (!defined $check_lvl || $check_lvl !~ /^[012]$/) {
+    $check_lvl = $pms->{conf}->{fns_check};
+  }
+
+  my $result = sub {
+    my @array = (
+      ($pms->{fromname_address_different}),
+      ($pms->{fromname_address_different} && $pms->{fromname_owner_different}),
+      ($pms->{fromname_address_different} && $pms->{fromname_domain_different})
+    );
+    $array[$check_lvl];
+  };
 
-  my @array = (
-    ($pms->{fromname_address_different}) ,
-    ($pms->{fromname_address_different} && $pms->{fromname_owner_different}) ,
-    ($pms->{fromname_address_different} && $pms->{fromname_domain_different})
-  );
+  return $self->_check_eval($pms, $result);
+}
+
+sub check_fromname_different {
+  my ($self, $pms) = @_;
 
-  return $array[$check_lvl];
+  my $result = sub {
+    $pms->{fromname_address_different};
+  };
 
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_contains_email
-{
+sub check_fromname_domain_differ {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_contains_email};
+
+  my $result = sub {
+    $pms->{fromname_domain_different};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_equals_replyto
-{
+sub check_fromname_contains_email {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_equals_replyto};
+
+  my $result = sub {
+    $pms->{fromname_contains_email};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_equals_to
-{
+sub check_fromname_equals_to {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_equals_to_addr};
+
+  my $result = sub {
+    $pms->{fromname_equals_to_addr};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub check_fromname_owners_differ
-{
+sub check_fromname_owners_differ {
   my ($self, $pms) = @_;
-  $self->_check_fromnamespoof($pms);
-  return $pms->{fromname_owner_different};
+
+  my $result = sub {
+    $pms->{fromname_owner_different};
+  };
+
+  return $self->_check_eval($pms, $result);
 }
 
-sub _check_fromnamespoof
-{
+sub check_fromname_equals_replyto {
   my ($self, $pms) = @_;
 
-  return if (defined $pms->{fromname_contains_email});
+  my $result = sub {
+    $pms->{fromname_equals_replyto};
+  };
 
-  my $conf = $pms->{conf};
+  return $self->_check_eval($pms, $result);
+}
 
-  $pms->{fromname_contains_email} = 0;
-  $pms->{fromname_address_different} = 0;
-  $pms->{fromname_equals_to_addr} = 0;
-  $pms->{fromname_domain_different} = 0;
-  $pms->{fromname_owner_different} = 0;
-  $pms->{fromname_equals_replyto} = 0;
+sub check_cleanup {
+  my ($self, $opts) = @_;
+  my $pms = $opts->{permsgstatus};
+
+  if (exists $pms->{fromname_async_queue} && @{$pms->{fromname_async_queue}}) {
+    $self->_check_fromnamespoof($pms);
+    $_->() foreach (@{$pms->{fromname_async_queue}});
+  }
+}
+
+sub _check_fromnamespoof {
+  my ($self, $pms) = @_;
+
+  return if $pms->{fromname_checked};
+  $pms->{fromname_checked} = 1;
+
+  my $conf = $pms->{conf};
 
-  foreach my $addr (split / /, $pms->get_tag('DKIMDOMAIN') || '') {
-    if ($conf->{fns_ignore_dkim}->{lc($addr)}) {
+  foreach my $addr (split(/\s+/, $pms->get_tag('DKIMDOMAIN')||'')) {
+    if ($conf->{fns_ignore_dkim}->{lc $addr}) {
       dbg("ignoring, DKIM signed: $addr");
-      return 0;
+      return;
     }
   }
 
   foreach my $iheader (keys %{$conf->{fns_ignore_header}}) {
     if ($pms->get($iheader)) {
       dbg("ignoring, header $iheader found");
-      return 0 if ($pms->get($iheader));
+      return;
     }
   }
 
-  my $list_refs = {};
+  # Parse From addr
+  my $from_addr = lc $pms->get('From:addr');
+  my $from_domain = $self->{main}->{registryboundaries}->uri_to_domain("mailto:$from_addr");
+  return unless defined $from_domain;
+
+  # Parse From name
+  my $fromname = lc $pms->get('From:name');
+  # Very common to have From address cloned into name, ignore?
+  #if ($fromname eq $from_addr) {
+  #  dbg("ignoring, From-name is exactly same as From addr: $fromname");
+  #  return;
+  #}
+  my ($fromname_addr, $fromname_domain);
+  if ($fromname =~ /\b([\w\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~-]+\@\w[\w-]*\.\w[\w.-]++)\b/i) {
+    $fromname_addr = $1;
+    $fromname_domain = $self->{main}->{registryboundaries}->uri_to_domain("mailto:$fromname_addr");
+    # No valid domain/TLD found? Any reason to keep testing a possibly obfuscated one?
+    if (!defined $fromname_domain) {
+      dbg("no From-name addr found");
+      return;
+    }
+    $pms->{fromname_contains_email} = 1; # check_fromname_contains_email hit
+    # Calculate "closeness" (this really needs documentation, as it's hard to understand)
+    my $nochar = ($fromname =~ y/a-z0-9//c);
+    $nochar -= ($fromname_addr =~ y/a-z0-9//c);
+    my $len = length($fromname) + $nochar - length($fromname_addr);
+    unless ($len <= $conf->{fns_extrachars}) {
+      dbg("not enough closeness for From-name/addr: $fromname <=> $fromname_addr ($len <= $conf->{fns_extrachars})");
+      return;
+    }
+  } else {
+    # No point continuing if email was not found inside name
+    dbg("no From-name addr found");
+    return;
+  }
 
+  # Parse owners
+  my $list_refs = {};
   if ($conf->{fns_addrlists}) {
     my @lists = keys %{$conf->{fns_addrlists}};
     foreach my $list (@lists) {
       $list_refs->{$list} = $conf->{$list};
     }
-    s/^FNS_// foreach (@lists);
-    dbg("using addrlists: ".join(', ', @lists));
+    dbg("using addrlists for owner aliases: ".join(', ', map { s/^FNS_//; $_ } @lists));
   }
+  my $fromname_owner = $self->_find_address_owner($fromname_addr, $fromname_domain, $list_refs);
+  my $from_owner = $self->_find_address_owner($from_addr, $from_domain, $list_refs);
 
-  my %fnd = ();
-  my %fad = ();
-  my %tod = ();
-
-  $fnd{'addr'} = $pms->get("From:name");
-
-  if ($fnd{'addr'} =~ /\b([\w\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[\w\-\.]+\.[\w\-\.]++)\b/i) {
-    my $nochar = ($fnd{'addr'} =~ y/A-Za-z0-9//c);
-    $nochar -= ($1 =~ y/A-Za-z0-9//c);
+  dbg("Parsed From-name addr/domain/owner: $fromname_addr/$fromname_domain/$fromname_owner");
+  dbg("Parsed From-addr addr/domain/owner: $from_addr/$from_domain/$from_owner");
 
-    return 0 unless ((length($fnd{'addr'})+$nochar) - length($1) <= $conf->{'fns_extrachars'});
-
-    $fnd{'addr'} = lc $1;
-  } else {
-    return 0;
+  if ($fromname_addr ne $from_addr) {
+    dbg("From-name addr differs from From addr: $fromname_addr != $from_addr");
+    $pms->{fromname_address_different} = 1;
+  }
+  if ($fromname_domain ne $from_domain) {
+    dbg("From-name domain differs from From domain: $fromname_domain != $from_domain");
+    $pms->{fromname_domain_different} = 1;
+  }
+  if ($fromname_owner ne $from_owner) {
+    dbg("From-name owner differs from From owner: $fromname_owner != $from_owner");
+    $pms->{fromname_owner_different} = 1;
   }
 
-  my $replyto = lc $pms->get("Reply-To:addr");
-
-  $fad{'addr'} = lc $pms->get("From:addr");
-  my @toaddrs = $pms->all_to_addrs();
-  return 0 unless @toaddrs;
-
-  $tod{'addr'} = lc $toaddrs[0];
-
-  $fnd{'domain'} = $self->{main}->{registryboundaries}->uri_to_domain($fnd{'addr'});
-  $fad{'domain'} = $self->{main}->{registryboundaries}->uri_to_domain($fad{'addr'});
-  $tod{'domain'} = $self->{main}->{registryboundaries}->uri_to_domain($tod{'addr'});
-
-  return 0 unless (defined $fnd{'domain'} && defined $fad{'domain'});
-
-  $pms->{fromname_contains_email} = 1;
-
-  $fnd{'owner'} = $self->_find_address_owner($fnd{'addr'}, $list_refs);
-
-  $fad{'owner'} = $self->_find_address_owner($fad{'addr'}, $list_refs);
-
-  $tod{'owner'} = $self->_find_address_owner($tod{'addr'}, $list_refs);
-
-  $pms->{fromname_address_different} = 1 if ($fnd{'addr'} ne $fad{'addr'});
-
-  $pms->{fromname_domain_different} = 1 if ($fnd{'domain'} ne $fad{'domain'});
-
-  $pms->{fromname_equals_to_addr} = 1 if ($fnd{'addr'} eq $tod{addr});
-
-  $pms->{fromname_equals_replyto} = 1 if ($fnd{'addr'} eq $replyto);
+  # Check Reply-To related
+  my $replyto_addr = lc $pms->get('Reply-To:addr');
+  if ($fromname_addr eq $replyto_addr) {
+    dbg("From-name addr is same as Reply-To addr: $fromname_addr");
+    $pms->{fromname_equals_replyto} = 1;
+  }
 
-  if ($fnd{'owner'} ne $fad{'owner'}) {
-    $pms->{fromname_owner_different} = 1;
+  # Check To related
+  foreach my $to_addr ($pms->all_to_addrs()) {
+    if ($fromname_addr eq $to_addr) {
+      dbg("From-name addr is same as To addr: $fromname_addr");
+      $pms->{fromname_equals_to_addr} = 1;
+      last;
+    }
   }
 
-  if ($pms->{fromname_address_different}) {
-    $pms->set_tag("FNSFNAMEADDR", $fnd{'addr'});
-    $pms->set_tag("FNSFADDRADDR", $fad{'addr'});
-    $pms->set_tag("FNSFNAMEOWNER", $fnd{'owner'});
-    $pms->set_tag("FNSFADDROWNER", $fad{'owner'});
-    $pms->set_tag("FNSFNAMEDOMAIN", $fnd{'domain'});
-    $pms->set_tag("FNSFADDRDOMAIN", $fad{'domain'});
-
-    dbg("From name spoof: $fnd{addr} $fnd{domain} $fnd{owner}") if defined $fnd{owner};
-    dbg("Actual From: $fad{addr} $fad{domain} $fad{owner}") if defined $fad{owner};
-    dbg("To Address: $tod{addr} $tod{domain} $tod{owner}") if defined $tod{owner};
+  # Set tags
+  if ($pms->{fromname_address_different} || $pms->{fromname_owner_different}) {
+    $pms->set_tag("FNSFNAMEADDR", $fromname_addr);
+    $pms->set_tag("FNSFNAMEDOMAIN", $fromname_domain);
+    $pms->set_tag("FNSFNAMEOWNER", $fromname_owner);
+    $pms->set_tag("FNSFADDRADDR", $from_addr);
+    $pms->set_tag("FNSFADDRDOMAIN", $from_domain);
+    $pms->set_tag("FNSFADDROWNER", $from_owner);
   }
 }
 
-sub _find_address_owner
-{
-  my ($self, $check, $list_refs) = @_;
+sub _find_address_owner {
+  my ($self, $addr, $addr_domain, $list_refs) = @_;
+
+  # Check fns addrlist first for user defined mapping
   foreach my $owner (keys %{$list_refs}) {
-    foreach my $welcome_addr (keys %{$list_refs->{$owner}}) {
-      if ($check =~ $list_refs->{$owner}{$welcome_addr}) {
-        $owner =~ s/^FNS_//i;
+    foreach my $listaddr (keys %{$list_refs->{$owner}}) {
+      if ($addr =~ $list_refs->{$owner}{$listaddr}) {
+        $owner =~ s/^FNS_//;
         return lc $owner;
       }
     }
   }
 
-  my $owner = $self->{main}->{registryboundaries}->uri_to_domain($check);
-
-  return if not defined $owner;
-
-  $check =~ /^([^\@]+)\@(.*)$/;
-
-  if ($owner ne $2) {
-    return $self->_find_address_owner("$1\@$owner", $list_refs);
+  # If we have subdomain addr foo.bar@sub.domain.com,
+  # this will try to recheck foo.bar@domain.com from addrlist
+  local($1,$2);
+  if ($addr =~ /^([^\@]+)\@(.+)$/) {
+    if ($2 ne $addr_domain) {
+      return $self->_find_address_owner("$1\@$addr_domain", $addr_domain, $list_refs);
+    }
   }
 
-  $owner =~ /^([^\.]+)\./;
-  return lc $1;
+  # Grab the first component of TLD
+  if ($addr_domain =~ /^([^.]+)\./) {
+    return $1;
+  } else {
+    return $addr_domain;
+  }
 }
 
 1;