/* XXXXXXXX  memory leaks at most all the cmerr's in here */
/*
 * Copyright (c) 1986, 1987, 1988 by The Trustees of Columbia University
 * in the City of New York.  Permission is granted to any individual or
 * institution to use, copy, or redistribute this software so long as it
 * is not sold for profit, provided this copyright notice is retained.
 */

#ifndef lint
static char *rcsid = "$Header: file.c,v 1.62 88/07/22 20:03:06 howie Exp $";
#endif

/*
 * file.c - routines for handling mail files
 */

#include "mm.h"
#include "parse.h"
#include "cmds.h"
#include "rd.h"

msgvec *cf = nil;			/* current mail file */

msgvec *getfile ();

/*
 * check_cf:
 * see if we have a mail file we're looking at
 * if they want to modify it, see if they can and give warning if not
 * if we're processing command line arguments, they must have done one
 * that requires their mail_file, so get it if we have no file
 */
check_cf (modify)
{
    extern int gotargs;

    if (cf == nil) {
	if (!gotargs)
	    cmerr ("No current mail file");
	if (strlen(mail_file) != 0) {	/* want a file, get one */
	    printf("Reading %s\n", mail_file);
	    cf = getfile(mail_file, modify ? CMD_GET : CMD_EXAMINE);
	    if (cf == NULL)
		cmerr ("No current mail file");
	    do_flagged();		/* show flagged ones */
	} /* continue with other checks */
    }
    if (!modify || !(cf->flags & MF_RDONLY)) /* don't want to change, or can */
	return(true);
    if (modify_read_only == SET_ALWAYS)
	return (true);
    if (modify_read_only == SET_ASK)
	if (yesno("File is read-only, modify anyway? ", "no"))
	    return (true);
    cmerr ("Cannot modify a file visited with the \"examine\" command");
}


/*
 * cmd_get:
 * get (or "examine") a file, releasing the old one if we get the new
 * one successfully.   
 */
int
cmd_get (n)
int n;
{
    msgvec *nf, *f;
    char *fname, *parse_output_file(), *parse_input_file();
    extern int gotargs;			/* are we processing command line? */
    int i;

    gotargs = FALSE;			/* don't exit after get in cmd line */
    noise ("mail file");
    if (n == CMD_GET)
	fname = parse_output_file ("mail file", mail_file, true);
    else
	fname = parse_input_file ("mail file", mail_file, true);
    confirm ();				/* XXX leak */

    if (cf)   /* save it before reading new file in case it's the same file */
	if (!update (&cf,UPD_SAVEMOD) && (cf->flags & MF_WRITERR)) {
	    fprintf (stderr,
		     "?Cannot save old file -- %s command aborted.\n",
		     n == CMD_EXAMINE ? "EXAMINE" : "GET");
	    free (fname);
	    return (false);
	}

    nf = getfile(fname, n);
    free (fname);
    if (nf == nil)
	return false;

    if (cf) {				/* get rid of old current file */
	f = cf;			/* remember this another minute... */
	cf = nil;			/* ...but don't update it anymore */
	(*msg_ops[f->type].close)(f->filep); /* close old file */
	msgvec_free (f);		/* free all the bits and pieces */
    }

    cf = nf;				/* make new file current */

    do_flagged();

    return true;
}

/*
 * do_flagged:
 * display flagged messages of new file
 */
do_flagged()
{
    int i,count;
    extern int display_length;
    extern FILE *header_pipe;
    FILE *more_pipe_open();

    if (!display_flagged_messages) 
	return;
    /* first find out how many */
    for (count = 0, i = 1; i <= cf->count; i++) {
	if (cf->msgs[i].flags & M_FLAGGED)
	    ++count;
    }

    if (count >= display_length)
	header_pipe = more_pipe_open(cmcsb._cmoj);
    else
	header_pipe = cmcsb._cmoj;
    if (header_pipe == nil)
	header_pipe == stdout;
    header_print(0);
    for (i = 1; i <= cf->count; i++) {
	if (cf->msgs[i].flags & M_FLAGGED)
	    header_print(i);		/* uses header_pipe */
    }
    header_print(-1);
    if (header_pipe == cmcsb._cmoj){
	if (cmcsb._cmoj)
	    fflush(cmcsb._cmoj);	/* didn't open the pipe */
    }
    else if (header_pipe != stdout)
	more_pipe_close(header_pipe);
    header_pipe = NULL;
}

