/* MemoServ functions.
 *
 * 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.
 */

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

#include "memoserv.h"

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

static Module *module;
static Module *module_nickserv;
static Module *module_chanserv;

/* Import */
static int (*p_check_access)(User *u, ChannelInfo *ci, int what);

static int cb_command      = -1;
static int cb_receive_memo = -1;
static int cb_help         = -1;
static int cb_help_cmds    = -1;
static int cb_set          = -1;

       char * s_MemoServ;
static char * desc_MemoServ;
       int32  MSMaxMemos;
static time_t MSExpire;
static int    MSExpireUnread;
static time_t MSSendDelay;
static int    MSNotifyAll;
EXPORT_VAR(int32,MSMaxMemos)

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

/* Error codes for get_memoinfo(). */
#define GMI_NOTFOUND	-1
#define GMI_FORBIDDEN	-2
#define GMI_SUSPENDED	-3
#define GMI_INTERR	-99

/* Macro to return the real memo maximum for a `mi->memomax' value (i.e.
 * convert MEMOMAX_DEFAULT to MSMaxMemos). */
#define REALMAX(n) ((n)==MEMOMAX_DEFAULT ? MSMaxMemos : (n))

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

static void check_memos(User *u);

static MemoInfo *get_memoinfo(const char *name, void **owner_ret,
			      int *ischan_ret, int *error_ret);
static MemoInfo *get_memoinfo_from_cmd(User *u, char **param_ret,
				       char **chan_ret, ChannelInfo **ci_ret);
static void expire_memos(MemoInfo *mi);
static int list_memo(User *u, int index, MemoInfo *mi, int *sent_header,
			int new, const char *chan);
static int list_memo_callback(User *u, int num, va_list args);
static int read_memo(User *u, int index, MemoInfo *mi, const char *chan);
static int read_memo_callback(User *u, int num, va_list args);
static int del_memo(MemoInfo *mi, int num);
static int del_memo_callback(User *u, int num, va_list args);

static void do_help(User *u);
static void do_send(User *u);
static void do_list(User *u);
static void do_read(User *u);
static void do_save(User *u);
static void do_del(User *u);
static void do_set(User *u);
static void do_set_notify(User *u, MemoInfo *mi, char *param);
static void do_set_limit(User *u, MemoInfo *mi, char *param);
static void do_info(User *u);

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

static Command cmds[] = {
    { "HELP",       do_help, NULL,  -1,                      -1,-1 },
    { "SEND",       do_send, NULL,  MEMO_HELP_SEND,          -1,-1 },
    { "LIST",       do_list, NULL,  MEMO_HELP_LIST,          -1,-1 },
    { "READ",       do_read, NULL,  MEMO_HELP_READ,          -1,-1 },
    { "SAVE",       do_save, NULL,  MEMO_HELP_SAVE,          -1,-1 },
    { "DEL",        do_del,  NULL,  MEMO_HELP_DEL,           -1,-1 },
    { "SET",        do_set,  NULL,  MEMO_HELP_SET,           -1,-1 },
    { "SET NOTIFY", NULL,    NULL,  MEMO_HELP_SET_NOTIFY,    -1,-1,
		"NickServ" },
    { "SET LIMIT",  NULL,    NULL,  -1,
    		MEMO_HELP_SET_LIMIT, MEMO_OPER_HELP_SET_LIMIT },
    { "INFO",       do_info, NULL,  -1,
		MEMO_HELP_INFO, MEMO_OPER_HELP_INFO },
    { NULL }
};

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

/* Introduce the MemoServ pseudoclient. */

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

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

/* memoserv:  Main MemoServ routine.
 *            Note that the User structure passed to the do_* routines will
 *            always be valid (non-NULL) and will always have a valid
 *            NickInfo pointer in the `ni' field.
 */

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

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

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

    cmd = strtok(buf, " ");
    if (!cmd) {
	return 1;
    } else if (stricmp(cmd, "\1PING") == 0) {
	const char *s;
	if (!(s = strtok(NULL, "")))
	    s = "\1";
	notice(s_MemoServ, source, "\1PING %s", s);
    } else {
	if (!valid_ngi(u) && stricmp(cmd, "HELP") != 0)
	    notice_lang(s_MemoServ, u, NICK_NOT_REGISTERED_HELP, s_NickServ);
	else if (call_callback_2(module, cb_command, u, cmd) <= 0)
	    run_cmd(s_MemoServ, u, module, cmd);
    }
    return 1;
}

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

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

static int memoserv_whois(const char *source, char *who, char *extra)
{
    if (irc_stricmp(who, s_MemoServ) != 0)
	return 0;
    send_cmd(ServerName, "311 %s %s %s %s * :%s", source, who,
	     ServiceUser, ServiceHost, desc_MemoServ);
    send_cmd(ServerName, "312 %s %s %s :%s", source, who,
	     ServerName, ServerDesc);
    send_cmd(ServerName, "318 %s %s End of /WHOIS response.", source, who);
    return 1;
}

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

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

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

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

/* Callback for users changing nicknames. */

static int do_user_nickchange(User *user, const char *oldnick)
{
    NickInfo *old_ni;
    uint32 old_nickgroup, new_nickgroup;

    /* user->{ni,ngi} are already changed, so look it up again */
    old_ni = get_nickinfo(oldnick);
    old_nickgroup = old_ni ? old_ni->nickgroup : 0;
    new_nickgroup = user->ngi ? user->ngi->id : 0;
    if (old_nickgroup != new_nickgroup)
	check_memos(user);
    return 0;
}

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

/* Callback to check for un-away. */

static int do_receive_message(const char *source, const char *cmd,
			      int ac, char **av)
{
    if (stricmp(cmd, "AWAY") == 0 && (ac == 0 || *av[0] == 0)) {
	User *u = get_user(source);
	if (u)
	    check_memos(u);
    }
    return 0;
}

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

/* Callback for NickServ REGISTER/LINK check; we disallow
 * registration/linking of the MemoServ pseudoclient nickname.
 */

static int do_reglink_check(const User *u, const char *nick,
			    const char *pass, const char *email)
{
    return irc_stricmp(nick, s_MemoServ) == 0;
}

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

/* Callback for users identifying for nicks. */

static int do_nick_identified(User *user, int old_authstat)
{
    if (!(old_authstat & (NA_IDENTIFIED | NA_RECOGNIZED)))
	check_memos(user);
    return 0;
}

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

