예제 #1
0
  protected void parse(InputStream in) throws IOException, MessagingException {
    mHeader.clear();
    mFrom = null;
    mTo = null;
    mCc = null;
    mBcc = null;
    mReplyTo = null;

    mMessageId = null;
    mReferences = null;
    mInReplyTo = null;

    mSentDate = null;

    mBody = null;

    MimeEntityConfig parserConfig = new MimeEntityConfig();
    parserConfig.setMaxHeaderLen(-1); // The default is a mere 10k
    parserConfig.setMaxLineLen(-1); // The default is 1000 characters. Some MUAs generate
    // REALLY long References: headers
    MimeStreamParser parser = new MimeStreamParser(parserConfig);
    parser.setContentHandler(new MimeMessageBuilder());
    try {
      parser.parse(new EOLConvertingInputStream(in));
    } catch (MimeException me) {
      throw new Error(me);
    }
  }
  @NonNull
  @Override
  public InputDataResult execute(InputDataParcel input, final CryptoInputParcel cryptoInput) {

    final OperationLog log = new OperationLog();

    log.add(LogType.MSG_DATA, 0);

    Uri currentInputUri;

    DecryptVerifyResult decryptResult = null;

    PgpDecryptVerifyInputParcel decryptInput = input.getDecryptInput();

    if (!input.getMimeDecode() && decryptInput == null) {
      throw new AssertionError("no decryption or mime decoding, this is probably a bug");
    }

    if (decryptInput != null) {

      log.add(LogType.MSG_DATA_OPENPGP, 1);

      PgpDecryptVerifyOperation op =
          new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);

      decryptInput.setInputUri(input.getInputUri());

      currentInputUri = TemporaryFileProvider.createFile(mContext);
      decryptInput.setOutputUri(currentInputUri);

      decryptResult = op.execute(decryptInput, cryptoInput);
      if (decryptResult.isPending()) {
        return new InputDataResult(log, decryptResult);
      }
      log.addByMerge(decryptResult, 1);

      if (!decryptResult.success()) {
        return new InputDataResult(InputDataResult.RESULT_ERROR, log);
      }

      // inform the storage provider about the mime type for this uri
      if (decryptResult.getDecryptionMetadata() != null) {
        OpenPgpMetadata meta = decryptResult.getDecryptionMetadata();
        TemporaryFileProvider.setName(mContext, currentInputUri, meta.getFilename());
        TemporaryFileProvider.setMimeType(mContext, currentInputUri, meta.getMimeType());
      }

    } else {
      currentInputUri = input.getInputUri();
    }

    // don't even attempt if we know the data isn't suitable for mime content, or if we have a
    // filename
    boolean skipMimeParsing = false;
    if (decryptResult != null && decryptResult.getDecryptionMetadata() != null) {
      OpenPgpMetadata metadata = decryptResult.getDecryptionMetadata();
      String fileName = metadata.getFilename();
      String contentType = metadata.getMimeType();
      if (!TextUtils.isEmpty(fileName)
          || contentType != null
              && !contentType.startsWith("multipart/")
              && !contentType.startsWith("text/")
              && !"application/octet-stream".equals(contentType)) {
        skipMimeParsing = true;
      }
    }

    // If we aren't supposed to attempt mime decode after decryption, we are done here
    if (skipMimeParsing || !input.getMimeDecode()) {

      log.add(LogType.MSG_DATA_SKIP_MIME, 1);

      ArrayList<Uri> uris = new ArrayList<>();
      uris.add(currentInputUri);
      ArrayList<OpenPgpMetadata> metadatas = new ArrayList<>();
      metadatas.add(decryptResult.getDecryptionMetadata());

      log.add(LogType.MSG_DATA_OK, 1);
      return new InputDataResult(InputDataResult.RESULT_OK, log, decryptResult, uris, metadatas);
    }

    final MimeStreamParser parser = new MimeStreamParser((MimeConfig) null);

    final ArrayList<Uri> outputUris = new ArrayList<>();
    final ArrayList<OpenPgpMetadata> metadatas = new ArrayList<>();

    parser.setContentDecoding(true);
    parser.setRecurse();
    parser.setContentHandler(
        new AbstractContentHandler() {
          private boolean mFoundContentTypeHeader = false;
          private Uri uncheckedSignedDataUri;
          String mFilename;

          @Override
          public void startMultipart(BodyDescriptor bd) throws MimeException {
            if ("signed".equals(bd.getSubType())) {
              if (mSignedDataUri != null) {
                // recursive signed data is not supported, and will just be parsed as-is
                log.add(LogType.MSG_DATA_DETACHED_NESTED, 2);
                return;
              }
              log.add(LogType.MSG_DATA_DETACHED, 2);
              if (!outputUris.isEmpty()) {
                // we can't have previous data if we parse a detached signature!
                log.add(LogType.MSG_DATA_DETACHED_CLEAR, 3);
                outputUris.clear();
                metadatas.clear();
              }
              // this is signed data, we require the next part raw
              parser.setRaw();
            }
          }

          @Override
          public void raw(InputStream is) throws MimeException, IOException {

            if (uncheckedSignedDataUri != null) {
              throw new AssertionError(
                  "raw parts must only be received as first part of multipart/signed!");
            }

            log.add(LogType.MSG_DATA_DETACHED_RAW, 3);

            uncheckedSignedDataUri =
                TemporaryFileProvider.createFile(mContext, mFilename, "text/plain");
            OutputStream out =
                mContext.getContentResolver().openOutputStream(uncheckedSignedDataUri, "w");

            if (out == null) {
              throw new IOException("Error getting file for writing!");
            }

            int len;
            while ((len = is.read(buf)) > 0) {
              out.write(buf, 0, len);
            }

            out.close();
            // continue to next body part the usual way
            parser.setFlat();
          }

          @Override
          public void startHeader() throws MimeException {
            mFilename = null;
          }

          @Override
          public void endHeader() throws MimeException {
            if (!mFoundContentTypeHeader) {
              parser.stop();
            }
          }

          @Override
          public void field(Field field) throws MimeException {
            field = DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT);
            if (field instanceof ContentDispositionField) {
              mFilename = ((ContentDispositionField) field).getFilename();
            }
            if (field instanceof ContentTypeField) {
              mFoundContentTypeHeader = true;
            }
          }

          private void bodySignature(BodyDescriptor bd, InputStream is)
              throws MimeException, IOException {

            if (!"application/pgp-signature".equals(bd.getMimeType())) {
              log.add(LogType.MSG_DATA_DETACHED_UNSUPPORTED, 3);
              uncheckedSignedDataUri = null;
              parser.setRecurse();
              return;
            }

            log.add(LogType.MSG_DATA_DETACHED_SIG, 3);

            ByteArrayOutputStream detachedSig = new ByteArrayOutputStream();

            int len, totalLength = 0;
            while ((len = is.read(buf)) > 0) {
              totalLength += len;
              detachedSig.write(buf, 0, len);
              if (totalLength > 4096) {
                throw new IOException("detached signature is unreasonably large!");
              }
            }
            detachedSig.close();

            PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel();
            decryptInput.setInputUri(uncheckedSignedDataUri);
            decryptInput.setDetachedSignature(detachedSig.toByteArray());

            PgpDecryptVerifyOperation op =
                new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);
            DecryptVerifyResult verifyResult = op.execute(decryptInput, cryptoInput);

            log.addByMerge(verifyResult, 4);

            mSignedDataUri = uncheckedSignedDataUri;
            mSignedDataResult = verifyResult;

            // reset parser state
            uncheckedSignedDataUri = null;
            parser.setRecurse();
          }

          @Override
          public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {

            // if we have signed data waiting, we expect a signature for checking
            if (uncheckedSignedDataUri != null) {
              bodySignature(bd, is);
              return;
            }

            // we read first, no need to create an output file if nothing was read!
            int len = is.read(buf);
            if (len < 0) {
              return;
            }

            // If mSignedDataUri is non-null, we already parsed a signature. If mSignedDataResult is
            // non-null
            // too, we are still in the same parsing stage, so this is trailing data - skip it!
            if (mSignedDataUri != null && mSignedDataResult != null) {
              log.add(LogType.MSG_DATA_DETACHED_TRAILING, 2);
              return;
            }

            log.add(LogType.MSG_DATA_MIME_PART, 2);

            String mimeType = bd.getMimeType();

            if (mFilename != null) {
              log.add(LogType.MSG_DATA_MIME_FILENAME, 3, mFilename);
              boolean isGenericMimeType =
                  ClipDescription.compareMimeTypes(mimeType, "application/octet-stream")
                      || ClipDescription.compareMimeTypes(mimeType, "application/x-download");
              if (isGenericMimeType) {
                String extension = MimeTypeMap.getFileExtensionFromUrl(mFilename);
                String extMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
                if (extMimeType != null) {
                  mimeType = extMimeType;
                  log.add(LogType.MSG_DATA_MIME_FROM_EXTENSION, 3);
                }
              }
            }
            log.add(LogType.MSG_DATA_MIME_TYPE, 3, mimeType);

            Uri uri = TemporaryFileProvider.createFile(mContext, mFilename, mimeType);
            OutputStream out = mContext.getContentResolver().openOutputStream(uri, "w");

            if (out == null) {
              throw new IOException("Error getting file for writing!");
            }

            // If this data looks like text, we pipe the incoming data into a charset
            // decoder, to see if the data is legal for the assumed charset.
            String charset = bd.getCharset();
            CharsetVerifier charsetVerifier = new CharsetVerifier(buf, mimeType, charset);

            int totalLength = 0;
            do {
              totalLength += len;
              out.write(buf, 0, len);
              charsetVerifier.readBytesFromBuffer(0, len);
            } while ((len = is.read(buf)) > 0);

            log.add(LogType.MSG_DATA_MIME_LENGTH, 3, Long.toString(totalLength));

            OpenPgpMetadata metadata;
            if (charsetVerifier.isDefinitelyBinary()) {
              metadata = new OpenPgpMetadata(mFilename, mimeType, 0L, totalLength);
            } else {
              if (charsetVerifier.isCharsetFaulty() && charsetVerifier.isCharsetGuessed()) {
                log.add(
                    LogType.MSG_DATA_MIME_CHARSET_UNKNOWN,
                    3,
                    charsetVerifier.getMaybeFaultyCharset());
              } else if (charsetVerifier.isCharsetFaulty()) {
                log.add(LogType.MSG_DATA_MIME_CHARSET_FAULTY, 3, charsetVerifier.getCharset());
              } else if (charsetVerifier.isCharsetGuessed()) {
                log.add(LogType.MSG_DATA_MIME_CHARSET_GUESS, 3, charsetVerifier.getCharset());
              } else {
                log.add(LogType.MSG_DATA_MIME_CHARSET, 3, charsetVerifier.getCharset());
              }

              metadata =
                  new OpenPgpMetadata(
                      mFilename,
                      charsetVerifier.getGuessedMimeType(),
                      0L,
                      totalLength,
                      charsetVerifier.getCharset());
            }

            out.close();
            outputUris.add(uri);
            metadatas.add(metadata);
          }
        });

    try {

      log.add(LogType.MSG_DATA_MIME, 1);

      try {

        // open current uri for input
        InputStream in = mContext.getContentResolver().openInputStream(currentInputUri);
        parser.parse(in);

        if (mSignedDataUri != null) {

          if (decryptResult != null) {
            decryptResult.setSignatureResult(mSignedDataResult.getSignatureResult());
          } else {
            decryptResult = mSignedDataResult;
          }

          // the actual content is the signed data now (and will be passed verbatim, if parsing
          // fails)
          currentInputUri = mSignedDataUri;
          in = mContext.getContentResolver().openInputStream(currentInputUri);
          // reset signed data result, to indicate to the parser that it is in the inner part
          mSignedDataResult = null;
          parser.parse(in);
        }
      } catch (MimeException e) {
        // a mime error likely means that this wasn't mime data, after all
        e.printStackTrace();
        log.add(LogType.MSG_DATA_MIME_BAD, 2);
      }

      // if we found data, return success
      if (!outputUris.isEmpty()) {
        log.add(LogType.MSG_DATA_MIME_OK, 2);

        log.add(LogType.MSG_DATA_OK, 1);
        return new InputDataResult(
            InputDataResult.RESULT_OK, log, decryptResult, outputUris, metadatas);
      }

      // if no mime data parsed, just return the raw data as fallback
      log.add(LogType.MSG_DATA_MIME_NONE, 2);

      OpenPgpMetadata metadata;
      if (decryptResult != null) {
        metadata = decryptResult.getDecryptionMetadata();
      } else {
        // if we neither decrypted nor mime-decoded, should this be treated as an error?
        // either way, we know nothing about the data
        metadata = new OpenPgpMetadata();
      }

      outputUris.add(currentInputUri);
      metadatas.add(metadata);

      log.add(LogType.MSG_DATA_OK, 1);
      return new InputDataResult(
          InputDataResult.RESULT_OK, log, decryptResult, outputUris, metadatas);

    } catch (FileNotFoundException e) {
      log.add(LogType.MSG_DATA_ERROR_IO, 2);
      return new InputDataResult(InputDataResult.RESULT_ERROR, log);
    } catch (IOException e) {
      e.printStackTrace();
      log.add(LogType.MSG_DATA_ERROR_IO, 2);
      return new InputDataResult(InputDataResult.RESULT_ERROR, log);
    }
  }