/*
* marshal - gather mail message for transmission
*/
#include "common.h"
#include <ctype.h>

typedef struct Attach Attach;
typedef struct Alias Alias;
typedef struct Addr Addr;
typedef struct Ctype Ctype;

struct Attach {
       Attach  *next;
       char    *path;
       char    *type;
       int     ainline;
       Ctype   *ctype;
};

struct Alias
{
       Alias   *next;
       int     n;
       Addr    *addr;
};

struct Addr
{
       Addr    *next;
       char    *v;
};

enum {
       Hfrom,
       Hto,
       Hcc,
       Hbcc,
       Hsender,
       Hreplyto,
       Hinreplyto,
       Hdate,
       Hsubject,
       Hmime,
       Hpriority,
       Hmsgid,
       Hcontent,
       Hx,
       Hprecedence,
       Hattach,
       Hinclude,
       Nhdr,
};

enum {
       PGPsign = 1,
       PGPencrypt = 2,
};

char *hdrs[Nhdr] = {
[Hfrom]         "from:",
[Hto]           "to:",
[Hcc]           "cc:",
[Hbcc]          "bcc:",
[Hreplyto]      "reply-to:",
[Hinreplyto]    "in-reply-to:",
[Hsender]       "sender:",
[Hdate]         "date:",
[Hsubject]      "subject:",
[Hpriority]     "priority:",
[Hmsgid]        "message-id:",
[Hmime]         "mime-",
[Hcontent]      "content-",
[Hx]            "x-",
[Hprecedence]   "precedence",
[Hattach]       "attach:",
[Hinclude]      "include:",
};

struct Ctype {
       char    *type;
       char    *ext;
       int     display;
};

Ctype ctype[] = {
       { "text/plain",                 "txt",  1,      },
       { "text/html",                  "html", 1,      },
       { "text/html",                  "htm",  1,      },
       { "text/tab-separated-values",  "tsv",  1,      },
       { "text/richtext",              "rtx",  1,      },
       { "message/rfc822",             "txt",  1,      },
       { "",                           0,      0,      },
};

Ctype *mimetypes;

int pid = -1;
int pgppid = -1;

void    Bdrain(Biobuf*);
void    attachment(Attach*, Biobuf*);
void    body(Biobuf*, Biobuf*, int);
int     doublequote(Fmt*);
void*   emalloc(int);
void*   erealloc(void*, int);
char*   estrdup(char*);
Addr*   expand(int, char**);
Addr*   expandline(String**, Addr*);
void    freeaddr(Addr*);
void    freeaddr(Addr *);
void    freeaddrs(Addr*);
void    freealias(Alias*);
void    freealiases(Alias*);
Attach* mkattach(char*, char*, int);
char*   mkboundary(void);
char*   hdrval(char*);
char*   mksubject(char*);
int     pgpfilter(int*, int, int);
int     pgpopts(char*);
int     printcc(Biobuf*, Addr*);
int     printdate(Biobuf*);
int     printfrom(Biobuf*);
int     printinreplyto(Biobuf*, char*);
int     printsubject(Biobuf*, char*);
int     printto(Biobuf*, Addr*);
Alias*  readaliases(void);
int     readheaders(Biobuf*, int*, String**, Addr**, Addr**, Addr**, Attach**, int);
void    readmimetypes(void);
int     rfc2047fmt(Fmt*);
int     sendmail(Addr*, Addr*, Addr*, int*, char*);
char*   waitforsubprocs(void);

int rflag, lbflag, xflag, holding, nflag, Fflag, eightflag, dflag;
int pgpflag = 0;
char *user;
char *login;
Alias *aliases;
int rfc822syntaxerror;
char lastchar;
char *replymsg;

enum
{
       Ok = 0,
       Nomessage = 1,
       Nobody = 2,
       Error = -1,
};

#pragma varargck        type    "Z"     char*
#pragma varargck        type    "U"     char*

void
usage(void)
{
       fprint(2, "usage: %s [-Fr#xn] [-s subject] [-C ccrecipient] [-t type]"
           " [-aA attachment] [-p[es]] [-R replymsg] -8 | recipient-list\n",
               argv0);
       exits("usage");
}

void
fatal(char *fmt, ...)
{
       char buf[1024];
       va_list arg;

       if(pid >= 0)
               postnote(PNPROC, pid, "die");
       if(pgppid >= 0)
               postnote(PNPROC, pgppid, "die");

       va_start(arg, fmt);
       vseprint(buf, buf+sizeof(buf), fmt, arg);
       va_end(arg);
       fprint(2, "%s: %s\n", argv0, buf);
       holdoff(holding);
       exits(buf);
}

static void
bwritesfree(Biobuf *bp, String **str)
{
       if(Bwrite(bp, s_to_c(*str), s_len(*str)) != s_len(*str))
               fatal("write error");
       s_free(*str);
       *str = nil;
}

