From 239d06bd4710e8463c6cc7e5411965066a6d134e Mon Sep 17 00:00:00 2001 From: Denis Vlasenko Date: Thu, 6 Nov 2008 23:42:42 +0000 Subject: [PATCH] add mailutils/* --- mailutils/Config.in | 64 +++++++ mailutils/Kbuild | 11 ++ mailutils/mail.c | 242 +++++++++++++++++++++++++ mailutils/mail.h | 35 ++++ mailutils/mime.c | 354 +++++++++++++++++++++++++++++++++++++ mailutils/popmaildir.c | 237 +++++++++++++++++++++++++ mailutils/sendmail.c | 388 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1331 insertions(+) create mode 100644 mailutils/Config.in create mode 100644 mailutils/Kbuild create mode 100644 mailutils/mail.c create mode 100644 mailutils/mail.h create mode 100644 mailutils/mime.c create mode 100644 mailutils/popmaildir.c create mode 100644 mailutils/sendmail.c diff --git a/mailutils/Config.in b/mailutils/Config.in new file mode 100644 index 000000000..b8d697737 --- /dev/null +++ b/mailutils/Config.in @@ -0,0 +1,64 @@ +menu "Mail Utilities" + +config MAKEMIME + bool "makemime" + default n + help + Create MIME-formatted messages. + +config FEATURE_MIME_CHARSET + string "Default charset" + default "us-ascii" + depends on MAKEMIME || REFORMIME || SENDMAIL + help + Default charset of the message. + +config POPMAILDIR + bool "popmaildir" + default n + help + Simple yet powerful POP3 mail popper. Delivers content of remote mailboxes to local Maildir. + +config FEATURE_POPMAILDIR_DELIVERY + bool "Allow message filters and custom delivery program" + default n + depends on POPMAILDIR + help + Allow to use a custom program to filter the content of the message before actual delivery (-F "prog [args...]"). + Allow to use a custom program for message actual delivery (-M "prog [args...]"). + +config REFORMIME + bool "reformime" + default n + help + Parse MIME-formatted messages. + +config FEATURE_REFORMIME_COMPAT + bool "Accept and ignore options other than -x and -X" + default y + depends on REFORMIME + help + Accept (for compatibility only) and ignore options other than -x and -X. + +config SENDMAIL + bool "sendmail" + default n + help + Barebones sendmail. + +config FEATURE_SENDMAIL_MAILX + bool "Allow to specify subject, attachments, their charset and connection helper" + default y + depends on SENDMAIL + help + Allow to specify subject, attachments and their charset. + Allow to use custom connection helper. + +config FEATURE_SENDMAIL_MAILXX + bool "Allow to specify Cc: addresses and some additional headers" + default n + depends on FEATURE_SENDMAIL_MAILX + help + Allow to specify Cc: addresses and some additional headers: Errors-To:. + +endmenu diff --git a/mailutils/Kbuild b/mailutils/Kbuild new file mode 100644 index 000000000..871e87981 --- /dev/null +++ b/mailutils/Kbuild @@ -0,0 +1,11 @@ +# Makefile for busybox +# +# Copyright (C) 1999-2005 by Erik Andersen +# +# Licensed under the GPL v2, see the file LICENSE in this tarball. + +lib-y:= +lib-$(CONFIG_MAKEMIME) += mime.o mail.o +lib-$(CONFIG_POPMAILDIR) += popmaildir.o mail.o +lib-$(CONFIG_REFORMIME) += mime.o mail.o +lib-$(CONFIG_SENDMAIL) += sendmail.o mail.o diff --git a/mailutils/mail.c b/mailutils/mail.c new file mode 100644 index 000000000..ab1304a7f --- /dev/null +++ b/mailutils/mail.c @@ -0,0 +1,242 @@ +/* vi: set sw=4 ts=4: */ +/* + * helper routines + * + * Copyright (C) 2008 by Vladimir Dronnikov + * + * Licensed under GPLv2, see file LICENSE in this tarball for details. + */ +#include "libbb.h" +#include "mail.h" + +static void kill_helper(void) +{ + // TODO!!!: is there more elegant way to terminate child on program failure? + if (G.helper_pid > 0) + kill(G.helper_pid, SIGTERM); +} + +// generic signal handler +static void signal_handler(int signo) +{ +#define err signo + if (SIGALRM == signo) { + kill_helper(); + bb_error_msg_and_die("timed out"); + } + + // SIGCHLD. reap zombies + if (safe_waitpid(G.helper_pid, &err, WNOHANG) > 0) + if (WIFEXITED(err)) { + G.helper_pid = 0; + if (WEXITSTATUS(err)) + bb_error_msg_and_die("child exited (%d)", WEXITSTATUS(err)); + } +#undef err +} + +void FAST_FUNC launch_helper(const char **argv) +{ + // setup vanilla unidirectional pipes interchange + int idx; + int pipes[4]; + + xpipe(pipes); + xpipe(pipes+2); + G.helper_pid = vfork(); + if (G.helper_pid < 0) + bb_perror_msg_and_die("vfork"); + idx = (!G.helper_pid) * 2; + xdup2(pipes[idx], STDIN_FILENO); + xdup2(pipes[3-idx], STDOUT_FILENO); + if (ENABLE_FEATURE_CLEAN_UP) + for (int i = 4; --i >= 0; ) + if (pipes[i] > STDOUT_FILENO) + close(pipes[i]); + if (!G.helper_pid) { + // child: try to execute connection helper + BB_EXECVP(*argv, (char **)argv); + _exit(127); + } + // parent: check whether child is alive + bb_signals(0 + + (1 << SIGCHLD) + + (1 << SIGALRM) + , signal_handler); + signal_handler(SIGCHLD); + // child seems OK -> parent goes on + atexit(kill_helper); +} + +const FAST_FUNC char *command(const char *fmt, const char *param) +{ + const char *msg = fmt; + if (timeout) + alarm(timeout); + if (msg) { + msg = xasprintf(fmt, param); + printf("%s\r\n", msg); + } + fflush(stdout); + return msg; +} + +// NB: parse_url can modify url[] (despite const), but only if '@' is there +/* +static char FAST_FUNC *parse_url(char *url, char **user, char **pass) +{ + // parse [user[:pass]@]host + // return host + char *s = strchr(url, '@'); + *user = *pass = NULL; + if (s) { + *s++ = '\0'; + *user = url; + url = s; + s = strchr(*user, ':'); + if (s) { + *s++ = '\0'; + *pass = s; + } + } + return url; +} +*/ + +void FAST_FUNC encode_base64(char *fname, const char *text, const char *eol) +{ + enum { + SRC_BUF_SIZE = 45, /* This *MUST* be a multiple of 3 */ + DST_BUF_SIZE = 4 * ((SRC_BUF_SIZE + 2) / 3), + }; + +#define src_buf text + FILE *fp = fp; + ssize_t len = len; + char dst_buf[DST_BUF_SIZE + 1]; + + if (fname) { + fp = (NOT_LONE_DASH(fname)) ? xfopen_for_read(fname) : (FILE *)text; + src_buf = bb_common_bufsiz1; + // N.B. strlen(NULL) segfaults! + } else if (text) { + // though we do not call uuencode(NULL, NULL) explicitly + // still we do not want to break things suddenly + len = strlen(text); + } else + return; + + while (1) { + size_t size; + if (fname) { + size = fread((char *)src_buf, 1, SRC_BUF_SIZE, fp); + if ((ssize_t)size < 0) + bb_perror_msg_and_die(bb_msg_read_error); + } else { + size = len; + if (len > SRC_BUF_SIZE) + size = SRC_BUF_SIZE; + } + if (!size) + break; + // encode the buffer we just read in + bb_uuencode(dst_buf, src_buf, size, bb_uuenc_tbl_base64); + if (fname) { + printf("%s\n", eol); + } else { + src_buf += size; + len -= size; + } + fwrite(dst_buf, 1, 4 * ((size + 2) / 3), stdout); + } + if (fname && NOT_LONE_DASH(fname)) + fclose(fp); +#undef src_buf +} + +void FAST_FUNC decode_base64(FILE *src_stream, FILE *dst_stream) +{ + int term_count = 1; + + while (1) { + char translated[4]; + int count = 0; + + while (count < 4) { + char *table_ptr; + int ch; + + /* Get next _valid_ character. + * global vector bb_uuenc_tbl_base64[] contains this string: + * "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n" + */ + do { + ch = fgetc(src_stream); + if (ch == EOF) { + bb_error_msg_and_die(bb_msg_read_error); + } + // - means end of MIME section + if ('-' == ch) { + // push it back + ungetc(ch, src_stream); + return; + } + table_ptr = strchr(bb_uuenc_tbl_base64, ch); + } while (table_ptr == NULL); + + /* Convert encoded character to decimal */ + ch = table_ptr - bb_uuenc_tbl_base64; + + if (*table_ptr == '=') { + if (term_count == 0) { + translated[count] = '\0'; + break; + } + term_count++; + } else if (*table_ptr == '\n') { + /* Check for terminating line */ + if (term_count == 5) { + return; + } + term_count = 1; + continue; + } else { + translated[count] = ch; + count++; + term_count = 0; + } + } + + /* Merge 6 bit chars to 8 bit */ + if (count > 1) { + fputc(translated[0] << 2 | translated[1] >> 4, dst_stream); + } + if (count > 2) { + fputc(translated[1] << 4 | translated[2] >> 2, dst_stream); + } + if (count > 3) { + fputc(translated[2] << 6 | translated[3], dst_stream); + } + } +} + + +/* + * get username and password from a file descriptor + */ +void FAST_FUNC get_cred_or_die(int fd) +{ + // either from TTY + if (isatty(fd)) { + G.user = xstrdup(bb_askpass(0, "User: ")); + G.pass = xstrdup(bb_askpass(0, "Password: ")); + // or from STDIN + } else { + FILE *fp = fdopen(fd, "r"); + G.user = xmalloc_fgetline(fp); + G.pass = xmalloc_fgetline(fp); + fclose(fp); + } + if (!G.user || !*G.user || !G.pass || !*G.pass) + bb_error_msg_and_die("no username or password"); +} diff --git a/mailutils/mail.h b/mailutils/mail.h new file mode 100644 index 000000000..bb747c4c5 --- /dev/null +++ b/mailutils/mail.h @@ -0,0 +1,35 @@ + +struct globals { + pid_t helper_pid; + unsigned timeout; + unsigned opts; + char *user; + char *pass; + FILE *fp0; // initial stdin + char *opt_charset; + char *content_type; +}; + +#define G (*ptr_to_globals) +#define timeout (G.timeout ) +#define opts (G.opts ) +//#define user (G.user ) +//#define pass (G.pass ) +//#define fp0 (G.fp0 ) +//#define opt_charset (G.opt_charset) +//#define content_type (G.content_type) +#define INIT_G() do { \ + SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \ + G.opt_charset = (char *)CONFIG_FEATURE_MIME_CHARSET; \ + G.content_type = (char *)"text/plain"; \ +} while (0) + +//char FAST_FUNC *parse_url(char *url, char **user, char **pass); + +void FAST_FUNC launch_helper(const char **argv); +void FAST_FUNC get_cred_or_die(int fd); + +const FAST_FUNC char *command(const char *fmt, const char *param); + +void FAST_FUNC encode_base64(char *fname, const char *text, const char *eol); +void FAST_FUNC decode_base64(FILE *src_stream, FILE *dst_stream); diff --git a/mailutils/mime.c b/mailutils/mime.c new file mode 100644 index 000000000..b81cfd54c --- /dev/null +++ b/mailutils/mime.c @@ -0,0 +1,354 @@ +/* vi: set sw=4 ts=4: */ +/* + * makemime: create MIME-encoded message + * reformime: parse MIME-encoded message + * + * Copyright (C) 2008 by Vladimir Dronnikov + * + * Licensed under GPLv2, see file LICENSE in this tarball for details. + */ +#include "libbb.h" +#include "mail.h" + +/* + makemime -c type [-o file] [-e encoding] [-C charset] [-N name] \ + [-a "Header: Contents"] file + -m [ type ] [-o file] [-e encoding] [-a "Header: Contents"] file + -j [-o file] file1 file2 + @file + + file: filename - read or write from filename + - - read or write from stdin or stdout + &n - read or write from file descriptor n + \( opts \) - read from child process, that generates [ opts ] + +Options: + + -c type - create a new MIME section from "file" with this + Content-Type: (default is application/octet-stream). + -C charset - MIME charset of a new text/plain section. + -N name - MIME content name of the new mime section. + -m [ type ] - create a multipart mime section from "file" of this + Content-Type: (default is multipart/mixed). + -e encoding - use the given encoding (7bit, 8bit, quoted-printable, + or base64), instead of guessing. Omit "-e" and use + -c auto to set Content-Type: to text/plain or + application/octet-stream based on picked encoding. + -j file1 file2 - join mime section file2 to multipart section file1. + -o file - write ther result to file, instead of stdout (not + allowed in child processes). + -a header - prepend an additional header to the output. + + @file - read all of the above options from file, one option or + value on each line. +*/ + +int makemime_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int makemime_main(int argc UNUSED_PARAM, char **argv) +{ + llist_t *opt_headers = NULL, *l; + const char *opt_output; +#define boundary opt_output + + enum { + OPT_c = 1 << 0, // Content-Type: + OPT_e = 1 << 1, // Content-Transfer-Encoding. Ignored. Assumed base64 + OPT_o = 1 << 2, // output to + OPT_C = 1 << 3, // charset + OPT_N = 1 << 4, // COMPAT + OPT_a = 1 << 5, // additional headers + OPT_m = 1 << 6, // COMPAT + OPT_j = 1 << 7, // COMPAT + }; + + INIT_G(); + + // parse options + opt_complementary = "a::"; + opts = getopt32(argv, + "c:e:o:C:N:a:m:j:", + &G.content_type, NULL, &opt_output, &G.opt_charset, NULL, &opt_headers, NULL, NULL + ); + //argc -= optind; + argv += optind; + + // respect -o output + if (opts & OPT_o) + freopen(opt_output, "w", stdout); + + // no files given on command line? -> use stdin + if (!*argv) + *--argv = (char *)"-"; + + // put additional headers + for (l = opt_headers; l; l = l->link) + puts(l->data); + + // make a random string -- it will delimit message parts + srand(monotonic_us()); + boundary = xasprintf("%d-%d-%d", rand(), rand(), rand()); + + // put multipart header + printf( + "Mime-Version: 1.0\n" + "Content-Type: multipart/mixed; boundary=\"%s\"\n" + , boundary + ); + + // put attachments + while (*argv) { + printf( + "\n--%s\n" + "Content-Type: %s; charset=%s\n" + "Content-Disposition: inline; filename=\"%s\"\n" + "Content-Transfer-Encoding: base64\n" + , boundary + , G.content_type + , G.opt_charset + , bb_get_last_path_component_strip(*argv) + ); + encode_base64(*argv++, (const char *)stdin, ""); + } + + // put multipart footer + printf("\n--%s--\n" "\n", boundary); + + return EXIT_SUCCESS; +#undef boundary +} + +static const char *find_token(const char *const string_array[], const char *key, const char *defvalue) +{ + const char *r = NULL; + for (int i = 0; string_array[i] != 0; i++) { + if (strcasecmp(string_array[i], key) == 0) { + r = (char *)string_array[i+1]; + break; + } + } + return (r) ? r : defvalue; +} + +static const char *xfind_token(const char *const string_array[], const char *key) +{ + const char *r = find_token(string_array, key, NULL); + if (r) + return r; + bb_error_msg_and_die("header: %s", key); +} + +enum { + OPT_x = 1 << 0, + OPT_X = 1 << 1, +#if ENABLE_FEATURE_REFORMIME_COMPAT + OPT_d = 1 << 2, + OPT_e = 1 << 3, + OPT_i = 1 << 4, + OPT_s = 1 << 5, + OPT_r = 1 << 6, + OPT_c = 1 << 7, + OPT_m = 1 << 8, + OPT_h = 1 << 9, + OPT_o = 1 << 10, + OPT_O = 1 << 11, +#endif +}; + +static int parse(const char *boundary, char **argv) +{ + char *line, *s, *p; + const char *type; + int boundary_len = strlen(boundary); + const char *delims = " ;\"\t\r\n"; + const char *uniq; + int ntokens; + const char *tokens[32]; // 32 is enough + + // prepare unique string pattern + uniq = xasprintf("%%llu.%u.%s", (unsigned)getpid(), safe_gethostname()); + +//bb_info_msg("PARSE[%s]", terminator); + + while ((line = xmalloc_fgets_str(stdin, "\r\n\r\n")) != NULL) { + + // seek to start of MIME section + // N.B. to avoid false positives let us seek to the _last_ occurance + p = NULL; + s = line; + while ((s=strcasestr(s, "Content-Type:")) != NULL) + p = s++; + if (!p) + goto next; +//bb_info_msg("L[%s]", p); + + // split to tokens + // TODO: strip of comments which are of form: (comment-text) + ntokens = 0; + tokens[ntokens] = NULL; + for (s = strtok(p, delims); s; s = strtok(NULL, delims)) { + tokens[ntokens] = s; + if (ntokens < ARRAY_SIZE(tokens) - 1) + ntokens++; +//bb_info_msg("L[%d][%s]", ntokens, s); + } + tokens[ntokens] = NULL; +//bb_info_msg("N[%d]", ntokens); + + // analyse tokens + type = find_token(tokens, "Content-Type:", "text/plain"); +//bb_info_msg("T[%s]", type); + if (0 == strncasecmp(type, "multipart/", 10)) { + if (0 == strcasecmp(type+10, "mixed")) { + parse(xfind_token(tokens, "boundary="), argv); + } else + bb_error_msg_and_die("no support of content type '%s'", type); + } else { + pid_t pid = pid; + int rc; + FILE *fp; + // fetch charset + const char *charset = find_token(tokens, "charset=", CONFIG_FEATURE_MIME_CHARSET); + // fetch encoding + const char *encoding = find_token(tokens, "Content-Transfer-Encoding:", "7bit"); + // compose target filename + char *filename = (char *)find_token(tokens, "filename=", NULL); + if (!filename) + filename = xasprintf(uniq, monotonic_us()); + else + filename = bb_get_last_path_component_strip(xstrdup(filename)); + + // start external helper, if any + if (opts & OPT_X) { + int fd[2]; + xpipe(fd); + pid = fork(); + if (0 == pid) { + // child reads from fd[0] + xdup2(fd[0], STDIN_FILENO); + close(fd[0]); close(fd[1]); + xsetenv("CONTENT_TYPE", type); + xsetenv("CHARSET", charset); + xsetenv("ENCODING", encoding); + xsetenv("FILENAME", filename); + BB_EXECVP(*argv, argv); + _exit(EXIT_FAILURE); + } + // parent dumps to fd[1] + close(fd[0]); + fp = fdopen(fd[1], "w"); + signal(SIGPIPE, SIG_IGN); // ignore EPIPE + // or create a file for dump + } else { + char *fname = xasprintf("%s%s", *argv, filename); + fp = xfopen_for_write(fname); + free(fname); + } + + // housekeeping + free(filename); + + // dump to fp + if (0 == strcasecmp(encoding, "base64")) { + decode_base64(stdin, fp); + } else if (0 != strcasecmp(encoding, "7bit") + && 0 != strcasecmp(encoding, "8bit")) { + // quoted-printable, binary, user-defined are unsupported so far + bb_error_msg_and_die("no support of encoding '%s'", encoding); + } else { + // N.B. we have written redundant \n. so truncate the file + // The following weird 2-tacts reading technique is due to + // we have to not write extra \n at the end of the file + // In case of -x option we could truncate the resulting file as + // fseek(fp, -1, SEEK_END); + // if (ftruncate(fileno(fp), ftell(fp))) + // bb_perror_msg("ftruncate"); + // But in case of -X we have to be much more careful. There is + // no means to truncate what we already have sent to the helper. + p = xmalloc_fgets_str(stdin, "\r\n"); + while (p) { + if ((s = xmalloc_fgets_str(stdin, "\r\n")) == NULL) + break; + if ('-' == s[0] && '-' == s[1] + && 0 == strncmp(s+2, boundary, boundary_len)) + break; + fputs(p, fp); + p = s; + } + +/* + while ((s = xmalloc_fgetline_str(stdin, "\r\n")) != NULL) { + if ('-' == s[0] && '-' == s[1] + && 0 == strncmp(s+2, boundary, boundary_len)) + break; + fprintf(fp, "%s\n", s); + } + // N.B. we have written redundant \n. so truncate the file + fseek(fp, -1, SEEK_END); + if (ftruncate(fileno(fp), ftell(fp))) + bb_perror_msg("ftruncate"); +*/ + } + fclose(fp); + + // finalize helper + if (opts & OPT_X) { + signal(SIGPIPE, SIG_DFL); + // exit if helper exited >0 + rc = wait4pid(pid); + if (rc) + return rc+20; + } + + // check multipart finalized + if (s && '-' == s[2+boundary_len] && '-' == s[2+boundary_len+1]) { + free(line); + break; + } + } + next: + free(line); + } + +//bb_info_msg("ENDPARSE[%s]", boundary); + + return EXIT_SUCCESS; +} + +/* +Usage: reformime [options] + -d - parse a delivery status notification. + -e - extract contents of MIME section. + -x - extract MIME section to a file. + -X - pipe MIME section to a program. + -i - show MIME info. + -s n.n.n.n - specify MIME section. + -r - rewrite message, filling in missing MIME headers. + -r7 - also convert 8bit/raw encoding to quoted-printable, if possible. + -r8 - also convert quoted-printable encoding to 8bit, if possible. + -c charset - default charset for rewriting, -o, and -O. + -m [file] [file]... - create a MIME message digest. + -h "header" - decode RFC 2047-encoded header. + -o "header" - encode unstructured header using RFC 2047. + -O "header" - encode address list header using RFC 2047. +*/ + +int reformime_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int reformime_main(int argc UNUSED_PARAM, char **argv) +{ + const char *opt_prefix = ""; + + INIT_G(); + + // parse options + // N.B. only -x and -X are supported so far + opt_complementary = "x--X:X--x" USE_FEATURE_REFORMIME_COMPAT(":m::"); + opts = getopt32(argv, + "x:X" USE_FEATURE_REFORMIME_COMPAT("deis:r:c:m:h:o:O:"), + &opt_prefix + USE_FEATURE_REFORMIME_COMPAT(, NULL, NULL, &G.opt_charset, NULL, NULL, NULL, NULL) + ); + //argc -= optind; + argv += optind; + + return parse("", (opts & OPT_X) ? argv : (char **)&opt_prefix); +} diff --git a/mailutils/popmaildir.c b/mailutils/popmaildir.c new file mode 100644 index 000000000..d2cc7c0b9 --- /dev/null +++ b/mailutils/popmaildir.c @@ -0,0 +1,237 @@ +/* vi: set sw=4 ts=4: */ +/* + * popmaildir: a simple yet powerful POP3 client + * Delivers contents of remote mailboxes to local Maildir + * + * Inspired by original utility by Nikola Vladov + * + * Copyright (C) 2008 by Vladimir Dronnikov + * + * Licensed under GPLv2, see file LICENSE in this tarball for details. + */ +#include "libbb.h" +#include "mail.h" + +static void pop3_checkr(const char *fmt, const char *param, char **ret) +{ + const char *msg = command(fmt, param); + char *answer = xmalloc_fgetline(stdin); + if (answer && '+' == *answer) { + if (timeout) + alarm(0); + if (ret) + *ret = answer+4; // skip "+OK " + else if (ENABLE_FEATURE_CLEAN_UP) + free(answer); + return; + } + bb_error_msg_and_die("%s failed: %s", msg, answer); +} + +static void pop3_check(const char *fmt, const char *param) +{ + pop3_checkr(fmt, param, NULL); +} + +int popmaildir_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int popmaildir_main(int argc UNUSED_PARAM, char **argv) +{ + char *buf; + unsigned nmsg; + char *hostname; + pid_t pid; + const char *retr; +#if ENABLE_FEATURE_POPMAILDIR_DELIVERY + const char *delivery; +#endif + unsigned opt_nlines = 0; + + enum { + OPT_b = 1 << 0, // -b binary mode. Ignored + OPT_d = 1 << 1, // -d,-dd,-ddd debug. Ignored + OPT_m = 1 << 2, // -m show used memory. Ignored + OPT_V = 1 << 3, // -V version. Ignored + OPT_c = 1 << 4, // -c use tcpclient. Ignored + OPT_a = 1 << 5, // -a use APOP protocol + OPT_s = 1 << 6, // -s skip authorization + OPT_T = 1 << 7, // -T get messages with TOP instead with RETR + OPT_k = 1 << 8, // -k keep retrieved messages on the server + OPT_t = 1 << 9, // -t90 set timeout to 90 sec + OPT_R = 1 << 10, // -R20000 remove old messages on the server >= 20000 bytes (requires -k). Ignored + OPT_Z = 1 << 11, // -Z11-23 remove messages from 11 to 23 (dangerous). Ignored + OPT_L = 1 << 12, // -L50000 not retrieve new messages >= 50000 bytes. Ignored + OPT_H = 1 << 13, // -H30 type first 30 lines of a message; (-L12000 -H30). Ignored + OPT_M = 1 << 14, // -M\"program arg1 arg2 ...\"; deliver by program. Treated like -F + OPT_F = 1 << 15, // -F\"program arg1 arg2 ...\"; filter by program. Treated like -M + }; + + // init global variables + INIT_G(); + + // parse options + opt_complementary = "-1:dd:t+:R+:L+:H+"; + opts = getopt32(argv, + "bdmVcasTkt:" "R:Z:L:H:" USE_FEATURE_POPMAILDIR_DELIVERY("M:F:"), + &timeout, NULL, NULL, NULL, &opt_nlines + USE_FEATURE_POPMAILDIR_DELIVERY(, &delivery, &delivery) // we treat -M and -F the same + ); + //argc -= optind; + argv += optind; + + // get auth info + if (!(opts & OPT_s)) + get_cred_or_die(STDIN_FILENO); + + // goto maildir + xchdir(*argv++); + + // launch connect helper, if any + if (*argv) + launch_helper((const char **)argv); + + // get server greeting + pop3_checkr(NULL, NULL, &buf); + + // authenticate (if no -s given) + if (!(opts & OPT_s)) { + // server supports APOP and we want it? -> use it + if ('<' == *buf && (opts & OPT_a)) { + md5_ctx_t md5; + // yes! compose + char *s = strchr(buf, '>'); + if (s) + strcpy(s+1, G.pass); + s = buf; + // get md5 sum of + md5_begin(&md5); + md5_hash(s, strlen(s), &md5); + md5_end(s, &md5); + // NOTE: md5 struct contains enough space + // so we reuse md5 space instead of xzalloc(16*2+1) +#define md5_hex ((uint8_t *)&md5) +// uint8_t *md5_hex = (uint8_t *)&md5; + *bin2hex((char *)md5_hex, s, 16) = '\0'; + // APOP + s = xasprintf("%s %s", G.user, md5_hex); +#undef md5_hex + pop3_check("APOP %s", s); + if (ENABLE_FEATURE_CLEAN_UP) { + free(s); + free(buf-4); // buf is "+OK " away from malloc'ed string + } + // server ignores APOP -> use simple text authentication + } else { + // USER + pop3_check("USER %s", G.user); + // PASS + pop3_check("PASS %s", G.pass); + } + } + + // get mailbox statistics + pop3_checkr("STAT", NULL, &buf); + + // prepare message filename suffix + hostname = safe_gethostname(); + pid = getpid(); + + // get messages counter + // NOTE: we don't use xatou(buf) since buf is "nmsg nbytes" + // we only need nmsg and atoi is just exactly what we need + // if atoi fails to convert buf into number it returns 0 + // in this case the following loop simply will not be executed + nmsg = atoi(buf); + if (ENABLE_FEATURE_CLEAN_UP) + free(buf-4); // buf is "+OK " away from malloc'ed string + + // loop through messages + retr = (opts & OPT_T) ? xasprintf("TOP %%u %u", opt_nlines) : "RETR %u"; + for (; nmsg; nmsg--) { + + char *filename; + char *target; + char *answer; + FILE *fp; +#if ENABLE_FEATURE_POPMAILDIR_DELIVERY + int rc; +#endif + // generate unique filename + filename = xasprintf("tmp/%llu.%u.%s", + monotonic_us(), (unsigned)pid, hostname); + + // retrieve message in ./tmp/ unless filter is specified + pop3_check(retr, (const char *)(ptrdiff_t)nmsg); + +#if ENABLE_FEATURE_POPMAILDIR_DELIVERY + // delivery helper ordered? -> setup pipe + if (opts & (OPT_F|OPT_M)) { + // helper will have $FILENAME set to filename + xsetenv("FILENAME", filename); + fp = popen(delivery, "w"); + unsetenv("FILENAME"); + if (!fp) { + bb_perror_msg("delivery helper"); + break; + } + } else +#endif + // create and open file filename + fp = xfopen_for_write(filename); + + // copy stdin to fp (either filename or delivery helper) + while ((answer = xmalloc_fgets_str(stdin, "\r\n")) != NULL) { + char *s = answer; + if ('.' == answer[0]) { + if ('.' == answer[1]) + s++; + else if ('\r' == answer[1] && '\n' == answer[2] && '\0' == answer[3]) + break; + } + //*strchrnul(s, '\r') = '\n'; + fputs(s, fp); + free(answer); + } + +#if ENABLE_FEATURE_POPMAILDIR_DELIVERY + // analyse delivery status + if (opts & (OPT_F|OPT_M)) { + rc = pclose(fp); + if (99 == rc) // 99 means bail out + break; +// if (rc) // !0 means skip to the next message + goto skip; +// // 0 means continue + } else { + // close filename + fclose(fp); + } +#endif + + // delete message from server + if (!(opts & OPT_k)) + pop3_check("DELE %u", (const char*)(ptrdiff_t)nmsg); + + // atomically move message to ./new/ + target = xstrdup(filename); + strncpy(target, "new", 3); + // ... or just stop receiving on failure + if (rename_or_warn(filename, target)) + break; + free(target); + +#if ENABLE_FEATURE_POPMAILDIR_DELIVERY + skip: +#endif + free(filename); + } + + // Bye + pop3_check("QUIT", NULL); + + if (ENABLE_FEATURE_CLEAN_UP) { + free(G.user); + free(G.pass); + } + + return EXIT_SUCCESS; +} diff --git a/mailutils/sendmail.c b/mailutils/sendmail.c new file mode 100644 index 000000000..55555c326 --- /dev/null +++ b/mailutils/sendmail.c @@ -0,0 +1,388 @@ +/* vi: set sw=4 ts=4: */ +/* + * bare bones sendmail + * + * Copyright (C) 2008 by Vladimir Dronnikov + * + * Licensed under GPLv2, see file LICENSE in this tarball for details. + */ +#include "libbb.h" +#include "mail.h" + +static int smtp_checkp(const char *fmt, const char *param, int code) +{ + char *answer; + const char *msg = command(fmt, param); + // read stdin + // if the string has a form \d\d\d- -- read next string. E.g. EHLO response + // parse first bytes to a number + // if code = -1 then just return this number + // if code != -1 then checks whether the number equals the code + // if not equal -> die saying msg + while ((answer = xmalloc_fgetline(stdin)) != NULL) + if (strlen(answer) <= 3 || '-' != answer[3]) + break; + if (answer) { + int n = atoi(answer); + if (timeout) + alarm(0); + free(answer); + if (-1 == code || n == code) + return n; + } + bb_error_msg_and_die("%s failed", msg); +} + +static int smtp_check(const char *fmt, int code) +{ + return smtp_checkp(fmt, NULL, code); +} + +// strip argument of bad chars +static char *sane_address(char *str) +{ + char *s = str; + char *p = s; + while (*s) { + if (isalnum(*s) || '_' == *s || '-' == *s || '.' == *s || '@' == *s) { + *p++ = *s; + } + s++; + } + *p = '\0'; + return str; +} + +static void rcptto(const char *s) +{ + smtp_checkp("RCPT TO:<%s>", s, 250); +} + +int sendmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int sendmail_main(int argc UNUSED_PARAM, char **argv) +{ +#if ENABLE_FEATURE_SENDMAIL_MAILX + llist_t *opt_attachments = NULL; + const char *opt_subject; +#if ENABLE_FEATURE_SENDMAIL_MAILXX + llist_t *opt_carboncopies = NULL; + char *opt_errors_to; +#endif +#endif + char *opt_connect = opt_connect; + char *opt_from, *opt_fullname; + char *boundary; + llist_t *l; + llist_t *headers = NULL; + char *domain = sane_address(safe_getdomainname()); + int code; + + enum { + OPT_w = 1 << 0, // network timeout + OPT_t = 1 << 1, // read message for recipients + OPT_N = 1 << 2, // request notification + OPT_f = 1 << 3, // sender address + OPT_F = 1 << 4, // sender name, overrides $NAME + OPT_s = 1 << 5, // subject + OPT_j = 1 << 6, // assumed charset + OPT_a = 1 << 7, // attachment(s) + OPT_H = 1 << 8, // use external connection helper + OPT_S = 1 << 9, // specify connection string + OPT_c = 1 << 10, // carbon copy + OPT_e = 1 << 11, // errors-to address + }; + + // init global variables + INIT_G(); + + // save initial stdin since body is piped! + xdup2(STDIN_FILENO, 3); + G.fp0 = fdopen(3, "r"); + + // parse options + opt_complementary = "w+" USE_FEATURE_SENDMAIL_MAILX(":a::H--S:S--H") USE_FEATURE_SENDMAIL_MAILXX(":c::"); + opts = getopt32(argv, + "w:t" "N:f:F:" USE_FEATURE_SENDMAIL_MAILX("s:j:a:H:S:") USE_FEATURE_SENDMAIL_MAILXX("c:e:") + "X:V:vq:R:O:o:nmL:Iih:GC:B:b:A:" // postfix compat only, ignored + // r:Q:p:M:Dd are candidates from another man page. TODO? + "46E", // ssmtp introduces another quirks. TODO?: -a[upm] (user, pass, method) to be supported + &timeout /* -w */, NULL, &opt_from, &opt_fullname, + USE_FEATURE_SENDMAIL_MAILX(&opt_subject, &G.opt_charset, &opt_attachments, &opt_connect, &opt_connect,) + USE_FEATURE_SENDMAIL_MAILXX(&opt_carboncopies, &opt_errors_to,) + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL + ); + //argc -= optind; + argv += optind; + + // connect to server + +#if ENABLE_FEATURE_SENDMAIL_MAILX + // N.B. -H and -S are mutually exclusive so they do not spoil opt_connect + // connection helper ordered? -> + if (opts & OPT_H) { + const char *args[] = { "sh", "-c", opt_connect, NULL }; + // plug it in + launch_helper(args); + // vanilla connection + } else +#endif + { + int fd; + // host[:port] not explicitly specified ? -> use $SMTPHOST + // no $SMTPHOST ? -> use localhost + if (!(opts & OPT_S)) { + opt_connect = getenv("SMTPHOST"); + if (!opt_connect) + opt_connect = (char *)"127.0.0.1"; + } + // do connect + fd = create_and_connect_stream_or_die(opt_connect, 25); + // and make ourselves a simple IO filter + xmove_fd(fd, STDIN_FILENO); + xdup2(STDIN_FILENO, STDOUT_FILENO); + } + // N.B. from now we know nothing about network :) + + // wait for initial server OK + // N.B. if we used openssl the initial 220 answer is already swallowed during openssl TLS init procedure + // so we need to push the server to see whether we are ok + code = smtp_check("NOOP", -1); + // 220 on plain connection, 250 on openssl-helped TLS session + if (220 == code) + smtp_check(NULL, 250); // reread the code to stay in sync + else if (250 != code) + bb_error_msg_and_die("INIT failed"); + + // we should start with modern EHLO + if (250 != smtp_checkp("EHLO %s", domain, -1)) { + smtp_checkp("HELO %s", domain, 250); + } + + // set sender + // N.B. we have here a very loosely defined algotythm + // since sendmail historically offers no means to specify secrets on cmdline. + // 1) server can require no authentication -> + // we must just provide a (possibly fake) reply address. + // 2) server can require AUTH -> + // we must provide valid username and password along with a (possibly fake) reply address. + // For the sake of security username and password are to be read either from console or from a secured file. + // Since reading from console may defeat usability, the solution is either to read from a predefined + // file descriptor (e.g. 4), or again from a secured file. + + // got no sender address? -> use system username as a resort + if (!(opts & OPT_f)) { + // N.B. IMHO getenv("USER") can be way easily spoofed! + G.user = bb_getpwuid(NULL, -1, getuid()); + opt_from = xasprintf("%s@%s", G.user, domain); + } + if (ENABLE_FEATURE_CLEAN_UP) + free(domain); + + code = -1; // first try softly without authentication + while (250 != smtp_checkp("MAIL FROM:<%s>", opt_from, code)) { + // MAIL FROM failed -> authentication needed + if (334 == smtp_check("AUTH LOGIN", -1)) { + // we must read credentials + get_cred_or_die(4); + encode_base64(NULL, G.user, NULL); + smtp_check("", 334); + encode_base64(NULL, G.pass, NULL); + smtp_check("", 235); + } + // authenticated OK? -> retry to set sender + // but this time die on failure! + code = 250; + } + + // recipients specified as arguments + while (*argv) { + char *s = sane_address(*argv); + // loose test on email address validity +// if (strchr(s, '@')) { + rcptto(s); + llist_add_to_end(&headers, xasprintf("To: %s", s)); +// } + argv++; + } + +#if ENABLE_FEATURE_SENDMAIL_MAILXX + // carbon copies recipients specified as -c options + for (l = opt_carboncopies; l; l = l->link) { + char *s = sane_address(l->data); + // loose test on email address validity +// if (strchr(s, '@')) { + rcptto(s); + // TODO: do we ever need to mangle the message? + //llist_add_to_end(&headers, xasprintf("Cc: %s", s)); +// } + } +#endif + + // if -t specified or no recipients specified -> read recipients from message + // i.e. scan stdin for To:, Cc:, Bcc: lines ... + // ... and then use the rest of stdin as message body + // N.B. subject read from body can be further overrided with one specified on command line. + // recipients are merged. Bcc: lines are deleted + // N.B. other headers are collected and will be dumped verbatim + if (opts & OPT_t || !headers) { + // fetch recipients and (optionally) subject + char *s; + while ((s = xmalloc_fgetline(G.fp0)) != NULL) { + if (0 == strncasecmp("To: ", s, 4) || 0 == strncasecmp("Cc: ", s, 4)) { + rcptto(sane_address(s+4)); + llist_add_to_end(&headers, s); + } else if (0 == strncasecmp("Bcc: ", s, 5)) { + rcptto(sane_address(s+5)); + free(s); + // N.B. Bcc vanishes from headers! + } else if (0 == strncmp("Subject: ", s, 9)) { + // we read subject -> use it verbatim unless it is specified + // on command line + if (!(opts & OPT_s)) + llist_add_to_end(&headers, s); + else + free(s); + } else if (s[0]) { + // misc header + llist_add_to_end(&headers, s); + } else { + free(s); + break; // stop on the first empty line + } + } + } + + // enter "put message" mode + smtp_check("DATA", 354); + + // put headers we could have preread with -t + for (l = headers; l; l = l->link) { + printf("%s\r\n", l->data); + if (ENABLE_FEATURE_CLEAN_UP) + free(l->data); + } + + // put (possibly encoded) subject +#if ENABLE_FEATURE_SENDMAIL_MAILX + if (opts & OPT_s) { + printf("Subject: "); + if (opts & OPT_j) { + printf("=?%s?B?", G.opt_charset); + encode_base64(NULL, opt_subject, NULL); + printf("?="); + } else { + printf("%s", opt_subject); + } + printf("\r\n"); + } +#endif + + // put sender name, $NAME is the default + if (!(opts & OPT_F)) + opt_fullname = getenv("NAME"); + if (opt_fullname) + printf("From: \"%s\" <%s>\r\n", opt_fullname, opt_from); + + // put notification + if (opts & OPT_N) + printf("Disposition-Notification-To: %s\r\n", opt_from); + +#if ENABLE_FEATURE_SENDMAIL_MAILXX + // put errors recipient + if (opts & OPT_e) + printf("Errors-To: %s\r\n", opt_errors_to); +#endif + + // make a random string -- it will delimit message parts + srand(monotonic_us()); + boundary = xasprintf("%d=_%d-%d", rand(), rand(), rand()); + + // put common headers + // TODO: do we really need this? +// printf("Message-ID: <%s>\r\n", boundary); + +#if ENABLE_FEATURE_SENDMAIL_MAILX + // have attachments? -> compose multipart MIME + if (opt_attachments) { + const char *fmt; + const char *p; + char *q; + + printf( + "Mime-Version: 1.0\r\n" + "%smultipart/mixed; boundary=\"%s\"\r\n" + , "Content-Type: " + , boundary + ); + + // body is pseudo attachment read from stdin in first turn + llist_add_to(&opt_attachments, (char *)"-"); + + // put body + attachment(s) + // N.B. all these weird things just to be tiny + // by reusing string patterns! + fmt = + "\r\n--%s\r\n" + "%stext/plain; charset=%s\r\n" + "%s%s\r\n" + "%s" + ; + p = G.opt_charset; + q = (char *)""; + l = opt_attachments; + while (l) { + printf( + fmt + , boundary + , "Content-Type: " + , p + , "Content-Disposition: inline" + , q + , "Content-Transfer-Encoding: base64\r\n" + ); + p = ""; + fmt = + "\r\n--%s\r\n" + "%sapplication/octet-stream%s\r\n" + "%s; filename=\"%s\"\r\n" + "%s" + ; + encode_base64(l->data, (const char *)G.fp0, "\r"); + l = l->link; + if (l) + q = bb_get_last_path_component_strip(l->data); + } + + // put message terminator + printf("\r\n--%s--\r\n" "\r\n", boundary); + + // no attachments? -> just dump message + } else +#endif + { + char *s; + // terminate headers + printf("\r\n"); + // put plain text respecting leading dots + while ((s = xmalloc_fgetline(G.fp0)) != NULL) { + // escape leading dots + // N.B. this feature is implied even if no -i (-oi) switch given + // N.B. we need to escape the leading dot regardless of + // whether it is single or not character on the line + if ('.' == s[0] /*&& '\0' == s[1] */) + printf("."); + // dump read line + printf("%s\r\n", s); + } + } + + // leave "put message" mode + smtp_check(".", 250); + // ... and say goodbye + smtp_check("QUIT", 221); + // cleanup + if (ENABLE_FEATURE_CLEAN_UP) + fclose(G.fp0); + + return EXIT_SUCCESS; +} -- 2.25.1