/*
 * Unreal Internet Relay Chat Daemon, src/modules/tkl.c
 * TKL Commands: server bans, spamfilters, etc.
 * (C) 1999-2019 Bram Matthys and The UnrealIRCd Team
 *
 * See file AUTHORS in IRC package for additional names of
 * the programmers.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 1, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

ModuleHeader MOD_HEADER
= {
	"tkl",
	"5.0",
	"Server ban commands such as /GLINE, /SPAMFILTER, etc.",
	"UnrealIRCd Team",
	"unrealircd-6",
};

/* Forward declarations */
int tkl_config_test_spamfilter(ConfigFile *, ConfigEntry *, int, int *);
int tkl_config_run_spamfilter(ConfigFile *, ConfigEntry *, int);
int tkl_config_test_ban(ConfigFile *, ConfigEntry *, int, int *);
int tkl_config_run_ban(ConfigFile *, ConfigEntry *, int);
int tkl_config_test_except(ConfigFile *, ConfigEntry *, int, int *);
int tkl_config_run_except(ConfigFile *, ConfigEntry *, int);
int tkl_config_test_set(ConfigFile *, ConfigEntry *, int, int *);
int tkl_config_run_set(ConfigFile *, ConfigEntry *, int);
int tkl_ip_change(Client *client, const char *oldip);
int tkl_accept(Client *client);
void check_set_spamfilter_utf8_setting_changed(void);
CMD_FUNC(cmd_gline);
CMD_FUNC(cmd_shun);
CMD_FUNC(cmd_tempshun);
CMD_FUNC(cmd_gzline);
CMD_FUNC(cmd_kline);
CMD_FUNC(cmd_zline);
CMD_FUNC(cmd_spamfilter);
CMD_FUNC(cmd_eline);
CMD_FUNC(cmd_spaminfo);
void cmd_tkl_line(Client *client, int parc, const char *parv[], char *type);
int _tkl_hash(unsigned int c);
void _tkl_hit(Client *client, TKL *tkl);
char _tkl_typetochar(int type);
int _tkl_chartotype(char c);
char _tkl_configtypetochar(const char *name);
int tkl_banexception_chartotype(char c);
const char *_tkl_type_string(TKL *tk);
const char *_tkl_type_config_string(TKL *tk);
char *tkl_banexception_configname_to_chars(char *name);
TKL *_tkl_add_serverban(int type, const char *usermask, const char *hostmask, SecurityGroup *match,
                            const char *reason, const char *set_by,
                            time_t expire_at, time_t set_at, int soft, int flags);
TKL *_tkl_add_banexception(int type, const char *usermask, const char *hostmask, SecurityGroup *match,
                           const char *reason, const char *set_by,
                           time_t expire_at, time_t set_at, int soft, const char *bantypes, int flags);
TKL *_tkl_add_nameban(int type, const char *name, int hold, const char *reason, const char *set_by,
                          time_t expire_at, time_t set_at, int flags);
TKL *_tkl_add_spamfilter(int type, const char *id, unsigned short target, BanAction *action,
                         Match *match, const char *rule, SecurityGroup *except,
                         const char *set_by,
                         time_t expire_at, time_t set_at,
                         time_t spamf_tkl_duration, const char *spamf_tkl_reason,
                         int input_conversion,
                         SpamfilterShowMessageContentOnHit show_message_content_on_hit,
                         int flags);
void _sendnotice_tkl_del(const char *removed_by, TKL *tkl);
void _sendnotice_tkl_add(TKL *tkl);
void _free_tkl(TKL *tkl);
void _tkl_del_line(TKL *tkl);
static void _tkl_check_local_remove_shun(TKL *tmp);
char *_tkl_uhost(TKL *tkl, char *buf, size_t buflen, int options);
void tkl_expire_entry(TKL * tmp);
EVENT(tkl_check_expire);
int _find_tkline_match(Client *client, int skip_soft);
int _find_shun(Client *client);
int _find_spamfilter_user(Client *client, int flags);
TKL *_find_qline(Client *client, const char *nick, int *ishold);
TKL *_find_tkline_match_zap(Client *client);
void _tkl_stats(Client *client, int type, const char *para, int *cnt);
void _tkl_sync(Client *client);
CMD_FUNC(_cmd_tkl);
int _take_action(Client *client, BanAction *action, const char *reason, long duration, int take_action_flags, int *stopped);
int _match_spamfilter(Client *client, const char *str_in, int type, const char *cmd, const char *target, int flags, ClientContext *clictx, TKL **rettk);
int _match_spamfilter_mtags(Client *client, MessageTag *mtags, const char *cmd);
void _run_deferred_rule_only_spamfilters(Client *client);
int check_special_spamfilters_present(void);
char *tkl_hits_key(TKL *tkl, char *buf, size_t len);
void config_tkl_hits_free(ModData *m);
void config_tkl_hits_snapshot(TKL *tkl);
void _config_tkl_hits_restore(void);
void _remove_config_tkls(int flag);
int _join_viruschan(Client *client, TKL *tk, int type);
void _spamfilter_build_user_string(char *buf, const char *nick, Client *client);
int _match_user(const char *rmask, Client *client, int options);
int _unreal_match_iplist(Client *client, NameList *l);
int _match_user_extended_server_ban(const char *banstr, Client *client);
void ban_target_to_tkl_layer(BanTarget ban_target, BanActionValue action, Client *client, const char **tkl_username, const char **tkl_hostname);
int _tkl_ip_hash(const char *ip);
int _tkl_ip_hash_type(int type);
TKL *_find_tkl_serverban(int type, const char *usermask, const char *hostmask, int softban);
TKL *_find_tkl_banexception(int type, const char *usermask, const char *hostmask, int softban);
TKL *_find_tkl_nameban(int type, const char *name, int hold);
TKL *_find_tkl_spamfilter(int type, const char *match_string, BanActionValue action, unsigned short target);
int _find_tkl_exception(int ban_type, Client *client);
int _server_ban_parse_mask(Client *client, int add, char type, const char *str, char **usermask_out, char **hostmask_out, int *soft, const char **error);
int _server_ban_exception_parse_mask(Client *client, int add, const char *bantypes, const char *str, char **usermask_out, char **hostmask_out, int *soft, const char **error);
static void add_default_exempts(void);
int parse_extended_server_ban(const char *mask_in, Client *client, char **error, int skip_checking, char *buf1, size_t buf1len, char *buf2, size_t buf2len);
void _tkl_added(Client *client, TKL *tkl);
int spamfilter_pre_command(Client *from, MessageTag *mtags, const char *buf);

/* Externals (only for us :D) */
extern int MODVAR spamf_ugly_vchanoverride;

typedef struct TKLTypeTable TKLTypeTable;
struct TKLTypeTable
{
	char *config_name;        /**< The name as used in the configuration file */
	char letter;              /**< The letter ised in the TKL S2S command */
	int type;                 /**< TKL_xxx, optionally OR'ed with TKL_GLOBAL */
	char *log_name;           /**< Used for logging and server notices */
	unsigned tkltype:1;       /**< Is a type available in cmd_tkl() and friends */
	unsigned exceptiontype:1; /**< Is a type available for exceptions */
	unsigned needip:1;        /**< When using this exempt option, only IP addresses are permitted (processed before DNS/ident lookups etc) */
	char *id_prefix;          /**< Prefix for generated TKL ids, eg "G", "K", "SPAM". NULL for exempt-only options. Note that shun has "H" here while its type.letter is 's'. */
};

/** This table which defines all TKL types and TKL exception types.
 * If you wonder about the messy order: gline/kline/gzline/zline
 * are at the top for performance reasons. They make up 99% of the TKLs.
 *
 * IMPORTANT IF YOU ARE ADDING A NEW TYPE TO THIS TABLE:
 * - also update eline_syntax()
 * - update help.conf (HELPOP ELINE)
 * - more?
 */
/* clang-format off */
TKLTypeTable tkl_types[] = {
	/* <config name> <letter> <TKL_xxx type>               <logging name> <tkl option?> <exempt option?> <ip address only?> <TKLID Prefix> */
	{ "gline",                'G', TKL_KILL       | TKL_GLOBAL, "G-Line",               1, 1, 0, "G" },
	{ "kline",                'k', TKL_KILL,                    "K-Line",               1, 1, 0, "K" },
	{ "gzline",               'Z', TKL_ZAP        | TKL_GLOBAL, "Global Z-Line",        1, 1, 1, "Z" },
	{ "zline",                'z', TKL_ZAP,                     "Z-Line",               1, 1, 1, "Z" },
	{ "spamfilter",           'F', TKL_SPAMF      | TKL_GLOBAL, "Spamfilter",           1, 1, 0, "SPAM" },
	{ "qline",                'Q', TKL_NAME       | TKL_GLOBAL, "Q-Line",               1, 1, 0, "Q" },
	{ "except",               'E', TKL_EXCEPTION  | TKL_GLOBAL, "Exception",            1, 0, 0, "E" },
	{ "shun",                 's', TKL_SHUN       | TKL_GLOBAL, "Shun",                 1, 1, 0, "H" },
	{ "local-qline",          'q', TKL_NAME,                    "Local Q-Line",         1, 0, 0, "Q" },
	{ "local-exception",      'e', TKL_EXCEPTION,               "Local Exception",      1, 0, 0, "E" },
	{ "local-spamfilter",     'f', TKL_SPAMF,                   "Local Spamfilter",     1, 0, 0, "SPAM" },
	{ "blacklist",            'b', TKL_BLACKLIST,               "Blacklist",            0, 1, 1, NULL },
	{ "connect-flood",        'c', TKL_CONNECT_FLOOD,           "Connect flood",        0, 1, 0, NULL },
	{ "maxperip",             'm', TKL_MAXPERIP,                "Max-per-IP",           0, 1, 0, NULL },
	{ "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD,    "Handshake data flood", 0, 1, 1, NULL },
	{ "antirandom",           'r', TKL_ANTIRANDOM,              "Antirandom",           0, 1, 0, NULL },
	{ "antimixedutf8",        '8', TKL_ANTIMIXEDUTF8,           "Antimixedutf8",        0, 1, 0, NULL },
	{ "ban-version",          'v', TKL_BAN_VERSION,             "Ban Version",          0, 1, 0, NULL },
	{ NULL,                   '\0', 0,                          NULL,                   0, 0, 0, NULL },
};
/* clang-format on */
#define ALL_VALID_EXCEPTION_TYPES "kline, gline, zline, gzline, spamfilter, shun, qline, blacklist, connect-flood, handshake-data-flood, antirandom, antimixedutf8, ban-version"

/* Global variables for this module */

int max_stats_matches = 1000;
int mtag_spamfilters_present = 0; /**< Are any spamfilters with type SPAMF_MTAG present? */
int raw_spamfilters_present = 0; /**< Are any spamfilters with type SPAMF_RAW present? */
int confusables_spamfilters_present = 0; /**< Are any spamfilters with input-conversion confusables present? */
long previous_spamfilter_utf8 = 0;
static int firstboot = 0;

/* Config-file (and central) TKLs are freed and re-created on every /REHASH (and
 * central spamfilters on every feed refresh) since they live in the config, not
 * in the tkldb, which would otherwise reset their hit counters. To keep the
 * counters we stash them here right before the old entry is freed, keyed by a
 * stable per-TKL identity (see tkl_hits_key), and copy them back onto the freshly
 * re-added entry with the same key. The list is carried across the rehash
 * module-unload by Save/LoadPersistentPointer (same trick connthrottle uses).
 * This is rehash/refresh-only; on a full restart the counters still reset.
 * Covers config server bans, name bans (qlines) and spamfilters; not exceptions.
 */
typedef struct ConfigTKLHits ConfigTKLHits;
struct ConfigTKLHits {
	ConfigTKLHits *prev, *next;
	char key[256];
	long long hits;
	time_t lasthit;
	long long hits_except;    /* spamfilter only */
	time_t lasthit_except;    /* spamfilter only */
};
ConfigTKLHits *config_tkl_hits = NULL;

/* s2s-tkl/<field>: per-TKL fields carried over S2S as @s2s-tkl/<field>=<value> on the
 * TKL command (modeled on s2s-md/). Old servers ignore unknown tags. To add a field,
 * add a row here plus its serialize/unserialize pair (defined further down).
 */
typedef struct {
	const char *name;
	const char *(*serialize)(TKL *tkl);              /**< value to send, or NULL to omit the tag */
	void (*unserialize)(TKL *tkl, const char *value);
} TKLS2SField;
static const char *tkl_s2s_get_id(TKL *tkl);
static void tkl_s2s_set_id(TKL *tkl, const char *value);
static const char *tkl_s2s_get_spamfilter_id(TKL *tkl);
static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value);
static const TKLS2SField tkl_s2s_fields[] = {
	{ "id",     tkl_s2s_get_id,     tkl_s2s_set_id },
	{ "spamfilter_id", tkl_s2s_get_spamfilter_id, tkl_s2s_set_spamfilter_id },
	{ NULL,     NULL,               NULL },
};

MOD_TEST()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkl_config_test_spamfilter);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkl_config_test_ban);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkl_config_test_except);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkl_config_test_set);
	EfunctionAdd(modinfo->handle, EFUNC_TKL_HASH, _tkl_hash);
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
	EfunctionAdd(modinfo->handle, EFUNC_TKL_TYPETOCHAR, TO_INTFUNC(_tkl_typetochar));
	EfunctionAdd(modinfo->handle, EFUNC_TKL_CHARTOTYPE, TO_INTFUNC(_tkl_chartotype));
	EfunctionAdd(modinfo->handle, EFUNC_TKL_CONFIGTYPETOCHAR, TO_INTFUNC(_tkl_configtypetochar));
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
	EfunctionAddConstString(modinfo->handle, EFUNC_TKL_TYPE_STRING, _tkl_type_string);
	EfunctionAddConstString(modinfo->handle, EFUNC_TKL_TYPE_CONFIG_STRING, _tkl_type_config_string);
	EfunctionAddPVoid(modinfo->handle, EFUNC_TKL_ADD_SERVERBAN, TO_PVOIDFUNC(_tkl_add_serverban));
	EfunctionAddPVoid(modinfo->handle, EFUNC_TKL_ADD_BANEXCEPTION, TO_PVOIDFUNC(_tkl_add_banexception));
	EfunctionAddPVoid(modinfo->handle, EFUNC_TKL_ADD_NAMEBAN, TO_PVOIDFUNC(_tkl_add_nameban));
	EfunctionAddPVoid(modinfo->handle, EFUNC_TKL_ADD_SPAMFILTER, TO_PVOIDFUNC(_tkl_add_spamfilter));
	EfunctionAddVoid(modinfo->handle, EFUNC_TKL_DEL_LINE, _tkl_del_line);
	EfunctionAddVoid(modinfo->handle, EFUNC_FREE_TKL, _free_tkl);
	EfunctionAddVoid(modinfo->handle, EFUNC_TKL_CHECK_LOCAL_REMOVE_SHUN, _tkl_check_local_remove_shun);
	EfunctionAdd(modinfo->handle, EFUNC_FIND_TKLINE_MATCH, _find_tkline_match);
	EfunctionAddVoid(modinfo->handle, EFUNC_TKL_HIT, _tkl_hit);
	EfunctionAddVoid(modinfo->handle, EFUNC_REMOVE_CONFIG_TKLS, _remove_config_tkls);
	EfunctionAddVoid(modinfo->handle, EFUNC_CONFIG_TKL_HITS_RESTORE, _config_tkl_hits_restore);
	EfunctionAdd(modinfo->handle, EFUNC_FIND_SHUN, _find_shun);
	EfunctionAdd(modinfo->handle, EFUNC_FIND_SPAMFILTER_USER, _find_spamfilter_user);
	EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_QLINE, TO_PVOIDFUNC(_find_qline));
	EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_TKLINE_MATCH_ZAP, TO_PVOIDFUNC(_find_tkline_match_zap));
	EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_TKL_SERVERBAN, TO_PVOIDFUNC(_find_tkl_serverban));
	EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_TKL_BANEXCEPTION, TO_PVOIDFUNC(_find_tkl_banexception));
	EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_TKL_NAMEBAN, TO_PVOIDFUNC(_find_tkl_nameban));
	EfunctionAddPVoid(modinfo->handle, EFUNC_FIND_TKL_SPAMFILTER, TO_PVOIDFUNC(_find_tkl_spamfilter));
	EfunctionAddVoid(modinfo->handle, EFUNC_TKL_STATS, _tkl_stats);
	EfunctionAddVoid(modinfo->handle, EFUNC_TKL_SYNCH, _tkl_sync);
	EfunctionAddVoid(modinfo->handle, EFUNC_CMD_TKL, _cmd_tkl);
	EfunctionAdd(modinfo->handle, EFUNC_TAKE_ACTION, _take_action);
	EfunctionAdd(modinfo->handle, EFUNC_MATCH_SPAMFILTER, _match_spamfilter);
	EfunctionAdd(modinfo->handle, EFUNC_MATCH_SPAMFILTER_MTAGS, _match_spamfilter_mtags);
	EfunctionAddVoid(modinfo->handle, EFUNC_RUN_DEFERRED_RULE_ONLY_SPAMFILTERS, _run_deferred_rule_only_spamfilters);
	EfunctionAdd(modinfo->handle, EFUNC_JOIN_VIRUSCHAN, _join_viruschan);
	EfunctionAddVoid(modinfo->handle, EFUNC_SPAMFILTER_BUILD_USER_STRING, _spamfilter_build_user_string);
	EfunctionAdd(modinfo->handle, EFUNC_MATCH_USER, _match_user);
	EfunctionAdd(modinfo->handle, EFUNC_TKL_IP_HASH, _tkl_ip_hash);
	EfunctionAdd(modinfo->handle, EFUNC_TKL_IP_HASH_TYPE, _tkl_ip_hash_type);
	EfunctionAddVoid(modinfo->handle, EFUNC_SENDNOTICE_TKL_ADD, _sendnotice_tkl_add);
	EfunctionAddVoid(modinfo->handle, EFUNC_SENDNOTICE_TKL_DEL, _sendnotice_tkl_del);
	EfunctionAdd(modinfo->handle, EFUNC_FIND_TKL_EXCEPTION, _find_tkl_exception);
	EfunctionAddString(modinfo->handle, EFUNC_TKL_UHOST, _tkl_uhost);
	EfunctionAdd(modinfo->handle, EFUNC_UNREAL_MATCH_IPLIST, _unreal_match_iplist);
	EfunctionAdd(modinfo->handle, EFUNC_SERVER_BAN_PARSE_MASK, TO_INTFUNC(_server_ban_parse_mask));
	EfunctionAdd(modinfo->handle, EFUNC_SERVER_BAN_EXCEPTION_PARSE_MASK, TO_INTFUNC(_server_ban_exception_parse_mask));
	EfunctionAddVoid(modinfo->handle, EFUNC_TKL_ADDED, _tkl_added);
	return MOD_SUCCESS;
}

MOD_INIT()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);
	if (loop.booted == 0)
		firstboot = 1;
	LoadPersistentLong(modinfo, previous_spamfilter_utf8);
	LoadPersistentPointer(modinfo, config_tkl_hits, config_tkl_hits_free);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_spamfilter);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_ban);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_except);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkl_config_run_set);
	HookAdd(modinfo->handle, HOOKTYPE_IP_CHANGE, 2000000000, tkl_ip_change);
	HookAdd(modinfo->handle, HOOKTYPE_ACCEPT, -1000, tkl_accept);
	HookAdd(modinfo->handle, HOOKTYPE_PRE_COMMAND, -1000, spamfilter_pre_command);
	CommandAdd(modinfo->handle, "GLINE", cmd_gline, 3, CMD_OPER);
	CommandAdd(modinfo->handle, "SHUN", cmd_shun, 3, CMD_OPER);
	CommandAdd(modinfo->handle, "TEMPSHUN", cmd_tempshun, 2, CMD_OPER);
	CommandAdd(modinfo->handle, "ZLINE", cmd_zline, 3, CMD_OPER);
	CommandAdd(modinfo->handle, "KLINE", cmd_kline, 3, CMD_OPER);
	CommandAdd(modinfo->handle, "GZLINE", cmd_gzline, 3, CMD_OPER);
	CommandAdd(modinfo->handle, "SPAMFILTER", cmd_spamfilter, 7, CMD_OPER);
	CommandAdd(modinfo->handle, "ELINE", cmd_eline, 4, CMD_OPER);
	CommandAdd(modinfo->handle, "TKL", _cmd_tkl, MAXPARA, CMD_OPER|CMD_SERVER);
	CommandAdd(modinfo->handle, "SPAMINFO", cmd_spaminfo, 1, CMD_OPER|CMD_TEXTANALYSIS);
	add_default_exempts();
	return MOD_SUCCESS;
}

MOD_LOAD()
{
	check_special_spamfilters_present();
	check_set_spamfilter_utf8_setting_changed();
	_config_tkl_hits_restore();
	EventAdd(modinfo->handle, "tklexpire", tkl_check_expire, NULL, 5000, 0);
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	/* Free our config TKLs here (rather than from config_rehash) so the same pass
	 * can snapshot the spamfilter hit counters. Call the local impl: the snapshot
	 * store is a per-instance global, and we must write to ours (saved just below),
	 * not the new instance the efunc pointer now points to.
	 */
	_remove_config_tkls(TKL_FLAG_CONFIG);
	SavePersistentLong(modinfo, previous_spamfilter_utf8);
	SavePersistentPointer(modinfo, config_tkl_hits);
	return MOD_SUCCESS;
}

// Simple for now; will be improved later
int input_conversion_strtoval(const char *name)
{
	if (!strcmp(name, "none"))
		return 0;
	if (!strcmp(name, "strip-control-codes"))
		return INPUT_CONVERSION_STRIP_CONTROL_CODES;
	if (!strcmp(name, "deconfused") || !strcmp(name, "deconfuse") || !strcmp(name, "confusables"))
		return INPUT_CONVERSION_CONFUSABLES;
	return -1;
}

/** Test a spamfilter { } block in the configuration file */
int tkl_config_test_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	ConfigEntry *cep, *cepp;
	int errors = 0;
	char *match = NULL, *reason = NULL;
	char has_target = 0, has_id = 0, has_match = 0, has_rule = 0, has_action = 0, has_reason = 0;
	char has_bantime = 0, has_match_type = 0, has_input_conversion = 0;
	char has_show_message_content_on_hit = 0;
	char central_spamfilter = 0;
	int match_type = 0;

	/* We are only interested in spamfilter { } blocks */
	if ((type != CONFIG_MAIN) || strcmp(ce->name, "spamfilter"))
		return 0;

	if (!strcmp(cf->filename, "central_spamfilter.conf"))
		central_spamfilter = 1;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "id"))
		{
			if (has_id)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::id");
				continue;
			}
			has_id = 1;
			if (!valid_spamfilter_id(cep->value))
			{
				config_error("%s:%i: spamfilter::id invalid: maximum size (%d chars) exceeded "
				             "or forbidden characters encountered: only A-Z, 0-9 and _ are permitted.",
				             cep->file->filename, cep->line_number, TKLIDLEN-1);
				errors++;
			}
		} else
		if (!strcmp(cep->name, "target"))
		{
			if (has_target)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::target");
				continue;
			}
			has_target = 1;
			if (cep->value)
			{
				if (!spamfilter_getconftargets(cep->value))
				{
					config_error("%s:%i: unknown spamfiler target type '%s'",
						cep->file->filename, cep->line_number, cep->value);
					errors++;
				}
			}
			else if (cep->items)
			{
				for (cepp = cep->items; cepp; cepp = cepp->next)
				{
					if (!spamfilter_getconftargets(cepp->name))
					{
						config_error("%s:%i: unknown spamfiler target type '%s'",
							cepp->file->filename,
							cepp->line_number, cepp->name);
						errors++;
					}
				}
			}
			else
			{
				config_error_empty(cep->file->filename,
					cep->line_number, "spamfilter", cep->name);
				errors++;
			}
			continue;
		} else
		if (!strcmp(cep->name, "input-conversion"))
		{
			if (has_input_conversion)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::input-conversion");
				continue;
			}
			has_input_conversion = 1;
			if (cep->value)
			{
				if (input_conversion_strtoval(cep->value)<0)
				{
					config_error("%s:%i: unknown input-conversion method '%s'",
						cep->file->filename, cep->line_number, cep->value);
					errors++;
				}
			}
			else if (cep->items)
			{
				for (cepp = cep->items; cepp; cepp = cepp->next)
				{
					if (input_conversion_strtoval(cepp->name)<0)
					{
						config_error("%s:%i: unknown input-conversion method '%s'",
							cepp->file->filename,
							cepp->line_number, cepp->name);
						errors++;
					}
				}
			}
			else
			{
				config_error_empty(cep->file->filename,
					cep->line_number, "spamfilter", cep->name);
				errors++;
			}
			continue;
		}
		else if (!strcmp(cep->name, "action"))
		{
			if (has_action)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::action");
				continue;
			}
			has_action = 1;
			errors += test_ban_action_config(cep);
		}
		else if (!strcmp(cep->name, "except"))
		{
			test_match_block(cf, cep, &errors);
		}
		else if (!cep->value)
		{
			config_error_empty(cep->file->filename, cep->line_number,
				"spamfilter", cep->name);
			errors++;
			continue;
		}
		else if (!strcmp(cep->name, "reason"))
		{
			if (has_reason)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::reason");
				continue;
			}
			has_reason = 1;
			reason = cep->value;
		}
		else if (!strcmp(cep->name, "match") || !strcmp(cep->name, "match-string"))
		{
			if (has_match)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::match-string");
				continue;
			}
			has_match = 1;
			match = cep->value;
		}
		else if (!strcmp(cep->name, "rule"))
		{
			int val;
			if (has_rule)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::rule");
				continue;
			}
			has_rule = 1;
			if ((val = crule_test(cep->value)))
			{
				config_error("%s:%i: spamfilter::rule contains an invalid expression: %s",
					cep->file->filename,
					cep->line_number,
					crule_errstring(val));
				errors++;
			}
		}
		else if (!strcmp(cep->name, "ban-time"))
		{
			if (has_bantime)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::ban-time");
				continue;
			}
			has_bantime = 1;
		}
		else if (!strcmp(cep->name, "match-type"))
		{
			if (has_match_type)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::match-type");
				continue;
			}
			if (!strcasecmp(cep->value, "posix"))
			{
				config_error("%s:%i: this spamfilter uses match-type 'posix' which is no longer supported. "
				             "You must switch over to match-type 'regex' instead. "
				             "See https://www.unrealircd.org/docs/FAQ#spamfilter-posix-deprecated",
				             ce->file->filename, ce->line_number);
				errors++;
				*errs = errors;
				return -1; /* return now, otherwise there will be issues */
			}
			match_type = unreal_match_method_strtoval(cep->value);
			if (match_type == 0)
			{
				config_error("%s:%i: spamfilter::match-type: unknown match type '%s', "
				             "should be one of: 'simple', 'regex' or 'posix'",
				             cep->file->filename, cep->line_number,
				             cep->value);
				errors++;
				continue;
			}
			has_match_type = 1;
		}
		else if (!strcmp(cep->name, "show-message-content-on-hit"))
		{
			if (has_show_message_content_on_hit)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "spamfilter::show-message-content-on-hit");
				continue;
			}
			has_show_message_content_on_hit = 1;
			if (!spamfilter_show_message_content_on_hit_strtoval(cep->value))
			{
				config_error("%s:%i: spamfilter::show-message-content-on-hit: unknown value '%s'",
				             cep->file->filename, cep->line_number, cep->value);
				errors++;
			}
		}
		else
		{
			config_error_unknown(cep->file->filename, cep->line_number,
				"spamfilter", cep->name);
			errors++;
			continue;
		}
	}

	if (match && match_type)
	{
		Match *m;
		const char *err;

		m = unreal_create_match(match_type, match, &err);
		if (!m)
		{
			config_error("%s:%i: spamfilter::match contains an invalid regex: %s",
				ce->file->filename,
				ce->line_number,
				err);
			errors++;
		} else
		{
			unreal_delete_match(m);
		}
	}

	if (!has_match && !has_rule)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"spamfilter::match or spamfilter::rule");
		errors++;
	}
	if (!has_match && has_match_type && has_rule)
	{
		config_error("%s:%i: spamfilter::match-type makes no sense if you only have a spamfilter::rule. Remove the match-type.",
		             ce->file->filename, ce->line_number);
		errors++;
	}
	if (!has_target && match)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"spamfilter::target");
		errors++;
	}
	if (!has_action)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"spamfilter::action");
		errors++;
	}
	if (match && reason && (strlen(match) + strlen(reason) > 505))
	{
		config_error("%s:%i: spamfilter block problem: match + reason field are together over 505 bytes, "
		             "please choose a shorter regex or reason",
		             ce->file->filename, ce->line_number);
		errors++;
	}
	if (!has_match_type && match)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"spamfilter::match-type");
		errors++;
	}

	if (match && !strcmp(match, "^LOL! //echo -a \\$\\(\\$decode\\(.+,m\\),[0-9]\\)$"))
	{
		config_warn("*** IMPORTANT ***");
		config_warn("You have old examples in your spamfilter.conf. "
		             "We suggest you to edit this file and replace the examples.");
		config_warn("Please read https://www.unrealircd.org/docs/FAQ#old-spamfilter-conf !!!");
		config_warn("*****************");
	}

	if (central_spamfilter && !has_id)
	{
		config_error("%s:%i: central spamfilter encountered without 'id', rejected.",
		             ce->file->filename, ce->line_number);
		errors++;
	}

	if (central_spamfilter && !has_reason)
	{
		config_error("%s:%i: central spamfilter encountered without 'reason', rejected.",
		             ce->file->filename, ce->line_number);
		errors++;
	}

	if (central_spamfilter && errors)
	{
		ce->bad = 1;
		*errs = 0;
		return 1;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

/** Process a spamfilter { } block in the configuration file */
int tkl_config_run_spamfilter(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry *cep;
	ConfigEntry *cepp;
	char *id = NULL;
	char *match = NULL;
	char *rule = NULL;
	time_t bantime = tempiConf.spamfilter_ban_time;
	char *banreason = tempiConf.spamfilter_ban_reason;
	BanAction *action = NULL;
	int target = 0;
	int input_conversion = INPUT_CONVERSION_DEFAULT;
	int match_type = 0;
	Match *m = NULL;
	int flag = TKL_FLAG_CONFIG;
	SecurityGroup *except = NULL;
	SpamfilterShowMessageContentOnHit show_message_content_on_hit = 0;

	/* We are only interested in spamfilter { } blocks */
	if ((type != CONFIG_MAIN) || strcmp(ce->name, "spamfilter"))
		return 0;

	/* Bit silly hard coded shit... */
	if (!strcmp(cf->filename, "central_spamfilter.conf"))
		flag = TKL_FLAG_CENTRAL_SPAMFILTER;

	/* Ignore previously marked 'bad' spamfilter blocks */
	if (ce->bad)
		return 1;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "id"))
		{
			id = cep->value;
		}
		if (!strcmp(cep->name, "match") || !strcmp(cep->name, "match-string"))
		{
			match = cep->value;
		}
		else if (!strcmp(cep->name, "rule"))
		{
			rule = cep->value;
		}
		else if (!strcmp(cep->name, "target"))
		{
			if (cep->value)
				target = spamfilter_getconftargets(cep->value);
			else
			{
				for (cepp = cep->items; cepp; cepp = cepp->next)
					target |= spamfilter_getconftargets(cepp->name);
			}
		}
		else if (!strcmp(cep->name, "input-conversion"))
		{
			if (cep->value)
				input_conversion = input_conversion_strtoval(cep->value);
			else
			{
				for (cepp = cep->items; cepp; cepp = cepp->next)
				{
					if (!strcmp(cepp->name, "none"))
						input_conversion = 0;
					else
						input_conversion |= input_conversion_strtoval(cepp->name);
				}
			}
		}
		else if (!strcmp(cep->name, "action"))
		{
			parse_ban_action_config(cep, &action);
		}
		else if (!strcmp(cep->name, "reason"))
		{
			banreason = cep->value;
		}
		else if (!strcmp(cep->name, "ban-time"))
		{
			bantime = config_checkval(cep->value, CFG_TIME);
		}
		else if (!strcmp(cep->name, "match-type"))
		{
			match_type = unreal_match_method_strtoval(cep->value);
		}
		else if (!strcmp(cep->name, "except"))
		{
			conf_match_block(cf, cep, &except);
		}
		else if (!strcmp(cep->name, "show-message-content-on-hit"))
		{
			show_message_content_on_hit = spamfilter_show_message_content_on_hit_strtoval(cep->value);
		}
	}

	if (!match && rule)
		match_type = MATCH_NONE;

	/* Limiting stuff for central spamfilters */
	if (IsCentralSpamfilterType(flag))
	{
		if (iConf.central_spamfilter_limit_ban_action)
			lower_ban_action_to_maximum(action, iConf.central_spamfilter_limit_ban_action);
		if (iConf.central_spamfilter_limit_ban_time && (bantime > iConf.central_spamfilter_limit_ban_time))
			bantime = iConf.central_spamfilter_limit_ban_time;
	} else {
		id = NULL;
	}

	if (match)
	{
		const char *err;
		m = unreal_create_match(match_type, match, &err);
		if (!m)
		{
			config_warn("%s:%i: This spamfilter block is ignored because spamfilter::match contained an invalid regex: %s",
				ce->file->filename,
				ce->line_number,
				err);
			/* This is great, now we need to undo everything.. */
			free_security_group(except);
			safe_free_all_ban_actions(action);
			return 1;
		}
	}

	banreason = unreal_encodespace(banreason);
	tkl_add_spamfilter(TKL_SPAMF,
	                   id,
	                   target,
	                   action,
	                   m,
	                   rule,
	                   except,
	                   (flag & TKL_FLAG_CENTRAL_SPAMFILTER ? "-centralspamfilter-" : "-config-"),
	                   0,
	                   TStime(),
	                   bantime,
	                   banreason,
	                   input_conversion,
	                   show_message_content_on_hit,
	                   flag);
	return 1;
}

