fix inline images support (use both owa generated page and htmlBody), optimize Base64 performance

git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@13 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
mguessan 2006-12-15 16:44:31 +00:00
parent f3ed595bab
commit 98868aa199
4 changed files with 452 additions and 25 deletions

View File

@ -15,11 +15,10 @@ public class DavGateway {
*/
public static void main(String[] args) {
String configFilePath = System.getProperty("user.home") + "/.davmail.properties";
if (args.length >= 1) {
configFilePath = args[0];
Settings.setConfigFilePath(args[0]);
}
Settings.setConfigFilePath(configFilePath);
Settings.load();
DavGatewayTray.init();

View File

@ -19,6 +19,9 @@ public class Settings {
public static synchronized void load() {
try {
if (configFilePath == null) {
configFilePath = System.getProperty("user.home") + "/.davmail.properties";
}
File configFile = new File(configFilePath);
if (configFile.exists()) {
settings.load(new FileReader(configFile));

View File

@ -0,0 +1,394 @@
package davmail.exchange;
// Imports
import java.io.OutputStream;
import java.io.FilterOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
/**
* Encodes the input data using the BASE64 transformation as specified in
* <A HREF="http://www.faqs.org/rfcs/rfc2045.html">RFC 2045</A>, section
* 6.8, and outputs the encoded data to the underlying
* <code>OutputStream</code>.
*
* @author David A. Herman
* @version 1.0 of September 2000
* @see java.io.FilterOutputStream
**/
public class BASE64EncoderStream extends FilterOutputStream {
/**
* Useful constant representing the default maximum number of output
* characters per line (76).
**/
public static final int LINE_LENGTH = 76;
/**
* The BASE64 alphabet.
**/
private static final byte[] alphabet;
/**
* Fills the BASE64 alphabet table with the ASCII byte values of
* the characters.
**/
static {
try {
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".getBytes("US-ASCII");
}
catch (UnsupportedEncodingException e) {
throw new RuntimeException("ASCII character encoding not supported.");
}
}
/**
* The internal buffer of encoded output bytes.
**/
private byte output[] = new byte[4];
/**
* The internal buffer of input bytes to be encoded.
**/
private byte input[] = new byte[3];
/**
* The index of the next position in the internal buffer of input bytes
* at which to store input.
**/
private int inputIndex = 0;
/**
* The number of characters that have been output on the current line.
**/
private int chars = 0;
/**
* The maximum number of characters to output per line.
**/
private int maxLineLength;
/**
* The index into the BASE64 alphabet to generate the next encoded
* character of output data. This index is generated as input data comes
* in, sometimes requiring more than one byte of input before it is
* completely calculated, so it is shared in the object.
**/
private int index;
/**
* Builds a BASE64 encoding stream on top of the given underlying output
* stream, with the default maximum number of characters per line.
**/
public BASE64EncoderStream(OutputStream out) {
this(out, LINE_LENGTH);
}
/**
* Builds a BASE64 encoding stream on top of the given underlying output
* stream, with the specified maximum number of characters per line. For
* For every <code>max</code> characters that are output to the
* underlying stream, a CRLF sequence (<code>'\r'</code>,
* <code>'\n'</code>) is written.
*
* @param out the underlying output stream.
* @param max the maximum number of output bytes per line.
**/
public BASE64EncoderStream(OutputStream out, int max) {
super(out);
maxLineLength = max;
}
/**
* Completes the encoding of data, padding the input data if necessary
* to end the input on a multiple of 4 bytes, writes a terminating
* CRLF sequence (<code>'\r'</code>, <code>'\n'</code>) to the
* underlying output stream, and closes the underlying output stream.
*
* @throws IOException if an I/O error occurs.
**/
public void close() throws IOException {
try {
flush();
}
catch (IOException ignored) {
}
// Make sure the number of bytes output is a multiple of three.
pad();
// Add a terminating CRLF sequence.
out.write('\r');
out.write('\n');
// Close the underlying output stream.
out.close();
}
/**
* Encodes the given byte array, to be written to the underlying output
* stream.
*
* @param b the byte array to be encoded.
* @throws IOException if an I/O error occurs.
**/
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
/**
* Encodes <code>len</code> bytes from the given byte array starting
* at offset <code>off</code>, to be written to the underlying output
* stream.
*
* @param b the byte array to be encoded.
* @param off the offset at which to start reading from the byte array.
* @param len the number of bytes to read.
* @throws IOException if an I/O error occurs.
**/
public void write(byte b[], int off, int len) throws IOException {
for (int i = 0; i < len; i++) {
write(b[off + i]);
}
}
/**
* Encodes the 8 low-order bits of the given integer, to be written to
* the underlying output stream. The 24 high-order bits are discarded.
* If the internal buffer of encoded data is filled upon appending the
* encoded data to it, the buffer is written to the underlying output
* stream.
*
* @param b the integer whose low-order byte is to be encoded.
* @throws IOException if an I/O error occurs.
**/
public void write(int b) throws IOException {
switch (inputIndex) {
case 0:
// The first output character generates its
// index from the first six bits of the first byte.
//
// Input: XXXXXXoo oooooooo oooooooo
// Mask: 11111100 &
// ----------------------------
// Output: 00 XXXXXX
input[0] = (byte)(b & 0xFF);
index = ((input[0] & 0xFC) >> 2);
output[0] = alphabet[index];
// Pre-calculate the first two bits of the
// second output character. If this turns out
// to be the last byte of input, then it will
// already be padded with zeroes, and the rest
// can be padded with '=' characters.
index = ((input[0] & 0x03) << 4);
break;
case 1:
// The second output character generates its
// index from the last two bits of the first
// byte and the first four bits of the second.
//
// Input: ooooooXX YYYYoooo oooooooo
// Mask: 00000011 11110000 &
// ----------------------------
// Output: 00 XX YYYY
input[1] = (byte)(b & 0xFF);
// The first two bits of the second output character
// have already been calculated and stored in the
// member variable 'index'. Add the last four bits
// to the index and generate the output character.
index += ((input[1] & 0xF0) >> 4);
output[1] = alphabet[index];
// Pre-calculate the first four bits of the
// third output character. If this turns out
// to be the last byte of input, then it will
// already be padded with zeroes, and the rest
// can be padded with '=' characters.
index = ((input[1] & 0x0F) << 2);
break;
case 2:
// The third output character generates its
// index from the last four bits of the second
// byte and the first two bits of the third.
//
// Input: oooooooo ooooXXXX YYoooooo
// Mask: 00001111 11000000 &
// ----------------------------
// Output: 00 XXXX YY
input[2] = (byte)(b & 0xFF);
// The first four bits of the third output character
// have already been calculated and stored in the
// member variable 'index'. Add the last two bits
// to the index and generate the output character.
index += ((input[2] & 0xC0) >> 6);
output[2] = alphabet[index];
// The fourth output character generates its
// index from the last six bits of the third byte.
//
// Input: oooooooo oooooooo ooXXXXXX
// Mask: 00111111 &
// ----------------------------
// Output: 00 XXXXXX
index = (b & 0x3F);
output[3] = alphabet[index];
break;
}
inputIndex = ((inputIndex + 1) % 3);
// If the internal buffer is filled, write its contents to the
// underlying output stream.
if (inputIndex == 0) {
writeOutput();
}
}
/**
* Writes the internal buffer of encoded output bytes to the underlying
* output stream. This method is called whenever the 4-byte internal
* buffer is filled.
*
* @throws IOException if an I/O error occurs.
**/
private void writeOutput() throws IOException {
int newchars = (chars + 4) % maxLineLength;
if (newchars == 0) {
out.write(output);
out.write('\r');
out.write('\n');
}
else if (newchars < chars) {
out.write(output, 0, 4 - newchars);
out.write('\r');
out.write('\n');
out.write(output, 4 - newchars, newchars);
}
else
out.write(output);
chars = newchars;
}
/**
* Pads the encoded data to a multiple of 4 bytes, if necessary. Since
* BASE64 encodes every 3 bytes as 4 bytes of text, if the input is not
* a multiple of 3, the end of the input data must be padded in order
* to send a final quantum of 4 bytes. The BASE64 special character
* <code>'='</code> is used for this purpose. See
* <A HREF="http://www.faqs.org/rfcs/rfc2045.html">RFC 2045</A>, section
* 6.8, for more information.
*
* @throws IOException if an I/O error occurs.
**/
private void pad() throws IOException {
// If the input index is 0, then we ended on a multiple of 3 bytes
// of input, so no padding is necessary.
if (inputIndex > 0) {
// If the input index is 1, then the input text is equivalent
// to 1 modulus 3 bytes, so two input bytes need to be padded.
// We pad the final two output bytes as '=' characters.
if (inputIndex == 1) {
output[1] = alphabet[index];
output[2] = alphabet[64];
output[3] = alphabet[64];
}
// If the input index is 2, then the input text is equivalent
// to 2 modulus 3 bytes, so one input byte needs to be padded.
// We pad the final output byte as a '=' character.
else if (inputIndex == 2) {
output[2] = alphabet[index];
output[3] = alphabet[64];
}
// This is unnecessary, but just for the sake of clarity.
inputIndex = 0;
writeOutput();
}
}
public static byte[] encode(byte[] bytes) {
// Note: This is a public method on Sun's implementation
// and so it should be supported for compatibility.
// Also this method is used by the "B" encoding for now.
// This implementation usesthe encoding stream to
// process the bytes. Possibly, the BASE64 encoding
// stream should use this method for it's encoding.
// Variables
ByteArrayOutputStream byteStream;
BASE64EncoderStream encoder;
// Create Streams
byteStream = new ByteArrayOutputStream();
encoder = new BASE64EncoderStream(byteStream);
try {
// Write Bytes
encoder.write(bytes);
encoder.flush();
encoder.close();
} catch (IOException e) {
} // try
// Return Encoded Byte Array
return byteStream.toByteArray();
} // encode()
/**
* <I>For testing.</I> Takes in a file name from the command line, or
* prompts the user for one if there are no command line options, and
* encodes the data in the file to BASE64, outputting the encoded
* data to <code>System.out</code>.
*
* @param args the command line arguments.
**/
public static void main(String args[]) {
String fileName = "";
if (args.length > 0)
fileName = args[0];
else {
System.out.print("File name: ");
java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
try {
fileName = in.readLine();
}
catch (Throwable e) {
e.printStackTrace();
System.exit(1);
}
}
try {
java.io.FileInputStream in = new java.io.FileInputStream(fileName);
BASE64EncoderStream out = new BASE64EncoderStream(System.out);
int d = in.read();
while (d != -1) {
out.write(d);
d = in.read();
}
in.close();
out.close();
}
catch (Throwable e) {
e.printStackTrace();
}
}
}

View File

@ -601,7 +601,8 @@ public class ExchangeSession {
boundary = "----_=_NextPart_001_" + uid;
String contentType = "multipart/mixed";
// use multipart/related with inline images
if (htmlBody != null && htmlBody.indexOf("src=\"cid:") > 0) {
if (htmlBody != null && (htmlBody.indexOf("src=\"cid:") > 0 ||
htmlBody.indexOf("src=\"1_multipart") > 0)) {
contentType = "multipart/related";
}
line = CONTENT_TYPE_HEADER + contentType + ";\n\tboundary=\"" + boundary + "\"";
@ -662,7 +663,14 @@ public class ExchangeSession {
.append(htmlBody.substring(attachmentIdStartIndex + 4, attachmentIdEndIndex))
.append(">");
}
} else if (htmlBody.indexOf("src=\"1_multipart/" + attachmentName) >= 0) {
// detect html body without cid in image link
result.append("\nContent-ID: <")
.append(attachmentName)
.append(">");
}
result.append("\nContent-Transfer-Encoding: ").append(attachmentContentEncoding)
.append("\n\n");
}
@ -763,7 +771,12 @@ public class ExchangeSession {
try {
OutputStream quotedOs;
try {
quotedOs = (MimeUtility.encode(os, mimeHeader.contentTransferEncoding));
// try another base64Encoder implementation
if ("base64".equalsIgnoreCase(mimeHeader.contentTransferEncoding)) {
quotedOs = new BASE64EncoderStream(os);
} else {
quotedOs = (MimeUtility.encode(os, mimeHeader.contentTransferEncoding));
}
} catch (MessagingException e) {
throw new IOException(e.getMessage());
}
@ -782,7 +795,7 @@ public class ExchangeSession {
attachedMessage.write(quotedOs);
} else {
GetMethod method = new GetMethod(URIUtil.encodePathQuery(decodedPath));
GetMethod method = new GetMethod(URIUtil.encodePath(decodedPath));
wdr.retrieveSessionInstance().executeMethod(method);
// encode attachment
@ -942,10 +955,33 @@ public class ExchangeSession {
} catch (Exception e) {
throw new RuntimeException("Exception retrieving " + attachmentUrl + " : " + e + " " + e.getCause());
}
// fix content type for known extension
if ("application/octet-stream".equals(result) && attachmentUrl.endsWith(".pdf")) {
result = "application/pdf";
}
return result;
}
protected XmlDocument tidyDocument(InputStream inputStream) {
Tidy tidy = new Tidy();
tidy.setXmlTags(false); //treat input not XML
tidy.setQuiet(true);
tidy.setShowWarnings(false);
tidy.setDocType("omit");
DOMBuilder builder = new DOMBuilder();
XmlDocument xmlDocument = new XmlDocument();
try {
xmlDocument.load(builder.build(tidy.parseDOM(inputStream, null)));
} catch (IOException ex1) {
logger.error("Exception parsing document", ex1);
} catch (JDOMException ex1) {
logger.error("Exception parsing document", ex1);
}
return xmlDocument;
}
public Map<String, String> getAttachmentsUrls(String messageUrl) throws IOException {
if (attachmentsMap != null) {
// do not load attachments twice
@ -959,23 +995,7 @@ public class ExchangeSession {
+ " " + getMethod.getStatusLine());
}
InputStream in = getMethod.getResponseBodyAsStream();
Tidy tidy = new Tidy();
tidy.setXmlTags(false); //treat input not XML
tidy.setQuiet(true);
tidy.setShowWarnings(false);
tidy.setDocType("omit");
DOMBuilder builder = new DOMBuilder();
XmlDocument xmlDocument = new XmlDocument();
try {
xmlDocument.load(builder.build(tidy.parseDOM(in, null)));
} catch (IOException ex1) {
ex1.printStackTrace();
} catch (JDOMException ex1) {
ex1.printStackTrace();
}
XmlDocument xmlDocument = tidyDocument(getMethod.getResponseBodyAsStream());
// Release the connection.
getMethod.releaseConnection();
@ -1018,24 +1038,35 @@ public class ExchangeSession {
}
}
// use htmlBody and owa generated body to look for inline images
ByteArrayInputStream bais = new ByteArrayInputStream(htmlBody.getBytes("UTF-8"));
XmlDocument xmlBody = tidyDocument(bais);
// get inline images
List<Attribute> imgList = xmlDocument.getNodes("//img/@src");
List<Attribute> imgList = xmlBody.getNodes("//img/@src");
imgList.addAll(xmlDocument.getNodes("//img/@src"));
for (Attribute element : imgList) {
String attachmentHref = element.getValue();
if (attachmentHref.startsWith("1_multipart")) {
attachmentHref = URIUtil.decode(attachmentHref);
attachmentHref = URIUtil.decode(attachmentHref);
if (attachmentHref.endsWith("?Security=3")) {
attachmentHref = attachmentHref.substring(0, attachmentHref.indexOf('?'));
}
String attachmentName = attachmentHref.substring(attachmentHref.lastIndexOf('/') + 1);
// handle strange cases
if (attachmentName.charAt(1) == '_') {
attachmentName = attachmentName.substring(2);
}
if (attachmentName.startsWith("%31_multipart%3F2_")) {
attachmentName = attachmentName.substring(18);
}
// exclude inline external images
if (!attachmentHref.startsWith("http://") && !attachmentHref.startsWith("https://")) {
attachmentsMap.put(attachmentName, messageUrl + "/" + attachmentHref);
logger.debug("Inline image attachment " + attachmentIndex + " : " + attachmentName);
attachmentsMap.put(String.valueOf(attachmentIndex++), attachmentHref);
// fix html body
htmlBody = htmlBody.replaceFirst(attachmentHref, "cid:" + attachmentName);
}
}
}