#define AUTHOR "Scott 'Simba' Garron <simba - anthrochat.net>"
#define VERSION "1.0.5"
#define MODNAME "os_blacklistemail"
/*-----------------------------------------------------------------------
 * Name:     os_blacklistemail
 * Author:   Scott 'Simba' Garron  <simba -at- anthrochat -dot- net>
 * Date:     2005-02-26
 * Version:  1.0.5
 *
 * Description:  Allows services admins to maintain a list of domain names
 * which users are not permitted to use in their e-mail addresses.
 * This comes in handy for preventing users from using publicly viewable, 
 * anonymous e-mail addresses provided by services like mailinater.com, 
 * jetable.org, and mailexpire.com.
 *
 * This module requires that MySQL is configured in Anope.  The MySQL
 * data is used in realtime, which means that other interfaces can be 
 * made (ie web) to manipulate the data, and changes are reflected by 
 * the module immediately.
 *
 *-----------------------------------------------------------------------
 * Supported IRCD: All Anope-supported IRCd's
 * Tested with:    Unreal 3.2.2b and Anope 1.7.8 (600)
 * Requires:  MySQL
 *-----------------------------------------------------------------------
 * New/modified commands:
 *
 * /ns REGISTER
 *
 * /ns SET
 *
 * /os BLEM
 *   Add, remove, clear or list blacklisted e-mail domain names.
 *
 *-----------------------------------------------------------------------    
 * 
 *      Consistently updated versions of all of my modules can be found
 * at http://www.cimeris.com/~simba/anopemodules/
 *
 *      If you find a bug, please send me a Mail or contact me on 
 * irc.anthrochat.net  #AnthroChat
 *
 * ----------------------------------------------------------------------
 * Change log:
 *  v1.0.5 2005-02-26:  Removed trailing semi-colons from all of the SQL
 *    queries.  Apparently, some versions of MySQL treat them as a syntax
 *    error.
 *  v1.0.4 2005-02-25:  Added a new callback function which is used to
 *    keep the connection to the SQL database alive.
 *  v1.0.3 2005-02-24:  Added some debugging code.
 *  v1.0.2 2005-02-21:  Made to compile in Windows.
 *  v1.0.1 2005-02-15:  notice_user_ww was hardcoded to use s_NickServ for
 *    its notices.  Changed it to use the "source" parametner.
 *  v1.0.0 2005-02-15:  First release
 */

/* Sets the maximum number of characters that will be displayed on a
 * line before doing a word wrap when being displayed to the user */
#define LINELENGTHMAX  58


/* Language text definitions */

#define SYNTAX_WORD   "Syntax:  "
#define SYNTAX_INDENT "         "
#define HELP_USAGE_HEAD "Type \002/msg %s HELP "
#define HELP_USAGE_TAIL "\002 for more information."
#define NOTICE_HELP_USAGE HELP_USAGE_HEAD "BLEM %s" HELP_USAGE_TAIL

#define MAIN_HELP_SUMMARY_BLEM "BLEM        Maintain e-mail domain blacklist"

#define HELP_BLEM "\002BLEM\002 Maintains a list of domain names in which\
 users will not be permitted to register or set in their e-mail addresses."

#define BLEM_ADD_SYNTAX "\002BLEM ADD \037domain\037 \037reason\037"
#define HELP_BLEM_ADD "Adds a domain to the e-mail domain blacklist."

#define BLEM_DEL_SYNTAX "\002BLEM DEL \037domain\037"
#define HELP_BLEM_DEL "Removes a domain from the e-mail domain blacklist.\
  Regular expressions can be used to remove multiple domains."

#define BLEM_LIST_SYNTAX "\002BLEM LIST [(\037DOM | WHO\037) \037search\
 string\037]"
#define HELP_BLEM_LIST "Lists domains in the e-mail blacklist.  A regular\
 expression search string may be specified to search for a specific domain.\n\
 \nOptionally, a search type can be specified.  DOM will search by domain\
 (the default) and WHO searches by the admin who added the entry."

#define BLEM_CLEAR_SYNTAX "\002BLEM CLEAR"
#define HELP_BLEM_CLEAR "Wipes the entire e-mail blacklist."

#define NOTICE_DOMAIN_BLACKLISTED "The e-mail domain \"%s\" is blacklisted.\
  Reason: %s.  Please use another, more legitimate e-mail provider."

#define NOTICE_ADDED "%s has been added to the e-mail domain blacklist."

#define NOTICE_DELETED "Deleted the following entries from the e-mail domain\
 blacklist: %s"
#define NOTICE_NUM_DELETED "%lu entries deleted."

#define NOTICE_NUM_LISTED "%lu blacklisted e-mail domains shown"

#define NOTICE_EXISTS "%s is already in the blacklist."

#define NOTICE_NOT_EXIST "No entries match %s."

#define NOTICE_CLEARED "E-mail domain blacklist cleared.  %lu entries\
 deleted."

/*********************************/
/** END OF LANGUAGE DEFINITION ***/
/*********************************/


/**************************************************************************
 ** DO NOT CHANGE BELOW THIS LINE UNLESS YOU KNOW WHAT ARE YOU DOING  !!
 **************************************************************************/

/*--------------------------------------------------------------------------
 * -- Database schema -- These are the definitions for the new table that
 * os_blacklistemail.  It will be added to the database from this definition
 * if it didn't previously exist, but if you are changing the schema, you'll
 * need to do the ALTER TABLE queries manually with a MySQL client and
 * update the rest of the code to use the proper queries, of course
 *------------------------------------------------------------------------*/