/* check_memos:  See if the given user has any unread memos, and send a
 *               NOTICE to that user if so (and if the appropriate flag is
 *               set).
 */

static void check_memos(User *u)
{
    NickGroupInfo *ngi = u->ngi;
    int i, newcnt = 0, max;

    if (!ngi || !user_recognized(u) || !(ngi->flags & NF_MEMO_SIGNON))
	return;

    expire_memos(&ngi->memos);

    ARRAY_FOREACH (i, ngi->memos.memos) {
	if (ngi->memos.memos[i].flags & MF_UNREAD)
	    newcnt++;
    }
    if (newcnt > 0) {
	notice_lang(s_MemoServ, u,
		newcnt==1 ? MEMO_HAVE_NEW_MEMO : MEMO_HAVE_NEW_MEMOS, newcnt);
	if (newcnt == 1 && (ngi->memos.memos[i-1].flags & MF_UNREAD)) {
	    notice_lang(s_MemoServ, u, MEMO_TYPE_READ_LAST, s_MemoServ);
	} else if (newcnt == 1) {
	    ARRAY_FOREACH (i, ngi->memos.memos) {
		if (ngi->memos.memos[i].flags & MF_UNREAD)
		    break;
	    }
	    notice_lang(s_MemoServ, u, MEMO_TYPE_READ_NUM, s_MemoServ,
			ngi->memos.memos[i].number);
	} else {
	    notice_lang(s_MemoServ, u, MEMO_TYPE_LIST_NEW, s_MemoServ);
	}
    }
    max = REALMAX(ngi->memos.memomax);
    if (max > 0 && ngi->memos.memos_count >= max) {
	if (ngi->memos.memos_count > max)
	    notice_lang(s_MemoServ, u, MEMO_OVER_LIMIT, max);
	else
	    notice_lang(s_MemoServ, u, MEMO_AT_LIMIT, max);
    }
}

/*************************************************************************/
/*********************** MemoServ private routines ***********************/
/*************************************************************************/

/* Return the MemoInfo corresponding to the given nick or channel name.
 * Return in `owner' the NickGroupInfo or ChannelInfo owning the MemoInfo.
 * Return in `ischan' 1 if the name was a channel name, else 0.
 * Return in `error' a GMI_* error code if the return value is NULL.
 * Also set `error' to GMI_SUSPENDED if the nick/channel is suspended,
 * even though a valid MemoInfo is returned.
 */

static MemoInfo *get_memoinfo(const char *name, void **owner_ret,
			      int *ischan_ret, int *error_ret)
{
    MemoInfo *mi = NULL;
    void *dummy_owner;
    static int dummy_ischan, dummy_error;
    void **owner = owner_ret  ? owner_ret  : &dummy_owner;
    int *ischan  = ischan_ret ? ischan_ret : &dummy_ischan;
    int *error   = error_ret  ? error_ret  : &dummy_error;

    *error = 0;

    if (*name == '#') {
	ChannelInfo *ci;

	*ischan = 1;
	ci = get_channelinfo(name);
	if (ci) {
	    if (ci->flags & CI_VERBOTEN) {
		*error = GMI_FORBIDDEN;
		return NULL;
	    } else {
		if (ci->suspendinfo)
		    *error = GMI_SUSPENDED;
		*owner = ci;
		mi = &ci->memos;
	    }
	} else {
	    *error = GMI_NOTFOUND;
	    return NULL;
	}

    } else {
	NickInfo *ni;
	NickGroupInfo *ngi;

	*ischan = 0;
	ni = get_nickinfo(name);
	if (ni) {
	    if (ni->status & NS_VERBOTEN) {
		*error = GMI_FORBIDDEN;
		return NULL;
	    }
	    ngi = get_ngi(ni);
	    if (!ngi) {
		*error = GMI_INTERR;
		return NULL;
	    }
	    if (ngi->suspendinfo)
		*error = GMI_SUSPENDED;
	    *owner = ngi;
	    mi = &ngi->memos;
	} else {
	    *error = GMI_NOTFOUND;
	    return NULL;
	}
    }

    if (!mi) {
	module_log("BUG: get_memoinfo(): mi==NULL after checks");
	*error = GMI_INTERR;
	return NULL;
    }
    expire_memos(mi);
    return mi;
}

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

/* Retrieve the MemoInfo applicable to a LIST/READ/etc. command based on
 * parameters:
 *    *param_ret set to the first parameter (excluding any channel name)
 *    *chan_ret set to the channel name (NULL if none)
 *    *ci_ret set to the ChannelInfo for the channel (NULL if none)
 * All parameters must be non-NULL.
 */

static MemoInfo *get_memoinfo_from_cmd(User *u, char **param_ret,
				       char **chan_ret, ChannelInfo **ci_ret)
{
    char *param = strtok(NULL, " ");
    char *chan = NULL;
    ChannelInfo *ci = NULL;
    MemoInfo *mi;

    if (module_chanserv && param && *param == '#') {
	chan = param;
	param = strtok(NULL, " ");
	if (!(ci = get_channelinfo(chan))) {
	    notice_lang(s_MemoServ, u, CHAN_X_NOT_REGISTERED, chan);
	    return NULL;
	} else if (ci->flags & CI_VERBOTEN) {
	    notice_lang(s_MemoServ, u, CHAN_X_FORBIDDEN, chan);
	    return NULL;
	} else if (!p_check_access(u, ci, CA_MEMO)) {
	    notice_lang(s_MemoServ, u, ACCESS_DENIED);
	    return NULL;
	}
	mi = &ci->memos;
    } else {
	if (!user_identified(u)) {
	    notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
	    return NULL;
	}
	mi = &u->ngi->memos;
    }
    expire_memos(mi);
    *param_ret = param;
    *chan_ret = chan;
    *ci_ret = ci;
    return mi;
}

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

/* Expire memos for the given MemoInfo. */

static void expire_memos(MemoInfo *mi)
{
    int i;
    time_t limit = time(NULL) - MSExpire;

    if (!MSExpire)
	return;
    ARRAY_FOREACH (i, mi->memos) {
	if ((mi->memos[i].flags & MF_EXPIREOK)
	 && (MSExpireUnread || !(mi->memos[i].flags & MF_UNREAD))
	 && mi->memos[i].time <= limit
	) {
	    free(mi->memos[i].text);
	    ARRAY_REMOVE(mi->memos, i);
	    i--;
	}
    }
}

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

