#!/usr/bin/perl
# v6.0.25

use strict;
use Fcntl qw(:flock);
use Sys::Hostname;
use File::Copy;
use Digest::SHA;
use DB_File;

#use File::stat; #This will break it without changes.
use lib '/usr/local/noc/lib/dnsexport-DNSZoneParse';
require DNS::ZoneParse;

my $_PACKAGE_EXISTS_NETDNSZONEFILE;
my $_PACKAGE_EXISTS_DATAVALIDATEDOMAIN;
my $dnsexport_use_netdns = 0;
if ( -f "/usr/local/noc/etc/dnsexport_use_netdns" ) {
    $_PACKAGE_EXISTS_DATAVALIDATEDOMAIN
        = eval { require Data::Validate::Domain; };    # Use this to validate if domain and if no ending . then to add it.
    $_PACKAGE_EXISTS_NETDNSZONEFILE = eval { require Net::DNS::ZoneFile; };
    if ( defined $_PACKAGE_EXISTS_DATAVALIDATEDOMAIN && defined $_PACKAGE_EXISTS_NETDNSZONEFILE ) {
        use Data::Validate::Domain qw(is_domain);
        $dnsexport_use_netdns = 1;
    }
}

# Global stuff
my $hostname = hostname();
my $date     = localtime();
my $currzone;
my $cache_zone_dir = "/usr/local/noc/var/dnsexport-zones-cache/";
my $use_full_server_cache_if_available = 0;                      # Disable this by default due to outstanding issues that need further investigation.
if ( -f "/usr/local/noc/etc/dnsexport_use_fullservercache" ) {
    $use_full_server_cache_if_available = 1;
}

# Catch ZoneParse errors:
*DNS::ZoneParse::carp = sub {
    my $err = shift;
    if ( defined $_PACKAGE_EXISTS_NETDNSZONEFILE && $dnsexport_use_netdns ) {
        if   ( $err =~ m/IN\s+TYPE257/ ) { }
        elsif   ( $err =~ m/IN\s+CAA/ ) { }
        else                             { log_it("ZoneParse(NetDNS) error for $currzone: $err"); }
    } else {
        log_it("ZoneParse error for $currzone: $err");
    }
};

# Now check for requirements for the zones to be checked properly
if ( !-x '/usr/sbin/named-checkzone' ) {
    log_it("On checking for /usr/sbin/named-checkzone - failed.\n");
    exit 1;
}

# Check cache dir exists
if ( !-d "$cache_zone_dir" ) {
    mkdir $cache_zone_dir, 0600;
}

# TODO: handle overriding $use_full_server_cache_if_available with override disable taking preference over override enabled.

# Corrects ownership and permissions of cached zonefile, can also be used on the folder of said cache.
# TODO: look at removing $ and moving this down into subroutine bit.
sub correct_zonefile_ownership($) {
    my $zonefile               = shift || return 0;
    my $zonefile_correct_owner = "named";
    my $zonefile_correct_perm  = "600";
    return 0 if ( !-e "$zonefile" );
    my $zonefile_owner = getpwuid( ( stat($zonefile) )[4] );
    my $user_uid       = getpwnam($zonefile_correct_owner);
    my $user_gid       = getgrnam($zonefile_correct_owner);
    chown $user_uid, $user_gid, $zonefile;
    my $zonefile_perm = sprintf( "%o", ( stat($zonefile) )[2] & 07777 );

    # For now lets keep 600 for both files and folder.
    if ( -d $zonefile ) {
        if ( $zonefile_perm ne $zonefile_correct_perm ) { chmod( 0600, $zonefile ); }
    } else {
        if ( $zonefile_perm ne $zonefile_correct_perm ) { chmod( 0600, $zonefile ); }
    }
}

# Make sure cache dir is owned correctly:
correct_zonefile_ownership($cache_zone_dir);

# DB for cache zones hash
my $cache_zone_hashdb = $cache_zone_dir . "zones_hash.bdb";
my $cache_zones_db    = tie my %cache_zones_db, 'DB_File', $cache_zone_hashdb, O_RDWR | O_CREAT, 0644, $DB_HASH
    or die "Can't tie DB hash: $!\n";
correct_zonefile_ownership($cache_zone_hashdb);

# Get the suspended domains - ONCE!
#my @suspended_domains = &get_suspended();
# Disabled for now.
my @suspended_domains;

