///////////////////////////////////////////////////////////////////////////// // Handle attaching files to outgoing messages // Bobbi July 2020 ///////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include "email_common.h" #define PROMPT_ROW 23 #define EOL '\r' #define BELL 0x07 #define BACKSPACE 0x08 #define RETURN 0x0d #define NORMAL 0x0e #define INVERSE 0x0f #define ESC 0x1b #define DELETE 0x7f #define NETBUFSZ 1500 #define LINEBUFSZ 1000 // According to RFC2822 Section 2.1.1 (998+CRLF) #define READSZ 1024 // Must be less than NETBUFSZ to fit in buf[] #define IOBUFSZ 8192 unsigned char buf[NETBUFSZ+1]; // One extra byte for null terminator char linebuf[LINEBUFSZ]; char userentry[80]; uint8_t quit_to_email; // If 1, launch EMAIL.SYSTEM on quit char filename[80]; char iobuf[IOBUFSZ]; struct attachinfo { char filename[16]; uint32_t size; struct attachinfo *next; }; struct attachinfo *attachments = NULL; #define ERR_NONFATAL 0 #define ERR_FATAL 1 /* * Annoying beep */ void beep(void) { uint8_t *p = (uint8_t*)0xc030; // Speaker uint8_t junk; uint16_t i; for (i = 0; i < 200; ++i) { junk = *p; for (junk = 0; junk < 50; ++junk); // Reduce pitch } } /* * Clear to EOL */ void clreol(void) { uint8_t x = wherex(), y = wherey(); cclear(80 - x); gotoxy(x, y); } /* * Clear line */ void clrline(void) { uint8_t x = wherex(); gotox(0); cclear(80); gotox(x); } /* * Put cursor at beginning of PROMPT_ROW */ void goto_prompt_row(void) { gotoxy(0, PROMPT_ROW); } /* * Show error messages */ void error(uint8_t fatal, const char *fmt, ...) { va_list v; if (fatal) { videomode(VIDEOMODE_80COL); clrscr(); printf("\n\n%cFATAL ERROR:%c\n\n", INVERSE, NORMAL); va_start(v, fmt); vprintf(fmt, v); va_end(v); printf("\n\n\n\n[Press Any Key To Quit]"); cgetc(); exit(1); } else { putchar('\r'); va_start(v, fmt); vprintf(fmt, v); va_end(v); printf(" - [Press Any Key]"); cgetc(); putchar('\r'); } } /* * Spinner while encoding attachments */ void spinner(uint32_t sz, uint8_t final) { static char chars[] = "|/-\\"; static char buf[10] = ""; static uint8_t i = 0; uint8_t j; for (j = 0; j < strlen(buf); ++j) putchar(BACKSPACE); if (final) { sprintf(buf, " [%lu]\n", sz); printf("%s", buf); strcpy(buf, ""); } else { sprintf(buf, "%c %lu", chars[(i++) % 4], sz); printf("%s", buf); } } /* * Read parms from EMAIL.CFG */ void readconfigfile(void) { FILE *fp = fopen("EMAIL.CFG", "r"); if (!fp) error(ERR_FATAL, "Can't open config file EMAIL.CFG"); fscanf(fp, "%s", cfg_server); fscanf(fp, "%s", cfg_user); fscanf(fp, "%s", cfg_pass); fscanf(fp, "%s", cfg_pop_delete); fscanf(fp, "%s", cfg_smtp_server); fscanf(fp, "%s", cfg_smtp_domain); fscanf(fp, "%s", cfg_instdir); fscanf(fp, "%s", cfg_emaildir); fscanf(fp, "%s", cfg_emailaddr); fclose(fp); } /* * 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 * writep - Pointer to buffer into which line will be written * n - length of buffer. Longer lines will be truncated and terminated with CR. */ uint16_t get_line(FILE *fp, char *writep, uint16_t n) { static uint16_t rd = 0; // Read static uint16_t end = 0; // End of valid data in buf uint16_t i = 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++]; if (writep[i - 1] == '\r') goto done; } done: writep[i] = '\0'; return i; } /* * Base64 encode table */ static const char b64enc[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /* * Encode Base64 format * p - Pointer to source buffer * q - Pointer to destination buffer * len - Length of buffer to encode * Returns length of encoded data */ uint16_t encode_base64(char *p, char *q, uint16_t len) { uint16_t j = 0; uint16_t i, ii; for (i = 0; i < len / 3; ++i) { ii = 3 * i; q[j++] = b64enc[(p[ii] & 0xfc) >> 2]; q[j++] = b64enc[((p[ii] & 0x03) << 4) | ((p[ii + 1] & 0xf0) >> 4)]; q[j++] = b64enc[((p[ii + 1] & 0x0f) << 2) | ((p[ii + 2] & 0xc0) >> 6)]; q[j++] = b64enc[(p[ii + 2] & 0x3f)]; if (((i + 1) % 18) == 0) q[j++] = '\r'; } ii += 3; i = len - ii; // Bytes remaining to encode switch (i) { case 1: q[j++] = b64enc[(p[ii] & 0xfc) >> 2]; q[j++] = b64enc[(p[ii] & 0x03) << 4]; q[j++] = '='; q[j++] = '='; break; case 2: q[j++] = b64enc[(p[ii] & 0xfc) >> 2]; q[j++] = b64enc[((p[ii] & 0x03) << 4) | ((p[ii + 1] & 0xf0) >> 4)]; q[j++] = b64enc[(p[ii + 1] & 0x0f) << 2]; q[j++] = '='; break; } q[j] = '\0'; return j; } /* * Prompt for a name in the bottom line of the screen * Returns number of chars read. * prompt - Message to display before > prompt * is_file - if 1, restrict chars to those allowed in ProDOS filename * 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; cursor(0); goto_prompt_row(); clreol(); revers(1); cprintf("%s>", prompt); revers(0); gotox(2 + strlen(prompt)); i = 0; while (1) { c = cgetc(); if (is_file && !isalnum(c) && (c != RETURN) && (c != BACKSPACE) && (c != DELETE) && (c != ESC) && (c != '.') && (c != '/')) { beep(); continue; } switch (c) { case RETURN: goto done; case BACKSPACE: case DELETE: if (i > 0) { gotox(wherex() - 1); cputc(' '); gotox(wherex() - 1); --i; } else beep(); break; case ESC: userentry[0] = '\0'; i = 255; goto esc_pressed; default: cputc(c); userentry[i++] = c; } if (i == 79) goto done; } done: userentry[i] = '\0'; esc_pressed: clrline(); cursor(1); return i; } /* * Prompt ok? * Returns 0 for yes * 1 for no * 2 for ESC */ char prompt_okay(char *msg) { char c; cursor(0); goto_prompt_row(); clreol(); revers(1); cprintf("%s (y/n/ESC)", msg); revers(0); while (1) { c = cgetc(); if ((c == 'y') || (c == 'Y') || (c == 'n') || (c == 'N') || c == ESC) break; beep(); } if ((c == 'y') || (c == 'Y')) c = 0; else if (c == ESC) c = 2; else c = 1; return c; } struct tabent { char name[16]; uint8_t type; uint32_t size; } *entry; // It is best if FILESPERPAGE is a multiple of FILELINES #define FILELINES 16 // Number of lines displayed in file_ui #define FILESPERPAGE 160 // Number of files loaded into iobuf[] /* * Draw one line in file chooser UI * i - index of file to draw * first - index of first file on screen * selected - index of currently selected file * entries - total number of file entries in directory */ void file_ui_draw(uint16_t i, uint16_t first, uint16_t selected, uint16_t entries) { struct tabent *entry; if (i < first) return; gotoxy(5, i - first + 6); if (i < entries) { entry = (struct tabent*)iobuf + i; if (entry->type == 0x0f) { snprintf(userentry, 80, "[ %s ] ", entry->name); userentry[34] = '\0'; } else { snprintf(userentry, 80, " %s ", entry->name); snprintf(&userentry[18], 60, " %8lu ", entry->size); switch (entry->type) { case 0x04: sprintf(&userentry[30], "TXT "); break; case 0x06: sprintf(&userentry[30], "BIN "); break; case 0x19: sprintf(&userentry[30], "ADB "); break; case 0x1a: sprintf(&userentry[30], "AWP "); break; case 0x1b: sprintf(&userentry[30], "ASP "); break; case 0xfc: sprintf(&userentry[30], "BAS "); break; case 0xff: sprintf(&userentry[30], "SYS "); break; default: sprintf(&userentry[30], "$%02x ", entry->type); } } if (i == selected) revers(1); cputs(userentry); if (i == selected) revers(0); } else { strcpy(userentry, " "); cputs(userentry); } } /* * File chooser UI * first - index of first file on screen * selected - index of currently selected file * entries - total number of file entries in directory */ void file_ui_draw_all(uint16_t first, uint16_t selected, uint16_t entries) { uint16_t i; uint16_t last = first + FILELINES; for (i = first; i < last; ++i) file_ui_draw(i, first, selected, entries); } /* * Asm code for online() */ #pragma optimize (push, off) void onlineasm(void) { __asm__("lda #$00"); // All devices __asm__("sta mliparam + 1"); __asm__("lda #<%v", iobuf); // iobuf LSB __asm__("sta mliparam + 2"); __asm__("lda #>%v", iobuf); // iobuf MSB __asm__("ina"); // Bump up 256 bytes into iobuf __asm__("sta mliparam + 3"); __asm__("lda #$c5"); // ON_LINE __asm__("ldx #$02"); // Two parms __asm__("jsr callmli"); } #pragma optimize (pop) /* * Perform ProDOS MLI ON_LINE call to * write all online volume names into iobuf[] * Return the number of entries */ uint16_t online(void) { uint16_t entries = 0; struct tabent *entry; uint8_t i, j, len; onlineasm(); entry = (struct tabent*)iobuf; for (i = 0; i < 16; ++i) { len = iobuf[256 + i * 16] & 0x0f; if (len > 0) { entry->type = 0x0f; entry->name[0] = '/'; for (j = 0; j < len; ++j) entry->name[j + 1] = iobuf[256 + i * 16 + j + 1]; entry->name[j + 1] = '\0'; ++entry; ++entries; } } return entries; } /* * Read directory entries and populate table in iobuf[] * for file_ui() * page - page index, each page has FILESPERPAGE entries * page is 0, 1, 2, 3 ... * num_entries - number of entries read * Returns 0 there are more files, 1 if end of directory reached */ uint8_t read_dir(uint8_t page, uint16_t *num_entries) { DIR *dp; struct dirent *ent; struct tabent *entry; uint8_t rc = 0; uint16_t entries = 0, i = 0; entry = (struct tabent*)iobuf; if (page == 0) { strcpy(entry->name, ".."); // Add fake '..' entry entry->type = 0x0f; ++entry; ++entries; } dp = opendir("."); while (1) { ent = readdir(dp); if (!ent) { rc = 1; break; } if (++i < page * FILESPERPAGE) continue; if (entries >= FILESPERPAGE) break; memcpy(entry->name, ent->d_name, 16); entry->type = ent->d_type; entry->size = ent->d_size; ++entry; ++entries; } closedir(dp); *num_entries = entries; return rc; } /* * File chooser UI * Leaves file name in userentry[], or empty string if error/cancel * msg1 - Message for top line * msg2 - Message for second line * msg3 - Message for third line */ void file_ui(char *msg1, char *msg2, char *msg3) { struct tabent *entry; char c; uint16_t entries; uint8_t end_of_dir; uint16_t first = 0, current = 0; uint8_t toplevel = 0, page = 0; restart: clrscr(); cursor(0); gotoxy(0,0); revers(1); cprintf("%s", msg1); revers(0); gotoxy(0,1); cprintf("%s", msg2); gotoxy(0,2); cprintf("%s", msg3); getcwd(userentry, 80); gotoxy(0,4); revers(1); cprintf("%s", (toplevel ? "Volumes" : userentry)); revers(0); if (toplevel) entries = online(); else end_of_dir = read_dir(page, &entries); redraw: file_ui_draw_all(first, current, entries); while (1) { c = cgetc(); switch (c) { case 0x0b: // Up if (current > 0) --current; else if (page > 0) { --page; current = FILESPERPAGE - 1; first = current - FILELINES + 1; goto restart; } if (current < first) { if (first > FILELINES) first -= FILELINES; else first = 0; goto redraw; } file_ui_draw(current, first, current, entries); file_ui_draw(current + 1, first, current, entries); break; case 0x0a: // Down if (current < entries - 1) ++current; else if (!end_of_dir) { ++page; first = current = 0; goto restart; } if (current >= first + FILELINES) { first += FILELINES; goto redraw; } file_ui_draw(current - 1, first, current, entries); file_ui_draw(current, first, current, entries); break; case EOL: entry = (struct tabent*)iobuf + current; switch (entry->type) { case 0x0f: // Directory getcwd(userentry, 80); if (strcmp(entry->name, "..") == 0) { for (c = strlen(userentry); c > 0; --c) if (userentry[c] == '/') { userentry[c] = '\0'; break; } if (c <= 1) toplevel = 1; else chdir(userentry); first = current = 0; goto restart; } else { if (toplevel) { strcpy(userentry, entry->name); chdir(userentry); } else { getcwd(userentry, 80); strcat(userentry, "/"); strcat(userentry, entry->name); chdir(userentry); } toplevel = 0; first = current = 0; goto restart; } break; case 0x04: // ASCII text getcwd(userentry, 80); strcat(userentry, "/"); strcat(userentry, entry->name); goto done; break; default: getcwd(userentry, 80); strcat(userentry, "/"); strcat(userentry, entry->name); goto done; } break; case ESC: strcpy(userentry, ""); goto done; break; case 0x09: // Tab if (prompt_for_name("Enter filename", 0) == 255) goto restart; // ESC pressed if (userentry[0] == '/') // Absolute path goto done; getcwd(linebuf, 80); // Otherwise relative path strcat(linebuf, "/"); strcat(linebuf, userentry); strcpy(userentry, linebuf); strcpy(linebuf, ""); goto done; } } done: clrscr(); cursor(1); } /* * Return pointer to start of the filename portion of a full path. * /FOO/BAR/BAZ -> BAZ */ char *shortfilename(char *fullfilename) { uint8_t i = 0; int8_t lastslash = -1; while (fullfilename[i]) { if (fullfilename[i] == '/') lastslash = i; ++i; } if (lastslash == -1) return fullfilename; return fullfilename + lastslash + 1; } /* * Optionally attach files to outgoing email. * filename - Name of file containing email message */ void attach(char *fname) { FILE *fp, *fp2, *destfp; uint16_t chars, i; uint32_t size; char *s, c; struct attachinfo *a; struct attachinfo *latest = NULL; uint8_t attachcount = 0; videomode(VIDEOMODE_80COL); printf("%c%s ATTACHER%c\n\n", 0x0f, PROGNAME, 0x0e); fp = fopen(fname, "rb+"); if (!fp) error(ERR_FATAL, "Can't open %s", fname); snprintf(filename, 80, "%s/OUTBOX/TMPFILE", cfg_emaildir); _filetype = PRODOS_T_TXT; _auxtype = 0; destfp = fopen(filename, "wb"); if (!destfp) error(ERR_FATAL, "Can't open TMPFILE"); printf(" Copying email content ... "); // Space is for spinner to eat size = 0; while ((chars = get_line(fp, linebuf, LINEBUFSZ)) != 0) { size += chars; if (linebuf[0] == '\r') break; fputs(linebuf, destfp); spinner(size, 0); } fprintf(destfp, "MIME-Version: 1.0\r"); fprintf(destfp, "Content-Type: multipart/mixed; boundary=a2forever\r\r"); fprintf(destfp, "This is a multi-part message in MIME format.\r"); fprintf(destfp, "--a2forever\r"); fprintf(destfp, "Content-Type: text/plain; charset=US-ASCII\r"); fprintf(destfp, "Content-Transfer-Encoding: 7bit\r\r"); while ((chars = get_line(fp, linebuf, LINEBUFSZ)) != 0) { size += chars; fputs(linebuf, destfp); spinner(size, 0); } spinner(size, 1); while (1) { cursor(0); if (attachcount == 1) printf("\n There is currently 1 attachment.\n\n", attachcount); else printf("\n There are currently %u attachments.\n\n", attachcount); a = attachments; i = 1; if (attachcount > 0) printf(" # Bytes Filename\n"); while (a) { if (i == 13) { printf(" < ... More Attachments, Not Shown ... >\n"); break; } printf("%3d %7lu %s\n", i++, a->size, a->filename); a = a->next; } gotoxy(0, 21); printf("%c A)dd attachment | D)one with attachments | %c", INVERSE, NORMAL); ask: c = cgetc(); if ((c == 'D') || (c == 'd')) goto done; if ((c != 'A') && (c != 'a')) { beep(); goto ask; } snprintf(userentry, 80, "Attachment #%u : Select a File to Attach", attachcount + 1); file_ui(userentry, "", " Select file from tree browser, or [Tab] to enter filename. [Esc] cancels."); printf("%c%s ATTACHER%c\n\n", 0x0f, PROGNAME, 0x0e); if (strlen(userentry) == 0) { beep(); printf(" ** No file was selected!\n"); continue; } s = strrchr(userentry, '/'); if (!s) s = userentry; else s = s + 1; // Character after the slash if (strlen(s) == 0) { beep(); sprintf(linebuf, " ** Illegal trailing slash '%s'!\n", userentry); printf(linebuf); continue; } fp2 = fopen(userentry, "rb"); if (!fp2) { beep(); sprintf(linebuf, " ** Can't open '%s'!\n", userentry); printf(linebuf); continue; } fprintf(destfp, "\r--a2forever\r"); fprintf(destfp, "Content-Type: application/octet-stream\r"); fprintf(destfp, "Content-Transfer-Encoding: base64\r"); fprintf(destfp, "Content-Disposition: attachment; filename=%s;\r\r", s); printf(" Attaching '%s' ... ", userentry); // Space is for spinner to eat size = 0; do { i = fread(buf, 1, 72 * 3 / 4 * 5, fp2); // Multiple of 72*3/4 bytes size += i; if (i == 0) break; i = encode_base64(buf, buf + READSZ / 2, i); i = fwrite(buf + READSZ / 2, 1, i, destfp); spinner(size, 0); } while (!feof(fp2)); fclose(fp2); spinner(size, 1); ++attachcount; if (!attachments) attachments = latest = malloc(sizeof(struct attachinfo)); else { latest->next = malloc(sizeof(struct attachinfo)); latest = latest->next; } strcpy(latest->filename, shortfilename(userentry)); latest->size = size; latest->next = NULL; } done: fprintf(destfp, "\r--a2forever--\r"); fclose(fp); fclose(destfp); if (unlink(fname)) error(ERR_FATAL, "Can't delete %s", fname); if (rename(filename, fname)) error(ERR_FATAL, "Can't rename %s to %s", filename, fname); } /* * Exec EMAIL.SYSTEM */ void load_email(void) { revers(0); clrscr(); chdir(cfg_instdir); exec("EMAIL.SYSTEM", NULL); } void main(int argc, char *argv[]) { readconfigfile(); if (argc == 2) { quit_to_email = 1; attach(argv[1]); } else error(ERR_FATAL, "No email file specified"); if (quit_to_email) load_email(); }