/************************************************************************
 *	Routines that deal with understanding the folder types		*
 *									*
 *	Copyright (c) 1990-1999, S.R. van den Berg, The Netherlands	*
 *	Copyright (c) 1999-2001, Philip Guenther, The United States	*
 *							of America	*
 *	#include "../README"						*
 ************************************************************************/
#ifdef RCS
static /*const*/char rcsid[]=
 "$Id: foldinfo.c,v 1.10 2001/06/27 17:07:21 guenther Exp $";
#endif
#include "procmail.h"
#include "misc.h"
#include "lastdirsep.h"
#include "robust.h"
#include "exopen.h"
#include "goodies.h"
#include "locking.h"
#include "foldinfo.h"

static const char
 maildirtmp[]=MAILDIRtmp,maildircur[]=MAILDIRcur;
const char
 maildirnew[]=MAILDIRnew;
int accspooldir;		     /* can we write to the spool directory? */

/* determine the requested type, chopping off the type specifier and any
   extra trailing slashes */
static int folderparse P((void))
{ char*chp;int type;
  type=ft_FILE;chp=strchr(buf,'\0');
  switch(chp-buf)
   { case 2:
	if(chp[-1]==*MCDIRSEP_)				     /* "//" or "x/" */
	   chp--,type=ft_MAILDIR;
     case 0:case 1:						/* "" or "x" */
	goto ret;
   }			    /* Okay, put chp right before the type specifier */
  if(chp[-1]==chCURDIR&&chp[-2]==*MCDIRSEP_)			    /* foo/. */
     chp-=2,type=ft_MH;
  else if(chp[-1]==*MCDIRSEP_)					     /* foo/ */
     chp--,type=ft_MAILDIR;
  else							     /* no specifier */
     goto ret;
  while(chp-1>buf&&strchr(dirsep,chp[-1]))		    /* chop extra /s */
     chp--;
ret:
  *chp='\0';
  return type;
}

int rnmbogus(name,stbuf,i,dolog)const char*const name;	      /* move a file */
 const struct stat*const stbuf;const int i,dolog;	   /* out of the way */
{ static const char renbogus[]="Renamed bogus \"%s\" into \"%s\"",
   renfbogus[]="Couldn't rename bogus \"%s\" into \"%s\"",
   bogusprefix[]=BOGUSprefix;char*p;
  p=strchr(strcpy(strcpy(buf2+i,bogusprefix)+STRLEN(bogusprefix),
   getenv(lgname)),'\0');
  *p++='.';ultoan((unsigned long)stbuf->st_ino,p);	  /* i-node numbered */
  if(dolog)
   { nlog("Renaming bogus mailbox \"");elog(name);elog("\" info");
     logqnl(buf2);
   }
  if(rename(name,buf2))			   /* try and move it out of the way */
   { syslog(LOG_ALERT,renfbogus,name,buf2);		 /* danger!  danger! */
     return 1;
   }
  syslog(LOG_CRIT,renbogus,name,buf2);
  return 0;
}

/* return the named object's mode, making it a directory if it doesn't exist
 * and renaming it out of the way if it's not _just_right_ and we're being
 * paranoid */
static mode_t trymkdir(dir,paranoid,i)const char*const dir;
 const int paranoid,i;
{ struct stat stbuf;int tries=3-1;	     /* minus one for post-decrement */
  do
   { if(!(paranoid?lstat(dir,&stbuf):stat(dir,&stbuf)))	   /* does it exist? */
      { if(!paranoid||		     /* is it okay?  If we're trusting it is */
	   (S_ISDIR(stbuf.st_mode)&&	      /* else it must be a directory */
	    (stbuf.st_uid==uid||	       /* and have the correct owner */
	     !stbuf.st_uid&&!chown(dir,uid,sgid))))  /* or be safely fixable */
	   return stbuf.st_mode;				   /* bingo! */
	else if(rnmbogus(dir,&stbuf,i,1))  /* try and move it out of the way */
	   break;						/* couldn't! */
      }
     else if(errno!=ENOENT)	    /* something more fundamental went wrong */
	break;
     else if(!mkdir(dir,NORMdirperm))	  /* it's not there, can we make it? */
      { if(!paranoid)	      /* do we need to double check the permissions? */
	   return S_IFDIR|NORMdirperm&~cumask;			     /* nope */
	tries++;		/* the mkdir succeeded, so take another shot */
      }
   }while(tries-->0);
  return 0;
}