/** Test a ban { } block in the configuration file */
int tkl_config_test_ban(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	ConfigEntry *cep;
	int errors = 0;
	int test_match = 0;
	char has_mask = 0, has_match = 0, has_reason = 0;

	/* We are only interested in ban { } blocks */
	if (type != CONFIG_BAN)
		return 0;

	if (strcmp(ce->value, "nick") && strcmp(ce->value, "user") &&
	    strcmp(ce->value, "ip"))
	{
		return 0;
	}

	if (!strcmp(ce->value, "user"))
		test_match = 1;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "mask"))
		{
			if (test_match)
			{
				if (cep->value || cep->items)
				{
					has_mask = 1;
					test_match_block(cf, cep, &errors);
				}
			} else {
				if (!cep->value && !(cep->items && cep->items->name))
				{
					config_error("%s:%d: ban %s with invalid or no mask",
						cep->file->filename, cep->line_number,
						ce->value);
					errors++;
				}
				has_mask = 1;
			}
		} else
		if (!strcmp(cep->name, "match"))
		{
			if (test_match)
			{
				if (cep->value || cep->items)
				{
					has_match = 1;
					test_match_block(cf, cep, &errors);
				}
			} else {
				config_error("%s:%d: ban %s only supports simple mask and not match.",
					cep->file->filename, cep->line_number, ce->value);
				errors++;
				has_match = 1;
			}
		} else
		if (!strcmp(cep->name, "soft"))
		{
			if (config_is_blankorempty(cep, "ban::soft"))
				errors++;
			if (strcmp(ce->value, "user"))
			{
				config_error("%s:%d: ban %s::soft can only be used in a ban user { } block",
					cep->file->filename, cep->line_number, ce->value);
				errors++;
			}
		} else
		if (!strcmp(cep->name, "reason"))
		{
			if (has_reason)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "ban::reason");
				continue;
			}
			has_reason = 1;
		}
		else
		{
			config_error("%s:%i: unknown directive ban %s::%s",
				cep->file->filename, cep->line_number,
				ce->value,
				cep->name);
			errors++;
		}
	}

	if (!has_mask && !has_match)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"ban::mask");
		errors++;
	}

	if (has_mask && has_match)
	{
		config_error("%s:%d: You cannot have both ::mask and ::match. "
		             "You should only use ban::match.",
		             ce->file->filename, ce->line_number);
		errors++;
	}

	if (!has_reason)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"ban::reason");
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

/* Helper function for ban ip { } and ban nick { } code so multiple masks are possible there.
 * These are not Mask items, hence special code.
 */
void tkl_config_run_ban_nickip_helper(ConfigFile *cf, ConfigEntry *cep, const char *mask, int tkltype, char *reason)
{
	char *usermask = NULL;
	char *hostmask = NULL;

	if (is_extended_server_ban(mask))
	{
		char mask1buf[512], mask2buf[512];
		char *err = NULL;

		if (!parse_extended_server_ban(mask, NULL, &err, 0, mask1buf, sizeof(mask1buf), mask2buf, sizeof(mask2buf)))
		{
			config_warn("%s:%d: Could not add extended server ban '%s': %s",
				cep->file->filename, cep->line_number, mask, err);
			return;
		}
		safe_strdup(usermask, mask1buf);
		safe_strdup(hostmask, mask2buf);
	} else
	{
		char buf[512];
		char *p;

		strlcpy(buf, mask, sizeof(buf));
		p = strchr(buf, '@');
		if (p)
		{
			*p++ = '\0';
			safe_strdup(usermask, buf);
			safe_strdup(hostmask, p);
		} else {
			safe_strdup(hostmask, mask);
		}
	}

	/* Placeholder when usermask is empty */
	if (!usermask)
		safe_strdup(usermask, "*");

	if (TKLIsNameBanType(tkltype))
		tkl_add_nameban(tkltype, hostmask, 0, reason, "-config-", 0, TStime(), TKL_FLAG_CONFIG);
	else if (TKLIsServerBanType(tkltype))
		tkl_add_serverban(tkltype, usermask, hostmask, NULL, reason, "-config-", 0, TStime(), 0, TKL_FLAG_CONFIG);

	safe_free(usermask);
	safe_free(hostmask);
}

/* ban nick { } and ban ip { } */
int tkl_config_run_ban_nickip(ConfigFile *cf, ConfigEntry *ce, int configtype)
{
	ConfigEntry *cep, *cepp;
	char *reason = NULL;
	int tkltype;

	for (cep = ce->items; cep; cep = cep->next)
		if (!strcmp(cep->name, "reason"))
			safe_strdup(reason, cep->value);

	if (!reason)
		safe_strdup(reason, "-");

	if (!strcmp(ce->value, "nick"))
		tkltype = TKL_NAME;
	else if (!strcmp(ce->value, "ip"))
		tkltype = TKL_ZAP;
	else
		abort(); /* impossible */

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "mask"))
		{
			if (cep->value)
			{
				tkl_config_run_ban_nickip_helper(cf, cep, cep->value, tkltype, reason);
			} else if (cep->items)
			{
				for (cepp = cep->items; cepp; cepp = cepp->next)
					tkl_config_run_ban_nickip_helper(cf, cepp, cepp->name, tkltype, reason);
			}
		}
	}

	safe_free(reason);
	return 1;
}

int tkl_config_run_ban_user(ConfigFile *cf, ConfigEntry *ce, int configtype)
{
	ConfigEntry *cep;
	SecurityGroup *match = NULL;
	char *reason = NULL;
	int tkltype;
	int soft = 0;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
		{
			conf_match_block(cf, cep, &match);
		} else
		if (!strcmp(cep->name, "reason"))
		{
			safe_strdup(reason, cep->value);
		} else
		if (!strcmp(cep->name, "soft"))
		{
			soft = config_checkval(cep->value, CFG_YESNO);
		}
	}

	tkltype = TKL_KILL;
	tkl_add_serverban(tkltype, NULL, NULL, match, reason, "-config-", 0, TStime(), soft, TKL_FLAG_CONFIG);
	safe_free(reason);

	return 1;
}

/** Process a ban { } block in the configuration file - delegate to helpers above */
int tkl_config_run_ban(ConfigFile *cf, ConfigEntry *ce, int configtype)
{
	/* We are only interested in ban { } blocks */
	if (configtype != CONFIG_BAN)
		return 0;

	if (!strcmp(ce->value, "nick") || !strcmp(ce->value, "ip"))
		return tkl_config_run_ban_nickip(cf, ce, configtype);
	else if (!strcmp(ce->value, "user"))
		return tkl_config_run_ban_user(cf, ce, configtype);
	else
		return 0; /* not handled by us */
}

int tkl_config_test_except(ConfigFile *cf, ConfigEntry *ce, int configtype, int *errs)
{
	ConfigEntry *cep, *cepp;
	int errors = 0;
	char has_mask = 0, has_match = 0;

	/* We are only interested in except { } blocks */
	if (configtype != CONFIG_EXCEPT)
		return 0;

	/* These are the types that we handle */
	if (strcmp(ce->value, "ban") && strcmp(ce->value, "throttle") &&
	    strcmp(ce->value, "tkl") && strcmp(ce->value, "blacklist") &&
	    strcmp(ce->value, "spamfilter"))
	{
		return 0;
	}

	if (!strcmp(ce->value, "tkl"))
	{
		config_error("%s:%d: except tkl { } has been renamed to except ban { }",
			ce->file->filename, ce->line_number);
		config_status("Please rename your block in the configuration file.");
		*errs = 1;
		return -1;
	}

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "mask"))
		{
			if (cep->value || cep->items)
			{
				has_mask = 1;
				test_match_block(cf, cep, &errors);
			}
		} else
		if (!strcmp(cep->name, "match"))
		{
			if (cep->value || cep->items)
			{
				has_match = 1;
				test_match_block(cf, cep, &errors);
			}
		} else
		if (!strcmp(cep->name, "type"))
		{
			if (cep->items)
			{
				/* type { x; y; z; }; */
				for (cepp = cep->items; cepp; cepp = cepp->next)
					if (!tkl_banexception_configname_to_chars(cepp->name))
					{
						config_error("%s:%d: except ban::type '%s' unknown. Must be one of: %s",
							cepp->file->filename, cepp->line_number, cepp->name,
							ALL_VALID_EXCEPTION_TYPES);
						errors++;
					}
			} else
			if (cep->value)
			{
				/* type x; */
				if (!tkl_banexception_configname_to_chars(cep->value))
				{
					config_error("%s:%d: except ban::type '%s' unknown. Must be one of: %s",
						cep->file->filename, cep->line_number, cep->value,
						ALL_VALID_EXCEPTION_TYPES);
					errors++;
				}
			}
		} else {
			config_error_unknown(cep->file->filename,
				cep->line_number, "except", cep->name);
			errors++;
			continue;
		}
	}

	if (!has_mask && !has_match)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"except ban::match");
		errors++;
	}
	if (has_mask && has_match)
	{
		config_error("%s:%d: You cannot have both ::mask and ::match. "
		             "You should only use except::match.",
		             ce->file->filename, ce->line_number);
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

int tkl_config_run_except(ConfigFile *cf, ConfigEntry *ce, int configtype)
{
	ConfigEntry *cep, *cepp;
	SecurityGroup *match = NULL;
	char bantypes[64];

	/* We are only interested in except { } blocks */
	if (configtype != CONFIG_EXCEPT)
		return 0;

	/* These are the types that we handle */
	if (strcmp(ce->value, "ban") && strcmp(ce->value, "throttle") &&
	    strcmp(ce->value, "blacklist") &&
	    strcmp(ce->value, "spamfilter"))
	{
		return 0;
	}

	*bantypes = '\0';

	/* First configure all the types */
	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "type"))
		{
			if (cep->items)
			{
				/* type { x; y; z; }; */
				for (cepp = cep->items; cepp; cepp = cepp->next)
				{
					char *str = tkl_banexception_configname_to_chars(cepp->name);
					strlcat(bantypes, str, sizeof(bantypes));
				}
			} else
			if (cep->value)
			{
				/* type x; */
				char *str = tkl_banexception_configname_to_chars(cep->value);
				strlcat(bantypes, str, sizeof(bantypes));
			}
		} else
		if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
		{
			conf_match_block(cf, cep, &match);
		}
	}

	if (!*bantypes)
	{
		/* Default setting if no 'type' is specified: */
		if (!strcmp(ce->value, "ban"))
			strlcpy(bantypes, "kGzZs", sizeof(bantypes));
		else if (!strcmp(ce->value, "throttle"))
			strlcpy(bantypes, "c", sizeof(bantypes));
		else if (!strcmp(ce->value, "blacklist"))
			strlcpy(bantypes, "b", sizeof(bantypes));
		else if (!strcmp(ce->value, "spamfilter"))
			strlcpy(bantypes, "f", sizeof(bantypes));
		else
			abort(); /* someone can't code */
	}

	tkl_add_banexception(TKL_EXCEPTION, "-", "-", match, "Added in configuration file",
	                     "-config-", 0, TStime(), 0, bantypes, TKL_FLAG_CONFIG);

	return 1;
}

int tkl_config_test_set(ConfigFile *cf, ConfigEntry *ce, int configtype, int *errs)
{
	int errors = 0;

	/* We are only interested in set { } blocks */
	if (configtype != CONFIG_SET)
		return 0;

	if (!strcmp(ce->name, "max-stats-matches"))
	{
		if (!ce->value)
		{
			config_error("%s:%i: set::max-stats-matches: no value specified",
				ce->file->filename, ce->line_number);
			errors++;
		}
		// allow any other value, including 0 and negative.
		*errs = errors;
		return errors ? -1 : 1;
	}
	return 0;
}

int tkl_config_run_set(ConfigFile *cf, ConfigEntry *ce, int configtype)
{
	/* We are only interested in set { } blocks */
	if (configtype != CONFIG_SET)
		return 0;

	if (!strcmp(ce->name, "max-stats-matches"))
	{
		max_stats_matches = atoi(ce->value);
		return 1;
	}

	return 0;
}

/** Recompile all spamfilters due to set::spamfilter::utf8 setting change */
void recompile_spamfilters(void)
{
	TKL *tkl;
	Match *m;
	const char *err;
	int converted = 0;
	int index;

	index = tkl_hash('F');
	for (tkl = tklines[index]; tkl; tkl = tkl->next)
	{
		if (!TKLIsSpamfilter(tkl) || (tkl->ptr.spamfilter->match->type != MATCH_PCRE_REGEX))
			continue;
		m = unreal_create_match(MATCH_PCRE_REGEX, tkl->ptr.spamfilter->match->str, &err);
		if (!m)
		{
			unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_COMPILE_ERROR", NULL,
			           "Spamfilter no longer compiles upon utf8 change, error: $error. "
			           "Spamfilter '$tkl' ($tkl.reason). "
			           "Spamfilter not transformed to/from utf8.",
			           log_data_tkl("tkl", tkl),
			           log_data_string("error", err ? err : "Unknown"));
			continue;
		}

		unreal_delete_match(tkl->ptr.spamfilter->match); /* unset old one */
		tkl->ptr.spamfilter->match = m; /* set new one */
		converted++;
	}
	unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_UTF8_CONVERTED", NULL,
	           "Spamfilter: Recompiled $count spamfilters due to set::spamfilter::utf8 change.",
	           log_data_integer("count", converted));
}

void check_set_spamfilter_utf8_setting_changed(void)
{
	if (firstboot)
	{
		/* First boot, not a rehash */
		previous_spamfilter_utf8 = iConf.spamfilter_utf8;
		return;
	}

	if (previous_spamfilter_utf8 != iConf.spamfilter_utf8)
		recompile_spamfilters();

	previous_spamfilter_utf8 = iConf.spamfilter_utf8;
}

/* === TKL unique IDs ===
 * Every TKL typically has a unique id (tkl->id), such as "G7K2MP9WQX3" for a gline.
 * See the comment below about TKL_GENERATED_ID_LEN for more info.
 * These TKLIDs are communicated in S2S via @s2s-tkl/id mtag, and they
 * also persist via tkldb.
 */

/* The generated TKLID (not to be confused with the maximum allowed length of TKLID):
 * This consists of the prefix + the random/hashed base32 chars. Obviously, the idea
 * is that this will never clash, so we have to look at birthday attack collision rates:
 * - Spamfilter: "SPAM" prefix + 7 chars = 35 bits = ~0.0015% for 1000 spamfilters.
 * - All the rest: 1 letter prefix + 10 chars = 50 bits = ~0.0018% for 200k entries
 * And obviously both the 1000 spamfilters and 200k GLINE/whatever are a rather
 * absurd number of entries, but possible in odd/attack scenarios.
 */
#define TKL_GENERATED_ID_LEN 11

/** The TKL ID prefix string. Eg. "G" for a gline or "SPAM" for a spamfilter. */
static const char *tkl_id_prefix(int type)
{
	int i;

	for (i = 0; tkl_types[i].config_name; i++)
		if ((tkl_types[i].type == type) && tkl_types[i].id_prefix)
			return tkl_types[i].id_prefix;
#ifdef DEBUGMODE
	abort(); /* this should never happen */
#endif
	return "?";
}

/** Find a TKL by its exact id (case-insensitive). Returns NULL on no match or empty id. */
static TKL *find_tkl_by_id(const char *id)
{
	TKL *tkl;
	int index, index2;

	if (BadPtr(id))
		return NULL;

	for (index = 0; index < TKLIPHASHLEN1; index++)
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
				if (!strcasecmp(tkl->id, id))
					return tkl;

	for (index = 0; index < TKLISTLEN; index++)
		for (tkl = tklines[index]; tkl; tkl = tkl->next)
			if (!strcasecmp(tkl->id, id))
				return tkl;

	return NULL;
}

/** Assign a fresh, locally-unique random id to 'tkl' (overwrites tkl->id).
 * Called when this server is the origin of the entry.
 */
static void tkl_generate_id(TKL *tkl)
{
	/* Crockford base32 (no I/L/O/U) so the id survives being read aloud */
	static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
	const char *prefix = tkl_id_prefix(tkl->type);
	int prefixlen = strlen(prefix);
	int nrandom = TKL_GENERATED_ID_LEN - prefixlen;
	TKL *other;
	int attempt, i;

	for (attempt = 0; attempt < 16; attempt++)
	{
		strlcpy(tkl->id, prefix, sizeof(tkl->id));
		for (i = 0; i < nrandom; i++)
			tkl->id[prefixlen + i] = b32[getrandom8() % 32];
		tkl->id[prefixlen + nrandom] = '\0';
		other = find_tkl_by_id(tkl->id);
		if (!other || (other == tkl))
			return; /* unique (or only matches ourselves, if already linked) */
	}
	/* 16 collisions in a row is astronomically unlikely; keep the last candidate. */
}

/* s2s-tkl/xxx message-tag field handlers */

static const char *tkl_s2s_get_id(TKL *tkl)
{
	return tkl->id[0] ? tkl->id : NULL;
}

static void tkl_s2s_set_id(TKL *tkl, const char *value)
{
	if (!BadPtr(value) && (strlen(value) < sizeof(tkl->id)))
		strlcpy(tkl->id, value, sizeof(tkl->id));
}

static const char *tkl_s2s_get_spamfilter_id(TKL *tkl)
{
	return tkl->spamfilter_id[0] ? tkl->spamfilter_id : NULL;
}

static void tkl_s2s_set_spamfilter_id(TKL *tkl, const char *value)
{
	if (!BadPtr(value) && (strlen(value) < sizeof(tkl->spamfilter_id)))
		strlcpy(tkl->spamfilter_id, value, sizeof(tkl->spamfilter_id));
}

/** Append s2s-tkl/<field> message tags describing 'tkl' onto '*mtags' (for sending over S2S). */
static void tkl_add_s2s_mtags(TKL *tkl, MessageTag **mtags)
{
	const TKLS2SField *f;
	const char *value;
	MessageTag *m;
	char name[64];

	for (f = tkl_s2s_fields; f->name; f++)
	{
		value = f->serialize(tkl);
		if (!value)
			continue;
		snprintf(name, sizeof(name), "s2s-tkl/%s", f->name);
		m = safe_alloc(sizeof(MessageTag));
		safe_strdup(m->name, name);
		safe_strdup(m->value, value);
		AddListItem(m, *mtags);
	}
}

/** Build a one-element s2s-tkl/spamfilter_id message tag from 'spamfilter_id', or NULL if it is empty.
 * Lets a locally-created server ban carry its spamfilter id through the same path as a remote one.
 */
static MessageTag *tkl_spamfilter_id_mtag(const char *spamfilter_id)
{
	MessageTag *m;

	if (BadPtr(spamfilter_id))
		return NULL;
	m = safe_alloc(sizeof(MessageTag));
	safe_strdup(m->name, "s2s-tkl/spamfilter_id");
	safe_strdup(m->value, spamfilter_id);
	return m;
}

/** Read s2s-tkl/<field> message tags from 'mtags' into 'tkl' (on receiving an S2S TKL command). */
static void tkl_extract_s2s_mtags(TKL *tkl, MessageTag *mtags)
{
	MessageTag *m;
	const TKLS2SField *f;

	for (m = mtags; m; m = m->next)
	{
		if (strncmp(m->name, "s2s-tkl/", 8))
			continue;
		for (f = tkl_s2s_fields; f->name; f++)
		{
			if (!strcmp(f->name, m->name + 8))
			{
				f->unserialize(tkl, m->value);
				break;
			}
		}
	}
}

/** Return the value of the s2s-tkl/<field> message tag in 'mtags', or NULL if absent.
 * Used by the merge logic to peek at an incoming field without overwriting the existing entry.
 */
static const char *tkl_s2s_mtag_value(MessageTag *mtags, const char *field)
{
	MessageTag *m;
	char name[64];

	snprintf(name, sizeof(name), "s2s-tkl/%s", field);
	for (m = mtags; m; m = m->next)
		if (!strcmp(m->name, name))
			return m->value;
	return NULL;
}

/** A fallback spamfilter id (not the normal randomly generated ID).
 * This ID is a hash based on (target, action, match method+string). So when those
 * properties are the same, it will create the same ID. Used for:
 *  - Config spamfilter or Central Spamfilter without an explicit id.
 *    For these it behaves very much like a regular tkl->id, is shown in STATS etc.
 *    These entries never propagate servers, as they are local, but it is still nice
 *    for the ID to be unique on each server if you use the exact same spamfilter
 *    (think remote includes or central spamfilter).
 *  - Global spamfilter with a blank id (eg from an older server).
 *    In this case it is ONLY used for '/SPAMFILTER del' and not in regular STATS
 *    or used as a true id.
 * And to be clear: for both cases they never appear in S2S and not on disk either
 * (it is computed, each time).
 */
static const char *spamfilter_fallback_id(TKL *tkl)
{
	static const char b32[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
	/* Fixed, non-secret key: the fallback id is opers-only and not security-sensitive, the
	 * key just makes the hash deterministic and identical across servers. */
	static const char key[16] = "UnrealIRCd rocks";
	static char buf[16];
	const char *prefix;
	int prefixlen;
	char content[8192];
	char actbuf[2];
	uint64_t h;
	int i;

	actbuf[0] = banact_valtochar(tkl->ptr.spamfilter->action->action);
	actbuf[1] = '\0';
	snprintf(content, sizeof(content), "%s\t%s\t%s\t%s",
	         spamfilter_target_inttostring(tkl->ptr.spamfilter->target),
	         actbuf,
	         unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type),
	         tkl->ptr.spamfilter->match->str);

	h = siphash(content, key);

	prefix = tkl_id_prefix(tkl->type);
	prefixlen = strlen(prefix);
	strlcpy(buf, prefix, sizeof(buf));
	for (i = 0; i < TKL_GENERATED_ID_LEN - prefixlen; i++)
	{
		buf[prefixlen + i] = b32[h & 31];
		h >>= 5;
	}
	buf[prefixlen + i] = '\0';
	return buf;
}

/** Called when a TKL is enforced against a client: bump its hit counter and
 * remember when. Call this while 'client' is still valid (before the client is
 * exited), so it stays safe if we later add a hook here.
 */
void _tkl_hit(Client *client, TKL *tkl)
{
	tkl->hits++;
	tkl->lasthit = TStime();
}

/* Warn opers when a spamfilter regex could not finish (eg. it hit the
 * PCRE2 match or depth limit). The match is treated as no-match, so we
 * only warn and do not remove the spamfilter.
 */
static void spamfilter_regex_error(TKL *tkl, const char *regex_error)
{
	if (tkl->type & TKL_GLOBAL)
	{
		unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL,
		           "[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? "
		           "To delete, use: /SPAMFILTER del $tkl.id",
		           log_data_string("regex_error", regex_error),
		           log_data_tkl("tkl", tkl));
	} else {
		unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_REGEX_ERROR", NULL,
		           "[Spamfilter] Regex aborted ($regex_error) for '$tkl'. Possibly too complex regex? "
		           "To remove it, edit your config file",
		           log_data_string("regex_error", regex_error),
		           log_data_tkl("tkl", tkl));
	}
}

int tkl_ip_change(Client *client, const char *oldip)
{
	TKL *tkl;
	if ((tkl = find_tkline_match_zap(client)))
	{
		tkl_hit(client, tkl);
		banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT);
	}
	return 0;
}

int tkl_accept(Client *client)
{
	TKL *tkl;
	if ((tkl = find_tkline_match_zap(client)))
	{
		tkl_hit(client, tkl);
		banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, NO_EXIT_CLIENT);
		return 2; // TODO: HOOK_DENY_ALWAYS;
	}
	return 0;
}

/** GLINE - Global kline.
** Syntax: /gline [+|-]u@h mask time :reason
**
** parv[1] = [+|-]u@h mask
** parv[2] = for how long
** parv[3] = reason
*/
CMD_FUNC(cmd_gline)
{
	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:gline",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (parc == 1)
	{
		const char *parv[3];
		parv[0] = NULL;
		parv[1] = "gline";
		parv[2] = NULL;
		do_cmd(client, recv_mtags, "STATS", 2, parv);
	} else
	{
		cmd_tkl_line(client, parc, parv, "G");
	}
}

/** GZLINE - Global zline.
 */