#define BLEM_DBTAB "mod_blem"
#define BLEM_SCHEMA "CREATE TABLE " BLEM_DBTAB " (\
   id int(11) unsigned NOT NULL auto_increment, \
   domain varchar(255) NOT NULL default '', \
   reason varchar(255) NOT NULL default '', \
   addedby varchar(255) NOT NULL default '', \
   whenadded int(11) NOT NULL default '0', \
   PRIMARY KEY  (id), \
   UNIQUE KEY domain (domain), \
   KEY addedby_index (addedby(10)) \
) TYPE=myISAM "

#define BLEM_ALLCOLUMNS "id, domain, reason, addedby, whenadded"

#define SEARCH_DOMAIN 1
#define SEARCH_ADDEDBY 2
/* Defining __GNU_SOURCE is required for the use of asprintf().  If this is 
 * undefined for portability, the module still works, but there's more
 * possibility for buffer overflow. */
#ifndef _WIN32
#define __GNU_SOURCE
#endif

#include "module.h"

static MYSQL *mysql;

typedef struct bleminfo_ BlemInfo;
struct bleminfo_ {
  uint32 db_id;
  char *domain;
  char *reason;
  char *addedby;
  uint32 whenadded;
};

/* Utility functions */
void notice_user_ww(char *source, User *u, const char *fmt, ... );
void free_blem_info(BlemInfo *info);
void free_blem_infos(BlemInfo **infos);
int check_email(User *u, char *email);

/* Database related functions */
char *quote(char *arg);
int mydb_init(void);
int do_query(const char *fmt, ... );
int db_add_blem_info(User *u, char *domain, char *reason);
BlemInfo *fill_blem_info(MYSQL_ROW myRow);
BlemInfo *db_get_blem_info(char *domain);
BlemInfo **db_get_blem_infos(const int searchby, char *searchstr);
uint32 db_del_blem_infos(char **domains, BlemInfo **infos);
uint32 db_clear(void);
char *make_sql_list(char **strings);

/* User command functions */
int ns_register(User *u);
int ns_set(User *u);
int os_blem(User *u);
int os_blem_add(User *u, char *args);
int os_blem_del(User *u, char *args);
int os_blem_list(User *u, char *args);
int os_blem_clear(User *u, char *args);

/* Help command functions */
int null_func(User *u);
void main_os_help(User *u);
int os_help_blem(User *u);
int os_help_blem_add(User *u);
int os_help_blem_del(User *u);
int os_help_blem_list(User *u);
int os_help_blem_clear(User *u);

/***************************************************************************
 * log_error_sub():  Sub-function for AnopeInit() which sends a formatted
 * entry to the log file.
 */
int log_error_sub(char *errormsg)
{
  alog("[%s] %s", MODNAME, errormsg);
  alog("[%s] Module not loaded.", MODNAME);
  return MOD_STOP;
} /* End log_error_sub() */

/***************************************************************************
 * AnopeInit():  Function that's called by Anope at initial module load time
 */
int AnopeInit(int argc, char **argv)
{
  Command *c;

#ifndef USE_MYSQL
  return log_error_sub("MySQL is not enabled.  This module requires MySQL.");
#endif
  if (!do_mysql)
    return log_error_sub("MySQL is not configured.  Please edit your "
                             "services.conf.");

  /* Activate mysql connection and stop if failed */
  if (mydb_init()) return MOD_STOP;
  
  /* We tack our ns_register() onto the beginning of the /nickserv REGISTER
   * command.  This allows our module to perform its e-mail address checks 
   * before allowing the core REGISTER command to process the registration.
   */
  c = createCommand("REGISTER", ns_register, NULL, -1, -1, -1, -1, -1);
  moduleAddCommand(NICKSERV, c, MOD_HEAD);

  c = createCommand("SET", ns_set, NULL, -1, -1, -1, -1, -1);
  moduleAddCommand(NICKSERV, c, MOD_HEAD);

  c = createCommand("BLEM", os_blem, NULL, -1, -1, -1, -1, -1);
  moduleAddCommand(OPERSERV, c, MOD_UNIQUE);
  moduleAddHelp(c, os_help_blem);

/* Add help functions for BLEM */
  /* The following moduleAddCommand() / moduleAddHelp() methods add a
   * null function to the command table for each sub-command.  It's
   * null because the main command function will handle all of the
   * sub-command functionality.  Doing this just gives us a way to
   * add a help function for each sub-command. */
  moduleSetOperHelp(main_os_help);
  
  c = createCommand("BLEM ADD", null_func, NULL, -1, -1, -1, -1, -1);
  moduleAddCommand(OPERSERV, c, MOD_TAIL);
  moduleAddHelp(c, os_help_blem_add);

  c = createCommand("BLEM DEL", null_func, NULL, -1, -1, -1, -1, -1);
  moduleAddCommand(OPERSERV, c, MOD_TAIL);
  moduleAddHelp(c, os_help_blem_del);

  c = createCommand("BLEM LIST", null_func, NULL, -1, -1, -1, -1, -1);
  moduleAddCommand(OPERSERV, c, MOD_TAIL);
  moduleAddHelp(c, os_help_blem_list);
/* End help functions */

  db_keepalive(0, NULL);
  moduleAddAuthor(AUTHOR);
  moduleAddVersion(VERSION);
  return MOD_CONT;
} /* End of AnopeInit() */


/**************************************************************************
 * AnopeFini():  This function is called by Anope at module unload time.
 * Any final clean-up routines should be put in here
 */
