diff --git a/src/java/davmail/DavGateway.java b/src/java/davmail/DavGateway.java index 507cda1f..c8ab6421 100644 --- a/src/java/davmail/DavGateway.java +++ b/src/java/davmail/DavGateway.java @@ -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(); diff --git a/src/java/davmail/Settings.java b/src/java/davmail/Settings.java index 576bfb51..9846386a 100644 --- a/src/java/davmail/Settings.java +++ b/src/java/davmail/Settings.java @@ -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)); diff --git a/src/java/davmail/exchange/BASE64EncoderStream.java b/src/java/davmail/exchange/BASE64EncoderStream.java new file mode 100644 index 00000000..f17c9a52 --- /dev/null +++ b/src/java/davmail/exchange/BASE64EncoderStream.java @@ -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 + * RFC 2045, section + * 6.8, and outputs the encoded data to the underlying + * OutputStream. + * + * @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 max characters that are output to the + * underlying stream, a CRLF sequence ('\r', + * '\n') 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 ('\r', '\n') 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 len bytes from the given byte array starting + * at offset off, 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 + * '=' is used for this purpose. See + * RFC 2045, 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() + + /** + * For testing. 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 System.out. + * + * @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(); + } + } + +} \ No newline at end of file diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index 15e50141..9c172f04 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -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 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 imgList = xmlDocument.getNodes("//img/@src"); + List 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); } } }