/* -*-C-*-
*******************************************************************************
*
* File:         appnmail.m
* RCS:          /usr/local/sources/CVS/mailapp-utilities/appnmail.m,v 1.4 1997/04/19 17:37:18 tom Exp
* Description:  Append stdin to Mail.app mailbox
* Author:       Carl Edman
* Created:      Fri Mar 12 18:21:23 1993
* Modified:     Sat Nov 30 23:28:19 1996 Tom Hageman <tom@basil.icce.rug.nl>
* Language:     Objective C
* Package:      mailapp-utilities
* Status:       First release.
*
* (C) Copyright 1993, but otherwise this file is perfect freeware.
*
*******************************************************************************
*/

//#import <AppKit/AppKit.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#import <stdlib.h>
#import <unistd.h>
#import <stdio.h>
#import <string.h>
#import <strings.h>
#import <sys/file.h>
#import <sys/param.h>
#import <sys/types.h>
#import <sys/stat.h>
#import <varargs.h>
//#import <defaults/defaults.h>
#import "mailutil.h"
#import "mailtoc.h"
#import "optutil.h"
#import "regex.h"

#define USAGE "\
Usage: %s [-nv][-r|-f|-d|][-p pri][-H|-V] mbox...\n"

#define HELP "\
 -n           skip mailbox if locked\n\
 -v           talkative mode\n\
 -r           mark message as read\n\
 -f           mark message as flagged\n\
 -d           mark message as deleted\n\
 -p pri       set message's priority level to `pri'\n\
 -V           show version\n\
 -H           this help\n\
"

char *header=0,*content=0;
int headerlen=0,contentlen=0;
int headermaxlen=0,contentmaxlen=0;
char line[LINELEN];

/* ISO header conversion cruft. */
#import "iso2next.h"
/* Tables in there are expected to be in the "recode --header" format. */
/* XXX maybe we'd better use "recode --header --strict"? */

int iso_convert(char *line)
{
   static const struct
   {
      const char *name;
      const unsigned char *contents;
   }
   table[] = {
      /* The contents of these tables are defined in "iso2next.h".
	 If you add one there, don't forget to add it here too. */
      { "iso-8859-1", latin1_to_next },
      { "iso-8859-2", latin2_to_next },
   };
   static struct regex *isore = 0;
   const unsigned char *tt = 0;
   const char *name;
   int i, namelen;

   if (!isore) isore = re_compile("=?\\([^?]*\\)?Q?",1);
   if (re_match(line, isore) == 0) return 0;
   name = isore->braslist[0];
   namelen = (isore->braelist[0] - isore->braslist[0]);
   for (i = 0;  i < sizeof(table)/sizeof(table[0]);  i++)
   {
      if (strncasecmp(table[i].name, name, namelen) == 0 &&
	  table[i].name[namelen] == 0)
      {
	 tt = table[i].contents;
	 break;
      }
   }
   if (tt != 0)
   {
      int errors = 0;
      // hack to translate iso-stuff, in-place.
      char *s = (char *)isore->end, *t;

      for (; (t = strchr(s, '=')) != 0;  s = t)
      {
	 if (t > s && t[-1] == '?')
	 {
	    // ?= is end quote.
	    if (errors > 0)
	    {
	       fprintf(stderr, "%s: warning: MIME 8-bit header encoding `%.*s' incomplete conversion\n", progname(), namelen, name);
	       return -1;
	    }
	    strcpy(t-1, t+1);
	    strcpy((char *)isore->start, isore->end);
	    return 1;
	 }
	 else
	 {
	    //  parse "=XX" where XX is a 2-digit hex number
	    unsigned c = 0;
	    
	    sscanf(t+1, "%2x", &c);
	    if (c < 256 && tt[c] != 0)
	    {
	       *t = tt[c];
	       strcpy(t+1, t+3);
	    }
	    else
	       ++errors;
	    ++t;
	 }
      }
      fprintf(stderr, "%s: warning: Missing terminating `?=' in MIME 8-bit header `%s'\n", progname(), line);
      return 0;
   }
   fprintf(stderr, "%s: warning: MIME 8-bit header encoding `%.*s' is unsupported\n", progname(), namelen, name);
   return 0;
}

