/*
 * 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>
 *
 *          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 "config.h"

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
#include <ctype.h>
#include <signal.h>
#include <grp.h>
#include <time.h>
#include <getopt.h>
#include <locale.h>
#include <libgen.h>
#ifdef HAVE_NL_LANGINFO
#include <langinfo.h>
#endif
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#ifdef USE_LDAP_MAIL_LOOKUP
#include <ldap.h>
#endif

#include "mntopt.h"
#include "pot.h"
#include "bylabel.h"
#include "common.h"
#include "quotasys.h"
#include "quotaio.h"

/* 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_USER_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_USER_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 DEF_GROUP_MESSAGE	_("Hi,\n\nWe noticed that the group %s you are member of violates the quotasystem\n" \
                          "used on this system. We have found the following violations:\n\n")
#define DEF_GROUP_SIGNATURE	_("\nPlease cleanup the group data before the grace period expires.\n" \
	                  "\nBasically, this means that the system thinks group is using more disk space\n" \
	                  "on the above partition(s) than it is allowed.  If you do not delete files\n" \
	                  "and get below group quota before the grace period expires, the system will\n" \
	                  "prevent you and other members of the group from creating new files owned by\n" \
			  "the group.\n\n" \
                          "For additional assistance, please contact us at %s\nor via " \
                          "phone at %s.\n")

#define SHELL "/bin/sh"
#define QUOTATAB "/etc/quotatab"
#define CNF_BUFFER 2048
#define IOBUF_SIZE 16384		/* Size of buffer for line in config files */
#define ADMIN_TAB_ALLOC 256		/* How many entries to admins table should we allocate at once? */
#define WARNQUOTA_CONF "/etc/warnquota.conf"
#define ADMINSFILE "/etc/quotagrpadmins"

#define FL_USER 1
#define FL_GROUP 2
#define FL_NOAUTOFS 4
#define FL_NODETAILS 16
#define FL_IGNORE_CFG_ERR 32

struct usage {
	char *devicename;
	struct util_dqblk dq_dqb;
	struct usage *next;
};

#ifdef USE_LDAP_MAIL_LOOKUP
static LDAP *ldapconn = NULL;
#endif

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];
	char charset[CNF_BUFFER];
	char *user_message;
	char *user_signature;
	char *group_message;
	char *group_signature;
	time_t cc_before;
#ifdef USE_LDAP_MAIL_LOOKUP
	int use_ldap_mail; /* 0 */
	int ldap_is_setup; /* 0 */
	int ldap_starttls; /* 0 */
	int ldap_tls; /* LDAP_OPT_X_TLS_NEVER */
	int ldap_vers; /* LDAP_VERSION3 */
	char ldap_host[CNF_BUFFER];
	int ldap_port;
	char ldap_uri[CNF_BUFFER];
	char ldap_binddn[CNF_BUFFER];
	char ldap_bindpw[CNF_BUFFER];
	char ldap_basedn[CNF_BUFFER];
	char ldap_search_attr[CNF_BUFFER];
	char ldap_mail_attr[CNF_BUFFER];
	char default_domain[CNF_BUFFER];
#endif /* USE_LDAP_MAIL_LOOKUP */
};

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

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

struct adminstable {
	char *grpname;
	char *adminname;
};

static int qtab_i = 0, fmt = -1, flags;
static char maildev[CNF_BUFFER];
static struct quota_handle *maildev_handle;
static char *configfile = WARNQUOTA_CONF, *quotatabfile = QUOTATAB, *adminsfile = ADMINSFILE;
char *progname;
static char *hostname, *domainname;
static quotatable_t *quotatable;
static int adminscnt, adminsalloc;
static struct adminstable *adminstable;
static enum s2s_unit spaceunit = S2S_NONE, inodeunit = S2S_NONE;

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

/*
 * add any cleanup functions here
 */
static void wc_exit(int ex_stat)
{
#ifdef USE_LDAP_MAIL_LOOKUP
	if(ldapconn != NULL)
		ldap_unbind_ext(ldapconn, NULL, NULL);
#endif
	exit(ex_stat);
}

#ifdef USE_LDAP_MAIL_LOOKUP

#define LDAP_ERR_BUF_SIZE 1024

static void print_ldap_error(int err, char *prefix)
{
	char *msg = NULL;
	char outbuf[LDAP_ERR_BUF_SIZE];

	sstrncpy(outbuf, prefix, LDAP_ERR_BUF_SIZE);
	sstrncat(outbuf, ": %s\n", LDAP_ERR_BUF_SIZE);
	errstr(outbuf, ldap_err2string(err));
	ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void *)&msg);
	if (msg) {
	    if (strcmp(msg, ""))
		errstr(_("Additional error info: %s\n"), msg);
	    ldap_memfree(msg);
	}
}

