/*$Id: ezmlm-cgi.c,v 1.17 1999/12/24 04:21:26 lindberg Exp $*/
/*$Name: ezmlm-idx-040 $*/

/* Please leave. Will hopefully help pay for further improvement. */
#define EZ_CRIGHT "<a href=\"http://www.lindeinc.com\">(c) 1999 Lin-De, Inc</a>"
/******/
#include <sys/types.h>
#include "direntry.h"
#include "datetime.h"
#include "now.h"
#include "stralloc.h"
#include "strerr.h"
#include "error.h"
#include "env.h"
#include "sig.h"
#include "open.h"
#include "getln.h"
#include "case.h"
#include "scan.h"
#include "str.h"
#include "fmt.h"
#include "readwrite.h"
#include "fork.h"
#include "wait.h"
#include "exit.h"
#include "substdio.h"
#include "getconf.h"
#include "gen_alloc.h"
#include "gen_allocdefs.h"
#include "constmap.h"
#include "byte.h"
#include "subscribe.h"
#include "errtxt.h"
#include "makehash.h"
#include "mime.h"
#include "idx.h"
#include "yyyymm.h"

#define FATAL "ezmlm-cgi: fatal: "
#define GET "-getv"
#define THREAD "-threadv"
#define SUBSCRIBE "-subscribe"
#define FAQ "-faq"
#define TXT_CGI_SUBSCRIBE "\">[eSubscribe]</a>\n"
#define TXT_CGI_FAQ "\">[eFAQ]</a>\n"

int flagshowhtml = 1;	/* show text/html parts. This leads to duplication */
			/* when both text/plain and text/html are in a     */
			/* multipart/alternative message, but it is assumed*/
			/* that text/html is not frivolous, but only used  */
			/* when the formatting is important. */
int flagobscure = 0;	/* Don't remove Sender's E-mail address in message */
			/* view. Overridden by config file (- before list */
			/* name). */

/**************** Header processing ***********************/
char headers_used[] = "Subject\\From\\Date\\content-type\\"
		"content-transfer-encoding\\mime-version";
/* index of headers displayed (shown in order listed above) */
int headers_shown[] = {1,1,1,0,0,0};
/* index of specific headers */
#define NO_HDRS 6
#define HDR_SUBJECT 1
#define HDR_FROM 2
#define HDR_CT 4
#define HDR_CTENC 5
#define HDR_VERSION 6

/* Need to add inits if you increase NO_HDRS */
stralloc hdr[NO_HDRS] = { {0},{0},{0},{0},{0},{0} };
/**************** Header processing ***********************/


/* index of subject in above, first = 1 */

/* TODO: Sort headers before display. Find a way to display the body with the*/
/* correct charset, ideally letting the browser do the work (should really */
/* be able to specify charset for DIV ! */

/* ulong at least 32 bits. (Creating a Year 0xffffff problem ;-) */
#define MAXULONG 0xffffffff

char cmdstr[5] = "xxx:";
#define ITEM "-msadiz"
#define ITEM_MESSAGE 1
#define ITEM_SUBJECT 2
#define ITEM_AUTHOR 3
#define ITEM_DATE 4
#define ITEM_INDEX 5

#define DIRECT "psnpn"
#define DIRECT_SAME 0
#define DIRECT_NEXT 1
#define DIRECT_PREV -1
/* use only as the argument for some functions. Terrible hack for date links */
#define DIRECT_FIRST 3
#define DIRECT_LAST 2

char *dir = 0;
char *local = 0;
char *host = 0;
char *home = 0;
char *banner = 0;
char *charset = 0;
char *stylesheet = 0;
char *cmd;
char strnum[FMT_ULONG];
/* these are the only headers we really care about for message display */
/* one can always retrieve the complete message by E-mail */
stralloc charg = {0};
stralloc url = {0};
stralloc author = {0};
stralloc subject = {0};
stralloc base = {0};
stralloc line = {0};
stralloc decline = {0};		/* for rfc2047-decoded headers and QP/base64 */
stralloc cfline = {0};		/* from config file */
stralloc fn = {0};
stralloc dtline = {0};
stralloc headers = {0};
stralloc encoding = {0};
stralloc content = {0};
stralloc charsetbase = {0};
stralloc curcharset = {0};
stralloc sainit = {0};
struct constmap headermap;
unsigned long uid,euid;
int recursion_level;
int so = 0;
int ss23 = 0;
int state = 0;
int newlevel;
int match;	/* used everywhere and no overlap */
int fd;		/* same; never >1 open */
int cache;	/* 0 = don't; 1 = don't know; 2 = do */
int child,wstat;
int flagtoplevel;
unsigned int flagmime;
unsigned int cs,csbase;
int flagrobot;
int flagpre;
int precharcount;
char cn1 = 0;
char cn2 = 0;
char lastjp[] = "B";	/* to get back to the correct JP after line break */
char *bannerargs[4];


mime_info *mime_current = 0;
mime_info *mime_tmp = 0;

datetime_sec when;
struct datetime dt;

char inbuf[4096];
substdio ssin;

void die_nomem() { strerr_die2x(111,FATAL,ERR_NOMEM); }

void die_syntax(char *s)
{
  strerr_die4x(100,FATAL,ERR_SYNTAX,"config file: ",s);
}

char outbuf[4096];
substdio ssout = SUBSTDIO_FDBUF(write,1,outbuf,sizeof(outbuf));

void oput(register char *s, register unsigned int l)
/* unbuffered. Avoid extra copy as httpd buffers */
{
  if (substdio_put(&ssout,s,l) == -1)
    strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
}

void oputs(register char *s)
{
  oput(s,str_len(s));
}

/* this error is for things that happen only if program logic is screwed up */
void die_prog(char *s) { strerr_die5x(100,FATAL,"program error (please send bug report to bugs@ezmlm.org): ",s," Command: ",cmd); }

/* If we already issued a header than this will look ugly */
void cgierr(char *s,char *s1,char *s2)
{
  strerr_warn4(FATAL,s,s1,s2,(struct strerr *)0);
  oputs("Content-type: text/plain\n");
  oputs("Status: 500 Couldn't do it\n\n");
  oputs("I tried my best, but:\n\n");
  if (s) oputs(s);
  if (s1) oputs(s1);
  if (s2) oputs(s2);
  oputs("\n");
  substdio_flush(&ssout);
  _exit(0);
}

unsigned long msgnav[5]; /* 0 prev prev 1 prev 2 this 3 next 4 next-next */

struct msginfo {	/* clean info on the target message */
  char item;		/* What we want */
  char direction;	/* Relation to current msg */
  char axis;		/* Axis of desired movement [may be calculated] */
  unsigned long source;	/* reference message number */
  unsigned long target;
  unsigned long date;
  unsigned long *authnav;	/* msgnav structure */
  unsigned long *subjnav;	/* msgnav structure */
  char *author;
  char *subject;
  char *cgiarg;			/* sub/auth as expected from axis */
} msginfo;

void toggle_flagpre(int flag)
{
  flagpre = flag;
  precharcount = 0;
  cn1 = 0; cn2 = 0;		/* just in case */
}

unsigned int decode_charset(register char *s, register unsigned int l)
/* return charset code. CS_BAD means that base charset should be used, i.e. */
/* that charset is empty or likely invalid. CS_NONE are charsets for which  */
/* we don't need to do anything special. */
{
  unsigned int r;

  if (case_startb(s,l,"iso-8859") || case_startb(s,l,"us-ascii") ||
	case_startb(s,l,"utf"))	/* at the moment, we can do utf-8 right */
    return CS_NONE;		/* what is utf-7 (used by OE)? */
  if (case_startb(s,l,"x-cp") ||
	case_startb(s,l,"cp") ||
	case_startb(s,l,"x-mac") ||
	case_startb(s,l,"koi8")) return CS_NONE;
  if (!l || *s == 'x' || *s == 'X') return CS_BAD;
  if (case_startb(s,l,"iso-2022")) {
    if (case_startb(s+8,l-8,"-cn"))
      return CS_2022_CN;
    if (case_startb(s+8,l-8,"-jp"))
      return CS_2022_JP;
    return CS_2022_KR;
  }
  if (case_startb(s,l,"cn-") ||
		case_startb(s,l,"hz-gb") ||
		case_startb(s,l,"gb") ||
		case_startb(s,l,"big5"))
    return CS_CN;		/* Only consideration for linebreak */
  if (case_startb(s,l,"iso_8859") ||
	case_startb(s,l,"latin") ||
	case_startb(s,l,"windows")) return CS_NONE;
/* Add other charsets here. Later we will add code to replace a detected */
/* charset name with another, and to connect conversion routines, such as */
/* between windows-1251/koi-8r/iso-8859-5 */
  return CS_BAD;
}

