Email-Abuse-Investigator

 view release on metacpan or  search on metacpan

bin/submit_abuse_report  view on Meta::CPAN

                 // '';
    $mail_from =~ s/^\s*<?\s*|\s*>?\s*$//g;   # strip angle brackets and whitespace
    push @fields, "Original-Mail-From: <$mail_from>" if $mail_from;

    # Original-Rcpt-To -- envelope recipient(s) of the spam
    # Use the To: header; for each distinct address found
    my $rcpt_to = $inv->header_value('to') // '';
    # Extract individual addresses (bare addr@domain or <addr@domain>)
    my %rcpt_seen;
    while ($rcpt_to =~ /<?([^\s<>,;]+\@[\w.-]+)>?/g) {
        my $addr = lc $1;
        push @fields, "Original-Rcpt-To: <$addr>" unless $rcpt_seen{$addr}++;
    }

    # Arrival-Date -- when we (the reporter) received the spam
    my $arrival = $inv->header_value('date') // '';
    push @fields, "Arrival-Date: $arrival" if $arrival;

    # Reported-Domain -- the primary contact domain
    my ($rdomain) = $inv->all_domains();
    push @fields, "Reported-Domain: $rdomain" if $rdomain;

    # Reported-Uri -- each distinct URL in the spam body
    my %uri_seen;
    for my $u ($inv->embedded_urls()) {
        push @fields, "Reported-Uri: $u->{url}"
            unless $uri_seen{ $u->{url} }++;
    }

    # Authentication-Results -- forwarded verbatim from the spam headers
    my $auth_res = $inv->header_value('authentication-results') // '';
    push @fields, "Authentication-Results: $auth_res" if $auth_res;

    return join("\r\n", @fields) . "\r\n";
}

# -----------------------------------------------------------------------
# _build_mime_message( %args ) -> string
#
# Constructs a fully RFC 5965 compliant multipart/report MIME message
# suitable for transmission via Net::SMTP->datasend().
#
# Three-part structure per RFC 5965 s.2:
#   Part 1  text/plain; charset=UTF-8  -- human-readable abuse report
#   Part 2  message/feedback-report    -- ARF machine-readable metadata
#   Part 3  message/rfc822             -- original spam message verbatim
#
# All line endings in the returned string are CRLF (\r\n).
# Part 2 uses 7bit encoding (required by RFC 5965 s.3).
# -----------------------------------------------------------------------
sub _build_mime_message {
    my (%a) = @_;

    # Boundary unique per message; must not appear in any part body
    my $boundary = sprintf 'arf_report_%s_%d',
        strftime('%Y%m%d%H%M%S', gmtime), $$;

    # Normalise line endings to CRLF throughout, then encode to raw UTF-8
    # bytes.  Net::SMTP->datasend() calls syswrite() on a raw socket and
    # cannot handle Perl strings with the Unicode flag set (wide characters).
    # The body may contain non-ASCII characters (e.g. emoji in decoded subject
    # lines) so we must encode explicitly rather than rely on the socket layer.
    (my $body_crlf     = $a{body}) =~ s/\r?\n/\r\n/g;
    my $original_crlf  = '';
    if (defined $a{original} && length ${ $a{original} }) {
        ($original_crlf = ${ $a{original} }) =~ s/\r?\n/\r\n/g;
    }
    my $feedback_report = _build_feedback_report($a{inv});

    # ---- Outer envelope headers ----
    my @msg;
    push @msg, "Date: $a{date}";
    push @msg, "From: $a{from}";
    push @msg, "To: $a{to}";
    push @msg, "Subject: $a{subject}";
    push @msg, "Message-ID: $a{msg_id}";
    push @msg, "MIME-Version: 1.0";
    # multipart/report with report-type=feedback-report per RFC 5965 s.2
    push @msg, "Content-Type: multipart/report;";
    push @msg, "    report-type=feedback-report;";
    push @msg, "    boundary=\"$boundary\"";
    push @msg, "X-Mailer: Email::Abuse::Investigator submit_abuse_report";
    push @msg, "X-Report-Monitor: $a{bcc}" if $a{bcc};
    push @msg, "";

    # Preamble for non-MIME clients
    push @msg, "This is an ARF (Abuse Reporting Format) feedback report.";
    push @msg, "See https://datatracker.ietf.org/doc/html/rfc5965";
    push @msg, "";

    # ---- Part 1: human-readable summary (RFC 5965 s.2 "first part") ----
    push @msg, "--$boundary";
    push @msg, "Content-Type: text/plain; charset=UTF-8";
    push @msg, "Content-Transfer-Encoding: 8bit";
    push @msg, "Content-Disposition: inline";
    push @msg, "";
    push @msg, $body_crlf;

    # ---- Part 2: ARF machine-readable metadata (RFC 5965 s.3) ----
    push @msg, "--$boundary";
    push @msg, "Content-Type: message/feedback-report";
    push @msg, "Content-Transfer-Encoding: 7bit";   # required by RFC 5965
    push @msg, "";
    push @msg, $feedback_report;

    # ---- Part 3: original spam message (RFC 5965 s.2 "third part") ----
    # Use base64 encoding so Outlook and other clients reliably show
    # it as a downloadable .eml attachment rather than rendering it
    # inline or discarding it.
    my $original_b64 = MIME::Base64::encode_base64($original_crlf, "\r\n");
    push @msg, "--$boundary";
    push @msg, "Content-Type: application/octet-stream; name=\"original_message.eml.txt\"";
    push @msg, "Content-Transfer-Encoding: base64";
    push @msg, "Content-Disposition: attachment;";
    push @msg, "    filename=\"original_message.eml.txt\"";
    push @msg, "Content-Description: Original spam/phishing message";
    push @msg, "";
    push @msg, $original_b64;

    # Closing boundary
    push @msg, "--${boundary}--";



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