pennmush/game/data/
pennmush/game/log/
pennmush/game/save/
pennmush/game/txt/evt/
pennmush/game/txt/nws/
pennmush/os2/
pennmush/po/
pennmush/win32/msvc.net/
pennmush/win32/msvc6/
/**
 * \file extmail.c
 *
 * \brief The PennMUSH built-in mail system.
 *
 * \verbatim
 *---------------------------------------------------------------
 * extmail.c - Javelin's improved @mail system
 * Based on Amberyl's linked list mailer
 *
 * Summary of mail command syntax:
 * Sending:
 *   @mail[/sendswitch] player-list = message
 *     sendswitches: /silent, /urgent
 *     player-list is a space-separated list of players, aliases, or msg#'s 
 *     to reply to. Players can be names or dbrefs. Aliases start with *
 * Reading/Handling:
 *   @mail[/readswitch] [msg-list [= target]]
 *     With no readswitch, @mail reads msg-list (same as /read)
 *     With no readswitch and no msg-list, @mail lists all messages (/list)
 *     readswitches: /list, /read, /fwd (requires target list of players
 *      to forward messages to), /file (requires target folder to file
 *      to), /tag, /untag, /clear, /unclear, /purge (no msg-list),
 *      /count
 *     Assumes messages in current folder, set by @folder or @mail/folder
 *     msg-list can be one of: a message number, a message range,
 *     (2-3, 4-, -6), sender references (*player), date comparisons
 *     (~0, >2, <5), or the strings "urgent", "tagged", "cleared",
 *     "read", "unread", "all", or "folder"
 *     You can also use 1:2 (folder 1, message 2) and 1:2-3 for ranges.
 * Admin stuff:
 *   @mail[/switch] [player]
 *     Switches include: nuke (used to be "purge"), [efd]stats, debug
 *
 * THEORY OF OPERATION:
 *  Prior to pl11, mail was an unsorted linked list. When mail was sent,
 * it was added onto the end. To read mail, you scanned the whole list.
 * This is still how origmail.c works.
 *  As of pl11, extmail.c maintains mail as a sorted linked list, sorted
 * by recipient and order of receipt. This makes sending mail less
 * efficient (because you have to scan the list to figure out where to
 * insert), but reading/checking/deleting more efficient, 
 * because once you've found where the player's mail starts, you just
 * read from there. 
 *  That wouldn't be so exciting unless there was a fast way to find
 * where a player's mail chain started. Fortunately, there is. We
 * record that information for connected players when they connect,
 * on their descriptor. So, when connected players do reading/etc,
 * it's O(1). Sending to a connected player is O(1). Sending to an
 * unconnected player still requires scanning (O(n)), but you send once, 
 * and read/list/delete etc, multiple times.
 *  And just to make the sending to disconnected players faster, 
 * instead of scanning the whole maildb to find the insertion point,
 * we start the scan from the chain of the connected player with the
 * closest db# to the target player. This scales up very well.
 *--------------------------------------------------------------------
 * \endverbatim
 */

#include "config.h"
#include "copyrite.h"

#ifdef I_SYS_TIME
#include <sys/time.h>
#else
#include <time.h>
#endif
#include <ctype.h>
#ifdef I_SYS_TYPES
#include <sys/types.h>
#endif
#include <string.h>

#include "conf.h"
#include "externs.h"
#include "mushdb.h"
#include "dbdefs.h"
#include "match.h"
#include "extmail.h"
#include "function.h"
#include "malias.h"
#include "attrib.h"
#include "parse.h"
#include "mymalloc.h"
#include "pueblo.h"
#include "flags.h"
#include "log.h"
#include "lock.h"
#include "command.h"
#include "dbio.h"
#include "confmagic.h"


extern int do_convtime(const char *str, struct tm *ttm);	/* funtime.c */

static void do_mail_flags
  (dbref player, const char *msglist, mail_flag flag, int negate);
static char *mail_list_time(const char *the_time, int flag);
static MAIL *mail_fetch(dbref player, int num);
static MAIL *real_mail_fetch(dbref player, int num, int folder);
static MAIL *mailfun_fetch(dbref player, int nargs, char *arg1, char *arg2);
static void count_mail(dbref player,
		       int folder, int *rcount, int *ucount, int *ccount);
static void send_mail(dbref player,
		      dbref target, char *subject, char *message,
		      mail_flag flags, int silent, int nosig);
static int send_mail_alias(dbref player,
			   char *aname, char *subject,
			   char *message, mail_flag flags, int silent,
			   int nosig);
static MAIL *find_insertion_point(dbref player);
static int get_folder_number(dbref player, char *name);
static char *get_folder_name(dbref player, int fld);
static int player_folder(dbref player);
static int parse_folder(dbref player, char *folder_string);
static int mail_match(dbref player, MAIL *mp, struct mail_selector ms, int num);
static int parse_msglist
  (const char *msglist, struct mail_selector *ms, dbref player);
static int parse_message_spec
  (dbref player, const char *s, int *msglow, int *msghigh, int *folder);
static char *status_chars(MAIL *mp);
static char *status_string(MAIL *mp);
static int sign(int x);
static char *get_message(MAIL *mp);
static char *get_compressed_message(MAIL *mp);
static char *get_subject(MAIL *mp);
static char *get_sender(MAIL *mp, int full);
static int was_sender(dbref player, MAIL *mp);

MAIL *maildb;		 /**< The head of the mail list */
MAIL *tail_ptr;		 /**< The end of the mail list */

#define HEAD  maildb	 /**< The head of the mail list */
#define TAIL  tail_ptr	 /**< The end of the mail list */

/** A line of...dashes! */
#define DASH_LINE  \
  "-----------------------------------------------------------------------------"

int mdb_top = 0;		/**< total number of messages in mail db */

/*-------------------------------------------------------------------------*
 *   User mail functions (these are called from game.c)
 *
 * do_mail - cases without a /switch.
 * do_mail_send - sending mail
 * do_mail_read - read messages
 * do_mail_list - list messages
 * do_mail_flags - tagging, untagging, clearing, unclearing of messages
 * do_mail_file - files messages into a new folder
 * do_mail_fwd - forward messages to another player(s)
 * do_mail_count - count messages
 * do_mail_purge - purge cleared messages
 * do_mail_change_folder - change current folder
 * do_mail_unfolder - remove a folder name from MAILFOLDERS
 * do_mail_subject - set the current mail subject
 *-------------------------------------------------------------------------*/

/* Return the uncompressed text of a @mail in a static buffer */
static char *
get_message(MAIL *mp)
{
  static char text[BUFFER_LEN * 2];
  char tbuf[BUFFER_LEN * 2];

  if (!mp)
    return NULL;

  chunk_fetch(mp->msgid, tbuf, sizeof tbuf);
  strcpy(text, uncompress(tbuf));
  return text;
}

/* Return the compressed text of a @mail in a static buffer */
static char *
get_compressed_message(MAIL *mp)
{
  static char text[BUFFER_LEN * 2];

  if (!mp)
    return NULL;

  chunk_fetch(mp->msgid, text, sizeof text);
  return text;
}

/* Return the subject of a mail message, or (no subject) */
static char *
get_subject(MAIL *mp)
{
  static char sbuf[SUBJECT_LEN + 1];
  char *p;
  if (mp->subject) {
    strncpy(sbuf, uncompress(mp->subject), SUBJECT_LEN);
    sbuf[SUBJECT_LEN] = '\0';
    /* Stop at a return or a tab */
    for (p = sbuf; *p; p++) {
      if ((*p == '\r') || (*p == '\n') || (*p == '\t')) {
	*p = '\0';
	break;
      }
      if (!isprint((unsigned char) *p)) {
	*p = ' ';
      }
    }
  } else
    strcpy(sbuf, T("(no subject)"));
  return sbuf;
}

/* Return the name of the mail sender. */
static char *
get_sender(MAIL *mp, int full)
{
  static char tbuf1[BUFFER_LEN], *bp;
  bp = tbuf1;
  if (!GoodObject(mp->from))
    safe_str("!Purged!", tbuf1, &bp);
  else if (!was_sender(mp->from, mp))
    safe_str("!Purged!", tbuf1, &bp);
  else if (IsPlayer(mp->from) || !full)
    safe_str(Name(mp->from), tbuf1, &bp);
  else
    safe_format(tbuf1, &bp, "%s (owner: %s)", Name(mp->from),
		Name(Owner(mp->from)));
  *bp = '\0';
  return tbuf1;
}

/* Was this player the sender of this message? */
static int
was_sender(dbref player, MAIL *mp)
{
  /* If the dbrefs don't match, fail. */
  if (mp->from != player)
    return 0;
  /* If we don't know the creation time of the sender, succeed. */
  if (!mp->from_ctime)
    return 1;
  /* Succeed if and only if the creation times match. */
  return (mp->from_ctime == CreTime(player));
}

/** Change folders or rename a folder.
 * \verbatim
 * This implements @mail/folder
 * \endverbatim
 * \param player the enactor.
 * \param fld string containing folder number or name.
 * \param newname string containing folder name, if renaming.
 */
void
do_mail_change_folder(dbref player, char *fld, char *newname)
{
  int pfld;
  char *p;

  if (!fld || !*fld) {
    /* Check mail in all folders */
    for (pfld = MAX_FOLDERS; pfld >= 0; pfld--)
      check_mail(player, pfld, 1);
    pfld = player_folder(player);
    notify_format(player,
		  T("MAIL: Current folder is %d [%s]."), pfld,
		  get_folder_name(player, pfld));
    return;
  }
  pfld = parse_folder(player, fld);
  if (pfld < 0) {
    notify(player, T("MAIL: What folder is that?"));
    return;
  }
  if (newname && *newname) {
    /* We're changing a folder name here */
    if (strlen(newname) > FOLDER_NAME_LEN) {
      notify(player, T("MAIL: Folder name too long"));
      return;
    }
    for (p = newname; p && *p; p++) {
      if (!isdigit((unsigned char) *p) && !isalpha((unsigned char) *p)) {
	notify(player, T("MAIL: Illegal folder name"));
	return;
      }
    }
    add_folder_name(player, pfld, newname);
    notify_format(player, T("MAIL: Folder %d now named '%s'"), pfld, newname);
  } else {
    /* Set a new folder */
    set_player_folder(player, pfld);
    notify_format(player,
		  T("MAIL: Current folder set to %d [%s]."), pfld,
		  get_folder_name(player, pfld));
  }
}

