\!/ KyuuKazami \!/

Path : /scripts/
Upload :
Current File : //scripts/dovecot_maintenance

#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - scripts/dovecot_maintenance               Copyright 2017 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package scripts::dovecot_maintenance;

=pod

=head1 NAME

dovecot_maintenance - Run nightly maintenance for dovecot which includes
                      expunging trash and purging deleted messages from
                      mdbox.

=head1 SYNOPSIS

/usr/local/cpanel/scripts/dovecot_maintenance [options]

    Options:
      --help       This help message
      --background Run in the background

=head1 DESCRIPTION

B<This program> will expire any in accordance with the
expire_trash and expire_trash_ttl settings in the
dovecot service (AdvConfig) configuration.

All deleted email will be purged from mdbox users
who have logged in since this script was last run.

This program will also purge all expired APNs
registrations

=cut

use strict;
use Cpanel::IONice             ();
use Cpanel::PwCache            ();
use Cpanel::PwCache::Build     ();
use Cpanel::Config::LoadCpConf ();
use Cpanel::Config::LoadConfig ();
use Cpanel::ConfigFiles        ();
use Cpanel::Dovecot            ();
use Cpanel::Dovecot::Utils     ();
use Cpanel::AdvConfig          ();
use Cpanel::Locale             ();

use Cpanel::AcctUtils::DomainOwner::Tiny ();
use Cpanel::AcctUtils::Lookup            ();
use Cpanel::FileUtils::Open              ();
use Cpanel::Email::Exists                ();

use Cpanel::FileUtils::Dir ();

use Cpanel::SQLite::Compat ();

use DBD::SQLite         ();
use Cpanel::DBI::SQLite ();

use Cpanel::APNS::Mail::DB ();

use File::Path   ();
use Getopt::Long ();
use Pod::Usage   ();
use Umask::Local ();
use Try::Tiny;

our $DAYS_TO_KEEP_APNS_REGISTRATIONS = 7;

my $background = 0;
my $help       = 0;

unless ( caller() ) {
    Getopt::Long::GetOptions( 'background' => \$background, 'help' => \$help );
    Pod::Usage::pod2usage( -verbose => 2 ) if $help;

    if ($background) {
        require Cpanel::Daemonizer::Tiny;
        my $pid = Cpanel::Daemonizer::Tiny::run_as_daemon(
            sub {
                ####
                # The next two calls are unchecked because it cannot be captured when running as a daemon
                Cpanel::FileUtils::Open::sysopen_with_real_perms( \*STDERR, $Cpanel::ConfigFiles::CPANEL_ROOT . '/logs/error_log', 'O_WRONLY|O_APPEND|O_CREAT', 0600 );
                open( STDOUT, '>&', \*STDERR ) || warn "Failed to redirect STDOUT to STDERR";

                exit( __PACKAGE__->script() );
            }
        );
    }
    else {
        exit( __PACKAGE__->script() );
    }
}

our $DEFAULT_IO_NICE = 7;

sub script {
    my ($class) = @_;

    my $self = bless {}, $class;

    $self->_init();

    local $| = 1;
    my $exit_status = 0;

    # Order matters since for mdbox expunge will only mark it for purge
    foreach my $op (qw(_expunge_trash _purge_deleted_messages _purge_expired_xaps_registrations)) {
        try {
            $self->$op();
        }
        catch {
            warn $_;
            $exit_status = 1;
        };
    }

    return $exit_status;
}

sub _init {
    my ($self) = @_;

    $self->{'expires_dbh'} = Cpanel::DBI::SQLite->connect(
        {
            db => $Cpanel::Dovecot::SQLITE_EXPIRES_DB_FILE,

            #fail if there’s no database file
            sqlite_open_flags => DBD::SQLite::OPEN_READWRITE(),
        }
    );

    Cpanel::SQLite::Compat::upgrade_to_wal_journal_mode_if_needed( $self->{'expires_dbh'} );

    $self->{'mailbox_formats'} = scalar Cpanel::Config::LoadConfig::loadConfig( "/etc/mailbox_formats", undef, ": " );
    $self->{'dovecot_conf'}    = Cpanel::AdvConfig::load_app_conf('dovecot');

    Cpanel::AcctUtils::DomainOwner::Tiny::build_domain_cache();
    Cpanel::PwCache::Build::init_passwdless_pwcache();

    return;
}

