/* NickServ utility routines.
 *
 * IRC Services is copyright (c) 1996-2007 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

/* If STANDALONE_NICKSERV is defined when compiling this file, the
 * following four routines will be defined as `static' for use in other
 * modules/programs:
 *    new_nickinfo()
 *    free_nickinfo()
 *    new_nickgroupinfo()
 *    free_nickgroupinfo()
 * This file should be #include'd in the file making use of the routines.
 * Note that new_nickgroupinfo() will not check for the presence of any
 * nickgroup ID it assigns, since get_nickgroupinfo() is not assumed to be
 * available.
 */

#ifdef STANDALONE_NICKSERV

# define STANDALONE_STATIC static
# undef EXPORT_FUNC
# define EXPORT_FUNC(x) /*nothing*/

#else

# define STANDALONE_STATIC /*nothing*/

# include "services.h"
# include "modules.h"
# include "language.h"
# include "encrypt.h"

# include "nickserv.h"
# include "ns-local.h"

#endif

/*************************************************************************/

#ifndef STANDALONE_NICKSERV

static Module *module;
static int cb_cancel_user  = -1;
static int cb_check_expire = -1;
static int cb_check_recognized = -1;
static int cb_delete       = -1;
static int cb_groupdelete  = -1;

#endif

/*************************************************************************/
/************************ Global utility routines ************************/
/*************************************************************************/

/* Allocate and initialize a new NickInfo structure. */

EXPORT_FUNC(new_nickinfo)
STANDALONE_STATIC NickInfo *new_nickinfo(void)
{
    NickInfo *ni = scalloc(sizeof(*ni), 1);
    return ni;
}

/*************************************************************************/

/* Free a NickInfo structure and all associated data. */

EXPORT_FUNC(free_nickinfo)
STANDALONE_STATIC void free_nickinfo(NickInfo *ni)
{
    if (ni) {
	free(ni->last_usermask);
	free(ni->last_realmask);
	free(ni->last_realname);
	free(ni->last_quit);
	free(ni);
    }
}

/*************************************************************************/

/* Allocate and initialize a new NickGroupInfo structure.  If `seed' is
 * non-NULL, the group ID will be set to a value unique from any other
 * currently stored in the database (in this case `seed' is used in the
 * initial attempt to allocate the ID).  If `seed' is NULL, the ID value
 * is left at zero.
 */

EXPORT_FUNC(new_nickgroupinfo)
STANDALONE_STATIC NickGroupInfo *new_nickgroupinfo(const char *seed)
{
    NickGroupInfo *ngi = scalloc(sizeof(*ngi), 1);

    if (seed) {
	uint32 id;
	int count;

	id = 0;
	for (count = 0; seed[count] != 0; count++)
	    id ^= seed[count] << ((count % 6) * 5);
	if (id == 0)
	    id = 1;
#ifndef STANDALONE_NICKSERV
	count = 0;
	while (get_nickgroupinfo(id) != NULL && ++count < NEWNICKGROUP_TRIES) {
	    id = rand() + rand();	/* 32 bits of randomness, hopefully */
	    if (id == 0)
		id = 1;
	}
	if (count >= NEWNICKGROUP_TRIES) {
	    module_log("new_nickgroupinfo() unable to allocate unused ID"
		       " after %d tries!  Giving up.", NEWNICKGROUP_TRIES);
	    free(ngi);
	    return NULL;
	}
#endif
	ngi->id = id;
    }

    ngi->memos.memomax = MEMOMAX_DEFAULT;
    ngi->channelmax = CHANMAX_DEFAULT;
    ngi->language = LANG_DEFAULT;
    ngi->timezone = TIMEZONE_DEFAULT;
    return ngi;
}

/*************************************************************************/

/* Free a NickGroupInfo structure and all associated data. */

