#!%PERL%
# Copyright (c) vhffs project and its contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
#2. Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in
#   the documentation and/or other materials provided with the
#   distribution.
#3. Neither the name of vhffs nor the names of its contributors
#   may be used to endorse or promote products derived from this
#   software without specific prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
#FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
#COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
#INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# Mirroring script for exim on mx1.
# Set master & slave DB params and put it in a cron.
# Slave database must have at least vhffs_mxdomain,
# vhffs_boxes, vhffs_forward, vhffs_ml, vhffs_ml_subscribers
# and vhffs_object tables 

use DBI;
use strict;
use utf8;

# Master DB params
my $MASTER_DB_HOST = 'localhost';
my $MASTER_DB_PORT = 5432;
my $MASTER_DB_NAME = 'vhffs';
my $MASTER_DB_USER = 'vhffs';
my $MASTER_DB_PASS = 'vhffs';

# Slave DB params
my $SLAVE_DB_HOST = 'localhost';
my $SLAVE_DB_PORT = 5432;
my $SLAVE_DB_NAME = 'mailmirror';
my $SLAVE_DB_USER = 'mailmirror';
my $SLAVE_DB_PASS = 'vhffs';

# We've to connect to the master DB, fetch
# object, mxdomain, boxes, forward, ml & ml_subscribers
# tables and reinject them in slave DB
# We just fetch fields usefull for address verification

my $master_dbh = DBI->connect("DBI:Pg:dbname=$MASTER_DB_NAME;host=$MASTER_DB_HOST;port=$MASTER_DB_PORT", $MASTER_DB_USER, $MASTER_DB_PASS)
    or die("Unable to open master connection\n");

my $slave_dbh = DBI->connect("DBI:Pg:dbname=$SLAVE_DB_NAME;host=$SLAVE_DB_HOST;port=$SLAVE_DB_PORT", $SLAVE_DB_USER, $SLAVE_DB_PASS)
    or die("Unable to open slave connection\n");

# Create temporary tables
$slave_dbh->do('CREATE TEMPORARY TABLE tmp_mxdomain(LIKE vhffs_mxdomain)')
    or die("Unable to create temporary MX domain table\n");
$slave_dbh->do('CREATE TEMPORARY TABLE tmp_boxes(LIKE vhffs_boxes)')
    or die("Unable to create temporary boxes table\n");
$slave_dbh->do('CREATE TEMPORARY TABLE tmp_forward(LIKE vhffs_forward)')
    or die("Unable to create temporary forward table\n");
$slave_dbh->do('CREATE TEMPORARY TABLE tmp_ml(LIKE vhffs_ml)')
    or die("Unable to create temporary ml table\n");
$slave_dbh->do('CREATE TEMPORARY TABLE tmp_ml_subscribers(LIKE vhffs_ml_subscribers)')
    or die("Unable to create temporary ml_subscribers table\n");
$slave_dbh->do('CREATE TEMPORARY TABLE tmp_object(LIKE vhffs_object)')
    or die("Unable to create temporary object table\n");



$master_dbh->{AutoCommit} = 0;
$slave_dbh->{AutoCommit} = 0;

# We need to set transaction isolation level to serializable to avoid
# foreign key issues
$master_dbh->do('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE')
    or die("Unable to set transaction level on master DB\n");

# Replicate vhffs_object table

# Type 60 is mail objects and 61 is ml objects.
my $msth = $master_dbh->prepare(q{SELECT o.object_id, o.owner_uid,
    o.owner_gid, o.date_creation, o.state, o.description, o.refuse_reason, o.type
    FROM vhffs_object o
    WHERE o.state = 6 AND (o.type = 60 OR o.type = 61) })
    or die("Unable to prepare SELECT query for vhffs_object\n");
my $ssth = $slave_dbh->prepare(q{INSERT INTO tmp_object(object_id,
    owner_uid, owner_gid, date_creation, state, description, refuse_reason, type) VALUES(?, ?, ?, ?, ?, ?, ?, ?)}) 
    or die("Unable to prepare INSERT query for tmp_object\n");

$msth->execute()
    or die("Unable to execute SELECT query for vhffs_object\n");

while(my $row = $msth->fetchrow_hashref()) {
    $ssth->execute($row->{object_id}, $row->{owner_uid}, $row->{owner_gid},
    $row->{date_creation}, $row->{state}, $row->{description}, $row->{refuse_reason},
    $row->{type} )
    or die('Unable to insert object #'.$row->{object_id}."\n");
}

