/* Nickname mail address authentication module.
 *
 * IRC Services is copyright (c) 1996-2005 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 "commands.h"
#include "language.h"
#include "modules/mail/mail.h"
#include "modules/operserv/operserv.h"

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

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

/*
 * This module provides the ability to verify the accuracy of E-mail
 * addresses associated with nicknames.  Upon registering a new nickname
 * or changing the E-mail address of a nickname, a random authentication
 * code (see below) is generated and mailed to the user, with instructions
 * to use the AUTH command (provided by this module) to verify the nick
 * registration or address change.  Until this is done, the nick may not
 * be identified for, essentially preventing use of the nick.
 *
 * The access code generated is a random 9-digit number, lower-bounded to
 * 100,000,000 to avoid potential difficulties with leading zeroes.
 */

/*************************************************************************/
/*************************** Local variables *****************************/
/*************************************************************************/

static Module *module;
static Module *module_nickserv;
static Module *module_mail;

static time_t NSNoAuthExpire = 0;
static time_t NSSendauthDelay = 0;


static void do_auth(User *u);
static void do_sendauth(User *u);
static void do_setauth(User *u);
static void do_getauth(User *u);
static void do_clearauth(User *u);

static Command commands[] = {
    { "AUTH",     do_auth,     NULL,              NICK_HELP_AUTH,     -1,-1 },
    { "SENDAUTH", do_sendauth, NULL,              NICK_HELP_SENDAUTH, -1,-1 },
    { "SETAUTH",  do_setauth,  is_services_admin,
		-1,-1,NICK_OPER_HELP_SETAUTH   },
    { "GETAUTH",  do_getauth,  is_services_admin,
		-1,-1,NICK_OPER_HELP_GETAUTH   },
    { "CLEARAUTH",do_clearauth,is_services_admin,
		-1,-1,NICK_OPER_HELP_CLEARAUTH },
    { NULL }
};

/*************************************************************************/
/**************************** Local routines *****************************/
/*************************************************************************/

/* Create an authentication code for the given nickname group and store it
 * in the NickGroupInfo structure.  `reason' is the NICKAUTH_* value to
 * store in the `authreason' field.
 */

static void make_auth(NickGroupInfo *ngi, int16 reason)
{
    ngi->authcode = rand()%900000000 + 100000000;
    ngi->authset = time(NULL);
    ngi->authreason = reason;
    put_nickgroupinfo(ngi);
}

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

/* Send mail to a nick's E-mail address containing the authentication code.
 * `what' is one of the IS_* constants defined below.  `caller_line' is
 * filled in by the macro below.  Returns the result of sendmail().
 */

#define IS_REGISTRATION		NICK_AUTH_MAIL_TEXT_REG
#define IS_EMAIL_CHANGE		NICK_AUTH_MAIL_TEXT_EMAIL
#define IS_SENDAUTH		NICK_AUTH_MAIL_TEXT_SENDAUTH
#define IS_SETAUTH		-1

static int send_auth(User *u, NickGroupInfo *ngi, const char *nick,
		     int what, int caller_line)
{
    char subject[BUFSIZE], body[BUFSIZE];
    const char *text;

    if (!u || !ngi || !ngi->email) {
	module_log("send_auth() with %s! (called from line %d)",
		   !u ? "null User" :
		       ngi ? "NickGroupInfo with no E-mail" :
		       "null NickGroupInfo",
		   caller_line);
	return -1;
    }
    text = what<0 ? "" : getstring(ngi, what);
    snprintf(subject, sizeof(subject), getstring(ngi,NICK_AUTH_MAIL_SUBJECT),
	     nick);
    if (what == IS_SETAUTH) {
	snprintf(body, sizeof(body),
		 getstring(ngi,NICK_AUTH_MAIL_BODY_SETAUTH),
		 nick, ngi->authcode, s_NickServ, s_NickServ, ngi->authcode);
    } else {
	snprintf(body, sizeof(body), getstring(ngi,NICK_AUTH_MAIL_BODY),
		 nick, ngi->authcode, s_NickServ, s_NickServ, ngi->authcode,
		 s_NickServ, text, u->username, u->host);
    }
    return sendmail(ngi->email, subject, body);
}
#define send_auth(u,ngi,nick,what) send_auth(u,ngi,nick,what,__LINE__)

/*************************************************************************/
/*************************** Command handlers ****************************/
/*************************************************************************/

/* Handle the AUTH command. */

