Current File : //scripts/convert_roundcube_mysql2sqlite
#!/usr/local/cpanel/3rdparty/bin/perl

# cpanel - scripts/convert_roundcube_mysql2sqlite  Copyright 2022 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package Script::RCube::Mysql2Sqlite;

use strict;
## no critic (RequireUseWarnings)
use Try::Tiny;

use Cpanel::AccessIds::ReducedPrivileges ();
use Cpanel::Config::LoadCpConf           ();
use Cpanel::MysqlUtils                   ();
use Cpanel::DbUtils                      ();
use Cpanel::MysqlUtils::Connect          ();
use Cpanel::MysqlUtils::Command          ();
use Cpanel::MysqlRun                     ();
use Cpanel::Config::LoadUserDomains      ();
use Cpanel::Email::RoundCube             ();
use Cpanel::Email::RoundCube::DBI        ();
use Cpanel::Quota::Temp                  ();

use Cpanel::PwCache       ();
use Cpanel::Logger        ();
use Cpanel::Filesys::Home ();

use File::Basename ();
use File::Copy     ();
use File::Path     ();
use File::Slurper  ();
use DBI;
use XML::Simple ();
use IPC::Open3  ();

$XML::Simple::PREFERRED_PARSER = "XML::SAX::PurePerl";

my $sqlite_table_file = '/usr/local/cpanel/base/3rdparty/roundcube/SQL/sqlite.initial.sql';

my $log_file = '/usr/local/cpanel/logs/roundcube_sqlite_convert_log';
my %opts     = ( 'alternate_logfile' => $log_file );
my $logger   = Cpanel::Logger->new( \%opts );

my $mysql_dbname = 'roundcube';
my $dbh;

# If this updates, then do_all_rcube_xml_to_db() needs to be updated to match
my @dumptables = qw(users identities contacts contactgroups contactgroupmembers calendars caldav_calendars events caldav_events attachments caldav_attachments itipinvitations responses);

my $time = time();

## if invoked as a script, there is nothing in the call stack
my $invoked_as_script = !caller();
__PACKAGE__->script(@ARGV) if ($invoked_as_script);

my $mysql_plus_sqlite;

sub script {
    my ( $package, $opt_user, $opt_dbname ) = @_;
    my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf();

    my $db_type = 'mysql';
    if ( exists $cpconf->{'roundcube_db'} ) {
        $db_type           = 'sqlite' if $cpconf->{'roundcube_db'} eq 'sqlite';
        $mysql_plus_sqlite = $cpconf->{'roundcube_db'} eq 'mysql_plus_sqlite';
    }

    if ( defined $opt_user ) {
        ## the optional user arg feature is currently *only* called as a script;
        ##   otherwise, all these exits would check $invoked_as_script

        ## Transfers.pm is the only user of $opt_user, which it uses in conjunction with $opt_user;
        ##   this changes the global variable $opt_dbname just in time to be used in &init_check
        if ( defined $opt_dbname ) {
            $mysql_dbname = $opt_dbname;
        }

        if ( $db_type eq 'sqlite' && init_check($cpconf) ) {
            my $wasSuccess = convert_mysql_roundcube_to_sqlite($opt_user);
            if ($wasSuccess) {

                ## Transfers.pm operates over a different dbname, and is responsible for the
                ##   removal of the temporary database
                unless ( defined $opt_dbname ) {
                    Cpanel::Email::RoundCube::archive_and_drop_mysql_roundcube($logger);
                }
                return 1;
            }
            die "Conversion for user '$opt_user' was not successful.\n";
        }

        ## SOMEDAY @GOLIVE: update-roundcube-sqlite-db gets moved to update-roundcube-db. Update this message and the conditional @GOIVE.
        die "The optional user argument is to be used only when Roundcube has been converted to sqlite. Use bin/update-roundcube-sqlite-db Exiting.\n";
    }

    ## Has the roundcube conversion already happened?
    ## note: this check is here, and not in init_check, because Transfers.pm calls into this
    ##   from a different context. The src server uses MySQL but the dest machine has done
    ##   the sqlite conversion.
    if ( $db_type eq 'sqlite' ) {
        $logger->info("Roundcube conversion already occurred, bailing out.");
        if ($invoked_as_script) {
            exit(0);
        }
        return 1;
    }

    unless ( init_check($cpconf) ) {
        if ($invoked_as_script) {
            exit(0);
        }
        return 1;
    }

    my %TRUEDOMAINS;
    Cpanel::Config::LoadUserDomains::loadtrueuserdomains( \%TRUEDOMAINS );
    my @users          = sort values %TRUEDOMAINS;
    my $success_cnt    = 0;
    my $success_verify = scalar @users;

    for my $user (@users) {
        $success_cnt += convert_mysql_roundcube_to_sqlite($user);
    }

    unless ( $success_cnt == $success_verify ) {
        $logger->warn("Roundcube Mysql to sqlite conversion was not completely successful. Please check $log_file for details.");

        if ($invoked_as_script) {
            exit(1);
        }
        return;
    }

    ## Conversion is a success, so switch Roundcube configuration to use SQLite before dropping the database.
    my $DIR = '/usr/local/cpanel/base/3rdparty';
    Cpanel::Email::RoundCube::generate_roundcube_config_sqlite( $DIR, $logger );

    ## if the conversion was a complete success (as checked above), we can safely archive and
    ##   delete the MySQL roundcube database. This will prevent future problems with transfers,
    ##   and in fact is the final solution for the original Roundcube case 12162!
    ## already asserted via &init_check
    my $archive_success = Cpanel::Email::RoundCube::archive_and_drop_mysql_roundcube($logger);

    require Cpanel::Config::CpConfGuard;
    my $cpconf_guard = Cpanel::Config::CpConfGuard->new();
    $cpconf->{'roundcube_db'} = $cpconf_guard->{'data'}->{'roundcube_db'} = 'sqlite';
    $cpconf_guard->save();

    if ($invoked_as_script) {
        Cpanel::Email::RoundCube::restart_cpsrvd();
        exit(0);
    }

    return 1;
}

