/////////////////////////////////////////////////////////////////
// emai//er - Simple Email User Agent vaguely inspired by Elm
// Handles INBOX in the format created by POP65
// Bobbi June 2020 - June 2021
/////////////////////////////////////////////////////////////////

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <unistd.h>
#include <conio.h>
#include <string.h>
#include <ctype.h>
#include <apple2_filetype.h>

#include <errno.h> // DEBUG

#define EMAIL_C
#include "email_common.h"

// Program constants
#define MSGS_PER_PAGE 19     // Number of messages shown on summary screen
#define PROMPT_ROW    24     // Row that data entry prompt appears on
#define READSZ        512    // Size of buffer for copying files
#define LINEBUFSZ     1024   // Max line 1000 according to RFC2822 Sect 2.1.1
                             // We use 1024 because this is also used for scrollback

// Characters
#define BELL          0x07
#define BACKSPACE     0x08
#define INVERSE       0x0f
#define DOWNARROW     0x0a
#define UPARROW       0x0b
#define RETURN        0x0d
#define NORMAL        0x0e
#define CURDOWN       0x0a
#define HOME          0x19
#define CLRLINE       0x1a
#define ESC           0x1b
#define CURUP         0x1f
#define DELETE        0x7f

// Addresses
#define CURSORROW     0x0025
#define SYSTEMTIME    0xbf90
#define KEYBOARD      0xc000

static char closedapple[]  = "\x0f\x1b""@\x18\x0e";
static char openapple[]    = "\x0f\x1b""A\x18\x0e";
static char email[]        = "EMAIL";
static char email_cfg[]    = "EMAIL.CFG";
static char email_prefs[]  = "EMAIL.PREFS";
static char email_db[]     = "%s/%s/EMAIL.DB";
static char email_db_new[] = "%s/%s/EMAIL.DB.NEW";
static char next_email[]   = "%s/%s/NEXT.EMAIL";
static char email_file[]   = "%s/%s/EMAIL.%u";
static char inbox[]        = "INBOX";
static char outbox[]       = "OUTBOX";
static char news_outbox[]  = "NEWS.OUTBOX";
static char cant_open[]    = "Can't open %s";
static char cant_seek[]    = "Can't seek in %s";
static char cant_malloc[]  = "Can't alloc";
static char cant_delete[]  = "Can't delete %s";
static char cant_write[]   = "Can't write to %s";
static char mime_ver[]     = "MIME-Version: 1.0";
static char ct[]           = "Content-Type: ";
static char cte[]          = "Content-Transfer-Encoding: ";
static char sevenbit[]     = "7bit";
static char eightbit[]     = "8bit";
static char qp[]           = "quoted-printable";
static char b64[]          = "base64";
static char unsupp_enc[]   = "** Unsupp encoding %s\n";
static char sb_err[]       = "Scrollback error";
static char a2_forever[]   = "%s: %s - Apple II Forever!\r\r";

/*
 * Represents a date and time
 */
struct datetime {
    unsigned int  year;
    unsigned char month;
    unsigned char day;
    unsigned char hour;
    unsigned char minute;
    unsigned char ispd25format;
    unsigned char nodatetime;
};

static char              filename[80];
static char              userentry[80];
static uint8_t           linebuf[LINEBUFSZ];
static FILE              *fp;
static struct emailhdrs  *headers;
static uint16_t          selection = 1;
static uint16_t          prevselection;
static uint16_t          num_msgs;        // Num of msgs shown in current page
static uint16_t          total_msgs;      // Total number of message in mailbox
static uint16_t          total_new;       // Total number of new messages
static uint16_t          total_tag;       // Total number of tagged messages
static uint16_t          first_msg = 1;   // Msg numr: first message current page
static uint8_t           reverse = 0;     // 0 normal, 1 reverse order
static char              curr_mbox[80] = "INBOX";
static unsigned char     buf[READSZ];


/*
 * Save preferences
 */
#pragma code-name (push, "LC")
void save_prefs(void) {
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp = fopen(email_prefs, "wb");
  if (!fp)
    return;
  fprintf(fp, "o:%c\n", (reverse ? '<' : '>'));
  fprintf(fp, "m:%s\n", curr_mbox);
  fprintf(fp, "f:%d\n", first_msg);
  fprintf(fp, "s:%d\n", selection);
  fclose(fp);
}
#pragma code-name (pop)

/*
 * Load preferences
 */
#pragma code-name (push, "LC")
void load_prefs(void) {
  char order = 'a';
  fp = fopen(email_prefs, "rb");
  if (!fp)
    return;
  fscanf(fp, "o:%c\n", &order);
  fscanf(fp, "m:%s\n", curr_mbox);
  fscanf(fp, "f:%d\n", &first_msg);
  fscanf(fp, "s:%d\n", &selection);
  fclose(fp);
  reverse = (order == '<' ? 1 : 0);
}
#pragma code-name (pop)

/*
 * Load and run EDIT.SYSTEM
 * compose - Controls the arguments passed to EDIT.SYSTEM
 *           0: Email reading (-reademail)
 *           1: Email composition (-email)
 *           2: News composition (-news)
 */
#pragma code-name (push, "LC")
void load_editor(uint8_t compose) {
  save_prefs();
  snprintf(userentry, 80, "%s %s",
           (compose == 0 ? "-reademail" : (compose == 1 ? "-email" : "-news")),
           filename);
  snprintf(filename, 80, "%s/EDIT.SYSTEM", cfg_instdir);
  exec(filename, userentry);
}
#pragma code-name (pop)

static char *apps[] = {"POP65", "SMTP65", "NNTP65", "NNTP65UP", "DATE65"};
enum appidx {APP_POP, APP_SMTP, APP_NNTP_DOWN, APP_NNTP_UP, APP_DATE};

/*
 * Load and run an external app
 */
#pragma code-name (push, "LC")
void load_app(enum appidx a) {
  save_prefs();
  snprintf(filename, 80, "%s/%s.SYSTEM", cfg_instdir, apps[a]);
  exec(filename, email);
}
#pragma code-name (pop)

/*
 * Put cursor at beginning of PROMPT_ROW
 */
#pragma code-name (push, "LC")
void goto_prompt_row(void) {
  uint8_t i;
  putchar(HOME);
  for (i = 0; i < PROMPT_ROW - 1; ++i) 
    putchar(CURDOWN);
}
#pragma code-name (pop)

#pragma code-name (push, "LC")
void clrscr2(void) {
  videomode(VIDEOMODE_80COL);
  clrscr();
}
#pragma code-name (pop)

void status_bar(void); // Forward declaration

/*
 * Check for key pressed. If so, call cgetc().
 * Otherwise spin and update the status line waiting for a keypress.
 */
#pragma code-name (push, "LC")
char cgetc_update_status() {
  uint16_t ctr = 0;
  while (*((char*)KEYBOARD) < 128) {
    ++ctr;
    if (ctr == 20000) {
      status_bar();
      goto_prompt_row();
      ctr = 0;
    }
  }
  return cgetc();
}
#pragma code-name (pop)

enum err_level {ERR_NONFATAL, ERR_FATAL};
/*
 * Show non fatal error in PROMPT_ROW
 * Fatal errors are shown on a blank screen
 */
#pragma code-name (push, "LC")
void error(enum err_level fatal, const char *fmt, ...) {
  va_list v;
  va_start(v, fmt);
  if (fatal) {
    clrscr2();
    printf("\n\n%cFATAL ERROR:%c\n\n", INVERSE, NORMAL);
    vprintf(fmt, v);
    printf("\n\n\n\n[Press Any Key To Quit]");
    cgetc();
    exit(1);
  } else {
    goto_prompt_row();
    putchar(CLRLINE);
    vprintf(fmt, v);
    printf(" - [Press Any Key]");
    cgetc();
    putchar(CLRLINE);
  }
  va_end(v);
}
#pragma code-name (pop)

/*
 * Print spaces
 */
#pragma code-name (push, "LC")
void pr_spc(uint8_t n) {
  while (n--)
    putchar(' ');
}
#pragma code-name (pop)

/*
 * Print ASCII-art envelope
 */
#if 0
#pragma code-name (push, "LC")
void envelope(void) {
  uint8_t i;
  putchar(NORMAL);
  putchar(HOME);
  for (i = 0; i < 8 - 1; ++i) 
    putchar(CURDOWN);
  pr_spc(30); puts("+------------------+");
  pr_spc(30); puts("|\\  \"Inbox Zero\"  /|");
  pr_spc(30); puts("| \\              / |");
  pr_spc(30); puts("|  \\            /  |");
  pr_spc(30); puts("|   +----------+   |");
  pr_spc(30); puts("|            Bobbi |");
  pr_spc(30); puts("+------------------+");
  pr_spc(30); puts("                 ... No messages in this mailbox");
}
#pragma code-name (pop)
#endif

/*
 * Busy spinner
 */
#pragma code-name (push, "LC")
void spinner(void) {
  static char chars[] = "|/-\\";
  static uint8_t i = 0;
  putchar(BACKSPACE);
  putchar(chars[(i++) % 4]);
}
#pragma code-name (pop)

/*
 * Read parms from EMAIL.CFG
 */
#pragma code-name (push, "LC")
void readconfigfile(void) {
  fp = fopen(email_cfg, "r");
  if (!fp)
    error(ERR_FATAL, cant_open, email_cfg);
  fscanf(fp, "%s%s%s%s%s%s%s%s%s", cfg_server, cfg_user, cfg_pass,
                                   cfg_pop_delete, cfg_smtp_server,
                                   cfg_smtp_domain, cfg_instdir,
                                   cfg_emaildir, cfg_emailaddr);
  fclose(fp);
}
#pragma code-name (pop)

/* Defined in gettime.s */
void gettime(void);

/*
 * Convert date/time bytes into struct datetime format.
 */