/** Remove a folder name.
 * \verbatim
 * This implements @mail/unfolder
 * \endverbatim
 * \param player the enactor.
 * \param fld string containing folder number or name.
 */
void
do_mail_unfolder(dbref player, char *fld)
{
  int pfld;

  if (!fld || !*fld) {
    notify(player, T("MAIL: You must specify a folder name or number"));
    return;
  }
  pfld = parse_folder(player, fld);
  if (pfld < 0) {
    notify(player, T("MAIL: What folder is that?"));
    return;
  }
  add_folder_name(player, pfld, NULL);
  notify_format(player, T("MAIL: Folder %d now has no name"), pfld);
}


/** Tag a set of mail messages.
 * \param player the enactor.
 * \param msglist string specifying messages to tag.
 */
void
do_mail_tag(dbref player, const char *msglist)
{
  do_mail_flags(player, msglist, M_TAG, 0);
}

/** Clear a set of mail messages.
 * \param player the enactor.
 * \param msglist string specifying messages to clear.
 */
void
do_mail_clear(dbref player, const char *msglist)
{
  do_mail_flags(player, msglist, M_CLEARED, 0);
}

/** Untag a set of mail messages.
 * \param player the enactor.
 * \param msglist string specifying messages to untag.
 */
void
do_mail_untag(dbref player, const char *msglist)
{
  do_mail_flags(player, msglist, M_TAG, 1);
}

/** Unclear a set of mail messages.
 * \param player the enactor.
 * \param msglist string specifying messages to unclear.
 */
void
do_mail_unclear(dbref player, const char *msglist)
{
  do_mail_flags(player, msglist, M_CLEARED, 1);
}


/** Set or clear a flag on a set of messages.
 * \param player the enactor.
 * \param msglist string representing list of messages to operate on.
 * \param flag flag to set or clear.
 * \param negate if 1, clear the flag; if 0, set the flag.
 */
static void
do_mail_flags(dbref player, const char *msglist, mail_flag flag, int negate)
{
  MAIL *mp;
  struct mail_selector ms;
  int j, folder;
  folder_array i;
  int notified = 0;

  if (!parse_msglist(msglist, &ms, player)) {
    return;
  }
  FA_Init(i, j);
  j = 0;
  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
  for (mp = find_exact_starting_point(player);
       mp && (mp->to == player); mp = mp->next) {
    if ((mp->to == player) && (All(ms) || (Folder(mp) == folder))) {
      i[Folder(mp)]++;
      if (mail_match(player, mp, ms, i[Folder(mp)])) {
	j++;
	if (negate) {
	  mp->read &= ~flag;
	} else {
	  mp->read |= flag;
	}
	switch (flag) {
	case M_TAG:
	  if (All(ms)) {
	    if (!notified) {
	      notify_format(player,
			    T("MAIL: All messages in all folders %s."),
			    negate ? "untagged" : "tagged");
	      notified++;
	    }
	  } else
	    notify_format(player,
			  "MAIL: Msg #%d:%d %s.", Folder(mp),
			  i[Folder(mp)], negate ? "untagged" : "tagged");
	  break;
	case M_CLEARED:
	  if (All(ms)) {
	    if (!notified) {
	      notify_format(player,
			    T("MAIL: All messages in all folders %s."),
			    negate ? "uncleared" : "cleared");
	      notified++;
	    }
	  } else {
	    if (Unread(mp) && !negate) {
	      notify_format(player,
			    T
			    ("MAIL: Unread Msg #%d:%d cleared! Use @mail/unclear %d:%d to recover."),
			    Folder(mp), i[Folder(mp)], Folder(mp),
			    i[Folder(mp)]);
	    } else {
	      notify_format(player,
			    (negate ? T("MAIL: Msg #%d:%d uncleared.") :
			     T("MAIL: Msg #%d:%d cleared.")), Folder(mp),
			    i[Folder(mp)]);
	    }
	  }
	  break;
	}
      }
    }
  }
  if (!j) {
    /* ran off the end of the list without finding anything */
    notify(player, T("MAIL: You don't have any matching messages!"));
  }
  return;
}

/** File messages into a folder.
 * \verbatim
 * This implements @mail/file.
 * \endverbatim
 * \param player the enactor.
 * \param msglist list of messages to file.
 * \param folder name or number of folder to put messages in.
 */
void
do_mail_file(dbref player, char *msglist, char *folder)
{
  MAIL *mp;
  struct mail_selector ms;
  int j, foldernum, origfold;
  folder_array i;
  int notified = 0;

  if (!parse_msglist(msglist, &ms, player)) {
    return;
  }
  if ((foldernum = parse_folder(player, folder)) == -1) {
    notify(player, T("MAIL: Invalid folder specification"));
    return;
  }
  FA_Init(i, j);
  j = 0;
  origfold = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
  for (mp = find_exact_starting_point(player);
       mp && (mp->to == player); mp = mp->next) {
    if ((mp->to == player) && (All(ms) || (Folder(mp) == origfold))) {
      i[Folder(mp)]++;
      if (mail_match(player, mp, ms, i[Folder(mp)])) {
	j++;
	mp->read &= M_FMASK;	/* Clear the folder */
	mp->read &= ~M_CLEARED;	/* Unclear it if it was marked cleared */
	mp->read |= FolderBit(foldernum);
	if (All(ms)) {
	  if (!notified) {
	    notify_format(player,
			  T("MAIL: All messages filed in folder %d [%s]"),
			  foldernum, get_folder_name(player, foldernum));
	    notified++;
	  }
	} else
	  notify_format(player,
			T("MAIL: Msg %d:%d filed in folder %d [%s]"),
			origfold, i[origfold], foldernum,
			get_folder_name(player, foldernum));
      }
    }
  }
  if (!j) {
    /* ran off the end of the list without finding anything */
    notify(player, T("MAIL: You don't have any matching messages!"));
  }
  return;
}

/** Read mail messages.
 * This displays the contents of a set of mail messages.
 * \param player the enactor.
 * \param msglist list of messages to read.
 */
void
do_mail_read(dbref player, char *msglist)
{
  MAIL *mp;
  char tbuf1[BUFFER_LEN];
  char folderheader[BUFFER_LEN];
  struct mail_selector ms;
  int j, folder;
  folder_array i;

  if (!parse_msglist(msglist, &ms, player)) {
    return;
  }
  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
  FA_Init(i, j);
  j = 0;
  for (mp = find_exact_starting_point(player);
       mp && (mp->to == player); mp = mp->next) {
    if ((mp->to == player) && (All(ms) || Folder(mp) == folder)) {
      i[Folder(mp)]++;
      if (mail_match(player, mp, ms, i[Folder(mp)])) {
	/* Read it */
	j++;
	if (SUPPORT_PUEBLO) {
	  notify_noenter(player, tprintf("%cSAMP%c", TAG_START, TAG_END));
	  sprintf(folderheader,
		  "%cA XCH_HINT=\"List messages in this folder\" XCH_CMD=\"@mail/list %d:1-\"%c%s%c/A%c",
		  TAG_START, Folder(mp), TAG_END, T("Folder:"), TAG_START,
		  TAG_END);
	} else
	  strcpy(folderheader, T("Folder:"));
	notify(player, DASH_LINE);
	strcpy(tbuf1, get_sender(mp, 1));
	notify_format(player,
		      T
		      ("From: %-55s %s\nDate: %-25s   %s %2d   Message: %d\nStatus: %s"),
		      tbuf1, ((*tbuf1 != '!') && IsPlayer(mp->from)
			      && Connected(mp->from)
			      && (!hidden(mp->from)
				  || Priv_Who(player))) ? " (Conn)" : "      ",
		      show_time(mp->time, 0), folderheader, Folder(mp),
		      i[Folder(mp)], status_string(mp));
	notify_format(player, T("Subject: %s"), get_subject(mp));
	notify(player, DASH_LINE);
	if (SUPPORT_PUEBLO)
	  notify_noenter(player, tprintf("%c/SAMP%c", TAG_START, TAG_END));
	strcpy(tbuf1, get_message(mp));
	notify(player, tbuf1);
	if (SUPPORT_PUEBLO)
	  notify_format(player, "%cSAMP%c%s%c/SAMP%c", TAG_START, TAG_END,
			DASH_LINE, TAG_START, TAG_END);
	else
	  notify(player, DASH_LINE);
	if (Unread(mp))
	  mp->read |= M_MSGREAD;	/* mark message as read */
      }
    }
  }
  if (!j) {
    /* ran off the end of the list without finding anything */
    notify(player, T("MAIL: You don't have that many matching messages!"));
  }
  return;
}


/** List the flags, number, sender, subject, and date of messages in a
 * concise format.
 * \param player the enactor.
 * \param msglist list of messages to list.
 */
void
do_mail_list(dbref player, const char *msglist)
{
  char subj[30];
  char sender[30];
  MAIL *mp;
  struct mail_selector ms;
  int j, folder;
  folder_array i;

  if (!parse_msglist(msglist, &ms, player)) {
    return;
  }
  FA_Init(i, j);
  j = 0;
  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
  if (SUPPORT_PUEBLO)
    notify_noenter(player, tprintf("%cSAMP%c", TAG_START, TAG_END));
  notify_format(player,
		T
		("---------------------------  MAIL (folder %2d)  ------------------------------"),
		folder);
  for (mp = find_exact_starting_point(player); mp && (mp->to == player);
       mp = mp->next) {
    if ((mp->to == player) && (All(ms) || Folder(mp) == folder)) {
      i[Folder(mp)]++;
      if (mail_match(player, mp, ms, i[Folder(mp)])) {
	/* list it */
	if (SUPPORT_PUEBLO)
	  notify_noenter(player,
			 tprintf
			 ("%cA XCH_CMD=\"@mail/read %d:%d\" XCH_HINT=\"Read message %d in folder %d\"%c",
			  TAG_START, Folder(mp), i[Folder(mp)],
			  i[Folder(mp)], Folder(mp), TAG_END));
	strcpy(subj, chopstr(get_subject(mp), 28));
	strcpy(sender, chopstr(get_sender(mp, 0), 12));
	notify_format(player, "[%s] %2d:%-3d %c%-12s  %-*s %s",
		      status_chars(mp), Folder(mp), i[Folder(mp)],
		      ((*sender != '!') && (Connected(mp->from) &&
					    (!hidden(mp->from)
					     || Priv_Who(player)))
		       ? '*' : ' '), sender, 30, subj,
		      mail_list_time(show_time(mp->time, 0), 1));
	if (SUPPORT_PUEBLO)
	  notify_noenter(player, tprintf("%c/A%c", TAG_START, TAG_END));
      }
    }
  }
  notify(player, DASH_LINE);
  if (SUPPORT_PUEBLO)
    notify_format(player, "%c/SAMP%c", TAG_START, TAG_END);
  return;
}