sub convert_mysql_roundcube_to_sqlite {
    my ($user) = @_;
    my @domains = Cpanel::Email::RoundCube::collect_domains($user);

    my @uid_name_pairs = collect_roundcube_user_info( \@domains, $user );

    ## returning 1: no conversion necessary
    return 1 unless ( scalar @uid_name_pairs );

    ## e.g. '/home'
    my $best_mnt_point   = Cpanel::Filesys::Home::get_homematch_with_most_free_space();
    my $tmpdir_root_base = "$best_mnt_point/roundcube_convert";

    if ( -d $tmpdir_root_base ) {
        rename( $tmpdir_root_base, "$tmpdir_root_base.$time" );
    }
    mkdir($tmpdir_root_base);

    my $tmpdir_root_sys = "$tmpdir_root_base/$user";
    mkdir($tmpdir_root_sys);

    my @pwinfo = Cpanel::PwCache::getpwnam($user);
    my ( $mmuid, $mmgid, $homedir ) = @pwinfo[ 2, 3, 7 ];
    my $tmpdir_emailuser_base = "$homedir/tmp/roundcube_convert";

    my $success_cnt = 0;
    my %has_sqlite_trapper_keeper;

    for my $email_user_info (@uid_name_pairs) {
        my ( $uid, $email_user ) = @$email_user_info;

        if ($mysql_plus_sqlite) {
            my $db_path = _roundcube_db_path( $user, $homedir, $email_user );
            $has_sqlite_trapper_keeper{$db_path} = -f $db_path;
            next if $has_sqlite_trapper_keeper{$db_path};
        }

        my $tmpdir_root_sys_emailuser = "$tmpdir_root_sys/$email_user";
        mkdir($tmpdir_root_sys_emailuser);

        do_mysqldumps_for_user( $uid, $tmpdir_root_sys_emailuser );
    }

    ## ensures that $best/rcube/$sysuser/$emailuser is readable by $sysuser,
    ##   so that $sysuser can move it to their $homedir after the setuid
    my $tempquota = Cpanel::Quota::Temp->new( user => $user, log => 1 );
    $tempquota->disable();

    my $rv_chmod = system("chmod -R 700 $tmpdir_root_sys");
    my $rv_chown = system("chown -Rf $mmuid:$mmgid $tmpdir_root_sys");

    for my $email_user_info (@uid_name_pairs) {
        my ( $uid, $email_user ) = @$email_user_info;

        if ($mysql_plus_sqlite) {
            my $db_path = _roundcube_db_path( $user, $homedir, $email_user );
            if ( $has_sqlite_trapper_keeper{$db_path} ) {
                $logger->info("$email_user already has an SQLite DB, skipping...");
                $success_cnt++;
                next;
            }
        }

        chdir($tmpdir_root_base);

        my $tmpdir_root_sys_emailuser = "$tmpdir_root_sys/$email_user";
        my $rv_convert                = xml_to_sqlite( $user, $tmpdir_root_sys_emailuser, $tmpdir_emailuser_base, $email_user, $homedir );

        unless ($rv_convert) {
            $logger->warn("Conversion was not successful for user '$user'.");
        }
        $success_cnt += $rv_convert;

        ## note: no need to "rmdir($tmpdir_root_sys_emailuser)", as this dir was moved
        ##   to $homedir/tmp/rcube/$emailuser (during the setuid)

        ## SOMEDAY @GOLIVE: remove the mysql roundcube database (steal mysql/roundcube backup/archive
        ##   clauses from update-roundcube-db, the mysql version)
    }

    $tempquota->restore();

    rmdir($tmpdir_root_sys);
    rmdir($tmpdir_root_base);

    return $success_cnt == scalar(@uid_name_pairs);
}

sub collect_roundcube_user_info {
    my ( $ar_domains, $user ) = @_;
    my $regexp = '@(' . join( '|', map { s/\./\\./gr } @$ar_domains ) . ')$';
    my @ids;

    try {
        @ids = $dbh->selectall_array( "SELECT user_id, username FROM users WHERE username REGEXP ? or username = ?", {}, $regexp, $user );
    };

    return @ids;
}

