/* Main NickServ module.
 *
 * IRC Services is copyright (c) 1996-2002 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.
 */

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "language.h"
#include "commands.h"
#include "encrypt.h"
#include "modules/operserv/operserv.h"

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

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

static Module *module;

static int cb_command       = -1;
static int cb_help          = -1;
static int cb_help_cmds     = -1;
       int cb_reglink_check = -1;  /* used by link module */
static int cb_registered    = -1;
static int cb_id_check      = -1;
static int cb_identified    = -1;

static int db_opened = 0;

       char *s_NickServ;
static char *desc_NickServ;
static char *NickDBName;
EXPORT_VAR(char *,s_NickServ)

       int32  NSRegEmailMax;
       int    NSRequireEmail;
       time_t NSRegDelay;
       int32  NSDefFlags;
       time_t NSExpire;
       time_t NSExpireWarning;
       int    NSShowPassword;
       char * NSEnforcerUser;
       char * NSEnforcerHost;
       int    NSForceNickChange;
       time_t NSReleaseTimeout;
       int    NSAllowKillImmed;
       int    NSListOpersOnly;
       int32  NSListMax;
       int    NSSecureAdmins;
       time_t NSSuspendExpire;
       time_t NSSuspendGrace;
static int    NSHelpWarning;

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

static void do_help(User *u);
static void do_register(User *u);
static void do_identify(User *u);
static void do_drop(User *u);
static void do_dropnick(User *u);
static void do_info(User *u);
static void do_listchans(User *u);
static void do_list(User *u);
static void do_listemail(User *u);
static void do_recover(User *u);
static void do_release(User *u);
static void do_ghost(User *u);
static void do_status(User *u);
static void do_getpass(User *u);
static void do_forbid(User *u);
static void do_suspend(User *u);
static void do_unsuspend(User *u);
#ifdef DEBUG_COMMANDS
static void do_listnick(User *u);
#endif

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

static Command cmds[] = {
    { "HELP",     do_help,     NULL,  -1,                     -1,-1 },
    { "REGISTER", do_register, NULL,  NICK_HELP_REGISTER,     -1,-1 },
    { "IDENTIFY", do_identify, NULL,  NICK_HELP_IDENTIFY,     -1,-1 },
    { "SIDENTIFY",do_identify, NULL,  -1,                     -1,-1 },
    { "DROP",     do_drop,     NULL,  NICK_HELP_DROP,         -1,-1 },
    { "SET",      do_set,      NULL,  NICK_HELP_SET,
		-1, NICK_OPER_HELP_SET },
    { "SET PASSWORD", NULL,    NULL,  NICK_HELP_SET_PASSWORD, -1,-1 },
    { "SET URL",      NULL,    NULL,  NICK_HELP_SET_URL,      -1,-1 },
    { "SET EMAIL",    NULL,    NULL,  NICK_HELP_SET_EMAIL,    -1,-1 },
    { "SET INFO",     NULL,    NULL,  NICK_HELP_SET_INFO,     -1,-1 },
    { "SET KILL",     NULL,    NULL,  NICK_HELP_SET_KILL,     -1,-1 },
    { "SET SECURE",   NULL,    NULL,  NICK_HELP_SET_SECURE,   -1,-1 },
    { "SET PRIVATE",  NULL,    NULL,  NICK_HELP_SET_PRIVATE,  -1,-1 },
    { "SET HIDE",     NULL,    NULL,  NICK_HELP_SET_HIDE,     -1,-1 },
    { "SET TIMEZONE", NULL,    NULL,  NICK_HELP_SET_TIMEZONE, -1,-1 },
    { "SET NOEXPIRE", NULL,    NULL,  -1, -1,
		NICK_OPER_HELP_SET_NOEXPIRE },
    { "UNSET",    do_unset,    NULL,  NICK_HELP_UNSET,
		-1, NICK_OPER_HELP_UNSET },
    { "RECOVER",  do_recover,  NULL,  NICK_HELP_RECOVER,      -1,-1 },
    { "RELEASE",  do_release,  NULL,  NICK_HELP_RELEASE,      -1,-1 },
    { "GHOST",    do_ghost,    NULL,  NICK_HELP_GHOST,        -1,-1 },
    { "INFO",     do_info,     NULL,  NICK_HELP_INFO,
		-1, NICK_OPER_HELP_INFO },
    { "LIST",     do_list,     NULL,  -1,
		NICK_HELP_LIST, NICK_OPER_HELP_LIST },
    { "LISTEMAIL",do_listemail,NULL,  -1,
		NICK_HELP_LISTEMAIL, NICK_OPER_HELP_LISTEMAIL },
    { "STATUS",   do_status,   NULL,  NICK_HELP_STATUS,       -1,-1 },
    { "LISTCHANS",do_listchans,NULL,  NICK_HELP_LISTCHANS,
		-1, NICK_OPER_HELP_LISTCHANS },

    { "DROPNICK", do_dropnick, is_services_admin, -1,
		-1, NICK_OPER_HELP_DROPNICK },
    { "GETPASS",  do_getpass,  is_services_admin, -1,
		-1, NICK_OPER_HELP_GETPASS },
    { "FORBID",   do_forbid,   is_services_admin, -1,
		-1, NICK_OPER_HELP_FORBID },
    { "SUSPEND",  do_suspend,  is_services_admin, -1,
		-1, NICK_OPER_HELP_SUSPEND },
    { "UNSUSPEND",do_unsuspend,is_services_admin, -1,
		-1, NICK_OPER_HELP_UNSUSPEND },
#ifdef DEBUG_COMMANDS
    { "LISTNICK", do_listnick, is_services_root, -1, -1, -1 },
#endif
    { NULL }
};

/*************************************************************************/
/************************ Main NickServ routines *************************/
/*************************************************************************/

/* Introduce the NickServ pseudoclient. */

static int introduce_nickserv(const char *nick)
{
    if (!nick || irc_stricmp(nick, s_NickServ) == 0) {
	char modebuf[BUFSIZE];
	snprintf(modebuf, sizeof(modebuf), "o%s", pseudoclient_modes);
	send_nick(s_NickServ, ServiceUser, ServiceHost, ServerName,
		  desc_NickServ, modebuf);
	return nick ? 1 : 0;
    }
    return 0;
}

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

/* Main NickServ routine. */

static int nickserv(const char *source, const char *target, char *buf)
{
    char *cmd, *s;
    User *u = get_user(source);

    if (irc_stricmp(target, s_NickServ) != 0)
	return 0;

    if (!u) {
	module_log("user record for %s not found", source);
	notice(s_NickServ, source,
		getstring(NULL, INTERNAL_ERROR));
	return 1;
    }

    cmd = strtok(buf, " ");

    if (!cmd) {
	return 1;
    } else if (stricmp(cmd, "\1PING") == 0) {
	if (!(s = strtok(NULL, "")))
	    s = "\1";
	notice(s_NickServ, source, "\1PING %s", s);
    } else {
	if (call_callback_2(module, cb_command, u, cmd) <= 0)
	    run_cmd(s_NickServ, u, module, cmd);
    }
    return 1;

}

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

/* Return a /WHOIS response for NickServ. */

static int nickserv_whois(const char *source, char *who, char *extra)
{
    if (irc_stricmp(who, s_NickServ) != 0)
	return 0;
    send_cmd(ServerName, "311 %s %s %s %s * :%s", source, who,
	     ServiceUser, ServiceHost, desc_NickServ);
    send_cmd(ServerName, "312 %s %s %s :%s", source, who,
	     ServerName, ServerDesc);
    send_cmd(ServerName, "313 %s %s :is a network service", source, who);
    send_cmd(ServerName, "318 %s %s End of /WHOIS response.", source, who);
    return 1;
}

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

/* Save nickname database. */

static int do_save_data()
{
    sync_nick_db(NickDBName);
    return 0;
}

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

/* Callback for users connecting to the network. */

static int do_user_create(User *user, int ac, char **av)
{
    validate_user(user);
    return 0;
}

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

/* Callbacks for users changing nicknames (before and after). */

static int do_user_nickchange_before(User *user, const char *newnick)
{
    /* Changing nickname case isn't a real change; pop out immediately
     * in that case. */
    if (irc_stricmp(newnick, user->nick) == 0)
	return 0;

    cancel_user(user);
    return 0;
}