CMD_FUNC(cmd_gzline)
{
	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:zline:global",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (parc == 1)
	{
		const char *parv[3];
		parv[0] = NULL;
		parv[1] = "gline"; /* (there's no /STATS gzline, it's included in /STATS gline output) */
		parv[2] = NULL;
		do_cmd(client, recv_mtags, "STATS", 2, parv);
	} else {
		cmd_tkl_line(client, parc, parv, "Z");
	}
}

/** SHUN - Shun a user so it can no longer execute any meaningful commands.
 */
CMD_FUNC(cmd_shun)
{
	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:shun",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (parc == 1)
	{
		const char *parv[3];
		parv[0] = NULL;
		parv[1] = "shun";
		parv[2] = NULL;
		do_cmd(client, recv_mtags, "STATS", 2, parv);
	} else {
		cmd_tkl_line(client, parc, parv, "s");
	}
}

/** TEMPSHUN - Temporarily shun a user so it can no longer execute
 *  any meaningful commands - until the user disconnects (session only).
 */
CMD_FUNC(cmd_tempshun)
{
	Client *target;
	const char *comment = ((parc > 2) && !BadPtr(parv[2])) ? parv[2] : "no reason";
	const char *name;
	int remove = 0;

	if (MyUser(client) && (!ValidatePermissionsForPath("server-ban:shun:temporary",client,NULL,NULL,NULL)))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}
	if ((parc < 2) || BadPtr(parv[1]))
	{
		sendnumeric(client, ERR_NEEDMOREPARAMS, "TEMPSHUN");
		return;
	}
	if (parv[1][0] == '+')
		name = parv[1]+1;
	else if (parv[1][0] == '-')
	{
		name = parv[1]+1;
		remove = 1;
	} else
		name = parv[1];

	target = find_user(name, NULL);
	if (!target)
	{
		sendnumeric(client, ERR_NOSUCHNICK, name);
		return;
	}
	if (!MyUser(target))
	{
		sendto_one(target, NULL, ":%s TEMPSHUN %c%s :%s",
		           client->id, remove ? '-' : '+', target->id, comment);
	} else {
		char buf[1024];
		if (!remove)
		{
			if (IsShunned(target))
			{
				sendnotice(client, "User '%s' already shunned", target->name);
			} else if (ValidatePermissionsForPath("immune:server-ban:shun",target,NULL,NULL,NULL))
			{
				sendnotice(client, "You cannot tempshun '%s' because (s)he is an oper with 'immune:server-ban:shun' privilege", target->name);
			} else
			{
				SetShunned(target);
				unreal_log(ULOG_INFO, "tkl", "TKL_ADD_TEMPSHUN", client,
					   "Temporary shun added on user $target.details [reason: $shun_reason] [by: $client]",
					   log_data_string("shun_reason", comment),
					   log_data_client("target", target));
			}
		} else {
			if (!IsShunned(target))
			{
				sendnotice(client, "User '%s' is not shunned", target->name);
			} else {
				ClearShunned(target);
				unreal_log(ULOG_INFO, "tkl", "TKL_DEL_TEMPSHUN", client,
					   "Temporary shun removed from user $target.details [by: $client]",
					   log_data_client("target", target));
			}
		}
	}
}

/** KLINE - Kill line (ban user from local server)
 */
CMD_FUNC(cmd_kline)
{
	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:kline:local:add",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (parc == 1)
	{
		const char *parv[3];
		parv[0] = NULL;
		parv[1] = "kline";
		parv[2] = NULL;
		do_cmd(client, recv_mtags, "STATS", 2, parv);
		return;
	}

	if (!ValidatePermissionsForPath("server-ban:kline:remove",client,NULL,NULL,NULL) && *parv[1] == '-')
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	cmd_tkl_line(client, parc, parv, "k");
}

/** Generate stats for '/GLINE -stats' and such */
void tkl_general_stats(Client *client)
{
	int index, index2;
	TKL *tkl;
	int total = 0;
	int subtotal;

	/* First, hashed entries.. */
	for (index = 0; index < TKLIPHASHLEN1; index++)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			subtotal = 0;
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
				subtotal++;
			if (subtotal > 0)
				sendnotice(client, "Slot %d:%d has %d item(s)", index, index2, subtotal);
			total += subtotal;
		}
	}
	sendnotice(client, "Hashed TKL items: %d item(s)", total);

	/* Now normal entries.. */
	subtotal = 0;
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tkl = tklines[index]; tkl; tkl = tkl->next)
			subtotal++;
	}
	sendnotice(client, "Standard TKL items: %d item(s)", subtotal);
	total += subtotal;
	sendnotice(client, "Grand total TKL items: %d item(s)", total);
}

/** ZLINE - Kill a user as soon as it tries to connect to the server.
 * This happens before any DNS/ident lookups have been done and
 * before any data has been processed (including no TLS handshake, etc.)
 */
CMD_FUNC(cmd_zline)
{
	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:zline:local:add",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (parc == 1)
	{
		const char *parv[3];
		parv[0] = NULL;
		parv[1] = "kline"; /* (there's no /STATS zline, it's included in /STATS kline output) */
		parv[2] = NULL;
		do_cmd(client, recv_mtags, "STATS", 2, parv);
		return;
	}

	if ((parc > 1) && !BadPtr(parv[1]) && !strcasecmp(parv[1], "-stats"))
	{
		/* Print some statistics */
		tkl_general_stats(client);
		return;
	}

	cmd_tkl_line(client, parc, parv, "z");
}

/** Check if a ban is placed with a too broad mask (like '*') */
int ban_too_broad(char *usermask, char *hostmask)
{
	char *p;
	int cnt = 0;

	/* Scary config setting. Hmmm. */
	if (ALLOW_INSANE_BANS)
		return 0;

	/* Allow things like clone@*, dsfsf@*, etc.. */
	if (!strchr(usermask, '*') && !strchr(usermask, '?'))
		return 0;

	/* If it's a CIDR, then check /mask first.. */
	p = strchr(hostmask, '/');
	if (p)
	{
		int cidrlen = atoi(p+1);
		if (strchr(hostmask, ':'))
		{
			if (cidrlen < 48)
				return 1; /* too broad IPv6 CIDR mask */
		} else {
			if (cidrlen < 16)
				return 1; /* too broad IPv4 CIDR mask */
		}
	}

	/* Must at least contain 4 non-wildcard/non-dot characters.
	 * This will deal with non-CIDR and hosts, but any correct
	 * CIDR mask will also pass this test (which is fine).
	 */
	for (p = hostmask; *p; p++)
		if (*p != '*' && *p != '.' && *p != '?' && *p != ':')
			cnt++;

	if (cnt >= 4)
		return 0;

	return 1;
}

/** Ugly function, only meant to be called by cmd_tkl_line() */
static int xline_exists(char *type, char *usermask, char *hostmask)
{
	char *umask = usermask;
	int softban = 0;
	int tpe = tkl_chartotype(type[0]);

	if (*umask == '%')
	{
		umask++;
		softban = 1;
	}

	return find_tkl_serverban(tpe, umask, hostmask, softban) ? 1 : 0;
}

/** Parse an extended server ban such as ~S:aabbccddetc..
 * Used for both syntax checking and to split it into userbuf/hostbuf for TKL protocol.
 * @param mask_in	The input mask (eg: ~S:aabbccddetc)
 * @param client	Client doing the request (used to send errors), can be NULL.
 * @param error		Pointer to set to the error buffer (must be set!)
 * @param skip_checking	Set this to 1 if coming from a remote user/server to skip the .is_ok() check.
 *                      Note that a .conv_param() call can still fail.
 * @param buf1		Buffer to store the extban starter in (eg "~S:") -- can be NULL if you don't need it
 * @param buf1len	Length of buf1
 * @param buf2		Buffer to store the extban remainder in (eg "aabbccddetc") -- can be NULL if you don't need it
 * @param buf2len	Length of buf2
 * @returns 1 if the server ban is acceptable. The ban will then be stored in buf1/buf2 (unless those
 *            were set to NULL by the caller). On failure we return 0 and 'error' is set appropriately.
 */
int parse_extended_server_ban(const char *mask_in, Client *client, char **error, int skip_checking, char *buf1, size_t buf1len, char *buf2, size_t buf2len)
{
	const char *nextbanstr = NULL;
	Extban *extban;
	const char *str;
	char *p;
	BanContext *b = NULL;
	char    mask[USERLEN + NICKLEN + HOSTLEN + 32]; // same as extban_conv_param_nuh_or_extban()
	char newmask[USERLEN + NICKLEN + HOSTLEN + 32];
	char soft_ban = 0;

	*error = NULL;
	if (buf1 && buf2)
		*buf1 = *buf2 = '\0';

	/* Work on a copy */
	if (*mask_in == '%')
	{
		strlcpy(mask, mask_in+1, sizeof(mask));
		soft_ban = 1;
	} else {
		strlcpy(mask, mask_in, sizeof(mask));
	}

	extban = findmod_by_bantype(mask, &nextbanstr);
	if (!extban || !(extban->options & EXTBOPT_TKL))
	{
		*error = "Invalid or unsupported extended server ban requested. Valid types are for example ~a, ~r, ~S.";
		goto fail_parse_extended_server_ban;
	}

	b = safe_alloc(sizeof(BanContext));
	b->client = client;
	b->banstr = nextbanstr;
	b->is_ok_check = EXBCHK_PARAM;
	b->what = MODE_ADD;
	b->ban_type = EXBTYPE_TKL;

	/* Run .is_ok() for the extban. This check is skipped if coming from a remote user/server */
	if (skip_checking == 0)
	{
		if (extban->is_ok && !extban->is_ok(b))
		{
			*error = "Invalid extended server ban";
			goto fail_parse_extended_server_ban;
		}
	}

	b->banstr = nextbanstr;
	str = extban->conv_param(b, extban);
	if (!str)
	{
		*error = "Invalid extended server ban";
		goto fail_parse_extended_server_ban;
	}
	str = prefix_with_extban(str, b, extban, newmask, sizeof(newmask));
	if (str == NULL)
	{
		*error = "Unexpected error (1)";
		goto fail_parse_extended_server_ban;
	}

	p = strchr(newmask, ':');
	if (!p)
	{
		*error = "Unexpected error (2)";
		goto fail_parse_extended_server_ban;
	}

	if (p[1] == ':')
	{
		*error = "For technical reasons you cannot use a double : at the beginning of an extended server ban (eg ~a::xyz)";
		goto fail_parse_extended_server_ban;
	}

	if (!p[1])
	{
		*error = "Empty / too short extended server ban";
		goto fail_parse_extended_server_ban;
	}

	/* Now convert the result into two buffers for TKL protocol usage */
	if (buf1 && buf2)
	{
		char save;
		p++;
		save = *p;
		*p = '\0';
		/* First buffer is eg ~S: or %~S: */
		snprintf(buf1, buf1len, "%s%s",
		         soft_ban ? "%" : "",
		         newmask);
		*p = save;
		strlcpy(buf2, p, buf2len); /* eg 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef */
	}
	safe_free(b);
	return 1;

fail_parse_extended_server_ban:
	safe_free(b);
	return 0;
}

/** Parse a server ban request such as 'blah@blah.com' or '~account:EvilUser'
 * @param client	Client requesting the operation (can be NULL)
 * @param add		Set to 1 for add ban, 0 for remove ban
 * @param type		TKL type (character), see 2nd column of tkl_types[], eg 'G' for gline.
 * @param str		The input string
 * @param usermask_out	Will be set to the TKL usermask
 * @param hostmask_out	Will be set to the TKL hostmask
 * @param soft		Will be set to 1 if it's a softban, otherwise 0
 * @param error		On failure, this will contain the error string
 * @retval 1	Success: usermask_out, hostmask_out and soft are set appropriately.
 * @retval 0	Failed: error is set appropriately
 */
int _server_ban_parse_mask(Client *client, int add, char type, const char *str, char **usermask_out, char **hostmask_out, int *soft, const char **error)
{
	static char maskbuf[BUFSIZE], mask1buf[BUFSIZE], mask2buf[BUFSIZE];
	char *hostmask = NULL, *usermask = NULL;
	char *mask, *p;

	/* Set defaults */
	*usermask_out = *hostmask_out = NULL;
	*soft = 0;

	strlcpy(maskbuf, str, sizeof(maskbuf));
	mask = maskbuf;

	if ((*mask != '~') && strchr(mask, '!'))
	{
		*error = "Cannot have '!' in masks.";
		return 0;
	}

	if (*mask == ':')
	{
		*error = "Mask cannot start with a ':'.";
		return 0;
	}

	if (strchr(mask, ' '))
	{
		*error = "Mask may not contain spaces";
		return 0;
	}

	/* Check if it's a softban */
	if (*mask == '%')
	{
		*soft = 1;
		if (!strchr("kGs", type))
		{
			*error = "The %% prefix (soft ban) is only available for KLINE, GLINE and SHUN. "
			         "For technical reasons this will not work for (G)ZLINE.";
			return 0;
		}
	}

	/* Check if it's an extended server ban */
	if (is_extended_server_ban(mask))
	{
		char *err;

		if (!parse_extended_server_ban(mask, client, &err, 0, mask1buf, sizeof(mask1buf), mask2buf, sizeof(mask2buf)))
		{
			/* If adding, reject it */
			if (add)
			{
				*error = err;
				return 0;
			} else
			{
				/* Always allow any removal attempt... */
				char *p;
				char save;
				p = strchr(mask, ':');
				p++;
				save = *p;
				*p = '\0';
				strlcpy(mask1buf, mask, sizeof(mask1buf));
				*p = save;
				strlcpy(mask2buf, p, sizeof(mask2buf));
				/* fallthrough */
			}
		}
		if (add && ((type == 'z') || (type == 'Z')))
		{
			*error = "(G)Zlines must be placed at *@\037IPMASK\037. "
			         "Extended server bans don't work here because (g)zlines are processed "
			         "BEFORE dns and ident lookups are done and before reading any client data. "
			         "If you want to use extended server bans then use a KLINE/GLINE instead.";
			return 0;
		}
		usermask = mask1buf; /* eg ~S: */
		hostmask = mask2buf; /* eg 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef */
	} else
	{
		/* Check if it's a hostmask and legal .. */
		p = strchr(mask, '@');
		if (p) {
			if ((p == mask) || !p[1])
			{
				*error = "No user@host specified";
				return 0;
			}
			usermask = strtok(mask, "@");
			hostmask = strtok(NULL, "");
			if (BadPtr(hostmask))
			{
				if (BadPtr(usermask))
				{
					*error = "Invalid mask";
					return 0;
				}
				hostmask = usermask;
				usermask = "*";
			}
			if (*hostmask == ':')
			{
				*error = "For technical reasons you cannot start the host with a ':', sorry";
				return 0;
			}
			if (add && ((type == 'z') || (type == 'Z')))
			{
				/* It's a (G)ZLINE, make sure the user isn't specyfing a HOST.
				 * Just a warning in 3.2.3, but an error in 3.2.4.
				 */
				if (strcmp(usermask, "*"))
				{
					*error = "(G)Zlines must be placed at \037*\037@ipmask, not \037user\037@ipmask. This is "
					         "because (g)zlines are processed BEFORE dns and ident lookups are done. "
					         "If you want to use usermasks, use a KLINE/GLINE instead.";
					return 0;
				}
				for (p=hostmask; *p; p++)
				{
					if (isalpha(*p) && !isxdigit(*p))
					{
						*error = "ERROR: (g)zlines must be placed at *@\037IPMASK\037, not *@\037HOSTMASK\037 "
						         "(so for example *@192.168.* is ok, but *@*.aol.com is not). "
						         "This is because (g)zlines are processed BEFORE dns and ident lookups are done. "
						         "If you want to use hostmasks instead of ipmasks, use a KLINE/GLINE instead.";
						return 0;
					}
				}
			}
		}
		else
		{
			/* It's seemingly a nick .. let's see if we can find the user */
			Client *acptr;
			if ((acptr = find_user(mask, NULL)))
			{
				BanActionValue action = BAN_ACT_KLINE; // just a dummy default
				if ((type == 'z') || (type == 'Z'))
					action = BAN_ACT_ZLINE; // to indicate zline (no hostname, no dns, etc)
				ban_target_to_tkl_layer(iConf.manual_ban_target, action, acptr, (const char **)&usermask, (const char **)&hostmask);
			}
			else
			{
				*error = "Nickname not found";
				return 0;
			}
		}
	}

	/* Success! */
	*usermask_out = usermask;
	*hostmask_out = hostmask;
	return 1;
}

/** Intermediate layer between user functions such as KLINE/GLINE
 * and the TKL layer (cmd_tkl).
 * This allows us doing some syntax checking and other helpful
 * things that are the same for many types of *LINES.
 */
void cmd_tkl_line(Client *client, int parc, const char *parv[], char *type)
{
	time_t secs;
	int add = 1, soft;
	time_t i;
	Client *acptr = NULL;
	const char *mask;
	const char *error;
	char mo[64], mo2[64], reasonbuf[512];
	char *p, *usermask, *hostmask;
	const char *tkllayer[10] = {
		me.name,		/*0  server.name */
		NULL,			/*1  +|- */
		NULL,			/*2  G   */
		NULL,			/*3  user */
		NULL,			/*4  host */
		NULL,			/*5  set_by */
		"0",			/*6  expire_at */
		NULL,			/*7  set_at */
		"no reason",	/*8  reason */
		NULL
	};
	struct tm *t;

	if ((parc == 1) || BadPtr(parv[1]))
		return; /* shouldn't happen */

	mask = parv[1];

	if (*mask == '-')
	{
		add = 0;
		mask++;
	}
	else if (*mask == '+')
	{
		add = 1;
		mask++;
	}

	if (!server_ban_parse_mask(client, add, *type, mask, &usermask, &hostmask, &soft, &error))
	{
		sendnotice(client, "[ERROR] %s", error);
		return;
	}

	if (add && ban_too_broad(usermask, hostmask))
	{
		sendnotice(client, "*** [error] Too broad mask");
		return;
	}

	secs = 0;
	if (add && (parc >= 3))
	{
		secs = config_checkval(parv[2], CFG_TIME);
		if (secs < 0)
		{
			sendnotice(client, "*** [error] The time you specified is out of range!");
			return;
		}
	}
	tkllayer[1] = add ? "+" : "-";
	tkllayer[2] = type;
	tkllayer[3] = usermask;
	tkllayer[4] = hostmask;
	tkllayer[5] = make_nick_user_host(client->name, client->user->username, GetHost(client));
	if (add)
	{
		if (secs == 0)
		{
			if (DEFAULT_BANTIME && (parc <= 3))
				ircsnprintf(mo, sizeof(mo), "%lld", (long long)(DEFAULT_BANTIME + TStime()));
			else
				ircsnprintf(mo, sizeof(mo), "%lld", (long long)secs); /* "0" */
		}
		else
			ircsnprintf(mo, sizeof(mo), "%lld", (long long)(secs + TStime()));
		ircsnprintf(mo2, sizeof(mo2), "%lld", (long long)TStime());
		tkllayer[6] = mo;
		tkllayer[7] = mo2;

		if (parc > 2 && !secs && (parv[2][0] != '0'))
		{
			if (parc > 3)
			{
				snprintf(reasonbuf, sizeof(reasonbuf), "%s %s", parv[2], parv[3]);
				tkllayer[8] = reasonbuf;
			} else {
				tkllayer[8] = parv[2];
			}
		}
		else if (parc > 3)
		{
			tkllayer[8] = parv[3];
		}

		/* Blerghhh... */
		i = atol(mo);
		t = gmtime(&i);
		if (!t)
		{
			sendnotice(client, "*** [error] The time you specified is out of range");
			return;
		}

		/* Some stupid checking */
		if (xline_exists(type, usermask, hostmask))
		{
			sendnotice(client, "ERROR: Ban for %s@%s already exists.", usermask, hostmask);
			return;
		}

		/* call the tkl layer .. */
		cmd_tkl(NULL, &me, NULL, 9, tkllayer);
	}
	else
	{
		/* call the tkl layer .. */
		cmd_tkl(NULL, &me, NULL, 6, tkllayer);

	}
}

void eline_syntax(Client *client)
{
	sendnotice(client, " Syntax: /ELINE <user@host> <bantypes> <expiry-time> <reason>");
	sendnotice(client, "     Or: /ELINE <extserverban> <bantypes> <expiry-time> <reason>");
	sendnotice(client, "Valid bantypes are:");
	sendnotice(client, "k: K-Line     G: G-Line");
	sendnotice(client, "z: Z-Line     Z: Global Z-Line");
	sendnotice(client, "Q: Q-Line");
	sendnotice(client, "s: Shun");
	sendnotice(client, "F: Spamfilter");
	sendnotice(client, "b: Blacklist checking");
	sendnotice(client, "c: Connect flood (bypass set::anti-flood::connect-flood))");
	sendnotice(client, "d: Handshake data flood (no ZLINE on too much data before registration)");
	sendnotice(client, "m: Bypass allow::maxperip restriction");
	sendnotice(client, "r: Bypass antirandom module");
	sendnotice(client, "8: Bypass antimixedutf8 module");
	sendnotice(client, "v: Bypass ban version { } blocks");
	sendnotice(client, "Examples:");
	sendnotice(client, "/ELINE *@unrealircd.org kGF 0 This user is exempt");
	sendnotice(client, "/ELINE ~S:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef kGF 0 Trusted user with this certificate fingerprint");
	sendnotice(client, "-");
	sendnotice(client, "To get a list of all current ELINEs, type: /STATS except");
}

/** Check if any of the specified types require the
 * exception to be placed on *@ip rather than
 * user@host or *@host. For eg zlines.
 */
TKLTypeTable *eline_type_requires_ip(const char *bantypes)
{
	int i;

	for (i=0; tkl_types[i].config_name; i++)
		if (tkl_types[i].needip && strchr(bantypes, tkl_types[i].letter))
			return &tkl_types[i];
	return NULL;
}

/** Checks a string to see if it contains invalid ban exception types */
int contains_invalid_server_ban_exception_type(const char *str, char *c)
{
	const char *p;
	for (p = str; *p; p++)
	{
		if (!tkl_banexception_chartotype(*p))
		{
			*c = *p;
			return 1;
		}
	}
	return 0;
}

/** Parse a server ban exception (ELINE) request such as 'blah@blah.com' or '~account:EvilUser'
 * @param client	Client requesting the operation (can be NULL)
 * @param add		Set to 1 for add ban, 0 for remove ban
 * @param bantypes	Ban types to exempt from
 * @param str		The input string
 * @param usermask_out	Will be set to the TKL usermask
 * @param hostmask_out	Will be set to the TKL hostmask
 * @param soft		Will be set to 1 if it's a softban, otherwise 0
 * @param error		On failure, this will contain the error string
 * @retval 1	Success: usermask_out, hostmask_out and soft are set appropriately.
 * @retval 0	Failed: error is set appropriately
 */
int _server_ban_exception_parse_mask(Client *client, int add, const char *bantypes, const char *str, char **usermask_out, char **hostmask_out, int *soft, const char **error)
{
	static char maskbuf[BUFSIZE], mask1buf[BUFSIZE], mask2buf[BUFSIZE], errbuf[BUFSIZE];
	char *hostmask = NULL, *usermask = NULL;
	char *mask, *p;
	TKLTypeTable *t;

	/* Set defaults */
	*usermask_out = *hostmask_out = NULL;
	*soft = 0;

	strlcpy(maskbuf, str, sizeof(maskbuf));
	mask = maskbuf;

	if ((*mask != '~') && strchr(mask, '!'))
	{
		*error = "Cannot have '!' in masks.";
		return 0;
	}

	if (*mask == ':')
	{
		*error = "Mask cannot start with a ':'.";
		return 0;
	}

	if (strchr(mask, ' '))
	{
		*error = "Mask may not contain spaces";
		return 0;
	}

	if (*mask == '%')
	{
		*soft = 1;
		/* do we need more sanity checks here? */
	}

	/* Check if it's an extended server ban */
	if (is_extended_server_ban(mask))
	{
		char *err;

		if (!parse_extended_server_ban(mask, client, &err, 0, mask1buf, sizeof(mask1buf), mask2buf, sizeof(mask2buf)))
		{
			/* If adding, reject it */
			if (add)
			{
				*error = err;
				return 0;
			} else
			{
				/* Always allow any removal attempt... */
				char *p;
				char save;
				p = strchr(mask, ':');
				p++;
				save = *p;
				*p = '\0';
				strlcpy(mask1buf, mask, sizeof(mask1buf));
				*p = save;
				strlcpy(mask2buf, p, sizeof(mask2buf));
				/* fallthrough */
			}
		}
		if (add && (t = eline_type_requires_ip(bantypes)))
		{
			snprintf(errbuf, sizeof(errbuf),
			         "ERROR: Ban exception with type '%c' does not work on extended server bans. "
			         "This is because checking for %s takes places BEFORE "
			         "extended bans can be checked.", t->letter, t->log_name);
			*error = errbuf;
			return 0;
		}
		usermask = mask1buf; /* eg ~S: */
		hostmask = mask2buf; /* eg 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef */
	} else
	{
		/* Check if it's a hostmask and legal .. */
		p = strchr(mask, '@');
		if (p) {
			if ((p == mask) || !p[1])
			{
				*error = "No user@host specified";
				return 0;
			}
			usermask = strtok(mask, "@");
			hostmask = strtok(NULL, "");
			if (BadPtr(hostmask))
			{
				if (BadPtr(usermask))
				{
					*error = "Invalid mask";
					return 0;
				}
				hostmask = usermask;
				usermask = "*";
			}
			if (*hostmask == ':')
			{
				*error = "For technical reasons you cannot start the host with a ':', sorry";
				return 0;
			}
			if (add && ((t = eline_type_requires_ip(bantypes))))
			{
				/* Trying to exempt a user from a (G)ZLINE,
				 * make sure the user isn't specifying a host then.
				 */
				if (strcmp(usermask, "*"))
				{
					snprintf(errbuf, sizeof(errbuf),
					         "Ban exception with type '%c' need to be placed at \037*\037@ipmask, not \037user\037@ipmask. "
					         "This is because checking %s takes places (possibly) BEFORE any dns and ident lookups.",
					         t->letter, t->log_name);
					*error = errbuf;
					return 0;
				}
				for (p=hostmask; *p; p++)
				{
					if (isalpha(*p) && !isxdigit(*p))
					{
						snprintf(errbuf, sizeof(errbuf),
						         "Ban exception with type '%c' needs to be placed at *@\037ipmask\037, not *@\037hostmask\037. "
						         "(so for example *@192.168.* is OK, but *@*.aol.com is not). "
						         "This is because checking %s takes places (possibly) BEFORE any dns and ident lookups.",
						         t->letter, t->log_name);
						*error = errbuf;
						return 0;
					}
				}
			}
		}
		else
		{
			/* It's seemingly a nick .. let's see if we can find the user */
			Client *acptr;
			if ((acptr = find_user(mask, NULL)))
			{
				BanActionValue action = BAN_ACT_KLINE; // just a dummy default
				if (add && eline_type_requires_ip(bantypes))
					action = BAN_ACT_ZLINE; // to indicate zline (no hostname, no dns, etc)
				ban_target_to_tkl_layer(iConf.manual_ban_target, action, acptr, (const char **)&usermask, (const char **)&hostmask);
			}
			else
			{
				*error = "Nickname not found";
				return 0;
			}
		}
	}

	/* Success! */
	*usermask_out = usermask;
	*hostmask_out = hostmask;
	return 1;
}

CMD_FUNC(cmd_eline)
{
	time_t secs = 0;
	int add = 1;
	int soft = 0;
	const char *error = NULL;
	Client *acptr = NULL;
	char *mask = NULL;
	char mo[64], mo2[64];
	char maskbuf[BUFSIZE];
	char mask1buf[BUFSIZE];
	char mask2buf[BUFSIZE];
	const char *p, *bantypes=NULL, *reason=NULL;
	char *usermask, *hostmask;
	const char *tkllayer[11] = {
		me.name,		/*0  server.name */
		NULL,			/*1  +|- */
		NULL,			/*2  E   */
		NULL,			/*3  user */
		NULL,			/*4  host */
		NULL,			/*5  set_by */
		"0",			/*6  expire_at */
		"-",			/*7  set_at */
		"-",			/*8  ban types */
		"-",			/*9  reason */
		NULL
	};
	TKLTypeTable *t;

	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:eline",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	/* For del we need at least:
	 * ELINE -user@host
	 * The 'add' case is checked later.
	 */
	if ((parc < 2) || BadPtr(parv[1]))
	{
		eline_syntax(client);
		return;
	}

	strlcpy(maskbuf, parv[1], sizeof(maskbuf));
	mask = maskbuf;
	if (*mask == '-')
	{
		add = 0;
		mask++;
	}
	else if (*mask == '+')
	{
		add = 1;
		mask++;
	}

	/* For add we need more:
	 * ELINE user@host bantypes expiry :reason
	 */
	if (add)
	{
		if ((parc < 5) || BadPtr(parv[4]))
		{
			eline_syntax(client);
			return;
		}
		bantypes = parv[2];
		reason = parv[4];
	}

	if (!server_ban_exception_parse_mask(client, add, bantypes, mask, &usermask, &hostmask, &soft, &error))
	{
		sendnotice(client, "[ERROR] %s", error);
		return;
	}

	if (add)
	{
		secs = config_checkval(parv[3], CFG_TIME);
		if ((secs <= 0) && (*parv[3] != '0'))
		{
			sendnotice(client, "*** [error] The expiry time you specified is out of range!");
			eline_syntax(client);
			return;
		}
	}

	tkllayer[1] = add ? "+" : "-";
	tkllayer[2] = "E";
	tkllayer[3] = usermask;
	tkllayer[4] = hostmask;
	tkllayer[5] = make_nick_user_host(client->name, client->user->username, GetHost(client));

	if (add)
	{
		char c;
		/* Add ELINE */
		if (secs == 0)
			ircsnprintf(mo, sizeof(mo), "%lld", (long long)secs); /* "0" */
		else
			ircsnprintf(mo, sizeof(mo), "%lld", (long long)(secs + TStime()));
		ircsnprintf(mo2, sizeof(mo2), "%lld", (long long)TStime());
		tkllayer[6] = mo;
		tkllayer[7] = mo2;
		tkllayer[8] = bantypes;
		if (contains_invalid_server_ban_exception_type(bantypes, &c))
		{
			sendnotice(client, "ERROR: bantype '%c' is unrecognized (in '%s'). "
			                   "Note that the bantypes are case sensitive. "
			                   "Type /ELINE to see a list of all possible bantypes.",
			                   c, bantypes);
			return;
		}
		tkllayer[9] = reason;
		/* call the tkl layer .. */
		cmd_tkl(NULL, &me, NULL, 10, tkllayer);
	}
	else
	{
		/* Remove ELINE */
		/* call the tkl layer .. */
		cmd_tkl(NULL, &me, NULL, 10, tkllayer);

	}
}


