Email-Abuse-Investigator

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

  - Expanded t/integration.t with scenarios 30-42 (black-box end-to-end):
    concurrent object isolation, future-date suspicious_date flag, plain-HTTP
    http_not_https flag, DKIM pass with different registrable domain (INFO) vs
    DKIM absent with different domain (MEDIUM), sending_software and
    received_trail sections in report(), form_contacts via URL host route,
    high_spam_country INFO flag, parse_email() named-arg calling convention,
    abuse_report_text() WEB-FORM section, encoded_subject flag, unresolved_contacts()
    full pipeline, all_domains() idempotency.  Total: 48 subtests.

  - Expanded t/edge_cases.t with sections 26-40 (49 new tests):
    - _parse_rfc2822_date: all 12 months, day-of-week prefix, single/double-digit
      day, case-insensitive months, timezone offset ignored, bogus string -> undef.
    - _country_name: all 7 known spam-country codes, unknown code returned as-is.
    - parse_email hostile reference types: CODE ref and non-empty ARRAY ref cause
      module croak; REF-to-hashref silently consumed as empty named-params
      (Params::Get unwraps REF -> HASH); GLOB ref causes Params::Get Usage error;
      undef treated as empty email without croaking.
    - All 13 public methods called on a fresh object (no prior parse_email) must
      not die and must return empty/undef sentinel values.
    - Non-HTTP URL schemes (javascript:, data:, ftp:, file:) not extracted by
      embedded_urls() -- only http:// and https:// are recognised.
    - CRLF and C0 control injection: NUL, ESC, SOH, BEL, and BS in header values
      are stripped by parse_email sanitisation and _sanitise_output before reaching
      report() output.
    - header_value() edge cases: all-caps, mixed-case, and lowercase lookups return
      the same result; missing header returns undef; all standard headers accessible.
    - sending_software() fingerprints: X-PHP-Originating-Script, X-Mailer, X-Source
      all captured; multiple SW headers all returned; absent headers -> empty list.
    - received_trail() specifics: RFC 1918 IPs included (not filtered); entries in
      oldest-first order; for= and id= fields extracted correctly.
    - Context abuse: all seven list-returning public methods behave safely in scalar
      context without dying.
    - _parse_whois_text injection safety: shell metacharacters, HTML special chars,
      CRLF-terminated lines, and 64 KB org names all handled safely.
    - all_domains() idempotency: repeated calls return the same sorted list; result
      is a deduplicated union of URL hosts and mailto domains.
    - parse_email cache invalidation: _contacts, _risk, and _auth_results slots are
      all reset to undef on a second parse_email() call.
    - Sparse internal state: risk_assessment() does not die when _origin, _urls,
      or _mailto_domains contain empty hashrefs (all keys absent).
    - unresolved_contacts() and form_contacts(): return empty lists for an empty
      object; domain with no abuse contacts appears in unresolved list.
    Total: 155 tests.

  - Expanded t/extended_tests.t with sections 47-61 (35 new tests targeting
    branches previously at 0% coverage due to 198.51.100.x TEST-NET-2 IPs
    being filtered as private in all prior tests):
    - report() originating host with rdns/country/org/abuse/note all populated,
      and fallback text when no origin found.
    - report() multiple URLs per hostname ("URLs (N)" grouped format).
    - report() URL metadata block with IP/country/org/abuse fields.
    - report() domain block with all WHOIS/MX/NS metadata fields, recently-
      registered warning, and no-A-record/no-MX fallback lines.
    - report() abuse contact note field (ip-whois route).
    - report() web-form section with form_paste word-wrap (>ROLE_WRAP_LEN),
      form_domain, form_upload, and note fields.
    - report() SENDING SOFTWARE section via X-Mailer and X-PHP-Originating-Script.
    - report() RECEIVED CHAIN TRACKING IDs section with for/id fields.
    - abuse_report_text() with risk flags, originating IP, abuse contacts,
      and form contacts with form_domain/form_paste/form_upload.
    - report() MIME-encoded Subject header ($decoded ne $v branch).
    - unresolved_contacts(): URL with unknown abuse, URL with real abuse,
      duplicate URL host, From:/Return-Path:/Sender: filter, Reply-To: not
      filtered, already-covered domain, and duplicate domain deduplication.
    - form_contacts() DKIM signer and List-Unsubscribe form-only providers
      (both https and mailto: variants); Route 3 preemption documented.
    - form_contacts() Route 4 bare address (no angle-brackets) in From:.
    - abuse_contacts() From: with display-name only (no email addr) skip path.
    - risk_assessment() cached-result path (second call returns same hashref).
    - form_contacts() Route 3 registrar form-only (markmonitor.com via WHOIS).
    - abuse_contacts() Route 3 form-only registrar suppressed from email list.
    Total: 186 tests (up from 151).

  - Added t/mutant_killers.t: 49 subtests explicitly targeting HIGH and MEDIUM
    difficulty survivors from xt/mutant_20260612_003229.t.  Covers all six
    survivor classes with precise boundary and branch engineering:
    - NUM_BOUNDARY (6 subtests): all three risk score thresholds (SCORE_HIGH=9,
      SCORE_MEDIUM=5, SCORE_LOW=2) tested at boundary and boundary-1; TZ offset
      boundaries (+14:00/−12:00) at exact limit and one-minute over; date skew
      (±8 days flags, ±6 days clean); domain expiry "expires soon" vs "expired"
      boundary tested with UTC-anchored gmtime dates to prevent timezone drift.
    - COND_INV (38 subtests): both true and false branches exercised for every
      conditional in _risk_check_origin (residential rDNS, absent rDNS, low
      confidence, high-spam-country), _risk_check_auth (SPF/DKIM/DMARC flags),
      _risk_check_date (missing date, TZ regex, implausible_timezone guard),
      _risk_check_identity (display-name spoof, free webmail, Reply-To mismatch,
      undisclosed recipients, encoded subject), _risk_check_urls_and_domains
      (URL shortener, plain HTTP, recently_registered, domain expiry, lookalike
      domain), abuse_report_text() flag/IP/contact/form sections, and
      _compute_abuse_contacts() dedup and all six contact routes.
    - BOOL_NEGATE (5 subtests): parse_email(), originating_ip(), all_domains(),
      unresolved_contacts(), and risk_assessment() return exact typed values.
    - COND_INV_1094_3: unresolved_contacts() unless($dom) domain-extraction path;
      killed by injecting a contact whose address domain covers a mailto domain
      (source set to Reply-To: to bypass the spoofable-header skip at line 1125).
    - BOOL_NEGATE_596_2: new() always returns a distinct blessed object regardless
      of CHI cache state.

  - Overall test count: 968 tests across 18 files (up from 920).


  [Code Quality]

  - Removed dead-code double-dereference block in parse_email().  A second
    `if (ref $text eq 'SCALAR')` branch was unreachable because the preceding
    line had already dereferenced $text.  Replaced with a single clean
    dereference followed by a croak on any remaining non-scalar reference.

  - Added =head1 LIMITATIONS POD section documenting eight known constraints:
    no charset conversion, hand-rolled MIME parser, IPv4-only CIDR matching,
    WHOIS rate-limiting, non-thread-safe class-level cache, DMARC policy not
    fetched, routing logic duplication risk, and CHI cache as a global.

  - Fixed all Perl::Critic severity-5 findings: replaced every `return undef`
    with bare `return` across _extract_ip_from_received, _reverse_dns,
    _domain_whois, _raw_whois, _provider_abuse_for_host,
    _provider_abuse_for_ip, _registrable, _header_value, _parse_date_to_epoch,
    and _parse_rfc2822_date.

  - Fixed Perl::Critic severity-3 findings: converted all `unless` with
    comparison operators to negated `if`; moved capture variables inside