void htmlencode_put (register char *s,register unsigned int l)
/* At this time, us-ascii, iso-8859-? create no problems. We just encode  */
/* some html chars. iso-2022 may have these chars as character components.*/
/* cs is set for these, 3 for CN, 2 for others. Bit 0 set means 2 byte    */
/* chars for SS2/SS3 shiftouts (JP doesn't use them, KR has single byte.  */
/* If cs is set and we're shifted out (so set) we don't substitute. We    */
/* also look for SI/SO to adjust so, and ESC to detect SS2/SS3. Need to   */
/* ignore other ESC seqs correctly. JP doesn't use SI/SO, but uses        */
/* ESC ( B/J and ESC $ B/@ analogously, so we use these to toggle so.     */
/* "Roman", i.e. ESC ( J is treated as ascii - no differences in html-    */
/* relevant chars. Together, this allows us to deal with all iso-2022-*   */
/* as a package. see rfc1468, 1554, 1557, 1922 for more info.             */
/* line break at 84 to avoid splits with lines just a little too long. */
{
  if (!cs) {		/* us-ascii & iso-8859- & unrecognized */
    for (;l--;s++) {
      precharcount++;
      switch (*s) {
        case '>': oputs("&gt;"); break;
        case '<': oputs("&lt;"); break;
        case '"': oputs("&quot;"); break;
        case '&': oputs("&amp;"); break;
	case '\n': precharcount = 0; oput(s,1); break;
	case ' ':
	  if (precharcount >= 84 && flagpre) {
	    oput("\n",1);			/* in place of ' ' */
	    precharcount = 0;
	  } else
	    oput(s,1);				/* otherwise out with it. */
	  break;
        default: oput(s,1); break;
      }
    }
  } else if (cs == CS_CN) {			/* cn-, gb*, big5 */
    for (;l--;s++) {
      precharcount++;
      if (cn1) { cn2 = cn1; cn1 = 0; }		/* this is byte 2 */
      else { cn2 = 0; cn1 = *s & 0x80; }	/* this is byte 1/2 or ascii */
      if (!cn1 && !cn2) {			/* ascii */
	switch (*s) {
          case '>': oputs("&gt;"); break;
          case '<': oputs("&lt;"); break;
          case '"': oputs("&quot;"); break;
          case '&': oputs("&amp;"); break;
	  case '\n': precharcount = 0; oput(s,1); break;
          case ' ':
		if (precharcount >= 84 && flagpre) {
		  oput("\n",1);		/* break in ascii sequence */
		  precharcount = 0;
		} else
		  oput(s,1);
		break;
	  default: oput(s,1); break;
	}
      } else if (precharcount >= 84 && flagpre && cn2) {
	  oput("\n",1);			/* break after 2-byte code */
	  precharcount = 0;
      }
    }
  } else {					/* iso-2022 => PAIN! */
    for (;l--;s++) {
      precharcount++;
      if (ss23) {				/* ss2/ss3 character */
	ss23--;
	oput(s,1);
        continue;
      }
      if (so) {					/* = 0 ascii, = 1 SO charset */
        if (!(*s & 0xe0)) {			/* ctrl-char */
	  switch (*s) {
	    case ESC: state = 1; break;
	    case SI: so = 0; break;
	    case '\n': precharcount = 0; break;
	    default: break;
	  }
	}
	oput(s,1);
      } else {					/* check only ascii */
	switch (*s) {
	  case '>': oputs("&gt;"); break;
	  case '<': oputs("&lt;"); break;
	  case '"': oputs("&quot;"); break;
	  case '&': oputs("&amp;"); break;
          case ' ':
		if (precharcount >= 84 && flagpre) {
		  oput("\n",1);		/* break in ascii sequence */
		  precharcount = 0;
		} else
		  oput(s,1);
		break;
	  default:
		  oput(s,1);
		  if (!(*s & 0xe0)) {
		    switch (*s) {
		      case SO: so = 1; break;
		      case ESC: state = 1; break;
		      case SI: so = 0; break;	/* shouldn't happen */
		      case '\n': precharcount = 0; break;
		      default: break;
		    }
		  }
	}
      }		/* by now all output is done, now ESC interpretation */
      if (state) {
		/* ESC code - don't count */
	  if (precharcount) precharcount--;
	  state++;
	  switch (state) {
	    case 2: break;			/* this was the ESC */
	    case 3: switch (*s) {
			case 'N': ss23 = (cs & 1) + 1; state = 0; break;
			case 'O': ss23 = 2; state = 0; break;
			case '(': state = 20; so = 0; break;	/* JP ascii */
			case '$': break;		/* var S2/SS2/SS3 des*/
			case '.': state = 10;	/* g3 settings, one more char */
			default: state = 0; break;	/* or JP */
		}
		break;
	    case 4: switch (*s) {	/* s2/ss2/ss3 or JP 2 byte shift */
		   case 'B':
		   case '@': lastjp[0] = *s;
			     so = 1; state = 0; break;	/* JP */
		   default: break;			/* other SS2/3 des */
		 }
		 break;
	    case 5:  state = 0; break;		/* 4th char of ESC $ *|+|) X */
	    case 11: state = 0; break;		/* 3nd char of ESC . */
	    case 21: state = 0; break;		/* ESC ( X for JP */
	    default: die_prog("bad state in htmlencode_put"); break;
	  }
      } else if (so && flagpre && precharcount >= 84) {
		/* 84 is nicer than 78/80 since most use GUI browser */
		/* iso-2022-* line splitter here. SO only, SI done above */
		/* For JP need even precharcount, add ESC ( B \n ESC $B */
	if (so && !(precharcount & 1)) {	/* even */
	  precharcount = 0;			/* reset */
	  if (cs == CS_2022_JP) {		/* JP uses ESC like SI/SO */
	    oputs(TOASCII);
	    oput("\n",1);
	    oputs(TOJP);
	    oput(lastjp,1);
	  } else {
	    if (so) {
		/* For iso-2022-CN: nothing if SI, otherwise SI \n SO */
		/* For iso-2022-KR same */
	      oputs(SI_LF_SO);
	    } else
	      oput("\n",1);
	  }
	}
      }
    }
  }
}

char hexchar[] = "0123456789ABCDEF";
char enc_url[] = "%00";

void urlencode_put (register char *s,register unsigned int l)
{
  for (;l--;s++) {
    register unsigned char ch;
    ch = (unsigned char) *s;
    if (ch <= 32 || ch > 127 || byte_chr("?<>=/:%+#\"",10,ch) != 10) {
      enc_url[2] = hexchar[ch & 0xf];
      enc_url[1] = hexchar[(ch >> 4) & 0xf];
      oput(enc_url,3);
    } else
      oput(s,1);
  }
}

void urlencode_puts(register char *s)
{
  urlencode_put(s,str_len(s));
}

int checkhash(register char *s)
{
  register int l = HASHLEN;
  while (l--) {
    if (*s < 'a' || *s > 'p') return 0;	/* illegal */
    s++;
  }
  if (*s) return 0;			/* extraneous junk */
  return 1;
}

int makefn(stralloc *sa,char item, unsigned long n, char *hash)
{
  if (!stralloc_copys(sa,"archive/")) die_nomem();
  if (item == ITEM_MESSAGE) {
    if (!stralloc_catb(sa,strnum,fmt_ulong(strnum, n / 100))) die_nomem();
    if (!stralloc_cats(sa,"/")) die_nomem();
    if (!stralloc_catb(sa,strnum,fmt_uint0(strnum,(unsigned int) (n % 100),2)))
			die_nomem();
  } else if (item == ITEM_DATE) {
    if (!stralloc_cats(sa,"threads/")) die_nomem();
    if (!stralloc_catb(sa,strnum,fmt_ulong(strnum,n)))
	die_nomem();
  } else if (item == ITEM_INDEX) {
    if (!stralloc_catb(sa,strnum,fmt_ulong(strnum, n / 100))) die_nomem();
    if (!stralloc_cats(sa,"/index")) die_nomem();
  } else {
    if (item == ITEM_AUTHOR) {
      if (!stralloc_cats(sa,"authors/")) die_nomem();
    } else {
      if (!stralloc_cats(sa,"subjects/")) die_nomem();
    }
    if (!hash) return 0;
    if (!stralloc_catb(sa,hash,2)) die_nomem();
    if (!stralloc_cats(sa,"/")) die_nomem();
    if (!stralloc_catb(sa,hash+2,HASHLEN-2)) die_nomem();
  }
  if (!stralloc_0(sa)) die_nomem();
  return 1;
}

void link(struct msginfo *infop,char item,char axis,unsigned long msg,
		char *data,unsigned int l)
/* links with targets other msg -> msg. If the link is for author, we    */
/* still supply subject, since most navigation at the message level will */
/* be along threads rather than author and we don't have an author index.*/
{
  char *cp;

  cp = (char *) 0;
	/* this should be separate routine. Works because all index views */
	/* have at least a subject link */
  if (axis == ITEM_SUBJECT && infop->target == msg)
    oputs("<a name=b></a>");
  oput(url.s,url.len);
  cmdstr[0] = ITEM[item];
  cmdstr[1] = ITEM[axis];
  cmdstr[2] = DIRECT[DIRECT_SAME + 1];
  if (item == ITEM_MESSAGE && axis == ITEM_AUTHOR) {
    if (infop->subject) {
      cmdstr[1] = ITEM[ITEM_SUBJECT];
      cp = infop->subject;	/* always HASLEN in length due to decode_cmd */
    }
  }
  oputs(cmdstr);		/* e.g. map: */
  oput(strnum,fmt_ulong(strnum,msg));
  if (!cp && l >= HASHLEN)
    cp = data;
  if (infop->date) {
    oput(":",1);
    oput(strnum,fmt_ulong(strnum,infop->date));
  }
  if (cp) {
    oput(":",1);
    oput(cp,HASHLEN);
  }
  switch (item) {
    case ITEM_MESSAGE: oputs("\" class=mlk>"); break;
    case ITEM_AUTHOR: oputs("#b\" class=alk>"); break;
    case ITEM_SUBJECT: oputs("#b\" class=slk>"); break;
    default: oputs("#b\">"); break;
  }
  if (HASHLEN + 1 < l)
    htmlencode_put(data + HASHLEN + 1,l - HASHLEN - 1);
  else
    oputs("(none)");
  oputs("</A>");
}

void linktoindex(struct msginfo *infop,char item)
/* for links from message view back to author/subject/threads index */
{
  oput(url.s,url.len);
  cmdstr[0] = ITEM[item];
  cmdstr[1] = ITEM[item];
  cmdstr[2] = DIRECT[DIRECT_SAME + 1];
  oputs(cmdstr);		/* e.g. map: */
  oput(strnum,fmt_ulong(strnum,infop->target));
  if (infop->date) {
    oput(":",1);
    oput(strnum,fmt_ulong(strnum,infop->date));
  }
  switch (item) {
    case ITEM_AUTHOR:
      if (infop->author) {
	oput(":",1);
	oputs(infop->author);
      }
      break;
    case ITEM_SUBJECT:
      if (infop->subject) {
	oput(":",1);
	oputs(infop->subject);
      }
      break;
    default:
      break;
  }
  oputs("#b\"");
}