static int setup_ldap(struct configparams *config)
{
	int ret;
	struct berval cred = { .bv_val = config->ldap_bindpw,
			       .bv_len = strlen(config->ldap_bindpw) };

	ret = ldap_initialize(&ldapconn, config->ldap_uri);

	if (ret != LDAP_SUCCESS) {
		print_ldap_error(ret, _("ldap_initialize() failed"));
		return -1;
	}

	if (config->ldap_starttls) {
		ldap_set_option(ldapconn, LDAP_OPT_PROTOCOL_VERSION, &(config->ldap_vers));
		ldap_set_option(ldapconn, LDAP_OPT_X_TLS_REQUIRE_CERT, &(config->ldap_tls));
		ret = ldap_start_tls_s(ldapconn, NULL, NULL);
		if (ret != LDAP_SUCCESS) {
			print_ldap_error(ret, _("ldap_start_tls_s() failed"));
			return -1;
		}
	}
	ret = ldap_sasl_bind_s(ldapconn, config->ldap_binddn, LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL);
	if (ret != LDAP_SUCCESS) {
		print_ldap_error(ret, _("ldap_sasl_bind_s() failed"));
		return -1;
	}
	return 0;
}
		
#endif

static struct offenderlist *add_offender(int type, int id, char *name)
{
	struct offenderlist *offender;
	char namebuf[MAXNAMELEN];
	
	if (!name) {
		if (id2name(id, type, namebuf)) {
			errstr(_("Cannot get name for uid/gid %u.\n"), id);
			return NULL;
		}
		name = namebuf;
	}
	offender = (struct offenderlist *)smalloc(sizeof(struct offenderlist));
	offender->offender_type = type;
	offender->offender_id = id;
	offender->offender_name = sstrdup(name);
	offender->usage = (struct usage *)NULL;
	offender->next = offenders;
	offenders = offender;
	return offender;
}

static void add_offence(struct dquot *dquot, char *name)
{
	struct offenderlist *lptr;
	struct usage *usage;

	for (lptr = offenders; lptr; lptr = lptr->next)
		if (dquot->dq_h->qh_type == lptr->offender_type && lptr->offender_id == dquot->dq_id)
			break;

	if (!lptr)
		if (!(lptr = add_offender(dquot->dq_h->qh_type, dquot->dq_id, name)))
			return;

	usage = (struct usage *)smalloc(sizeof(struct usage));
	memcpy(&usage->dq_dqb, &dquot->dq_dqb, sizeof(struct util_dqblk));

	usage->devicename = sstrdup(dquot->dq_h->qh_quotadev);
	/*
	 * Stuff it in front
	 */
	usage->next = lptr->usage;
	lptr->usage = usage;
}

static int deliverable(struct dquot *dquot)
{
	time_t now;
	struct dquot *mdquot;
	
	if (!maildev[0])
		return 1;

	time(&now);
	
	if (!strcasecmp(maildev, "any") && 
	   ((dquot->dq_dqb.dqb_bhardlimit && toqb(dquot->dq_dqb.dqb_curspace) >= dquot->dq_dqb.dqb_bhardlimit)
	   || ((dquot->dq_dqb.dqb_bsoftlimit && toqb(dquot->dq_dqb.dqb_curspace) >= dquot->dq_dqb.dqb_bsoftlimit)
	   && (dquot->dq_dqb.dqb_btime && dquot->dq_dqb.dqb_btime <= now))))
		return 0;
	if (!maildev_handle)
		return 1;
	mdquot = maildev_handle->qh_ops->read_dquot(maildev_handle, dquot->dq_id);
	if (mdquot &&
	   ((mdquot->dq_dqb.dqb_bhardlimit && toqb(mdquot->dq_dqb.dqb_curspace) >= mdquot->dq_dqb.dqb_bhardlimit)
	   || ((mdquot->dq_dqb.dqb_bsoftlimit && toqb(mdquot->dq_dqb.dqb_curspace) >= mdquot->dq_dqb.dqb_bsoftlimit)
	   && (mdquot->dq_dqb.dqb_btime && mdquot->dq_dqb.dqb_btime <= now)))) {
		free(mdquot);
		return 0;
	}
	free(mdquot);
	return 1;
}

static int check_offence(struct dquot *dquot, char *name)
{
	if ((dquot->dq_dqb.dqb_bsoftlimit && toqb(dquot->dq_dqb.dqb_curspace) >= dquot->dq_dqb.dqb_bsoftlimit)
	    || (dquot->dq_dqb.dqb_isoftlimit && dquot->dq_dqb.dqb_curinodes >= dquot->dq_dqb.dqb_isoftlimit)) {
		if(deliverable(dquot))
			add_offence(dquot, name);
	}
	return 0;
}

static FILE *run_mailer(char *command)
{
	int pipefd[2];
	FILE *f;

	if (pipe(pipefd) < 0) {
		errstr(_("Cannot create pipe: %s\n"), strerror(errno));
		return NULL;
	}
	signal(SIGPIPE, SIG_IGN);
	switch(fork()) {
		case -1:
			errstr(_("Cannot fork: %s\n"), strerror(errno));
			return NULL;
		case 0:
			close(pipefd[1]);
			if (dup2(pipefd[0], 0) < 0) {
				errstr(_("Cannot duplicate descriptor: %s\n"), strerror(errno));
				wc_exit(1);
			}			
			execl(SHELL, SHELL, "-c", command, NULL);
			errstr(_("Cannot execute '%s': %s\n"), command, strerror(errno));
			wc_exit(1);
		default:
			close(pipefd[0]);
			if (!(f = fdopen(pipefd[1], "w")))
				errstr(_("Cannot open pipe: %s\n"), strerror(errno));
			return f;
	}
}