# We eval this so we can handle an alarm locally
my $command;
eval {
    # Local sigalrm callback
    local $SIG{ALRM} = sub { print "Input timeout\n"; exit; };

    # Try and grab from STDIN
    alarm 10;
    $command = <STDIN>;
    alarm 0;
};

# See what we need to do
if ($command) {

    # Strip out the command
    $command =~ s/^\s+//;
    $command =~ s/\s+$//;

    # Do we want one zone?
    if ( $command =~ /^get zone (.+)$/ ) { dump_zones($1); }

    # Do we want all zones?
    elsif ( $command =~ /^get sub6hidednszones/ ) { dump_zones(); }

    # Do we want timestamps?
    elsif ( $command =~ /^get stamps$/ ) { getstamps(); }

    # Who knows?
    else { print "Unknown command specified\n"; exit 1; }
} else {
    print "No command specified\n";
    exit 1;
}

# Sync cache zone db and close down
$cache_zones_db->sync;
untie $cache_zones_db;

exit 0;    # normal exit from main program.

###################################################################################
###### Subroutines ################################################################
###################################################################################

sub log_it {
    my $message = shift;
    return if ( !$message );

    if ( open( my $sm, "|-", "/usr/sbin/sendmail -t -oi" ) ) {
        print $sm "From: dnsexport-alert\@$hostname\n";
        print $sm "To: sys-alerts\@clook.net\n";
        print $sm "Reply-To: devnull\@clook.net\n";
        print $sm "Subject: $hostname : DNS Export error.\n\n";
        print $sm "[$date] $message\n";
        close($sm) or warn "Could not close sendmail fh: $!\n";
    } else {
        warn "Could not open sendmail fh: $!\n";
    }

    if ( open( my $log, '>>', '/var/log/dump_zones' ) ) {
        flock( $log, 2 ) or warn "Could not get a lock on the log fh: $!\n";
        print $log "[$date] $message\n";
        close($log) or warn "Could not close log fh: $!\n";
    } else {
        warn "Could not open log fh: $!\n";
    }
}

###################################################################################
# Sub to load suspended domains
###################################################################################
sub get_suspended {
    my @suspended_users;
    my @suspended_domains;

    # Loop though the users
    while ( glob "/var/cpanel/suspended/*" ) {
        if ( $_ =~ /\/var\/cpanel\/suspended\/(.+)/ ) {

            # Add them to the array
            push( @suspended_users, $1 );
        }
    }

    # Loop though the domains
    if ( open( my $userdomains, '<', '/etc/userdomains' ) ) {
        while (<$userdomains>) {
            chomp $_;
            my ( $domain, $user ) = split( /: /, $_ );

            # If the user is suspended add the domain to the array
            if ( grep( /^$user$/, @suspended_users ) ) {
                push( @suspended_domains, $domain );
            }
        }
        close($userdomains);
    }

    # Return the suspended domains
    return @suspended_domains;
}

###################################################################################
# Sub to return the hash of a file, returning sha1 and using system tool for now
###################################################################################
sub sha1_file {
    my $file = shift;
    if ( -e $file ) {
        my $sha1 = Digest::SHA->new('sha1');
        open( my $sha1_fh, '<', $file ) or return 0;
        $sha1->addfile($sha1_fh);
        close($sha1_fh);
        return $sha1->hexdigest;

        #my $sha1 = `sha1sum '$file' | awk '{print \$1}'`;
        #chomp($sha1);
        #return $sha1;
    } else {
        return 0;
    }
}