static char *
mail_list_time(const char *the_time, int flag /* 1 for no year */ )
{
  static char newtime[BUFFER_LEN];
  const char *p;
  char *q;
  int i;
  p = the_time;
  q = newtime;
  if (!p || !*p)
    return NULL;
  /* Format of the_time is: day mon dd hh:mm:ss yyyy */
  /* Chop out :ss */
  for (i = 0; i < 16; i++) {
    if (*p)
      *q++ = *p++;
  }
  if (!flag) {
    for (i = 0; i < 3; i++) {
      if (*p)
	p++;
    }
    for (i = 0; i < 5; i++) {
      if (*p)
	*q++ = *p++;
    }
  }
  *q = '\0';
  return newtime;
}


/** Expunge mail that's marked for deletion.
 * \verbatim
 * This implements @mail/purge.
 * \endverbatim
 * \param player the enactor.
 */
void
do_mail_purge(dbref player)
{
  MAIL *mp, *nextp;

  /* Go through player's mail, and remove anything marked cleared */
  for (mp = find_exact_starting_point(player);
       mp && (mp->to == player); mp = nextp) {
    if ((mp->to == player) && Cleared(mp)) {
      /* Delete this one */
      /* head and tail of the list are special */
      if (mp == HEAD)
	HEAD = mp->next;
      else if (mp == TAIL)
	TAIL = mp->prev;
      /* relink the list */
      if (mp->prev != NULL)
	mp->prev->next = mp->next;
      if (mp->next != NULL)
	mp->next->prev = mp->prev;
      /* save the pointer */
      nextp = mp->next;
      /* then wipe */
      mdb_top--;
      free(mp->subject);
      chunk_delete(mp->msgid);
      mush_free((Malloc_t) mp, "mail");
    } else {
      nextp = mp->next;
    }
  }
  /* Clean up the player's mailp */
  if (Connected(player)) {
    desc_mail_set(player, NULL);
    desc_mail_set(player, find_exact_starting_point(player));
  }
  if (command_check_byname(player, "@MAIL"))
    notify(player, T("MAIL: Mailbox purged."));
  return;
}

/** Forward mail messages to someone(s) else.
 * \verbatim
 * This implements @mail/forward.
 * \endverbatim
 * \param player the enactor.
 * \param msglist list of messages to forward.
 * \param tolist list of recipients to forwared to.
 */
void
do_mail_fwd(dbref player, char *msglist, char *tolist)
{
  MAIL *mp;
  MAIL *last;
  struct mail_selector ms;
  int j, num, folder;
  folder_array i;
  const char *head;
  MAIL *temp;
  dbref target;
  int num_recpts = 0;
  const char **start;
  char *current;
  start = &head;

  if (!parse_msglist(msglist, &ms, player)) {
    return;
  }
  if (!tolist || !*tolist) {
    notify(player, T("MAIL: To whom should I forward?"));
    return;
  }
  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
  /* Mark the player's last message. This prevents a loop if
   * the forwarding command happens to forward a message back
   * to the player itself 
   */
  last = mp = find_exact_starting_point(player);
  if (!last) {
    notify(player, T("MAIL: You have no messages to forward."));
    return;
  }
  while (last->next && (last->next->to == player))
    last = last->next;

  FA_Init(i, j);
  while (mp && (mp->to == player) && (mp != last->next)) {
    if ((mp->to == player) && (All(ms) || (Folder(mp) == folder))) {
      i[Folder(mp)]++;
      if (mail_match(player, mp, ms, i[Folder(mp)])) {
	/* forward it to all players listed */
	head = tolist;
	while (head && *head) {
	  current = next_in_list(start);
	  /* Now locate a target */
	  num = atoi(current);
	  if (num) {
	    /* reply to a mail message */
	    temp = mail_fetch(player, num);
	    if (!temp) {
	      notify(player, T("MAIL: You can't reply to nonexistant mail."));
	    } else {
	      char tbuf1[BUFFER_LEN], tbuf2[BUFFER_LEN];
	      strcpy(tbuf1, uncompress(mp->subject));
	      strcpy(tbuf2, get_compressed_message(mp));
	      send_mail(player, temp->from, tbuf1, tbuf2, M_FORWARD | M_REPLY,
			1, 0);
	      num_recpts++;
	    }
	  } else {
	    /* forwarding to a player */
	    target =
	      match_result(player, current, TYPE_PLAYER,
			   MAT_ME | MAT_ABSOLUTE | MAT_PLAYER);
	    if (!GoodObject(target))
	      target = lookup_player(current);
	    if (!GoodObject(target))
	      target = short_page(current);
	    if (!GoodObject(target) || !IsPlayer(target)) {
	      notify_format(player, T("No such unique player: %s."), current);
	    } else {
	      char tbuf1[BUFFER_LEN], tbuf2[BUFFER_LEN];
	      strcpy(tbuf1, uncompress(mp->subject));
	      strcpy(tbuf2, get_compressed_message(mp));
	      send_mail(player, target, tbuf1, tbuf2, M_FORWARD, 1, 0);
	      num_recpts++;
	    }
	  }
	}
      }
    }
    mp = mp->next;
  }
  notify_format(player, T("MAIL: %d messages forwarded."), num_recpts);
}

/** Send a mail message.
 * \param player the enactor.
 * \param tolist list of recipients.
 * \param message message text.
 * \param flags flags to apply to the message.
 * \param silent if 1, don't notify sender for each message sent.
 * \param nosig if 1, don't apply sender's MAILSIGNATURE.
 */
void
do_mail_send(dbref player, char *tolist, char *message, mail_flag flags,
	     int silent, int nosig)
{
  const char *head;
  int num;
  dbref target;
  int mail_flags;
  char sbuf[SUBJECT_LEN + 1], *sb, *mb;
  int i = 0, subject_given = 0;
  const char **start;
  char *current;
  start = &head;

  if (!tolist || !*tolist) {
    notify(player, T("MAIL: I can't figure out who you want to mail to."));
    return;
  }
  if (!message || !*message) {
    notify(player, T("MAIL: I can't figure out what you want to send."));
    return;
  }
  sb = sbuf;
  mb = message;			/* Save the message pointer */
  while (*message && (i < SUBJECT_LEN) && *message != SUBJECT_COOKIE) {
    *sb++ = *message++;
    i++;
  }
  *sb = '\0';
  if (*message && (*message == SUBJECT_COOKIE)) {
    message++;
    subject_given = 1;
  } else
    message = mb;		/* Rewind the pointer to the beginning */
  /* Parse the player list */
  head = tolist;
  while (head && *head) {
    mail_flags = flags;
    current = next_in_list(start);
    /* Now locate a target */
    if (is_strict_integer(current)) {
      /* reply to a mail message */
      MAIL *temp;

      num = parse_integer(current);

      temp = mail_fetch(player, num);
      if (!temp) {
	notify(player, T("MAIL: You can't reply to nonexistent mail."));
	return;
      }
      if (subject_given)
	send_mail(player, temp->from, sbuf, message, mail_flags, silent, nosig);
      else
	send_mail(player, temp->from, uncompress(temp->subject), message,
		  mail_flags | M_REPLY, silent, nosig);
    } else {
      /* send a new mail message */
      target =
	match_result(player, current, TYPE_PLAYER,
		     MAT_ME | MAT_ABSOLUTE | MAT_PLAYER);
      if (!GoodObject(target))
	target = lookup_player(current);
      if (!GoodObject(target))
	target = short_page(current);
      if (!GoodObject(target) || !IsPlayer(target)) {
	if (!send_mail_alias
	    (player, current, sbuf, message, mail_flags, silent, nosig))
	  notify_format(player, T("No such unique player: %s."), current);
      } else
	send_mail(player, target, sbuf, message, mail_flags, silent, nosig);
    }
  }
}

/*-------------------------------------------------------------------------*
 *   Admin mail functions
 *
 * do_mail_nuke - clear & purge mail for a player, or all mail in db.
 * do_mail_stat - stats on mail for a player, or for all db.
 * do_mail_debug - fix mail with a sledgehammer
 *-------------------------------------------------------------------------*/

/*-------------------------------------------------------------------------*
 *   Basic mail functions
 *-------------------------------------------------------------------------*/
static MAIL *
mail_fetch(dbref player, int num)
{
  /* get an arbitrary mail message in the current folder */
  return real_mail_fetch(player, num, player_folder(player));
}

static MAIL *
real_mail_fetch(dbref player, int num, int folder)
{
  MAIL *mp;
  int i = 0;

  for (mp = find_exact_starting_point(player); mp != NULL; mp = mp->next) {
    if (mp->to > player)
      break;
    if ((mp->to == player) && ((folder < 0) || (Folder(mp) == folder)))
      i++;
    if (i == num)
      return mp;
  }
  return NULL;
}


static void
count_mail(dbref player, int folder, int *rcount, int *ucount, int *ccount)
{
  /* returns count of read, unread, & cleared messages as rcount, ucount,
   * ccount. folder=-1 returns for all folders */

  MAIL *mp;
  int rc, uc, cc;

  cc = rc = uc = 0;
  for (mp = find_exact_starting_point(player);
       mp && (mp->to == player); mp = mp->next) {
    if ((mp->to == player) && ((folder == -1) || (Folder(mp) == folder))) {
      if (Cleared(mp))
	cc++;
      else if (Read(mp))
	rc++;
      else
	uc++;
    }
  }
  *rcount = rc;
  *ucount = uc;
  *ccount = cc;
}

/* ARGSUSED */
FUNCTION(fun_mailsend)
{
  /* mailsend(<target>,[<subject>/]<message>) */
  if ((fun->flags & FN_NOSIDEFX) || Gagged(executor) ||
      !command_check_byname(executor, "@MAIL"))
    safe_str(T(e_perm), buff, bp);
  else if (FUNCTION_SIDE_EFFECTS)
    do_mail_send(executor, args[0], args[1], 0, 1, 0);
  else
    safe_str(T(e_disabled), buff, bp);
}