/** Helper function for cmd_spamfilter, explaining usage. */
void spamfilter_usage(Client *client)
{
	sendnotice(client, "Use: /spamfilter [add|del|remove|+|-] [-simple|-regex] [type] [action] [tkltime] [tklreason] [regex]");
	sendnotice(client, "See '/helpop ?spamfilter' for more information.");
	sendnotice(client, "For an easy way to remove an existing spamfilter, use '/spamfilter del' without additional parameters");
}

/** Helper function for cmd_spamfilter, explaining usage has changed. */
void spamfilter_new_usage(Client *client, const char *parv[])
{
	sendnotice(client, "Unknown match-type '%s'. Must be one of: -regex (new fast PCRE regexes) or "
	                 "-simple (simple text with ? and * wildcards)",
	                 parv[2]);

	if (*parv[2] != '-')
		sendnotice(client, "Using the old 3.2.x /SPAMFILTER syntax? Note the new -regex/-simple field!!");

	spamfilter_usage(client);
}

/** Delete a spamfilter by ID (the ID can be obtained via '/SPAMFILTER del' */
void spamfilter_del_by_id(Client *client, const char *id)
{
	int index;
	TKL *tk;
	int matches = 0;
	TKL *match = NULL;
	char mo[32], mo2[32];
	const char *tkllayer[13] = {
		me.name,	/*  0 server.name */
		NULL,		/*  1 +|- */
		"F",		/*  2 F   */
		NULL,		/*  3 usermask (targets) */
		NULL,		/*  4 hostmask (action) */
		NULL,		/*  5 set_by */
		"0",		/*  6 expire_at */
		"0",		/*  7 set_at */
		"",			/*  8 tkl time */
		"",			/*  9 tkl reason */
		"",			/* 10 match method */
		"",			/* 11 regex */
		NULL
	};

	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tk = tklines[index]; tk; tk = tk->next)
		{
			if ((tk->type & (TKL_GLOBAL|TKL_SPAMF)) != (TKL_GLOBAL|TKL_SPAMF))
				continue;
			/* Match the real id, or for an id-less spamfilter (eg from an old server)
			 * its locally-computed fallback handle.
			 */
			if (tk->id[0] ? !strcmp(tk->id, id) : !strcmp(spamfilter_fallback_id(tk), id))
			{
				match = tk;
				matches++;
			}
		}
	}

	if (matches == 0)
	{
		sendnotice(client, "Sorry, no spamfilter found with that ID. Did you run '/spamfilter del' to get the appropriate id?");
		return;
	}
	if (matches > 1)
	{
		sendnotice(client, "That ID is ambiguous (it matches multiple spamfilters). Please remove the spamfilter by its full definition instead.");
		return;
	}
	tk = match;

	/* Spamfilter found. Now fill the tkllayer */
	tkllayer[1] = "-";
	tkllayer[3] = spamfilter_target_inttostring(tk->ptr.spamfilter->target); /* target(s) */
	mo[0] = banact_valtochar(tk->ptr.spamfilter->action->action);
	mo[1] = '\0';
	tkllayer[4] = mo; /* action */
	tkllayer[5] = make_nick_user_host(client->name, client->user->username, GetHost(client));
	tkllayer[8] = "-";
	tkllayer[9] = "-";
	tkllayer[10] = unreal_match_method_valtostr(tk->ptr.spamfilter->match->type); /* matching type */
	tkllayer[11] = tk->ptr.spamfilter->match->str; /* regex */
	ircsnprintf(mo2, sizeof(mo2), "%lld", (long long)TStime());
	tkllayer[7] = mo2; /* deletion time */

	cmd_tkl(NULL, &me, NULL, 12, tkllayer);
}

/** Spamfilter to fight spam, advertising, worms and other bad things on IRC.
 * See https://www.unrealircd.org/docs/Spamfilter for general documentation.
 *
 * /SPAMFILTER [add|del|remove|+|-] [match-type] [type] [action] [tkltime] [reason] [regex]
 *                   1                    2         3       4        5        6        7
 */
CMD_FUNC(cmd_spamfilter)
{
	int add = 1;
	char mo[32], mo2[32];
	const char *tkllayer[13] = {
		me.name,	/*  0 server.name */
		NULL,		/*  1 +|- */
		"F",		/*  2 F   */
		NULL,		/*  3 usermask (targets) */
		NULL,		/*  4 hostmask (action) */
		NULL,		/*  5 set_by */
		"0",		/*  6 expire_at */
		"0",		/*  7 set_at */
		"",			/*  8 tkl time */
		"",			/*  9 tkl reason */
		"",			/* 10 match method */
		"",			/* 11 regex */
		NULL
	};
	int targets = 0, action = 0;
	char targetbuf[64], actionbuf[2];
	char reason[512];
	int n;
	Match *m;
	int match_type = 0;
	const char *err = NULL;

	if (IsServer(client))
		return;

	if (!ValidatePermissionsForPath("server-ban:spamfilter",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (parc == 1)
	{
		const char *parv[3];
		parv[0] = NULL;
		parv[1] = "spamfilter";
		parv[2] = NULL;
		do_cmd(client, recv_mtags, "STATS", 2, parv);
		return;
	}

	if ((parc <= 3) && !strcmp(parv[1], "del"))
	{
		if (!parv[2])
		{
			/* Show STATS with appropriate SPAMFILTER del command */
			const char *parv[5];
			parv[0] = NULL;
			parv[1] = "spamfilter";
			parv[2] = me.name;
			parv[3] = "del";
			parv[4] = NULL;
			do_cmd(client, recv_mtags, "STATS", 4, parv);
			return;
		}
		spamfilter_del_by_id(client, parv[2]);
		return;
	}

	if ((parc == 7) && (*parv[2] != '-'))
	{
		spamfilter_new_usage(client,parv);
		return;
	}

	if ((parc < 8) || BadPtr(parv[7]))
	{
		spamfilter_usage(client);
		return;
	}

	/* parv[1]: [add|del|+|-]
	 * parv[2]: match-type
	 * parv[3]: type
	 * parv[4]: action
	 * parv[5]: tkl time
	 * parv[6]: tkl reason (or block reason..)
	 * parv[7]: regex
	 */
	if (!strcasecmp(parv[1], "add") || !strcmp(parv[1], "+"))
		add = 1;
	else if (!strcasecmp(parv[1], "del") || !strcmp(parv[1], "-") || !strcasecmp(parv[1], "remove"))
		add = 0;
	else
	{
		sendnotice(client, "1st parameter invalid");
		spamfilter_usage(client);
		return;
	}

	if (add && !strcasecmp(parv[2]+1, "posix"))
	{
		sendnotice(client, "ERROR: Spamfilter type 'posix' is DEPRECATED. You must use type 'regex' instead.");
		sendnotice(client, "See https://www.unrealircd.org/docs/FAQ#spamfilter-posix-deprecated");
		return;
	}

	match_type = unreal_match_method_strtoval(parv[2]+1);
	if (!match_type)
	{
		spamfilter_new_usage(client, parv);
		return;
	}

	targets = spamfilter_gettargets(parv[3], client);
	if (!targets)
	{
		spamfilter_usage(client);
		return;
	}

	strlcpy(targetbuf, spamfilter_target_inttostring(targets), sizeof(targetbuf));

	action = banact_stringtoval(parv[4]);
	if (!action)
	{
		sendnotice(client, "Invalid 'action' field (%s)", parv[4]);
		spamfilter_usage(client);
		return;
	}
	actionbuf[0] = banact_valtochar(action);
	actionbuf[1] = '\0';

	if (add)
	{
		/* now check the regex / match field... */
		m = unreal_create_match(match_type, parv[7], &err);
		if (!m)
		{
			sendnotice(client, "Error in regex '%s': %s", parv[7], err);
			return;
		}
		unreal_delete_match(m);
	}

	tkllayer[1] = add ? "+" : "-";
	tkllayer[3] = targetbuf;
	tkllayer[4] = actionbuf;
	tkllayer[5] = make_nick_user_host(client->name, client->user->username, GetHost(client));

	if (parv[5][0] == '-')
	{
		ircsnprintf(mo, sizeof(mo), "%lld", (long long)SPAMFILTER_BAN_TIME);
		tkllayer[8] = mo;
	}
	else
		tkllayer[8] = parv[5];

	if (parv[6][0] == '-')
		strlcpy(reason, unreal_encodespace(SPAMFILTER_BAN_REASON), sizeof(reason));
	else
		strlcpy(reason, parv[6], sizeof(reason));

	tkllayer[9] = reason;
	tkllayer[10] = parv[2]+1; /* +1 to skip the '-' */
	tkllayer[11] = parv[7];

	/* SPAMFILTER LENGTH CHECK.
	 * We try to limit it here so '/stats f' output shows ok, output of that is:
	 * :servername 229 destname F <target> <action> <num> <num> <num> <reason> <set_by> :<regex>
	 * : ^NICKLEN       ^ NICKLEN                                       ^check   ^check   ^check
	 * And for the other fields (and spacing/etc) we count on max 40 characters.
	 * We also do >500 instead of >510, since that looks cleaner ;).. so actually we count
	 * on 50 characters for the rest... -- Syzop
	 */
	n = strlen(reason) + strlen(parv[7]) + strlen(tkllayer[6]) + (NICKLEN * 2) + 40;
	if ((n > 500) && add)
	{
		sendnotice(client, "Sorry, spamfilter too long. You'll either have to trim down the "
		                 "reason or the regex (exceeded by %d bytes)", n - 500);
		return;
	}

	if (add)
	{
		ircsnprintf(mo2, sizeof(mo2), "%lld", (long long)TStime());
		tkllayer[7] = mo2;
	}

	cmd_tkl(NULL, &me, NULL, 12, tkllayer);
}

/** tkl hash method.
 * @param c   The tkl type character, see tkl_typetochar().
 * @note      The input value 'c' is assumed to be in range a-z or A-Z!
 *            Also, don't blindly change the hashmethod here, some things
 *            depend on 'z' and 'Z' ending up in the same bucket.
 */
int _tkl_hash(unsigned int c)
{
#ifdef DEBUGMODE
	if ((c >= 'a') && (c <= 'z'))
		return c-'a';
	else if ((c >= 'A') && (c <= 'Z'))
		return c-'A';
	else {
		unreal_log(ULOG_ERROR, "bug", "TKL_HASH_INVALID", NULL,
		           "tkl_hash() called with out of range parameter (c = '$tkl_char') !!!",
		           log_data_char("tkl_char", c));
		return 0;
	}
#else
	return (isupper(c) ? c-'A' : c-'a');
#endif
}

/** tkl type to tkl character.
 * NOTE: type is assumed to be valid.
 */
char _tkl_typetochar(int type)
{
	int i;
	for (i=0; tkl_types[i].config_name; i++)
		if ((tkl_types[i].type == type) && tkl_types[i].tkltype)
			return tkl_types[i].letter;
	unreal_log(ULOG_ERROR, "bug", "TKL_TYPETOCHAR_INVALID", NULL,
	           "tkl_typetochar(): unknown type $tkl_type!!!",
	           log_data_integer("tkl_type", type));
	return 0;
}

/** tkl character to tkl type
 * Returns 0 if invalid type.
 */
int _tkl_chartotype(char c)
{
	int i;
	for (i=0; tkl_types[i].config_name; i++)
		if ((tkl_types[i].letter == c) && tkl_types[i].tkltype)
			return tkl_types[i].type;
	return 0;
}

char _tkl_configtypetochar(const char *name)
{
	int i;
	for (i=0; tkl_types[i].config_name; i++)
		if (!strcmp(tkl_types[i].config_name, name))
			return tkl_types[i].letter;
	return 0;
}

int tkl_banexception_chartotype(char c)
{
	int i;
	for (i=0; tkl_types[i].config_name; i++)
		if ((tkl_types[i].letter == c) && tkl_types[i].exceptiontype)
			return tkl_types[i].type;
	return 0;
}

char *tkl_banexception_configname_to_chars(char *name)
{
	static char buf[128];
	int i;

	if (!strcasecmp(name, "all"))
	{
		/* 'all' means everything except qline: */
		char *p = buf;
		for (i=0; tkl_types[i].config_name; i++)
		{
			if (tkl_types[i].exceptiontype && !(tkl_types[i].type & TKL_NAME))
				*p++ = tkl_types[i].letter;
		}
		*p = '\0';
		return buf;
	}

	for (i=0; tkl_types[i].config_name; i++)
	{
		if (!strcasecmp(name, tkl_types[i].config_name) && tkl_types[i].exceptiontype)
		{
			buf[0] = tkl_types[i].letter;
			buf[1] = '\0';
			return buf;
		}
	}
	return NULL;
}

/** Show TKL type as a string (used when adding/removing) */
const char *_tkl_type_string(TKL *tkl)
{
	static char txt[256];
	int i;

	*txt = '\0';

	if (TKLIsServerBan(tkl) && (tkl->ptr.serverban->subtype == TKL_SUBTYPE_SOFT))
		strlcpy(txt, "Soft ", sizeof(txt));

	for (i=0; tkl_types[i].config_name; i++)
	{
		if ((tkl_types[i].type == tkl->type) && tkl_types[i].tkltype)
		{
			strlcat(txt, tkl_types[i].log_name, sizeof(txt));
			return txt;
		}
	}

	strlcpy(txt, "Unknown *-Line", sizeof(txt));
	return txt;
}

/** Short config string, lowercase alnum with possibly hyphens (eg: 'kline') */
const char *_tkl_type_config_string(TKL *tkl)
{
	int i;

	for (i=0; tkl_types[i].config_name; i++)
		if ((tkl_types[i].type == tkl->type) && tkl_types[i].tkltype)
			return tkl_types[i].config_name;

	return "???";
}

int tkl_banexception_matches_type(TKL *except, int bantype)
{
	char *p;
	int extype;

	if (!TKLIsBanException(except))
		abort();

	for (p = except->ptr.banexception->bantypes; *p; p++)
	{
		extype = tkl_banexception_chartotype(*p);
		if ((extype & TKL_SPAMF) || (extype & TKL_SHUN) || (extype & TKL_NAME))
		{
			/* For spamfilter, shun and qline we don't care
			 * whether they are global or not. That would only
			 * be confusing to the admin.
			 */
			extype &= ~TKL_GLOBAL;
			if (bantype & extype)
				return 1;
		} else {
			/* Rest requires an exact match */
			if (bantype == extype)
				return 1;
		}
	}

	return 0;
}

/** Used for finding out which element of the tkl_ip hash table is used (primary element) */
int _tkl_ip_hash(const char *ip)
{
	const char *p;
	char ipbuf[64];

	if (!ip)
		return -1; /* possible if eg tkl->...match instead of tkl->...host */

	for (p = ip; *p; p++)
	{
		if ((*p == '?') || (*p == '*') || (*p == '/'))
			return -1; /* not an entry suitable for the ip hash table */
	}
	if (inet_pton(AF_INET, ip, &ipbuf) == 1)
	{
		/* IPv4 */
		unsigned int v = (ipbuf[0] << 24) +
		                 (ipbuf[1] << 16) +
		                 (ipbuf[2] << 8)  +
		                 ipbuf[3];
		return v % TKLIPHASHLEN2;
	} else
	if (inet_pton(AF_INET6, ip, &ipbuf) == 1)
	{
		/* IPv6 (only upper 64 bits) */
		unsigned int v1 = (ipbuf[0] << 24) +
		                 (ipbuf[1] << 16) +
		                 (ipbuf[2] << 8)  +
		                 ipbuf[3];
		unsigned int v2 = (ipbuf[4] << 24) +
		                 (ipbuf[5] << 16) +
		                 (ipbuf[6] << 8)  +
		                 ipbuf[7];
		return (v1 ^ v2) % TKLIPHASHLEN2;
	} else
	{
		return -1;
	}
}

// TODO: consider efunc
int tkl_ip_hash_tkl(TKL *tkl)
{
	if (TKLIsServerBan(tkl))
		return tkl_ip_hash(tkl->ptr.serverban->hostmask);
	if (TKLIsBanException(tkl))
		return tkl_ip_hash(tkl->ptr.banexception->hostmask);
	return -1;
}

/** Used for finding out which tkl_ip hash table needs to be used (secondary element).
 * NOTE: Returns -1 for types that are never on the TKL ip hash table, such as spamfilter.
 *       This can be used by the caller as a quick way to find out if the type is supported.
 */
int _tkl_ip_hash_type(int type)
{
	if ((type == 'Z') || (type == 'z'))
		return 0;
	else if (type == 'G')
		return 1;
	else if (type == 'k')
		return 2;
	else if ((type == 'e') || (type == 'E'))
		return 3;
	else
		return -1;
}

/* Find the appropriate list 'head' that we need to iterate.
 * This is simply a helper that is used at 3 places and I hate duplicate code.
 * NOTE: this function may return NULL.
 */
TKL *tkl_find_head(char type, const char *hostmask, TKL *def)
{
	int index, index2;

	/* First, check ip hash table TKL's... */
	index = tkl_ip_hash_type(type);
	if (index >= 0)
	{
		index2 = tkl_ip_hash(hostmask);
		if (index2 >= 0)
		{
			/* iterate tklines_ip_hash[index][index2] */
			return tklines_ip_hash[index][index2];
		}
	}
	/* Fallback to the default */
	return def;
}

/** Add a spamfilter entry to the list.
 * @param id                  ID
 * @param type                TKL_SPAMF or TKL_SPAMF|TKL_GLOBAL.
 * @param target              The spamfilter target (SPAMF_*)
 * @param action              The spamfilter action (BAN_ACT_*)
 * @param match               The match (this struct may contain a regex for example)
 * @param rule                Rule, if present, then only run spamfilter if true
 * @param except              When not to run the spamfilter
 * @param set_by              Who (or what) set the ban
 * @param expire_at           When will the ban expire (0 for permanent)
 * @param set_at              When was the ban set
 * @param spamf_tkl_duration  When will the ban placed by spamfilter expire
 * @param spamf_tkl_reason    What is the reason for bans placed by spamfilter
 * @param input_conversion    Input conversion(s) like stripping control codes
 * @param flags               Any TKL_FLAG_* (TKL_FLAG_CONFIG, etc..)
 * @returns                   The TKL entry, or NULL in case of a problem,
 *                            such as a regex failing to compile, memory problem, ..
 */
TKL *_tkl_add_spamfilter(int type, const char *id, unsigned short target, BanAction *action,
                         Match *match, const char *rule, SecurityGroup *except,
                         const char *set_by,
                         time_t expire_at, time_t set_at,
                         time_t tkl_duration, const char *tkl_reason,
                         int input_conversion,
                         SpamfilterShowMessageContentOnHit show_message_content_on_hit,
                         int flags)
{
	TKL *tkl;
	int index;

	if (!(type & TKL_SPAMF))
		abort();

	tkl = safe_alloc(sizeof(TKL));
	tkl->type = type;
	tkl->flags = flags;
	tkl->set_at = set_at;
	safe_strdup(tkl->set_by, set_by);
	tkl->expire_at = expire_at;
	tkl->ptr.spamfilter = safe_alloc(sizeof(Spamfilter));
	if (rule)
	{
		tkl->ptr.spamfilter->rule = crule_parse(rule);
		safe_strdup(tkl->ptr.spamfilter->prettyrule, rule);
	}
	if (rule && !match)
	{
		/* When no spamfilter::match, name it $RULE:xxx */
		char buf[512];
		snprintf(buf, sizeof(buf), "$RULE:%s", rule);
		match = safe_alloc(sizeof(Match));
		match->type = MATCH_NONE;
		safe_strdup(match->str, buf);
	}
	tkl->ptr.spamfilter->target = target;
	tkl->ptr.spamfilter->action = action;
	tkl->ptr.spamfilter->input_conversion = input_conversion;
	tkl->ptr.spamfilter->match = match;
	safe_strdup(tkl->ptr.spamfilter->tkl_reason, tkl_reason);
	tkl->ptr.spamfilter->except = except;
	tkl->ptr.spamfilter->tkl_duration = tkl_duration;
	safe_strdup(tkl->ptr.spamfilter->id, id);
	tkl->ptr.spamfilter->show_message_content_on_hit = show_message_content_on_hit;

	/* Config (and central) spamfilters are local per server, so they never receive
	 * a random network id. Give them a deterministic id instead, identical on every
	 * server: the config/central id label if set, otherwise a content hash. This is
	 * what lets a central spamfilter be referenced by the same id on every server.
	 */
	if (flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER))
	{
		if (!BadPtr(id))
			strlcpy(tkl->id, id, sizeof(tkl->id));
		else
			strlcpy(tkl->id, spamfilter_fallback_id(tkl), sizeof(tkl->id));
	}

	if (tkl->ptr.spamfilter->target & SPAMF_USER)
		loop.do_bancheck_spamf_user = 1;
	if (tkl->ptr.spamfilter->target & SPAMF_AWAY)
		loop.do_bancheck_spamf_away = 1;

	/* Spamfilters go via the normal TKL list... */
	index = tkl_hash(tkl_typetochar(type));
	AppendListItem(tkl, tklines[index]);

	if (target & SPAMF_MTAG)
		mtag_spamfilters_present = 1;
	if (target & SPAMF_RAW)
		raw_spamfilters_present = 1;
	if (input_conversion & INPUT_CONVERSION_CONFUSABLES)
		confusables_spamfilters_present = 1;

	return tkl;
}

/** Add a server ban TKL entry.
 * @param type                The TKL type, one of TKL_*,
 *                            optionally OR'ed with TKL_GLOBAL.
 * @param usermask            The user mask
 * @param hostmask            The host mask
 * @param match               A match item / security group (if set,
 *                            then user/host is ignored and this must be a local ban)
 * @param reason              The reason for the ban
 * @param set_by              Who (or what) set the ban
 * @param expire_at           When will the ban expire (0 for permanent)
 * @param set_at              When was the ban set
 * @param soft                Whether it's a soft-ban
 * @param flags               Any TKL_FLAG_* (TKL_FLAG_CONFIG, etc..)
 * @returns                   The TKL entry, or NULL in case of a problem,
 *                            such as a regex failing to compile, memory problem, ..
 * @note
 * Be sure not to call this function for spamfilters,
 * qlines or exempts, which have their own function!
 */
TKL *_tkl_add_serverban(int type, const char *usermask, const char *hostmask, SecurityGroup *match,
                        const char *reason, const char *set_by,
                        time_t expire_at, time_t set_at, int soft, int flags)
{
	TKL *tkl;
	int index, index2;

	if (!TKLIsServerBanType(type))
		abort();

	tkl = safe_alloc(sizeof(TKL));
	/* First the common fields */
	tkl->type = type;
	tkl->flags = flags;
	tkl->set_at = set_at;
	safe_strdup(tkl->set_by, set_by);
	tkl->expire_at = expire_at;
	/* Now the server ban fields */
	tkl->ptr.serverban = safe_alloc(sizeof(ServerBan));
	if (soft)
		tkl->ptr.serverban->subtype = TKL_SUBTYPE_SOFT;
	safe_strdup(tkl->ptr.serverban->reason, reason);

	if (match)
	{
		tkl->ptr.serverban->match = match;
	} else {
		safe_strdup(tkl->ptr.serverban->usermask, usermask);
		safe_strdup(tkl->ptr.serverban->hostmask, hostmask);
		/* For ip hash table TKL's... */
		index = tkl_ip_hash_type(tkl_typetochar(type));
		if (index >= 0)
		{
			index2 = tkl_ip_hash_tkl(tkl);
			if (index2 >= 0)
			{
				AddListItem(tkl, tklines_ip_hash[index][index2]);
				return tkl;
			}
		}
	}

	/* If we get here it's just for our normal list.. */
	index = tkl_hash(tkl_typetochar(type));
	AddListItem(tkl, tklines[index]);

	return tkl;
}

/** Add a ban exception TKL entry.
 * @param type                TKL_EXCEPTION or TKLEXCEPT|TKL_GLOBAL.
 * @param usermask            The user mask
 * @param hostmask            The host mask
 * @param match               A securitygroup used for matching (can be NULL,
 *                            if not NULL then this field is used as-is and not copied
 *                            so caller should not free!)
 * @param reason              The reason for the ban
 * @param set_by              Who (or what) set the ban
 * @param expire_at           When will the ban expire (0 for permanent)
 * @param set_at              When was the ban set
 * @param soft                Whether it's a soft-ban
 * @param bantypes            The ban types to exempt from
 * @param flags               Any TKL_FLAG_* (TKL_FLAG_CONFIG, etc..)
 * @returns                   The TKL entry, or NULL in case of a problem,
 *                            such as a regex failing to compile, memory problem, ..
 * @note
 * Be sure not to call this function for spamfilters,
 * qlines or exempts, which have their own function!
 */
TKL *_tkl_add_banexception(int type, const char *usermask, const char *hostmask, SecurityGroup *match,
                           const char *reason, const char *set_by,
                           time_t expire_at, time_t set_at, int soft, const char *bantypes, int flags)
{
	TKL *tkl;
	int index, index2;

	if (!TKLIsBanExceptionType(type))
		abort();
	tkl = safe_alloc(sizeof(TKL));
	/* First the common fields */
	tkl->type = type;
	tkl->flags = flags;
	tkl->set_at = set_at;
	safe_strdup(tkl->set_by, set_by);
	tkl->expire_at = expire_at;
	/* Now the ban except fields */
	tkl->ptr.banexception = safe_alloc(sizeof(BanException));
	safe_strdup(tkl->ptr.banexception->usermask, usermask);
	safe_strdup(tkl->ptr.banexception->hostmask, hostmask);
	tkl->ptr.banexception->match = match;
	if (soft)
		tkl->ptr.banexception->subtype = TKL_SUBTYPE_SOFT;
	safe_strdup(tkl->ptr.banexception->bantypes, bantypes);
	safe_strdup(tkl->ptr.banexception->reason, reason);

	/* For ip hash table TKL's... */
	index = tkl_ip_hash_type(tkl_typetochar(type));
	if (index >= 0)
	{
		index2 = tkl_ip_hash_tkl(tkl);
		if (index2 >= 0)
		{
			AddListItem(tkl, tklines_ip_hash[index][index2]);
			return tkl;
		}
	}

	/* If we get here it's just for our normal list.. */
	index = tkl_hash(tkl_typetochar(type));
	AddListItem(tkl, tklines[index]);

	return tkl;
}

/** Add a name ban TKL entry (Q-Line), used for banning nicks and channels.
 * @param type                The TKL type, one of TKL_*,
 *                            optionally OR'ed with TKL_GLOBAL.
 * @param name                The nick or channel to be banned (wildcards accepted)
 * @param hold                Flag to indicate services hold
 * @param reason              The reason for the ban
 * @param set_by              Who (or what) set the ban
 * @param expire_at           When will the ban expire (0 for permanent)
 * @param set_at              When was the ban set
 * @param flags               Any TKL_FLAG_* (TKL_FLAG_CONFIG, etc..)
 * @returns                   The TKL entry, or NULL in case of a problem,
 *                            such as a regex failing to compile, memory problem, ..
 * @note
 * Be sure not to call this function for spamfilters,
 * qlines or exempts, which have their own function!
 */
TKL *_tkl_add_nameban(int type, const char *name, int hold, const char *reason, const char *set_by,
                          time_t expire_at, time_t set_at, int flags)
{
	TKL *tkl;
	int index;

	if (!TKLIsNameBanType(type))
		abort();

	tkl = safe_alloc(sizeof(TKL));
	/* First the common fields */
	tkl->type = type;
	tkl->flags = flags;
	tkl->set_at = set_at;
	safe_strdup(tkl->set_by, set_by);
	tkl->expire_at = expire_at;
	/* Now the name ban fields */
	tkl->ptr.nameban = safe_alloc(sizeof(ServerBan));
	safe_strdup(tkl->ptr.nameban->name, name);
	tkl->ptr.nameban->hold = hold;
	safe_strdup(tkl->ptr.nameban->reason, reason);

	/* Name bans go via the normal TKL list.. */
	index = tkl_hash(tkl_typetochar(type));
	AddListItem(tkl, tklines[index]);

	return tkl;
}


/** Free a TKL entry but do not remove from the list.
 * (this assumes that it was not added yet or is already removed)
 * Most people will use tkl_del_line() instead.
 */
void _free_tkl(TKL *tkl)
{
	/* Free the entry */
	/* First, the common fields */
	safe_free(tkl->set_by);
	/* Now the type specific fields */
	if (TKLIsServerBan(tkl) && tkl->ptr.serverban)
	{
		safe_free(tkl->ptr.serverban->usermask);
		safe_free(tkl->ptr.serverban->hostmask);
		safe_free(tkl->ptr.serverban->reason);
		safe_free_security_group(tkl->ptr.serverban->match);
		safe_free(tkl->ptr.serverban);
	} else
	if (TKLIsNameBan(tkl) && tkl->ptr.nameban)
	{
		safe_free(tkl->ptr.nameban->name);
		safe_free(tkl->ptr.nameban->reason);
		safe_free(tkl->ptr.nameban);
	} else
	if (TKLIsSpamfilter(tkl) && tkl->ptr.spamfilter)
	{
		/* Spamfilter */
		safe_free(tkl->ptr.spamfilter->tkl_reason);
		if (tkl->ptr.spamfilter->match)
			unreal_delete_match(tkl->ptr.spamfilter->match);
		safe_crule_free(tkl->ptr.spamfilter->rule);
		safe_free_all_ban_actions(tkl->ptr.spamfilter->action);
		safe_free(tkl->ptr.spamfilter->prettyrule);
		safe_free(tkl->ptr.spamfilter->id);
		safe_free_security_group(tkl->ptr.spamfilter->except);
		safe_free(tkl->ptr.spamfilter);
	} else
	if (TKLIsBanException(tkl) && tkl->ptr.banexception)
	{
		safe_free(tkl->ptr.banexception->usermask);
		safe_free(tkl->ptr.banexception->hostmask);
		if (tkl->ptr.banexception->match)
			free_security_group(tkl->ptr.banexception->match);
		safe_free(tkl->ptr.banexception->bantypes);
		safe_free(tkl->ptr.banexception->reason);
		safe_free(tkl->ptr.banexception);
	}
	safe_free(tkl);
}

/** Build a stable match key for a hit-bearing config/central TKL (server ban,
 * name ban or spamfilter), used to pair an old entry with its rebuilt self across
 * a rehash / feed refresh. Returns buf, or NULL for types we don't preserve
 * (exceptions) or with no usable key. Format: "<typechar> <id|mask|name>", joined
 * by a single space (these fields never contain spaces).
 */
char *tkl_hits_key(TKL *tkl, char *buf, size_t len)
{
	char tmp[BUFSIZE];
	char t = tkl_typetochar(tkl->type);

	if (TKLIsSpamfilter(tkl) && tkl->id[0])
		snprintf(buf, len, "%c %s", t, tkl->id);
	else if (TKLIsServerBan(tkl))
		snprintf(buf, len, "%c %s", t, tkl_uhost(tkl, tmp, sizeof(tmp), 0));
	else if (TKLIsNameBan(tkl))
		snprintf(buf, len, "%c %s", t, tkl->ptr.nameban->name);
	else
		return NULL;
	return buf;
}

/** Stash a config/central TKL's hit counters before it is freed (from the removal
 * loop in _remove_config_tkls), so they can be copied back onto the same entry
 * after a rehash / feed refresh re-adds it. Covers server bans, name bans and
 * spamfilters (whatever tkl_hits_key recognises); skips entries that never hit.
 */
void config_tkl_hits_snapshot(TKL *tkl)
{
	ConfigTKLHits *e;
	char key[256];

	if (!(tkl->flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER)))
		return;
	if (!tkl_hits_key(tkl, key, sizeof(key)))
		return; /* not a hit-bearing config type */
	if (!tkl->hits && !tkl->lasthit &&
	    (!TKLIsSpamfilter(tkl) ||
	     (!tkl->ptr.spamfilter->hits_except && !tkl->ptr.spamfilter->lasthit_except)))
		return; /* never hit, nothing to keep */

	e = safe_alloc(sizeof(ConfigTKLHits));
	strlcpy(e->key, key, sizeof(e->key));
	e->hits = tkl->hits;
	e->lasthit = tkl->lasthit;
	if (TKLIsSpamfilter(tkl))
	{
		e->hits_except = tkl->ptr.spamfilter->hits_except;
		e->lasthit_except = tkl->ptr.spamfilter->lasthit_except;
	}
	AddListItem(e, config_tkl_hits);
}