$ssth->finish();
$msth->finish();

# Replicate vhffs_mxdomain table
my $msth = $master_dbh->prepare(q{SELECT d.mxdomain_id, d.domain,
    d.boxes_path, d.catchall, d.object_id FROM vhffs_mxdomain d 
    INNER JOIN vhffs_object o ON o.object_id=d.object_id
    WHERE o.state = 6})
    or die("Unable to prepare SELECT query for vhffs_mxdomain\n");
my $ssth = $slave_dbh->prepare(q{INSERT INTO tmp_mxdomain(mxdomain_id,
    domain, boxes_path, catchall, object_id) VALUES(?, ?, ?, ?, ?)}) 
    or die("Unable to prepare INSERT query for tmp_mxdomain\n");

$msth->execute()
    or die("Unable to execute SELECT query for vhffs_mxdomain\n");

while(my $row = $msth->fetchrow_hashref()) {
    $ssth->execute($row->{mxdomain_id}, $row->{domain},
    $row->{boxes_path}, $row->{catchall}, $row->{object_id})
    or die('Unable to insert mxdomain #'.$row->{id}."\n");
}

$ssth->finish();
$msth->finish();

# Replicate vhffs_boxes table

$msth = $master_dbh->prepare(q{SELECT b.domain, b.local_part, b.domain_hash,
    b.mbox_name, b.password, b.nospam, b.novirus, b.allowpop, b.allowimap,
    b.state FROM vhffs_boxes b 
    INNER JOIN vhffs_mxdomain d ON d.domain=b.domain 
    INNER JOIN vhffs_object o ON o.object_id=d.object_id 
    WHERE o.state = 6})
    or die("Unable to prepare SELECT query for vhffs_boxes\n");
$ssth = $slave_dbh->prepare(q{INSERT INTO tmp_boxes(domain, local_part,
    domain_hash, mbox_name, password, nospam, novirus, allowpop, allowimap,
    state)
    VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)})
    or die("Unable to prepare INSERT query for tmp_boxes\n");

$msth->execute()
    or die("Unable to execute SELECT query for vhffs_boxes\n");

while(my $row = $msth->fetchrow_hashref()) {
    $ssth->execute($row->{domain}, $row->{local_part}, $row->{domain_hash},
    $row->{mbox_name}, $row->{password}, $row->{nospam}, $row->{novirus},
    $row->{allowpop}, $row->{allowimap}, $row->{state} )
            or die('Unable to insert box '.$row->{local_part}.'@'.
                $row->{domain}."\n");
}
$ssth->finish();
$msth->finish();

# Replicate vhffs_forward table

$msth = $master_dbh->prepare(q{SELECT f.domain, f.local_part, f.remote_name,
    f.password
    FROM vhffs_forward f
    INNER JOIN vhffs_mxdomain d ON d.domain = f.domain 
    INNER JOIN vhffs_object o ON o.object_id = d.object_id
    WHERE o.state = 6})
    or die("Unable to prepare SELECT query for vhffs_forward\n");
$ssth = $slave_dbh->prepare(q{INSERT INTO tmp_forward(domain,
        local_part, remote_name, password) VALUES(?, ?, ?, ?)})
    or die("Unable to prepare INSERT query for vhffs_forward\n");

$msth->execute()
    or die("Unable to execute SELECT query for vhffs_forward\n");

while(my $row = $msth->fetchrow_hashref()) {
    $ssth->execute($row->{domain}, $row->{local_part},
        $row->{remote_name}, $row->{password})
            or die('Unable to insert forward '.$row->{local_part}.
                '@'.$row->{domain}."\n");
}

$ssth->finish();
$msth->finish();

# Replicate vhffs_ml table
# Exim & listengine don't really need all the fields but the VHFFS API does.

$msth = $master_dbh->prepare(q{SELECT ml.ml_id, ml.local_part, ml.domain,
    ml.prefix, ml.object_id, ml.sub_ctrl, ml.post_ctrl, ml.reply_to, ml.open_archive,
    ml.signature FROM vhffs_ml ml
    INNER JOIN vhffs_object o ON o.object_id=ml.object_id
    WHERE o.state = 6})
    or die("Unable to prepare SELECT query for vhffs_ml\n");
$ssth = $slave_dbh->prepare(q{INSERT INTO tmp_ml(ml_id, local_part, domain,
    prefix, object_id, sub_ctrl, post_ctrl, reply_to, open_archive, 
    signature) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)})
    or die("Unable to prepare INSERT query for tmp_ml\n");