static void
send_mail(dbref player, dbref target, char *subject, char *message,
	  mail_flag flags, int silent, int nosig)
{
  /* send a mail message */

  MAIL *newp, *mp;
  int rc, uc, cc;
  char *newmsg, *nm, *buff, *bp;
  char const *ms;
  char *mailsig;
  char sbuf[BUFFER_LEN];
  ATTR *a;
  char *cp;

  if (!IsPlayer(target)) {
    notify(player, T("MAIL: You cannot send mail to non-existent people."));
    return;
  }
  if (!strcasecmp(message, "clear")) {
    notify(player,
	   T("MAIL: You probably don't wanna send mail saying 'clear'."));
    return;
  }
  if (!(Hasprivs(player) || eval_lock(player, target, Mail_Lock))) {
    notify_format(player,
		  T("MAIL: %s is not accepting mail from you right now."),
		  Name(target));
    return;
  }
  count_mail(target, 0, &rc, &uc, &cc);
  if ((rc + uc + cc) >= MAIL_LIMIT) {
    notify_format(player, T("MAIL: %s's mailbox is full. Can't send."),
		  Name(target));
    return;
  }

  /* initialize the appropriate fields */
  newp = (MAIL *) mush_malloc(sizeof(struct mail), "mail");
  newp->to = target;
  newp->from = player;
  newp->from_ctime = CreTime(player);

  /* Deal with the subject */
  cp = remove_markup(subject, NULL);
  if (subject && cp && *cp)
    strcpy(sbuf, cp);
  else
    strcpy(sbuf, T("(no subject)"));
  if ((flags & M_FORWARD) && !string_prefix(sbuf, "Fwd:"))
    newp->subject = compress(chopstr(tprintf("Fwd: %s", sbuf), SUBJECT_LEN));
  else if ((flags & M_REPLY) && !string_prefix(sbuf, "Re:"))
    newp->subject = compress(chopstr(tprintf("Re: %s", sbuf), SUBJECT_LEN));
  else if ((a = atr_get_noparent(player, "MAILSUBJECT")) != NULL)
    /* Don't bother to uncompress a->value */
    newp->subject = u_strdup(AL_STR(a));
  else
    newp->subject = compress(sbuf);
  if (flags & M_FORWARD) {
    /* Forwarding passes the message already compressed */
    u_int_16 len = strlen(message) + 1;
    newp->msgid = chunk_create(message, len, 1);
  } else {
    u_int_16 len;
    unsigned char *text;
    newmsg = (char *) mush_malloc(BUFFER_LEN, "string");
    if (!newmsg)
      mush_panic(T("Failed to allocate string in send_mail"));
    nm = newmsg;
    safe_str(message, newmsg, &nm);
    if (!nosig && ((a = atr_get_noparent(player, "MAILSIGNATURE")) != NULL)) {
      /* Append the MAILSIGNATURE to the mail - Cordin@Dune's idea */
      buff = (char *) mush_malloc(BUFFER_LEN, "string");
      if (!buff)
	mush_panic(T("Failed to allocate string in send_mail"));
      ms = mailsig = safe_atr_value(a);
      bp = buff;
      process_expression(buff, &bp, &ms, player, player, player,
			 PE_DEFAULT, PT_DEFAULT, NULL);
      *bp = '\0';
      free(mailsig);
      safe_str(buff, newmsg, &nm);
      mush_free((Malloc_t) buff, "string");
    }
    *nm = '\0';
    text = compress(newmsg);
    len = strlen(text) + 1;
    newp->msgid = chunk_create(text, len, 1);
    free(text);
    mush_free((Malloc_t) newmsg, "string");
  }

  newp->time = mudtime;
  newp->read = flags & M_FMASK;	/* Send to folder 0 */

  /* Where do we insert it? After mp, wherever that is.
   * This can return NULL if there are no messages or
   * if we insert at the head of the list 
   */
  mp = find_insertion_point(target);

  if (mp) {
    newp->prev = mp;
    newp->next = mp->next;
    if (mp == TAIL)
      TAIL = newp;
    else
      mp->next->prev = newp;
    mp->next = newp;
  } else {
    if (HEAD) {
      /* Insert at the front */
      newp->next = HEAD;
      newp->prev = NULL;
      HEAD->prev = newp;
      HEAD = newp;
    } else {
      /* This is the first message in the maildb */
      HEAD = newp;
      TAIL = newp;
      newp->prev = NULL;
      newp->next = NULL;
    }
  }

  /* If the target's mailp isn't pointing to their list, we'd better
   * set it
   */
  if (Connected(target))
    desc_mail_set(target, find_exact_starting_point(target));

  mdb_top++;

  /* notify people */
  if (!silent)
    notify_format(player,
		  T("MAIL: You sent your message to %s."), Name(target));
  notify_format(target,
		T("MAIL: You have a new message (%d) from %s."),
		rc + uc + cc + 1, Name(player));

  if (AMAIL_ATTR && (atr_get_noparent(target, "AMAIL"))
      && (player != target) && Hasprivs(target))
    did_it(player, target, NULL, NULL, NULL, NULL, "AMAIL", NOTHING);

  return;
}


/** Wipe the entire maildb.
 * \param player the enactor.
 */
void
do_mail_nuke(dbref player)
{
  MAIL *mp, *nextp;

  if (!God(player)) {
    notify(player, T("The postal service issues a warrant for your arrest."));
    return;
  }
  /* walk the list */
  for (mp = HEAD; mp != NULL; mp = nextp) {
    nextp = mp->next;
    if (mp->subject)
      free(mp->subject);
    chunk_delete(mp->msgid);
    mush_free((Malloc_t) mp, "mail");
  }

  HEAD = TAIL = NULL;
  mdb_top = 0;
  desc_mail_clear();

  do_log(LT_ERR, 0, 0, T("** MAIL PURGE ** done by %s(#%d)."),
	 Name(player), player);
  notify(player, T("You annihilate the post office. All messages cleared."));
}

/** Low-level mail sanity checking or debugging.
 * "how to fix mail with a sledgehammer".
 * \param player the enactor.
 * \param action action to take (sanity, clear, fix)
 * \param victim name of player whose mail is to be checked (NULL to check all)
 */
void
do_mail_debug(dbref player, const char *action, const char *victim)
{
  dbref target;
  MAIL *mp, *nextp;
  int i;

  if (!Wizard(player)) {
    notify(player, T("Go get some bugspray."));
    return;
  }
  if (string_prefix("clear", action)) {
    target = lookup_player(victim);
    if (target == NOTHING) {
      target = match_result(player, victim, NOTYPE, MAT_ABSOLUTE);
    }
    if (target == NOTHING) {
      notify_format(player, T("%s: No such player."), victim);
      return;
    }
    do_mail_clear(target, "ALL");
    do_mail_purge(target);
    notify_format(player, T("Mail cleared for %s(#%d)."), Name(target), target);
    return;
  } else if (string_prefix("sanity", action)) {
    for (i = 0, mp = HEAD; mp != NULL; i++, mp = mp->next) {
      if (!GoodObject(mp->to))
	notify_format(player, T("Bad object #%d has mail."), mp->to);
      else if (!IsPlayer(mp->to))
	notify_format(player, T("%s(#%d) has mail but is not a player."),
		      Name(mp->to), mp->to);
    }
    if (i != mdb_top) {
      notify_format(player,
		    T
		    ("Mail database top is %d, actual message count is %d. Fixing."),
		    mdb_top, i);
      mdb_top = i;
    }
    notify(player, T("Mail sanity check completed."));
  } else if (string_prefix("fix", action)) {
    for (i = 0, mp = HEAD; mp != NULL; i++, mp = nextp) {
      if (!GoodObject(mp->to) || !IsPlayer(mp->to)) {
	notify_format(player, T("Fixing mail for #%d."), mp->to);
	/* Delete this one */
	/* head and tail of the list are special */
	if (mp == HEAD)
	  HEAD = mp->next;
	else if (mp == TAIL)
	  TAIL = mp->prev;
	/* relink the list */
	if (mp->prev != NULL)
	  mp->prev->next = mp->next;
	if (mp->next != NULL)
	  mp->next->prev = mp->prev;
	/* save the pointer */
	nextp = mp->next;
	/* then wipe */
	mdb_top--;
	if (mp->subject)
	  free(mp->subject);
	chunk_delete(mp->msgid);
	mush_free((Malloc_t) mp, "mail");
      } else if (!GoodObject(mp->from)) {
	/* Oops, it's from a player whose dbref is out of range!
	 * We'll make it appear to be from #0 instead because there's 
	 * no really good choice
	 */
	mp->from = 0;
	nextp = mp->next;
      } else {
	nextp = mp->next;
      }
    }
    notify(player, T("Mail sanity fix completed."));
  } else {
    notify(player, T("That is not a debugging option."));
    return;
  }
}

/** Display mail database statistics.
 * \verbatim
 * This implements @mail/stat, @mail/dstat, @mail/fstat.
 * \endverbatim
 * \param player the enactor.
 * \param name name of player whose mail stats are to be checked (NULL for all)
 * \param full level of verbosity: MSTATS_COUNT = just count messages,
 * MSTATS_READ = break down by read/unread, MSTATS_SIZE = include
 * space usage.
 */