EXPORT_FUNC(free_nickgroupinfo)
STANDALONE_STATIC void free_nickgroupinfo(NickGroupInfo *ngi)
{
    int i;

    if (!ngi)
	return;
    free(ngi->url);
    free(ngi->email);
    free(ngi->info);
    if (ngi->suspendinfo)
	free_suspendinfo(ngi->suspendinfo);
    free(ngi->nicks);
    ARRAY_FOREACH (i, ngi->access)
	free(ngi->access[i]);
    free(ngi->access);
    ARRAY_FOREACH (i, ngi->ajoin)
	free(ngi->ajoin[i]);
    free(ngi->ajoin);
    free(ngi->channels);
    ARRAY_FOREACH (i, ngi->memos.memos)
	free(ngi->memos.memos[i].text);
    free(ngi->memos.memos);
    ARRAY_FOREACH (i, ngi->ignore)
	free(ngi->ignore[i]);
    free(ngi->ignore);
    ARRAY_FOREACH (i, ngi->id_users) {
	User *u = ngi->id_users[i];
	int j;
	ARRAY_SEARCH_PLAIN_SCALAR(u->id_nicks, ngi->id, j);
	if (j < u->id_nicks_count) {
	    ARRAY_REMOVE(u->id_nicks, j);
#ifndef STANDALONE_NICKSERV
	} else {
	    module_log("BUG: free_nickgroupinfo(): user %p (%s) listed in"
		       " id_users for nickgroup %u, but nickgroup not in"
		       " id_nicks!", u, u->nick, ngi->id);
#endif
	}
    }
    free(ngi->id_users);
    free(ngi);
}

/*************************************************************************/

#ifndef STANDALONE_NICKSERV  /* to the end of the file */

/*************************************************************************/

/* Check whether the given nickname (or its suspension) should be expired,
 * and do the expiration if so.  Return 1 if the nickname was deleted, else
 * 0.  Note that we do last-seen-time updates here as well.
 */

EXPORT_FUNC(check_expire_nick)
int check_expire_nick(NickInfo *ni)
{
    User *u = ni->user;
    NickGroupInfo *ngi;
    time_t now = time(NULL);

    if (u && user_id_or_rec(u)) {
	if (debug >= 2)
	    module_log("debug: updating last seen time for %s", u->nick);
	ni->last_seen = time(NULL);
	put_nickinfo(ni);
    }
    ngi = ni->nickgroup ? get_ngi_id(ni->nickgroup) : NULL;
    if (call_callback_2(module, cb_check_expire, ni, ngi) > 0) {
	if (u)
	    notice_lang(s_NickServ, u, NICK_EXPIRED);
	delnick(ni);
	return 1;
    }
    if (NSExpire
     && now >= ni->last_seen + NSExpire
     && !(ni->status & (NS_VERBOTEN | NS_NOEXPIRE))
     && (!ngi || !ngi->suspendinfo)
    ) {
	module_log("Expiring nickname %s", ni->nick);
	if (u)
	    notice_lang(s_NickServ, u, NICK_EXPIRED);
	delnick(ni);
	return 1;
    }
    if (ngi && ngi->suspendinfo && ngi->suspendinfo->expires
     && now >= ngi->suspendinfo->expires
    ) {
	module_log("Expiring suspension for %s (nick group %u)",
		   ni->nick, ngi->id);
	unsuspend_nick(ngi, 1);
    }
    return 0;
}

/*************************************************************************/

/* Retrieve a NickInfo structure without expiring it.  Routines which hold
 * onto a NickGroupInfo pointer while calling get_nickinfo() need to use
 * this routine instead to avoid the NickGroupInfo being deallocated if the
 * nick being searched for is found to have expired.
 */

NickInfo *get_nickinfo_noexpire(const char *nick)
{
    NickInfo *ni;
    int old_noexpire = noexpire;
    noexpire = 1;
    ni = get_nickinfo(nick);
    noexpire = old_noexpire;
    return ni;
}

/*************************************************************************/

/* Retrieve the NickGroupInfo structure associated with the given NickInfo.
 * If it cannot be retrieved, log an error message and return NULL.
 * Note: Callers should use get_ngi[_id]() or check_ngi[_id](), which embed
 *       the current source file and line number in the log message.
 */

EXPORT_FUNC(_get_ngi)
NickGroupInfo *_get_ngi(NickInfo *ni, const char *file, int line)
{
    NickGroupInfo *ngi;

    if (!ni) {
	module_log("BUG: ni==NULL in get_ngi() (called from %s:%d)",
		   file, line);
	return NULL;
    }
    ngi = get_nickgroupinfo(ni->nickgroup);
    if (!ngi)
	module_log("Unable to get NickGroupInfo (id %u) for %s at %s:%d",
		   ni->nickgroup, ni->nick, file, line);
    return ngi;
}

EXPORT_FUNC(_get_ngi_id)
NickGroupInfo *_get_ngi_id(uint32 id, const char *file, int line)
{
    NickGroupInfo *ngi = get_nickgroupinfo(id);
    if (!ngi)
	module_log("Unable to get NickGroupInfo (id %u) at %s:%d",
		   id, file, line);
    return ngi;
}

