/*
 * QUOTA    An implementation of the diskquota system for the LINUX operating
 *          system. QUOTA is implemented using the BSD systemcall interface
 *          as the means of communication with the user level. Should work for
 *          all filesystems because of integration into the VFS layer of the
 *          operating system. This is based on the Melbourne quota system wich
 *          uses both user and group quota files.
 * 
 *          Program to mail to users that they are over there quota.
 * 
 * Author:  Marco van Wieringen <mvw@planets.elm.net>
 *
 * Version: $Id: warnquota.c,v 1.9 2000/09/05 18:47:15 mvw Exp mvw $
 *
 *          This program is free software; you can redistribute it and/or
 *          modify it under the terms of the GNU General Public License as
 *          published by the Free Software Foundation; either version 2 of
 *          the License, or (at your option) any later version.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <stdio.h>
#include <sys/quota.h>
#include <stdarg.h>
#include <fcntl.h>
#include <mntent.h>
#include <limits.h>
#include <unistd.h>
#include <stdlib.h>
#include <pwd.h>
#include <string.h>

#include "pot.h"
#include "bylabel.h"
#include "dqblk.h"

int need_dquot_convert;

/* these are just defaults, overridden in the WARNQUOTA_CONF file */
#define MAIL_CMD "/usr/lib/sendmail -t"
#define FROM     "support@localhost"
#define SUBJECT  "Disk Quota usage on system"
#define CC_TO    "root"
#define SUPPORT  "support@localhost"
#define PHONE    "(xxx) xxx-xxxx or (yyy) yyy-yyyy"
/* */

#define DEF_MESSAGE	_("Hi,\n\nWe noticed that you are in violation with the quotasystem\n" \
                          "used on this system. We have found the following violations:\n\n")
#define DEF_SIGNATURE	_("\nWe hope that you will cleanup before your grace period expires.\n" \
	                  "\nBasically, this means that the system thinks you are using more disk space\n" \
	                  "on the above partition(s) than you are allowed.  If you do not delete files\n" \
	                  "and get below your quota before the grace period expires, the system will\n" \
	                  "prevent you from creating new files.\n\n" \
                          "For additional assistance, please contact us at %s\nor via " \
                          "phone at %s.\n")

#define QUOTATAB "/etc/quotatab"
#define CNF_BUFFER 2048
#define WARNQUOTA_CONF "/etc/warnquota.conf"
 
struct usage
{
   char *devicename;
   struct dqblk dq_dqb;
   struct usage *next;
};

struct configparams {
   char mail_cmd[CNF_BUFFER];
   char from[CNF_BUFFER];
   char subject[CNF_BUFFER];
   char cc_to[CNF_BUFFER];
   char support[CNF_BUFFER];
   char phone[CNF_BUFFER];
};

struct offenderlist
{
   int offender_id;
   char *offender_name;
   struct usage *usage;
   struct offenderlist *next;
};

typedef struct quotatable
{
  char *devname;
  char *devdesc;
} quotatable_t;

int qtab_i = 0;
quotatable_t *quotatable = (quotatable_t *)NULL;

/*
 * Global pointers to list.
 */
static struct offenderlist *offenders = (struct offenderlist *)0;

void *xmalloc(size_t size)
{
   void *new;

   if ((new = (void *)malloc(size)) == (void *)0) {
      fprintf(stderr, _("Virtual memory exhausted\n"));
      exit(1);
   }
   return(new);
}

struct offenderlist *add_offender(int id)
{
   struct passwd *pwd;
   struct group *grp;
   struct offenderlist *offender;

   if ((pwd = getpwuid(id)) == (struct passwd *)0)
      return ((struct offenderlist *)0);

   offender = (struct offenderlist *)xmalloc(sizeof(struct offenderlist));
   offender->offender_id = id;
   offender->offender_name = (char *)xmalloc(strlen(pwd->pw_name) + 1);
   offender->usage = (struct usage *)NULL;
   strcpy(offender->offender_name, pwd->pw_name);
   offender->next = offenders;
   offenders = offender;
   return offender;
}

void add_offence(int id, const char *devicename, struct dqblk *used)
{
   struct offenderlist *lptr;
   struct usage *usage;

   for (lptr = offenders; lptr != (struct offenderlist *)0; lptr = lptr->next)
      if (lptr->offender_id == id)
         break;

   if (lptr == (struct offenderlist *)0)
      if ((lptr = add_offender(id)) == (struct offenderlist *)0)
         return;

   usage = (struct usage *)xmalloc(sizeof(struct usage));
   memcpy(&usage->dq_dqb, used, sizeof(struct dqblk));
   usage->devicename = xmalloc(strlen(devicename) + 1);
   strcpy(usage->devicename, devicename);
   /*
    * Stuff it in front
    */
   usage->next = lptr->usage;
   lptr->usage = usage;
}