/* Display a single memo entry, possibly printing the header first. */

static int list_memo(User *u, int index, MemoInfo *mi, int *sent_header,
			int new, const char *chan)
{
    Memo *m;
    char timebuf[64];

    if (index < 0 || index >= mi->memos_count)
	return 0;
    if (!*sent_header) {
	if (chan) {
	    notice_lang(s_MemoServ, u,
			new ? MEMO_LIST_CHAN_NEW_MEMOS : MEMO_LIST_CHAN_MEMOS,
			chan, s_MemoServ, chan);
	} else {
	    notice_lang(s_MemoServ, u,
			new ? MEMO_LIST_NEW_MEMOS : MEMO_LIST_MEMOS,
			u->nick, s_MemoServ);
	}
	notice_lang(s_MemoServ, u, MEMO_LIST_HEADER);
	*sent_header = 1;
    }
    m = &mi->memos[index];
    strftime_lang(timebuf, sizeof(timebuf), u->ngi,
		  STRFTIME_DATE_TIME_FORMAT, m->time);
    timebuf[sizeof(timebuf)-1] = 0;	/* just in case */
    notice_lang(s_MemoServ, u, MEMO_LIST_FORMAT,
		(m->flags & MF_UNREAD) ? '*' : ' ',
		(!MSExpire || (m->flags & MF_EXPIREOK)) ? ' ' : '+',
		m->number, m->sender, timebuf);
    return 1;
}

/* List callback. */

static int list_memo_callback(User *u, int num, va_list args)
{
    MemoInfo *mi = va_arg(args, MemoInfo *);
    int *sent_header = va_arg(args, int *);
    const char *chan = va_arg(args, const char *);
    int i;

    ARRAY_FOREACH (i, mi->memos) {
	if (mi->memos[i].number == num)
	    break;
    }
    /* Range checking done by list_memo() */
    return list_memo(u, i, mi, sent_header, 0, chan);
}

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

/* Send a single memo to the given user. */

static int read_memo(User *u, int index, MemoInfo *mi, const char *chan)
{
    Memo *m;
    char timebuf[BUFSIZE];

    if (index < 0 || index >= mi->memos_count)
	return 0;
    m = &mi->memos[index];
    strftime_lang(timebuf, sizeof(timebuf), u->ngi,
		  STRFTIME_DATE_TIME_FORMAT, m->time);
    timebuf[sizeof(timebuf)-1] = 0;
    if (chan)
	notice_lang(s_MemoServ, u, MEMO_CHAN_HEADER, m->number,
		m->sender, timebuf, s_MemoServ, chan, m->number);
    else
	notice_lang(s_MemoServ, u, MEMO_HEADER, m->number,
		m->sender, timebuf, s_MemoServ, m->number);
    notice(s_MemoServ, u->nick, "%s", m->text);
    m->flags &= ~MF_UNREAD;
    return 1;
}

/* Read callback. */

static int read_memo_callback(User *u, int num, va_list args)
{
    MemoInfo *mi = va_arg(args, MemoInfo *);
    const char *chan = va_arg(args, const char *);
    int i;

    ARRAY_FOREACH (i, mi->memos) {
	if (mi->memos[i].number == num)
	    break;
    }
    /* Range check done in read_memo */
    return read_memo(u, i, mi, chan);
}

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

/* Mark a given memo as non-expiring. */

static int save_memo(User *u, int index, MemoInfo *mi)
{
    if (index < 0 || index >= mi->memos_count)
	return 0;
    mi->memos[index].flags &= ~MF_EXPIREOK;
    return 1;
}

/* Save callback. */

static int save_memo_callback(User *u, int num, va_list args)
{
    MemoInfo *mi = va_arg(args, MemoInfo *);
    int *last = va_arg(args, int *);
    int i;

    ARRAY_FOREACH (i, mi->memos) {
	if (mi->memos[i].number == num)
	    break;
    }
    /* Range check done in save_memo */
    if (save_memo(u, i, mi)) {
	*last = num;
	return 1;
    } else {
	return 0;
    }
}

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

/* Delete a memo by number.  Return 1 if the memo was found, else 0. */

static int del_memo(MemoInfo *mi, int num)
{
    int i;

    ARRAY_FOREACH (i, mi->memos) {
	if (mi->memos[i].number == num)
	    break;
    }
    if (i < mi->memos_count) {
	free(mi->memos[i].text);
	ARRAY_REMOVE(mi->memos, i);
	return 1;
    } else {
	return 0;
    }
}

/* Delete a single memo from a MemoInfo. */

static int del_memo_callback(User *u, int num, va_list args)
{
    MemoInfo *mi = va_arg(args, MemoInfo *);
    int *last = va_arg(args, int *);

    if (del_memo(mi, num)) {
	*last = num;
	return 1;
    } else {
	return 0;
    }
}

/*************************************************************************/
/*********************** MemoServ command routines ***********************/
/*************************************************************************/

/* Return a help message. */

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

    if (!cmd) {
	const char *def_s_ChanServ = "ChanServ";
	const char **p_s_ChanServ = NULL;
	const char *levstr;
	if (module_chanserv)
	    p_s_ChanServ = get_module_symbol(module_chanserv, "s_ChanServ");
	if (!p_s_ChanServ)
	    p_s_ChanServ = &def_s_ChanServ;
	if (find_module("chanserv/access-xop")) {
	    if (find_module("chanserv/access-levels"))
		levstr = getstring(u->ngi, CHAN_HELP_REQSOP_LEVXOP);
	    else
		levstr = getstring(u->ngi, CHAN_HELP_REQSOP_XOP);
	} else {
	    levstr = getstring(u->ngi, CHAN_HELP_REQSOP_LEV);
	}
	notice_help(s_MemoServ, u, MEMO_HELP);
	if (MSExpire) {
	    notice_help(s_MemoServ, u, MEMO_HELP_EXPIRES,
			maketime(u->ngi,MSExpire,MT_DUALUNIT));
	}
	if (find_module("chanserv/access-levels")) {
	    notice_help(s_MemoServ, u, MEMO_HELP_END_LEVELS, levstr,
			*p_s_ChanServ);
	} else {
	    notice_help(s_MemoServ, u, MEMO_HELP_END_XOP);
	}
    } else if (call_callback_2(module, cb_help, u, cmd) > 0) {
	return;
    } else if (stricmp(cmd, "COMMANDS") == 0) {
	notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS);
	if (find_module("memoserv/forward"))
	    notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_FORWARD);
	if (MSExpire)
	    notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_SAVE);
	notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_DEL);
	if (find_module("memoserv/ignore"))
	    notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_IGNORE);
	call_callback_2(module, cb_help_cmds, u, 0);
	if (is_oper(u)) {
	    notice_help(s_MemoServ, u, MEMO_OPER_HELP_COMMANDS);
	    call_callback_2(module, cb_help_cmds, u, 1);
	}
    } else if (stricmp(cmd, "SET") == 0) {
	notice_help(s_MemoServ, u, MEMO_HELP_SET);
	if (find_module("memoserv/forward"))
	    notice_help(s_MemoServ, u, MEMO_HELP_SET_OPTION_FORWARD);
	notice_help(s_MemoServ, u, MEMO_HELP_SET_END);
    } else {
	help_cmd(s_MemoServ, u, module, cmd);
    }
}

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