/*************************************************************************/

/* Return whether the user has previously identified for the given nick
 * group. (1=yes, 0=no)  If the nick group does not exist or is in a
 * not-authenticated state (i.e. authcode != 0), this function will always
 * return 0.
 */

EXPORT_FUNC(has_identified_nick)
int has_identified_nick(const User *u, uint32 group)
{
    int i;
    NickGroupInfo *ngi;

    if (!(ngi = get_ngi_id(group)) || ngi->authcode)
	return 0;
    ARRAY_SEARCH_PLAIN_SCALAR(u->id_nicks, group, i);
    return i < u->id_nicks_count;
}

/*************************************************************************/
/*********************** Internal utility routines ***********************/
/*************************************************************************/

/* Check whether the given nickname is allowed to be registered (with the
 * given password and E-mail address) or linked (password/email NULL).
 * Returns nonzero if allowed, 0 if not.  A wrapper for the "REGISTER/LINK
 * check" callback.
 */

int reglink_check(User *u, const char *nick, char *password, char *email)
{
    int res = call_callback_4(module, cb_reglink_check, u,nick,password,email);
    if (res < 0)
	module_log("REGISTER/LINK callback returned an error");
    return res == 0;
}

/*************************************************************************/

/* Update the last_usermask, last_realmask, and last_realname nick fields
 * from the given user's data, and sets last_seen to the current time.
 * Assumes u->ni is non-NULL.
 */

void update_userinfo(User *u)
{
    const char *host;
    NickInfo *ni = u->ni;

    ni->last_seen = time(NULL);
    free(ni->last_usermask);
    if (u->fakehost)
	host = u->fakehost;
    else
	host = u->host;
    ni->last_usermask = smalloc(strlen(u->username)+strlen(host)+2);
    sprintf(ni->last_usermask, "%s@%s", u->username, host);
    free(ni->last_realmask);
    ni->last_realmask = smalloc(strlen(u->username)+strlen(u->host)+2);
    sprintf(ni->last_realmask, "%s@%s", u->username, u->host);
    free(ni->last_realname);
    ni->last_realname = sstrdup(u->realname);
}

/*************************************************************************/

/* Check whether a user is recognized for the nick they're using, or if
 * they're the same user who last identified for the nick.  If not, send
 * warnings as appropriate.  If so (and not NI_SECURE), update last seen
 * info.  Return 1 if the user is valid and recognized, 0 otherwise (note
 * that this means an NI_SECURE nick will return 0 from here unless the
 * user has identified for the nick before).  If the user's nick is not
 * registered, 0 is returned.
 *
 * The `nick' parameter is the nick the user is using.  It is passed
 * separately to handle cases where the User structure has a different
 * nick (e.g. when changing nicks).
 */

int validate_user(User *u)
{
    NickInfo *ni;
    NickGroupInfo *ngi;
    int is_recognized;

    ni = get_nickinfo(u->nick);
    if (ni && ni->nickgroup) {
	ngi = get_ngi(ni);
	if (ngi) {
	    ni->locked++;
	    ngi->locked++;
	} else {
	    ni = NULL;
	    ngi = NICKGROUPINFO_INVALID;
	}
    } else {
	if (ni)
	    ni->locked++;
	ngi = NULL;
    }
    if (u->ni)
	u->ni->locked--;
    if (u->ngi && u->ngi != NICKGROUPINFO_INVALID)
	u->ngi->locked--;
    u->ni = ni;
    u->ngi = ngi;
    if (!ni)
	return 0;

    ni->authstat = 0;
    ni->user = u;

    if ((ni->status & NS_VERBOTEN) || ngi->suspendinfo) {
	if (usermode_reg) {
	    send_cmd(s_NickServ, "SVSMODE %s :-%s", u->nick,
		     mode_flags_to_string(usermode_reg, MODE_USER));
	}
	notice_lang(s_NickServ, u, NICK_MAY_NOT_BE_USED);
	notice_lang(s_NickServ, u, DISCONNECT_IN_1_MINUTE);
	add_ns_timeout(ni, TO_SEND_433, 40);
	add_ns_timeout(ni, TO_COLLIDE, 60);
	return 0;
    }

    if (!ngi->authcode) {
	/* Neither of these checks should be done if the nick is awaiting
	 * authentication */

	if (has_identified_nick(u, ngi->id)) {
	    set_identified(u, ni, ngi);
	    return 1;
	}

	if (!NoSplitRecovery) {
	    /*
	     * This can be exploited to gain improper privilege if an attacker
	     * has the same Services stamp, username and hostname as the
	     * victim.
	     *
	     * Under ircd.dal 4.4.15+ (Dreamforge) and other servers supporting
	     * a Services stamp, Services guarantees that the first condition
	     * cannot occur unless the stamp counter rolls over (2^31-1 client
	     * connections).  This is practically infeasible given present
	     * technology.  As an example, on a network of 30 servers, an
	     * attack introducing 50 new clients every second on every server,
	     * requiring at least 10-15 megabits of bandwidth, would need to be
	     * sustained for over 16 days to cause the stamp to roll over.
	     *
	     * Under other servers, an attack is theoretically possible, but
	     * would require access to either the computer the victim is using
	     * for IRC or the DNS servers for the victim's domain and IP
	     * address range in order to have the same hostname, and would
	     * require that the attacker connect so that he has the same server
	     * timestamp as the victim.  Practically, the former can be
	     * accomplished either by finding a victim who uses a shell account
	     * on a multiuser system and obtaining an account on the same
	     * system, or through the scripting capabilities of many IRC
	     * clients combined with social engineering; the latter could be
	     * accomplished by finding a server with a clock slower than that
	     * of the victim's server and timing the connection attempt
	     * properly.
	     *
	     * If someone gets a hacked server into your network, all bets are
	     * off.
	     */
	    if (ni->id_stamp != 0 && u->servicestamp == ni->id_stamp) {
		char buf[BUFSIZE];
		snprintf(buf, sizeof(buf), "%s@%s", u->username, u->host);
		if (ni->last_realmask && strcmp(buf, ni->last_realmask) == 0) {
		    set_identified(u, ni, ngi);
		    return 1;
		}
	    }
	}

    }  /* if (!ngi->authcode) */

    /* From here on down, the user is known to be not identified.  Clear
     * any "registered nick" mode from them. */
    if (usermode_reg) {
	send_cmd(s_NickServ, "SVSMODE %s :-%s", u->nick,
		 mode_flags_to_string(usermode_reg, MODE_USER));
    }

    is_recognized = (call_callback_1(module, cb_check_recognized, u) == 1);

    if (!(ngi->flags & NF_SECURE) && !ngi->authcode && is_recognized) {
	ni->authstat |= NA_RECOGNIZED;
	update_userinfo(u);
	put_nickinfo(u->ni);
	return 1;
    }

    if (is_recognized || !NSAllowKillImmed || !(ngi->flags & NF_KILL_IMMED)) {
	if (ngi->flags & NF_SECURE)
	    notice_lang(s_NickServ, u, NICK_IS_SECURE, s_NickServ);
	else
	    notice_lang(s_NickServ, u, NICK_IS_REGISTERED, s_NickServ);
    }

    if ((ngi->flags & NF_KILLPROTECT) && !is_recognized) {
	if (!ngi->authcode && NSAllowKillImmed && (ngi->flags&NF_KILL_IMMED)) {
	    collide(ni, 0);
	} else if (!ngi->authcode && (ngi->flags & NF_KILL_QUICK)) {
	    notice_lang(s_NickServ, u, DISCONNECT_IN_20_SECONDS);
	    add_ns_timeout(ni, TO_COLLIDE, 20);
	    add_ns_timeout(ni, TO_SEND_433, 10);
	} else {
	    notice_lang(s_NickServ, u, DISCONNECT_IN_1_MINUTE);
	    add_ns_timeout(ni, TO_COLLIDE, 60);
	    add_ns_timeout(ni, TO_SEND_433, 40);
	}
    }

    if (!noexpire && NSExpire && NSExpireWarning
     && !(ni->status & NS_NOEXPIRE)
    ) {
	int time_left = NSExpire - (time(NULL) - ni->last_seen);
	if (time_left <= NSExpireWarning) {
	    notice_lang(s_NickServ, u, NICK_EXPIRES_SOON,
			maketime(get_ngi(ni),time_left,0),
			s_NickServ, s_NickServ);
	}
    }

    return 0;
}

/*************************************************************************/

/* Cancel validation flags for a nick (i.e. when the user with that nick
 * signs off or changes nicks).  Also cancels any impending collide, sets
 * the nick's last seen time if the user is recognized for the nick and
 * unlocks the nick's info records.
 */

void cancel_user(User *u)
{
    NickInfo *ni = u->ni;
    NickGroupInfo *ngi = (u->ngi==NICKGROUPINFO_INVALID ? NULL : u->ngi);

    if (ni) {
	int old_status = ni->status;
	int old_authstat = ni->authstat;
	if (nick_id_or_rec(ni)) {
	    ni->last_seen = time(NULL);
	    put_nickinfo(ni);
	}
	ni->user = NULL;
	ni->status &= ~NS_TEMPORARY;
	ni->authstat = 0;
	if (old_status & NS_GUESTED)
	    introduce_enforcer(ni);
	call_callback_3(module, cb_cancel_user, u, old_status,
			old_authstat);
	rem_ns_timeout(ni, TO_COLLIDE, 1);
	ni->locked--;
	if (ngi)
	    ngi->locked--;
    }
}

/*************************************************************************/

/* Marks a user as having identified for the given nick. */

void set_identified(User *u, NickInfo *ni, NickGroupInfo *ngi)
{
    ni->authstat &= ~NA_IDENT_NOMAIL;
    ni->authstat |= NA_IDENTIFIED;
    ni->id_stamp = u->servicestamp;
    if (!nick_recognized(ni)) {
	update_userinfo(u);
	ni->authstat |= NA_RECOGNIZED;
    }
    put_nickinfo(ni);
    if (!has_identified_nick(u, ngi->id)) {
	ARRAY_EXTEND(u->id_nicks);
	u->id_nicks[u->id_nicks_count-1] = ngi->id;
	ARRAY_EXTEND(ngi->id_users);
	ngi->id_users[ngi->id_users_count-1] = u;
    }
    if (usermode_reg) {
	send_cmd(s_NickServ, "SVSMODE %s :+%s", u->nick,
		 mode_flags_to_string(usermode_reg, MODE_USER));
    }
}

/*************************************************************************/
/*************************************************************************/

/* Add a nick to the database.  Returns a pointer to the new NickInfo
 * structure if the nick was successfully registered, NULL otherwise.
 * Assumes nick does not already exist.  If `nickgroup_ret' is non-NULL,
 * a new NickGroupInfo structure is created, ni->nickgroup/ngi->*nick*
 * are set appropriately, and a pointer to the NickGroupInfo is returned
 * in that variable.  Note that makenick() may return NULL if a nick group
 * is requested and new_nickgroupinfo() returns NULL.
 *
 * If `nick' is longer than NICKMAX-1 characters, it is silently truncated
 * in the new NickInfo and NickGroupInfo.
 */

NickInfo *makenick(const char *nick, NickGroupInfo **nickgroup_ret)
{
    NickInfo *ni;
    NickGroupInfo *ngi;

#ifdef CLEAN_COMPILE
    ngi = NULL;
#endif
    if (nickgroup_ret) {
	ngi = new_nickgroupinfo(nick);
	if (!ngi)
	    return NULL;
    }
    ni = new_nickinfo();
    strscpy(ni->nick, nick, NICKMAX);
    if (nickgroup_ret) {
	ni->nickgroup = ngi->id;
	ARRAY_EXTEND(ngi->nicks);
	strscpy(ngi->nicks[0], nick, NICKMAX);
	*nickgroup_ret = add_nickgroupinfo(ngi);
    }
    return add_nickinfo(ni);
}

/*************************************************************************/

/* Remove a nick from the NickServ database.  Return 1 on success, 0
 * otherwise.
 */

int delnick(NickInfo *ni)
{
    int i;
    NickGroupInfo *ngi;

    rem_ns_timeout(ni, -1, 1);
    if (ni->status & NS_KILL_HELD)
	release(ni, 0);
    if (ni->user) {
	if (usermode_reg)
	    send_cmd(s_NickServ, "SVSMODE %s :-%s", ni->nick,
		     mode_flags_to_string(usermode_reg, MODE_USER));
	ni->user->ni = NULL;
	ni->user->ngi = NULL;
    }

    ngi = ni->nickgroup ? get_nickgroupinfo(ni->nickgroup) : NULL;
    if (ngi) {
	ARRAY_SEARCH_PLAIN(ngi->nicks, ni->nick, irc_stricmp, i);
	if (i >= ngi->nicks_count) {
	    module_log("BUG: delete nick: no entry in ngi->nicks[] for"
		       " nick %s", ni->nick);
	} else {
	    ARRAY_REMOVE(ngi->nicks, i);
	    /* Adjust ngi->mainnick as needed; if the main nick is being
	     * deleted, switch it to the next one in the array, or the
	     * previous if it's currently the last one. */
	    if (ngi->mainnick > i || ngi->mainnick >= ngi->nicks_count)
		ngi->mainnick--;
	}
    }
    call_callback_1(module, cb_delete, ni);
    if (ngi) {
	if (ngi->nicks_count == 0) {
	    call_callback_2(module, cb_groupdelete, ngi, ni->nick);
	    del_nickgroupinfo(ngi);
	} else {
	    put_nickgroupinfo(ngi);
	}
    }
    del_nickinfo(ni);
    return 1;
}

/*************************************************************************/

/* Remove a nick group and all associated nicks from the NickServ database.
 * Return 1 on success, 0 otherwise.
 */

int delgroup(NickGroupInfo *ngi)
{
    int i;

    ARRAY_FOREACH (i, ngi->nicks) {
	NickInfo *ni = get_nickinfo_noexpire(ngi->nicks[i]);
	if (!ni) {
	    module_log("delgroup(%u): missing NickInfo for nick %s",
		       ngi->id, ngi->nicks[i]);
	    continue;
	}
	rem_ns_timeout(ni, -1, 1);
	if (ni->user) {
	    if (usermode_reg)
		send_cmd(s_NickServ, "SVSMODE %s :-%s", ni->nick,
			 mode_flags_to_string(usermode_reg, MODE_USER));
	    ni->user->ni = NULL;
	    ni->user->ngi = NULL;
	}
	call_callback_1(module, cb_delete, ni);
	del_nickinfo(ni);
    }
    call_callback_2(module, cb_groupdelete, ngi, ngi_mainnick(ngi));
    del_nickgroupinfo(ngi);
    return 1;
}

/*************************************************************************/

/* Drop a nickgroup, logging appropriate information about it first.  `u'
 * is the user dropping the nickgroup; `dropemail' should have one of the
 * following values:
 *    - NULL for a user dropping their own nick
 *    - PTR_INVALID for a DROPNICK from a Services admin
 *    - The pattern used for a DROPEMAIL from a Services admin
 * Returns the result of delgroup(ngi).
 */

int drop_nickgroup(NickGroupInfo *ngi, const User *u, const char *dropemail)
{
    int i;

    module_log("%s!%s@%s dropped nickgroup %u%s%s%s%s%s%s%s:",
	       u->nick, u->username, u->host, ngi->id,
	       ngi->email ? " (E-mail " : "",
	       ngi->email ? ngi->email : "",
	       ngi->email ? ")" : "",
	       dropemail ? " as Services admin" : "",
	       (dropemail && dropemail!=PTR_INVALID) ? " (DROPEMAIL on " : "",
	       (dropemail && dropemail!=PTR_INVALID) ? dropemail : "",
	       (dropemail && dropemail!=PTR_INVALID) ? ")" : "");
    ARRAY_FOREACH (i, ngi->nicks) {
	module_log(" -- %s!%s@%s dropped nick %s",
		   u->nick, u->username, u->host, ngi->nicks[i]);
    }
    return delgroup(ngi);
}

/*************************************************************************/

/* Create a new SuspendInfo structure and associate it with the given nick
 * group.
 */

void suspend_nick(NickGroupInfo *ngi, const char *reason,
		  const char *who, const time_t expires)
{
    SuspendInfo *si = new_suspendinfo(who, sstrdup(reason), expires);
    ngi->suspendinfo = si;
    put_nickgroupinfo(ngi);
}

/*************************************************************************/

/* Delete the suspension data for the given nick group.  We also alter the
 * last_seen value for all nicks in this group to ensure that they do not
 * expire within the next NSSuspendGrace seconds, giving the owner a chance
 * to reclaim them (but only if set_time is nonzero).
 */

void unsuspend_nick(NickGroupInfo *ngi, int set_time)
{
    time_t now = time(NULL);

    if (!ngi->suspendinfo) {
	module_log("unsuspend: called on non-suspended nick group %u [%s]",
		   ngi->id, ngi->nicks[0]);
	return;
    }
    free_suspendinfo(ngi->suspendinfo);
    ngi->suspendinfo = NULL;
    put_nickgroupinfo(ngi);
    if (set_time && NSExpire && NSSuspendGrace) {
	int i;
	if (ngi->authcode) {
	    ngi->authset = now;
	    module_log("unsuspend: altering authset time for %s (nickgroup"
		       " %u) to %ld", ngi_mainnick(ngi), ngi->id,
		       (long)ngi->authset);
	}
	ARRAY_FOREACH (i, ngi->nicks) {
	    NickInfo *ni = get_nickinfo_noexpire(ngi->nicks[i]);
	    if (!ni) {
		module_log("unsuspend: unable to retrieve NickInfo for %s"
			   " (nick group %u)", ngi->nicks[i], ngi->id);
		continue;
	    }
	    if (now - ni->last_seen >= NSExpire - NSSuspendGrace) {
		ni->last_seen = now - NSExpire + NSSuspendGrace;
		put_nickinfo(ni);
		module_log("unsuspend: altering last_seen time for %s to %ld",
			   ni->nick, (long)ni->last_seen);
	    }
	}
    }
}

/*************************************************************************/
/*************************************************************************/

/* Check the password given by the user and handle errors if it's not
 * correct (or the encryption routine fails).  `command' is the name of
 * the command calling this function, and `failure_msg' is the message to
 * send to the user if the encryption routine fails.  Returns 1 if the
 * password matches, 0 otherwise.
 */

int nick_check_password(User *u, NickInfo *ni, const char *password,
			const char *command, int failure_msg)
{
    int res;
    NickGroupInfo *ngi = get_ngi(ni);

    if (!ngi) {
	module_log("%s: no nickgroup for %s, aborting password check",
		   command, ni->nick);
	notice_lang(s_NickServ, u, failure_msg);
	return 0;
    }

    res = check_password(password, ngi->pass);
    if (res == 0) {
	module_log("%s: bad password for %s from %s!%s@%s",
		   command, ni->nick, u->nick, u->username, u->host);
	bad_password(s_NickServ, u, ni->nick);
	ni->bad_passwords++;
	if (BadPassWarning && ni->bad_passwords == BadPassWarning) {
	    wallops(s_NickServ, "\2Warning:\2 Repeated bad password attempts"
		    " for nick %s", ni->nick);
	}
	return 0;
    } else if (res == -1) {
	module_log("%s: check_password failed for %s",
		   command, ni->nick);
	notice_lang(s_NickServ, u, failure_msg);
	return 0;
    } else {
	ni->bad_passwords = 0;
	return 1;
    }
}

/*************************************************************************/

/* Return the number of registered nicknames with the given E-mail address.
 * If a registered nickname matches the given E-mail address and has an
 * authcode set (i.e. the address has not been authenticated), return the
 * negative of the number of registered nicknames: for example, if there
 * are 5 nicks with the given address, -5 would be returned.
 *
 * Note that this function is O(n) in the total number of registered nick
 * groups, so do not use it lightly!
 */

int count_nicks_with_email(const char *email)
{
    int count = 0, has_authcode = 0;
    NickGroupInfo *ngi;

    for (ngi = first_nickgroupinfo(); ngi; ngi = next_nickgroupinfo()) {
	if (ngi->email && stricmp(ngi->email, email) == 0) {
	    if (ngi->authcode)
		has_authcode = 1;
	    count += ngi->nicks_count;
	}
    }
    return has_authcode ? -count : count;
}

/*************************************************************************/
/*************************************************************************/

int init_util(Module *my_module)
{
    module = my_module;
    cb_cancel_user  = register_callback(module, "cancel_user");
    cb_check_expire = register_callback(module, "check_expire");
    cb_check_recognized = register_callback(module, "check recognized");
    cb_delete       = register_callback(module, "nick delete");
    cb_groupdelete  = register_callback(module, "nickgroup delete");
    if (cb_cancel_user < 0 || cb_check_expire < 0 || cb_check_recognized < 0
     || cb_delete < 0 || cb_groupdelete < 0
    ) {
	module_log("Unable to register callbacks (util.c)");
	return 0;
    }
    return 1;
}

/*************************************************************************/

void exit_util()
{
    unregister_callback(module, cb_groupdelete);
    unregister_callback(module, cb_delete);
    unregister_callback(module, cb_check_recognized);
    unregister_callback(module, cb_check_expire);
    unregister_callback(module, cb_cancel_user);
}

/*************************************************************************/

#endif  /* !STANDALONE_NICKSERV */


syntax highlighted by Code2HTML, v. 0.9.1