$msth->execute()
    or die("Unable to execute SELECT query for vhffs_ml\n");

while(my $row = $msth->fetchrow_hashref()) {
    $ssth->execute($row->{ml_id}, $row->{local_part}, $row->{domain},
    $row->{prefix}, $row->{object_id}, $row->{sub_ctrl}, $row->{post_ctrl},
    $row->{reply_to}, $row->{open_archive}, $row->{signature})
            or die('Unable to insert ml '.$row->{local_part}.'@'.
                $row->{domain}."\n");
}

$ssth->finish();
$msth->finish();

# Replicate vhffs_ml_subsribers table

$msth = $master_dbh->prepare(q{SELECT ms.sub_id, ms.member, ms.perm, ms.hash,
    ms.ml_id, ms.language
    FROM vhffs_ml_subscribers ms
    INNER JOIN vhffs_ml ml ON ms.ml_id = ml.ml_id
    INNER JOIN vhffs_object o ON o.object_id = ml.object_id
    WHERE o.state = 6})
    or die("Unable to prepare SELECT query for vhffs_ml_subscribers\n");
$ssth = $slave_dbh->prepare(q{INSERT INTO tmp_ml_subscribers(sub_id, member,
    perm, hash, ml_id, language) VALUES(?, ?, ?, ?, ?, ?)})
    or die("Unable to prepare INSERT query for tmp_ml_subscribers\n");

$msth->execute()
    or die("Unable to execute SELECT query for vhffs_ml_subscribers\n");

while(my $row = $msth->fetchrow_hashref()) {
    $ssth->execute($row->{sub_id}, $row->{member}, $row->{perm}, $row->{hash},
    $row->{ml_id}, $row->{language})
            or die('Unable to insert ml_subscriber #'.$row->{sub_id});
}

$ssth->finish();
$msth->finish();

# We're done fetching data
$master_dbh->disconnect();

my $count;

($count = $slave_dbh->do(q{DELETE FROM vhffs_ml_subscribers WHERE 
    sub_id NOT IN (SELECT sub_id FROM tmp_ml_subscribers)}))
    or die("Unable to delete no more existing ml users\n");
print "$count subscribers deleted\n";

($count = $slave_dbh->do(q{DELETE FROM vhffs_ml
    WHERE ml_id NOT IN (SELECT ml_id FROM tmp_ml)}))
    or die("Unable to delete no more existing ml\n");
print "$count mailing lists deleted\n";

($count = $slave_dbh->do(q{DELETE FROM vhffs_forward
    WHERE (local_part, domain)
    NOT IN (SELECT local_part, domain FROM tmp_forward)}))
    or die("Unable to delete no more existing forwards\n");
print "$count forwards deleted\n";

($count = $slave_dbh->do(q{DELETE FROM vhffs_boxes
    WHERE (local_part, domain)
    NOT IN (SELECT local_part, domain FROM tmp_boxes)}))
    or die("Unable to delete no more existing boxes\n");
print "$count boxes deleted\n";

($count = $slave_dbh->do(q{DELETE FROM vhffs_mxdomain
    WHERE (domain) NOT IN (SELECT domain FROM tmp_mxdomain)}))
    or die("Unable to DELETE no more existing domains\n"); 
print "$count domains deleted\n";

($count = $slave_dbh->do(q{DELETE FROM vhffs_object
    WHERE object_id NOT IN(SELECT object_id FROM vhffs_object)}))
    or die("Unable to delete no more existing objects\n");
print "$count objects deleted\n";

# Boxes & forwards don't need to be updated since all data are
# unique (ie. for us, a box/forward is only deleted or inserted).

# The only potential change in object is the description.
# Type and state are always set to 61 and 6 for us, owner_uid can't be
# changed.
($count = $slave_dbh->do(q{UPDATE vhffs_object SET description = tmp.description,
    refuse_reason = tmp.refuse_reason, owner_uid = tmp.owner_uid, owner_gid = tmp.owner_gid,
    state = tmp.state FROM tmp_object tmp WHERE tmp.object_id = vhffs_object.object_id}))
    or die("Unable to update object table\n");

# For now, others fields aren't updatable
$slave_dbh->do(q{UPDATE vhffs_mxdomain SET catchall = tmp.catchall
    FROM tmp_mxdomain tmp WHERE tmp.domain = vhffs_mxdomain.domain})
    or die("Unable to update catchall for existing domains\n");