sub _ionice {
    my ($self) = @_;
    return if $self->{'did_ionice'};
    $self->{'did_ionice'} = 1;
    my $cpconf_ref = Cpanel::Config::LoadCpConf::loadcpconf();
    if ( Cpanel::IONice::ionice( 'best-effort', exists $cpconf_ref->{'ionice_dovecot_maintenance'} ? $cpconf_ref->{'ionice_dovecot_maintenance'} : $$DEFAULT_IO_NICE ) ) {
        print "[dovecot_maintenance] Setting I/O priority to reduce system load: " . Cpanel::IONice::get_ionice() . "\n";
    }
    return 1;
}

sub _purge_deleted_messages {
    my ($self) = @_;

    return if !-d $Cpanel::Dovecot::LASTLOGIN_DIR;    # may not be created yet

    my $nodes_ar = Cpanel::FileUtils::Dir::get_directory_nodes($Cpanel::Dovecot::LASTLOGIN_DIR);
    my $locale   = $self->_locale();
    foreach my $username (@$nodes_ar) {
        if ( index( $username, q{__cpanel__service__auth__} ) == -1 && $self->_has_mdbox($username) ) {
            $self->_ionice();
            print $locale->maketext( "Purging deleted messages for “[_1]” …", $username );
            Cpanel::Dovecot::Utils::purge($username);
            print $locale->maketext("Done") . "\n";
        }

        if ( -d "$Cpanel::Dovecot::LASTLOGIN_DIR/$username" ) {

            # Handle user/sent logins
            try {
                File::Path::rmtree("$Cpanel::Dovecot::LASTLOGIN_DIR/$username");
            }
            catch {
                local $@ = $_;
                warn;
            };
        }
        else {
            # Handle normal logins
            unlink("$Cpanel::Dovecot::LASTLOGIN_DIR/$username");
        }
    }

    return 1;

}

sub _expunge_trash {
    my ($self) = @_;

    return 1 unless $self->{'dovecot_conf'}->{'expire_trash'};

    my $TTL         = $self->{'dovecot_conf'}->{'expire_trash_ttl'} // 30;         # auto expunge messages in trash older than 30 days
    my $EXPIRE_TIME = ( time() - ( 86400 * $TTL ) );
    my $valid_rows  = $self->_get_valid_rows_and_with_invalid_usernames_removed(
        'expires',
        "SELECT username,mailbox FROM expires WHERE expire_stamp > 0 AND expire_stamp < $EXPIRE_TIME;"
    );

    my $locale = $self->_locale();
    foreach my $row (@$valid_rows) {
        $self->_ionice();
        print $locale->maketext( "Expiring trash for “[_1]” in the “[_2]” mailbox …", $row->{'username'}, $row->{'mailbox'} );
        if ( Cpanel::Dovecot::Utils::expunge( $row->{'username'}, $row->{'mailbox'}, $TTL ) ) {
            $self->{'expires_dbh'}->do( "UPDATE expires set expire_stamp=strftime('%s', 'now') - 60 where username=? AND mailbox=?", undef, $row->{'username'}, $row->{'mailbox'} );
        }
        print $locale->maketext("Done") . "\n";
    }
    return 1;
}

sub _locale {
    my ($self) = @_;
    return ( $self->{'locale'} ||= Cpanel::Locale->get_handle() );
}

sub _get_valid_rows_and_with_invalid_usernames_removed {
    my ( $self, $table, $sql ) = @_;

    my $dbh = $self->_get_dbh_from_table_name($table);

    my $sql_query = $dbh->prepare($sql);

    $sql_query->execute() or die "Cannot execute: $sql: " . $dbh->errstr();

    my ( $valid_rows, $invalid_users_hr ) = $self->_find_valid_users_from_query($sql_query);

    $self->_delete_invalid_username_rows_from_table( $table, $invalid_users_hr );

    return $valid_rows;
}