/*
 * getfile:
 * open the file, initialize the msgvec structure
 */
msgvec *
getfile (file, flag)
char *file;
int flag;
{
    msgvec *nf;
    int type;
    extern int num_msg_ops;		/* how many mail formats we know */
    int probeval;
    int err;
    char **q;

    if (strcmp(file, mail_file) == 0)
	for (q = &(incoming_mail[0]); q && *q; q++)
	    if (strcmp (file, *q) == 0) {
		cmerr("Cannot read a primary mail file that is also an incoming mail file.");
	    }

    switch (probeval = mail_probe (file, &type)) /* can we read this? */
    {
    case PR_NAME:
	cmerr ("Badly formed filename: %s", file);
    case PR_NOEX:
	if (flag == CMD_EXAMINE)
	    cmerr ("File not found: %s", file);
	if (!yesno("File does not exist.  Do you want to create it? ", "yes"))
	    cmerjnp();
	for (type = 0; type < num_msg_ops; type++)
	    if (strcmp(default_mail_type.current,msg_ops[type].typename) == 0)
		break;			/* use their default, if okay */
	if (type == num_msg_ops)	/* bad typename */
	    type = TYPE_MTXT;		/* pick some type */
	break;
    case PR_PERM:
	cmerr ("Cannot read file: %s", file);
    case PR_EMPTY:
	for (type = 0; type < num_msg_ops; type++)
	    if (strcmp(default_mail_type.current,msg_ops[type].typename) == 0)
		break;			/* use their default, if okay */
	if (type == num_msg_ops)	/* bad typename */
	    type = TYPE_MTXT;		/* pick some type */
	break;
    case PR_NOTOK:
	cmerr ("File is damaged or in unknown format: %s", file);
    }
    nf = (msgvec *) malloc (sizeof (msgvec)); /* room to put all this stuff */
    if (!nf)
	cmerr ("Out of memory");
    nf->type = type;

    strcpy (nf->filename, file);
    nf->flags = ((flag == CMD_GET) ? 0 : MF_RDONLY); /* only flag so far */
    if (strcmp(file, mail_file) == 0)
	nf->flags |= MF_MAILBOX;	/* primary mailbox? */

    nf->count = 0;
    if (probeval == PR_NOEX) {		/*open file, interactive errors*/
	nf->msgs = NULL;		/* nothing there */
	err = (*msg_ops[nf->type].open)(nf,OP_APP|OP_INT);
    }
    else
	err = (*msg_ops[nf->type].open)(nf,OP_INT);
    if (err != 0) {
#ifdef EWOULDBLOCK
	if (errno == EWOULDBLOCK && flag == CMD_GET)
	    fprintf (stderr, "\
 Use the EXAMINE command if you want to open it in read-only mode.\n");
#endif /* EWOULDBLOCK */
	return (NULL);
    }
    printf ("%d message%s read\n",nf->count,((nf->count==1) ? "" : "s"));

    record_mtime (nf);			/* save modify time for this file */

    nf->current = nf->count;		/* point to last message */
    time (&nf->when_read);
    if (!sequence_init (nf))
	cmerr ("Out of memory");
    return (nf);
}


/* 
 * mail_probe:
 * probe the file to see what mail format it's in
 * pass back probe error code, set typep if PR_OK
 */
int
mail_probe (file, typep)
char *file;
int *typep;
{
    int stat, newstat;
    extern int num_msg_ops;		/* how many formats we know */

    stat = PR_NAME;			/* assume the lowest level problem */
    for (*typep = 0; *typep < num_msg_ops; (*typep)++) { /* test each type */
	if ((newstat = (*msg_ops[*typep].probe)(file)) == PR_OK)
	    return (PR_OK);		/* that's the one */
	if (newstat > stat)		/* did we get any further? */
	    stat = newstat;
    }
    return (stat);			/* tell them how we did */
}


