diff --git a/apps/susimail/src/src/i2p/susi/debug/Debug.java b/apps/susimail/src/src/i2p/susi/debug/Debug.java index 14f55849f..b3f280642 100644 --- a/apps/susimail/src/src/i2p/susi/debug/Debug.java +++ b/apps/susimail/src/src/i2p/susi/debug/Debug.java @@ -44,11 +44,19 @@ public class Debug { return level; } - public static void debug( int msgLevel, String msg ) + public static void debug( int msgLevel, String msg ) { + debug(msgLevel, msg, null); + } + + /** @since 0.9.34 */ + public static void debug(int msgLevel, String msg, Throwable t) { - if( msgLevel <= level ) + if( msgLevel <= level ) { System.err.println("SusiMail: " + msg); + if (t != null) + t.printStackTrace(); + } if (msgLevel <= ERROR) - I2PAppContext.getGlobalContext().logManager().getLog(Debug.class).error(msg); + I2PAppContext.getGlobalContext().logManager().getLog(Debug.class).error(msg, t); } } diff --git a/apps/susimail/src/src/i2p/susi/util/Buffer.java b/apps/susimail/src/src/i2p/susi/util/Buffer.java new file mode 100644 index 000000000..2585ba86c --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/Buffer.java @@ -0,0 +1,34 @@ +package i2p.susi.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Base interface for all Buffers. + * Data may only be read or written via streams, + * unless implemented via additional methods in subclasses. + * + * @since 0.9.34 + */ +public interface Buffer { + + public InputStream getInputStream() throws IOException; + + public OutputStream getOutputStream() throws IOException; + + /** + * Top-level reader MUST call this to close the input stream. + */ + public void readComplete(boolean success); + + /** + * Writer MUST call this when done. + * @param success if false, deletes any resources + */ + public void writeComplete(boolean success); + + public int getLength(); + + public int getOffset(); +} diff --git a/apps/susimail/src/src/i2p/susi/util/CountingInputStream.java b/apps/susimail/src/src/i2p/susi/util/CountingInputStream.java new file mode 100644 index 000000000..583b5b418 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/CountingInputStream.java @@ -0,0 +1,50 @@ +package i2p.susi.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An InputStream that implements ReadCounter. + * + * @since 0.9.34 + */ +public class CountingInputStream extends FilterInputStream implements ReadCounter { + + protected long count; + + /** + * + */ + public CountingInputStream(InputStream in) { + super(in); + } + + @Override + public long skip(long n) throws IOException { + long rv = in.skip(n); + count += rv; + return rv; + } + + public long getRead() { + return count; + } + + @Override + public int read() throws IOException { + int rv = in.read(); + if (rv >= 0) + count++; + return rv; + } + + @Override + public int read(byte buf[], int off, int len) throws IOException { + int rv = in.read(buf, off, len); + if (rv > 0) + count += rv; + return rv; + } + +} diff --git a/apps/susimail/src/src/i2p/susi/util/CountingOutputStream.java b/apps/susimail/src/src/i2p/susi/util/CountingOutputStream.java new file mode 100644 index 000000000..f3628c3e2 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/CountingOutputStream.java @@ -0,0 +1,36 @@ +package i2p.susi.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An OutputStream that counts how many bytes are written + * and returns the count via getWritten(). + * + * @since 0.9.34 + */ +public class CountingOutputStream extends FilterOutputStream { + + private long count; + + public CountingOutputStream(OutputStream out) { + super(out); + } + + public long getWritten() { + return count; + } + + @Override + public void write(int val) throws IOException { + out.write(val); + count++; + } + + @Override + public void write(byte src[], int off, int len) throws IOException { + out.write(src, off, len); + count += len; + } +} diff --git a/apps/susimail/src/src/i2p/susi/util/DecodingOutputStream.java b/apps/susimail/src/src/i2p/susi/util/DecodingOutputStream.java new file mode 100644 index 000000000..62069c63d --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/DecodingOutputStream.java @@ -0,0 +1,122 @@ +package i2p.susi.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; + + +/** + * Buffering decoder, with output to a Writer. + * Adapted from SAM UTF8Reader. + * + * @since 0.9.34 + */ +public class DecodingOutputStream extends OutputStream { + + private final Writer _out; + private final ByteBuffer _bb; + private final CharBuffer _cb; + private final CharsetDecoder _dc; + + // Charset.forName("UTF-8").newDecoder().replacement().charAt(0) & 0xffff + private static final int REPLACEMENT = 0xfffd; + + /** + * @param out UTF-8 + */ + public DecodingOutputStream(Writer out, String charset) { + super(); + _out = out; + _dc = Charset.forName(charset).newDecoder(); + _bb = ByteBuffer.allocate(1024); + _cb = CharBuffer.allocate(1024); + } + + @Override + public void write(int b) throws IOException { + if (!_bb.hasRemaining()) + flush(); + _bb.put((byte) b); + } + + @Override + public void write(byte buf[], int off, int len) throws IOException { + while (len > 0) { + if (_bb.hasRemaining()) { + int toWrite = Math.min(len, _bb.remaining()); + _bb.put(buf, off, toWrite); + len -= toWrite; + } + flush(); + } + } + + private void decodeAndWrite(boolean endOfInput) throws IOException { + _bb.flip(); + if (!_bb.hasRemaining()) + return; + CoderResult result; + try { + result = _dc.decode(_bb, _cb, endOfInput); + } catch (IllegalStateException ise) { + System.out.println("Decoder error with endOfInput=" + endOfInput); + ise.printStackTrace(); + result = null; + } + _bb.compact(); + // Overflow and underflow are not errors. + // It seems to return underflow every time. + // So just check if we got a character back in the buffer. + if (result == null || (result.isError() && !_cb.hasRemaining())) { + _out.write(REPLACEMENT); + } else { + _cb.flip(); + _out.append(_cb); + _cb.clear(); + } + } + + @Override + public void flush() throws IOException { + decodeAndWrite(false); + } + + /** Only flushes. Does NOT close the writer */ + @Override + public void close() throws IOException { + decodeAndWrite(true); + } + +/**** + public static void main(String[] args) { + try { + String s = "Consider the encoding of the Euro sign, €." + + " The Unicode code point for \"€\" is U+20AC."; + byte[] test = s.getBytes("UTF-8"); + InputStream bais = new java.io.ByteArrayInputStream(test); + DecodingOutputStream r = new DecodingOutputStream(bais); + int b; + StringBuilder buf = new StringBuilder(128); + while ((b = r.write()) >= 0) { + buf.append((char) b); + } + System.out.println("Received: " + buf); + System.out.println("Test passed? " + buf.toString().equals(s)); + buf.setLength(0); + bais = new java.io.ByteArrayInputStream(new byte[] { 'x', (byte) 0xcc, 'x' } ); + r = new DecodingOutputStream(bais); + while ((b = r.write()) >= 0) { + buf.append((char) b); + } + System.out.println("Received: " + buf); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } +****/ +} diff --git a/apps/susimail/src/src/i2p/susi/util/DummyOutputStream.java b/apps/susimail/src/src/i2p/susi/util/DummyOutputStream.java new file mode 100644 index 000000000..d03e759d0 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/DummyOutputStream.java @@ -0,0 +1,29 @@ +package i2p.susi.util; + +import java.io.OutputStream; + +/** + * Write to nowhere + * + * @since 0.9.34 + */ +public class DummyOutputStream extends OutputStream { + + public DummyOutputStream() { + super(); + } + + public void write(int val) {} + + @Override + public void write(byte src[]) {} + + @Override + public void write(byte src[], int off, int len) {} + + @Override + public void flush() {} + + @Override + public void close() {} +} diff --git a/apps/susimail/src/src/i2p/susi/util/EOFOnMatchInputStream.java b/apps/susimail/src/src/i2p/susi/util/EOFOnMatchInputStream.java new file mode 100644 index 000000000..89e6ab49c --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/EOFOnMatchInputStream.java @@ -0,0 +1,194 @@ +package i2p.susi.util; + +import java.io.PushbackInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import net.i2p.data.DataHelper; + +/** + * A stream that returns EOF when the input matches + * the bytes provided. The reader will never see any bytes + * from a full match. + * + * We extend PushbackInputStream for convenience, + * but we use its buffer as a fifo, not a stack. + * Do not call the unread() methods externally. + * + * @since 0.9.34 + */ +public class EOFOnMatchInputStream extends PushbackInputStream implements ReadCounter { + private final byte[] match; + private final int size; + private final ReadCounter cis; + + /** + * Non-counter mode. getRead() will return 0. + * @param match will be copied + */ + public EOFOnMatchInputStream(InputStream in, byte[] match) { + this(in, null, match); + } + + /** + * Counter mode. getRead() will the ReadCounter's value, not including the match bytes. + * @param match will be copied + */ + public EOFOnMatchInputStream(InputStream in, ReadCounter ctr, byte[] match) { + super(in, match.length); + size = match.length; + if (size <= 0) + throw new IllegalArgumentException(); + // buf grows down, so flip for easy matching + this.match = reverse(match); + cis = ctr; + } + + private static byte[] reverse(byte[] m) { + int j = m.length; + byte[] rv = new byte[j]; + for (int i = 0; i < m.length; i++) { + rv[--j] = m[i]; + } + return rv; + } + + /** + * If constructed with a counter, returns the count + * (not necessarily starting at 0) minus the buffered/matched count. + * Otherwise returns 0. + */ + public long getRead() { + if (cis != null) + return cis.getRead() - (size - pos); + return 0; + } + + /** + * @return true if we returned EOF because we hit the match + */ + public boolean wasFound() { + return pos <= 0; + } + + /** + * Debug only. Return the number of bytes currently in the buffer. + * + * @return number of bytes buffered + */ +/* + public int getBuffered() { + return size - pos; + } +*/ + + /** + * Debug only. Return the buffer. + * + * @return the buffer + */ +/* + public byte[] getBuffer() { + int len = getBuffered(); + byte[] b = new byte[len]; + if (len <= 0) + return b; + System.arraycopy(buf, pos, b, 0, len); + return reverse(b); + } +*/ + + @Override + public int read() throws IOException { + if (pos <= 0) + return -1; + while(true) { + // read, pushback, compare + int c = in.read(); + if (c < 0) { + if (pos < size) + return pop(); + return -1; + } + if (pos >= size) { + // common case, buf is empty, no match + if (c != (match[size - 1] & 0xff)) + return c; + // push first byte into buf, go around again + unread(c); + continue; + } + unread(c); + if (!DataHelper.eq(buf, pos, match, pos, size - pos)) { + return pop(); + } + // partial or full match + if (pos <= 0) + return -1; // full match + // partial match, go around again + } + } + + /** + * FIFO output. Pop the oldest (not the newest). + * Only call if pos < size. + * We never call super.read(), it returns the newest. + */ + private int pop() { + // return oldest, shift up + int rv = buf[size - 1] & 0xff; + for (int i = size - 1; i > pos; i--) { + buf[i] = buf[i - 1]; + } + pos++; + return rv; + } + + @Override + public int read(byte buf[], int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + int c = read(); + if (c == -1) { + if (i == 0) + return -1; + return i; + } + buf[off++] = (byte)c; + } + return len; + } + + @Override + public long skip(long n) throws IOException { + long rv = 0; + int c; + while (rv < n && (c = read()) >= 0) { + rv++; + } + return rv; + } + +/**** + public static void main(String[] args) { + String match = "xxa"; + String test = "xxbxaxoaaxxyyyyyyxxxazzzz"; + byte[] m = DataHelper.getASCII(match); + byte[] in = DataHelper.getASCII(test); + try { + InputStream eof = new EOFOnMatchInputStream(new java.io.ByteArrayInputStream(in), m); + byte[] out = new byte[in.length + 10]; + int read = eof.read(out); + if (read != test.indexOf(match)) + System.out.println("EOFOMIS test failed, read " + read); + else if (!DataHelper.eq(in, 0, out, 0, read)) + System.out.println("EOFOMIS test failed, bad data"); + else + System.out.println("EOFOMIS test passed"); + } catch (Exception e) { + System.out.println("EOFOMIS test failed"); + e.printStackTrace(); + } + } +****/ +} diff --git a/apps/susimail/src/src/i2p/susi/util/EscapeHTMLOutputStream.java b/apps/susimail/src/src/i2p/susi/util/EscapeHTMLOutputStream.java new file mode 100644 index 000000000..0074ee0d9 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/EscapeHTMLOutputStream.java @@ -0,0 +1,68 @@ +package i2p.susi.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.data.DataHelper; + +/** + * Escape HTML on the fly. + * Streaming version of DataHelper.escapeHTML(), + * and we escape '-' too since we stick debugging stuff inside comments, + * and '--' is disallowed inside comments. + * + * @since 0.9.34 + */ +public class EscapeHTMLOutputStream extends FilterOutputStream { + + private static final byte[] AMP = DataHelper.getASCII("&"); + private static final byte[] QUOT = DataHelper.getASCII("""); + private static final byte[] LT = DataHelper.getASCII("<"); + private static final byte[] GT = DataHelper.getASCII(">"); + private static final byte[] APOS = DataHelper.getASCII("'"); + private static final byte[] MDASH = DataHelper.getASCII("-"); + private static final byte[] BR = DataHelper.getASCII("
\n"); + + + public EscapeHTMLOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int val) throws IOException { + switch (val) { + case '&': + out.write(AMP); + break; + case '"': + out.write(QUOT); + break; + case '<': + out.write(LT); + break; + case '>': + out.write(GT); + break; + case '\'': + out.write(APOS); + break; + case '-': + out.write(MDASH); + break; + case '\r': + break; + case '\n': + out.write(BR); + break; + default: + out.write(val); + } + } + + /** + * Does nothing. Does not close the underlying stream. + */ + @Override + public void close() {} +} diff --git a/apps/susimail/src/src/i2p/susi/util/EscapeHTMLWriter.java b/apps/susimail/src/src/i2p/susi/util/EscapeHTMLWriter.java new file mode 100644 index 000000000..c801db39e --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/EscapeHTMLWriter.java @@ -0,0 +1,82 @@ +package i2p.susi.util; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +import net.i2p.data.DataHelper; + +/** + * Escape HTML on the fly. + * Streaming version of DataHelper.escapeHTML(), + * and we escape '-' too since we stick debugging stuff inside comments, + * and '--' is disallowed inside comments. + * + * @since 0.9.34 + */ +public class EscapeHTMLWriter extends FilterWriter { + + private static final String AMP = "&"; + private static final String QUOT = """; + private static final String LT = "<"; + private static final String GT = ">"; + private static final String APOS = "'"; + private static final String MDASH = "-"; + private static final String BR = "
\n"; + + + public EscapeHTMLWriter(Writer out) { + super(out); + } + + @Override + public void write(int c) throws IOException { + switch (c) { + case '&': + out.write(AMP); + break; + case '"': + out.write(QUOT); + break; + case '<': + out.write(LT); + break; + case '>': + out.write(GT); + break; + case '\'': + out.write(APOS); + break; + case '-': + out.write(MDASH); + break; + case '\r': + break; + case '\n': + out.write(BR); + break; + default: + out.write(c); + } + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + for (int i = off; i < off + len; i++) { + write(cbuf[i]); + } + } + + @Override + public void write(String str, int off, int len) throws IOException { + for (int i = off; i < off + len; i++) { + write(str.charAt(i)); + } + } + + /** + * Does nothing. Does not close the underlying writer. + */ + @Override + public void close() {} +} diff --git a/apps/susimail/src/src/i2p/susi/util/FileBuffer.java b/apps/susimail/src/src/i2p/susi/util/FileBuffer.java new file mode 100644 index 000000000..0b3d92040 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/FileBuffer.java @@ -0,0 +1,108 @@ +package i2p.susi.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; + +import net.i2p.data.DataHelper; +import net.i2p.util.SecureFileOutputStream; + +/** + * File implementation of Buffer. + * + * @since 0.9.34 + */ +public class FileBuffer implements Buffer { + + protected final File _file; + protected final int _offset; + protected final int _sublen; + private InputStream _is; + private OutputStream _os; + + public FileBuffer(File file) { + this(file, 0, 0); + } + + public FileBuffer(File file, int offset, int sublen) { + _file = file; + _offset = offset; + _sublen = sublen; + } + + /** + * @return the underlying file + */ + public File getFile() { + return _file; + } + + /** + * Caller must call readComplete() + * + * @return new FileInputStream + */ + public synchronized InputStream getInputStream() throws IOException { + if (_is != null && _offset <= 0) + return _is; + _is = new FileInputStream(_file); + if (_offset > 0) + DataHelper.skip(_is, _offset); + // TODO if _sublen > 0, wrap with a read limiter + return _is; + } + + /** + * Caller must call writeComplete() + * + * @return new FileOutputStream + */ + public synchronized OutputStream getOutputStream() throws IOException { + if (_os != null) + throw new IllegalStateException(); + _os = new SecureFileOutputStream(_file); + return _os; + } + + public synchronized void readComplete(boolean success) { + if (_is != null) { + try { _is.close(); } catch (IOException ioe) {} + _is = null; + } + } + + /** + * Deletes the file if success is false + */ + public synchronized void writeComplete(boolean success) { + if (_os != null) { + try { _os.close(); } catch (IOException ioe) {} + _os = null; + } + if (!success) + _file.delete(); + } + + /** + * Always valid if file exists + */ + public int getLength() { + if (_sublen > 0) + return _sublen; + return (int) _file.length(); + } + + /** + * Always valid + */ + public int getOffset() { + return _offset; + } + + @Override + public String toString() { + return "FB " + _file; + } +} diff --git a/apps/susimail/src/src/i2p/susi/util/GzipFileBuffer.java b/apps/susimail/src/src/i2p/susi/util/GzipFileBuffer.java new file mode 100644 index 000000000..3f20ebb3b --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/GzipFileBuffer.java @@ -0,0 +1,105 @@ +package i2p.susi.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import net.i2p.data.DataHelper; +import net.i2p.util.SecureFileOutputStream; + +/** + * Gzip File implementation of Buffer. + * + * @since 0.9.34 + */ +public class GzipFileBuffer extends FileBuffer { + + private long _actualLength; + private CountingInputStream _cis; + private CountingOutputStream _cos; + + public GzipFileBuffer(File file) { + super(file); + } + + public GzipFileBuffer(File file, int offset, int sublen) { + super(file, offset, sublen); + } + + /** + * @return new FileInputStream + */ + @Override + public synchronized InputStream getInputStream() throws IOException { + if (_cis != null && (_offset <= 0 || _offset == _cis.getRead())) + return _cis; + if (_cis != null && _offset > _cis.getRead()) { + DataHelper.skip(_cis, _offset - _cis.getRead()); + return _cis; + } + _cis = new CountingInputStream(new GZIPInputStream(new BufferedInputStream(new FileInputStream(_file)))); + if (_offset > 0) + DataHelper.skip(_cis, _offset); + // TODO if _sublen > 0, wrap with a read limiter + return _cis; + } + + /** + * @return new FileOutputStream + */ + @Override + public synchronized OutputStream getOutputStream() throws IOException { + if (_offset > 0) + throw new IllegalStateException(); + if (_cos != null) + throw new IllegalStateException(); + _cos = new CountingOutputStream(new BufferedOutputStream(new GZIPOutputStream(new SecureFileOutputStream(_file)))); + return _cos; + } + + @Override + public synchronized void readComplete(boolean success) { + if (_cis != null) { + if (success) + _actualLength = _cis.getRead(); + try { _cis.close(); } catch (IOException ioe) {} + _cis = null; + } + } + + /** + * Sets the length if success is true + */ + @Override + public synchronized void writeComplete(boolean success) { + if (_cos != null) { + if (success) + _actualLength = _cos.getWritten(); + try { _cos.close(); } catch (IOException ioe) {} + _cos = null; + } + } + + /** + * Returns the actual uncompressed size. + * + * Only known after reading and calling readComplete(true), + * or after writing and calling writeComplete(true), + * oherwise returns 0. + */ + @Override + public int getLength() { + return (int) _actualLength; + } + + @Override + public String toString() { + return "GZFB " + _file; + } +} diff --git a/apps/susimail/src/src/i2p/susi/util/LimitInputStream.java b/apps/susimail/src/src/i2p/susi/util/LimitInputStream.java new file mode 100644 index 000000000..f36eaed33 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/LimitInputStream.java @@ -0,0 +1,69 @@ +package i2p.susi.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Limit total reads and skips to a specified maximum, then return EOF + * + * @since 0.9.34 + */ +public class LimitInputStream extends CountingInputStream { + + private final long maxx; + + /** + * @param max max number of bytes to read + */ + public LimitInputStream(InputStream in, long max) { + super(in); + if (max < 0) + throw new IllegalArgumentException("negative limit: " + max); + maxx = max; + } + + @Override + public int available() throws IOException { + return (int) Math.min(maxx - count, super.available()); + } + + @Override + public long skip(long n) throws IOException { + return super.skip(Math.min(maxx - count, n)); + } + + @Override + public int read() throws IOException { + if (count >= maxx) + return -1; + return super.read(); + } + + @Override + public int read(byte buf[], int off, int len) throws IOException { + if (count >= maxx) + return -1; + return super.read(buf, off, (int) Math.min(maxx - count, len)); + } + +/**** + public static void main(String[] args) { + try { + LimitInputStream lim = new LimitInputStream(new java.io.ByteArrayInputStream(new byte[20]), 5); + lim.read(); + lim.skip(2); + byte[] out = new byte[10]; + int read = lim.read(out); + if (read != 2) + System.out.println("LIS test failed, read " + read); + else if (lim.getRead() != 5) + System.out.println("CIS test failed, read " + lim.getRead()); + else + System.out.println("LIS/CIS test passed"); + } catch (Exception e) { + e.printStackTrace(); + } + } +****/ +} diff --git a/apps/susimail/src/src/i2p/susi/util/MemoryBuffer.java b/apps/susimail/src/src/i2p/susi/util/MemoryBuffer.java new file mode 100644 index 000000000..a94d191b2 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/MemoryBuffer.java @@ -0,0 +1,91 @@ +package i2p.susi.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Buffer backed by a byte array. + * Use for small amounts of data only. + * + * @since 0.9.34 + */ +public class MemoryBuffer implements Buffer { + + private ByteArrayOutputStream _baos; + private byte content[]; + private final int _size; + + public MemoryBuffer() { + this(4096); + } + + public MemoryBuffer(int size) { + _size = size; + } + + /** + * @return new ByteArrayInputStream + */ + public synchronized InputStream getInputStream() throws IOException { + if (content == null) + throw new IOException("no data"); + return new ByteArrayInputStream(content); + } + + /** + * @return new or existing ByteArrayOutputStream + */ + public synchronized OutputStream getOutputStream() { + if (_baos == null) + _baos = new ByteArrayOutputStream(_size); + return _baos; + } + + public void readComplete(boolean success) {} + + /** + * Deletes the data if success is false + */ + public synchronized void writeComplete(boolean success) { + if (success) { + if (content == null) + content = _baos.toByteArray(); + } else { + content = null; + } + _baos = null; + } + + /** + * Current size. + */ + public synchronized int getLength() { + if (content != null) + return content.length; + if (_baos != null) + return _baos.size(); + return 0; + } + + /** + * @return 0 always + */ + public int getOffset() { + return 0; + } + + /** + * @return content if writeComplete(true) was called, otherwise null + */ + public byte[] getContent() { + return content; + } + + @Override + public String toString() { + return "SB " + (content == null ? "empty" : content.length + " bytes"); + } +} diff --git a/apps/susimail/src/src/i2p/susi/util/OutputStreamBuffer.java b/apps/susimail/src/src/i2p/susi/util/OutputStreamBuffer.java new file mode 100644 index 000000000..b5a7579e1 --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/OutputStreamBuffer.java @@ -0,0 +1,64 @@ +package i2p.susi.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Output only. Input unsupported. + * + * @since 0.9.34 + */ +public class OutputStreamBuffer implements Buffer { + + private final OutputStream _out; + + public OutputStreamBuffer(OutputStream out) { + _out = out; + } + + /** + * @throws UnsupportedOperationException + */ + public InputStream getInputStream() { + throw new UnsupportedOperationException(); + } + + /** + * @return new OutputStreamOutputStream + */ + public OutputStream getOutputStream() { + return _out; + } + + /** + * Does nothing + */ + public void readComplete(boolean success) {} + + /** + * Closes the output stream + */ + public void writeComplete(boolean success) { + try { _out.close(); } catch (IOException ioe) {} + } + + /** + * @return 0 always + */ + public int getLength() { + return 0; + } + + /** + * @return 0 always + */ + public int getOffset() { + return 0; + } + + @Override + public String toString() { + return "OSB"; + } +} diff --git a/apps/susimail/src/src/i2p/susi/util/ReadBuffer.java b/apps/susimail/src/src/i2p/susi/util/ReadBuffer.java index fb65e33a1..ad1b55dc2 100644 --- a/apps/susimail/src/src/i2p/susi/util/ReadBuffer.java +++ b/apps/susimail/src/src/i2p/susi/util/ReadBuffer.java @@ -23,12 +23,19 @@ */ package i2p.susi.util; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; + import net.i2p.data.DataHelper; /** + * Input only for constant data, initialized from a byte array. + * See MemoryBuffer for read/write. + * * @author susi */ -public class ReadBuffer { +public class ReadBuffer implements Buffer { public final byte content[]; public final int length, offset; @@ -39,6 +46,49 @@ public class ReadBuffer { this.length = length; } + /** + * @return new ByteArrayInputStream over the content + * @since 0.9.34 + */ + public InputStream getInputStream() { + return new ByteArrayInputStream(content, offset, length); + } + + /** + * @throws IllegalStateException always + * @since 0.9.34 + */ + public OutputStream getOutputStream() { + throw new IllegalStateException(); + } + + /** + * Does nothing + * @since 0.9.34 + */ + public void readComplete(boolean success) {} + + /** + * Does nothing + * @since 0.9.34 + */ + public void writeComplete(boolean success) {} + + /** + * Always valid + */ + public int getLength() { + return length; + } + + /** + * Always valid + */ + public int getOffset() { + return offset; + } + + @Override public String toString() { return content != null ? DataHelper.getUTF8(content, offset, length) : ""; diff --git a/apps/susimail/src/src/i2p/susi/util/ReadCounter.java b/apps/susimail/src/src/i2p/susi/util/ReadCounter.java new file mode 100644 index 000000000..4f0776cfa --- /dev/null +++ b/apps/susimail/src/src/i2p/susi/util/ReadCounter.java @@ -0,0 +1,14 @@ +package i2p.susi.util; + +/** + * Count the bytes that have been read or skipped + * + * @since 0.9.34 + */ +public interface ReadCounter { + + /** + * The total number of bytes that have been read or skipped + */ + public long getRead(); +} diff --git a/apps/susimail/src/src/i2p/susi/webmail/Mail.java b/apps/susimail/src/src/i2p/susi/webmail/Mail.java index 6d050b184..d1007bd2a 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/Mail.java +++ b/apps/susimail/src/src/i2p/susi/webmail/Mail.java @@ -23,15 +23,19 @@ */ package i2p.susi.webmail; -import i2p.susi.util.Config; import i2p.susi.debug.Debug; -import i2p.susi.util.ReadBuffer; -import i2p.susi.webmail.encoding.DecodingException; +import i2p.susi.util.Buffer; +import i2p.susi.util.Config; +import i2p.susi.util.CountingInputStream; +import i2p.susi.util.EOFOnMatchInputStream; +import i2p.susi.util.MemoryBuffer; import i2p.susi.webmail.encoding.Encoding; import i2p.susi.webmail.encoding.EncodingFactory; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.text.DateFormat; import java.text.ParseException; @@ -59,6 +63,11 @@ class Mail { private static final String P2 = "^<[^@< \t]+@[^> \t]+>$"; private static final Pattern PATTERN1 = Pattern.compile(P1); private static final Pattern PATTERN2 = Pattern.compile(P2); + /** + * Also used by MailPart + * See MailPart for why we don't do \r\n\r\n + */ + static final byte HEADER_MATCH[] = DataHelper.getASCII("\r\n\r"); private int size; public String sender, // as received, trimmed only, not HTML escaped @@ -72,7 +81,7 @@ class Mail { quotedDate; // Current Locale, local time zone, longer format public final String uidl; public Date date; - private ReadBuffer header, body; + private Buffer header, body; private MailPart part; String[] to, cc; // addresses only, enclosed by <> private boolean isNew, isSpam; @@ -100,15 +109,27 @@ class Mail { * This may or may not contain the body also. * @return if null, nothing has been loaded yet for this UIDL */ - public synchronized ReadBuffer getHeader() { + public synchronized Buffer getHeader() { return header; } - public synchronized void setHeader(ReadBuffer rb) { + public synchronized void setHeader(Buffer rb) { + try { + setHeader(rb, rb.getInputStream(), true); + } catch (IOException ioe) { + // TODO... + } + } + + /** @since 0.9.34 */ + private synchronized String[] setHeader(Buffer rb, InputStream in, boolean closeIn) { if (rb == null) - return; + return null; header = rb; - parseHeaders(); + String[] rv = parseHeaders(in); + if (closeIn) + rb.readComplete(true); + return rv; } /** @@ -122,23 +143,40 @@ class Mail { * This contains the header also. * @return may be null */ - public synchronized ReadBuffer getBody() { + public synchronized Buffer getBody() { return body; } - public synchronized void setBody(ReadBuffer rb) { + public synchronized void setBody(Buffer rb) { if (rb == null) return; - if (header == null) - setHeader(rb); + // In the common case where we have the body, we only parse the headers once. + // we always re-set the header, even if it was non-null before, + // as we have to parse them to find the start of the body + // and who knows, the headers could have changed. + //if (header == null) + // setHeader(rb); body = rb; - size = rb.length; + boolean success = false; + CountingInputStream in = null; try { - part = new MailPart(uidl, rb); - } catch (DecodingException de) { - Debug.debug(Debug.ERROR, "Decode error: " + de); + in = new CountingInputStream(rb.getInputStream()); + String[] headerLines = setHeader(rb, in, false); + // TODO just fail? + if (headerLines == null) + headerLines = new String[0]; + part = new MailPart(uidl, rb, in, in, headerLines); + rb.readComplete(true); + // may only be available after reading and calling readComplete() + size = rb.getLength(); + success = true; + } catch (IOException de) { + Debug.debug(Debug.ERROR, "Decode error", de); } catch (RuntimeException e) { - Debug.debug(Debug.ERROR, "Parse error: " + e); + Debug.debug(Debug.ERROR, "Parse error", e); + } finally { + try { in.close(); } catch (IOException ioe) {} + rb.readComplete(success); } } @@ -293,8 +331,12 @@ class Mail { longLocalDateFormatter.setTimeZone(tz); } - private void parseHeaders() + /** + * @return all headers, to pass to MailPart, or null on error + */ + private String[] parseHeaders(InputStream in) { + String[] headerLines = null; error = ""; if( header != null ) { @@ -317,10 +359,15 @@ class Mail { if( ok ) { try { - ReadBuffer decoded = hl.decode( header ); - BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( decoded.content, decoded.offset, decoded.length ), "UTF-8" ) ); - String line; - while( ( line = reader.readLine() ) != null ) { + EOFOnMatchInputStream eofin = new EOFOnMatchInputStream(in, HEADER_MATCH); + MemoryBuffer decoded = new MemoryBuffer(4096); + hl.decode(eofin, decoded); + if (!eofin.wasFound()) + Debug.debug(Debug.DEBUG, "EOF hit before \\r\\n\\r\\n in Mail"); + // Fixme UTF-8 to bytes to UTF-8 + headerLines = DataHelper.split(new String(decoded.getContent(), decoded.getOffset(), decoded.getLength()), "\r\n"); + for (int j = 0; j < headerLines.length; j++) { + String line = headerLines[j]; if( line.length() == 0 ) break; @@ -418,9 +465,11 @@ class Mail { } catch( Exception e ) { error += "Error parsing mail header: " + e.getClass().getName() + '\n'; + Debug.debug(Debug.ERROR, "Parse error", e); } } } + return headerLines; } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/MailCache.java b/apps/susimail/src/src/i2p/susi/webmail/MailCache.java index 27221b299..3394f8aa7 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/MailCache.java +++ b/apps/susimail/src/src/i2p/susi/webmail/MailCache.java @@ -25,10 +25,14 @@ package i2p.susi.webmail; import i2p.susi.debug.Debug; import i2p.susi.util.Config; +import i2p.susi.util.Buffer; +import i2p.susi.util.FileBuffer; import i2p.susi.util.ReadBuffer; +import i2p.susi.util.MemoryBuffer; import i2p.susi.webmail.pop3.POP3MailBox; import i2p.susi.webmail.pop3.POP3MailBox.FetchRequest; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -38,6 +42,8 @@ import java.util.Hashtable; import java.util.List; import java.util.Set; +import net.i2p.I2PAppContext; + /** * @author user */ @@ -50,16 +56,17 @@ class MailCache { private final POP3MailBox mailbox; private final Hashtable mails; private final PersistentMailCache disk; + private final I2PAppContext _context; /** Includes header, headers are generally 1KB to 1.5 KB, * and bodies will compress well. */ - private static final int FETCH_ALL_SIZE = 8192; + private static final int FETCH_ALL_SIZE = 32*1024; /** * @param mailbox non-null */ - MailCache(POP3MailBox mailbox, + MailCache(I2PAppContext ctx, POP3MailBox mailbox, String host, int port, String user, String pass) { this.mailbox = mailbox; mails = new Hashtable(); @@ -71,6 +78,7 @@ class MailCache { Debug.debug(Debug.ERROR, "Error creating disk cache: " + ioe); } disk = pmc; + _context = ctx; if (disk != null) loadFromDisk(); } @@ -142,7 +150,8 @@ class MailCache { mail.setHeader(mailbox.getHeader(uidl)); } else if (mode == FetchMode.ALL) { if(!mail.hasBody()) { - ReadBuffer rb = mailbox.getBody(uidl); + File file = new File(_context.getTempDir(), "susimail-new-" + _context.random().nextLong()); + Buffer rb = mailbox.getBody(uidl, new FileBuffer(file)); if (rb != null) { mail.setBody(rb); if (disk != null && disk.saveMail(mail) && @@ -216,7 +225,7 @@ class MailCache { continue; // found on disk, woo } } - POP3Request pr = new POP3Request(mail, true); + POP3Request pr = new POP3Request(mail, true, new MemoryBuffer(1024)); fetches.add(pr); } else { if (mail.hasBody() && @@ -238,7 +247,8 @@ class MailCache { continue; // found on disk, woo } } - POP3Request pr = new POP3Request(mail, false); + File file = new File(_context.getTempDir(), "susimail-new-" + _context.random().nextLong()); + POP3Request pr = new POP3Request(mail, false, new FileBuffer(file)); fetches.add(pr); } else { if (!Boolean.parseBoolean(Config.getProperty(WebMail.CONFIG_LEAVE_ON_SERVER))) { @@ -258,15 +268,14 @@ class MailCache { mailbox.getBodies(bar); // Process results for (POP3Request pr : fetches) { - ReadBuffer rb = pr.buf; - if (rb != null) { + if (pr.getSuccess()) { Mail mail = pr.mail; if (!mail.hasHeader()) mail.setNew(true); if (pr.getHeaderOnly()) { - mail.setHeader(rb); + mail.setHeader(pr.getBuffer()); } else { - mail.setBody(rb); + mail.setBody(pr.getBuffer()); } rv = true; if (disk != null) { @@ -326,24 +335,41 @@ class MailCache { */ private static class POP3Request implements FetchRequest { public final Mail mail; - private final boolean headerOnly; - public ReadBuffer buf; + private boolean headerOnly, success; + public final Buffer buf; - public POP3Request(Mail m, boolean hOnly) { + public POP3Request(Mail m, boolean hOnly, Buffer buffer) { mail = m; headerOnly = hOnly; + buf = buffer; } public String getUIDL() { return mail.uidl; } - public boolean getHeaderOnly() { + /** @since 0.9.34 */ + public synchronized void setHeaderOnly(boolean headerOnly) { + this.headerOnly = headerOnly; + } + + public synchronized boolean getHeaderOnly() { return headerOnly; } - public void setBuffer(ReadBuffer buffer) { - buf = buffer; + /** @since 0.9.34 */ + public Buffer getBuffer() { + return buf; + } + + /** @since 0.9.34 */ + public synchronized void setSuccess(boolean success) { + this.success = success; + } + + /** @since 0.9.34 */ + public synchronized boolean getSuccess() { + return success; } } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/MailPart.java b/apps/susimail/src/src/i2p/susi/webmail/MailPart.java index ec4670524..49cda3d94 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/MailPart.java +++ b/apps/susimail/src/src/i2p/susi/webmail/MailPart.java @@ -24,11 +24,22 @@ package i2p.susi.webmail; import i2p.susi.debug.Debug; +import i2p.susi.util.Buffer; +import i2p.susi.util.CountingOutputStream; +import i2p.susi.util.DummyOutputStream; +import i2p.susi.util.EOFOnMatchInputStream; +import i2p.susi.util.LimitInputStream; import i2p.susi.util.ReadBuffer; +import i2p.susi.util.ReadCounter; +import i2p.susi.util.OutputStreamBuffer; +import i2p.susi.util.MemoryBuffer; import i2p.susi.webmail.encoding.DecodingException; import i2p.susi.webmail.encoding.Encoding; import i2p.susi.webmail.encoding.EncodingFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -40,53 +51,68 @@ import net.i2p.data.DataHelper; */ class MailPart { + private static final OutputStream DUMMY_OUTPUT = new DummyOutputStream(); public final String[] headerLines; public final String type, encoding, name, description, disposition, charset, version; + /** begin, end, and beginBody are relative to readBuffer.getOffset(). + * begin is before the headers + * beginBody is after the headers + * warning - end is exclusive + */ private final int beginBody, begin, end; /** fixme never set */ public final String filename = null; public final List parts; public final boolean multipart, message; - public final ReadBuffer buffer; + public final Buffer buffer; + + /** + * the decoded length if known, else -1 + * @since 0.9.34 + */ + public int decodedLength = -1; + /** * the UIDL of the mail, same for all parts * @since 0.9.33 */ public final String uidl; - - public MailPart(String uidl, ReadBuffer readBuffer) throws DecodingException - { - this(uidl, readBuffer, readBuffer.offset, readBuffer.length); - } - - public MailPart(String uidl, ReadBuffer readBuffer, int offset, int length) throws DecodingException + /** + * @param readBuffer has zero offset for top-level MailPart. + * @param in used for reading (NOT readBuffer.getInputStream()) + * @param counter used for counting how much we have read. + * Probably the same as InputStream but a different interface. + * @param hdrlines non-null for top-level MailPart, where they + * were already parsed in Mail. Null otherwise + */ + public MailPart(String uidl, Buffer readBuffer, InputStream in, ReadCounter counter, String[] hdrlines) throws IOException { this.uidl = uidl; - begin = offset; - end = offset + length; buffer = readBuffer; parts = new ArrayList(); - /* - * parse header lines - */ - int bb = end; - for( int i = begin; i < end - 4; i++ ) { - if( buffer.content[i] == '\r' && - buffer.content[i+1] == '\n' && - buffer.content[i+2] == '\r' && - buffer.content[i+3] == '\n' ) { - bb = i + 2; - break; - } + if (hdrlines != null) { + // from Mail headers + headerLines = hdrlines; + begin = 0; + } else { + begin = (int) counter.getRead(); + // parse header lines + // We don't do \r\n\r\n because then we can miss the \r\n-- + // of the multipart boundary. So we do \r\n\r here, + // and \n-- below. If it's not multipart, we will swallow the + // \n below. + EOFOnMatchInputStream eofin = new EOFOnMatchInputStream(in, Mail.HEADER_MATCH); + MemoryBuffer decodedHeaders = new MemoryBuffer(4096); + EncodingFactory.getEncoding("HEADERLINE").decode(eofin, decodedHeaders); + if (!eofin.wasFound()) + Debug.debug(Debug.DEBUG, "EOF hit before \\r\\n\\r\\n in MailPart"); + // Fixme UTF-8 to bytes to UTF-8 + headerLines = DataHelper.split(new String(decodedHeaders.getContent(), decodedHeaders.getOffset(), decodedHeaders.getLength()), "\r\n"); } - beginBody = bb; - - ReadBuffer decodedHeaders = EncodingFactory.getEncoding( "HEADERLINE" ).decode( buffer.content, begin, beginBody - begin ); - headerLines = DataHelper.split(new String(decodedHeaders.content, decodedHeaders.offset, decodedHeaders.length), "\r\n"); String boundary = null; String x_encoding = null; @@ -123,7 +149,7 @@ class MailPart { boundary = str; if (x_type.startsWith( "multipart" ) && boundary != null ) x_multipart = true; - if (x_type.startsWith( "message" ) ) + else if (x_type.startsWith( "message" ) ) x_message = true; str = getHeaderLineAttribute( headerLines[i], "name" ); if( str != null ) @@ -150,60 +176,117 @@ class MailPart { description = x_description; version = x_version; + // see above re: \n + if (multipart) { + // EOFOnMatch will eat the \n + beginBody = (int) counter.getRead() + 1; + } else { + // swallow the \n + int c = in.read(); + if (c != '\n') + Debug.debug(Debug.DEBUG, "wasn't a \\n, it was " + c); + beginBody = (int) counter.getRead(); + } + + int tmpEnd = 0; /* * parse body */ - int beginLastPart = -1; if( multipart ) { - byte boundaryArray[] = DataHelper.getUTF8(boundary); - for( int i = beginBody; i < end - 4; i++ ) { - if( buffer.content[i] == '\r' && - buffer.content[i+1] == '\n' && - buffer.content[i+2] == '-' && - buffer.content[i+3] == '-' ) { - /* - * begin of possible boundary line - */ - int j = 0; - for( ; j < boundaryArray.length && i + 4 + j < end; j++ ) - if( buffer.content[ i + 4 + j ] != boundaryArray[j] ) - break; - if( j == boundaryArray.length ) { - int k = i + 4 + j; - if( k < end - 2 && - buffer.content[k] == '-' && - buffer.content[k+1] == '-' ) - k += 2; - - if( k < end - 2 && - buffer.content[k] == '\r' && - buffer.content[k+1] == '\n' ) { - - k += 2; - - if( beginLastPart != -1 ) { - int endLastPart = Math.min(i + 2, end); - MailPart newPart = new MailPart(uidl, buffer, beginLastPart, endLastPart - beginLastPart); - parts.add( newPart ); - } - beginLastPart = k; - } - i = k; + // See above for why we don't include the \r + byte[] match = DataHelper.getASCII("\n--" + boundary); + for (int i = 0; ; i++) { + EOFOnMatchInputStream eofin = new EOFOnMatchInputStream(in, counter, match); + if (i == 0) { + // Read through first boundary line, not including "\r\n" or "--\r\n" + OutputStream dummy = new DummyOutputStream(); + DataHelper.copy(eofin, dummy); + if (!eofin.wasFound()) + Debug.debug(Debug.DEBUG, "EOF hit before first boundary " + boundary); + if (readBoundaryTrailer(in)) { + if (!eofin.wasFound()) + Debug.debug(Debug.DEBUG, "EOF hit before first part body " + boundary); + tmpEnd = (int) eofin.getRead(); + break; } - } + // From here on we do include the \r + match = DataHelper.getASCII("\r\n--" + boundary); + eofin = new EOFOnMatchInputStream(in, counter, match); + } + MailPart newPart = new MailPart(uidl, buffer, eofin, eofin, null); + parts.add( newPart ); + tmpEnd = (int) eofin.getRead(); + if (!eofin.wasFound()) { + // if MailPart contains a MailPart, we may not have drained to the end + DataHelper.copy(eofin, DUMMY_OUTPUT); + if (!eofin.wasFound()) + Debug.debug(Debug.DEBUG, "EOF hit before end of body " + i + " boundary: " + boundary); + } + if (readBoundaryTrailer(in)) + break; } } else if( message ) { - MailPart newPart = new MailPart(uidl, buffer, beginBody, end - beginBody); + MailPart newPart = new MailPart(uidl, buffer, in, counter, null); + // TODO newPart doesn't save message headers we might like to display, + // like From, To, and Subject parts.add( newPart ); + tmpEnd = (int) counter.getRead(); + } else { + // read through to the end + DataHelper.copy(in, DUMMY_OUTPUT); + tmpEnd = (int) counter.getRead(); } + end = tmpEnd; + if (encoding == null || encoding.equals("7bit") || encoding.equals("8bit")) { + decodedLength = end - beginBody; + } + //if (Debug.getLevel() >= Debug.DEBUG) + // Debug.debug(Debug.DEBUG, "New " + this); + } + + /** + * Swallow "\r\n" or "--\r\n". + * We don't have any pushback if this goes wrong. + * + * @return true if end of input + */ + private static boolean readBoundaryTrailer(InputStream in) throws IOException { + int c = in.read(); + if (c == '-') { + // end of parts with this boundary + c = in.read(); + if (c != '-') { + Debug.debug(Debug.DEBUG, "Unexpected char after boundary-: " + c); + return true; + } + c = in.read(); + if (c == -1) { + return true; + } + if (c != '\r') { + Debug.debug(Debug.DEBUG, "Unexpected char after boundary--: " + c); + return true; + } + c = in.read(); + if (c != '\n') + Debug.debug(Debug.DEBUG, "Unexpected char after boundary--\\r: " + c); + return true; + } else if (c == '\r') { + c = in.read(); + if (c != '\n') + Debug.debug(Debug.DEBUG, "Unexpected char after boundary\\r: " + c); + } else { + Debug.debug(Debug.DEBUG, "Unexpected char after boundary: " + c); + } + return c == -1; } /** * @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n * @since 0.9.13 */ - public ReadBuffer decode(int offset) throws DecodingException { + public void decode(int offset, Buffer out) throws IOException { String encg = encoding; if (encg == null) { //throw new DecodingException("No encoding specified"); @@ -213,7 +296,44 @@ class MailPart { Encoding enc = EncodingFactory.getEncoding(encg); if(enc == null) throw new DecodingException(_t("No encoder found for encoding \\''{0}\\''.", WebMail.quoteHTML(encg))); - return enc.decode(buffer.content, beginBody + offset, end - beginBody - offset); + InputStream in = null; + LimitInputStream lin = null; + CountingOutputStream cos = null; + Buffer dout = null; + try { + in = buffer.getInputStream(); + DataHelper.skip(in, buffer.getOffset() + beginBody + offset); + lin = new LimitInputStream(in, end - beginBody - offset); + if (decodedLength < 0) { + cos = new CountingOutputStream(out.getOutputStream()); + dout = new OutputStreamBuffer(cos); + } else { + dout = out; + } + enc.decode(lin, dout); + //dout.getOutputStream().flush(); + } catch (IOException ioe) { + if (lin != null) + Debug.debug(Debug.DEBUG, "Decode IOE at in position " + lin.getRead() + + " offset " + offset, ioe); + else if (cos != null) + Debug.debug(Debug.DEBUG, "Decode IOE at out position " + cos.getWritten() + + " offset " + offset, ioe); + else + Debug.debug(Debug.DEBUG, "Decode IOE", ioe); + throw ioe; + } finally { + if (in != null) try { in.close(); } catch (IOException ioe) {}; + if (lin != null) try { lin.close(); } catch (IOException ioe) {}; + buffer.readComplete(true); + // let the servlet do this + //if (cos != null) try { cos.close(); } catch (IOException ioe) {}; + //if (dout != null) + // dout.writeComplete(true); + //out.writeComplete(true); + } + if (cos != null) + decodedLength = (int) cos.getWritten(); } private static String getFirstAttribute( String line ) @@ -305,4 +425,40 @@ class MailPart { private static String _t(String s, Object o) { return Messages.getString(s, o); } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(1024); + buf.append( + "MailPart:" + + "\n\tuidl:\t" + uidl + + "\n\tbuffer:\t" + buffer + + "\n\tbuffer offset:\t" + buffer.getOffset() + + "\n\tbegin:\t" + begin + + "\n\theader lines:\t" + headerLines.length + + "\n" + ); + for (int i = 0; i < headerLines.length; i++) { + buf.append("\t\t\"").append(headerLines[i]).append("\"\n"); + } + buf.append( + "\tmultipart?\t" + multipart + + "\n\tmessage?\t" + message + + "\n\ttype:\t" + type + + "\n\tencoding:\t" + encoding + + "\n\tname:\t" + name + + "\n\tdescription:\t" + description + + "\n\tdisposition:\t" + disposition + + "\n\tcharset:\t" + charset + + "\n\tversion:\t" + version + + "\n\tsubparts:\t" + parts.size() + + "\n\tbeginbody:\t" + beginBody + + "\n\tbody len:\t" + (end - beginBody) + + "\n\tdecoded len:\t" + decodedLength + + "\n\tend:\t" + (end - 1) + + "\n\ttotal len:\t" + (end - begin) + + "\n\tbuffer len:\t" + buffer.getLength() + ); + return buf.toString(); + } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/PersistentMailCache.java b/apps/susimail/src/src/i2p/susi/webmail/PersistentMailCache.java index 408117ad0..653927264 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/PersistentMailCache.java +++ b/apps/susimail/src/src/i2p/susi/webmail/PersistentMailCache.java @@ -2,6 +2,9 @@ package i2p.susi.webmail; import i2p.susi.debug.Debug; import i2p.susi.webmail.Messages; +import i2p.susi.util.Buffer; +import i2p.susi.util.FileBuffer; +import i2p.susi.util.GzipFileBuffer; import i2p.susi.util.ReadBuffer; import java.io.BufferedInputStream; @@ -127,7 +130,7 @@ class PersistentMailCache { private boolean locked_getMail(Mail mail, boolean headerOnly) { File f = getFullFile(mail.uidl); if (f.exists()) { - ReadBuffer rb = read(f); + Buffer rb = read(f); if (rb != null) { mail.setBody(rb); return true; @@ -135,7 +138,7 @@ class PersistentMailCache { } f = getHeaderFile(mail.uidl); if (f.exists()) { - ReadBuffer rb = read(f); + Buffer rb = read(f); if (rb != null) { mail.setHeader(rb); return true; @@ -156,7 +159,7 @@ class PersistentMailCache { } private boolean locked_saveMail(Mail mail) { - ReadBuffer rb = mail.getBody(); + Buffer rb = mail.getBody(); if (rb != null) { File f = getFullFile(mail.uidl); if (f.exists()) @@ -249,16 +252,21 @@ class PersistentMailCache { * * @return success */ - private static boolean write(ReadBuffer rb, File f) { + private static boolean write(Buffer rb, File f) { + InputStream in = null; OutputStream out = null; try { - out = new BufferedOutputStream(new GZIPOutputStream(new SecureFileOutputStream(f))); - out.write(rb.content, rb.offset, rb.length); + in = rb.getInputStream(); + GzipFileBuffer gb = new GzipFileBuffer(f); + out = gb.getOutputStream(); + DataHelper.copy(in, out); return true; } catch (IOException ioe) { Debug.debug(Debug.ERROR, "Error writing: " + f + ": " + ioe); return false; } finally { + if (in != null) + try { in.close(); } catch (IOException ioe) {} if (out != null) try { out.close(); } catch (IOException ioe) {} } @@ -267,28 +275,8 @@ class PersistentMailCache { /** * @return null on failure */ - private static ReadBuffer read(File f) { - InputStream in = null; - try { - long len = f.length(); - if (len > 16 * 1024 * 1024) { - throw new IOException("too big"); - } - in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(f))); - ByteArrayOutputStream out = new ByteArrayOutputStream((int) len); - DataHelper.copy(in, out); - ReadBuffer rb = new ReadBuffer(out.toByteArray(), 0, out.size()); - return rb; - } catch (IOException ioe) { - Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + ioe); - return null; - } catch (OutOfMemoryError oom) { - Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + oom); - return null; - } finally { - if (in != null) - try { in.close(); } catch (IOException ioe) {} - } + private static Buffer read(File f) { + return new GzipFileBuffer(f); } /** @@ -309,7 +297,7 @@ class PersistentMailCache { } if (uidl == null) return null; - ReadBuffer rb = read(f); + Buffer rb = read(f); if (rb == null) return null; Mail mail = new Mail(uidl); diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java index 8a597125b..87f9f6fe9 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java +++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java @@ -24,12 +24,16 @@ package i2p.susi.webmail; import i2p.susi.debug.Debug; +import i2p.susi.util.Buffer; import i2p.susi.util.Config; +import i2p.susi.util.DecodingOutputStream; +import i2p.susi.util.EscapeHTMLOutputStream; +import i2p.susi.util.EscapeHTMLWriter; import i2p.susi.util.Folder; import i2p.susi.util.Folder.SortOrder; -import i2p.susi.util.ReadBuffer; +import i2p.susi.util.Buffer; +import i2p.susi.util.OutputStreamBuffer; import i2p.susi.webmail.Messages; -import i2p.susi.webmail.encoding.DecodingException; import i2p.susi.webmail.encoding.Encoding; import i2p.susi.webmail.encoding.EncodingException; import i2p.susi.webmail.encoding.EncodingFactory; @@ -48,6 +52,7 @@ import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.io.UnsupportedEncodingException; +import java.io.Writer; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; @@ -76,6 +81,7 @@ import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.servlet.RequestWrapper; import net.i2p.servlet.util.ServletUtil; +import net.i2p.servlet.util.WriterOutputStream; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.Translate; @@ -131,6 +137,7 @@ public class WebMail extends HttpServlet private static final String PREV_PAGE_NUM = "prevpagenum"; private static final String NEXT_PAGE_NUM = "nextpagenum"; private static final String CURRENT_SORT = "currentsort"; + private static final String CURRENT_FOLDER = "currentfolder"; private static final String DEBUG_STATE = "currentstate"; /* @@ -607,14 +614,14 @@ public class WebMail extends HttpServlet private static void showPart( PrintWriter out, MailPart mailPart, int level, boolean html ) { String br = html ? "
\r\n" : "\r\n"; - + if( html ) { out.println( "" ); } @@ -644,7 +651,6 @@ public class WebMail extends HttpServlet boolean showBody = false; boolean prepareAttachment = false; String reason = ""; - StringBuilder body = null; String ident = quoteHTML( ( mailPart.description != null ? mailPart.description + ", " : "" ) + @@ -658,53 +664,60 @@ public class WebMail extends HttpServlet */ showBody = true; } - if( showBody == false && mailPart.type != null ) { + if (!showBody && mailPart.type != null) { if( mailPart.type.equals("text/plain")) { showBody = true; } else prepareAttachment = true; } - if( showBody ) { - String charset = mailPart.charset; - if( charset == null ) { - charset = "ISO-8859-1"; - // don't show this in text mode which is used to include the mail in the reply or forward - if (html) - reason += _t("Warning: no charset found, fallback to US-ASCII.") + br; - } - try { - ReadBuffer decoded = mailPart.decode(0); - BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( decoded.content, decoded.offset, decoded.length ), charset ) ); - body = new StringBuilder(); - String line; - while( ( line = reader.readLine() ) != null ) { - body.append( quoteHTML( line ) ); - body.append( br ); - } - } - catch( UnsupportedEncodingException uee ) { - showBody = false; - reason = _t("Charset \\''{0}\\'' not supported.", quoteHTML( mailPart.charset )) + br; - } - catch (IOException e1) { - showBody = false; - reason += _t("Part ({0}) not shown, because of {1}", ident, e1.toString()) + br; - } - } - if( html ) - out.println( "" ); if( reason != null && reason.length() > 0 ) { if( html ) out.println( "

"); out.println( reason ); if( html ) out.println( "

" ); + reason = ""; } - if( showBody ) { + if( html ) + out.println( "" ); + if( showBody ) { if( html ) - out.println( "

" ); - out.println( body.toString() ); + out.println( "


" ); + String charset = mailPart.charset; + if( charset == null ) { + charset = "ISO-8859-1"; + // don't show this in text mode which is used to include the mail in the reply or forward + if (html) + reason = _t("Warning: no charset found, fallback to US-ASCII.") + br; + } + try { + Writer escaper; + if (html) + escaper = new EscapeHTMLWriter(out); + else + escaper = out; + Buffer ob = new OutputStreamBuffer(new DecodingOutputStream(escaper, charset)); + mailPart.decode(0, ob); + // todo Finally + ob.writeComplete(true); + } + catch( UnsupportedEncodingException uee ) { + showBody = false; + reason = _t("Charset \\''{0}\\'' not supported.", quoteHTML( mailPart.charset )) + br; + } + catch (IOException e1) { + showBody = false; + reason += _t("Part ({0}) not shown, because of {1}", ident, e1.toString()) + br; + } + if( html ) + out.println( "

" ); + } + if( reason != null && reason.length() > 0 ) { + // FIXME css has -32 margin + if( html ) + out.println( "

"); + out.println( reason ); if( html ) out.println( "

" ); } @@ -758,7 +771,7 @@ public class WebMail extends HttpServlet } if( html ) { out.println( "" ); } } @@ -868,7 +881,8 @@ public class WebMail extends HttpServlet sessionObject.host = host; sessionObject.smtpPort = smtpPortNo; state = State.LIST; - MailCache mc = new MailCache(mailbox, host, pop3PortNo, user, pass); + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + MailCache mc = new MailCache(ctx, mailbox, host, pop3PortNo, user, pass); sessionObject.mailCache = mc; sessionObject.folder = new Folder(); if (!offline) { @@ -1459,7 +1473,9 @@ public class WebMail extends HttpServlet return null; if( part.hashCode() == hashCode ) +{ return part; +} if( part.multipart || part.message ) { for( MailPart p : part.parts ) { @@ -2012,6 +2028,7 @@ public class WebMail extends HttpServlet // UP is reverse sort. DOWN is normal sort. String fullSort = curOrder == SortOrder.UP ? '-' + curSort : curSort; out.println(""); + out.println(""); } if( sessionObject.error != null && sessionObject.error.length() > 0 ) { out.println( "

" + quoteHTML(sessionObject.error).replace("\n", "
") + "

" ); @@ -2081,18 +2098,6 @@ public class WebMail extends HttpServlet { boolean shown = false; if(part != null) { - ReadBuffer content = part.buffer; - - // we always decode, even if part.encoding is null, will default to 7bit - try { - // +2 probably for \r\n - content = part.decode(2); - } catch (DecodingException e) { - sessionObject.error += _t("Error decoding content: {0}", e.getMessage()) + '\n'; - content = null; - } - if(content == null) - return false; String name = part.filename; if (name == null) { name = part.name; @@ -2110,12 +2115,15 @@ public class WebMail extends HttpServlet "filename*=" + name3); if (part.type != null) response.setContentType(part.type); - response.setContentLength(content.length); + if (part.decodedLength >= 0) + response.setContentLength(part.decodedLength); + Debug.debug(Debug.DEBUG, "Sending raw attachment " + name + " length " + part.decodedLength); // cache-control? - response.getOutputStream().write(content.content, content.offset, content.length); + // was 2 + part.decode(0, new OutputStreamBuffer(response.getOutputStream())); shown = true; } catch (IOException e) { - e.printStackTrace(); + Debug.debug(Debug.ERROR, "Error sending raw attachment " + name + " length " + part.decodedLength, e); } } else { ZipOutputStream zip = null; @@ -2126,13 +2134,13 @@ public class WebMail extends HttpServlet "filename*=" + name3 + ".zip"); ZipEntry entry = new ZipEntry( name ); zip.putNextEntry( entry ); - zip.write( content.content, content.offset, content.length ); + // was 2 + part.decode(0, new OutputStreamBuffer(zip)); zip.closeEntry(); zip.finish(); shown = true; } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + Debug.debug(Debug.ERROR, "Error sending zip attachment " + name + " length " + part.decodedLength, e); } finally { if ( zip != null) try { zip.close(); } catch (IOException ioe) {} @@ -2153,7 +2161,7 @@ public class WebMail extends HttpServlet private static boolean sendMailSaveAs(SessionObject sessionObject, Mail mail, HttpServletResponse response) { - ReadBuffer content = mail.getBody(); + Buffer content = mail.getBody(); if(content == null) return false; @@ -2164,17 +2172,21 @@ public class WebMail extends HttpServlet name = "message.eml"; String name2 = sanitizeFilename(name); String name3 = encodeFilenameRFC5987(name); + InputStream in = null; try { response.setContentType("message/rfc822"); - response.setContentLength(content.length); + response.setContentLength(content.getLength()); // cache-control? response.addHeader("Content-Disposition", "attachment; filename=\"" + name2 + "\"; " + "filename*=" + name3); - response.getOutputStream().write(content.content, content.offset, content.length); + in = content.getInputStream(); + DataHelper.copy(in, response.getOutputStream()); return true; } catch (IOException e) { e.printStackTrace(); return false; + } finally { + if (in != null) try { in.close(); } catch (IOException ioe) {} } } @@ -2376,7 +2388,7 @@ public class WebMail extends HttpServlet SMTPClient relay = new SMTPClient(); if( relay.sendMail( sessionObject.host, sessionObject.smtpPort, sessionObject.user, sessionObject.pass, - sender, recipients.toArray(), sessionObject.sentMail, + sender, recipients.toArray(new String[recipients.size()]), sessionObject.sentMail, sessionObject.attachments, boundary)) { sessionObject.info += _t("Mail sent."); sessionObject.sentMail = null; @@ -2714,8 +2726,19 @@ public class WebMail extends HttpServlet if(!RELEASE && mail != null && mail.hasBody()) { out.println( "" ); } out.println("
"); diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/Base64.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/Base64.java index be82a28a1..d61143f7f 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/Base64.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/Base64.java @@ -23,11 +23,14 @@ */ package i2p.susi.webmail.encoding; -import i2p.susi.util.ReadBuffer; +import i2p.susi.util.Buffer; +import i2p.susi.util.MemoryBuffer; import java.io.ByteArrayInputStream; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; @@ -142,7 +145,10 @@ public class Base64 extends Encoding { return b; } - private static byte decodeByte( byte b ) throws DecodingException { + private static byte decodeByte( int c ) throws IOException { + if (c < 0) + throw new EOFException(); + byte b = (byte) (c & 0xff); if( b >= 'A' && b <= 'Z' ) b -= 'A'; else if( b >= 'a' && b <= 'z' ) @@ -156,43 +162,49 @@ public class Base64 extends Encoding { else if( b == '=' ) b = 0; else - throw new DecodingException( "Decoding base64 failed (invalid data)." ); + throw new DecodingException("Decoding base64 failed, invalid data: " + c); // System.err.println( "decoded " + (char)a + " to " + b ); return b; } - public ReadBuffer decode(byte[] in, int offset, int length) throws DecodingException { - byte out[] = new byte[length * 3 / 4 + 1 ]; - int written = 0; - while( length > 0 ) { - if( in[offset] == '\r' || in[offset] == '\n' || - in[offset] == ' ' || in[offset] == '\t' ) { - offset++; - length--; - continue; - } - if( length >= 4 ) { - // System.out.println( "decode: " + (char)in[offset] + (char)in[offset+1]+ (char)in[offset+2]+ (char)in[offset+3] ); - byte b1 = decodeByte( in[offset++] ); - byte b2 = decodeByte( in[offset++] ); - out[written++] = (byte) (( b1 << 2 ) | ( ( b2 >> 4 ) & 3 ) ); - byte b3 = in[offset++]; - if( b3 != '=' ) { - b3 = decodeByte( b3 ); - out[written++] = (byte)( ( ( b2 & 15 ) << 4 ) | ( ( b3 >> 2 ) & 15 ) ); - } - byte b4 = in[offset++]; - if( b4 != '=' ) { - b4 = decodeByte( b4 ); - out[written++] = (byte)( ( ( b3 & 3 ) << 6 ) | b4 & 63 ); - } - length -= 4; - } - else { - //System.err.println( "" ); - throw new DecodingException( "Decoding base64 failed (trailing garbage)." ); + private static int readIn(InputStream in) throws IOException { + int c; + do { + c = in.read(); + } while (c == '\r' || c == '\n' || c == ' ' || c == '\t'); + return c; + } + + /** + * @since 0.9.34 + */ + public void decode(InputStream in, Buffer bout) throws IOException { + OutputStream out = bout.getOutputStream(); + while (true) { + int c = readIn(in); + if (c < 0) + break; + + // System.out.println( "decode: " + (char)in[offset] + (char)in[offset+1]+ (char)in[offset+2]+ (char)in[offset+3] ); + byte b1 = decodeByte(c); + c = readIn(in); + byte b2 = decodeByte(c); + out.write(((b1 << 2) | ((b2 >> 4) & 3)) & 0xff); + + c = readIn(in); + if (c < 0) + break; + byte b3 = 0; + if (c != '=') { + b3 = decodeByte(c); + out.write((((b2 & 15) << 4) | ((b3 >> 2) & 15)) & 0xff); } + c = readIn(in); + if (c < 0) + break; + if (c == '=') break; // done + byte b4 = decodeByte(c); + out.write((((b3 & 3) << 6) | (b4 & 63)) & 0xff); } - return new ReadBuffer(out, 0, written); } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/DecodingException.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/DecodingException.java index 46277d7e5..3a4303245 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/DecodingException.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/DecodingException.java @@ -35,4 +35,11 @@ public class DecodingException extends IOException { super( msg ); } + /** + * @since 0.9.34 + */ + public DecodingException(String msg, Exception cause) + { + super(msg, cause); + } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/EightBit.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/EightBit.java index e19b035b5..36203fbf8 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/EightBit.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/EightBit.java @@ -23,8 +23,14 @@ */ package i2p.susi.webmail.encoding; +import java.io.IOException; +import java.io.InputStream; + +import i2p.susi.util.Buffer; import i2p.susi.util.ReadBuffer; +import net.i2p.data.DataHelper; + /** * Decode only. See encode(). * @author susi @@ -51,8 +57,8 @@ public class EightBit extends Encoding { throw new EncodingException("unsupported"); } - public ReadBuffer decode(byte[] in, int offset, int length) - throws DecodingException { + @Override + public Buffer decode(byte[] in, int offset, int length) { return new ReadBuffer(in, offset, length); } @@ -60,7 +66,16 @@ public class EightBit extends Encoding { * @return in unchanged */ @Override - public ReadBuffer decode(ReadBuffer in) throws DecodingException { + public Buffer decode(Buffer in) { return in; } + + /** + * Copy in to out, unchanged + * @since 0.9.34 + */ + public void decode(InputStream in, Buffer out) throws IOException { + DataHelper.copy(in, out.getOutputStream()); + // read complete, write complete + } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/Encoding.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/Encoding.java index a6a9d7479..9e4d0e562 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/Encoding.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/Encoding.java @@ -28,7 +28,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.Writer; +import i2p.susi.util.Buffer; import i2p.susi.util.ReadBuffer; +import i2p.susi.util.MemoryBuffer; import net.i2p.data.DataHelper; @@ -106,7 +108,7 @@ public abstract class Encoding { * @throws DecodingException * @since 0.9.33 implementation moved from subclasses */ - public ReadBuffer decode(byte in[]) throws DecodingException { + public Buffer decode(byte in[]) throws DecodingException { return decode(in, 0, in.length); } @@ -118,7 +120,14 @@ public abstract class Encoding { * @return Output buffer containing decoded String. * @throws DecodingException */ - public abstract ReadBuffer decode( byte in[], int offset, int length ) throws DecodingException; + public Buffer decode(byte in[], int offset, int length) throws DecodingException { + try { + ReadBuffer rb = new ReadBuffer(in, offset, length); + return decode(rb); + } catch (IOException ioe) { + throw new DecodingException("decode error", ioe); + } + } /** * This implementation just converts the string to a byte array @@ -131,7 +140,7 @@ public abstract class Encoding { * @throws DecodingException * @since 0.9.33 implementation moved from subclasses */ - public ReadBuffer decode(String str) throws DecodingException { + public Buffer decode(String str) throws DecodingException { return str != null ? decode(DataHelper.getUTF8(str)) : null; } @@ -144,7 +153,27 @@ public abstract class Encoding { * @throws DecodingException * @since 0.9.33 implementation moved from subclasses */ - public ReadBuffer decode(ReadBuffer in) throws DecodingException { - return decode(in.content, in.offset, in.length); + public Buffer decode(Buffer in) throws IOException { + MemoryBuffer rv = new MemoryBuffer(4096); + decode(in, rv); + return rv; } + + /** + * @param in + * @see Encoding#decode(byte[], int, int) + * @throws DecodingException + * @since 0.9.34 + */ + public void decode(Buffer in, Buffer out) throws IOException { + decode(in.getInputStream(), out); + } + + /** + * @param in + * @see Encoding#decode(byte[], int, int) + * @throws DecodingException + * @since 0.9.34 + */ + public abstract void decode(InputStream in, Buffer out) throws IOException; } diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java index 9b20c6742..6e8c680e6 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java @@ -25,7 +25,7 @@ package i2p.susi.webmail.encoding; import i2p.susi.debug.Debug; import i2p.susi.util.Config; -import i2p.susi.util.ReadBuffer; +import i2p.susi.util.Buffer; import java.io.IOException; import java.util.HashMap; @@ -65,8 +65,6 @@ public class EncodingFactory { } } } - // TEST - //main(null); } /** @@ -104,7 +102,7 @@ public class EncodingFactory { System.out.println(s + "\tFAIL - null encode result"); continue; } - ReadBuffer rb = e.decode(enc); + Buffer rb = e.decode(enc); if (rb == null) { System.out.println(s + "\tFAIL - null decode result"); continue; diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/HTML.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/HTML.java index 0bfc32449..b74a3197b 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/HTML.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/HTML.java @@ -23,7 +23,9 @@ */ package i2p.susi.webmail.encoding; -import i2p.susi.util.ReadBuffer; +import java.io.InputStream; + +import i2p.susi.util.Buffer; /** * @author user @@ -47,8 +49,7 @@ public class HTML extends Encoding { .replaceAll( "\r{0,1}\n", "
\r\n" ); } - public ReadBuffer decode(byte[] in, int offset, int length) - throws DecodingException { + public void decode(InputStream in, Buffer out) throws DecodingException { throw new DecodingException("unsupported"); } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java index 134ae2cd1..d7a1e0fbe 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java @@ -25,12 +25,13 @@ package i2p.susi.webmail.encoding; import i2p.susi.debug.Debug; import i2p.susi.util.HexTable; +import i2p.susi.util.Buffer; import i2p.susi.util.ReadBuffer; +import i2p.susi.util.MemoryBuffer; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.Locale; import net.i2p.data.DataHelper; @@ -213,157 +214,225 @@ public class HeaderLine extends Encoding { * Decode all the header lines, up through \r\n\r\n, * and puts them in the ReadBuffer, including the \r\n\r\n */ - public ReadBuffer decode( byte in[], int offset, int length ) throws DecodingException { - ByteArrayOutputStream out = new ByteArrayOutputStream(4096); - int written = 0; - int end = offset + length; - if( end > in.length ) - throw new DecodingException( "Index out of bound." ); + public void decode(InputStream in, Buffer bout) throws IOException { + OutputStream out = bout.getOutputStream(); boolean linebreak = false; boolean lastCharWasQuoted = false; - int lastSkip = 0; - while( length-- > 0 ) { - byte c = in[offset++]; + byte[] encodedWord = null; + // we support one char of pushback, + // to catch some simple malformed input + int pushbackChar = 0; + boolean hasPushback = false; + while (true) { + int c; + if (hasPushback) { + c = pushbackChar; + hasPushback = false; + //Debug.debug(Debug.DEBUG, "Loop " + count + " Using pbchar(dec) " + c); + } else { + c = in.read(); + if (c < 0) + break; + } if( c == '=' ) { - if( length > 0 ) { - if( in[offset] == '?' ) { - // System.err.println( "=? found at " + ( offset -1 ) ); - // save charset position here f1+1 to f2-1 - int f1 = offset; - int f2 = f1 + 1; - for( ; f2 < end && in[f2] != '?'; f2++ ); - if( f2 < end ) { - /* - * 2nd question mark found - */ - // System.err.println( "2nd ? found at " + f2 ); - int f3 = f2 + 1; - for( ; f3 < end && in[f3] != '?'; f3++ ); - if( f3 < end ) { - /* - * 3rd question mark found - */ - // System.err.println( "3rd ? found at " + f3 ); - int f4 = f3 + 1; - for( ; f4 < end && in[f4] != '?'; f4++ ); - if( f4 < end - 1 && in[f4+1] == '=' ) { - /* - * 4th question mark found, we are complete, so lets start - */ - String enc = ( in[f2+1] == 'Q' || in[f2+1] == 'q' ) ? "quoted-printable" : ( ( in[f2+1] == 'B' || in[f2+1] == 'b' ) ? "base64" : null ); - // System.err.println( "4th ? found at " + f4 + ", encoding=" + enc ); - if( enc != null ) { - Encoding e = EncodingFactory.getEncoding( enc ); - if( e != null ) { - // System.err.println( "encoder found" ); - ReadBuffer tmp = null; - try { - // System.err.println( "decode(" + (f3 + 1) + "," + ( f4 - f3 - 1 ) + ")" ); - tmp = e.decode( in, f3 + 1, f4 - f3 - 1 ); - // get charset - String charset = new String(in, f1 + 1, f2 - f1 - 1, "ISO-8859-1"); - String clc = charset.toLowerCase(Locale.US); - if (clc.equals("utf-8") || clc.equals("utf8")) { - if (enc.equals("quoted-printable")) { - for( int j = 0; j < tmp.length; j++ ) { - byte d = tmp.content[ tmp.offset + j ]; - out.write( d == '_' ? 32 : d ); - } - } else { - out.write(tmp.content, tmp.offset, tmp.length); - } - } else { - // decode string - String decoded = new String(tmp.content, tmp.offset, tmp.length, charset); - // encode string - byte[] utf8 = DataHelper.getUTF8(decoded); - if (enc.equals("quoted-printable")) { - for( int j = 0; j < utf8.length; j++ ) { - byte d = utf8[j]; - out.write( d == '_' ? 32 : d ); - } - } else { - out.write(utf8); - } - } - int distance = f4 + 2 - offset; - offset += distance; - length -= distance; - lastCharWasQuoted = true; - continue; - } catch (IOException e1) { - Debug.debug(Debug.ERROR, e1.toString()); - } catch (RuntimeException e1) { - Debug.debug(Debug.ERROR, e1.toString()); - } - } - } - } - } + // An encoded-word is 75 chars max including the delimiters, and must be on a single line + // Store the full encoded word, including =? through ?=, in the buffer + if (encodedWord == null) + encodedWord = new byte[75]; + int offset = 0; + int f1 = 0, f2 = 0, f3 = 0, f4 = 0; + encodedWord[offset++] = (byte) c; + // Read until we have 4 '?', stored in encodedWord positions f1, f2, f3, f4, + // plus one char after the 4th '?', which should be '=' + // We make a small attempt to pushback one char if it's not what we expect, + // but for the most part it gets thrown out, as RFC 2047 allows + for (; offset < 75; offset++) { + c = in.read(); + if (c == '?') { + if (f1 == 0) + f1 = offset; + else if (f2 == 0) + f2 = offset; + else if (f3 == 0) + f3 = offset; + else if (f4 == 0) + f4 = offset; + } else if (c == -1) { + break; + } else if (c == '\r' || c == '\n') { + pushbackChar = c; + hasPushback = true; + break; + } else if (offset == 1) { + // no '?' after '=' + out.write('='); + pushbackChar = c; + hasPushback = true; + break; + } + encodedWord[offset] = (byte) c; + // store one past the 4th '?', presumably the '=' + if (f4 > 0 && offset >= f4 + 1) { + if (c == '=') { + offset++; + } else { + pushbackChar = c; + hasPushback = true; } + break; } } - } + //if (f1 > 0) + // Debug.debug(Debug.DEBUG, "End of encoded word, f1 " + f1 + " f2 " + f2 + " f3 " + f3 + " f4 " + f4 + + // " offset " + offset + " pushback? " + hasPushback + " pbchar(dec) " + c + '\n' + + // net.i2p.util.HexDump.dump(encodedWord, 0, offset)); + if (f4 == 0) { + // at most 1 byte is pushed back, the rest is discarded + if (f1 == 0) { + // This is normal + continue; + } else if (f2 == 0) { + Debug.debug(Debug.DEBUG, "2nd '?' not found"); + continue; + } else if (f3 == 0) { + Debug.debug(Debug.DEBUG, "3rd '?' not found"); + continue; + } else { + // probably just too long, but could be end of line without the "?=". + // synthesize a 4th '?' in an attempt to output + // something, probably with some trailing garbage + Debug.debug(Debug.DEBUG, "4th '?' not found"); + f4 = offset + 1; + // keep going and output what we have + } + } + /* + * 4th question mark found, we are complete, so lets start + */ + String enc = (encodedWord[f2+1] == 'Q' || encodedWord[f2+1] == 'q') ? + "quoted-printable" : + ((encodedWord[f2+1] == 'B' || encodedWord[f2+1] == 'b') ? + "base64" : + null); + // System.err.println( "4th ? found at " + f4 + ", encoding=" + enc ); + if (enc != null) { + Encoding e = EncodingFactory.getEncoding( enc ); + if( e != null ) { + // System.err.println( "encoder found" ); + try { + // System.err.println( "decode(" + (f3 + 1) + "," + ( f4 - f3 - 1 ) + ")" ); + ReadBuffer tmpIn = new ReadBuffer(encodedWord, f3 + 1, f4 - f3 - 1); + MemoryBuffer tmp = new MemoryBuffer(75); + e.decode(tmpIn, tmp); + tmp.writeComplete(true); + // get charset + String charset = new String(encodedWord, f1 + 1, f2 - f1 - 1, "ISO-8859-1"); + String clc = charset.toLowerCase(Locale.US); + if (clc.equals("utf-8") || clc.equals("utf8")) { + // FIXME could be more efficient? + InputStream tis = tmp.getInputStream(); + if (enc.equals("quoted-printable")) { + int d; + while ((d = tis.read()) != -1) { + out.write(d == '_' ? 32 : d); + } + } else { + DataHelper.copy(tis, out); + } + } else { + // FIXME could be more efficient? + // decode string + String decoded = new String(tmp.getContent(), tmp.getOffset(), tmp.getLength(), charset); + // encode string + byte[] utf8 = DataHelper.getUTF8(decoded); + if (enc.equals("quoted-printable")) { + for (int j = 0; j < utf8.length; j++) { + byte d = utf8[j]; + out.write(d == '_' ? 32 : d); + } + } else { + out.write(utf8); + } + } + lastCharWasQuoted = true; + continue; + } catch (IOException e1) { + Debug.debug(Debug.DEBUG, "q-w", e1); + Debug.debug(Debug.DEBUG, "Decoder: " + enc + " Input:"); + Debug.debug(Debug.DEBUG, net.i2p.util.HexDump.dump(encodedWord, f3 + 1, f4 - f3 - 1)); + } catch (RuntimeException e1) { + Debug.debug(Debug.DEBUG, "q-w", e1); + Debug.debug(Debug.DEBUG, "Decoder: " + enc + " Input:"); + Debug.debug(Debug.DEBUG, net.i2p.util.HexDump.dump(encodedWord, f3 + 1, f4 - f3 - 1)); + } + } else { + // can't happen + Debug.debug(Debug.DEBUG, "No decoder for " + enc); + } // e != null + } else { + Debug.debug(Debug.DEBUG, "Invalid encoding '" + (char) encodedWord[f2+1] + '\''); + } // enc != null + } // c == '=' else if( c == '\r' ) { - if( length > 0 && in[offset] == '\n' ) { + if ((c = in.read()) == '\n' ) { /* * delay linebreak in case of long line */ linebreak = true; - // The ReadBuffer can contain the body too. - // If we just had a linebreak, we are done... - // don't keep parsing! - if( length > 2 && in[offset+1] == '\r' && in[offset+2] == '\n') - break; - length--; - offset++; - continue; + } else { + // pushback? + Debug.debug(Debug.DEBUG, "No \\n after \\r"); } } + // swallow whitespace here if lastCharWasQuoted if( linebreak ) { linebreak = false; - if( c != ' ' && c != '\t' ) { - /* - * new line does not start with whitespace, so its not a new part of a - * long line - */ - out.write('\r'); - out.write('\n'); - lastSkip = 0; - } - else { - if( !lastCharWasQuoted ) - out.write(' '); + for (int i = 0; ; i++) { + c = in.read(); + if (c == -1) + break; + if (c != ' ' && c != '\t') { + if (i == 0) { + /* + * new line does not start with whitespace, so its not a new part of a + * long line + */ + out.write('\r'); + out.write('\n'); + if (c == '\r') { + linebreak = true; + in.read(); // \n + break; + } + } else { + // treat all preceding whitespace as a single one + if (!lastCharWasQuoted) + out.write(' '); + } + pushbackChar = c; + hasPushback = true; + break; + } /* * skip whitespace */ - int skipped = 1; - while( length > 0 && ( in[offset] == ' ' || in[offset] == '\t' ) ) { - if( lastSkip > 0 && skipped >= lastSkip ) { - break; - } - offset++; - length--; - skipped++; - } - if( lastSkip == 0 && skipped > 0 ) { - lastSkip = skipped; - } - continue; } + // if \r\n\r\n, we are done + if (linebreak) + break; + } else { + /* + * print out everything else literally + */ + out.write(c); + lastCharWasQuoted = false; } - /* - * print out everything else literally - */ - out.write(c); - lastCharWasQuoted = false; - } + } // while true if( linebreak ) { out.write('\r'); out.write('\n'); } - - return new ReadBuffer(out.toByteArray(), 0, out.size()); + bout.writeComplete(true); } /***** diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/QuotedPrintable.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/QuotedPrintable.java index 5100c17a5..9d214d93b 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/QuotedPrintable.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/QuotedPrintable.java @@ -24,11 +24,13 @@ package i2p.susi.webmail.encoding; import i2p.susi.util.HexTable; -import i2p.susi.util.ReadBuffer; +import i2p.susi.util.Buffer; +import i2p.susi.util.MemoryBuffer; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; @@ -136,24 +138,33 @@ public class QuotedPrintable extends Encoding { } } - public ReadBuffer decode(byte[] in, int offset, int length) { - byte[] out = new byte[length]; - int written = 0; - while( length-- > 0 ) { - byte c = in[offset++]; + /** + * @since 0.9.34 + */ + public void decode(InputStream in, Buffer bout) throws IOException { + OutputStream out = bout.getOutputStream(); + while (true) { + int c = in.read(); + if (c < 0) + break; if( c == '=' ) { - if( length >= 2 ) { - byte a = in[offset]; - byte b = in[offset + 1]; + int a = in.read(); + if (a < 0) { + out.write(c); + break; + } + int b = in.read(); + if (b < 0) { + out.write(c); + out.write(a); + break; + } if( ( ( a >= '0' && a <= '9' ) || ( a >= 'A' && a <= 'F' ) ) && ( ( b >= '0' && b <= '9' ) || ( b >= 'A' && b <= 'F' ) ) ) { /* * decode sequence */ // System.err.println( "decoding 0x" + (char)a + "" + (char)b ); - length -= 2 ; - offset += 2; - if( a >= '0' && a <= '9' ) a -= '0'; else if( a >= 'A' && a <= 'F' ) @@ -164,28 +175,23 @@ public class QuotedPrintable extends Encoding { else if( b >= 'A' && b <= 'F' ) b = (byte) (b - 'A' + 10); - out[written++]=(byte) (a*16 + b); + out.write(a*16 + b); continue; } else if( a == '\r' && b == '\n' ) { /* * softbreak, simply ignore it */ - length -= 2; - offset += 2; continue; + } else { + throw new DecodingException("Bad q-p data after '='"); } - /* - * no correct encoded sequence found, ignore it and print it literally - */ - } } /* * print out everything else literally */ - out[written++] = c; + out.write(c); } - - return new ReadBuffer(out, 0, written); + bout.writeComplete(true); } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/SevenBit.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/SevenBit.java index 7bca4b47d..90021dfd6 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/encoding/SevenBit.java +++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/SevenBit.java @@ -23,8 +23,14 @@ */ package i2p.susi.webmail.encoding; +import java.io.IOException; +import java.io.InputStream; + +import i2p.susi.util.Buffer; import i2p.susi.util.ReadBuffer; +import net.i2p.data.DataHelper; + /** * Decode only. * @author susi @@ -35,17 +41,18 @@ public class SevenBit extends Encoding { return "7bit"; } - /** + /** * @throws EncodingException always */ public String encode(byte[] in) throws EncodingException { throw new EncodingException("unsupported"); } - /** + /** * @throws DecodingException on illegal characters */ - public ReadBuffer decode(byte[] in, int offset, int length) + @Override + public Buffer decode(byte[] in, int offset, int length) throws DecodingException { int backupLength = length; int backupOffset = offset; @@ -61,4 +68,23 @@ public class SevenBit extends Encoding { } return new ReadBuffer(in, backupOffset, backupLength); } + + /** + * We don't do any 8-bit checks like we do for decode(byte[]) + * @return in, unchanged + */ + @Override + public Buffer decode(Buffer in) { + return in; + } + + /** + * Copy in to out, unchanged + * We don't do any 8-bit checks like we do for decode(byte[]) + * @since 0.9.34 + */ + public void decode(InputStream in, Buffer out) throws IOException { + DataHelper.copy(in, out.getOutputStream()); + // read complete, write complete + } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java b/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java index 36c691ad7..ab6723cde 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java +++ b/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java @@ -28,9 +28,11 @@ import i2p.susi.webmail.Messages; import i2p.susi.webmail.NewMailListener; import i2p.susi.webmail.WebMail; import i2p.susi.util.Config; +import i2p.susi.util.Buffer; import i2p.susi.util.ReadBuffer; +import i2p.susi.util.MemoryBuffer; -import java.io.ByteArrayOutputStream; +import java.io.OutputStream; import java.io.IOException; import java.io.InputStream; import java.net.Socket; @@ -111,7 +113,7 @@ public class POP3MailBox implements NewMailListener { * @param uidl * @return Byte buffer containing header data or null */ - public ReadBuffer getHeader( String uidl ) { + public Buffer getHeader( String uidl ) { synchronized( synchronizer ) { try { // we must be connected to know the UIDL to ID mapping @@ -134,19 +136,19 @@ public class POP3MailBox implements NewMailListener { * @param id message id * @return Byte buffer containing header data or null */ - private ReadBuffer getHeader( int id ) { + private Buffer getHeader( int id ) { Debug.debug(Debug.DEBUG, "getHeader(" + id + ")"); - ReadBuffer header = null; + Buffer header = null; if (id >= 1 && id <= mails) { /* * try 'TOP n 0' command */ - header = sendCmdN("TOP " + id + " 0" ); + header = sendCmdN("TOP " + id + " 0", new MemoryBuffer(1024)); if( header == null) { /* * try 'RETR n' command */ - header = sendCmdN("RETR " + id ); + header = sendCmdN("RETR " + id, new MemoryBuffer(2048)); if (header == null) Debug.debug( Debug.DEBUG, "RETR returned null" ); } @@ -160,9 +162,9 @@ public class POP3MailBox implements NewMailListener { * Fetch the body. Does not cache. * * @param uidl - * @return Byte buffer containing body data or null + * @return the buffer containing body data or null */ - public ReadBuffer getBody( String uidl ) { + public Buffer getBody(String uidl, Buffer buffer) { synchronized( synchronizer ) { try { // we must be connected to know the UIDL to ID mapping @@ -174,7 +176,7 @@ public class POP3MailBox implements NewMailListener { int id = getIDfromUIDL(uidl); if (id < 0) return null; - return getBody(id); + return getBody(id, buffer); } } @@ -200,10 +202,12 @@ public class POP3MailBox implements NewMailListener { if (id < 0) continue; SendRecv sr; - if (fr.getHeaderOnly() && supportsTOP) - sr = new SendRecv("TOP " + id + " 0", Mode.RB); - else - sr = new SendRecv("RETR " + id, Mode.RB); + if (fr.getHeaderOnly() && supportsTOP) { + sr = new SendRecv("TOP " + id + " 0", fr.getBuffer()); + } else { + fr.setHeaderOnly(false); + sr = new SendRecv("RETR " + id, fr.getBuffer()); + } sr.savedObject = fr; srs.add(sr); } @@ -222,10 +226,8 @@ public class POP3MailBox implements NewMailListener { } } for (SendRecv sr : srs) { - if (sr.result) { - FetchRequest fr = (FetchRequest) sr.savedObject; - fr.setBuffer(sr.rb); - } + FetchRequest fr = (FetchRequest) sr.savedObject; + fr.setSuccess(sr.result); } } @@ -234,14 +236,14 @@ public class POP3MailBox implements NewMailListener { * Caller must sync. * * @param id message id - * @return Byte buffer containing body data or null + * @return the buffer containing body data or null */ - private ReadBuffer getBody(int id) { + private Buffer getBody(int id, Buffer buffer) { Debug.debug(Debug.DEBUG, "getBody(" + id + ")"); - ReadBuffer body = null; + Buffer body = null; if (id >= 1 && id <= mails) { try { - body = sendCmdN( "RETR " + id ); + body = sendCmdN("RETR " + id, buffer); if (body == null) Debug.debug( Debug.DEBUG, "RETR returned null" ); } catch (OutOfMemoryError oom) { @@ -828,7 +830,7 @@ public class POP3MailBox implements NewMailListener { case RB: try { - sr.rb = getResultNa(); + getResultNa(sr.rb); sr.result = true; } catch (IOException ioe) { Debug.debug( Debug.DEBUG, "Error getting RB: " + ioe); @@ -889,13 +891,13 @@ public class POP3MailBox implements NewMailListener { * Tries twice * Caller must sync. * - * @return buffer or null + * @return the buffer or null */ - private ReadBuffer sendCmdN(String cmd ) + private Buffer sendCmdN(String cmd, Buffer buffer) { synchronized (synchronizer) { try { - return sendCmdNa(cmd); + return sendCmdNa(cmd, buffer); } catch (IOException e) { lastError = e.toString(); Debug.debug( Debug.DEBUG, "sendCmdNa throws: " + e); @@ -908,7 +910,7 @@ public class POP3MailBox implements NewMailListener { connect(); if (connected) { try { - return sendCmdNa(cmd); + return sendCmdNa(cmd, buffer); } catch (IOException e2) { lastError = e2.toString(); Debug.debug( Debug.DEBUG, "2nd sendCmdNa throws: " + e2); @@ -929,13 +931,14 @@ public class POP3MailBox implements NewMailListener { * No total timeout (result could be large) * Caller must sync. * - * @return buffer or null + * @return the buffer or null * @throws IOException */ - private ReadBuffer sendCmdNa(String cmd) throws IOException + private Buffer sendCmdNa(String cmd, Buffer buffer) throws IOException { if (sendCmd1a(cmd)) { - return getResultNa(); + getResultNa(buffer); + return buffer; } else { Debug.debug( Debug.DEBUG, "sendCmd1a returned false" ); return null; @@ -966,34 +969,41 @@ public class POP3MailBox implements NewMailListener { * No total timeout (result could be large) * Caller must sync. * - * @return buffer non-null + * @param buffer non-null * @throws IOException */ - private ReadBuffer getResultNa() throws IOException + private void getResultNa(Buffer buffer) throws IOException { InputStream input = socket.getInputStream(); - StringBuilder buf = new StringBuilder(512); - ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); - while (DataHelper.readLine(input, buf)) { - updateActivity(); - int len = buf.length(); - if (len == 0) - break; // huh? no \r? - if (len == 2 && buf.charAt(0) == '.' && buf.charAt(1) == '\r') - break; - String line; - // RFC 1939 sec. 3 de-byte-stuffing - if (buf.charAt(0) == '.') - line = buf.substring(1); - else - line = buf.toString(); - baos.write(DataHelper.getASCII(line)); - if (buf.charAt(len - 1) != '\r') - baos.write((byte) '\n'); - baos.write((byte) '\n'); - buf.setLength(0); + OutputStream out = null; + boolean success = false; + try { + out = buffer.getOutputStream(); + StringBuilder buf = new StringBuilder(512); + while (DataHelper.readLine(input, buf)) { + updateActivity(); + int len = buf.length(); + if (len == 0) + break; // huh? no \r? + if (len == 2 && buf.charAt(0) == '.' && buf.charAt(1) == '\r') + break; + String line; + // RFC 1939 sec. 3 de-byte-stuffing + if (buf.charAt(0) == '.') + line = buf.substring(1); + else + line = buf.toString(); + out.write(DataHelper.getASCII(line)); + if (buf.charAt(len - 1) != '\r') + out.write('\n'); + out.write('\n'); + buf.setLength(0); + } + success = true; + } finally { + if (out != null) try { out.close(); } catch (IOException ioe) {} + buffer.writeComplete(success); } - return new ReadBuffer(baos.toByteArray(), 0, baos.size()); } /** @@ -1278,8 +1288,10 @@ public class POP3MailBox implements NewMailListener { public final String send; public final Mode mode; public String response; + /** true for success */ public boolean result; - public ReadBuffer rb; + /** non-null for RB mode only */ + public final Buffer rb; public List ls; // to remember things public Object savedObject; @@ -1288,13 +1300,30 @@ public class POP3MailBox implements NewMailListener { public SendRecv(String s, Mode m) { send = s; mode = m; + rb = null; + } + + /** + * RB mode only + * @param s may be null + * @since 0.9.34 + */ + public SendRecv(String s, Buffer buffer) { + send = s; + mode = Mode.RB; + rb = buffer; } } public interface FetchRequest { public String getUIDL(); public boolean getHeaderOnly(); - public void setBuffer(ReadBuffer buffer); + /** @since 0.9.34 */ + public Buffer getBuffer(); + /** @since 0.9.34 */ + public void setSuccess(boolean success); + /** @since 0.9.34 */ + public void setHeaderOnly(boolean headerOnly); } /** translate */ diff --git a/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java b/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java index 188571c91..c83e4232a 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java +++ b/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java @@ -226,7 +226,7 @@ public class SMTPClient { * @return success */ public boolean sendMail(String host, int port, String user, String pass, String sender, - Object[] recipients, StringBuilder body, + String[] recipients, StringBuilder body, List attachments, String boundary) { boolean mailSent = false; diff --git a/history.txt b/history.txt index 058400447..01d9e1bdc 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,12 @@ +2018-02-07 zzz + * SusiMail: Use input streams for reading mail (ticket #2119) + - Rewrite Base64, HeaderLine, and QuotedPrintable decoders + - Rewrite ReadBuffer class and utilities for streams + - ReadBuffer becomes Buffer interface with multiple implementations + - Rewrite Mail and MailPart to parse the headers only once + - Rewrite MailPart parser to use streams + - MailPart decoder rewrite to decode stream-to-stream + 2018-02-01 zzz * Console: Fix number formatting (tickets #1912, #1913, #2126) * i2psnark: URL escape fixes diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 87e5bffca..282c18b42 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 1; + public final static long BUILD = 2; /** for example "-test" */ public final static String EXTRA = "";