## Stolen from pkgacct; needs to be modularized!
## SOMEDAY: this version has converged; move pkgacct version to module, and get rid of below
sub mysqldumpdb {
    my ($args) = @_;

    my @options   = @{ $args->{'options'} };
    my $db        = $args->{'db'};
    my $table     = $args->{'table'};
    my $file      = $args->{'file'};
    my $file_mode = $args->{'append'} ? '>>' : '>';

    # for testing
    my @extra_options = _get_extra_options();

    my $mysqldump = Cpanel::DbUtils::find_mysqldump();
    my @db        = ($db);
    if ($table) {
        push @db, $table;
    }

    my $pid = IPC::Open3::open3( my $w, my $r, '', $mysqldump, @extra_options, @options, @db );

    my $first_line = 1;
    if ( open( my $fh, $file_mode, $file ) ) {
        while (<$r>) {
            if ( $first_line && ( !$_ || m/^mysqldump:/ ) ) {
                warn join( '.', @db ) . ': ' . $_;
                close $w;
                close $r;
                waitpid( $pid, 0 );
                $first_line = 0;
                my $mysqlcheck = Cpanel::DbUtils::find_mysqlcheck();
                system( $mysqlcheck, '--repair', @extra_options, @db );
                $pid = IPC::Open3::open3( $w, $r, '', $mysqldump, @extra_options, @options, @db );
            }
            else {
                print {$fh} $_;
            }
        }
    }
    close $w;
    close $r;
    waitpid( $pid, 0 );
    return;
}

sub do_mysqldumps_for_user {
    my ( $uid, $tmp_convertdir ) = @_;

    for my $tbl (@dumptables) {
        ## these two tables do not key off user_id
        next if grep { $tbl eq $_ } qw{contactgroupmembers events ical_events caldav_events attachments ical_attachments caldav_attachments caldav_calendars};

        ## XML output, not suppressing table structure, with a where clause on user_id list
        my @opts = ( '--xml', '-w', qq{user_id = $uid} );
        mysqldumpdb(
            {
                'options' => [@opts],
                'db'      => $mysql_dbname,
                'file'    => "$tmp_convertdir/rcube.$tbl.xml",
                'table'   => $tbl
            }
        );
    }

    ## contactgroupmembers is gathered in two steps, otherwise would be a LEFT JOIN that
    ##   &mysqldumpdb can not currently support

    my $contacts = _get_ids_from_col( 'contactgroups', 'contactgroup_id', $uid, 'user_id' );
    _dump_stuff_in( 'contactgroupmembers', 'contactgroup_id', $contacts, $tmp_convertdir ) if scalar(@$contacts);

    # Handle events & attachments for calendaring, as those are identified by
    # calendar & event respectively.
    my $cals = _get_ids_from_col( "calendars", 'calendar_id', $uid, 'user_id' );
    next if !scalar(@$cals);
    my @events2export;
    my @attachments2export;
    foreach my $cal (@$cals) {
        my $events = _get_ids_from_col( "events", 'event_id', $cal, 'calendar_id' );

        # Can't have attachments if no events exist to attach em to, so
        # next here is appropriate.
        next if !scalar(@$events);
        push @events2export, @$events;

        my $attachments = _get_ids_from_col( "attachments", 'attachment_id', $events, 'event_id' );
        next if !scalar(@$attachments);
        push @attachments2export, @$attachments;
    }
    _dump_stuff_in( "events",      'event_id',      \@events2export,      $tmp_convertdir ) if @events2export;
    _dump_stuff_in( "attachments", 'attachment_id', \@attachments2export, $tmp_convertdir ) if @attachments2export;

    return undef;
}

# Only here so that I don't "repeat myself".
sub _get_ids_from_col {
    my ( $tbl, $col, $search_term, $filter_on, $filter_mode ) = @_;
    my @ids;
    try {

        # Quoting anything other than values here is actually harmful.
        # Don't do it as such. Same for IN statement here.
        my $where_clause = "WHERE $filter_on = ?";
        if ( ref $search_term eq 'ARRAY' ) {
            my $count = scalar(@$search_term);
            die "Bad call to _get_ids_from_col: no search term specified!" if !$count;
            my $qs = ( '?,' x ( $count - 1 ) ) . '?';
            $where_clause = "WHERE $filter_on IN ($qs)";
            @ids          = map { $_->[0] } $dbh->selectall_array( "SELECT $col FROM $tbl $where_clause", {}, @$search_term );
        }
        else {
            @ids = map { $_->[0] } $dbh->selectall_array( "SELECT $col FROM $tbl $where_clause", {}, $search_term );
        }
    }
    catch {
        $logger->warn( "Attempt to find necessary data from $col in $tbl failed: " . $dbh->errstr() );
    };
    return \@ids;

}