void link_msg(struct msginfo *infop,char axis,char direction)
/* Creates <a href="mapa:123:aaaaa...."> using a maximum of available */
/* info only for links where the target is a message */
{
  unsigned long msg;
  char *acc;
  oput(url.s,url.len);
  cmdstr[0] = ITEM[ITEM_MESSAGE];
  cmdstr[1] = ITEM[axis];
  cmdstr[2] = DIRECT[direction + 1];
  msg = infop->target;
  acc = 0;
      switch(axis) {
	case ITEM_SUBJECT:
	  if (infop->subject)
	    acc = infop->subject;
	  if (infop->subjnav)	/* translate to message navigation */
	    if (infop->subjnav[direction]) {
	      msg = infop->subjnav[direction];
	      cmdstr[2] = DIRECT[DIRECT_SAME + 1];
	  }
	  acc = infop->subject;
	  break;
	case ITEM_AUTHOR:
	  if (infop->author)
	    acc = infop->author;
	  if (infop->authnav)	/* translate to message navigation */
	    if (infop->authnav[direction]) {
	      msg = infop->authnav[direction];
	      cmdstr[2] = DIRECT[DIRECT_SAME + 1];
	    }
	  acc = infop->author;
	  break;
	default:
	  break;
	}
	oputs(cmdstr);
	oput(strnum,fmt_ulong(strnum,msg));
	if (acc) {
	  oputs(":");
	  oputs(acc);
	}
	oputs("\">");
}

void justpress()
{
  oputs("?subject=");
  urlencode_puts("Just Click \"SEND\"!");
}

void homelink()
{
  register char *cp,*cp1,*cp2;

  if (home && *home) {
    cp = home;
    for(;;) {
      cp1 = cp;
      while(*cp1 && *cp1 != '=') cp1++;
      if (!*cp1) break;
      cp2 = cp1;
      while(*cp2 && *cp2 != ',') cp2++;
      oputs("<a href=\"");
      oput(cp1 + 1,cp2 - cp1 - 1);
      oputs("\">");
      oput(cp,cp1 - cp);
      oputs("</a>\n");
      if (!*cp2) break;
      cp = cp2 + 1;
    }
  }
}

void subfaqlinks()
{
  oputs("<a href=\"mailto:");
  oputs(local);
  oputs(SUBSCRIBE);
  oputs("@");
  oputs(host);
  justpress();
  oputs(TXT_CGI_SUBSCRIBE);
  oputs("<a href=\"mailto:");
  oputs(local);
  oputs(FAQ);
  oputs("@");
  oputs(host);
  justpress();
  oputs(TXT_CGI_FAQ);
}

void msglinks(struct msginfo *infop)
/* Creates the html for all links from one message view */
{
  oputs("<DIV class=msglinks><STRONG>Msg by: ");
  link_msg(infop,ITEM_SUBJECT,DIRECT_PREV);
  oputs("[&lt;-</A> ");
  linktoindex(infop,ITEM_SUBJECT);
  oputs(">thread</A> ");
  link_msg(infop,ITEM_SUBJECT,DIRECT_NEXT);
  oputs("-&gt;]</A> \n");
  link_msg(infop,ITEM_MESSAGE,DIRECT_PREV);
  oputs("[&lt;-</A> ");
  linktoindex(infop,ITEM_INDEX);
  oputs(">time</A> ");
  link_msg(infop,ITEM_MESSAGE,DIRECT_NEXT);
  oputs("-&gt;]</A> \n");
  link_msg(infop,ITEM_AUTHOR,DIRECT_PREV);
  oputs("[&lt;-</A> ");
  linktoindex(infop,ITEM_AUTHOR);
  oputs(">author</A> ");
  link_msg(infop,ITEM_AUTHOR,DIRECT_NEXT);
  oputs("-&gt;]</A> |\n");
  linktoindex(infop,ITEM_DATE);
  oputs(">[Threads]</A>\n");
  homelink();
  oputs("\n<a href=\"mailto:");
  oputs(local);
  oputs(GET);
  strnum[fmt_ulong(strnum,infop->target)] = '\0';
  oputs(strnum);
  oputs("@");
  oputs(host);
  justpress();
  oputs("\">[eMsg]</A>\n");
  oputs("<a href=\"mailto:");
  oputs(local);
  oputs(THREAD);
  oputs(strnum);
  oputs("@");
  oputs(host);
  justpress();
  oputs("\">[eThread]</A>\n");
  subfaqlinks();
  oputs("</STRONG></DIV>\n");
}

#define SPC_BASE 1
#define SPC_BANNER 2

void html_header(char *t,char *s, unsigned int l,char *class,int flagspecial)
/* flagspecial: 0x1 => robot index; no style sheet, no BASE */
/* flagspecial: 0x2 => banner, if available */
{
  oputs("Content-Type: text/html; charset=");
  oput(curcharset.s,curcharset.len);

  oputs("\nCache-Control: ");
  switch (cache) {
    case 0:
	oputs("no-cache");		/* known upper border */
	break;
    case 1:
	oputs("max-age=300");		/* 5 min - most lists aren't that fast*/
	break;
    case 2:
	oputs("max-age=1209600");	/* 14 days is a long time */
	break;
  }
  oputs("\n\n");
  oputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\">\n");
  oputs("<HTML><HEAD>\n<TITLE>");
  if (local) {
    oputs(local);
    oputs("@");
    oputs(host);
    oputs(": ");
  }
  if (t) oputs(t);
  if (s) htmlencode_put(s,l);
  oputs("</TITLE>\n");
  if (class && *class && stylesheet && *stylesheet) {
    oputs("<LINK href=\"");
    oputs(stylesheet);
    oputs("\" rel=\"stylesheet\" type=\"text/css\">\n");
  }
  if (!flagrobot)	/* robot access allowed to follow */
    oputs("<META NAME=\"robots\" CONTENT=\"noindex\">\n");
  if (flagrobot < 2)
    oputs("<META NAME=\"robots\" CONTENT=\"nofollow\">\n");
  if (flagspecial & SPC_BASE)
    oput(base.s,base.len);
  oputs("</HEAD>\n");
  if (class && *class) {
    oputs("<BODY class=");
    oputs(class);
    oputs(">\n");
  } else
    oputs("<BODY>\n");

}

void html_footer(int flagspecial)
{
  oputs("<HR><DIV class=copyright>");
  oputs(EZ_CRIGHT);
  oputs("</DIV>");
  if ((flagspecial & SPC_BANNER) && banner && *banner) {
    oputs("<DIV class=banner>\n");
    if (*banner == '<') oputs(banner);
    else {
      substdio_flush(&ssout);
      sig_pipeignore();
      bannerargs[0] = banner;
      bannerargs[1] = host;
      bannerargs[2] = local;
      bannerargs[3] = 0;
	/* We log errors but just complete the page anyway, since we're */
	/* already committed to output something. */
      switch(child = fork()) {
        case -1:
          strerr_warn3(FATAL,ERR_FORK,"banner program: ",&strerr_sys);
          break;
        case 0:
          execv(*bannerargs,bannerargs);
          strerr_die3x(100,FATAL,ERR_EXECUTE,"banner program: ");
	  break;
      }
         /* parent */
      wait_pid(&wstat,child);
      if (wait_crashed(wstat))
        strerr_warn2(FATAL,ERR_CHILD_CRASHED,(struct strerr *) 0);
      if (wait_exitcode(wstat))
        strerr_warn2(FATAL,ERR_CHILD_UNKNOWN,(struct strerr *) 0);
    }
    oputs("</DIV>\n");
  }
  oputs("</BODY>\n</HTML>\n");
  substdio_flush(&ssout);
}

/* DATE functions */

void datelink(struct msginfo *infop,unsigned long d,char direction)
/* output a date with link back to thread index */
{
  oput(url.s,url.len);
  cmdstr[0] = ITEM[ITEM_DATE];
  cmdstr[1] = ITEM[ITEM_DATE];
  cmdstr[2] = DIRECT[direction + 1];
  oputs(cmdstr);
  if (direction == DIRECT_LAST)
    oput("0",1);	/* suppress msgnum to avoid going there */
  else
    oput(strnum,fmt_ulong(strnum,infop->target));
  oputs(":");
  oput(strnum,fmt_ulong(strnum,d));
  oputs("#b\">");
  switch (direction) {
    case DIRECT_SAME:
	if (dateline(&dtline,d) < 0) die_nomem();
	oput(dtline.s,dtline.len);
	break;
    case DIRECT_PREV:
	oputs("[&lt;-]");
	break;
    case DIRECT_NEXT:
	oputs("[-&gt;]");
	break;
    case DIRECT_FIRST:
	oputs("[&lt;&lt;-]");
	break;
    case DIRECT_LAST:
	oputs("[-&gt;&gt;]");
	break;
  }
  oputs("</A>");
}

void finddate(struct msginfo *infop)
/* DIRECT_SAME works as DIRECT_PREV, dvs returns previous date or last date */
{
  DIR *archivedir;
  direntry *d;
  unsigned long ddate, startdate;
  unsigned long below, above;

  below = 0L;
  above = MAXULONG;	/* creating a Y 0xffffff problem */
  startdate = infop->date;
  archivedir = opendir("archive/threads/");
  if (!archivedir)
    if (errno != error_noent)
      strerr_die4sys(111,FATAL,ERR_OPEN,dir,"/archive/threads: ");
    else
      strerr_die4sys(100,FATAL,ERR_OPEN,dir,"/archive/threads: ");

  while ((d = readdir(archivedir))) {		/* dxxx/ */
    if (str_equal(d->d_name,".")) continue;
    if (str_equal(d->d_name,"..")) continue;
    scan_ulong(d->d_name,&ddate);
    if (!ddate) continue;	/* just in case some smart guy ... */
    if (startdate) {
      if (ddate > startdate && ddate < above) above = ddate;
      if (ddate < startdate && ddate > below) below = ddate;
    } else {
      if (ddate < above) above = ddate;
      if (ddate > below) below = ddate;
    }
  }
  closedir(archivedir);

  if (infop->direction == DIRECT_NEXT && above != MAXULONG || !below)
	/* we always give a valid date as long as there is at least one */
    infop->date = above;
  else
    infop->date = below;
  return;
}

void latestdate(struct msginfo *infop,int flagfail)
{
  if (!flagfail) {
    datetime_tai(&dt,now());
    infop->date = ((unsigned long) dt.year + 1900) * 100 + dt.mon + 1;
  } else {
    infop->date = 0;
    infop->direction = DIRECT_PREV;
    finddate(infop);
  }
}