/** Free the whole snapshot list and forget it. */
void config_tkl_hits_free_all(void)
{
	ConfigTKLHits *e, *e_next;

	for (e = config_tkl_hits; e; e = e_next)
	{
		e_next = e->next;
		safe_free(e);
	}
	config_tkl_hits = NULL;
}

/** PersistentPointer free callback: used only if the snapshot is never reloaded
 * (e.g. final module unload), otherwise MOD_LOAD consumes and frees it.
 */
void config_tkl_hits_free(ModData *m)
{
	config_tkl_hits_free_all();
}

/** Copy stashed counters back onto one re-added TKL, matched by key. */
static void config_tkl_hits_restore_one(TKL *tkl)
{
	ConfigTKLHits *e;
	char key[256];

	if (!(tkl->flags & (TKL_FLAG_CONFIG | TKL_FLAG_CENTRAL_SPAMFILTER)))
		return;
	if (!tkl_hits_key(tkl, key, sizeof(key)))
		return;
	for (e = config_tkl_hits; e; e = e->next)
	{
		if (!strcmp(e->key, key))
		{
			tkl->hits = e->hits;
			tkl->lasthit = e->lasthit;
			if (TKLIsSpamfilter(tkl))
			{
				tkl->ptr.spamfilter->hits_except = e->hits_except;
				tkl->ptr.spamfilter->lasthit_except = e->lasthit_except;
			}
			return;
		}
	}
}

/** After config/central TKLs have been re-added, copy the stashed hit counters
 * back onto the ones with a matching key, then drop the whole stash. Called from
 * MOD_LOAD (config rehash) and from the central feed refresh; a no-op on a normal
 * boot (the stash is empty).
 */
void _config_tkl_hits_restore(void)
{
	TKL *tkl;
	int index, index2;

	if (!config_tkl_hits)
		return;

	/* IP-hashed list (zlines, klines, glines) */
	for (index = 0; index < TKLIPHASHLEN1; index++)
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
				config_tkl_hits_restore_one(tkl);

	/* Generic list (name bans, spamfilters, non-ip-hashed server bans) */
	for (index = 0; index < TKLISTLEN; index++)
		for (tkl = tklines[index]; tkl; tkl = tkl->next)
			config_tkl_hits_restore_one(tkl);

	/* Matched entries are applied; entries no longer in the config are discarded. */
	config_tkl_hits_free_all();
}

/** Remove all TKLs with the given flag (TKL_FLAG_CONFIG or
 * TKL_FLAG_CENTRAL_SPAMFILTER). For config/central spamfilters the hit counters
 * are snapshotted first, so a later re-add can restore them. Lives here (rather
 * than in conf.c) so the snapshot sits right next to the removal it rides on.
 */
void _remove_config_tkls(int flag)
{
	TKL *tk, *tk_next;
	int index, index2;

	/* IP hashed TKL list */
	for (index = 0; index < TKLIPHASHLEN1; index++)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			for (tk = tklines_ip_hash[index][index2]; tk; tk = tk_next)
			{
				tk_next = tk->next;
				if (tk->flags & flag)
				{
					config_tkl_hits_snapshot(tk);
					tkl_del_line(tk);
				}
			}
		}
	}

	/* Generic TKL list */
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tk = tklines[index]; tk; tk = tk_next)
		{
			tk_next = tk->next;
			if (tk->flags & flag)
			{
				config_tkl_hits_snapshot(tk);
				tkl_del_line(tk);
			}
		}
	}
}

/** Delete a TKL entry from the list and free it.
 * @param tkl The TKL entry.
 */
void _tkl_del_line(TKL *tkl)
{
	int index, index2;
	int found = 0;

	/* Try to find it in the ip TKL hash table first
	 * (this only applies to server bans)
	 */
	index = tkl_ip_hash_type(tkl_typetochar(tkl->type));
	if (index >= 0)
	{
		index2 = tkl_ip_hash_tkl(tkl);
		if (index2 >= 0)
		{
#if 1
			/* Temporary validation until an rmtkl(?) bug is fixed */
			TKL *d;
			int really_found = 0;
			for (d = tklines_ip_hash[index][index2]; d; d = d->next)
				if (d == tkl)
				{
					really_found = 1;
					break;
				}
			if (!really_found)
			{
				unreal_log(ULOG_FATAL, "tkl", "BUG_TKL_DEL_LINE_HASH", NULL,
				           "[BUG] [Crash] tkl_del_line() for $tkl (type: $tkl.type_string): "
				           "NOT found in tklines_ip_hash. This should never happen!",
				           log_data_tkl("tkl", tkl));
				abort();
			}
#endif
			DelListItem(tkl, tklines_ip_hash[index][index2]);
			found = 1;
		}
	}

	if (!found)
	{
		/* If we get here it's just for our normal list.. */
		index = tkl_hash(tkl_typetochar(tkl->type));
		DelListItem(tkl, tklines[index]);
	}

	/* Finally, free the entry */
	free_tkl(tkl);
	check_special_spamfilters_present();
}

/** Add some default ban exceptions - for localhost */
static void add_default_exempts(void)
{
	/* The exempted ban types are only ones that will affect other connections as well,
	 * such as gline, and not policy decissions such as maxperip exempt or bypass qlines.
	 * Currently the list is: gline, kline, gzline, zline, shun, blacklist,
	 *                        connect-flood, handshake-data-flood.
	 */
	tkl_add_banexception(TKL_EXCEPTION, "*", "127.0.0.1", NULL, "localhost is always exempt",
	                     "-default-", 0, TStime(), 0, "GkZzsbcd", TKL_FLAG_CONFIG);
	tkl_add_banexception(TKL_EXCEPTION, "*", "::1", NULL, "localhost is always exempt",
	                     "-default-", 0, TStime(), 0, "GkZzsbcd", TKL_FLAG_CONFIG);
}

/*
 * tkl_check_local_remove_shun:
 * removes shun from currently connected users affected by tmp.
 */
// TODO / FIXME: audit this function, it looks crazy
void _tkl_check_local_remove_shun(TKL *tmp)
{
	long i;
	char *chost, *cname, *cip;
	int is_ip;
	Client *client;

	TKL *tk;
	int keep_shun;

	for (i = 0; i <= 5; i++)
	{
		list_for_each_entry(client, &lclient_list, lclient_node)
			if (MyUser(client) && IsShunned(client))
			{
				chost = client->local->sockhost;
				cname = client->user->username;

				cip = GetIP(client);

				if ((*tmp->ptr.serverban->hostmask >= '0') && (*tmp->ptr.serverban->hostmask <= '9'))
					is_ip = 1;
				else
					is_ip = 0;

				if (is_ip == 0 ?
				    (match_simple(tmp->ptr.serverban->hostmask, chost) && match_simple(tmp->ptr.serverban->usermask, cname)) :
				    (match_simple(tmp->ptr.serverban->hostmask, chost) || match_simple(tmp->ptr.serverban->hostmask, cip))
				    && match_simple(tmp->ptr.serverban->usermask, cname))
				{
					/*
					  before blindly marking this user as un-shunned, we need to check
					  if the user is under any other existing shuns. (#0003906)
					  Unfortunately, this requires crazy amounts of indentation ;-).

					  This enumeration code is based off of _tkl_stats()
					 */
					keep_shun = 0;
					for(tk = tklines[tkl_hash('s')]; tk && !keep_shun; tk = tk->next)
						if (tk != tmp && match_simple(tk->ptr.serverban->usermask, cname))
						{
							if ((*tk->ptr.serverban->hostmask >= '0') && (*tk->ptr.serverban->hostmask <= '9')
							    /* the hostmask is an IP */
							    && (match_simple(tk->ptr.serverban->hostmask, chost) || match_simple(tk->ptr.serverban->hostmask, cip)))
								keep_shun = 1;
							else
								/* the hostmask is not an IP */
								if (match_simple(tk->ptr.serverban->hostmask, chost) && match_simple(tk->ptr.serverban->usermask, cname))
									keep_shun = 1;
						}

					if (!keep_shun)
					{
						ClearShunned(client);
					}
				}
			}
	}
}


/** This returns something like user@host, or %user@host, or ~a:Trusted
 * that can be used in oper notices like expiring kline, added kline, etc.
 */
#define NO_SOFT_PREFIX	1
char *_tkl_uhost(TKL *tkl, char *buf, size_t buflen, int options)
{
	if (TKLIsServerBan(tkl))
	{
		if (tkl->ptr.serverban->match)
		{
			ircsnprintf(buf, buflen, "<match-item>");
		} else
		if (is_extended_server_ban(tkl->ptr.serverban->usermask))
		{
			ircsnprintf(buf, buflen, "%s%s%s",
				(!(options & NO_SOFT_PREFIX) && (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT)) ? "%" : "",
				tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask);
		} else {
			ircsnprintf(buf, buflen, "%s%s@%s",
				(!(options & NO_SOFT_PREFIX) && (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT)) ? "%" : "",
				tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask);
		}
	} else
	if (TKLIsBanException(tkl))
	{
		if (tkl->ptr.banexception->match)
		{
			ircsnprintf(buf, buflen, "<match-item>");
		} else
		if (is_extended_server_ban(tkl->ptr.banexception->usermask))
		{
			ircsnprintf(buf, buflen, "%s%s%s",
				(!(options & NO_SOFT_PREFIX) && (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT)) ? "%" : "",
				tkl->ptr.banexception->usermask, tkl->ptr.banexception->hostmask);
		} else {
			ircsnprintf(buf, buflen, "%s%s@%s",
				(!(options & NO_SOFT_PREFIX) && (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT)) ? "%" : "",
				tkl->ptr.banexception->usermask, tkl->ptr.banexception->hostmask);
		}
	} else
		abort();

	return buf;
}

/** Deal with expiration of a specific TKL entry.
 * This is a helper function for tkl_check_expire().
 */
void tkl_expire_entry(TKL *tkl)
{
	if (TKLIsServerBan(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_EXPIRE", NULL,
		           "Expiring $tkl.type_string '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id $spamfilter_id",
		           log_data_tkl("tkl", tkl),
		           log_data_optional_name_value("id", "id", tkl->id),
		           log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->spamfilter_id));
	}
	else if (TKLIsNameBan(tkl))
	{
		if (!tkl->ptr.nameban->hold)
		{
			unreal_log(ULOG_INFO, "tkl", "TKL_EXPIRE", NULL,
			           "Expiring $tkl.type_string '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id",
				   log_data_tkl("tkl", tkl),
				   log_data_optional_name_value("id", "id", tkl->id));
		}
	}
	else if (TKLIsBanException(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_EXPIRE", NULL,
			   "Expiring $tkl.type_string '$tkl' [type: $tkl.exception_types] [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_optional_name_value("id", "id", tkl->id));
	}

	// FIXME: so.. this isn't logged? or what?
	if (tkl->type & TKL_SHUN)
		tkl_check_local_remove_shun(tkl);

	RunHook(HOOKTYPE_TKL_DEL, NULL, tkl);
	tkl_del_line(tkl);
}

/** Regularly check TKL entries for expiration */
EVENT(tkl_check_expire)
{
	TKL *tkl, *next;
	time_t nowtime;
	int index, index2;

	nowtime = TStime();

	/* First, hashed entries.. */
	for (index = 0; index < TKLIPHASHLEN1; index++)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = next)
			{
				next = tkl->next;
				if (tkl->expire_at <= nowtime && !(tkl->expire_at == 0))
				{
					tkl_expire_entry(tkl);
				}
			}
		}
	}

	/* Now normal entries.. */
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tkl = tklines[index]; tkl; tkl = next)
		{
			next = tkl->next;
			if (tkl->expire_at <= nowtime && !(tkl->expire_at == 0))
			{
				tkl_expire_entry(tkl);
			}
		}
	}
}

/* This is just a helper function for find_tkl_exception() */
static int find_tkl_exception_matcher(Client *client, int ban_type, TKL *except_tkl)
{
	char uhost[NICKLEN+HOSTLEN+1];

	if (!TKLIsBanException(except_tkl))
		return 0;

	if (!tkl_banexception_matches_type(except_tkl, ban_type))
		return 0;

	/* For config file except ban { } we use security groups instead of simple user/host */
	if (except_tkl->ptr.banexception->match)
		return user_allowed_by_security_group(client, except_tkl->ptr.banexception->match);

	tkl_uhost(except_tkl, uhost, sizeof(uhost), NO_SOFT_PREFIX);

	if (match_user(uhost, client, MATCH_CHECK_REAL))
	{
		if (!(except_tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT))
			return 1; /* hard ban exempt */
		if ((except_tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) && IsLoggedIn(client))
			return 1; /* soft ban exempt - only matches if user is logged in */
	}

	return 0; /* not found */
}

/** Search for TKL Exceptions for this user.
 * @param ban_type   The ban type to check, normally ban_tkl->type.
 * @param client     The user
 * @returns 1 if ban exempt, 0 if not.
 * @note
 * If you have a TKL ban that matched, say, 'ban_tkl'.
 * Then you call this function like this:
 * if (find_tkl_exception(ban_tkl->type, client))
 *     return 0; // User is exempt
 * [.. continue and ban the user..]
 */
int _find_tkl_exception(int ban_type, Client *client)
{
	TKL *tkl;
	int index, index2;
	Hook *hook;

	if (IsServer(client) || IsMe(client))
		return 1;

	/* First, the TKL ip hash table entries.. */
	index = tkl_ip_hash_type('e');
	index2 = tkl_ip_hash(GetIP(client));
	if (index2 >= 0)
	{
		for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
		{
			if (find_tkl_exception_matcher(client, ban_type, tkl))
				return 1; /* exempt */
		}
	}

	/* If not banned (yet), then check regular entries.. */
	for (tkl = tklines[tkl_hash('e')]; tkl; tkl = tkl->next)
	{
			if (find_tkl_exception_matcher(client, ban_type, tkl))
				return 1; /* exempt */
	}

	for (hook = Hooks[HOOKTYPE_TKL_EXCEPT]; hook; hook = hook->next)
	{
		if (hook->func.intfunc(client, ban_type) > 0)
			return 1; /* exempt by hook */
	}
	return 0; /* Not exempt */
}

/** Helper function for find_tkline_match() */
int find_tkline_match_matcher(Client *client, int skip_soft, TKL *tkl)
{
	char uhost[NICKLEN+HOSTLEN+1];

	if (!TKLIsServerBan(tkl) || (tkl->type & TKL_SHUN))
		return 0;

	if (skip_soft && (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT))
		return 0;

	/* For config file ban { } we use security groups instead of simple user/host */
	if (tkl->ptr.serverban->match)
	{
		/* If hard-ban, or soft-ban&unauthenticated.. */
		if (!(tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ||
		    ((tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) && !IsLoggedIn(client)))
		{
			if (user_allowed_by_security_group(client, tkl->ptr.serverban->match))
			{
				if (find_tkl_exception(tkl->type, client))
					return 0; /* exempted */
				return 1; /* banned */
			}
			return 0;
		}
	}

	tkl_uhost(tkl, uhost, sizeof(uhost), NO_SOFT_PREFIX);

	if (match_user(uhost, client, MATCH_CHECK_REAL))
	{
		/* If hard-ban, or soft-ban&unauthenticated.. */
		if (!(tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ||
		    ((tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) && !IsLoggedIn(client)))
		{
			/* Found match. Now check for exception... */
			if (find_tkl_exception(tkl->type, client))
				return 0; /* exempted */
			return 1; /* banned */
		}
	}

	return 0; /* no match */
}

/** Check if user matches a *LINE. If so, kill the user.
 * @retval 1 if client is banned, 0 if not
 * @note Do not continue processing if the client is killed (0 return value).
 * @note Return value changed with regards to UnrealIRCd 4!
 */
int _find_tkline_match(Client *client, int skip_soft)
{
	TKL *tkl;
	int banned = 0;
	int index, index2;

	if (IsServer(client) || IsMe(client))
		return 0;

	/* First, the TKL ip hash table entries.. */
	index2 = tkl_ip_hash(GetIP(client));
	if (index2 >= 0)
	{
		for (index = 0; index < TKLIPHASHLEN1; index++)
		{
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
			{
				banned = find_tkline_match_matcher(client, skip_soft, tkl);
				if (banned)
					break;
			}
			if (banned)
				break;
		}
	}

	/* If not banned (yet), then check regular entries.. */
	if (!banned)
	{
		for (index = 0; index < TKLISTLEN; index++)
		{
			for (tkl = tklines[index]; tkl; tkl = tkl->next)
			{
				banned = find_tkline_match_matcher(client, skip_soft, tkl);
				if (banned)
					break;
			}
			if (banned)
				break;
		}
	}

	if (!banned)
		return 0;

	/* User is banned... */

	RunHookReturnInt(HOOKTYPE_FIND_TKLINE_MATCH, !=99, client, tkl);

	if (tkl->type & TKL_KILL)
	{
		ircstats.is_ref++;
		tkl_hit(client, tkl);
		if (tkl->type & TKL_GLOBAL)
			banned_client(client, "G-Lined", tkl->ptr.serverban->reason, tkl->id, 1, 0);
		else
			banned_client(client, "K-Lined", tkl->ptr.serverban->reason, tkl->id, 0, 0);
		return 1; /* killed */
	} else
	if (tkl->type & TKL_ZAP)
	{
		ircstats.is_ref++;
		tkl_hit(client, tkl);
		banned_client(client, "Z-Lined", tkl->ptr.serverban->reason, tkl->id, (tkl->type & TKL_GLOBAL)?1:0, 0);
		return 1; /* killed */
	}

	return 0;
}

/** Check if user is shunned.
 * @param client   Client to check.
 * @returns 1 if shunned, 0 if not.
 */
int _find_shun(Client *client)
{
	TKL *tkl;

	if (IsServer(client) || IsMe(client))
		return 0;

	if (IsShunned(client))
		return 1;

	if (ValidatePermissionsForPath("immune:server-ban:shun",client,NULL,NULL,NULL))
		return 0;

	for (tkl = tklines[tkl_hash('s')]; tkl; tkl = tkl->next)
	{
		char uhost[NICKLEN+HOSTLEN+1];

		if (!(tkl->type & TKL_SHUN))
			continue;

		tkl_uhost(tkl, uhost, sizeof(uhost), NO_SOFT_PREFIX);

		if (match_user(uhost, client, MATCH_CHECK_REAL))
		{
			/* If hard-ban, or soft-ban&unauthenticated.. */
			if (!(tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ||
			    ((tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) && !IsLoggedIn(client)))
			{
				/* Found match. Now check for exception... */
				if (find_tkl_exception(TKL_SHUN, client))
					return 0;
				tkl_hit(client, tkl);
				SetShunned(client);
				return 1;
			}
		}
	}

	return 0;
}

/** Helper function for spamfilter_build_user_string().
 * This ensures IPv6 hosts are in brackets.
 */
char *SpamfilterMagicHost(char *i)
{
	static char buf[256];

	if (!strchr(i, ':'))
		return i;

	/* otherwise, it's IPv6.. prepend it with [ and append a ] */
	ircsnprintf(buf, sizeof(buf), "[%s]", i);
	return buf;
}

/** Build the nick:user@host:realname string
 * @param buf     The buffer used for storage, the size of
 *                which should be at least NICKLEN+USERLEN+HOSTLEN+REALLEN+1.
 * @param nick    The nickname (because client can be nick-changing).
 * @param client  The affected client.
 */
void _spamfilter_build_user_string(char *buf, const char *nick, Client *client)
{
	snprintf(buf, NICKLEN+USERLEN+HOSTLEN+REALLEN+1, "%s!%s@%s:%s",
		nick, client->user->username, SpamfilterMagicHost(client->user->realhost), client->info);
}


/** Checks if the user matches a spamfilter of type 'u' (user,
 * nick!user@host:realname ban).
 * Written by: Syzop
 * Assumes: only call for clients, possible assume on local clients [?]
 * Return values: see match_spamfilter()
 */
int _find_spamfilter_user(Client *client, int flags)
{
	char spamfilter_user[NICKLEN + USERLEN + HOSTLEN + REALLEN + 64]; /* n!u@h:r */

	if (ValidatePermissionsForPath("immune:server-ban:spamfilter",client,NULL,NULL,NULL))
		return 0;

	spamfilter_build_user_string(spamfilter_user, client->name, client);
	return match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, NULL, flags, NULL, NULL);
}

/** Check a spamfilter against all local users and print a message.
 * This is only used for the 'warn' action (BAN_ACT_WARN).
 */
int spamfilter_check_users(TKL *tkl)
{
	char spamfilter_user[NICKLEN + USERLEN + HOSTLEN + REALLEN + 64]; /* n!u@h:r */
	char buf[1024];
	int matches = 0;
	Client *client;

	list_for_each_entry_reverse(client, &lclient_list, lclient_node)
	{
		if (MyUser(client))
		{
			const char *regex_error = NULL;

			spamfilter_build_user_string(spamfilter_user, client->name, client);
			if (!unreal_match(tkl->ptr.spamfilter->match, spamfilter_user, &regex_error))
			{
				if (regex_error)
					spamfilter_regex_error(tkl, regex_error);
				continue; /* No match */
			}

			/* matched! */
			unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
			           "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id",
				   log_data_tkl("tkl", tkl),
				   log_data_string("command", "USER"),
				   log_data_string("str", spamfilter_user),
				   log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id));

			RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, spamfilter_user, spamfilter_user, SPAMF_USER, NULL, tkl);
			matches++;
		}
	}

	return matches;
}

/** Check if the nick or channel name is banned (Q-Line).
 * @param client   The possibly affected user.
 * @param name     The nick or channel to check.
 * @param is_hold  This will be SET (so OUT) if it's a services hold.
 *
 * @note Special handling:
 * #*ble* will match with #bbleh
 * *ble* will NOT match with #bbleh, will with bbleh
 */
TKL *_find_qline(Client *client, const char *name, int *ishold)
{
	TKL *tkl;
	int	points = 0;
	*ishold = 0;

	if (IsServer(client) || IsMe(client))
		return NULL;

	for (tkl = tklines[tkl_hash('q')]; tkl; tkl = tkl->next)
	{
		points = 0;

		if (!TKLIsNameBan(tkl))
			continue;

		if (((*tkl->ptr.nameban->name == '#' && *name == '#') || (*tkl->ptr.nameban->name != '#' && *name != '#'))
		    && match_simple(tkl->ptr.nameban->name, name))
		{
			points = 1;
			break;
		}
	}

	if (points != 1)
		return NULL;

	/* It's a services hold (except bans don't override this) */
	if (tkl->ptr.nameban->hold)
	{
		*ishold = 1;
		return tkl;
	}

	if (find_tkl_exception(TKL_NAME, client))
		return NULL; /* exempt */

	return tkl;
}

/** Helper function for find_tkline_match_zap() */
TKL *find_tkline_match_zap_matcher(Client *client, TKL *tkl)
{
	if (!(tkl->type & TKL_ZAP))
		return NULL;

	if (match_user(tkl->ptr.serverban->hostmask, client, MATCH_CHECK_IP))
	{
		if (find_tkl_exception(TKL_ZAP, client))
			return NULL; /* exempt */
		return tkl; /* banned */
	}

	return NULL; /* no match */
}

/** Find matching (G)ZLINE, if any.
 * Note: function prototype changed as per UnrealIRCd 4.2.0.
 * @retval The (G)Z-Line that matched, or NULL if no such ban was found.
 */
TKL *_find_tkline_match_zap(Client *client)
{
	TKL *tkl, *ret;
	int index, index2;

	if (IsServer(client) || IsMe(client))
		return NULL;

	/* First, the TKL ip hash table entries.. */
	index = tkl_ip_hash_type('z');
	index2 = tkl_ip_hash(GetIP(client));
	if (index2 >= 0)
	{
		for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
		{
			ret = find_tkline_match_zap_matcher(client, tkl);
			if (ret)
				return ret;
		}
	}

	/* If not banned (yet), then check regular entries.. */
	for (tkl = tklines[tkl_hash('z')]; tkl; tkl = tkl->next)
	{
		ret = find_tkline_match_zap_matcher(client, tkl);
		if (ret)
			return ret;
	}

	return NULL;
}

