diff --git a/src/scratchpad/src/org/apache/poi/hmef/Attachment.java b/src/scratchpad/src/org/apache/poi/hmef/Attachment.java index 1d74ccc74..e961481a1 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/Attachment.java +++ b/src/scratchpad/src/org/apache/poi/hmef/Attachment.java @@ -18,12 +18,16 @@ package org.apache.poi.hmef; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.apache.poi.hmef.attribute.MAPIAttribute; +import org.apache.poi.hmef.attribute.MAPIStringAttribute; import org.apache.poi.hmef.attribute.TNEFAttribute; +import org.apache.poi.hmef.attribute.TNEFDateAttribute; import org.apache.poi.hmef.attribute.TNEFMAPIAttribute; import org.apache.poi.hmef.attribute.TNEFProperty; +import org.apache.poi.hmef.attribute.TNEFStringAttribute; import org.apache.poi.hsmf.datatypes.MAPIProperty; @@ -87,8 +91,68 @@ public final class Attachment { return mapiAttributes; } + + /** + * Return the string value of the mapi property, or null + * if it isn't set + */ + private String getString(MAPIProperty id) { + return MAPIStringAttribute.getAsString( getMessageMAPIAttribute(id) ); + } + /** + * Returns the string value of the TNEF property, or + * null if it isn't set + */ + private String getString(TNEFProperty id) { + return TNEFStringAttribute.getAsString( getMessageAttribute(id) ); + } + + /** + * Returns the short filename + */ public String getFilename() { - TNEFAttribute attr = null; - return null; + return getString(TNEFProperty.ID_ATTACHTITLE); + } + /** + * Returns the long filename + */ + public String getLongFilename() { + return getString(MAPIProperty.ATTACH_LONG_FILENAME); + } + /** + * Returns the file extension + */ + public String getExtension() { + return getString(MAPIProperty.ATTACH_EXTENSION); + } + + /** + * Return when the file was last modified, if known. + */ + public Date getModifiedDate() { + return TNEFDateAttribute.getAsDate( + getMessageAttribute(TNEFProperty.ID_ATTACHMODIFYDATE) + ); + } + + /** + * Returns the contents of the attachment. + */ + public byte[] getContents() { + TNEFAttribute contents = getMessageAttribute(TNEFProperty.ID_ATTACHDATA); + if(contents == null) { + throw new IllegalArgumentException("Attachment corrupt - no Data section"); + } + return contents.getData(); + } + + /** + * Returns the Meta File rendered representation + * of the attachment, or null if not set. + */ + public byte[] getRenderedMetaFile() { + TNEFAttribute meta = getMessageAttribute(TNEFProperty.ID_ATTACHMETAFILE); + if(meta == null) return null; + return meta.getData(); } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java b/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java index c23718ee6..7da39f9c6 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java +++ b/src/scratchpad/src/org/apache/poi/hmef/HMEFMessage.java @@ -155,19 +155,7 @@ public final class HMEFMessage { * if it isn't set */ private String getString(MAPIProperty id) { - MAPIAttribute attr = getMessageMAPIAttribute(id); - if(id == null) { - return null; - } - if(attr instanceof MAPIStringAttribute) { - return ((MAPIStringAttribute)attr).getDataString(); - } - if(attr instanceof MAPIRtfAttribute) { - return ((MAPIRtfAttribute)attr).getDataString(); - } - - System.err.println("Warning, no string property found: " + attr.toString()); - return null; + return MAPIStringAttribute.getAsString( getMessageMAPIAttribute(id) ); } /** diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java index 1ebd12114..31c3cbb88 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIAttribute.java @@ -165,6 +165,8 @@ public class MAPIAttribute { MAPIAttribute attr; if(type == Types.UNICODE_STRING || type == Types.ASCII_STRING) { attr = new MAPIStringAttribute(prop, type, data); + } else if(type == Types.APP_TIME || type == Types.TIME) { + attr = new MAPIDateAttribute(prop, type, data); } else if(id == MAPIProperty.RTF_COMPRESSED.id) { attr = new MAPIRtfAttribute(prop, type, data); } else { diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIDateAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIDateAttribute.java new file mode 100644 index 000000000..bf786366d --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIDateAttribute.java @@ -0,0 +1,70 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hmef.attribute; + +import java.util.Date; + +import org.apache.poi.hmef.Attachment; +import org.apache.poi.hmef.HMEFMessage; +import org.apache.poi.hpsf.Util; +import org.apache.poi.hsmf.datatypes.MAPIProperty; +import org.apache.poi.util.LittleEndian; + +/** + * A pure-MAPI attribute holding a Date, which applies + * to a {@link HMEFMessage} or one of its {@link Attachment}s. + */ +public final class MAPIDateAttribute extends MAPIAttribute { + private Date data; + + /** + * Constructs a single new date attribute from the id, type, + * and the contents of the stream + */ + protected MAPIDateAttribute(MAPIProperty property, int type, byte[] data) { + super(property, type, data); + + // The value is a 64 bit Windows Filetime + this.data = Util.filetimeToDate( + LittleEndian.getLong(data, 0) + ); + } + + public Date getDate() { + return this.data; + } + + public String toString() { + return getProperty().toString() + " " + data.toString(); + } + + /** + * Returns the Date of a Attribute, converting as appropriate + */ + public static Date getAsDate(MAPIAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof MAPIDateAttribute) { + return ((MAPIDateAttribute)attr).getDate(); + } + + System.err.println("Warning, non date property found: " + attr.toString()); + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java index e7550de73..d53e59c7d 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/MAPIStringAttribute.java @@ -63,4 +63,22 @@ public final class MAPIStringAttribute extends MAPIAttribute { public String toString() { return getProperty().toString() + " " + data; } + + /** + * Returns the string of a Attribute, converting as appropriate + */ + public static String getAsString(MAPIAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof MAPIStringAttribute) { + return ((MAPIStringAttribute)attr).getDataString(); + } + if(attr instanceof MAPIRtfAttribute) { + return ((MAPIRtfAttribute)attr).getDataString(); + } + + System.err.println("Warning, non string property found: " + attr.toString()); + return null; + } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java index b2152a5e3..af0ac3dcb 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFAttribute.java @@ -70,6 +70,9 @@ public class TNEFAttribute { type == TNEFProperty.TYPE_TEXT) { return new TNEFStringAttribute(id, type, inp); } + if(type == TNEFProperty.TYPE_DATE) { + return new TNEFDateAttribute(id, type, inp); + } return new TNEFAttribute(id, type, inp); } diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFDateAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFDateAttribute.java new file mode 100644 index 000000000..70793899f --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFDateAttribute.java @@ -0,0 +1,91 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hmef.attribute; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +import org.apache.poi.hmef.Attachment; +import org.apache.poi.hmef.HMEFMessage; +import org.apache.poi.hpsf.Util; +import org.apache.poi.util.LittleEndian; + +/** + * A Date attribute which applies to a {@link HMEFMessage} + * or one of its {@link Attachment}s. + */ +public final class TNEFDateAttribute extends TNEFAttribute { + private Date data; + + /** + * Constructs a single new date attribute from the id, type, + * and the contents of the stream + */ + protected TNEFDateAttribute(int id, int type, InputStream inp) throws IOException { + super(id, type, inp); + + byte[] data = getData(); + if(data.length == 8) { + // The value is a 64 bit Windows Filetime + this.data = Util.filetimeToDate( + LittleEndian.getLong(getData(), 0) + ); + } else if(data.length == 14) { + // It's the 7 date fields. We think it's in UTC... + Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + c.set(Calendar.YEAR, LittleEndian.getUShort(data, 0)); + c.set(Calendar.MONTH, LittleEndian.getUShort(data, 2) - 1); // Java months are 0 based! + c.set(Calendar.DAY_OF_MONTH, LittleEndian.getUShort(data, 4)); + c.set(Calendar.HOUR_OF_DAY, LittleEndian.getUShort(data, 6)); + c.set(Calendar.MINUTE, LittleEndian.getUShort(data, 8)); + c.set(Calendar.SECOND, LittleEndian.getUShort(data, 10)); + // The 7th field is day of week, which we don't require + c.set(Calendar.MILLISECOND, 0); // Not set in the file + this.data = c.getTime(); + } else { + throw new IllegalArgumentException("Invalid date, found " + data.length + " bytes"); + } + } + + public Date getDate() { + return this.data; + } + + public String toString() { + return "Attribute " + getProperty().toString() + ", type=" + getType() + + ", date=" + data.toString(); + } + + /** + * Returns the Date of a Attribute, converting as appropriate + */ + public static Date getAsDate(TNEFAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof TNEFDateAttribute) { + return ((TNEFDateAttribute)attr).getDate(); + } + + System.err.println("Warning, non date property found: " + attr.toString()); + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java index 6063c5e2c..983d3f9a2 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java +++ b/src/scratchpad/src/org/apache/poi/hmef/attribute/TNEFStringAttribute.java @@ -25,31 +25,57 @@ import org.apache.poi.hmef.HMEFMessage; import org.apache.poi.util.StringUtil; /** - * An String attribute which applies to a {@link HMEFMessage} + * A String attribute which applies to a {@link HMEFMessage} * or one of its {@link Attachment}s. */ public final class TNEFStringAttribute extends TNEFAttribute { + private String data; + /** * Constructs a single new string attribute from the id, type, * and the contents of the stream */ protected TNEFStringAttribute(int id, int type, InputStream inp) throws IOException { super(id, type, inp); + + String tmpData = null; + byte[] data = getData(); + if(getType() == TNEFProperty.TYPE_TEXT) { + tmpData = StringUtil.getFromUnicodeLE(data); + } else { + tmpData = StringUtil.getFromCompressedUnicode( + data, 0, data.length + ); + } + + // Strip off the null terminator if present + if(tmpData.endsWith("\0")) { + tmpData = tmpData.substring(0, tmpData.length()-1); + } + this.data = tmpData; } public String getString() { - byte[] data = getData(); - // TODO Verify if these are the right way around - if(getType() == TNEFProperty.TYPE_TEXT) { - return StringUtil.getFromUnicodeLE(data); - } - return StringUtil.getFromCompressedUnicode( - data, 0, data.length - ); + return this.data; } public String toString() { return "Attribute " + getProperty().toString() + ", type=" + getType() + ", data=" + getString(); } + + /** + * Returns the string of a Attribute, converting as appropriate + */ + public static String getAsString(TNEFAttribute attr) { + if(attr == null) { + return null; + } + if(attr instanceof TNEFStringAttribute) { + return ((TNEFStringAttribute)attr).getString(); + } + + System.err.println("Warning, non string property found: " + attr.toString()); + return null; + } } diff --git a/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java b/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java index e1ad196ee..7addb6b84 100644 --- a/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java +++ b/src/scratchpad/src/org/apache/poi/hmef/dev/HMEFDumper.java @@ -25,7 +25,9 @@ import java.util.List; import org.apache.poi.hmef.HMEFMessage; import org.apache.poi.hmef.attribute.TNEFAttribute; import org.apache.poi.hmef.attribute.MAPIAttribute; +import org.apache.poi.hmef.attribute.TNEFDateAttribute; import org.apache.poi.hmef.attribute.TNEFProperty; +import org.apache.poi.hmef.attribute.TNEFStringAttribute; import org.apache.poi.util.HexDump; import org.apache.poi.util.LittleEndian; @@ -108,6 +110,14 @@ public final class HMEFDumper { // Print the contents String indent = " "; + + if(attr instanceof TNEFStringAttribute) { + System.out.println(indent + indent + indent + ((TNEFStringAttribute)attr).getString()); + } + if(attr instanceof TNEFDateAttribute) { + System.out.println(indent + indent + indent + ((TNEFDateAttribute)attr).getDate()); + } + System.out.println(indent + "Data of length " + attr.getData().length); if(attr.getData().length > 0) { int len = attr.getData().length; diff --git a/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java b/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java index f5ddb8aeb..13a82024a 100644 --- a/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java +++ b/src/scratchpad/testcases/org/apache/poi/hmef/TestAttachments.java @@ -17,30 +17,67 @@ package org.apache.poi.hmef; +import java.io.IOException; +import java.text.DateFormat; +import java.util.List; +import java.util.Locale; + import junit.framework.TestCase; import org.apache.poi.POIDataSamples; +import org.apache.poi.util.IOUtils; public final class TestAttachments extends TestCase { private static final POIDataSamples _samples = POIDataSamples.getHMEFInstance(); + private HMEFMessage quick; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + quick = new HMEFMessage( + _samples.openResourceAsStream("quick-winmail.dat") + ); + } /** * Check the file is as we expect */ public void testCounts() throws Exception { - HMEFMessage msg = new HMEFMessage( - _samples.openResourceAsStream("quick-winmail.dat") - ); - // Should have 5 attachments - assertEquals(5, msg.getAttachments().size()); + assertEquals(5, quick.getAttachments().size()); } /** * Check some basic bits about the attachments */ public void testBasicAttachments() throws Exception { - // TODO + List attachments = quick.getAttachments(); + + // Word first + assertEquals("quick.doc", attachments.get(0).getFilename()); + assertEquals("quick.doc", attachments.get(0).getLongFilename()); + assertEquals(".doc", attachments.get(0).getExtension()); + + // Then HTML + assertEquals("QUICK~1.HTM", attachments.get(1).getFilename()); + assertEquals("quick.html", attachments.get(1).getLongFilename()); + assertEquals(".html", attachments.get(1).getExtension()); + + // Then PDF + assertEquals("quick.pdf", attachments.get(2).getFilename()); + assertEquals("quick.pdf", attachments.get(2).getLongFilename()); + assertEquals(".pdf", attachments.get(2).getExtension()); + + // Then Text + assertEquals("quick.txt", attachments.get(3).getFilename()); + assertEquals("quick.txt", attachments.get(3).getLongFilename()); + assertEquals(".txt", attachments.get(3).getExtension()); + + // And finally XML + assertEquals("quick.xml", attachments.get(4).getFilename()); + assertEquals("quick.xml", attachments.get(4).getLongFilename()); + assertEquals(".xml", attachments.get(4).getExtension()); } /** @@ -48,13 +85,52 @@ public final class TestAttachments extends TestCase { * the right values for key things */ public void testAttachmentDetails() throws Exception { - // TODO + List attachments = quick.getAttachments(); + + DateFormat fmt = DateFormat.getDateTimeInstance( + DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.UK + ); + + // They should all have the same date on them + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(0).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(1).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(2).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(3).getModifiedDate())); + assertEquals("28-Apr-2010 13:40:56", fmt.format( attachments.get(4).getModifiedDate())); + + // They should all have a 3512 byte metafile rendered version + assertEquals(3512, attachments.get(0).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(1).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(2).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(3).getRenderedMetaFile().length); + assertEquals(3512, attachments.get(4).getRenderedMetaFile().length); } /** * Ensure the attachment contents come back as they should do */ public void testAttachmentContents() throws Exception { - // TODO + List attachments = quick.getAttachments(); + + assertContents("quick.doc", attachments.get(0)); + assertContents("quick.html", attachments.get(1)); + assertContents("quick.pdf", attachments.get(2)); + assertContents("quick.txt", attachments.get(3)); + assertContents("quick.xml", attachments.get(4)); + } + + private void assertContents(String filename, Attachment attachment) + throws IOException { + assertEquals(filename, attachment.getLongFilename()); + + byte[] expected = IOUtils.toByteArray( + _samples.openResourceAsStream("quick-contents/" + filename) + ); + byte[] actual = attachment.getContents(); + + assertEquals(expected.length, actual.length); + for(int i=0; i 0 && !sampleFileName.equals(f.getCanonicalFile().getName())){ - throw new RuntimeException("File name is case-sensitive: requested '" + sampleFileName + if(sampleFileName.length() > 0) { + String fn = sampleFileName; + if(fn.indexOf('/') > 0) { + fn = fn.substring(fn.indexOf('/')+1); + } + if(!fn.equals(f.getCanonicalFile().getName())){ + throw new RuntimeException("File name is case-sensitive: requested '" + fn + "' but actual file is '" + f.getCanonicalFile().getName() + "'"); + } } } catch (IOException e){ throw new RuntimeException(e);