void firstdate(struct msginfo *infop,int flagfail)
{
    infop->date = 0;
    infop->direction = DIRECT_NEXT;
    finddate(infop);
}

void getdate(struct msginfo *infop,int flagfail)
/* infop->date has to be 0 or valid on entry. Month outside of [1-12] on */
/* entry causes GIGO */
{
  if (!flagfail) {				/* guess */
    if (infop->direction == DIRECT_NEXT) {
      infop->date++;
      if (infop->date % 100 > 12) infop->date += (100 - 12);
    } else if (infop->direction == DIRECT_PREV) {
      infop->date--;
      if (!infop->date % 100) infop->date -= (100 - 12);
    }
  } else
    finddate(infop);
  return;
}

indexlinks(struct msginfo *infop)
{
  unsigned long tmpmsg;

  tmpmsg = infop->target;
  infop->target = 1;
  oputs("<DIV class=idxlinks><STRONG>");
  linktoindex(infop,ITEM_INDEX);
  oputs(">[&lt;&lt;-]</A>\n");
  if (tmpmsg >= 100) infop->target = tmpmsg - 100;
  linktoindex(infop,ITEM_INDEX);
  oputs(">[&lt;-]</A>\n");
  infop->target = tmpmsg + 100;
  linktoindex(infop,ITEM_INDEX);
  oputs(">[-&gt;]</A>\n");
  infop->target = MAXULONG;
  linktoindex(infop,ITEM_INDEX);
  oputs(">[-&gt;&gt;]</A> |\n");
  infop->target = tmpmsg;
  linktoindex(infop,ITEM_DATE);
  oputs(">[Threads by date]</A>\n");
  subfaqlinks();
  homelink();
  oputs("</STRONG></DIV>\n");
}

int show_index(struct msginfo *infop)
{
  unsigned long thismsg;
  unsigned int pos,l;
  char ch;

  (void) makefn(&fn,ITEM_INDEX,msginfo.target,"");
  if ((fd = open_read(fn.s)) == -1)
    if (errno == error_noent)
      return 0;
    else
      strerr_die4sys(111,FATAL,ERR_OPEN,fn.s,": ");
  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));
  if (!stralloc_copyb(&line,strnum,
	fmt_ulong(strnum,(unsigned long) (infop->target / 100))))
		die_nomem();
  if (!stralloc_cats(&line,"xx")) die_nomem();
  html_header("Messages ",line.s,line.len,"idxbody",SPC_BANNER | SPC_BASE);
  indexlinks(infop);
  oputs("<HR><H1 id=\"idxhdr\">");
  oputs("Messages ");
  oput(line.s,line.len);
  oputs("</H1>\n");
  oputs("<HR><DIV class=idx>\n");
  for (;;) {
    if (getln(&ssin,&line,&match,'\n') == -1)
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match)
      break;
    pos = scan_ulong(line.s,&thismsg);
    l = pos;
    ch = line.s[pos++];
    pos++;
    if (line.len < pos + 1 + HASHLEN)
       strerr_die2x(100,FATAL,"index line with truncated subject entry");
    if (!stralloc_copyb(&subject,line.s+pos,HASHLEN)) die_nomem();
    if (!stralloc_0(&subject)) die_nomem();
    infop->axis = ITEM_SUBJECT;
    infop->subject = subject.s;
    oput(strnum,fmt_uint0(strnum,(unsigned int) thismsg % 100,2));
    oputs(": ");
    link(infop,ITEM_MESSAGE,ITEM_SUBJECT,thismsg,line.s+pos,line.len - pos - 1);
    oputs("\n");
    if (ch == ':') {
      if (getln(&ssin,&line,&match,'\n') == -1)
        strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
      if (!match)
        break;
      pos = byte_chr(line.s,line.len,';');
      if (pos != line.len) {
	infop->date = date2yyyymm(line.s);
	oputs("(");
	link(infop,ITEM_AUTHOR,ITEM_AUTHOR,thismsg,line.s+pos+1,
		line.len - pos - 2);
	oputs(")<BR>\n");
      }
    }
  }
  close(fd);
  oputs("\n</DIV><HR>\n");
  indexlinks(infop);
  html_footer(SPC_BANNER);
  return 1;
}

void objectlinks(struct msginfo *infop, char item)
{
  oputs("<DIV class=objlinks><STRONG>\n");
  if (item == ITEM_DATE) {
    datelink(infop,0,DIRECT_FIRST);
    datelink(infop,infop->date,DIRECT_PREV);
    datelink(infop,infop->date,DIRECT_NEXT);
    datelink(infop,0,DIRECT_LAST);
    oputs("\n");
  } else {
    if (!infop->target) infop->axis = ITEM_DATE;
    linktoindex(infop,ITEM_DATE);
    oputs(">[Threads by date]</A>\n");
  }
  if (item != ITEM_INDEX) {
    linktoindex(infop,ITEM_INDEX);
    oputs(">[Messages by date]</A>\n");
  }
  homelink();
  subfaqlinks();
  oputs("</STRONG></DIV>\n");
}

int show_object(struct msginfo *infop,char item)
/* shows thread, threads, author */
/* infop has the info needed to access the author/subject/thread file */
{
  unsigned long lastdate,thisdate,thismsg;
  char linkitem;
  char targetitem;
  unsigned int pos;

  lastdate = 0L;
  targetitem = ITEM_MESSAGE;			/* default message is target */
  switch (item) {
    case ITEM_SUBJECT:
	if (!makefn(&fn,ITEM_SUBJECT,0L,infop->subject)) return 0;
	break;
    case ITEM_AUTHOR:
	if (!makefn(&fn,ITEM_AUTHOR,0L,infop->author)) return 0;
	break;
    case ITEM_DATE:
	if (!makefn(&fn,ITEM_DATE,infop->date,"")) return 0;
	break;
    default:
	die_prog("Bad object type in show_object");
  }
if ((fd = open_read(fn.s)) == -1)
    if (errno == error_noent)
      return 0;
    else
      strerr_die4sys(111,FATAL,ERR_OPEN,fn.s,": ");
  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));
  if (item != ITEM_DATE) {
    if (getln(&ssin,&line,&match,'\n') == -1)	/* read subject */
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match || line.len < HASHLEN + 2)
      strerr_die4x(111,FATAL,ERR_READ,fn.s,": nothing there");
  }
  switch (item) {
    case ITEM_SUBJECT:
	html_header("Thread on: ",line.s + HASHLEN + 1,
		line.len - HASHLEN - 2,"subjbody",SPC_BANNER | SPC_BASE);
	objectlinks(infop,item);
	oputs("<HR><H1>On: ");
	oput(line.s+HASHLEN+1,line.len-HASHLEN-2);
	oputs("</H1>\n");
	break;
    case ITEM_AUTHOR:
	html_header("Posts by: ",line.s + HASHLEN + 1,
		line.len - HASHLEN - 2,"authbody",SPC_BANNER | SPC_BASE);
	objectlinks(infop,item);
	oputs("<HR><H1>By: ");
	oput(line.s+HASHLEN+1,line.len-HASHLEN-2);
	oputs("</H1>\n");
	break;
    case ITEM_DATE:
/*	targetitem = ITEM_SUBJECT;*/	/* thread index is target */
	thisdate = infop->date;
	if (dateline(&dtline,infop->date) < 0) die_nomem();
	html_header("Threads for ",
		dtline.s,dtline.len,"threadsbody",SPC_BANNER | SPC_BASE);
	objectlinks(infop,item);
	oputs("<HR><H1>Threads for ");
	oput(dtline.s,dtline.len);
	oputs("</H1>\n");
	break;
    default: die_prog("unrecognized object type in show_object");
  }

  oputs("<DIV class=obj>\n");
  for (;;) {
    if (getln(&ssin,&line,&match,'\n') == -1)	/* read subject */
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match)
      break;
    pos = scan_ulong(line.s,&thismsg);
    if (line.s[pos++] != ':')
	strerr_die4x(100,FATAL,"entry in ",fn.s," lacks message number");
    if (item != ITEM_DATE) {		/* no date for threads by date */
      pos += scan_ulong(line.s+pos,&thisdate);
      infop->date = thisdate;
      if (line.s[pos++] != ':')
	strerr_die4x(100,FATAL,"entry in ",fn.s," lacks date");
    }
    if (line.len < pos + HASHLEN + 2)
	strerr_die4x(100,FATAL,"entry in ",fn.s," lacks hash");
    if (thisdate != lastdate) {
      if (!lastdate)
	oputs("<UL>\n");
      else
        oputs("<P>");
      oputs("<LI><H2>");
      datelink(infop,thisdate,DIRECT_SAME);
      lastdate = thisdate;
      oputs("</H2>\n");
    }
    if (item == ITEM_SUBJECT)
      linkitem = ITEM_AUTHOR;
    else
      linkitem = ITEM_SUBJECT;
    link(infop,targetitem,linkitem,thismsg,line.s+pos,line.len - pos - 1);
    oputs("<BR>\n");
  }
  close(fd);
  oputs("</UL>\n");
  if (!infop->target)
    oputs("<a name=b></a>");
  oputs("<HR></DIV>\n");
  objectlinks(infop,item);
  html_footer(SPC_BANNER);
  return 1;
}

void clear_mime()
{
  mime_current->charset.len = 0;	/* exist but need emptying */
  mime_current->boundary.len = 0;
  mime_current->ctype.len = 0;
  mime_current->mimetype = MIME_NONE;
  mime_current->ctenc = CTENC_NONE;
  mime_current->cs = CS_NONE;
}

void new_mime()
{
    mime_tmp = mime_current;
    if (mime_current)
      mime_current = mime_current->next;
    if (!mime_current) {
      if (!(mime_current = (mime_info *) alloc(sizeof (mime_info))))
	die_nomem();
      mime_current->charset = sainit;		/* init */
      mime_current->boundary = sainit;
      mime_current->ctype = sainit;
      mime_current->next = (mime_info *) 0;
      mime_current->previous = mime_tmp;
    }
    clear_mime();
    if (mime_tmp)
      mime_current->level = mime_tmp->level + 1;
    else
      mime_current->level = 1;
}