static int admin_name_cmp(const void *key, const void *mem)
{
	return strcmp(key, ((struct adminstable *)mem)->grpname);
}

static int should_cc(struct offenderlist *offender, struct configparams *config)
{
	struct usage *lptr;
	struct util_dqblk *dqb;
	time_t atime;

	/* Noone to send CC to? */
	if (!strcmp(config->cc_to, ""))
		return 0;

	if (config->cc_before == -1)
		return 1;
	time(&atime);
	for (lptr = offender->usage; lptr; lptr = lptr->next) {
		dqb = &lptr->dq_dqb;
		if (dqb->dqb_bsoftlimit && dqb->dqb_bsoftlimit <= toqb(dqb->dqb_curspace) && dqb->dqb_btime-config->cc_before <= atime)
			return 1;
		if (dqb->dqb_isoftlimit && dqb->dqb_isoftlimit <= dqb->dqb_curinodes && dqb->dqb_itime-config->cc_before <= atime)
			return 1;
	}
	return 0;
}

/* Substitute %s and %i for 'name' and %h for hostname */
static void format_print(FILE *fp, char *fmt, char *name)
{
	char *ch, *lastch = fmt;

	for (ch = strchr(fmt, '%'); ch; lastch = ch+2, ch = strchr(ch+2, '%')) {
		*ch = 0;
		fputs(lastch, fp);
		*ch = '%';
		switch (*(ch+1)) {
			case 's':
			case 'i':
				fputs(name, fp);
				break;
			case 'h':
				fputs(hostname, fp);
				break;
			case 'd':
				fputs(domainname, fp);
				break;
			case '%':
				fputc('%', fp);
				break;
		}
	}
	fputs(lastch, fp);
}

static char *lookup_user(struct configparams *config, char *user)
{
#ifdef USE_LDAP_MAIL_LOOKUP
       	char searchbuf[256];
	LDAPMessage *result, *entry;
	BerElement     *ber = NULL;
	struct berval  **bvals = NULL;
	int ret, cnt;
	char *a;
	char *to = NULL;

	if (!config->use_ldap_mail)
		return sstrdup(user);

	if (ldapconn == NULL && config->ldap_is_setup == 0) {
		/* need init */
		if (setup_ldap(config)) {
			errstr(_("Could not setup ldap connection.\n"));
			return NULL;
		}
		config->ldap_is_setup = 1;
	}

	if (ldapconn == NULL) {
		/*
		 * ldap was never setup correctly so just use
		 * the offender_name
		 */
		return sstrdup(user);
	}

	/* search for the offender_name in ldap */
	if (256 <= snprintf(searchbuf, 256, "(%s=%s)", config->ldap_search_attr,
		    user)) {
		errstr(_("Could not format LDAP search filter for %s user and "
			"%s search attribute due to excessive length.\n"),
			user, config->ldap_search_attr);
		return NULL;
	}
	ret = ldap_search_ext_s(ldapconn,
		config->ldap_basedn, LDAP_SCOPE_SUBTREE,
		searchbuf, NULL, 0, NULL, NULL, NULL,
		0, &result);

	if (ret != LDAP_SUCCESS) {
		errstr(_("Error with %s.\n"), user);
		print_ldap_error(ret, _("ldap_search_ext_s() failed"));
		return NULL;
	}
		
	cnt = ldap_count_entries(ldapconn, result);
	if (cnt > 1) {
		errstr(_("Multiple entries found for client %s (%d).\n"),
		       user, cnt);
		return NULL;
	} else if (cnt == 0) {
		errstr(_("Entry not found for client %s.\n"), user);
		return NULL;
	}
	/* get the attr */
	entry = ldap_first_entry(ldapconn, result);
	for (a = ldap_first_attribute(ldapconn, entry, &ber); a != NULL;
	     a = ldap_next_attribute(ldapconn, entry, ber)) {
		if (strcasecmp(a, config->ldap_mail_attr) == 0) {
			bvals = ldap_get_values_len(ldapconn, entry, a);
			if (bvals == NULL) {
				errstr(_("Could not get values for %s.\n"), 
				       user);
				return NULL;
			}
			to = sstrdup(bvals[0]->bv_val);
			ldap_memfree(a);
			ldap_value_free_len(bvals);
			break;
		}
		ldap_memfree(a);
	} 

	ber_free(ber, 0);

	if (to == NULL) {
		/* 
		 * use just the name and default domain as we didn't find the
		 * attribute we wanted in this entry
		 */
		to = smalloc(strlen(user) + strlen(config->default_domain) + 1);
		sprintf(to, "%s@%s", user, config->default_domain);
	}
	return to;
#else
	return sstrdup(user);
#endif
}

