summaryrefslogtreecommitdiffstats
path: root/tools/mime-maker.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mime-maker.c')
-rw-r--r--tools/mime-maker.c777
1 files changed, 777 insertions, 0 deletions
diff --git a/tools/mime-maker.c b/tools/mime-maker.c
new file mode 100644
index 0000000..91eab82
--- /dev/null
+++ b/tools/mime-maker.c
@@ -0,0 +1,777 @@
+/* mime-maker.c - Create MIME structures
+ * Copyright (C) 2016 g10 Code GmbH
+ * Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik
+ *
+ * This file is part of GnuPG.
+ *
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../common/util.h"
+#include "../common/zb32.h"
+#include "rfc822parse.h"
+#include "mime-maker.h"
+
+
+/* An object to store an header. Also used for a list of headers. */
+struct header_s
+{
+ struct header_s *next;
+ char *value; /* Malloced value. */
+ char name[1]; /* Name. */
+};
+typedef struct header_s *header_t;
+
+
+/* An object to store a MIME part. A part is the header plus the
+ * content (body). */
+struct part_s
+{
+ struct part_s *next; /* Next part in the current container. */
+ struct part_s *child; /* Child container. */
+ char *boundary; /* Malloced boundary string. */
+ header_t headers; /* List of headers. */
+ header_t *headers_tail;/* Address of last header in chain. */
+ size_t bodylen; /* Length of BODY. */
+ char *body; /* Malloced buffer with the body. This is the
+ * non-encoded value. */
+ unsigned int partid; /* The part ID. */
+};
+typedef struct part_s *part_t;
+
+
+
+/* Definition of the mime parser object. */
+struct mime_maker_context_s
+{
+ void *cookie; /* Cookie passed to all callbacks. */
+
+ unsigned int verbose:1; /* Enable verbose mode. */
+ unsigned int debug:1; /* Enable debug mode. */
+
+ part_t mail; /* The MIME tree. */
+ part_t current_part;
+
+ unsigned int partid_counter; /* Counter assign part ids. */
+
+ int boundary_counter; /* Used to create easy to read boundaries. */
+ char *boundary_suffix; /* Random string used in the boundaries. */
+
+ struct b64state *b64state; /* NULL or malloced Base64 decoder state. */
+
+ /* Helper to convey the output stream to recursive functions. */
+ estream_t outfp;
+};
+
+
+/* Create a new mime make object. COOKIE is a values woich will be
+ * used as first argument for all callbacks registered with this
+ * object. */
+gpg_error_t
+mime_maker_new (mime_maker_t *r_maker, void *cookie)
+{
+ mime_maker_t ctx;
+
+ *r_maker = NULL;
+
+ ctx = xtrycalloc (1, sizeof *ctx);
+ if (!ctx)
+ return gpg_error_from_syserror ();
+ ctx->cookie = cookie;
+
+ *r_maker = ctx;
+ return 0;
+}
+
+
+static void
+release_parts (part_t part)
+{
+ while (part)
+ {
+ part_t partnext = part->next;
+ while (part->headers)
+ {
+ header_t hdrnext = part->headers->next;
+ xfree (part->headers);
+ part->headers = hdrnext;
+ }
+ release_parts (part->child);
+ xfree (part->boundary);
+ xfree (part->body);
+ xfree (part);
+ part = partnext;
+ }
+}
+
+
+/* Release a mime maker object. */
+void
+mime_maker_release (mime_maker_t ctx)
+{
+ if (!ctx)
+ return;
+
+ release_parts (ctx->mail);
+ xfree (ctx->boundary_suffix);
+ xfree (ctx);
+}
+
+
+/* Set verbose and debug mode. */
+void
+mime_maker_set_verbose (mime_maker_t ctx, int level)
+{
+ if (!level)
+ {
+ ctx->verbose = 0;
+ ctx->debug = 0;
+ }
+ else
+ {
+ ctx->verbose = 1;
+ if (level > 10)
+ ctx->debug = 1;
+ }
+}
+
+
+static void
+dump_parts (part_t part, int level)
+{
+ header_t hdr;
+
+ for (; part; part = part->next)
+ {
+ log_debug ("%*s[part %u]\n", level*2, "", part->partid);
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ {
+ log_debug ("%*s%s: %s\n", level*2, "", hdr->name, hdr->value);
+ }
+ if (part->body)
+ log_debug ("%*s[body %zu bytes]\n", level*2, "", part->bodylen);
+ if (part->child)
+ {
+ log_debug ("%*s[container]\n", level*2, "");
+ dump_parts (part->child, level+1);
+ }
+ }
+}
+
+
+/* Dump the mime tree for debugging. */
+void
+mime_maker_dump_tree (mime_maker_t ctx)
+{
+ dump_parts (ctx->mail, 0);
+}
+
+
+/* Find the parent node for NEEDLE starting at ROOT. */
+static part_t
+find_parent (part_t root, part_t needle)
+{
+ part_t node, n;
+
+ for (node = root->child; node; node = node->next)
+ {
+ if (node == needle)
+ return root;
+ if ((n = find_parent (node, needle)))
+ return n;
+ }
+ return NULL;
+}
+
+/* Find the part node from the PARTID. */
+static part_t
+find_part (part_t root, unsigned int partid)
+{
+ part_t node, n;
+
+ for (node = root->child; node; node = node->next)
+ {
+ if (node->partid == partid)
+ return root;
+ if ((n = find_part (node, partid)))
+ return n;
+ }
+ return NULL;
+}
+
+
+/* Create a boundary string. Outr codes is aware of the general
+ * structure of that string (gebins with "=-=") so that
+ * it can protect against accidentally-used boundaries within the
+ * content. */
+static char *
+generate_boundary (mime_maker_t ctx)
+{
+ if (!ctx->boundary_suffix)
+ {
+ char buffer[12];
+
+ gcry_create_nonce (buffer, sizeof buffer);
+ ctx->boundary_suffix = zb32_encode (buffer, 8 * sizeof buffer);
+ if (!ctx->boundary_suffix)
+ return NULL;
+ }
+
+ ctx->boundary_counter++;
+ return es_bsprintf ("=-=%02d-%s=-=",
+ ctx->boundary_counter, ctx->boundary_suffix);
+}
+
+
+/* Ensure that the context has a MAIL and CURRENT_PART object and
+ * return the parent object if available */
+static gpg_error_t
+ensure_part (mime_maker_t ctx, part_t *r_parent)
+{
+ if (!ctx->mail)
+ {
+ ctx->mail = xtrycalloc (1, sizeof *ctx->mail);
+ if (!ctx->mail)
+ {
+ if (r_parent)
+ *r_parent = NULL;
+ return gpg_error_from_syserror ();
+ }
+ log_assert (!ctx->current_part);
+ ctx->current_part = ctx->mail;
+ ctx->current_part->headers_tail = &ctx->current_part->headers;
+ }
+ log_assert (ctx->current_part);
+ if (r_parent)
+ *r_parent = find_parent (ctx->mail, ctx->current_part);
+
+ return 0;
+}
+
+
+/* Check whether a header with NAME has already been set into PART.
+ * NAME must be in canonical capitalized format. Return true or
+ * false. */
+static int
+have_header (part_t part, const char *name)
+{
+ header_t hdr;
+
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ if (!strcmp (hdr->name, name))
+ return 1;
+ return 0;
+}
+
+
+/* Helper to add a header to a part. */
+static gpg_error_t
+add_header (part_t part, const char *name, const char *value)
+{
+ gpg_error_t err;
+ header_t hdr;
+ size_t namelen;
+ const char *s;
+ char *p;
+
+ if (!value)
+ {
+ s = strchr (name, '=');
+ if (!s)
+ return gpg_error (GPG_ERR_INV_ARG);
+ namelen = s - name;
+ value = s+1;
+ }
+ else
+ namelen = strlen (name);
+
+ hdr = xtrymalloc (sizeof *hdr + namelen);
+ if (!hdr)
+ return gpg_error_from_syserror ();
+ hdr->next = NULL;
+ memcpy (hdr->name, name, namelen);
+ hdr->name[namelen] = 0;
+
+ /* Check that the header name is valid. */
+ if (!rfc822_valid_header_name_p (hdr->name))
+ {
+ xfree (hdr);
+ return gpg_error (GPG_ERR_INV_NAME);
+ }
+
+ rfc822_capitalize_header_name (hdr->name);
+ hdr->value = xtrystrdup (value);
+ if (!hdr->value)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (hdr);
+ return err;
+ }
+
+ for (p = hdr->value + strlen (hdr->value) - 1;
+ (p >= hdr->value
+ && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r'));
+ p--)
+ *p = 0;
+ if (!(p >= hdr->value))
+ {
+ xfree (hdr->value);
+ xfree (hdr);
+ return gpg_error (GPG_ERR_INV_VALUE); /* Only spaces. */
+ }
+
+ if (part)
+ {
+ *part->headers_tail = hdr;
+ part->headers_tail = &hdr->next;
+ }
+ else
+ xfree (hdr);
+
+ return 0;
+}
+
+
+/* Add a header with NAME and VALUE to the current mail. A LF in the
+ * VALUE will be handled automagically. If NULL is used for VALUE it
+ * is expected that the NAME has the format "NAME=VALUE" and VALUE is
+ * taken from there.
+ *
+ * If no container has been added, the header will be used for the
+ * regular mail headers and not for a MIME part. If the current part
+ * is in a container and a body has been added, we append a new part
+ * to the current container. Thus for a non-MIME mail the caller
+ * needs to call this function followed by a call to add a body. When
+ * adding a Content-Type the boundary parameter must not be included.
+ */
+gpg_error_t
+mime_maker_add_header (mime_maker_t ctx, const char *name, const char *value)
+{
+ gpg_error_t err;
+ part_t part, parent;
+
+ /* Hack to use this function for a syntax check of NAME and VALUE. */
+ if (!ctx)
+ return add_header (NULL, name, value);
+
+ err = ensure_part (ctx, &parent);
+ if (err)
+ return err;
+ part = ctx->current_part;
+
+ if ((part->body || part->child) && !parent)
+ {
+ /* We already have a body but no parent. Adding another part is
+ * thus not possible. */
+ return gpg_error (GPG_ERR_CONFLICT);
+ }
+ if (part->body || part->child)
+ {
+ /* We already have a body and there is a parent. We now append
+ * a new part to the current container. */
+ part = xtrycalloc (1, sizeof *part);
+ if (!part)
+ return gpg_error_from_syserror ();
+ part->partid = ++ctx->partid_counter;
+ part->headers_tail = &part->headers;
+ log_assert (!ctx->current_part->next);
+ ctx->current_part->next = part;
+ ctx->current_part = part;
+ }
+
+ /* If no NAME and no VALUE has been given we do not add a header.
+ * This can be used to create a new part without any header. */
+ if (!name && !value)
+ return 0;
+
+ /* If we add Content-Type, make sure that we have a MIME-version
+ * header first; this simply looks better. */
+ if (!ascii_strcasecmp (name, "Content-Type")
+ && !have_header (ctx->mail, "MIME-Version"))
+ {
+ err = add_header (ctx->mail, "MIME-Version", "1.0");
+ if (err)
+ return err;
+ }
+ return add_header (part, name, value);
+}
+
+
+/* Helper for mime_maker_add_{body,stream}. */
+static gpg_error_t
+add_body (mime_maker_t ctx, const void *data, size_t datalen)
+{
+ gpg_error_t err;
+ part_t part, parent;
+
+ err = ensure_part (ctx, &parent);
+ if (err)
+ return err;
+ part = ctx->current_part;
+ if (part->body)
+ return gpg_error (GPG_ERR_CONFLICT);
+
+ part->body = xtrymalloc (datalen? datalen : 1);
+ if (!part->body)
+ return gpg_error_from_syserror ();
+ part->bodylen = datalen;
+ if (data)
+ memcpy (part->body, data, datalen);
+
+ return 0;
+}
+
+
+/* Add STRING as body to the mail or the current MIME container. A
+ * second call to this function or mime_make_add_body_data is not
+ * allowed.
+ *
+ * FIXME: We may want to have an append_body to add more data to a body.
+ */
+gpg_error_t
+mime_maker_add_body (mime_maker_t ctx, const char *string)
+{
+ return add_body (ctx, string, strlen (string));
+}
+
+
+/* Add (DATA,DATALEN) as body to the mail or the current MIME
+ * container. Note that a second call to this function or to
+ * mime_make_add_body is not allowed. */
+gpg_error_t
+mime_maker_add_body_data (mime_maker_t ctx, const void *data, size_t datalen)
+{
+ return add_body (ctx, data, datalen);
+}
+
+
+/* This is the same as mime_maker_add_body but takes a stream as
+ * argument. As of now the stream is copied to the MIME object but
+ * eventually we may delay that and read the stream only at the time
+ * it is needed. Note that the address of the stream object must be
+ * passed and that the ownership of the stream is transferred to this
+ * MIME object. To indicate the latter the function will store NULL
+ * at the ADDR_STREAM so that a caller can't use that object anymore
+ * except for es_fclose which accepts a NULL pointer. */
+gpg_error_t
+mime_maker_add_stream (mime_maker_t ctx, estream_t *stream_addr)
+{
+ void *data;
+ size_t datalen;
+
+ es_rewind (*stream_addr);
+ if (es_fclose_snatch (*stream_addr, &data, &datalen))
+ return gpg_error_from_syserror ();
+ *stream_addr = NULL;
+ return add_body (ctx, data, datalen);
+}
+
+
+/* Add a new MIME container. A container can be used instead of a
+ * body. */
+gpg_error_t
+mime_maker_add_container (mime_maker_t ctx)
+{
+ gpg_error_t err;
+ part_t part;
+
+ err = ensure_part (ctx, NULL);
+ if (err)
+ return err;
+ part = ctx->current_part;
+
+ if (part->body)
+ return gpg_error (GPG_ERR_CONFLICT); /* There is already a body. */
+ if (part->child || part->boundary)
+ return gpg_error (GPG_ERR_CONFLICT); /* There is already a container. */
+
+ /* Create a child node. */
+ part->child = xtrycalloc (1, sizeof *part->child);
+ if (!part->child)
+ return gpg_error_from_syserror ();
+ part->child->headers_tail = &part->child->headers;
+
+ part->boundary = generate_boundary (ctx);
+ if (!part->boundary)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (part->child);
+ part->child = NULL;
+ return err;
+ }
+
+ part = part->child;
+ part->partid = ++ctx->partid_counter;
+ ctx->current_part = part;
+
+ return 0;
+}
+
+
+/* Finish the current container. */
+gpg_error_t
+mime_maker_end_container (mime_maker_t ctx)
+{
+ gpg_error_t err;
+ part_t parent;
+
+ err = ensure_part (ctx, &parent);
+ if (err)
+ return err;
+ if (!parent)
+ return gpg_error (GPG_ERR_CONFLICT); /* No container. */
+ while (parent->next)
+ parent = parent->next;
+ ctx->current_part = parent;
+ return 0;
+}
+
+
+/* Return the part-ID of the current part. */
+unsigned int
+mime_maker_get_partid (mime_maker_t ctx)
+{
+ if (ensure_part (ctx, NULL))
+ return 0; /* Ooops. */
+ return ctx->current_part->partid;
+}
+
+
+/* Write a header and handle emdedded LFs. If BOUNDARY is not NULL it
+ * is appended to the value. */
+/* Fixme: Add automatic line wrapping. */
+static gpg_error_t
+write_header (mime_maker_t ctx, const char *name, const char *value,
+ const char *boundary)
+{
+ const char *s;
+
+ es_fprintf (ctx->outfp, "%s: ", name);
+
+ /* Note that add_header made sure that VALUE does not end with a LF.
+ * Thus we can assume that a LF is followed by non-whitespace. */
+ for (s = value; *s; s++)
+ {
+ if (*s == '\n')
+ es_fputs ("\r\n\t", ctx->outfp);
+ else
+ es_fputc (*s, ctx->outfp);
+ }
+ if (boundary)
+ {
+ if (s > value && s[-1] != ';')
+ es_fputc (';', ctx->outfp);
+ es_fprintf (ctx->outfp, "\r\n\tboundary=\"%s\"", boundary);
+ }
+
+ es_fputs ("\r\n", ctx->outfp);
+
+ return es_ferror (ctx->outfp)? gpg_error_from_syserror () : 0;
+}
+
+
+static gpg_error_t
+write_gap (mime_maker_t ctx)
+{
+ es_fputs ("\r\n", ctx->outfp);
+ return es_ferror (ctx->outfp)? gpg_error_from_syserror () : 0;
+}
+
+
+static gpg_error_t
+write_boundary (mime_maker_t ctx, const char *boundary, int last)
+{
+ es_fprintf (ctx->outfp, "\r\n--%s%s\r\n", boundary, last?"--":"");
+ return es_ferror (ctx->outfp)? gpg_error_from_syserror () : 0;
+}
+
+
+/* Fixme: Apply required encoding. */
+static gpg_error_t
+write_body (mime_maker_t ctx, const void *body, size_t bodylen)
+{
+ const char *s;
+
+ for (s = body; bodylen; s++, bodylen--)
+ {
+ if (*s == '\n' && !(s > (const char *)body && s[-1] == '\r'))
+ es_fputc ('\r', ctx->outfp);
+ es_fputc (*s, ctx->outfp);
+ }
+
+ return es_ferror (ctx->outfp)? gpg_error_from_syserror () : 0;
+}
+
+
+/* Recursive worker for mime_maker_make. */
+static gpg_error_t
+write_tree (mime_maker_t ctx, part_t parent, part_t part)
+{
+ gpg_error_t err;
+ header_t hdr;
+
+ for (; part; part = part->next)
+ {
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ {
+ if (part->child && !strcmp (hdr->name, "Content-Type"))
+ err = write_header (ctx, hdr->name, hdr->value, part->boundary);
+ else
+ err = write_header (ctx, hdr->name, hdr->value, NULL);
+ if (err)
+ return err;
+ }
+ err = write_gap (ctx);
+ if (err)
+ return err;
+ if (part->body)
+ {
+ err = write_body (ctx, part->body, part->bodylen);
+ if (err)
+ return err;
+ }
+ if (part->child)
+ {
+ log_assert (part->boundary);
+ err = write_boundary (ctx, part->boundary, 0);
+ if (!err)
+ err = write_tree (ctx, part, part->child);
+ if (!err)
+ err = write_boundary (ctx, part->boundary, 1);
+ if (err)
+ return err;
+ }
+
+ if (part->next)
+ {
+ log_assert (parent && parent->boundary);
+ err = write_boundary (ctx, parent->boundary, 0);
+ if (err)
+ return err;
+ }
+ }
+ return 0;
+}
+
+
+/* Add headers we always require. */
+static gpg_error_t
+add_missing_headers (mime_maker_t ctx)
+{
+ gpg_error_t err;
+
+ if (!ctx->mail)
+ return gpg_error (GPG_ERR_NO_DATA);
+ if (!have_header (ctx->mail, "MIME-Version"))
+ {
+ /* Even if a Content-Type has never been set, we want to
+ * announce that we do MIME. */
+ err = add_header (ctx->mail, "MIME-Version", "1.0");
+ if (err)
+ goto leave;
+ }
+
+ if (!have_header (ctx->mail, "Date"))
+ {
+ char *p = rfctimestamp (make_timestamp ());
+ if (!p)
+ err = gpg_error_from_syserror ();
+ else
+ err = add_header (ctx->mail, "Date", p);
+ xfree (p);
+ if (err)
+ goto leave;
+ }
+
+ err = 0;
+
+ leave:
+ return err;
+}
+
+
+/* Create message from the tree MIME and write it to FP. Note that
+ * the output uses only a LF and a later called sendmail(1) is
+ * expected to convert them to network line endings. */
+gpg_error_t
+mime_maker_make (mime_maker_t ctx, estream_t fp)
+{
+ gpg_error_t err;
+
+ err = add_missing_headers (ctx);
+ if (err)
+ return err;
+
+ ctx->outfp = fp;
+ err = write_tree (ctx, NULL, ctx->mail);
+
+ ctx->outfp = NULL;
+ return err;
+}
+
+
+/* Create a stream object from the MIME part identified by PARTID and
+ * store it at R_STREAM. If PARTID identifies a container the entire
+ * tree is returned. Using that function may read stream objects
+ * which have been added as MIME bodies. The caller must close the
+ * stream object. */
+gpg_error_t
+mime_maker_get_part (mime_maker_t ctx, unsigned int partid, estream_t *r_stream)
+{
+ gpg_error_t err;
+ part_t part;
+ estream_t fp;
+
+ *r_stream = NULL;
+
+ /* When the entire tree is requested, we make sure that all missing
+ * headers are applied. We don't do that if only a part is
+ * requested because the additional headers (like Date:) will only
+ * be added to part 0 headers anyway. */
+ if (!partid)
+ {
+ err = add_missing_headers (ctx);
+ if (err)
+ return err;
+ part = ctx->mail;
+ }
+ else
+ part = find_part (ctx->mail, partid);
+
+ /* For now we use a memory stream object; however it would also be
+ * possible to create an object created on the fly while the caller
+ * is reading the returned stream. */
+ fp = es_fopenmem (0, "w+b");
+ if (!fp)
+ return gpg_error_from_syserror ();
+
+ ctx->outfp = fp;
+ err = write_tree (ctx, NULL, part);
+ ctx->outfp = NULL;
+
+ if (!err)
+ {
+ es_rewind (fp);
+ *r_stream = fp;
+ }
+ else
+ es_fclose (fp);
+
+ return err;
+}