#                                      Copyright 2024 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.
#
package Cpanel::UpdateGatherer::modules::SpamAssassin;

use Cpanel::Config::Users ();
use Cpanel::Slurper       ();

use Cwd 'abs_path';
use Cpanel::UpdateGatherer::Std;

sub compile ( $self, $meta_data ) {
    $meta_data->{spamassassin_stats} = _add_spamassassin_stats();
    return 1;
}

sub _add_spamassassin_stats {

    my $sa_stats = {
        sa_accounts                     => 0,
        spambox_accounts                => 0,
        total_email                     => 0,
        scanned                         => 0,
        not_scanned                     => 0,
        scored_spam                     => 0,
        scored_not_spam                 => 0,
        scores                          => {},
        total_custom_global_rules_files => 0,
        total_custom_user_rules_files   => 0,
        has_custom_local_cf             => 0,
    };

    _get_sa_and_spambox_accounts($sa_stats);
    _get_exim_log_stats($sa_stats);
    _get_custom_filters($sa_stats);

    return $sa_stats;
}

sub _get_sa_and_spambox_accounts ($sa_stats) {
    require Cpanel::SpamAssassin::Enable;

    my @all_users = Cpanel::Config::Users::getcpusers();

    foreach my $user (@all_users) {
        $sa_stats->{sa_accounts}++      if Cpanel::SpamAssassin::Enable::is_enabled($user);
        $sa_stats->{spambox_accounts}++ if Cpanel::SpamAssassin::Enable::is_spam_box_enabled($user);
    }

    return;
}

sub _get_exim_log_stats ($sa_stats) {
    my $exim_mainlog = _exim_mainlog();
    return if !-e $exim_mainlog;

    require Time::Piece;
    require File::ReadBackwards;

    my $timestamp_str = '[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}:[0-9]{2}';
    my $id_str        = '[0-9a-zA-Z]+-[0-9a-zA-Z]+-[0-9a-zA-Z]+';
    my $username_str  = '[a-zA-Z0-9]+';
    my $score_str     = '[0-9.-]+';

    my $timestamp_re            = qr/^($timestamp_str)/;
    my $email_completed_re      = qr/^$timestamp_str.+?($id_str) Completed/;
    my $email_marked_as_spam_re = qr/^$timestamp_str.+?($id_str).+SpamAssassin as $username_str detected message as (NOT )?spam \(($score_str)\)/;

    my @email_ids;

    my $one_day_ago = _one_day_ago();

    my $bwfh = File::ReadBackwards->new($exim_mainlog);

    # This works backwards through the logfile so the first line we see about an email is its "Completed" message.
    # It takes note of the email's id and continues backwards in the logfile until it finds a matching
    # spamassassin line. If no matching line is found for a completed email id, we know that email was not scanned.
    while ( defined( my $line = $bwfh->readline ) ) {

        if ( $line =~ $timestamp_re ) {
            my $timestamp = $1;
            my $log_time  = Time::Piece->strptime( $timestamp, '%Y-%m-%d %k:%M:%S' );

            last if $log_time < $one_day_ago;

            if ( $line =~ $email_completed_re ) {
                push( @email_ids, $1 );
                $sa_stats->{total_email}++;
            }
            elsif ( $line =~ $email_marked_as_spam_re ) {
                my $id         = $1;
                my $not_spam   = $2 // 0;
                my $spam_score = int($3);

                $sa_stats->{scores}{$spam_score}++;
                $sa_stats->{scanned}++;
                $not_spam ? $sa_stats->{scored_not_spam}++ : $sa_stats->{scored_spam}++;
                @email_ids = grep { $_ ne $id } @email_ids;
            }
        }
    }
    $bwfh->close;

    $sa_stats->{not_scanned} = scalar @email_ids;

    return;
}

sub _get_custom_filters ($sa_stats) {

    require Cpanel::Autodie;
    require Cpanel::MD5;

    my $filter_dir = _sa_filter_dir();
    Cpanel::Autodie::opendir( my $dfh, $filter_dir );

    # look for any non-symlinks ending with .cf or symlinks leading outside /usr/local/cpanel
    # skip local.cf here, it is special and handled elsewhere.
    while ( my $file = readdir($dfh) ) {
        next if $file !~ /\.cf$/;
        next if $file eq 'local.cf';

        my $fullpath = "$filter_dir/$file";

        if ( -l $fullpath ) {
            my $link_target         = abs_path($fullpath);
            my $ulc_filter_location = _ulc_filter_dir();

            if ( $link_target !~ /^$ulc_filter_location/ ) {
                $sa_stats->{total_custom_global_rules_files}++;
            }
        }
        else {
            $sa_stats->{total_custom_global_rules_files}++;
        }
    }

    # check ~/.spamassassin/user_prefs for differences with the template
    my $template_prefs = _ulc_filter_dir() . "/user_prefs.template";
    my $template_md5   = Cpanel::MD5::getmd5sum($template_prefs);
    foreach my $user ( Cpanel::Config::Users::getcpusers() ) {
        my $homedir       = Cpanel::PwCache::gethomedir($user);
        my $homedir_prefs = "$homedir/.spamassassin/user_prefs";
        next if !-f $homedir_prefs;

        if ( Cpanel::MD5::getmd5sum($homedir_prefs) ne $template_md5 ) {
            $sa_stats->{total_custom_user_rules_files}++;
        }
    }

    # Check local.cf and try to determine if it has been edited. This file is owned by a package, but
    # cpanel edits this file post install, so we can't query the package manager to determine if this
    # file has changed. We know what keys should exist in the default local.cf, so we'll look for anything
    # outside of those few keys.
    my $local_cf = _sa_filter_dir() . "/local.cf";
    if ( -f $local_cf ) {
        my @expected_keys = qw(trusted_networks ifplugin endif loadplugin resource_limit_mem dns_available);
        foreach my $line ( Cpanel::Slurper::read_lines($local_cf) ) {
            if ( $line =~ /^\s*([a-zA-Z_]+)\s+/ ) {
                my $key = $1;
                $sa_stats->{has_custom_local_cf} = 1 if !grep { $key eq $_ } @expected_keys;
            }
        }
    }

    return;
}

sub _ulc_filter_dir {
    return '/usr/local/cpanel/etc/mail/spamassassin';
}

sub _sa_filter_dir {
    return '/etc/mail/spamassassin';
}

sub _exim_mainlog {
    return '/var/log/exim_mainlog';
}

sub _one_day_ago {
    my $now = Time::Piece->new();
    return $now - 24 * 60 * 60;
}

1;