static void do_auth(User *u)
{
    char *s = strtok(NULL, " ");
    int32 code;
    NickInfo *ni;
    NickGroupInfo *ngi;

    if (!s || !*s) {
	syntax_error(s_NickServ, u, "AUTH", NICK_AUTH_SYNTAX);
    } else if (readonly) {
	notice_lang(s_NickServ, u, NICK_AUTH_DISABLED);
    } else if (!(ni = u->ni)) {
	notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, u->nick);
    } else if (!(ngi = u->ngi) || ngi == NICKGROUPINFO_INVALID) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (!ngi->authcode) {
	notice_lang(s_NickServ, u, NICK_AUTH_NOT_NEEDED, s_NickServ);
    } else if (!ngi->email) {
	module_log("BUG: do_auth() for %s[%u]: authcode set but no email!",
		   ni->nick, ngi->id);
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else {
	const char *what = "(unknown)";
	int16 authreason = ngi->authreason;

	errno = 0;
	code = strtol(s, &s, 10);
	if (errno == ERANGE || *s || code != ngi->authcode) {
	    char buf[BUFSIZE];
	    snprintf(buf, sizeof(buf), "AUTH for %s", ni->nick);
	    notice_lang(s_NickServ, u, NICK_AUTH_FAILED);
	    if (bad_password(NULL, u, buf) == 1)
		notice_lang(s_NickServ, u, PASSWORD_WARNING_FOR_AUTH);
	    ngi->bad_auths++;
	    if (BadPassWarning && ngi->bad_auths >= BadPassWarning) {
		wallops(s_NickServ, "\2Warning:\2 Repeated bad AUTH attempts"
			" for nick %s", ni->nick);
	    }
	    return;
	}
	ngi->authcode = 0;
	ngi->authset = 0;
	ngi->authreason = 0;
	ngi->bad_auths = 0;
	if (authreason == NICKAUTH_REGISTER)
	    ngi->flags = NSDefFlags;
	put_nickgroupinfo(ngi);
	set_identified(u, ni, ngi);
	switch (authreason) {
	  case NICKAUTH_REGISTER:
	    notice_lang(s_NickServ, u, NICK_AUTH_SUCCEEDED_REGISTER);
	    what = "REGISTER";
	    break;
	  case NICKAUTH_SET_EMAIL:
	    notice_lang(s_NickServ, u, NICK_AUTH_SUCCEEDED_SET_EMAIL);
	    what = "SET EMAIL";
	    break;
	  case NICKAUTH_SETAUTH:
	    what = "SETAUTH";
	    /* fall through */
	  default:
	    /* "you may now continue using your nick", good for a default */
	    notice_lang(s_NickServ, u, NICK_AUTH_SUCCEEDED_SETAUTH);
	    break;
	}
	module_log("%s@%s authenticated %s for %s", u->username, u->host,
		   what, ni->nick);
    }
}

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

/* Handle the SENDAUTH command.  To prevent abuse the command is limited to
 * one use per nick per day (reset when Services starts up).
 */

static void do_sendauth(User *u)
{
    char *s = strtok(NULL, " ");
    NickInfo *ni;
    NickGroupInfo *ngi;
    time_t now = time(NULL);

    if (s) {
	syntax_error(s_NickServ, u, "SENDAUTH", NICK_SENDAUTH_SYNTAX);
    } else if (!(ni = u->ni)) {
	notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);
    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, u->nick);
    } else if (!(ngi = u->ngi) || ngi == NICKGROUPINFO_INVALID) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else if (!ngi->authcode) {
	notice_lang(s_NickServ, u, NICK_AUTH_NOT_NEEDED);
    } else if (ngi->last_sendauth
	       && now - ngi->last_sendauth < NSSendauthDelay) {
	notice_lang(s_NickServ, u, NICK_SENDAUTH_TOO_SOON,
		    maketime(ngi,NSSendauthDelay-(now-ngi->last_sendauth),0));
    } else if (!ngi->email) {
	module_log("BUG: do_sendauth() for %s[%u]: authcode set but no email!",
		   ni->nick, ngi->id);
	notice_lang(s_NickServ, u, INTERNAL_ERROR);
    } else {
	int res = send_auth(u, ngi, ni->nick, IS_SENDAUTH);
	if (res == 0) {
	    ngi->last_sendauth = time(NULL);
	    notice_lang(s_NickServ, u, NICK_AUTH_SENT, ngi->email);
	} else if (res == 1) {
	    notice_lang(s_NickServ, u, SENDMAIL_NO_RESOURCES);
	} else {
	    module_log("Valid SENDAUTH by %s!%s@%s failed",
		       u->nick, u->username, u->host);
	    notice_lang(s_NickServ, u, NICK_SENDAUTH_FAILED);
	}
    }
}

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

