#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <string.h>
#include <ctype.h>

#ifdef HAVE_DIRENT_H
# include <dirent.h>
#else
# define dirent direct
# ifdef HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# ifdef HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
#endif

#ifdef HAVE_LIMITS_H
#include <limits.h>
#endif

#include "suck_config.h"

#ifdef HAVE_REGEX_H
#include <regex.h>
#endif

#ifdef DMALLOC
#include <dmalloc.h>
#endif

#include "both.h"
#include "suck.h"
#include "suckutils.h"
#include "killfile.h"
#include "phrases.h"
#include "timer.h"

#define GROUP_KEEP "keep"
#define GROUP_DELETE "delete"		/* word in param file parsed for on group line to signify keep or delete */
#define NEWSGROUP_HEADER "Newsgroups: "	/* header for line to match group files to */

/* local function prototypes */
int get_chunk_mem(PMaster, unsigned long *len, int which, char **buf);
int check_newsgroups(char *, const char *);
void free_node(OneKill);
int parse_a_file(const char *, const char *, POneKill, int, int, int);
int pass_two(PKillStruct, int, const char *fname);
int check_a_group(PMaster, POneKill, char *, char **);
void print_debug(PKillStruct, const char *);
void debug_one_kill(POneKill);
void add_to_linkedlist(pmy_regex *, pmy_regex);
pmy_regex regex_scan(char *, char, int, int, char);
int regex_check(char *, pmy_regex, int);

void initialize_one_kill(POneKill);
#ifdef HAVE_REGEX_H
const char regex_chars[] = "*[]()^$\\?.";  /* characters which make it a regex */
#endif

void display_skiparray(unsigned char [SIZEOF_SKIPARRAY]);
int find_string(int, pmy_regex, char *);

/*-------------------------------------------------------------------------*/
/* the enum must match the phrases in suckengl.c  killf_reasons[] */
enum { REASON_NONE, REASON_TOOMANYLINES, REASON_NOTENUFLINES, REASON_NRGRPS, REASON_NOKEEP, REASON_TIE, REASON_HEADER,  \
       REASON_BODY, REASON_BODYBIG, REASON_BODYSMALL, REASON_NRXREF };
enum { CHECK_EXACT, CHECK_CHECK };
enum { CHUNK_HEADER, CHUNK_BODY };

const struct {
	int len;
	const char *name;
	int headerlen;
	const char *header;
} Params[] = {
	{8, "HILINES=", 7, "Lines: " },
	{9, "LOWLINES=", 7, "Lines: "},
	{7, "NRGRPS=", 12, "Newsgroups: "},
	{6, "GROUP=", 0, ""},
	{6, "QUOTE=", 0, ""},
	{8, "PROGRAM=", 0, ""},
	{7, "HEADER:", 0, ""},
	{5, "BODY:", 0, ""},
	{9, "BODYSIZE>", 0, ""},
	{9, "BODYSIZE<", 0, ""},
	{21, "GROUP_OVERRIDE_MASTER", 0, ""},	/* string in masterfile to signify that group files override the master */
	{17, "TIEBREAKER_DELETE", 0, ""},	/* what to do if match multi groups override default of keep */
	{18, "USE_EXTENDED_REGEX", 0, ""}, /* do we use extended regular expressions */
	{10, "NON_REGEX=", 0, ""},
	{5,  "PERL=", 0, ""},
	{14, "XOVER_LOG_LONG", 0, ""}, /* make Xover killlog look like regular kill lo)*/
	{7,  "NRXREF=", 6, "Xref: "}, 
};


enum { PARAM_HILINE, PARAM_LOWLINE, PARAM_NRGRPS, PARAM_GROUP, PARAM_QUOTE, PARAM_PROGRAM,PARAM_HDRSCAN,
	PARAM_BODY, PARAM_BODYBIG, PARAM_BODYSMALL, PARAM_GRPOVERRIDE, PARAM_TIEDELETE, PARAM_E_REGEX,
	PARAM_NONREGEX, PARAM_PERL, PARAM_XOVER_LOG_LONG, PARAM_NRXREF};
#define NR_PARAMS ((int) (sizeof(Params)/sizeof(Params[0])))