/* Send a memo to a nick/channel. */

static void do_send(User *u)
{
    char *source = u->nick;
    int ischan, error;
    void *owner;
    MemoInfo *mi;
    Memo *m;
    char *name = strtok(NULL, " ");
    char *text = strtok_remaining();
    time_t now = time(NULL);
    int is_servadmin = is_services_admin(u);

    if (readonly) {
	notice_lang(s_MemoServ, u, MEMO_SEND_DISABLED);

    } else if (!text) {
	syntax_error(s_MemoServ, u, "SEND", MEMO_SEND_SYNTAX);

    } else if (!user_identified(u)) {
	notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);

    } else if (!(mi = get_memoinfo(name, &owner, &ischan, &error))) {
	if (error == GMI_FORBIDDEN)
	    notice_lang(s_MemoServ, u,
		ischan ? CHAN_X_FORBIDDEN: NICK_X_FORBIDDEN,
		name);
	else
	    notice_lang(s_MemoServ, u,
		ischan ? CHAN_X_NOT_REGISTERED : NICK_X_NOT_REGISTERED, name);

    } else if (error == GMI_SUSPENDED) {
	notice_lang(s_MemoServ, u,
	    ischan ? CHAN_X_SUSPENDED_MEMOS : NICK_X_SUSPENDED_MEMOS, name);

    } else if (MSSendDelay > 0 &&
		u && u->lastmemosend+MSSendDelay > now && !is_servadmin) {
	u->lastmemosend = now;
	notice_lang(s_MemoServ, u, MEMO_SEND_PLEASE_WAIT,
		    maketime(u->ngi,MSSendDelay,MT_SECONDS));

    } else if (mi->memomax == 0 && !is_servadmin) {
	notice_lang(s_MemoServ, u, MEMO_X_GETS_NO_MEMOS, name);

    } else if (mi->memomax != MEMOMAX_UNLIMITED
	       && mi->memos_count >= REALMAX(mi->memomax)
	       && !is_servadmin) {
	notice_lang(s_MemoServ, u, MEMO_X_HAS_TOO_MANY_MEMOS, name);

    } else {
	u->lastmemosend = now;
	if (call_callback_5(module, cb_receive_memo, ischan, owner, name,
			    u, text) <= 0) {
	    ARRAY_EXTEND(mi->memos);
	    m = &mi->memos[mi->memos_count-1];
	    strscpy(m->sender, source, NICKMAX);
	    if (mi->memos_count > 1) {
		m->number = m[-1].number + 1;
		if (m->number < 1) {
		    int i;
		    ARRAY_FOREACH (i, mi->memos)
			mi->memos[i].number = i+1;
		}
	    } else {
		m->number = 1;
	    }
	    m->time = time(NULL);
	    m->text = sstrdup(text);
	    m->flags = MF_UNREAD;
	    if (MSExpire)
		m->flags |= MF_EXPIREOK;
	    if (!ischan) {
		NickInfo *ni = get_nickinfo(name);
		NickGroupInfo *ngi = ni ? get_ngi(ni) : NULL;
		if (ngi && (ngi->flags & NF_MEMO_RECEIVE)) {
		    User *u2;
		    if (MSNotifyAll) {
			int i;
			ARRAY_FOREACH (i, ngi->nicks) {
			    if (irc_stricmp(ngi->nicks[i], name) == 0) {
				u2 = ni->user;
			    } else {
				NickInfo *ni2 = get_nickinfo(ngi->nicks[i]);
				u2 = ni2 ? ni2->user : NULL;
			    }
			    if (u2 && user_recognized(u2)) {
				notice_lang(s_MemoServ, u2,
					    MEMO_NEW_MEMO_ARRIVED,
					    source, s_MemoServ, m->number);
			    }
			}
		    } else {
			u2 = ni->user;
			if (u2 && user_recognized(u2)) {
			    notice_lang(s_MemoServ, u2, MEMO_NEW_MEMO_ARRIVED,
					source, s_MemoServ, m->number);
			}
		    } /* if (MSNotifyAll) */
		} /* if (flags & MEMO_RECEIVE) */
	    } /* if (!ischan) */
	    if (ischan)
		put_channelinfo(owner);
	    else
		put_nickgroupinfo(owner);
	    notice_lang(s_MemoServ, u, MEMO_SENT, name);
	} /* call_callback returned <=0 */
    } /* if command is valid */
}

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

/* List memos for the source nick or given channel. */

static void do_list(User *u)
{
    char *param, *chan;
    ChannelInfo *ci;
    MemoInfo *mi;
    int i;

    mi = get_memoinfo_from_cmd(u, &param, &chan, &ci);
    if (!mi)
	return;
    if (param && !isdigit(*param) && stricmp(param, "NEW") != 0) {
	syntax_error(s_MemoServ, u, "LIST", MEMO_LIST_SYNTAX);
    } else if (mi->memos_count == 0) {
	if (chan)
	    notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan);
	else
	    notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS);
    } else {
	int sent_header = 0;
	if (param && isdigit(*param)) {
	    process_numlist(param, NULL, list_memo_callback, u,
					mi, &sent_header, chan);
	} else {
	    if (param) {
		ARRAY_FOREACH (i, mi->memos) {
		    if (mi->memos[i].flags & MF_UNREAD)
			break;
		}
		if (i == mi->memos_count) {
		    if (chan)
			notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_NEW_MEMOS,
					chan);
		    else
			notice_lang(s_MemoServ, u, MEMO_HAVE_NO_NEW_MEMOS);
		    return;
		}
	    }
	    ARRAY_FOREACH (i, mi->memos) {
		if (param && !(mi->memos[i].flags & MF_UNREAD))
		    continue;
		list_memo(u, i, mi, &sent_header, param != NULL, chan);
	    }
	}
    }
}

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