#pragma code-name (push, "LC")
void readdatetime(struct datetime *dt) {
    unsigned char *time = (unsigned char*)SYSTEMTIME;
    unsigned int d, t;
    gettime();
    d = time[0] + 256U * time[1];
    t = time[2] + 256U * time[3];
    if ((d | t) == 0) {
        dt->nodatetime = 1;
        return;
    }
    dt->nodatetime = 0;
    if (!(t & 0xe000)) {
        /* ProDOS 1.0 to 2.4.2 date format */
        dt->year   = (d & 0xfe00) >> 9;
        dt->month  = (d & 0x01e0) >> 5;
        dt->day    = d & 0x001f;
        dt->hour   = (t & 0x1f00) >> 8;
        dt->minute = t & 0x003f;
        dt->ispd25format = 0;
        if (dt->year < 40) /* See ProDOS-8 Tech Note 48 */
            dt->year += 2000;
        else
            dt->year += 1900;
    } else {
        /* ProDOS 2.5.0+ */
        dt->year   = t & 0x0fff;
        dt->month  = ((t & 0xf000) >> 12) - 1;
        dt->day    = (d & 0xf800) >> 11;
        dt->hour   = (d & 0x07c0) >> 6;
        dt->minute = d & 0x003f;
        dt->ispd25format = 1;
    }
}
#pragma code-name (pop)

/*
 * Print a date/time value in short format for the status bar
 */
#pragma code-name (push, "LC")
void printdatetime(struct datetime *dt) {
  if (dt->nodatetime)
    fputs("????-??-?? ??:??", stdout);
  else {
    printf("%04d-%02d-%02d %02d:%02d",
           dt->year, dt->month, dt->day, dt->hour, dt->minute);
  }
}
#pragma code-name (pop)

/*
 * Format a date/time in format suitable for an email
 * dt - structure representing date/time
 * s  - result is returned through this pointer
 */