###################################################################################
# Sub to cache the zones
###################################################################################
sub cache_zone {
    my $zone_name = shift;

    my $zone_file       = "/var/named/${zone_name}.db";
    my $cache_zone_file = $cache_zone_dir . "${zone_name}.db";

    if ( !-f "$zone_file" ) {
        logme("[ERROR] cache_zone failed: zone_file does not exist: ${zone_file}");
        return 0;
    }

    # First check if already exists, if not then copy
    if ( !-f $cache_zone_file ) {

        # Check to make sure the zone we wish to cache via copy is OK
        my $checkzone_output = `/usr/sbin/named-checkzone $zone_name $zone_file`;

        # If it's not then log and return failure
        if ( $? ne 0 ) {
            log_it( "[ERROR] cache_zone checkzone failure on '" . $zone_name . "' invalid file '$zone_file' :" . $! . $checkzone_output );
            return 0;
        }

        copy( $zone_file, $cache_zone_file ) or warn "cache filed: $zone_file to $cache_zone_file: $!";
        correct_zonefile_ownership($cache_zone_file);

        # For now we do the same thing if a cache entry exists or not
        if   ( $cache_zones_db{$zone_file} ) { $cache_zones_db{$zone_file} = sha1_file($cache_zone_file); }
        else                                 { $cache_zones_db{$zone_file} = sha1_file($cache_zone_file); }
    } else {

        # If hashes do not match, then copy.
        if ( sha1_file($zone_file) ne sha1_file($cache_zone_file) ) {

            # Check to make sure the zone we wish to cache via copy is OK
            my $checkzone_output = `/usr/sbin/named-checkzone $zone_name $zone_file`;

            # If it's not then log and return failure
            if ( $? ne 0 ) {
                log_it( "[ERROR] cache_zone checkzone failure on '" . $zone_name . "' invalid file '$zone_file' :" . $! . $checkzone_output );
                return 0;
            }

            copy( $zone_file, $cache_zone_file ) or warn "cache filed: $zone_file to $cache_zone_file: $!";
            correct_zonefile_ownership($cache_zone_file);

            # For now we do the same thing if a cache entry exists or not
            if   ( $cache_zones_db{$zone_file} ) { $cache_zones_db{$zone_file} = sha1_file($cache_zone_file); }
            else                                 { $cache_zones_db{$zone_file} = sha1_file($cache_zone_file); }
        }
    }

}

###################################################################################
# Sub to cleanup cache zones
###################################################################################
sub cleanup_cache_zones {

    # TODO:
    # My thought is based upon $cache_zone_dir if last touch is more than say 1d
    # then loop through said folder and if a zone exists there that do not in
    # /var/named then remove it
}

###################################################################################
# Sub to cache zone to full server zone to save I/O
###################################################################################
sub dump_full_server_cache {
    my $zone_file_count = shift;

    my $return_cache;

    my $count_start = 0;
    my $count_end   = 0;

    open( my $cfz, '<', '/var/named/full_server_zones.cache' );
    while (<$cfz>) {
        $return_cache .= $_;
        if    ( $_ =~ m/^\$start/ ) { $count_start++; }
        elsif ( $_ =~ m/^\$end/ )   { $count_end++; }
    }
    close($cfz);

    if ( $zone_file_count == $count_start && $zone_file_count == $count_end ) {
        return $return_cache;
    } else {
        return 0;
    }
}

###################################################################################
# Sub to cache zone to full server zone to save I/O
###################################################################################
sub append_zone_to_full_server_cache {
    my $zone_data = shift;

    open( my $cfz, '>>', '/var/named/full_server_zones.cache.' . $$ );
    print $cfz $zone_data;
    close($cfz);

}

###################################################################################
# Sub to sync zone cache tmp to live
###################################################################################
sub sync_to_full_server_cache {
    my $zone_file_count = shift;

    my $file                   = "/var/named/full_server_zones.cache.$$";
    my $full_server_cache_file = "/var/named/full_server_zones.cache";

    # Clean up any old cache files, use find for now
    `find /var/named -maxdepth 1 -mmin +30 -type f -name 'full_server_zones.cache.[0-9]*' -delete`;

    if ( -f $file ) {

        my $count_start = 0;
        my $count_end   = 0;

        open( my $cfz, '<', $file );
        while (<$cfz>) {
            if    ( $_ =~ m/^\$start/ ) { $count_start++; }
            elsif ( $_ =~ m/^\$end/ )   { $count_end++; }
        }
        close($cfz);

        if ( $zone_file_count == $count_start && $zone_file_count == $count_end ) {
            rename $file, $full_server_cache_file;
            return 1;
        } else {
            log_it(
                "[sync_to_full_server_cache] sync to full server cache failed as zones in new cache did not match expected: Start:[$zone_file_count|$count_start] End:[$zone_file_count|$count_end]"
            );
            unlink $file;
            return 0;
        }
    } else {
        return 0;
    }

}