/* Read memos. */

static void do_read(User *u)
{
    MemoInfo *mi;
    ChannelInfo *ci = NULL;
    char *numstr, *chan;
    int num, count;

    mi = get_memoinfo_from_cmd(u, &numstr, &chan, &ci);
    if (!mi)
	return;
    num = numstr ? atoi(numstr) : -1;
    if (!numstr || (stricmp(numstr,"LAST") != 0 && stricmp(numstr,"NEW") != 0
                    && num <= 0)) {
	syntax_error(s_MemoServ, u, "READ", MEMO_READ_SYNTAX);
    } else if (mi->memos_count == 0) {
	if (chan)
	    notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan);
	else
	    notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS);
    } else {
	int i;

	if (stricmp(numstr, "NEW") == 0) {
	    int readcount = 0;
	    ARRAY_FOREACH (i, mi->memos) {
		if (mi->memos[i].flags & MF_UNREAD) {
		    read_memo(u, i, mi, chan);
		    readcount++;
		}
	    }
	    if (!readcount) {
		if (chan)
		    notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_NEW_MEMOS, chan);
		else
		    notice_lang(s_MemoServ, u, MEMO_HAVE_NO_NEW_MEMOS);
	    }
	} else if (stricmp(numstr, "LAST") == 0) {
	    read_memo(u, mi->memos_count-1, mi, chan);
	} else {	/* number[s] */
	    if (!process_numlist(numstr, &count, read_memo_callback, u,
								mi, chan)) {
		if (count == 1)
		    notice_lang(s_MemoServ, u, MEMO_DOES_NOT_EXIST, num);
		else
		    notice_lang(s_MemoServ, u, MEMO_LIST_NOT_FOUND, numstr);
	    }
	}
	if (chan)
	    put_channelinfo(ci);
	else
	    put_nickgroupinfo(u->ngi);
    }
}

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

/* Save memos (mark them as non-expiring). */

static void do_save(User *u)
{
    MemoInfo *mi;
    ChannelInfo *ci = NULL;
    char *numstr, *chan;
    int num, count;

    mi = get_memoinfo_from_cmd(u, &numstr, &chan, &ci);
    if (!mi)
	return;
    num = numstr ? atoi(numstr) : -1;
    if (!numstr || num <= 0) {
	syntax_error(s_MemoServ, u, "SAVE", MEMO_SAVE_SYNTAX);
    } else if (mi->memos_count == 0) {
	if (chan)
	    notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan);
	else
	    notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS);
    } else {
	int last = 0;
	int savecount =
	    process_numlist(numstr, &count, save_memo_callback, u, mi, &last);
	if (savecount) {
	    /* Some memos got saved. */
	    if (savecount > 1)
		notice_lang(s_MemoServ, u, MEMO_SAVED_SEVERAL, savecount);
	    else
		notice_lang(s_MemoServ, u, MEMO_SAVED_ONE, last);
	} else {
	    /* No matching memos found. */
	    if (count == 1)
		notice_lang(s_MemoServ, u, MEMO_DOES_NOT_EXIST, num);
	    else
		notice_lang(s_MemoServ, u, MEMO_LIST_NOT_FOUND, numstr);
	}
	if (chan)
	    put_channelinfo(ci);
	else
	    put_nickgroupinfo(u->ngi);
    }
}

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

/* Delete memos. */

static void do_del(User *u)
{
    MemoInfo *mi;
    ChannelInfo *ci = NULL;
    char *numstr, *chan;
    int last, i;
    int delcount, count;

    mi = get_memoinfo_from_cmd(u, &numstr, &chan, &ci);
    if (!mi)
	return;
    if (!numstr || (!isdigit(*numstr) && stricmp(numstr, "ALL") != 0)) {
	syntax_error(s_MemoServ, u, "DEL", MEMO_DEL_SYNTAX);
    } else if (mi->memos_count == 0) {
	if (chan)
	    notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan);
	else
	    notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS);
    } else {
	if (isdigit(*numstr)) {
	    /* Delete a specific memo or memos. */
	    delcount = process_numlist(numstr, &count, del_memo_callback,
				       u, mi, &last);
	    if (delcount) {
		/* Some memos got deleted. */
		if (delcount > 1)
		    notice_lang(s_MemoServ, u, MEMO_DELETED_SEVERAL, delcount);
		else
		    notice_lang(s_MemoServ, u, MEMO_DELETED_ONE, last);
	    } else {
		/* No memos were deleted. */
		if (count == 1)
		    notice_lang(s_MemoServ, u, MEMO_DOES_NOT_EXIST,
				atoi(numstr));
		else
		    notice_lang(s_MemoServ, u, MEMO_DELETED_NONE);
	    }
	} else {
	    /* Delete all memos. */
	    ARRAY_FOREACH (i, mi->memos)
		free(mi->memos[i].text);
	    free(mi->memos);
	    mi->memos = NULL;
	    mi->memos_count = 0;
	    notice_lang(s_MemoServ, u, MEMO_DELETED_ALL);
	}
	if (chan)
	    put_channelinfo(ci);
	else
	    put_nickgroupinfo(u->ngi);
    }
}

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

static void do_set(User *u)
{
    char *cmd    = strtok(NULL, " ");
    char *param  = strtok_remaining();
    MemoInfo *mi = &u->ngi->memos;

    if (readonly) {
	notice_lang(s_MemoServ, u, MEMO_SET_DISABLED);
	return;
    }
    if (!param) {
	syntax_error(s_MemoServ, u, "SET", MEMO_SET_SYNTAX);
    } else if (!user_identified(u)) {
	notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
	return;
    } else if (call_callback_4(module, cb_set, u, mi, cmd, param) > 0) {
	return;
    } else if (stricmp(cmd, "NOTIFY") == 0) {
	do_set_notify(u, mi, param);
    } else if (stricmp(cmd, "LIMIT") == 0) {
	do_set_limit(u, mi, param);
    } else {
	notice_lang(s_MemoServ, u, MEMO_SET_UNKNOWN_OPTION, strupper(cmd));
	notice_lang(s_MemoServ, u, MORE_INFO, s_MemoServ, "SET");
    }
}

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

