emailler/apps/smtp65.c

752 lines
18 KiB
C

/////////////////////////////////////////////////////////////////
// SMTP65
// Simple Mail Transport Protocol (SMTP) Client for IP65
// https://www.ietf.org/rfc/rfc821
// (Based on IP65's wget65.c)
// Bobbi June 2020
/////////////////////////////////////////////////////////////////
#include <cc65.h>
#include <errno.h>
#include <ctype.h>
#include <fcntl.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <apple2_filetype.h>
#include "../inc/ip65.h"
#include "w5100.h"
#include "email_common.h"
#define BELL 7
#define BACKSPACE 8
#define NORMAL 0x0e
#define INVERSE 0x0f
#define CLRLINE 0x1a
// Both pragmas are obligatory to have cc65 generate code
// suitable to access the W5100 auto-increment registers.
#pragma optimize (on)
#pragma static-locals (on)
#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[]
static unsigned char buf[NETBUFSZ+1]; // One extra byte for null terminator
static char linebuf[LINEBUFSZ];
uint8_t exec_email_on_exit = 0;
char filename[80];
int len;
FILE *fp;
uint32_t filesize;
uint16_t smtp_port;
/*
* Keypress before quit
*/
void confirm_exit(void) {
printf("\n[Press Any Key]");
cgetc();
if (exec_email_on_exit) {
sprintf(filename, "%s/EMAIL.SYSTEM", cfg_instdir);
exec(filename, NULL);
}
exit(0);
}
/*
* Called for all non IP65 errors
*/
void error_exit() {
confirm_exit();
}
/*
* Called if IP65 call fails
*/
void ip65_error_exit(void) {
printf("%s\n", ip65_strerror(ip65_error));
confirm_exit();
}
/*
* Print message to the console, stripping extraneous CRLF stuff
* from the end.
*/
void print_strip_crlf(char *s) {
uint8_t i = 0;
while ((s[i] != '\0') && (s[i] != '\r') && (s[i] != '\n'))
putchar(s[i++]);
putchar('\n');
}
/*
* Spinner while uploading files
*/
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 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.
*/
uint16_t get_line(FILE *fp, uint8_t reset, 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;
if (reset) {
rd = end = 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++];
if (writep[i - 1] == '\r')
goto done;
}
done:
writep[i] = '\0';
return i;
}
#define DO_SEND 1 // For do_send param
#define DONT_SEND 0 // For do_send param
#define CMD_MODE 0 // For mode param
#define DATA_MODE 1 // For mode param
// Modified verson of w5100_http_open from w5100_http.c
// Sends a TCP message and receives a the first packet of the response.
// sendbuf is the buffer to send (null terminated)
// recvbuf is the buffer into which the received message will be written
// length is the length of recvbuf[]
// do_send Do the sending if true, otherwise skip
// mode Binary mode for sent message, pump data from disk file fp
bool w5100_tcp_send_recv(char* sendbuf, char* recvbuf, size_t length,
uint8_t do_send, uint8_t mode) {
if (do_send == DO_SEND) {
if (mode == DATA_MODE) {
//
// Handle sending of email body
//
uint16_t pos = 0;
uint8_t cont = 1;
uint16_t snd;
uint16_t len;
filesize = 0;
len = get_line(fp, 1, linebuf, LINEBUFSZ); // Reset buffer
while (cont) {
len = get_line(fp, 0, linebuf, LINEBUFSZ - 1);
pos = 0;
if (len == 0) {
strcpy(linebuf, "\r\n.\r\n");
len = 5;
cont = 0;
} else {
linebuf[len++] = '\n'; // CR -> CRLF
linebuf[len] = '\0';
filesize += len;
}
while (len) {
if (input_check_for_abort_key())
{
printf("User abort\n");
w5100_disconnect();
return false;
}
snd = w5100_send_request();
if (!snd) {
if (!w5100_connected()) {
printf("Connection lost\n");
return false;
}
continue;
}
if (len < snd)
snd = len;
{
// One less to allow for faster pre-increment below
const char *dataptr = linebuf + pos - 1;
uint16_t i;
for (i = 0; i < snd; ++i) {
// The variable is necessary to have cc65 generate code
// suitable to access the W5100 auto-increment register.
char data = *++dataptr;
*w5100_data = data;
}
}
w5100_send_commit(snd);
len -= snd;
pos += snd;
}
spinner(filesize, 0);
}
spinner(filesize, 1);
} else {
//
// Handle short ASCII text transmissions
//
uint16_t snd;
uint16_t pos = 0;
uint16_t len = strlen(sendbuf);
putchar('>');
print_strip_crlf(sendbuf);
while (len) {
if (input_check_for_abort_key())
{
printf("User abort\n");
w5100_disconnect();
return false;
}
snd = w5100_send_request();
if (!snd) {
if (!w5100_connected()) {
printf("Connection lost\n");
return false;
}
continue;
}
if (len < snd)
snd = len;
{
// One less to allow for faster pre-increment below
const char *dataptr = sendbuf + pos - 1;
uint16_t i;
for (i = 0; i < snd; ++i) {
// The variable is necessary to have cc65 generate code
// suitable to access the W5100 auto-increment register.
char data = *++dataptr;
*w5100_data = data;
}
}
w5100_send_commit(snd);
len -= snd;
pos += snd;
}
}
}
{
//
// Handle short single packet ASCII text responses
// Must fit in recvbuf[]
//
uint16_t rcv;
uint16_t len = 0;
uint8_t cont = 1;
--length; // Leave space for NULL at end in case of buffer overrun
while (cont) {
if (input_check_for_abort_key()) {
printf("User abort\n");
w5100_disconnect();
return false;
}
rcv = w5100_receive_request();
if (!rcv) {
cont = w5100_connected();
if (cont)
continue;
}
if ((length - len) == 0)
cont = 0;
if (rcv > length - len)
rcv = length - len;
{
// One less to allow for faster pre-increment below
char *dataptr = recvbuf + len - 1;
uint16_t i;
for (i = 0; i < rcv; ++i) {
// The variable is necessary to have cc65 generate code
// suitable to access the W5100 auto-increment register.
char data = *w5100_data;
*++dataptr = data;
if (!memcmp(dataptr - 1, "\r\n", 2))
cont = 0;
}
}
w5100_receive_commit(rcv);
len += rcv;
}
recvbuf[len + 1] = '\0';
putchar('<');
print_strip_crlf(recvbuf);
}
return true;
}
/*
* Check expected string from server
* Returns 0 if expected, 1 otherwise
*/
uint8_t expect(char *buf, char *s) {
if (strncmp(buf, s, strlen(s)) != 0) {
printf("\nExpected '%s' got '%s'\n", s, buf);
return 1;
}
return 0;
}
/*
* Read parms from EMAIL.CFG
*/
void readconfigfile(void) {
char *colon;
fp = fopen("EMAIL.CFG", "r");
if (!fp) {
puts("Can't open config file EMAIL.CFG");
error_exit();
}
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);
colon = strchr(cfg_smtp_server, ':');
if (!colon)
smtp_port = 25;
else {
smtp_port = atoi(colon + 1);
*colon = '\0';
}
}
/*
* Update EMAIL.DB - quick access database for header info
*/
void update_email_db(struct emailhdrs *h) {
FILE *fp;
sprintf(filename, "%s/SENT/EMAIL.DB", cfg_emaildir);
_filetype = PRODOS_T_BIN;
_auxtype = 0;
fp = fopen(filename, "ab");
if (!fp) {
printf("Can't open %s\n", filename);
error_exit();
}
fwrite(h, sizeof(struct emailhdrs), 1, fp);
fclose(fp);
}
/*
* Copy one header from source->dest, removing '\r' from end
*/
void copyheader(char *dest, char *source, uint16_t len) {
uint16_t i;
memset(dest, ' ', len);
for (i = 0; i < len; ++i) {
if ((*source == '\0') || (*source == '\r'))
return;
*dest++ = *source++;
}
}
/*
* Write NEXT.EMAIL file with number of next EMAIL.n file to be created
*/
void write_next_email(uint16_t num) {
sprintf(filename, "%s/SENT/NEXT.EMAIL", cfg_emaildir);
_filetype = PRODOS_T_TXT;
_auxtype = 0;
fp = fopen(filename, "wb");
if (!fp) {
printf("Can't open %s\n", filename);
fclose(fp);
error_exit();
}
fprintf(fp, "%u", num);
fclose(fp);
}
/*
* Update SENT when a message has been sent
* Copy a messages from OUTBOX to SENT and find headers of interest
* (Date, From, To, BCC, Subject)
* filename - Filename in OUTBOX for message just sent
*/
void update_sent_mbox(char *name) {
static struct emailhdrs hdrs;
uint16_t nextemail, chars, headerchars;
uint8_t headers;
FILE *destfp;
sprintf(filename, "%s/SENT/NEXT.EMAIL", cfg_emaildir);
fp = fopen(filename, "r");
if (!fp) {
nextemail = 1;
write_next_email(nextemail);
} else {
fscanf(fp, "%u", &nextemail);
fclose(fp);
}
strcpy(linebuf, "");
sprintf(filename, "%s/OUTBOX/%s", cfg_emaildir, name);
fp = fopen(filename, "r");
if (!fp) {
printf("Can't open %s\n", filename);
error_exit();
}
hdrs.emailnum = nextemail;
sprintf(filename, "%s/SENT/EMAIL.%u", cfg_emaildir, nextemail++);
puts(filename);
_filetype = PRODOS_T_TXT;
_auxtype = 0;
destfp = fopen(filename, "wb");
if (!destfp) {
printf("Can't open %s\n", filename);
fclose(fp);
error_exit();
}
headers = 1;
headerchars = 0;
hdrs.skipbytes = 0; // Just in case it doesn't get set
hdrs.status = 'N';
hdrs.tag = ' ';
get_line(fp, 1, linebuf, LINEBUFSZ); // Reset buffer
while ((chars = get_line(fp, 0, linebuf, LINEBUFSZ)) != 0) {
if (headers) {
headerchars += chars;
if (!strncmp(linebuf, "Date: ", 6)) {
copyheader(hdrs.date, linebuf + 6, 39);
hdrs.date[39] = '\0';
}
if (!strncmp(linebuf, "From: ", 6)) {
copyheader(hdrs.from, linebuf + 6, 79);
hdrs.from[79] = '\0';
}
if (!strncmp(linebuf, "To: ", 4)) {
copyheader(hdrs.to, linebuf + 4, 79);
hdrs.to[79] = '\0';
}
if (!strncmp(linebuf, "Cc: ", 4)) {
copyheader(hdrs.cc, linebuf + 4, 79);
hdrs.cc[79] = '\0';
}
if (!strncmp(linebuf, "Subject: ", 9)) {
copyheader(hdrs.subject, linebuf + 9, 79);
hdrs.subject[79] = '\0';
}
if (linebuf[0] == '\r') {
headers = 0;
hdrs.skipbytes = headerchars;
}
}
fputs(linebuf, destfp);
}
fclose(fp);
fclose(destfp);
update_email_db(&hdrs);
write_next_email(nextemail);
}
void main(int argc, char *argv[]) {
static char sendbuf[80], recipients[160];
uint8_t linecount;
DIR *dp;
struct dirent *d;
char *p, *q, c;
uint8_t eth_init = ETH_INIT_DEFAULT, connected = 0;
if ((argc == 2) && (strcmp(argv[1], "EMAIL") == 0))
exec_email_on_exit = 1;
videomode(VIDEOMODE_80COL);
printf("%c%s SMTP%c\n", 0x0f, PROGNAME, 0x0e);
printf("\nReading EMAIL.CFG -");
readconfigfile();
printf(" Ok");
{
int file;
printf("\nSetting slot - ");
file = open("ethernet.slot", O_RDONLY);
if (file != -1) {
read(file, &eth_init, 1);
close(file);
eth_init &= ~'0';
}
}
printf("%d\nInitializing %s - ", eth_init, eth_name);
if (ip65_init(eth_init)) {
ip65_error_exit();
}
// Abort on Ctrl-C to be consistent with Linenoise
abort_key = 0x83;
printf("Ok\nObtaining IP address - ");
if (dhcp_init()) {
ip65_error_exit();
}
printf("Ok\n");
// Copy IP config from IP65 to W5100
w5100_init(eth_init);
w5100_config();
sprintf(filename, "%s/OUTBOX", cfg_emaildir);
dp = opendir(filename);
if (!dp) {
printf("Can't open dir %s\n", filename);
error_exit();
}
while (d = readdir(dp)) {
sprintf(filename, "%s/OUTBOX/%s", cfg_emaildir, d->d_name);
fp = fopen(filename, "rb");
if (!fp) {
printf("Can't open %s\n", d->d_name);
continue;
}
// Skip special files
if (!strncmp(d->d_name, "EMAIL.DB", 8))
goto skiptonext;
if (!strncmp(d->d_name, "NEXT.EMAIL", 10))
goto skiptonext;
printf("\n** Processing file %s ...\n", d->d_name);
linecount = 0;
strcpy(recipients, "");
while (1) {
if ((get_line(fp, 0, linebuf, LINEBUFSZ) == 0) || (linecount == 20) || (linebuf[0] == '\r')) {
if (strlen(recipients) == 0) {
printf("No recipients (To or Cc) in %s. Skipping msg.\n", d->d_name);
goto skiptonext;
}
break;
}
++linecount;
if (!strncmp(linebuf, "To: ", 4) || (!strncmp(linebuf, "cc: ",4))) {
printf("%s", linebuf);
linebuf[strlen(linebuf) - 1] = '\0'; // Chop off \r
if (strlen(linebuf + 4) > 0) {
if (strlen(recipients) > 0)
strcat(recipients, ",");
strcat(recipients, linebuf + 4);
}
}
if (!strncmp(linebuf, "Subject: ", 9))
printf("%s", linebuf);
}
printf("\n%cS)end message | H)old message in OUTBOX | D)elete message from OUTBOX %c",
INVERSE, NORMAL);
while (1) {
c = cgetc();
switch (c) {
case 'S':
case 's':
goto sendmessage;
case 'H':
case 'h':
printf("\n Holding message\n");
fclose(fp);
goto skiptonext;
case 'D':
case 'd':
printf("\nSure? (y/n)");
while (1) {
c = cgetc();
switch (c) {
case 'Y':
case 'y':
putchar(CLRLINE);
printf("\n Deleting message\n");
fclose(fp);
goto unlink;
case 'N':
case 'n':
putchar(CLRLINE);
printf("\n Holding message\n");
fclose(fp);
goto skiptonext;
default:
putchar(BELL);
}
}
break;
default:
putchar(BELL);
}
}
sendmessage:
if (!connected) {
printf("\nConnecting to %s - ", cfg_smtp_server);
if (!w5100_connect_addr(parse_dotted_quad(cfg_smtp_server), smtp_port)) {
printf("Fail\n");
error_exit();
}
printf("Ok\n\n");
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DONT_SEND, CMD_MODE)) {
error_exit();
}
if (expect(buf, "220 "))
error_exit();
sprintf(sendbuf, "HELO %s\r\n", cfg_smtp_domain);
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
error_exit();
}
if (expect(buf, "250 "))
error_exit();
connected = 1;
}
sprintf(sendbuf, "MAIL FROM:<%s>\r\n", cfg_emailaddr);
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
error_exit();
}
if (expect(buf, "250 ")) {
printf("Skipping msg\n");
goto skiptonext;
}
// Handle multiple comma-separated recipients
p = recipients;
while (q = strchr(p, ',')) {
*q = '\0';
while (*p == ' ')
++p;
sprintf(sendbuf, "RCPT TO:<%s>\r\n", p);
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
error_exit();
}
if (expect(buf, "250 ")) {
printf("Skipping msg\n");
goto skiptonext;
}
p = q + 1;
}
while (*p == ' ')
++p;
sprintf(sendbuf, "RCPT TO:<%s>\r\n", p);
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
error_exit();
}
if (expect(buf, "250 ")) {
printf("Skipping msg\n");
goto skiptonext;
}
sprintf(sendbuf, "DATA\r\n");
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
error_exit();
}
if (expect(buf, "354 ")) {
printf("Skipping msg\n");
goto skiptonext;
}
fseek(fp, 0, SEEK_SET);
if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, DATA_MODE)) {
error_exit();
}
expect(buf, "250 ");
fclose(fp);
printf("Updating SENT mailbox ...\n");
update_sent_mbox(d->d_name);
printf("Removing from OUTBOX ...\n");
sprintf(filename, "%s/OUTBOX/%s", cfg_emaildir, d->d_name);
unlink:
if (unlink(filename))
printf("Can't remove %s\n", filename);
skiptonext:
if (fp)
fclose(fp);
}
closedir(dp);
// Ignore any error - can be a race condition where other side
// disconnects too fast and we get an error
if (connected) {
w5100_tcp_send_recv("QUIT\r\n", buf, NETBUFSZ, DO_SEND, CMD_MODE);
printf("Disconnecting\n");
w5100_disconnect();
} else
printf("\n** No messages were sent **\n");
confirm_exit();
}