###################################################################################
# Sub to dump out the zones
###################################################################################
sub dump_zones {

    # If we have an argument, push it to @zone_files, otherwise push all zones there.
    my $zone_name = shift;
    my @zone_files;
    if ( $zone_name && -f "/var/named/$zone_name.db" ) {
        push @zone_files, "/var/named/$zone_name.db";
    } else {
        @zone_files = glob "/var/named/*.db";
    }

    # Output the start line if no argument (i.e. doing all zones)
    print "#Start zones#\n" if !$zone_name;

    # TODO: handle the caching of the big zone and return if no changes.
    # This needs to handle checking if any recent changes and also verify the full server zone file is OK to return or fallback.
    if ( $use_full_server_cache_if_available == 1 ) {
        my $var_named_zonechanges_recent = `find /var/named -maxdepth 1 -mmin -2 -type f -name '*.db' | wc -l`;

        # If no recent changes, assume OK to send cache if it's OK
        if ( $var_named_zonechanges_recent == 0 ) {
            my $full_server_cache_available = 0;
            my $full_server_cache_file      = "/var/named/full_server_zones.cache";
            if ( -f $full_server_cache_file ) {

                # TODO: improve this check, it is less than ideal.
                my $full_server_cache_file_mtime     = ( stat($full_server_cache_file) )[9];
                my $full_server_cache_file_timestamp = localtime($full_server_cache_file_mtime);
                my $check_if_zones_newer = `find /var/named -maxdepth 1 -type f -name '*.db' -newermt '$full_server_cache_file_timestamp' | wc -l`;

                if ( $check_if_zones_newer == 0 ) {
                    $full_server_cache_available = 1;
                }
            }
            if ($full_server_cache_available) {

                # TODO: Currently only send number of zone files, maybe send array instead so have list of domains expect and can return any failed.
                my $zone_file_count        = scalar @zone_files;
                my $dump_full_server_cache = dump_full_server_cache($zone_file_count);
                if ( $dump_full_server_cache && $dump_full_server_cache ne '0' ) {
                    print $dump_full_server_cache;
                    print "#End zones#\n";
                    return;
                } else {
                    log_it("[dump_zones] Full server cache dump failed, failover to dumping each zone file.");
                }
            } else {
                log_it("[dump_zones] Full server cache unavailable.");
            }
        }
    }

    # Loop though the zone files
    for (@zone_files) {
        my $zone_file = $_;

        # Skip if the file looks dodgy or is the rvskin db
        next if ( $zone_file !~ /\/var\/named\/(.*)\.db/ || $zone_file =~ /\.zz\.db$/ );
        my $zone_name = $1;

        # Skip if the zone is suspended
        next if ( grep $_ eq $zone_name, @suspended_domains );

        # Check when the zonefile was last changed
        my @stat  = stat($zone_file);
        my $ctime = $stat[10];
        my $now   = time();

        if ( !$ctime ) {

            # Couldn't get ctime - log it
            if ( -f $zone_file ) {
                log_it("Zone: $zone_name - couldn't get ctime from $zone_file setting to time().");

                # Rather than skip, going to instead set the ctime to now and just pay the cost of checking.
                $ctime = time();

                #next;
            } else {

                #log_it("Zone: $zone_name - couldn't get ctime from $zone_file as zone has been removed.");
                next;
            }
        }

        # If the file has been recently updated, check the zone's syntax through named-checkzone:
        if ( ( $now - $ctime ) < 86400 ) {    #modified in last 24 hours
            my $output = `/usr/sbin/named-checkzone $zone_name $zone_file`;
            if ( $? ne 0 ) {

                # Try to serve the last good one.
                my $cache_zone_file = $cache_zone_dir . $zone_name . ".db";
                if ( -f $cache_zone_file ) {
                    my $cache_output = `/usr/sbin/named-checkzone $zone_name $cache_zone_file`;

                    # checkzone failed on cached zone, that is not supposed to happen.
                    if ( $? ne 0 ) {
                        log_it( "Zone cached '" . $zone_name . "' invalid file '$cache_zone_file' :" . $! . $cache_output );
                        next;
                    } else {
                        log_it( "Zone '" . $zone_name . "' invalid so serving cached zone: " . $! . $output );
                        print dump_zone( $zone_name, 1 );
                        if ( $use_full_server_cache_if_available == 1 ) { append_zone_to_full_server_cache( dump_zone( $zone_name, 1 ) ); }
                        next;
                    }
                } else {
                    log_it( "Zone not cached '" . $zone_name . " file not found : '" . $cache_zone_file . "'" );
                }

                # checkzone has failed for some reason, need to log and alert.
                log_it( "Zone '" . $zone_name . "' invalid: " . $! . $output );
                next;
            }
        }

        # Cache the zone
        cache_zone($zone_name);

        # Still here? Zone must be ok. Parse the zone file
        print dump_zone($zone_name);

        # Update full server zone file
        if ( $use_full_server_cache_if_available == 1 ) { append_zone_to_full_server_cache( dump_zone($zone_name) ); }
    }

    # Sync full server cache
    if ( $use_full_server_cache_if_available == 1 ) {
        my $zone_file_count = scalar @zone_files;
        sync_to_full_server_cache($zone_file_count);
    }

    # Output the end line if no argument
    print "#End zones#\n" if !$zone_name;
}