Changes  view on Meta::CPAN

    Such addresses are syntactically valid but explicitly non-functional per
    the provider's own autoresponse.  The $add closure now checks whether the
    address domain belongs to a form-only %PROVIDER_ABUSE entry (one with a
    'form' key but no 'email' key) and suppresses it; the contact is surfaced
    correctly via form_contacts() instead.

  - Refactored the domain WHOIS fallback in _extract_and_resolve_urls() to
    call _analyse_domain() instead of the separate _parse_domain_whois_abuse()
    helper introduced in 0.05.  _analyse_domain() is already cached in
    $self->{_domain_info}, so when the same spam domain appears in multiple
    URLs the domain WHOIS is performed only once rather than once per unique
    hostname.  _parse_domain_whois_abuse() has been removed.

  - Added use utf8; pragma to Investigator.pm so that the em-dash characters
    in user-facing detail strings are handled correctly under all Perl
    configurations without relying on the source file's implicit encoding.

  - Added GoDaddy to %PROVIDER_ABUSE as a form-only entry.  GoDaddy
    explicitly rejects email abuse reports per their autoresponse, directing
    reporters to their web form instead:
      godaddy.com -> https://supportcenter.godaddy.com/AbuseReport
    The form_upload hint reflects that GoDaddy accepts screenshots or PDFs,
    not .eml files.

  - Added googlegroups.com and groups.google.com to %TRUSTED_DOMAINS.
    Google Groups message IDs (e.g. @googlegroups.com) were entering the
    domain intelligence pipeline and triggering a MarkMonitor web-form
    contact, since Google uses MarkMonitor to register its infrastructure
    domains.  This was a false positive -- MarkMonitor cannot act on a
    Google-owned domain.  Both domains are now filtered at the same point
    as google.com, gmail.com, and other trusted Google infrastructure.

  - Added form_domain field to form_contacts() hashrefs, surfaced as
    Domain/URL in report(), Domain in abuse_report_text(), and Domain in
    the --dry-run footer of submit_abuse_report.  Providers such as
    MarkMonitor and GoDaddy have a dedicated "Domain or URL" field in their
    web forms; this field gives the user the exact value to paste into it
    without having to work it out from context.

  - Improved clarity of role strings in abuse_contacts():
    1. Removed the "(provider table)" suffix from Sending ISP, URL host,
       Web host, and DKIM signer roles.  This was implementation detail
       that added length without helping the recipient understand what action
       to take.  "Sending ISP" is clearer than "Sending ISP (provider
       table)"; the via field already records how the contact was found.
    2. Included the hostname in URL host roles -- "URL host: host.example"
       rather than the generic "URL host".  When multiple routes converge on
       the same abuse address (e.g. a Blogspot URL and a Gmail sending IP
       both map to abuse@google.com), the merged role now makes clear which
       specific URL is being reported, giving the abuse team an actionable
       reference without requiring them to read the full report headers.
    3. Stripped the display name from Account provider roles.  The role
       previously included the full From: header value including the
       spammer's chosen display name (e.g. "Account provider (from: Evil
       Spammer <spam@gmail.com>)").  The display name is irrelevant to the
       abuse report, may contain non-ASCII characters, and makes the merged
       role string much longer than necessary.  The role now shows only the
       email address: "Account provider (from: spam@gmail.com)".

  - Fixed Wide character in syswrite error when submitting reports for
    messages whose decoded subject line contains non-ASCII characters (e.g.
    emoji).  submit_abuse_report uses "use utf8" and
    "use open qw(:std :encoding(UTF-8))", which causes all string operations
    to produce Unicode-flagged strings.  Net::SMTP->datasend() calls
    syswrite() on a raw socket and cannot handle wide characters.
    _build_mime_message() now calls Encode::encode('UTF-8', ...) on the
    report body and original message after CRLF normalisation, converting
    the internal Unicode strings to raw byte strings before transmission.
    Encode is a core module since Perl 5.8 so no new dependency is added.