static int mail_user(struct offenderlist *offender, struct configparams *config)
{
	struct usage *lptr;
	FILE *fp;
	int cnt, status;
	char timebuf[MAXTIMELEN];
	char numbuf[3][MAXNUMLEN];
	struct util_dqblk *dqb;
	char *to = NULL;

	if (offender->offender_type == USRQUOTA) {
		to = lookup_user(config, offender->offender_name);
		if (!to)
			return -1;
	} else {
		struct adminstable *admin;

		if (!(admin = bsearch(offender->offender_name, adminstable, adminscnt, sizeof(struct adminstable), admin_name_cmp))) {
			errstr(_("Administrator for a group %s not found. Cancelling mail.\n"), offender->offender_name);
			return -1;
		}
		to = sstrdup(admin->adminname);
	}
	if (!(fp = run_mailer(config->mail_cmd))) {
		if (to)
			free(to);
		return -1;
	}
	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", to);
	if (should_cc(offender, config)) {
		char *cc_to = lookup_user(config, config->cc_to);

		if (cc_to) {
			fprintf(fp, "Cc: %s\n", cc_to);
			free(cc_to);
		}
	}
	if ((config->charset)[0] != '\0') { /* are we supposed to set the encoding */
		fprintf(fp, "MIME-Version: 1.0\n");
		fprintf(fp, "Content-Type: text/plain; charset=%s\n", config->charset);
		fprintf(fp, "Content-Disposition: inline\n");
		fprintf(fp, "Content-Transfer-Encoding: 8bit\n");
	}
	fprintf(fp, "\n");
	free(to);

	if (offender->offender_type == USRQUOTA)
		if (config->user_message)
			format_print(fp, config->user_message, offender->offender_name);
		else
			fputs(DEF_USER_MESSAGE, fp);
	else
		if (config->group_message)
			format_print(fp, config->group_message, offender->offender_name);
		else
			fprintf(fp, DEF_GROUP_MESSAGE, offender->offender_name);

	if (!(flags & FL_NODETAILS)) {
		for (lptr = offender->usage; lptr; lptr = lptr->next) {
			dqb = &lptr->dq_dqb;
			for (cnt = 0; cnt < qtab_i; cnt++)
				if (!strcmp(quotatable[cnt].devname, lptr->devicename)) {
					fprintf(fp, "\n%s (%s)\n", quotatable[cnt].devdesc, quotatable[cnt].devname);
					break;
				}
			if (cnt == qtab_i)	/* Description not found? */
				fprintf(fp, "\n%s\n", lptr->devicename);
			fprintf(fp, _("\n                        Block limits               File limits\n"));
			fprintf(fp, _("Filesystem           used    soft    hard  grace    used  soft  hard  grace\n"));
			if (strlen(lptr->devicename) > 15)
				fprintf(fp, "%s\n%15s", lptr->devicename, "");
			else
				fprintf(fp, "%-15s", lptr->devicename);
			if (dqb->dqb_bsoftlimit && dqb->dqb_bsoftlimit <= toqb(dqb->dqb_curspace))
				difftime2str(dqb->dqb_btime, timebuf);
			else
				timebuf[0] = '\0';
			space2str(toqb(dqb->dqb_curspace), numbuf[0], spaceunit);
			space2str(dqb->dqb_bsoftlimit, numbuf[1], spaceunit);
			space2str(dqb->dqb_bhardlimit, numbuf[2], spaceunit);
			fprintf(fp, "%c%c %7s %7s %7s %6s",
			        dqb->dqb_bsoftlimit && toqb(dqb->dqb_curspace) >= dqb->dqb_bsoftlimit ? '+' : '-',
				dqb->dqb_isoftlimit && dqb->dqb_curinodes >= dqb->dqb_isoftlimit ? '+' : '-',
				numbuf[0], numbuf[1], numbuf[2], timebuf);
			if (dqb->dqb_isoftlimit && dqb->dqb_isoftlimit <= dqb->dqb_curinodes)
				difftime2str(dqb->dqb_itime, timebuf);
			else
				timebuf[0] = '\0';
			number2str(dqb->dqb_curinodes, numbuf[0], inodeunit);
			number2str(dqb->dqb_isoftlimit, numbuf[1], inodeunit);
			number2str(dqb->dqb_ihardlimit, numbuf[2], inodeunit);
			fprintf(fp, " %7s %5s %5s %6s\n\n", numbuf[0], numbuf[1], numbuf[2], timebuf);
		}
	}


	if (offender->offender_type == USRQUOTA)
		if (config->user_signature)
			format_print(fp, config->user_signature, offender->offender_name);
		else
			fprintf(fp, DEF_USER_SIGNATURE, config->support, config->phone);
	else
		if (config->group_signature)
			format_print(fp, config->group_signature, offender->offender_name);
		else
			fprintf(fp, DEF_GROUP_SIGNATURE, config->support, config->phone);
	fclose(fp);
	if (wait(&status) < 0)	/* Wait for mailer */
		errstr(_("Cannot wait for mailer: %s\n"), strerror(errno));
	else if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
		errstr(_("Warning: Mailer exitted abnormally.\n"));

	return 0;
}

static int mail_to_offenders(struct configparams *config)
{
	struct offenderlist *lptr;
	int ret = 0;

	/*
	 * Dump offenderlist.
	 */
	for (lptr = offenders; lptr; lptr = lptr->next)
		ret |= mail_user(lptr, config);
	return ret;
}

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

	/* first put a \0 at the tight place to end the string */
	for (i = strlen(*buff) - 1; i >= 0 && (isspace((*buff)[i]) || (*buff)[i] == '"'
	     || (*buff)[i] == '\''); i--);
	(*buff)[i+1] = 0;

	/* then determine the position to start */
	for (i = 0; (*buff)[i] && (isspace((*buff)[i]) || (*buff)[i] == '"' || (*buff)[i] == '\''); i++);
	*buff += i;
}