# Only here so that I don't "repeat myself".
sub _dump_stuff_in {
    my ( $tbl, $col, $in, $tmp_convertdir ) = @_;

    # It is possible that we have a very large number of ID's.
    # If so, we need to do this in chunks.
    my $chunk_size = 1000;
    my @chunks;

    while ( $in->@* ) {
        push @chunks, [ splice( $in->@*, 0, $chunk_size ) ];
    }

    my $file_num = 0;
    foreach my $chunk (@chunks) {

        my $f_name = $file_num ? "$tmp_convertdir/rcube.$tbl.$file_num.xml" : "$tmp_convertdir/rcube.$tbl.xml";
        my $csv    = join( ',', $chunk->@* );
        mysqldumpdb(
            {
                'options' => [ '--xml', '-w', qq{$col IN ($csv)} ],
                'db'      => $mysql_dbname,
                'file'    => $f_name,
                'table'   => $tbl,
            }
        );
        $file_num++;
    }

    return;
}

sub create_rcube_sqlite_tables {
    my ($dbh) = @_;
    Cpanel::Email::RoundCube::DBI::ensure_schema_update( $dbh, 'sqlite' );
    return undef;
}

# This used to do all sorts of things like telling rcube_xml_to_db what needed
# quoting, what needed to be set to NULL, etc. -- thankfully all that is
# absolutely unnecessary with sqlite3.
sub do_all_rcube_xml_to_db {
    my ( $dbh, $xmldir ) = @_;

    my $success_cnt = 0;
    ## verify the number of XML docs to convert to SQL
    my $success_verify = scalar(@dumptables);
    {
        ## note: there is a disparity in the sqlite.initial.sql, and the "table_structure" for "users";
        ##   preferences is not NULLABLE.
        my @_cols = qw(user_id username mail_host created last_login language preferences);
        $success_cnt += rcube_xml_to_db( $xmldir, 'users', $dbh, \@_cols );
    }

    {
        my @_cols = qw( identity_id user_id changed del standard name organization email
          reply-to bcc signature html_signature);
        $success_cnt += rcube_xml_to_db( $xmldir, 'identities', $dbh, \@_cols );
    }

    {
        my @_cols = qw(contact_id user_id changed del name email firstname surname vcard);
        $success_cnt += rcube_xml_to_db( $xmldir, 'contacts', $dbh, \@_cols );
    }

    ## new contactgroups table
    {
        my @_cols = qw( contactgroup_id user_id changed del name );
        $success_cnt += rcube_xml_to_db( $xmldir, 'contactgroups', $dbh, \@_cols );
    }

    ## new contactgroupmembers table
    ## guard clause: contactgroupmembers is gathered conditionally, so it is the only file which
    ##   may not exist
    if ( -e _xml_file( $xmldir, 'contactgroupmembers' ) ) {
        my @_cols = qw( contactgroup_id contact_id created );
        $success_cnt += rcube_xml_to_db( $xmldir, 'contactgroupmembers', $dbh, \@_cols );
    }
    else {
        $success_cnt++;
    }

    # Restore calendar data
    foreach my $cal_tbl (qw{calendars caldav_calendars}) {
        unless ( -e _xml_file( $xmldir, $cal_tbl ) ) {
            $success_cnt++;
            next;
        }
        my @_cols = qw( calendar_id user_id name color showalarms );
        push( @_cols, qw{ical_url ical_user ical_pass ical_last_change} )                                                   if $cal_tbl eq 'ical_calendars';
        push( @_cols, qw{readonly caldav_url caldav_tag caldav_user caldav_pass caldav_oauth_provider caldav_last_change} ) if $cal_tbl eq 'caldav_calendars';
        $success_cnt += rcube_xml_to_db( $xmldir, $cal_tbl, $dbh, \@_cols );
    }
    foreach my $evt_tbl (qw{events caldav_events}) {
        unless ( -e _xml_file( $xmldir, $evt_tbl ) ) {
            $success_cnt++;
            next;
        }
        my @_cols = qw( event_id calendar_id recurrence_id uid instance isexception created changed sequence start end recurrence title description location categories url all_day free_busy priority sensitivity status alarms attendees notifyat );
        push( @_cols, qw{ical_url ical_last_change} )                if $evt_tbl eq 'ical_events';
        push( @_cols, qw{caldav_url caldav_tag caldav_last_change} ) if $evt_tbl eq 'caldav_events';

        $success_cnt += rcube_xml_to_db( $xmldir, $evt_tbl, $dbh, \@_cols );
    }
    foreach my $atc_tbl (qw{attachments caldav_attachments}) {
        unless ( -e _xml_file( $xmldir, $atc_tbl ) ) {
            $success_cnt++;
            next;
        }
        my @_cols = qw( attachment_id event_id filename mimetype size data );
        $success_cnt += rcube_xml_to_db( $xmldir, $atc_tbl, $dbh, \@_cols );
    }
    if ( -e _xml_file( $xmldir, 'itipinvitations' ) ) {
        my @_cols = qw(token event_uid user_id event expires cancelled);
        $success_cnt += rcube_xml_to_db( $xmldir, 'itipinvitations', $dbh, \@_cols );
    }
    else {
        $success_cnt++;
    }

    ## Restore responses table, if available
    if ( -e _xml_file( $xmldir, 'responses' ) ) {
        my @_cols = qw( response_id user_id name data is_html changed del );
        $success_cnt += rcube_xml_to_db( $xmldir, 'responses', $dbh, \@_cols );
    }
    else {
        $success_cnt++;
    }

    return $success_cnt == $success_verify;
}