void mime_getarg(stralloc *sa,char **s, unsigned int *l)
/* copies next token or "token" into sa and sets s & l appropriately */
/* for continuing the search */
{
  char *cp, *cpafter, *cpnext;

      if (!*l || !**s) return;
      if (**s == '"') {
	(*s)++; (*l)--;
	cp = *s; cpnext = cp + *l; cpafter = cpnext;
	while (cp < cpafter) {
	  if (*cp == '"') {
	    break;
	  }
	  cp++;
	}
	cpnext = cp;
      } else {
	cp = *s; cpnext = cp + *l; cpafter = cpnext;
	while (cp < cpafter) {
	  if (*cp == ' ' || *cp == '\t' || *cp == '\n' || *cp == ';') {
	    break;
	  }
	  cp++;
	}
	cpnext = cp;
      }
      if (!stralloc_copyb(sa,*s,cp - *s)) die_nomem();
      *l = cpafter - cpnext;		/* always >= 0 */
      *s = cpnext;
      return;
}

void decode_mime_type(char *s,unsigned int l,unsigned int flagmime)
{
  char *st;
  unsigned int r,lt;
  if (!flagmime || !l) {		/* treat non-MIME as plain text */
    mime_current->mimetype = MIME_TEXT_PLAIN;
    if (!stralloc_copys(&curcharset,charset)) die_nomem();
	/* should be us-ascii, but this is very likely better */
    return;
  }
  r = MIME_APPLICATION_OCTETSTREAM;
  while (l && (*s == ' ' || *s == '\t')) { s++; l--; }	/* skip LWSP */
  mime_getarg(&(mime_current->ctype),&s,&l);
  st = mime_current->ctype.s;
  lt = mime_current->ctype.len;
  if (case_startb(st,lt,"text")) {			/* text types */
    r = MIME_TEXT; st+= 4; lt-= 4;
    if (case_startb(st,lt,"/plain")) {
      r = MIME_TEXT_PLAIN; st+= 6; lt-= 6;
    } else if (case_startb(st,lt,"/html")) {
      r = MIME_TEXT_HTML; st+= 5; lt-= 5;
    } else if (case_startb(st,lt,"/enriched")) {
      r = MIME_TEXT_ENRICHED; st+= 9; lt-= 9;
    } else if (case_startb(st,lt,"/x-vcard")) {
      r = MIME_TEXT_ENRICHED; st+= 8; lt-= 8;
    }
  } else if (case_startb(st,lt,"multipart")) {		/* multipart types */
    r = MIME_MULTI; st += 9; lt-= 9;
    if (case_startb(st,lt,"/alternative")) {
      r = MIME_MULTI_ALTERNATIVE; st+= 12; lt-= 12;
    } else if (case_startb(st,lt,"/mixed")) {
      r = MIME_MULTI_MIXED; st+= 6; lt-= 6;
    } else if (case_startb(st,lt,"/digest")) {
      r = MIME_MULTI_DIGEST; st+= 7; lt-= 7;
    } else if (case_startb(st,lt,"/signed")) {
      r = MIME_MULTI_SIGNED; st+= 7; lt-= 7;
    }
  } else if (case_startb(st,lt,"message")) {		/* message types */
    r = MIME_MESSAGE; st += 7; lt -= 7;
    if (case_startb(st,lt,"/rfc822")) {
      r = MIME_MESSAGE_RFC822; st+= 7; lt-= 7;
    }
  }
  mime_current->mimetype = r;
  while (l) {
    while (l && (*s == ' ' || *s == '\t' || *s == ';' || *s == '\n')) {
	 s++; l--; }					/* skip ;LWSP */
    if (case_startb(s,l,"boundary=")) {
      s += 9; l-= 9;
      mime_getarg(&(mime_current->boundary),&s,&l);
    } else if (case_startb(s,l,"charset=")) {
      s += 8; l-= 8;
      mime_getarg(&(mime_current->charset),&s,&l);
      cs = decode_charset(mime_current->charset.s,
		mime_current->charset.len);
      if (cs == CS_BAD) cs = csbase;			/* keep base cs */
      else
	if (!stralloc_copy(&curcharset,&mime_current->charset)) die_nomem();
    } else {						/* skip non LWSP */
      for (;;) {
	if (!l) break;
	if (*s == '"') {
	  s++, l--;
	  while (l && *s != '"') { s++, l--; }
	  if (l) { s++, l--; }
	  break;
	} else {
	  if (!l || *s == ' ' || *s == '\t' || *s == '\n') break;
	  s++; l--;
	}
      }
    }
  }
  return;
}

void decode_transfer_encoding(register char *s,register unsigned int l)
{
  unsigned int r;
  mime_current->ctenc = CTENC_NONE;
  if (!l || (mime_current->mimetype & MIME_MULTI)) return;
			/* base64/QP ignored for multipart */
  r = CTENC_NONE;
  while (l && (*s == ' ' || *s == '\t')) { s++; l--; }	/* skip LWSP */
  if (case_startb(s,l,"quoted-printable")) {
    r = CTENC_QP;
  } else if (case_startb(s,l,"base64")) {
    r = CTENC_BASE64;
  }
  mime_current->ctenc = r;
  return;
}

int check_boundary()
/* return 0 if no boundary, 1 if start, 2 if end */
{
  mime_info *tmp;

  if (*line.s != '-' || line.s[1] != '-') return 0;
  tmp = mime_current;
  while (tmp) {
    if (tmp->boundary.len) {
    if (line.len > tmp->boundary.len + 2 &&
	!case_diffb(line.s+2,tmp->boundary.len,tmp->boundary.s)) {
      if (line.s[tmp->boundary.len + 2] == '-' &&
		line.s[tmp->boundary.len + 3] == '-') {	/* end */
	mime_current = tmp;
	clear_mime();
	return 2;

      } else {						/* start */
	mime_current = tmp;
	new_mime();
	return 1;
      }
    }
    }
    tmp = tmp->previous;
  }
  if (!stralloc_copys(&curcharset,charset)) die_nomem();
			/* suprtfluous since header done by now */
  cs = csbase;
  return 0;
}

void start_message_page(struct msginfo *infop)
/* header etc for message. Delayed to collect subject so that we can put */
/* that in TITLE. This in turn needed for good looking robot index.      */
/* Yep, not pretty, but it works and it's abhorrent to seek()/rewind     */
/* and another hack: it's hard to mix charsets within a doc. So, we disp */
/* messages entirely in the charset of the message. This is ok, since    */
/* headers will be us-ascii or have encoded segments usually matching    */
/* the charset in the message. Of course, we should be able to used e.g. */
/* <DIV charset=iso-2022-jp> with internal resources as well as internal */
/* ones. One might make other-charset messages external resources as well*/
/* Now, the problem is that we need to "preview" MIME info _before_      */
/* seeing the start boundary. */
{
  if (!stralloc_copyb(&decline,strnum,fmt_ulong(strnum,infop->target)))
	die_nomem();
  if (!stralloc_cats(&decline,":")) die_nomem();
  if (!stralloc_0(&decline)) die_nomem();
  decodeHDR(hdr[HDR_SUBJECT - 1].s,hdr[HDR_SUBJECT - 1].len,&line,"",FATAL);
  if (!mime_current)
    new_mime();			/* allocate */
  else
    clear_mime();
  decode_mime_type(hdr[HDR_CT - 1].s,hdr[HDR_CT - 1].len,
	hdr[HDR_VERSION - 1].len);
  html_header(decline.s,line.s,line.len - 1,
		"msgbody",SPC_BASE);
  msglinks(infop);
  oputs("<DIV class=message>\n");
}

void show_part(struct msginfo *infop,int flagshowheaders,
	int flagskip,int flagstartseen)