void AnopeFini(void)
{
  moduleDelCallback("OSBLEMKEEPALIVE");
  mysql_close(mysql);
}


/**************************************************************************
 *                Functions which display help to the user
 *************************************************************************/
int null_func(User *u)
{
  return MOD_CONT;
}

void main_os_help(User *u)
{
  notice_user_ww(s_OperServ, u, "    " MAIN_HELP_SUMMARY_BLEM);
}

int os_help_blem(User *u)
{
  notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_ADD_SYNTAX);
  notice_user_ww(s_OperServ, u, SYNTAX_INDENT BLEM_DEL_SYNTAX);
  notice_user_ww(s_OperServ, u, SYNTAX_INDENT BLEM_LIST_SYNTAX);
  notice_user_ww(s_OperServ, u, SYNTAX_INDENT BLEM_CLEAR_SYNTAX);
  notice_user_ww(s_OperServ, u, " \n" HELP_BLEM);
  return MOD_CONT;
}

int os_help_blem_add(User *u)
{
  notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_ADD_SYNTAX);
  notice_user_ww(s_OperServ, u, " \n" HELP_BLEM_ADD);
  return MOD_CONT;
}

int os_help_blem_del(User *u)
{
  notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_DEL_SYNTAX);
  notice_user_ww(s_OperServ, u, " \n" HELP_BLEM_DEL);
  return MOD_CONT;
}

int os_help_blem_list(User *u)
{
  notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_LIST_SYNTAX);
  notice_user_ww(s_OperServ, u, " \n" HELP_BLEM_LIST);
  return MOD_CONT;
}

int os_help_blem_clear(User *u)
{
  notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_CLEAR_SYNTAX);
  notice_user_ww(s_OperServ, u, " \n" HELP_BLEM_CLEAR);
  return MOD_CONT;
}


/**************************************************************************
 *            General utility and memory management functions
 **************************************************************************/

/****************************************************************
 * line_lastchar():  Finds the character at the maximum line
 * length, taking into account non-printable characters and
 * increasing the max length by the number of non-printable
 * characters that it finds.
 */
char *line_lastchar(char *s)
{
  char *ptr;
  uint32 num_nonprint = 0;
    
  ptr = s;
  if (ptr) 
    while ( (*ptr) && ((ptr - s) < (LINELENGTHMAX + num_nonprint)) )
    {
      switch (*ptr) 
      {  /* Still need to find the character for reverse */
        case '\002': /* Bold */
        case '\037': /* Underline */
          ++num_nonprint;
          break;
        default:
          break;
      }
      ++ptr;
    }
  return ptr;
} /* End of line_lastchar() sub-function */

/***************************************************************************
 * Uses LINELENGTHMAX definition to determine how many characters are
 * sent to the user before sending the next line as a new notice.  If
 * a newline character (\n) is encountered in the paragraph, following
 * text will be sent as a new notice regardless of the length of the line.
 * (So it's notice_user() with word wrap.)
 */
void notice_user_ww(char *source, User *u, const char *fmt, ...)
{
  va_list args;
#ifdef __GNU_SOURCE
  char *paragraph = NULL;
#else
  char paragraph[BUFSIZE];
#endif
  char *paragraphend = NULL, *linestart = NULL, 
       *lineend = NULL, *marker = NULL;

    
  if (!fmt) return;

  /* Do any printf()-style formatting substitutions */
  va_start(args, fmt);
#ifdef __GNU_SOURCE
  vasprintf(&paragraph, 
#else
  *paragraph = '\0';
  vsnprintf(paragraph, BUFSIZE, 
#endif
    fmt, args);
#if 0
  )  /* Fixes vim's context highlighting */
#endif
  va_end(args);

  if (!paragraph || !(*paragraph))
    return;

  /* Find and replace newlines ('\n') with null characters ('\0') */
  for (paragraphend = paragraph; *paragraphend; paragraphend++) 
    if ( *paragraphend == '\n' ) *paragraphend = '\0';

  /* Back paragraphend up one so that it's pointing to the 
   * last character instead of the null termination */
  --paragraphend;
  linestart = paragraph;
  marker = paragraph;

  /* As long as we haven't reached the end of the paragraph ... */
  while ( marker < paragraphend )
  {
    /* Set the lineend pointer to the character at the maximum line length */
    lineend = line_lastchar(marker);
    /* If we haven't reached the end of the line */
    if (*lineend)
    {
      marker = lineend;
      /* Back the marker up until it finds a space 
       * or it hits the beginning of the line */
      while ( (*marker != ' ') && (marker != linestart) )
        --marker;
      /* If it hit the beginning of the line, put it back 
       * to the maximum line length, and increment until 
       * we find the end of the line or a space */
      if ( marker == linestart ) 
      {
        marker = lineend;
        while ( (*marker) && (*marker != ' ') )
          ++marker;
      }
      lineend = marker;
      *lineend = '\0';
    } 
    else /* (If we were at the end of the line, set the marker to the end) */
      marker = lineend;

    /* Display the line to the user.  If the first character is null, 
     * display a line with a single space. */
    notice_user(source, u, *linestart ? linestart : " ");

    /* At this point, marker is pointing to the null character at the
     * end of the line that we just processed.  Incrementing the pointer
     * by one will put us at the first character in the next line of the
     * paragraph.  The linestart pointer is also set to that location 
     * because we want it to know where the beginning of the next line is.
     */
    ++marker;
    linestart = marker;
  }

#ifdef __GNU_SOURCE
  free(paragraph);
#endif
} /* End of notice_user_ww() */