sub _xml_file {
    my ( $xmldir, $tbl ) = @_;
    return "$xmldir/rcube.$tbl.xml";
}

sub _get_all_xml_files {
    my ( $xmldir, $tbl ) = @_;

    opendir( my $dh, $xmldir ) or die "Cannot open directory $xmldir: $!";
    my @files = map { "$xmldir/$_" } grep { /^rcube\.$tbl\.(?:[0-9]+\.)?xml$/ } readdir($dh);
    return @files;
}

sub rcube_xml_to_db {
    my ( $xmldir, $tbl, $dbh, $ar_cols ) = @_;

    my @xml_fnames  = _get_all_xml_files( $xmldir, $tbl );
    my %common_opts = ( ForceArray => 1, KeyAttr => [], ContentKey => '__content' );

    foreach my $xml_fname (@xml_fnames) {
        ## $ref->{database}->[0]->{table_data}->[0]->{row}->[$x]->{field}->[$x]->{__content}
        my $ref;
        my $err;
        try {
            $ref = XML::Simple::XMLin( $xml_fname, %common_opts );
        }
        catch {
            $err = $_;
        };

        if ($err) {
            $logger->warn("Failed to load xml for $tbl: $err");
            return;
        }

        my $rows = $ref->{'database'}->[0]->{'table_data'}->[0]->{'row'};

        ## FWIW, Perl does not complain if $rows is undef, when called in a for loop context.
        for my $row (@$rows) {

            # XXX Placeholders don't work on columns, but stuff with dashes
            # still needs quoting. As such, quote all col names.
            # Sorry, couldn't find a way around it.
            my $update_hr = { map { $dbh->quote_identifier( $_->{'name'} ) => $_->{'__content'} || '' } @{ $row->{'field'} } };
            my $rv        = $dbh->do( rcube_make_sql( $tbl, $update_hr ) );
            unless ($rv) {
                $logger->warn( "Conversion was not successful: " . $dbh->errstr() );
                return;
            }
        }
    }
    return 1;
}

sub rcube_make_sql {
    my ( $table, $update_hr ) = @_;

    # Lets hope these actually are nonzero
    my @values = values(%$update_hr);
    my $qs     = ( '?,' x ( scalar(@values) - 1 ) ) . '?';
    return (
        "INSERT OR REPLACE INTO $table (" . join( ",", keys(%$update_hr) ) . ") VALUES ($qs)",
        undef,
        @values,
    );
}

sub check_mysqlup {
    ## note: this call is cached with a ttl of 600
    my $isrunning = Cpanel::MysqlRun::running();
    if ($isrunning) {
        return 1;
    }

    return undef;
}

sub ensure_roundcube_tables {
    my ($dbh) = @_;

    my %tables = map { $_ => undef } $dbh->tables();

    # system table is created during a migration
    my @expected = qw(session cache system);
    push( @expected, @dumptables );

    for my $exp (@expected) {
        next if $exp =~ /^caldav_|^ical_/;    ## ignoring as of 120
        if ( !exists $tables{$exp} && !exists $tables{qq{"main"."$exp"}} ) {
            $logger->warn("ERROR: missing $exp table");
            return;
        }
    }

    return 1;
}

sub init_check {
    my ($cpconf) = @_;
    ## Ensure root
    return unless ( 0 == $> );

    unless ( -e $sqlite_table_file ) {
        $logger->info("Roundcube is not installed; conversion is irrelevant.");
        return;
    }

    if ( exists $cpconf->{'skiproundcube'} and $cpconf->{'skiproundcube'} ) {
        $logger->info("Roundcube should be skipped, bailing out.");
        return;
    }

    my $mysqlup = check_mysqlup();
    unless ($mysqlup) {
        $logger->warn("Mysql not currently running, bailing out.");
        return;
    }

    ## Does this installation use Roundcube/MySQL?
    unless ( Cpanel::MysqlUtils::Command::db_exists($mysql_dbname) ) {
        $logger->warn("mysql database $mysql_dbname is missing");
        return;
    }

    my $error;

    try {
        my $dbc = Cpanel::MysqlUtils::Connect->new( database => $mysql_dbname );
        $dbh = $dbc->db_handle();
    }
    catch {
        $error = $_;
    };

    if ($error) {
        $logger->warn("Failed to connect to MySQL database '$mysql_dbname'.");
        return;
    }

    return 1;
}

##############################################################
# These functions allow tests to use a temporary MySQL server

sub _get_extra_options_string {

    if ( scalar _get_extra_options() ) {
        return join( ' ', _get_extra_options() );
    }

    return;
}

sub _get_extra_options {
    return @Cpanel::MysqlUtils::_EXTRA_MYSQL_ARGS;
}

#
##############################################################