static int mkmaildir(buffer,chp,paranoid)char*const buffer,*const chp;
 const int paranoid;
{ mode_t mode;int i;
  if(paranoid)
     strncpy(buf2,buffer,i=chp-buffer+1),buf2[i-1]= *MCDIRSEP_,buf2[i]='\0';
  return
   (strcpy(chp,maildirnew),mode=trymkdir(buffer,paranoid,i),S_ISDIR(mode))&&
   (strcpy(chp,maildircur),mode=trymkdir(buffer,paranoid,i),S_ISDIR(mode))&&
   (strcpy(chp,maildirtmp),mode=trymkdir(buffer,paranoid,i),S_ISDIR(mode));
}					      /* leave tmp in buf on success */

int foldertype(type,forcedir,modep,paranoid)int type,forcedir;
 mode_t*const modep;struct stat*const paranoid;
{ struct stat stbuf;mode_t mode;int i;char*chp;
  if(!type)
     type=folderparse();
  switch(type)
   { case ft_MAILDIR:i=MAILDIRLEN;break;
     case ft_MH:i=0;break;
     case ft_FILE:
	i=0;					    /* resolve the ambiguity */
	if(!forcedir)
	 { if(paranoid?lstat(buf,&stbuf):stat(buf,&stbuf))
	    { if(paranoid)
	       { type=ft_NOTYET;
		 goto ret;
	       }
	      goto newfile;
	    }
	   else if(mode=stbuf.st_mode,!S_ISDIR(mode))
	      goto file;
	 }
	type=ft_DIR;
	break;
     default:					     /* "this cannot happen" */
	nlog("Internal error: improper type (");
	ltstr(0,type,buf2);elog(buf2);
	elog(") passed to foldertype for folder ");logqnl(buf);
	Terminate();
   }
  chp=strchr(buf,'\0');
  if((chp-buf)+UNIQnamelen+1+i>linebuf)
   { type=ft_TOOLONG;
     goto ret;
   }
  if(type==ft_DIR&&!forcedir)		  /* we've already checked this case */
     goto done;
  if(paranoid)
     strncpy(buf2,buf,i=lastdirsep(buf)-buf),buf2[i]='\0';
  mode=trymkdir(buf,paranoid!=0,i);
  if(!S_ISDIR(mode)||(type==ft_MAILDIR&&
   (forcedir=1,!mkmaildir(buf,chp,paranoid!=0))))
   { nlog("Unable to treat as directory");logqnl(buf);	 /* we can't make it */
     if(forcedir)				     /* fallback or give up? */
      { *chp='\0';skipped(buf);type=ft_CANTCREATE;
	goto ret;
      }
     if(!mode)
newfile:mode=S_IFREG|NORMperm&~cumask;
file:type=ft_FILE;
   }
done:
  if(paranoid)
     *paranoid=stbuf;
  else
     *modep=mode;
ret:
  return type;
}

			     /* lifted out of main() to reduce main()'s size */