/***************************************************************************
 * free_blem_info():  Release memory used by the strings within a BlemInfo
 * structure as well as the structure itself.
 */
void free_blem_info(BlemInfo *info)
{
  free(info->domain);
  free(info->reason);
  free(info->addedby);

  free(info);
  return;
} /* End free_blem_infos() */

/***************************************************************************
 * free_blem_infos():  Runs an array of BlemInfo structures thruogh
 * free_blem_info() to release all of the memory being referenced by them.
 */
void free_blem_infos(BlemInfo **infos)
{
  for ( ; *infos; infos++ )
    free_blem_info(*infos);
  
  return;
} /* End free_blem_infos() */

/****************************************************************************
 * check_email():  Check whether or not the e-mail is using a domain in the
 * blacklist.  This function is coded so that top-level domain names cannot
 * be blacklisted.  If someone can provide me a good reason to blacklist a
 * whole top level, let me know, and I'll re-work it.
 */
int check_email(User *u, char *email)
{
  char *domain = NULL;
  BlemInfo *info = NULL;


  if (!(MailValidate(email)))
    return MOD_CONT;

  /* Find the second from the last dot in the e-mail address and 
   * search by that as the domain name, back-up through each dot 
   * and do searches for each.  I suppose that I could turn this 
   * into one query, but this way should be fast enough.*/
  domain = strrchr(email, '.');
  --domain;

  while (domain != email)
  {
    while ( (*domain != '.') && (*domain != '@') && (domain != email) )
      --domain;

    if (domain != email)
    {
      ++domain; /* Put it one char past the dot (or @) */
      if (info = db_get_blem_info(domain))
      {
        notice_user_ww( s_NickServ, u, NOTICE_DOMAIN_BLACKLISTED,
                        info->domain, info->reason );
        alog( "[%s] %s!%s@%s tried to set a blacklisted e-mail domain: %s",
              MODNAME, u->nick, u->username, common_get_vhost(u), email );

        free_blem_info(info);
        return MOD_STOP;
      }

      domain -= 2; /* Put it one char before the dot (or @) */
    }
  }

  return MOD_CONT;
} /* End ns_register_sub() */


/***************************************************************************
 * Routines called directly by Anope on command events
 ***************************************************************************/

/**************************************************************************
 * ns_register():  Our function which is tacked onto the beginning of a
 * /nickserv REGISTER command.  It's really more of an error-checking
 * stub function for check_email().
 */
int ns_register(User *u) 
{
  char *args = moduleGetLastBuffer(), *email = NULL;
  int retval;

  /* If no arguments were passed by the user to the REGISTER command 
   * or an e-mail address wasn't given, this module is really not good 
   * for anything.  Return MOD_CONT to allow the core REGISTER command 
   * to continue processing or handling errors. */
  if ( !args || !(email = myStrGetToken(args, ' ', 1)) )
    return MOD_CONT;

  if (debug)
    alog("[%s] debug: %s!%s@%s requested nickname registration.  Checking "
         "blacklist for match on %s.", MODNAME, u->nick, u->username, 
         common_get_vhost(u), email);

  retval = check_email(u, email);
  free(email);

  if (debug && (retval == MOD_CONT))
    alog("[%s] debug: ns_register() %s didn't match any domain in "
         " the blacklist; continuing flow with MOD_CONT", MODNAME, email);

  return retval;
} /* End of ns_register() */

/***************************************************************************
 * ns_set():  Prepended to the core /nickserv SET command, if the user 
 * requests to set their e-mail address, and it's using a domain that's in
 * the blacklist, give them an error, and return MOD_STOP.  If it's not in 
 * the blacklist, return MOD_CONT and allow the core SET command to handle 
 * the change.
 */
int ns_set(User *u)
{
  char *args = NULL, *email = NULL;
  int retval = 0;

  /* If the user is an admin, there are no arguments, or the first argument 
   * is not EMAIL, it doesn't pertain to us, so return and allow the core 
   * SET command function handle the command */
  if ( is_services_admin(u) ||
       !(args = moduleGetLastBuffer()) || 
       !(email = myStrGetToken(args, ' ', 0)) || 
       (stricmp(email, "EMAIL") != 0) )
  {
    if (email) free(email);
    return MOD_CONT;
  }
  /* Reset the temp string */
  free(email);  
  email = NULL;

  /* Nothing that we do is any good if the 
   * user isn't registered and identified. */
  if ( !(nick_identified(u)) || !(email = myStrGetToken(args, ' ', 1)) )
    return MOD_CONT;

  if (debug)
    alog("[%s] debug: %s!%s@%s requested e-mail address change.  Checking "
         "blacklist for match on %s.", MODNAME, u->nick, u->username, 
         common_get_vhost(u), email);

  retval = check_email(u, email);
  free(email);

  if (debug && (retval == MOD_CONT))
    alog("[%s] debug: ns_set() %s didn't match any domain in "
         " the blacklist; continuing flow with MOD_CONT", MODNAME, email);
  
  return retval;
} /* End ns_set() */


/***************************************************************************
 * os_blem_add():  Executed when a user issues a "/operserv BLEM ADD" 
 * command.  Does some error checking and adds a domain name to the
 * blacklist, using db_add_blem_info().
 */