void
main(int argc, char **argv)
{
       int ccargc, bccargc, flags, fd, noinput, headersrv;
       char *subject, *type, *boundary;
       char *ccargv[32], *bccargv[32];
       Addr *to, *cc, *bcc;
       Attach *first, **l, *a;
       Biobuf in, out, *b;
       String *hdrstring;
       char file[Pathlen];

       noinput = 0;
       subject = nil;
       first = nil;
       l = &first;
       type = nil;
       hdrstring = nil;
       ccargc = bccargc = 0;

       quotefmtinstall();
       fmtinstall('Z', doublequote);
       fmtinstall('U', rfc2047fmt);

       ARGBEGIN{
       case 'a':
               flags = 0;
               goto aflag;
       case 'A':
               flags = 1;
       aflag:
               a = mkattach(EARGF(usage()), type, flags);
               if(a == nil)
                       exits("bad args");
               type = nil;
               *l = a;
               l = &a->next;
               break;
       case 'C':
               if(ccargc >= nelem(ccargv)-1)
                       sysfatal("too many cc's");
               ccargv[ccargc++] = EARGF(usage());
               break;
       case 'B':
               if(bccargc >= nelem(bccargv)-1)
                       sysfatal("too many bcc's");
               bccargv[bccargc++] = EARGF(usage());
               break;
       case 'd':
               dflag = 1;              /* for sendmail */
               break;
       case 'F':
               Fflag = 1;              /* file message */
               break;
       case 'n':                       /* no standard input */
               nflag = 1;
               break;
       case 'p':                       /* pgp flag: encrypt, sign, or both */
               if(pgpopts(EARGF(usage())) < 0)
                       sysfatal("bad pgp options");
               break;
       case 'r':
               rflag = 1;              /* for sendmail */
               break;
       case 'R':
               replymsg = EARGF(usage());
               break;
       case 's':
               subject = EARGF(usage());
               break;
       case 't':
               type = EARGF(usage());
               break;
       case 'x':
               xflag = 1;              /* for sendmail */
               break;
       case '8':                       /* read recipients from rfc822 header */
               eightflag = 1;
               break;
       case '#':
               lbflag = 1;             /* for sendmail */
               break;
       default:
               usage();
               break;
       }ARGEND;

       login = getlog();
       user = getenv("upasname");
       if(user == nil || *user == 0)
               user = login;
       if(user == nil || *user == 0)
               sysfatal("can't read user name");

       if(Binit(&in, 0, OREAD) < 0)
               sysfatal("can't Binit 0: %r");

       if(nflag && eightflag)
               sysfatal("can't use both -n and -8");
       if(!eightflag && argc < 1)
               usage();

       aliases = readaliases();
       to = cc = bcc = nil;
       if(argc > 0)
               to = expand(argc, argv);
       if(ccargc > 0)
               cc = expand(ccargc, ccargv);
       if(bccargc > 0)
               bcc = expand(bccargc, bccargv);

       flags = 0;
       headersrv = Nomessage;
       if(!nflag && !xflag && !lbflag && !dflag) {
               /*
                * pass through headers, keeping track of which we've seen,
                * perhaps building to list.
                */
               holding = holdon();
               headersrv = readheaders(&in, &flags, &hdrstring,
                       eightflag? &to: nil, eightflag? &cc: nil, eightflag? &bcc: nil, l, 1);
               if(rfc822syntaxerror){
                       Bdrain(&in);
                       fatal("rfc822 syntax error, message not sent");
               }
               if(to == nil){
                       Bdrain(&in);
                       fatal("no addresses found, message not sent");
               }

               switch(headersrv){
               case Error:                     /* error */
                       fatal("reading");
                       break;
               case Nomessage: /* no message, just exit mimicking old behavior */
                       noinput = 1;
                       if(first == nil)
                               exits(0);
                       break;
               }
       }

       fd = sendmail(to, cc, bcc, &pid, Fflag ? argv[0] : nil);
       if(fd < 0)
               sysfatal("execing sendmail: %r\n:");
       if(xflag || lbflag || dflag){
               close(fd);
               exits(waitforsubprocs());
       }

       if(Binit(&out, fd, OWRITE) < 0)
               fatal("can't Binit 1: %r");

       if(!nflag)
               bwritesfree(&out, &hdrstring);

       /* read user's standard headers */
       mboxpathbuf(file, sizeof file, user, "headers");
       if(b = Bopen(file, OREAD)){
               if (readheaders(b, &flags, &hdrstring, nil, nil, nil, nil, 0) == Error)
                       fatal("reading");
               Bterm(b);
               bwritesfree(&out, &hdrstring);
       }

       /* add any headers we need */
       if((flags & (1<<Hdate)) == 0)
               if(printdate(&out) < 0)
                       fatal("writing");
       if((flags & (1<<Hfrom)) == 0)
               if(printfrom(&out) < 0)
                       fatal("writing");
       if((flags & (1<<Hto)) == 0)
               if(printto(&out, to) < 0)
                       fatal("writing");
       if((flags & (1<<Hcc)) == 0)
               if(printcc(&out, cc) < 0)
                       fatal("writing");
       if((flags & (1<<Hsubject)) == 0 && subject != nil)
               if(printsubject(&out, subject) < 0)
                       fatal("writing");
       if(replymsg != nil)
               if(printinreplyto(&out, replymsg) < 0)
                       fatal("writing");
       Bprint(&out, "MIME-Version: 1.0\n");

       if(pgpflag){
               /* interpose pgp process between us and sendmail to handle body */
               Bflush(&out);
               Bterm(&out);
               fd = pgpfilter(&pgppid, fd, pgpflag);
               if(Binit(&out, fd, OWRITE) < 0)
                       fatal("can't Binit 1: %r");
       }

       /* if attachments, stick in multipart headers */
       boundary = nil;
       if(first != nil){
               boundary = mkboundary();
               Bprint(&out, "Content-Type: multipart/mixed;\n");
               Bprint(&out, "\tboundary=\"%s\"\n\n", boundary);
               Bprint(&out, "This is a multi-part message in MIME format.\n");
               Bprint(&out, "--%s\n", boundary);
               Bprint(&out, "Content-Disposition: inline\n");
       }

       if(!nflag){
               if(!noinput && headersrv == Ok)
                       body(&in, &out, 1);
       } else
               Bprint(&out, "\n");
       holdoff(holding);

       Bflush(&out);
       for(a = first; a != nil; a = a->next){
               if(lastchar != '\n')
                       Bprint(&out, "\n");
               Bprint(&out, "--%s\n", boundary);
               attachment(a, &out);
       }

       if(first != nil){
               if(lastchar != '\n')
                       Bprint(&out, "\n");
               Bprint(&out, "--%s--\n", boundary);
       }

       Bterm(&out);
       close(fd);
       exits(waitforsubprocs());
}

/* evaluate pgp option string */
int
pgpopts(char *s)
{
       if(s == nil || s[0] == '\0')
               return -1;
       while(*s){
               switch(*s++){
               case 's':  case 'S':
                       pgpflag |= PGPsign;
                       break;
               case 'e': case 'E':
                       pgpflag |= PGPencrypt;
                       break;
               default:
                       return -1;
               }
       }
       return 0;
}

/*
* read headers from stdin into a String, expanding local aliases,
* keep track of which headers are there, which addresses we have
* remove Bcc: line.
*/
int
readheaders(Biobuf *in, int *fp, String **sp, Addr **top, Addr **ccp, Addr **bccp, Attach **att, int strict)
{
       int i, seen, hdrtype;
       Addr *to, *cc, *bcc;
       String *s, *sline;
       char *p;

       s = s_new();
       to = cc = bcc = nil;
       sline = nil;
       hdrtype = -1;
       seen = 0;
       for(;;) {
               if((p = Brdline(in, '\n')) != nil) {
                       seen = 1;
                       p[Blinelen(in)-1] = 0;

                       /* coalesce multiline headers */
                       if((*p == ' ' || *p == '\t') && sline){
                               s_append(sline, "\n");
                               s_append(sline, p);
                               p[Blinelen(in)-1] = '\n';
                               continue;
                       }
               }

               /* process the current header, it's all been read */
               if(sline) {
                       switch(hdrtype){
                       default:
                       Addhdr:
                               s_append(s, s_to_c(sline));
                               s_append(s, "\n");
                               break;
                       case Hto:
                               if(top)
                                       to = expandline(&sline, to);
                               goto Addhdr;
                       case Hcc:
                               if(ccp)
                                       cc = expandline(&sline, cc);
                               goto Addhdr;
                       case Hbcc:
                               if(bccp)
                                       bcc = expandline(&sline, bcc);
                               break;
                       case Hsubject:
                               s_append(s, mksubject(s_to_c(sline)));
                               s_append(s, "\n");
                               break;
                       case Hattach:
                       case Hinclude:
                               if(att == nil)
                                       break;
                               *att = mkattach(hdrval(s_to_c(sline)), nil, hdrtype == Hinclude);
                               if(*att != nil)
                                       att = &(*att)->next;
                               break;
                       }
                       s_free(sline);
                       sline = nil;
               }

               if(p == nil)
                       break;

               /* if no :, it's not a header, seek back and break */
               if(strchr(p, ':') == nil){
                       p[Blinelen(in)-1] = '\n';
                       Bseek(in, -Blinelen(in), 1);
                       break;
               }

               sline = s_copy(p);

               /*
                * classify the header.  If we don't recognize it, break.
                * This is to take care of users who start messages with
                * lines that contain ':'s but that aren't headers.
                * This is a bit hokey.  Since I decided to let users type
                * headers, I need some way to distinguish.  Therefore,
                * marshal tries to know all likely headers and will indeed
                * screw up if the user types an unlikely one.  -- presotto
                */
               hdrtype = -1;
               for(i = 0; i < nelem(hdrs); i++){
                       if(cistrncmp(hdrs[i], p, strlen(hdrs[i])) == 0){
                               *fp |= 1<<i;
                               hdrtype = i;
                               break;
                       }
               }
               if(strict){
                       if(hdrtype == -1){
                               p[Blinelen(in)-1] = '\n';
                               Bseek(in, -Blinelen(in), 1);
                               break;
                       }
               } else
                       hdrtype = 0;
               p[Blinelen(in)-1] = '\n';
       }

       *sp = s;

       if(to){
               freeaddrs(*top);
               *top = to;
       }else
               freeaddrs(to);

       if(cc){
               freeaddrs(*ccp);
               *ccp = cc;
       }else
               freeaddrs(cc);

       if(bcc){
               freeaddrs(*bccp);
               *bccp = bcc;
       }else
               freeaddrs(bcc);

       if(seen == 0){
               if(Blinelen(in) == 0)
                       return Nomessage;
               else
                       return Ok;
       }
       if(p == nil)
               return Nobody;
       return Ok;
}

/* pass the body to sendmail, make sure body starts and ends with a newline */
void
body(Biobuf *in, Biobuf *out, int docontenttype)
{
       char *buf, *p;
       int i, n, len;

       n = 0;
       len = 16*1024;
       buf = emalloc(len);

       /* first char must be newline */
       i = Bgetc(in);
       if(i > 0){
               if(i != '\n')
                       buf[n++] = '\n';
               buf[n++] = i;
       } else
               buf[n++] = '\n';

       /* read into memory */
       if(docontenttype){
               while(docontenttype){
                       if(n == len){
                               len += len >> 2;
                               buf = realloc(buf, len);
                               if(buf == nil)
                                       sysfatal("%r");
                       }
                       p = buf+n;
                       i = Bread(in, p, len - n);
                       if(i < 0)
                               fatal("input error2");
                       if(i == 0)
                               break;
                       n += i;
                       for(; i > 0; i--)
                               if((*p++ & 0x80) && docontenttype){
                                       Bprint(out, "Content-Type: text/plain; charset=\"UTF-8\"\n");
                                       Bprint(out, "Content-Transfer-Encoding: 8bit\n");
                                       docontenttype = 0;
                                       break;
                               }
               }
               if(docontenttype){
                       Bprint(out, "Content-Type: text/plain; charset=\"US-ASCII\"\n");
                       Bprint(out, "Content-Transfer-Encoding: 7bit\n");
               }
       }

       /* write what we already read */
       if(Bwrite(out, buf, n) < 0)
               fatal("output error");
       if(n > 0)
               lastchar = buf[n-1];
       else
               lastchar = '\n';


       /* pass the rest */
       for(;;){
               n = Bread(in, buf, len);
               if(n < 0)
                       fatal("input error2");
               if(n == 0)
                       break;
               if(Bwrite(out, buf, n) < 0)
                       fatal("output error");
               lastchar = buf[n-1];
       }
}

/*
* pass the body to sendmail encoding with base64
*
*  the size of buf is very important to enc64.  Anything other than
*  a multiple of 3 will cause enc64 to output a termination sequence.
*  To ensure that a full buf corresponds to a multiple of complete lines,
*  we make buf a multiple of 3*18 since that's how many enc64 sticks on
*  a single line.  This avoids short lines in the output which is pleasing
*  but not necessary.
*/
static int
enc64x18(char *out, int lim, uchar *in, int n)
{
       int m, mm, nn;

       for(nn = 0; n > 0; n -= m, nn += mm){
               m = 18 * 3;
               if(m > n)
                       m = n;
               nn++;   /* \n */
               assert(nn < lim);
               mm = enc64(out, lim - nn, in, m);
               assert(mm > 0);
               in += m;
               out += mm;
               *out++ = '\n';
       }
       return nn;
}

void
body64(Biobuf *in, Biobuf *out)
{
       int m, n;
       uchar buf[3*18*54];
       char obuf[3*18*54*2];

       Bprint(out, "\n");
       for(;;){
               n = Bread(in, buf, sizeof(buf));
               if(n < 0)
                       fatal("input error");
               if(n == 0)
                       break;
               m = enc64x18(obuf, sizeof(obuf), buf, n);
               if(Bwrite(out, obuf, m) < 0)
                       fatal("output error");
       }
       lastchar = '\n';
}

/* pass message to sendmail, make sure body starts with a newline */
void
copy(Biobuf *in, Biobuf *out)
{
       int n;
       char buf[4*1024];

       for(;;){
               n = Bread(in, buf, sizeof(buf));
               if(n < 0)
                       fatal("input error");
               if(n == 0)
                       break;
               if(Bwrite(out, buf, n) < 0)
                       fatal("output error");
       }
}

void
attachment(Attach *a, Biobuf *out)
{
       Biobuf *f;
       char *p;

       /* if it's already mime encoded, just copy */
       if(strcmp(a->type, "mime") == 0){
               f = Bopen(a->path, OREAD);
               if(f == nil){
                       /*
                        * hack: give marshal time to stdin, before we kill it
                        * (for dead.letter)
                        */
                       sleep(500);
                       postnote(PNPROC, pid, "interrupt");
                       sysfatal("opening %s: %r", a->path);
               }
               copy(f, out);
               Bterm(f);
       }

       /* if it's not already mime encoded ... */
       if(strcmp(a->type, "text/plain") != 0)
               Bprint(out, "Content-Type: %s\n", a->type);

       if(a->ainline)
               Bprint(out, "Content-Disposition: inline\n");
       else {
               p = strrchr(a->path, '/');
               if(p == nil)
                       p = a->path;
               else
                       p++;
               Bprint(out, "Content-Disposition: attachment; filename=%Z\n", p);
       }

       f = Bopen(a->path, OREAD);
       if(f == nil){
               /*
                * hack: give marshal time to stdin, before we kill it
                * (for dead.letter)
                */
               sleep(500);
               postnote(PNPROC, pid, "interrupt");
               sysfatal("opening %s: %r", a->path);
       }

       /* dump our local 'From ' line when passing along mail messages */
       if(strcmp(a->type, "message/rfc822") == 0){
               p = Brdline(f, '\n');
               if(strncmp(p, "From ", 5) != 0)
                       Bseek(f, 0, 0);
       }
       if(a->ctype->display)
               body(f, out, strcmp(a->type, "text/plain") == 0);
       else {
               Bprint(out, "Content-Transfer-Encoding: base64\n");
               body64(f, out);
       }
       Bterm(f);
}