#define BY_MASK 0x1
#define BY_REASON 0x2
#define NOT_BY_MASK 0x4
#define NOT_BY_REASON 0x8
#define BY_SETBY 0x10
#define NOT_BY_SETBY 0x20
#define BY_ID 0x40
#define NOT_BY_ID 0x80

typedef struct {
	int flags;
	const char *mask;
	const char *reason;
	const char *set_by;
	const char *id;
} TKLFlag;

/** Parse STATS tkl parameters.
 * TODO: I don't think this is documented anywhere? Or underdocumented at least.
 */
static void parse_stats_params(const char *para, TKLFlag *flag)
{
	static char paratmp[512]; /* <- copy of para, because it gets fragged by strtok() */
	char *flags, *tmp;
	char what = '+';

	memset(flag, 0, sizeof(TKLFlag));
	strlcpy(paratmp, para, sizeof(paratmp));
	flags = strtok(paratmp, " ");
	if (!flags)
		return;

	for (; *flags; flags++)
	{
		switch (*flags)
		{
			case '+':
				what = '+';
				break;
			case '-':
				what = '-';
				break;
			case 'm':
				if (flag->mask || !(tmp = strtok(NULL, " ")))
					continue;
				if (what == '+')
					flag->flags |= BY_MASK;
				else
					flag->flags |= NOT_BY_MASK;
				flag->mask = tmp;
				break;
			case 'r':
				if (flag->reason || !(tmp = strtok(NULL, " ")))
					continue;
				if (what == '+')
					flag->flags |= BY_REASON;
				else
					flag->flags |= NOT_BY_REASON;
				flag->reason = tmp;
				break;
			case 's':
				if (flag->set_by || !(tmp = strtok(NULL, " ")))
					continue;
				if (what == '+')
					flag->flags |= BY_SETBY;
				else
					flag->flags |= NOT_BY_SETBY;
				flag->set_by = tmp;
				break;
			case 'i':
				if (flag->id || !(tmp = strtok(NULL, " ")))
					continue;
				if (what == '+')
					flag->flags |= BY_ID;
				else
					flag->flags |= NOT_BY_ID;
				flag->id = tmp;
				break;
		}
	}
}

/** Does this TKL entry match the search terms?
 * This is a helper function for tkl_stats().
 */
int tkl_stats_matcher(Client *client, int type, const char *para, TKLFlag *tklflags, TKL *tkl)
{
	const char *id_str = tkl->id[0] ? tkl->id : "-";
	const char *spamfilter_id_str = tkl->spamfilter_id[0] ? tkl->spamfilter_id : "-";

	/***** First, handle the selection ******/

	if (!BadPtr(para))
	{
		if (tklflags->flags & BY_SETBY)
			if (!match_simple(tklflags->set_by, tkl->set_by))
				return 0;
		if (tklflags->flags & NOT_BY_SETBY)
			if (match_simple(tklflags->set_by, tkl->set_by))
				return 0;
		if (tklflags->flags & BY_ID)
			if (!match_simple(tklflags->id, tkl->id))
				return 0;
		if (tklflags->flags & NOT_BY_ID)
			if (match_simple(tklflags->id, tkl->id))
				return 0;
		if (TKLIsServerBan(tkl))
		{
			if (tklflags->flags & BY_MASK)
			{
				if (!match_simple(tklflags->mask, make_user_host(tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask)))
					return 0;
			}
			if (tklflags->flags & NOT_BY_MASK)
			{
				if (match_simple(tklflags->mask, make_user_host(tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask)))
					return 0;
			}
			if (tklflags->flags & BY_REASON)
				if (!match_simple(tklflags->reason, tkl->ptr.serverban->reason))
					return 0;
			if (tklflags->flags & NOT_BY_REASON)
				if (match_simple(tklflags->reason, tkl->ptr.serverban->reason))
					return 0;
		} else
		if (TKLIsNameBan(tkl))
		{
			if (tklflags->flags & BY_MASK)
			{
				if (!match_simple(tklflags->mask, tkl->ptr.nameban->name))
					return 0;
			}
			if (tklflags->flags & NOT_BY_MASK)
			{
				if (match_simple(tklflags->mask, tkl->ptr.nameban->name))
					return 0;
			}
			if (tklflags->flags & BY_REASON)
				if (!match_simple(tklflags->reason, tkl->ptr.nameban->reason))
					return 0;
			if (tklflags->flags & NOT_BY_REASON)
				if (match_simple(tklflags->reason, tkl->ptr.nameban->reason))
					return 0;
		} else
		if (TKLIsBanException(tkl))
		{
			if (tklflags->flags & BY_MASK)
			{
				if (!match_simple(tklflags->mask, make_user_host(tkl->ptr.banexception->usermask, tkl->ptr.banexception->hostmask)))
					return 0;
			}
			if (tklflags->flags & NOT_BY_MASK)
			{
				if (match_simple(tklflags->mask, make_user_host(tkl->ptr.banexception->usermask, tkl->ptr.banexception->hostmask)))
					return 0;
			}
			if (tklflags->flags & BY_REASON)
				if (!match_simple(tklflags->reason, tkl->ptr.banexception->reason))
					return 0;
			if (tklflags->flags & NOT_BY_REASON)
				if (match_simple(tklflags->reason, tkl->ptr.banexception->reason))
					return 0;
		}
	}

	/***** If we are still here then we have a match and will will send the STATS entry */
	if (TKLIsServerBan(tkl))
	{
		if (tkl->ptr.banexception->match)
		{
			/* Config-added: uses security groups - only KLINE at the moment */
			NameValuePrioList *m;
			for (m = tkl->ptr.serverban->match->printable_list; m; m = m->next)
			{
				sendnumeric(client, RPL_STATSGLINE, 'K', namevalue_nospaces(m),
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);

			}
		} else {
			char uhostbuf[BUFSIZE];
			char *uhost = tkl_uhost(tkl, uhostbuf, sizeof(uhostbuf), 0);
			if (tkl->type == (TKL_KILL | TKL_GLOBAL))
			{
				sendnumeric(client, RPL_STATSGLINE, 'G', uhost,
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
			} else
			if (tkl->type == (TKL_ZAP | TKL_GLOBAL))
			{
				sendnumeric(client, RPL_STATSGLINE, 'Z', uhost,
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
			} else
			if (tkl->type == (TKL_SHUN | TKL_GLOBAL))
			{
				sendnumeric(client, RPL_STATSGLINE, 's', uhost,
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
			} else
			if (tkl->type == (TKL_KILL))
			{
				sendnumeric(client, RPL_STATSGLINE, 'K', uhost,
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
			} else
			if (tkl->type == (TKL_ZAP))
			{
				sendnumeric(client, RPL_STATSGLINE, 'z', uhost,
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, tkl->hits, (long long)tkl->lasthit, spamfilter_id_str, id_str, tkl->ptr.serverban->reason);
			}
		}
	} else
	if (TKLIsSpamfilter(tkl))
	{
		sendnumeric(client, RPL_STATSSPAMF,
			(tkl->type & TKL_GLOBAL) ? 'F' : 'f',
			unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type),
			spamfilter_target_inttostring(tkl->ptr.spamfilter->target),
			ban_actions_to_string(tkl->ptr.spamfilter->action),
			(tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
			(long long)(TStime() - tkl->set_at),
			(long long)tkl->ptr.spamfilter->tkl_duration,
			tkl->ptr.spamfilter->tkl_reason,
			tkl->set_by,
			tkl->hits,
			tkl->ptr.spamfilter->hits_except,
			(long long)tkl->lasthit,
			(long long)tkl->ptr.spamfilter->lasthit_except,
			id_str,
			tkl->ptr.spamfilter->match->str);
		if (para && !strcasecmp(para, "del"))
		{
			if (!(tkl->type & TKL_GLOBAL))
				sendtxtnumeric(client, "This spamfilter is stored in the configuration file and cannot be removed with /SPAMFILTER del");
			else
				sendtxtnumeric(client, "To delete this spamfilter, use /SPAMFILTER del %s", tkl->id[0] ? tkl->id : spamfilter_fallback_id(tkl));
			sendtxtnumeric(client, "-");
		}
	} else
	if (TKLIsNameBan(tkl))
	{
		sendnumeric(client, RPL_STATSQLINE,
		            (tkl->type & TKL_GLOBAL) ? 'Q' : 'q',
		            tkl->ptr.nameban->name,
		            (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
		            (long long)(TStime() - tkl->set_at),
		            tkl->set_by,
		            tkl->hits,
		            (long long)tkl->lasthit,
		            id_str,
		            tkl->ptr.nameban->reason);
	} else
	if (TKLIsBanException(tkl))
	{
		if (tkl->ptr.banexception->match)
		{
			/* Config-added: uses security groups */
			NameValuePrioList *m;
			for (m = tkl->ptr.banexception->match->printable_list; m; m = m->next)
			{
				sendnumeric(client, RPL_STATSEXCEPTTKL, namevalue_nospaces(m),
					   tkl->ptr.banexception->bantypes,
					   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
					   (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason);
			}
		} else {
			/* IRC-added: uses simple user/host mask */
			char uhostbuf[BUFSIZE];
			char *uhost = tkl_uhost(tkl, uhostbuf, sizeof(uhostbuf), 0);
			sendnumeric(client, RPL_STATSEXCEPTTKL, uhost,
				   tkl->ptr.banexception->bantypes,
				   (tkl->expire_at != 0) ? (long long)(tkl->expire_at - TStime()) : 0,
				   (long long)(TStime() - tkl->set_at), tkl->set_by, id_str, tkl->ptr.banexception->reason);
		}
	} else
	{
		/* That's weird, unknown TKL type */
		return 0;
	}
	return 1;
}

/* TKL Stats. This is used by /STATS gline and all the others */
void _tkl_stats(Client *client, int type, const char *para, int *cnt)
{
	TKL *tk;
	TKLFlag tklflags;
	int index, index2;

	if ((max_stats_matches > 0) && (*cnt >= max_stats_matches))
		return;

	if (!BadPtr(para))
		parse_stats_params(para, &tklflags);

	/* First the IP hashed entries (if applicable).. */
	index = tkl_ip_hash_type(tkl_typetochar(type));
	if (index >= 0)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			for (tk = tklines_ip_hash[index][index2]; tk; tk = tk->next)
			{
				if (type && tk->type != type)
					continue;
				if (tkl_stats_matcher(client, type, para, &tklflags, tk))
				{
					*cnt += 1;
					if ((max_stats_matches > 0) && (*cnt >= max_stats_matches))
					{
						sendnumeric(client, ERR_TOOMANYMATCHES, "STATS", "too many matches (set::max-stats-matches)");
						sendnotice(client, "Consider searching on something more specific, eg '/STATS gline +m *.nl'. See '/STATS' (without parameters) for help.");
						return;
					}
				}
			}
		}
	}

	/* Then the normal entries... */
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tk = tklines[index]; tk; tk = tk->next)
		{
			if (type && tk->type != type)
				continue;
			if (tkl_stats_matcher(client, type, para, &tklflags, tk))
			{
				*cnt += 1;
				if ((max_stats_matches > 0) && (*cnt >= max_stats_matches))
				{
					sendnumeric(client, ERR_TOOMANYMATCHES, "STATS", "too many matches (set::max-stats-matches)");
					sendnotice(client, "Consider searching on something more specific, eg '/STATS gline +m *.nl'. See '/STATS' (without parameters) for help.");
					return;
				}
			}
		}
	}

	if ((type == (TKL_SPAMF|TKL_GLOBAL)) && (!para || strcasecmp(para, "del")))
	{
		/* If requesting spamfilter stats and not spamfilter del, then suggest it. */
		sendnotice(client, "Tip: if you are looking for an easy way to remove a spamfilter, run '/SPAMFILTER del'.");
	}
}

/** Synchronize a TKL entry with the other server.
 * @param sender  The sender (eg: &me).
 * @param to      The remote server.
 * @param tkl     The TKL entry.
 */
void tkl_sync_send_entry(int add, Client *sender, Client *to, TKL *tkl)
{
	MessageTag *mtags = NULL;
	char typ;

	if (!(tkl->type & TKL_GLOBAL))
		return; /* nothing to sync */

	typ = tkl_typetochar(tkl->type);

	/* Carry the s2s-tkl/ fields (id, spamfilter_id) on add; del matches by mask so needs no tags. */
	if (add)
		tkl_add_s2s_mtags(tkl, &mtags);

	if (TKLIsServerBan(tkl))
	{
		sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld :%s", sender->name,
			   add ? '+' : '-',
			   typ,
			   (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) ? "%" : "",
			   *tkl->ptr.serverban->usermask ? tkl->ptr.serverban->usermask : "*",
			   tkl->ptr.serverban->hostmask, tkl->set_by,
			   (long long)tkl->expire_at, (long long)tkl->set_at,
			   tkl->ptr.serverban->reason);
	} else
	if (TKLIsNameBan(tkl))
	{
		sendto_one(to, mtags, ":%s TKL %c %c %c %s %s %lld %lld :%s", sender->name,
			   add ? '+' : '-',
			   typ,
			   tkl->ptr.nameban->hold ? 'H' : '*',
			   tkl->ptr.nameban->name,
			   tkl->set_by,
			   (long long)tkl->expire_at, (long long)tkl->set_at,
			   tkl->ptr.nameban->reason);
	} else
	if (TKLIsSpamfilter(tkl))
	{
		sendto_one(to, mtags, ":%s TKL %c %c %s %c %s %lld %lld %lld %s %s :%s", sender->name,
			   add ? '+' : '-',
			   typ,
			   spamfilter_target_inttostring(tkl->ptr.spamfilter->target),
			   banact_valtochar(tkl->ptr.spamfilter->action->action),
			   tkl->set_by,
			   (long long)tkl->expire_at, (long long)tkl->set_at,
			   (long long)tkl->ptr.spamfilter->tkl_duration, tkl->ptr.spamfilter->tkl_reason,
			   unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type),
			   tkl->ptr.spamfilter->match->str);
	} else
	if (TKLIsBanException(tkl))
	{
		sendto_one(to, mtags, ":%s TKL %c %c %s%s %s %s %lld %lld %s :%s", sender->name,
			   add ? '+' : '-',
			   typ,
			   (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) ? "%" : "",
			   *tkl->ptr.banexception->usermask ? tkl->ptr.banexception->usermask : "*",
			   tkl->ptr.banexception->hostmask, tkl->set_by,
			   (long long)tkl->expire_at, (long long)tkl->set_at,
			   tkl->ptr.banexception->bantypes,
			   tkl->ptr.banexception->reason);
	} else
	{
		unreal_log(ULOG_FATAL, "tkl", "BUG_TKL_SYNC_SEND_ENTRY", NULL,
			   "[BUG] tkl_sync_send_entry() called, but unknown type: $tkl.type_string ($tkl_type_int)",
			   log_data_tkl("tkl", tkl),
			   log_data_integer("tkl_type_int", typ));
		abort();
	}

	safe_free_message_tags(mtags);
}

/** Broadcast a TKL entry.
 * @param sender  The sender, eg &me
 * @param skip    The client to skip, eg 'client' or NULL.
 * @param tkl     The TKL entry to synchronize with the other servers.
 */
void tkl_broadcast_entry(int add, Client *sender, Client *skip, TKL *tkl)
{
	Client *acptr;

	/* Silly fix for RPC calls that lead to broadcasts from this sender */
	if (!IsUser(sender) && !IsServer(sender))
		sender = &me;

	list_for_each_entry(acptr, &server_list, special_node)
	{
		if (skip && acptr == skip->direction)
			continue;

		tkl_sync_send_entry(add, sender, acptr, tkl);
	}
}

/** Synchronize all TKL entries with this server.
 * @param client The server to synchronize with.
 */
void _tkl_sync(Client *client)
{
	TKL *tkl;
	int index, index2;

	/* First, hashed entries.. */
	for (index = 0; index < TKLIPHASHLEN1; index++)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
			{
				tkl_sync_send_entry(1, &me, client, tkl);
			}
		}
	}

	/* Then, regular entries.. */
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tkl = tklines[index]; tkl; tkl = tkl->next)
		{
			tkl_sync_send_entry(1, &me, client, tkl);
		}
	}
}

/** Find a server ban TKL - only used to prevent duplicates and for deletion */
TKL *_find_tkl_serverban(int type, const char *usermask, const char *hostmask, int softban)
{
	char tpe = tkl_typetochar(type);
	TKL *head, *tkl;

	if (!TKLIsServerBanType(type))
		abort();

	head = tkl_find_head(tpe, hostmask, tklines[tkl_hash(tpe)]);
	for (tkl = head; tkl; tkl = tkl->next)
	{
		if (tkl->type == type)
		{
			if (tkl->ptr.serverban->match)
				continue;
			if (!strcasecmp(tkl->ptr.serverban->hostmask, hostmask) &&
			    !strcasecmp(tkl->ptr.serverban->usermask, usermask))
			{
				/* And an extra check for soft/hard ban mismatches.. */
				if ((tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) == softban)
					return tkl;
			}
		}
	}
	return NULL; /* Not found */
}

/** Find a ban exception TKL - only used to prevent duplicates and for deletion */
TKL *_find_tkl_banexception(int type, const char *usermask, const char *hostmask, int softban)
{
	char tpe = tkl_typetochar(type);
	TKL *head, *tkl;

	if (!TKLIsBanExceptionType(type))
		abort();

	head = tkl_find_head(tpe, hostmask, tklines[tkl_hash(tpe)]);
	for (tkl = head; tkl; tkl = tkl->next)
	{
		if (tkl->type == type)
		{
			if (!strcasecmp(tkl->ptr.banexception->hostmask, hostmask) &&
			    !strcasecmp(tkl->ptr.banexception->usermask, usermask))
			{
				/* And an extra check for soft/hard ban mismatches.. */
				if ((tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT) == softban)
					return tkl;
			}
		}
	}
	return NULL; /* Not found */
}

/** Find a name ban TKL (qline) - only used to prevent duplicates and for deletion */
TKL *_find_tkl_nameban(int type, const char *name, int hold)
{
	char tpe = tkl_typetochar(type);
	TKL *tkl;

	if (!TKLIsNameBanType(type))
		abort();

	for (tkl = tklines[tkl_hash(tpe)]; tkl; tkl = tkl->next)
	{
		if ((tkl->type == type) && !strcasecmp(tkl->ptr.nameban->name, name))
			return tkl;
	}
	return NULL; /* Not found */
}

/** Find a spamfilter TKL - only used to prevent duplicates and for deletion */
TKL *_find_tkl_spamfilter(int type, const char *match_string, BanActionValue action, unsigned short target)
{
	char tpe = tkl_typetochar(type);
	TKL *tkl;

	if (!TKLIsSpamfilterType(type))
		abort();

	for (tkl = tklines[tkl_hash(tpe)]; tkl; tkl = tkl->next)
	{
		if ((type == tkl->type) &&
		    !strcmp(match_string, tkl->ptr.spamfilter->match->str) &&
		    (action == tkl->ptr.spamfilter->action->action) &&
		    (target == tkl->ptr.spamfilter->target))
		{
			return tkl;
		}
	}
	return NULL; /* Not found */
}

/** Send a notice to opers about the TKL that is being added */
void _sendnotice_tkl_add(TKL *tkl)
{
	/* Don't show notices for temporary nick holds (issued by services) */
	if (TKLIsNameBan(tkl) && tkl->ptr.nameban->hold)
		return;

	if (TKLIsServerBan(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL,
			   "$tkl.type_string added: '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id $spamfilter_id",
			   log_data_tkl("tkl", tkl),
			   log_data_optional_name_value("id", "id", tkl->id),
			   log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->spamfilter_id));
	} else
	if (TKLIsNameBan(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL,
			   "$tkl.type_string added: '$tkl' [reason: $tkl.reason] [by: $tkl.set_by] [duration: $tkl.duration_string] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_optional_name_value("id", "id", tkl->id));
	} else
	if (TKLIsSpamfilter(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL,
			   "Spamfilter added: '$tkl' [type: $tkl.match_type] [targets: $tkl.spamfilter_targets] "
			   "[action: $tkl.ban_action] [reason: $tkl.reason] [by: $tkl.set_by] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_optional_name_value("id", "id", tkl->id));
	} else
	if (TKLIsBanException(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_ADD", NULL,
			   "$tkl.type_string added: '$tkl' [types: $tkl.exception_types] [by: $tkl.set_by] [duration: $tkl.duration_string] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_optional_name_value("id", "id", tkl->id));
	} else
	{
		unreal_log(ULOG_ERROR, "tkl", "BUG_UNKNOWN_TKL", NULL,
		           "[BUG] TKL added of unknown type, unhandled in sendnotice_tkl_add()!!!!");
	}
}

/** Send a notice to opers about the TKL that is being deleted */
void _sendnotice_tkl_del(const char *removed_by, TKL *tkl)
{
	/* Don't show notices for temporary nick holds (issued by services) */
	if (TKLIsNameBan(tkl) && tkl->ptr.nameban->hold)
		return;

	if (TKLIsServerBan(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL,
			   "$tkl.type_string removed: '$tkl' [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string] $id $spamfilter_id",
			   log_data_tkl("tkl", tkl),
			   log_data_string("removed_by", removed_by),
			   log_data_optional_name_value("id", "id", tkl->id),
			   log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->spamfilter_id));
	} else
	if (TKLIsNameBan(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL,
			   "$tkl.type_string removed: '$tkl' [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_string("removed_by", removed_by),
			   log_data_optional_name_value("id", "id", tkl->id));
	} else
	if (TKLIsSpamfilter(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL,
			   "Spamfilter removed: '$tkl' [type: $tkl.match_type] [targets: $tkl.spamfilter_targets] "
			   "[action: $tkl.ban_action] [reason: $tkl.reason] [by: $removed_by] [set at: $tkl.set_at_string] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_string("removed_by", removed_by),
			   log_data_optional_name_value("id", "id", tkl->id));
	} else
	if (TKLIsBanException(tkl))
	{
		unreal_log(ULOG_INFO, "tkl", "TKL_DEL", NULL,
			   "$tkl.type_string removed: '$tkl' [types: $tkl.exception_types] [by: $removed_by] [set at: $tkl.set_at_string] $id",
			   log_data_tkl("tkl", tkl),
			   log_data_string("removed_by", removed_by),
			   log_data_optional_name_value("id", "id", tkl->id));
	} else
	{
		unreal_log(ULOG_ERROR, "tkl", "BUG_UNKNOWN_TKL", NULL,
		           "[BUG] TKL removed of unknown type, unhandled in sendnotice_tkl_del()!!!!");
	}
}

/** Called when a TKL is added by a remote user, local user, RPC user, ..
 * (but not when a TKL is added via the config)
 */
void _tkl_added(Client *client, TKL *tkl)
{
	RunHook(HOOKTYPE_TKL_ADD, client, tkl);

	sendnotice_tkl_add(tkl);

	/* spamfilter 'warn' action is special */
	if ((tkl->type & TKL_SPAMF) &&
	    has_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_WARN) &&
	    (tkl->ptr.spamfilter->target & SPAMF_USER))
	{
		spamfilter_check_users(tkl);
	}

	/* Ban checking executes during run loop for efficiency */
	loop.do_bancheck = 1;

	if (tkl->type & TKL_GLOBAL)
		tkl_broadcast_entry(1, client, client, tkl);
}

/** Add a TKL using the TKL layer. See cmd_tkl for parv[] and protocol documentation. */
CMD_FUNC(cmd_tkl_add)
{
	TKL *tkl;
	int type;
	time_t expire_at, set_at;
	const char *set_by;
	char tkl_entry_exists = 0;

	/* we rely on servers to be failsafe.. */
	if (!IsServer(client) && !IsMe(client))
		return;

	if (parc < 9)
		return;

	type = tkl_chartotype(parv[2][0]);
	if (!type)
		return;

	/* All TKL types have the following fields in common when adding:
	 * parv[5]: set_by
	 * parv[6]: expire_at
	 * parv[7]: set_at
	 * ... so we validate them here at the beginning.
	 */

	set_by = parv[5];
	expire_at = atol(parv[6]);
	set_at = atol(parv[7]);

	/* Validate set and expiry time */
	if ((set_at < 0) || !short_date(set_at, NULL))
	{
		unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
			"Invalid TKL entry from $client: "
			"The set-at time is out of range ($set_at). Clock on other server incorrect or bogus entry.",
			log_data_integer("set_at", set_at));
		return;
	}
	if ((expire_at < 0) || !short_date(expire_at, NULL))
	{
		unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
			"Invalid TKL entry from $client: "
			"The expire-at time is out of range ($expire_at). Clock on other server incorrect or bogus entry.",
			log_data_integer("expire_at", expire_at));
		return;
	}

	/* Now comes type-specific validation
	 * and we check if the TKL entry already exists and needs updating too.
	 */

	if (TKLIsServerBanType(type))
	{
		/* Validate server ban TKL fields */
		int softban = 0;
		const char *usermask = parv[3];
		const char *hostmask = parv[4];
		const char *reason = parv[8];

		/* Some simple validation on usermask and hostmask:
		 * may not contain an @. Yeah, some services or self-written
		 * linked servers are known to have sent this in the past.
		 */
		if (strchr(usermask, '@') || strchr(hostmask, '@'))
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
				"Invalid TKL entry from $client: "
				"Invalid user@host $usermask@$hostmask",
				log_data_string("usermask", usermask),
				log_data_string("hostmask", hostmask));
			return;
		}

		/* In case of a soft ban, strip the percent sign early,
		 * so parv[3] (username) is really the username without any prefix.
		 * Set the 'softban' flag if this is the case.
		 */
		if (*usermask == '%')
		{
			usermask++;
			softban = 1;
		}

		tkl = find_tkl_serverban(type, usermask, hostmask, softban);
		if (tkl)
		{
			tkl_entry_exists = 1;
		} else {
			tkl = tkl_add_serverban(type, usermask, hostmask, NULL, reason,
			                        set_by, expire_at, set_at, softban, 0);
		}
	} else
	if (TKLIsBanExceptionType(type))
	{
		/* Validate ban exception TKL fields */
		int softban = 0;
		const char *usermask = parv[3];
		const char *hostmask = parv[4];
		const char *bantypes = parv[8];
		const char *reason;

		if (parc < 10)
			return;

		reason = parv[9];

		/* Some simple validation on usermask and hostmask:
		 * may not contain an @. Yeah, some services or self-written
		 * linked servers are known to have sent this in the past.
		 */
		if (strchr(usermask, '@') || strchr(hostmask, '@'))
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
				"Invalid TKL entry from $client: "
				"Invalid TKL except user@host $usermask@$hostmask",
				log_data_string("usermask", usermask),
				log_data_string("hostmask", hostmask));
			return;
		}

		/* In case of a soft ban, strip the percent sign early,
		 * so parv[3] (username) is really the username without any prefix.
		 * Set the 'softban' flag if this is the case.
		 */
		if (*usermask == '%')
		{
			usermask++;
			softban = 1;
		}

		/* At this moment we do not validate 'bantypes' since a missing
		 * or wrong type does not cause harm anyway.
		 */
		tkl = find_tkl_banexception(type, usermask, hostmask, softban);
		if (tkl)
		{
			tkl_entry_exists = 1;
		} else {
			tkl = tkl_add_banexception(type, usermask, hostmask, NULL, reason,
			                           set_by, expire_at, set_at, softban, bantypes, 0);
		}
	} else
	if (TKLIsNameBanType(type))
	{
		/* Validate name ban TKL fields */
		int hold = 0;
		const char *name = parv[4];
		const char *reason = parv[8];

		if (*parv[3] == 'H')
			hold = 1;

		tkl = find_tkl_nameban(type, name, hold);
		if (tkl)
		{
			tkl_entry_exists = 1;
		} else {
			tkl = tkl_add_nameban(type, name, hold, reason, set_by, expire_at,
			                      set_at, 0);
		}
	} else
	if (TKLIsSpamfilterType(type))
	{
		/* Validate spamfilter-specific TKL fields */
		MatchType match_method;
		const char *match_string;
		Match *m; /* compiled match_string */
		time_t tkl_duration;
		const char *tkl_reason;
		BanActionValue action;
		unsigned short target;
		/* helper variables */
		const char *err;

		if (parc < 12)
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
				"Invalid TKL entry from $client: "
				"Spamfilter with too few parameters. Running very old UnrealIRCd protocol (3.2.X?)");
			return;
		}

		match_string = parv[11];

		match_method = unreal_match_method_strtoval(parv[10]);
		if (match_method == 0)
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
				"Invalid TKL entry from $client: "
				"Spamfilter '$spamfilter_string' has unknown match-type '$spamfilter_type'",
				log_data_string("spamfilter_string", match_string),
				log_data_string("spamfilter_type", parv[10]));
			return;
		}

		if (!(target = spamfilter_gettargets(parv[3], NULL)))
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
				"Invalid TKL entry from $client: "
				"Spamfilter '$spamfilter_string' has unknown targets '$spamfilter_targets'",
				log_data_string("spamfilter_string", match_string),
				log_data_string("spamfilter_targets", parv[3]));
			return;
		}

		if (!(action = banact_chartoval(*parv[4])) || banact_config_only(action))
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
				"Invalid TKL entry from $client: "
				"Spamfilter '$spamfilter_string' has unknown action '$spamfilter_action'",
				log_data_string("spamfilter_string", match_string),
				log_data_string("spamfilter_action", parv[4]));
			return;
		}

		tkl_duration = config_checkval(parv[8], CFG_TIME);
		tkl_reason = parv[9];

		tkl = find_tkl_spamfilter(type, match_string, action, target);

		if (tkl)
		{
			tkl_entry_exists = 1;
		} else {
			m = unreal_create_match(match_method, match_string, &err);
			if (!m)
			{
				unreal_log(ULOG_WARNING, "tkl", "TKL_ADD_INVALID", client,
					"Invalid TKL entry from $client: "
					"Spamfilter '$spamfilter_string': regex does not compile: $spamfilter_regex_error",
					log_data_string("spamfilter_string", match_string),
					log_data_string("spamfilter_regex_error", err));
				return;
			}
			tkl = tkl_add_spamfilter(type, NULL, target, banact_value_to_struct(action), m, NULL, NULL,
			                         set_by, expire_at, set_at,
			                         tkl_duration, tkl_reason, INPUT_CONVERSION_DEFAULT, 0, 0);
		}
	} else
	{
		/* Unhandled, should never happen */
		abort();
	}

	if (!tkl)
		return;

	if (tkl_entry_exists)
	{
		/* Let's see if we need to update the existing entry.
		 * Note that we only update common fields,
		 * which is acceptable to me. -- Syzop
		 */
		const char *incoming_id = tkl_s2s_mtag_value(recv_mtags, "id");
		const char *incoming_spamfilter_id = NULL;
		int changed = 0;

		/* id: an assigned id beats a blank one; two assigned ids tie-break by older
		 * set_at, then lexicographically. The "spamfilter_id" travels with the winning "id".
		 * This is done before set_at is lowered below, so the tie-break sees the original set_at.
		 */
		if (!BadPtr(incoming_id) && (strlen(incoming_id) < sizeof(tkl->id)) && strcmp(tkl->id, incoming_id))
		{
			if (!tkl->id[0] ||
			    (set_at < tkl->set_at) ||
			    ((set_at == tkl->set_at) && (strcmp(incoming_id, tkl->id) < 0)))
			{
				strlcpy(tkl->id, incoming_id, sizeof(tkl->id));
				incoming_spamfilter_id = tkl_s2s_mtag_value(recv_mtags, "spamfilter_id");
				if (!BadPtr(incoming_spamfilter_id) && (strlen(incoming_spamfilter_id) < sizeof(tkl->spamfilter_id)))
					strlcpy(tkl->spamfilter_id, incoming_spamfilter_id, sizeof(tkl->spamfilter_id));
				else
					tkl->spamfilter_id[0] = '\0';
				changed = 1;
			}
		}

		if ((set_at < tkl->set_at) || (expire_at != tkl->expire_at) || strcmp(tkl->set_by, parv[5]))
		{
			/* here's how it goes:
			 * set_at: oldest wins
			 * expire_at: longest wins
			 * set_by: highest strcmp wins
			 *
			 * We broadcast the result of this back to all servers except
			 * sptr->direction, because that side will do the same thing and
			 * send it back to his servers (except us)... no need for a
			 * double networkwide flood ;p. -- Syzop
			 */
			tkl->set_at = MIN(tkl->set_at, set_at);

			if (!tkl->expire_at || !expire_at)
				tkl->expire_at = 0;
			else
				tkl->expire_at = MAX(tkl->expire_at, expire_at);

			if (strcmp(tkl->set_by, parv[5]) < 0)
				safe_strdup(tkl->set_by, parv[5]);

			changed = 1;
		}

		if (changed && (type & TKL_GLOBAL))
			tkl_broadcast_entry(1, client, client, tkl);
		return;
	}

	/* New entry: assign its unique id. Honor a supplied s2s-tkl/id (and spamfilter_id) from a
	 * remote server or services; if none and we are the originating server, generate a random one;
	 * otherwise leave it blank (eg the tag was dropped by an old server in the path).
	 */
	tkl_extract_s2s_mtags(tkl, recv_mtags);
	if (!tkl->id[0] && IsMe(client))
		tkl_generate_id(tkl);

	tkl_added(client, tkl);
}

