add mailutils/*
authorDenis Vlasenko <vda.linux@googlemail.com>
Thu, 6 Nov 2008 23:42:42 +0000 (23:42 -0000)
committerDenis Vlasenko <vda.linux@googlemail.com>
Thu, 6 Nov 2008 23:42:42 +0000 (23:42 -0000)
mailutils/Config.in [new file with mode: 0644]
mailutils/Kbuild [new file with mode: 0644]
mailutils/mail.c [new file with mode: 0644]
mailutils/mail.h [new file with mode: 0644]
mailutils/mime.c [new file with mode: 0644]
mailutils/popmaildir.c [new file with mode: 0644]
mailutils/sendmail.c [new file with mode: 0644]

diff --git a/mailutils/Config.in b/mailutils/Config.in
new file mode 100644 (file)
index 0000000..b8d6977
--- /dev/null
@@ -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 (file)
index 0000000..871e879
--- /dev/null
@@ -0,0 +1,11 @@
+# Makefile for busybox
+#
+# Copyright (C) 1999-2005 by Erik Andersen <andersen@codepoet.org>
+#
+# 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 (file)
index 0000000..ab1304a
--- /dev/null
@@ -0,0 +1,242 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * helper routines
+ *
+ * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
+ *
+ * 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 (file)
index 0000000..bb747c4
--- /dev/null
@@ -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 (file)
index 0000000..b81cfd5
--- /dev/null
@@ -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 <dronnikov@gmail.com>
+ *
+ * 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 (file)
index 0000000..d2cc7c0
--- /dev/null
@@ -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 <dronnikov@gmail.com>
+ *
+ * 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 <stamp><password>
+                       char *s = strchr(buf, '>');
+                       if (s)
+                               strcpy(s+1, G.pass);
+                       s = buf;
+                       // get md5 sum of <stamp><password>
+                       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 (file)
index 0000000..55555c3
--- /dev/null
@@ -0,0 +1,388 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * bare bones sendmail
+ *
+ * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
+ *
+ * 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;
+}