/*
 * get the size of a local file
 */
local_get_size(mail)
msgvec *mail;
{
    struct stat sbuf;
    if (fstat(fileno(mail->filep), &sbuf) == 0)
	mail->size = sbuf.st_size;
    else {
	fprintf(stderr,"?Couldn't determine size of %s\n", mail->filename);
	mail->size = 0;
    }
}

/*
 * local_contig_open:
 * Contiguous local files are opened the same way, regardless of format...
 */
local_contig_open (mail,flags)
msgvec *mail;
int flags;
{
    FILE *fp;
    char *openflags;			/* how to open the file */
    int err;
    struct stat sbuf;
    int lockable;			/* can we lock it? */

    if (flags & OP_APP)			/* gonna save to this file */
      openflags = "a";			/* write only at end */
    else if (mail->flags & MF_RDONLY)
      openflags = "r";
    else
      openflags = "r+";
	
    if ((flags & OP_APP) || ((mail->flags & MF_RDONLY) == 0)) {
	if (stat (mail->filename, &sbuf) != 0) {
	    /*
	     * if filename doesn't exist, make sure it gets created
	     * with the proper mode.
	     */
	    int fd = creat (mail->filename, new_file_mode);
	    if (fd < 0) {
		fprintf (stderr, "Cannot create %s: %s\n", mail->filename,
			 errstr (errno));
		(void) free (mail);	/* XXX bogus! */
		return errno;
	    }
	    else
		(void) close (fd);
	}
    }

    if ((fp = fopen (mail->filename, openflags)) == NULL) {
        if (flags & OP_INT) {		/* interactive? */
	    fprintf (stderr, "?Cannot open %s: %s\n", 
		     mail->filename, errstr (errno));
	}
	free (mail);
	return (errno);
    }

    if (fstat(fileno(fp), &sbuf) == 0) {
	mail->size = sbuf.st_size;
	mail->mtime = sbuf.st_mtime;	/* record_mtime(mail) */
	lockable = ((sbuf.st_mode & S_IFMT) == S_IFREG);
    }
    else {
	fprintf(stderr,"fstat: Couldn't determine size and type of %s\n",
		mail->filename);
	mail->size = 0;
	lockable = FALSE;
    }

    /* if opened for write, lock the file away */
    if (lockable && ((flags & OP_APP) || !(mail->flags & MF_RDONLY))) {
	if (lock_file (mail->filename, fileno (fp)) < 0) {
	    if (errno == EINVAL) {
		if (debug)
		    fprintf (stderr, "%%File %s could not be locked\n",
			     mail->filename);
		/*
		 * pretend we locked it, and hope for the best
		 */
	    }
	    else {
		local_close(fp);		/* close it again */
		if (!(flags & OP_INT)) {
		    free (mail);
		    return (errno);
		}
		fprintf (stderr, "?Can't lock %s: %s.\n\
 Apparently some other process is accessing this file in read/write mode.\n",
			 mail->filename,
#ifdef EWOULDBLOCK
			 (errno == EWOULDBLOCK) ? "file is busy" :
#endif
			 errstr (errno));
		free (mail);
		return (errno);
	    }
	}
    }

    mail->filep = fp;
    if (! (flags & OP_APP))		/* don't count if for append */
        local_contig_count (mail);	/* count 'em */
    return 0;				/* no error */
}


/*
 * local_contig_count:
 * count the messages in this mail file.  For formats which call this
 * routine, (ones which are local, contiguous mail files), this
 * requires reading them all in, so do that and keep in as many as we
 * have room for.  This fills in mail->count and mail->msgs.
 */