sub xml_to_sqlite {
    my ( $user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir ) = @_;
    my $code_ref = _closure_maker( $user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir );

    #fixup a user who's purposefully deleted their "etc" dir
    Cpanel::AccessIds::ReducedPrivileges::call_as_user( sub { File::Path::make_path( File::Basename::dirname( _roundcube_db_path( $user, $homedir, $email_user ) ), { chmod => 0750 } ); }, $user, 'mail' );
    my $rv = Cpanel::AccessIds::ReducedPrivileges::call_as_user( $code_ref, $user );
    return $rv;
}

sub _roundcube_db_path {
    my ( $system_user, $homedir, $mail_account ) = @_;
    my ( $mail_user, $mail_domain ) = split( '@', $mail_account );
    my $db_path = ( defined $mail_domain ) ? "$homedir/etc/$mail_domain/$mail_user.rcube.db" : "$homedir/etc/$mail_user.rcube.db";
    return $db_path;
}

## defining as a closure, as ::run_as_user does not currently handle sub args
sub _closure_maker {
    my ( $system_user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir ) = @_;

    my $code_ref = sub {
        my $tmp_convertdir = "$tmpdir_convert_base/$email_user";

        if ( -d $tmp_convertdir ) {
            rename( $tmp_convertdir, "$tmp_convertdir.$time" );
        }

        # Copy the XML files over to the user's tempdir.
        # Hopefully these are simple renames. But directories may be on different
        # mount points requiring a copy.

        File::Path::make_path($tmp_convertdir);

        for my $table (@dumptables) {
            my @all_src = _get_all_xml_files( $tmpdir_root, $table );
            foreach my $src (@all_src) {
                if ( -e $src ) {
                    my $filename = File::Basename::basename($src);
                    my $dest     = "$tmp_convertdir/$filename";
                    File::Copy::move( $src, $dest );
                }
            }
        }

        # Clean up empty source directory.
        # This will fail if the above moves failed.

        rmdir($tmpdir_root);

        ## note: similar clause in cpsrvd to set up $ENV{'_RCUBE'}
        my ( $mail_user, $mail_domain ) = split( '@', $email_user );

        my $rcube_sqlite_loc8 = _roundcube_db_path( $system_user, $homedir, $email_user );

        if ( -e $rcube_sqlite_loc8 ) {
            rename( $rcube_sqlite_loc8, "$rcube_sqlite_loc8.$time" );
        }

        my $dbh = DBI->connect( "dbi:SQLite:dbname=$rcube_sqlite_loc8", "", "" ) or do {
            $logger->info("Connection error to $rcube_sqlite_loc8: $!");
            return;
        };
        ## note: $dbh->{sqlite_version} is confirmed 2.8.15
        create_rcube_sqlite_tables($dbh);
        return unless ensure_roundcube_tables($dbh);

        unless ( do_all_rcube_xml_to_db( $dbh, $tmp_convertdir ) ) {
            $logger->info("Conversion of roundcube XML dump to sqlite database failed");
            return;
        }

        $dbh->disconnect();

        chmod( 0600, $rcube_sqlite_loc8 );

        ## the return values on these rm calls are not critical
        ## only remove the /tmp directory if the XML converion has been successful thus far

        try {
            File::Path::remove_tree($tmp_convertdir);
        };

        ## attempt to rmdir, which will intentionally fail if any of the converts did not succeed
        rmdir($tmpdir_convert_base);

        return 1;
    };
    return $code_ref;
}

1;
Vox Kasyno Sieciowy Poglądy Fachowców i Bonusy 2025

Vox Kasyno Sieciowy Poglądy Fachowców i Bonusy 2025

Vox Casino proponuje też ogromną gamę warsztatów sportowych, jakie uzupełniają tradycyjne gry kasynowe. Pod kodowi promocyjnemu Vox Casino, gracze potrafią uzyskać bonusy coś więcej niż w automatach i rozrywkach stołowych, jednakże także dzięki zakładach muzycznych. Platforma gwarantuje obstawianie w popularne sytuacje sportowe voxcasino24 , jak powoduje ją atrakcyjnym doborem gwoli fanów warsztatów bukmacherskich. W ten sposób gracze mogą w tej chwili korzystać pochodzące z kody atrakcyjne Vox Casino jak i również odgrywać bez ryzyka, co sprawia, iż praktyka wydaje się być dostępne zarówno w celu nowych, jak i fachowych internautów. Kompletny przebieg wydaje się ciekawy oraz nie wymaga jakichkolwiek złożonych prac.

Przetestowałem sobie Roulette Green, Mega Sic Bac oraz Las Vegas Roulette – jakichkolwiek zarzutów gdy do odwiedzenia jakości transmisji, tak bardzo jak i również do samej gry. Należy pamiętać, iż żadne zapłaty w takich gracz nie zaliczają czujności do odwiedzenia warunku ruchu. Korzystając z Vox Casino kodów rabatowych, osiągasz wejście do szczególnych przywilejów, jak na przykład bezpłatne spiny, bonusowe nakłady w grę czy cashback w całej złotówkach. Vox Casino przyjmuje rozległy wybór metod płatności, w tym karty kredytowe (Visa, MasterCard), portfele elektroniczne (Skrill, Neteller) i kryptowaluty, na przykład Bitcoin.

