diff --git a/bin/check_password_expiration.pl b/bin/check_password_expiration.pl new file mode 100755 index 0000000000000000000000000000000000000000..5021fb7e18d9d24abc6e45e98b01da5b819ad8b3 --- /dev/null +++ b/bin/check_password_expiration.pl @@ -0,0 +1,546 @@ +#!/usr/bin/perl + +# ------------------------------------------------------------------------------ +# $Id$ +# +# ------------------------------------------------------------------------------ + +use strict; +use warnings; +use Getopt::Long; +use Data::Dumper; +use Config::IniFiles; +use Net::LDAP; +use File::Copy; +use File::Basename; +use Sys::Hostname; +use DateTime; +use MIME::Lite; + +# unbuffered output: +$| = 1; + +use lib ( new Config::IniFiles( -file => "/opt/etc/ini/global.ini" )->val( 'APPLICATION', 'LIBRARY' ) ); + +BEGIN { + my $iniFile = new Config::IniFiles( -file => "/opt/etc/ini/global.ini" ); + push( @INC, $iniFile->val( 'APPLICATION', 'LIBRARY' ) ); +} + +use SNET::common; +use SNET::snmpd; +use SNET::LdapNS qw(:all); +use DateTime::Format::LDAP; + +use vars qw($verbose $debug $help $force $cli_mode $dry_run ); +$verbose = 0; +$debug = 0; +$cli_mode = 1; + +my $PROGNAME = basename( $0 ); +$PROGNAME =~ s/\.p[lm]$//; + +my %options = ( + "help" => \$help, + "debug" => \$debug, + "verbose" => \$verbose, + "force" => \$force, + "dry-run" => \$dry_run, +); + +my $SNMP_ENTERPRISEOID = "53"; +my $SNMP_OID = "1.3.6.1.4.1.99999.$SNMP_ENTERPRISEOID"; +my $SNMP_GEN = "6"; +my $SNMP_SPE = "1"; +my $msg = ''; +my $title = "Check Password"; + +help() if !GetOptions( %options ) or $help; +$verbose = 1 if $debug; + +$dry_run = 1; +metaprint('warning', "Dry-run is activated") if $dry_run; + +# ldap_find_users_and_groups() +# +# Read users and groups from SNet LDAP. + +sub ldap_find_users_in_group ($$$$$$$$$$) +{ + my ( + $cfg_ldap_server, $cfg_ldap_user, $cfg_ldap_passwd, $cfg_ldap_group_search, $cfg_ldap_search_scope, + $cfg_ldap_group_search_filter, $cfg_ldap_group_attribute, $cfg_ldap_groupname, $hostname, $cfg_ldap_cafile + ) = @_; + + my %users; + + # Connect to the LDAP server + metaprint( 'verbose', "Initiating connection to LDAP server <$cfg_ldap_server>:" ) if $verbose; + my $ldap = Net::LDAP->new( + $cfg_ldap_server, + async => 0, + onerror => ( + ( $debug == 0 ) ? sub { return $_[0] } : sub { + my $message = shift; + my $error = defined( $message->error_desc ) ? $message->error_desc : $message->error(); + $msg = "Ldap: Unable to process request: $error."; + metaprint( 'error', $title . ": " . $msg ); + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + return $message; + } + ), + ); + if ( !$ldap ) { + $msg = "LDAP connection to <$cfg_ldap_server> failed."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + metaprint( 'verbose', "* LDAP connection completed successfully." ) if $verbose; + + my $message; + eval { + print STDERR 'Starting tls' . "\n" if ( $debug ); + $message = $ldap->start_tls( verify => 'require', + cafile => $cfg_ldap_cafile, ); + if ( $message->is_error() ) { + $msg = "Could not encrypt LDAP connection."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + }; + if ( $@ ) { + $msg = "Crash - Could not encrypt LDAP connection."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + + eval { + print STDERR 'binding' . "\n" if ( $debug ); + $message = $ldap->bind( + $cfg_ldap_user, + password => $cfg_ldap_passwd, + version => 3, + ); + if ( $message->is_error() ) { + $msg = "LDAP bind error occurred."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + }; + if ( $@ ) { + $msg = "Crash - LDAP bind error occurred ('" . $message->error_name . "')."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + + metaprint( 'verbose', "* LDAP bind operation completed successfully." ) if $verbose; + + # Search AD for objects in a particular group using LDAP + + metaprint( 'info', "Getting the LDAP member with expiration." ) if $verbose; + my %searchargs; + $searchargs{base} = $cfg_ldap_group_search; + $searchargs{scope} = $cfg_ldap_search_scope; + $searchargs{filter} = $cfg_ldap_group_search_filter; + $searchargs{attrs} = $cfg_ldap_group_attribute; + + print Dumper( \%searchargs ) if $verbose; + + my $results; + eval { $results = $ldap->search( %searchargs ); }; + if ( $@ ) { + my $title = "Check Password"; + my $msg = "Crash - LDAP Users Search."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + + if ( $results->is_error() ) { + metaprint( 'error', 'search failed: ' . $results->error_text ); + metaprint( 'error', 'search failed: ' . $results->code ); + metaprint( 'error', 'search failed: ' . $results->error ); + } elsif ( $results->count() == 0 ) { + metaprint( 'error', 'no result' ); + } else { + metaprint( 'verbose', "* Search returned " . $results->count . " object." ) if $verbose; + print Dumper( $results->as_struct() ) if $verbose; + my $ldap_hash = $results->as_struct(); + + my $attribute = $searchargs{attrs}[0]; + if ( defined( $ldap_hash->{ "cn=$cfg_ldap_groupname," . $searchargs{base} }{$attribute} ) ) { + foreach my $url ( @{ $ldap_hash->{ "cn=$cfg_ldap_groupname," . $searchargs{base} }{$attribute} } ) { + + print "$url\n" if $verbose; + push( @{ $users{$url}{'groups'} }, $cfg_ldap_groupname ); + + } + } else { + metaprint( 'error', "Could not parse the hash result: {" . "cn=$cfg_ldap_groupname," . $searchargs{base} . "} { " . $attribute . " }" ); + } + } + + print "\nClosing LDAP connection.\n" if $verbose; + $ldap->unbind; + return %users; +} + +sub ldap_find_users ($$$$$$$$$$) +{ + my ( + $cfg_ldap_server, $cfg_ldap_user, $cfg_ldap_passwd, $cfg_ldap_group_search, $cfg_ldap_search_scope, + $cfg_ldap_group_search_filter, $cfg_ldap_group_attribute, $cfg_ldap_groupname, $hostname, $cfg_ldap_cafile + ) = @_; + + my %users; + + # Connect to the LDAP server + metaprint( 'verbose', "Initiating connection to LDAP server <$cfg_ldap_server>:" ) if $verbose; + my $ldap = Net::LDAP->new( + $cfg_ldap_server, + async => 0, + onerror => ( + ( $debug == 0 ) ? sub { return $_[0] } : sub { + my $message = shift; + my $error = defined( $message->error_desc ) ? $message->error_desc : $message->error(); + $msg = "Ldap: Unable to process request: $error."; + metaprint( 'error', $title . ": " . $msg ); + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + return $message; + } + ), + + # debug => 15, + ); + if ( !$ldap ) { + $msg = "LDAP connection to <$cfg_ldap_server> failed."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + metaprint( 'verbose', "* LDAP connection completed successfully." ) if $verbose; + + my $message; + eval { + print STDERR 'Starting tls' . "\n" if ( $debug ); + $message = $ldap->start_tls( verify => 'require', + cafile => $cfg_ldap_cafile, ); + if ( $message->is_error() ) { + $msg = "Could not encrypt LDAP connection."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + }; + if ( $@ ) { + $msg = "Crash - Could not encrypt LDAP connection."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + + eval { + print STDERR 'binding' . "\n" if ( $debug ); + $message = $ldap->bind( + $cfg_ldap_user, + password => $cfg_ldap_passwd, + version => 3, + ); + if ( $message->is_error() ) { + $msg = "LDAP bind error occurred."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + }; + if ( $@ ) { + $msg = "Crash - LDAP bind error occurred ('" . $message->error_name . "')."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + + metaprint( 'verbose', "* LDAP bind operation completed successfully." ) if $verbose; + + # Search AD for objects in a particular group using LDAP + + metaprint( 'info', "Getting the LDAP member with expiration." ) if $verbose; + my %searchargs; + $searchargs{base} = $cfg_ldap_group_search; + $searchargs{scope} = $cfg_ldap_search_scope; + $searchargs{filter} = $cfg_ldap_group_search_filter; + $searchargs{attrs} = $cfg_ldap_group_attribute; + + print Dumper( \%searchargs ) if $verbose; + + my $results; + my $ldap_hash; + eval { $results = $ldap->search( %searchargs ); }; + if ( $@ ) { + my $title = "Check Password"; + my $msg = "Crash - LDAP Users Search."; + metaprint( 'error', $title . ": " . $msg ) if $verbose; + snmp_trap_send_multi_vars( $SNMP_OID, $SNMP_GEN, $SNMP_SPE, [ $hostname, $title, $msg ] ); + exit 1; + } + + if ( $results->is_error() ) { + metaprint( 'error', 'search failed: ' . $results->error_text ); + metaprint( 'error', 'search failed: ' . $results->code ); + metaprint( 'error', 'search failed: ' . $results->error ); + } elsif ( $results->count() == 0 ) { + metaprint( 'error', 'no result' ); + } else { + metaprint( 'verbose', "* Search returned " . $results->count . " object." ) if $verbose; + print Dumper( $results->as_struct() ) if $verbose; + $ldap_hash = $results->as_struct(); + } + + print "\nClosing LDAP connection.\n" if $verbose; + $ldap->unbind; + return $ldap_hash; +} + +sub send_email_template ($$$;$) +{ + my ( $uid, $email, $status, $days ) = @_; + + return if $dry_run; + + my $subject = 'Information - Your SNet LDAP password is '; + my $template_file = '/opt/etc/template/InfoTemplate.htm'; + + my $text = ''; + + if ( $status !~ /^LOCKED|EXPIRATION$/ ) { + return 1; + } elsif ( $status eq 'LOCKED' ) { + $subject .= 'expired.'; + $text = + 'Please note that your SNet LDAP account \'' + . $uid + . '\' is expired. Please change your password using the following URL: <ul><li><a href="https://intragate.ec.europa.eu/snet-bxl1/">SNet Portal</a></li><li> -> Authentication</li><li> -> SNet LDAP Manager</li></ul> Without this step, your SNet account is not usable.'; + } elsif ( ( $status eq 'EXPIRATION' ) && ( $days eq 0 ) ) { + $subject .= 'going to expire TODAY.'; + $text = + 'Please note that your SNet LDAP account \'' + . $uid + . '\' is going to <b>expire TODAY</b>. Please change your password now using the following URL: <ul><li><a href="https://intragate.ec.europa.eu/snet-bxl1/">SNet Portal</a></li><li> -> Authentication</li><li> -> SNet LDAP Manager</li></ul> Without this step, your SNet account will become not usable <b>Today</b>.'; + } elsif ( $status eq 'EXPIRATION' ) { + $subject .= 'going to expire soon.'; + $text = + 'Please note that your SNet LDAP account \'' + . $uid + . '\' is going to <b>expire in ' + . $days . ' day' + . ( $days > 1 ? 's' : '' ) + . '</b>. Please change your password, before it\'s expiration using the following URL: <ul><li><a href="https://intragate.ec.europa.eu/snet-bxl1/">SNet Portal</a></li><li> -> Authentication</li><li> -> SNet LDAP Manager</li></ul> Without this step, your SNet account will become not usable in a few days.'; + } + + # Open email template. + die "Session date files not found\n" if !-f "$template_file"; + open( TEMPL, "<$template_file" ); + my $htmltext = ''; + while ( my $l = <TEMPL> ) { + $l =~ s/\r*//g; + chomp( $l ); + if ( $l =~ /##TITLE##/ ) { + $l =~ s/##TITLE##/$subject/; + } elsif ( $l =~ /##DESCRIPTION##/ ) { + $l =~ s/##DESCRIPTION##/$text/; + } + + $htmltext .= $l; + } + + # + my $msg = MIME::Lite->new( + From => 'snet@ec.europa.eu', + To => $email, + Subject => $subject, + Type => 'multipart/related' + ); + + $msg->attach( + Type => 'text/html', + Data => $htmltext, + Encoding => 'quoted-printable' + ); + + $msg->attach( + Encoding => 'base64', + Type => 'image/jpg', + Path => "/opt/etc/template/snet-banner.jpg", + Id => "image", + Disposition => 'inline', + ); + + $msg->scrub( [ 'x-mailer', 'Content-Disposition' ] ); + + print $msg->as_string; + $msg->send(); +} + +# +# Global Declarations +# +# load the INI +metaprint( "info", "Loading INI file Parameters" ); +my $global_iniFile = new Config::IniFiles( -file => "/opt/etc/ini/global.ini" ); + +my $CiniFile = new Config::IniFiles( -file => $global_iniFile->val( 'INI', 'RME' ) ); +metaprint( "error", "error value of CiniFile is undefined" ) if ( !defined( $CiniFile ) ); + +my $outpath = $CiniFile->val( 'GLOBAL', 'OUTPATH' ); +metaprint( "error", "The defined outpath is not valid, please correct-it" ) if ( !defined( $outpath ) ); + +my $AiniFile = new Config::IniFiles( -file => $global_iniFile->val( 'INI', 'LDAP' ) ); +metaprint( "error", "error value of AiniFile is undefined" ) if ( !defined( $AiniFile ) ); + +my $cfg_ldap_server = $AiniFile->val( 'LDAP_SNET_NG', 'SERVER' ); +metaprint( "error", "error value of cfg_ldap_server is undefined" ) if ( !defined( $cfg_ldap_server ) ); +my $cfg_ldap_user = $AiniFile->val( 'LDAP_SNET_NG', 'USER' ); +metaprint( "error", "error value of cfg_ldap_user is undefined" ) if ( !defined( $cfg_ldap_user ) ); +my $cfg_ldap_passwd = $AiniFile->val( 'LDAP_SNET_NG', 'PASSWORD' ); +metaprint( "error", "error value of cfg_ldap_passwd is undefined" ) if ( !defined( $cfg_ldap_passwd ) ); +my $cfg_ldap_people_base = $AiniFile->val( 'LDAP_SNET_NG', 'PEO_SEARCH' ); +metaprint( "error", "error value of cfg_ldap_people_base is undefined" ) if ( !defined( $cfg_ldap_people_base ) ); +my $cfg_ldap_group_base = $AiniFile->val( 'LDAP_SNET_NG', 'GRP_SEARCH' ); +metaprint( "error", "error value of cfg_ldap_group_base is undefined" ) if ( !defined( $cfg_ldap_group_base ) ); + +my $cfg_ldap_group_search_filter = $AiniFile->val( 'LDAP_SNET_NG', 'FILTER' ); +metaprint( "error", "error value of cfg_ldap_group_search_filter is undefined" ) if ( !defined( $cfg_ldap_group_search_filter ) ); +my $cfg_ldap_group_attribute = $AiniFile->val( 'LDAP_SNET_NG', 'GRP_ATTRIBUTE' ); +metaprint( "error", "error value of cfg_ldap_group_attribute is undefined" ) if ( !defined( $cfg_ldap_group_attribute ) ); +my $cfg_ldap_search_scope = $AiniFile->val( 'LDAP_SNET_NG', 'SEARCH_SCOPE' ); +metaprint( "error", "error value of cfg_ldap_search_scope is undefined" ) if ( !defined( $cfg_ldap_search_scope ) ); +my $cfg_ldap_cafile = $AiniFile->val( 'LDAP_SNET_NG', 'CA' ); +metaprint( "error", "error value of cfg_ldap_cafile is undefined" ) if ( !defined( $cfg_ldap_cafile ) ); + +my $hostname = hostname(); + +# Main Application +metaprint( 'info', "Starting password policy check." ); + +my $expiration = 90 * 24 * 60 * 60; +my $warning = 10 * 24 * 60 * 60; +my $date_now = DateTime->now()->set_time_zone( "Europe/Luxembourg" ); +my $date_exp = DateTime->now()->set_time_zone( "Europe/Luxembourg" )->subtract( seconds => $expiration )->subtract( seconds => $warning ); +my $date_str = DateTime::Format::LDAP->format_datetime( $date_exp ); + +metaprint( 'info', "checking everything with pwdchangedtime age < '$date_str'" ); + +# -- Create the import file +my $email_to_send = (); + +metaprint( 'info', "Checking account '$cfg_ldap_group_base'." ); + +my $filter = 'bcp'; +$cfg_ldap_group_attribute = ['memberuid']; +$cfg_ldap_group_search_filter = "(&(objectclass=posixGroup)(cn=REPLACE))"; +my $cfg_ldap_group_search_f = $cfg_ldap_group_search_filter; +$cfg_ldap_group_search_f =~ s/REPLACE/$filter/; +print "$cfg_ldap_group_search_f\n" if $verbose; +metaprint( 'info', "Checking account '$cfg_ldap_group_base'." ) if $verbose; +my %ldap_bcp_users = ldap_find_users_in_group( $cfg_ldap_server, $cfg_ldap_user, $cfg_ldap_passwd, $cfg_ldap_group_base, $cfg_ldap_search_scope, + $cfg_ldap_group_search_f, $cfg_ldap_group_attribute, $filter, $hostname, $cfg_ldap_cafile ); +metaprint( 'debug', "BCP Users:" . Dumper( \%ldap_bcp_users ) ) if $verbose; + +########### + +# TODO: This should be working according to RFC, but not... +$cfg_ldap_group_search_filter = '(|(&(!(pwdChangedTime=\*))(userPassword=\*))(!(pwdChangedTime<' . $date_str . ')))'; +$cfg_ldap_group_search_filter = '(&(!(pwdChangedTime=*))(userPassword=*))'; + +# $cfg_ldap_group_search_filter = '(&(!(pwdChangedTime))(userPassword))'; +$cfg_ldap_group_search_filter = '(objectClass=posixAccount)'; +$cfg_ldap_group_attribute = [ 'uid', 'pwdchangedtime', 'pwdreset', 'mail' ]; + +$filter = ''; +metaprint( 'info', "Checking account '$cfg_ldap_people_base'." ) if $verbose; +my $ldap_users = ldap_find_users( $cfg_ldap_server, $cfg_ldap_user, $cfg_ldap_passwd, $cfg_ldap_people_base, $cfg_ldap_search_scope, + $cfg_ldap_group_search_filter, $cfg_ldap_group_attribute, $filter, $hostname, $cfg_ldap_cafile ); +metaprint( 'debug', "Matched Users:" . Dumper( $ldap_users ) ) if $verbose; + +foreach my $u ( keys %$ldap_users ) { + my $uid = $u; + $uid =~ s/^uid=//; + $uid =~ s/,.*$//; + my $email = $ldap_users->{$u}{'mail'}[0]; + + if ( lc( $email ) ne $email ) { + metaprint( 'error', "Email '$u' is not lowercase '" . $email . "'." ); + } + + # Overwrigth for DVE test + # $email = 'david.vernazobres@ext.ec.europa.eu'; + #if ( lc( $email ) ne 'david.vernazobres@ext.ec.europa.eu' ) { + # next; + #} + + if ( defined( $ldap_bcp_users{$uid} ) ) { + metaprint( 'info', "'$uid': This is a BCP account, skipping." ); + next; + } elsif ( defined( $ldap_users->{$u}{'pwdreset'} ) + && defined( $ldap_users->{$u}{'pwdreset'}[0] ) + && ( $ldap_users->{$u}{'pwdreset'}[0] eq 'TRUE' ) ) { + metaprint( 'info', "'$uid': Accound is locked." ); + send_email_template( $uid, $email, 'LOCKED' ) + + } elsif ( defined( $ldap_users->{$u}{'pwdchangedtime'} ) + && defined( $ldap_users->{$u}{'pwdchangedtime'}[0] ) + && ( $ldap_users->{$u}{'pwdchangedtime'}[0] =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/ ) ) { + + my $pass_date = DateTime->new( + year => $1, + month => $2, + day => $3, + hour => $4, + minute => $5, + second => $6, + time_zone => 'Europe/Luxembourg', + ); + my $date_duration = $date_now->subtract_datetime( $pass_date )->add( seconds => $expiration )->subtract( seconds => $warning ); + my $date_days = $date_duration->in_units( 'days' ); + if ( $date_duration->is_positive() ) { + metaprint( 'info', "'$uid': Accound validity is positiv ($date_days)." ); + if ( $date_days == 10 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_days == 7 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_days == 5 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_days == 3 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_days == 2 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_days == 1 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_days == 0 ) { + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } + } elsif ( $date_duration->is_zero() ) { + metaprint( 'info', "'$uid': Accound validity is zero ($date_days)." ); + send_email_template( $uid, $email, 'EXPIRATION', $date_days ); + } elsif ( $date_duration->is_negative() ) { + metaprint( 'info', "'$uid': Accound validity is negativ ($date_days)." ); + send_email_template( $uid, $email, 'LOCKED' ); + } else { + metaprint( 'info', "'$uid': Accound validity is should never happends ($date_days)." ); + } + } else { + metaprint( 'info', "'$uid': Accound is really locked, or not parsable." ); + } +} + +#if ( defined( $email_to_send ) && ( ( keys $email_to_send )++ > 0 ) ) { +# metaprint 'info', "some account are expired. Email need to be send." ); +#} + +metaprint( "info", "--- Process Done ---" ); +exit( 0 );