#!/usr/bin/perl # # FormChecker - uses the Neutrino API to screen our input forms # for items like nonsense phone numbers, IP addresses, bogus email # addresses, spammy domains with low reputation, and spammy bots # who put crap in the message box. geolocate when we can, must # resolve to the USA. # # https://www.neutrinoapi.com # # Order of form validation # # 1. Check local ban log to see if we've banned the IP before, if exists the validation failed # 2. Check the hidden input field named email, if blank continue, if not the validation failed # 3. Check IP location, if not US, then validation fails and the IP is added to the local ban log # 4. Check the message is written in English (Latin letters), if not the validation fails # 5. Check the message input textarea for bad words, if exists the validation fails # 6. Check the phone number, if not a valid US number, then validation fails # 7. Check IP ban log via API to see if IP is listed, if exists the validation failed # 8. Check the domain's reputation, if low, the validation fails # 9. Check the email's syntax and other attributes, if there are issues, validation fails # 10. Check the email against the domain's SMTP server, if rejected, validation fails # package FormChecker2; use strict; #no warnings 'uninitialized'; use rlib '..'; use rlib '../../dntk'; use rlib '../../dntk/misc'; use rlib './db'; use rlib '../db'; use rlib '../misc'; use rlib '../../db'; use rlib '../../misc'; use LWP::UserAgent; use URI::Escape; use Encode; use Fcntl qw(:flock); use JSON; use Data::Dumper; use Data::Printer; use Number::Phone; use Date::Manip; use vars qw( $VERSION ); our $BAN_LOG_FILE = "/some/dir/form_checker_ip_ban_log.txt"; # # new() # sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; # Neutrino API $self->{_api_key} = "getyourkey"; $self->{_secret} = "addyoursecret"; bless ($self, $class); return ($self); } # # checkForm() - arrange order and combine according to how strict we want to check the form # sub checkForm { my $self = shift; my $debug = 0; my $today = &UnixDate('today', "%Y-%m-%d %H:%M:%S"); my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " Form Data - $today \n"; print FC "---------------------------------------------\n"; print FC " IP: " . $self->getIp . "\n"; print FC " Email: " . $self->getEmail . "\n"; print FC " Email2Hidden: " . $self->getEmail2Hidden . "\n"; print FC " Phone: " . $self->getPhone. "\n"; print FC " Message: " . $self->getMessage . "\n"; print FC "\n\n"; close FC; my ($id, $msg); # #1 ($id, $msg) = split /::/, $self->runIpCheckLocalBanLog($self->getIp()); if ( $id >= 1 ) { return "$msg"; } # #2 ($id, $msg) = split /::/, $self->runEmail2Hidden($self->getEmail2Hidden()); if ( $id >= 1 ) { return "$msg"; } # #3 ($id, $msg) = split /::/, $self->runIpInfo($self->getIp()); if ( $id >= 1 ) { return "$msg"; } # #4 ($id, $msg) = split /::/, $self->runMessageLanguage($self->getMessage()); if ( $id >= 1 ) { return "$msg"; } # #5 ($id, $msg) = split /::/, $self->runBadWordFilter($self->getMessage()); if ( $id >= 1 ) { return "$msg"; } # #6 ($id, $msg) = split /::/, $self->runPhoneValidate($self->getPhone(), $self->getIp()); if ( $id >= 1 ) { return "$msg"; } # #7 ($id, $msg) = split /::/, $self->runIpBlockList($self->getIp()); if ( $id >= 1 ) { return "$msg"; } # #8 ($id, $msg) = split /::/, $self->runDomainReputation($self->getDomain()); if ( $id >= 1 ) { return "$msg"; } # #9 ($id, $msg) = split /::/, $self->runEmailValidate($self->getEmail()); if ( $id >= 1 ) { return "$msg"; } # #10 ($id, $msg) = split /::/, $self->runEmailVerify($self->getEmail()); if ( $id >= 1 ) { return "$msg"; } return 0; } # # setLogPrefix() - set the prefix on the log file name # sub setLogPrefix { my $self = shift; my $log_prefix = shift; my $debug = 0; $self->{'_log_prefix'} = $log_prefix; return 0; } # # setIp() - set the form submitter's IP # sub setIp { my $self = shift; my $ip = shift; my $debug = 0; $self->{'_ip'} = $ip; return 0; } # # getIp() - get the form submitter's IP # sub getIp { my $self = shift; my $debug = 0; return $self->{'_ip'}; } # # setEmail() - set the email addr from the form # sub setEmail { my $self = shift; my $email = shift; my $debug = 0; my ($bla, $domain) = split (/@/, $email); $self->{_domain} = $domain; $self->{_email} = $email; return 0; } # # getEmail() - get the email addr from the form # sub getEmail { my $self = shift; my $debug = 0; if ( $self->{_email} ) { return $self->{_email}; } else { return 1; } return 0; } # # setEmail2Hidden() - set the hidden email addr from the form # sub setEmail2Hidden { my $self = shift; my $email2hidden = shift; my $debug = 0; my ($bla, $domain2hidden) = split (/@/, $email2hidden); $self->{"_domain2hidden"} = $domain2hidden; $self->{"_email2hidden"} = $email2hidden; return 0; } # # getEmail2Hidden() - get the hidden email addr from the form # sub getEmail2Hidden { my $self = shift; my $debug = 0; if ( $self->{"_email2hidden"} ) { return $self->{"_email2hidden"}; } else { # no email address found return; } # no email address found return; } # # getDomain2Hidden () - chop the domain off the hidden email # sub getDomain2Hidden { my $self = shift; my $debug = 0; if ( $self->{"_domain2hidden"} ) { return $self->{"_domain2hidden"}; } else { return 0; } return 0; } # # getDomain () - chop the domain off the email # sub getDomain { my $self = shift; my $debug = 0; if ( $self->{_domain} ) { return $self->{_domain}; } else { return 1; } return 0; } # # setPhone() - set the phone # from the form # sub setPhone { my $self = shift; my $phone = shift; my $debug = 0; $self->{'_phone'} = $phone; return 0; } # # getPhone() - get the phone # from the form # sub getPhone { my $self = shift; my $debug = 0; if ( $self->{_phone} ) { return $self->{_phone}; } else { return 1; } return 0; } # # setMessage() - get the message from the form # sub setMessage { my $self = shift; my $message = shift; my $debug = 0; $self->{'_message'} = $message; return 0; } # # getMessage () - set the message from the form # sub getMessage { my $self = shift; my $debug = 0; if ( $self->{_message} ) { return $self->{_message}; } else { return 1; } return 0; } # # setListRating () - level of rating for bloclist check # sub setListRating { my $self = shift; my $list_rating = shift; $self->{'_list_rating'} = $list_rating; my $debug = 0; return 0; } # # getListRating () - level of rating for bloclist check # sub getListRating { my $self = shift; my $debug = 0; if ( $self->{_list_rating} ) { return $self->{_list_rating}; } else { return 1; } return 0; } # # _formBody() - build an x-www-form-urlencoded body with every value escaped, # so an '&', '=' or non-ASCII text in a form field can't break the request # sub _formBody { my @pairs = @_; my @body; while ( my ($key, $value) = splice(@pairs, 0, 2) ) { $value = '' unless defined $value; $value = utf8::is_utf8($value) ? uri_escape_utf8($value) : uri_escape($value); push @body, "$key=$value"; } return join('&', @body); } # # addIpToBanLog() - append an IP to the local ban log so check #1 rejects it # next time without any API calls # sub addIpToBanLog { my $self = shift; my $ip = shift; my $reason = shift; return 0 if ( $ip eq '' ); if ( open (my $ban, ">>", $BAN_LOG_FILE) ) { flock($ban, LOCK_EX); print $ban "$ip\n"; close $ban; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC " Added IP address [$ip] to local ban log ($reason)\n\n\n"; close FC; return 0; } # ----------------------------------------------------- # # # IP Check Local Ban Log # --------------------------------------------- # master ban log is located in $BAN_LOG_FILE, one IP or IP prefix per line # (e.g. 195.2.67.184 bans that address, 195.2.67 bans 195.2.67.*) # sub runIpCheckLocalBanLog { my $self = shift; my $ip = shift; my $ip_self = $self->{"_ip"}; my $debug = 0; open(LBAN, ">>/tmp/runIpCheckLocalBanLog.txt"); print LBAN "---------------------- \n\n"; if ( $ip eq '' && $ip_self ne '' ) { $ip = $self->{"_ip"}; print LBAN "ip is null, so using ip_self \n"; } elsif ( $ip ne '' && $ip_self eq '' ) { $ip = $ip; print LBAN "self->{'_ip'} is null, so using ip \n"; } else { print LBAN "both ip and self->{'_ip'} are null \n"; } open (BAN_LOG, "<$BAN_LOG_FILE"); my @ban_log = ; my $ban_ip; my $found = 0; my $rc; my $word; foreach $ban_ip (@ban_log) { chomp($ban_ip); $ban_ip =~ s/^\s+|\s+$//g; next if ( $ban_ip eq '' ); # 2026/10/13 - blank line would match every IP $ban_ip =~ s/\.$//; # 2026/10/13 - match whole octets from the start, so 1.2.3.4 no longer bans 11.2.3.45 if ( $ip =~ /^\Q$ban_ip\E(\.|$)/ ) { print LBAN "FOUND: ban_ip: $ban_ip - ip: $ip\n"; $found=1; last; } else { print LBAN "NOTTT: ban_ip: $ban_ip - ip: $ip\n"; $found=0; } } if ( $found eq '1' ) { $word = "IP address [$ip] FOUND in local ban log"; $rc = "1::$word"; } else { $word = "IP address [$ip] not in local ban log"; $rc = "0::$word"; } print LBAN "rc: $rc word: $word \n"; close LBAN; my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " IP Local Ban Log - $word \n"; print FC "---------------------------------------------\n"; print FC " ban IP addresses: " . @ban_log . "\n"; print FC " IP address: " . $ip . "\n"; print FC " status: " . $word . "\n"; print FC "\n\n"; close FC; return $rc; } # # # # --------------------------------------------- # runEmail2Hidden() # --------------------------------------------- # # sub runEmail2Hidden { my $self = shift; my $email2hidden = shift; my $debug = 0; if ( $email2hidden eq '' ) { $email2hidden = $self->{"_email2hidden"}; } # 2026/10/13 - always return "rc::message"; the old bare "1" made checkForm # return an empty string, which reads as "passed", when the hidden field was filled in my $rc; my $word; if ( $email2hidden eq '' ) { $word = "IS BLANK SO NOT AN ISSUE"; $rc = "0::$word"; } else { $word = "IS NOT BLANK, SO TEST FAILED"; $rc = "1::runEmail2Hidden failed"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " Email2Hidden - $word\n"; print FC "---------------------------------------------\n"; print FC " email2hidden: " . $email2hidden . "\n"; print FC "\n\n"; close FC; return $rc; } # # # # IP info # --------------------------------------------- # valid boolean Is this a valid IPv4 or IPv6 address # ip string The IP address # country string Full country name # country-code string ISO 2-letter country code # country-code3 string ISO 3-letter country code # continent-code string ISO 2-letter continent code # currency-code string ISO 4217 currency code associated with the country # city string Full city name (if detectable) # region string Full region name (if detectable) # longitude float Location longitude # latitude float Location latitude # hostname string The IPs hostname (only set if reverse-lookup has been used) # # # sub runIpInfo { my $self = shift; my $ip = shift; my $debug = 0; if ( $ip eq '' ) { $ip = $self->{"_ip"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/ip-info"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "ip" => $ip )); my $ip_info_json = $ua->request($req); my $result = decode_json($ip_info_json->content); my $rc; my $word; # 2025/12/10 - allow private IPs (192.168.x.x, 10.x.x.x, 172.16-31.x.x) which won't have geo info my $is_private_ip = ($ip =~ /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/); if ( $result->{"country-code"} eq '' && $is_private_ip ) { $word = "TEST PASSED (private IP)"; $rc = "0::$word"; } elsif ( $result->{"country-code"} eq '' ) { $word = "runIpInfo ambiguous"; $rc = "1::$word"; } elsif ( $result->{"country-code"} eq 'US' ) { $word = "TEST LIKELY ACCEPTABLE"; $rc = "0::$word"; } else { $word = "runIpInfo failed [" . $result->{"country-code"} . "]"; $rc = "1::$word"; } # 2026/10/13 - remember non-US IPs so they are rejected locally next time if ( $result->{"country-code"} ne '' && $result->{"country-code"} ne 'US' ) { $self->addIpToBanLog($ip, "IP Info " . $result->{"country-code"}); } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " IP Info - $word \n"; print FC "---------------------------------------------\n"; print FC " country: " . $result->{"country"} . "\n"; print FC " country-code: " . $result->{"country-code"} . "\n"; print FC " region: " . $result->{"region"} . "\n"; print FC " city: " . $result->{"city"} . "\n"; print FC "\n\n"; close FC; return $rc; } # # IP Blocklist # --------------------------------------------- # ip string The IP address # is-listed boolean Is this IP on a blocklist # last-seen integer The last time this IP was seen on a blocklist (in Unix time or 0 if not listed recently) # list-count integer The number of blocklists the IP is listed on # blocklists array An array of strings indicating which blocklists this IP is listed on (empty if not listed) # sensors array An array of objects containing details on which sensors were used to detect this IP # is-proxy boolean IP has been detected as an anonymous web proxy or anonymous HTTP proxy # is-tor boolean IP is a Tor node or running a Tor related service # is-vpn boolean IP has been detected as belonging to a VPN provider # is-malware boolean IP is involved in distributing or is running malware # is-spyware boolean IP is involved in distributing or is running spyware # is-dshield boolean IP has been flagged as an attack source on DShield (dshield.org) # is-hijacked boolean IP is part of a hijacked netblock or a netblock controlled by a criminal organization # is-spider boolean IP is running a hostile web spider / web crawler # is-bot boolean IP is hosting a malicious bot or is part of a botnet. Includes brute-force crackers # is-spam-bot boolean IP address is hosting a spam bot, comment spamming or any other spamming type software # is-exploit-bot boolean IP is hosting an exploit finding bot or is running exploit scanning software # # sub runIpBlockList { my $self = shift; my $ip = shift; my $debug = 0; my $i; my $list; if ( $ip eq '' ) { $ip = $self->{"_ip"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/ip-blocklist"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "ip" => $ip, "list-rating" => $self->getListRating() )); my $ip_info_blocklist_json = $ua->request($req); my $result = decode_json($ip_info_blocklist_json->content); my $word; my $rc; # 2025/12/10 - allow private IPs and check for falsy value (not string 'false') my $is_private_ip = ($ip =~ /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/); if ( ! $result->{"is-listed"} || $is_private_ip ) { $word = "TEST PASSED"; $rc = "0::$word"; } else { $word = "runIpBlockList failed"; $rc = "1::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " IP Blocklist Check - $word \n"; print FC "---------------------------------------------\n"; print FC " ip: " . $result->{"ip"} . "\n"; print FC " is-listed: " . $result->{"is-listed"} . "\n"; print FC " is-count: " . $result->{"is-count"} . "\n"; print FC " is-proxy: " . $result->{"is-proxy"} . "\n"; print FC " is-tor: " . $result->{"is-tor"} . "\n"; print FC " is-vpn: " . $result->{"is-vpn"} . "\n"; print FC " is-malware: " . $result->{"is-malware"} . "\n"; print FC " is-spyware: " . $result->{"is-spyware"} . "\n"; print FC " is-dshield: " . $result->{"is-dshield"} . "\n"; print FC " is-hijacked: " . $result->{"is-hijacked"} . "\n"; print FC " is-spider: " . $result->{"is-spider"} . "\n"; print FC " is-bot: " . $result->{"is-bot"} . "\n"; print FC " is-spam-bot: " . $result->{"is-spam-bot"} . "\n"; print FC " is-exploit-bot: " . $result->{"is-exploit-bot"} . "\n"; print FC " blocklists: \n"; for ( $i=0; $i < $result->{"list-count"}; $i++ ) { print FC " blocklist: " . $result->{"blocklists"}[$i] . "\n"; } print FC " sensors: \n"; for ( $i=0; $i < 3; $i++ ) { print FC " sensors: " . $result->{"sensors"}[$i] . "\n"; } print FC "\n\n"; close FC; return $rc; } # # Host Reputation # ---------------------------------- # host string The IP address or host name # is-listed boolean Is this host blacklisted # list-count integer The number of DNSBLs the host is listed on # lists array An array of objects for each DNSBL checked, with the following keys: # is-listed - true if listed, false if not # list-name - the name of the DNSBL # list-host - the domain/hostname of the DNSBL # list-rating - the list rating [1-3] with 1 being the best rating and 3 the lowest rating # txt-record - the TXT record returned for this listing (if listed) # return-code - the specific return code for this listing (if listed) # # sub runDomainReputation { my $self = shift; my $domain = shift; my $debug = 0; my $i = 0; if ( $domain eq '' ) { $domain = $self->{"_domain"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/host-reputation"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "host" => $domain )); my $host_reputation_json = $ua->request($req); my $result = decode_json($host_reputation_json->content); my $rc; my $word; # 2026/10/13 - truthy check; decode_json gives 1/0, never the string 'true', so this never failed if ( $result->{"is-listed"} ) { $word = "runDomainReputation failed"; $rc = "1::$word"; } else { $word = " TEST PASSED"; $rc = "0::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " Domain Reputation - $word \n"; print FC "---------------------------------------------\n"; print FC " is-listed: " . $result->{"is-listed"} . "\n"; print FC " list-count: " . $result->{"list-count"} . "\n"; print FC " list-name: " . $result->{"list-name"} . "\n"; print FC " list-host: " . $result->{"list-host"} . "\n"; print FC " list-rating: " . $result->{"last-rating"} . "\n"; print FC " txt-record: " . $result->{"txt-record"} . "\n"; print FC " return-code: " . $result->{"return-code"} . "\n"; print FC " response-time: " . $result->{"response-time"} . "\n"; for ( $i=0; $i < $result->{"list-count"}; $i++ ) { print FC " $i: " . $result->{"lists"}[$i] . "\n"; } print FC "\n\n"; close FC; return $rc; } # # Email Validate - not-SMTP # ----------------------------------- # valid boolean Is this a valid email address (syntax and domain is valid) # # Email Validate - not-SMTP # ----------------------------------- # sub runEmailValidate { my $self = shift; my $email = shift; my $debug = 0; if ( $email eq '' ) { $email = $self->{"_email"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/email-verify"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "email" => $email, "fix-typos" => 0 )); my $email_validate_json = $ua->request($req); my $result = decode_json($email_validate_json->content); my $rc; my $word; # 2025/12/10 - check 'valid' field with truthy check (API returns 1/0 not 'true'/'false') if ( $result->{"valid"} ) { $word = "TEST PASSED"; $rc = "0::$word"; } else { $word = "runEmailValidate failed"; $rc = "1::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " E-Mail Validate - $word \n"; print FC "---------------------------------------------\n"; print FC " valid email: " . $result->{"valid"} . "\n"; print FC " email: " . $result->{"email"} . "\n"; print FC " is freemail: " . $result->{"is-freemail"} . "\n"; print FC " is disposal: " . $result->{"is-disposable"} . "\n"; print FC " is personal: " . $result->{"is-personal"} . "\n"; print FC " domain: " . $result->{"domain"} . "\n"; print FC " provider: " . $result->{"provider"} . "\n"; print FC " syntax error: " . $result->{"syntax-error"} . "\n"; print FC " domain error: " . $result->{"domain-error"} . "\n"; print FC " fix typo: " . $result->{"typos-fixed"} . "\n"; print FC "\n\n"; close FC; return $rc; } # # Email Verify - SMTP # ----------------------------------- # valid boolean Is this a valid email address (syntax and domain is valid) # verified boolean True if this address has passed SMTP verification. Check the smtp-status and smtp-response fields for specific verification details # email string The full email address (this could be different to the supplied address if typos-fixed is true) # typos-fixed boolean True if typos have been fixed # syntax-error boolean True if this address has a syntax error # domain-error boolean True if this address has a domain error (e.g. no valid mail server records) # domain string The email domain # provider string The email service provider domain # is-freemail boolean True if this address is a free-mail address # is-disposable boolean True if this address is a disposable, temporary or darknet related email address # is-personal boolean True if this address is for a person. False if this is a role based address, e.g. admin@, help@, office@, etc. # smtp-status string The SMTP verification status for the address: # ok - SMTP verification was successful, this is a real address that can receive mail # invalid - this is not a valid email address (has either a domain or syntax error) # absent - this address is not registered with the email service provider # unresponsive - the mail server(s) for this address timed-out or refused to open an SMTP connection # unknown - sorry, we could not reliably determine the real status of this address (this address may or may not exist) # smtp-response string The raw SMTP response message received during verification # is-deferred boolean True if the mail server responded with a temporary failure (either a 4xx response code or unresponsive server). # - You can retry this address later, we recommend waiting at least 15 minutes before retrying # is-catch-all boolean True if this email domain has a catch-all policy (it will accept mail for any username) # # sub runEmailVerify { my $self = shift; my $email = shift; my $debug = 0; if ( $email eq '' ) { $email = $self->{"_email"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/email-verify"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "email" => $email, "fix-typos" => 0 )); my $email_verify_json = $ua->request($req); my $result = decode_json($email_verify_json->content); my $word; my $rc; # 2025/12/10 - check with truthy value (API returns 1/0 not 'true'/'false') if ( $result->{"verified"} || $result->{"valid"} ) { $word = " TEST PASSED"; $rc = "0::$word"; } else { $word = "runEmailVerify failed"; $rc = "1::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " SMTP E-Mail Verify - $word \n"; print FC "---------------------------------------------\n"; print FC " valid email: " . $result->{"valid"} . "\n"; print FC " verified: " . $result->{"verified"} . "\n"; print FC " fix typo: " . $result->{"typos-fixed"} . "\n"; print FC " syntax-error: " . $result->{"syntax-error"} . "\n"; print FC " domain-error: " . $result->{"domain-error"} . "\n"; print FC " domain: " . $result->{"domain"} . "\n"; print FC " provider: " . $result->{"provider"} . "\n"; print FC " is-freemail: " . $result->{"is-freemail"} . "\n"; print FC " is-personal: " . $result->{"is-personal"} . "\n"; print FC " is-disposable: " . $result->{"is-disposable"} . "\n"; print FC " smtp-status: " . $result->{"smtp-status"} . "\n"; print FC " smtp-response: " . $result->{"smtp-response"} . "\n"; print FC " is-deferred: " . $result->{"is-deferred"} . "\n"; print FC " is-catch-all: " . $result->{"is-catch-all"} . "\n"; print FC "\n\n"; close FC; return $rc; } # # Phone Validate # --------------------- # number Yes string A phone number. This can be in international format (E.164) or local format. # If passing local format you should use the 'country-code' or 'ip' options as well # country-code No string ISO 2-letter country code, assume numbers are based in this country. # If not set numbers are assumed to be in international format (with or without # ip No string Pass in a users IP address and we will assume numbers are based in the country of the IP address # # output # valid boolean Is this a valid phone number # type string The predicted number type. # Note: type detection is not possible in some countries which have no predictable prefix pattern # (you can use the HLR Lookup API in these cases) # Possible values are: # mobile # fixed-line # premium-rate # toll-free # voip # unknown (use HLR lookup instead) # is-mobile boolean True if this is a mobile number (only true with 100% certainty, if the number type is unknown this value will be false) # location string The phone number location. Could be a city, region or country depending on the type of number # country string The phone number country # country-code string The phone number country as an ISO 2-letter country code # country-code3 string The phone number country as an ISO 3-letter country code # currency-code string ISO 4217 currency code associated with the country # international-calling-code integer The international calling code # international-number string The number represented in full international format (E.164) # local-number string The number represented in local dialing format # # sub runPhoneValidate { my $self = shift; my $phone = shift; my $ip = shift; my $debug = 0; if ( $phone eq '' ) { $phone = $self->{"_phone"}; } if ( $ip eq '' ) { $ip = $self->{"_ip"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/phone-validate"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "number" => $phone, "country-code" => "US", "ip" => $ip )); my $phone_validate_json = $ua->request($req); my $result = decode_json($phone_validate_json->content); my $rc; my $word; # 2026/10/13 - must be a valid number AND a US number; 'valid || US' let # invalid US-looking numbers through, and valid foreign numbers (e.g. DE, RU) pass 'valid' alone if ( $result->{"valid"} && $result->{"country-code"} eq 'US' ) { $word = "TEST PASSED"; $rc = "0::$word"; } else { $word = "runPhoneValidate failed"; $rc = "1::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " Phone Validate - $word \n"; print FC "---------------------------------------------\n"; print FC " type: " . $result->{"type"} . "\n"; print FC " is-mobile: " . $result->{"is-mobile"} . "\n"; print FC " location: " . $result->{"location"} . "\n"; print FC " country: " . $result->{"country"} . "\n"; print FC " country-code: " . $result->{"country-code"} . "\n"; print FC " country-code3: " . $result->{"country-code3"} . "\n"; print FC " currency-code: " . $result->{"currency-code"} . "\n"; print FC " international-calling-code: " . $result->{"international-calling-code"} . "\n"; print FC " international-number: " . $result->{"international-number"} . "\n"; print FC " local-number: " . $result->{"local-number"} . "\n"; print FC "\n\n"; close FC; return $rc; } # # Message Language # --------------------- # Local check, no API call. Our customers are US only, so the message must be # written with Latin letters. Counts letters from any other script (Cyrillic, # Chinese, Arabic, ...) and rejects the message when there are more than # $max_non_latin of them. A few are allowed for a stray symbol or name. # sub runMessageLanguage { my $self = shift; my $message = shift; my $debug = 0; my $max_non_latin = 3; if ( $message eq '' ) { $message = $self->{"_message"}; } # form input normally arrives as UTF-8 bytes; bytes that are not UTF-8 become U+FFFD my $text = $message; if ( ! utf8::is_utf8($text) ) { $text = Encode::decode('UTF-8', $text, Encode::FB_DEFAULT); } my $latin = () = $text =~ /\p{Latin}/g; my $non_latin = () = $text =~ /[\x{FFFD}\p{L}](? $max_non_latin ) { $word = "runMessageLanguage failed"; $rc = "1::$word"; } else { $word = "TEST PASSED"; $rc = "0::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " Message Language - $word \n"; print FC "---------------------------------------------\n"; print FC " latin letters: " . $latin . "\n"; print FC " non-latin letters: " . $non_latin . "\n"; print FC "\n\n"; close FC; return $rc; } # # Bad Word Filter # --------------------- # is-bad boolean Does the text contain bad words # bad-words-total integer Total number of bad words detected # bad-words-list array An array of the bad words found # censored-content string The censored content (only set if censor-character has been set) # # sub runBadWordFilter { my $self = shift; my $message = shift; my $debug = 0; my $i = 0; if ( $message eq '' ) { $message = $self->{"_message"}; } my $ua = LWP::UserAgent->new; my $req = HTTP::Request->new(POST => "https://neutrinoapi.com/bad-word-filter"); $req->content_type("application/x-www-form-urlencoded"); $req->content(_formBody( "user-id" => $self->{"_api_key"}, "api-key" => $self->{"_secret"}, "content" => $message, "censor-character" => "*", "fix-typos" => 0 )); my $bad_word_filter_json = $ua->request($req); my $result = decode_json($bad_word_filter_json->content); my $rc; my $word; if ( ! $result->{"is-bad"} ) { # 2025/12/10 - fixed typo 'fase' and handle numeric 0 $word = " TEST PASSED"; $rc = "0::$word"; } else { $word = "runBadWordFilter failed"; $rc = "1::$word"; } my $log_file_name = $self->{'_log_prefix'} . ".$$.log"; open (FC, ">>/tmp/$log_file_name"); print FC "---------------------------------------------\n"; print FC " Bad Word Filter - $word \n"; print FC "---------------------------------------------\n"; print FC " is-bad: " . $result->{"is-bad"} . "\n"; print FC " bad-words-total: " . $result->{"bad-words-total"} . "\n"; print FC " censored-content: " . $result->{"censored-content"} . "\n"; print FC " bad-words-list: \n"; for ( $i=0; $i < $result->{"bad-words-total"}; $i++ ) { print FC " word: " . $result->{"bad-words-list"}[$i] . "\n"; } print FC "\n\n"; close FC; return $rc; } 1;