static int do_user_nickchange_after(User *user, const char *oldnick)
{
    /* Changing nickname case isn't a real change; pop out immediately
     * in that case. */
    if (irc_stricmp(oldnick, user->nick) == 0)
	return 0;

    user->my_signon = time(NULL);
    validate_user(user);
    if (usermode_reg) {
	if (user_identified(user)) {
	    send_cmd(s_NickServ, "SVSMODE %s :+%s", user->nick,
		     mode_flags_to_string(usermode_reg, MODE_USER));
	    user->mode |= usermode_reg;
	} else if (user->mode & usermode_reg) {
	    send_cmd(s_NickServ, "SVSMODE %s :-%s", user->nick,
		     mode_flags_to_string(usermode_reg, MODE_USER));
	    user->mode &= ~usermode_reg;
	}
    }
    return 0;
}

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

/* Callback for users disconnecting from the network. */

static int do_user_delete(User *user, const char *reason)
{
    NickInfo *ni = user->ni;
    int i, j;

    if (user_recognized(user)) {
	free(ni->last_quit);
	ni->last_quit = *reason ? sstrdup(reason) : NULL;
	put_nickinfo(ni);
    }
    ARRAY_FOREACH (i, user->id_nicks) {
	NickGroupInfo *ngi = get_ngi_id(user->id_nicks[i]);
	if (!ngi)
	    continue;
	ARRAY_SEARCH_PLAIN_SCALAR(ngi->id_users, user, j);
	if (j < ngi->id_users_count) {
	    ARRAY_REMOVE(ngi->id_users, j);
	} else {
	    module_log("BUG: do_user_delete(): nickgroup %u listed in"
		       " id_nicks for user %p (%s), but user not in"
		       " id_users!", ngi->id, user, user->nick);
	}
    }
    cancel_user(user);
    return 0;
}

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

/* Callback for REGISTER/LINK check; we disallow registration/linking of
 * the NickServ pseudoclient nickname or guest nicks.  This is done here
 * instead of in the routines themselves to avoid duplication of code at an
 * insignificant performance cost.
 */

static int do_reglink_check(const User *u, const char *nick,
			    const char *pass, const char *email)
{
    if ((protocol_features & PF_CHANGENICK) && is_guest_nick(u->nick)) {
	/* Don't allow guest nicks to be registered or linked.  This check
	 * has to be done regardless of the state of NSForceNickChange
	 * because other modules might take advantage of forced nick
	 * changing. */
	return 1;
    }
    return irc_stricmp(nick, s_NickServ) == 0;
}

/*************************************************************************/
/*********************** NickServ command routines ***********************/
/*************************************************************************/

/* Return a help message. */

static void do_help(User *u)
{
    char *cmd = strtok_remaining();

    if (!cmd) {
	notice_help(s_NickServ, u, NICK_HELP);
	if (NSExpire)
	    notice_help(s_NickServ, u, NICK_HELP_EXPIRES,
			maketime(u->ngi,NSExpire,0));
	if (NSHelpWarning)
	    notice_help(s_NickServ, u, NICK_HELP_WARNING);
    } else if (call_callback_2(module, cb_help, u, cmd) > 0) {
	return;
    } else if (stricmp(cmd, "COMMANDS") == 0) {
	notice_help(s_NickServ, u, NICK_HELP_COMMANDS);
	if (find_module("nickserv/mail-auth"))
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_AUTH);
	notice_help(s_NickServ, u, NICK_HELP_COMMANDS_IDENTIFY);
	if (find_module("nickserv/sendpass"))
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_SENDPASS);
	notice_help(s_NickServ, u, NICK_HELP_COMMANDS_DROP);
	if (find_module("nickserv/link"))
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_LINK);
	if (find_module("nickserv/oldlink"))
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_OLDLINK);
	if (find_module("nickserv/access"))
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_ACCESS);
	if (find_module("nickserv/autojoin"))
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_AJOIN);
	notice_help(s_NickServ, u, NICK_HELP_COMMANDS_SET);
	if (!NSListOpersOnly)
	    notice_help(s_NickServ, u, NICK_HELP_COMMANDS_LIST);
	notice_help(s_NickServ, u, NICK_HELP_COMMANDS_LISTCHANS);
	call_callback_2(module, cb_help_cmds, u, 0);
	if (is_oper(u)) {
	    notice_help(s_NickServ, u, NICK_OPER_HELP_COMMANDS);
	    if (EnableGetpass)
		notice_help(s_NickServ, u, NICK_OPER_HELP_COMMANDS_GETPASS);
	    notice_help(s_NickServ, u, NICK_OPER_HELP_COMMANDS_FORBID);
	    if (find_module("nickserv/oldlink"))
		notice_help(s_NickServ, u, NICK_OPER_HELP_COMMANDS_LISTLINKS);
	    if (NSListOpersOnly)
		notice_help(s_NickServ, u, NICK_HELP_COMMANDS_LIST);
	    if (find_module("nickserv/mail-auth"))
		notice_help(s_NickServ, u, NICK_OPER_HELP_COMMANDS_SETAUTH);
	    call_callback_2(module, cb_help_cmds, u, 1);
	    notice_help(s_NickServ, u, NICK_OPER_HELP_COMMANDS_END);
	}
    } else if (stricmp(cmd, "REGISTER") == 0) {
	notice_help(s_NickServ, u, NICK_HELP_REGISTER,
		    getstring(u->ngi,NICK_REGISTER_SYNTAX));
	notice_help(s_NickServ, u, NICK_HELP_REGISTER_EMAIL);
	notice_help(s_NickServ, u, NICK_HELP_REGISTER_END);
    } else if (stricmp(cmd, "DROP") == 0) {
	notice_help(s_NickServ, u, NICK_HELP_DROP);
	if (find_module("nickserv/link") || find_module("nickserv/oldlink"))
	    notice_help(s_NickServ, u, NICK_HELP_DROP_LINK);
	notice_help(s_NickServ, u, NICK_HELP_DROP_END);
    } else if (stricmp(cmd, "SET") == 0) {
	notice_help(s_NickServ, u, NICK_HELP_SET);
	if (find_module("nickserv/link"))
	    notice_help(s_NickServ, u, NICK_HELP_SET_OPTION_MAINNICK);
	notice_help(s_NickServ, u, NICK_HELP_SET_END);
	if (is_oper(u))
	    notice_help(s_NickServ, u, NICK_OPER_HELP_SET);
    } else if (strnicmp(cmd, "SET", 3) == 0
	       && isspace(cmd[3])
	       && stricmp(cmd+4+strspn(cmd+4," \t"), "LANGUAGE") == 0) {
	int i;
	notice_help(s_NickServ, u, NICK_HELP_SET_LANGUAGE);
	for (i = 0; i < NUM_LANGS && langlist[i] >= 0; i++) {
	    notice(s_NickServ, u->nick, "    %2d) %s",
		   i+1, getstring_lang(langlist[i],LANG_NAME));
	}
    } else if (stricmp(cmd, "INFO") == 0) {
	notice_help(s_NickServ, u, NICK_HELP_INFO);
	if (find_module("nickserv/mail-auth"))
	    notice_help(s_NickServ, u, NICK_HELP_INFO_AUTH);
	if (is_oper(u))
	    notice_help(s_NickServ, u, NICK_OPER_HELP_INFO);
    } else if (stricmp(cmd, "LIST") == 0) {
	notice_help(s_NickServ, u, NICK_HELP_LIST);
	if (NSListOpersOnly)
	    notice_help(s_NickServ, u, NICK_HELP_LIST_OPERSONLY);
    } else {
	help_cmd(s_NickServ, u, module, cmd);
    }
}

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

/* Register a nick. */