void
do_mail_stats(dbref player, char *name, enum mail_stats_type full)
{
  dbref target;
  int fc, fr, fu, tc, tr, tu, fchars, tchars, cchars;
  char last[50];
  MAIL *mp;

  fc = fr = fu = tc = tr = tu = cchars = fchars = tchars = 0;

  /* find player */

  if (*name == '\0') {
    if (Wizard(player))
      target = AMBIGUOUS;
    else
      target = player;
  } else if (*name == NUMBER_TOKEN) {
    target = atoi(&name[1]);
    if (!GoodObject(target) || !IsPlayer(target))
      target = NOTHING;
  } else if (!strcasecmp(name, "me")) {
    target = player;
  } else {
    target = lookup_player(name);
  }

  /* Don't use GoodObject here! */
  if (target == NOTHING) {
    target = match_result(player, name, NOTYPE, MAT_ABSOLUTE);
  }
  if ((target == NOTHING) || ((target == AMBIGUOUS) && !Wizard(player))) {
    notify_format(player, T("%s: No such player."), name);
    return;
  }
  if (!Wizard(player) && (target != player)) {
    notify(player, T("The post office protects privacy!"));
    return;
  }
  /* this comand is computationally expensive */

  if (!payfor(player, FIND_COST)) {
    notify_format(player, T("Finding mail stats costs %d %s."), FIND_COST,
		  (FIND_COST == 1) ? MONEY : MONIES);
    return;
  }
  if (target == AMBIGUOUS) {	/* stats for all */
    if (full == MSTATS_COUNT) {
      notify_format(player,
		    T("There are %d messages in the mail spool."), mdb_top);
      return;
    } else if (full == MSTATS_READ) {
      for (mp = HEAD; mp != NULL; mp = mp->next) {
	if (Cleared(mp))
	  fc++;
	else if (Read(mp))
	  fr++;
	else
	  fu++;
      }
      notify_format(player,
		    T
		    ("MAIL: There are %d msgs in the mail spool, %d unread, %d cleared."),
		    fc + fr + fu, fu, fc);
      return;
    } else {
      for (mp = HEAD; mp != NULL; mp = mp->next) {
	if (Cleared(mp)) {
	  fc++;
	  cchars += strlen(get_message(mp));
	} else if (Read(mp)) {
	  fr++;
	  fchars += strlen(get_message(mp));
	} else {
	  fu++;
	  tchars += strlen(get_message(mp));
	}
      }
      notify_format(player,
		    T
		    ("MAIL: There are %d old msgs in the mail spool, totalling %d characters."),
		    fr, fchars);
      notify_format(player,
		    T
		    ("MAIL: There are %d new msgs in the mail spool, totalling %d characters."),
		    fu, tchars);
      notify_format(player,
		    ("MAIL: There are %d cleared msgs in the mail spool, totalling %d characters."),
		    fc, cchars);
      return;
    }
  }
  /* individual stats */

  if (full == MSTATS_COUNT) {
    /* just count number of messages */
    for (mp = HEAD; mp != NULL; mp = mp->next) {
      if (was_sender(target, mp))
	fr++;
      if (mp->to == target)
	tr++;
    }
    notify_format(player, T("%s sent %d messages."), Name(target), fr);
    notify_format(player, T("%s has %d messages."), Name(target), tr);
    return;
  }
  /* more detailed message count */
  for (mp = HEAD; mp != NULL; mp = mp->next) {
    if (was_sender(target, mp)) {
      if (Cleared(mp))
	fc++;
      else if (Read(mp))
	fr++;
      else
	fu++;
      if (full == MSTATS_SIZE)
	fchars += strlen(get_message(mp));
    }
    if (mp->to == target) {
      if (!tr && !tu)
	strcpy(last, show_time(mp->time, 0));
      if (Cleared(mp))
	tc++;
      else if (Read(mp))
	tr++;
      else
	tu++;
      if (full == MSTATS_SIZE)
	tchars += strlen(get_message(mp));
    }
  }

  notify_format(player, T("Mail statistics for %s:"), Name(target));

  if (full == MSTATS_READ) {
    notify_format(player, T("%d messages sent, %d unread, %d cleared."),
		  fc + fr + fu, fu, fc);
    notify_format(player, T("%d messages received, %d unread, %d cleared."),
		  tc + tr + tu, tu, tc);
  } else {
    notify_format(player,
		  T
		  ("%d messages sent, %d unread, %d cleared, totalling %d characters."),
		  fc + fr + fu, fu, fc, fchars);
    notify_format(player,
		  T
		  ("%d messages received, %d unread, %d cleared, totalling %d characters."),
		  tc + tr + tu, tu, tc, tchars);
  }

  if (tc + tr + tu > 0)
    notify_format(player, T("Last is dated %s"), last);
  return;
}


/** Main mail wrapper.
 * \verbatim
 * This implements the @mail command when called with no switches.
 * \endverbatim
 * \param player the enactor.
 * \param arg1 left-hand argument (before the =)
 * \param arg2 right-hand argument (after the =)
 */
void
do_mail(dbref player, char *arg1, char *arg2)
{
  dbref sender;
  /* Force player to be a real player, but keep track of the
   * enactor in case we're sending mail, which objects can do
   */
  sender = player;
  player = Owner(player);
  if (!arg1 || !*arg1) {
    if (arg2 && *arg2) {
      notify(player, T("MAIL: Invalid mail command."));
      return;
    }
    /* just the "@mail" command */
    do_mail_list(player, "");
    return;
  }
  /* purge a player's mailbox */
  if (!strcasecmp(arg1, "purge")) {
    do_mail_purge(player);
    return;
  }
  /* clear message */
  if (!strcasecmp(arg1, "clear")) {
    do_mail_clear(player, arg2);
    return;
  }
  if (!strcasecmp(arg1, "unclear")) {
    do_mail_unclear(player, arg2);
    return;
  }
  if (arg2 && *arg2) {
    /* Sending mail */
    if (Gagged(sender))
      notify(sender, T("You cannot do that while gagged."));
    else
      do_mail_send(sender, arg1, arg2, 0, 0, 0);
  } else {
    /* Must be reading or listing mail - no arg2 */
    if (isdigit((unsigned char) *arg1) && !strchr(arg1, '-'))
      do_mail_read(player, arg1);
    else
      do_mail_list(player, arg1);
  }
  return;
}

/*-------------------------------------------------------------------------*
 *   Auxiliary functions
 *-------------------------------------------------------------------------*/
/* ARGSUSED */
FUNCTION(fun_folderstats)
{
  /* This function can take one of four formats:
   * folderstats() -> returns stats for my current folder
   * folderstats(folder#) -> returns stats for my folder folder#
   * folderstats(player) -> returns stats for player's current folder
   * folderstats(player,folder#) -> returns stats for player's folder folder#
   */
  dbref player;
  int rc, uc, cc;

  switch (nargs) {
  case 0:
    count_mail(executor, player_folder(executor), &rc, &uc, &cc);
    break;
  case 1:
    if (!is_integer(args[0])) {
      /* handle the case of wanting to count the number of messages */
      if ((player =
	   noisy_match_result(executor, args[0], TYPE_PLAYER,
			      MAT_ME | MAT_ABSOLUTE | MAT_PLAYER)) == NOTHING) {
	safe_str(T("#-1 NO SUCH PLAYER"), buff, bp);
	return;
      } else if (!controls(executor, player)) {
	safe_str(T(e_perm), buff, bp);
	return;
      } else {
	count_mail(player, player_folder(player), &rc, &uc, &cc);
      }
    } else {
      count_mail(executor, parse_integer(args[0]), &rc, &uc, &cc);
    }
    break;
  case 2:
    if ((player =
	 noisy_match_result(executor, args[0], TYPE_PLAYER,
			    MAT_ME | MAT_ABSOLUTE | MAT_PLAYER)) == NOTHING) {
      safe_str(T("#-1 NO SUCH PLAYER"), buff, bp);
      return;
    } else if (!controls(executor, player)) {
      safe_str(T(e_perm), buff, bp);
      return;
    }
    if (!is_integer(args[1])) {
      safe_str(T("#-1 FOLDER MUST BE INTEGER"), buff, bp);
      return;
    }
    count_mail(player, parse_integer(args[1]), &rc, &uc, &cc);
    break;
  default:
    /* This should never happen */
    return;
  }

  safe_integer(rc, buff, bp);
  safe_chr(' ', buff, bp);
  safe_integer(uc, buff, bp);
  safe_chr(' ', buff, bp);
  safe_integer(cc, buff, bp);
  return;
}

/* ARGSUSED */
FUNCTION(fun_mail)
{
  /* mail([<player>,] [<folder #>:]<message #>)
   * mail() --> return total # of messages for executor
   * mail(<player>) --> return total # of messages for player
   */

  MAIL *mp;
  dbref player;
  int rc, uc, cc;

  if (nargs == 0) {
    count_mail(executor, -1, &rc, &uc, &cc);
    safe_integer(rc + uc + cc, buff, bp);
    return;
  }
  /* Try mail(<player>) */
  if (nargs == 1) {
    player =
      match_result(executor, args[0], TYPE_PLAYER,
		   MAT_ME | MAT_ABSOLUTE | MAT_PLAYER);
    if (GoodObject(player)) {
      if (!controls(executor, player)) {
	safe_str(T(e_perm), buff, bp);
      } else {
	count_mail(player, -1, &rc, &uc, &cc);
	safe_integer(rc, buff, bp);
	safe_chr(' ', buff, bp);
	safe_integer(uc, buff, bp);
	safe_chr(' ', buff, bp);
	safe_integer(cc, buff, bp);
      }
      return;
    }
  }
  /* That didn't work. Ok, try mailfun_fetch */
  mp = mailfun_fetch(executor, nargs, args[0], args[1]);
  if (mp) {
    safe_str(get_message(mp), buff, bp);
    return;
  }
  safe_str(T("#-1 INVALID MESSAGE OR PLAYER"), buff, bp);
  return;
}

/* A helper routine used by all the mail*() functions
 * We parse the following format:
 *  func([<player>,] [<folder #>:]<message #>)
 * and return the matching message or NULL
 */
static MAIL *
mailfun_fetch(dbref player, int nargs, char *arg1, char *arg2)
{
  dbref target;
  int msg;
  int folder;

  if (nargs == 1) {
    /* Simply a message number or folder:message */
    if (parse_message_spec(player, arg1, &msg, NULL, &folder))
      return real_mail_fetch(player, msg, folder);
    else {
      return NULL;
    }
  } else {
    /* Both a target and a message */
    if ((target =
	 noisy_match_result(player, arg1, TYPE_PLAYER,
			    MAT_ME | MAT_ABSOLUTE | MAT_PLAYER)) == NOTHING) {
      return NULL;
    } else if (!controls(player, target)) {
      notify(player, T("Permission denied"));
      return NULL;
    }
    if (parse_message_spec(target, arg2, &msg, NULL, &folder))
      return real_mail_fetch(target, msg, folder);
    else {
      notify(player, T("Invalid message specification"));
      return NULL;
    }
  }
  /* NOTREACHED */
  return NULL;
}


/* ARGSUSED */
FUNCTION(fun_mailfrom)
{
  MAIL *mp;

  mp = mailfun_fetch(executor, nargs, args[0], args[1]);
  if (!mp)
    safe_str("#-1", buff, bp);
  else if (!was_sender(mp->from, mp))
    safe_str("#-1", buff, bp);
  else
    safe_dbref(mp->from, buff, bp);
  return;
}