sub _delete_invalid_username_rows_from_table {
    my ( $self, $table, $invalid_users_hr ) = @_;

    my $dbh = $self->_get_dbh_from_table_name($table);

    foreach my $user ( keys %$invalid_users_hr ) {
        $dbh->do( "DELETE from $table WHERE username=?", undef, $user ) or warn $dbh->errstr();
    }
    return;
}

sub _get_dbh_from_table_name {
    my ( $self, $table ) = @_;

    if ( $table eq 'expires' ) {
        return $self->{'expires_dbh'};
    }

    die "Unknown table '$table': implementor error!";
}

sub _has_mdbox {
    my ( $self, $username ) = @_;

    my $system_user;

    # get_system_user generates an exception when the user or the
    # domain does not exist. UserNotFound/DomainDoesNotExist.
    #
    # anything else is a fail
    try {
        $system_user = Cpanel::AcctUtils::Lookup::get_system_user($username);
    }
    catch {
        local $@ = $_;
        die if !try { $_->isa('Cpanel::Exception::UserNotFound') || $_->isa('Cpanel::Exception::DomainDoesNotExist') };
    };

    return 0 if !$system_user;

    # The email account may have a different setting than the main account, so
    # we check here.
    if ( $username =~ tr{@}{} ) {
        my ( $user, $domain ) = split /@/, $username;
        my $homedir = Cpanel::PwCache::gethomedir($system_user);

        # cannot have mdbox if there is no dir
        if ( !-d "$homedir/mail/$domain/$user/storage" ) {
            if ( !$! ) {
                warn "“$homedir/mail/$domain/$user/storage” exists but isn’t a directory??";
            }
            elsif ( !$!{'ENOENT'} ) {
                warn "stat($homedir/mail/$domain/$user/storage) as EUID $>: $!";
            }

            return 0;
        }

        my $size = ( stat("$homedir/mail/$domain/$user/mailbox_format.cpanel") )[7];
        if ( !$size ) {
            require Cpanel::AcctUtils::Lookup::MailUser;

            # no mailbox_format.cpanel file? fallback to the logic
            # we use to lookup a user
            my $response;
            try {
                $response = Cpanel::AcctUtils::Lookup::MailUser::lookup_mail_user( $username, q{} );
            }
            catch {
                local $@ = $_;
                warn;
            };
            if ( $response && $response->{'user_info'}{'mailbox'}{'format'} eq 'mdbox' ) {
                return 1;
            }
            return 0;
        }
        return $size == length 'mdbox' ? 1 : 0;
    }

    return $self->{'mailbox_formats'}->{$system_user} eq 'mdbox' ? 1 : 0;
}

sub _find_valid_users_from_query {
    my ( $self, $query ) = @_;

    my ( @valid, %invalid );

  EXPIRED_ENTRY:
    while ( my $entry = $query->fetchrow_hashref() ) {
        local $@;
        if (
            !try {
                my $system_user = Cpanel::AcctUtils::Lookup::get_system_user( $entry->{'username'} );
                local $Cpanel::homedir = Cpanel::PwCache::gethomedir($system_user);
                Cpanel::Email::Exists::pop_exists( split( q{@}, $entry->{'username'} ) );
            }
        ) {
            print "$entry->{'username'} does not exist. Removing stale entries.\n";
            $invalid{ $entry->{'username'} } = 1;
            next EXPIRED_ENTRY;
        }

        push @valid, $entry;
    }
    return ( \@valid, \%invalid );
}

sub _purge_expired_xaps_registrations {
    my ($self) = @_;

    return Cpanel::APNS::Mail::DB->new()->purge_registrations_older_than($DAYS_TO_KEEP_APNS_REGISTRATIONS);
}

1;

@KyuuKazami