/*
 * Substitute '|' with end of lines
 */
static void create_eoln(char *buf)
{
	char *colpos = buf;

	while ((colpos = strchr(colpos, '|')))
		*colpos = '\n';
}

/*
 * Read /etc/quotatab (description of devices for users)
 */
static int get_quotatable(void)
{
	FILE *fp;
	char buffer[IOBUF_SIZE], *colpos, *devname, *devdesc;
	int line;
	struct stat st;

	if (!(fp = fopen(quotatabfile, "r"))) {
		errstr(_("Cannot open %s: %s\nWill use device names.\n"), quotatabfile, strerror(errno));
		qtab_i = 0;
		return 0;
	}

	line = 0;
	for (qtab_i = 0; quotatable = srealloc(quotatable, sizeof(quotatable_t) * (qtab_i + 1)),
	     fgets(buffer, sizeof(buffer), fp); qtab_i++) {
		line++;
		quotatable[qtab_i].devname = NULL;
		quotatable[qtab_i].devdesc = NULL;
		if (buffer[0] == '#' || buffer[0] == ';') {	/* Comment? */
			qtab_i--;
			continue;
		}
		/* Empty line? */
		for (colpos = buffer; isspace(*colpos); colpos++);
		if (!*colpos) {
			qtab_i--;
			continue;
		}
		/* Parse line */
		if (!(colpos = strchr(buffer, ':'))) {
			errstr(_("Cannot parse line %d in quotatab (missing ':')\n"), line);
			qtab_i--;
			continue;
		}
		*colpos = 0;
		devname = buffer;
		devdesc = colpos+1;
		stripstring(&devname);
		stripstring(&devdesc);
		quotatable[qtab_i].devname = sstrdup(devname);
		quotatable[qtab_i].devdesc = sstrdup(devdesc);
		create_eoln(quotatable[qtab_i].devdesc);

		if (stat(quotatable[qtab_i].devname, &st) < 0)
			errstr(_("Cannot stat device %s (maybe typo in quotatab)\n"), quotatable[qtab_i].devname);
	}
	fclose(fp);
	return 0;
}

/* Check correctness of the given format */
static void verify_format(char *fmt, char *varname)
{
	char *ch;

	for (ch = strchr(fmt, '%'); ch; ch = strchr(ch+2, '%')) {
		switch (*(ch+1)) {
			case 's':
			case 'i':
			case 'h':
			case 'd':
			case '%':
				continue;
			default:
				die(1, _("Incorrect format string for variable %s.\n\
Unrecognized expression %%%c.\n"), varname, *(ch+1));
		}
	}
}

static void print_cfg_err(char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);
	errstrv(fmt, args);
	va_end(args);
	if (flags & FL_IGNORE_CFG_ERR) {
		errstr(_("Ignoring error in config file.\n"));
	} else {
		errstr(_("Aborting. Use option -I if you want warnquota to "
			"ignore errors in the config file as it used to in "
			"older versions.\n"));
	}
}

/*
 * Reads config parameters from configfile
 * uses default values if errstr occurs
 */