/* if flagshowheaders we display headers, otherwise not */
/* if flagstartseen we've already see the start boundary for this part, */
/* if not we'll ignore what's there up to it */
/* if flagskip we skip this part */
{
  char *cp;
  int flaginheader;
  int whatheader;
  int flaggoodfield;
  int flaghtml;
  int btype,i;
  unsigned int colpos,l,pos;
  char linetype;

  flaginheader = 1;
  for (i = 0; i < NO_HDRS; i++) hdr[i].len = 0;
  flaggoodfield = 1;
  match = 1;
  recursion_level++;			/* one up */
  for (;;) {
    if (!match) return;
    if (getln(&ssin,&line,&match,'\n') == -1)
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match) return;
    if ((btype = check_boundary())) {
      if (flagpre) {
	oputs("</PRE>");
	toggle_flagpre(0);
      }
      if (mime_current->level < recursion_level) {
        return;
      }
      if (btype == 1) {
	flagstartseen = 1;
	flaggoodfield = 1;
	flaginheader = 1;
      } else
	flagstartseen = 0;
      continue;
    }
    if (!flagstartseen) continue;	/* skip to start */
    if (flaginheader) {
      if (line.len == 1) {
	if (flagshowheaders) {		/* rfc822hdr only */
	  if (flagtoplevel)
	    start_message_page(infop);	/* so we can put subj in TITLE */
	  oputs("<DIV class=rfc822hdr><HR>\n");
	  for (i = 0; i < NO_HDRS; i++) {
	    if (!hdr[i].len || !headers_shown[i]) continue;
	    if (i == HDR_SUBJECT - 1 && flagtoplevel)
	      oputs("<SPAN class=subject>");
	    oputs("<EM>");
	    oputs(constmap_get(&headermap,i + 1));
	    oputs(":</EM>");
	    decodeHDR(hdr[i].s,hdr[i].len,&line,"",FATAL);
	    if (i == HDR_SUBJECT - 1 && flagtoplevel) {
	      oputs("<A class=relk href=\"mailto:");
	      oputs(local);
	      oput("@",1);
	      oputs(host);
	      oputs("?subject=");
	      urlencode_put(line.s + 1,line.len - 2);
	      oputs("\">");
	    }
	    if (flagobscure && i == HDR_FROM - 1) {
	      oputs(" ");
	      decodeHDR(cp,author_name(&cp,line.s,line.len),&decline,"",FATAL);
	      htmlencode_put(decline.s,decline.len);
	    } else {
	      decodeHDR(hdr[i].s,hdr[i].len,&decline,"",FATAL);
              htmlencode_put(decline.s,decline.len - 1);
	    }
	    if (i == HDR_SUBJECT - 1 && flagtoplevel)
	      oputs("</A></SPAN>");
	    oputs("\n<BR>");
	  }
	  oputs("</DIV>\n");
	}
        flaginheader = 0;
	flagtoplevel = 0;
        flaggoodfield = 1;
	flaghtml = 0;
	if (!flagmime)
	  flagmime = hdr[HDR_VERSION - 1].len;	/* MIME-Version header */
	decode_mime_type(hdr[HDR_CT - 1].s,hdr[HDR_CT - 1].len,flagmime);
	decode_transfer_encoding(hdr[HDR_CTENC - 1].s,hdr[HDR_CTENC - 1].len);
	content.len = 0; encoding.len = 0;
	switch (mime_current->mimetype) {
	  case MIME_MULTI_SIGNED:
	  case MIME_MULTI_MIXED:
	  case MIME_MULTI_ALTERNATIVE:
	  case MIME_MULTI_DIGEST:
		show_part(infop,0,0,0);
		recursion_level--;
		flagstartseen = 0;
		flaginheader = 1;
		continue;
	  case MIME_MESSAGE_RFC822:
		oputs("\n<PRE>");
		toggle_flagpre(1);
		flagshowheaders = 1;
		flaginheader = 1;
		flagmime = 0;		/* need new MIME-Version header */
		continue;
	  case MIME_TEXT_HTML:
		if (flagshowhtml) {
		  oputs("<HR>\n");
		  flaghtml = 1;
		} else {
		  oputs("<strong>[\"");
		  oput(mime_current->ctype.s,mime_current->ctype.len);
		  oputs("\" not shown]</strong>\n");
		  flaggoodfield = 0;	/* hide */
		}
		continue;
	  case MIME_TEXT_PLAIN:
	  case MIME_TEXT:		/* in honor of Phil using "text" on */
	  case MIME_NONE:		/* the qmail list and rfc2045:5.2 */
		oputs("<HR>\n<PRE>\n");
		toggle_flagpre(1);
		continue;
	  case MIME_TEXT_VCARD:
	  default:		/* application/octetstream...*/
		oputs("<HR><strong>[\"");
		oput(mime_current->ctype.s,mime_current->ctype.len);
		oputs("\" not shown]</strong>\n");
		flaggoodfield = 0;	/* hide */
		continue;
	}
      } else if (line.s[0] != ' ' && line.s[0] != '\t') {
	linetype = ' ';
        flaggoodfield = 0;
	colpos = byte_chr(line.s,line.len,':');
	if ((whatheader = constmap_index(&headermap,line.s,colpos))) {
          flaggoodfield = 1;
	  if (!stralloc_copyb(&hdr[whatheader - 1],line.s + colpos + 1,
		line.len - colpos - 1)) die_nomem();
	}
      } else {
	if (whatheader)
	  if (!stralloc_catb(&hdr[whatheader - 1],line.s,line.len))
		die_nomem();
      }
    } else {
      if (flaggoodfield) {
	if (mime_current->ctenc) {
	  if (!stralloc_copy(&decline,&line)) die_nomem();
	  line.len = 0;
	  if (mime_current->ctenc == CTENC_QP)
	    decodeQ(decline.s,decline.len,&line);
	  else
	    decodeB(decline.s,decline.len,&line);
	}
	if (flaghtml)
	  oput(line.s,line.len);
	else {
          htmlencode_put(line.s,line.len);		/* body */
	}
      }
    }
  }
}

int show_message(struct msginfo *infop)
{
  char *psz;

  if(!stralloc_copys(&headers,(char *) headers_used)) die_nomem();
  if (!stralloc_0(&headers)) die_nomem();
  psz = headers.s;
  while (*psz) {
    if (*psz == '\\') *psz = '\0';
    ++psz;
  }
  if (!constmap_init(&headermap,headers.s,headers.len,0))
	die_nomem();

  (void) makefn(&fn,ITEM_MESSAGE,msginfo.target,"");
  if ((fd = open_read(fn.s)) == -1)
    if (errno == error_noent)
      return 0;
    else
      strerr_die4sys(111,FATAL,ERR_OPEN,fn.s,": ");
  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));
  toggle_flagpre(0);
  recursion_level = 0;	/* recursion level for show_part */
  flagmime = 0;		/* no active mime */
  flagtoplevel = 1;	/* top message/rfc822 get special rx */
  new_mime();		/* initiate a MIME info storage slot */

  show_part(infop,1,0,1);	/* do real work, including html header etc */
  if (flagpre)
    oputs("</PRE>\n");
  close(fd);
  oputs("<HR></DIV>\n");
  msglinks(infop);
  html_footer(0);
  return 1;
}

char decode_item(char ch)
{
  switch (ch) {
	case 'm': return ITEM_MESSAGE;
	case 'a': return ITEM_AUTHOR ;
	case 's': return ITEM_SUBJECT;
	case 'd': return ITEM_DATE   ;
	case 'i': return ITEM_INDEX  ;
	default: cgierr("Navigation command contains ",
		"illegal item code","");
  }
  return 0;	/* never reached */
}

char decode_direction(char ch)
{
  switch (ch) {
	case 's': return DIRECT_SAME;
	case 'n': return DIRECT_NEXT;
	case 'p': return DIRECT_PREV;
	default: cgierr("Navigation command contains ",
		"illegal direction code","");
  }
  return 0;	/* never reached */
}

int decode_cmd(char *s,struct msginfo *infop)
/* decodes s into infop. Assures that no security problems slip through by */
/* checking everything */
/* commands xyd:123[:abc]. x what we want, y is the axis, d the direction. */
/* 123 is the current message number. abc is a date/subject/author hash,   */
/* depending on axis, or empty if not available. */
/* returns: 0 no command+msgnum. */
/*          1 empty or at least cmd + msgnum. */
/*            Guarantee: Only legal values accepted */
{
  register char ch;

  infop->source = 0L;
  infop->date = 0L;
  infop->author = (char *)0;
  infop->subject = (char *)0;
  infop->cgiarg = (char *)0;

  if (!s || !*s) {	/* main index */
    infop->item = ITEM_DATE;
    infop->axis = ITEM_DATE;
    infop->direction = DIRECT_SAME;
    latestdate(&msginfo,0);
    infop->target = MAXULONG;
    return 1;
  }
  ch = *(s++);
  if (ch >= '0' && ch <= '9') {	/* numeric - simplified cmd: msgnum ... */
    s--;
    infop->item = ITEM_MESSAGE;
    infop->axis = ITEM_MESSAGE;
    infop->direction = DIRECT_SAME;
  } else {			/* what:axis:direction:msgnum ... */
    infop->item = decode_item(ch);
    ch = *(s++);
    infop->axis = decode_item(ch);
    ch = *(s++);
    infop->direction = decode_direction(ch);
    if (*(s++) != ':') return 0;
  }
  s+= scan_ulong(s,&(infop->source));
  if (*(s++) != ':') return 0;
  if (*s >= '0' && *s <= '9') {	/* numeric nav hint [date] */
    s+= scan_ulong(s,&(infop->date));
    if (!*s++) return 1;	/* skip any char - should be ':' unless NUL */
  }
  if (checkhash(s)) {		/* Ignore if illegal rather than complaining*/
    if (!stralloc_copyb(&charg,s,HASHLEN)) die_nomem();
    if (!stralloc_0(&charg)) die_nomem();
    infop->cgiarg = charg.s;
  }
  return 1;
}

int msg2hash(struct msginfo *infop)
{
  unsigned int pos;
  unsigned long tmpmsg;

  if (!infop->source) die_prog("source is 0 in msg2hash");
  (void) makefn(&fn,ITEM_INDEX,infop->source,"");
  if ((fd = open_read(fn.s)) == -1) {
    if (errno == error_noent)
      return 0;
    else
      strerr_die4sys(111,FATAL,ERR_OPEN,fn.s,": ");
  }
  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));
  for (;;) {
        if (getln(&ssin,&line,&match,'\n') == -1)
          strerr_die3sys(111,FATAL,ERR_READ,"index: ");
        if (!match)
	  return 0;				/* didn't find message */
	if (*line.s == '\t') continue;		/* author line */
        pos = scan_ulong(line.s,&tmpmsg);
	if (tmpmsg == infop->source) {
          if (line.s[pos++] != ':' || line.s[pos++] != ' ')
	    strerr_die3x(100,ERR_SYNTAX,fn.s,": missing subject separator");
	  if (line.len < HASHLEN + pos)
	    strerr_die3x(100,ERR_SYNTAX,fn.s,": missing subject hash");
	  if (!stralloc_copyb(&subject,line.s+pos,HASHLEN)) die_nomem();
	  if (!stralloc_0(&subject)) die_nomem();
	  infop->subject = subject.s;
          if (getln(&ssin,&line,&match,'\n') == -1)
            strerr_die3sys(111,FATAL,ERR_READ,"index: ");
          if (!match)
	    strerr_die3x(100,ERR_SYNTAX,fn.s,
		": author info missing. Truncated?");
	  pos = byte_chr(line.s,line.len,';');
	  if (pos == line.len)
	    strerr_die3x(100,ERR_SYNTAX,fn.s,"missing ';' after date");
	  if (pos > 1)
	    infop->date = date2yyyymm(line.s+1);	/* ';' marks end ok */
	  pos++;
	  if (line.len < HASHLEN + pos)
	    strerr_die3x(100,ERR_SYNTAX,fn.s,": missing author hash");
	  if (!stralloc_copyb(&author,line.s+pos,HASHLEN)) die_nomem();
	  if (!stralloc_0(&author)) die_nomem();
	  infop->author = author.s;
	  close(fd);
	  return 1;	/* success */
        }
  }
  close(fd);
  return 0;		/* failed to match */
}

