#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VERSION "0.4" #define STDIN 0 #define STDOUT 1 #define STDERR 2 #define READ 0 #define WRITE 1 #define TCP "tcp" #define POP3 "pop3" #define POP3OK "+OK" #define BUFFER_SIZE 4096 #define TIMEOUT 60 #define MAXMSGSIZE 20 * 1024 * 1024 #define NULLSTRING { .s = NULL, .l = 0, ._l = 0 } const char *wdays[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; const char *months[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; typedef struct String { char *s; unsigned long l, _l; } String; void nomem () { write(STDERR, "no memory!\n", 11); exit(111); } char *ultoa (const unsigned long l) { static char buf[BUFFER_SIZE], *c; static unsigned long tmp; tmp = l; c = &buf[0]; do { tmp /= 10; ++c; } while (tmp); *(c--) = 0; tmp = l; do { *c = '0' + tmp % 10; tmp /= 10; --c; } while (tmp); return &buf[0]; } int a2l (char *left, char *right, unsigned long *ret) { static unsigned long expo; for (expo = 1, *ret = 0, --right; right >= left; --right, expo *= 10) { if ((*right < '0') || (*right > '9')) return -1; *ret += expo * (*right - '0'); } return 0; } #if 0 char *remove_r (char *buf, int buflen) { char *c; while ((c = strchr(buf, '\r'))) memmove(c, c + 1, buflen - 1 - (c - buf)); return buf; } #endif void str_print (int fd, String *s) { write(fd, s->s, s->l); } String *str_cat (String *s, const char *c) { size_t clen; unsigned long newlen; clen = strlen(c); newlen = ((s->l + clen + BUFFER_SIZE) / BUFFER_SIZE) * BUFFER_SIZE; if (newlen != s->_l) { s->s = realloc(s->s, newlen); if (!s->s) nomem(); s->_l = newlen; } memmove(s->s + s->l, c, clen + 1); s->l += clen; return s; } String *str_cat_ul2d (String *s, long l) { if (l < 10) str_cat(s, "0"); str_cat(s, ultoa(l)); return s; } String *str_cat_date (String *s) { time_t secepoch; struct tm *now; secepoch = time(NULL); now = localtime(&secepoch); str_cat(s, "["); str_cat(s, ultoa(now->tm_year + 1900)); str_cat(s, "-"); str_cat_ul2d(s, now->tm_mon + 1); str_cat(s, "-"); str_cat_ul2d(s, now->tm_mday); str_cat(s, " "); str_cat_ul2d(s, now->tm_hour); str_cat(s, ":"); str_cat_ul2d(s, now->tm_min); str_cat(s, ":"); str_cat_ul2d(s, now->tm_sec); str_cat(s, "] "); return s; } String *str_cat_date_rfc822 (String *s) { time_t secepoch; struct tm *now; long leap; secepoch = time(NULL); now = localtime(&secepoch); leap = now->tm_gmtoff; str_cat(s, wdays[now->tm_wday]); str_cat(s, ", "); str_cat_ul2d(s, now->tm_mday); str_cat(s, " "); str_cat(s, months[now->tm_mon]); str_cat(s, " "); str_cat(s, ultoa(now->tm_year + 1900)); str_cat(s, " "); str_cat_ul2d(s, now->tm_hour); str_cat(s, ":"); str_cat_ul2d(s, now->tm_min); str_cat(s, ":"); str_cat_ul2d(s, now->tm_sec); if (leap < 0) { str_cat(s, " -"); leap *= -1; } else str_cat(s, " +"); str_cat_ul2d(s, leap / 3600); leap %= 3600; str_cat_ul2d(s, leap / 60); str_cat(s, " ("); str_cat(s, now->tm_zone); str_cat(s, ")"); return s; } void str_free (String *s) { if (s->s) { free(s->s); s->s = NULL; s->l = 0; s->_l = 0; } } void str_flush (String *s) { s->l = 0; } void logger (char *s1, char *s2, long l1, long l2, char *s3) { static String s = NULLSTRING; str_flush(&s); str_cat_date(&s); str_cat(&s, s1); if (s2) { str_cat(&s, ": "); str_cat(&s, s2); str_cat(&s, ultoa(l1)); } str_cat(&s, ": "); str_cat(&s, ultoa(l2)); str_cat(&s, s3); if (*(s.s + s.l - 1) != '\n') str_cat(&s, "\n"); str_print(STDOUT, &s); } int warn (char *s1, char *s2, const char *s3) { static String s = NULLSTRING; str_flush(&s); str_cat_date(&s); str_cat(&s, s1); if (s2) { str_cat(&s, ": "); str_cat(&s, s2); } if (s3) { str_cat(&s, ": "); str_cat(&s, s3); } if (*(s.s + s.l - 1) != '\n') str_cat(&s, "\n"); str_print(STDERR, &s); return -1; } void die (char *s1, char *s2) { warn(s1, s2, NULL); exit(1); } int w2s (char *host, int sock, char *cmd, char *arg, char *buf, int buflen, char *ok) { static String s = NULLSTRING; struct pollfd fd; ssize_t len; int ret; if (cmd) { str_flush(&s); str_cat(&s, cmd); if (arg) { str_cat(&s, " "); str_cat(&s, arg); } str_cat(&s, "\r\n"); str_print(sock, &s); } fd.fd = sock; fd.events = POLLIN | POLLPRI; ret = poll(&fd, 1, TIMEOUT * 1000); if (ret == -1) return warn(host, "poll()", strerror(errno)); if (!ret) return warn(host, "timeout", NULL); if (fd.revents & POLLERR) return warn(host, "socket error", NULL); if (fd.revents & POLLNVAL) return warn(host, "fd closed", NULL); len = read(sock, buf, buflen - 1); if (len < 0) return warn(host, "read()", strerror(errno)); if (len < buflen) buf[len] = '\0'; else buf[buflen - 1] = '\0'; if (ok) { if (!strstr(buf, "\r\n")) return warn(host, "invalid return", buf); if (strncmp(buf, ok, strlen(ok))) return warn(host, buf, NULL); } if (fd.revents & POLLHUP) return warn(host, "hangup", NULL); return 0; } int has_mda_died (char *mda, int fd, pid_t mda_pid) { pid_t pid; close(fd); for (;;) { pid = wait(&fd); if (pid == -1) return warn(mda, "wait()", strerror(errno)); if (pid == mda_pid) break; warn(mda, "wait()", "not our child"); } if (!WIFEXITED(fd)) return warn(mda, "died", NULL); if (WEXITSTATUS(fd)) return warn(mda, "could not be started", NULL); return 0; } pid_t start_mda (char *host, char **mda, int *ret) { pid_t pid; int fd[2]; if (pipe(fd)) return warn(host, "pipe()", strerror(errno)); pid = fork(); if (pid == -1) return warn(host, "fork()", strerror(errno)); if (!pid) { if (dup2(fd[READ], STDIN)) die("dup2()", strerror(errno)); close(fd[READ]); close(fd[WRITE]); execve(*mda, mda, NULL); die(*mda, strerror(errno)); } close(fd[READ]); *ret = fd[WRITE]; return pid; } void print_received_line (int fd, char *buf, size_t *pos, size_t *len, char *host, char *method, char *username, int *printed_received_line) { static String s = NULLSTRING, s2 = NULLSTRING, s3 = NULLSTRING; char *c; if (*printed_received_line) return; c = strstr(buf + *pos, "\r\n"); if (!c) return; write(fd, buf + *pos, (c - buf) - *pos); *len -= (c - buf) - *pos; *pos = c - buf; if (!s2.l) { char hname[BUFFER_SIZE]; struct hostent *he = NULL; str_cat(&s2, "\r\n\tby "); if (gethostname(hname, BUFFER_SIZE)) warn(host, "gethostname()", strerror(errno)); else if (!(he = gethostbyname(hname))) warn(host, "gethostbyname()", strerror(errno)); if (he) str_cat(&s2, he->h_name); else str_cat(&s2, "localhost"); str_cat(&s2, " (mailfetch V" VERSION ") with "); str_cat(&s3, "\r\n\tfor <"); str_cat(&s3, username); str_cat(&s3, ">; "); str_cat_date_rfc822(&s3); } str_flush(&s); str_cat(&s, "\r\nReceived: from "); str_cat(&s, host); str_cat(&s, s2.s); str_cat(&s, method); str_cat(&s, s3.s); write(fd, s.s, s.l); *printed_received_line = 1; } int pop3fetch (char *host, int sock, char *user, char *pass, char **mda, char *username) { size_t pos, len; unsigned long msgcnt, msgsize, read_so_far; ssize_t ret; int fd, printed_received_line; char buf[BUFFER_SIZE], *left, *right = NULL; pid_t mda_pid; if (w2s(host, sock, NULL, NULL, buf, BUFFER_SIZE, POP3OK)) return -1; if (w2s(host, sock, "USER", user, buf, BUFFER_SIZE, POP3OK)) return -1; if (w2s(host, sock, "PASS", pass, buf, BUFFER_SIZE, POP3OK)) return -1; if (w2s(host, sock, "STAT", NULL, buf, BUFFER_SIZE, POP3OK)) return -1; left = strchr(buf, ' '); if (left) right = strchr(++left, ' '); if (!left || !right) return warn(host, "missing space in reply to STAT", buf); if (a2l(left, right, &msgcnt)) return warn(host, "invalid message count", buf); logger(host, NULL, 0, msgcnt, (msgcnt == 1)?(" message"):(" messages")); for (; msgcnt > 0; --msgcnt) { if (w2s(host, sock, "LIST", ultoa(msgcnt), buf, BUFFER_SIZE, POP3OK)) return -1; left = strchr(buf, ' '); if (left) left = strchr(++left, ' '); if (left) right = strchr(++left, '\r'); else right = NULL; if (!left || !right) return warn(host, "missing space in reply to LIST", buf); if (a2l(left, right, &msgsize)) return warn(host, "invalid message size", buf); logger(host, "messsage ", msgcnt, msgsize, " bytes"); if ((mda_pid = start_mda(host, mda, &fd)) == -1) return -1; if (w2s(host, sock, "RETR", ultoa(msgcnt), buf, BUFFER_SIZE, POP3OK)) return -1; if (!(left = strstr(buf, "\r\n"))) return warn(host, "no newline in reply to RETR", buf); left += 2; memmove(buf, left, BUFFER_SIZE - (left - buf)); pos = 0; read_so_far = 0; printed_received_line = 0; for (;;) { len = strlen(&buf[pos]); read_so_far += len; print_received_line(fd, buf, &pos, &len, host, "POP3", username, &printed_received_line); right = strstr(&buf[pos], "\r\n.\r\n"); if (right) { if (read_so_far > msgsize) len = right - &buf[pos] + 2; else { right = NULL; warn(host, "unescaped single dot before end of message", ultoa(msgcnt)); } } ret = write(fd, &buf[pos], len); if (ret < 0) return warn(host, *mda, strerror(errno)); if ((unsigned) ret != len) return warn(host, *mda, "could not write all data"); if (right) break; pos += len; if (pos > 4) { memmove(buf, &buf[pos - 4], 4); pos = 4; } if (w2s(host, sock, NULL, NULL, &buf[pos], BUFFER_SIZE - pos, NULL)) return -1; } if (has_mda_died(*mda, fd, mda_pid)) return -1; if (w2s(host, sock, "DELE", ultoa(msgcnt), &buf[0], BUFFER_SIZE, POP3OK)) return -1; } if (w2s(host, sock, "QUIT", NULL, &buf[0], BUFFER_SIZE, POP3OK)) return -1; return 0; } int config_line (char *line, char **proto, char **user, char **pass, char **host, char **port) { /* pop3://user:pass@pop.provider.com[:110] */ if (*line == '#') return 1; *proto = line; *user = strstr(*proto, "://"); if (!*user) return -1; **user = '\0'; *user += 3; *pass = strchr(*user, ':'); if (!*pass) return -1; **pass = '\0'; ++*pass; *host = strchr(*pass, '@'); if (!*host) return -1; **host = '\0'; ++*host; *port = strchr(*host, ':'); if (*port) { **port = '\0'; ++*port; } else *port = NULL; return 0; } char *next_line (char *line) { while ((*line != '\r') && (*line != '\n') && (*line)) ++line; if (*line) { *line++ = '\0'; while ((*line == '\r') || (*line == '\n')) ++line; } return line; } int socket_connect (char *host, char *cfgfile, char *port, char *proto, struct protoent *pe, int *sock) { struct servent *se; struct hostent *he; struct sockaddr_in sin; he = gethostbyname(host); if (!he) return warn(cfgfile, host, hstrerror(h_errno)); memcpy((char*) &sin.sin_addr, he->h_addr, he->h_length); sin.sin_family = PF_INET; if (!port) { se = getservbyname(proto, pe->p_name); if (!se) return warn(cfgfile, "unknown service", proto); sin.sin_port = se->s_port; } else { unsigned long l; if (a2l(port, port + strlen(port), &l) == -1) return warn(cfgfile, "invalid port", port); sin.sin_port = htons((in_port_t) l); } *sock = socket(AF_INET, SOCK_STREAM, 0); if (*sock < 0) return warn(host, "socket()", strerror(errno)); if (!connect(*sock, (struct sockaddr*) &sin, sizeof(sin))) return 0; warn(cfgfile, host, strerror(errno)); close(*sock); return -1; } int (*get_proto_func(char *proto, char *cfgfile))(char *, int, char *, char *, char **, char *) { if (!strcmp(POP3, proto)) return pop3fetch; warn(cfgfile, "unknown protocol", proto); return NULL; } void usage () { die("mailfetch V" VERSION ": usage", "mailfetch [-c ] [ ...]"); } int main (int argc, char **argv) { String cfgfile = NULLSTRING; struct passwd *pw; struct protoent *pe; struct stat st; struct sigaction sa; ssize_t ret; int cfgfh, sock; char *cfg, *proto, *user, *pass, *host, *port, *row, *next_row, *username, **mda; int (*proto_func)(char *, int, char *, char *, char **, char *); argc = argc; close(STDIN); for (mda = argv + 1; *mda; ++mda) { if (!memcmp(*mda, "-c", 2)) { ++mda; if (!*mda) usage(); str_cat(&cfgfile, *mda); continue; } break; } if (!*mda) usage(); pw = getpwuid(getuid()); if (pw) username = pw->pw_name; else username = "unknown"; if (!cfgfile.l) { if (!pw) die("getpwuid()", "you do not exist (and you did not specify a config file)"); str_cat(&cfgfile, pw->pw_dir); str_cat(&cfgfile, "/.mailfetch"); } pe = getprotobyname(TCP); if (!pe) die("unknown protocol", TCP); cfgfh = open(cfgfile.s, O_RDONLY); if (cfgfh < 0) die(cfgfile.s, strerror(errno)); if (fstat(cfgfh, &st)) die("fstat()", strerror(errno)); /* p://u:p@h */ if (st.st_size < 9) die(cfgfile.s, "too small"); cfg = malloc(st.st_size + 1); if (!cfg) nomem(); ret = read(cfgfh, cfg, st.st_size); if (ret == -1) die(cfgfile.s, strerror(errno)); if (ret != (ssize_t) st.st_size) die(cfgfile.s, "could not read the whole file"); close(cfgfh); *(cfg + st.st_size) = '\0'; sa.sa_handler = SIG_IGN; if (sigaction(SIGPIPE, &sa, NULL)) die("sigaction()", strerror(errno)); for (row = cfg; *row; row = next_row) { next_row = next_line(row); switch (config_line(row, &proto, &user, &pass, &host, &port)) { case -1: warn(cfgfile.s, "invalid config line", row); break; case 0: proto_func = get_proto_func(proto, cfgfile.s); if (!proto_func) break; if (socket_connect(host, cfgfile.s, port, proto, pe, &sock)) break; proto_func(host, sock, user, pass, mda, username); close(sock); break; default: break; } } str_free(&cfgfile); free(cfg); return 0; }