static int readconfigfile(const char *filename, struct configparams *config)
{
	FILE *fp;
	char buff[IOBUF_SIZE];
	char *var;
	char *value;
	char *pos;
	int line, len, bufpos;
	char *locale;

	/* set default values */
	memset(config, 0, sizeof(*config));
	sstrncpy(config->mail_cmd, MAIL_CMD, CNF_BUFFER);
	sstrncpy(config->from, FROM, CNF_BUFFER);
	sstrncpy(config->subject, SUBJECT, CNF_BUFFER);
	sstrncpy(config->cc_to, CC_TO, CNF_BUFFER);
	sstrncpy(config->support, SUPPORT, CNF_BUFFER);
	sstrncpy(config->phone, PHONE, CNF_BUFFER);
	setlocale(LC_ALL, NULL);
	locale = setlocale(LC_MESSAGES, NULL);
#ifdef HAVE_NL_LANGINFO
	if (locale && strcasecmp(locale, "posix") && strcasecmp(locale, "c")) {
		locale = nl_langinfo(CODESET);
		sstrncpy(config->charset, locale, CNF_BUFFER);
	}
#endif
	maildev[0] = 0;
	config->cc_before = -1;

#ifdef USE_LDAP_MAIL_LOOKUP
	config->ldap_tls = LDAP_OPT_X_TLS_NEVER;
	config->ldap_vers = LDAP_VERSION3;
#endif

	if (!(fp = fopen(filename, "r"))) {
		errstr(_("Cannot open %s: %s\n"), filename, strerror(errno));
		return -1;
	}

	line = 0;
	bufpos = 0;
	while (fgets(buff + bufpos, sizeof(buff) - bufpos, fp)) {	/* start reading lines */
		line++;

		if (!bufpos) {
			/* check for comments or empty lines */
			if (buff[0] == '#' || buff[0] == ';')
				continue;
			/* Is line empty? */
			for (pos = buff; isspace(*pos); pos++);
			if (!*pos)			/* Nothing else was on the line */
				continue;
		}
		len = bufpos + strlen(buff+bufpos);
		if (buff[len-1] != '\n') {
			if (len == IOBUF_SIZE-1) {
				print_cfg_err(
					_("line %d: Line too long! Maximum is %d.\n"),
				        line, IOBUF_SIZE-1);
				if (flags & FL_IGNORE_CFG_ERR)
					continue;
				return -1;
			}
			/* Last line without \n. Just pretend there is one. */
			len++;
		} else {
			len--;
			if (buff[len-1] == '\\') {	/* Should join with next line? */
				bufpos = len-1;
				continue;
			}
		}
		buff[len] = 0;
		bufpos = 0;
		
		/* check for a '=' char */
		if ((pos = strchr(buff, '='))) {
			*pos = 0;	/* split buff in two parts: var and value */
			var = buff;
			value = pos + 1;

			stripstring(&var);
			stripstring(&value);

			/* check if var matches anything */
			if (!strcmp(var, "MAIL_CMD"))
				sstrncpy(config->mail_cmd, value, CNF_BUFFER);
			else if (!strcmp(var, "FROM"))
				sstrncpy(config->from, value, CNF_BUFFER);
			else if (!strcmp(var, "SUBJECT"))
				sstrncpy(config->subject, value, CNF_BUFFER);
			else if (!strcmp(var, "CC_TO"))
				sstrncpy(config->cc_to, value, CNF_BUFFER);
			else if (!strcmp(var, "SUPPORT"))
				sstrncpy(config->support, value, CNF_BUFFER);
			else if (!strcmp(var, "PHONE"))
				sstrncpy(config->phone, value, CNF_BUFFER);
			else if (!strcmp(var, "CHARSET"))
				sstrncpy(config->charset, value, CNF_BUFFER);
			else if (!strcmp(var, "MAILDEV"))
				/* set the global */
				sstrncpy(maildev, value, CNF_BUFFER);
			else if (!strcmp(var, "MESSAGE")) {
				config->user_message = sstrdup(value);
				create_eoln(config->user_message);
				verify_format(config->user_message, "MESSAGE");
			}
			else if (!strcmp(var, "SIGNATURE")) {
				config->user_signature = sstrdup(value);
				create_eoln(config->user_signature);
				verify_format(config->user_signature, "SIGNATURE");
			}
			else if (!strcmp(var, "GROUP_MESSAGE")) {
				config->group_message = sstrdup(value);
				create_eoln(config->group_message);
				verify_format(config->group_message, "GROUP_MESSAGE");
			}
			else if (!strcmp(var, "GROUP_SIGNATURE")) {
				config->group_signature = sstrdup(value);
				create_eoln(config->group_signature);
				verify_format(config->group_signature, "GROUP_SIGNATURE");
			}
			else if (!strcmp(var, "CC_BEFORE")) {
				int num;
				char unit[10];

				if (sscanf(value, "%d%s", &num, unit) != 2)
					goto cc_parse_err;
				if (str2timeunits(num, unit, &config->cc_before) < 0) {
cc_parse_err:
					die(1, _("Cannot parse time at CC_BEFORE variable (line %d).\n"), line);
				}
			}
#ifdef USE_LDAP_MAIL_LOOKUP
			else if (!strcmp(var, "LDAP_MAIL")) {
				if(strcasecmp(value, "true") == 0) 
					config->use_ldap_mail = 1;
				else
					config->use_ldap_mail = 0;
			}
			else if (!strcmp(var, "LDAP_TLS")) {
				if (strcasecmp(value, "never") == 0) {
					config->ldap_starttls = 1;
					config->ldap_tls = LDAP_OPT_X_TLS_NEVER;
				}
				else if (strcasecmp(value, "demand") == 0) {
					config->ldap_starttls = 1;
					config->ldap_tls = LDAP_OPT_X_TLS_DEMAND;
				}
				else if (strcasecmp(value, "allow") == 0) {
					config->ldap_starttls = 1;
					config->ldap_tls = LDAP_OPT_X_TLS_ALLOW;
				}
				else if (strcasecmp(value, "try") == 0) {
					config->ldap_starttls = 1;
					config->ldap_tls = LDAP_OPT_X_TLS_TRY;
				}
				else
					config->ldap_starttls = 0;
			}
			else if (!strcmp(var, "LDAP_HOST"))
				sstrncpy(config->ldap_host, value, CNF_BUFFER);
			else if (!strcmp(var, "LDAP_PORT"))
				config->ldap_port = (int)strtol(value, NULL, 10);
			else if (!strcmp(var, "LDAP_URI"))
				sstrncpy(config->ldap_uri, value, CNF_BUFFER);
			else if(!strcmp(var, "LDAP_BINDDN"))
				sstrncpy(config->ldap_binddn, value, CNF_BUFFER);
			else if(!strcmp(var, "LDAP_BINDPW"))
				sstrncpy(config->ldap_bindpw, value, CNF_BUFFER);
			else if(!strcmp(var, "LDAP_BASEDN"))
				sstrncpy(config->ldap_basedn, value, CNF_BUFFER);
			else if(!strcmp(var, "LDAP_SEARCH_ATTRIBUTE"))
				sstrncpy(config->ldap_search_attr, value, CNF_BUFFER);
			else if(!strcmp(var, "LDAP_MAIL_ATTRIBUTE"))
				sstrncpy(config->ldap_mail_attr, value, CNF_BUFFER);
			else if(!strcmp(var, "LDAP_DEFAULT_MAIL_DOMAIN"))
				sstrncpy(config->default_domain, value, CNF_BUFFER);
#else
			else if (!strcmp(var, "LDAP_MAIL") ||
				 !strcmp(var, "LDAP_TLS") ||
				 !strcmp(var, "LDAP_HOST") ||
				 !strcmp(var, "LDAP_PORT") ||
				 !strcmp(var, "LDAP_URI") ||
				 !strcmp(var, "LDAP_BINDDN") ||
				 !strcmp(var, "LDAP_BINDPW") ||
				 !strcmp(var, "LDAP_BASEDN") ||
				 !strcmp(var, "LDAP_SEARCH_ATTRIBUTE") ||
				 !strcmp(var, "LDAP_MAIL_ATTRIBUTE") ||
				 !strcmp(var, "LDAP_DEFAULT_MAIL_DOMAIN")) {
				print_cfg_err(_("line %d: LDAP variable in config "
					"file but LDAP support is not compiled.\n"), line);
				if (flags & FL_IGNORE_CFG_ERR)
					continue;
				return -1;
			}
#endif
			else {	/* not matched at all */
				print_cfg_err(_("line %d: Unknown variable %s in "
					"config file.\n"), line, var);
				if (flags & FL_IGNORE_CFG_ERR)
					continue;
				return -1;
			}
		}
		else {		/* no '=' char in this line */
			print_cfg_err(_("line %d: Missing '=' in config file.\n"), line);
			if (flags & FL_IGNORE_CFG_ERR)
				continue;
			return -1;
		}
	}
	if (bufpos) {
		print_cfg_err(_("line %d: Unterminated last line.\n"), line);
		if (!(flags & FL_IGNORE_CFG_ERR))
			return -1;
	}
#ifdef USE_LDAP_MAIL_LOOKUP
	if (config->use_ldap_mail)
	{
		if (!config->ldap_uri[0]) {
			if (CNF_BUFFER <= snprintf(config->ldap_uri, CNF_BUFFER,
				    "ldap://%s:%d", config->ldap_host,
				    config->ldap_port)) {
				errstr(_("Could not format LDAP URI because "
					    "it's longer than %d bytes.\n"),
					    CNF_BUFFER);
				return -1;
			}
			errstr(_("LDAP library version >= 2.3 detected. Please use LDAP_URI instead of hostname and port.\nGenerated URI %s\n"), config->ldap_uri);
		}
	}
#endif
	fclose(fp);

	return 0;
}