/** Delete a TKL using the TKL layer. See cmd_tkl for parv[] and protocol documentation. */
CMD_FUNC(cmd_tkl_del)
{
	TKL *tkl;
	int type;
	const char *removed_by;

	if (!IsServer(client) && !IsMe(client))
		return;

	if (parc < 6)
		return;

	type = tkl_chartotype(parv[2][0]);
	if (type == 0)
		return;

	removed_by = parv[5];

	if (TKLIsServerBanType(type))
	{
		const char *usermask = parv[3];
		const char *hostmask = parv[4];
		int softban = 0;

		if (*usermask == '%')
		{
			usermask++;
			softban = 1;
		}

		tkl = find_tkl_serverban(type, usermask, hostmask, softban);
	}
	else if (TKLIsBanExceptionType(type))
	{
		const char *usermask = parv[3];
		const char *hostmask = parv[4];
		int softban = 0;
		/* other parameters are ignored */

		if (*usermask == '%')
		{
			usermask++;
			softban = 1;
		}

		tkl = find_tkl_banexception(type, usermask, hostmask, softban);
	}
	else if (TKLIsNameBanType(type))
	{
		int hold = 0;
		const char *name = parv[4];

		if (*parv[3] == 'H')
			hold = 1;
		tkl = find_tkl_nameban(type, name, hold);
	}
	else if (TKLIsSpamfilterType(type))
	{
		const char *match_string;
		unsigned short target;
		BanActionValue action;

		if (parc < 9)
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_DEL_INVALID", client,
				"Invalid TKL deletion request from $client: "
				"Spamfilter with too few parameters. Running very old UnrealIRCd protocol (3.2.X?)");
			return; /* bogus */
		}
		if (parc >= 12)
			match_string = parv[11];
		else if (parc >= 11)
			match_string = parv[10];
		else
			match_string = parv[8];

		if (!(target = spamfilter_gettargets(parv[3], NULL)))
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_DEL_INVALID", client,
				"Invalid TKL deletion request from $client: "
				"Spamfilter '$spamfilter_string' has unknown targets '$spamfilter_targets'",
				log_data_string("spamfilter_string", match_string),
				log_data_string("spamfilter_targets", parv[3]));
			return;
		}

		if (!(action = banact_chartoval(*parv[4])))
		{
			unreal_log(ULOG_WARNING, "tkl", "TKL_DEL_INVALID", client,
				"Invalid TKL deletion request from $client: "
				"Spamfilter '$spamfilter_string' has unknown action '$spamfilter_action'",
				log_data_string("spamfilter_string", match_string),
				log_data_string("spamfilter_action", parv[4]));
			return;
		}
		tkl = find_tkl_spamfilter(type, match_string, action, target);
	} else
	{
		/* This can never happen, unless someone added a TKL type
		 * to UnrealIRCd but forgot to add the removal code :D.
		 */
		abort();
	}

	if (!tkl)
		return; /* Item not found, nothing to remove. */

	if (tkl->flags & TKL_FLAG_CONFIG)
		return; /* Item is in the configuration file (persistent) */

	/* broadcast remove msg to opers... */
	sendnotice_tkl_del(removed_by, tkl);

	if (type & TKL_SHUN)
		tkl_check_local_remove_shun(tkl);

	RunHook(HOOKTYPE_TKL_DEL, client, tkl);

	if (type & TKL_GLOBAL)
	{
		/* This is a bit of a hack for #5629. Will consider real fix post-release. */
		safe_strdup(tkl->set_by, removed_by);
		tkl_broadcast_entry(0, client, client, tkl);
	}

	if (TKLIsBanException(tkl))
	{
		/* Since an exception has been removed we have to re-check if
		 * any connected user is now matched by a ban.
		 * Set flag here, actual checking takes place in main loop.
		 */
		loop.do_bancheck = 1;
	}

	tkl_del_line(tkl);
}

/** TKL command: server to server handling of *LINEs and SPAMFILTERs.
 * HISTORY:
 * This was originall called Timed KLines, but today it's
 * used by various *line types eg: zline, gline, gzline, shun,
 * but also by spamfilter etc...
 * DOCUMENTATION
 * See (also) https://www.unrealircd.org/docs/Server_protocol:TKL_command
 * USAGE:
 * This routine is used both internally by the ircd (to
 * for example add local klines, zlines, etc) and over the
 * network (glines, gzlines, spamfilter, etc).
 *
 *           serverban  serverban  spamfilter      spamfilter         sqline:    ban exception:
 *           add:       remove:    remove in U4:   with TKLEXT2:
 * parv[ 1]: +          -          -               +/-                +/-        +/-
 * parv[ 2]: type       type       type            type               type       type
 * parv[ 3]: user       user       target          target             hold       user
 * parv[ 4]: host       host       action          action             host       host
 * parv[ 5]: set_by     removedby  (un)set_by      set_by/unset_by    set_by     set_by
 * parv[ 6]: expire_at             expire_at (0)   expire_at (0)      expire_at  expire_at
 * parv[ 7]: set_at                set_at          set_at             set_at     set_at
 * parv[ 8]: reason                regex           tkl duration       reason     except_type
 * parv[ 9]:                                       tkl reason [A]                reason
 * parv[10]:                                       match-type [B]
 * parv[11]:                                       match-string [C]
 *
 * [A] tkl reason field must be escaped by caller [eg: use unreal_encodespace()
 *     if cmd_tkl is called internally].
 * [B] match-type must be one of: regex, simple.
 * [C] Could be a regex or a regular string with wildcards, depending on [B]
 */
CMD_FUNC(_cmd_tkl)
{
	if (!IsServer(client) && !IsOper(client) && !IsMe(client))
		return;

	if (parc < 2)
		return;

	switch (*parv[1])
	{
		case '+':
			CALL_CMD_FUNC(cmd_tkl_add);
			break;
		case '-':
			CALL_CMD_FUNC(cmd_tkl_del);
			break;
		default:
			break;
	}
}

/** Configure the username/hostname TKL layer based on the BAN_TARGET_* configuration */
void ban_target_to_tkl_layer(BanTarget ban_target, BanActionValue action, Client *client, const char **tkl_username, const char **tkl_hostname)
{
	static char username[USERLEN+1];
	static char hostname[HOSTLEN+8];

	if ((action == BAN_ACT_ZLINE) || (action == BAN_ACT_GZLINE))
		ban_target = BAN_TARGET_IP; /* The only possible choice with ZLINE/GZLINE, other info is unavailable */

	if (ban_target == BAN_TARGET_ACCOUNT)
	{
		if (IsLoggedIn(client) && (*client->user->account != ':'))
		{
			/* Place a ban on ~a:Accountname */
			strlcpy(username, "~a:", sizeof(username));
			strlcpy(hostname, client->user->account, sizeof(hostname));
			*tkl_username = username;
			*tkl_hostname = hostname;
			return;
		}
		ban_target = BAN_TARGET_IP; /* fallback */
	} else
	if (ban_target == BAN_TARGET_CERTFP)
	{
		const char *fp = moddata_client_get(client, "certfp");
		if (fp)
		{
			/* Place a ban on ~S:sha256sumofclientcertificate */
			strlcpy(username, "~S:", sizeof(username));
			strlcpy(hostname, fp, sizeof(hostname));
			*tkl_username = username;
			*tkl_hostname = hostname;
			return;
		}
		ban_target = BAN_TARGET_IP; /* fallback */
	}

	/* Below we deal with the more common choices... */

	/* First, set the username */
	if (((ban_target == BAN_TARGET_USERIP) || (ban_target == BAN_TARGET_USERHOST)) && strcmp(client->ident, "unknown"))
		strlcpy(username, client->ident, sizeof(username));
	else
		strlcpy(username, "*", sizeof(username));

	/* Now set the host-portion of the TKL */
	if (((ban_target == BAN_TARGET_HOST) || (ban_target == BAN_TARGET_USERHOST)) && client->user && *client->user->realhost)
		strlcpy(hostname, client->user->realhost, sizeof(hostname));
	else
		strlcpy(hostname, GetIP(client), sizeof(hostname));

	*tkl_username = username;
	*tkl_hostname = hostname;
}

void ban_act_set(Client *client, BanAction *action)
{
	Tag *tag;

	if (!MyConnect(client))
		return;

	if (!strcmp(action->var, "REPUTATION"))
	{
		ban_act_set_reputation(client, action);
		return;
	}

	tag = find_tag(client, action->var);
	if (!tag)
		tag = add_tag(client, action->var, 0);
	switch (action->var_action)
	{
		case VAR_ACT_SET:
			tag->value = action->value;
			break;
		case VAR_ACT_INCREASE:
			tag->value += action->value;
			if (tag->value > 65535)
				tag->value = 65535;
			break;
		case VAR_ACT_DECREASE:
			tag->value -= action->value;
			if (tag->value < 0)
				tag->value = 0;
			break;
		default:
			abort();
	}
	bump_tag_serial(client);
	unreal_log(ULOG_DEBUG, "tkl", "TAG_CLIENT", client,
	           "Client $nick tag $tag is now set to $value",
	           log_data_string("nick", client->name),
	           log_data_string("tag", tag->name),
	           log_data_integer("value", tag->value));
}

void ban_action_run_all_sets_and_stops(Client *client, BanAction *action, int *stopped)
{
	*stopped = 0;
	for (; action; action = action->next)
	{
		if (action->action == BAN_ACT_SET)
			ban_act_set(client, action);
		if (action->action == BAN_ACT_STOP)
		{
			*stopped = 1;
			break;
		}
	}
}

/** Take an action on the user, such as banning or killing.
 * @author Bram Matthys (Syzop), 2003-present
 * @param client             The client which is affected.
 * @param action             The type of ban (one of BAN_ACT_*).
 * @param reason             The ban reason.
 * @param duration           The ban duration in seconds.
 * @param take_action_flags  One of TAKE_ACTION_*
 * @note This function assumes that client is a locally connected user.
 * @retval 0	user is exempt or no action needs to be taken for other reasons (eg only var setting)
 * @retval BAN_ACT_*	the highest BAN_ACT_xxx value, eg BAN_ACT_BLOCK or BAN_ACT_GLINE, etc...
 * @note BAN_ACT_SET and BAN_ACT_REPORT are never returned since they are no 'real actions' (have no impact on user)
 * @note Be sure to check IsDead(client) if return value is 1 and you are
 *       considering to continue processing.
 */
static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id);

int _take_action(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped)
{
	return take_action_ex(client, actions, reason, duration, take_action_flags, stopped, NULL);
}

/** Internal variant of take_action() that records the spamfilter id on any
 * *LINE/SHUN it creates, for the gline->spamfilter trace. Kept internal so the public
 * take_action() efunc stays free of this spamfilter-specific concept.
 */
static int take_action_ex(Client *client, BanAction *actions, const char *reason, long duration, int take_action_flags, int *stopped, const char *spamfilter_id)
{
	BanAction *action;
	int previous_highest = 0;
	int highest = 0;

	if (stopped)
		*stopped = 0;

	for (action = actions; action; action = action->next)
	{
		/* If this is a soft action and the user is logged in, then the ban does not apply. */
		if (IsSoftBanAction(action->action) && IsLoggedIn(client))
			return 0;

		previous_highest = highest;
		if ((action->action > highest) && (action->action != BAN_ACT_SET) && (action->action != BAN_ACT_REPORT))
			highest = action->action;

		switch(action->action)
		{
			case BAN_ACT_GZLINE:
			case BAN_ACT_GLINE:
			case BAN_ACT_SOFT_GLINE:
			case BAN_ACT_ZLINE:
			case BAN_ACT_KLINE:
			case BAN_ACT_SOFT_KLINE:
			case BAN_ACT_SHUN:
			case BAN_ACT_SOFT_SHUN:
			{
				char ip[128], user[USERLEN+3], mo[100], mo2[100];
				const char *tkllayer[9] = {
					me.name,	/*0  server.name */
					"+",		/*1  +|- */
					"?",		/*2  type */
					"*",		/*3  user */
					NULL,		/*4  host */
					NULL,
					NULL,		/*6  expire_at */
					NULL,		/*7  set_at */
					NULL		/*8  reason */
				};

				if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
					break;
				ban_target_to_tkl_layer(iConf.automatic_ban_target, action->action, client, &tkllayer[3], &tkllayer[4]);

				/* For soft bans we need to prefix the % in the username */
				if (IsSoftBanAction(action->action))
				{
					char tmp[USERLEN+3];
					snprintf(tmp, sizeof(tmp), "%%%s", tkllayer[3]);
					strlcpy(user, tmp, sizeof(user));
					tkllayer[3] = user;
				}

				if ((action->action == BAN_ACT_KLINE) || (action->action == BAN_ACT_SOFT_KLINE))
					tkllayer[2] = "k";
				else if (action->action == BAN_ACT_ZLINE)
					tkllayer[2] = "z";
				else if (action->action == BAN_ACT_GZLINE)
					tkllayer[2] = "Z";
				else if ((action->action == BAN_ACT_GLINE) || (action->action == BAN_ACT_SOFT_GLINE))
					tkllayer[2] = "G";
				else if ((action->action == BAN_ACT_SHUN) || (action->action == BAN_ACT_SOFT_SHUN))
					tkllayer[2] = "s";
				tkllayer[5] = me.name;
				if (!duration)
					strlcpy(mo, "0", sizeof(mo)); /* perm */
				else
					ircsnprintf(mo, sizeof(mo), "%lld", (long long)(duration + TStime()));
				ircsnprintf(mo2, sizeof(mo2), "%lld", (long long)TStime());
				tkllayer[6] = mo;
				tkllayer[7] = mo2;
				tkllayer[8] = reason;
				{
					MessageTag *m = tkl_spamfilter_id_mtag(spamfilter_id); /* NULL unless set by a spamfilter */
					cmd_tkl(NULL, &me, m, 9, tkllayer);
					safe_free_message_tags(m);
				}
				RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration);
				if ((action->action == BAN_ACT_SHUN) || (action->action == BAN_ACT_SOFT_SHUN))
				{
					find_shun(client);
					break;
				} /* else.. */
				if (!find_tkline_match(client, 0))
				{
					/* Not banned! revert the return value that we had in mind... */
					highest = previous_highest;
				}
				break;
			}
			case BAN_ACT_SOFT_KILL:
			case BAN_ACT_KILL:
				if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
					break;
				RunHookReturnInt(HOOKTYPE_TAKE_ACTION, !=99, client, action->action, reason, duration);
				exit_client(client, NULL, reason);
				break;
			case BAN_ACT_SOFT_TEMPSHUN:
			case BAN_ACT_TEMPSHUN:
				if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
					break;
				/* We simply mark this connection as shunned and do not add a ban record */
				unreal_log(ULOG_INFO, "tkl", "TKL_ADD_TEMPSHUN", &me,
					   "Temporary shun added on user $target.details [reason: $shun_reason] [by: $client]",
					   log_data_string("shun_reason", reason),
					   log_data_client("target", client));
				SetShunned(client);
				break;
			case BAN_ACT_REPORT:
				if (take_action_flags & TAKE_ACTION_SIMULATE_USER_ACTION)
					break;
				spamreport(client, client->ip, NULL, action->var, NULL);
				break;
			case BAN_ACT_SET:
				if (!(take_action_flags & TAKE_ACTION_SKIP_SET))
					ban_act_set(client, action);
				break;
			case BAN_ACT_STOP:
				if (stopped)
					*stopped = 1;
				break;
			default:
				/* (BAN_ACT_BLOCK, BAN_ACT_SOFT_BLOCK, BAN_ACT_WARN, etc...) */
				/* We don't actively do something, up to caller */
				break;
		}

		if (IsDead(client))
			break; /* stop processing actions */
	}

	return highest;
}

/** This function compares two spamfilters ('one' and 'two') and will return
 * a 'winner' based on which one has the strongest action.
 * If both have equal action then some additional logic is applied simply
 * to ensure we (almost) always return the same winner regardless of the
 * order of the spamfilters (which may differ between servers).
 */
TKL *choose_winning_spamfilter(TKL *one, TKL *two)
{
	int n;
	int highest_action_one;
	int highest_action_two;

	if (!TKLIsSpamfilter(one) || !TKLIsSpamfilter(two))
		abort();

	/* First, see if the action field differs... */
	highest_action_one = highest_ban_action(one->ptr.spamfilter->action);
	highest_action_two = highest_ban_action(two->ptr.spamfilter->action);
	if (highest_action_one > highest_action_two)
		return one;
	else if (highest_action_one < highest_action_two)
		return two;
	// else.. they are equal..

	/* Ok, try comparing the regex then.. */
	n = strcmp(one->ptr.spamfilter->match->str, two->ptr.spamfilter->match->str);
	if (n < 0)
		return one;
	if (n > 0)
		return two;

	/* Hmm.. regex is identical. Try the 'reason' field. */
	n = strcmp(one->ptr.spamfilter->tkl_reason, two->ptr.spamfilter->tkl_reason);
	if (n < 0)
		return one;
	if (n > 0)
		return two;

	/* Hmm.. 'reason' is identical as well.
	 * Make a final decision, could still be identical but would be unlikely.
	 */
	return (one->ptr.spamfilter->target > two->ptr.spamfilter->target) ? one : two;
}

/** Make user join the virus channel.
 * @param client  The user that was doing something bad.
 * @param tk    The TKL entry that matched this user.
 * @param type  The spamfilter type (SPAMF_*)
 *              TODO: Looks redundant?
 */
int _join_viruschan(Client *client, TKL *tkl, int type)
{
	const char *xparv[3];
	char chbuf[CHANNELLEN + 16], buf[2048];
	Channel *channel;
	int ret;

	snprintf(buf, sizeof(buf), "0,%s", SPAMFILTER_VIRUSCHAN);
	xparv[0] = NULL;
	xparv[1] = buf;
	xparv[2] = NULL;

	/* RECURSIVE CAUTION in case we ever add blacklisted chans */
	spamf_ugly_vchanoverride = 1;
	do_cmd(client, NULL, "JOIN", 2, xparv);
	spamf_ugly_vchanoverride = 0;

	if (IsDead(client))
		return 0; /* killed due to JOIN */

	sendnotice(client, "You are now restricted to talking in %s: %s",
		SPAMFILTER_VIRUSCHAN, unreal_decodespace(tkl->ptr.spamfilter->tkl_reason));

	channel = find_channel(SPAMFILTER_VIRUSCHAN);
	if (channel)
	{
		MessageTag *mtags = NULL;
		ircsnprintf(chbuf, sizeof(chbuf), "@%s", channel->name);
		ircsnprintf(buf, sizeof(buf), "[Spamfilter] %s matched filter '%s' [%s] [%s]",
			client->name, tkl->ptr.spamfilter->match->str, cmdname_by_spamftarget(type),
			unreal_decodespace(tkl->ptr.spamfilter->tkl_reason));
		new_message(&me, NULL, &mtags);
		sendto_channel(channel, &me, NULL, "o",
		               0, SEND_ALL|SKIP_DEAF, mtags,
		               ":%s NOTICE %s :%s", me.name, chbuf, buf);
		free_message_tags(mtags);
	}
	SetVirus(client);
	return 1;
}

int match_spamfilter_exempt(TKL *tkl, char user_is_exempt_general, char user_is_exempt_central)
{
	if (user_is_exempt_general)
		return 1;
	if (IsCentralSpamfilter(tkl) && user_is_exempt_central)
		return 1;
	return 0;
}

/** Tells if the message content should be hidden in the spamfilter hit log message. Helper function. */
static int spamfilter_hide_content(int target, SpamfilterShowMessageContentOnHit setting)
{
	if ((target == SPAMF_USERMSG) || (target == SPAMF_USERNOTICE))
	{
		if (setting == SPAMFILTER_SHOW_MESSAGE_CONTENT_ON_HIT_ALWAYS)
			return 0;
		return 1;
	} else
	if ((target == SPAMF_CHANMSG) || (target == SPAMF_CHANNOTICE))
	{
		if ((setting == SPAMFILTER_SHOW_MESSAGE_CONTENT_ON_HIT_ALWAYS) ||
		    (setting == SPAMFILTER_SHOW_MESSAGE_CONTENT_ON_HIT_CHANNEL_ONLY))
		{
			return 0;
		}
		return 1;
	}
	return 0;
}

/** Called when a spamfilter is hit. Helper for match_spamfilter().
 * @retval 1 to break processing other spamfilters, 0 to continue.
 */
static void match_spamfilter_hit(Client *client, const char *str_in, const char *str, int target,
                                 const char *cmd, const char *destination, TKL *tkl, TKL **winner_tkl,
                                 char user_is_exempt_general, char user_is_exempt_central,
                                 int *stop_processing_general_spamfilters, int *stop_processing_central_spamfilters,
                                 int *content_revealed,
                                 char no_stop_first_match)
{
	int hide_content = spamfilter_hide_content(target,
	                                           tkl->ptr.spamfilter->show_message_content_on_hit ?
	                                           tkl->ptr.spamfilter->show_message_content_on_hit :
	                                           iConf.spamfilter_show_message_content_on_hit);
	int stopped;
	int highest_action;

	if (match_spamfilter_exempt(tkl, user_is_exempt_general, user_is_exempt_central))
	{
		tkl->ptr.spamfilter->hits_except++;
		tkl->ptr.spamfilter->lasthit_except = TStime();
	} else
	{
		tkl_hit(client, tkl);
		highest_action = highest_ban_action(tkl->ptr.spamfilter->action);
		if (highest_action > BAN_ACT_SET)
		{
			if (!cmd)
			{
				/* On-tag-change has no context, so log without the [cmd: ...] part */
				unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
					   "[Spamfilter] $client.details matches filter '$tkl': [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id",
					   log_data_tkl("tkl", tkl),
					   log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id));
			} else
			if (hide_content || (target == SPAMF_RAW))
			{
				unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
					   "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination] [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id",
					   log_data_tkl("tkl", tkl),
					   log_data_string("command", cmd),
					   log_data_string("_space", destination ? " " : ""),
					   log_data_string("destination", destination ? destination : ""),
					   log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id));
			} else {
				// Yeah we are re-running the text analysis.
				TextAnalysis textanalysis;
				memset(&textanalysis, 0, sizeof(textanalysis));
				RunHook(HOOKTYPE_ANALYZE_TEXT, client, str, &textanalysis);
				unreal_log(ULOG_INFO, "tkl", "SPAMFILTER_MATCH", client,
					   "[Spamfilter] $client.details matches filter '$tkl': [cmd: $command$_space$destination: '$str'] [reason: $tkl.reason] [action: $tkl.ban_action] $spamfilter_id",
					   log_data_tkl("tkl", tkl),
					   log_data_string("command", cmd),
					   log_data_string("_space", destination ? " " : ""),
					   log_data_string("destination", destination ? destination : ""),
					   log_data_string("str", str),
					   log_data_textanalysis("text_analysis", &textanalysis),
					   log_data_optional_name_value("spamfilter_id", "spamfilter-id", tkl->id));
				*content_revealed = 1;
			}

			RunHook(HOOKTYPE_LOCAL_SPAMFILTER, client, str, str_in, target, destination, tkl);
		}
	}

	/* Run any SET actions */
	ban_action_run_all_sets_and_stops(client, tkl->ptr.spamfilter->action, &stopped);

	/* Set 'winner_tkl' to the spamfilter with the strongest action. */
	if (!*winner_tkl)
		*winner_tkl = tkl;
	else
		*winner_tkl = choose_winning_spamfilter(tkl, *winner_tkl);

	/* If set::spamfilter::stop-on-first-match is enabled, then we also stop
	 * (this is different than an ::action stop but the effect is the same...
	 */
	if ((no_stop_first_match == 0) && SPAMFILTER_STOP_ON_FIRST_MATCH)
		stopped = 1;

	/* Tell caller what actually should be stopped... */
	if (stopped)
	{
		if (IsCentralSpamfilter(tkl))
			*stop_processing_central_spamfilters = 1;
		else
			*stop_processing_general_spamfilters = 1;
	}

}

/** Run rule-only spamfilters: ones with no ::match/::target, only a ::rule.
 * These run when a tag changes. The most severe hit (highest action) wins and is set in *winner_tkl.
 * The caller then takes that action. This function is used both by:
 * 1) match_spamfilter(): when a tag changed during a message
 * 2) parse_client_queued(): after a command ran and there was a tag change
 *    or similar, like server_flood_count('..') changed value.
 */
