forked from I2P_Developers/i2p.i2p
SusiMail: Use input streams for reading mail (ticket #2119)
Rewrite Base64, HeaderLine, and QuotedPrintable decoders Rewrite and expansion of ReadBuffer class and utilities for streams Rewrite Mail and MailPart to parse the headers only once Rewrite MailPart parser MailPart parser rewrite skips over the data without reading into memory or decoding MailPart decoder rewrite to decode stream-to-stream ReadBuffer becomes Buffer interface with multiple implementations Logging and debugging tweaks
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
|
34
apps/susimail/src/src/i2p/susi/util/Buffer.java
Normal file
34
apps/susimail/src/src/i2p/susi/util/Buffer.java
Normal file
@ -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();
|
||||
}
|
50
apps/susimail/src/src/i2p/susi/util/CountingInputStream.java
Normal file
50
apps/susimail/src/src/i2p/susi/util/CountingInputStream.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
122
apps/susimail/src/src/i2p/susi/util/DecodingOutputStream.java
Normal file
122
apps/susimail/src/src/i2p/susi/util/DecodingOutputStream.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
29
apps/susimail/src/src/i2p/susi/util/DummyOutputStream.java
Normal file
29
apps/susimail/src/src/i2p/susi/util/DummyOutputStream.java
Normal file
@ -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() {}
|
||||
}
|
194
apps/susimail/src/src/i2p/susi/util/EOFOnMatchInputStream.java
Normal file
194
apps/susimail/src/src/i2p/susi/util/EOFOnMatchInputStream.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
@ -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("<br>\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() {}
|
||||
}
|
82
apps/susimail/src/src/i2p/susi/util/EscapeHTMLWriter.java
Normal file
82
apps/susimail/src/src/i2p/susi/util/EscapeHTMLWriter.java
Normal file
@ -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 = "<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() {}
|
||||
}
|
108
apps/susimail/src/src/i2p/susi/util/FileBuffer.java
Normal file
108
apps/susimail/src/src/i2p/susi/util/FileBuffer.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
105
apps/susimail/src/src/i2p/susi/util/GzipFileBuffer.java
Normal file
105
apps/susimail/src/src/i2p/susi/util/GzipFileBuffer.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
69
apps/susimail/src/src/i2p/susi/util/LimitInputStream.java
Normal file
69
apps/susimail/src/src/i2p/susi/util/LimitInputStream.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
91
apps/susimail/src/src/i2p/susi/util/MemoryBuffer.java
Normal file
91
apps/susimail/src/src/i2p/susi/util/MemoryBuffer.java
Normal file
@ -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");
|
||||
}
|
||||
}
|
64
apps/susimail/src/src/i2p/susi/util/OutputStreamBuffer.java
Normal file
64
apps/susimail/src/src/i2p/susi/util/OutputStreamBuffer.java
Normal file
@ -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";
|
||||
}
|
||||
}
|
@ -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) : "";
|
||||
|
14
apps/susimail/src/src/i2p/susi/util/ReadCounter.java
Normal file
14
apps/susimail/src/src/i2p/susi/util/ReadCounter.java
Normal file
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<String, Mail> 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<String, Mail>();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<MailPart> 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<MailPart>();
|
||||
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 ? "<br>\r\n" : "\r\n";
|
||||
|
||||
|
||||
if( html ) {
|
||||
out.println( "<!-- " );
|
||||
out.println( "Debug: Showing Mail Part with hash code " + mailPart.hashCode());
|
||||
out.println( "Debug: Showing Mail Part at level " + level + " with hash code " + mailPart.hashCode());
|
||||
out.println( "Debug: Mail Part headers follow");
|
||||
for( int i = 0; i < mailPart.headerLines.length; i++ ) {
|
||||
// fix Content-Type: multipart/alternative; boundary="----------8CDE39ECAF2633"
|
||||
out.println( mailPart.headerLines[i].replace("--", "—") );
|
||||
out.println( mailPart.headerLines[i].replace("--", "--") );
|
||||
}
|
||||
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( "<tr class=\"mailbody\"><td colspan=\"2\" align=\"center\">" );
|
||||
if( reason != null && reason.length() > 0 ) {
|
||||
if( html )
|
||||
out.println( "<p class=\"info\">");
|
||||
out.println( reason );
|
||||
if( html )
|
||||
out.println( "</p>" );
|
||||
reason = "";
|
||||
}
|
||||
if( showBody ) {
|
||||
if( html )
|
||||
out.println( "<tr class=\"mailbody\"><td colspan=\"2\" align=\"center\">" );
|
||||
if( showBody ) {
|
||||
if( html )
|
||||
out.println( "<p class=\"mailbody\">" );
|
||||
out.println( body.toString() );
|
||||
out.println( "<p class=\"mailbody\"><br>" );
|
||||
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( "<br></p>" );
|
||||
}
|
||||
if( reason != null && reason.length() > 0 ) {
|
||||
// FIXME css has -32 margin
|
||||
if( html )
|
||||
out.println( "<p class=\"info\">");
|
||||
out.println( reason );
|
||||
if( html )
|
||||
out.println( "</p>" );
|
||||
}
|
||||
@ -758,7 +771,7 @@ public class WebMail extends HttpServlet
|
||||
}
|
||||
if( html ) {
|
||||
out.println( "<!-- " );
|
||||
out.println( "Debug: End of Mail Part with hash code " + mailPart.hashCode());
|
||||
out.println( "Debug: End of Mail Part at level " + level + " with hash code " + mailPart.hashCode());
|
||||
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<String>();
|
||||
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("<input type=\"hidden\" name=\"" + CURRENT_SORT + "\" value=\"" + fullSort + "\">");
|
||||
out.println("<input type=\"hidden\" name=\"" + CURRENT_FOLDER + "\" value=\"" + PersistentMailCache.DIR_FOLDER + "\">");
|
||||
}
|
||||
if( sessionObject.error != null && sessionObject.error.length() > 0 ) {
|
||||
out.println( "<div class=\"notifications\" onclick=\"this.remove()\"><p class=\"error\">" + quoteHTML(sessionObject.error).replace("\n", "<br>") + "</p></div>" );
|
||||
@ -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( "Debug: Mail header and body follow");
|
||||
ReadBuffer body = mail.getBody();
|
||||
out.println(quoteHTML(new String(body.content, body.offset, body.length)).replace("--", "—"));
|
||||
Buffer body = mail.getBody();
|
||||
InputStream in = null;
|
||||
OutputStream sout = null;
|
||||
try {
|
||||
in = body.getInputStream();
|
||||
sout = new EscapeHTMLOutputStream(new WriterOutputStream(out));
|
||||
DataHelper.copy(in, sout);
|
||||
} catch (IOException ioe) {
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
if (sout != null) try { sout.close(); } catch (IOException ioe) {}
|
||||
body.readComplete(true);
|
||||
}
|
||||
out.println( "-->" );
|
||||
}
|
||||
out.println("<div class=\"topbuttons\">");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,11 @@ public class DecodingException extends IOException {
|
||||
super( msg );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 0.9.34
|
||||
*/
|
||||
public DecodingException(String msg, Exception cause)
|
||||
{
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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", "<br>\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");
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/*****
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<String> 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 */
|
||||
|
@ -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<Attachment> attachments, String boundary)
|
||||
{
|
||||
boolean mailSent = false;
|
||||
|
@ -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
|
||||
|
@ -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 = "";
|
||||
|
Reference in New Issue
Block a user