local_contig_count (mail)
msgvec *mail;
{
    int alloccnt;			/* how many we've allocated */
    int err;

    alloccnt = 10;			/* start with ten messages */
    mail->count = 0;			/* none yet */
    /* get space for headers */
    mail->msgs = (message *) malloc (alloccnt * sizeof (message));
    mail->last_read = 0;		/* haven't read any yet */
    mail->keywords = nil;
    /* read the next one as long as we can */
    while ((err = (*msg_ops[mail->type].rd_msg)(mail, ++mail->count)) == 
	   RD_OK) { 
	mail->msgs[mail->count].hdrsum = NULL; /* not used yet */
        if (mail->count+1 == alloccnt)	/* filled 'em all up (except #0) */
	    mail->msgs = (message *)
	        realloc (mail->msgs, (alloccnt+=10) * sizeof (message));
    }
    /* read 1:n-1, failed on n -> n-1 msgs */
    mail->count--;

    if (err == RD_FAIL) { /* didn't make it to the end */
	fprintf (stderr, "\
Could not read entire mail file: something is wrong with the format of\n\
%s --\n\
see a consultant or systems administrator about fixing the file.\n", 
		 mail->filename);
	if (!(mail->flags & MF_RDONLY)) {
	    mail->flags |= MF_RDONLY;	/* turn it on */
	    fprintf (stderr, "\
Since MM only has a truncated version, the file has has been set to\n\
READ-ONLY mode.  Any changes you make WILL NOT be saved.\n");
	}
    }
    /* snug it down to fit just right */
    mail->msgs = (message *) realloc (mail->msgs, 
				      (mail->count+1)*sizeof (message));
}

/*
 * local_contig_proben:
 * open a local contiguous file for probin'
 * probe-open = proben -- get it??
 */
int
local_contig_proben (file,fpp)
char *file;
FILE **fpp;
{
    if ((*fpp = fopen (file, "r")) == NULL)
        switch (errno) {		/* nice switch */
	case EACCES:
	    return (PR_PERM);		/* couldn't look at it */
	default:
	    return (PR_NOEX);		/* say it doesn't exist */
	}
    return (PR_OK);			/* okay, we opened it */
}

/*
 * fail_msg:
 * send a nice printf that we failed on this message
 */
fail_msg(num)
int num;
{
  fprintf (stderr,
	   "Message %d has bad format, ignoring rest of file\n",
	   num);
}


/*
 * local_close:
 * local files are also mostly closed the same way
 * gotta free up the lock, though that probably gets done automagically
 * when we let go of the file...
 */
local_close (fp)
FILE *fp;
{
    fclose (fp);
}


/*
 * fgetline:
 * Keep doing fgets till we get the whole line.
 * on end of file, return what we have, or NULL if we have nothing
 */
char *
fgetline (filep)
FILE *filep;
{
    int buflen;
    char *buf;
    char *bufp;

    buflen = 1;				/* start with room for NULL :-) */
    bufp = buf = malloc (buflen+=100);	/* need some space to start */
    while (true) {			/* till we return */
        if (fgets (bufp, buflen, filep) == NULL) { /* get the line */
	    if (bufp == buf) {		/* should be end of file */
		free (buf);
		return (NULL);
	    }
	    else {			/* read some before EOF */
		*bufp = '\0';		/* close it off */
		return (buf);
	    }
	}
	if ((bufp = index (bufp, '\n')) != NULL) { /* get it? */
	    *bufp = '\0';		/* we don't need the CR */
	    return (buf);		/* return the whole line */
	}
	buf = (char *) realloc (buf,buflen+=100); /* get more space */
	bufp = &buf[buflen-101];	/* point to null of what we have */
    }
}


/*
 * cmd_check:
 * check for new messages in the current mail file
 */
cmd_check (n)
int n;
{
    noise ("for new mail");
    confirm ();
    if (new_mail(false) == false)
	printf("No new mail.\n");
}