static void do_register(User *u)
{
    NickInfo *ni;
    NickGroupInfo *ngi;
    char *pass = strtok(NULL, " ");
    char *email = strtok(NULL, " ");
    int n;

    if (readonly) {
	notice_lang(s_NickServ, u, NICK_REGISTRATION_DISABLED);
	return;
    }

    if (time(NULL) < u->lastnickreg + NSRegDelay) {
	notice_lang(s_NickServ, u, NICK_REG_PLEASE_WAIT,
		    maketime(u->ngi,NSRegDelay,MT_SECONDS));

    } else if (!pass || (NSRequireEmail && !email)
	       || (stricmp(pass, u->nick) == 0
		   && (strtok(NULL, "")
		       || (email && (!strchr(email,'@')
				     || !strchr(email,'.')))))
    ) {
	/* No password/email, or they (apparently) tried to include the nick
	 * in the command. */
	syntax_error(s_NickServ, u, "REGISTER", NICK_REGISTER_SYNTAX);

    } else if (call_callback_4(module, cb_reglink_check, u, u->nick, pass,
			       email) > 0
    ) {
	/* Denied by the callback. */
	notice_lang(s_NickServ, u, NICK_CANNOT_BE_REGISTERED, u->nick);
	return;

    } else if (u->ni) {  /* i.e. there's already such a nick regged */
	if (u->ni->status & NS_VERBOTEN) {
	    module_log("%s@%s tried to register forbidden nick %s",
		       u->username, u->host, u->nick);
	    notice_lang(s_NickServ, u, NICK_CANNOT_BE_REGISTERED, u->nick);
	} else {
	    if (u->ngi->suspendinfo)
		module_log("%s@%s tried to register suspended nick %s",
			   u->username, u->host, u->nick);
	    notice_lang(s_NickServ, u, NICK_X_ALREADY_REGISTERED, u->nick);
	}

    } else if (u->ngi == NICKGROUPINFO_INVALID) {
	module_log("%s@%s tried to register nick %s with missing nick group",
		   u->username, u->host, u->nick);
	notice_lang(s_NickServ, u, NICK_REGISTRATION_FAILED);

    } else if (stricmp(pass, u->nick) == 0
	       || (StrictPasswords && strlen(pass) < 5)
    ) {
	notice_lang(s_NickServ, u, MORE_OBSCURE_PASSWORD);

    } else if (email && !valid_email(email)) {
	notice_lang(s_NickServ, u, BAD_EMAIL);

    } else if (NSRegEmailMax && email && !is_services_admin(u)
	       && ((n = count_nicks_with_email(email)) < 0
		   || n >= NSRegEmailMax)) {
	if (n < 0) {
	    notice_lang(s_NickServ, u, NICK_REGISTER_EMAIL_UNAUTHED);
	} else {
	    notice_lang(s_NickServ, u, NICK_REGISTER_TOO_MANY_NICKS, n,
			NSRegEmailMax);
	}

    } else {
	int replied = 0;
	int len = strlen(pass), max;
	char passbuf[PASSMAX];

	/* Make sure the password will fit in a PASSMAX-size buffer */
	max = encrypt_check_len(len, PASSMAX);
	/* Require the original password (including trailing NULL) to fit
	 * in a PASSMAX-size buffer as well */
	if ((max == 0 && len > PASSMAX-1) || max > PASSMAX-1)
	    max = PASSMAX-1;
	/* Truncate password if too long */
	if (max > 0) {
	    memset(pass+max, 0, len-max);
	    len = max;
	    notice_lang(s_NickServ, u, PASSWORD_TRUNCATED, max);
	}
	/* Encrypt password */
	if (encrypt(pass, len, passbuf, PASSMAX) < 0) {
	    memset(pass, 0, len);
	    module_log("Failed to encrypt password for %s (register)",
		       u->nick);
	    notice_lang(s_NickServ, u, NICK_REGISTRATION_FAILED);
	    return;
	}
	/* Do nick setup stuff */
	ni = makenick(u->nick, &ngi);
	if (!ni) {
	    module_log("makenick(%s) failed", u->nick);
	    notice_lang(s_NickServ, u, NICK_REGISTRATION_FAILED);
	    return;
	}
	memcpy(ngi->pass, passbuf, PASSMAX);
	memset(passbuf, 0, PASSMAX);
	ni->time_registered = ni->last_seen = time(NULL);
	ni->id_stamp = u->servicestamp;
	ni->authstat = NA_IDENTIFIED | NA_RECOGNIZED;
	if (email)
	    ngi->email = sstrdup(email);
	ngi->flags = NSDefFlags;
	ngi->memos.memomax = MEMOMAX_DEFAULT;
	ngi->channelmax = CHANMAX_DEFAULT;
	ngi->language = LANG_DEFAULT;
	ngi->timezone = TIMEZONE_DEFAULT;
	call_callback_4(module, cb_registered, u, ni, ngi, &replied);
	/* Link back and forth to user record and store modified data */
	u->ni = ni;
	u->ngi = ngi;
	ni->user = u;
	update_userinfo(u);
	put_nickinfo(ni);
	put_nickgroupinfo(ngi);
	/* Tell people about it */
	if (email) {
	    module_log("%s registered by %s@%s (%s)",
		       u->nick, u->username, u->host, email);
	} else {
	    module_log("%s registered by %s@%s",
		       u->nick, u->username, u->host);
	}
	if (!replied)
	    notice_lang(s_NickServ, u, NICK_REGISTERED, u->nick);
	if (NSShowPassword)
	    notice_lang(s_NickServ, u, NICK_PASSWORD_IS, pass);
	/* Clear password from memory and other last-minute things */
	memset(pass, 0, len);
	/* Note time REGISTER command was used */
	u->lastnickreg = time(NULL);
	/* Set +r (or other registered-nick mode) */
	if (nick_identified(ni) && usermode_reg) {
	    /* Re-check IDENTIFIED because a module might have cleared it */
	    send_cmd(s_NickServ, "SVSMODE %s :+%s", u->nick,
		     mode_flags_to_string(usermode_reg, MODE_USER));
	}

    }

}

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

static void do_identify(User *u)
{
    char *pass = strtok_remaining();
    NickInfo *ni = NULL;
    NickGroupInfo *ngi = NULL;

    if (!pass) {
	syntax_error(s_NickServ, u, "IDENTIFY", NICK_IDENTIFY_SYNTAX);

    } else if (!(ni = u->ni) || !(ngi = u->ngi)
	       || ngi == NICKGROUPINFO_INVALID) {
	if (ni)
	    module_log("IDENTIFY: missing NickGroupInfo for %s", u->nick);
	notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);

    } else if (ngi->suspendinfo) {
	notice_lang(s_NickServ, u, NICK_X_SUSPENDED, u->nick);

    } else if (!nick_check_password(u, u->ni, pass, "IDENTIFY",
				    NICK_IDENTIFY_FAILED)) {
	/* nothing */

    } else if (NSRequireEmail && !ngi->email) {
	ni->authstat |= NA_IDENT_NOMAIL;
	notice_lang(s_NickServ, u, NICK_IDENTIFY_EMAIL_MISSING, s_NickServ);

    } else if (call_callback_2(module, cb_id_check, u, pass) <= 0) {
	int old_authstat = ni->authstat;
	set_identified(u, ni, ngi);
	if (usermode_reg)
	    send_cmd(s_NickServ, "SVSMODE %s :+%s", u->nick,
		     mode_flags_to_string(usermode_reg, MODE_USER));
	module_log("%s!%s@%s identified for nick %s",
		   u->nick, u->username, u->host, u->nick);
	notice_lang(s_NickServ, u, NICK_IDENTIFY_SUCCEEDED);
	call_callback_2(module, cb_identified, u, old_authstat);
    }
}

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

static void do_drop(User *u)
{
    char *pass = strtok_remaining();
    NickInfo *ni = u->ni;
    NickGroupInfo *ngi = (u->ngi==NICKGROUPINFO_INVALID ? NULL : u->ngi);

    if (readonly && !is_services_admin(u)) {
	notice_lang(s_NickServ, u, NICK_DROP_DISABLED);
	return;
    }

    if (!pass) {
	syntax_error(s_NickServ, u, "DROP", NICK_DROP_SYNTAX);
	if (find_module("nickserv/link") || find_module("nickserv/oldlink"))
	    notice_lang(s_NickServ, u, NICK_DROP_WARNING);
    } else if (!ni || !ngi) {
	notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);
    } else if (ngi->suspendinfo) {
	notice_lang(s_NickServ, u, NICK_X_SUSPENDED, u->nick);
    } else if (!nick_check_password(u, u->ni, pass, "DROP",
				    NICK_DROP_FAILED)) {
	/* nothing */
    } else {
	int i;

	if (readonly)  /* they must be a servadmin in this case */
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
	module_log("%s!%s@%s dropped nickgroup %u:",
		   u->nick, u->username, u->host, ngi->id);
	ARRAY_FOREACH (i, ngi->nicks) {
	    module_log(" -- %s!%s@%s dropped nick %s",
		       u->nick, u->username, u->host, ngi->nicks[i]);
	}
	delgroup(ngi);
	notice_lang(s_NickServ, u, NICK_DROPPED);
    }
}

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

/* Services admin only function (privileges assumed to be pre-checked) */

