diff --git a/src/java/org/apache/poi/util/LittleEndian.java b/src/java/org/apache/poi/util/LittleEndian.java index c07c4beb30..6ceb12a4a1 100644 --- a/src/java/org/apache/poi/util/LittleEndian.java +++ b/src/java/org/apache/poi/util/LittleEndian.java @@ -724,6 +724,24 @@ public class LittleEndian implements LittleEndianConsts } return ( ch4 << 24 ) + ( ch3 << 16 ) + ( ch2 << 8 ) + ( ch1 << 0 ); } + + /** + * get an unsigned int value from an InputStream + * + * @param stream + * the InputStream from which the int is to be read + * @return the unsigned int (32-bit) value + * @exception IOException + * will be propagated back to the caller + * @exception BufferUnderrunException + * if the stream cannot provide enough bytes + */ + public static long readUInt( InputStream stream ) throws IOException, + BufferUnderrunException + { + long retNum = readInt(stream); + return retNum & 0x00FFFFFFFFl; + } /** * get a long value from an InputStream diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MAPIProperty.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MAPIProperty.java index 2f43c6bf9e..a5ee6cdca5 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MAPIProperty.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MAPIProperty.java @@ -1026,6 +1026,8 @@ public class MAPIProperty { new MAPIProperty(-1, Types.UNKNOWN, "Unknown", null); // 0x8??? ones are outlook specific, and not standard MAPI + // TODO See http://msdn.microsoft.com/en-us/library/ee157150%28v=exchg.80%29 for some + // info on how we might decode them properly in the future private static final int ID_FIRST_CUSTOM = 0x8000; private static final int ID_LAST_CUSTOM = 0xFFFE; diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MessagePropertiesChunk.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MessagePropertiesChunk.java new file mode 100644 index 0000000000..ab9b1bfaf5 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/MessagePropertiesChunk.java @@ -0,0 +1,89 @@ +/* ==================================================================== + 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.hsmf.datatypes; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.poi.util.LittleEndian; + +/** + * A {@link PropertiesChunk} for a Message or Embedded-Message. + * This has a 32 byte header + */ +public class MessagePropertiesChunk extends PropertiesChunk { + private long nextRecipientId; + private long nextAttachmentId; + private long recipientCount; + private long attachmentCount; + + public MessagePropertiesChunk() { + super(); + } + + public long getNextRecipientId() { + return nextRecipientId; + } + public long getNextAttachmentId() { + return nextAttachmentId; + } + + public long getRecipientCount() { + return recipientCount; + } + public long getAttachmentCount() { + return attachmentCount; + } + + @Override + public void readValue(InputStream stream) throws IOException { + // 8 bytes of reserved zeros + LittleEndian.readLong(stream); + + // Nexts and counts + nextRecipientId = LittleEndian.readUInt(stream); + nextAttachmentId = LittleEndian.readUInt(stream); + recipientCount = LittleEndian.readUInt(stream); + attachmentCount = LittleEndian.readUInt(stream); + + // 8 bytes of reserved zeros + LittleEndian.readLong(stream); + + // Now properties + readProperties(stream); + } + + @Override + public void writeValue(OutputStream out) throws IOException { + // 8 bytes of reserved zeros + out.write(new byte[8]); + + // Nexts and counts + LittleEndian.putUInt(nextRecipientId, out); + LittleEndian.putUInt(nextAttachmentId, out); + LittleEndian.putUInt(recipientCount, out); + LittleEndian.putUInt(attachmentCount, out); + + // 8 bytes of reserved zeros + out.write(new byte[8]); + + // Now properties + writeProperties(out); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/NameIdChunks.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/NameIdChunks.java index cbcce93d12..bb78ea308e 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/NameIdChunks.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/NameIdChunks.java @@ -26,7 +26,7 @@ import java.util.List; * NameID part of an outlook file */ public final class NameIdChunks implements ChunkGroup { - public static final String PREFIX = "__nameid_version1.0"; + public static final String NAME = "__nameid_version1.0"; /** Holds all the chunks that were found. */ private List allChunks = new ArrayList(); diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/PropertiesChunk.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/PropertiesChunk.java index 846c3d213e..b83ae7eb46 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/PropertiesChunk.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/PropertiesChunk.java @@ -20,31 +20,68 @@ package org.apache.poi.hsmf.datatypes; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * A Chunk which holds fixed-length properties, and pointer - * to the variable length ones (which get their own chunk) + * to the variable length ones (which get their own chunk). + * There are two kinds of PropertiesChunks, which differ only in + * their headers. */ -public class PropertiesChunk extends Chunk { - public static final String PREFIX = "__properties_version1.0"; +public abstract class PropertiesChunk extends Chunk { + public static final String NAME = "__properties_version1.0"; + + /** + * Holds properties, indexed by type. Properties can be multi-valued + */ + private Map> properties = + new HashMap>(); /** * Creates a Properties Chunk. */ - public PropertiesChunk() { - super(PREFIX, -1, Types.UNKNOWN); + protected PropertiesChunk() { + super(NAME, -1, Types.UNKNOWN); } @Override public String getEntryName() { - return PREFIX; + return NAME; } + + /** + * Returns all the properties in the chunk + */ + public Map> getProperties() { + return properties; + } + + /** + * Returns all values for the given property, of null if none exist + */ + public List getValues(MAPIProperty property) { + return properties.get(property); + } + + /** + * Returns the (first/only) value for the given property, or + * null if none exist + */ + public PropertyValue getValue(MAPIProperty property) { + List values = properties.get(property); + if (values != null && values.size() > 0) { + return values.get(0); + } + return null; + } - public void readValue(InputStream value) throws IOException { + protected void readProperties(InputStream value) throws IOException { // TODO } - public void writeValue(OutputStream out) throws IOException { + protected void writeProperties(OutputStream out) throws IOException { // TODO } } diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StoragePropertiesChunk.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StoragePropertiesChunk.java new file mode 100644 index 0000000000..166d38ba9c --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StoragePropertiesChunk.java @@ -0,0 +1,53 @@ +/* ==================================================================== + 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.hsmf.datatypes; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.poi.util.LittleEndian; + +/** + * A {@link PropertiesChunk} for a Storage Properties, such as + * Attachments and Recipients. + * This only has a 8 byte header + */ +public class StoragePropertiesChunk extends PropertiesChunk { + public StoragePropertiesChunk() { + super(); + } + + @Override + public void readValue(InputStream stream) throws IOException { + // 8 bytes of reserved zeros + LittleEndian.readLong(stream); + + // Now properties + readProperties(stream); + } + + @Override + public void writeValue(OutputStream out) throws IOException { + // 8 bytes of reserved zeros + out.write(new byte[8]); + + // Now properties + writeProperties(out); + } +} \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StringChunk.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StringChunk.java index f45989a7ea..2051f44c28 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StringChunk.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/StringChunk.java @@ -22,7 +22,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import org.apache.poi.hsmf.datatypes.Types; import org.apache.poi.hsmf.datatypes.Types.MAPIType; import org.apache.poi.util.IOUtils; import org.apache.poi.util.StringUtil; diff --git a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/Types.java b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/Types.java index aa2864660e..a4732f081f 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/datatypes/Types.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/datatypes/Types.java @@ -26,6 +26,7 @@ import java.util.Map; */ public final class Types { private static Map builtInTypes = new HashMap(); + private static Map customTypes = new HashMap(); /** Unspecified */ public static final MAPIType UNSPECIFIED = new MAPIType(0x0000, "Unspecified", -1); @@ -95,6 +96,7 @@ public final class Types { this.id = id; this.name = asCustomName(id); this.length = length; + customTypes.put(id, this); } /** @@ -150,6 +152,24 @@ public final class Types { } public static MAPIType createCustom(int typeId) { - return new MAPIType(typeId, -1); + // Check they're not being silly, and asking for a built-in one... + if (getById(typeId) != null) { + return getById(typeId); + } + + // Try to get an existing definition of this + MAPIType type = customTypes.get(typeId); + + // If none, do a thread-safe creation + if (type == null) { + synchronized (customTypes) { + type = customTypes.get(typeId); + if (type == null) { + type = new MAPIType(typeId, -1); + } + } + } + + return type; } } diff --git a/src/scratchpad/src/org/apache/poi/hsmf/dev/HSMFDump.java b/src/scratchpad/src/org/apache/poi/hsmf/dev/HSMFDump.java index 8091e71780..436298de45 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/dev/HSMFDump.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/dev/HSMFDump.java @@ -23,6 +23,8 @@ import java.io.IOException; import org.apache.poi.hsmf.datatypes.Chunk; import org.apache.poi.hsmf.datatypes.ChunkGroup; import org.apache.poi.hsmf.datatypes.MAPIProperty; +import org.apache.poi.hsmf.datatypes.PropertiesChunk; +import org.apache.poi.hsmf.datatypes.PropertyValue; import org.apache.poi.hsmf.parsers.POIFSChunkParser; import org.apache.poi.poifs.filesystem.POIFSFileSystem; @@ -42,17 +44,35 @@ public class HSMFDump { for(Chunk chunk : chunks.getChunks()) { MAPIProperty attr = MAPIProperty.get(chunk.getChunkId()); - String idName = attr.id + " - " + attr.name; - if(attr == MAPIProperty.UNKNOWN) { - idName = chunk.getChunkId() + " - (unknown)"; + if (chunk instanceof PropertiesChunk) { + PropertiesChunk props = (PropertiesChunk)chunk; + System.out.println( + " Properties - " + props.getProperties().size() + ":" + ); + + for (MAPIProperty prop : props.getProperties().keySet()) { + System.out.println( + " * " + prop + ); + for (PropertyValue v : props.getValues(prop)) { + System.out.println( + " = " + v.toString() + ); + } + } + } else { + String idName = attr.id + " - " + attr.name; + if(attr == MAPIProperty.UNKNOWN) { + idName = chunk.getChunkId() + " - (unknown)"; + } + + System.out.println( + " " + idName + " - " + chunk.getType().getName() + ); + System.out.println( + " " + chunk.toString() + ); } - - System.out.println( - " " + idName + " - " + chunk.getType().getName() - ); - System.out.println( - " " + chunk.toString() - ); } System.out.println(); } diff --git a/src/scratchpad/src/org/apache/poi/hsmf/parsers/POIFSChunkParser.java b/src/scratchpad/src/org/apache/poi/hsmf/parsers/POIFSChunkParser.java index b412fad05d..6800b0ac77 100644 --- a/src/scratchpad/src/org/apache/poi/hsmf/parsers/POIFSChunkParser.java +++ b/src/scratchpad/src/org/apache/poi/hsmf/parsers/POIFSChunkParser.java @@ -27,9 +27,12 @@ import org.apache.poi.hsmf.datatypes.ChunkGroup; import org.apache.poi.hsmf.datatypes.Chunks; import org.apache.poi.hsmf.datatypes.DirectoryChunk; import org.apache.poi.hsmf.datatypes.MAPIProperty; +import org.apache.poi.hsmf.datatypes.MessagePropertiesChunk; import org.apache.poi.hsmf.datatypes.MessageSubmissionChunk; import org.apache.poi.hsmf.datatypes.NameIdChunks; +import org.apache.poi.hsmf.datatypes.PropertiesChunk; import org.apache.poi.hsmf.datatypes.RecipientChunks; +import org.apache.poi.hsmf.datatypes.StoragePropertiesChunk; import org.apache.poi.hsmf.datatypes.StringChunk; import org.apache.poi.hsmf.datatypes.Types; import org.apache.poi.hsmf.datatypes.Types.MAPIType; @@ -66,7 +69,7 @@ public final class POIFSChunkParser { if(dir.getName().startsWith(AttachmentChunks.PREFIX)) { group = new AttachmentChunks(dir.getName()); } - if(dir.getName().startsWith(NameIdChunks.PREFIX)) { + if(dir.getName().startsWith(NameIdChunks.NAME)) { group = new NameIdChunks(); } if(dir.getName().startsWith(RecipientChunks.PREFIX)) { @@ -110,87 +113,98 @@ public final class POIFSChunkParser { */ protected static void process(Entry entry, ChunkGroup grouping) { String entryName = entry.getName(); + Chunk chunk = null; - if(entryName.length() < 9) { - // Name in the wrong format - return; - } - if(entryName.indexOf('_') == -1) { - // Name in the wrong format - return; - } - - // Split it into its parts - int splitAt = entryName.lastIndexOf('_'); - String namePrefix = entryName.substring(0, splitAt+1); - String ids = entryName.substring(splitAt+1); - - // Make sure we got what we expected, should be of - // the form ___ - if(namePrefix.equals("Olk10SideProps") || - namePrefix.equals("Olk10SideProps_")) { - // This is some odd Outlook 2002 thing, skip - return; - } else if(splitAt <= entryName.length()-8) { - // In the right form for a normal chunk - // We'll process this further in a little bit + // Is it a properties chunk? (They have special names) + if (entryName.equals(PropertiesChunk.NAME)) { + if (grouping instanceof Chunks) { + // These should be the properties for the message itself + chunk = new MessagePropertiesChunk(); + } else { + // Will be properties on an attachment or recipient + chunk = new StoragePropertiesChunk(); + } } else { - // Underscores not the right place, something's wrong - throw new IllegalArgumentException("Invalid chunk name " + entryName); - } - - // Now try to turn it into id + type - try { - int chunkId = Integer.parseInt(ids.substring(0, 4), 16); - int typeId = Integer.parseInt(ids.substring(4, 8), 16); - - MAPIType type = Types.getById(typeId); - if (type == null) { - type = Types.createCustom(typeId); + // Check it's a regular chunk + if(entryName.length() < 9) { + // Name in the wrong format + return; + } + if(entryName.indexOf('_') == -1) { + // Name in the wrong format + return; } - Chunk chunk = null; + // Split it into its parts + int splitAt = entryName.lastIndexOf('_'); + String namePrefix = entryName.substring(0, splitAt+1); + String ids = entryName.substring(splitAt+1); - // Special cases based on the ID - if(chunkId == MAPIProperty.MESSAGE_SUBMISSION_ID.id) { - chunk = new MessageSubmissionChunk(namePrefix, chunkId, type); - } - else { - // Nothing special about this ID - // So, do the usual thing which is by type - if (type == Types.BINARY) { - chunk = new ByteChunk(namePrefix, chunkId, type); + // Make sure we got what we expected, should be of + // the form ___ + if(namePrefix.equals("Olk10SideProps") || + namePrefix.equals("Olk10SideProps_")) { + // This is some odd Outlook 2002 thing, skip + return; + } else if(splitAt <= entryName.length()-8) { + // In the right form for a normal chunk + // We'll process this further in a little bit + } else { + // Underscores not the right place, something's wrong + throw new IllegalArgumentException("Invalid chunk name " + entryName); + } + + // Now try to turn it into id + type + try { + int chunkId = Integer.parseInt(ids.substring(0, 4), 16); + int typeId = Integer.parseInt(ids.substring(4, 8), 16); + + MAPIType type = Types.getById(typeId); + if (type == null) { + type = Types.createCustom(typeId); } - else if (type == Types.DIRECTORY) { - if(entry instanceof DirectoryNode) { - chunk = new DirectoryChunk((DirectoryNode)entry, namePrefix, chunkId, type); - } - } - else if (type == Types.ASCII_STRING || - type == Types.UNICODE_STRING) { - chunk = new StringChunk(namePrefix, chunkId, type); + + // Special cases based on the ID + if(chunkId == MAPIProperty.MESSAGE_SUBMISSION_ID.id) { + chunk = new MessageSubmissionChunk(namePrefix, chunkId, type); } else { - // Type of an unsupported type! Skipping... + // Nothing special about this ID + // So, do the usual thing which is by type + if (type == Types.BINARY) { + chunk = new ByteChunk(namePrefix, chunkId, type); + } + else if (type == Types.DIRECTORY) { + if(entry instanceof DirectoryNode) { + chunk = new DirectoryChunk((DirectoryNode)entry, namePrefix, chunkId, type); + } + } + else if (type == Types.ASCII_STRING || + type == Types.UNICODE_STRING) { + chunk = new StringChunk(namePrefix, chunkId, type); + } + else { + // Type of an unsupported type! Skipping... + } } + } catch(NumberFormatException e) { + // Name in the wrong format + return; } + } - if(chunk != null) { - if(entry instanceof DocumentNode) { - try { - DocumentInputStream inp = new DocumentInputStream((DocumentNode)entry); - chunk.readValue(inp); - grouping.record(chunk); - } catch(IOException e) { - System.err.println("Error reading from part " + entry.getName() + " - " + e.toString()); - } - } else { + if(chunk != null) { + if(entry instanceof DocumentNode) { + try { + DocumentInputStream inp = new DocumentInputStream((DocumentNode)entry); + chunk.readValue(inp); grouping.record(chunk); + } catch(IOException e) { + System.err.println("Error reading from part " + entry.getName() + " - " + e.toString()); } - } - } catch(NumberFormatException e) { - // Name in the wrong format - return; + } else { + grouping.record(chunk); + } } } }