void setmsg(struct msginfo *infop)
/* Reads the file corresponding to infop->axis and assumes fn.s is set */
/* correctly for this. Sets up a msgnav structure and links it in      */
/* correction for axis=author/subject. For axis=date it supports also  */
/* direction=DIRECT_FIRST which will return the first message of the   */
/* first thread in the date file. DIRECT_LAST is not supported.        */
/* DIRECT_FIRST is supported ONLY for date. */
{
  if (infop->direction == DIRECT_SAME) {
    infop->target = infop->source;
    return;
  }
  if ((fd = open_read(fn.s)) == -1) {
    if (errno == error_noent)
      strerr_die4x(100,FATAL,ERR_OPEN,fn.s,
	" in listmsgs. Rerun ezmlm-archive!");
    else
      strerr_die4sys(111,FATAL,ERR_OPEN,fn.s,": ");
  }
  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));
  if (infop->source != ITEM_DATE) {
    if (getln(&ssin,&line,&match,'\n') == -1)	/* first line */
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match)
      strerr_die3x(100,ERR_SYNTAX,fn.s,": first line missing");
  }
  msgnav[3] = 0L;		/* next */
  msgnav[4] = 0L;		/* after */
  infop->target = 0L;
  for (;;) {
    if (getln(&ssin,&line,&match,'\n') == -1)
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match) break;
    msgnav[0] = msgnav[1];
    msgnav[1] = msgnav[2];
    (void) scan_ulong(line.s,&(msgnav[2]));
    if (infop->direction == DIRECT_FIRST) break;
    if (msgnav[2] == infop->source) {
      if (getln(&ssin,&line,&match,'\n') == -1)
        strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
      if (!match) break;
      (void) scan_ulong(line.s,&(msgnav[3]));
      if (getln(&ssin,&line,&match,'\n') == -1)
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
      if (!match) break;
      (void) scan_ulong(line.s,&(msgnav[4]));
      break;
    }
  }
  close(fd);
  switch (infop->axis) {
    case ITEM_AUTHOR:
      infop->authnav = msgnav + 2 + infop->direction;
      infop->target = *(infop->authnav);
      infop->subject = (char *)0;	/* what we know is not for this msg */
      infop->date = 0;
      break;
    case ITEM_SUBJECT:
      infop->subjnav = msgnav + 2 + infop->direction;
      infop->target = *(infop->subjnav);
      infop->author = (char *)0;	/* what we know is not for this msg */
      infop->date = 0;
      break;
    case ITEM_DATE:
      infop->target = msgnav[2];
      infop->subject = (char *)0;	/* what we know is not for this msg */
      infop->author = (char *)0;	/* what we know is not for this msg */
    default:
      die_prog("Bad item in setmsg");
  }
  return;
}

void auth2msg(struct msginfo *infop)
{
  if (!infop->author) die_prog("no such author in authmsg");
  if (!makefn(&fn,ITEM_AUTHOR,0L,infop->author)) die_prog("auth2msg");
  setmsg(infop);
}

void subj2msg(struct msginfo *infop)
{
  if (!infop->subject) die_prog("no such subject in subj2msg");
  if (!makefn(&fn,ITEM_SUBJECT,0L,infop->subject)) die_prog("subj2msg");
  setmsg(infop);
}

void date2msg(struct msginfo *infop)
{
  (void) makefn(&fn,ITEM_DATE,infop->date,"");
  setmsg(infop);
}

void findlastmsg(struct msginfo *infop)
{
  if (!getconf_line(&line,"num",dir,0,FATAL))
    cgierr("Sorry, there are no messages in the archive","","");
  if (!stralloc_0(&line)) die_nomem();
  (void) scan_ulong(line.s,&(infop->target));
}

int do_cmd(struct msginfo *infop)
/* interprets msginfo to create msginfo. Upon return, msginfo can be trusted */
/* to have all info needed, and that all info is correct. There may be more */
/* info than needed. This can be used to build more specific links. NOTE:   */
/* there is no guarantee that a message meeting the criteria actually exists*/
{
  infop->target = infop->source;

  switch (infop->item) {
	case ITEM_MESSAGE:	/* we want to get a message back */
	  {
	    switch (infop->axis) {
	      case ITEM_MESSAGE:
	        if (infop->direction == DIRECT_SAME)
		  break;
	        else if (infop->direction == DIRECT_NEXT)
		  (infop->target)++;
	        else {		/* previous */
		  cache = 2;
		  if (infop->target >= 2)
		    (infop->target)--;
		  else
		    infop->target = 1;
	        }
		break;
	      case ITEM_AUTHOR:
	          infop->author = infop->cgiarg;
		if (!infop->author)	 /* we don't know author hash */
		  if (!msg2hash(infop)) return 0;
	        auth2msg(infop);
	        break;
	      case ITEM_SUBJECT:
	          infop->subject = infop->cgiarg;
	        if (!infop->subject)	 /* we don't know Subject hash */
		  if (!msg2hash(infop)) return 0;
	        subj2msg(infop);
	        break;
	    }
	    break;
	  }
	case ITEM_AUTHOR:
	  switch (infop->axis) {
	    case ITEM_MESSAGE:
	      if (!infop->author)
		if (!msg2hash(infop)) return 0;
	      break;
	    case ITEM_AUTHOR:
	      infop->author = infop->cgiarg;
	      if (!infop->author)
		if (!msg2hash(infop)) return 0;
	        auth2msg(infop);
	      break;
	    case ITEM_SUBJECT:
	      infop->subject = infop->cgiarg;
	      if (!infop->subject)	 /* we don't know Subject hash */
		if (!msg2hash(infop)) return 0;
	      subj2msg(infop);
	      break;
	    }
	    break;
	case ITEM_SUBJECT:
	  switch (infop->axis) {
	    case ITEM_MESSAGE:
	      if (!msg2hash(infop)) return 0;
	      break;
	    case ITEM_AUTHOR:
	      infop->author = infop->cgiarg;
	      if (!infop->author)
		if (!msg2hash(infop)) return 0;
	      auth2msg(infop);
	      break;
	    case ITEM_SUBJECT:
	      infop->subject = infop->cgiarg;
	      if (!infop->subject)	 /* we don't know Subject hash */
		if (!msg2hash(infop)) return 0;
	      subj2msg(infop);
	      break;
	    }
	    break;
	  case ITEM_DATE:	/* want a date reference */
	    switch (infop->axis) {
	      case ITEM_MESSAGE:
	      case ITEM_AUTHOR:
	      case ITEM_SUBJECT:
	      case ITEM_DATE:
		if (!infop->date && infop->source)
		  if (!msg2hash(infop)) return 0;
		  getdate(infop,0);
		break;
	    }
	    break;
	  case ITEM_INDEX:	/* ignore direction etc - only for index */
	    if (!infop->target)
	      infop->target = infop->source;
	    if (!infop->target)
	      findlastmsg(infop);
	    break;
  }
  return 1;
}

void list_lists()
{
  unsigned long lno;
  cache = 2;
  flagrobot = 2;
  html_header("Robot index of lists",0,0,0,0);
  for (;;) {
    if (getln(&ssin,&cfline,&match,'\n') == -1)		/* read line */
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match)
      break;
    if (cfline.s[0] == '#') continue;			/* skip comment */
    cfline.s[cfline.len - 1] = '\0';			/* so all are sz */
    (void) scan_ulong(cfline.s,&lno);			/* listno for line */
    if (lno) {				/* don't expose default list */
      oputs("<A href=\"");
      oput(strnum,fmt_ulong(strnum,lno));
      oputs("/index\">[link]</a>\n");
   }
  }
  html_footer(0);
}

void list_list(unsigned long listno)
/* Make one link [for list_set()] per set of 100 archive messages. */
/* Assumption: Any directory DIR/archive/xxx where 'xxx' is a numeric,*/
/* is part of the list archive and has in it an index file and one    */
/* or more messages. */
{
  DIR *archivedir;
  direntry *d;
  unsigned long msgset;

  flagrobot = 2;
  strnum[fmt_ulong(strnum,listno)] = '\0';
  archivedir = opendir("archive/");
  if (!archivedir)
    if (errno != error_noent)
      strerr_die4sys(111,FATAL,ERR_OPEN,dir,"/archive: ");
    else
      strerr_die4sys(100,FATAL,ERR_OPEN,dir,"/archive: ");

  cache = 1;
  html_header("Robot index for message sets in list",0,0,0,0);

  while ((d = readdir(archivedir))) {
    if (d->d_name[scan_ulong(d->d_name,&msgset)])
	continue;		/* not numeric */
    oputs("<a href=\"../");	/* from /ezcgi/0/index to /ezcgi/listno/index*/
    oputs(strnum);
    oputs("/index/");
    oputs(d->d_name);
    oputs("\">[link]</a>\n");
  }
  closedir(archivedir);
  html_footer(0);
}

void list_set(unsigned long listno,unsigned long msgset)
{
  unsigned int msgfirst,msgmax;
  unsigned long lastset;

  flagrobot = 2;
  findlastmsg(&msginfo);
  if (!stralloc_copys(&line,"<a href=\"../")) die_nomem();
  if (!stralloc_catb(&line,strnum,fmt_ulong(strnum,msgset))) die_nomem();
  lastset = msginfo.target / 100;
  cache = 2;
  msgfirst = 0;
  if (!msgset)
    msgfirst = 1;
  msgmax = 99;
  if (msgset > lastset) {		/* assure empty list */
    msgmax = 0;
    msgfirst = 1;
  } else if (msgset == lastset) {
    cache = 0;				/* still changing */
    msgmax = msginfo.target % 100;
  }
  html_header("Robot index for messages in set",0,0,0,0);
  while (msgfirst <= msgmax) {
    oput(line.s,line.len);
    oput(strnum,fmt_uint0(strnum,msgfirst,2));
    oputs("\">[link]</a>\n");
    msgfirst++;
  }
  html_footer(0);
}

/**************** MAY BE SUID ROOT HERE ****************************/
void drop_priv(int flagchroot)
{
  if (!uid) strerr_die2x(100,FATAL,ERR_SUID);		/* not as root */
  if (!euid) {
    if (flagchroot)
      if (chroot(dir) == -1)				/* chroot listdir */
        strerr_die4sys(111,FATAL,"failed to chroot ",dir,": ");
    if (setuid(uid) == -1)				/* setuid */
      strerr_die2sys(111,FATAL,ERR_SETUID);
  }
  euid = (unsigned long) geteuid();
  if (!euid) strerr_die2x(100,FATAL,ERR_SUID);		/* setuid didn't do it*/
}
/*******************************************************************/