/* ARGSUSED */
FUNCTION(fun_mailstats)
{
  /* Copied from extmail.c: do_mail_stats with changes to refer to
   * args[0] rather than name, etc
   *
   * Todo: Change it so it is just mailstats and depending on what
   * it is called as, it will change if it is doing a fstats, dstats,
   * or stats. Looks like stats -> full=0, dstats -> full=1, fstats ->
   * full=2
   */

  /* mail database statistics */

  dbref target;
  int fc, fr, fu, tc, tr, tu, fchars, tchars, cchars;
  char last[50];
  MAIL *mp;
  int full;

  /* Figure out how we were called */
  if (string_prefix(called_as, "mailstats")) {
    full = 0;
  } else if (string_prefix(called_as, "maildstats")) {
    full = 1;
  } else if (string_prefix(called_as, "mailfstats")) {
    full = 2;
  } else {
    safe_str(T("#-? fun_mailstats called with invalid called_as!"), buff, bp);
    return;
  }

  fc = fr = fu = tc = tr = tu = cchars = fchars = tchars = 0;

  /* find player */
  if (*args[0] == '\0') {
    if Wizard
      (executor)
	target = AMBIGUOUS;
    else
      target = executor;
  } else if (*args[0] == NUMBER_TOKEN) {
    target = atoi(&args[0][1]);
    if (!GoodObject(target) || !IsPlayer(target))
      target = NOTHING;
  } else if (!strcasecmp(args[0], "me")) {
    target = executor;
  } else {
    target = lookup_player(args[0]);
  }

  if (!GoodObject(target)) {
    target = match_result(executor, args[0], NOTYPE, MAT_ABSOLUTE);
  }
  if (!GoodObject(target) || !IsPlayer(target)) {
    notify_format(executor, T("%s: No such player."), args[0]);
    return;
  }
  if (!controls(executor, target)) {
    notify(executor, T("The post office protects privacy!"));
    return;
  }
  /* this comand is computationally expensive */

  if (!payfor(executor, FIND_COST)) {
    notify_format(executor, T("Finding mail stats costs %d %s."), FIND_COST,
		  (FIND_COST == 1) ? MONEY : MONIES);
    return;
  }
  if (target == AMBIGUOUS) {	/* stats for all */
    if (full == 0) {
      /* FORMAT
       * total mail
       */
      safe_integer(mdb_top, buff, bp);
      return;
    } else if (full == 1) {
      for (mp = HEAD; mp != NULL; mp = mp->next) {
	if (Cleared(mp))
	  fc++;
	else if (Read(mp))
	  fr++;
	else
	  fu++;
      }
      /* FORMAT
       * sent, sent_unread, sent_cleared
       */
      safe_format(buff, bp, "%d %d %d", fc + fr + fu, fu, fc);
    } else {
      for (mp = HEAD; mp != NULL; mp = mp->next) {
	if (Cleared(mp)) {
	  fc++;
	  cchars += strlen(get_message(mp));
	} else if (Read(mp)) {
	  fr++;
	  fchars += strlen(get_message(mp));
	} else {
	  fu++;
	  tchars += strlen(get_message(mp));
	}
      }
      /* FORMAT
       * sent_read, sent_read_characters,
       * sent_unread, sent_unread_characters,
       * sent_clear, sent_clear_characters
       */
      safe_format(buff, bp, "%d %d %d %d %d %d",
		  fr, fchars, fu, tchars, fc, cchars);
      return;
    }
  }

  /* individual stats */

  if (full == 0) {
    /* just count number of messages */
    for (mp = HEAD; mp != NULL; mp = mp->next) {
      if (was_sender(target, mp))
	fr++;
      if (mp->to == target)
	tr++;
    }
    /* FORMAT
     * sent, received
     */
    safe_format(buff, bp, "%d %d", fr, tr);
    return;
  }
  /* more detailed message count */
  for (mp = HEAD; mp != NULL; mp = mp->next) {
    if (was_sender(target, mp)) {
      if (Cleared(mp))
	fc++;
      else if (Read(mp))
	fr++;
      else
	fu++;
      if (full == 2)
	fchars += strlen(get_message(mp));
    }
    if (mp->to == target) {
      if (!tr && !tu)
	strcpy(last, show_time(mp->time, 0));
      if (Cleared(mp))
	tc++;
      else if (Read(mp))
	tr++;
      else
	tu++;
      if (full == 2)
	tchars += strlen(get_message(mp));
    }
  }

  if (full == 1) {
    /* FORMAT
     * sent, sent_unread, sent_cleared
     * received, rec_unread, rec_cleared
     */
    safe_format(buff, bp, "%d %d %d %d %d %d",
		fc + fr + fu, fu, fc, tc + tr + tu, tu, tc);
  } else {
    /* FORMAT
     * sent, sent_unread, sent_cleared, sent_bytes
     * received, rec_unread, rec_cleared, rec_bytes
     */
    safe_format(buff, bp, "%d %d %d %d %d %d %d %d",
		fc + fr + fu, fu, fc, fchars, tc + tr + tu, tu, tc, tchars);
  }
}


/* ARGSUSED */
FUNCTION(fun_mailtime)
{
  MAIL *mp;

  mp = mailfun_fetch(executor, nargs, args[0], args[1]);
  if (!mp)
    safe_str("#-1", buff, bp);
  else
    safe_str(show_time(mp->time, 0), buff, bp);
}

/* ARGSUSED */
FUNCTION(fun_mailstatus)
{
  MAIL *mp;

  mp = mailfun_fetch(executor, nargs, args[0], args[1]);
  if (!mp)
    safe_str("#-1", buff, bp);
  else
    safe_str(status_chars(mp), buff, bp);
  return;
}


/* ARGSUSED */
FUNCTION(fun_mailsubject)
{
  MAIL *mp;

  mp = mailfun_fetch(executor, nargs, args[0], args[1]);
  if (!mp)
    safe_str("#-1", buff, bp);
  else
    safe_str(uncompress(mp->subject), buff, bp);
  return;
}

/** Save mail to disk.
 * \param fp pointer to filehandle to save mail to.
 * \return number of mail messages saved.
 */
int
dump_mail(FILE * fp)
{
  MAIL *mp;
  int count = 0;
  int mail_flags = 0;

  mail_flags += MDBF_SUBJECT;
  mail_flags += MDBF_ALIASES;
  mail_flags += MDBF_NEW_EOD;
  mail_flags += MDBF_SENDERCTIME;

  if (mail_flags)
    fprintf(fp, "+%d\n", mail_flags);

  save_malias(fp);

  OUTPUT(fprintf(fp, "%d\n", mdb_top));

  for (mp = HEAD; mp != NULL; mp = mp->next) {
    putref(fp, mp->to);
    putref(fp, mp->from);
    putref(fp, mp->from_ctime);
    putstring(fp, show_time(mp->time, 0));
    if (mp->subject)
      putstring(fp, uncompress(mp->subject));
    else
      putstring(fp, "");
    putstring(fp, get_message(mp));
    putref(fp, mp->read);
    count++;
  }

  OUTPUT(fputs(EOD, fp));
  fflush(fp);

  if (count != mdb_top) {
    do_log(LT_ERR, 0, 0, T("MAIL: Count of messages is %d, mdb_top is %d."),
	   count, mdb_top);
    mdb_top = count;		/* Doesn't help if we forked, but oh well */
  }
  return (count);
}



/** Find the first message in a player's mail chain, or NULL if none.
 * We first try to find a good place to start by looking for a connected
 * player with a dbref nearest to our target player. Then it's a linear
 * search.
 * \param player the player to search for.
 * \return pointer to first message in their mail chain, or NULL.
 */
MAIL *
find_exact_starting_point(dbref player)
{
  static MAIL *mp;

  if (!HEAD)
    return NULL;
  mp = desc_mail(player);
  if (!mp) {
    /* Player is connected and has no mail, or nobody's connected who
     * has mail - we have to scan the maildb.
     */
    if (HEAD->to > player)
      return NULL;		/* No mail chain */
    for (mp = HEAD; mp && (mp->to < player); mp = mp->next) ;
  } else {
    while (mp && (mp->to >= player))
      mp = mp->prev;
    if (!mp)
      mp = HEAD;
    while (mp && (mp->to < player))
      mp = mp->next;
  }
  if (mp && (mp->to == player))
    return mp;
  return NULL;
}


/* Find the place where new mail to this player should go (after):
 *  1. The last message in the player's mail chain, or
 *  2. The last message before where the player's chain should start, or
 *  3. NULL (meaning TAIL)
 */
static MAIL *
find_insertion_point(dbref player)
{
  static MAIL *mp;

  if (!HEAD)
    return NULL;
  mp = desc_mail(player);
  if (!mp) {
    /* Player is connected and has no mail, or nobody's connected who
     * has mail - we have to scan the maildb.
     */
    if (HEAD->to > player)
      return NULL;		/* No mail chain */
    for (mp = TAIL; mp && (mp->to > player); mp = mp->prev) ;
  } else {
    while (mp && (mp->to <= player))
      mp = mp->next;
    if (!mp)
      mp = TAIL;
    while (mp && (mp->to > player))
      mp = mp->prev;
  }
  return mp;
}


/** Initialize the mail database pointers */
void
mail_init(void)
{
  mdb_top = 0;
  HEAD = NULL;
  TAIL = NULL;
}

/** Load mail from disk.
 * \param fp pointer to filehandle from which to load mail.
 */