#pragma code-name (push, "LC")
void datetimelong(struct datetime *dt, char *s) {
  static char *months[] = {"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
  if (dt->nodatetime)
    sprintf(s, "No system clock");
  else {
    // eg: "20 Jun 2020 hh:mm:00"
    sprintf(s, "%2d %s %04d %02d:%02d:00",
            dt->day, months[dt->month], dt->year, dt->hour, dt->minute);
  }
}
#pragma code-name (pop)

/*
 * Obtain the system date and time for the status bar
 */
#pragma code-name (push, "LC")
void printsystemdate(void) {
  struct datetime dt;
  readdatetime(&dt);
  printdatetime(&dt);
}
#pragma code-name (pop)

/*
 * Free linked list rooted at headers
 */
#pragma code-name (push, "LC")
void free_headers_list(void) {
  struct emailhdrs *h = headers;
  while (h) {
    free(h);
    h = h->next; // Not strictly legal, but will work
  }
  headers = NULL;
}
#pragma code-name (pop)

/*
 * Read EMAIL.DB and populate linked list rooted at headers
 * startnum - number of the first message to load (1 is the first)
 * initialize - if 1, then total_new and total_msgs are calculated
 * switchmbox - if 1, then errors are treated as non-fatal (for S)witch command)
 * Returns 0 if okay, 1 on non-fatal error.
 */
#pragma code-name (push, "LC")
uint8_t read_email_db(uint16_t startnum, uint8_t initialize, uint8_t switchmbox) {
  struct emailhdrs *curr = NULL, *prev = NULL;
  uint16_t count = 0;
  uint8_t done_visible = 0;
  int32_t pos;
  uint16_t l;
  if (initialize) {
    total_new = total_msgs = total_tag = 0;
  }
  free_headers_list();
  snprintf(filename, 80, email_db, cfg_emaildir, curr_mbox);
  fp = fopen(filename, "rb");
  if (!fp) {
    error(switchmbox ? ERR_NONFATAL : ERR_FATAL, cant_open, filename);
    if (switchmbox)
      return 1;
  }
  if (reverse) {
    if (fseek(fp, 0, SEEK_END)) {
      fclose(fp);
      error(switchmbox ? ERR_NONFATAL : ERR_FATAL, cant_seek, filename);
      if (switchmbox)
        return 1;
    }
    // If the mailbox is empty this seek will fail
    if (fseek(fp, ftell(fp) - (uint32_t)startnum * EMAILHDRS_SZ_ON_DISK, SEEK_SET)) {
      fclose(fp);
      num_msgs = total_new = total_msgs = total_tag = 0;
      return 0;
    }
  } else {
    if (fseek(fp, (uint32_t)(startnum - 1) * EMAILHDRS_SZ_ON_DISK, SEEK_SET)) {
      fclose(fp);
      error(switchmbox ? ERR_NONFATAL : ERR_FATAL, cant_seek, filename);
      if (switchmbox)
        return 1;
    }
  }
  num_msgs = 0;
  goto_prompt_row();
  putchar(CLRLINE);
  fputs("Loading  ", stdout);
  while (1) {
    spinner();
    curr = (struct emailhdrs*)malloc(sizeof(struct emailhdrs));
    if (!curr)
      error(ERR_FATAL, cant_malloc);
    curr->next = NULL;
    curr->tag = ' ';
    l = fread(curr, 1, EMAILHDRS_SZ_ON_DISK, fp);
    ++count;
    if (l != EMAILHDRS_SZ_ON_DISK) {
      free(curr);
      break;
    }
    if (count <= MSGS_PER_PAGE) {
      if (!prev)
        headers = curr;
      else
        prev->next = curr;
      prev = curr;
      ++num_msgs;
    } else {
      if (!initialize) {
        free(curr);
        break;
      }
      done_visible = 1;
    }
    if (initialize) {
      ++total_msgs;
      if (curr->status == 'N')
        ++total_new;
      if (curr->tag == 'T')
        ++total_tag;
      if (done_visible)
        free(curr);
    }
    if (reverse) {
      pos = ftell(fp) - 2L * EMAILHDRS_SZ_ON_DISK;
      if (pos == -1L * EMAILHDRS_SZ_ON_DISK)
        break;
      if (fseek(fp, pos, SEEK_SET)) {
        error(switchmbox ? ERR_NONFATAL : ERR_FATAL, cant_seek, filename);
        break;
      }
    }
  }
  fclose(fp);
  return 0;
}
#pragma code-name (pop)

/*
 * Base64 decode table
 */
const int8_t b64dec[] =
  {62,-1,-1,-1,63,52,53,54,55,56,
   57,58,59,60,61,-1,-1,-1,-2,-1,
   -1,-1, 0, 1, 2, 3, 4, 5, 6, 7,
    8, 9,10,11,12,13,14,15,16,17,
   18,19,20,21,22,23,24,25,-1,-1,
   -1,-1,-1,-1,26,27,28,29,30,31,
   32,33,34,35,36,37,38,39,40,41,
   42,43,44,45,46,47,48,49,50,51};

/*
 * Decode Base64 format in place
 * Each line of base64 has up to 76 chars, which decodes to up to 57 bytes
 * p - Pointer to buffer to decode. Results written in place.
 * Returns number of bytes decoded
 */
uint16_t decode_base64(char *p) {
  uint16_t i = 0, j = 0;
  const int8_t *b = b64dec - 43;
  while (p[i] && (p[i] != '\r') && (p[i] != '?')) {
    p[j++] = b[p[i]] << 2 | b[p[i + 1]] >> 4;
    if (p[i + 2] != '=')
      p[j++] = b[p[i + 1]] << 4 | b[p[i + 2]] >> 2;
    if (p[i + 3] != '=')
      p[j++] = b[p[i + 2]] << 6 | b[p[i + 3]];
    i += 4;
  }
  p[j] = '\0';
  return j;
}

/*
 * Convert hex char to value
 */
#pragma code-name (push, "LC")
uint8_t hexdigit(char c) {
  if ((c >= '0') && (c <= '9'))
    return c - '0';
  else
    return c - 'A' + 10;
}
#pragma code-name (pop)

/*
 * Decode buffer from quoted-printable format in place
 * p - Pointer to buffer to decode. Results written in place.
 * Returns number of bytes decoded
 */
uint16_t decode_quoted_printable(uint8_t *p, uint8_t isheader) {
  uint16_t i = 0, j = 0;
  uint8_t c;
  while (c = p[i]) {
    if (c == '=') {
      if (p[i + 1] == '\r') // Trailing '=' is a soft '\r'
        break;
      // Otherwise '=xx' where x is a hex digit
      c = 16 * hexdigit(p[i + 1]) + hexdigit(p[i + 2]);
      p[j++] = c;
      i += 3;
    } else if ((c == '?') && isheader)
      break;
    else {
      p[j++] = c;
      ++i;
    }
  }
  p[j] = '\0';
  return j;
}

/*
 * Print a header field from char postion start to end,
 * padding with spaces as needed
 */
#pragma code-name (push, "LC")
void printfield(char *s, uint8_t start, uint8_t end) {
  uint8_t i;
  uint8_t l = strlen(s);
  for (i = start; i < end; i++)
    putchar(i < l ? s[i] : ' ');
}
#pragma code-name (pop)

/*
 * Decode Subject or From header which may be encoded
 * Quoted-Printable or Base64
 * p - pointer to subject header content
 * Decoded (and sanitized) text is returned in linebuf[]
 */
void decode_qp_header(char *p) {
  uint8_t i = 0, j = 0;
  if (strncasecmp(p, "=?utf-8?", 8) == 0) {
    strcpy(linebuf, p + 10); // Skip '=?UTF-8?x?'
    if (p[8] == 'B')
      decode_base64(linebuf);
    else
      decode_quoted_printable(linebuf, 1);
    while (linebuf[i]) {
      if ((linebuf[i] <= 127) && (linebuf[i] >= 32))
        linebuf[j++] = linebuf[i];
      else if (linebuf[i] > 191)     // 11xxxxxx
        linebuf[j++] = '#';
      ++i;
    }
    linebuf[j] = '\0';
  } else
    strcpy(linebuf, p);
}

/*
 * Print one line summary of email headers for one message
 */
void print_one_email_summary(struct emailhdrs *h, uint8_t inverse) {
  putchar(inverse ? INVERSE : NORMAL);
  putchar(h->tag == 'T' ? 'T' : ' ');
  switch(h->status) {
  case 'N':
    putchar('*'); // New
    break;
  case 'R':
    putchar(' '); // Read
    break;
  case 'D':
    putchar('D'); // Deleted
    break;
  }
  putchar('|');
  printfield(h->date, 0, 16);
  putchar('|');
  decode_qp_header(h->from);
  printfield(linebuf, 0, 20);
  putchar('|');
  decode_qp_header(h->subject);
  printfield(linebuf, 0, 39);
  putchar(NORMAL);
}

/*
 * Get emailhdrs for nth email in list of headers
 */
#pragma code-name (push, "LC")
struct emailhdrs *get_headers(uint16_t n) {
  uint16_t i = 1;
  struct emailhdrs *h = headers;
  while (h && (i < n)) {
    ++i;
    h = h->next;
  }
  return h;
}
#pragma code-name (pop)

/*
 * Print status bar at the top
 */
#define MAXSTATLEN 62
void status_bar(void) {
  uint8_t i;
  if (num_msgs == 0) {
    sprintf(linebuf, "%s [%s] No messages ", PROGNAME, curr_mbox);
    //envelope();
  } else
    sprintf(linebuf, "[%s] %u msgs, %u new, %u tagged. Showing %u-%u. %c ",
           curr_mbox, total_msgs, total_new, total_tag, first_msg,
           first_msg + num_msgs - 1, (reverse ? '<' : '>'));
  if (strlen(linebuf) > MAXSTATLEN) {
    linebuf[MAXSTATLEN] = '\0';
    linebuf[MAXSTATLEN-3] = linebuf[MAXSTATLEN-2] = linebuf[MAXSTATLEN-1] = '.';
  }
  putchar(HOME);
  putchar(INVERSE);
  fputs(linebuf, stdout);
  for (i = 0; i < MAXSTATLEN+2 - strlen(linebuf); ++i)
    fputc(' ', stdout);
  printsystemdate();
  putchar(HOME);
  putchar(NORMAL);
  putchar(CURDOWN);
  printf("\n");
}

/*
 * Show email summary
 */
void email_summary(void) {
  uint8_t i = 1;
  struct emailhdrs *h = headers;
  clrscr2();
  status_bar();
  while (h) {
    print_one_email_summary(h, (i == selection));
    ++i;
    h = h->next;
  }
  putchar(HOME);
  for (i = 0; i < PROMPT_ROW - 2; ++i) 
    putchar(CURDOWN);
  printf("%cOA-? Help                                                                       %c", INVERSE, NORMAL);
}

/*
 * Show email summary for nth email message in list of headers
 */
void email_summary_for(uint16_t n) {
  struct emailhdrs *h = headers;
  uint16_t j;
  h = get_headers(n);
  putchar(HOME);
  for (j = 0; j < n + 1; ++j) 
    putchar(CURDOWN);
  print_one_email_summary(h, (n == selection));
}

/*
 * Move the highlight bar when user selects different message
 */
#pragma code-name (push, "LC")
void update_highlighted(void) {
  email_summary_for(prevselection);
  email_summary_for(selection);
}
#pragma code-name (pop)

/*
 * Read a text file a line at a time
 * Returns number of chars in the line, or 0 if EOF.
 * Expects Apple ][ style line endings (CR) and does no conversion
 * fp - file to read from
 * reset - if 1 then just reset the buffer and return
 * writep - Pointer to buffer into which line will be written
 * n - length of buffer. Longer lines will be truncated and terminated with CR.
 * pos - position in file is updated via this pointer
 */
uint16_t get_line(FILE *fp, uint8_t reset, char *writep, uint16_t n, uint32_t *pos) {
  static uint16_t rd = 0; // Read
  static uint16_t end = 0; // End of valid data in buf
  uint16_t i = 0;
  if (reset) {
    rd = end = 0;
    *pos = 0;
    return 0;
  }
  while (1) {
    if (rd == end) {
      end = fread(buf, 1, READSZ, fp);
      rd = 0;
    }
    if (end == 0)
      goto done;
    if (i == n - 1) {
      writep[i - 1] = '\r';
      goto done;
    }
    writep[i++] = buf[rd++];
    ++(*pos);
    if (writep[i - 1] == '\r')
      goto done;
  }
done:
  writep[i] = '\0';
  return i;
}

/*
 * Print line up to first '\r' or '\0'
 */
void putline(FILE *f, char *s) {
  while ((*s != NULL) && (*s != '\r')) {
    if ((*s <= 127) && (*s >= 32))
      fputc(*s, f);
    else if (*s > 191)   // 11xxxxxx
      fputc('#', f);
    ++s;
  }
}

/*
 * Perform word wrapping, for a line of text, which may contain multiple
 * embedded '\r' carriage returns, or no carriage return at all.
 * fp - File handle to use for output.
 * s - Pointer to pointer to input buffer. If all text is consumed, this is
 *     set to NULL.  If there is text left in the buffer to be consumed then
 *     the pointer will be advanced to point to the next text to process.
 * cols - Number of columns to break at
 * mode - 'R' for reply, 'F' for forward, 'N' for news follow-up, otherwise ' '
 * Returns 1 if the caller should invoke the routine again before obtaining
 * more input, or 0 if there is nothing more to do or caller needs to get more
 * input before next call.
 */
uint8_t word_wrap_line(FILE *fp, char **s, uint8_t cols, char mode) {
  static uint8_t col = 0;           // Keeps track of screen column
  char *ss = *s;
  char *ret = strchr(ss, '\r');
  uint16_t l = strlen(ss);
  char *nextline = NULL;
  uint16_t i;
  if (l == 0)
    return 0;                      // Need more input to proceed
  if (ret) {
    if (l > (ret - ss) + 1)        // If '\r' is not at the end ...
      nextline = ss + (ret - ss) + 1; // Keep track of next line(s)
    l = ret - ss;
    if ((col + l) <= cols) {         // Fits on this line
      col += l;
      putline(fp, ss);
      if (ret) {
        col = 0;
        if (col + l != cols) {
          fputc('\r', fp);
          if ((mode == 'R') || (mode == 'N')) {
            fputc('>', fp);
            ++col;
          }
        }
      }
      *s = nextline;
      return (*s ? 1 : 0);         // Caller should invoke again
    }
    i = cols - col;                  // Doesn't fit, need to break
    while ((ss[--i] != ' ') && (i > 0));
    if (i == 0) {                  // No space character found
      if (col == 0) {              // Doesn't fit on full line
        for (i = 0; i <= cols; ++i) { // Truncate @cols chars
          if ((ss[i] <= 127) && (ss[i] >= 32))
            fputc(ss[i], fp);
          else if (ss[i] > 191)    // 11xxxxxx
            fputc('#', fp);
        }
        *s = ss + l + 1;
      } else {                     // There is stuff on this line already
        col = 0;
        fputc('\r', fp);           // Try a blank line
        if ((mode == 'R') || (mode == 'N')) {
          fputc('>', fp);
          ++col;
        }
      }
      return (ret ? (*s ? 0 : 1) : 0); // If EOL, caller should invoke again
    }
  } else {                         // No ret
    i = cols - col;                  // Space left on line
    if (i > l)
      return 0;                    // Need more input to proceed
    while ((ss[--i] != ' ') && (i > 0));
    if (i == 0)                    // No space character found
      return 0;                    // Need more input to proceed
  }
  ss[i] = '\0';                    // Space was found, split line
  putline(fp, ss);
  fputc('\r', fp);
  col = 0;
  if ((mode == 'R') || (mode == 'N')) {
    fputc('>', fp);
    ++col;
  }
  *s = ss + i + 1;
  return (*s ? 1 : 0);             // Caller should invoke again
}

uint8_t prompt_for_name(char *, uint8_t); // Forward declaration

/*
 * OK to d/l attachment?
 */
char prompt_okay_attachment(char *filename) {
  char c;
  while (1) {
    printf("ProDOS filename:  %s\n", filename);
    printf("%c                                                      A)ccept | S)kip | R)ename%c\n", INVERSE, NORMAL);
    c = cgetc();
    switch (c | 0x20) {
    case 'a':
      return 1;
      break;
    case 's':
      return 0;
      break;
    case 'r':
      c = wherey();
      if (prompt_for_name("Save As", 2) == 255) {
        gotoxy(0, c);
        break; // ESC pressed
      }
      if (strlen(userentry) > 0) {
        if (userentry[0] == '/')
          strcpy(filename, userentry);
        else
          snprintf(filename, 80, "%s/ATTACHMENTS/%s", cfg_emaildir, userentry);
      }
      gotoxy(0, c);
      break;
    default:
      putchar(BELL);
    }
  } 
    return 0;
}

/*
 * Sanitize filename for ProDOS
 * Modifies s in place
 */
void sanitize_filename(char *s) {
  uint8_t i = 0, j = 0;
  char c;
  while (1) {
    c = s[i++];
    if (c == '\r')
      break;
    if (isalnum(c) || c == '.' || c == '/')
      s[j++] = c;
    if (j == 15) {
      break;
    }
  }
  s[j] = '\0';
}

enum aux_ops {FROMAUX, TOAUX};

/*
 * Asm code for copyaux()
 */
#pragma optimize (push, off)
void copyauxasm(enum aux_ops dir) {
    if (dir == TOAUX)
        __asm__("sec");   // Copy main->aux
    else
        __asm__("clc");   // Copy aux->main
    __asm__("sta $c000"); // Turn off 80STORE
    __asm__("jsr $c311"); // AUXMOVE
    __asm__("sta $c001"); // Turn on 80STORE
}
#pragma optimize (pop)

/*
 * Aux memory copy routine
 */
void copyaux(char *src, char *dst, uint16_t len, enum aux_ops dir) {
    char **a1 = (char**)0x3c;
    char **a2 = (char**)0x3e;
    char **a4 = (char**)0x42;
    *a1 = src;
    *a2 = src + len - 1;  // AUXMOVE moves length+1 bytes!!
    *a4 = dst;
    copyauxasm(dir);
}

/*
 * Save the current screen to the scrollback file
 * We preserve linebuf[] by copying it to aux 0x0800, and recover
 * it at the end.  This saves us having an additional 1KB buffer.
 */
void save_screen_to_scrollback(FILE *fp) {
  copyaux(linebuf, (void*)0x800, 1024, TOAUX); // Preserve linebuf[]
  if (fwrite((void*)0x0400, 0x0400, 1, fp) != 1) { // Even cols
    error(ERR_NONFATAL, sb_err);
    goto done;
  }
  copyaux((void*)0x400, linebuf, 0x400, FROMAUX);
  if (fwrite(linebuf, 0x0400, 1, fp) != 1) { // Odd cols
    error(ERR_NONFATAL, sb_err);
    goto done;
  }
done:
  copyaux((void*)0x800, linebuf, 1024, FROMAUX); // Recover linebuf[]
}

/*
 * Load a screen from the scrollback file
 * Screens are numbered 1, 2, 3 ...
 * Does not trash the screen holes, which must be preserved!
 * We preserve linebuf[] by copying it to aux 0x0800, and recover
 * it at the end.  This saves us having an additional 1KB buffer.
 */
void load_screen_from_scrollback(FILE *fp, uint8_t screen) {
  uint8_t i;
  copyaux(linebuf, (void*)0x800, 1024, TOAUX); // Preserve linebuf[]
  if (fseek(fp, (screen - 1) * 0x0800, SEEK_SET) ||
     (fread(linebuf, 0x0400, 1, fp) != 1)) { // Even cols
    error(ERR_NONFATAL, sb_err);
    goto done;
  }
  for (i = 0; i < 8; ++i)
    memcpy((char*)0x400 + i * 0x80, linebuf + i * 0x80, 0x078);
  if (fread(linebuf, 0x0400, 1, fp) != 1) { // Odd cols
    error(ERR_NONFATAL, sb_err);
    goto done;
  }
  for (i = 0; i < 8; ++i)
    copyaux(linebuf + i * 0x80, (char*)0x400 + i * 0x80, 0x078, TOAUX);
  if (fseek(fp, 0, SEEK_END)) {
    error(ERR_NONFATAL, sb_err);
    goto done;
  }
done:
  copyaux((void*)0x800, linebuf, 1024, FROMAUX); // Recover linebuf[]
}

/*
 * MIME encodings: 7 bit, quoted printable or base64
 */
enum mime_enc {ENC_7BIT, ENC_QP, ENC_B64, ENC_SKIP = 255};

/*
 * Parse text at s and return the MIME encoding
 * s - Pointer to text being parsed
 */
enum mime_enc mime_encoding(char *s) {
  if (!strncmp(s + 27, sevenbit, 4))
    return ENC_7BIT;
  else if (!strncmp(s + 27, eightbit, 4))
    return ENC_7BIT;
  else if (!strncmp(s + 27, qp, 16))
    return ENC_QP;
  else if (!strncmp(s + 27, b64, 6))
    return ENC_B64;
  else
    return ENC_SKIP;
}

/*
 * Support three levels of nesting
 */
static char mime_boundaries[3][71];
static uint8_t mime_idx;

/*
 * linebuf[] is expected to contain the Content-Type: header
 * Parse it and obtain the MIME boundary, write it to buf[]
 * Returns 1 if boundary was found or if there is no room for more
 * boundaries to be stored, 0 if boundary was not found.
 *
 * Three levels of nesting are supported, to avoid mime_boundaries[]
 * chewing up too much memory.
 */
uint8_t mime_get_boundary(void) {
  char *p, *q;
  if (mime_idx > 2)
    return 1; // No point in trying if nested too deep
  p = strstr(linebuf, "boundary=");
  if (p) {
    q = strstr(p + 9, "\r");  // Terminated by EOL ..
    if (!q)
      q = strstr(p + 9, ";"); // .. or by semicolon
    if (q) {
      if (*(p + 9) == '\"') {
	// Trim quotes, if present
        strncpy(mime_boundaries[mime_idx], p + 10, q - p - 10);
        mime_boundaries[mime_idx][q - p - 11] = '\0';
      } else {
        strncpy(mime_boundaries[mime_idx], p + 9, q - p - 9);
        mime_boundaries[mime_idx][q - p - 9] = '\0';
      }
      //printf("Boundary: %s\n", mime_boundaries[mime_idx]);
      ++mime_idx;
    }
    return 1; // Got boundary
  }
  return 0; // Did not get boundary
}

/*
 * Check if text at p is a MIME boundary.
 * Uses the MIME boundaries stored in mime_boundaries[]
 * p is assumed to point to start of line.
 */
uint8_t is_mime_boundary(char *p) {
 uint8_t i;
 if (strncmp(p, "--", 2))  // Must start with "--"
   return 0;
 p += 2;
 for (i = 0; i < mime_idx; ++i) {
   // Match boundary followed by '\r' or "--" (or anything else, actually)
   if (strncmp(mime_boundaries[i], p, strlen(mime_boundaries[i])) == 0)
     return 1;
 }
 return 0;
}

/*
 * Display email with simple pager functionality
 * Includes support for decoding MIME headers
 */
void email_pager(struct emailhdrs *h) {
  static struct emailhdrs hh;
  uint32_t pos = 0;
  uint8_t *cursorrow = (uint8_t*)CURSORROW, mime = 0;
  FILE *sbackfp = NULL;
  const int8_t *b = b64dec - 43;
  FILE *attachfp;
  uint16_t linecount, chars;
  uint8_t mime_enc, mime_binary, mime_hasfile, eof,
          screennum, maxscreennum, attnum;
  uint8_t c, *readp, *writep;

  // We do not need all the email headers for the summary screen right now.
  // Freeing them can release up to nearly 8KB. The caller rebuilds the
  // summary info by calling read_email_db().
  hh = *h;
  free_headers_list();

  clrscr2();
  snprintf(filename, 80, email_file, cfg_emaildir, curr_mbox, hh.emailnum);
  fp = fopen(filename, "rb");
  if (!fp) {
    if (sbackfp)
      fclose(sbackfp);
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }
  pos = hh.skipbytes;
  fseek(fp, pos, SEEK_SET); // Skip over headers
  mime_enc = ENC_7BIT;
  mime_idx = 0;
restart:
  eof = 0;
  linecount = 0;
  readp = linebuf;
  writep = linebuf;
  attachfp = NULL;
  mime_binary = 0;
  mime_hasfile = 0;
  attnum = 0;
  if (sbackfp)
    fclose(sbackfp);
  _filetype = PRODOS_T_BIN;
  _auxtype = 0;
  snprintf(filename, 80, "%s/SCROLLBACK", cfg_emaildir);
  unlink(filename);
  sbackfp = fopen(filename, "wb+");
  if (!sbackfp) {
    error(ERR_NONFATAL, sb_err);
  }
  maxscreennum = screennum = 0;
  clrscr2();
  fputs("Date:    ", stdout);
  printfield(hh.date, 0, 39);
  fputs("\nFrom:    ", stdout);
  decode_qp_header(hh.from);
  printfield(linebuf, 0, 70);
  if (strncmp(hh.to, "News:", 5) == 0) {
    fputs("\nNewsgrp: ", stdout);
    printfield(&(hh.to[5]), 0, 70);
    if (hh.cc[0] != '\0') {
      fputs("\nOrg:     ", stdout);
      printfield(hh.cc, 0, 70);
    }
  } else {
    fputs("\nTo:      ", stdout);
    printfield(hh.to, 0, 70);
    if (hh.cc[0] != '\0') {
      fputs("\nCC:      ", stdout);
      printfield(hh.cc, 0, 70);
    }
  }
  fputs("\nSubject: ", stdout);
  decode_qp_header(hh.subject);
  printfield(linebuf, 0, 70);
  fputs("\n\n", stdout);
  get_line(fp, 1, linebuf, LINEBUFSZ, &pos); // Reset buffer
  while (1) {
    if (!readp)
      readp = linebuf;
    if (!writep)
      writep = linebuf;
    if (get_line(fp, 0, writep, (LINEBUFSZ - (writep - linebuf)), &pos) == 0) {
      eof = 1;
      goto endscreen;
    }
    ++linecount;
    if ((mime >= 1) && is_mime_boundary(writep)) {
      if (attachfp)
        fclose(attachfp);
      if ((mime == 4) && mime_hasfile) {
        putchar(BACKSPACE); // Erase spinner
        puts("[OK]");
      }
      attachfp = NULL;
      mime = 2;
      mime_enc = ENC_7BIT;
      mime_binary = 0;
      mime_hasfile = 0;
      readp = writep = NULL;
    } else if ((mime < 4) && (mime >= 2)) {
      if (!strncasecmp(writep, ct, 14)) {
        if (!strncmp(writep + 14, "text/plain", 10)) {
          mime = 3;
        } else if (!strncmp(writep + 14, "text/html", 9)) {
          printf("\n<Not showing HTML>\n");
          mime = 1;
        } else {
          mime = 2 + mime_get_boundary(); // 3 if boundary, 2 otherwise
          mime_binary = 1;
        }
      } else if (!strncasecmp(writep, cte, 27)) {
        mime = 3;
        mime_enc = mime_encoding(writep);
        if (mime_enc == ENC_SKIP) {
          printf(unsupp_enc, writep + 27);
          mime = 1;
        }
      } else if (strstr(writep, "filename=")) {
        mime_hasfile = 1;
        snprintf(filename, 80, "%s", strstr(writep, "filename=") + 9);
        printf("%cAttachment %u                                                                   %c\n",
               INVERSE, ++attnum, NORMAL); 
        printf("  MIME filename:  %s", filename);
        sanitize_filename(filename);
        snprintf(userentry, 80, "%s/ATTACHMENTS/%s",
                cfg_emaildir, filename);
        strcpy(filename, userentry);
prompt_dl:
        if (prompt_okay_attachment(filename)) {
          printf("*** Attachment -> %s  ", filename);
          attachfp = fopen(filename, "wb");
          if (!attachfp) {
            printf("\n*** Can't open %s\n", filename);
            goto prompt_dl;
          }
        } else
          attachfp = NULL;
      } else if ((mime == 3) && (!strncmp(writep, "\r", 1))) {
        mime = 4;
        if (!attachfp && mime_hasfile) {
          mime_enc = ENC_SKIP; // Skip over MIME parts user chose to skip
          printf("*** Skipping      %s  ", filename);
        } else if (!attachfp && mime_binary) {
          mime_enc = ENC_SKIP; // Skip over binary MIME parts with no filename
          printf("\n");
        }
      } else if (mime == 2) {
        mime = 2 + mime_get_boundary(); // 3 if boundary, 2 otherwise
      }
      readp = writep = NULL;
    } else if (mime == 4) {
      switch (mime_enc) {
      case ENC_QP:
        chars = decode_quoted_printable(writep, 0);
        break;
       case ENC_B64:
        chars = decode_base64(writep);
        break;
       case ENC_SKIP:
        readp = writep = NULL;
        break;
      }
      if (mime_hasfile && !(linecount % 10))
        spinner();
    }
    if (readp) {
      if ((mime == 0) || ((mime == 4) && !mime_hasfile)) {
        do {
          c = word_wrap_line(stdout, &readp, 80, 0);
          if (*cursorrow == 22)
            break; 
        } while (c == 1);
        if (readp) {
          chars = strlen(readp);
          memmove(linebuf, readp, strlen(readp));
          readp = linebuf;
          writep = linebuf + chars;
        } else
          writep = NULL;
      }
      if ((mime == 4) && mime_hasfile) {
        if (attachfp)
          fwrite(readp, 1, chars, attachfp);
        readp = writep = NULL;
      }
      if (mime == 1) {
        readp = writep = NULL;
      }
    }
endscreen:
    if (!mime_hasfile && ((*cursorrow == 22) || eof)) {
      printf("\n%c[%07lu] %s         | B)ack | T)op | H)drs | M)IME | Q)uit%c",
             INVERSE,
             pos,
             (eof ? "       ** END **      " : "SPACE continue reading"),
             NORMAL);
      if (sbackfp) {
        save_screen_to_scrollback(sbackfp);
        ++screennum;
        ++maxscreennum;
      }
retry:
      c = cgetc();
      switch (c | 0x20) {
      case ' ':
        if (sbackfp && (screennum < maxscreennum)) {
          load_screen_from_scrollback(sbackfp, ++screennum);
          goto retry;
        } else {
          if (eof) {
            putchar(BELL);
            goto retry;
          }
        }
        break;
      case 'b':
        if (sbackfp && (screennum > 1)) {
          load_screen_from_scrollback(sbackfp, --screennum);
          goto retry;
        } else {
          putchar(BELL);
          goto retry;
        }
        break;
      case 't':
        mime = 0;
        pos = hh.skipbytes;
        fseek(fp, pos, SEEK_SET);
        goto restart;
        break;
      case 'h':
        mime = 0;
        pos = 0;
        fseek(fp, pos, SEEK_SET);
        goto restart;
      break;
      case 'm':
        mime = 1;
        mime_enc = ENC_7BIT;
        mime_binary = 0;
        pos = 0;
        fseek(fp, pos, SEEK_SET);
        get_line(fp, 1, linebuf, LINEBUFSZ, &pos); // Reset buffer
        do {
          get_line(fp, 0, linebuf, LINEBUFSZ, &pos);
          if (!strncasecmp(linebuf, ct, 14)) {
            mime = 3 + mime_get_boundary(); // 4 if boundary, 3 otherwise
            continue;
          }
          if (!strncasecmp(linebuf, cte, 27)) {
            mime = 4;
            mime_enc = mime_encoding(linebuf);
            if (mime_enc == ENC_SKIP) {
              mime = 0;
              break;
            }
            continue;
          }
          if (mime == 3) {       // We have Content-Type but no boundary yet
            mime_get_boundary();
            mime = 4;
          }
        } while (linebuf[0] != '\r');
        pos = hh.skipbytes;
        fseek(fp, pos, SEEK_SET);
        goto restart;
      case 'q':
        if (attachfp)
          fclose(attachfp);
        if (sbackfp)
          fclose(sbackfp);
        fclose(fp);
        return;
      default:
        putchar(BELL);
        goto retry;
      }
      clrscr2();
    }
  }
}

/*
 * Write updated email headers to EMAIL.DB
 */
void write_updated_headers(struct emailhdrs *h, uint16_t pos) {
  uint16_t l;
  snprintf(filename, 80, email_db, cfg_emaildir, curr_mbox);
  _filetype = PRODOS_T_BIN;
  _auxtype = 0;
  fp = fopen(filename, "rb+");
  if (!fp)
    error(ERR_FATAL, cant_open, filename);
  if (fseek(fp, (uint32_t)(pos - 1) * EMAILHDRS_SZ_ON_DISK, SEEK_SET))
    error(ERR_FATAL, cant_seek, filename);
  l = fwrite(h, 1, EMAILHDRS_SZ_ON_DISK, fp);
  if (l != EMAILHDRS_SZ_ON_DISK)
    error(ERR_FATAL, cant_write, filename);
  fclose(fp);
}

/*
 * Create new mailbox
 * Create directory, EMAIL.DB and NEXT.EMAIL files
 */
void new_mailbox(char *mbox) {
  snprintf(filename, 80, "%s/%s", cfg_emaildir, mbox);
  if (mkdir(filename)) {
    error(ERR_NONFATAL, "Can't create dir %s", filename);
    return;
  }
  snprintf(filename, 80, email_db, cfg_emaildir, mbox);
  _filetype = PRODOS_T_BIN;
  _auxtype = 0;
  fp = fopen(filename, "wb");
  if (!fp) {
    error(ERR_NONFATAL, "Can't create EMAIL.DB");
    return;
  }
  fclose(fp);
  snprintf(filename, 80, next_email, cfg_emaildir, mbox);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp = fopen(filename, "wb");
  if (!fp) {
    error(ERR_NONFATAL, "Can't create NEXT.EMAIL");
    return;
  }
  fprintf(fp, "1");
  fclose(fp);
}

/*
 * Change current mailbox
 */
void switch_mailbox(char *mbox) {
  char prev_mbox[80];
  uint8_t i = 0;
  // Treat '.' as shortcut for INBOX
  if (!strcmp(mbox, "."))
    strcpy(mbox, inbox);
  while (mbox[i]) {
    mbox[i] = toupper(mbox[i]);
    ++i;
  }
  strcpy(prev_mbox, curr_mbox);
  strcpy(curr_mbox, mbox);
  first_msg = 1;
  i = read_email_db(first_msg, 1, 1); // Errors non-fatal
  if (i) {
    strcpy(curr_mbox, prev_mbox);
    i = read_email_db(first_msg, 1, 0); // Errors fatal
  }
  selection = 1;
  email_summary();
}

/*
 * Purge deleted messages from current mailbox
 */
void purge_deleted(void) {
  uint16_t count = 0, delcount = 0;
  struct emailhdrs *h;
  FILE *fp2;
  uint16_t l;
  h = (struct emailhdrs*)malloc(sizeof(struct emailhdrs));
  if (!h)
    error(ERR_FATAL, cant_malloc);
  snprintf(filename, 80, email_db, cfg_emaildir, curr_mbox);
  fp = fopen(filename, "rb");
  if (!fp) {
    free(h);
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }
  snprintf(filename, 80, email_db_new, cfg_emaildir, curr_mbox);
  _filetype = PRODOS_T_BIN;
  _auxtype = 0;
  fp2 = fopen(filename, "wb");
  if (!fp2) {
    free(h);
    fclose(fp);
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }
  while (1) {
    l = fread(h, 1, EMAILHDRS_SZ_ON_DISK, fp);
    ++count;
    if (l != EMAILHDRS_SZ_ON_DISK)
      goto done;
    if (h->status == 'D') {
      snprintf(filename, 80, email_file, cfg_emaildir, curr_mbox, h->emailnum);
      if (unlink(filename))
        error(ERR_NONFATAL, cant_delete, filename);
      goto_prompt_row();
      putchar(CLRLINE);
      printf("%u msgs deleted", ++delcount);
    } else {
      l = fwrite(h, 1, EMAILHDRS_SZ_ON_DISK, fp2);
      if (l != EMAILHDRS_SZ_ON_DISK) {
        error(ERR_NONFATAL, cant_write, filename);
        free(h);
        fclose(fp);
        fclose(fp2);
        return;
      }
    }
  }
done:
  free(h);
  fclose(fp);
  fclose(fp2);
  snprintf(filename, 80, email_db, cfg_emaildir, curr_mbox);
  if (unlink(filename)) {
    error(ERR_NONFATAL, cant_delete, filename);
    return;
  }
  snprintf(userentry, 80, email_db_new, cfg_emaildir, curr_mbox);
  if (rename(userentry, filename)) {
    error(ERR_NONFATAL, "Can't rename %s", userentry);
    return;
  }
}

enum ne_op {NEXT_EMAIL_GET, NEXT_EMAIL_UPD};

/*
 * If op is NEXT_EMAIL_GET: Get next email number from NEXT.EMAIL
 * If op is NEXT_EMAIL_UPD: Write next email number to NEXT.EMAIL
 * op - NEXT_EMAIL_GET or NEXT_EMAIL_UPD
 * mbox - Name of mailbox
 * num - pointer to the value to get or set
 * Returns 1 on error, 0 if all is good
 */
uint8_t next_email_op(enum ne_op op, char *mbox, uint16_t *num) {
  snprintf(filename, 80, next_email, cfg_emaildir, mbox);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp = fopen(filename, (op == NEXT_EMAIL_UPD ? "wb" : "rb"));
  if (!fp) {
    error(ERR_NONFATAL, cant_open, filename);
    return 1;
  }
  if (op == NEXT_EMAIL_UPD)
    fprintf(fp, "%u", *num + 1);
  else
    fscanf(fp, "%u", num);
  fclose(fp);
  return 0;
}

/*
 * Copy string p to q truncating any trailing spaces.
 */
void truncate_header(char *p, char *q, uint8_t l) {
  int16_t last_char = -1;
  uint8_t i;
  for (i = 0; i < l; ++i) {
    q[i] = p[i];
    if ((p[i] != ' ') && (p[i] != '\0'))
      last_char = i;
  }
  q[last_char + 1] = '\0';
}

/*
 * Parse 'from' address, of the form 'Joe Smith <jsmith@example.org>'
 * If < > angle brackets are present, just return the text within them,
 * otherwise return the whole string.
 * p - input buffer
 * q - output buffer
 * Returns 0 if okay, 1 on error
 */
uint8_t parse_from_addr(char *p, char *q) {
  char *start = strchr(p, '<');
  char *end   = strchr(p, '>');
  uint8_t l;
  if (!start) {
    strcpy(q, p);
    return 0;
  } else {
    if (end)
      l = end - start - 1;
    else {
      error(ERR_NONFATAL, "Bad address '%s'", p);
      return 1;
    }
  }
  strncpy(q, start + 1, l);
  q[l] = '\0';
  return 0;
}

/*
 * Prompt for a name in the line below the menu, store it in userentry
 * Returns number of chars read.
 * prompt - Message to display before > prompt
 * is_file - if 1, restrict chars to those allowed in ProDOS filename
 *           if 2, restrict chars to those allowed in ProDOS path
 * Returns number of chars read, or 255 if ESC pressed
 */
uint8_t prompt_for_name(char *prompt, uint8_t is_file) {
  uint16_t i;
  char c;
  goto_prompt_row();
  printf("%s>", prompt);
  i = 0;
  while (1) {
    c = cgetc();
    if ((is_file > 0) && !isalnum(c) && (c != RETURN) && (c != BACKSPACE) &&
        (c != DELETE) && (c != ESC) && (c != '.') && (c != '/')) {
      putchar(BELL);
      continue;
    }
    if ((is_file == 1) && (c == '/')) {
      putchar(BELL);
      continue;
    }
    switch (c) {
    case RETURN:
      goto done;
    case BACKSPACE:
    case DELETE:
      if (i > 0) {
        putchar(BACKSPACE);
        putchar(' ');
        putchar(BACKSPACE);
        --i;
      } else
        putchar(BELL);
      break;
    case ESC:
      userentry[0] = '\0';
      i = 255;
      goto esc_pressed;
    default:
      putchar(c);
      userentry[i++] = c;
    }
    if (i == 79)
      goto done;
  }
done:
  userentry[i] = '\0';
esc_pressed:
  putchar(CLRLINE);
  goto_prompt_row();
  return i;
}

/*
 * Add prefix to subject line if it is not already there
 * f - File descriptor to write to
 * subject - Existing subject text
 * prefix - Text to prefix ("Re: " or "Fwd: ")
 * Adds 'Re: ' to subject line unless it is already there
 */
void prefix_subject(FILE *f, char *subject, char *prefix) {
  decode_qp_header(subject);
  fprintf(f, "Subject: %s%s\r",
          (strncmp(subject, prefix, strlen(prefix)) ? prefix : ""), linebuf);
}

/*
 * Write email headers for replies and forwarded messages
 * fp1  - File handle of the mail message being replied/forwarded
 * fp2  - File handle of the destination mail message
 * h    - headers of the message being replied/forwarded
 * mode - 'R' for reply, 'F' for forward
 * fwd_to - Recipient (used for mode=='F' only)
 * Returns 0 if okay, 1 on error, 255 if ESC pressed
 */
uint8_t write_email_headers(FILE *fp1, FILE *fp2, struct emailhdrs *h,
                            char mode, char *fwd_to) {
  struct datetime dt;
  fprintf(fp2, "From: %s\r", cfg_emailaddr);
  truncate_header(h->subject, buf, 80);
  prefix_subject(fp2, buf, (mode == 'F' ? "Fwd: " : "Re: "));
  readdatetime(&dt);
  datetimelong(&dt, buf);
  fprintf(fp2, "Date: %s\r", buf);
  if (mode == 'R') {
    truncate_header(h->from, filename, 80);
    if (parse_from_addr(filename, buf))
      return 1;
    fprintf(fp2, "To: %s\r", buf);
  } else
    fprintf(fp2, "To: %s\r", fwd_to);
  if (prompt_for_name("cc", 0) == 255)
    return 255; // ESC pressed
  if (strlen(userentry) > 0)
    fprintf(fp2, "cc: %s\r", userentry);
  fprintf(fp2, a2_forever, "X-Mailer", PROGNAME);
  if (mode == 'R') {
    truncate_header(h->date, buf, 40);
    fprintf(fp2, "On %s, ", buf);
    decode_qp_header(h->from);
    truncate_header(linebuf, buf, 80);
    fprintf(fp2, "%s wrote:\r\r", buf);
  } else {
    fprintf(fp2, "-------- Forwarded Message --------\r");
    decode_qp_header(h->subject);
    truncate_header(linebuf, buf, 80);
    fprintf(fp2, "Subject: %s\r", buf);
    truncate_header(h->date, buf, 40);
    fprintf(fp2, "Date: %s\r", buf);
    decode_qp_header(h->from);
    truncate_header(linebuf, buf, 80);
    fprintf(fp2, "From: %s\r", buf);
    truncate_header(h->to, buf, 80);
    fprintf(fp2, "To: %s\r\r", buf);
  }
  fseek(fp1, h->skipbytes, SEEK_SET); // Skip headers when copying
  return 0;
}

/*
 * Write news article headers for follow-up postings
 * fp1  - File handle of the article being replied/forwarded
 * fp2  - File handle of the destination article
 * h    - headers of the article being replied/forwarded
 * Returns 0 if okay, 1 on error.
 */
uint8_t write_news_headers(FILE *fp1, FILE *fp2, struct emailhdrs *h, uint16_t num) {
  struct datetime dt;
  uint32_t pos;
  uint8_t refstate = 0, idstate = 0;
  fprintf(fp2, "From: %s\r", cfg_emailaddr);
  truncate_header(&(h->to[5]), buf, 75);
  fprintf(fp2, "Newsgroups: %s\r", buf);
  truncate_header(h->subject, buf, 80);
  prefix_subject(fp2, buf, "Re: ");
  readdatetime(&dt);
  datetimelong(&dt, userentry);
  fprintf(fp2, "Date: %s\r", userentry);
  fprintf(fp2, "Message-ID: <%05d-%s>\r", num, cfg_emailaddr);
  get_line(fp1, 1, linebuf, LINEBUFSZ, &pos); // Reset buffer
  while (1) {
    get_line(fp1, 0, linebuf, LINEBUFSZ, &pos);
    if (linebuf[0] == '\r') { // End of headers
      if ((idstate == 1) && (refstate == 0))
        fprintf(fp2, "References: %s", userentry);
      break;
    }
    if (strncmp(linebuf, "Message-ID:", 11) == 0) {
      if (refstate == 2)
        fprintf(fp2, &(linebuf[11]));
      else
        strncpy(userentry, &(linebuf[11]), 80);
      idstate = 1;
    } else if (strncmp(linebuf, "References:", 11) == 0) {
      if (refstate == 0) {
        fprintf(fp2, linebuf);
        refstate = 1;
      }
    } else if (linebuf[0] == ' ') {
      if (refstate == 1)
        fprintf(fp2, "%s", linebuf);
    } else {
      if (refstate == 1) {
        if (idstate == 1)
          fprintf(fp2, "%s", userentry);
        refstate = 2;
      }
    }
  }
  fprintf(fp2, a2_forever, "User-Agent", PROGNAME);
  truncate_header(h->from, buf, 80);
  fprintf(fp2, "%s wrote:\r\r", buf);
  fseek(fp1, h->skipbytes, SEEK_SET); // Skip headers when copying
  return 0;
}

/*
 * Prompt ok?
 * Returns 1 for yes
 *         0 for no or ESC
 */
char prompt_okay(char *msg) {
  char c;
  goto_prompt_row();
  printf("%sSure? (y/n/ESC)", msg);
  while (1) {
    c = cgetc_update_status();
    if ((c == 'y') || (c == 'Y') || (c == 'n') || (c == 'N') || (c == ESC))
      break;
    putchar(BELL);
  } 
  if ((c == 'y') || (c == 'Y'))
    c = 1;
  else 
    c = 0;
  putchar(CLRLINE);
  return c;
}

/*
 * Obtain the body of an email to include in a reply or forwarded message
 * For a plain text email, the body is everything after the headers
 * For a MIME multipart email, we take the first Text/Plain section
 * Email file to read is expected to be already open using fp
 * f - File handle for destination file (also already open)
 * mode - 'R' if reply, 'F' if forward, 'N' if news follow-up
 */
void get_email_body(struct emailhdrs *h, FILE *f, char mode) {
  uint16_t chars;
  uint8_t c, *readp, *writep;
  uint32_t pos = 0;
  const int8_t *b = b64dec - 43;
  uint8_t  mime = 0, mime_enc = ENC_7BIT, mime_binary, mime_hasfile;
  fseek(fp, pos, SEEK_SET);
  get_line(fp, 1, linebuf, LINEBUFSZ, &pos); // Reset buffer
  mime_idx = 0;
  do {
    spinner();
    get_line(fp, 0, linebuf, LINEBUFSZ, &pos);
    if (!strncasecmp(linebuf, mime_ver, 17))
      mime = 1;
    if (!strncasecmp(linebuf, ct, 14)) {
      mime = 3 + mime_get_boundary(); // 4 if boundary, 3 otherwise
      continue;
    }
    if (!strncasecmp(linebuf, cte, 27)) {
      mime = 4;
      mime_enc = mime_encoding(linebuf);
      if (mime_enc == ENC_SKIP) {
        error(ERR_NONFATAL, unsupp_enc, linebuf + 27);
        return;
      }
      continue;
    }
    if (mime == 3) {       // We have Content-Type but no boundary yet
      mime_get_boundary();
      mime = 4;
    }
  } while (linebuf[0] != '\r');
  pos = h->skipbytes;
  fseek(fp, pos, SEEK_SET);
  readp = linebuf;
  writep = linebuf;
  mime_binary = 0;
  mime_hasfile = 0;
  get_line(fp, 1, linebuf, LINEBUFSZ, &pos); // Reset buffer
  while (1) {
    spinner();
    if (!readp)
      readp = linebuf;
    if (!writep)
      writep = linebuf;
    if (get_line(fp, 0, writep, (LINEBUFSZ - (writep - linebuf)), &pos) == 0)
      break;
    if ((mime >= 1) && is_mime_boundary(writep)) {
      mime = 2;
      mime_enc = ENC_7BIT;
      mime_binary = 0;
      mime_hasfile = 0;
      readp = writep = NULL;
    } else if ((mime < 4) && (mime >= 2)) {
      if (!strncasecmp(writep, ct, 14)) {
        if (!strncmp(writep + 14, "text/plain", 10)) {
          mime = 3;
        } else if (!strncmp(writep + 14, "text/html", 9)) {
          mime = 1;
        } else {
          mime = 2 + mime_get_boundary(); // 3 if boundary, 2 otherwise
          mime_binary = 1;
        }
      } else if (!strncasecmp(writep, cte, 27)) {
        mime = 3;
        mime_enc = mime_encoding(writep);
        if (mime_enc == ENC_SKIP) {
          printf(unsupp_enc, writep + 27);
          mime = 1;
        }
      } else if (strstr(writep, "filename=")) {
        mime_hasfile = 1;
      } else if ((mime == 3) && (!strncmp(writep, "\r", 1))) {
        mime = 4;
        if (mime_binary)
          mime_enc = ENC_SKIP; // Skip over binary MIME parts
      } else if (mime == 2) {
        mime = 2 + mime_get_boundary(); // 3 if boundary, 2 otherwise
      }
      readp = writep = NULL;
    } else if (mime == 4) {
      switch (mime_enc) {
      case ENC_QP:
        chars = decode_quoted_printable(writep, 0);
        break;
       case ENC_B64:
        chars = decode_base64(writep);
        break;
       case ENC_SKIP:
        readp = writep = NULL;
        break;
      }
    }
    if (readp) {
      if ((mime == 0) || ((mime == 4) && !mime_hasfile)) {
        do {
          c = word_wrap_line(f, &readp, 78, mode);
        } while (c == 1);
        if (readp) {
          chars = strlen(readp);
          memmove(linebuf, readp, strlen(readp));
          readp = linebuf;
          writep = linebuf + chars;
        } else
          writep = NULL;
      }
      if ((mime == 4) && mime_binary) {
        readp = writep = NULL;
      }
      if (mime == 1) {
        readp = writep = NULL;
      }
    }
  }
}

/*
 * Copies the current message to mailbox mbox.
 * h is a pointer to the emailheaders for the message to copy
 * idx is the index of the message in EMAIL.DB in the source mailbox (1-based)
 * mbox is the name of the destination mailbox
 * delete - if set to 1 then the message will be marked as deleted in the
 *          source mbox
 * mode - 'R' for reply, 'F' for forward, 'N' for news follow-up, otherwise ' '
 */
void copy_to_mailbox(struct emailhdrs *h, uint16_t idx,
                     char *mbox, uint8_t delete, char mode) {
  uint16_t num, buflen, l, written;
  FILE *fp2;

  if (mode == 'N') {
    if (strncmp(h->to, "News:", 5) != 0) {
      error(ERR_NONFATAL, "Not a news article");
      return;
    }
  }

  if (mode == 'F') {
    if (prompt_for_name("Fwd to", 0) == 255)
      return; // ESC pressed
    if (strlen(userentry) == 0)
      return;
  }

  // Read next number from dest/NEXT.EMAIL
  if (next_email_op(NEXT_EMAIL_GET, mbox, &num))
    return;

  // Open source email file
  snprintf(filename, 80, email_file, cfg_emaildir, curr_mbox, h->emailnum);
  fp = fopen(filename, "rb");
  if (!fp) {
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }

  // Open destination email file
  snprintf(filename, 80, email_file, cfg_emaildir, mbox, num);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp2 = fopen(filename, "wb");
  if (!fp2) {
    fclose(fp);
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }

  l = 0;
  if ((mode == 'R') || (mode == 'F')) {
    l = write_email_headers(fp, fp2, h, mode, userentry);
    if (l == 1)
      error(ERR_NONFATAL, "Invalid email header");
  } else if (mode == 'N') {
    l = write_news_headers(fp, fp2, h, num);
    if (l == 1)
      error(ERR_NONFATAL, "Invalid article header");
  }
  if (l > 0) {
    fclose(fp);
    fclose(fp2);
    return;
  }

  // Make sure spinner is in the right place
  if ((mode == 'R') || (mode == 'F') || (mode == 'N'))
    goto_prompt_row();

  // Copy email body
  putchar(' '); // For spinner
  if ((mode == 'R') || (mode == 'N'))
    fputc('>', fp2);
  if ((mode == 'R') || (mode == 'F') || (mode == 'N')) {
    get_email_body(h, fp2, mode);
  } else {
    while (1) {
      buflen = fread(buf, 1, READSZ, fp);
      spinner();
      if (buflen == 0)
        break;
      written = fwrite(buf, 1, buflen, fp2);
      if (written != buflen) {
        error(ERR_NONFATAL, "Write error during copy");
        fclose(fp);
        fclose(fp2);
        return;
      }
    }
  }

  putchar(BACKSPACE);
  putchar(' ');
  putchar(BACKSPACE);

  fclose(fp);
  fclose(fp2);

  // Update dest/EMAIL.DB unless this is R)eply or F)orward
  // The upshot of this is we never create EMAIL.DB in OUTBOX
  if (mode == ' ') {
    snprintf(filename, 80, email_db, cfg_emaildir, mbox);
    _filetype = PRODOS_T_BIN;
    _auxtype = 0;
    fp = fopen(filename, "ab");
    if (!fp) {
      error(ERR_NONFATAL, cant_open, filename);
      return;
    }
    buflen = h->emailnum; // Just reusing buflen as a temporary
    h->emailnum = num;
    l = fwrite(h, 1, EMAILHDRS_SZ_ON_DISK, fp);
    if (l != EMAILHDRS_SZ_ON_DISK) {
      error(ERR_NONFATAL, "Can't write to %s/EMAIL.DB %u %u", mbox);
      return;
    }
    h->emailnum = buflen ;
    fclose(fp);
  }

  // Update dest/NEXT.EMAIL, incrementing count by 1
  if (next_email_op(NEXT_EMAIL_UPD, mbox, &num))
    return;

  if (delete)
    h->status = 'D';
  h->tag = ' '; // Untag files after copy or move
  write_updated_headers(h, idx);
  email_summary_for(selection);

  if (mode != ' ') {
    snprintf(filename, 80, email_file, cfg_emaildir, mbox, num);
    load_editor(mode == 'N' ? 2 : 1);
  }
}

/*
 * Return index into EMAIL.DB for current selection.
 */
uint16_t get_db_index(void) {
  if (!reverse)
    return first_msg + selection - 1;
  else
    return total_msgs - (first_msg + selection - 1) + 1;
}

/*
 * Check if there are tagged messages.  If not, just call copy_to_mailbox()
 * on the current message.  If they are, prompt the user and, if affirmative,
 * iterate through the tagged messages calling copy_to_mailbox() on each.
 */
uint8_t copy_to_mailbox_tagged(char *mbox, char mode, uint8_t delete) {
  uint16_t count = 0, tagcount = 0;
  struct emailhdrs *h;
  uint16_t l;
  if (total_tag == 0) {
    h = get_headers(selection);
    copy_to_mailbox(h, get_db_index(), mbox, delete, ' ');
    return 0;
  }
  snprintf(filename, 80, "%s %u tagged - ",
           (mode == 'C' ? "Copy" : (mode == 'M' ? "Move" : "Archive")), total_tag);
  if (!prompt_okay(filename))
    return 0;
  h = (struct emailhdrs*)malloc(sizeof(struct emailhdrs));
  if (!h)
    error(ERR_FATAL, cant_malloc);
  while (1) {
    snprintf(filename, 80, email_db, cfg_emaildir, curr_mbox);
    _filetype = PRODOS_T_BIN;
    _auxtype = 0;
    fp = fopen(filename, "rb+");
    if (!fp) {
      free(h);
      error(ERR_NONFATAL, cant_open, filename);
      return 1;
    }
    if (fseek(fp, count * EMAILHDRS_SZ_ON_DISK, SEEK_SET)) {
      error(ERR_NONFATAL, cant_seek, filename);
      goto err;
    }
    l = fread(h, 1, EMAILHDRS_SZ_ON_DISK, fp);
    fclose(fp);
    ++count;
    if (l != EMAILHDRS_SZ_ON_DISK) {
      free(h);
      read_email_db(first_msg, 1, 0);
      email_summary();
      return 0;
    }
    if (h->tag == 'T') {
      h->tag = ' '; // Don't want it tagged in the destination
      goto_prompt_row();
      putchar(CLRLINE);
      printf("%u/%u:", ++tagcount, total_tag);
      copy_to_mailbox(h, count, mbox, delete, ' ');
    }
  }
err:
  free(h);
  fclose(fp);
  return 1;
}

/*
 * Create a blank outgoing message and put it in OUTBOX.
 * OUTBOX is not a 'proper' mailbox (no EMAIL.DB)
 */
void create_blank_outgoing(void) {
  struct datetime dt;
  uint16_t num;
  if (next_email_op(NEXT_EMAIL_GET, outbox, &num))
    return;
  snprintf(filename, 80, email_file, cfg_emaildir, outbox, num);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp = fopen(filename, "wb");
  if (!fp) {
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }
  fprintf(fp, "From: %s\r", cfg_emailaddr);
  if (prompt_for_name("To", 0) == 255)
    goto done; // ESC pressed
  if (strlen(userentry) == 0)
    goto done;
  fprintf(fp, "To: %s\r", userentry);
  if (prompt_for_name("Subject", 0) == 255)
    goto done; // ESC pressed
  fprintf(fp, "Subject: %s\r", userentry);
  readdatetime(&dt);
  datetimelong(&dt, userentry);
  fprintf(fp, "Date: %s\r", userentry);
  if (prompt_for_name("cc", 0) == 255)
    goto done; // ESC pressed
  if (strlen(userentry) > 0)
    fprintf(fp, "cc: %s\r", userentry);
  fprintf(fp, a2_forever, "X-Mailer", PROGNAME);
  fclose(fp);
  if (next_email_op(NEXT_EMAIL_UPD, outbox, &num))
    return;
  snprintf(filename, 80, email_file, cfg_emaildir, outbox, num);
  load_editor(1);
done:
  fclose(fp);
}

/*
 * Create a blank news article and put it in NEWS.OUTBOX
 * NEWS.OUTBOX is not a 'proper' mailbox (no EMAIL.DB)
 */
void create_blank_news(void) {
  struct datetime dt;
  uint16_t num;
  if (next_email_op(NEXT_EMAIL_GET, news_outbox, &num))
    return;
  snprintf(filename, 80, email_file, cfg_emaildir, news_outbox, num);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp = fopen(filename, "wb");
  if (!fp) {
    error(ERR_NONFATAL, cant_open, filename);
    return;
  }
  fprintf(fp, "From: %s\r", cfg_emailaddr);
  if (prompt_for_name("Newsgroup(s)", 0) == 255)
    goto done; // ESC pressed
  if (strlen(userentry) == 0)
    goto done;
  fprintf(fp, "Newsgroups: %s\r", userentry);
  if (prompt_for_name("Subject", 0) == 255)
    goto done; // ESC pressed
  fprintf(fp, "Subject: %s\r", userentry);
  readdatetime(&dt);
  datetimelong(&dt, userentry);
  fprintf(fp, "Date: %s\r", userentry);
  fprintf(fp, "Message-ID: <%05d-%s>\r", num, cfg_emailaddr);
  fprintf(fp, a2_forever, "User-Agent", PROGNAME);
  fclose(fp);
  if (next_email_op(NEXT_EMAIL_UPD, news_outbox, &num))
    return;
  snprintf(filename, 80, email_file, cfg_emaildir, news_outbox, num);
  load_editor(2);
done:
  fclose(fp);
}

/*
 * Display help screen
 */
void help(uint8_t num) {
  char *p;
  char c;
  uint16_t i, s;
  uint8_t cont;
  revers(0);
  cursor(0);
  clrscr2();
  snprintf(filename, 80, "%s/EMAILHELP%u.TXT", cfg_instdir, num);
  fp = fopen(filename, "rb");
  if (!fp) {
    printf("Can't open help file\n\n");
    goto done;
  }
  p = buf;
  do {
    s = fread(p, 1, READSZ, fp);
    cont = (s == READSZ ? 1 : 0);
    for (i = 0; i < s; ++i) {
      c = p[i];
      if (c == '{')
        printf("%s", openapple);
      else if (c == '}')
        printf("%s", closedapple);
      else if ((c != '\r') && (c != '\n'))
        putchar(c);
    }
  } while (cont);
done:
  fclose(fp);
}

/*
 * Move to the next message, if there is one.
 * Called by the code that handles down arrow.
 * Returns 1 if there are more messages below, 0 otherwise
 */
uint8_t go_to_next_message(void) {
  if (selection < num_msgs) {
    prevselection = selection;
    ++selection;
    update_highlighted();
    return 1;
  } else if (first_msg + selection - 1 < total_msgs) {
    first_msg += MSGS_PER_PAGE;
    read_email_db(first_msg, 0, 0);
    selection = 1;
    email_summary();
    return 1;
  } else
    return 0;
}

/*
 * Keyboard handler
 */
void keyboard_hdlr(void) {
  struct emailhdrs *h;
  char c;
  while (1) {
    h = get_headers(selection);
    c = cgetc_update_status();
    if (*(uint8_t*)0xc062 & 0x80) { // Closed Apple depressed
      switch (c | 0x20) {
      case 'r':    // CA-R "Retrieve news via NNTP"
        load_app(APP_NNTP_DOWN);
        break;
      case 's':    // CA-S "Sent news via NNTP"
        load_app(APP_NNTP_UP);
        break;
      case 'p':    // CA-P "Post news article"
        create_blank_news();
        break;
      case 'f':    // CA-F "Follow-up news article"
        if (h)
          copy_to_mailbox(h, get_db_index(), news_outbox, 0, 'N');
        break;
      }
      continue;
    }
    switch (c) {
    case 'k':
    case 'K':
    case UPARROW:
      if (selection > 1) {
        prevselection = selection;
        --selection;
        update_highlighted();
      } else if (first_msg > MSGS_PER_PAGE) {
        first_msg -= MSGS_PER_PAGE;
        read_email_db(first_msg, 0, 0);
        selection = num_msgs;
        email_summary();
      }
      break;
    case 'j':
    case 'J':
    case DOWNARROW:
      go_to_next_message();
      break;
    case RETURN:
    case ' ':
      if (h) {
        if (h->status == 'N')
          --total_new;
        h->status = 'R'; // Mark email read
        write_updated_headers(h, get_db_index());
        email_pager(h);
        read_email_db(first_msg, 0, 0); // email_pager() deletes the headers
        email_summary();
      }
      break;
    case 't':
    case 'T':
      if (h) {
        if (h->tag == 'T') {
          h->tag = ' ';
          --total_tag;
        } else {
          h->tag = 'T';
          ++total_tag;
        }
        write_updated_headers(h, get_db_index());
        email_summary_for(selection);
        status_bar();
        go_to_next_message();
      }
      break;
    case 'd':
    case 'D':
      if (h) {
        h->status = 'D';
        write_updated_headers(h, get_db_index());
        email_summary_for(selection);
        go_to_next_message();
      }
      break;
    case 'u':
    case 'U':
      if (h) {
        h->status = 'R';
        write_updated_headers(h, get_db_index());
        email_summary_for(selection);
        go_to_next_message();
      }
      break;
    case 'c':
    case 'C':
      if (h) {
        c = prompt_for_name("Copy to mbox", 1);
        if ((c != 0) && (c != 255))
          copy_to_mailbox_tagged(userentry, 'C', 0);
      }
      break;
    case 'm':
    case 'M':
      if (h) {
        c = prompt_for_name("Move to mbox", 1);
        if ((c != 0) && (c != 255))
          copy_to_mailbox_tagged(userentry, 'M', 1);
      }
      break;
    case 'a':
    case 'A':
      if (h) {
        goto_prompt_row();
        copy_to_mailbox_tagged("RECEIVED", 'A', 1);
      }
      break;
    case 'p':
    case 'P':
      if (h) {
        if (prompt_okay("Purge - ")) {
          purge_deleted();
          first_msg = 1;
          read_email_db(first_msg, 1, 0);
          selection = 1;
          email_summary();
        }
      }
      break;
    case 'r':
    case 'R':
      if (h)
        copy_to_mailbox(h, get_db_index(), outbox, 0, 'R');
      break;
    case 'f':
    case 'F':
      if (h)
        copy_to_mailbox(h, get_db_index(), outbox, 0, 'F');
      break;
    // Everything above here needs a selected message (h != NULL)
    // Everything below here does NOT need a selected message
    case 'n':
    case 'N':
      c = prompt_for_name("New mbox", 1);
      if ((c != 0) && (c != 255))
        new_mailbox(userentry);
      break;
    case 's':
    case 'S':
      c = prompt_for_name("Switch mbox", 1);
      if ((c != 0) && (c != 255))
        switch_mailbox(userentry);
      break;
    case 'w':
    case 'W':
      create_blank_outgoing();
      break;
    case ',':
    case '<':
      reverse = 1;
      switch_mailbox(curr_mbox);
      break;
    case '.':
    case '>':
      reverse = 0;
      switch_mailbox(curr_mbox);
      break;
    case 0x80 + 'd': // OA-D "Update date using NTP"
    case 0x80 + 'D':
      load_app(APP_DATE);
      break;
    case 0x80 + 'e': // OA-E "Open message in editor"
    case 0x80 + 'E':
      if (h) {
        snprintf(filename, 80, email_file, cfg_emaildir, curr_mbox, h->emailnum);
        load_editor(0);
      }
      break;
    case 0x80 + 'r': // OA-R "Retrieve messages from server"
    case 0x80 + 'R':
      load_app(APP_POP);
      break;
    case 0x80 + 's': // OA-S "Send messages in Outbox to server"
    case 0x80 + 'S':
      load_app(APP_SMTP);
      break;
    case 0x80 + '?': // OA-? "Help"
    case 0x80 + '/': // OA-/ "Help"
      help(1);
      c = cgetc();
      email_summary();
      break;
    case 'q':
    case 'Q':
      if (prompt_okay("Quit - ")) {
        save_prefs();
        clrscr2();
        exit(0);
      }
    default:
      putchar(BELL);
    }
  }
}

void main(void) {
  uint8_t *pp;
  pp = (uint8_t*)0xbf98;
  if (!(*pp & 0x02))
    error(ERR_FATAL, "Need 80 cols");
  if ((*pp & 0x30) != 0x30)
    error(ERR_FATAL, "Need 128K");

  videomode(VIDEOMODE_80COL);
  //printf("heapmemavail=%d heapmaxavail=%d\n", _heapmemavail(), _heapmaxavail());
  readconfigfile();
  load_prefs();
  read_email_db(first_msg, 1, 0);
  email_summary();
  keyboard_hdlr();
}