struct message_index *readmail(void)
{
   struct message_index *mi;
   int i,pri=0;
   char from[LINELEN]="",subject[LINELEN]="",reference[LINELEN]="";
   struct regex *fre=re_compile("^From:  *\\(.*\\)\n",0);
   struct regex *sre=re_compile("^Subject:  *\\(.*\\)\n",0);
   struct regex *dre=re_compile("^Date: .*\\([ 0123][0-9]\\) \\([A-Z][a-z][a-z][a-z]*\\) 1*9*\\([789][0-9]\\)",0);
   struct regex *rre=re_compile("^Next-Attachment: \\.tar\\.\\([0-9]*\\)\\.\\(.*\\)\\.attach, \\(E*,* *\\)\\([0-9]*\\), \\([0-9]*/[0-9]*\\), \\([0-9]*\\), \\([0-9]*\\)",0);
   struct regex *mre=re_compile("^Content-Type: \\([a-zA-Z]*/[a-zA-Z]*\\)",0);
   struct regex *pre=0;
   const char *pridef=0;
/*
   if (pridef=NXGetDefaultValue("Mail","PriorityHeader"))
   {
      static char buf[MAXPATHLEN];
      sprintf(buf,"^%s: [ \t]*\\([^ \t]*\\)[ \t]*\n",pridef);
      pre=re_compile(buf,0);
      pridef=NXGetDefaultValue("Mail","PriorityValues");
   }
*/
   mi=malloc(sizeof(*mi));
   
   mi->record_length=sizeof(*mi);
   mi->mes_offset=-1;
   mi->status='*';
   mi->msgtype=' ';
   mi->encrypted=' ';
   mi->sync=' ';
   mi->mes_date=message_current_date();

   headerlen=0;
   
   while(fgets(line,LINELEN,stdin))
   {
      if (*line=='\n') break;
      if (re_match(line,fre)==1)
      {
	 strpcpy(from,fre->braslist[0],fre->braelist[0]);
	 iso_convert(from);
      }
      if (re_match(line,sre)==1)
      {
	 strpcpy(subject,sre->braslist[0],sre->braelist[0]);
	 iso_convert(subject);
      }
      if (re_match(line,dre)==1)
	 mi->mes_date=message_date(atoi(dre->braslist[2])+1900,
				   dre->braslist[1],atoi(dre->braslist[0]));
      if (pre && pridef && (re_match(line,pre)==1))
      {
	 const char *beg,*end;

	 for(beg=pridef,pri=1;*beg;(beg=*end ? end+1 : end),(pri++))
	 {
	    if (!(end=index(beg,' '))) end=beg+strlen(beg);
	    if (((pre->braelist[0]-pre->braslist[0])==(end-beg))
		&&(strncmp(pre->braslist[0],beg,end-beg)==0))
	       break;
	 }
	 if (!*beg) pri=0;
      }
      if ((mi->msgtype==' ') && (re_match(line,mre)==1))
      {
	 if (!strpcaseequ("text/plain",mre->braslist[0],mre->braelist[0]))
	    mi->msgtype='m';
      }
      if (re_match(line,rre)==1)
      {
	 char *c;
	 mi->msgtype='r';
	 strpcpy(reference,rre->braslist[1],rre->braelist[1]);
	 c=reference+strlen(reference);
	 *c++='_';
	 while(c-reference<22) *c++='_';
	 *c='\0';
	 sprintf(c,"%ld",time(0));
	 strcat(reference,".attach");
	 c=reference+strlen(reference);
	 strcat(reference,", ");
	 if (*(rre->braslist[2])=='E')
	 {
	    strcat(reference,"E, ");
	    mi->encrypted='e';
	 }
	 strpcat(reference,rre->braslist[4],rre->braelist[4]);
	 strcat(reference,"\n");
	 appstring(&header,&headerlen,&headermaxlen,"Next-Reference: ",
		   strlen("Next-Reference: "));
	 appstring(&header,&headerlen,&headermaxlen,reference,strlen(reference));
	 *c='\0';
      }
      else
      {
	 appstring(&header,&headerlen,&headermaxlen,line,strlen(line));
      }
   }
   appstring(&header,&headerlen,&headermaxlen,"\n",1);

   contentlen=0;
   
   while((i=fread(growstring(&content,&contentlen,&contentmaxlen,LINELEN*16),
		  1,16*LINELEN,stdin))>0)
      contentlen+=i;
   if (content[contentlen-1]!='\n')
      appstring(&content,&contentlen,&contentmaxlen,"\n",1);

   mi->mes_length=headerlen;
   if (mi->msgtype!='r') mi->mes_length+=contentlen;
   mi->record_length+=strlen(from)+1+strlen(subject)+1+strlen(reference)+1;
   mi->record_length+=sizeof(int)+sizeof(time_t)+sizeof(int);
   mi=realloc(mi,mi->record_length);
   strcpy(mi->data,from);
   strcpy(mi->data+strlen(from)+1,subject); 
   strcpy(mi->data+strlen(from)+1+strlen(subject)+1,reference);

   message_set_attachsize(mi,0);
   message_set_attachtime(mi,0);
   message_set_priority(mi,pri);
   
   free(fre); free(sre); free(rre); free(dre);
   if (pre) free(pre);
   return mi;
}

const char *get_command(const char *defaultKey, ...)
{
   /* Find command named `defaultKey' in Mail's defaults database, and check
      if it is an executable.  If not, try each of the defaults given on
      the (NULL-terminated) argument list in turn. */
   const char *command = NULL; //NXGetDefaultValue("Mail", defaultKey);
   va_list ap;

   va_start(ap/*, defaultKey*/);
   do
   {
      struct stat st;
      if (command == NULL) continue;
      if (command[0] != '/') break; /* XXX should check PATH? */
      if (access(command, X_OK) < 0) continue;
      if (stat(command, &st) < 0) continue;
      if ((st.st_mode & S_IFMT) == S_IFREG) break;
   }
   while ((command = va_arg(ap, const char *)) != NULL);
   va_end(ap);
   if (command == NULL)
   {
      fprintf(stderr,"%s: cannot find executable for `%s'.\n", progname(), defaultKey);
   }
   return command;
}