int screenmailbox(chp,egid,Deliverymode)
 char*chp;const gid_t egid;const int Deliverymode;
{ char ch;struct stat stbuf;int basetype,type;
  /*
   *	  do we need sgidness to access the mail-spool directory/files?
   */
  accspooldir=3;	   /* assume we can write to the spool directory and */
  sgid=gid;	      /* that we don't need to setgid() to create a lockfile */
  strcpy(buf,chp);
  basetype=folderparse();			       /* strip off the type */
  if(buf[0]=='\0')				/* don't even bother with "" */
     return 0;
  ch= *(chp=lastdirsep(buf));
  if(chp>buf)
     *chp='\0';					   /* strip off the filename */
  if(!stat(buf,&stbuf))
   { unsigned wwsdir;
     accspooldir=(wwsdir=			/* world writable spool dir? */
	    ((S_IWGRP|S_IXGRP|S_IWOTH|S_IXOTH)&stbuf.st_mode)==
	     (S_IWGRP|S_IXGRP|S_IWOTH|S_IXOTH)
	  <<1|						 /* note it in bit 1 */
	  uid==stbuf.st_uid);	   /* we own the spool dir, note it in bit 0 */
     if((CAN_toggle_sgid||accspooldir)&&privileged)
	privileged=priv_DONTNEED;	     /* we don't need root to setgid */
     if(uid!=stbuf.st_uid&&		 /* we don't own the spool directory */
	(stbuf.st_mode&S_ISGID||!wwsdir))	  /* it's not world writable */
      { if(stbuf.st_gid==egid)			 /* but we have setgid privs */
	   doumask(GROUPW_UMASK);		   /* make it group-writable */
	goto keepgid;
      }
     else if(stbuf.st_mode&S_ISGID)
keepgid:			   /* keep the gid from the parent directory */
	if((sgid=stbuf.st_gid)!=egid&&		  /* we were started nosgid, */
	 setgid(sgid))				     /* but we might need it */
	   checkroot('g',(unsigned long)sgid);
   }
  else				/* panic, mail-spool directory not available */
   { setids();mkdir(buf,NORMdirperm);	     /* try creating the last member */
   }
  *chp=ch;
 /*
  *	  check if the default-mailbox-lockfile is owned by the
  *	  recipient, if not, mark it for further investigation, it
  *	  might need to be removed
  */
  chp=strchr(buf,'\0')-1;
  for(;;)				     /* what type of folder is this? */
   { type=foldertype(basetype,0,0,&stbuf);
     if(type==ft_NOTYET)
      { if(errno!=EACCES||(setids(),lstat(buf,&stbuf)))
	   goto nobox;
      }
     else if(!ft_checkcloser(type))
      { setids();
	if(type<0)
	   goto fishy;
	goto nl;					   /* no lock needed */
      }
    /*
     *	  check if the original/default mailbox of the recipient
     *	  exists, if it does, perform some security checks on it
     *	  (check if it's a regular file, check if it's owned by
     *	  the recipient), if something is wrong try and move the
     *	  bogus mailbox out of the way, create the
     *	  original/default mailbox file, and chown it to
     *	  the recipient
     */
     ;{ int checkiter=1;
	for(;;)
	 { if(stbuf.st_uid!=uid||		      /* recipient not owner */
	      !(stbuf.st_mode&S_IWUSR)||	     /* recipient can write? */
	      S_ISLNK(stbuf.st_mode)||			/* no symbolic links */
	      (S_ISDIR(stbuf.st_mode)?	      /* directories, yes, hardlinks */
		!(stbuf.st_mode&S_IXUSR):stbuf.st_nlink!=1))	       /* no */
	     /*
	      * If another procmail is about to create the new
	      * mailbox, and has just made the link, st_nlink==2
	      */
	      if(checkiter--)		    /* maybe it was a race condition */
		 suspend();		 /* close eyes, and hope it improves */
	      else			/* can't deliver to this contraption */
	       { int i=lastdirsep(buf)-buf;
		 strncpy(buf2,buf,i);buf2[i]='\0';
		 if(rnmbogus(buf,&stbuf,i,1))
		    goto fishy;
		 goto nobox;
	       }
	   else
	      break;
	   if(lstat(buf,&stbuf))
	      goto nobox;
	 }					/* SysV type autoforwarding? */
	if(Deliverymode&&(stbuf.st_mode&S_ISUID||
	 !S_ISDIR(stbuf.st_mode)&&stbuf.st_mode&S_ISGID))
	 { nlog("Autoforwarding mailbox found\n");
	   exit(EX_NOUSER);
	 }
	else
	 { if(!(stbuf.st_mode&OVERRIDE_MASK)&&
	      stbuf.st_mode&cumask&
	       (accspooldir?~(mode_t)0:~(S_IRGRP|S_IWGRP)))	/* hold back */
	    { static const char enfperm[]=
	       "Enforcing stricter permissions on";
	      nlog(enfperm);logqnl(buf);
	      syslog(LOG_NOTICE,slogstr,enfperm,buf);setids();
	      chmod(buf,stbuf.st_mode&=~cumask);
	    }
	   break;				  /* everything is just fine */
	 }
      }
nobox:
     if(!(accspooldir&1))	     /* recipient does not own the spool dir */
      { if(!xcreat(buf,NORMperm,(time_t*)0,doCHOWN|doCHECK))	   /* create */
	   break;		   /* mailbox... yes we could, fine, proceed */
	if(!lstat(buf,&stbuf))			     /* anything in the way? */
	   continue;			       /* check if it could be valid */
      }
     setids();						   /* try some magic */
     if(!xcreat(buf,NORMperm,(time_t*)0,doCHECK))		/* try again */
	break;
     if(lstat(buf,&stbuf))			      /* nothing in the way? */
fishy:
      { nlog("Couldn't create");logqnl(buf);
	return 0;
      }
   }
  if(!S_ISDIR(stbuf.st_mode))
   { int isgrpwrite=stbuf.st_mode&S_IWGRP;
     strcpy(chp=strchr(buf,'\0'),lockext);
     defdeflock=tstrdup(buf);
     if(!isgrpwrite&&!lstat(defdeflock,&stbuf)&&stbuf.st_uid!=uid&&
      stbuf.st_uid!=ROOT_uid)
      { int i=lastdirsep(buf)-buf;
	strncpy(buf2,buf,i);buf2[i]='\0';     /* try & rename bogus lockfile */
	rnmbogus(defdeflock,&stbuf,i,0);		   /* out of the way */
      }
     *chp='\0';
   }
  else
nl:  defdeflock=empty;					   /* no lock needed */
  return 1;
}