void read_quotafile(const char *qfpathname, const char *devicename)
{
   int fd, id = 0;
   struct ondisk_dqblk od_dqblk;
   struct dqblk dq_dqb;

   if ((fd = open(qfpathname, O_RDONLY)) < 0) {
      perror(qfpathname);
      exit(1);
   }

   while (read(fd, &od_dqblk, ondisk_dqblk_size) == ondisk_dqblk_size) {
      CONVERT_TO_USER_DQUOT(&dq_dqb, &od_dqblk);
      if ((dq_bsoftlimit && dq_curblocks >= dq_bsoftlimit) ||
          (dq_isoftlimit && dq_curinodes >= dq_isoftlimit)) {
         add_offence(id, devicename, &dq_dqb);
      }
      id++;
   }
   close(fd);
}

/*
 * Calculate the grace period and return a printable string for it.
 */
char *timeprt(time_t seconds)
{
   time_t hours, minutes;
   static char buf[20];
   static time_t now;

   if (now == 0)
      time(&now);
   if (now > seconds)
      return (_("none"));
   seconds -= now;
   minutes = (seconds + 30) / 60;
   hours = (minutes + 30) / 60;
   if (hours >= 36) {
      sprintf(buf, _("%d days"), (hours + 12) / 24);
      return (buf);
   }
   if (minutes >= 60) {
      sprintf(buf, "%2d:%d", minutes / 60, minutes % 60);
      return (buf);
   }
   sprintf(buf, "%2d", minutes);
   return (buf);
}

void mail_user(struct offenderlist *offender, struct configparams *config)
{
   struct usage *lptr;
   FILE *fp;
   int cnt;

   if ((fp = popen(config->mail_cmd, "w")) != (FILE *)0) {
      fprintf(fp, "From: %s\n", config->from);
      fprintf(fp, "Reply-To: %s\n", config->support);
      fprintf(fp, "Subject: %s\n", config->subject);
      fprintf(fp, "To: %s\n", offender->offender_name);
      fprintf(fp, "Cc: %s\n", config->cc_to);
      fprintf(fp, "\n");
      fprintf(fp, DEF_MESSAGE);
      for (lptr = offender->usage; lptr != (struct usage *)0; lptr = lptr->next) {
         for (cnt = 0; cnt < qtab_i ; cnt++) {
            if (!strncmp(quotatable[cnt].devname, lptr->devicename, strlen(quotatable[cnt].devname))) 
               fprintf(fp, "\n%s\n", quotatable[cnt].devdesc);
         }
         fprintf(fp, _("\n                        Block limits               File limits\n"));
         fprintf(fp, _("Filesystem           used    soft    hard  grace    used  soft  hard  grace\n"));
         fprintf(fp, "%-15s", lptr->devicename);
         fprintf(fp, "%c%c%8d%8d%8d%7s",
                lptr->dq_bsoftlimit &&
                lptr->dq_curblocks >=
                lptr->dq_bsoftlimit ? '+' : '-',
                lptr->dq_isoftlimit &&
                lptr->dq_curinodes >=
                lptr->dq_isoftlimit ? '+' : '-',
                lptr->dq_curblocks,
                lptr->dq_bsoftlimit,
                lptr->dq_bhardlimit,
                lptr->dq_bsoftlimit &&
                lptr->dq_curblocks >=
                lptr->dq_bsoftlimit ?
                timeprt(lptr->dq_btime) : "");
         fprintf(fp, "  %6d%6d%6d%7s\n\n",
                lptr->dq_curinodes,
                lptr->dq_isoftlimit,
                lptr->dq_ihardlimit,
                lptr->dq_isoftlimit &&
                lptr->dq_curinodes >=
                lptr->dq_isoftlimit ?
                timeprt(lptr->dq_itime) : "");
      }
      fprintf(fp, DEF_SIGNATURE, config->support, config->phone);
      fclose(fp);
   }
}

void mail_to_offenders(struct configparams *config)
{
   struct offenderlist *lptr;
   struct usage *used;
   /*
    * Dump offenderlist.
    */
   for (lptr = offenders; lptr != (struct offenderlist *)0; lptr = lptr->next)
      mail_user(lptr, config);
}

void get_quotatable(void) 
{
   FILE *fp;
   char buffer[2048], *filename, *colpos;
    
   filename = (char *)malloc(strlen(QUOTATAB) + 1);
   sprintf(filename, "%s", QUOTATAB);
    
   if ((fp = fopen(filename, "r")) == (FILE *)NULL)
      return;

   for (qtab_i = 0; quotatable = (quotatable_t *)realloc(quotatable, sizeof(quotatable_t) * (qtab_i + 1)),
                    fgets(buffer, sizeof(buffer), fp) != (char *)NULL; qtab_i++) {
      if (colpos = strchr(buffer, ':')) {
         *colpos=0;
         quotatable[qtab_i].devname = (char *)malloc(strlen(buffer) + 1);
         strcpy(quotatable[qtab_i].devname, buffer);
         quotatable[qtab_i].devdesc = (char *)malloc(strlen(++colpos) + 1);
         strcpy(quotatable[qtab_i].devdesc, colpos);
         if (colpos = strchr(quotatable[qtab_i].devdesc, '\n'))
            *colpos=0;
         while (colpos = strchr(quotatable[qtab_i].devdesc, '|'))
            *colpos='\n';
      }

      if (buffer[0] == '#' || /* comment */
          !quotatable[qtab_i].devname || !quotatable[qtab_i].devdesc ||
          strlen(quotatable[qtab_i].devname) < 2 ||
          strlen(quotatable[qtab_i].devdesc) < 2 /* stupid root */) 
         qtab_i--;
   }    
   fclose(fp);
   free(filename);
}