static int admin_cmp(const void *a1, const void *a2)
{
	return strcmp(((struct adminstable *)a1)->grpname, ((struct adminstable *)a2)->grpname);
}

/* Get administrators of the groups */
static int get_groupadmins(void)
{
	FILE *f;
	int line = 0;
	char buffer[IOBUF_SIZE], *colpos, *grouppos, *endname, *adminpos;

	if (!(f = fopen(adminsfile, "r"))) {
		errstr(_("Cannot open file with group administrators: %s\n"), strerror(errno));
		return -1;
	}
	
	while (fgets(buffer, IOBUF_SIZE, f)) {
		line++;
		if (buffer[0] == ';' || buffer[0] == '#')
			continue;
		/* Skip initial spaces */
		for (colpos = buffer; isspace(*colpos); colpos++);
		if (!*colpos)	/* Empty line? */
			continue;
		/* Find splitting colon */
		for (grouppos = colpos; *colpos && *colpos != ':'; colpos++);
		if (!*colpos || grouppos == colpos) {
			errstr(_("Parse error at line %d. Cannot find end of group name.\n"), line);
			continue;
		}
		/* Cut trailing spaces */
		for (endname = colpos-1; isspace(*endname); endname--);
		*(++endname) = 0;
		/* Skip initial spaces at admins name */
		for (colpos++; isspace(*colpos); colpos++);
		if (!*colpos) {
			errstr(_("Parse error at line %d. Cannot find administrators name.\n"), line);
			continue;
		}
		/* Go through admins name */
		for (adminpos = colpos; !isspace(*colpos); colpos++);
		if (*colpos) {	/* Some characters after name? */
			*colpos = 0;
			/* Skip trailing spaces */
			for (colpos++; isspace(*colpos); colpos++);
			if (*colpos) {
				errstr(_("Parse error at line %d. Trailing characters after administrators name.\n"), line);
				continue;
			}
		}
		if (adminscnt >= adminsalloc)
			adminstable = srealloc(adminstable, sizeof(struct adminstable)*(adminsalloc+=ADMIN_TAB_ALLOC));
		adminstable[adminscnt].grpname = sstrdup(grouppos);
		adminstable[adminscnt++].adminname = sstrdup(adminpos);
	}

	fclose(f);
	qsort(adminstable, adminscnt, sizeof(struct adminstable), admin_cmp);
	return 0;
}

static struct quota_handle *find_handle_dev(char *dev, struct quota_handle **handles)
{
	int i;

	for (i = 0; handles[i] && strcmp(dev, handles[i]->qh_quotadev); i++);
	return handles[i];
}