int os_blem_add(User *u, char *args)
{
  char *domain = NULL, *reason = NULL;
  int retval;
  BlemInfo *info;

  /* Check for too few or too many arguments */
  if ( !args || !(reason = myStrGetTokenRemainder(args, ' ', 1)) )
  {
    notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_ADD_SYNTAX);
    notice_user_ww(s_OperServ, u, NOTICE_HELP_USAGE, s_OperServ, "ADD");
    return MOD_STOP;
  }

  domain = myStrGetToken(args, ' ', 0);

  /* If the domain is already in the blacklist, display
   * an error to the user and do some clean-up. */
  if (info = db_get_blem_info(domain))
  {
    notice_user_ww(s_OperServ, u, NOTICE_EXISTS, info->domain);
    free_blem_info(info);
    retval = MOD_STOP;
  }
  else
  /* If the domain isn't in the blacklist, add it to the database */
  {
    db_add_blem_info(u, domain, reason);
    notice_user_ww(s_OperServ, u, NOTICE_ADDED, domain);
    retval = MOD_CONT;
  }

  free(domain);

  return retval;
} /* End os_blem_add() */

/***************************************************************************
 * os_blem_del():  Executed when a user issues an "/operserv BLEM DEL"
 * command.  If the specified domain exists in the blacklist, remove it
 * from the database.  Because we're using SQL's RLIKE clause, multiple
 * domains can be deleted by using regular expressions.
 */
int os_blem_del(User *u, char *args)
{
  char *tempstr = NULL;
  BlemInfo **infos;
  uint32 num_infos;

  /* Check for too few or too many arguments */
  if ( !args || (tempstr = myStrGetToken(args, ' ', 1)) )
  {
    notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_DEL_SYNTAX);
    notice_user_ww(s_OperServ, u, NOTICE_HELP_USAGE, s_OperServ, "DEL");
    if (tempstr) free(tempstr);
    return MOD_STOP;
  }

  infos = db_get_blem_infos(SEARCH_DOMAIN, args);
  if (*infos)
  {
    num_infos = db_del_blem_infos(&tempstr, infos);
    notice_user_ww(s_OperServ, u, NOTICE_DELETED, tempstr);
    notice_user_ww(s_OperServ, u, NOTICE_NUM_DELETED, num_infos);
    free_blem_infos(infos);
    free(tempstr);
  }
  else
    notice_user_ww(s_OperServ, u, NOTICE_NOT_EXIST, args);

  return MOD_CONT;
} /* End os_blem_del() */

/***************************************************************************
 * os_blem_list():  Executed when a user issues an "/operserv BLEM LIST"
 * command and allows them to list the contents of the blacklist.  If no
 * arguments are given, it lists the entire contents of the blacklist.
 * If one argument is specified, search by domain is assumed and the 
 * argument is treated as a regular expression search string.  If two
 * arguments are given, the second is treated as the regular expression
 * search string and the first one is the indicator as to which column
 * to apply the search (DOM for domain, WHO for addedby).
 */
int os_blem_list(User *u, char *args)
{
  BlemInfo **infos;
  uint32 num_infos = 0;
  char *searchtypestr = NULL, *searchstr = NULL;

  if (args)
  {
    /* Use searchstr as a temporary string to hold a third 
     * argument.  If a third argument exists, there are too 
     * many.  Display usage info and return MOD_STOP */
    if (searchstr = myStrGetToken(args, ' ', 2))
    {
      free(searchstr);
      notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_LIST_SYNTAX);
      notice_user_ww(s_OperServ, u, NOTICE_HELP_USAGE, s_OperServ, "LIST");
      return MOD_STOP;
    }

    /* Get the first and second arguments */
    searchtypestr = myStrGetToken(args, ' ', 0);
    searchstr = myStrGetToken(args, ' ', 1);
    /* If a second argument didn't exist, set the first 
     * argument to be the searchstr */
    if (!searchstr)
    {
      searchstr = searchtypestr;
      searchtypestr = NULL;
    }
  } 
  else /* If no arguments were given, set searchstr to the regex wildcard */
    searchstr = sstrdup(".*");

  /* Get all of the BlemInfo structures from the 
   * database which match the search criteria */
  if (searchtypestr) 
  {
    if (stricmp(searchtypestr, "DOM") == 0)
      infos = db_get_blem_infos(SEARCH_DOMAIN, searchstr);
    else if (stricmp(searchtypestr, "WHO") == 0 )
      infos = db_get_blem_infos(SEARCH_ADDEDBY, searchstr);

    free(searchtypestr);
  }
  else infos = db_get_blem_infos(SEARCH_DOMAIN, searchstr);

  free(searchstr);

  /* Loop through the returned BlemInfo strctures 
   * and display their details to the user */
  for ( ; *infos; infos++, num_infos++)
  {
    notice_user(s_OperServ, u, "\002%s\002:", (*infos)->domain);
    notice_user(s_OperServ, u, "  Reason:   %s", (*infos)->reason);
    notice_user(s_OperServ, u, "  Added by: %s - %s", (*infos)->addedby,
                ctime((time_t *) &((*infos)->whenadded)) );
  }
  notice_user_ww(s_OperServ, u, NOTICE_NUM_LISTED, num_infos);

  /* Rewind the pointer and do clean-up */
  infos -= num_infos;
  free_blem_infos(infos);

  return MOD_CONT;
} /* End os_blem_list() */

/***************************************************************************
 * os_blem_clear():  Executed when a user issues an "/operserv BLEM CLEAR"
 * command and deletes all of the blacklisted e-mail domains from the
 * database.
 */