void main(int ac,char *av[])
{
   struct message_index *mi;
   const char *decodeCmd=0;
   const char*uncompressCmd=0;
   const char *tarCmd=0;
   int c;
   int readflg=0,nowaitflg=0,verboseflg=0,multiflg=0,flagflg=0,deleteflg=0;
   int pri=-1;
   time_t mboxtime;
   int mfd=-1,tocfd=-1;
   FILE *tocf=NULL;
   struct table_of_contents_header *toch=0;
   int status=EXIT_SUCCESS;

   set_progname(av[0]);

   while((c=getopt(ac,av,"rfdvnp:VH"))!=EOF) switch(c)
   {
    case 'r':
      readflg++;
      if (flagflg||deleteflg) status=EXIT_USAGE;
      break;
    case 'f':
      flagflg++;
      if (readflg||deleteflg) status=EXIT_USAGE;
      break;
    case 'd':
      deleteflg++;
      if (readflg||flagflg) status=EXIT_USAGE;
      break;
    case 'n':
      nowaitflg++;
      break;
    case 'v':
      verboseflg++;
      break;
    case 'm': // XXX currently unused.
      multiflg++;
      break;
    case 'p':
      if (pri!=-1) status=EXIT_USAGE;
      pri=atoi(optarg);
      if (pri<0) status=EXIT_USAGE;
      break;
    case 'V':
      status=EXIT_VERSION;
      break;
    case 'H':
      status=EXIT_HELP;
      break;
    case '?':
    default:
      status=EXIT_USAGE;
   }
   if (status==EXIT_SUCCESS && optind>=ac) status=EXIT_USAGE;
   handle_usage_help_version(status, USAGE, HELP);

   mi=readmail();

   if (!header || (headerlen<5) || strncmp(header,"From ",5))
   {
      fprintf(stderr,"%s: message is not in UNIX mailbox format\n", progname());
      exit(EXIT_FAILURE);
   }

   if (readflg) mi->status=' ';
   else if (flagflg) mi->status='+';
   else if (deleteflg) mi->status='d';

   if (pri>=0) message_set_priority(mi,pri);
   
   for(;optind<ac;optind++)
   {
      if (verboseflg) fprintf(stderr,"Entering %s...\n",av[optind]);
      if (cd_mbox(av[optind],1)) goto next;
      if (lock_mbox(nowaitflg)) goto next;

      mboxtime=mtime("mbox");
      
      if ((mfd=open("mbox",O_WRONLY|O_APPEND|O_CREAT,0644))==-1)
      {
	 fprintf(stderr,"%s: %s: %s\n",progname(),av[optind],strerror(errno));
	 status=EXIT_FAILURE;
	 goto unlock;
      }

      /* Even if we cannot update the table of contents,
         we'll try our damnedest to deliver the message anyway. */

      if ((tocfd=open("table_of_contents",O_CREAT|O_RDWR,0644))==-1 ||
	  (tocf=fdopen(tocfd,"r+b"))==NULL)
      {
	 fprintf(stderr,"%s: %s: %s\n",progname(),av[optind],strerror(errno));
	 status=EXIT_FAILURE;
	 //goto unlock;
      }
      else
      {
	 toch=get_table_of_contents_header(tocf,1);
	 if (!toch)
	 {
	    fprintf(stderr,"%s: %s: warning: invalid table_of_contents\n",progname(),av[optind]);
	    status=EXIT_FAILURE;
	 }
	 else if (toch->mbox_time!=mboxtime)
	 {
	    fprintf(stderr,"%s: %s: warning: table_of_contents out of sync\n",progname(),av[optind]);
	    status=EXIT_FAILURE;
	 }
      }

      /* FOR EACH */
      lseek(mfd,0,SEEK_END);
      mi->mes_offset=lseek(mfd,0,SEEK_CUR);

      if (mi->msgtype=='r')
      {
	 char path[MAXPATHLEN];
	 FILE *f;

	 /* Locate executables, if not already done so. */
	 if (!decodeCmd && !(decodeCmd=get_command("appnmailDecodeCommand","/NextApps/Mail.app/decode",NULL)))
	 {
	    /* ...not really a regular Mail default. */
	    status=EXIT_FAILURE;
	    goto unlock;
	 }
	 if (!uncompressCmd && !(uncompressCmd=get_command("UncompressCommand","/usr/bin/gunzip","/usr/ucb/uncompress",NULL)))
	 {
	    status=EXIT_FAILURE;
	    goto unlock;
	 }
	 if (!tarCmd && !(tarCmd=get_command("TarCommand","/NextApps/Mail.app/safetar","/usr/bin/gnutar",NULL)))
	 {
	    status=EXIT_FAILURE;
	    goto unlock;
	 }
	 strcpy(path,message_reference(mi));
	 if ((mkdir(path,0755))==-1)
	 {
	    fprintf(stderr,"%s: %s: %s: %s\n",progname(),av[optind],path,strerror(errno));
	    status=EXIT_FAILURE;
	    goto unlock;
	 }
	 sprintf(line,"cd %s && %s | %s | %s xf -",path,decodeCmd,uncompressCmd,tarCmd);
	 if (!(f=popen(line,"w")))
	 {
	    fprintf(stderr,"%s: %s: %s: %s\n",progname(),av[optind],path,strerror(errno));
	    status=EXIT_FAILURE;
	    goto unlock;
	 }
	 fwrite(content,contentlen,1,f);
	 if ((c=pclose(f))!=0)
	 {
	    fprintf(stderr,"%s: %s: %s failed (status %d)\n",progname(),av[optind],line,c);
	    status=EXIT_FAILURE;
	    goto unlock;
	 }
         message_set_attachsize(mi,dirsize(path));
         message_set_attachtime(mi,mtime(path));
	 /* Add message body *after* all failure points, to avoid TOC corruption. */
	 write(mfd,header,headerlen);
         write(mfd,"\n",1);
	 // XXX should check status.
      }
      else
      {
	 write(mfd,header,headerlen);
         write(mfd,content,contentlen);
	 // XXX should check status.
      }
      if (toch)
      {
	 toch->num_msgs++;
	 fseek(tocf,0,SEEK_END);
	 put_message_index(tocf,mi);
      }
      /* END FOR EACH */
      
      close(mfd), mfd=-1;
      if (toch)
      {
	 if (toch->mbox_time==mboxtime) toch->mbox_time=mtime("mbox");
	 fseek(tocf,0,SEEK_SET);
	 put_table_of_contents_header(tocf,toch);
      }
    unlock:
      if (tocf!=NULL) fclose(tocf), tocf=NULL;
      else if (tocfd>=0) close(tocfd), tocfd=-1;
      if (mfd>=0) close(mfd), mfd=-1;
      unlock_mbox();
    next:;
      if (toch) { free(toch); toch=0; }
      uncd_mbox();
   }
   exit(status);
}