char *ascwday[] =
{
       "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

char *ascmon[] =
{
       "Jan", "Feb", "Mar", "Apr", "May", "Jun",
       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

int
printdate(Biobuf *b)
{
       int tz;
       Tm *tm;

       tm = localtime(time(0));
       tz = (tm->tzoff/3600)*100 + (tm->tzoff/60)%60;

       return Bprint(b, "Date: %s, %d %s %d %2.2d:%2.2d:%2.2d %s%.4d\n",
               ascwday[tm->wday], tm->mday, ascmon[tm->mon], 1900 + tm->year,
               tm->hour, tm->min, tm->sec, tz>=0?"+":"", tz);
}

int
printfrom(Biobuf *b)
{
       return Bprint(b, "From: %s\n", user);
}

int
printaddr(Biobuf *b, char *s, Addr *a)
{
       int i;

       if(a == nil)
               return 0;
       if(Bprint(b, "%s %s", s, a->v) < 0)
               return -1;
       i = 0;
       for(a = a->next; a != nil; a = a->next)
               if(Bprint(b, "%s%s", ((i++ & 7) == 7)?",\n\t":", ", a->v) < 0)
                       return -1;
       if(Bprint(b, "\n") < 0)
               return -1;
       return 0;
}

int
printto(Biobuf *b, Addr *a)
{
       return printaddr(b, "To:", a);
}

int
printcc(Biobuf *b, Addr *a)
{
       return printaddr(b, "Cc:", a);
}

int
printsubject(Biobuf *b, char *subject)
{
       return Bprint(b, "Subject: %U\n", subject);
}

int
printinreplyto(Biobuf *out, char *dir)
{
       int fd, n;
       char buf[256];
       String *s = s_copy(dir);

       s_append(s, "/messageid");
       fd = open(s_to_c(s), OREAD);
       s_free(s);
       if(fd < 0)
               return 0;
       n = read(fd, buf, sizeof(buf)-1);
       close(fd);
       if(n <= 0)
               return 0;
       buf[n] = 0;
       return Bprint(out, "In-Reply-To: <%s>\n", buf);
}

int
hassuffix(char *a, char *b)
{
       int na, nb;

       na = strlen(a), nb = strlen(b);
       if(na <= nb + 1 || a[na - nb - 1] != '.')
               return 0;
       return strcmp(a + (na - nb), b) == 0;
}

Attach*
mkattach(char *file, char *type, int ainline)
{
       int n, pfd[2];
       char ftype[64];
       Attach *a;
       Ctype *c;

       if(file == nil)
               return nil;
       if(access(file, 4) == -1){
               fprint(2, "%s: %s can't read file\n", argv0, file);
               return nil;
       }
       a = emalloc(sizeof(*a));
       a->path = estrdup(file);
       a->next = nil;
       a->type = type;
       a->ainline = ainline;
       a->ctype = nil;
       if(type != nil){
               for(c = ctype; ; c++)
                       if(strncmp(type, c->type, strlen(c->type)) == 0){
                               a->ctype = c;
                               break;
                       }
               return a;
       }

       /* pick a type depending on extension */
       for(c = ctype; c->ext != nil; c++)
               if(hassuffix(file, c->ext)){
                       a->type = c->type;
                       a->ctype = c;
                       return a;
               }

       /* try the mime types file */
       if(mimetypes == nil)
               readmimetypes();
       for(c = mimetypes; c != nil && c->ext != nil; c++)
               if(hassuffix(file, c->ext)){
                       a->type = c->type;
                       a->ctype = c;
                       return a;
               }

       /* run file to figure out the type */
       a->type = "application/octet-stream";   /* safest default */
       if(pipe(pfd) < 0)
               return a;
       switch(fork()){
       case -1:
               break;
       case 0:
               close(pfd[1]);
               close(0);
               dup(pfd[0], 0);
               close(1);
               dup(pfd[0], 1);
               execl("/bin/file", "file", "-m", file, nil);
               exits(0);
       default:
               close(pfd[0]);
               n = read(pfd[1], ftype, sizeof(ftype));
               if(n > 0){
                       ftype[n-1] = 0;
                       a->type = estrdup(ftype);
               }
               close(pfd[1]);
               waitpid();
               break;
       }

       for(c = ctype; ; c++)
               if(strncmp(a->type, c->type, strlen(c->type)) == 0){
                       a->ctype = c;
                       break;
               }
       return a;
}

char*
mkboundary(void)
{
       int i;
       char buf[32];

       srand((time(0)<<16)|getpid());
       strcpy(buf, "upas-");
       for(i = 5; i < sizeof(buf)-1; i++)
               buf[i] = 'a' + nrand(26);
       buf[i] = 0;
       return estrdup(buf);
}

/* copy types to two fd's */
static void
tee(int in, int out1, int out2)
{
       int n;
       char buf[8*1024];

       while ((n = read(in, buf, sizeof buf)) > 0)
               if (write(out1, buf, n) != n ||
                   write(out2, buf, n) != n)
                       break;
}

/* print the unix from line */
int
printunixfrom(int fd)
{
       int tz;
       Tm *tm;

       tm = localtime(time(0));
       tz = (tm->tzoff/3600)*100 + (tm->tzoff/60)%60;

       return fprint(fd, "From %s %s %s %d %2.2d:%2.2d:%2.2d %s%.4d %d\n",
               user,
               ascwday[tm->wday], ascmon[tm->mon], tm->mday,
               tm->hour, tm->min, tm->sec, tz>=0?"+":"", tz, 1900 + tm->year);
}

char *specialfile[] =
{
       "pipeto",
       "pipefrom",
       "L.mbox",
       "forward",
       "names"
};

/* return 1 if this is a special file */
static int
special(String *s)
{
       int i;
       char *p;

       p = strrchr(s_to_c(s), '/');
       if(p == nil)
               p = s_to_c(s);
       else
               p++;
       for(i = 0; i < nelem(specialfile); i++)
               if(strcmp(p, specialfile[i]) == 0)
                       return 1;
       return 0;
}

/* start up sendmail and return an fd to talk to it with */
int
sendmail(Addr *to, Addr *cc, Addr *bcc, int *pid, char *rcvr)
{
       int ac, fd, pfd[2];
       char **v, cmd[Pathlen];
       Addr *a;
       Biobuf *b;

       ac = 0;
       for(a = to; a != nil; a = a->next)
               ac++;
       for(a = cc; a != nil; a = a->next)
               ac++;
       for(a = bcc; a != nil; a = a->next)
               ac++;
       v = emalloc(sizeof(char*)*(ac+20));
       ac = 0;
       v[ac++] = "sendmail";
       if(xflag)
               v[ac++] = "-x";
       if(rflag)
               v[ac++] = "-r";
       if(lbflag)
               v[ac++] = "-#";
       if(dflag)
               v[ac++] = "-d";
       for(a = to; a != nil; a = a->next)
               v[ac++] = a->v;
       for(a = cc; a != nil; a = a->next)
               v[ac++] = a->v;
       for(a = bcc; a != nil; a = a->next)
               v[ac++] = a->v;
       v[ac] = 0;

       if(pipe(pfd) < 0)
               fatal("%r");
       switch(*pid = rfork(RFFDG|RFREND|RFPROC|RFENVG)){
       case -1:
               fatal("%r");
               break;
       case 0:
               if(holding)
                       close(holding);
               close(pfd[1]);
               dup(pfd[0], 0);
               close(pfd[0]);

               if(rcvr != nil){
                       if(pipe(pfd) < 0)
                               fatal("%r");
                       switch(fork()){
                       case -1:
                               fatal("%r");
                               break;
                       case 0:
                               close(pfd[0]);
                               b = 0;
                               /* BOTCH; "From " time gets changed */
                               if(rcvr)
                                       b = openfolder(foldername(nil, user, rcvr), time(0));
                               fd = b? Bfildes(b): -1;
                               printunixfrom(fd);
                               tee(0, pfd[1], fd);
                               write(fd, "\n", 1);
                               closefolder(b);
                               exits(0);
                       default:
                               close(pfd[1]);
                               dup(pfd[0], 0);
                               break;
                       }
               }

               if(replymsg != nil)
                       putenv("replymsg", replymsg);
               mboxpathbuf(cmd, sizeof cmd, login, "pipefrom");
               exec(cmd, v);
               exec("/bin/myupassend", v);
               exec("/bin/upas/send", v);
               fatal("execing: %r");
               break;
       default:
               free(v);
               close(pfd[0]);
               break;
       }
       return pfd[1];
}

/*
* start up pgp process and return an fd to talk to it with.
* its standard output will be the original fd, which goes to sendmail.
*/
int
pgpfilter(int *pid, int fd, int pgpflag)
{
       int ac;
       int pfd[2];
       char **av, **v;

       v = av = emalloc(sizeof(char*)*8);
       ac = 0;
       v[ac++] = "pgp";
       v[ac++] = "-fat";               /* operate as a filter, generate text */
       if(pgpflag & PGPsign)
               v[ac++] = "-s";
       if(pgpflag & PGPencrypt)
               v[ac++] = "-e";
       v[ac] = 0;

       if(pipe(pfd) < 0)
               fatal("%r");
       switch(*pid = fork()){
       case -1:
               fatal("%r");
               break;
       case 0:
               close(pfd[1]);
               dup(pfd[0], 0);
               close(pfd[0]);
               dup(fd, 1);
               close(fd);

               /* add newline to avoid confusing pgp output with 822 headers */
               write(1, "\n", 1);
               exec("/bin/pgp", av);
               fatal("execing: %r");
               break;
       default:
               close(pfd[0]);
               break;
       }
       close(fd);
       return pfd[1];
}

/* wait for sendmail and pgp to exit; exit here if either failed */
char*
waitforsubprocs(void)
{
       Waitmsg *w;
       char *err;

       err = nil;
       while((w = wait()) != nil){
               if(w->pid == pid || w->pid == pgppid)
                       if(w->msg[0] != 0)
                               err = estrdup(w->msg);
               free(w);
       }
       if(err)
               exits(err);
       return nil;
}

void
freealias(Alias *a)
{
       freeaddrs(a->addr);
       free(a);
}

void
freealiases(Alias *a)
{
       Alias *next;

       while(a != nil){
               next = a->next;
               freealias(a);
               a = next;
       }
}

/*
*  read alias file
*/
Alias*
readaliases(void)
{
       char file[Pathlen];
       Addr *addr, **al;
       Alias *a, **l, *first;
       Sinstack *sp;
       String *line, *token;
       static int already;

       first = nil;
       line = s_new();
       token = s_new();

       /* open and get length */
       mboxpathbuf(file, Pathlen, login, "names");
       sp = s_allocinstack(file);
       if(sp == nil)
               goto out;

       l = &first;

       /* read a line at a time. */
       while(s_rdinstack(sp, s_restart(line))!=nil) {
               s_restart(line);
               a = emalloc(sizeof(Alias));
               al = &a->addr;
               while(s_parse(line, s_restart(token)) != 0) {
                       addr = emalloc(sizeof(Addr));
                       addr->v = strdup(s_to_c(token));
                       addr->next = 0;
                       *al = addr;
                       al = &addr->next;
               }
               if(a->addr == nil || a->addr->next == nil){
                       freealias(a);
                       continue;
               }
               a->next = nil;
               *l = a;
               l = &a->next;
       }
       s_freeinstack(sp);
out:
       s_free(line);
       s_free(token);
       return first;
}

Addr*
newaddr(char *name)
{
       Addr *a;

       a = emalloc(sizeof(*a));
       a->next = nil;
       a->v = estrdup(name);
       if(a->v == nil)
               sysfatal("%r");
       return a;
}

/*
*  expand personal aliases since the names are meaningless in
*  other contexts
*/
Addr*
_expand(Addr *old, int *changedp)
{
       Addr *first, *next, **l, *a;
       Alias *al;

       *changedp = 0;
       first = nil;
       l = &first;
       for(;old != nil; old = next){
               next = old->next;
               for(al = aliases; al != nil; al = al->next){
                       if(strcmp(al->addr->v, old->v) == 0){
                               for(a = al->addr->next; a != nil; a = a->next){
                                       *l = newaddr(a->v);
                                       if(*l == nil)
                                               sysfatal("%r");
                                       l = &(*l)->next;
                                       *changedp = 1;
                               }
                               break;
                       }
               }
               if(al != nil){
                       freeaddr(old);
                       continue;
               }
               *l = old;
               old->next = nil;
               l = &(*l)->next;
       }
       return first;
}

Addr*
rexpand(Addr *old)
{
       int i, changed;

       changed = 0;
       for(i = 0; i < 32; i++){
               old = _expand(old, &changed);
               if(changed == 0)
                       break;
       }
       return old;
}

Addr*
unique(Addr *first)
{
       Addr *a, **l, *x;

       for(a = first; a != nil; a = a->next){
               for(l = &a->next; *l != nil;){
                       if(strcmp(a->v, (*l)->v) == 0){
                               x = *l;
                               *l = x->next;
                               freeaddr(x);
                       } else
                               l = &(*l)->next;
               }
       }
       return first;
}

Addr*
expand(int ac, char **av)
{
       int i;
       Addr *first, **l;

       first = nil;

       /* make a list of the starting addresses */
       l = &first;
       for(i = 0; i < ac; i++){
               *l = newaddr(av[i]);
               if(*l == nil)
                       sysfatal("%r");
               l = &(*l)->next;
       }

       /* recurse till we don't change any more */
       return unique(rexpand(first));
}

Addr*
concataddr(Addr *a, Addr *b)
{
       Addr *oa;

       if(a == nil)
               return b;

       oa = a;
       for(; a->next; a=a->next)
               ;
       a->next = b;
       return oa;
}

void
freeaddr(Addr *ap)
{
       free(ap->v);
       free(ap);
}

void
freeaddrs(Addr *ap)
{
       Addr *next;

       for(; ap; ap=next) {
               next = ap->next;
               freeaddr(ap);
       }
}

String*
s_copyn(char *s, int n)
{
       return s_nappend(s_reset(nil), s, n);
}

/*
* fetch the next token from an RFC822 address string
* we assume the header is RFC822-conformant in that
* we recognize escaping anywhere even though it is only
* supposed to be in quoted-strings, domain-literals, and comments.
*
* i'd use yylex or yyparse here, but we need to preserve
* things like comments, which i think it tosses away.
*
* we're not strictly RFC822 compliant.  we misparse such nonsense as
*
*      To: gre @ (Grace) plan9 . (Emlin) bell-labs.com
*
* make sure there's no whitespace in your addresses and
* you'll be fine.
*/
enum {
       Twhite,
       Tcomment,
       Twords,
       Tcomma,
       Tleftangle,
       Trightangle,
       Terror,
       Tend,
};

// char *ty82[] = {"white", "comment", "words", "comma", "<", ">", "err", "end"};

#define ISWHITE(p) ((p)==' ' || (p)=='\t' || (p)=='\n' || (p)=='\r')

int
get822token(String **tok, char *p, char **pp)
{
       int type, quoting;
       char *op;

       op = p;
       switch(*p){
       case '\0':
               *tok = nil;
               *pp = nil;
               return Tend;

       case ' ':               /* get whitespace */
       case '\t':
       case '\n':
       case '\r':
               type = Twhite;
               while(ISWHITE(*p))
                       p++;
               break;

       case '(':               /* get comment */
               type = Tcomment;
               for(p++; *p && *p != ')'; p++)
                       if(*p == '\\') {
                               if(*(p+1) == '\0') {
                                       *tok = nil;
                                       return Terror;
                               }
                               p++;
                       }

               if(*p != ')') {
                       *tok = nil;
                       return Terror;
               }
               p++;
               break;
       case ',':
               type = Tcomma;
               p++;
               break;
       case '<':
               type = Tleftangle;
               p++;
               break;
       case '>':
               type = Trightangle;
               p++;
               break;
       default:        /* bunch of letters, perhaps quoted strings tossed in */
               type = Twords;
               quoting = 0;
               for (; *p && (quoting ||
                   (!ISWHITE(*p) && *p != '>' && *p != '<' && *p != ',')); p++) {
                       if(*p == '"')
                               quoting = !quoting;
                       if(*p == '\\') {
                               if(*(p+1) == '\0') {
                                       *tok = nil;
                                       return Terror;
                               }
                               p++;
                       }
               }
               break;
       }

       if(pp)
               *pp = p;
       *tok = s_copyn(op, p-op);
       return type;
}

/*
* expand local aliases in an RFC822 mail line
* add list of expanded addresses to to.
*/
Addr*
expandline(String **s, Addr *to)
{
       int tok, inangle, hadangle, nword;
       char *p;
       Addr *na, *nto, *ap;
       String *os, *ns, *stok, *lastword, *sinceword;

       os = s_copy(s_to_c(*s));
       p = strchr(s_to_c(*s), ':');
       assert(p != nil);
       p++;

       ns = s_copyn(s_to_c(*s), p-s_to_c(*s));
       stok = nil;
       nto = nil;
       /*
        * the only valid mailbox namings are word
        * and word* < addr >
        * without comments this would be simple.
        * we keep the following:
        * lastword - current guess at the address
        * sinceword - whitespace and comment seen since lastword
        */
       lastword = s_new();
       sinceword = s_new();
       inangle = 0;
       nword = 0;
       hadangle = 0;
       for(;;) {
               stok = nil;
               switch(tok = get822token(&stok, p, &p)){
               default:
                       abort();
               case Tcomma:
               case Tend:
                       if(inangle)
                               goto Error;
                       if(nword != 1)
                               goto Error;
                       na = rexpand(newaddr(s_to_c(lastword)));
                       s_append(ns, na->v);
                       s_append(ns, s_to_c(sinceword));
                       for(ap=na->next; ap; ap=ap->next) {
                               s_append(ns, ", ");
                               s_append(ns, ap->v);
                       }
                       nto = concataddr(na, nto);
                       if(tok == Tcomma){
                               s_append(ns, ",");
                               s_free(stok);
                       }
                       if(tok == Tend)
                               goto Break2;
                       inangle = 0;
                       nword = 0;
                       hadangle = 0;
                       s_reset(sinceword);
                       s_reset(lastword);
                       break;
               case Twhite:
               case Tcomment:
                       s_append(sinceword, s_to_c(stok));
                       s_free(stok);
                       break;
               case Trightangle:
                       if(!inangle)
                               goto Error;
                       inangle = 0;
                       hadangle = 1;
                       s_append(sinceword, s_to_c(stok));
                       s_free(stok);
                       break;
               case Twords:
               case Tleftangle:
                       if(hadangle)
                               goto Error;
                       if(tok != Tleftangle && inangle && s_len(lastword))
                               goto Error;
                       if(tok == Tleftangle) {
                               inangle = 1;
                               nword = 1;
                       }
                       s_append(ns, s_to_c(lastword));
                       s_append(ns, s_to_c(sinceword));
                       s_reset(sinceword);
                       if(tok == Tleftangle) {
                               s_append(ns, "<");
                               s_reset(lastword);
                       } else {
                               s_free(lastword);
                               lastword = stok;
                       }
                       if(!inangle)
                               nword++;
                       break;
               case Terror:            /* give up, use old string, addrs */
               Error:
                       ns = os;
                       os = nil;
                       freeaddrs(nto);
                       nto = nil;
                       werrstr("rfc822 syntax error");
                       rfc822syntaxerror = 1;
                       goto Break2;
               }
       }
Break2:
       s_free(*s);
       s_free(os);
       *s = ns;
       nto = concataddr(nto, to);
       return nto;
}

void
Bdrain(Biobuf *b)
{
       char buf[8192];

       while(Bread(b, buf, sizeof buf) > 0)
               ;
}

void
readmimetypes(void)
{
       char *p;
       char type[256];
       char *f[6];
       Biobuf *b;
       static int alloced, inuse;

       if(mimetypes == 0){
               alloced = 256;
               mimetypes = emalloc(alloced*sizeof(Ctype));
               mimetypes[0].ext = "";
       }

       b = Bopen("/sys/lib/mimetype", OREAD);
       if(b == nil)
               return;
       for(;;){
               p = Brdline(b, '\n');
               if(p == nil)
                       break;
               p[Blinelen(b)-1] = 0;
               if(tokenize(p, f, 6) < 4)
                       continue;
               if (strcmp(f[0], "-") == 0 || strcmp(f[1], "-") == 0 ||
                   strcmp(f[2], "-") == 0)
                       continue;
               if(inuse + 1 >= alloced){
                       alloced += 256;
                       mimetypes = erealloc(mimetypes, alloced*sizeof(Ctype));
               }
               snprint(type, sizeof(type), "%s/%s", f[1], f[2]);
               mimetypes[inuse].type = estrdup(type);
               mimetypes[inuse].ext = estrdup(f[0]+1);
               mimetypes[inuse].display = !strcmp(type, "text/plain");
               inuse++;

               /* always make sure there's a terminator */
               mimetypes[inuse].ext = 0;
       }
       Bterm(b);
}

char*
estrdup(char *x)
{
       x = strdup(x);
       if(x == nil)
               fatal("memory");
       return x;
}

void*
emalloc(int n)
{
       void *x;

       x = malloc(n);
       if(x == nil)
               fatal("%r");
       return x;
}

void*
erealloc(void *x, int n)
{
       x = realloc(x, n);
       if(x == nil)
               fatal("%r");
       return x;
}

/*
* Formatter for %"
* Use double quotes to protect white space, frogs, \ and "
*/
enum
{
       Qok = 0,
       Qquote,
       Qbackslash,
};

static int
needtoquote(Rune r)
{
       if(r >= Runeself)
               return Qquote;
       if(r <= ' ')
               return Qquote;
       if(r=='\\' || r=='"')
               return Qbackslash;
       return Qok;
}

int
doublequote(Fmt *f)
{
       int w, quotes;
       char *s, *t;
       Rune r;

       s = va_arg(f->args, char*);
       if(s == nil || *s == '\0')
               return fmtstrcpy(f, "\"\"");

       quotes = 0;
       for(t = s; *t; t += w){
               w = chartorune(&r, t);
               quotes |= needtoquote(r);
       }
       if(quotes == 0)
               return fmtstrcpy(f, s);

       fmtrune(f, '"');
       for(t = s; *t; t += w){
               w = chartorune(&r, t);
               if(needtoquote(r) == Qbackslash)
                       fmtrune(f, '\\');
               fmtrune(f, r);
       }
       return fmtrune(f, '"');
}

int
rfc2047fmt(Fmt *fmt)
{
       char *s, *p;

       s = va_arg(fmt->args, char*);
       if(s == nil)
               return fmtstrcpy(fmt, "");
       for(p=s; *p; p++)
               if((uchar)*p >= 0x80)
                       goto hard;
       return fmtstrcpy(fmt, s);

hard:
       fmtprint(fmt, "=?utf-8?q?");
       for(p = s; *p; p++){
               if(*p == ' ')
                       fmtrune(fmt, '_');
               else if(*p == '_' || *p == '\t' || *p == '=' || *p == '?' ||
                   (uchar)*p >= 0x80)
                       fmtprint(fmt, "=%.2uX", (uchar)*p);
               else
                       fmtrune(fmt, (uchar)*p);
       }
       fmtprint(fmt, "?=");
       return 0;
}

char*
hdrval(char *p)
{
       char *e;

       p = strchr(p, ':') + 1;
       while(*p == ' ' || *p == '\t')
               p++;
       e = strchr(p, 0) - 1;
       while(e >= p && (*e == ' ' || *e == '\t'))
               *e-- = 0;
       return p;
}

char*
mksubject(char *line)
{
       char *p, *q;
       static char buf[1024];

       p = hdrval(line);
       for(q = p; *q; q++)
               if((uchar)*q >= 0x80)
                       goto hard;
       return line;

hard:
       snprint(buf, sizeof buf, "Subject: %U", p);
       return buf;
}