int os_blem_clear(User *u, char *args)
{
  uint32 num_cleared;

  /* This command doesn't use any arguments, so if there are 
   * any specified, display usage information to the user */
  if (args)
  {
    notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_CLEAR_SYNTAX);
    notice_user_ww(s_OperServ, u, NOTICE_HELP_USAGE, s_OperServ, "CLEAR");
    return MOD_STOP;
  }

  num_cleared = db_clear();
  notice_user_ww(s_OperServ, u, NOTICE_CLEARED, num_cleared);
  alog( "[%s] %s (%s!%s@%s) cleared the email domain blacklist", MODNAME, 
        u->na->nc->display, u->nick, u->username, common_get_vhost(u));

  return MOD_CONT;
} /* End os_blem_clear() */

/***************************************************************************
 * os_blem_sub():  Sub-function for os_blem which is just a sub-command
 * parser that calls the appropriate sub-command functions.
 */
int os_blem_sub(User *u, char *cmd, char *subcmd)
{
  if (cmd)
  {
    if ( stricmp(cmd, "ADD") == 0 )
      return os_blem_add(u, subcmd);
    if ( stricmp(cmd, "DEL") == 0 )
      return os_blem_del(u, subcmd);
    if ( stricmp(cmd, "LIST") == 0 )
      return os_blem_list(u, subcmd);
    if ( stricmp(cmd, "CLEAR") == 0 )
      return os_blem_clear(u, subcmd);
  }
    
  notice_user_ww(s_OperServ, u, SYNTAX_WORD BLEM_ADD_SYNTAX);
  notice_user_ww(s_OperServ, u, SYNTAX_INDENT BLEM_DEL_SYNTAX);
  notice_user_ww(s_OperServ, u, SYNTAX_INDENT BLEM_LIST_SYNTAX);
  notice_user_ww(s_OperServ, u, SYNTAX_INDENT BLEM_CLEAR_SYNTAX);
  notice_user_ww(s_OperServ, u, NOTICE_HELP_USAGE, s_OperServ, "");
  return MOD_STOP;
} /* End os_blem_sub() */

/***************************************************************************
 * os_blem():  The core "/operserv BLEM" command function.  Determines which
 * sub-command to run, handles errors, and displays usage information.
 */