Profesjonalni krupierzy jak i również możliwość interakcji pochodzące z innymi graczami powodują, iż rozgrywka jest niezmiernie angażująca oraz autentyczna. Ażeby całkiem skorzystać Vox Casino bonusy, warto przyjąć strategiczne postępowanie do odwiedzenia rozrywki, jakie pozwoli zoptymalizować możliwości na wygraną jak i również dobrze zarządzać przyznanymi środkami. Niżej znajdziesz parę możliwych strategii, jakie mogą pomóc ci po efektywniejszym korzystaniu z bonusów. Vox dysponuje też program lojalnościowy, gdzie gracze zdobywają punkty, obstawiając oryginalne kapitał. Owe punkty pomagają Ci ukończyć za pośrednictwem rozmaite poziomy, w poniższym Początkujący, Amator, Profesjonalny, Fachowiec, Czempion jak i również Legenda. Naprawdę, duża liczba wygranych pochodzące z bonusów dysponuje limit wypłaty oraz potrzeba obrotu, jaki to winna pozostać zaspokojony poprzednio wypłatą.

Jak korzystać wraz z kodu reklamowego w warsztaty sportowe czy bukmacherskie na stronie VoxCasino?: voxcasino24

Sterowanie w całej odrębnej partii lobby Casino Vox nie będzie ci na szczęście nastręczać niepotrzebnych problemu – w nim twórca poskąpił zakładek jak i również dodatkowych lokalizacji po menu. Firm gier nie ma zbyt dużo i operator wyróżnił tylko świeżości, chodliwe tytuły, uciechy stołowe online, Aviatora, zabawy typu crash oraz sloty. Zabawy on-line zupełnie nie doczekały uwagi własnej pozycji w karta – wydobędziemy te rolety, przewijając stronę kluczową na dół.

Bonus powitalny na start

voxcasino24

Ów klasa kodu zasilana wydaje się za sprawą nad czterdzieści renomowanych wytwórców, w tym tego typu firmy kiedy Nolimit City jak i również Big Time Gaming. Za sprawą tego gwarantujemy najlepszą klasa oraz zawrotną wielorakość komputerów. Zagrasz tutaj w szczególności przy popularne automaty online na temat rozmaitych motywach i mechanikach. Miłośnicy klasyki potrafią liczyć na ogromny selekcja gierek stołowych jak i również karcianych RNG – posiadamy szachy, ruletkę, blackjacka, a także bakarata. Zawodnicy poszukujący wrażeń oryginalnych jak i również aury realnego kasyna, mają do odwiedzenia dyspozycji bogato zaopatrzone kasyno dzięki energicznie. Poza tym udostępniamy chodliwe zabawy crash, takie jak Aviator, cechujące się zwykłym gameplayem oraz natychmiastowym biegiem rywalizacji.

W ciągu dowolną grę jak i również wykonane zadanie uzyskujesz punkty, jakie wymieniasz pod ciekawe gratyfikacyj oraz korzystasz wraz ze osobliwych możliwości. Warunki aktywacji wszystkich bonusu są wprost pewne jak i również odróżniają się w zależności od etapu oferty. Dzięki temu możesz uporządkować zastosowanie działaniu do własnego nurcie uciechy i dysponować ryzykiem pod naszych ustaleniach. Drobiazgowe dane odnośnie Vox Casino istotnie deposit bonus, będziesz wyszukać na polskiej formalnej witrynie. VOX Casino nadprogram wyjąwszy depozytu, lub nadprogram w start owo zniżki rzadkie, jakimi na osobisty rodzaj potrzebujemy zachęcić do odwiedzenia nas.

By korzystać pochodzące z szyfrów rabatowych Vox Casino oraz zdobyć dostęp do odwiedzenia niepowtarzalnych bonusów, wystarczy kilka prostych kroków. Jak się zarejestrować z systemem wydaje się być łatwa i żwawa, a Vox Casino nadprogram z brakiem depozytu wydaje się być dostępny natychmiast w całej wpisaniu należytego systemu kodowania w całej odpowiednie pole w ciągu zarejestrowania się. W ten sposób fani mają możliwość skorzystać z Vox Casino free spins i odmiennych zalety wyjąwszy potrzeby wpłacania podstawowego depozytu. Respektujemy wszelakiego gracza oraz chcemy zaoferować fascynujące aplikacje bonusowe również w celu nowicjuszy, jak i stałych konsumentów kasyna VOX. Na swoim koncie swoim odnajdziesz sporo przydatnych ofert, choćby takich jak darmowe spiny oraz środki bonusowe, które to można korzystać w całej dużej liczby pozostałych grach.