static void do_dropnick(User *u)
{
    char *nick = strtok(NULL, " ");
    NickInfo *ni;
    NickGroupInfo *ngi;

#ifdef CLEAN_COMPILE
    ngi = NULL;
#endif

    if (!nick) {
	syntax_error(s_NickServ, u, "DROPNICK", NICK_DROPNICK_SYNTAX);
    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (ni->nickgroup && !(ngi = get_ngi(ni))) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (NSSecureAdmins && nick_is_services_admin(ni) &&
    						!is_services_root(u)) {
	notice_lang(s_NickServ, u, PERMISSION_DENIED);
    } else {
	int i;

	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
	if (ni->nickgroup) {
	    module_log("%s!%s@%s dropped nickgroup %u as Services admin:",
		       u->nick, u->username, u->host, ngi->id);
	    ARRAY_FOREACH (i, ngi->nicks) {
		module_log(" -- %s!%s@%s dropped nick %s",
			   u->nick, u->username, u->host, ngi->nicks[i]);
	    }
	    delgroup(ngi);
	} else {
	    module_log("%s!%s@%s dropped forbidden nick %s",
		       u->nick, u->username, u->host, ni->nick);
	    delnick(ni);
	}
	notice_lang(s_NickServ, u, NICK_X_DROPPED, nick);
    }
}

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

/* Show hidden info to nick owners and sadmins when the "ALL" parameter is
 * supplied. If a nick is online, the "Last seen address" changes to "Is
 * online from".
 * Syntax: INFO <nick> {ALL}
 * -TheShadow (13 Mar 1999)
 */

/* Check the status of show_all and make a note of having done so.  This is
 * used at the end, to see whether we should print a "use ALL for more info"
 * message.  Note that this should be the last test in a boolean expression,
 * to ensure that used_all isn't set inappropriately. */
#define CHECK_SHOW_ALL (used_all++, show_all)

static void do_info(User *u)
{
    char *nick = strtok(NULL, " ");
    char *param = strtok(NULL, " ");
    NickInfo *ni;
    NickGroupInfo *ngi;

    if (!nick) {
    	syntax_error(s_NickServ, u, "INFO", NICK_INFO_SYNTAX);

    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);

    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);

    } else {
	char buf[BUFSIZE], *end;
	const char *commastr = getstring(u->ngi, COMMA_SPACE);
	int need_comma = 0;
	int nick_online = 0;
	int can_show_all = 0, show_all = 0, used_all = 0;

	ngi = get_ngi(ni);
	if (!ngi) {
	    notice_lang(s_NickServ, u, INTERNAL_ERROR);
	    return;
	}

	/* Is the real owner of the nick we're looking up online? -TheShadow */
	if (ni->user && nick_id_or_rec(ni))
	    nick_online = 1;

        /* Only show hidden fields to owner and sadmins and only when the ALL
	 * parameter is used. -TheShadow */
	can_show_all = ((u==ni->user && nick_online) || is_services_admin(u));

	if (can_show_all && (param && stricmp(param, "ALL") == 0))
	    show_all = 1;

	notice_lang(s_NickServ, u, NICK_INFO_REALNAME,
		    nick, ni->last_realname);

	/* Ignore HIDE and show the real hostmask to anyone who can use
	 * INFO ALL. */
	if (nick_online) {
	    if (!(ngi->flags & NF_HIDE_MASK) || can_show_all)
		notice_lang(s_NickServ, u, NICK_INFO_ADDRESS_ONLINE,
			can_show_all ? ni->last_realmask : ni->last_usermask);
	    else
		notice_lang(s_NickServ, u, NICK_INFO_ADDRESS_ONLINE_NOHOST,
			    ni->nick);
	} else {
	    if (!(ngi->flags & NF_HIDE_MASK) || can_show_all)
		notice_lang(s_NickServ, u, NICK_INFO_ADDRESS,
			can_show_all ? ni->last_realmask : ni->last_usermask);
            strftime_lang(buf, sizeof(buf), u->ngi,
			  STRFTIME_DATE_TIME_FORMAT, ni->last_seen);
            notice_lang(s_NickServ, u, NICK_INFO_LAST_SEEN, buf);
	}

	strftime_lang(buf, sizeof(buf), u->ngi, STRFTIME_DATE_TIME_FORMAT,
		      ni->time_registered);
	notice_lang(s_NickServ, u, NICK_INFO_TIME_REGGED, buf);
	if (ni->last_quit && (!(ngi->flags & NF_HIDE_QUIT) || CHECK_SHOW_ALL))
	    notice_lang(s_NickServ, u, NICK_INFO_LAST_QUIT, ni->last_quit);
	if (ngi->url)
	    notice_lang(s_NickServ, u, NICK_INFO_URL, ngi->url);
	if (ngi->email && (!(ngi->flags & NF_HIDE_EMAIL) || CHECK_SHOW_ALL)) {
	    if (ngi->authcode) {
		if (can_show_all) {
		    notice_lang(s_NickServ, u, NICK_INFO_EMAIL_UNAUTHED,
				ngi->email);
		}
	    } else {
		notice_lang(s_NickServ, u, NICK_INFO_EMAIL, ngi->email);
	    }
	}
	if (ngi->info)
	    notice_lang(s_NickServ, u, NICK_INFO_INFO, ngi->info);
	*buf = 0;
	end = buf;
	if (ngi->flags & NF_KILLPROTECT) {
	    end += snprintf(end, sizeof(buf)-(end-buf), "%s",
			    getstring(u->ngi, NICK_INFO_OPT_KILL));
	    need_comma = 1;
	}
	if (ngi->flags & NF_SECURE) {
	    end += snprintf(end, sizeof(buf)-(end-buf), "%s%s",
			    need_comma ? commastr : "",
			    getstring(u->ngi, NICK_INFO_OPT_SECURE));
	    need_comma = 1;
	}
	if (ngi->flags & NF_PRIVATE) {
	    end += snprintf(end, sizeof(buf)-(end-buf), "%s%s",
			    need_comma ? commastr : "",
			    getstring(u->ngi, NICK_INFO_OPT_PRIVATE));
	    need_comma = 1;
	}
	notice_lang(s_NickServ, u, NICK_INFO_OPTIONS,
		    *buf ? buf : getstring(u->ngi, NICK_INFO_OPT_NONE));

	if ((ni->status & NS_NOEXPIRE) && CHECK_SHOW_ALL)
	    notice_lang(s_NickServ, u, NICK_INFO_NO_EXPIRE);

	if (ngi->suspendinfo) {
	    notice_lang(s_NickServ, u, NICK_X_SUSPENDED, nick);
	    if (CHECK_SHOW_ALL) {
		SuspendInfo *si = ngi->suspendinfo;
		char timebuf[BUFSIZE], expirebuf[BUFSIZE];

		strftime_lang(timebuf, sizeof(timebuf), u->ngi,
			      STRFTIME_DATE_TIME_FORMAT, si->suspended);
		expires_in_lang(expirebuf, sizeof(expirebuf), u->ngi,
				si->expires);
		notice_lang(s_NickServ, u, NICK_INFO_SUSPEND_DETAILS,
			    si->who, timebuf, expirebuf);
		notice_lang(s_NickServ, u, NICK_INFO_SUSPEND_REASON,
			    si->reason);
	    }
	}

	if (can_show_all && !show_all && used_all)
	    notice_lang(s_NickServ, u, NICK_INFO_SHOW_ALL, s_NickServ,
			ni->nick);
    }
}

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

static void do_listchans(User *u)
{
    NickInfo *ni = u->ni;
    NickGroupInfo *ngi;

    if (is_services_admin(u)) {
	char *nick = strtok(NULL, " ");
	if (nick) {
	    ni = get_nickinfo(nick);
	    if (!ni) {
		notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
		return;
	    }
	}
    } else if (strtok_remaining()) {
	if (is_oper(u))
	    notice_lang(s_NickServ, u, PERMISSION_DENIED);
	else
	    syntax_error(s_NickServ, u, "LISTCHANS", NICK_LISTCHANS_SYNTAX);
    }
    if (!ni) {
	notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);
	return;
    }
    if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, ni->nick);
    } else if (!user_identified(u)) {
	notice_lang(s_NickServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
    } else if (!(ngi = get_ngi(ni))) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (!ngi->channels_count) {
	notice_lang(s_NickServ, u, NICK_LISTCHANS_NONE, ni->nick);
    } else {
	int i;
	notice_lang(s_NickServ, u, NICK_LISTCHANS_HEADER, ni->nick);
	ARRAY_FOREACH (i, ngi->channels)
	    notice(s_NickServ, u->nick, "    %s", ngi->channels[i]);
	notice_lang(s_NickServ, u, NICK_LISTCHANS_END, ngi->channels_count);
    }
}

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

static void do_list(User *u)
{
    char *pattern = strtok(NULL, " ");
    char *keyword;
    NickInfo *ni;
    NickGroupInfo *ngi;
    int nnicks;
    char buf[BUFSIZE];
    int is_servadmin = is_services_admin(u);
    int16 match_NS = 0;  /* NS_ flags a nick must match one of to qualify */
    int32 match_NF = 0;  /* NF_ flags a nick must match one of to qualify */
    int match_susp = 0;  /* 1 if we match on suspended nicks */
    int match_auth = 0;  /* 1 if we match on nicks with auth codes */
    int have_auth_module = 0;  /* so we don't show no-auth char if no module */

    if (NSListOpersOnly && !is_oper(u)) {
	notice_lang(s_NickServ, u, PERMISSION_DENIED);
	return;
    }

    have_auth_module = (find_module("nickserv/mail-auth") != NULL);

    if (!pattern) {
	syntax_error(s_NickServ, u, "LIST",
		     is_oper(u) ? NICK_LIST_OPER_SYNTAX : NICK_LIST_SYNTAX);
    } else {
	int mask_has_at = (strchr(pattern,'@') != 0);

	nnicks = 0;

	while (is_servadmin && (keyword = strtok(NULL, " "))) {
	    if (stricmp(keyword, "FORBIDDEN") == 0) {
		match_NS |= NS_VERBOTEN;
	    } else if (stricmp(keyword, "NOEXPIRE") == 0) {
		match_NS |= NS_NOEXPIRE;
	    } else if (stricmp(keyword, "SUSPENDED") == 0) {
		match_susp = 1;
	    } else if (stricmp(keyword, "NOAUTH") == 0 && have_auth_module) {
		match_auth = 1;
	    } else {
		syntax_error(s_NickServ, u, "LIST",
		     is_oper(u) ? NICK_LIST_OPER_SYNTAX : NICK_LIST_SYNTAX);
	    }
	}

	notice_lang(s_NickServ, u, NICK_LIST_HEADER, pattern);
	for (ni = first_nickinfo(); ni; ni = next_nickinfo()) {
	    int usermask_seen = 0;  /* does user get to see the usermask? */

	    ngi = get_nickgroupinfo(ni->nickgroup);
	    if (!is_servadmin && ((ngi && (ngi->flags & NF_PRIVATE))
				  || (ni->status & NS_VERBOTEN)))
		continue;
	    if (match_NS || match_NF || match_susp || match_auth)
		/* ok, we have flags, now see if they match */
		if (!((ni->status & match_NS)
		   || (ngi && (ngi->flags & match_NF))
		   || (ngi && ngi->suspendinfo && match_susp)
		   || (ngi && ngi->authcode && match_auth)
		)) {
		    continue;
		}
	    if (!is_servadmin && (ngi->flags & NF_HIDE_MASK)) {
		snprintf(buf, sizeof(buf), "%-20s  [Hidden]", ni->nick);
	    } else if (ni->status & NS_VERBOTEN) {
		snprintf(buf, sizeof(buf), "%-20s  [Forbidden]", ni->nick);
	    } else {
		usermask_seen = 1;
		snprintf(buf, sizeof(buf), "%-20s  %s", ni->nick,
			 ni->last_usermask);
	    }
	    if ((!mask_has_at && match_wild_nocase(pattern, ni->nick))
	     || (mask_has_at && usermask_seen
		 && match_wild_nocase(pattern, ni->last_usermask))
	    ) {
		if (++nnicks <= NSListMax) {
		    char suspended_char = ' ';
		    char noexpire_char = ' ';
		    char *auth_char = have_auth_module ? " " : "";
		    if (is_servadmin) {
			if (ngi && ngi->suspendinfo)
			    suspended_char = '*';
			if (ni->status & NS_NOEXPIRE)
			    noexpire_char = '!';
			if (have_auth_module && ngi && ngi->authcode)
			    auth_char = "?";
		    }
		    notice(s_NickServ, u->nick, "   %c%c%s %s",
			   suspended_char, noexpire_char, auth_char, buf);
		}
	    }
	}
	notice_lang(s_NickServ, u, NICK_LIST_RESULTS,
			nnicks>NSListMax ? NSListMax : nnicks, nnicks);
    }
}

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

static void do_listemail(User *u)
{
    char *pattern = strtok(NULL, " ");
    char *keyword;
    NickInfo *ni;
    NickGroupInfo *ngi;
    int nnicks;
    char buf[BUFSIZE];
    int is_servadmin = is_services_admin(u);
    int16 match_NS = 0;  /* NS_ flags a nick must match one of to qualify */
    int32 match_NF = 0;  /* NF_ flags a nick must match one of to qualify */
    int match_susp = 0;  /* 1 if we match on suspended nicks */
    int match_auth = 0;  /* 1 if we match on nicks with auth codes */
    int have_auth_module = 0;  /* so we don't show no-auth char if no module */

    if (NSListOpersOnly && !is_oper(u)) {
	notice_lang(s_NickServ, u, PERMISSION_DENIED);
	return;
    }

    have_auth_module = (find_module("nickserv/mail-auth") != NULL);

    if (!pattern) {
	syntax_error(s_NickServ, u, "LISTEMAIL",
		     is_oper(u) ? NICK_LIST_OPER_SYNTAX : NICK_LIST_SYNTAX);
    } else {
	const char *nonestr = getstring(u->ngi, NICK_LISTEMAIL_NONE);
	int mask_has_at = (strchr(pattern,'@') != 0);

	nnicks = 0;

	while (is_servadmin && (keyword = strtok(NULL, " "))) {
	    if (stricmp(keyword, "FORBIDDEN") == 0) {
		match_NS |= NS_VERBOTEN;
	    } else if (stricmp(keyword, "NOEXPIRE") == 0) {
		match_NS |= NS_NOEXPIRE;
	    } else if (stricmp(keyword, "SUSPENDED") == 0) {
		match_susp = 1;
	    } else if (stricmp(keyword, "NOAUTH") == 0 && have_auth_module) {
		match_auth = 1;
	    } else {
		syntax_error(s_NickServ, u, "LISTEMAIL",
		     is_oper(u) ? NICK_LIST_OPER_SYNTAX : NICK_LIST_SYNTAX);
	    }
	}

	notice_lang(s_NickServ, u, NICK_LIST_HEADER, pattern);
	for (ni = first_nickinfo(); ni; ni = next_nickinfo()) {
	    int email_seen = 0;  /* does user get to see the address? */

	    ngi = get_nickgroupinfo(ni->nickgroup);
	    if (!is_servadmin && ((ngi && (ngi->flags & NF_PRIVATE))
				  || (ni->status & NS_VERBOTEN)))
		continue;
	    if (match_NS || match_NF || match_susp || match_auth)
		/* ok, we have flags, now see if they match */
		if (!((ni->status & match_NS)
		   || (ngi && (ngi->flags & match_NF))
		   || (ngi && ngi->suspendinfo && match_susp)
		   || (ngi && ngi->authcode && match_auth)
		)) {
		    continue;
		}
	    if (!is_servadmin && (ngi->flags & NF_HIDE_EMAIL)
	     && (!valid_ngi(u) || ngi->id!=u->ngi->id || !user_identified(u))){
		snprintf(buf, sizeof(buf), "%-20s  [Hidden]", ni->nick);
	    } else if (ni->status & NS_VERBOTEN) {
		snprintf(buf, sizeof(buf), "%-20s  [Forbidden]", ni->nick);
	    } else {
		email_seen = 1;
		snprintf(buf, sizeof(buf), "%-20s  %s", ni->nick,
			 ngi->email ? ngi->email : nonestr);
	    }
	    if ((!mask_has_at && match_wild_nocase(pattern, ni->nick))
	     || (mask_has_at && email_seen && ngi->email
		 && match_wild_nocase(pattern, ngi->email))
	    ) {
		if (++nnicks <= NSListMax) {
		    char suspended_char = ' ';
		    char noexpire_char = ' ';
		    char *auth_char = have_auth_module ? " " : "";
		    if (is_servadmin) {
			if (ngi && ngi->suspendinfo)
			    suspended_char = '*';
			if (ni->status & NS_NOEXPIRE)
			    noexpire_char = '!';
			if (have_auth_module && ngi && ngi->authcode)
			    auth_char = "?";
		    }
		    notice(s_NickServ, u->nick, "   %c%c%s %s",
			   suspended_char, noexpire_char, auth_char, buf);
		}
	    }
	}
	notice_lang(s_NickServ, u, NICK_LIST_RESULTS,
			nnicks>NSListMax ? NSListMax : nnicks, nnicks);
    }
}

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

static void do_recover(User *u)
{
    char *nick = strtok(NULL, " ");
    char *pass = strtok_remaining();
    NickInfo *ni;
    User *u2;

    if (!nick) {
	syntax_error(s_NickServ, u, "RECOVER", NICK_RECOVER_SYNTAX);
    } else if (!(u2 = get_user(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_IN_USE, nick);
    } else if (!(ni = u2->ni)) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (ni->status & NS_GUESTED) {
	notice_lang(s_NickServ, u, NICK_X_NOT_IN_USE, nick);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);
    } else if (irc_stricmp(nick, u->nick) == 0) {
	notice_lang(s_NickServ, u, NICK_NO_RECOVER_SELF);
    } else {
	if (pass) {
	    if (!nick_check_password(u, ni, pass, "RECOVER", ACCESS_DENIED))
		return;
	} else if (!has_identified_nick(u, ni->nickgroup)) {
	    notice_lang(s_NickServ, u, ACCESS_DENIED);
	    return;
	}
	collide(ni, 0);
	notice_lang(s_NickServ, u, NICK_RECOVERED, s_NickServ, nick);
    }
}

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