int os_blem(User *u)
{
  char *args = moduleGetLastBuffer();
  char *cmd = NULL;
  char *subcmd = NULL;
  int retval = 0;

  if ( !(nick_identified(u)) )
  {
    notice_lang(s_OperServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
    return MOD_CONT;
  }

  cmd = myStrGetToken(args, ' ', 0);
  subcmd = myStrGetTokenRemainder(args, ' ', 1);

  retval = os_blem_sub(u, cmd, subcmd);
  if (cmd) free(cmd);
  if (subcmd) free(subcmd);
  return retval;
} /* End os_blem() */


/***************************************************************************
 *                    MySQL Input/Output/Utility Functions
 **************************************************************************/


/***************************************************************************
 * quote():  Creates a string that is safe to use in a database query.  
 * In other words, it uses mysql_real_escape_string() in order to put
 *  backslashes in front of characters that usually mean something else to 
 *  SQL ( ', %, \, etc. ).
 *
 *  This function uses malloc(), so remember to free() the string when
 *  you're done with it.
 */
char *quote(char *arg)
{
  int slen;
  char *qbuf;

  if (!arg) return sstrdup("");
  slen = strlen(arg);
  qbuf = smalloc((1 + (slen * 2)) * sizeof(char));
  mysql_real_escape_string(mysql, qbuf, arg, slen);
  return qbuf;
} /* End quote() */

/***************************************************************************
 * make_sql_list():  Takes an array of strings and returns a newly-allocated
 * copy of the string information in the format:
 * "'string1', 'string2', 'string3'", used for listing values in SQL queries.
 * The strings argument must have a pointer to NULL as its last element.
 * All of the strings returned within the single quotes have been passed
 * through quote() so that they are sql-safe.
 */
char *make_sql_list(char **strings)
{
  char *sql_list_str = NULL, *marker = NULL;
  char **escaped_strings = NULL;
  uint32 num_strings = 0, allstrings_len = 0;
  uint32 *string_lens = NULL;

  /* Count the number of strings in the array */
  for (; *strings != NULL; strings++)
    ++num_strings;
  strings -= num_strings;

  /* Create an array of integers to correspond with the lengths of
   * each of the strings in the strings array. */
  string_lens = scalloc(num_strings, sizeof(uint32));

  /* Create another string array to hold the results from quote() */
  escaped_strings = scalloc(num_strings + 1, sizeof(char *));

  /* Iterate through each string in the array, generating the array of
   * quote()d strings and the array of lengths of each of those strings. */
  for ( ; *strings != NULL; strings++, string_lens++, escaped_strings++ )
  {
    *escaped_strings = quote(*strings);
    *string_lens = strlen(*escaped_strings);
    allstrings_len += *string_lens;
  }
  *escaped_strings = NULL;  /* Set the last one to null */

  /* "Rewind" the pointers */
  escaped_strings -= num_strings;
  strings -= num_strings;
  string_lens -= num_strings;

  /* Create a string large enough to hold the resulting sql list */
  sql_list_str = smalloc((num_strings * (allstrings_len + 4)) + 1);

  /* Loop through each of the new, sql-quoted strings, append them to
   * the final location, and mark them up with the single quotes, commas,
   * and spaces. */
  marker = sql_list_str;
  for ( ; *escaped_strings != NULL; escaped_strings++, string_lens++ )
  {
    *marker++ = '\'';
    memcpy(marker, *escaped_strings, *string_lens);
    marker += *string_lens;
    *marker++ = '\''; 
    if ( *(escaped_strings + 1) != NULL )
    {
      *marker++ = ','; 
      *marker++ = ' ';
    }
  }
  *marker = '\0';

  /* Rewind the pointers again */
  escaped_strings -= num_strings;
  string_lens -= num_strings;
  
  /* Clean up after ourselves.  Because we obtained each of our escaped
   * strings by way of quote() and because quote uses malloc(), we free()
   * each string first, before free()ing the array */
  for (; *escaped_strings != NULL; escaped_strings++)
    free(*escaped_strings);
  escaped_strings -= num_strings;
  free(escaped_strings);
  free(string_lens);
  
  return sql_list_str;
} /* End make_sql_list() */

/**************************************************************************
 * fill_blem_info():  Takes a MySQL row from BLEM_DBTAB, in the order of:
 * BLEM_ALLCOLUMNS
 * Creates a new BlemInfo structure, and populates the data in it with
 * data from the row, then returns a pointer to the new structure
 */
BlemInfo *fill_blem_info(MYSQL_ROW myRow)
{
  BlemInfo *info = smalloc(sizeof(BlemInfo));

  info->db_id = (uint32) atoi(*myRow++);
  info->domain = sstrdup(*myRow++);
  info->reason = sstrdup(*myRow++);
  info->addedby = sstrdup(*myRow++);
  info->whenadded = (uint32) atoi(*myRow++);

  return info;
} /* End fill_blem_info() */

/***************************************************************************
 * db_get_blem_info():  Searches the blacklist table for a specified domain
 * and returns a BlemInfo structure, populated with the information retuned.
 * If a matching domain is not found, NULL is returned.
 */
BlemInfo *db_get_blem_info(char *domain)
{
  char *qdomain = NULL;
  MYSQL_RES *myResult;
  BlemInfo *info;

  qdomain = quote(domain);
  do_query( "SELECT " BLEM_ALLCOLUMNS " FROM " BLEM_DBTAB
            " WHERE domain = '%s'", qdomain );
  free(qdomain);

  myResult = mysql_store_result(mysql);
  if (mysql_num_rows(myResult) == 1) 
    info = fill_blem_info(mysql_fetch_row(myResult));
  else
    info = NULL;

  mysql_free_result(myResult);

  if (debug)
  {
    if (info)
      alog("[%s] debug: Received blacklist info from database: %s, %s:, %s",
           MODNAME, info->domain, info->addedby, info->reason);
    else
      alog("[%s] debug: %s queried and did not match anything in the "
           "blacklist", MODNAME, domain);
  }

  return info;
} /* End of db_get_blem_info() */

/***************************************************************************
 * search_query():  Perform a database query based on specific search
 * criteria.  Sub-function for db_get_blem_infos()
 */
int search_query(MYSQL_RES **myResult, const char *searchby, char *searchstr)
{
  char *qsearchstr = quote(searchstr);
  int retval;
    
  retval = do_query("SELECT " BLEM_ALLCOLUMNS " FROM " BLEM_DBTAB
                    " WHERE %s RLIKE '%s' ORDER BY domain", searchby, 
                    qsearchstr, searchby);
  free(qsearchstr);
  *myResult = mysql_store_result(mysql);
  return retval;
} /* End search_query() sub-function */

/***************************************************************************
 * db_get_blem_infos():  Returns an array of BlemInfo structures, populated
 * by the result of a regular expression search.  searchtype determines to
 * which column the search will apply.
 */
BlemInfo **db_get_blem_infos(const int searchtype, char *searchstr)
{
  MYSQL_RES *myResult;
  MYSQL_ROW myRow;
  BlemInfo **infos;
  uint32 num_rows;
  int retval;

  switch (searchtype)
  {
    case SEARCH_DOMAIN:
      retval = search_query(&myResult, "domain", searchstr);
      break;
    case SEARCH_ADDEDBY:
      retval = search_query(&myResult, "addedby", searchstr);
      break;
    default:
      retval = search_query(&myResult, "domain", searchstr);
      break;
  }

  if (retval == 0)
  {
    num_rows = mysql_num_rows(myResult);
    infos = scalloc(num_rows + 1, sizeof(BlemInfo *));
    for ( ; myRow = mysql_fetch_row(myResult); infos++)
      *infos = fill_blem_info(myRow);
    *infos = NULL;
    infos -= num_rows;
  }
  else
  {
    infos = smalloc(sizeof(BlemInfo *));
    *infos = NULL;
  }

  return infos;
} /* End db_get_blem_infos */

/***************************************************************************
 * db_add_blem_info():  Adds a blacklisted domain to the database.
 */
int db_add_blem_info(User *u, char *domain, char *reason)
{
  MYSQL_RES *myResult;
  char *qdomain, *qreason, *qaddedby;

  qdomain = quote(domain);
  qreason = quote(reason);
  qaddedby = quote(u->na->nc->display);
  do_query( "INSERT INTO " BLEM_DBTAB 
            " ( domain, reason, addedby, whenadded ) "
            "VALUES ( '%s', '%s', '%s', '%lu' )", qdomain, qreason,
            qaddedby, time(NULL) );
  free(qdomain);
  free(qreason);
  free(qaddedby);
  myResult = mysql_store_result(mysql);
  mysql_free_result(myResult);

  return MOD_CONT;
} /* End db_add_blem_info() */

/***************************************************************************
 * db_del_blem_infos():  Deletes infos from the database, and returns
 * the number of entries deleted.  A string containing a comma-separated 
 * list of domains is returned to the domains_sqlist pointer.
 */
uint32 db_del_blem_infos(char **domains_sqlist, BlemInfo **infos)
{
  MYSQL_RES *myResult;
  uint32 num_infos;
  char **domains;
  
  for (num_infos = 0; *infos; infos++, num_infos++)
    ; /* Count the number of infos */
  domains = scalloc(num_infos + 1, sizeof(char *));
  infos -= num_infos;

  for ( ; *infos; infos++, domains++ )
    *domains = (*infos)->domain;
  infos -= num_infos;
  domains -= num_infos;

  *domains_sqlist = make_sql_list(domains);
  free(domains);

  do_query( "DELETE FROM " BLEM_DBTAB " WHERE domain IN ( %s )", 
            *domains_sqlist );
  myResult = mysql_store_result(mysql);
  mysql_free_result(myResult);

  return num_infos;
} /* End db_del_blem_infos() */

/***************************************************************************
 * db_clear():  Deletes all of the domains in the blacklist and returns
 * the number of domains deleted.
 */
uint32 db_clear(void)
{
  MYSQL_RES *myResult;
  uint32 num_cleared;

  do_query( "DELETE FROM " BLEM_DBTAB );
  myResult = mysql_store_result(mysql);
  num_cleared = (uint32) mysql_affected_rows(mysql);
  mysql_free_result(myResult);
  
  return num_cleared;
} /* End db_clear() */

/***************************************************************************
 * do_query():  Perform a database query, consisting of the specified query 
 * string which is a printf()-style format string and variable list.  Be 
 * sure that the query string and sub-strings are properly escaped before 
 * calling this function.
 */
int do_query(const char *fmt, ... ) 
{
  va_list args;
#ifdef __GNU_SOURCE
  char *query = NULL;
#else
  char query[MAX_SQL_BUF];
#endif
  int result, slen;

  va_start(args, fmt);
#ifdef __GNU_SOURCE
  vasprintf( &query,
#else
  *query = '\0';
  vsnprintf( query, MAX_SQL_BUF,
#endif
             fmt, args );
#if 0
  /* This just fixes vim's unmatched parenthesis syntax highlight */
  )
#endif
  va_end(args);

#ifdef __GNU_SOURCE  
  if (!query)
#else
  if (*query == '\0')
#endif
    return -1;

  slen = strlen(query);

  if (debug) alog("[%s] debug: executing mysql_ping()", MODNAME);

  mysql_ping(mysql); /* checks for mysql connection and reconnect */

  if (debug) alog("[%s] debug: SQL query:  %s", MODNAME, query);

  result = mysql_real_query(mysql, query , slen);
#ifdef __GNU_SOURCE
  free(query);
#endif
  if (result)
  {
    log_perror(mysql_error(mysql));
    return result;
  }
  return 0;
} /* End do_query() */

/*******************************************************
 * sub_display_error():  Puts an informative error in the
 * log as to why the init failed.
 */
int sub_display_error(void)
{
  alog("[%s] Can't connect to MySQL: %s\n", MODNAME, mysql_error(mysql));
  alog("%s: unloading", MODNAME);
  return MOD_STOP;
} /* End sub_display_error() */

/*******************************************************
 * mydb_init_sub():  Checks for tables and creates them
 * if they don't exist.
 */
void mydb_init_sub(const char *tablename, const char *schema)
{
  MYSQL_RES *res;

  do_query("SHOW TABLES LIKE '%s'", tablename);
  res = mysql_store_result(mysql);
  if (mysql_num_rows(res) == 0)
  {
    do_query(schema);
    alog("[%s] Created table %s", MODNAME, tablename);
  }
  mysql_free_result(res);
} /* End mydb_init_sub() */

/***************************************************************************
 * mydb_init():  Attempt a connection to the MySQL database and handle 
 * any errors that may happen during that connection.  If the connection
 * succeeds, the global variable "mysql" contains a valid connection.
 *
 * A check is also done to make sure that the tables needed by this module
 * exist in the database.  If they don't, they are created.
 */
int mydb_init(void) 
{
  /* initializing mysql connection */
  mysql = mysql_init(NULL);
  if (MysqlSock) 
  {
    if ((!mysql_real_connect(mysql, MysqlHost, MysqlUser, MysqlPass, 
                             MysqlName, MysqlPort, MysqlSock, 0))) 
      return sub_display_error();
  } 
  else 
  {
    if ((!mysql_real_connect(mysql, MysqlHost, MysqlUser, MysqlPass, 
                             MysqlName, MysqlPort, NULL, 0))) 
      return sub_display_error();
  }
  if (mysql_select_db(mysql, MysqlName)) 
    return sub_display_error();

  /* checking for tables and creating them, if not found */
  mydb_init_sub(BLEM_DBTAB, BLEM_SCHEMA);
  
  return 0;
} /* End mydb_init() */

/***************************************************************************
 * db_keepalive():  This function is a kludge, put in place as a workaround
 * for a problem where the MySQL server would close the connection after
 * periods of idleness and mysql_ping() was causing us to receive a fatal
 * SIGPIPE signal instead of just re-establishing the connection.  When I
 * figure out how to prevent this problem in another way, this function adds
 * a callback at a 5 minute interval which calls mysql_ping() to keep the
 * connection alive.
 */
int db_keepalive(int argc, char *argv[])
{
  mysql_ping(mysql);
  moduleAddCallback( "OSBLEMKEEPALIVE", time(NULL) + dotime("1h"),
                     db_keepalive, 0, NULL );
  return MOD_CONT;
} /* End db_keepalive() */

/*************************  END OF FILE ***********************************/