static void warn_quota(int fs_count, char **fs)
{
	struct quota_handle **handles;
	struct configparams config;
	int i;

	if (readconfigfile(configfile, &config) < 0)
		wc_exit(1);
	if (get_quotatable() < 0)
		wc_exit(1);

	if (flags & FL_USER) {
		handles = create_handle_list(fs_count, fs, USRQUOTA, -1, IOI_READONLY | IOI_INITSCAN, MS_LOCALONLY | (flags & FL_NOAUTOFS ? MS_NO_AUTOFS : 0));
		if (!maildev[0] || !strcasecmp(maildev, "any"))
			maildev_handle = NULL;
		else
			maildev_handle = find_handle_dev(maildev, handles);
		for (i = 0; handles[i]; i++)
			handles[i]->qh_ops->scan_dquots(handles[i], check_offence);
		dispose_handle_list(handles);
	}
	if (flags & FL_GROUP) {
		if (get_groupadmins() < 0)
			wc_exit(1);
		handles = create_handle_list(fs_count, fs, GRPQUOTA, -1, IOI_READONLY | IOI_INITSCAN, MS_LOCALONLY | (flags & FL_NOAUTOFS ? MS_NO_AUTOFS : 0));
		if (!maildev[0] || !strcasecmp(maildev, "any"))
			maildev_handle = NULL;
		else
			maildev_handle = find_handle_dev(maildev, handles);
		for (i = 0; handles[i]; i++)
			handles[i]->qh_ops->scan_dquots(handles[i], check_offence);
		dispose_handle_list(handles);
	}
	if (mail_to_offenders(&config) < 0)
		wc_exit(1);
}

/* Print usage information */
static void usage(void)
{
	errstr(_("Usage:\n  warnquota [-ugsid] [-F quotaformat] [-c configfile] [-q quotatabfile] [-a adminsfile] [filesystem...]\n\n\
-u, --user                      warn users\n\
-g, --group                     warn groups\n\
-s, --human-readable[=units]    display numbers in human friendly units (MB,\n\
                                GB, ...). Units can be also specified\n\
                                explicitely by an optional argument in format\n\
                                [kgt],[kgt] where the first character specifies\n\
                                space units and the second character specifies\n\
                                inode units\n\
-i, --no-autofs                 avoid autofs mountpoints\n\
-d, --no-details                do not send quota information itself\n\
-F, --format=formatname         use quotafiles of specific format\n\
-c, --config=config-file        non-default config file\n\
-q, --quota-tab=quotatab-file   non-default quotatab\n\
-a, --admins-file=admins-file   non-default admins file\n\
-I, --ignore-config-errors	ignore unknown statements in config file\n\
-h, --help                      display this help message and exit\n\
-V, --version                   display version information and exit\n\n"));
	errstr(_("Bugs to %s\n"), PACKAGE_BUGREPORT);
	wc_exit(1);
}
 
static void parse_options(int argcnt, char **argstr)
{
	int ret;
	struct option long_opts[] = {
		{ "user", 0, NULL, 'u' },
		{ "group", 0, NULL, 'g' },
		{ "version", 0, NULL, 'V' },
		{ "help", 0, NULL, 'h' },
		{ "format", 1, NULL, 'F' },
		{ "config", 1, NULL, 'c' },
		{ "quota-tab", 1, NULL, 'q' },
		{ "admins-file", 1, NULL, 'a' },
		{ "no-autofs", 0, NULL, 'i' },
		{ "human-readable", 2, NULL, 's' },
		{ "no-details", 0, NULL, 'd' },
		{ "ignore-config-errors", 0, NULL, 'I' },
		{ NULL, 0, NULL, 0 }
	};
 
	while ((ret = getopt_long(argcnt, argstr, "ugVF:hc:q:a:is::dI", long_opts, NULL)) != -1) {
		switch (ret) {
		  case '?':
		  case 'h':
			usage();
		  case 'V':
			version();
			exit(0);
		  case 'F':
			if ((fmt = name2fmt(optarg)) == QF_ERROR)
				wc_exit(1);
			break;
		  case 'c':
			configfile = optarg;
			break;
		  case 'q':
			quotatabfile = optarg;
			break;
		  case 'a':
			adminsfile = optarg;
			break;
		  case 'u':
			flags |= FL_USER;
			break;
		  case 'g':
			flags |= FL_GROUP;
			break;
		  case 'i':
			flags |= FL_NOAUTOFS;
			break;
		  case 's':
			inodeunit = spaceunit = S2S_AUTO;
			if (optarg) {
				if (unitopt2unit(optarg, &spaceunit, &inodeunit) < 0)
					die(1, _("Bad output format units for human readable output: %s\n"), optarg);
			}
			break;
		  case 'd':
			flags |= FL_NODETAILS;
			break;
		  case 'I':
			flags |= FL_IGNORE_CFG_ERR;
			break;
		}
	}
	if (!(flags & FL_USER) && !(flags & FL_GROUP))
		flags |= FL_USER;
}
 
static void get_host_name(void)
{
	struct utsname uts;

	if (uname(&uts))
		die(1, _("Cannot get host name: %s\n"), strerror(errno));
	hostname = sstrdup(uts.nodename);
	domainname = sstrdup(uts.domainname);
}

int main(int argc, char **argv)
{
	gettexton();
	progname = basename(argv[0]);
	get_host_name();

	parse_options(argc, argv);
	init_kernel_interface();
	warn_quota(argc - optind, argc > optind ? argv + optind : NULL);

	wc_exit(0);
	return 0;
}
