package com.hubspot.smtp.messages; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.OptionalInt; import java.util.function.Supplier; import com.google.common.io.ByteSource; import com.google.common.io.CharStreams; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; /** * A {@link MessageContent} implementation backed by an {@code InputStream}. * */ public class InputStreamMessageContent extends MessageContent { private static final float UNCOUNTED = -1F; private static final float DEFAULT_8BIT_PROPORTION = 0.1F; private static final int READ_LIMIT = 8192; private final Supplier<InputStream> streamSupplier; private final OptionalInt size; private final MessageContentEncoding encoding; private float eightBitCharProportion = UNCOUNTED; private InputStream stream; public InputStreamMessageContent(Supplier<InputStream> streamSupplier, OptionalInt size, MessageContentEncoding encoding) { this.streamSupplier = streamSupplier; this.size = size; this.encoding = encoding; } public InputStreamMessageContent(ByteSource byteSource, OptionalInt size, MessageContentEncoding encoding) { this(getStream(byteSource), size, encoding); } @Override public OptionalInt size() { return size; } @Override public Object getContent() { return new CrlfTerminatingChunkedStream(getStream()); } /** * Returns an iterator that lazily reads chunks of content from the wrapped stream, * ensuring the last is terminated with CRLF. */ @Override public Iterator<ByteBuf> getContentChunkIterator(ByteBufAllocator allocator) { CrlfTerminatingChunkedStream chunkedStream = new CrlfTerminatingChunkedStream(getStream()); return new Iterator<ByteBuf>() { @Override public boolean hasNext() { try { return !chunkedStream.isEndOfInput(); } catch (Exception e) { // isEndOfInput can throw IOException though it declares Exception throw new RuntimeException(e); } } @Override public ByteBuf next() { try { return chunkedStream.readChunk(allocator); } catch (Exception e) { throw new RuntimeException(e); } } }; } @Override public Object getDotStuffedContent() { // note: size is hard to predict for dot-stuffed content as // the transformation might add a few extra bytes return new DotStuffingChunkedStream(getStream()); } @Override public MessageContentEncoding getEncoding() { return encoding; } @Override public float get8bitCharacterProportion() { if (eightBitCharProportion != UNCOUNTED) { return eightBitCharProportion; } int eightBitCharCount = 0; InputStream inputStream = getStream(); if (!inputStream.markSupported()) { // if we can't examine the stream non-destructively, // assume it has some 8 bit characters, but not enough // to require encoding the body as base64 eightBitCharProportion = DEFAULT_8BIT_PROPORTION; return eightBitCharProportion; } inputStream.mark(READ_LIMIT); try { int c, bytesRead = 0; while (bytesRead < READ_LIMIT && (c = inputStream.read()) != -1) { if (0 != (c & 0x80)) { eightBitCharCount++; } bytesRead++; } inputStream.reset(); eightBitCharProportion = 1.0F * eightBitCharCount / bytesRead; } catch (IOException e) { throw new RuntimeException(e); } return eightBitCharProportion; } @Override public String getContentAsString() { try { return CharStreams.toString(new InputStreamReader(getStream(), StandardCharsets.UTF_8)); } catch (IOException e) { throw new RuntimeException(e); } } private InputStream getStream() { if (stream == null) { stream = streamSupplier.get(); } return stream; } private static Supplier<InputStream> getStream(ByteSource byteSource) { return () -> { try { return byteSource.openStream(); } catch (IOException e) { throw new RuntimeException("Could not open stream", e); } }; } }