int
load_mail(FILE * fp)
{
  char nbuf1[8];
  unsigned char *tbuf = NULL;
  unsigned char *text;
  u_int_16 len;
  int mail_top = 0;
  int mail_flags = 0;
  int i = 0;
  MAIL *mp, *tmpmp;
  int done = 0;
  char sbuf[BUFFER_LEN];
  struct tm ttm;

  /* find out how many messages we should be loading */
  fgets(nbuf1, sizeof(nbuf1), fp);
  /* If it starts with +, it's telling us the mail db flags */
  if (*nbuf1 == '+') {
    mail_flags = atoi(nbuf1 + 1);
    /* If the flags indicates aliases, we'll read them now */
    if (mail_flags & MDBF_ALIASES) {
      load_malias(fp);
    }
    fgets(nbuf1, sizeof(nbuf1), fp);
  }
  mail_top = atoi(nbuf1);
  if (!mail_top) {
    /* mail_top could be 0 from an error or actually be 0. */
    if (nbuf1[0] == '0' && nbuf1[1] == '\n') {
      char buff[20];
      fgets(buff, sizeof buff, fp);
      if (!buff)
	do_rawlog(LT_ERR,
		  T("MAIL: Missing end-of-dump marker in mail database."));
      else if (strcmp(buff, (mail_flags & MDBF_NEW_EOD)
		      ? "***END OF DUMP***\n" : "*** END OF DUMP ***\n") == 0)
	return 1;
      else
	do_rawlog(LT_ERR, T("MAIL: Trailing garbage in the mail database."));
    }
    return 0;
  }
  /* first one is a special case */
  mp = (MAIL *) mush_malloc(sizeof(struct mail), "mail");
  mp->to = getref(fp);
  mp->from = getref(fp);
  if (mail_flags & MDBF_SENDERCTIME)
    mp->from_ctime = getref(fp);
  else
    mp->from_ctime = 0;		/* No one will have this creation time */

  if (do_convtime(getstring_noalloc(fp), &ttm))
    mp->time = mktime(&ttm);
  else				/* do_convtime failed. Odd. */
    mp->time = mudtime;

  if (mail_flags & MDBF_SUBJECT) {
    tbuf = compress(getstring_noalloc(fp));
  }
  text = compress(getstring_noalloc(fp));
  len = strlen(text) + 1;
  mp->msgid = chunk_create(text, len, 1);
  free(text);
  if (mail_flags & MDBF_SUBJECT)
    mp->subject = tbuf;
  else {
    strcpy(sbuf, get_message(mp));
    mp->subject = compress(chopstr(sbuf, SUBJECT_LEN));
  }
  mp->read = getref(fp);
  mp->next = NULL;
  mp->prev = NULL;
  HEAD = mp;
  TAIL = mp;
  i++;

  /* now loop through */
  for (; i < mail_top; i++) {
    mp = (MAIL *) mush_malloc(sizeof(struct mail), "mail");
    mp->to = getref(fp);
    mp->from = getref(fp);
    if (mail_flags & MDBF_SENDERCTIME)
      mp->from_ctime = getref(fp);
    else
      mp->from_ctime = 0;	/* No one will have this creation time */
    if (do_convtime(getstring_noalloc(fp), &ttm))
      mp->time = mktime(&ttm);
    else			/* do_convtime failed. Odd. */
      mp->time = mudtime;
    if (mail_flags & MDBF_SUBJECT)
      tbuf = compress(getstring_noalloc(fp));
    else
      tbuf = NULL;
    text = compress(getstring_noalloc(fp));
    len = strlen(text) + 1;
    mp->msgid = chunk_create(text, len, 1);
    free(text);
    if (tbuf)
      mp->subject = tbuf;
    else {
      strcpy(sbuf, get_message(mp));
      mp->subject = compress(chopstr(sbuf, SUBJECT_LEN));
    }
    mp->read = getref(fp);

    /* We now to a sorted insertion, sorted by recipient db# */
    if (mp->to >= TAIL->to) {
      /* Pop it onto the end */
      mp->next = NULL;
      mp->prev = TAIL;
      TAIL->next = mp;
      TAIL = mp;
    } else {
      /* Search for where to put it */
      mp->prev = NULL;
      for (done = 0, tmpmp = HEAD; tmpmp && !done; tmpmp = tmpmp->next) {
	if (tmpmp->to > mp->to) {
	  /* Insert before tmpmp */
	  mp->next = tmpmp;
	  mp->prev = tmpmp->prev;
	  if (tmpmp->prev) {
	    /* tmpmp isn't HEAD */
	    tmpmp->prev->next = mp;
	  } else {
	    /* tmpmp is HEAD */
	    HEAD = mp;
	  }
	  tmpmp->prev = mp;
	  done = 1;
	}
      }
      if (!done) {
	/* This is bad */
	do_rawlog(LT_ERR, T("MAIL: bad code."));
      }
    }
  }

  mdb_top = i;

  if (i != mail_top) {
    do_rawlog(LT_ERR, T("MAIL: mail_top is %d, only read in %d messages."),
	      mail_top, i);
  }
  {
    char buff[20];
    fgets(buff, sizeof buff, fp);
    if (!buff)
      do_rawlog(LT_ERR,
		T("MAIL: Missing end-of-dump marker in mail database."));
    else if (strcmp(buff, (mail_flags & MDBF_NEW_EOD)
		    ? EOD : "*** END OF DUMP ***\n") != 0)
      /* There's still stuff. Icky. */
      do_rawlog(LT_ERR, T("MAIL: Trailing garbage in the mail database."));
  }

  do_mail_debug(GOD, "fix", "");
  return (mdb_top);
}

static int
get_folder_number(dbref player, char *name)
{
  ATTR *a;
  char str[BUFFER_LEN], pat[BUFFER_LEN], *res, *p;
  /* Look up a folder name and return the appopriate number */
  a = (ATTR *) atr_get_noparent(player, "MAILFOLDERS");
  if (!a)
    return -1;
  strcpy(str, atr_value(a));
  sprintf(pat, ":%s:", strupper(name));
  res = strstr(str, pat);
  if (!res)
    return -1;
  res += 2 + strlen(name);
  p = res;
  while (isdigit((unsigned char) *p))
    p++;
  *p = '\0';
  return atoi(res);
}

static char *
get_folder_name(dbref player, int fld)
{
  static char str[BUFFER_LEN];
  char pat[BUFFER_LEN];
  static char *old;
  char *r;
  ATTR *a;

  /* Get the name of the folder, or "nameless" */
  sprintf(pat, "%d:", fld);
  old = NULL;
  a = (ATTR *) atr_get_noparent(player, "MAILFOLDERS");
  if (!a) {
    strcpy(str, "unnamed");
    return str;
  }
  strcpy(str, atr_value(a));
  old = (char *) string_match(str, pat);
  if (old) {
    r = old + strlen(pat);
    while (*r != ':')
      r++;
    *r = '\0';
    return old + strlen(pat);
  } else {
    strcpy(str, "unnamed");
    return str;
  }
}

/** Assign a name to a folder.
 * \param player dbref of player whose folder is to be named.
 * \param fld folder number.
 * \param name new name for folder.
 */
void
add_folder_name(dbref player, int fld, const char *name)
{
  char *old, *res, *r;
  char *new, *pat, *str, *tbuf;
  ATTR *a;

  /* Muck with the player's MAILFOLDERS attrib to add a string of the form:
   * number:name:number to it, replacing any such string with a matching
   * number.
   */
  new = (char *) mush_malloc(BUFFER_LEN, "string");
  pat = (char *) mush_malloc(BUFFER_LEN, "string");
  str = (char *) mush_malloc(BUFFER_LEN, "string");
  tbuf = (char *) mush_malloc(BUFFER_LEN, "string");
  if (!new || !pat || !str || !tbuf)
    mush_panic(T("Failed to allocate strings in add_folder_name"));

  if (name && *name)
    sprintf(new, "%d:%s:%d ", fld, strupper(name), fld);
  else
    strcpy(new, " ");
  sprintf(pat, "%d:", fld);
  /* get the attrib and the old string, if any */
  old = NULL;
  a = (ATTR *) atr_get_noparent(player, "MAILFOLDERS");
  if (a) {
    strcpy(str, atr_value(a));
    old = (char *) string_match(str, pat);
  }
  if (old && *old) {
    strcpy(tbuf, str);
    r = old;
    while (!isspace((unsigned char) *r))
      r++;
    *r = '\0';
    res = replace_string(old, new, tbuf);	/* mallocs mem! */
  } else {
    r = res = (char *) mush_malloc(BUFFER_LEN + 1, "replace_string.buff");
    if (a)
      safe_str(str, res, &r);
    safe_str(new, res, &r);
    *r = '\0';
  }
  /* put the attrib back */
  (void) atr_add(player, "MAILFOLDERS", res, GOD,
		 AF_WIZARD | AF_NOPROG | AF_LOCKED);
  mush_free((Malloc_t) res, "replace_string.buff");
  mush_free((Malloc_t) new, "string");
  mush_free((Malloc_t) pat, "string");
  mush_free((Malloc_t) str, "string");
  mush_free((Malloc_t) tbuf, "string");
}

static int
player_folder(dbref player)
{
  /* Return the player's current folder number. If they don't have one, set
   * it to 0 */
  ATTR *a;

  a = (ATTR *) atr_get_noparent(player, "MAILCURF");
  if (!a) {
    set_player_folder(player, 0);
    return 0;
  }
  return atoi(atr_value(a));
}

/** Set a player's current mail folder to a given folder.
 * \param player the player whose current folder is to be set.
 * \param fnum folder number to make current.
 */
void
set_player_folder(dbref player, int fnum)
{
  /* Set a player's folder to fnum */
  ATTR *a;
  char tbuf1[BUFFER_LEN];

  sprintf(tbuf1, "%d", fnum);
  a = (ATTR *) atr_match("MAILCURF");
  if (a)
    (void) atr_add(player, a->name, tbuf1, GOD, a->flags);
  else				/* Shouldn't happen, but... */
    (void) atr_add(player, "MAILCURF", tbuf1, GOD,
		   AF_WIZARD | AF_NOPROG | AF_LOCKED);
}

static int
parse_folder(dbref player, char *folder_string)
{
  int fnum;

  /* Given a string, return a folder #, or -1 The string is just a number,
   * for now. Later, this will be where named folders are handled */
  if (!folder_string || !*folder_string)
    return -1;
  if (isdigit((unsigned char) *folder_string)) {
    fnum = atoi(folder_string);
    if ((fnum < 0) || (fnum > MAX_FOLDERS))
      return -1;
    else
      return fnum;
  }
  /* Handle named folders here */
  return get_folder_number(player, folder_string);
}

static int
mail_match(dbref player, MAIL *mp, struct mail_selector ms, int num)
{
  int diffdays;
  mail_flag mpflag;

  /* Does a piece of mail match the mail_selector? */
  if (ms.low && num < ms.low)
    return 0;
  if (ms.high && num > ms.high)
    return 0;
  if (ms.player && !was_sender(ms.player, mp))
    return 0;
  mpflag = Read(mp) ? mp->read : (mp->read | M_MSUNREAD);
  if (!All(ms) && !(ms.flags & mpflag))
    return 0;
  if (AllInFolder(ms) && (Folder(mp) == player_folder(player)))
    return 1;
  if (ms.days != -1) {
    /* Get the time now, subtract mp->time, and compare the results with
     * ms.days (in manner of ms.day_comp) */
    diffdays = (int) (difftime(mudtime, mp->time) / 86400);
    if (sign(diffdays - ms.days) != ms.day_comp)
      return 0;
    else
      return 1;
  }
  return 1;
}