/*
 * Wipe spaces, tabs, quotes and newlines from beginning and end of string
 */
void stripstring(char **buff) {
   char *p;

   /* first put a \0 at the tight place to end the string */
   p = *buff + strlen(*buff) - 1;
   while ( *p==' '  || *p=='\n' ||
           *p=='\t' || *p=='"'  ||
           *p=='\'' ) p--;
   p[1] = 0;

   /* then determine the position to start */
   p = *buff;
   while ( *p==' '  || *p=='\n' ||
           *p=='\t' || *p=='"'  ||
           *p=='\'' ) p++;

   *buff = p;
}

/*
 * Reads config parameters from configfile
 * uses default values if error occurs
 */
void readconfigfile(const char *filename, struct configparams *config)
{
   FILE *fp;
   char *buff;
   char *var;
   char *value;
   char *pos;
   int line;

   /* set default values */
   strncpy(config->mail_cmd, MAIL_CMD, CNF_BUFFER);
   strncpy(config->from, FROM, CNF_BUFFER);
   strncpy(config->subject, SUBJECT, CNF_BUFFER);
   strncpy(config->cc_to, CC_TO, CNF_BUFFER);
   strncpy(config->support, SUPPORT, CNF_BUFFER);
   strncpy(config->phone, PHONE, CNF_BUFFER);

   fp = fopen(filename, "r");
   if (fp == (FILE *)NULL) { /* if config file doesn't exist or is not readable */
      return;
   }
   
   buff = (char *) malloc(CNF_BUFFER);
   line = 0;
   while (fgets(buff, CNF_BUFFER,fp)) { /* start reading lines */
      line++;
      
      /* check for comments or empty lines */
      if ( buff[0]=='#' || buff[0]==';' || buff[0]=='\n' )
         continue;
      
      /* check for a '=' char */
      if ((pos = strchr(buff, '='))) {
         pos[0] = '\0'; /* split buff in two parts: var and value */
         var = buff;
         value = pos+1;

         stripstring(&var); /* clean up var and value */
         stripstring(&value);

         /* check if var matches anything */
         if (!strncmp(var, "MAIL_CMD", CNF_BUFFER)) { 
	    strncpy(config->mail_cmd, value, CNF_BUFFER); 
         } else if (!strncmp(var, "FROM", CNF_BUFFER)) { 
	    strncpy(config->from, value, CNF_BUFFER);
         } else if (!strncmp(var, "SUBJECT", CNF_BUFFER)) { 
	    strncpy(config->subject, value, CNF_BUFFER);
         } else if (!strncmp(var, "CC_TO", CNF_BUFFER)) { 
	    strncpy(config->cc_to, value, CNF_BUFFER);
         } else if (!strncmp(var, "SUPPORT", CNF_BUFFER)) { 
	    strncpy(config->support, value, CNF_BUFFER);
         } else if (!strncmp(var, "PHONE", CNF_BUFFER)) { 
	    strncpy(config->phone, value, CNF_BUFFER);
         } else { /* not matched at all */
            fprintf(stderr, "Error in config file (line %d), ignoring\n", line);
         }
      } else { /* no '=' char in this line */
         fprintf(stderr, "Possible error in config file (line %d), ignoring\n", line);
      }
   }
   fclose(fp);
   
   free(buff);
   
   return;
}

warn_quota(void)
{
   FILE *fp;
   struct mntent *mnt;
   char *qfpathname;
   const char *mnt_fsname;
   struct configparams config;

   readconfigfile(WARNQUOTA_CONF, &config);

   fp = setmntent(MNTTAB, "r");
   while ((mnt = getmntent(fp)) != (struct mntent *)0) {
      if (hasquota(mnt, USRQUOTA, &qfpathname)) {
         mnt_fsname = get_device_name(mnt->mnt_fsname);

         /*
          * First sync quotafile to disk.
          */
         quotactl(QCMD(Q_SYNC, USRQUOTA), mnt_fsname, 0, (caddr_t)0);

         read_quotafile(qfpathname, mnt_fsname);
      }
   }
   endmntent(fp);
   get_quotatable();
   mail_to_offenders(&config);
}

main(int argc, char **argv)
{
   DQUOT_CONVERT_INIT();
   gettexton(); 
   warn_quota();
}