/*
 * update:
 * Write out the current version of the mail, saving the old version
 * in mailname~.  Close old file.  If writing to an alternate file,
 * (name already in pf->filename) don't write a backup.
 *
 * Note that the msgvec isn't updated to reflect the deleted messages,
 * since if we're about to exit, there's no point.
 *
 * If file is now empty and wasn't before (and isn't main mail file),
 * delete it and free the msgvec.
 *
 * We cannot allow interrupts during update, since most of them want
 * to call update, and also we really don't want to stop in the middle
 * of writing the file.  So, we defer them until we're done (and
 * MF_DIRTY ensures we won't redo the update).  
 *
 * Return true on success, false if we fail anywhere.
 */
int
update (pf,updflags)
msgvec **pf;
int updflags;
{
    msgvec *f;				/* get a non-pointered */
    int i, err;
    FILE *old_fp;
    buffer ofile;
    int flags;				/* flags to send to wr_msg */
    int empty;				/* is the file empty? */
    int doit = FALSE;			/* do we need to do an update? */
    unsigned short filemode = new_file_mode;

    int exp = updflags & UPD_EXPUNGE;	/* should we do an expunge? */
    int savemods = updflags & UPD_SAVEMOD; /* save even if only MF_MODIFED */
    int always = updflags & UPD_ALWAYS;	/* write it out no matter what */
    int altfile = updflags & UPD_ALTFILE; /* write to an alternate file */
    int quiet = updflags & UPD_QUIET;	/* be quiet */

    int result = true;			/* assume we'll succeed */
    long mask = block_signals ();	/* block out interrupts */

    if (pf == 0)
	goto failed;

    f = *pf;
    if (f == 0)
	goto failed;

    if ((f->flags & MF_RDONLY) && !altfile)
	goto failed;

    if (exp) {				/* check for deletions to expunge */
	for (i = 1; i <= f->count && !doit; i++)
	    if (f->msgs[i].flags & M_DELETED)
		doit = TRUE;
	if (doit)			/* expunge! */
	    printf ("Expunging deleted messages.\n");
	else
	    printf ("No messages deleted.\n");
    }

    if (always ||			/* forced to do it */
	(f->flags & MF_DIRTY) ||	/* needs saving */
	((f->flags & MF_MODIFIED) && savemods)) /* want to save flags */
	doit = TRUE;

    if (!doit && !altfile)
	goto failed;

    /****** use msg_ops when we add the rename routine to it */
    {
	struct stat sb;
	if (stat (f->filename, &sb) == 0)
	    filemode = sb.st_mode & 07777;

	sprintf (ofile, "%s~", f->filename);
	if (rename (f->filename, ofile) != 0) {
	    switch(errno) {
	    case ENOENT:
		if (!quiet)
		    fprintf (stderr,"%s does not exist.  Creating new file.\n",
			     f->filename);
		break;
#ifndef HAVE_FLEXFILENAMES
	    case EINVAL:
		if (!quiet)
		    fprintf (stderr, "Filename too long for rename: %s\n\
Use the WRITE or COPY commands to save your messages.", f->filename);
		goto failed;
#endif
	    default:
		if (!quiet)
		    fprintf (stderr,"Can't rename %s to %s: %s\n\
 Use the WRITE or COPY commands to save your messages.",
			     f->filename, ofile, errstr (errno));
		goto failed;
	    }	  
	}
    }
    old_fp = f->filep;			/* don't overwrite that */
    /* now we've saved (or closed) the old file, open the new one */
    err = (*msg_ops[f->type].open)(f, OP_APP); /* new file, for append */

    if (!err)
	if (chmod (f->filename, filemode) != 0)
	    if (!quiet)
		perror ("update: error updating file modes");

    flags = exp ? (WR_SAVE|WR_EXP) : WR_SAVE;
    if (f->count == 0)
	empty = FALSE;		  /* if they started "empty", leave it alone */
    else
	empty = TRUE;
    if (!err) {
        for (i = 1, err = 0; (i <= f->count) && (err == 0); i++) {
	    if (!exp || !(f->msgs[i].flags & M_DELETED))
		empty = FALSE;
	    err=(*msg_ops[f->type].wr_msg)(f,&f->msgs[i],i,flags);
	    if (err) err = errno;
	}
    }
    if (err) {				/* trouble writing file */
        (*msg_ops[f->type].close)(f->filep); /* close failed file */
	f->filep = old_fp;		/* replace good one */
	if (rename (ofile, f->filename) != 0) {
	    if (!quiet)
		fprintf(stderr, "\
Can't write output file %s and can't restore from %s - %s\n",
			f->filename, ofile, errstr (err));
	}
	else {
	    if (!quiet)
		fprintf (stderr,"\
File not saved: can't write output file - %s\n",
			 errstr (err));
	}
	if (!altfile) {			/* don't set this for another file */
	    cf->flags |= MF_WRITERR;
	    record_mtime (f);
	}
	errno = err;
	goto failed;
    }
    if (altfile) {
	(*msg_ops[f->type].close)(f->filep); /* success, close the file */
	f->filep = old_fp;		/* restore original file */
	goto succeeded;
    }
	
    if (empty) {			/* nothing left, delete it */
	if (f->flags & MF_MAILBOX) {
	    if (!quiet)
		fprintf(stderr,
			"All messages deleted, not deleting main mail file\n");
	}
	else {
	    if (!quiet)
		fprintf (stderr, "All messages deleted, deleting file...");
	    /****************/
	    if (unlink (f->filename) != 0) { /* delete it */
		if (!quiet)
		    fprintf (stderr,"Can't delete %s - %s\n", 
			     f->filename, errstr (errno));
		goto failed;
	    }
	    /****************/
	    if (!quiet)
		fprintf (stderr, "OK\n");
	    (*msg_ops[f->type].close)(f->filep); /* close old file */
	    msgvec_free (f);		/* free all the bits and pieces */
	    *pf = nil;			/* no more file */
	    goto succeeded;
	}
    }
    f->flags &= ~(MF_DIRTY|MF_MODIFIED); /* not dirty any more */
    (*msg_ops[f->type].close)(old_fp);	/* success, close old file */

    record_mtime (f);			/* update saved modify time */
    cf->flags &= ~MF_WRITERR;
    goto succeeded;

  failed:
    result = false;
  succeeded:
    release_signals (mask);
    return result;
}