/* Handle the SETAUTH command (Services admins only). */

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

    if (!nick) {
	syntax_error(s_NickServ, u, "SETAUTH", NICK_SETAUTH_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->authcode) {
	notice_lang(s_NickServ, u, NICK_AUTH_HAS_AUTHCODE, ni->nick);
    } else if (!ngi->email) {
	notice_lang(s_NickServ, u, NICK_SETAUTH_NO_EMAIL, ni->nick);
    } else {
	int i;
	make_auth(ngi, NICKAUTH_SETAUTH);
	notice_lang(s_NickServ, u, NICK_SETAUTH_AUTHCODE_SET,
		    ngi->authcode, ni->nick);
	i = send_auth(u, ngi, ni->nick, IS_SETAUTH);
	if (i != 0) {
	    module_log("send_auth() failed%s for SETAUTH on %s by %s",
		       i==1 ? " temporarily" : "", nick, u->nick);
	    notice_lang(s_NickServ, u, i==1 ? NICK_SETAUTH_SEND_TEMPFAIL
					    : NICK_SETAUTH_SEND_FAILED,
			ngi->email);
	}
	ngi->last_sendauth = 0;
	ARRAY_FOREACH (i, ngi->nicks) {
	    NickInfo *ni2;
	    if (irc_stricmp(nick, ngi->nicks[i]) == 0)
		ni2 = ni;
	    else
		ni2 = get_nickinfo_noexpire(ngi->nicks[i]);
	    if (!ni2) {
		module_log("BUG: missing NickInfo for nick %d (%s) of"
			   " nickgroup %u", i, ngi->nicks[i], ngi->id);
		continue;
	    }
	    ni2->authstat &= ~NA_IDENTIFIED;
	    if (ni2->user) {
		notice_lang(s_NickServ, ni2->user, NICK_SETAUTH_USER_NOTICE,
			    ngi->email, s_NickServ);
	    }
	}
    }
}

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

/* Handle the GETAUTH command (Services admins only). */

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

    if (!nick) {
	syntax_error(s_NickServ, u, "GETAUTH", NICK_GETAUTH_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->authcode) {
	notice_lang(s_NickServ, u, NICK_AUTH_NO_AUTHCODE, ni->nick);
    } else {
	notice_lang(s_NickServ, u, NICK_GETAUTH_AUTHCODE_IS,
		    ni->nick, ngi->authcode);
    }
}

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

/* Handle the CLEARAUTH command (Services admins only). */

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

    if (!nick) {
	syntax_error(s_NickServ, u, "CLEARAUTH", NICK_CLEARAUTH_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->authcode) {
	notice_lang(s_NickServ, u, NICK_AUTH_NO_AUTHCODE, ni->nick);
    } else {
	ngi->authcode = 0;
	put_nickgroupinfo(ngi);
	notice_lang(s_NickServ, u, NICK_CLEARAUTH_CLEARED, ni->nick);
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
}

/*************************************************************************/
/*************************** Callback routines ***************************/
/*************************************************************************/

/* Nick-registration callback: clear IDENTIFIED flag, set nick flags
 * appropriately (no kill, secure), send authcode mail.
 */

static int do_registered(User *u, NickInfo *ni, NickGroupInfo *ngi,
			 int *replied)
{
    int i;

    make_auth(ngi, NICKAUTH_REGISTER);
    if ((i = send_auth(u, ngi, ni->nick, IS_REGISTRATION)) != 0) {
	module_log("send_auth() failed%s for registration (%s)",
		   i==1 ? " temporarily" : "", u->nick);
    }
    ni->authstat &= ~NA_IDENTIFIED;
    ngi->last_sendauth = 0;
    ngi->flags &= ~(NF_KILLPROTECT | NF_KILL_QUICK | NF_KILL_IMMED);
    ngi->flags |= NF_SECURE;
    if (!*replied) {
	notice_lang(s_NickServ, u, NICK_REGISTERED, u->nick);
	*replied = 1;
    }
    notice_lang(s_NickServ, u, NICK_AUTH_SENT, ngi->email);
    notice_lang(s_NickServ, u, NICK_AUTH_FOR_REGISTER, s_NickServ, ngi->authcode);
    return 0;
}

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

/* SET EMAIL callback: clear IDENTIFIED flag, send authcode mail. */

static int do_set_email(User *u, NickGroupInfo *ngi)
{
    int i;

    /* Note: we assume here that if it's not a servadmin doing the change,
     * it must be the user him/herself (and thus use u->nick and u->ni).
     * See also set.c:do_set_email(). */
    if (ngi->email && !is_services_admin(u)) {
	make_auth(ngi, NICKAUTH_SET_EMAIL);
	if ((i = send_auth(u, ngi, u->nick, IS_EMAIL_CHANGE)) != 0) {
	    module_log("send_auth() failed%s for E-mail change (%s)",
		       i==1 ? " temporarily" : "", u->nick);
	}
	u->ni->authstat &= ~NA_IDENTIFIED;
	ngi->last_sendauth = 0;
	notice_lang(s_NickServ, u, NICK_AUTH_SENT, ngi->email);
	notice_lang(s_NickServ, u, NICK_AUTH_FOR_SET_EMAIL, s_NickServ);
    }
    return 0;
}

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

/* IDENTIFY check: do not allow identification if nick not authenticated. */

static int do_identify_check(const User *u, const char *pass)
{
    if (u->ngi && u->ngi != NICKGROUPINFO_INVALID && u->ngi->authcode) {
	notice_lang(s_NickServ, u, NICK_PLEASE_AUTH, u->ngi->email);
	notice_lang(s_NickServ, u, MORE_INFO, s_NickServ, "AUTH");
	return 1;
    }
    return 0;
}

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

/* Expiration check callback. */

static int do_check_expire(NickInfo *ni, NickGroupInfo *ngi)
{
    time_t now = time(NULL);

    if (!NSNoAuthExpire)
	return 0;
    if (ngi && ngi->authcode
     && ngi->authreason != NICKAUTH_SET_EMAIL
     && now - ngi->authset >= NSNoAuthExpire
    ) {
	module_log("Expiring unauthenticated nick %s", ni->nick);
	return 1;
    }
    return 0;
}

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

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "NSNoAuthExpire",   { { CD_TIME, 0, &NSNoAuthExpire } } },
    { "NSSendauthDelay",  { { CD_TIME, 0, &NSSendauthDelay } } },
    { NULL }
};