0.05	Sat Mar 28 11:52:05 EDT 2026

  Bug fixes

  - Fixed _extract_and_resolve_urls() discarding the registrar abuse
    contact for URL hosts that cannot be resolved to an IP at analysis
    time.  Previously, when _resolve_host() returned undef, _whois_ip()
    was skipped entirely and the host was recorded with abuse=>'(unknown)',
    which caused abuse_contacts() to produce no contact for that host even
    though a domain WHOIS record (and therefore a registrar abuse address)
    existed.  _extract_and_resolve_urls() now falls back to a domain WHOIS
    lookup on the registrable parent of the host when the IP WHOIS yields
    no abuse address.  A new private helper _parse_domain_whois_abuse()
    performs this lookup without the full overhead of _analyse_domain().
    Combined with the protocol-relative URL fix above, this means that the
    badshamart.com spam campaign (PBS Health News / prostate supplement)
    now correctly produces a registrar abuse contact in abuse_contacts()
    even though all four badshamart.com URL hosts were unresolvable.

  - Fixed _extract_http_urls() not extracting protocol-relative URLs
    (scheme-omitted form //domain/path).  These are used in spam messages
    as tracking pixels and click-redirect links, e.g.:
      <img src="//badshamart.com/o/2516/19142/347/US" ...>
    The leading // was not matched by either the https?:// absolute-URL
    regex or the HTML::LinkExtor filter, which also required a full scheme.
    Both passes now recognise the //domain form and normalise it to
    https://domain before adding it to the URL list.  The regex pass
    anchors the match to whitespace, quotes, or = to avoid false positives
    on CSS path segments and HTML comments.
    Discovered via a real spam message (PBS Health News / badshamart.com)
    where three click-redirect hrefs and one tracking-pixel src all used
    protocol-relative URLs, causing badshamart.com to be entirely absent
    from embedded_urls() and therefore from abuse_contacts().

  - Fixed duplicate Salesforce Marketing Cloud comment block in
    %PROVIDER_ABUSE.  A leftover comment fragment introduced during 0.03
    appeared immediately before the real Salesforce entries, causing
    cosmetic confusion in the source.  Removed the orphaned fragment.

  - Fixed two stale references to Mail::Message::Abuse in the SUPPORT POD
    section: the perldoc command example and the CPAN Testers Dependencies
    URL both still named the old module.  Both now correctly reference
    Email::Abuse::Investigator.

  New features

  - Added Blogger/Blogspot and Google Sites to the built-in provider table
    alongside the existing Google entries:
      blogspot.com       -> abuse@google.com
      blogger.com        -> abuse@google.com
      sites.google.com   -> abuse@google.com



( run in 0.491 second using v1.01-cache-2.11-cpan-9169edd2b0e )