int main(argc,argv)
int argc;
char **argv;
{
  char *cp,*cppath;
  unsigned long listno,thislistno,tmpuid,msgset;
  unsigned long msgnum = 0;
  unsigned long port = 0L;
  unsigned long tmptarget;
  unsigned int pos,l;
  int flagindex = 0;
  int flagchroot = 1;		/* chroot listdir if SUID root */
  int ret;
  char sep;

/******************** we may be SUID ROOT ******************************/
  uid = (unsigned long) getuid();			/* should be http */
  euid = (unsigned long) geteuid();			/* chroot only if 0 */

  if (!euid) {
    if ((fd = open_read(EZ_CGIRC)) == -1)		/* open config */
      strerr_die4sys(111,FATAL,ERR_OPEN,EZ_CGIRC,": ");
  } else {
    if ((fd = open_read(EZ_CGIRC_LOC)) == -1)		/* open local config */
      strerr_die4sys(111,FATAL,ERR_OPEN,EZ_CGIRC_LOC,": ");
  }

  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));	/* set up buffer */
	/* ##### tainted info #####*/

  cmd = env_get("QUERY_STRING");			/* get command */
  cppath = env_get("PATH_INFO");			/* get path_info */

  if (!cppath || !*cppath) {
    if (cmd && *cmd) {
      cmd += scan_ulong(cmd,&thislistno);
      if (*cmd == ':') cmd++;				/* allow ':' after ln*/
    } else
      thislistno = 0L;
  } else {
    cppath++;
      cppath += scan_ulong(cppath,&thislistno);		/* this listno */
      if (!thislistno || *cppath++ == '/') {
	if (str_start(cppath,"index")) {
          cppath += 5;
	  flagindex = 1;
	  if (!thislistno) {				/* list index */
	    drop_priv(0);	/* <---- dropping privs */
	    list_lists();
	    close(fd);
	    _exit(0);
	  }
	}
      }							/* rest done per list */
    }

  for (;;) {
    if (getln(&ssin,&cfline,&match,'\n') == -1)		/* read line */
      strerr_die4sys(111,FATAL,ERR_READ,fn.s,": ");
    if (!match)
      break;
    if (*cfline.s == '#' || cfline.len == 1) continue;	/* skip comment/blank */
    cfline.s[cfline.len - 1] = '\0';			/* so all are sz */
    pos = scan_ulong(cfline.s,&listno);			/* listno for line */
    if (thislistno != listno) continue;
    sep = cfline.s[pos++];
    if (cfline.s[pos] == '-') {				/* no chroot if -uid*/
      flagchroot = 0;
      pos++;
    }
    pos += scan_ulong(cfline.s+pos,&tmpuid);		/* listno for line */
    if (tmpuid) uid = tmpuid;				/* override default */
    if (!cfline.s[pos++] == sep)
      die_syntax("missing separator after user id");
    if (cfline.s[pos] != '/')
	die_syntax("dir");				/* absolute path */
    l = byte_chr(cfline.s + pos, cfline.len - pos,sep);
    if (l == cfline.len - pos)				/* listno:path:...*/
      die_syntax("missing separator after path");
    dir = cfline.s + pos;
    pos += l;
    cfline.s[pos++] = '\0';				/* .../dir\0 */
    break;	/* do rest after dropping priv */
  }
  close(fd);						/* don't accept uid 0*/
  if (!dir) {
    drop_priv(0);	/* don't trust cgierr. No dir, no chroot */
    cgierr("list ",ERR_NOEXIST,"");
  }
  if (chdir(dir) == -1)					/* chdir listdir */
    strerr_die4sys(111,FATAL,ERR_SWITCH,dir,": ");
  drop_priv(flagchroot);

/******************************* RELAX **********************************/

/********************* continue to process config line ******************/

  flagrobot = 0;
  if (cfline.s[pos] == '-') {
    flagobscure = 1;
    pos++;
  }
  local = cfline.s + pos;
  l = byte_chr(cfline.s + pos, cfline.len - pos,sep);	/* ... home */
  if (l < cfline.len - pos) {				/* optional */
    pos += l;
    cfline.s[pos++] = '\0';
    home = cfline.s + pos;
    l = byte_chr(cfline.s + pos, cfline.len - pos,sep);	/* ... charset */
    if (l < cfline.len - pos) {				/* optional */
      pos += l;
      cfline.s[pos++] = '\0';
      charset = cfline.s + pos;
      l = byte_chr(cfline.s+pos,cfline.len - pos,sep);	/* ... stylesheet */
      if (l < cfline.len - pos) {			/* optional */
        pos += l;
        cfline.s[pos++] = '\0';
        stylesheet = cfline.s + pos;
        l = byte_chr(cfline.s+pos,cfline.len-pos,sep);	/* ... bannerURL */
	if (l < cfline.len - pos) {			/* optional */
	  pos += l;
	  cfline.s[pos++] = '\0';
	  banner = cfline.s + pos;
	}
      }
    }
  }
  if (!charset || !*charset)				/* rfc822 default */
    charset = EZ_CHARSET;
  if (!stralloc_copys(&curcharset,charset)) die_nomem();
  csbase = decode_charset(curcharset.s,curcharset.len);
  if (csbase == CS_BAD) csbase = CS_NONE;
  cs = csbase;
  pos = + str_rchr(local,'@');
  if (!local[pos])
    die_syntax("listaddress lacks '@'");		/* require host */
  local[pos++] = '\0';
  host = local + pos;

/********************* Accomodate robots and PATH_INFO ****************/

  if (flagindex) {
    if (*(cppath++) == '/') {		/* /2/index/123 */
      cppath += scan_ulong(cppath,&msgset);
      list_set(thislistno,msgset);
    } else				/* /2/index */
      list_list(thislistno);
    _exit(0);
  }

  if (cppath && *cppath) {		/* /2/msgnum */
    flagrobot = 1;			/* allow index, but "nofollow" */
    scan_ulong(cppath,&msgnum);
  }					/* dealt with normally */

/********************* Get info from server on BASE etc ****************/

  if (!stralloc_copys(&base,"<BASE href=\"http://")) die_nomem();
  cp = env_get("SERVER_PORT");
  if (cp) {			/* port */
    (void) scan_ulong(cp,&port);
    if ((unsigned int) port == 443) {		/* https: */
      if (!stralloc_copys(&base,"<BASE href=\"https://")) die_nomem();
    }
  }
  if (port && (unsigned int) port != 80 && (unsigned int) port != 443) {
      if (!stralloc_cats(&base,":")) die_nomem();
      if (!stralloc_catb(&base,strnum,fmt_ulong(strnum,port))) die_nomem();
  }
  if (!(cp = env_get("HTTP_HOST")))
    if (!(cp = env_get("SERVER_NAME")))
      strerr_die2x(100,FATAL,"both HTTP_HOST and SERVER_NAME are empty");
  if (!stralloc_cats(&base,cp)) die_nomem();
  if (!(cp = env_get("SCRIPT_NAME")))
    strerr_die2x(100,FATAL,"empty SCRIPT_NAME");
  if (!stralloc_cats(&base,cp)) die_nomem();
  if (!stralloc_cats(&base,"\">\n")) die_nomem();
  if (!stralloc_copys(&url,"<A HREF=\"")) die_nomem();
  pos = str_rchr(cp,'/');
  if (cp[pos])
    if (!stralloc_cats(&url,cp + pos + 1)) die_nomem();
  if (!stralloc_cats(&url,"?")) die_nomem();
  if (thislistno) {
    if (!stralloc_catb(&url,strnum,fmt_ulong(strnum,thislistno))) die_nomem();
    if (!stralloc_cats(&url,":")) die_nomem();
  }

  cache = 1;				/* don't know if we want to cache */

/****************************** Get command ****************************/

  if (msgnum) {				/* to support /listno/msgno */
   msginfo.target = msgnum;
   msginfo.item = ITEM_MESSAGE;
   cache = 2;
  } else {
      (void) decode_cmd(cmd,&msginfo);
      if (!do_cmd(&msginfo))
	cgierr("I'm sorry, Dave ... I can't do that, Dave ...","","");
  }

  switch (msginfo.item) {
    case ITEM_MESSAGE:
	if (!show_message(&msginfo)) {		/* assume next exists ... */
	  cache = 0;				/* border cond. - no cache */
	  msginfo.target = msginfo.source;	/* show same */
	  msginfo.subjnav = 0;
	  msginfo.authnav = 0;
	  ret = show_message(&msginfo);
	}
	break;
    case ITEM_AUTHOR:
	if (!show_object(&msginfo,ITEM_AUTHOR))
	  cgierr ("I couldn't find the author for that message","","");
	break;
    case ITEM_SUBJECT:
	if (!show_object(&msginfo,ITEM_SUBJECT))
	  cgierr ("I couldn't find the subject for that message","","");
	break;
    case ITEM_DATE:
	if (!show_object(&msginfo,ITEM_DATE)) {
	  finddate(&msginfo);
	  ret = show_object(&msginfo,ITEM_DATE);
	}
	break;
    case ITEM_INDEX:
	if (!show_index(&msginfo)) {
	  tmptarget = msginfo.target;
	  findlastmsg(&msginfo);
	  cache = 0;			/* latest one - no cache */
	  if (!msginfo.target || msginfo.target > tmptarget) {
	    cache = 2;			/* won't change */
            firstdate(&msginfo,1);	/* first thread index */
	    msginfo.direction = DIRECT_FIRST;
	    date2msg(&msginfo);		/* (may not be 1 if parts removed) */
	  }
	  ret = show_index(&msginfo);
	}
	break;
    default:
	strerr_die2x(100,FATAL,"bad item in main");
  }
  if (!ret) {
    findlastmsg(&msginfo);		/* as last resort; last msgindex */
    cache = 0;
    ret = show_message(&msginfo);
  }

 _exit(0);
}