static int
parse_msglist(const char *msglist, struct mail_selector *ms, dbref player)
{
  char *p;
  char tbuf1[BUFFER_LEN];
  int folder;

  /* Take a message list, and return the appropriate mail_selector setup. For
   * now, msglists are quite restricted. That'll change once all this is
   * working. Returns 0 if couldn't parse, and also notifys player of why. */
  /* Initialize the mail selector - this matches all messages */
  ms->low = 0;
  ms->high = 0;
  ms->flags = 0x00FF | M_MSUNREAD;
  ms->player = 0;
  ms->days = -1;
  ms->day_comp = 0;
  /* Now, parse the message list */
  if (!msglist || !*msglist) {
    /* All messages in current folder */
    ms->flags |= M_FOLDER;
    return 1;
  }
  /* Don't mess with msglist itself */
  strncpy(tbuf1, msglist, BUFFER_LEN - 1);
  p = tbuf1;
  while (p && *p && isspace((unsigned char) *p))
    p++;
  if (!p || !*p) {
    ms->flags |= M_FOLDER;
    return 1;			/* all messages in current folder */
  }
  if (isdigit((unsigned char) *p) || *p == '-') {
    if (!parse_message_spec(player, p, &ms->low, &ms->high, &folder)) {
      notify(player, T("MAIL: Invalid message specification"));
      return 0;
    }
    ms->flags |= FolderBit(folder);
  } else if (*p == '~') {
    /* exact # of days old */
    p++;
    if (!p || !*p) {
      notify(player, T("MAIL: Invalid age"));
      return 0;
    }
    if (!is_integer(p)) {
      notify(player, T("MAIL: Message ages must be integers"));
      return 0;
    }
    ms->day_comp = 0;
    ms->days = atoi(p);
    if (ms->days < 0) {
      notify(player, T("MAIL: Invalid age"));
      return 0;
    }
  } else if (*p == '<') {
    /* less than # of days old */
    p++;
    if (!p || !*p) {
      notify(player, T("MAIL: Invalid age"));
      return 0;
    }
    if (!is_integer(p)) {
      notify(player, T("MAIL: Message ages must be integers"));
      return 0;
    }
    ms->day_comp = -1;
    ms->days = atoi(p);
    if (ms->days < 0) {
      notify(player, T("MAIL: Invalid age"));
      return 0;
    }
  } else if (*p == '>') {
    /* greater than # of days old */
    p++;
    if (!p || !*p) {
      notify(player, T("MAIL: Invalid age"));
      return 0;
    }
    if (!is_integer(p)) {
      notify(player, T("MAIL: Message ages must be integers"));
      return 0;
    }
    ms->day_comp = 1;
    ms->days = atoi(p);
    if (ms->days < 0) {
      notify(player, T("MAIL: Invalid age"));
      return 0;
    }
  } else if (*p == '#') {
    /* From db# */
    if (!is_objid(p)) {
      notify(player, T("MAIL: Invalid dbref #"));
      return 0;
    }
    ms->player = parse_objid(p);
    if (!GoodObject(ms->player) || !(ms->player)) {
      notify(player, T("MAIL: Invalid dbref #"));
      return 0;
    }
  } else if (*p == '*') {
    /* From player name */
    p++;
    if (!p || !*p) {
      notify(player, T("MAIL: Invalid player"));
      return 0;
    }
    ms->player = lookup_player(p);
    if (ms->player == NOTHING) {
      notify(player, T("MAIL: Invalid player"));
      return 0;
    }
  } else if (!strcasecmp(p, "all")) {
    ms->flags = M_ALL;
  } else if (!strcasecmp(p, "folder")) {
    ms->flags = M_FOLDER;
  } else if (!strcasecmp(p, "urgent")) {
    ms->flags = M_URGENT | M_FOLDER;
  } else if (!strcasecmp(p, "unread")) {
    ms->flags = M_MSUNREAD | M_FOLDER;
  } else if (!strcasecmp(p, "read")) {
    ms->flags = M_MSGREAD | M_FOLDER;
  } else if (!strcasecmp(p, "clear") || !strcasecmp(p, "cleared")) {
    ms->flags = M_CLEARED | M_FOLDER;
  } else if (!strcasecmp(p, "tag") || !strcasecmp(p, "tagged")) {
    ms->flags = M_TAG;
  } else if (!strcasecmp(p, "mass")) {
    ms->flags = M_MASS;
  } else if (!strcasecmp(p, "me")) {
    ms->player = player;
  } else {
    notify(player, T("MAIL: Invalid message specification"));
    return 0;
  }
  return 1;
}

static char *
status_chars(MAIL *mp)
{
  /* Return a short description of message flags */
  static char res[10];
  char *p;

  res[0] = '\0';
  p = res;
  *p++ = Read(mp) ? '-' : 'N';
  *p++ = Cleared(mp) ? 'C' : '-';
  *p++ = Urgent(mp) ? 'U' : '-';
  /* *p++ = Mass(mp) ? 'M' : '-'; */
  *p++ = Forward(mp) ? 'F' : '-';
  *p++ = Tagged(mp) ? '+' : '-';
  *p = '\0';
  return res;
}

static char *
status_string(MAIL *mp)
{
  /* Return a longer description of message flags */
  static char tbuf1[BUFFER_LEN];

  tbuf1[0] = '\0';
  if (Read(mp))
    strcat(tbuf1, T("Read "));
  else
    strcat(tbuf1, T("Unread "));
  if (Cleared(mp))
    strcat(tbuf1, T("Cleared "));
  if (Urgent(mp))
    strcat(tbuf1, T("Urgent "));
  if (Mass(mp))
    strcat(tbuf1, T("Mass "));
  if (Forward(mp))
    strcat(tbuf1, T("Fwd "));
  if (Tagged(mp))
    strcat(tbuf1, T("Tagged"));
  return tbuf1;
}

/** Check for new mail.
 * \param player player to check for new mail (and to notify).
 * \param folder folder number to check.
 * \param silent if 1, don't tell player if they have no mail.
 */
void
check_mail(dbref player, int folder, int silent)
{
  int rc;			/* read messages */
  int uc;			/* unread messages */
  int cc;			/* cleared messages */
  int total;

  /* just count messages */
  count_mail(player, folder, &rc, &uc, &cc);
  total = rc + uc + cc;
  if (total > 0)
    notify_format(player,
		  T
		  ("MAIL: %d messages in folder %d [%s] (%d unread, %d cleared)."),
		  total, folder, get_folder_name(player, folder), uc, cc);
  else if (!silent)
    notify(player, T("\nMAIL: You have no mail.\n"));
  if ((folder == 0) && (total + 5 > MAIL_LIMIT))
    notify_format(player, T("MAIL: Warning! Limit on inbox messages is %d!"),
		  MAIL_LIMIT);
  return;
}

static int
sign(int x)
{
  if (x == 0) {
    return 0;
  } else if (x < 0) {
    return -1;
  } else {
    return 1;
  }
}

/* See if we've been given something of the form [f:]m1[-m2]
 * If so, return 1 and set f and mlow and mhigh
 * If not, return 0
 * If msghigh is given as NULL, don't allow ranges 
 * Used in parse_msglist, fun_mail and relatives.
 */
static int
parse_message_spec(dbref player, const char *s, int *msglow, int *msghigh,
		   int *folder)
{
  char buf[BUFFER_LEN];
  char *p, *q;
  if (!s || !*s)
    return 0;
  strcpy(buf, s);
  if ((p = strchr(buf, ':'))) {
    *p++ = '\0';
    if (!is_integer(buf))
      return 0;
    *folder = parse_integer(buf);
    if (msghigh && (q = strchr(p, '-'))) {
      /* f:low-high */
      *q++ = '\0';
      if (!*p)
	*msglow = 0;
      else if (!is_integer(p))
	return 0;
      else {
	*msglow = parse_integer(p);
	if (*msglow == 0)
	  *msglow = -1;
      }
      if (!*q)
	*msghigh = 0;
      else if (!is_integer(q))
	return 0;
      else {
	*msghigh = parse_integer(q);
	if (*msghigh == 0)
	  *msghigh = -1;
      }
    } else {
      /* f:m */
      if (!is_integer(p))
	return 0;
      *msglow = parse_integer(p);
      if (*msglow == 0)
	*msglow = -1;
      if (msghigh)
	*msghigh = *msglow;
    }
    if (*msglow < 0 || (msghigh && *msghigh < 0) || *folder < 0
	|| *folder > MAX_FOLDERS)
      return 0;
  } else {
    /* No folder spec */
    *folder = player_folder(player);
    if (msghigh && (q = strchr(buf, '-'))) {
      /* low-high */
      *q++ = '\0';
      if (!*buf)
	*msglow = 0;
      else if (!is_integer(buf))
	return 0;
      else {
	*msglow = parse_integer(buf);
	if (*msglow == 0)
	  *msglow = -1;
      }
      if (!*q)
	*msghigh = 0;
      else if (!is_integer(q))
	return 0;
      else {
	*msghigh = parse_integer(q);
	if (*msghigh == 0)
	  *msghigh = -1;
      }
    } else {
      /* m */
      if (!is_integer(buf))
	return 0;
      *msglow = parse_integer(buf);
      if (*msglow == 0)
	*msglow = -1;
      if (msghigh)
	*msghigh = *msglow;
    }
    if (*msglow < 0 || (msghigh && *msghigh < 0))
      return 0;
  }
  return 1;
}

static int
send_mail_alias(dbref player, char *aname, char *subject, char *message,
		mail_flag flags, int silent, int nosig)
{
  struct mail_alias *m;
  int i;

  /* send a mail message to each player on an alias */
  /* We return 0 if this wasn't an alias */
  m = get_malias(player, aname);
  if (!m)
    return 0;
  /* Is it an alias they can use? */
  if (!((m->owner == player) || (m->nflags == 0) ||
	(Hasprivs(player)) ||
	((m->nflags & ALIAS_MEMBERS) && ismember(m, player))))
    return 0;

  /* If they are not allowed to see the people on the alias, then
   * we must treat this as a case of silent mailing.
   */
  if (!((m->owner == player) || (m->mflags == 0) ||
	(Hasprivs(player)) ||
	((m->mflags & ALIAS_MEMBERS) && ismember(m, player)))) {
    silent = 1;
    notify_format(player,
		  T("You sent your message to the '%s' alias"), m->name);
  }

  for (i = 0; i < m->size; i++) {
    send_mail(player, m->members[i], subject, message, flags, silent, nosig);
  }
  return 1;			/* Success */
}