static void do_release(User *u)
{
    char *nick = strtok(NULL, " ");
    char *pass = strtok_remaining();
    NickInfo *ni;

    if (!nick) {
	syntax_error(s_NickServ, u, "RELEASE", NICK_RELEASE_SYNTAX);
    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);
    } else if (!(ni->status & NS_KILL_HELD)) {
	notice_lang(s_NickServ, u, NICK_RELEASE_NOT_HELD, nick);
    } else {
	if (pass) {
	    if (!nick_check_password(u, ni, pass, "RELEASE", ACCESS_DENIED))
		return;
	} else if (!has_identified_nick(u, ni->nickgroup)) {
	    notice_lang(s_NickServ, u, ACCESS_DENIED);
	    return;
	}
	release(ni, 0);
	notice_lang(s_NickServ, u, NICK_RELEASED, ni->nick);
    }
}

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

static void do_ghost(User *u)
{
    char *nick = strtok(NULL, " ");
    char *pass = strtok_remaining();
    NickInfo *ni;
    User *u2;

    if (!nick) {
	syntax_error(s_NickServ, u, "GHOST", NICK_GHOST_SYNTAX);
    } else if (!(u2 = get_user(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_IN_USE, nick);
    } else if (!(ni = u2->ni)) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (ni->status & NS_GUESTED) {
	notice_lang(s_NickServ, u, NICK_X_NOT_IN_USE, nick);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);
    } else if (irc_stricmp(nick, u->nick) == 0) {
	notice_lang(s_NickServ, u, NICK_NO_GHOST_SELF);
    } else {
	char buf[NICKMAX+32];
	if (pass) {
	    if (!nick_check_password(u, ni, pass, "GHOST", ACCESS_DENIED))
		return;
	} else if (!has_identified_nick(u, ni->nickgroup)) {
	    notice_lang(s_NickServ, u, ACCESS_DENIED);
	    return;
	}
	snprintf(buf, sizeof(buf), "GHOST command used by %s", u->nick);
	kill_user(s_NickServ, nick, buf);
	notice_lang(s_NickServ, u, NICK_GHOST_KILLED, nick);
    }
}

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

static void do_status(User *u)
{
    char *nick;
    User *u2;
    int i = 0;

    while ((nick = strtok(NULL, " ")) && (i++ < 16)) {
	if (!(u2 = get_user(nick)) || !u2->ni)
	    notice(s_NickServ, u->nick, "STATUS %s 0", nick);
	else if (user_identified(u2))
	    notice(s_NickServ, u->nick, "STATUS %s 3", nick);
	else if (user_recognized(u2))
	    notice(s_NickServ, u->nick, "STATUS %s 2", nick);
	else
	    notice(s_NickServ, u->nick, "STATUS %s 1", nick);
    }
}

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

