#!/usr/bin/perl
#  -- account expiration warning mail sender
#	This program sends mail to users whose account/password is going
#	to expire a due to shadow database settings. Good for users
#	that do not login for a long period, but download their mail,
#	so they get know about expiration.
#
#AUTHOR		:	Samuel Behan <behan@frida.fri.utc.sk>
#LICENSE	:	GNU GPL v2
#REQUIREMENTS	:	shadow database installed, MTA
#HOW-TO use	:	run once a day (ie. from cron)
#			for more info see README
#BUGS&TODO	:	nothing ???
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
################################################################################

$ENV{'PATH'}	= "/bin";
$ENV{'ENV'}	= undef;

################################################################################
# shadow.pl #
#############
# All function are compatible with shadow library functions as defined
#	in shadow.h file (currently reaonly)
#
# ($name, $password, $change_last, $change_may, $change_must, $change_warn,
#	$max_inactive, $expire_date)	= getsp*
#
{
package spent;
require Exporter;
@ISA	= qw(Exporter);
@EXPORT	= qw(setspent getspent getspnam endspent);
# My first perl module, so be patient about me :-(

my $open_state	= 0;
##
# Open shadow database for reading
  sub setspent
  {
    if(!open(SHADOW,"/etc/shadow"))
    { warn("setspent(): $!\n"); 
      return undef; }
    $open_state	= 1;
    return 1;
  }

##
# Close shadow database
  sub endspent
  {
    if(!$open_state)
    { return undef; }
    if(!close(SHADOW))
    { warn("endspent(): $!\n");
      return ''; }
    $open_state	= 0;
    return 1;
  }

##
# Get next entry from shadow database
  sub getspent
  {
    my ($line,@sp_ent);
    if(!$open_state)
    { setspent() || return undef; }
    if(!($line	= <SHADOW>))	#stupit way of reading file
    { return wantarray ? @sp_ent : "@sp_ent"; }
    chomp($line);
    @sp_ent	= split(/:/,$line,9);		
    return wantarray ? @sp_ent : "@sp_ent";
  }
 
##
# Get shadow entry matching name
  sub getspnam($)
  {
    if(!$open_state)
    { setspent() || return undef; }
    my ($line,$name,@sp_ent);
    while($line	= <SHADOW>)
    { chomp($line);
      $name	= $line;
      $name	=~ s/:.*//;
      if($name	eq $_[0])
      { my @sp_ent	= split(/:/,$line,9);
        return wantarray ? @sp_ent : "@sp_ent"; }
    }
    return wantarray ? @sp_ent : "@sp_ent";
  }

} 
#									       #
################################################################################

#Some internal settings : config file and messages printed if user password is
#   expiring, is expired but never inactive, expired gona inactive, inactive
$CONFIG_FILE	= '/etc/passwd_exp.conf';
$USER_EXPIRING	= " Account of '%user%' will expire in %expire_in% days on %expire_date%";
$USER_EXPIRED	= " Account of '%user%' has expired on %expire_date% ( %expire_in% days ago )";
$USER_INACTIVING= " Account of '%user%' will be inactive in %inactive_in% days on %inactive_date%";
$USER_INACTIVE	= " Account of '%user%' was inactived on %inactive_date% (%inactive_in% days ago)";
#do not modify bellow if(!$expert) B-)
$CHANGE_WARN	= 14;
$VERSION	= "passwd_exp version 0.4.6 by Samuel Behan <behan\@frida.fri.utc.sk>";
$CONFIG_BANNER	= 1;
$CONFIG_MAILER	= "/bin/mail '%recipient%' -s '%subject%'";
$HOST_NAME	= `hostname 2>/dev/null`;	chomp($HOST_NAME);
$HOST_IP	= `hostname -i 2>/dev/null`;	chomp($HOST_IP);
$WARN_SUBJECT	= "Warning: Your account will expire soon";
$WARN_BODY	= "                                                                   %time% %locale_date%

Dear user %user_name% (%user%@%hostname%),
   system has detected that your password will expire in %expire_in% days
   ( on %expire_date% ). Please, change your password 
   or your account will be automaticaly disabled.
   
                                       Yours root ;-)\n";
$EXPIRED_WARN	= 1;
$EXPIRED_SUBJECT= "Warning: Your account will be disabled soon";
$EXPIRED_BODY	= "                                                                   %time% %locale_date%

Dear user %user_name% (%user%@%hostname%),
   system has detected that your password has expired %expire_in% days
   ago and your account will be automaticaly disabled in %inactive_in% days 
   ( on %inactive_date% ). 
   Please change your password immediately.

                                       Yours root ;-)\n";
