/*
 * Copyright (c) 1998 Qualcomm Incorporated. All rights reserved.
 *
 * File        : main.c
 * Project     : Delivery agent implementing "On Server Filtering".
 * Comments    : This agent doesn't act as User Agent. This is meant
 *               for only deliver purpose to be invoked by sendmail
 *               as a local delivery agent or from users ~/.forward
 *               file. This delivery agent acts on the message based 
 *               on the sieve script in recipients home directory (~/.sieve).
 *
 * Caution     : Must specify -f command line option.
 *
 * Revisions   :
 *  xx/xx/xx   [PY]
 *         - File added.
 *  10/26/98   [mb]
 *         - Initial development release.  Many bugs.
 *  11/05/98   [mb]
 *         - Second development release.
 *         - Local config.h now is agent_config.h.
 *         - Envelope info now being used.
 *         - Delivers headers with modification by sieve.
 *         - Dot file locking.
 *         - Memory now only allocated to store header, not entire message.
 *
 *
 * CASES ACCOUNTED FOR SOFAR
 *    1. Invoked by sendmail with super user privileges. The directory
 *       /var/mail with sticky bit ON. Mailboxes permission rw-rw----. With
 *       group Id 'mail'. Mail boxes and users home directory are local the
 *       system (not on NFS or AFS). OS' (Solars 2.5.1, )
 *
 */

/* system include files */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/param.h>
#include <sys/file.h>
#include <pwd.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <ctype.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <time.h>

#ifdef __STDC__
# include <stdarg.h>
#else
# include <vararg.h>
#endif

/* common include files */
#include "config.h"
#include "sieve_lock.h"
#include "flock.h"

/* local include files */
#include "agent_config.h"
#include "daa.h"
#include "sieve-execute.h"


typedef enum { 
    REJECT  = 0x0001,
    DISCARD = 0x0002,
    FILETO  = 0x0004,
    SUBMIT  = 0x0008,
    KEEP    = 0x0010,
}ActionType;

typedef struct {
    unsigned int action;
    void *       forward_msg;
    void *       reply_msg;
    void *       file_to_path;
}SieveResultType;

typedef enum { TRACE, DEBUG, NOTIFY, FATAL } LogLevel;

int lprintf __PROTO((LogLevel level, const char *format, ...));
int SecureOpen __PROTO((char *file_name, int oflag, mode_t mode));
int DeliverContext __PROTO((Context *msg, unsigned int headerLen));
void FixLine __PROTO((char *line));
void FixHeaderLine __PROTO((char *line));

char *LocalDup __PROTO((char *s));
char *DomainDup __PROTO((char *s));

#define USE_ERR	do {                                                       \
    lprintf(FATAL, "Usage: %s [-f fromwhom] [-d recipient]", argv[0]);     \
    exit(1);                                                               \
} while(0);

/* 
 * Global Variables. This is not supposed to be any thread safe though.
 */
FILE *fpMsg = (FILE *)0;        /* Tmp file where the msg is located */
unsigned int uiMsgLen = 0;      /* for Content-length */
unsigned int uiTotalMsgLen = 0;
unsigned int uiHeaderLen = 0;
char localHost[MAXHOSTNAMELEN+1];
char *recipient = (char *)0;    /* username to recieve the msg */
char *rcptHomeDir = (char *)0;  /* Home dir obtained from pw */
uid_t rcptUID;                  /* UID from pw */
char *sender = (char *)0;       /* from signature to BSD spool */
char *timestamp = (char *)0;
FILE *fpSieve = (FILE *)0;      /* recipient's Sieve script */
unsigned char exitStatus = 0;   /* To contain the return status to sendmail */
/*-----End of Global Variables--------*/

int main (argc, argv)
int argc;
char **argv;
{
  extern char *optarg;
  extern int optind;
  int c;
  int updateDate;
  
  /*
    SieveResultType result;
    FILE *fpSieve = (FILE *)0;
  */

  int bInHeader = 1;
  char *msgbuf;
  Context msg;
  
#ifdef SYSLOG42
#else
  openlog("qd.local", LOG_PID, LOG_MAIL);
#endif
#ifdef _DEBUG
  {
    int i;
    char cmdline[1024];
    cmdline[0] = '\0';
    for(i = 0; i < argc; i++){
      strcat(cmdline, argv[i]);
      strcat(cmdline, " ");
    }
    lprintf(DEBUG, "Invoked as \"%s\"", cmdline);
  }
#endif
  
  /* get time for time stamp */
  {
    time_t clk;
    time(&clk);
    timestamp = strdup(ctime(&clk));
  }

  /* 
   * Read command line switches
   */
  while( (c = getopt(argc, argv, "d:f:")) != EOF ) {
    switch(c) {
    case 'd':
      recipient = optarg;
      break;
    case 'f':
      sender = optarg;
      break;
    case 'h':
    default:
      USE_ERR;
    }
  }

  if (argc!=optind) {
    USE_ERR;
  }

  /* see if we should update the date on From line */
  if (sender&&!strcmp(sender, "-")) {
    updateDate = 1;
    sender = 0;
  }
  else {
    updateDate = 0;
  }

  /* get fully qualified host name */
  {
    struct hostent *hp;

    gethostname(localHost, MAXHOSTNAMELEN);
    if ((hp = gethostbyname(localHost))) {
      strncpy(localHost, hp->h_name, MAXHOSTNAMELEN);
    }
  }

  if( (fpMsg = tmpfile()) == NULL ) {
    lprintf(FATAL, "Unable to create temporary file");
    exit(1);
  }

  {
    char line[1024]; /* lines wouldn't be larger than 1000 */
    char buf[1024];
    char *tmp;

    while(fgets(line, sizeof(line), stdin)) {

      /* just in case line is too long */
      line[sizeof(line)-1] = 0;

      if (!sender&&sscanf(line, "%s", buf)==1&&!strcmp(buf, "From")) {
	/* found from line */
	if (sscanf(line+strlen(buf), "%s", buf)==1) {
	  sender = strdup(buf);
	  
	  /* skip first two terms and whitespace*/
	  for (tmp=line; *tmp&&isspace((int)*tmp); tmp++);
	  for (; *tmp&&!isspace((int)*tmp); tmp++);
	  for (; *tmp&&isspace((int)*tmp); tmp++);
	  for (; *tmp&&!isspace((int)*tmp); tmp++);
	  for (; *tmp&&isspace((int)*tmp); tmp++);

	  /* use the existing time stamp if it exists and -f -
	   * is not specified.
	   */
	  if (*tmp&&!updateDate) {
	    free(timestamp);
	    timestamp = strdup(tmp);
	  }
	}
      }
      else {
	if((bInHeader) && ((line[0] == '\n') || 
			   (line[0] == '\r' && line[1] == '\n')) ) {
	  bInHeader = 0;
	}
	else {
	  uiTotalMsgLen += strlen(line);
	  if(!bInHeader)
	    uiMsgLen += strlen(line);
	  else
	    uiHeaderLen += strlen(line);
	  fputs(line, fpMsg);
	}
      }
    }

    fflush(fpMsg);
    fseek(fpMsg, 0L, SEEK_SET);
  }
  
  /* Check the sender */
  if(!sender) {
    lprintf(FATAL, "Unable to obtain sender envelope information");
    exit(1);
  }

  /* Check the recipient */
  if(!recipient) {
    /* 
     * Get the recipient name from process uid or login name
     * There could be multple passwd entries for same id.
     * Check with cuserid() if fails getuid and getpwuid.
     */
    uid_t uid;
    struct passwd *pw;

    uid = getuid();
    pw = getpwuid(uid);
    if (pw) {
      recipient = strdup(pw->pw_name);
    }
    else if ((recipient=cuserid(0))) {
      recipient = strdup(recipient);
    }
    else {
      lprintf(FATAL, "Unable to determine recipient");
      exit(1);
    }
  }

  /* create a memory buffer an read in message from temp file */
  {
    char *bufpos;
    int c;
    unsigned int hl;
    
    msgbuf = (char*)malloc(uiHeaderLen+1);
    if (!msgbuf) {
      lprintf(FATAL, "Unable to allocate message buffer");
      exit(1);
    }
    
    for (bufpos=msgbuf,hl=uiHeaderLen; (c=getc(fpMsg))!=EOF&&hl;
	 bufpos++,hl--) {
      *bufpos = c;
    }
    
    *bufpos = 0;
    
    fseek(fpMsg, 0L, SEEK_SET);
  }

  memset(&msg, 0, sizeof(msg));

  msg.msg_orig.local = LocalDup(sender);
  msg.msg_orig.domain = DomainDup(sender);

  msg.msg_recip.name.local = strdup(recipient);
  msg.msg_recip.name.domain = strdup(localHost);


  /* create linked list of header lines */
  {
    char *bufptr, *lineptr;
    ll_Line *line, *current;
    
    line = (ll_Line*)malloc(sizeof(ll_Line));
    line->next = line->prev = 0;
    current = line;
    
    /* TODO: handle \r \n and \r line terminations */
    for (bufptr=msgbuf,lineptr=msgbuf; *bufptr; bufptr++) {
      if (*bufptr=='\n'&&*(bufptr+1)!='\t'&&*(bufptr+1)!=' ') {

	current->next = (ll_Line*)malloc(sizeof(ll_Line));
	memset(current->next, 0, sizeof(ll_Line));
	current->next->prev = current;
	current = current->next;
	current->next = 0;
	current->buffer = lineptr;
	
	lineptr = bufptr + 1;
	if (*lineptr=='\n')
	  break;
      }
    }
    
    if (line->next) {
      line->next->prev = 0;
      msg.message.headers.first = line->next;
      msg.message.headers.last = current;
      line->next->prev = 0;
    }
    else {
      msg.message.headers.first = 0;
      msg.message.headers.last = 0;
    }

    free(line);
  }
  
  {
    ll_Line *current;
    
    for (current=msg.message.headers.first; current; current=current->next) {
      FixHeaderLine(current->buffer);
    }

  }
  
  msg.msg_size = uiTotalMsgLen;
  
  {
    struct passwd *r;
    if((r = getpwnam(recipient)) == NULL) {
      lprintf(NOTIFY, "Recipient not local : %s", recipient);
      exit(1);
    }
    rcptHomeDir = strdup(r->pw_dir);
    rcptUID = r->pw_uid;
    if (setuid(r->pw_uid)<0) {
      lprintf(FATAL, "setuid() failed for recipient");
      exit(1);
    }
  }
    
    
  {
    char sievePath[MAXPATHLEN];
    int action = DAA_DELIVER;
    LockStruct lock;
    
    sprintf(sievePath, SIEVE_PATH, rcptHomeDir);
      
    lock = ObtainLock(rcptHomeDir, LOCK_FILE);
    if (!lock.result) {
      lprintf(NOTIFY, "Unable to obtain lock on sieve script");
      if (DeliverContext(&msg, uiHeaderLen) == -1) {
	lprintf(NOTIFY, "Delivery problems");
	free(rcptHomeDir);
	exit(1);
      }
    }
    
    if (do_script(sievePath, &msg, &action,
		  PARSE_ABORT_ON_ERROR, TRACE_LEVEL)) {
      /* error */
      if (DeliverContext(&msg, uiHeaderLen) == -1) {
	lprintf(NOTIFY, "Delivery problems");
	exit(1);
      }
    }
    else if (action!=DAA_DISCARD) {
      if(DeliverContext(&msg, uiHeaderLen) == -1 ){
	lprintf(NOTIFY, "Delivery problems");
	exit(1);
      }
    }
    else {	
      lprintf(NOTIFY, "Message Rejected");
    }
    
    ReleaseLock(lock);
    free(rcptHomeDir);
  }

  return 0;
}

/*
 * Deliver the message, but use headers from Context struct instead of
 * original headers.
 */
int DeliverContext(msg, headerLen)
     Context *msg;
     unsigned int headerLen;
{
  int fd;
  FILE *destFp;
  char mBox[MAXPATHLEN];
  /* 
   * Get the path to recipient's mailbox
   */
  sprintf(mBox,MAILDIR_PATH,recipient);
  if( (fd = SecureOpen(mBox, O_RDWR | O_CREAT, 0600 )) == -1)
    return -1;
  /* Now lock the mail box 
   *    - flock(), fcntl(), lockf(), maillock/user.lock 
   */
  if( flock(fd, LOCK_EX) == -1 ) {
    lprintf(FATAL, "flock() : failed on Mail Box'%s' : %s (%d)",
	    mBox, strerror(errno), errno);
    close(fd);
    return -1;
  }
  /* Get the stream to destination */
  if( (destFp = fdopen(fd, "a+")) == NULL ) {
    lprintf(FATAL, "fdopen() : failed. : %s (%d)", strerror(errno), errno);
    close(fd);
    return -1;
  }
  /*
   * From envelope. 
   */
  (void) fprintf(destFp, "From %s %s", sender, timestamp);
  
  /* Now print out the headers */
  {
    ll_Line *current; 
    for (current=msg->message.headers.first;
	 current; current=current->next) {
      /* TODO: re-wrap headers */
      fprintf(destFp, "%s\n", current->buffer);
    }
  }
  
  /* print body */
  /* Hmm... someone had fun with the shift key... */
  fprintf(destFp, "cOnTeNt-lEnGtH: %d\n\n", uiMsgLen);
  
  {
    char line[1024];
    char tmp[6];
    
    fseek(fpMsg, headerLen, SEEK_SET);
    while (fgets(line, sizeof(line), fpMsg)) {
      /* Do we allow Lines starting with "From" in message body? */
      strncpy(tmp, line, sizeof(tmp));
      tmp[5] = 0;
      if (!strcmp(tmp, "From ")) {
	putc('>', destFp);
	fputs(line, destFp);
      }
      else {
	fputs(line, destFp);
      }
    }
    fflush(destFp);
  }

  return 0;

}


/* 
 * Try to open, leaving no holes for hackers.
 *
 * Parameters :
 *    file_name: Full path of file to open.
 *    oflag    : passed to open().
 *    mode     : to open().
 *
 * Returns :
 *    File descriptor of file opened on success, -1 on error.
 */
int SecureOpen(file_name, oflag, mode)
char *file_name;
int oflag;
mode_t mode;
{
    int fd;
    struct stat byNameStat, byFdStat;
    unsigned int ucCrtd;
    /* Check if the file exists */
    if((ucCrtd = lstat(file_name, &byNameStat)) == -1  && errno != ENOENT ) {
	lprintf(FATAL, "Stat failed on : %s", file_name);
	return -1;
    }
    if( (fd = open(file_name, O_RDWR | O_CREAT, 0600)) == -1 ) {
	lprintf(FATAL, "Failed to open : %s", file_name);
	return -1;
    }
    if( fstat(fd, &byFdStat) == -1 ) {
	lprintf(FATAL, "Failed to do fstat() : %s", file_name);
	goto rollback;
    }
    if( byFdStat.st_uid != rcptUID ) {
	lprintf(NOTIFY, "File %s not owned by the id : %d", 
		file_name, rcptUID);
	goto rollback;
    }
    if( byFdStat.st_nlink != 1 ) {
	lprintf(FATAL, "File %s seems to have more than one link", file_name);
	goto rollback;
    }
    if( ucCrtd && lstat(file_name, &byNameStat) == -1) {
	lprintf(FATAL, "Failed to do lstat() : %s", file_name);
	goto rollback;
    }
    if( !S_ISREG(byNameStat.st_mode) ) {
	lprintf(NOTIFY, "Not a regular file : %s", file_name);
	goto rollback;
    }
    return fd;
 rollback:
    close(fd);
    return -1;
}

#ifdef __STDC__
int lprintf(LogLevel level, const char *format, ...)
#else
int lprintf(level, format, va_alist)
LogLevel level;
char *format;
va_dcl
#endif
{
    va_list ap;
#ifdef __STDC__
    va_start(ap, format);
#else
    va_start(ap);
#endif
    vsyslog(LOG_DEBUG, format, ap);
    /* Messages printed at stderr are to be caught by Transport agent */
    vfprintf(stderr, format, ap);
    fprintf(stderr, "\n");
    va_end(ap);
    return 0;
}

#ifdef __STDC__
void FixLine(char *line)
#else
void FixLine(line)
     char *line;
#endif
{
  for (;*line&&*line!='\n'&&*line!='\r';line++);
  *line = 0;
}

#ifdef __STDC__
void FixHeaderLine(char *line)
#else
void FixHeaderLine(line)
     char *line;
#endif
{
  int diff = 0;

  /* TODO: handle other line terminations */
  while (*line) {
    if (*line=='\n'&&(*(line+1)==' '||*(line+1)=='\t')) {
      diff += 1;
      line += 1;
    }
    else if (*line=='\n') {
      *line = 0;
    }
    *(line-diff) = *line;
    line++;
  }
}


char *LocalDup(s)
     char *s;
{
  char *new, *ptr;
  
  new = strdup(s);

  if (!new) return 0;

  for (ptr=new; *ptr; ptr++) {
    if (*ptr=='@') {
      *ptr = 0;
      break;
    }
  }

  return new;
}


char *DomainDup(s)
     char *s;
{
  for (; *s&&*s!='@'; s++);

  if (*s)
    s++;

  return strdup(s);
}