###################################################################################
# Sub to dump out a single zone
###################################################################################
sub dump_zone {

    # Grab the zone name
    my $zone_name  = shift;
    my $get_cached = shift || 0;

    # Parse the zone file
    $currzone = $zone_name;

    # Check if we want to serve cached, otherwise get from expected /var/named
    my ( $zonefile, $netdns_zonefile );
    if ( $get_cached == 1 ) {
        $zonefile = DNS::ZoneParse->new( $cache_zone_dir . $zone_name . '.db', $zone_name );

        if ( defined $_PACKAGE_EXISTS_NETDNSZONEFILE && $dnsexport_use_netdns ) {
            $netdns_zonefile = Net::DNS::ZoneFile->new( $cache_zone_dir . $zone_name . '.db', $zone_name );
        }
    } else {
        $zonefile = DNS::ZoneParse->new( '/var/named/' . $zone_name . '.db', $zone_name );

        if ( defined $_PACKAGE_EXISTS_NETDNSZONEFILE && $dnsexport_use_netdns ) {
            $netdns_zonefile = Net::DNS::ZoneFile->new( '/var/named/' . $zone_name . '.db', $zone_name );
        }
    }

    # To return zone data, save to this variable and it will be return at the end of the function
    my $return_zone_data;

    # Grab the soa
    my $soa = $zonefile->soa();
    if ( $soa->{'serial'} ) { $return_zone_data .= "\$start " . $zone_name . ":" . $soa->{'serial'} . "\n"; }
    my $zone_origin = $zone_name . ".";
    my $zone_ttl    = $soa->{'ttl'} ? $soa->{'ttl'} : "14400";

    $return_zone_data .= "\$ORIGIN " . $zone_origin . "\n";
    $return_zone_data .= "\$TTL " . $zone_ttl . "\n";
    $return_zone_data .= "@ " . $zone_ttl . " IN SOA " . $soa->{'primary'} . " " . $soa->{'email'} . " (";
    $return_zone_data .= $soa->{'serial'} . " ";
    $return_zone_data .= $soa->{'refresh'} . " ";
    $return_zone_data .= $soa->{'retry'} . " ";
    $return_zone_data .= $soa->{'expire'} . " ";
    $return_zone_data .= $soa->{'minimumTTL'} . ")\n";

    foreach ( @{ $zonefile->ns() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin                    ? "@"               : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " NS " . $_->{'host'} . "\n";
    }

    foreach ( @{ $zonefile->mx() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin                    ? "@"               : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " MX " . ( defined $_->{'priority'} ? $_->{'priority'}  : 1 ) . " " . $_->{'host'} . "\n";
    }

    foreach ( @{ $zonefile->a() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin                    ? "@"               : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " A " . $_->{'host'} . "\n";
    }

    foreach ( @{ $zonefile->cname() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin                    ? "@"               : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " CNAME " . $_->{'host'} . "\n";
    }

    foreach ( @{ $zonefile->aaaa() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin                    ? "@"               : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " AAAA " . $_->{'host'} . "\n";
    }

    foreach ( @{ $zonefile->srv() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'port'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin ? "@" : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " SRV " . ( $_->{'priority'} ? $_->{'priority'} : '0' );
        $return_zone_data .= " " . ( $_->{'weight'} ? $_->{'weight'} : '0' ) . " " . $_->{'port'} . " " . $_->{'host'} . "\n";
    }

    foreach ( @{ $zonefile->txt() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'text'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin ? "@" : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );

        # Handle current issues with Cpanel generated DKIM public TXT entry
        # Replaced with general method
        #if (   $_->{'text'} =~ m/^(v=DKIM1;\s?k=rsa;\s?p=.+"\s+)([^"].+;?)$/
        #    || $_->{'text'} =~ m/^(v=DKIM1;\s?t=s;\s?k=rsa;\s?p=.+"\s+)([^"].+;?)$/
        #    || $_->{'text'} =~ m/^(v=DKIM1;\s?p=.+"\s+)([^"].+;?)$/
        #    || $_->{'text'} =~ m/^(k=rsa;\s?p=.+"\s+)([^"].+;?)$/ )
        #{
        #    my $formated_txt_field = "${1}\"${2}";
        #    $return_zone_data .= " " . $_->{'class'} . " TXT \"" . $formated_txt_field . "\"\n";
        #} else {
        #    $return_zone_data .= " " . $_->{'class'} . " TXT \"" . $_->{'text'} . "\"\n";
        #}

        # Workaround end for versions lower than Perl 5.14.0 {revert to old method}
        #$return_zone_data .= " " . $_->{'class'} . " TXT \"" . $_->{'text'} =~ s/\"(\s+)([^\"]|$)/\"\1\"\2/g . "\"\n";

        # General method to handle all current known cases.
        $_->{'text'} =~ s/(\" +)([^ ;"])/$1\"$2/g;
        my $qcount = () = $_->{'text'} =~ /"/g;
        if ( $qcount % 2 ) {

            # If there are an odd number of quotes in the output, add one at the start, but not the end.
            $return_zone_data .= " " . $_->{'class'} . " TXT \"" . $_->{'text'} . "\n";
        } else {

            # If there are an even number of quotes in the output, add one at each end.
            $return_zone_data .= " " . $_->{'class'} . " TXT \"" . $_->{'text'} . "\"\n";
        }

    }

    foreach ( @{ $zonefile->ptr() } ) {
        next if ( !$_->{'name'} || !$_->{'class'} || !$_->{'host'} );
        $return_zone_data .= ( $_->{'name'} eq $zone_origin                    ? "@"               : $_->{'name'} );
        $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
        $return_zone_data .= " " . $_->{'class'} . " PTR " . $_->{'host'} . "\n";
    }

    # Doing Net::DNS::ZoneFile for CAA record
    if ( defined $_PACKAGE_EXISTS_NETDNSZONEFILE && $dnsexport_use_netdns ) {

        #if (! $netdns_zonefile ) {} # Investigate handling errors.
        foreach ( $netdns_zonefile->read ) {
            my @caa = ();
            if ( $_->type eq 'CAA' ) {
                my $_temp = {};
                $_temp->{'name'} = $_->owner;
                if ( is_domain( $_temp->{'name'} ) ) { $_temp->{'name'} =~ s/([^\.])$/$1\./; }
                if ( $_temp->{'name'} =~ /^\.$/ )    { $_temp->{'name'} = "@"; }
                $_temp->{'ttl'}      = $_->ttl;
                $_temp->{'class'}    = $_->class;
                $_temp->{'flags'}    = $_->flags;
                $_temp->{'tag'}      = $_->tag;
                $_temp->{'provider'} = $_->value;
                push( @caa, $_temp );
            }
            foreach (@caa) {
                next if ( !$_->{'name'} || !$_->{'class'} || $_->{'flags'} !~ /^\d$/ || !$_->{'tag'} || !$_->{'provider'} );
                $return_zone_data .= ( $_->{'name'} eq $zone_origin ? "@" : $_->{'name'} );
                $return_zone_data .= ( ( $_->{'ttl'} && $_->{'ttl'} != $soa->{'ttl'} ) ? " " . $_->{'ttl'} : "" );
                if ( $_->{'flags'} > '1' ) { $_->{'flags'} = 0; }
                $return_zone_data .= " " . $_->{'class'} . " CAA " . $_->{'flags'} . " " . $_->{'tag'} . " \"" . $_->{'provider'} . "\"\n";
            }
        }
    }

    $return_zone_data .= "whatserveristhishostedonatsub6-wibble-wobble-123-fake-entry IN TXT \"" . hostname() . "\"\n";

    $return_zone_data .= "\$end " . $zone_name . "\n";

    return $return_zone_data;
}

###################################################################################
# Sub to list timestamps for all zonefiles
###################################################################################
sub getstamps {
    my $zonedir = "/var/named";

    my @dbfiles = `find $zonedir -type f -maxdepth 1 -name "*.db" -printf "%p||%T@\n"`;
    for (@dbfiles) {
        chomp $_;
        my ( $file, $ts ) = split( /\|\|/, $_ );
        $file =~ s|$zonedir/||;
        print "$file||$ts\n";
    }
    print "###EOL###\n";
}

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