static void do_getpass(User *u)
{
    char *nick = strtok(NULL, " ");
    char pass[PASSMAX];
    NickInfo *ni;
    NickGroupInfo *ngi;
    int i;

    /* Assumes that permission checking has already been done. */
    if (!nick) {
	syntax_error(s_NickServ, u, "GETPASS", NICK_GETPASS_SYNTAX);
    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (!(ngi = get_ngi(ni))) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (NSSecureAdmins && nick_is_services_admin(ni) &&
    							!is_services_root(u)) {
	notice_lang(s_NickServ, u, PERMISSION_DENIED);
    } else if ((i = decrypt(ngi->pass, pass, PASSMAX)) < 0) {
	module_log("decrypt() failed for GETPASS on %s", nick);
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (i == 0) {
	notice_lang(s_NickServ, u, NICK_GETPASS_UNAVAILABLE);
    } else {
	module_log("%s!%s@%s used GETPASS on %s",
		   u->nick, u->username, u->host, nick);
	if (WallGetpass)
	    wallops(s_NickServ, "\2%s\2 used GETPASS on \2%s\2", u->nick,nick);
	notice_lang(s_NickServ, u, NICK_GETPASS_PASSWORD_IS, nick, pass);
    }
}

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

static void do_forbid(User *u)
{
    NickInfo *ni;
    char *nick = strtok(NULL, " ");
    User *u2;

    /* Assumes that permission checking has already been done. */
    if (!nick) {
	syntax_error(s_NickServ, u, "FORBID", NICK_FORBID_SYNTAX);
	return;
    }
    u2 = get_user(nick);
    if ((ni = get_nickinfo(nick)) != NULL) {
	if (NSSecureAdmins && nick_is_services_admin(ni) &&
                                                        !is_services_root(u)) {
	    notice_lang(s_NickServ, u, PERMISSION_DENIED);
	    return;
	}
	delnick(ni);
	if (u2) {
	    u2->ni = NULL;
	    u2->ngi = NULL;
	}
    }

    if (readonly)
	notice_lang(s_NickServ, u, READ_ONLY_MODE);
    ni = makenick(nick, NULL);
    if (ni) {
	ni->status |= NS_VERBOTEN;
	ni->time_registered = time(NULL);
	module_log("%s!%s@%s set FORBID for nick %s",
		   u->nick, u->username, u->host, nick);
	notice_lang(s_NickServ, u, NICK_FORBID_SUCCEEDED, nick);
	/* If someone is using the nick, make them stop */
	if (u2)
	    validate_user(u2);
    } else {
	module_log("Valid FORBID for %s by %s!%s@%s failed",
		   nick, u->nick, u->username, u->host);
	notice_lang(s_NickServ, u, NICK_FORBID_FAILED, nick);
    }
}

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

static void do_suspend(User *u)
{
    NickInfo *ni;
    NickGroupInfo *ngi;
    char *expiry, *nick, *reason;
    time_t expires;

    nick = strtok(NULL, " ");
    if (nick && *nick == '+') {
	expiry = nick;
	nick = strtok(NULL, " ");
    } else {
	expiry = NULL;
    }
    reason = strtok_remaining();

    if (!reason) {
	syntax_error(s_NickServ, u, "SUSPEND", NICK_SUSPEND_SYNTAX);
    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);
    } else if (!(ngi = get_ngi(ni))) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (ngi->suspendinfo) {
	notice_lang(s_NickServ, u, NICK_SUSPEND_ALREADY_SUSPENDED, nick);
    } else {
	if (expiry)
	    expires = dotime(expiry);
	else
	    expires = NSSuspendExpire;
	if (expires < 0) {
	    notice_lang(s_NickServ, u, BAD_EXPIRY_TIME);
	    return;
	} else if (expires > 0) {
	    expires += time(NULL);	/* Set an absolute time */
	}
	module_log("%s!%s@%s suspended %s",
		   u->nick, u->username, u->host, ni->nick);
	suspend_nick(ngi, reason, u->nick, expires);
	notice_lang(s_NickServ, u, NICK_SUSPEND_SUCCEEDED, nick);
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
}

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

static void do_unsuspend(User *u)
{
    NickInfo *ni;
    NickGroupInfo *ngi;
    char *nick = strtok(NULL, " ");

    if (!nick) {
	syntax_error(s_NickServ, u, "UNSUSPEND", NICK_UNSUSPEND_SYNTAX);
    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);
    } else if (!(ngi = get_ngi(ni))) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (!ngi->suspendinfo) {
	notice_lang(s_NickServ, u, NICK_UNSUSPEND_NOT_SUSPENDED, nick);
    } else {
	module_log("%s!%s@%s unsuspended %s",
		   u->nick, u->username, u->host, ni->nick);
	unsuspend_nick(ngi, 1);
	notice_lang(s_NickServ, u, NICK_UNSUSPEND_SUCCEEDED, nick);
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
}

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

#ifdef DEBUG_COMMANDS

/* Return all the fields in the NickInfo structure. */

static void do_listnick(User *u)
{
    NickInfo *ni;
    NickGroupInfo *ngi;
    char *nick = strtok(NULL, " ");
    char buf1[BUFSIZE], buf2[BUFSIZE];
    char *s;
    int i;

    if (!nick)
	return;
    ni = get_nickinfo(nick);
    if (!ni) {
	notice(s_NickServ, u->nick, "%s", nick);
	notice(s_NickServ, u->nick, ":");
	return;
    }
    ngi = get_nickgroupinfo(ni->nickgroup);
    notice(s_NickServ, u->nick, "%s group:%u usermask:%s realmask:%s"
	   " reg:%d seen:%d stat:%04X auth:%04X idstamp:%d badpass:%d :%s;%s",
	   ni->nick, (int)ni->nickgroup, ni->last_usermask, ni->last_realmask,
	   (int)ni->time_registered, (int)ni->last_seen, ni->status & 0xFFFF,
	   ni->authstat & 0xFFFF, ni->id_stamp, ni->bad_passwords,
	   ni->last_realname, (ni->last_quit ? ni->last_quit : "-"));
    if (ngi) {
	if (ngi->authcode) {
	    snprintf(buf1, sizeof(buf1), "%d.%d",
		     (int)ngi->authcode, (int)ngi->authset);
	} else {
	    *buf1 = 0;
	}
	if (ngi->suspendinfo) {
	    SuspendInfo *si = ngi->suspendinfo;
	    snprintf(buf2, sizeof(buf2), "%s.%d.%d.%s",
		     si->who, (int)si->suspended, (int)si->expires,
		     si->reason ? si->reason : "-");
	    strnrepl(buf2, sizeof(buf2), " ", "_");
	} else {
	    *buf2 = 0;
	}
	notice(s_NickServ, u->nick, "+ flags:%08X ospriv:%04X authcode:%s"
	       " susp:%s chancnt:%d chanmax:%d lang:%d tz:%d acccnt:%d"
	       " ajoincnt:%d memocnt:%d memomax:%d igncnt:%d",
	       ngi->flags, ngi->os_priv, buf1, buf2, ngi->channels_count,
	       ngi->channelmax, ngi->language, ngi->timezone,
	       ngi->access_count, ngi->ajoin_count, ngi->memos.memos_count,
	       ngi->memos.memomax, ngi->ignore_count);
	notice(s_NickServ, u->nick, "+ url:%s", ngi->url ? ngi->url : "");
	notice(s_NickServ, u->nick, "+ email:%s", ngi->email?ngi->email:"");
	notice(s_NickServ, u->nick, "+ info:%s", ngi->info ? ngi->info : "");
	s = buf1;
	*buf1 = 0;
	ARRAY_FOREACH (i, ngi->access)
	    s += snprintf(s, sizeof(buf1)-(s-buf1), "%s%s",
			  *buf1 ? "," : "", ngi->access[i]);
	strnrepl(buf1, sizeof(buf1), " ", "_");
	notice(s_NickServ, u->nick, "+ acc:%s", buf1);
	s = buf1;
	*buf1 = 0;
	ARRAY_FOREACH (i, ngi->ajoin)
	    s += snprintf(s, sizeof(buf1)-(s-buf1), "%s%s",
			  *buf1 ? "," : "", ngi->ajoin[i]);
	strnrepl(buf1, sizeof(buf1), " ", "_");
	notice(s_NickServ, u->nick, "+ ajoin:%s", buf1);
	s = buf1;
	*buf1 = 0;
	ARRAY_FOREACH (i, ngi->ignore)
	    s += snprintf(s, sizeof(buf1)-(s-buf1), "%s%s",
			  *buf1 ? "," : "", ngi->ignore[i]);
	strnrepl(buf1, sizeof(buf1), " ", "_");
	notice(s_NickServ, u->nick, "+ ign:%s", buf1);
    } else {
	notice(s_NickServ, u->nick, ":");
    }
}

#endif /* DEBUG_COMMANDS */

/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/

const int32 module_version = MODULE_VERSION_CODE;

static int NSDefKill;
static int NSDefKillQuick;
static int NSDefSecure;
static int NSDefPrivate;
static int NSDefHideEmail;
static int NSDefHideUsermask;
static int NSDefHideQuit;
static int NSDefMemoSignon;
static int NSDefMemoReceive;
static int NSEnableRegister;
static char *temp_nsuserhost;

ConfigDirective module_config[] = {
    { "NickservDB",       { { CD_STRING, CF_DIRREQ, &NickDBName } } },
    { "NickServName",     { { CD_STRING, CF_DIRREQ, &s_NickServ },
                            { CD_STRING, 0, &desc_NickServ } } },
    { "NSAllowKillImmed", { { CD_SET, 0, &NSAllowKillImmed } } },
    { "NSDefHideEmail",   { { CD_SET, 0, &NSDefHideEmail } } },
    { "NSDefHideQuit",    { { CD_SET, 0, &NSDefHideQuit } } },
    { "NSDefHideUsermask",{ { CD_SET, 0, &NSDefHideUsermask } } },
    { "NSDefKill",        { { CD_SET, 0, &NSDefKill } } },
    { "NSDefKillQuick",   { { CD_SET, 0, &NSDefKillQuick } } },
    { "NSDefMemoReceive", { { CD_SET, 0, &NSDefMemoReceive } } },
    { "NSDefMemoSignon",  { { CD_SET, 0, &NSDefMemoSignon } } },
    { "NSDefPrivate",     { { CD_SET, 0, &NSDefPrivate } } },
    { "NSDefSecure",      { { CD_SET, 0, &NSDefSecure } } },
    { "NSEnableRegister", { { CD_SET, 0, &NSEnableRegister } } },
    { "NSEnforcerUser",   { { CD_STRING, CF_DIRREQ, &temp_nsuserhost } } },
    { "NSExpire",         { { CD_TIME, 0, &NSExpire } } },
    { "NSExpireWarning",  { { CD_TIME, 0, &NSExpireWarning } } },
    { "NSForceNickChange",{ { CD_SET, 0, &NSForceNickChange } } },
    { "NSHelpWarning",    { { CD_SET, 0, &NSHelpWarning } } },
    { "NSListMax",        { { CD_POSINT, CF_DIRREQ, &NSListMax } } },
    { "NSListOpersOnly",  { { CD_SET, 0, &NSListOpersOnly } } },
    { "NSRegDelay",       { { CD_TIME, 0, &NSRegDelay } } },
    { "NSRegEmailMax",    { { CD_POSINT, 0, &NSRegEmailMax } } },
    { "NSRequireEmail",   { { CD_SET, 0, &NSRequireEmail } } },
    { "NSReleaseTimeout", { { CD_TIME, CF_DIRREQ, &NSReleaseTimeout } } },
    { "NSSecureAdmins",   { { CD_SET, 0, &NSSecureAdmins } } },
    { "NSShowPassword",   { { CD_SET, 0, &NSShowPassword } } },
    { "NSSuspendExpire",  { { CD_TIME, 0 , &NSSuspendExpire },
                            { CD_TIME, 0 , &NSSuspendGrace } } },
    { NULL }
};

/* Pointer to command records (for EnableCommand) */
static Command *cmd_REGISTER;
static Command *cmd_GETPASS;

/* Old message numbers */
static int old_REGISTER_SYNTAX          = -1;
static int old_HELP_REGISTER_EMAIL      = -1;
static int old_HELP_UNSET               = -1;
static int old_DISCONNECT_IN_1_MINUTE   = -1;
static int old_DISCONNECT_IN_20_SECONDS = -1;

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

static void handle_config(void)
{
    char *s;

    if (temp_nsuserhost) {
	NSEnforcerUser = temp_nsuserhost;
	if (!(s = strchr(temp_nsuserhost, '@'))) {
	    NSEnforcerHost = ServiceHost;
	} else {
	    *s++ = 0;
	    NSEnforcerHost = s;
	}
    }

    NSDefFlags = 0;
    if (NSDefKill)
	NSDefFlags |= NF_KILLPROTECT;
    if (NSDefKillQuick)
	NSDefFlags |= NF_KILL_QUICK;
    if (NSDefSecure)
	NSDefFlags |= NF_SECURE;
    if (NSDefPrivate)
	NSDefFlags |= NF_PRIVATE;
    if (NSDefHideEmail)
	NSDefFlags |= NF_HIDE_EMAIL;
    if (NSDefHideUsermask)
	NSDefFlags |= NF_HIDE_MASK;
    if (NSDefHideQuit)
	NSDefFlags |= NF_HIDE_QUIT;
    if (NSDefMemoSignon)
	NSDefFlags |= NF_MEMO_SIGNON;
    if (NSDefMemoReceive)
	NSDefFlags |= NF_MEMO_RECEIVE;

    if (NSForceNickChange && !(protocol_features & PF_CHANGENICK)) {
	module_log("warning: forced nick changing not supported by IRC"
		   " server, disabling NSForceNickChange");
	NSForceNickChange = 0;
    }
}

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

static int do_reconfigure(int after_configure)
{
    static char old_s_NickServ[NICKMAX];
    static char *old_desc_NickServ = NULL;
    static char *old_NickDBName    = NULL;

    if (!after_configure) {
	/* Before reconfiguration: save old values. */
	strscpy(old_s_NickServ, s_NickServ, NICKMAX);
	old_desc_NickServ = strdup(desc_NickServ);
	old_NickDBName    = strdup(NickDBName);
    } else {
	/* After reconfiguration: handle value changes. */
	handle_config();
	if (strcmp(old_s_NickServ,s_NickServ) != 0)
	    send_nickchange(old_s_NickServ, s_NickServ);
	if (!old_desc_NickServ || strcmp(old_desc_NickServ,desc_NickServ) != 0)
	    send_namechange(s_NickServ, desc_NickServ);
	if (!old_NickDBName || strcmp(old_NickDBName,NickDBName) != 0) {
	    module_log("reconfigure: new database name will only take"
		       " effect after restart");
	    /* Restore the old database name */
	    free(NickDBName);
	    NickDBName = old_NickDBName;
	    /* Make sure the old name isn't freed below */
	    old_NickDBName = NULL;
	}
	free(old_desc_NickServ);
	free(old_NickDBName);
	if (NSEnableRegister)
	    cmd_REGISTER->name = "REGISTER";
	else
	    cmd_REGISTER->name = "";
	if (EnableGetpass)
	    cmd_GETPASS->name = "GETPASS";
	else
	    cmd_GETPASS->name = "";
	if (NSRequireEmail) {
	    setstring(NICK_REGISTER_SYNTAX, NICK_REGISTER_REQ_EMAIL_SYNTAX);
	    setstring(NICK_HELP_REGISTER_EMAIL, NICK_HELP_REGISTER_EMAIL_REQ);
	    setstring(NICK_HELP_UNSET, NICK_HELP_UNSET_REQ_EMAIL);
	} else {
	    setstring(NICK_REGISTER_SYNTAX, old_REGISTER_SYNTAX);
	    setstring(NICK_REGISTER_SYNTAX, old_HELP_REGISTER_EMAIL);
	    setstring(NICK_REGISTER_SYNTAX, old_HELP_UNSET);
	}
	if (NSForceNickChange) {
	    setstring(DISCONNECT_IN_1_MINUTE, FORCENICKCHANGE_IN_1_MINUTE);
	    setstring(DISCONNECT_IN_20_SECONDS, FORCENICKCHANGE_IN_20_SECONDS);
	} else {
	    setstring(DISCONNECT_IN_1_MINUTE, old_DISCONNECT_IN_1_MINUTE);
	    setstring(DISCONNECT_IN_20_SECONDS, old_DISCONNECT_IN_20_SECONDS);
	}
    }  /* if (!after_configure) */
    return 0;
}

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

int init_module(Module *module_)
{
    module = module_;

    handle_config();

    if (!new_commandlist(module) || !register_commands(module, cmds)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }
    cmd_REGISTER = lookup_cmd(module, "REGISTER");
    if (!cmd_REGISTER) {
	module_log("BUG: unable to find REGISTER command entry");
	exit_module(0);
	return 0;
    }
    cmd_GETPASS = lookup_cmd(module, "GETPASS");
    if (!cmd_GETPASS) {
	module_log("BUG: unable to find GETPASS command entry");
	exit_module(0);
	return 0;
    }
    if (!NSEnableRegister)
	cmd_REGISTER->name = "";
    if (!EnableGetpass)
	cmd_GETPASS->name = "";

    cb_command       = register_callback(module, "command");
    cb_help          = register_callback(module, "HELP");
    cb_help_cmds     = register_callback(module, "HELP COMMANDS");
    cb_reglink_check = register_callback(module, "REGISTER/LINK check");
    cb_registered    = register_callback(module, "registered");
    cb_id_check      = register_callback(module, "IDENTIFY check");
    cb_identified    = register_callback(module, "identified");
    if (cb_command < 0 || cb_help < 0 || cb_help_cmds < 0
     || cb_reglink_check < 0 || cb_registered < 0 || cb_id_check < 0
     || cb_identified < 0
    ) {
	module_log("Unable to register callbacks");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "reconfigure", do_reconfigure)
     || !add_callback(NULL, "introduce_user", introduce_nickserv)
     || !add_callback(NULL, "m_privmsg", nickserv)
     || !add_callback(NULL, "m_whois", nickserv_whois)
     || !add_callback(NULL, "save data", do_save_data)
     || !add_callback(NULL, "user create", do_user_create)
     || !add_callback(NULL, "user nickchange (before)",
		      do_user_nickchange_before)
     || !add_callback(NULL, "user nickchange (after)",
		      do_user_nickchange_after)
     || !add_callback(NULL, "user delete", do_user_delete)
     || !add_callback(module, "REGISTER/LINK check", do_reglink_check)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    if (!init_collide(module) || !init_set(module) || !init_util(module)) {
	exit_module(0);
	return 0;
    }

    open_nick_db(NickDBName);
    db_opened = 1;

    old_REGISTER_SYNTAX =
	setstring(NICK_REGISTER_SYNTAX, NICK_REGISTER_SYNTAX);
    old_HELP_REGISTER_EMAIL =
	setstring(NICK_HELP_REGISTER_EMAIL, NICK_HELP_REGISTER_EMAIL);
    old_HELP_UNSET =
	setstring(NICK_HELP_UNSET, NICK_HELP_UNSET);
    old_DISCONNECT_IN_1_MINUTE =
	setstring(DISCONNECT_IN_1_MINUTE, DISCONNECT_IN_1_MINUTE);
    old_DISCONNECT_IN_20_SECONDS =
	setstring(DISCONNECT_IN_20_SECONDS, DISCONNECT_IN_20_SECONDS);
    if (NSRequireEmail) {
	setstring(NICK_REGISTER_SYNTAX, NICK_REGISTER_REQ_EMAIL_SYNTAX);
	setstring(NICK_HELP_REGISTER_EMAIL, NICK_HELP_REGISTER_EMAIL_REQ);
	setstring(NICK_HELP_UNSET, NICK_HELP_UNSET_REQ_EMAIL);
    }
    if (NSForceNickChange) {
	setstring(DISCONNECT_IN_1_MINUTE, FORCENICKCHANGE_IN_1_MINUTE);
	setstring(DISCONNECT_IN_20_SECONDS, FORCENICKCHANGE_IN_20_SECONDS);
    }

    return 1;
}

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

int exit_module(int shutdown_unused)
{
#ifdef CLEAN_COMPILE
    shutdown_unused = shutdown_unused;
#endif

    if (old_REGISTER_SYNTAX >= 0) {
	setstring(NICK_REGISTER_SYNTAX, old_REGISTER_SYNTAX);
	old_REGISTER_SYNTAX = -1;
    }
    if (old_HELP_REGISTER_EMAIL >= 0) {
	setstring(NICK_HELP_REGISTER_EMAIL, old_HELP_REGISTER_EMAIL);
	old_HELP_REGISTER_EMAIL = -1;
    }
    if (old_HELP_UNSET >= 0) {
	setstring(NICK_HELP_UNSET, old_HELP_UNSET);
	old_HELP_UNSET = -1;
    }
    if (old_DISCONNECT_IN_1_MINUTE >= 0) {
	setstring(DISCONNECT_IN_1_MINUTE, old_DISCONNECT_IN_1_MINUTE);
	old_DISCONNECT_IN_1_MINUTE = -1;
    }
    if (old_DISCONNECT_IN_20_SECONDS >= 0) {
	setstring(DISCONNECT_IN_20_SECONDS, old_DISCONNECT_IN_20_SECONDS);
	old_DISCONNECT_IN_20_SECONDS = -1;
    }

    if (db_opened)
	close_nick_db(NickDBName);

    exit_util();
    exit_set();
    exit_collide();

    remove_callback(module, "REGISTER/LINK check", do_reglink_check);
    remove_callback(NULL, "user delete", do_user_delete);
    remove_callback(NULL, "user nickchange (after)",
		    do_user_nickchange_after);
    remove_callback(NULL, "user nickchange (before)",
		    do_user_nickchange_before);
    remove_callback(NULL, "user create", do_user_create);
    remove_callback(NULL, "save data", do_save_data);
    remove_callback(NULL, "m_whois", nickserv_whois);
    remove_callback(NULL, "m_privmsg", nickserv);
    remove_callback(NULL, "introduce_user", introduce_nickserv);
    remove_callback(NULL, "reconfigure", do_reconfigure);

    unregister_callback(module, cb_identified);
    unregister_callback(module, cb_id_check);
    unregister_callback(module, cb_registered);
    unregister_callback(module, cb_reglink_check);
    unregister_callback(module, cb_help_cmds);
    unregister_callback(module, cb_help);
    unregister_callback(module, cb_command);

    cmd_GETPASS->name = "GETPASS";
    cmd_REGISTER->name = "REGISTER";
    unregister_commands(module, cmds);
    del_commandlist(module);

    return 1;
}

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