/* mime-parser.c - Parse MIME structures (high level rfc822 parser).
 * 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 "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 the transition from header to body.  */
  gpg_error_t (*t2body) (void *cookie, int level);

  /* 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);

  /* The RFC822 parser context is stored here during callbacks.  */
  rfc822parse_t msg;

  /* 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 function assumes 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;

  /* Make the RFC822 parser context available for callbacks.  */
  ctx->msg = msg;

  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;

      if (ctx->t2body)
        {
          rc = ctx->t2body (ctx->cookie, ctx->nesting_level);
          if (rc)
            goto t2body_leave;
        }

      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.  */
            }
        }

    t2body_leave:
      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;
        }
    }

  ctx->msg = NULL;

  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 a callback for the transition from header to body.  LEVEL is
 * the current nesting level, starting with 0.  This callback can be
 * used to evaluate headers before any other action is done.  Note
 * that if a new NEW_PART callback needs to be called it is done after
 * this T2BODY callback.  */
void
mime_parser_set_t2body (mime_parser_t ctx,
                        gpg_error_t (*fnc) (void *cookie, int level))
{
  ctx->t2body = fnc;
}


/* 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;
}


/* Return the RFC888 parser context.  This is only available inside a
 * callback.  */
rfc822parse_t
mime_parser_rfc822parser (mime_parser_t ctx)
{
  return ctx->msg;
}


/* Helper for mime_parser_parse.  */
static gpg_error_t
process_part_data (mime_parser_t ctx, char *line, size_t *length)
{
  gpg_error_t err;
  size_t nbytes;

  if (!ctx->want_part)
    return 0;
  if (!ctx->part_data)
    return 0;

  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)
        return err;
      *length = nbytes;
    }

  return ctx->part_data (ctx->cookie, line, *length);
}


/* 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;
  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;

          err = process_part_data (ctx, line, &length);
          if (err)
            goto leave;
        }
      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
        {
          err = process_part_data (ctx, line, &length);
          if (err)
            goto leave;
        }
    }

  rfc822parse_close (msg);
  msg = NULL;
  err = 0;

 leave:
  rfc822parse_cancel (msg);
  return err;
}
