///////////////////////////////////////////////////////////////// // emai//er - Simple Email User Agent vaguely inspired by Elm // Handles INBOX in the format created by POP65 // Bobbi June 2020 - June 2021 ///////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include // 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\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 ' * 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(); }