/*--------------------------------------------------------------------------*/
PKillStruct parse_killfile(int which, int logfile_yn, int debug, int ignore_postfix) {
	FILE *fptr;
	char buf[MAXLINLEN+1];
	int i, doprg = FALSE, mastergrp = RETVAL_OK, retval = TRUE;
	const char *filename;
#ifdef PERL_EMBED
	int doperl = FALSE;
#endif
	KillStruct *Masterkill = NULL;
	
	/* kill file is going to get three passes, 1st one to count how many group files to process */
	/* so can allocate memory for all the group stuff. */
	/* also check for group override of masterkillfile */
	/* and process PROGRAM line if it exists.  If we have one we don't do anything else */
	/* 2nd pass will be to actually process the group files */
	/* 3rd pass will be to process the master delete stuff */

	/* first malloc our master structure */
	if((Masterkill = malloc(sizeof(KillStruct))) == NULL) {
		error_log(ERRLOG_REPORT, killf_phrases[1], NULL);
		retval = FALSE;
	}
	else {
		/* now initialize everything */
		Masterkill->logfp = NULL;
		Masterkill->grp_override = FALSE;
		Masterkill->tie_delete = FALSE;
		Masterkill->totgrps = 0;
		Masterkill->grps = NULL;
		Masterkill->killfunc = chk_msg_kill;
		Masterkill->pbody= NULL;
		Masterkill->bodylen = 0;
		Masterkill->use_extended_regex = FALSE;
		Masterkill->xover_log_long = FALSE;
/* initialize the master struct */
		Masterkill->child.Stdin = Masterkill->child.Stdout = -1;
		Masterkill->child.Pid = -1;		
		initialize_one_kill(&(Masterkill->master));

#ifdef PERL_EMBED
		Masterkill->perl_int = NULL;
#endif
		
		Masterkill->logyn = logfile_yn;
		/* which option to we use in call to full_path() ?  Do we use the postfix or not? */
		Masterkill->ignore_postfix = ( ignore_postfix == TRUE) ? FP_GET_NOPOSTFIX : FP_GET ;

		filename = ( which == KILL_XOVER ) ? N_XOVER : N_KILLFILE;

		if(debug == TRUE) {
			do_debug("Trying to read killfile: %s\n", full_path(Masterkill->ignore_postfix, FP_DATADIR, filename));
		}
		
		/* FIRST PASS THRU MASTER KILLFILE - look for group delete/keeps and count em  and check for PROGRAM call*/
		if((fptr = fopen(full_path(Masterkill->ignore_postfix, FP_DATADIR, filename), "r")) == NULL) {
			/* this is not really an error, so don't report it as such */
			retval = FALSE;
		}
		else {
			if(debug == TRUE) {
				do_debug("Pass 1 & 2 kill file: %s\n", full_path(Masterkill->ignore_postfix, FP_DATADIR, filename));
			}
	
			while(fgets(buf, MAXLINLEN, fptr) != NULL && doprg == FALSE) {
				/* nuke nl, so file names are correct, etc */
				i = strlen(buf);
				if(buf[i-1] == '\n') {
					buf[i-1] = '\0';
				}
				if(debug ==  TRUE) {
					do_debug("Read kill file line: %s\n", buf);
				}
				if(strncmp(buf, Params[PARAM_GROUP].name, (size_t) Params[PARAM_GROUP].len) == 0) {
					Masterkill->totgrps++;	/* how many group files must we process */
				}						
				if(strncmp(buf, Params[PARAM_PROGRAM].name, (size_t) Params[PARAM_PROGRAM].len) == 0) {
#ifdef PERL_EMBED
					if(Masterkill->perl_int == NULL) {
#endif
						doprg = killprg_forkit(Masterkill, &buf[Params[PARAM_PROGRAM].len], debug, which);
#ifdef PERL_EMBED
					}
#endif
				}
				if(strncmp(buf, Params[PARAM_GRPOVERRIDE].name, (size_t) Params[PARAM_GRPOVERRIDE].len) == 0) {
					Masterkill->grp_override = TRUE;
				}
				if(strncmp(buf, Params[PARAM_TIEDELETE].name, (size_t) Params[PARAM_TIEDELETE].len) == 0) {
					Masterkill->tie_delete = TRUE;
				}
				if(strncmp(buf, Params[PARAM_E_REGEX].name, (size_t) Params[PARAM_E_REGEX].len) == 0) {
					Masterkill->use_extended_regex = TRUE;
				}
				if(strncmp(buf, Params[PARAM_XOVER_LOG_LONG].name, (size_t) Params[PARAM_XOVER_LOG_LONG].len) == 0) {
					Masterkill->xover_log_long = TRUE;
				}
	   
#ifdef PERL_EMBED				
				if(strncmp(buf, Params[PARAM_PERL].name, (size_t) Params[PARAM_PERL].len) == 0) {
					if(doprg == FALSE) {
						doperl = killperl_setup(Masterkill, &buf[Params[PARAM_PERL].len], debug, which);
					}
				}
#endif	
			}
			(void) fclose(fptr);
#ifndef PERL_EMBED
			if(doprg == TRUE) {
#else
			if(doprg == TRUE || Masterkill->perl_int != NULL) {
#endif		
				Masterkill->totgrps = 0;
			}
			else {
				/* SECOND PASS - call routine */
				if(Masterkill->totgrps > 0) {
					if(pass_two(Masterkill, debug, filename) == RETVAL_ERROR) {
						retval = FALSE;
					}
				}
				/* THIRD PASS - process master delete stuff */
				if(retval != FALSE && (mastergrp = parse_a_file(filename, "Master", &(Masterkill->master), debug, Masterkill->ignore_postfix, Masterkill->use_extended_regex)) == RETVAL_ERROR) {
					retval = FALSE;
				}
			}
		}
		
		/* do we have an error OR do we have empty killfiles */
		if((retval == FALSE) || (mastergrp == RETVAL_EMPTYKILL && Masterkill->totgrps == 0 && doprg == FALSE)) {
#ifdef PERL_EMBED
			if(Masterkill->perl_int == NULL) {
#endif
				free_killfile(Masterkill);	/* just in case any memory got allocated */
				Masterkill = NULL;
#ifdef PERL_EMBED
			}
#endif
		}
		else if(debug == TRUE) {
			print_debug(Masterkill, filename);
		}
	}
	
	return Masterkill;
}
/*-------------------------------------------------------------------------------------------*/
int pass_two(PKillStruct killp, int debug, const char *filename ) {

	int retval = RETVAL_OK;
	FILE *fptr;
	char buf[MAXLINLEN];
	int grpon = 0;
	size_t i;
	char *grpname, *grpfile, *delkeep;

	grpname = grpfile = delkeep = NULL;

	/* SECOND PASS - now that we know how many, we can allocate the space for em */
	/* and then have parse_a_file read em in. */
	if((killp->grps = calloc((size_t) killp->totgrps, sizeof(Group))) == NULL) {
		retval = RETVAL_ERROR;
		error_log(ERRLOG_REPORT, killf_phrases[1], NULL);
	}
	else if((fptr = fopen(full_path(killp->ignore_postfix, FP_DATADIR, filename), "r")) == NULL) {
		MyPerror(full_path(killp->ignore_postfix, FP_DATADIR, filename));
		retval = RETVAL_ERROR;
	}
	else {
		while(retval == RETVAL_OK && fgets(buf, MAXLINLEN, fptr) != NULL) {
			if(strncmp(buf, Params[PARAM_GROUP].name, (size_t) Params[PARAM_GROUP].len) == 0 ) {
				
				/* now parse the line for the 3 required elements */
				/* keep/delete group_name filename */
				delkeep = &buf[Params[PARAM_GROUP].len];
				if(strncmp(delkeep, GROUP_KEEP, (size_t) strlen(GROUP_KEEP)) == 0) {
					killp->grps[grpon].delkeep = DELKEEP_KEEP;
				}
				else if(strncmp(delkeep, GROUP_DELETE, (size_t) strlen(GROUP_DELETE)) == 0) {
					killp->grps[grpon].delkeep = DELKEEP_DELETE;
				}
				else {
					retval = RETVAL_ERROR;
				}
				if(retval == RETVAL_OK) {
					grpname = strchr(delkeep, ' ');	/* find the space */
					if(grpname == NULL) {
						retval = RETVAL_ERROR;
					}
					else {
						++grpname;	/* move past space */
						grpfile = strchr(grpname, ' ');
						if(grpfile == NULL) {
							retval = RETVAL_ERROR;
						}
						else {
							*grpfile = '\0';	/* truncate the group name for easier copying later */
							++grpfile;
						
							/* nuke newline */
							i = strlen(grpfile) - 1;
							if(grpfile[i] == '\n') {
								grpfile[i] = '\0';
							}
						}
					}
				}
				if(retval == RETVAL_ERROR) {
					error_log(ERRLOG_REPORT, killf_phrases[2], buf, NULL);
				}
				else {	/* have all three params, put them in place and parse the file */
					/* +1 for newline */
					if((killp->grps[grpon].group = malloc(strlen(grpname)+1)) == NULL) {
						error_log(ERRLOG_REPORT, killf_phrases[0], NULL);
						retval = RETVAL_ERROR;
					}
					else {
						strcpy(killp->grps[grpon].group, grpname);
						/* get, ignoring postfix, so use absolute filename on group line */
						if(parse_a_file(grpfile, grpname, &(killp->grps[grpon].match), debug,FP_GET_NOPOSTFIX,killp->use_extended_regex) != RETVAL_OK) {
							/* whoops couldn't open file, ignore */
							free(killp->grps[grpon].group);
							grpon--;  /* so that we reuse this grp entry */
							killp->totgrps--;
						}
						
					}
				}
				grpon++;	/* finished with this group */
			}
		}
		(void) fclose(fptr);
	}

	return retval;
}
/*--------------------------------------------------------------------------*/
void free_killfile(PKillStruct master) {

	int i;
		
	if(master != NULL) {
		/* first kill off killprg if its there */
		if(master->killfunc==chk_msg_kill_fork || master->child.Pid != -1) {
			killprg_closeit(master);
		}
#ifdef PERL_EMBED
		if(master->perl_int != NULL) {
			killperl_done(master);
		}
#endif
		/* close off log, if opened */
		if(master->logfp != NULL) {
			fclose(master->logfp);
		}
		free_node(master->master);
		if(master->totgrps > 0) {
			for(i=0;i<master->totgrps;i++) {
				free_node(master->grps[i].match);
				free(master->grps[i].group);
			}
			free(master->grps);
		}

		free(master);
	}
}
/*--------------------------------------------------------------------*/
void free_node(OneKill node) {

	pmy_regex curr, next;
	
		curr = node.list;

		if (node.header != NULL) {
#ifdef HAVE_REGEX_H
			if(node.header->ptrs != NULL) {
				regfree(node.header->ptrs);
			}
#endif
			if(node.header->header != NULL) {
				free(node.header->header);
			}
			if(node.header->string != NULL) {
				free(node.header->string);
			}
		}
		while(curr != NULL) {
#ifdef HAVE_REGEX_H
			if(curr->ptrs != NULL) {
				regfree(curr->ptrs);
			}
#endif
			if(curr->header != NULL) {
				free(curr->header);
			}
			if(curr->string != NULL) {
				free(curr->string);
			}
			next = curr->next;
			free(curr);
			curr = next;
		}
	
}
/*--------------------------------------------------------------------------*/
int get_one_article_kill(PMaster master, int logcount, long itemon) {

	char buf[MAXLINLEN+1], *inbuf;
	const char *tname;
	char fname[PATH_MAX+1];
	int retval, x;
	unsigned long len;
	FILE *fptr;
	PKillStruct killp;

	killp = master->killp;

	retval = RETVAL_OK;

	killp->pbody = NULL;	/* since we haven't downloaded anything yet for this article */
	killp->bodylen = 0;
	
	retval =get_chunk_mem(master, &len, CHUNK_HEADER, &inbuf);
	
	/* the killfunc pointer points to either chk_msg_kill(), chk_msg_kill_fork(), or chk_msg_kill_perl() */
	/* do we have to download this sucker, is it mandatory? */
	if(retval == RETVAL_OK && ((master->curr)->mandatory == MANDATORY_YES || (*killp->killfunc)(master, killp, inbuf, len) == FALSE)) {
		if(master->MultiFile == TRUE) {
				/* open file */
				/* file name will be ####-#### ex 001-166 (nron,total) */
			sprintf(buf,"%0*ld-%d", logcount, itemon, master->nritems);

				/* the strcpy to avoid wiping out fname in second call to full_path */
			strcpy(fname, full_path(FP_GET, FP_MSGDIR, buf));
			strcat(buf, N_TMP_EXTENSION);	/* add temp file extension */
			tname = full_path(FP_GET, FP_TMPDIR, buf);	/* temp file name */
			if(master->debug == TRUE) {
				do_debug("File name = \"%s\" temp = \"%s\"", fname, tname);
			}
			if((fptr = fopen(tname, "w")) == NULL) {
				MyPerror(tname);
				retval = RETVAL_ERROR;
			}
			else {
				/* write the header */
				x = fwrite(inbuf,sizeof(inbuf[0]),len, fptr);
				fputs("\n", fptr);	/* needed */
				if(x != len) {
					retval = RETVAL_ERROR;
					error_log(ERRLOG_REPORT, killf_phrases[9], NULL);
				}
				if(retval == RETVAL_OK && master->header_only == FALSE) {
					/* have we already downloaded the body? */
					if(killp->pbody != NULL) {
						inbuf = killp->pbody;
						len = killp->bodylen;
					}
					else {
						retval = get_chunk_mem(master, &len, CHUNK_BODY, &inbuf);
					}
					if(retval == RETVAL_OK) {
						x = fwrite(inbuf, sizeof(inbuf[0]), len, fptr);
						if(x != len) {
							retval = RETVAL_ERROR;
							error_log(ERRLOG_REPORT, killf_phrases[9], NULL);
						}
					}
				}
				(void) fclose(fptr);
				if(retval != RETVAL_OK) {
					unlink(tname);
				}
				/* now rename it to the permanent file name */
				else {
					move_file(tname, fname);
					if((master->batch == BATCH_LIHAVE || master->batch == BATCH_INNFEED) && master->innfeed != NULL) {
						/* write path name and msgid to file */
						fprintf(master->innfeed, "%s %s\n", fname, (master->curr)->msgnr);
						fflush(master->innfeed); /* so it gets written sooner */
					}
				}
			}
		}
		else {
			fputs(inbuf, stdout);
			fputs("\n", stdout);
			if(master->header_only == FALSE) {
				retval = get_chunk_mem(master, &len, CHUNK_BODY, &inbuf);
				if(retval == RETVAL_OK) {
					fwrite(inbuf, sizeof(inbuf[0]), len, stdout);
				}
			}
                        /* this is needed as a separator in stdout version */
			fputs(".\n", stdout);
		}
		if(retval == RETVAL_OK) {
			master->nrgot++;
		}
	}
	if(retval == RETVAL_UNEXPECTEDANS) {
		retval = RETVAL_OK; /* so don't abort */
	}
	
	return retval;
}
/*---------------------------------------------------------------*/
/* this routine gets the header or body into memory, keeping separate buffers for each */
int get_chunk_mem(PMaster master, unsigned long *size, int which, char **retbuf) {

	
	static char *header_buf = NULL;
	static char *body_buf = NULL;
	static int header_size = 8192;
	static unsigned long body_size = KILL_BODY_BUF_SIZE;
	
	int done, partial, len, i, retval;
	char *inbuf, *newbuf, *buf;
	const char *cmd;
	unsigned long temp, bufsize, currbuf; /* bufsize = alloced memory, currbuf = what retrieved so far */
	
	
	done = FALSE;
	partial = FALSE;
	currbuf = 0;
	retval = RETVAL_OK;

	if(which == CHUNK_HEADER) {
		buf = header_buf;
		bufsize = header_size;
	}
	else {
		buf = body_buf;
		bufsize = body_size;
	}

	if(buf == NULL) {
		if((buf=malloc((size_t) bufsize)) == NULL) {
			error_log(ERRLOG_REPORT, killf_phrases[0], NULL);
			retval = RETVAL_ERROR;
		}
	}
	
	if(buf != NULL) {
		/* build command */
		if(which == CHUNK_HEADER) {
			cmd = build_command(master, "head", master->curr);
			i = 221;
		}
		else {
			cmd = build_command(master, "body", master->curr);
			i = 222;
		}	
		if((retval = send_command(master, cmd, NULL, i)) != RETVAL_OK) {
			free(buf);
			buf = NULL;
		}
	}
	while(buf != NULL && done == FALSE) {
		len=sgetline(master->sockfd, &inbuf, master->do_ssl, master->ssl_struct);
		(void) TimerFunc(TIMER_ADDBYTES, len, NULL);
		if(len < 0) {
			free(buf);
			buf = NULL;
			done = TRUE;
			retval = RETVAL_ERROR;
		}
		else if(partial == FALSE && inbuf[0] == '.') {
			if(len == 2 && inbuf[1] == '\n') {
				done = TRUE;
			}
			else {
				/* handle double dots IAW RFC977 2.4.1*/
				inbuf++;	/* move past first dot */
				len--;
			}
		}
		if(done == FALSE) {
			while((len+currbuf) > bufsize && buf != NULL) {
				/* buffer not big enough realloc */
				/* how much do we increase buf */
				/* we do this test so in case KILL_CHUNK_BUF_INCREASE < len, we */
				/* don't get a buffer overflow */
				temp = (len > KILL_CHUNK_BUF_INCREASE) ? len : KILL_CHUNK_BUF_INCREASE;
				if(master->debug == TRUE) {
					do_debug("Re-allocing buffer from %lu to %lu\n", bufsize, bufsize+temp);
				}
				bufsize += temp;
				if((newbuf = realloc(buf, (size_t) bufsize)) == NULL) {
					free(buf);
					buf = NULL;
					currbuf = 0;
					error_log(ERRLOG_REPORT, killf_phrases[0], NULL);
					retval = RETVAL_ERROR;
				}
				else {
					buf = newbuf;
				}
			}	
			if(buf != NULL) {
				/* put string in buffer, use memmove in case of nulls*/
				memmove(buf+currbuf, inbuf, len);
				currbuf += len;
				partial= (len==MAXLINLEN&&inbuf[len-1]!='\n') ? TRUE : FALSE;
			}
		}
	}
	/* now save the values for next time */
	if(which == CHUNK_HEADER) {
		header_buf = buf;
		header_size = bufsize;
	}
	else {
		body_buf = buf;
		body_size = bufsize;
	}
	/* return the length */
	*size = currbuf;

	/* make sure buf is NULL terminated */
	/* this is needed so logging is correct */
	/* since using memmove() above, we don't copy the ending NULL */
	if(buf != NULL) {
		buf[currbuf] = '\0';
	}
	
	*retbuf = buf;
	return retval;
}
/*-------------------------------------------------------------------------*/
/* chk_msg_kill - return TRUE if kill article, FALSE if keep               */
/* if kill article, add it to killlog 					   */
int chk_msg_kill(PMaster master, PKillStruct killp, char *headerbuf, int buflen) {

	int killyn, i, del, keep, match, masterkill;
	const char *group = "Master";
	char *why, *goodwhy;

	goodwhy = why = killf_reasons[REASON_NONE];
	killyn = FALSE;
	
	/* first check against master delete */
	masterkill = check_a_group(master, &(killp->master), headerbuf, &why);
	if(masterkill == TRUE && killp->grp_override == FALSE) {
		killyn = masterkill;
	}
	else {
		/* okay now have to parse group line */
		/* then check to see if I have group keep/deletes for each group */
		/* default actions */
 		keep = FALSE;
		del = FALSE;
		for(i=0;i<killp->totgrps;i++) {
			if(check_newsgroups(headerbuf, killp->grps[i].group) == TRUE) {
				/* bingo this article matches one of our group check it */
				match = check_a_group(master, &(killp->grps[i].match), headerbuf, &why);
				if(killp->grps[i].delkeep == DELKEEP_KEEP) {
					/* matched keep group */
					if(match == TRUE) {
						keep = TRUE;
					}
					else {
						del = TRUE;
						group = killp->grps[i].group;
						goodwhy = killf_reasons[REASON_NOKEEP];
					}
				}
				else {
					if(match == TRUE) {
						del = TRUE;
						goodwhy = why;
						group = killp->grps[i].group;
					}
					else {
						keep = TRUE;
					}
				}
			}
		}
		/* now determine if we kill or keep this sucker */
		if(keep == FALSE && del == FALSE) {
			/* no group matches, do what masterkill says to do */
			killyn = masterkill;
		}
		else if(keep != del) {
			/* only matched one group, figure out which */
			killyn = ( del == TRUE) ? TRUE : FALSE;
			why = goodwhy;
		}
		else {
			/* matched both, use TIEBREAKER */
			why = killf_reasons[REASON_TIE];
			killyn = killp->tie_delete;
		}
	}
	if(master->debug == TRUE && killyn == TRUE) {
		do_debug("killing: %s: %s: %s", group, why, headerbuf);
	}
	if(killyn == TRUE && killp->logyn != KILL_LOG_NONE) {
		/* log it */
		/* only open this sucker once */
		if(killp->logfp == NULL) {
			if((killp->logfp = fopen(full_path(FP_GET, FP_TMPDIR, master->kill_log_name), "a")) == NULL) {
				MyPerror(killf_phrases[3]);
			}
		}
		if(killp->logfp != NULL) {
			/* first print our one-line reason */
			print_phrases(killp->logfp,killf_phrases[4], group ,why, (master->curr)->msgnr, NULL);
		     
			if(killp->logyn == KILL_LOG_LONG) {
				/* print the header as well */
				/* the nl so I get a blank line between em */
				print_phrases(killp->logfp, "%v1%\n", headerbuf, NULL);
			}

		}
	}
	return killyn;			
}
/*-----------------------------------------------------------------------*/
int check_a_group(PMaster master, POneKill killp, char *headerbuf, char **why) {

	int i, match = FALSE;
	char *startline, *tptr;
	pmy_regex curr;
	static char reason[MAXLINLEN];
	PKillStruct temp;
	
	/* check hilines first */
	if(killp->hilines > 0) { 
		if((startline = strstr(headerbuf, Params[PARAM_HILINE].header)) != NULL)  {
			i = 0;	/* just in case */
			sscanf(startline+Params[PARAM_HILINE].headerlen, "%d", &i);
			if(killp->hilines < i) {
				/* JACKPOT */
				match = TRUE;
				*why = killf_reasons[REASON_TOOMANYLINES];
			}
		}
		
	}
	/* now check low lines */
	if(match == FALSE && killp->lowlines > 0) { 
		if((startline = strstr(headerbuf, Params[PARAM_LOWLINE].header)) != NULL)  {
			i = 0;	/* just in case */
			sscanf(startline+Params[PARAM_LOWLINE].headerlen, "%d", &i);
			if(i < killp->lowlines) {
				/* JACKPOT */
				match = TRUE;
				*why = killf_reasons[REASON_NOTENUFLINES];
			}
		}
		
	}
	/* now check nrgrps */
	if(match == FALSE && killp->maxgrps > 0) {
		if((startline = strstr(headerbuf, Params[PARAM_NRGRPS].header)) != NULL) {
			/* count the nr of commas in the group line */
			i = 1;	/* have at least one group */
			tptr = startline;
			while(i <= killp->maxgrps && *tptr != '\n' && *tptr != '\0' ) {
				/* some news server use space vice comma for separator */
				if(*tptr == COMMA || *tptr == SPACE ) {
					i++;
				}
				tptr++;
			}
			if(i > killp->maxgrps) {
				match = TRUE;
				*why = killf_reasons[REASON_NRGRPS];
			}		
		}
	}
	/* now check nrxref */
	if(match == FALSE && killp->maxxref > 0) {
		if((startline = strstr(headerbuf, Params[PARAM_NRXREF].header)) != NULL) {
			/* count the nr of colons in  line, they separate the group/article nr */
			i = 0;
			tptr = startline;
			while(i <= killp->maxxref && *tptr != '\n' && *tptr != '\0' ) {
				if(*tptr == COLON) {
					i++;
				}
				tptr++;
			}
			if(i > killp->maxxref) {
				match = TRUE;
				*why = killf_reasons[REASON_NRXREF];
			}		
		}
	}
	curr = killp->list;
	
	while(match == FALSE && curr != NULL) {
		if(regex_check(headerbuf, curr, master->debug) == TRUE) {
			match = TRUE;
			sprintf(reason, "%s %s %s ", killf_reasons[REASON_HEADER], curr->header, curr->string);
			*why = reason;
		}
		curr = curr->next;
	}
	curr = killp->header;
	while(match == FALSE && curr != NULL) {
		/* scan the entire header */
		if(regex_block(headerbuf, curr, master->debug) == TRUE) {
			match  = TRUE;
			sprintf(reason,"%s %s", killf_reasons[REASON_HEADER], curr->string);
			*why = reason;
		}
		curr = curr->next;
	}
	if(match == FALSE && (killp->bodybig > 0 || killp->bodysmall > 0 || killp->body != NULL)) {
		/* may have to download the header first to */
		/* have a pointer to the master killstruct */
		temp = master->killp;
		if(temp->pbody == NULL) {
			/* have to get the body first */
			get_chunk_mem(master, &(temp->bodylen), CHUNK_BODY, &(temp->pbody));	
		}
		if(killp->bodybig > 0 && temp->bodylen > killp->bodybig) {
			match = TRUE;
			sprintf(reason, "%s %lu", killf_reasons[REASON_BODYBIG], temp->bodylen);
			*why = reason;
		}
		else if(killp->bodysmall > 0 && temp->bodylen < killp->bodysmall) {
			match = TRUE;
			sprintf(reason, "%s %lu", killf_reasons[REASON_BODYSMALL], temp->bodylen);
			*why = reason;
		}
		else {
			curr = killp->body;
			while(match == FALSE && curr != NULL) {  /* scan the body */
				if(regex_block(temp->pbody, curr, master->debug) == TRUE) {
					match = TRUE;
					sprintf(reason, "%s %s", killf_reasons[REASON_BODY], curr->string);
					*why = reason;
				}
				curr = curr->next;
			}
		}
	}
	
	return match;
}
/*--------------------------------------------------------------------------*/
int check_newsgroups(char *header, const char *whichgroup) {
	/* search Newsgroup headerline for whichgroup  */
	/* if in return TRUE else return FALSE; */

	int match = FALSE;
	char *startline, *ptr;
	const char *ptr_group;
	
	if((startline = strstr(header, NEWSGROUP_HEADER)) != NULL) {
		ptr = startline + strlen(NEWSGROUP_HEADER);
		while( match == FALSE && *ptr != '\0' && *ptr != '\n' ) {
			ptr_group = whichgroup;
			while( *ptr == *ptr_group && *ptr != '\0') {
				ptr++;
				ptr_group++;
			}
			if(*ptr_group == '*') {
				/* wildcard match, they match so far, so they match */
				match = TRUE;
			}
			else if((*ptr_group == '\0') && (*ptr == COMMA || *ptr == ' ' || *ptr == '\n' || *ptr == '\0')) {
				/* if we are at the end of both group names then we have a match */
				/* we check for space even though its not in the standard, apparently */
				/* some news server use it. */
				match = TRUE;
			}
			else {
				/* advance to next group on line */
				while(*ptr != COMMA && *ptr != '\n' && *ptr != '\0') {
					ptr++;
				}
				if(*ptr == COMMA) {
					/* advance past it */
					ptr++;
				}
			}
		}
	}
	return match;
}
/*-------------------------------------------------------------------------------*/
void initialize_one_kill(POneKill mykill) {
	/* initialize the various elements*/	
	mykill->list = NULL;
	mykill->header = NULL;
	mykill->body = NULL;
	mykill->hilines = mykill->lowlines = mykill->maxgrps = mykill->maxxref = 0;
	mykill->bodybig = mykill->bodysmall = 0;
	mykill->quote = KILLFILE_QUOTE;
	mykill->non_regex = KILLFILE_NONREGEX;

	return;
}

/*-------------------------------------------------------------------------------*/
int parse_a_file(const char *fname, const char *group, POneKill mykill, int debug, int ignore_prefix, int use_e_regex) {

	FILE *fptr;
	char buf[MAXLINLEN+1];
	int i, match;
	int retval = RETVAL_OK;
	pmy_regex curr;

	/* first initialize the killstruct */
	initialize_one_kill(mykill);
#ifdef HAVE_REGEX_H
	mykill->use_extended = use_e_regex;
#endif
	/* now read in the killfile and parse it */
	if((fptr = fopen(full_path(ignore_prefix, FP_DATADIR, fname), "r")) == NULL) {
		MyPerror(full_path(ignore_prefix, FP_DATADIR, fname));
		retval = RETVAL_ERROR;
	}
	else {
		if(debug == TRUE) {
			do_debug("Pass 3 kill file: %s\n", full_path(ignore_prefix, FP_DATADIR, fname));
			do_debug("Pass 3 options: group=%s, use_extended_regex=%s, ignore_prefix=%s\n",
				 group, true_str(use_e_regex), true_str(ignore_prefix));	 
		}
		while(fgets(buf, MAXLINLEN, fptr) != NULL) {
			buf[MAXLINLEN] = '\0';	/* just in case */
			
			i = strlen(buf);
			/* strip nls off so they don't get added to regex scan, etc..*/
			if(buf[i-1] == '\n') {
				buf[i-1] = '\0';
			}
			if(debug == TRUE) {
				do_debug("pass 3 kill file line: %s\n", buf);
			}
			if(buf[0] != KILLFILE_COMMENT_CHAR) {
				/* skip any comment lines */
				match = FALSE;
				for(i = 0 ; i < NR_PARAMS; i++) {
					if(strncmp(buf, Params[i].name, (size_t) Params[i].len) == 0) {
						match = TRUE; /* so don't try header field scan later */
						switch(i) {
						case PARAM_HILINE:
							(void) sscanf(&buf[Params[PARAM_HILINE].len], "%d", &(mykill->hilines));
							break;
						case PARAM_LOWLINE:
							(void) sscanf(&buf[Params[PARAM_LOWLINE].len], "%d", &(mykill->lowlines));
							break;
						case PARAM_NRGRPS:
							(void) sscanf(&buf[Params[PARAM_NRGRPS].len], "%d", &(mykill->maxgrps));
							break;
						case PARAM_NRXREF:
							(void) sscanf(&buf[Params[PARAM_NRXREF].len], "%d", &(mykill->maxxref));
							break;
						case PARAM_QUOTE:
							if(buf[Params[PARAM_QUOTE].len] == '\0') {
								error_log(ERRLOG_REPORT, "%s\n", killf_phrases[6], NULL);
							}
							else {
								mykill->quote = buf[Params[PARAM_QUOTE].len];
							}
							break;
						case PARAM_NONREGEX:
							if(buf[Params[PARAM_NONREGEX].len] == '\0') {
								error_log(ERRLOG_REPORT, "%s\n", killf_phrases[12], NULL);
							}
							else {
								mykill->non_regex = buf[Params[PARAM_NONREGEX].len];
							}
							break;
						case PARAM_HDRSCAN:
							if(buf[Params[PARAM_HDRSCAN].len] == '\0') {
								error_log(ERRLOG_REPORT, "%s\n", killf_phrases[7], NULL);
							}
							else {
								if((curr = regex_scan(buf, mykill->quote, debug, use_e_regex, mykill->non_regex)) != NULL) {
									add_to_linkedlist(&(mykill->header), curr);
								}
							}
							break;
						case PARAM_BODY:
							if(buf[Params[PARAM_BODY].len] == '\0') {
								error_log(ERRLOG_REPORT, "%s\n", killf_phrases[10], NULL);
							}
							else {
								if((curr = regex_scan(buf, mykill->quote, debug, use_e_regex, mykill->non_regex)) != NULL) {
									add_to_linkedlist(&(mykill->body), curr);
								}
							}
							break;
						case PARAM_BODYBIG:
							(void) sscanf(&buf[Params[PARAM_BODYBIG].len], "%lu", &(mykill->bodybig));
							break;
						case PARAM_BODYSMALL:
							(void) sscanf(&buf[Params[PARAM_BODYSMALL].len], "%lu", &(mykill->bodysmall));
							break;
						case PARAM_E_REGEX:
							/* in case specified for this file only */
							use_e_regex = TRUE;
#ifdef HAVE_REGEX_H
							mykill->use_extended = TRUE; /*  save for printing in debug */
#endif
							break;
						default:		/* handle all other lines aka do nothing*/
							break;
					  
						}
					}
				}
				if(match == FALSE) {
				/* it's a different line in the header add to linked-list */
					if((curr = regex_scan(buf, mykill->quote, debug, use_e_regex, mykill->non_regex)) != NULL) {
						add_to_linkedlist(&(mykill->list), curr);
					}
				}
			} /* comment line stuff */
		}
		(void) fclose(fptr);
		/* check for an empty killfile */
		if(mykill->hilines == 0 && mykill->lowlines == 0 && mykill->maxgrps == 0 && mykill->bodybig == 0 && mykill->bodysmall == 0 && mykill->header == NULL && mykill->list == NULL && mykill->body == NULL && mykill->maxxref == 0) {
			retval = RETVAL_EMPTYKILL;
		}
	}
	return retval;
}
/*---------------------------------------------------------------------------------------------*/
void add_to_linkedlist(pmy_regex *head_ptr, pmy_regex addon) {

	/* this routine adds a pointer the end of a linked list */
	pmy_regex curr;
	if(*head_ptr == NULL) {
		/* put on top of list */
		*head_ptr = addon;
	}
	else {
		curr = *head_ptr;
		while(curr->next != NULL) {
			curr = curr->next;
		}
		curr->next = addon;
	}
}
/*----------------------------------------------------------------------------------*/
/* scan a line and build the compiled regex pointers to call regexex() with */
/* ---------------------------------------------------------------------------------*/
pmy_regex regex_scan(char *linein, char quotechar, int debug, int use_e_regex, char non_regex) {

	char *tptr, *startline;
	int len,x,y, case_sens = FALSE, flags = FALSE;
#ifdef HAVE_REGEX_H
	char errmsg[256];
	int i,j, useregex = TRUE;
#endif

	pmy_regex which = NULL;
	int err = FALSE;
	
	if(linein != NULL) {
		/* first find the : in the line to skip the parameter part */
		tptr = linein;
		while(*tptr != '\0' && *tptr != ':') {
			tptr++;
		}
		startline = tptr+1;
		len = startline - linein;

		/* do we treat this string as a non-regex?? */
		if(*startline == non_regex) {
			startline++;
#ifdef HAVE_REGEX_H
			useregex = FALSE;
#endif		
		}
		/* case sensitive search?? */
		if(*startline == quotechar) {
			startline++;
			case_sens = TRUE;
		}
#ifdef HAVE_REGEX_H
		if(useregex == TRUE) {
			flags = REG_NOSUB;
			if(case_sens == FALSE) {
				flags |= REG_ICASE;
			}
			if(use_e_regex == TRUE) {
				flags |= REG_EXTENDED;
			}
			useregex  = FALSE;
			/* test to see whether or not we need to use regex */
			for(i = 0; i < strlen(regex_chars) && useregex != TRUE; i++) {
				for(j = 0 ; j < strlen(startline)&& useregex != TRUE; j++) {
					if(startline[j] == regex_chars[i]) {
						useregex = TRUE;
					}
				}
			}
		}
	
#endif
		/* now count each of em, so that can alloc my array of regex_t */
		if(*tptr != '\0') {
			/* now alloc memory */

			if((which = malloc(sizeof (my_regex))) == NULL) {
				error_log(ERRLOG_REPORT, killf_phrases[5], NULL);
			}
			else {
				/* first save the string for later printing, if necessary */
				if((which->string = malloc(strlen(startline)+1)) == NULL) {
					err = TRUE;
					error_log(ERRLOG_REPORT, killf_phrases[5], NULL);
				}
				else {					
					strcpy(which->string,startline);
					which->next = NULL;	/* initialize */
					which->case_sensitive = case_sens;
#ifdef HAVE_REGEX_H
					which->ptrs = NULL;	/* just to be on safe side */
#endif
					if((which->header=calloc(sizeof(char), len+1)) == NULL) {
						error_log(ERRLOG_REPORT, killf_phrases[5], NULL);
						err = TRUE;
					}
					else {
						strncpy(which->header, linein, len);
						which->header[len] = '\0';	/* just in case */
#ifdef HAVE_REGEX_H						
						if(useregex == TRUE) {
							if((which->ptrs = malloc(sizeof(regex_t))) == NULL) {
								error_log(ERRLOG_REPORT, killf_phrases[5], NULL);
							}
							else {
								if(debug == TRUE) {
									do_debug("Regcomping -%s-\n", startline);
								}
								if((err = regcomp(which->ptrs, startline, flags)) != 0) {
									/* whoops */
									regerror(err, which->ptrs, errmsg, sizeof(errmsg));
									error_log(ERRLOG_REPORT, killf_phrases[11], startline, errmsg, NULL);
									err= TRUE;
								}
							}
						}
						else {
#endif
							/* not using regex, fill in skiparray */
							/* first fill in max skip for all chars (len of string) */
							/* See Algorithms/Sedgewick pg 287-289 for explanation */
							y=strlen(which->string);
	
							for(x=0;x<sizeof(which->skiparray);x++) {
								which->skiparray[x] = y;
							}
							/* now go back and fill in those in our search string */
							if(case_sens == FALSE) {
								/* case-insensitive search */
								for(x=0;x<y;x++) {
									/* have to match both upper and lower case */
									char xx;
									xx = which->string[x];
									which->skiparray[(unsigned char) toupper(xx)] = (y-1)-x;
									which->skiparray[(unsigned char) tolower(xx)] = (y-1)-x;
								}
							}
							else {
								for(x=1;x<y;x++) {
									which->skiparray[(unsigned char) which->string[x]] = (y-1)-x;
								}
							}
							
							
#ifdef HAVE_REGEX_H
						}
#endif
					}
				}
			}
			
		}			
	}
	if(which != NULL && err == TRUE) {
		/* error, free up everything */
#ifdef HAVE_REGEX_H
		if(which->ptrs != NULL) {
			free(which->ptrs);
		}
#endif	
		if(which->string != NULL) {
			free(which->string);
		}
		if(which->header != NULL) {
			free(which->header);
		}
		if(which != NULL) {
			free(which);
			which = NULL;
		}	
	}
	
	return which;
}
/*------------------------------------------------------------------------------------*/
int regex_check(char *headerbuf, pmy_regex expr, int debug) {
	int match = FALSE;
	char *startline, *endline;
	
	if(expr->header != NULL) {
		if((startline = strstr(headerbuf, expr->header)) != NULL) {
			endline = strchr(startline, '\n');		
			/* end this line, so we only search the right header line */
			if(endline != NULL) {
				*endline = '\0';
			}
			/* the +strlen(expr->header) so the header field and the space */
			/* immediately following it aren't included in the search */
			if(debug == TRUE) {
				do_debug("checking -%s- for -%s-\n", startline+strlen(expr->header)+1,
					 expr->string);
			}
#ifdef HAVE_REGEX_H
			if(expr->ptrs != NULL) {
	
				
				if(regexec(expr->ptrs, startline+strlen(expr->header)+1, 0, NULL, 0) == 0) {
					match = TRUE;
				}
			}
			else {
#endif
				match = find_string(debug, expr, startline+strlen(expr->header)+1);
#ifdef HAVE_REGEX_H		
			}
#endif
			/* put the nl back on that we deleted */
			if(endline != NULL) {
				*endline = '\n';
			}
		}	
	}
	if(debug== TRUE) {
		do_debug("Returning match=%s\n", (match == TRUE) ? "True" : "False");
	}
	
	return match;
}
/*------------------------------------------------------------------------------------*/
int regex_block(char *what, pmy_regex where, int debug) {

	/* find a string in a block */
	int match = FALSE;
	
	if(debug == TRUE && where->string != NULL) {
		do_debug("Checking for -%s-\n", where->string);
	}
#ifdef HAVE_REGEX_H
	if(where->ptrs != NULL) {
		if(regexec(where->ptrs, what, 0, NULL, 0) == 0) {
			match = TRUE;
		}
	}
	else {
#endif
		match = find_string(debug, where, what);
#ifdef HAVE_REGEX_H
	}
#endif
	return match;	
}
/*---------------------------------------------------------------------------------------------*/
int find_string(int debug, pmy_regex expr, char *str) {

	/* the search mechanism is based on the Boyer-Moor Algorithms (pg 286- of Algorithms book) */
	int match = FALSE;	
	char *ptr;		/* pointer to pattern */
	int lenp;		/* length of pattern */
	int lens;		/* length of string we are searching  */
	int i;			/* current location in string */
	int j;			/* current location in pattern */
	int t;			/* value of skip array for string location */

	ptr = expr->string;
	lenp = strlen(ptr);
	lens = strlen(str);
	

	if(expr->case_sensitive == TRUE ) {
		/* do case-sensitive search */
		if(debug == TRUE) {
			do_debug("Case-sensitive search for %s\n", ptr);
		}
		i = j = lenp-1;
		do {
			if(str[i] == ptr[j]) {
				i--; 
				j--;
			}
			else {
				t = expr->skiparray[(unsigned char) str[i]];
				i += ( lenp -j > t) ? (lenp - j) : t;
				j = lenp - 1;
			}
		} while( j >= 0 && i < lens);
		if(j < 0) {
			match = TRUE;
		}

	}
	else {
		/* do case-insensitive search */
		if(debug == TRUE ) {
			do_debug("Case-insensitive search for --%s--\n", ptr);
		}
		i = j = lenp-1;
		do {
			if(tolower(str[i]) == tolower(ptr[j])) {
				i--; 
				j--;
			}
			else {
				t = expr->skiparray[(unsigned char) str[i]];
				i += ( lenp -j > t) ? (lenp - j) : t;
				j = lenp - 1;
			}
		} while( j >= 0 && i < lens);
		if(j < 0) {
			match = TRUE;
		}
	}
	return match;
	
}
/*------------------------------------------------------------------------------------*/
void print_debug(PKillStruct master, const char *fname) {
	int i;
	
	/* print out status of master killstruct to debug file */
	do_debug("--%s-- Master KillStruct\n",fname);
	do_debug("logyn=%s\n",true_str(master->logyn));
	do_debug("grp_override=%s\n",true_str(master->grp_override));
 	do_debug("tie_delete=%s\n",true_str(master->tie_delete));
	do_debug("totgrps=%d\n", master->totgrps);
	do_debug("ignore_postfix=%s\n",true_str(master->ignore_postfix));
	do_debug("use_extended_regex=%s\n", true_str(master->use_extended_regex));
	do_debug("xover_long_long=%s\n", true_str(master->xover_log_long));
	
	do_debug("Master kill group");
	debug_one_kill(&(master->master));
	
	for(i = 0 ; i < master->totgrps ; i++ ) {	
		do_debug("Group %d = %s\n", i, master->grps[i].group);
		do_debug("--delkeep =%s\n", (master->grps[i].delkeep == DELKEEP_KEEP) ? "keep" : "delete");
		debug_one_kill(&(master->grps[i].match));
	
	}
	do_debug("--%s-- End of Killstruct\n", fname);
}
/*------------------------------------------------------------------------*/
void debug_one_kill(POneKill ptr) {	
	pmy_regex temp;
	
	do_debug("--hilines =%d\n", ptr->hilines);
	do_debug("--lowlines=%d\n", ptr->lowlines);
	do_debug("--maxgrps=%d\n", ptr->maxgrps);
	do_debug("--maxxref=%d\n", ptr->maxxref);
	do_debug("--bodysize>%lu\n", ptr->bodybig);
	do_debug("--bodysize<%lu\n", ptr->bodysmall);
	do_debug("--quote=%c\n", ptr->quote);
	do_debug("--non_regex=%c\n", ptr->non_regex);
#ifdef HAVE_REGEX_H
	do_debug("--use_extended_regex=%s\n", true_str(ptr->use_extended));
#endif
	temp= ptr->header;
	while(temp != NULL) {
#ifdef HAVE_REGEX_H
		do_debug("--header scan is %sregex\n", (temp->ptrs == NULL) ? "non-" : "");
#endif
		do_debug("--Scan is %scase-sensitive\n", (temp->case_sensitive == TRUE) ? "" : "non-");
		if(temp->string != NULL) {
			do_debug("--header scan=%s\n", temp->string);
		}
		temp = temp->next;
	}
	temp = ptr->body;
	while(temp != NULL) {
#ifdef HAVE_REGEX_H
		do_debug("--body scan is %sregex\n", (temp->ptrs == NULL) ? "non-" : "");
#endif
		do_debug("--Scan is %scase-sensitive\n", (temp->case_sensitive == TRUE) ? "" : "non-");
		if(temp->string != NULL) {
			do_debug("--body  scan=%s\n", temp->string);
		}
		temp = temp->next;
	}
	temp = ptr->list;
	while(temp != NULL) {
#ifdef HAVE_REGEX_H
		do_debug("--header match is %sregex\n", (temp->ptrs == NULL) ? "non-" : "");
#endif
		do_debug("--Scan is %scase-sensitive\n", (temp->case_sensitive == TRUE) ? "" : "non-");
		do_debug("--header match= %s %s\n", temp->header, temp->string);
		temp = temp->next;
	}
	do_debug("--end of Killfile Group--\n");
}



syntax highlighted by Code2HTML, v. 0.9.1