Email-Abuse-Investigator

 view release on metacpan or  search on metacpan

lib/Email/Abuse/Investigator.pm  view on Meta::CPAN

# Entry criteria:
#   $domain -- a registrable domain name string.
#
# Exit status:
#   Returns { org, abuse } hashref; empty hashref on failure.

sub _parse_domain_whois_abuse :Private {
	my ($self, $domain) = @_;
	my $raw = $self->_domain_whois($domain) // return {};
	my %info;
	if ($raw =~ /Registrar:\s*(.+)/i) {
		($info{org} = $1) =~ s/\s+$//;
	}
	# Try multiple field name patterns for the abuse email
	for my $pat (
		qr/Registrar Abuse Contact Email:\s*(\S+\@\S+)/i,
		qr/Abuse Contact Email:\s*(\S+\@\S+)/i,
		qr/abuse-contact:\s*(\S+\@\S+)/i,
	) {
		if (!$info{abuse} && $raw =~ $pat) {
			($info{abuse} = $1) =~ s/\s+$//;
		}
	}
	return \%info;
}

# _rdap_lookup( $ip ) -> hashref
#
# Purpose:
#   Query the ARIN RDAP API for IP block ownership information.  RDAP is
#   preferred over raw WHOIS because it returns structured JSON.
#
# Entry criteria:
#   $ip     -- a defined IPv4 or IPv6 address string.
#   LWP::UserAgent must be installed.
#
# Exit status:
#   Returns { org, abuse, country } hashref; empty hashref on failure.

sub _rdap_lookup :Protected {
	my ($self, $ip) = @_;
	return {} unless $HAS_LWP;

	my $ua = $self->{ua};
	if(!defined($ua)) {
		$ua = LWP::UserAgent->new(
			timeout => $self->{timeout},
			agent   => "Email-Abuse-Investigator/$VERSION",
		);

		if($HAS_CONN_CACHE) {
			my $conn_cache = LWP::ConnCache->new();
			$conn_cache->total_capacity(10);
			$ua->conn_cache($conn_cache);
		}

		$ua->env_proxy(1);
		$self->{ua} = $ua;
	}

	# Use the ARIN RDAP endpoint; it covers the ARIN region and redirects
	# for RIPE/APNIC/LACNIC/AfriNIC allocations.
	my $res = eval { $ua->get("https://rdap.arin.net/registry/ip/$ip") };
	return {} unless $res && $res->is_success();

	my $j = $res->decoded_content();
	my %info;

	# Extract organisation name from the JSON response
	if ($j =~ /"name"\s*:\s*"([^"]+)"/)   { $info{org}    = $1 }
	if ($j =~ /"handle"\s*:\s*"([^"]+)"/) { $info{handle} = $1 }

	# Extract abuse email from the vcardArray contact block
	if ($j =~ /"abuse".*?"email"\s*:\s*"([^"]+)"/s) {
		$info{abuse} = $1;
	} elsif ($j =~ /"email"\s*:\s*"([^@"]+@[^"]+)"/) {
		$info{abuse} = $1;
	}

	# Country code from the network's country field
	if ($j =~ /"country"\s*:\s*"([A-Z]{2})"/) { $info{country} = $1 }

	return \%info;
}

# _raw_whois( $query, $server ) -> string | undef
#
# Purpose:
#   Open a TCP connection to a WHOIS server on port 43, send the query,
#   and return the full response as a string.  Uses IO::Select for read
#   timeouts so that alarm() is never needed (alarm() is unreliable on
#   Windows and in threaded Perl).  Supports IPv6 WHOIS servers via
#   IO::Socket::IP when that module is available.
#
# Entry criteria:
#   $query   -- the domain name or IP to query (defined, non-empty).
#   $server  -- the WHOIS server hostname (default: 'whois.iana.org').
#   $self->{timeout} -- seconds used for connect and per-read waits.
#
# Exit status:
#   Returns the raw WHOIS response string, or undef on connection/write failure.
#
# Notes:
#   Uses IO::Socket::IP (dual-stack) when available, falling back to
#   IO::Socket::INET (IPv4 only) otherwise.  The IO::Select loop reads
#   until the server closes the connection or the per-read timeout expires.

sub _raw_whois :Protected {
	my ($self, $query, $server) = @_;
	$server //= 'whois.iana.org';
	$self->_debug("WHOIS $server -> $query");

	# Choose the socket class based on what is installed.
	# IO::Socket::IP supports both IPv4 and IPv6 WHOIS servers.
	my $sock_class = $HAS_IO_SOCKET_IP ? 'IO::Socket::IP' : 'IO::Socket::INET';

	# Attempt TCP connection to port 43 on the WHOIS server
	my $sock = eval {
		$sock_class->new(
			PeerAddr => $server,
			PeerPort => $WHOIS_PORT,



( run in 1.985 second using v1.01-cache-2.11-cpan-524268b4103 )