$DATABASE_FILE	= "/var/lib/passwd_exp.status";


##
# Print usage help
sub usage
{
  if($_[0])
  { warn("$_[0]\n"); }
  print "usage: $0 [-u user] [options]
      -- $VERSION
[options]
	-l	list all expired users and users in warning stage
	-i	ignore user nocheck file
	-u user check only this user
	-c file	path to config file (default $CONFIG_FILE)
	-v	verbose mode
	-h	print this help\n";
  if(!$_[0])	{ exit(0);	}
  else		{ exit(1);	}
}

##
# Read config file ($file_name)
sub get_config($)
{
  my $linenum	= 0;
  my ($line,@tags);
  sub parse_line($)
  {  ## This is parser <var> = <value> style
    my $line	= $_[0];
    my (@tags);
    $line	=~ s/\n//g;
    $line	=~ s/\r//g;
    $line	=~ s/\#.*//g;			#remove comments
    $line	=~ s/\t/ /g;			#remove all tabs
    @tags	= split(/=/,$line,2);
    $tags[0]	=~ s/^ *//;		#remove white spaces from begin
    $tags[0]	=~ s/ *$//;		#remove white spaces from end
    if($tags[1] ne '')			#tha same
    { $tags[1]	=~ s/^ *//;
      $tags[1]	=~ s/ *$//; }
    return wantarray ? @tags : "@tags";
  }

  if(!open(CONFIG,"$_[0]"))
  {
    warn("passwd_exp: (error) can not open file $_[0] ($!)\n");
    return undef;
  }
    
  while($line	= <CONFIG>)
  {
    $linenum++;
    @tags	= parse_line($line);
    if(($tags[1]=~ /^\"/) && (($tags[1]	!~ /\"$/) || (($tags[1]	=~ /^\"$/))))
    { my ($sub_tags,$stop,$line_num);
      $tags[1]	= $line;
      $tags[1]	=~ s/(.*?)= *//;
      while(!$stop)
      { if(!($line	= <CONFIG>))
        { warn("passwd_exp: unexpected end of file (posible start at line $linenum)\n");
          return undef; }
	$line_num++;
	if($line	=~ s/\" *$/\"/)
	{ $stop		= 1; }
	$tags[1]	.= $line;
      }
      $linenum		+= $line_num; }
    if($tags[0] =~ /^ *$/) { next; }	#retry if blank line
     $tags[1]	=~ s/^(\"|\')//;	#remove value qoutes
     $tags[1]	=~ s/(\"|\')$//;
     $_ 	= $tags[0];
     ##system settings
    if(/^(mailer|mail|mail( |_)?sender)$/)
    { if($tags[1]	!~ /^\//)
      { warn("passwd_exp: (warning) absolute path required for 'mailer' on line $linenum\n"); 
        return undef; }
      $CONFIG_MAILER	= $tags[1]; }
    elsif(/^(print( |_)?)?banner$/)
    { $CONFIG_BANNER	= (($tags[1] =~ /(yes|true|ok|enable|allow)/i) ? 1:0); }
    elsif(/^no( |_)?warnings?$/)
    { $CONFIG_NOSEND	.= "$tags[1] "; }
    elsif(/^no( |_)?check$/)
    { $CONFIG_NOCHECK	= $tags[1]; }
    elsif(/^(warn( |_)?)?date( |_)?expired$/) 
    { $CONFIG_DATE_EXP	= (($tags[1] =~ /(yes|true|ok|enable|allow)/i) ? 1:0); }    ##expiring warning
    ##expiring warnings
    elsif(/^warning( |_)?subject$/)
    { $tags[1]		=~ s/\n.*//;
      $WARN_SUBJECT	= $tags[1];       
      (length($tags[1]) <= 60) 
         || warn("passwd_exp: (warning) mail subject is too long on line $linenum\n"); }
    elsif(/^warning( |_)?body$/)
    { $WARN_BODY	= $tags[1]; }
    ##expired warnings
    elsif(/^warn( |_)?expired$/)
    { $EXPIRED_WARN	= (($tags[1] =~ /(yes|true|ok|enable|allow)/i) ? 1:0); }
    elsif(/^expired( |_)?subject$/)
    { $tags[1]		=~ s/\n.*//;
      $EXPIRED_SUBJECT	= $tags[1];       
      (length($tags[1]) <= 60) 
         || warn("passwd_exp: (warning) mail subject is too long on line $linenum\n"); }
    elsif(/^expired( |_)?body$/)
    { $EXPIRED_BODY	= $tags[1]; }    
    else
    { warn("passwd_exp: (config error) unknow garbage on line $linenum\n"); 
      return undef; }
  }
  close(CONFIG);
}

##
# Send mail to user (user,subject,message);
sub sendmail($$$)
{
  my ($var);
  my $subject	= $_[1];
  my $message	= $_[2];
  my $mail	= $CONFIG_MAILER;

  $subject	=~ s/\"/\\"/g;	#escapes apostrofes
  $subject	= "\"$subject\"";  
  #prepare message
  foreach $var (keys(%ENV))
  { $message	=~ s/\$($var|\{$var\})/$ENV{$var}/g;
    $subject	=~ s/\$($var|\{$var\})/$ENV{$var}/g; }
  foreach $var (keys(%MESSG_ENV))
  { $message	=~ s/%$var%/$MESSG_ENV{$var}/g;
    $subject	=~ s/%$var%/$MESSG_ENV{$var}/g; }
  $message	=~ s/\%subject\%/$subject/g;
  $message	=~ s/\\n/\n/g;
  $message	=~ s/\\t/\t/g;
  $mail		=~ s/%subject%/$subject/g;
  if(!($mail	=~ s/(\'|\")?%recipient%(\'|\")?/'$_[0]'/g))
  { $opt_v	|| warn("ERROR\n");
    warn("passwd_exp: (sendmail) mail program does not accept mail recipient\n");
    warn("	      as a argument ( maybe configuration error )\n"); 
    return undef; }
  if(!open(MAIL,"|$mail &>/dev/null"))
  { $opt_v	|| warn("ERROR\n");
    warn("passwd_exp: (sendmail) can not send mail by '$mail'\n");
    warn("            $!\n");
    return undef; }
  print MAIL "$message\n";
  $CONFIG_BANNER && print MAIL "\n---\nThis message was produced by:\n   $VERSION\n\n";
  close(MAIL);
  wait;		#just to be safe (I dont think it does realy works here)
  return 1;
}

##
# Make message enviroment
sub make_env
{
  my ($expire_date);
  if(!@pw_ent)			#pw_ent can be defined from main stream
    { setpwent();
      @pw_ent	= getpwnam($sp_ent[0]);
      endpwent(); }
  $expire_date		= $sp_ent[2] + $sp_ent[4];
  if(($sp_ent[7] > 0) && ($sp_ent[7] < $expire_date))
  { $expire_date 	= $sp_ent[7]; } 
  $expire_date		= ($expire_date * 86400);
  $inactive_date	= time() + ($inactive * 86400);
  $inactive_date	= strftime("%A %e %B %Y",localtime($inactive_date));
  $expire_date		= strftime("%A %e %B %Y",localtime($expire_date));
  $MESSG_ENV{'user'}		= $MESSG_ENV{'recipient'}	= $sp_ent[0];
  $MESSG_ENV{'user_name'}	= $pw_ent[6] || $sp_ent[0];
  $MESSG_ENV{'home_dir'}	= $pw_ent[7];
  $MESSG_ENV{'expire_in'}	= abs($remains);
  $MESSG_ENV{'expire_date'}	= $expire_date;
  $MESSG_ENV{'inactive_in'}	= abs($inactive);
  $MESSG_ENV{'inactive_date'}	= $inactive_date;
  $MESSG_ENV{'deny_check'}	= $CONFIG_NOCHECK;
  $MESSG_ENV{'date'}		= strftime("%A %e %B %Y",localtime(time()));
  $MESSG_ENV{'locale_date'}	= strftime("%x",localtime(time()));
  $MESSG_ENV{'time'}		= strftime("%H:%M:%S",localtime(time())); 
  $MESSG_ENV{'locale_time'}	= strftime("%X",localtime(time()));
  $MESSG_ENV{'unix_time'}	= strftime("%s",localtime(time()));
  $MESSG_ENV{'hostname'}	= $HOST_NAME;
  $MESSG_ENV{'host'}		= $HOST_IP;
}

################################################################################
#Main program
#

#import (my) shadow db procedures
import spent;
use POSIX qw(strftime);

# get config from command line
require 'getopts.pl';
&Getopts("c:u:lfivh") || &usage("unknown parameter");
if($opt_h) { &usage;				}
if($opt_u) { $cuser		= $opt_u;	}
if($opt_c) { $CONFIG_FILE	= $opt_c;	}
if($opt_v) { $CONFIG_VERBOSE	= 1;		}

#get config from config file
get_config($CONFIG_FILE) || exit(1);

#check last runinug time
if((!$opt_f) && (!$opt_l) && (!$opt_u))
{ my @stat_info;
  if(@stat_info	= stat($DATABASE_FILE))
  { if($stat_info[9] > (time()-(23*3600)))
    { warn("passwd_exp: check performed too soon (use -f to override) !!!\n");
      warn("            NOTE: check should be performed maximaly once a day\n");
      exit(1); }
  }
  #update database file time
  if(open(DB,">$DATABASE_FILE"))
  { print DB strftime("%a %b %e %H:%M:%S %Y",localtime(time())); close(DB); }
}

setspent() || die("passwd_exp: error manipulating shadow database\n");
if($cuser)
{ @sp_ent	= getspnam($cuser);
  if(!$sp_ent[0])
  { warn("passwd_exp: user '$cuser' does not exists !!!\n");
    exit(2); }
  goto CHECK;
  exit; 
}

while((@sp_ent	= getspent()))
{
CHECK:
  @pw_ent  = undef; 
  $remains = $inactive = $date_expired = $mail_addr = undef;

#check account setting, nosend users and nocheck users
  if(($sp_ent[1] eq "*") || ($sp_ent[1] eq "!!")	#account disabled
     || (!$sp_ent[2]) || ($sp_ent[2] <= 0)		#account never used
     || ($sp_ent[4] == -1)				#account never expires
     || (($sp_ent[5] == -1) && (!$opt_l)))		#without warnings
  { next; }
  if((!$opt_l) && ($CONFIG_NOSEND	=~ /(^|,| ) *$sp_ent[0] *( |,|$)/))
  { next; }
  if((!$opt_i) && (!$opt_l) && ($CONFIG_NOCHECK))
  {
    setpwent() || die("passwd_exp: error manipulating passwd database\n");
    @pw_ent	= getpwnam($sp_ent[0]);
    endpwent();
    if( -e "$pw_ent[7]/$CONFIG_NOCHECK" )
    { next; }
  }

#default warning days value (superuser knows more ;-)
  if(($opt_l) && ($sp_ent[5]	== -1))
  { $sp_ent[5] = $CHANGE_WARN; }
  
## here are done all the expiration computes
  $remains	=  ($sp_ent[2] + $sp_ent[4]) - int(time() / 86400);
  $date_expired	= 0;
  if(($CONFIG_DATE_EXP) && ($sp_ent[7] > 0))		#check expire date
  { if(($remains > ($date_expired = ($sp_ent[7] - int(time() / 86400))))
        || ($remains	== 99999))
    { $remains	= $date_expired; }
    if(($sp_ent[0] == -1) || (!$sp_ent[5]))
    { $sp_ent[5] = $CHANGE_WARN; }
  }
  $inactive = $remains	+ $sp_ent[6];
#kickout people that are not expiring  and ignore them
  if($remains > $sp_ent[5]) { next; }

##here are mail send and messaged printed
  make_env();
  my ($message,$mode);$mode=$message=undef;
  if(($remains > 0) && ($remains 	<= $sp_ent[5]))
  { $message		= $USER_EXPIRING; 
    $mode	= "WARN"; }
  elsif((($inactive >= 0) && ($inactive <= $sp_ent[6]) || ($sp_ent[6] == -1)) 
  	&& (!$date_expired))
  { if($sp_ent[6]	!= -1)
    { $message	= $USER_INACTIVING;
      $mode	= (($EXPIRED_WARN) ? "EXPIRED" : undef); }
    else 
    { $message	= $USER_EXPIRED; } }
  elsif(($inactive) && ($inactive < 0))
  { $message	= $USER_INACTIVE; }
##print message
  if(($opt_l) && ($message))
  { foreach $var (keys(%MESSG_ENV))
    { $message	=~ s/%$var%/$MESSG_ENV{$var}/g; }  
    foreach $var (keys(%ENV))
    { $message	=~ s/\$($var|\{$var\})/$ENV{$var}/g; }
    print "$message\n"; }
##send main
  if((!$opt_l) && ($mode))
  { $opt_v && print " -->Sending $mode mail to user $sp_ent[0]...";
    if(!sendmail($sp_ent[0],${"${mode}_SUBJECT"},${"${mode}_BODY"}))
    { die("passwd_exp: error occured while sending mail\n"); }
    else { $opt_v && print "DONE\n"; }
  }
}
endspent();

#end of file