static void do_set_notify(User *u, MemoInfo *mi, char *param)
{
    if (stricmp(param, "ON") == 0) {
	u->ngi->flags |= NF_MEMO_SIGNON | NF_MEMO_RECEIVE;
	notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_ON, s_MemoServ);
    } else if (stricmp(param, "LOGON") == 0) {
	u->ngi->flags |= NF_MEMO_SIGNON;
	u->ngi->flags &= ~NF_MEMO_RECEIVE;
	notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_LOGON, s_MemoServ);
    } else if (stricmp(param, "NEW") == 0) {
	u->ngi->flags &= ~NF_MEMO_SIGNON;
	u->ngi->flags |= NF_MEMO_RECEIVE;
	notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_NEW, s_MemoServ);
    } else if (stricmp(param, "OFF") == 0) {
	u->ngi->flags &= ~(NF_MEMO_SIGNON | NF_MEMO_RECEIVE);
	notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_OFF, s_MemoServ);
    } else {
	syntax_error(s_MemoServ, u, "SET NOTIFY", MEMO_SET_NOTIFY_SYNTAX);
	return;
    }
    put_nickgroupinfo(u->ngi);
}

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

/* Regular user parameters: [#channel] number
 * Services admin parameters: [#channel|nick] {number|NONE|DEFAULT} [HARD]
 */

static void do_set_limit(User *u, MemoInfo *mi, char *param)
{
    char *p1 = strtok(param, " ");
    char *p2 = strtok(NULL, " ");
    char *user = NULL, *chan = NULL;
    int32 limit;
    NickInfo *ni = u->ni;
    NickGroupInfo *ngi = u->ngi;
    ChannelInfo *ci = NULL;
    int is_servadmin = is_services_admin(u);

    if (module_chanserv && p1 && *p1 == '#') {
	chan = p1;
	p1 = p2;
	p2 = strtok(NULL, " ");
	if (!(ci = get_channelinfo(chan))) {
	    notice_lang(s_MemoServ, u, CHAN_X_NOT_REGISTERED, chan);
	    return;
	} else if (ci->flags & CI_VERBOTEN) {
	    notice_lang(s_MemoServ, u, CHAN_X_FORBIDDEN, chan);
	    return;
	} else if (!is_servadmin && !p_check_access(u, ci, CA_MEMO)) {
	    notice_lang(s_MemoServ, u, ACCESS_DENIED);
	    return;
	}
	mi = &ci->memos;
    }
    if (is_servadmin) {
	if (p2 && stricmp(p2, "HARD") != 0 && !chan) {
	    if (!(ni = get_nickinfo(p1))) {
		notice_lang(s_MemoServ, u, NICK_X_NOT_REGISTERED, p1);
		return;
	    }
	    if (!(ngi = get_ngi(ni))) {
		notice_lang(s_MemoServ, u, INTERNAL_ERROR);
		return;
	    }
	    user = p1;
	    mi = &ngi->memos;
	    p1 = p2;
	    p2 = strtok(NULL, " ");
	} else if (!p1) {
	    syntax_error(s_MemoServ, u, "SET LIMIT",
			 MEMO_SET_LIMIT_OPER_SYNTAX);
	    return;
	}
	if ((!isdigit(*p1) && stricmp(p1, "NONE") != 0
	     && stricmp(p1, "DEFAULT") != 0)
	    || (p2 && stricmp(p2, "HARD") != 0)
	) {
	    syntax_error(s_MemoServ, u, "SET LIMIT",
			 MEMO_SET_LIMIT_OPER_SYNTAX);
	    return;
	}
	if (chan) {
	    if (p2)
		ci->flags |= CI_MEMO_HARDMAX;
	    else
		ci->flags &= ~CI_MEMO_HARDMAX;
	} else {
	    if (p2)
		ngi->flags |= NF_MEMO_HARDMAX;
	    else
		ngi->flags &= ~NF_MEMO_HARDMAX;
	}
	limit = atoi(p1);
	if (limit < 0 || limit > MEMOMAX_MAX) {
	    notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_OVERFLOW, MEMOMAX_MAX);
	    limit = MEMOMAX_MAX;
	}
	if (stricmp(p1, "NONE") == 0)
	    limit = MEMOMAX_UNLIMITED;
	else if (stricmp(p1, "DEFAULT") == 0)
	    limit = MEMOMAX_DEFAULT;
    } else {
	if (!p1 || p2 || !isdigit(*p1)) {
	    syntax_error(s_MemoServ, u, "SET LIMIT", MEMO_SET_LIMIT_SYNTAX);
	    return;
	}
	if (chan && (ci->flags & CI_MEMO_HARDMAX)) {
	    notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_FORBIDDEN, chan);
	    return;
	} else if (!chan && (ngi->flags & NF_MEMO_HARDMAX)) {
	    notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_FORBIDDEN);
	    return;
	}
	limit = atoi(p1);
	/* The first character is a digit, but we could still go negative
	 * from overflow... watch out!  (Actually, atoi() may not allow
	 * that, but it doesn't hurt to check anyway.) */
	if (limit < 0 || (MSMaxMemos > 0 && limit > MSMaxMemos)) {
	    if (chan) {
		notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_TOO_HIGH,
			chan, MSMaxMemos);
	    } else {
		notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_TOO_HIGH,
			MSMaxMemos);
	    }
	    return;
	} else if (limit > MEMOMAX_MAX) {
	    notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_OVERFLOW, MEMOMAX_MAX);
	    limit = MEMOMAX_MAX;
	}
    }
    mi->memomax = limit;
    if (chan)
	put_channelinfo(ci);
    else
	put_nickgroupinfo(ngi);
    if (limit > 0) {
	if (!chan && ni == u->ni)
	    notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT, limit);
	else
	    notice_lang(s_MemoServ, u, MEMO_SET_LIMIT,
			chan ? chan : user, limit);
    } else if (limit == 0) {
	if (!chan && ni == u->ni)
	    notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_ZERO);
	else
	    notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_ZERO,
			chan ? chan : user);
    } else if (limit == MEMOMAX_DEFAULT) {
	if (!chan && ni == u->ni)
	    notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_DEFAULT,
			MSMaxMemos);
	else
	    notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_DEFAULT,
			chan ? chan : user, MSMaxMemos);
    } else {
	if (!chan && ni == u->ni)
	    notice_lang(s_MemoServ, u, MEMO_UNSET_YOUR_LIMIT);
	else
	    notice_lang(s_MemoServ, u, MEMO_UNSET_LIMIT, chan ? chan : user);
    }
}

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