static void run_rule_only_spamfilter_loop(Client *client, const char *str_in, const char *str,
                                          int target, const char *cmd, const char *destination,
                                          int flags, ClientContext *clictx, TKL **winner_tkl,
                                          char user_is_exempt_general, char user_is_exempt_central,
                                          int *stop_processing_general_spamfilters,
                                          int *stop_processing_central_spamfilters,
                                          int *content_revealed)
{
	TKL *tkl;
	crule_context context;

	*stop_processing_general_spamfilters = *stop_processing_central_spamfilters = 0; /* reset */
	for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next)
	{
		if (tkl->ptr.spamfilter->target ||
		    (tkl->ptr.spamfilter->match->type != MATCH_NONE) ||
		    !tkl->ptr.spamfilter->rule)
		{
			continue;
		}

		/* Skip spamfilters due to a 'stop' action from an earlier spamfilter
		 * or set::spamfilter::stop-on-first-match.
		 * We treat such stops as separate for central & general spamfilters
		 * so they don't affect each other.
		 */
		if (IsCentralSpamfilter(tkl))
		{
			if (*stop_processing_central_spamfilters)
				continue;
		} else {
			if (*stop_processing_general_spamfilters)
				continue;
		}

		if ((flags & SPAMFLAG_NOWARN) && only_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_WARN))
			continue;

		/* If the action is 'soft' (for non-logged in users only) then
		 * don't bother running the spamfilter if the user is logged in.
		 */
		if (IsLoggedIn(client) && only_soft_actions(tkl->ptr.spamfilter->action))
			continue;

		memset(&context, 0, sizeof(context));
		context.client = client;
		context.text = str_in;
		context.destination = destination;
		context.clictx = clictx;
		if (!crule_eval(&context, tkl->ptr.spamfilter->rule))
			continue;

		match_spamfilter_hit(client, str_in, str, target, cmd, destination,
		                     tkl, winner_tkl,
		                     user_is_exempt_general, user_is_exempt_central,
		                     stop_processing_general_spamfilters, stop_processing_central_spamfilters,
		                     content_revealed,
		                     1);
		/* and continue (yes, always, no stopping on first match) */
	}
}

/** Run rule-only spamfilters, after a tag change happened. This gets called from
 * parse_client_queued(), so it is safe to kill a client without having to deal
 * with complex conditions. Since there is no "message" here, context is empty.
 */
void _run_deferred_rule_only_spamfilters(Client *client)
{
	TKL *winner_tkl = NULL;
	char user_is_exempt_general = 0;
	char user_is_exempt_central = 0;
	int stop_processing_general_spamfilters = 0;
	int stop_processing_central_spamfilters = 0;
	int content_revealed = 0;
	crule_context context;

	if (!client->local)
		return;

	/* Mark all tag changes up to now as handled. Done up-front so the early-returns below
	 * (exempt users etc) don't make us re-run on every following command.
	 */
	client->local->spamfilter_run_tags_serial = client->local->tags_serial;

	if (!client->user || ValidatePermissionsForPath("immune:server-ban:spamfilter",client,NULL,NULL,NULL) || IsULine(client))
		return;

	memset(&context, 0, sizeof(context));
	context.client = client;
	context.text = "";
	context.destination = NULL;
	context.clictx = NULL;

	/* Client exempt from spamfilter checking? */
	if (find_tkl_exception(TKL_SPAMF, client) ||
	    (iConf.spamfilter_except && user_allowed_by_security_group_context(client, iConf.spamfilter_except, &context)))
	{
		user_is_exempt_general = 1;
	}
	if (user_allowed_by_security_group_context(client, iConf.central_spamfilter_except, &context))
		user_is_exempt_central = 1;

	run_rule_only_spamfilter_loop(client, "", "", 0, NULL, NULL, 0, NULL,
	                              &winner_tkl, user_is_exempt_general, user_is_exempt_central,
	                              &stop_processing_general_spamfilters, &stop_processing_central_spamfilters,
	                              &content_revealed);

	if (winner_tkl && !match_spamfilter_exempt(winner_tkl, user_is_exempt_general, user_is_exempt_central))
	{
		char *reason = unreal_decodespace(winner_tkl->ptr.spamfilter->tkl_reason);
		take_action_ex(client, winner_tkl->ptr.spamfilter->action, reason,
		               winner_tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL, winner_tkl->id);
	}
}

/** match_spamfilter: executes the spamfilter on the input string.
 * @param str		The text (eg msg text, notice text, part text, quit text, etc
 * @param target	The spamfilter target (SPAMF_*)
 * @param cmd		The command (eg: "PRIVMSG")
 * @param destination	The destination as a text string (eg: "somenick", can be NULL.. eg for away)
 * @param flags		Any flags (SPAMFLAG_*)
 * @param rettkl	Pointer to an aTKLline struct, _used for special circumstances only_
 * @returns 0 if not matched, otherwise one of BAN_ACT_* (>=1) if spamfilter matched
 *          and it should be blocked or client exited. If >=1 then be sure to check IsDead(client)!!
 */
int _match_spamfilter(Client *client, const char *str_in, int target, const char *cmd, const char *destination, int flags, ClientContext *clictx, TKL **rettkl)
{
	TKL *tkl;
	TKL *winner_tkl = NULL;
	const char *str;
	const char *str_deconfused = NULL;
	char deconfused[8192];
	int ret = -1;
	char *reason = NULL;
#ifdef SPAMFILTER_DETECTSLOW
	struct rusage rnow, rprev;
	long ms_past;
#endif
	char user_is_exempt_general = 0;
	char user_is_exempt_central = 0;
	int stop_processing_general_spamfilters = 0;
	int stop_processing_central_spamfilters = 0;
	int content_revealed = 0;
	crule_context context;

	if (rettkl)
		*rettkl = NULL; /* initialize to NULL */

	if (!cmd)
		cmd = cmdname_by_spamftarget(target);

	if (target == SPAMF_USER)
		str = str_in;
	else
		str = StripControlCodes(str_in);

	if (confusables_spamfilters_present)
	{
		if (clictx && clictx->textanalysis && *clictx->textanalysis->deconfused)
			str_deconfused = clictx->textanalysis->deconfused;
		else
			str_deconfused = utf8_convert_confusables(str, deconfused, sizeof(deconfused));
	}

	/* (note: using client->user check here instead of IsUser()
	 * due to SPAMF_USER where user isn't marked as client/person yet.
	 */
	if (!client->user || ValidatePermissionsForPath("immune:server-ban:spamfilter",client,NULL,NULL,NULL) || IsULine(client))
		return 0;

	memset(&context, 0, sizeof(context));
	context.client = client;
	context.text = str_in;
	context.destination = destination;
	context.clictx = clictx;

	/* Client exempt from spamfilter checking?
	 * Let's check that early: going through elines is likely faster than running the regex(es).
	 */
	if (find_tkl_exception(TKL_SPAMF, client) ||
	    (iConf.spamfilter_except && user_allowed_by_security_group_context(client, iConf.spamfilter_except, &context)))
	{
		user_is_exempt_general = 1;
	}

	if (user_allowed_by_security_group_context(client, iConf.central_spamfilter_except, &context))
		user_is_exempt_central = 1;

	for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next)
	{
		if (!(tkl->ptr.spamfilter->target & target))
			continue;

		/* Skip spamfilters due to a 'stop' action from an earlier spamfilter
		 * or set::spamfilter::stop-on-first-match.
		 * We treat such stops as separate for central & general spamfilters
		 * so they don't affect each other.
		 */
		if (IsCentralSpamfilter(tkl))
		{
			if (stop_processing_central_spamfilters)
				continue;
		} else {
			if (stop_processing_general_spamfilters)
				continue;
		}

		if ((flags & SPAMFLAG_NOWARN) && only_actions_of_type(tkl->ptr.spamfilter->action, BAN_ACT_WARN))
			continue;

		/* If the action is 'soft' (for non-logged in users only) then
		 * don't bother running the spamfilter if the user is logged in.
		 */
		if (IsLoggedIn(client) && only_soft_actions(tkl->ptr.spamfilter->action))
			continue;

		/* Run any pre 'rule' if there is any (false means 'no hit') */
		if (tkl->ptr.spamfilter->rule)
		{
			if (!crule_eval(&context, tkl->ptr.spamfilter->rule))
				continue;
		}

		/* Check any 'except' rule if there is any (true means 'no hit') */
		if (tkl->ptr.spamfilter->except && user_allowed_by_security_group_context(client, tkl->ptr.spamfilter->except, &context))
			continue;

		if (tkl->ptr.spamfilter->match && (tkl->ptr.spamfilter->match->type != MATCH_NONE))
		{
			const char *regex_error = NULL;

#ifdef SPAMFILTER_DETECTSLOW
			if (tkl->ptr.spamfilter->match->type == MATCH_PCRE_REGEX)
			{
				memset(&rnow, 0, sizeof(rnow));
				memset(&rprev, 0, sizeof(rnow));

				getrusage(RUSAGE_SELF, &rprev);
			}
#endif

			if (tkl->ptr.spamfilter->input_conversion == INPUT_CONVERSION_STRIP_CONTROL_CODES)
				ret = unreal_match(tkl->ptr.spamfilter->match, str, &regex_error); /* StripControlCodes() */
			else if (tkl->ptr.spamfilter->input_conversion == INPUT_CONVERSION_CONFUSABLES)
				ret = unreal_match(tkl->ptr.spamfilter->match, str_deconfused ? str_deconfused : str, &regex_error); /* utf8_convert_confusables(), with fallback */
			else
				ret = unreal_match(tkl->ptr.spamfilter->match, str_in, &regex_error); /* raw */

#ifdef SPAMFILTER_DETECTSLOW
			if (tkl->ptr.spamfilter->match->type == MATCH_PCRE_REGEX)
			{
				getrusage(RUSAGE_SELF, &rnow);

				ms_past = ((rnow.ru_utime.tv_sec - rprev.ru_utime.tv_sec) * 1000) +
					  ((rnow.ru_utime.tv_usec - rprev.ru_utime.tv_usec) / 1000);

				if ((SPAMFILTER_DETECTSLOW_FATAL > 0) && (ms_past > SPAMFILTER_DETECTSLOW_FATAL))
				{
					unreal_log(ULOG_ERROR, "tkl", "SPAMFILTER_SLOW_FATAL", NULL,
						   "[Spamfilter] WARNING: Too slow spamfilter detected (took $msec_time msec to execute) "
						   "-- spamfilter will be \002REMOVED!\002: $tkl",
						   log_data_tkl("tkl", tkl),
						   log_data_integer("msec_time", ms_past));
					tkl_del_line(tkl);
					return 0; /* Act as if it didn't match, even if it did.. it's gone now anyway.. */
				} else
				if ((SPAMFILTER_DETECTSLOW_WARN > 0) && (ms_past > SPAMFILTER_DETECTSLOW_WARN))
				{
					unreal_log(ULOG_WARNING, "tkl", "SPAMFILTER_SLOW_WARN", NULL,
						   "[Spamfilter] WARNING: Slow spamfilter detected (took $msec_time msec to execute): $tkl",
						   log_data_tkl("tkl", tkl),
						   log_data_integer("msec_time", ms_past));
				}
			}
#endif

			if (regex_error)
				spamfilter_regex_error(tkl, regex_error);
		} else {
			/* There is no ::match but there was a ::rule, and that is enough for a match.. */
			if (tkl->ptr.spamfilter->rule)
				ret = 1;
		}

		if (ret)
		{
			match_spamfilter_hit(client, str_in, str, target, cmd, destination,
			                     tkl, &winner_tkl,
			                     user_is_exempt_general, user_is_exempt_central,
			                     &stop_processing_general_spamfilters, &stop_processing_central_spamfilters,
			                     &content_revealed,
			                     0);
		}
	}

	if (client->local && (client->local->tags_serial != client->local->spamfilter_run_tags_serial))
	{
		/* A tag changed (eg a spamfilter 'set' action during this message): run the
		 * rule-only spamfilters, which only run when a tag has changed.
		 */
		run_rule_only_spamfilter_loop(client, str_in, str, target, cmd, destination, flags, clictx,
		                              &winner_tkl, user_is_exempt_general, user_is_exempt_central,
		                              &stop_processing_general_spamfilters, &stop_processing_central_spamfilters,
		                              &content_revealed);
		client->local->spamfilter_run_tags_serial = client->local->tags_serial;
	}

	tkl = winner_tkl;
	if (!tkl)
		return 0; /* NOMATCH, we are done */

	if (match_spamfilter_exempt(tkl, user_is_exempt_general, user_is_exempt_central))
		return 0;

	/* Spamfilter matched */
	reason = unreal_decodespace(tkl->ptr.spamfilter->tkl_reason);
	ret = take_action_ex(client, tkl->ptr.spamfilter->action, reason, tkl->ptr.spamfilter->tkl_duration, TAKE_ACTION_SKIP_SET, NULL, tkl->id);
	if (!IsDead(client))
	{
		if ((ret == BAN_ACT_BLOCK) || (ret == BAN_ACT_SOFT_BLOCK))
		{
			switch(target)
			{
				case SPAMF_USERMSG:
				case SPAMF_USERNOTICE:
				{
					char errmsg[512];
					ircsnprintf(errmsg, sizeof(errmsg), "Message blocked: %s", reason);
					sendnumeric(client, ERR_CANTSENDTOUSER, destination, errmsg);
					break;
				}
				case SPAMF_CHANNOTICE:
					break; /* no replies to notices */
				case SPAMF_CHANMSG:
				{
					sendto_one(client, NULL, ":%s 404 %s %s :Message blocked: %s",
						me.name, client->name, destination, reason);
					break;
				}
				case SPAMF_MTAG:
				{
					sendnumericfmt(client, ERR_CANNOTDOCOMMAND, "%s :Command blocked: %s",
						cmd, reason);
					break;
				}
				case SPAMF_DCC:
				{
					char errmsg[512];
					ircsnprintf(errmsg, sizeof(errmsg), "DCC blocked: %s", reason);
					sendnumeric(client, ERR_CANTSENDTOUSER, destination, errmsg);
					break;
				}
				case SPAMF_AWAY:
					/* hack to deal with 'after-away-was-set-filters' */
					if (client->user->away && !strcmp(str_in, client->user->away))
					{
						/* free away & broadcast the unset */
						safe_free(client->user->away);
						client->user->away = NULL;
						sendto_server(client, 0, 0, NULL, ":%s AWAY", client->id);
					}
					break;
				case SPAMF_TOPIC:
					//...
					sendnotice(client, "Setting of topic on %s to that text is blocked: %s",
						destination, reason);
					break;
				default:
					break;
			}
			return ret;
		} else
		if ((ret == BAN_ACT_WARN) || (ret == BAN_ACT_SOFT_WARN))
		{
			if (content_revealed &&
			    ((target == SPAMF_USERMSG) || (target == SPAMF_USERNOTICE) ||
			     (target == SPAMF_CHANMSG) || (target == SPAMF_CHANNOTICE)))
			{
				sendnumeric(client, RPL_SPAMCMDFWD, cmd, reason);
			}
			return 0;
		} else
		if ((ret == BAN_ACT_DCCBLOCK) || (ret == BAN_ACT_SOFT_DCCBLOCK))
		{
			if (target == SPAMF_DCC)
			{
				sendnotice(client, "DCC to %s blocked: %s", destination, reason);
				sendnotice(client, "*** You have been blocked from sending files, reconnect to regain permission to send files");
				SetDCCBlock(client);
			}
			return ret;
		} else
		if ((ret == BAN_ACT_VIRUSCHAN) || (ret == BAN_ACT_SOFT_VIRUSCHAN))
		{
			if (IsVirus(client)) /* Already tagged */
				return ret; // this was 0, but the action should be blocked, right?

			/* There's a race condition for SPAMF_USER, so 'rettk' is used for SPAMF_USER
			 * when a user is currently connecting and filters are checked:
			 */
			if (!IsUser(client))
			{
				if (rettkl)
					*rettkl = tkl;
				return ret;
			}

			join_viruschan(client, tkl, target);
			return ret;
		}
	}

	return ret;
}

/** Check message-tag spamfilters.
 * @param client	The client
 * @param mtags		Message tags sent by client
 * @param cmd		Command to be executed (can be NULL)
 * @retval Return 1 to stop processing the command (ignore it) or 0 to allow/continue as normal
 */
int _match_spamfilter_mtags(Client *client, MessageTag *mtags, const char *cmd)
{
	MessageTag *m;
	char buf[4096];
	char *str;

	/* This is a shortcut: if there are no spamfilters present
	 * on message tags then we can return immediately.
	 * Saves a lot of CPU and it is quite likely too!
	 */
	if (mtag_spamfilters_present == 0)
		return 0;

	for (m = mtags; m; m = m->next)
	{
		if (m->value)
		{
			snprintf(buf, sizeof(buf), "%s=%s", m->name, m->value);
			str = buf;
		} else {
			str = m->name;
		}
		if (match_spamfilter(client, str, SPAMF_MTAG, cmd, NULL, 0, NULL, NULL))
			return 1;
	}
	return 0;
}

/** Updates 'mtag_spamfilters_present' and 'raw_spamfilters_present'
 * based on if any spamfilters are present with the SPAMF_MTAG / SPAMF_RAW.
 */
int check_special_spamfilters_present(void)
{
	TKL *tkl;

	mtag_spamfilters_present = 0;
	raw_spamfilters_present = 0;

	for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next)
	{
		if (tkl->ptr.spamfilter->target & SPAMF_MTAG)
			mtag_spamfilters_present = 1;
		if (tkl->ptr.spamfilter->target & SPAMF_RAW)
			raw_spamfilters_present = 1;
		if (tkl->ptr.spamfilter->input_conversion & INPUT_CONVERSION_CONFUSABLES)
			confusables_spamfilters_present = 1;
	}

	return 0;
}

/** CIDR function to compare the first 'mask' bits.
 * @author Taken from atheme
 * @returns 1 if equal, 0 if not.
 */
static int comp_with_mask(void *addr, void *dest, u_int mask)
{
	if (memcmp(addr, dest, mask / 8) == 0)
	{
		int n = mask / 8;
		int m = (0xffff << (8 - (mask % 8)));
		if (mask % 8 == 0 || (((u_char *) addr)[n] & m) == (((u_char *) dest)[n] & m))
		{
			return (1);
		}
	}
	return (0);
}

#define IPSZ 16

/** Match a user against a mask.
 * This will deal with 'nick!user@host', 'user@host' and just 'host'.
 * We try to match the 'host' portion against the client IP, real host, etc...
 * CIDR support is available so 'host' may be like '1.2.0.0/16'.
 * @returns 1 on match, 0 on no match.
 */
int _match_user(const char *rmask, Client *client, int options)
{
	char mask[NICKLEN+USERLEN+HOSTLEN+8];
	char clientip[IPSZ], maskip[IPSZ];
	char *p = NULL;
	char *nmask = NULL, *umask = NULL, *hmask = NULL;
	int cidr = -1; /* CIDR length, -1 for no CIDR */

	strlcpy(mask, rmask, sizeof(mask));

	if ((options & MATCH_CHECK_EXTENDED) &&
	    is_extended_server_ban(mask) &&
	    client->user)
	{
		/* Check user properties / extbans style */
		return _match_user_extended_server_ban(rmask, client);
	}

	if (!(options & MATCH_MASK_IS_UHOST))
	{
		p = strchr(mask, '!');
		if (p)
		{
			*p++ = '\0';
			if (!*mask)
				return 0; /* NOMATCH: '!...' */
			nmask = mask;
			umask = p;

			/* Could just as well check nick right now */
			if (!match_simple(nmask, client->name))
				return 0; /* NOMATCH: nick mask did not match */
		}
	}

	if (!(options & (MATCH_MASK_IS_HOST)))
	{
		p = strchr(p ? p : mask, '@');
		if (p)
		{
			char *client_username = (client->user && *client->user->username) ? client->user->username : client->ident;

			*p++ = '\0';
			if (!*p || !*mask)
				return 0; /* NOMATCH: '...@' or '@...' */
			hmask = p;
			if (!umask)
				umask = mask;

			/* Check user portion right away */
			if (!match_simple(umask, client_username))
				return 0; /* NOMATCH: user mask did not match */
		} else {
			if (nmask)
				return 0; /* NOMATCH: 'abc!def' (or even just 'abc!') */
			hmask = mask;
		}
	} else {
		hmask = mask;
	}

	/* If we get here then we have done checking nick / ident (if it was needed)
	 * and now need to match the 'host' portion.
	 */

	/**** Check visible host ****/
	if (options & MATCH_CHECK_VISIBLE_HOST)
	{
		char *hostname = client->user ? GetHost(client) : (MyUser(client) ? client->local->sockhost : NULL);
		if (hostname && match_simple(hmask, hostname))
			return 1; /* MATCH: visible host */
	}

	/**** Check cloaked host ****/
	if (options & MATCH_CHECK_CLOAKED_HOST)
	{
		if (client->user && match_simple(hmask, client->user->cloakedhost))
			return 1; /* MATCH: cloaked host */
	}

	/**** check on IP ****/
	if (options & MATCH_CHECK_IP)
	{
		p = strchr(hmask, '/');
		if (p)
		{
			*p++ = '\0';
			cidr = atoi(p);
			if (cidr <= 0)
				return 0; /* NOMATCH: invalid CIDR */
		}

		if (strchr(hmask, '?') || strchr(hmask, '*'))
		{
			/* Wildcards */
			if (client->ip && match_simple(hmask, client->ip))
				return 1; /* MATCH (IP with wildcards) */
		} else
		if (strchr(hmask, ':'))
		{
			/* IPv6 hostmask */

			/* We can actually return here on match/nomatch as we don't need to check the
			 * virtual host and things like that since ':' can never be in a hostname.
			 */
			if (!client->ip || !strchr(client->ip, ':'))
				return 0; /* NOMATCH: hmask is IPv6 address and client is not IPv6 */
			if (!inet_pton(AF_INET6, client->ip, clientip))
				return 0; /* NOMATCH: unusual failure */
			if (!inet_pton(AF_INET6, hmask, maskip))
				return 0; /* NOMATCH: invalid IPv6 IP in hostmask */

			if (cidr < 0)
				return comp_with_mask(clientip, maskip, 128); /* MATCH/NOMATCH by exact IP */

			if (cidr > 128)
				return 0; /* NOMATCH: invalid CIDR */

			return comp_with_mask(clientip, maskip, cidr);
		} else
		{
			/* Host is not IPv6 and does not contain wildcards.
			 * So could be a literal IPv4 address or IPv4 CIDR.
			 * NOTE: could also be neither (like a real hostname), so don't return 0 on nomatch,
			 * in that case we should just continue...
			 * The exception is CIDR. If we have CIDR mask then don't bother checking for
			 * virtual hosts and things like that since '/' can never be in a hostname.
			 */
			if (client->ip && inet_pton(AF_INET, client->ip, clientip) && inet_pton(AF_INET, hmask, maskip))
			{
				if (cidr < 0)
				{
					if (comp_with_mask(clientip, maskip, 32))
						return 1; /* MATCH: exact IP */
				}
				else if (cidr > 32)
					return 0; /* NOMATCH: invalid CIDR */
				else
					return comp_with_mask(clientip, maskip, cidr); /* MATCH/NOMATCH by CIDR */
			}
		}
	}

	/**** Check real host ****/
	if (options & MATCH_CHECK_REAL_HOST)
	{
		char *hostname = client->user ? client->user->realhost : (MyConnect(client) ? client->local->sockhost : NULL);
		if (hostname && match_simple(hmask, hostname))
			return 1; /* MATCH: hostname match */
	}

	return 0; /* NOMATCH: nothing of the above matched */
}

/** Returns 1 if the user is allowed by any of the security groups in the named list.
 * This is only used by security-group::security-group and
 * security-group::exclude-security-group.
 * @param client	Client to check
 * @param l		The NameList
 * @returns 1 if any of the security groups match, 0 if none of them matched.
 */
int _unreal_match_iplist(Client *client, NameList *l)
{
	char client_ipv6 = 0;
	char clientip[IPSZ], maskip[IPSZ];

	if (!client->ip)
		return 0; /* unusual, maybe services? */

	if (strchr(client->ip, ':'))
	{
		client_ipv6 = 1;
		if (!inet_pton(AF_INET6, client->ip, clientip))
			return 0; /* unusual failure */
	} else {
		if (!inet_pton(AF_INET, client->ip, clientip))
			return 0; /* unusual failure */
	}

	for (; l; l = l->next)
	{
		char mask[512], *p;
		int cidr = -1; /* CIDR length, -1 for no CIDR */

		strlcpy(mask, l->name, sizeof(mask));
		p = strchr(mask, '/');
		if (p)
		{
			*p++ = '\0';
			cidr = atoi(p);
			if (cidr <= 0)
				return 0; /* NOMATCH: invalid CIDR */
		}

		/* Three possible types: wildcard, ipv6, ipv4 */

		if (strchr(mask, '*') || strchr(mask, '?'))
		{
			/* Wildcards */
			if (match_simple(mask, client->ip))
				return 1; /* MATCH by wildcard IP */
		}
		else if (strchr(mask, ':'))
		{
			/* IPv6 */
			if (!client_ipv6)
				continue; /* NOMATCH: client is IPv4 */
			if (!inet_pton(AF_INET6, mask, maskip))
				continue; /* NOMATCH: invalid IPv6 IP in mask */
			if (cidr < 0)
			{
				/* Try to match by exact IP */
				if (comp_with_mask(clientip, maskip, 128))
					return 1; /* MATCH by exact IP */
			} else
			if (cidr > 128)
			{
				continue; /* NOMATCH: invalid CIDR */
			} else
			if (comp_with_mask(clientip, maskip, cidr))
			{
				return 1; /* MATCH by CIDR */
			}
		} else
		{
			/* IPv4 */
			if (client_ipv6)
				continue; /* NOMATCH: client is IPv6 */
			if (!inet_pton(AF_INET, mask, maskip))
				continue; /* NOMATCH: invalid IPv6 IP in mask */
			if (cidr < 0)
			{
				/* Try to match by exact IP */
				if (comp_with_mask(clientip, maskip, 32))
					return 1; /* MATCH: by exact IP */
			} else
			if (cidr > 32)
			{
				continue; /* NOMATCH: invalid CIDR */
			} else
			if (comp_with_mask(clientip, maskip, cidr))
			{
				return 1; /* MATCH by CIDR */
			}
		}
	}
	return 0;
}


int _match_user_extended_server_ban(const char *banstr, Client *client)
{
	const char *nextbanstr;
	Extban *extban;
	BanContext *b;
	int ret;

	if (!is_extended_server_ban(banstr))
		return 0; /* we should never have been called */

	extban = findmod_by_bantype(banstr, &nextbanstr);
	if (!extban ||
	    !(extban->options & EXTBOPT_TKL) ||
	    !(extban->is_banned_events & BANCHK_TKL))
	{
		return 0; /* extban not found or of incorrect type (eg ~T) */
	}

	b = safe_alloc(sizeof(BanContext));
	b->client = client;
	b->banstr = nextbanstr;
	b->ban_check_types = BANCHK_TKL;
	ret = extban->is_banned(b);
	safe_free(b);
	return ret;
}

const char *getcmd(const char *i, char *obuf, int obuflen)
{
	char *o = obuf;

	for (; *i; i++)
	{
		if ((*i == ' ') || (*i == '\t'))
			break;
		obuflen--;
		if (obuflen == 0)
			break;
		*o++ = *i;
	}
	*o = '\0';
	return obuf;
}

int spamfilter_pre_command(Client *from, MessageTag *mtags, const char *buf)
{
	int ret;
	const char *cmd;
	char cmdbuf[32];

	/* This is a shortcut: if there are no spamfilters present
	 * on raw commands then we can return immediately.
	 */
	if ((raw_spamfilters_present == 0) || IsServer(from) || IsRPC(from) || !MyConnect(from))
		return 0;

	cmd = getcmd(buf, cmdbuf, sizeof(cmdbuf));
	ret = match_spamfilter(from, buf, SPAMF_RAW, cmd, NULL, 0, NULL, NULL);
	if (ret > 0)
		return HOOK_DENY;

	return 0;
}

CMD_FUNC(cmd_spaminfo)
{
	const char *line;
	int i, cnt;

	if (!IsOper(client))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if ((parc < 2) || BadPtr(parv[1]))
	{
		sendnotice(client, "Use: /SPAMINFO <line with spam text>");
		return;
	}

	if (!clictx->textanalysis)
	{
		sendnotice(client, "ERROR: Text analysis is not available. Maybe the utf8functions module is not loaded?");
		return;
	}

	sendnotice(client, "*** SPAMINFO ***");
	sendnotice(client, "This will show the original text and the deconfused text which can be used in a spamfilter block with input-conversion deconfused;");

	line = parv[1];

	sendnotice(client, "Original spam text: %s", line);
	sendnotice(client, "Deconfused spam text: %s", clictx->textanalysis->deconfused);
	sendnotice(client, "AntiMixedUTF8 points: %d", clictx->textanalysis->antimixedutf8_points);
	sendnotice(client, "Number of Unicode characters in total: %d", clictx->textanalysis->num_unicode_characters);
	sendnotice(client, "Number of different Unicode blocks used: %d", clictx->textanalysis->unicode_blocks);
	sendnotice(client, "Unicode Block breakdown (name: bytes [capped at 255]):");
	for (i = 0, cnt = 0; i < UNICODE_BLOCK_COUNT; i++)
	{
		if (clictx->textanalysis->unicode_blockmap[i])
		{
			cnt += clictx->textanalysis->unicode_blockmap[i];
			sendnotice(client, "- %s: %d",
			           utf8_get_block_name(i),
			           (int)clictx->textanalysis->unicode_blockmap[i]);
		}
	}
	if (clictx->textanalysis->num_unicode_characters != cnt)
	{
		sendnotice(client, "- Non-alpha ASCII characters (digits/spaces/etc.): %d",
		                   clictx->textanalysis->num_unicode_characters - cnt);
	}
}