/*
 * msgvec_free:
 * Free all the various pieces of the msgvec we have here.
 * We don't null out the pointer though...
 */
msgvec_free (mv)
msgvec *mv;
{
    int i;
    char *text;
    keylist keys;

    sequence_free (mv);			/* free any sequencing info */
    for (i = 1; i <= mv->count; i++) {	/* free the message */
	if ((text = mv->msgs[i].text) != NULL) /* the text */
	    safe_free (text);
	if ((keys = mv->msgs[i].keywords) != NULL) /* keywords */
	    free_keylist(keys);
	if ((text = mv->msgs[i].from) != NULL) /* from field */
	    safe_free(text);
    }
    free_keylist(mv->keywords);
    safe_free (mv->msgs);		/* and the message structures */
    safe_free (mv);			/* the data structure itself */
}

/*
 * record the mtime value for a mail file, so we can detect later on if
 * some other process has changed the file.
 */
record_mtime (mf)
msgvec *mf;
{
    struct stat sb;
    if (stat (mf->filename, &sb) != 0) {
	perror (mf->filename);
	mf->mtime = 0;
    }
    else
	mf->mtime = sb.st_mtime;
}

char *
make_backup_file (filename)
char *filename;
{
    int err;
    char *cp;
    char backup[MAXPATHLEN];

    sprintf (backup, "%s~", filename);

    err = rename (filename, backup);
    if (err < 0) {
	cp = rindex (filename, '/');
	if (cp == 0)
	    sprintf (backup, "#%s#", filename);
	else {
	    strcpy (backup, filename);
	    sprintf (backup + (cp - filename) + 1, "#%s#", cp + 1);
	}
	err = rename (filename, backup);
    }
    
    if (err < 0)
	return 0;

    if (cp = malloc (strlen (backup) + 4)) {
	strcpy (cp, backup);
	return cp;
    }
    return 0;
}