static void do_info(User *u)
{
    MemoInfo *mi;
    NickInfo *ni = NULL;
    NickGroupInfo *ngi = NULL;
    ChannelInfo *ci = NULL;
    char *name = strtok(NULL, " ");
    int is_servadmin = is_services_admin(u);
    int max = 0;
    int is_hardmax = 0;

    if (is_servadmin && name && *name != '#') {
	ni = get_nickinfo(name);
	if (!ni) {
	    notice_lang(s_MemoServ, u, NICK_X_NOT_REGISTERED, name);
	    return;
	} else if (ni->status & NS_VERBOTEN) {
	    notice_lang(s_MemoServ, u, NICK_X_FORBIDDEN, name);
	    return;
	}
	ngi = get_ngi(ni);
	if (!ngi) {
	    notice_lang(s_MemoServ, u, INTERNAL_ERROR);
	    return;
	}
	mi = &ngi->memos;
	is_hardmax = ngi->flags & NF_MEMO_HARDMAX ? 1 : 0;
    } else if (module_chanserv && name && *name == '#') {
	ci = get_channelinfo(name);
	if (!ci) {
	    notice_lang(s_MemoServ, u, CHAN_X_NOT_REGISTERED, name);
	    return;
	} else if (ci->flags & CI_VERBOTEN) {
	    notice_lang(s_MemoServ, u, CHAN_X_FORBIDDEN, name);
	    return;
	} else if (!p_check_access(u, ci, CA_MEMO)) {
	    notice_lang(s_MemoServ, u, ACCESS_DENIED);
	    return;
	}
	mi = &ci->memos;
	is_hardmax = ci->flags & CI_MEMO_HARDMAX ? 1 : 0;
    } else { /* !name */
	if (!user_identified(u)) {
	    notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
	    return;
	}
	ni = u->ni;
	ngi = u->ngi;
	mi = &u->ngi->memos;
    }
    max = REALMAX(mi->memomax);

    if (name && ni != u->ni) {
	/* Report info for a channel or a nick other than the caller. */
	if (!mi->memos_count) {
	    notice_lang(s_MemoServ, u, MEMO_INFO_X_NO_MEMOS, name);
	} else if (mi->memos_count == 1) {
	    if (mi->memos[0].flags & MF_UNREAD)
		notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMO_UNREAD, name);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMO, name);
	} else {
	    int count = 0, i;
	    ARRAY_FOREACH (i, mi->memos) {
		if (mi->memos[i].flags & MF_UNREAD)
		    count++;
	    }
	    if (count == mi->memos_count)
		notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS_ALL_UNREAD,
			name, count);
	    else if (count == 0)
		notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS,
			name, mi->memos_count);
	    else if (count == 0)
		notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS_ONE_UNREAD,
			name, mi->memos_count);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS_SOME_UNREAD,
			name, mi->memos_count, count);
	}
	if (max >= 0) {
	    if (is_hardmax)
		notice_lang(s_MemoServ, u, MEMO_INFO_X_HARD_LIMIT, name, max);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_X_LIMIT, name, max);
	} else {
	    notice_lang(s_MemoServ, u, MEMO_INFO_X_NO_LIMIT, name);
	}
	if (ngi) {	/* only nicks are notified of new memos */
	    if ((ngi->flags&NF_MEMO_RECEIVE) && (ngi->flags&NF_MEMO_SIGNON)) {
		notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_ON, name);
	    } else if (ngi->flags & NF_MEMO_RECEIVE) {
		notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_RECEIVE, name);
	    } else if (ngi->flags & NF_MEMO_SIGNON) {
		notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_SIGNON, name);
	    } else {
		notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_OFF, name);
	    }
	}

    } else { /* !name || ni == u->ni */

	if (!mi->memos_count) {
	    notice_lang(s_MemoServ, u, MEMO_INFO_NO_MEMOS);
	} else if (mi->memos_count == 1) {
	    if (mi->memos[0].flags & MF_UNREAD)
		notice_lang(s_MemoServ, u, MEMO_INFO_MEMO_UNREAD);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_MEMO);
	} else {
	    int count = 0, i;
	    ARRAY_FOREACH (i, mi->memos) {
		if (mi->memos[i].flags & MF_UNREAD)
		    count++;
	    }
	    if (count == mi->memos_count)
		notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS_ALL_UNREAD, count);
	    else if (count == 0)
		notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS, mi->memos_count);
	    else if (count == 1)
		notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS_ONE_UNREAD,
			mi->memos_count);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS_SOME_UNREAD,
			mi->memos_count, count);
	}
	if (max == 0) {
	    if (!is_servadmin && is_hardmax)
		notice_lang(s_MemoServ, u, MEMO_INFO_HARD_LIMIT_ZERO);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_LIMIT_ZERO);
	} else if (max > 0) {
	    if (!is_servadmin && is_hardmax)
		notice_lang(s_MemoServ, u, MEMO_INFO_HARD_LIMIT, max);
	    else
		notice_lang(s_MemoServ, u, MEMO_INFO_LIMIT, max);
	} else {
	    notice_lang(s_MemoServ, u, MEMO_INFO_NO_LIMIT);
	}
	if ((ngi->flags & NF_MEMO_RECEIVE) && (ngi->flags & NF_MEMO_SIGNON)) {
	    notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_ON);
	} else if (ngi->flags & NF_MEMO_RECEIVE) {
	    notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_RECEIVE);
	} else if (ngi->flags & NF_MEMO_SIGNON) {
	    notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_SIGNON);
	} else {
	    notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_OFF);
	}

    } /* if (name && ni != u->ni) */
}

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

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "MemoServName",     { { CD_STRING, CF_DIRREQ, &s_MemoServ },
                            { CD_STRING, 0, &desc_MemoServ } } },
    { "MSExpire",         { { CD_TIME, 0, &MSExpire } } },
    { "MSExpireUnread",   { { CD_SET, 0, &MSExpireUnread } } },
    { "MSMaxMemos",       { { CD_POSINT, 0, &MSMaxMemos } } },
    { "MSNotifyAll",      { { CD_SET, 0, &MSNotifyAll } } },
    { "MSSendDelay",      { { CD_TIME, 0, &MSSendDelay } } },
    { NULL }
};