Wszyscy gracz ma swój unikalny odznaczenie ID oraz wyraźny przy prawym górnym rogu profilu status przy projekcie lojalnościowym. Regularnie aktualizujemy pferowane zakupy, aby zagwarantować tym fanom kiedy najkorzystniejsze wytyczne. Dzięki temu polski promokod może być niejednokrotnie wzbogacany na temat równoczesne bonusy, , którzy zapewnia cieszyć się grą wraz z nadal znaczniejszą zabawą. Żeby w pełni korzystać wraz z własnego promokodu, za każdym razem powinno się badać najświeższe propozycji oraz promocje dostępne w danym kasynie. Nasze kody atrakcyjne dają specjalną sposobność dzięki powiększenie szans dzięki wygraną, więc nie zapomnij, by użytkować pierwotnego bezzwłocznie.

voxcasino24

Dla przykładu, wówczas gdy kasyno proponuje 100% bonus powitalny, gracz, jaki wpłaci pięćset złotych, dostanie następujące 500 zł zdecydowanie nadprogram, jak daje jemu łącznie tysiąc zł dzięki zaczątek zabawy. Wszyscy bonus bez depozytu przy Vox Casino sprzęga się z określonymi warunkami, które to fani muszą zaspokoić zanim wypłatą wygranych. Szczegółowe doniesienia na temat wymogów ruchu istnieją w regulaminie każdej reklamy.

Normy odnoszące się do paliwa bonusowego z brakiem depozytu w VoxCasino

Vox Casino szyfr promocji wyjąwszy depozytu wydaje się być niejednokrotnie nadrzędnym powodem, w celu któregoż fani typują owe platformę, w zamian innych, które wymagają szybkiej wpłaty depozytu. Prócz bonusu powitalnego, kasyno Vox proponuje graczom propozycję cashback. Aktywni konsumenci potrafią odebrać do odwiedzenia dziesięciu% swoich przegranych zakładów. Odmienne prawidłowe zakupy zawierają darmowe spiny, bonusy bez depozytu, kody promocyjne i specjalne propozycji tymczasowe.

Nasze turnieje produkowane są we współpracy spośród wiodącymi producentami automatów do odwiedzenia gry. Na bazie jednym bądź 3 popularnych slotach, przygotowujemy zawody, gdzie ogół rotacja przybliża ciebie do zwycięstwa. Zawodnicy uzyskują punkty zbyt obstawianie, wygrywanie oraz wykonywanie niektórych zadań. Punkty tę określają obszary w całej klasyfikacji, w której każdy artykuł może być krokiem w odniesieniu do ekscytujących nagród.

Szyfr promocyjny VOX Casino — dlaczego powinno się fita skorzystać?

voxcasino24

Kod promocyjny do odwiedzenia VOX Casino może także przyjechać do Cię samodzielnie — pod Twój adres mailowy, przypisany do odwiedzenia konta pod własnej platformie. Stale sprawdzaj skrzynkę, bo czasami wysyłamy unikalne kody w specjalistyczne okazje, przykładowo na urodziny. Automaty online jest to czerwień ogłoszenia Vox Casino, pociągające zawodników rozmaitością tematów, dynamiczną rozgrywką i ogromnymi wygranymi.

Klub proponuje 3 poziomy – Złocisty, Platynowy i Diamentowy, jak i również daje dużo korzyści, w poniższym gry VIP, szybsze wypłaty oraz ekskluzywne bonusy. Szukasz licencjonowanego kasyna sieciowy, które to przynosi ekscytujące odczucia pochodzące z rozrywki, w którym miejscu elastyczna klasa kodu konsol jest wzbogacona dużym zaangażowaniem platformy po radość fanów? Twe wyszukiwania dobiegły naturalnie końca, gdyż VOX Casino jest tymże stanowiskiem, którego od chwili tak wielu lat szukałeś. W całej Casino VOX udostępniamy graczom zaawansowaną platformę hazardową z niekończącą się bibliotekę gier. Odkrywanie naszego obszernego katalogu automatów przez internet, komputerów stołowych, gierek dzięki energicznie i konsol szybkich wydaje się nieskomplikowana i potulna, na intuicyjnemu interfejsowi. Obfita podaż konsol gwarantuje doświadczyć hazardowy spektakl, a wszystko to wydaje się być okraszone hojnymi bonusami oraz rabatami, które to oczekują w Ciebie na każdym etapie zabawy.

Vox Casino Online

Dobrze chcemy zaoferować Panstwu rozległą paletę konsol, w tym także znane klasyki, oraz tę skromniej znane, jednak minimalna wartość ekscytujące. Nie czekaj, aż będzie za późno – dodaj do odwiedzenia Vox Casino już teraz i rozpocznij grupowanie paragrafów dzięki swe od razu nagrody! Nie zapomnij, ażeby systematycznie testować obiekt handlowy wraz z nagrodami, dokąd czekają  interesujące bonusy. Każdy dzienna pora owe nowa możliwość dzięki otrzymanie pobocznych paragrafów oraz wymianę ich pod pomocne gratyfikacyj. Zgłoś do mrowiska zadowolonych fanów Vox Casino jak i również sprawdź, jak łatwo wolno gromadzić bonusy wyjąwszy depozytu.


Publicado

en

por

Etiquetas: