summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWerner Koch <wk@gnupg.org>2016-06-29 11:02:36 +0200
committerWerner Koch <wk@gnupg.org>2016-06-29 12:04:11 +0200
commitc334fa8df0e3901857e1a277d3277a873ae4af74 (patch)
tree9845a54453dc8d3746ac5ea29e6b5660fc1a3b20
parentgpgscm: Fix memory leaks. (diff)
downloadgnupg2-c334fa8df0e3901857e1a277d3277a873ae4af74.tar.xz
gnupg2-c334fa8df0e3901857e1a277d3277a873ae4af74.zip
tools: Add modules for MIME parsing and creating.
* tools/mime-maker.c: New. * tools/mime-maker.h: New. * tools/mime-parser.c: New. * tools/mime-parser.h: New. Signed-off-by: Werner Koch <wk@gnupg.org>
-rw-r--r--tools/mime-maker.c624
-rw-r--r--tools/mime-maker.h43
-rw-r--r--tools/mime-parser.c772
-rw-r--r--tools/mime-parser.h52
-rw-r--r--tools/rfc822parse.h2
5 files changed, 1492 insertions, 1 deletions
diff --git a/tools/mime-maker.c b/tools/mime-maker.c
new file mode 100644
index 000000000..88f9d5fb8
--- /dev/null
+++ b/tools/mime-maker.c
@@ -0,0 +1,624 @@
+/* mime-maker.c - Create MIME structures
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+#include "zb32.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 *mediatype; /* Mediatype of the container (malloced). */
+ 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. */
+};
+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;
+
+ 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->mediatype);
+ 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]\n", level*2, "");
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ {
+ log_debug ("%*s%s: %s\n", level*2, "", hdr->name, hdr->value);
+ }
+ 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;
+}
+
+
+/* Create a boundary string. Outr codes is aware of the general
+ * structure of that string (gebins with "=-=") so that
+ * it can protect against accidently 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)
+ 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;
+}
+
+
+/* Transform a header name into a standard capitalized format.
+ * "Content-Type". Conversion stops at the colon. */
+static void
+capitalize_header_name (char *name)
+{
+ unsigned char *p = name;
+ int first = 1;
+
+ /* Special cases first. */
+ if (!ascii_strcasecmp (name, "MIME-Version"))
+ {
+ strcpy (name, "MIME-Version");
+ return;
+ }
+
+ /* Regular cases. */
+ for (; *p && *p != ':'; p++)
+ {
+ if (*p == '-')
+ first = 1;
+ else if (first)
+ {
+ if (*p >= 'a' && *p <= 'z')
+ *p = *p - 'a' + 'A';
+ first = 0;
+ }
+ else if (*p >= 'A' && *p <= 'Z')
+ *p = *p - 'A' + 'a';
+ }
+}
+
+
+/* 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;
+
+ hdr = xtrymalloc (sizeof *hdr + strlen (name));
+ if (!hdr)
+ return gpg_error_from_syserror ();
+ hdr->next = NULL;
+ strcpy (hdr->name, name);
+ capitalize_header_name (hdr->name);
+ hdr->value = xtrystrdup (value);
+ if (!hdr->value)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (hdr);
+ return err;
+ }
+ *part->headers_tail = hdr;
+ part->headers_tail = &hdr->next;
+
+ return 0;
+}
+
+
+/* Add a header with NAME and VALUE to the current mail. A LF in the
+ * VALUE will be handled automagically. 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;
+
+ err = ensure_part (ctx, &parent);
+ if (err)
+ return err;
+ part = ctx->current_part;
+
+ if (part->body && !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)
+ {
+ /* 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->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 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));
+}
+
+
+/* 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. The caller needs to provide the media
+ * and media-subtype in MEDIATYPE. If MEDIATYPE is NULL
+ * "multipart/mixed" is assumed. This function will then add a
+ * Content-Type header with that media type and an approriate boundary
+ * string to the parent part. */
+gpg_error_t
+mime_maker_add_container (mime_maker_t ctx, const char *mediatype)
+{
+ gpg_error_t err;
+ part_t part;
+
+ if (!mediatype)
+ mediatype = "multipart/mixed";
+
+ 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->mediatype || part->boundary)
+ return gpg_error (GPG_ERR_CONFLICT); /* There is already a container. */
+
+ /* If a content type has not yet been set, do it now. The boundary
+ * will be added while writing the headers. */
+ if (!have_header (ctx->mail, "Content-Type"))
+ {
+ err = add_header (ctx->mail, "Content-Type", mediatype);
+ if (err)
+ return err;
+ }
+
+ /* 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->mediatype = xtrystrdup (mediatype);
+ if (!part->mediatype)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (part->child);
+ part->child = NULL;
+ return err;
+ }
+
+ part->boundary = generate_boundary (ctx);
+ if (!part->boundary)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (part->child);
+ part->child = NULL;
+ xfree (part->mediatype);
+ part->mediatype = NULL;
+ return err;
+ }
+
+ part = part->child;
+ ctx->current_part = part;
+
+ return 0;
+}
+
+
+/* Write the Content-Type header with the boundary value. */
+static gpg_error_t
+write_ct_with_boundary (mime_maker_t ctx,
+ const char *value, const char *boundary)
+{
+ const char *s;
+
+ if (!*value)
+ return gpg_error (GPG_ERR_INV_VALUE); /* Empty string. */
+
+ for (s=value + strlen (value) - 1;
+ (s >= value
+ && (*s == ' ' || *s == '\t' || *s == '\n'));
+ s--)
+ ;
+ if (!(s >= value))
+ return gpg_error (GPG_ERR_INV_VALUE); /* Only spaces. */
+
+ /* Fixme: We should use a dedicated header write functions which
+ * properly wraps the header. */
+ es_fprintf (ctx->outfp, "Content-Type: %s%s\n\tboundary=\"%s\"\n",
+ value,
+ (*s == ';')? "":";",
+ boundary);
+ return 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"))
+ write_ct_with_boundary (ctx, hdr->value, part->boundary);
+ else
+ es_fprintf (ctx->outfp, "%s: %s\n", hdr->name, hdr->value);
+ }
+ es_fputc ('\n', ctx->outfp);
+ if (part->body)
+ {
+ if (es_write (ctx->outfp, part->body, part->bodylen, NULL))
+ return gpg_error_from_syserror ();
+ }
+ if (part->child)
+ {
+ log_assert (part->boundary);
+ if (es_fprintf (ctx->outfp, "\n--%s\n", part->boundary) < 0)
+ return gpg_error_from_syserror ();
+ err = write_tree (ctx, part, part->child);
+ if (err)
+ return err;
+ if (es_fprintf (ctx->outfp, "\n--%s--\n", part->boundary) < 0)
+ return gpg_error_from_syserror ();
+ }
+
+ if (part->next)
+ {
+ log_assert (parent && parent->boundary);
+ if (es_fprintf (ctx->outfp, "\n--%s\n", parent->boundary) < 0)
+ return gpg_error_from_syserror ();
+ }
+ }
+ 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;
+ }
+
+
+ leave:
+ return err;
+}
+
+
+/* Create message from the tree MIME and write it to FP. Noet 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;
+}
diff --git a/tools/mime-maker.h b/tools/mime-maker.h
new file mode 100644
index 000000000..b21f7dd3d
--- /dev/null
+++ b/tools/mime-maker.h
@@ -0,0 +1,43 @@
+/* mime-maker.h - Create MIME structures
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GNUPG_MIME_MAKER_H
+#define GNUPG_MIME_MAKER_H
+
+struct mime_maker_context_s;
+typedef struct mime_maker_context_s *mime_maker_t;
+
+gpg_error_t mime_maker_new (mime_maker_t *r_ctx, void *cookie);
+void mime_maker_release (mime_maker_t ctx);
+
+void mime_maker_set_verbose (mime_maker_t ctx, int level);
+
+void mime_maker_dump_tree (mime_maker_t ctx);
+
+gpg_error_t mime_maker_add_header (mime_maker_t ctx,
+ const char *name, const char *value);
+gpg_error_t mime_maker_add_body (mime_maker_t ctx, const char *string);
+gpg_error_t mime_maker_add_stream (mime_maker_t ctx, estream_t *stream_addr);
+gpg_error_t mime_maker_add_container (mime_maker_t ctx, const char *mediatype);
+
+gpg_error_t mime_maker_make (mime_maker_t ctx, estream_t fp);
+
+
+
+#endif /*GNUPG_MIME_MAKER_H*/
diff --git a/tools/mime-parser.c b/tools/mime-parser.c
new file mode 100644
index 000000000..5f3659ee5
--- /dev/null
+++ b/tools/mime-parser.c
@@ -0,0 +1,772 @@
+/* mime-parser.c - Parse MIME structures (high level rfc822 parser).
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+#include "rfc822parse.h"
+#include "mime-parser.h"
+
+
+enum pgpmime_states
+ {
+ PGPMIME_NONE = 0,
+ PGPMIME_WAIT_ENCVERSION,
+ PGPMIME_IN_ENCVERSION,
+ PGPMIME_WAIT_ENCDATA,
+ PGPMIME_IN_ENCDATA,
+ PGPMIME_GOT_ENCDATA,
+ PGPMIME_WAIT_SIGNEDDATA,
+ PGPMIME_IN_SIGNEDDATA,
+ PGPMIME_WAIT_SIGNATURE,
+ PGPMIME_IN_SIGNATURE,
+ PGPMIME_GOT_SIGNATURE,
+ PGPMIME_INVALID
+ };
+
+
+/* Definition of the mime parser object. */
+struct mime_parser_context_s
+{
+ void *cookie; /* Cookie passed to all callbacks. */
+
+ /* The callback to announce a new part. */
+ gpg_error_t (*new_part) (void *cookie,
+ const char *mediatype,
+ const char *mediasubtype);
+ /* The callback to return data of a part. */
+ gpg_error_t (*part_data) (void *cookie,
+ const void *data,
+ size_t datalen);
+ /* The callback to collect encrypted data. */
+ gpg_error_t (*collect_encrypted) (void *cookie, const char *data);
+ /* The callback to collect signed data. */
+ gpg_error_t (*collect_signeddata) (void *cookie, const char *data);
+ /* The callback to collect a signature. */
+ gpg_error_t (*collect_signature) (void *cookie, const char *data);
+
+ /* Helper to convey error codes from user callbacks. */
+ gpg_error_t err;
+
+ int nesting_level; /* The current nesting level. */
+ int hashing_at_level; /* The nesting level at which we are hashing. */
+ enum pgpmime_states pgpmime; /* Current PGP/MIME state. */
+ unsigned int delay_hashing:1;/* Helper for PGPMIME_IN_SIGNEDDATA. */
+ unsigned int want_part:1; /* Return the current part. */
+ unsigned int decode_part:2; /* Decode the part. 1 = QP, 2 = Base64. */
+
+ unsigned int verbose:1; /* Enable verbose mode. */
+ unsigned int debug:1; /* Enable debug mode. */
+
+ /* Flags to help with debug output. */
+ struct {
+ unsigned int n_skip; /* Skip showing these number of lines. */
+ unsigned int header:1; /* Show the header lines. */
+ unsigned int data:1; /* Show the data lines. */
+ unsigned int as_note:1; /* Show the next data line as a note. */
+ unsigned int boundary : 1;
+ } show;
+
+ struct b64state *b64state; /* NULL or malloced Base64 decoder state. */
+
+ /* A buffer for reading a mail line, */
+ char line[5000];
+};
+
+
+/* Print the event received by the parser for debugging. */
+static void
+show_message_parser_event (rfc822parse_event_t event)
+{
+ const char *s;
+
+ switch (event)
+ {
+ case RFC822PARSE_OPEN: s= "Open"; break;
+ case RFC822PARSE_CLOSE: s= "Close"; break;
+ case RFC822PARSE_CANCEL: s= "Cancel"; break;
+ case RFC822PARSE_T2BODY: s= "T2Body"; break;
+ case RFC822PARSE_FINISH: s= "Finish"; break;
+ case RFC822PARSE_RCVD_SEEN: s= "Rcvd_Seen"; break;
+ case RFC822PARSE_LEVEL_DOWN: s= "Level_Down"; break;
+ case RFC822PARSE_LEVEL_UP: s= "Level_Up"; break;
+ case RFC822PARSE_BOUNDARY: s= "Boundary"; break;
+ case RFC822PARSE_LAST_BOUNDARY: s= "Last_Boundary"; break;
+ case RFC822PARSE_BEGIN_HEADER: s= "Begin_Header"; break;
+ case RFC822PARSE_PREAMBLE: s= "Preamble"; break;
+ case RFC822PARSE_EPILOGUE: s= "Epilogue"; break;
+ default: s= "[unknown event]"; break;
+ }
+ log_debug ("*** RFC822 event %s\n", s);
+}
+
+
+/* Do in-place decoding of quoted-printable data of LENGTH in BUFFER.
+ Returns the new length of the buffer and stores true at R_SLBRK if
+ the line ended with a soft line break; false is stored if not.
+ This fucntion asssumes that a complete line is passed in
+ buffer. */
+static size_t
+qp_decode (char *buffer, size_t length, int *r_slbrk)
+{
+ char *d, *s;
+
+ if (r_slbrk)
+ *r_slbrk = 0;
+
+ /* Fixme: We should remove trailing white space first. */
+ for (s=d=buffer; length; length--)
+ {
+ if (*s == '=')
+ {
+ if (length > 2 && hexdigitp (s+1) && hexdigitp (s+2))
+ {
+ s++;
+ *(unsigned char*)d++ = xtoi_2 (s);
+ s += 2;
+ length -= 2;
+ }
+ else if (length > 2 && s[1] == '\r' && s[2] == '\n')
+ {
+ /* Soft line break. */
+ s += 3;
+ length -= 2;
+ if (r_slbrk && length == 1)
+ *r_slbrk = 1;
+ }
+ else if (length > 1 && s[1] == '\n')
+ {
+ /* Soft line break with only a Unix line terminator. */
+ s += 2;
+ length -= 1;
+ if (r_slbrk && length == 1)
+ *r_slbrk = 1;
+ }
+ else if (length == 1)
+ {
+ /* Soft line break at the end of the line. */
+ s += 1;
+ if (r_slbrk)
+ *r_slbrk = 1;
+ }
+ else
+ *d++ = *s++;
+ }
+ else
+ *d++ = *s++;
+ }
+
+ return d - buffer;
+}
+
+
+/* This function is called by parse_mail to communicate events. This
+ * callback communicates with the caller using a structure passed in
+ * OPAQUE. Should return 0 on success or set ERRNO and return -1. */
+static int
+parse_message_cb (void *opaque, rfc822parse_event_t event, rfc822parse_t msg)
+{
+ mime_parser_t ctx = opaque;
+ const char *s;
+ int rc = 0;
+
+ if (ctx->debug)
+ show_message_parser_event (event);
+
+ if (event == RFC822PARSE_BEGIN_HEADER || event == RFC822PARSE_T2BODY)
+ {
+ /* We need to check here whether to start collecting signed data
+ * because attachments might come without header lines and thus
+ * we won't see the BEGIN_HEADER event. */
+ if (ctx->pgpmime == PGPMIME_WAIT_SIGNEDDATA)
+ {
+ if (ctx->debug)
+ log_debug ("begin_hash\n");
+ ctx->hashing_at_level = ctx->nesting_level;
+ ctx->pgpmime = PGPMIME_IN_SIGNEDDATA;
+ ctx->delay_hashing = 0;
+ }
+ }
+
+ if (event == RFC822PARSE_OPEN)
+ {
+ /* Initialize for a new message. */
+ ctx->show.header = 1;
+ }
+ else if (event == RFC822PARSE_T2BODY)
+ {
+ rfc822parse_field_t field;
+
+ ctx->want_part = 0;
+ ctx->decode_part = 0;
+ field = rfc822parse_parse_field (msg, "Content-Type", -1);
+ if (field)
+ {
+ const char *s1, *s2;
+
+ s1 = rfc822parse_query_media_type (field, &s2);
+ if (s1)
+ {
+ if (ctx->verbose)
+ log_debug ("h media: %*s%s %s\n",
+ ctx->nesting_level*2, "", s1, s2);
+ if (ctx->pgpmime == PGPMIME_WAIT_ENCVERSION)
+ {
+ if (!strcmp (s1, "application")
+ && !strcmp (s2, "pgp-encrypted"))
+ {
+ if (ctx->debug)
+ log_debug ("c begin_encversion\n");
+ ctx->pgpmime = PGPMIME_IN_ENCVERSION;
+ }
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " expected '%s', got '%s/%s'\n",
+ "application/pgp-encrypted", s1, s2);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_WAIT_ENCDATA)
+ {
+ if (!strcmp (s1, "application")
+ && !strcmp (s2, "octet-stream"))
+ {
+ if (ctx->debug)
+ log_debug ("c begin_encdata\n");
+ ctx->pgpmime = PGPMIME_IN_ENCDATA;
+ }
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " expected '%s', got '%s/%s'\n",
+ "application/octet-stream", s1, s2);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_WAIT_SIGNATURE)
+ {
+ if (!strcmp (s1, "application")
+ && !strcmp (s2, "pgp-signature"))
+ {
+ if (ctx->debug)
+ log_debug ("c begin_signature\n");
+ ctx->pgpmime = PGPMIME_IN_SIGNATURE;
+ }
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " expected '%s', got '%s/%s'\n",
+ "application/pgp-signature", s1, s2);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (!strcmp (s1, "multipart")
+ && !strcmp (s2, "encrypted"))
+ {
+ s = rfc822parse_query_parameter (field, "protocol", 0);
+ if (s)
+ {
+ if (ctx->debug)
+ log_debug ("h encrypted.protocol: %s\n", s);
+ if (!strcmp (s, "application/pgp-encrypted"))
+ {
+ if (ctx->pgpmime)
+ log_error ("note: "
+ "ignoring nested PGP/MIME signature\n");
+ else
+ ctx->pgpmime = PGPMIME_WAIT_ENCVERSION;
+ }
+ else if (ctx->verbose)
+ log_debug ("# this protocol is not supported\n");
+ }
+ }
+ else if (!strcmp (s1, "multipart")
+ && !strcmp (s2, "signed"))
+ {
+ s = rfc822parse_query_parameter (field, "protocol", 1);
+ if (s)
+ {
+ if (ctx->debug)
+ log_debug ("h signed.protocol: %s\n", s);
+ if (!strcmp (s, "application/pgp-signature"))
+ {
+ if (ctx->pgpmime)
+ log_error ("note: "
+ "ignoring nested PGP/MIME signature\n");
+ else
+ ctx->pgpmime = PGPMIME_WAIT_SIGNEDDATA;
+ }
+ else if (ctx->verbose)
+ log_debug ("# this protocol is not supported\n");
+ }
+ }
+ else if (ctx->new_part)
+ {
+ ctx->err = ctx->new_part (ctx->cookie, s1, s2);
+ if (!ctx->err)
+ ctx->want_part = 1;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_FALSE)
+ ctx->err = 0;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_TRUE)
+ {
+ ctx->want_part = ctx->decode_part = 1;
+ ctx->err = 0;
+ }
+ }
+ }
+ else
+ {
+ if (ctx->debug)
+ log_debug ("h media: %*s none\n", ctx->nesting_level*2, "");
+ if (ctx->new_part)
+ {
+ ctx->err = ctx->new_part (ctx->cookie, "", "");
+ if (!ctx->err)
+ ctx->want_part = 1;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_FALSE)
+ ctx->err = 0;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_TRUE)
+ {
+ ctx->want_part = ctx->decode_part = 1;
+ ctx->err = 0;
+ }
+ }
+ }
+
+ rfc822parse_release_field (field);
+ }
+ else
+ {
+ if (ctx->verbose)
+ log_debug ("h media: %*stext plain [assumed]\n",
+ ctx->nesting_level*2, "");
+ if (ctx->new_part)
+ {
+ ctx->err = ctx->new_part (ctx->cookie, "text", "plain");
+ if (!ctx->err)
+ ctx->want_part = 1;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_FALSE)
+ ctx->err = 0;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_TRUE)
+ {
+ ctx->want_part = ctx->decode_part = 1;
+ ctx->err = 0;
+ }
+ }
+ }
+
+ /* Figure out the encoding if needed. */
+ if (ctx->decode_part)
+ {
+ char *value;
+ size_t valueoff;
+
+ ctx->decode_part = 0; /* Fallback for unknown encoding. */
+ value = rfc822parse_get_field (msg, "Content-Transfer-Encoding", -1,
+ &valueoff);
+ if (value)
+ {
+ if (!stricmp (value+valueoff, "quoted-printable"))
+ ctx->decode_part = 1;
+ else if (!stricmp (value+valueoff, "base64"))
+ {
+ ctx->decode_part = 2;
+ if (ctx->b64state)
+ b64dec_finish (ctx->b64state); /* Reuse state. */
+ else
+ {
+ ctx->b64state = xtrymalloc (sizeof *ctx->b64state);
+ if (!ctx->b64state)
+ rc = gpg_error_from_syserror ();
+ }
+ if (!rc)
+ rc = b64dec_start (ctx->b64state, NULL);
+ }
+ free (value); /* Right, we need a plain free. */
+ }
+ }
+
+ ctx->show.header = 0;
+ ctx->show.data = 1;
+ ctx->show.n_skip = 1;
+ }
+ else if (event == RFC822PARSE_PREAMBLE)
+ ctx->show.as_note = 1;
+ else if (event == RFC822PARSE_LEVEL_DOWN)
+ {
+ if (ctx->debug)
+ log_debug ("b down\n");
+ ctx->nesting_level++;
+ }
+ else if (event == RFC822PARSE_LEVEL_UP)
+ {
+ if (ctx->debug)
+ log_debug ("b up\n");
+ if (ctx->nesting_level)
+ ctx->nesting_level--;
+ else
+ log_error ("invalid structure (bad nesting level)\n");
+ }
+ else if (event == RFC822PARSE_BOUNDARY || event == RFC822PARSE_LAST_BOUNDARY)
+ {
+ ctx->show.data = 0;
+ ctx->show.boundary = 1;
+ if (event == RFC822PARSE_BOUNDARY)
+ {
+ ctx->show.header = 1;
+ ctx->show.n_skip = 1;
+ if (ctx->debug)
+ log_debug ("b part\n");
+ }
+ else if (ctx->debug)
+ log_debug ("b last\n");
+
+ if (ctx->pgpmime == PGPMIME_IN_ENCDATA)
+ {
+ if (ctx->debug)
+ log_debug ("c end_encdata\n");
+ ctx->pgpmime = PGPMIME_GOT_ENCDATA;
+ /* FIXME: We should assert (event == LAST_BOUNDARY). */
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNEDDATA
+ && ctx->nesting_level == ctx->hashing_at_level)
+ {
+ if (ctx->debug)
+ log_debug ("c end_hash\n");
+ ctx->pgpmime = PGPMIME_WAIT_SIGNATURE;
+ if (ctx->collect_signeddata)
+ ctx->err = ctx->collect_signeddata (ctx->cookie, NULL);
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNATURE)
+ {
+ if (ctx->debug)
+ log_debug ("c end_signature\n");
+ ctx->pgpmime = PGPMIME_GOT_SIGNATURE;
+ /* FIXME: We should assert (event == LAST_BOUNDARY). */
+ }
+ else if (ctx->want_part)
+ {
+ if (ctx->part_data)
+ {
+ /* FIXME: We may need to flush things. */
+ ctx->err = ctx->part_data (ctx->cookie, NULL, 0);
+ }
+ ctx->want_part = 0;
+ }
+ }
+
+ return rc;
+}
+
+
+/* Create a new mime parser object. COOKIE is a values which will be
+ * used as first argument for all callbacks registered with this
+ * parser object. */
+gpg_error_t
+mime_parser_new (mime_parser_t *r_parser, void *cookie)
+{
+ mime_parser_t ctx;
+
+ *r_parser = NULL;
+
+ ctx = xtrycalloc (1, sizeof *ctx);
+ if (!ctx)
+ return gpg_error_from_syserror ();
+ ctx->cookie = cookie;
+
+ *r_parser = ctx;
+ return 0;
+}
+
+
+/* Release a mime parser object. */
+void
+mime_parser_release (mime_parser_t ctx)
+{
+ if (!ctx)
+ return;
+
+ if (ctx->b64state)
+ {
+ b64dec_finish (ctx->b64state);
+ xfree (ctx->b64state);
+ }
+ xfree (ctx);
+}
+
+
+/* Set verbose and debug mode. */
+void
+mime_parser_set_verbose (mime_parser_t ctx, int level)
+{
+ if (!level)
+ {
+ ctx->verbose = 0;
+ ctx->debug = 0;
+ }
+ else
+ {
+ ctx->verbose = 1;
+ if (level > 10)
+ ctx->debug = 1;
+ }
+}
+
+
+/* Set the callback used to announce a new part. It will be called
+ * with the media type and media subtype of the part. If no
+ * Content-type header was given both values are the empty string.
+ * The callback should return 0 on success or an error code. The
+ * error code GPG_ERR_FALSE indicates that the caller is not
+ * interested in the part and data shall not be returned via a
+ * registered part_data callback. The error code GPG_ERR_TRUE
+ * indicates that the parts shall be redurned in decoded format
+ * (i.e. base64 or QP encoding is removed). */
+void
+mime_parser_set_new_part (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *mediatype,
+ const char *mediasubtype))
+{
+ ctx->new_part = fnc;
+}
+
+
+/* Set the callback used to return the data of a part to the caller.
+ * The end of the part is indicated by passing NUL for DATA. */
+void
+mime_parser_set_part_data (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const void *data,
+ size_t datalen))
+{
+ ctx->part_data = fnc;
+}
+
+
+/* Set the callback to collect encrypted data. A NULL passed to the
+ * callback indicates the end of the encrypted data; the callback may
+ * then decrypt the collected data. */
+void
+mime_parser_set_collect_encrypted (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data))
+{
+ ctx->collect_encrypted = fnc;
+}
+
+
+/* Set the callback to collect signed data. A NULL passed to the
+ * callback indicates the end of the signed data. */
+void
+mime_parser_set_collect_signeddata (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data))
+{
+ ctx->collect_signeddata = fnc;
+}
+
+
+/* Set the callback to collect the signature. A NULL passed to the
+ * callback indicates the end of the signature; the callback may the
+ * verify the signature. */
+void
+mime_parser_set_collect_signature (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data))
+{
+ ctx->collect_signature = fnc;
+}
+
+
+/* Read and parse a message from FP and call the appropriate
+ * callbacks. */
+gpg_error_t
+mime_parser_parse (mime_parser_t ctx, estream_t fp)
+{
+ gpg_error_t err;
+ rfc822parse_t msg = NULL;
+ unsigned int lineno = 0;
+ size_t length, nbytes;
+ char *line;
+
+ line = ctx->line;
+
+ msg = rfc822parse_open (parse_message_cb, ctx);
+ if (!msg)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("can't open mail parser: %s", gpg_strerror (err));
+ goto leave;
+ }
+
+ /* Fixme: We should not use fgets because it can't cope with
+ embedded nul characters. */
+ while (es_fgets (ctx->line, sizeof (ctx->line), fp))
+ {
+ lineno++;
+ if (lineno == 1 && !strncmp (line, "From ", 5))
+ continue; /* We better ignore a leading From line. */
+
+ length = strlen (line);
+ if (length && line[length - 1] == '\n')
+ line[--length] = 0;
+ else
+ log_error ("mail parser detected too long or"
+ " non terminated last line (lnr=%u)\n", lineno);
+ if (length && line[length - 1] == '\r')
+ line[--length] = 0;
+
+ ctx->err = 0;
+ if (rfc822parse_insert (msg, line, length))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("mail parser failed: %s", gpg_strerror (err));
+ goto leave;
+ }
+ if (ctx->err)
+ {
+ /* Error from a callback detected. */
+ err = ctx->err;
+ goto leave;
+ }
+
+
+ /* Debug output. Note that the boundary is shown before n_skip
+ * is evaluated. */
+ if (ctx->show.boundary)
+ {
+ if (ctx->debug)
+ log_debug ("# Boundary: %s\n", line);
+ ctx->show.boundary = 0;
+ }
+ if (ctx->show.n_skip)
+ ctx->show.n_skip--;
+ else if (ctx->show.data)
+ {
+ if (ctx->show.as_note)
+ {
+ if (ctx->verbose)
+ log_debug ("# Note: %s\n", line);
+ ctx->show.as_note = 0;
+ }
+ else if (ctx->debug)
+ log_debug ("# Data: %s\n", line);
+ }
+ else if (ctx->show.header && ctx->verbose)
+ log_debug ("# Header: %s\n", line);
+
+ if (ctx->pgpmime == PGPMIME_IN_ENCVERSION)
+ {
+ trim_trailing_spaces (line);
+ if (!*line)
+ ; /* Skip empty lines. */
+ else if (!strcmp (line, "Version: 1"))
+ ctx->pgpmime = PGPMIME_WAIT_ENCDATA;
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " garbage in pgp-encrypted part ('%s')\n", line);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_ENCDATA)
+ {
+ if (ctx->collect_encrypted)
+ {
+ err = ctx->collect_encrypted (ctx->cookie, line);
+ if (!err)
+ err = ctx->collect_encrypted (ctx->cookie, "\r\n");
+ if (err)
+ goto leave;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_GOT_ENCDATA)
+ {
+ ctx->pgpmime = PGPMIME_NONE;
+ if (ctx->collect_encrypted)
+ ctx->collect_encrypted (ctx->cookie, NULL);
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNEDDATA)
+ {
+ /* If we are processing signed data, store the signed data.
+ * We need to delay the hashing of the CR/LF because the
+ * last line ending belongs to the next boundary. This is
+ * the reason why we can't use the PGPMIME state as a
+ * condition. */
+ if (ctx->debug)
+ log_debug ("# hashing %s'%s'\n",
+ ctx->delay_hashing? "CR,LF+":"", line);
+ if (ctx->collect_signeddata)
+ {
+ if (ctx->delay_hashing)
+ ctx->collect_signeddata (ctx->cookie, "\r\n");
+ ctx->collect_signeddata (ctx->cookie, line);
+ }
+ ctx->delay_hashing = 1;
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNATURE)
+ {
+ if (ctx->collect_signeddata)
+ {
+ ctx->collect_signature (ctx->cookie, line);
+ ctx->collect_signature (ctx->cookie, "\r\n");
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_GOT_SIGNATURE)
+ {
+ ctx->pgpmime = PGPMIME_NONE;
+ if (ctx->collect_signeddata)
+ ctx->collect_signature (ctx->cookie, NULL);
+ }
+ else if (ctx->want_part)
+ {
+ if (ctx->part_data)
+ {
+ if (ctx->decode_part == 1)
+ {
+ length = qp_decode (line, length, NULL);
+ }
+ else if (ctx->decode_part == 2)
+ {
+ log_assert (ctx->b64state);
+ err = b64dec_proc (ctx->b64state, line, length, &nbytes);
+ if (err)
+ goto leave;
+ length = nbytes;
+ }
+ err = ctx->part_data (ctx->cookie, line, length);
+ if (err)
+ goto leave;
+ }
+ }
+ }
+
+ rfc822parse_close (msg);
+ msg = NULL;
+ err = 0;
+
+ leave:
+ rfc822parse_cancel (msg);
+ return err;
+}
diff --git a/tools/mime-parser.h b/tools/mime-parser.h
new file mode 100644
index 000000000..ab0d79288
--- /dev/null
+++ b/tools/mime-parser.h
@@ -0,0 +1,52 @@
+/* mime-parser.h - Parse MIME structures (high level rfc822 parser).
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GNUPG_MIME_PARSER_H
+#define GNUPG_MIME_PARSER_H
+
+struct mime_parser_context_s;
+typedef struct mime_parser_context_s *mime_parser_t;
+
+gpg_error_t mime_parser_new (mime_parser_t *r_ctx, void *cookie);
+void mime_parser_release (mime_parser_t ctx);
+
+void mime_parser_set_verbose (mime_parser_t ctx, int level);
+void mime_parser_set_new_part (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *mediatype,
+ const char *mediasubtype));
+void mime_parser_set_part_data (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const void *data,
+ size_t datalen));
+void mime_parser_set_collect_encrypted (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data));
+void mime_parser_set_collect_signeddata (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data));
+void mime_parser_set_collect_signature (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data));
+
+gpg_error_t mime_parser_parse (mime_parser_t ctx, estream_t fp);
+
+
+
+#endif /*GNUPG_MIME_PARSER_H*/
diff --git a/tools/rfc822parse.h b/tools/rfc822parse.h
index 8bb5536a1..c5579fe44 100644
--- a/tools/rfc822parse.h
+++ b/tools/rfc822parse.h
@@ -1,6 +1,6 @@
/* rfc822parse.h - Simple mail and MIME parser
* Copyright (C) 1999 Werner Koch, Duesseldorf
- * Copyright (C) 2003, g10 Code GmbH
+ * Copyright (C) 2003 g10 Code GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License