static Command *cmd_SAVE = NULL;  /* For restoring if !MSExpire */
static int old_HELP_LIST = -1;    /* For restoring if MSExpire */

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

static int do_load_module(Module *mod, const char *modname)
{
    if (strcmp(modname, "nickserv/main") == 0) {
	module_nickserv = mod;
	use_module(mod);
	if (!add_callback(module_nickserv, "REGISTER/LINK check",
			  do_reglink_check))
	    module_log("Unable to register NickServ REGISTER/LINK callback");
	if (!add_callback(mod, "identified", do_nick_identified))
	    module_log("Unable to register NickServ IDENTIFY callback");
    } else if (strcmp(modname, "chanserv/main") == 0) {
	p_check_access = get_module_symbol(mod, "check_access");
	if (p_check_access) {
	    module_chanserv = mod;
	    use_module(mod);
	} else {
	    module_log("Unable to resolve symbol `check_access' in module"
		       " `chanserv/main'; channel memos will not be"
		       " available");
	}
    }
    return 0;
}

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

static int do_unload_module(Module *mod)
{
    if (mod == module_nickserv) {
	remove_callback(module_nickserv, "identified", do_nick_identified);
	remove_callback(module_nickserv, "REGISTER/LINK check",
			do_reglink_check);
	unuse_module(module_nickserv);
	module_nickserv = NULL;
    } else if (mod == module_chanserv) {
	p_check_access = NULL;
	unuse_module(module_chanserv);
	module_chanserv = NULL;
    }
    return 0;
}

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

static int do_reconfigure(int after_configure)
{
    static char old_s_MemoServ[NICKMAX];
    static char *old_desc_MemoServ = NULL;

    if (!after_configure) {
	/* Before reconfiguration: save old values. */
	strscpy(old_s_MemoServ, s_MemoServ, NICKMAX);
	old_desc_MemoServ = strdup(desc_MemoServ);
	if (old_HELP_LIST >= 0) {
	    setstring(MEMO_HELP_LIST, old_HELP_LIST);
	    old_HELP_LIST = -1;
	}
    } else {
	/* After reconfiguration: handle value changes. */
	if (strcmp(old_s_MemoServ, s_MemoServ) != 0)
	    send_nickchange(old_s_MemoServ, s_MemoServ);
	if (!old_desc_MemoServ || strcmp(old_desc_MemoServ,desc_MemoServ) != 0)
	    send_namechange(s_MemoServ, desc_MemoServ);
	free(old_desc_MemoServ);
	if (MSExpire)
	    old_HELP_LIST = setstring(MEMO_HELP_LIST, MEMO_HELP_LIST_EXPIRE);
    }  /* if (!after_configure) */
    return 0;
}

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

int init_module(Module *module_)
{
    Command *cmd;
    Module *tmpmod;

    module = module_;

    if (!new_commandlist(module) || !register_commands(module, cmds)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }
    if (MSExpire) {
	old_HELP_LIST = setstring(MEMO_HELP_LIST, MEMO_HELP_LIST_EXPIRE);
    } else {
	/* Disable SAVE command if no expiration */
	cmd_SAVE = lookup_cmd(module, "SAVE");
	if (cmd_SAVE)
	    cmd_SAVE->name = "";
    }

    cb_command      = register_callback(module, "command");
    cb_receive_memo = register_callback(module, "receive memo");
    cb_help         = register_callback(module, "HELP");
    cb_help_cmds    = register_callback(module, "HELP COMMANDS");
    cb_set          = register_callback(module, "SET");
    if (cb_command < 0 || cb_receive_memo < 0 || cb_help < 0
     || cb_help_cmds < 0 || cb_set < 0) {
	module_log("Unable to register callbacks");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "load module", do_load_module)
     || !add_callback(NULL, "unload module", do_unload_module)
     || !add_callback(NULL, "reconfigure", do_reconfigure)
     || !add_callback(NULL, "introduce_user", introduce_memoserv)
     || !add_callback(NULL, "m_privmsg", memoserv)
     || !add_callback(NULL, "m_whois", memoserv_whois)
     || !add_callback(NULL, "receive message", do_receive_message)
     || !add_callback(NULL, "user create", do_user_create)
     || !add_callback(NULL, "user nickchange (after)", do_user_nickchange)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    tmpmod = find_module("nickserv/main");
    if (tmpmod)
	do_load_module(tmpmod, "nickserv/main");
    tmpmod = find_module("chanserv/main");
    if (tmpmod)
	do_load_module(tmpmod, "chanserv/main");

    cmd = lookup_cmd(module, "SET NOTIFY");
    if (cmd)
	cmd->help_param1 = s_NickServ;
    cmd = lookup_cmd(module, "SET LIMIT");
    if (cmd) {
	cmd->help_param1 = (char *)(long)MSMaxMemos;
	cmd->help_param2 = (char *)(long)MSMaxMemos;
    }

    if (linked)
	introduce_memoserv(NULL);

    return 1;
}

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

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

    if (linked)
	send_cmd(s_MemoServ, "QUIT :");

    if (module_chanserv)
	do_unload_module(module_chanserv);
    if (module_nickserv)
	do_unload_module(module_nickserv);

    remove_callback(NULL, "user nickchange (after)", do_user_nickchange);
    remove_callback(NULL, "user create", do_user_create);
    remove_callback(NULL, "receive message", do_receive_message);
    remove_callback(NULL, "m_whois", memoserv_whois);
    remove_callback(NULL, "m_privmsg", memoserv);
    remove_callback(NULL, "introduce_user", introduce_memoserv);
    remove_callback(NULL, "reconfigure", do_reconfigure);
    remove_callback(NULL, "unload module", do_unload_module);
    remove_callback(NULL, "load module", do_load_module);

    unregister_callback(module, cb_set);
    unregister_callback(module, cb_help_cmds);
    unregister_callback(module, cb_help);
    unregister_callback(module, cb_receive_memo);
    unregister_callback(module, cb_command);

    if (cmd_SAVE) {
	cmd_SAVE->name = "SAVE";
	cmd_SAVE = NULL;
    }
    if (old_HELP_LIST >= 0) {
	setstring(MEMO_HELP_LIST, old_HELP_LIST);
	old_HELP_LIST = -1;
    }
    unregister_commands(module, cmds);
    del_commandlist(module);

    return 1;
}

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


syntax highlighted by Code2HTML, v. 0.9.1