# Password should be deleted from master DB...
$slave_dbh->do(q{UPDATE vhffs_boxes SET password = tmp.password,
    nospam = tmp.nospam, novirus = tmp.novirus, allowpop = tmp.allowpop,
    allowimap = tmp.allowimap, state = tmp.state
    FROM tmp_boxes tmp WHERE tmp.local_part = vhffs_boxes.local_part AND
    tmp.domain = vhffs_boxes.domain})
    or die("Unable to update boxes data\n");

$slave_dbh->do(q{UPDATE vhffs_forward SET remote_name = tmp.remote_name,
    password = tmp.password FROM tmp_forward tmp
    WHERE tmp.local_part = vhffs_forward.local_part AND
    tmp.domain = vhffs_forward.domain})
    or die("Unable to update forwards data\n");

$slave_dbh->do(q{UPDATE vhffs_ml_subscribers SET perm = tmp.perm,
    hash = tmp.hash, language = tmp.language FROM tmp_ml_subscribers tmp
    WHERE tmp.sub_id = vhffs_ml_subscribers.sub_id})
    or die("Unable to update subscribers data\n");

$slave_dbh->do(q{UPDATE vhffs_ml SET prefix = tmp.prefix,
    sub_ctrl = tmp.sub_ctrl, post_ctrl = tmp.post_ctrl,
    reply_to = tmp.reply_to, open_archive = tmp.open_archive,
    signature = tmp.signature FROM tmp_ml tmp
    WHERE tmp.ml_id = vhffs_ml.ml_id})
    or die("Unable to update mailing lists data\n");


# Insert new boxes/forward/ml/domains

($count = $slave_dbh->do(q{INSERT INTO vhffs_object(object_id, owner_uid, owner_gid,
    date_creation, state, description, refuse_reason, type) SELECT object_id,
    owner_uid, owner_gid, date_creation, state, description, refuse_reason, type
    FROM tmp_object tmp WHERE tmp.object_id NOT IN(SELECT object_id FROM vhffs_object)}))
    or die("Unable to insert new objects\n");
print "$count objects inserted\n";

($count = $slave_dbh->do(q{INSERT INTO vhffs_mxdomain(mxdomain_id, domain,
    boxes_path, catchall, object_id)
    SELECT mxdomain_id, domain, boxes_path, catchall,
    object_id FROM tmp_mxdomain 
    WHERE mxdomain_id NOT IN(SELECT mxdomain_id FROM vhffs_mxdomain)}))
    or die("Unable to insert new mail domains\n");
print "$count domains inserted\n";

($count = $slave_dbh->do(q{INSERT INTO vhffs_boxes(domain, local_part, domain_hash,
    mbox_name, password, nospam, novirus, allowpop, allowimap, state)
    SELECT domain, local_part, domain_hash, mbox_name, password, nospam, novirus,
    allowpop, allowimap, state FROM tmp_boxes tmp 
    WHERE (tmp.local_part, tmp.domain)
    NOT IN(SELECT local_part, domain FROM vhffs_boxes)}))
    or die("Unable to insert new boxes\n");
print "$count boxes inserted\n";

($count = $slave_dbh->do(q{INSERT INTO vhffs_forward(domain, local_part, remote_name,
    password)
    SELECT domain, local_part, remote_name, password FROM tmp_forward tmp
    WHERE (tmp.local_part, tmp.domain) NOT IN (SELECT local_part, domain
    FROM vhffs_forward)}))
    or die("Unable to insert new forwards\n");
print "$count forwards inserted\n";

($count = $slave_dbh->do(q{INSERT INTO vhffs_ml(ml_id, domain, local_part, prefix,
    object_id, sub_ctrl, post_ctrl, reply_to, open_archive, signature)
    SELECT ml_id, domain, local_part, prefix, object_id, sub_ctrl, post_ctrl,
    reply_to, open_archive, signature
    FROM tmp_ml tmp WHERE tmp.ml_id NOT IN (SELECT ml_id FROM vhffs_ml)}))
    or die("Unable to insert new ml\n");
print "$count mailing lists inserted\n";

($count = $slave_dbh->do(q{INSERT INTO vhffs_ml_subscribers(sub_id, member, perm, hash,
    ml_id, language) SELECT sub_id, member, perm, hash, ml_id, language FROM
    tmp_ml_subscribers ms WHERE ms.sub_id NOT IN(SELECT sub_id FROM
    vhffs_ml_subscribers)}))
    or die("Unable to insert new subscribers\n");
print "$count subscribers inserted\n";
$slave_dbh->commit();
$slave_dbh->disconnect();