/* Old message number */
static int old_LIST_OPER_SYNTAX = -1;
static int old_HELP_REGISTER_EMAIL = -1;
static int old_OPER_HELP_LIST = -1;
static int old_OPER_HELP_LISTEMAIL = -1;

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

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

    module_nickserv = find_module("nickserv/main");
    if (!module_nickserv) {
	module_log("Main NickServ module not loaded");
	return 0;
    }
    use_module(module_nickserv);

    module_mail = find_module("mail/main");
    if (!module_mail) {
	module_log("Mail module not loaded");
	return 0;
    }
    use_module(module_mail);

    if (!NSRequireEmail) {
	module_log("NSRequireEmail must be set to use nickname"
		   " authentication");
	return 0;
    }

    if (!register_commands(module_nickserv, commands)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }

    if (!add_callback(module_nickserv, "registered", do_registered)
     || !add_callback(module_nickserv, "SET EMAIL", do_set_email)
     || !add_callback(module_nickserv, "IDENTIFY check", do_identify_check)
     || !add_callback(module_nickserv, "check_expire", do_check_expire)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    old_LIST_OPER_SYNTAX =
	setstring(NICK_LIST_OPER_SYNTAX, NICK_LIST_OPER_SYNTAX_AUTH);
    old_HELP_REGISTER_EMAIL =
	setstring(NICK_HELP_REGISTER_EMAIL, NICK_HELP_REGISTER_EMAIL_AUTH);
    old_OPER_HELP_LIST =
	setstring(NICK_OPER_HELP_LIST, NICK_OPER_HELP_LIST_AUTH);
    old_OPER_HELP_LISTEMAIL =
	setstring(NICK_OPER_HELP_LISTEMAIL, NICK_OPER_HELP_LISTEMAIL_AUTH);

    return 1;
}

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

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

    if (old_OPER_HELP_LISTEMAIL >= 0) {
	setstring(NICK_OPER_HELP_LISTEMAIL, old_OPER_HELP_LISTEMAIL);
	old_OPER_HELP_LISTEMAIL = -1;
    }
    if (old_OPER_HELP_LIST >= 0) {
	setstring(NICK_OPER_HELP_LIST, old_OPER_HELP_LIST);
	old_OPER_HELP_LIST = -1;
    }
    if (old_HELP_REGISTER_EMAIL >= 0) {
	setstring(NICK_HELP_REGISTER_EMAIL, old_HELP_REGISTER_EMAIL);
	old_HELP_REGISTER_EMAIL = -1;
    }
    if (old_LIST_OPER_SYNTAX >= 0) {
	setstring(NICK_LIST_OPER_SYNTAX, old_LIST_OPER_SYNTAX);
	old_LIST_OPER_SYNTAX = -1;
    }

    if (module_mail) {
	unuse_module(module_mail);
	module_mail = NULL;
    }
    if (module_nickserv) {
	remove_callback(module_nickserv, "check_expire", do_check_expire);
	remove_callback(module_nickserv, "IDENTIFY check", do_identify_check);
	remove_callback(module_nickserv, "SET EMAIL", do_set_email);
	remove_callback(module_nickserv, "registered", do_registered);
	unregister_commands(module_nickserv, commands);
	unuse_module(module_nickserv);
	module_nickserv = NULL;
    }

    return 1;
}

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