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