public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData) {
super(property, flags, offsetData);
}
+ public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData, Types.MAPIType actualType) {
+ super(property, flags, offsetData, actualType);
+ }
@Override
public Chunk getValue() {
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.List;
import org.apache.poi.util.LittleEndian;
* byte header
*/
public class MessagePropertiesChunk extends PropertiesChunk {
+ private boolean isEmbedded;
private long nextRecipientId;
private long nextAttachmentId;
private long recipientCount;
super(parentGroup);
}
+ public MessagePropertiesChunk(ChunkGroup parentGroup, boolean isEmbedded) {
+ super(parentGroup);
+ this.isEmbedded = isEmbedded;
+ }
+
public long getNextRecipientId() {
return nextRecipientId;
}
public long getAttachmentCount() {
return attachmentCount;
}
+
+ public void setNextRecipientId(long nextRecipientId) {
+ this.nextRecipientId = nextRecipientId;
+ }
+
+ public void setNextAttachmentId(long nextAttachmentId) {
+ this.nextAttachmentId = nextAttachmentId;
+ }
+
+ public void setRecipientCount(long recipientCount) {
+ this.recipientCount = recipientCount;
+ }
+
+ public void setAttachmentCount(long attachmentCount) {
+ this.attachmentCount = attachmentCount;
+ }
@Override
- public void readValue(InputStream stream) throws IOException {
+ protected void readProperties(InputStream stream) throws IOException {
// 8 bytes of reserved zeros
LittleEndian.readLong(stream);
recipientCount = LittleEndian.readUInt(stream);
attachmentCount = LittleEndian.readUInt(stream);
- // 8 bytes of reserved zeros
- LittleEndian.readLong(stream);
+ if (!isEmbedded) {
+ // 8 bytes of reserved zeros (top level properties stream only)
+ LittleEndian.readLong(stream);
+ }
// Now properties
- readProperties(stream);
+ super.readProperties(stream);
}
@Override
- public void writeValue(OutputStream out) throws IOException {
+ public void readValue(InputStream value) throws IOException {
+ readProperties(value);
+ }
+
+ @Override
+ protected List<PropertyValue> writeProperties(OutputStream stream) throws IOException
+ {
// 8 bytes of reserved zeros
- out.write(new byte[8]);
+ LittleEndian.putLong(0, stream);
// 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]);
+ LittleEndian.putUInt(nextRecipientId, stream);
+ LittleEndian.putUInt(nextAttachmentId, stream);
+ LittleEndian.putUInt(recipientCount, stream);
+ LittleEndian.putUInt(attachmentCount, stream);
+
+ if (!isEmbedded) {
+ // 8 bytes of reserved zeros (top level properties stream only)
+ LittleEndian.putLong(0, stream);
+ }
+
+ // Now properties.
+ return super.writeProperties(stream);
+ }
- // Now properties
- writeProperties(out);
+ @Override
+ public void writeValue(OutputStream stream) throws IOException {
+ // write properties without variable length properties
+ writeProperties(stream);
}
}
package org.apache.poi.hsmf.datatypes;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Map.Entry;
import org.apache.poi.hsmf.datatypes.PropertyValue.BooleanPropertyValue;
import org.apache.poi.hsmf.datatypes.PropertyValue.CurrencyPropertyValue;
import org.apache.poi.hsmf.datatypes.PropertyValue.ShortPropertyValue;
import org.apache.poi.hsmf.datatypes.PropertyValue.TimePropertyValue;
import org.apache.poi.hsmf.datatypes.Types.MAPIType;
+import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndian.BufferUnderrunException;
public abstract class PropertiesChunk extends Chunk {
public static final String NAME = "__properties_version1.0";
- //arbitrarily selected; may need to increase
+ // arbitrarily selected; may need to increase
private static final int MAX_RECORD_LENGTH = 1_000_000;
+ // standard prefix, defined in the spec
+ public static final String VARIABLE_LENGTH_PROPERTY_PREFIX = "__substg1.0_";
+
+ // standard property flags, defined in the spec
+ public static final int PROPERTIES_FLAG_READABLE = 2;
+ public static final int PROPERTIES_FLAG_WRITEABLE = 4;
+
/** For logging problems we spot with the file */
private POILogger logger = POILogFactory.getLogger(PropertiesChunk.class);
return props;
}
+ /**
+ * Defines a property. Multi-valued properties are not yet supported.
+ */
+ public void setProperty(PropertyValue value) {
+ properties.put(value.getProperty(), value);
+ }
+
/**
* Returns all values for the given property, looking up chunk based ones as
* required, of null if none exist
PropertyValue propVal = null;
if (isPointer) {
// We'll match up the chunk later
- propVal = new ChunkBasedPropertyValue(prop, flags, data);
+ propVal = new ChunkBasedPropertyValue(prop, flags, data, type);
} else if (type == Types.NULL) {
propVal = new NullPropertyValue(prop, flags, data);
} else if (type == Types.BOOLEAN) {
}
// TODO Add in the rest of the types
else {
- propVal = new PropertyValue(prop, flags, data);
+ propVal = new PropertyValue(prop, flags, data, type);
}
if (properties.get(prop) != null) {
}
}
- protected void writeProperties(OutputStream out) throws IOException {
- // TODO
+ /**
+ * Writes this chunk in the specified {@code DirectoryEntry}.
+ *
+ * @param directory
+ * The directory.
+ * @throws IOException
+ * If an I/O error occurs.
+ */
+ public void writeProperties(DirectoryEntry directory) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ List<PropertyValue> values = writeProperties(baos);
+ baos.close();
+
+ // write the header data with the properties declaration
+ directory.createDocument(org.apache.poi.hsmf.datatypes.PropertiesChunk.NAME,
+ new ByteArrayInputStream(baos.toByteArray()));
+
+ // write the property values
+ writeNodeData(directory, values);
+ }
+
+ /**
+ * Write the nodes for variable-length data. Those properties are returned by
+ * {@link #writeProperties(java.io.OutputStream)}.
+ *
+ * @param directory
+ * The directory.
+ * @param values
+ * The values.
+ * @throws IOException
+ * If an I/O error occurs.
+ */
+ protected void writeNodeData(DirectoryEntry directory, List<PropertyValue> values) throws IOException {
+ for (PropertyValue value : values) {
+ byte[] bytes = value.getRawValue();
+ String nodeName = VARIABLE_LENGTH_PROPERTY_PREFIX + getFileName(value.getProperty(), value.getActualType());
+ directory.createDocument(nodeName, new ByteArrayInputStream(bytes));
+ }
+ }
+
+ /**
+ * Writes the header of the properties.
+ *
+ * @param out
+ * The {@code OutputStream}.
+ * @return The variable-length properties that need to be written in another
+ * node.
+ * @throws IOException
+ * If an I/O error occurs.
+ */
+ protected List<PropertyValue> writeProperties(OutputStream out) throws IOException {
+ List<PropertyValue> variableLengthProperties = new ArrayList<>();
+ for (Entry<MAPIProperty, PropertyValue> entry : properties.entrySet()) {
+ MAPIProperty property = entry.getKey();
+ PropertyValue value = entry.getValue();
+ if (value == null) {
+ continue;
+ }
+ if (property.id < 0) {
+ continue;
+ }
+ // generic header
+ // page 23, point 2.4.2
+ // tag is the property id and its type
+ long tag = Long.parseLong(getFileName(property, value.getActualType()), 16);
+ LittleEndian.putUInt(tag, out);
+ LittleEndian.putUInt(value.getFlags(), out); // readable + writable
+
+ MAPIType type = getTypeMapping(value.getActualType());
+ if (type.isFixedLength()) {
+ // page 11, point 2.1.2
+ writeFixedLengthValueHeader(out, property, type, value);
+ } else {
+ // page 12, point 2.1.3
+ writeVariableLengthValueHeader(out, property, type, value);
+ variableLengthProperties.add(value);
+ }
+ }
+ return variableLengthProperties;
+ }
+
+ private void writeFixedLengthValueHeader(OutputStream out, MAPIProperty property, MAPIType type, PropertyValue value) throws IOException {
+ // fixed type header
+ // page 24, point 2.4.2.1.1
+ byte[] bytes = value.getRawValue();
+ int length = bytes != null ? bytes.length : 0;
+ if (bytes != null) {
+ // Little endian.
+ byte[] reversed = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; ++i) {
+ reversed[bytes.length - i - 1] = bytes[i];
+ }
+ out.write(reversed);
+ }
+ out.write(new byte[8 - length]);
+ }
+
+ private void writeVariableLengthValueHeader(OutputStream out, MAPIProperty propertyEx, MAPIType type,
+ PropertyValue value) throws IOException {
+ // variable length header
+ // page 24, point 2.4.2.2
+ byte[] bytes = value.getRawValue();
+ int length = bytes != null ? bytes.length : 0;
+ // alter the length, as specified in page 25
+ if (type == Types.UNICODE_STRING) {
+ length += 2;
+ } else if (type == Types.ASCII_STRING) {
+ length += 1;
+ }
+ LittleEndian.putUInt(length, out);
+ // specified in page 25
+ LittleEndian.putUInt(0, out);
+ }
+
+ private String getFileName(MAPIProperty property, MAPIType actualType) {
+ String str = Integer.toHexString(property.id).toUpperCase(Locale.ROOT);
+ while (str.length() < 4) {
+ str = "0" + str;
+ }
+ MAPIType type = getTypeMapping(actualType);
+ return str + type.asFileEnding();
+ }
+
+ private MAPIType getTypeMapping(MAPIType type) {
+ return type == Types.ASCII_STRING ? Types.UNICODE_STRING : type;
}
}
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LocaleUtil;
+import org.apache.poi.hsmf.datatypes.Types.MAPIType;
/**
* An instance of a {@link MAPIProperty} inside a {@link PropertiesChunk}. Where
*/
public class PropertyValue {
private MAPIProperty property;
+ private MAPIType actualType;
private long flags;
protected byte[] data;
public PropertyValue(MAPIProperty property, long flags, byte[] data) {
+ this(property, flags, data, property.usualType);
+ }
+ public PropertyValue(MAPIProperty property, long flags, byte[] data, MAPIType actualType) {
this.property = property;
this.flags = flags;
this.data = data;
+ this.actualType = actualType;
}
public MAPIProperty getProperty() {
return data;
}
+ public byte[] getRawValue() {
+ return data;
+ }
+
+ public MAPIType getActualType() {
+ return actualType;
+ }
+
public void setRawValue(byte[] value) {
this.data = value;
}
public static class NullPropertyValue extends PropertyValue {
public NullPropertyValue(MAPIProperty property, long flags,
byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.NULL);
}
@Override
public static class BooleanPropertyValue extends PropertyValue {
public BooleanPropertyValue(MAPIProperty property, long flags,
byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.BOOLEAN);
}
@Override
public static class ShortPropertyValue extends PropertyValue {
public ShortPropertyValue(MAPIProperty property, long flags,
byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.SHORT);
}
@Override
public static class LongPropertyValue extends PropertyValue {
public LongPropertyValue(MAPIProperty property, long flags, byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.LONG);
}
@Override
public static class LongLongPropertyValue extends PropertyValue {
public LongLongPropertyValue(MAPIProperty property, long flags,
byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.LONG_LONG);
}
@Override
public static class FloatPropertyValue extends PropertyValue {
public FloatPropertyValue(MAPIProperty property, long flags,
byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.FLOAT);
}
@Override
public static class DoublePropertyValue extends PropertyValue {
public DoublePropertyValue(MAPIProperty property, long flags, byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.DOUBLE);
}
@Override
private static final BigInteger SHIFT = BigInteger.valueOf(10000);
public CurrencyPropertyValue(MAPIProperty property, long flags, byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.CURRENCY);
}
@Override
* (365L * 369L + 89L);
public TimePropertyValue(MAPIProperty property, long flags, byte[] data) {
- super(property, flags, data);
+ super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.TIME);
}
@Override
if (entryName.equals(PropertiesChunk.NAME)) {
if (grouping instanceof Chunks) {
// These should be the properties for the message itself
- chunk = new MessagePropertiesChunk(grouping);
+ chunk = new MessagePropertiesChunk(grouping,
+ entry.getParent() != null && entry.getParent().getParent() != null);
} else {
// Will be properties on an attachment or recipient
chunk = new StoragePropertiesChunk(grouping);
TestOutlookTextExtractor.class,
TestPOIFSChunkParser.class,
TestMessageSubmissionChunkY2KRead.class,
- TestMessageSubmissionChunk.class
+ TestMessageSubmissionChunk.class,
+ TestExtractEmbeddedMSG.class
})
public class AllHSMFTests {
}
--- /dev/null
+package org.apache.poi.hsmf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.hsmf.datatypes.AttachmentChunks;
+import org.apache.poi.hsmf.datatypes.Chunk;
+import org.apache.poi.hsmf.datatypes.ChunkBasedPropertyValue;
+import org.apache.poi.hsmf.datatypes.MAPIProperty;
+import org.apache.poi.hsmf.datatypes.MessagePropertiesChunk;
+import org.apache.poi.hsmf.datatypes.NameIdChunks;
+import org.apache.poi.hsmf.datatypes.PropertiesChunk;
+import org.apache.poi.hsmf.datatypes.PropertyValue;
+import org.apache.poi.hsmf.datatypes.RecipientChunks;
+import org.apache.poi.hsmf.datatypes.Types;
+import org.apache.poi.hsmf.datatypes.Types.MAPIType;
+import org.apache.poi.hsmf.exceptions.ChunkNotFoundException;
+import org.apache.poi.poifs.filesystem.DirectoryEntry;
+import org.apache.poi.poifs.filesystem.Entry;
+import org.apache.poi.poifs.filesystem.EntryUtils;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestExtractEmbeddedMSG {
+ private static MAPIMessage pdfMsgAttachments;
+
+ /**
+ * Initialize this test, load up the attachment_msg_pdf.msg mapi message.
+ *
+ * @throws Exception
+ */
+ @BeforeClass
+ public static void setUp() throws IOException {
+ POIDataSamples samples = POIDataSamples.getHSMFInstance();
+ pdfMsgAttachments = new MAPIMessage(samples.openResourceAsStream("attachment_msg_pdf.msg"));
+ }
+
+ @AfterClass
+ public static void tearDown() throws IOException {
+ pdfMsgAttachments.close();
+ }
+
+ /**
+ * Test to see if embedded message properties can be read, extracted, and
+ * re-parsed
+ *
+ * @throws ChunkNotFoundException
+ *
+ */
+ @Test
+ public void testEmbeddedMSGProperties() throws IOException, ChunkNotFoundException {
+ AttachmentChunks[] attachments = pdfMsgAttachments.getAttachmentFiles();
+ assertEquals(2, attachments.length);
+ if (attachments.length == 2) {
+ MAPIMessage attachedMsg = attachments[0].getEmbeddedMessage();
+ assertNotNull(attachedMsg);
+ // test properties of embedded message
+ testFixedAndVariableLengthPropertiesOfAttachedMSG(attachedMsg);
+ // rebuild top level message from embedded message
+ try (POIFSFileSystem extractedAttachedMsg = rebuildFromAttached(attachedMsg)) {
+ try (ByteArrayOutputStream extractedAttachedMsgOut = new ByteArrayOutputStream()) {
+ extractedAttachedMsg.writeFilesystem(extractedAttachedMsgOut);
+ byte[] extratedAttachedMsgRaw = extractedAttachedMsgOut.toByteArray();
+ MAPIMessage extractedMsgTopLevel = new MAPIMessage(
+ new ByteArrayInputStream(extratedAttachedMsgRaw));
+ // test properties of rebuilt embedded message
+ testFixedAndVariableLengthPropertiesOfAttachedMSG(extractedMsgTopLevel);
+ }
+ }
+ }
+ }
+
+ private void testFixedAndVariableLengthPropertiesOfAttachedMSG(MAPIMessage msg) throws ChunkNotFoundException {
+ // test fixed length property
+ msg.setReturnNullOnMissingChunk(true);
+ Calendar messageDate = msg.getMessageDate();
+ assertNotNull(messageDate);
+ Calendar expectedMessageDate = Calendar.getInstance();
+ expectedMessageDate.set(2010, 05, 17, 23, 52, 19); // 2010/06/17 23:52:19 GMT
+ expectedMessageDate.setTimeZone(TimeZone.getTimeZone("GMT"));
+ expectedMessageDate.set(Calendar.MILLISECOND, 0);
+ assertEquals(expectedMessageDate.getTimeInMillis(), messageDate.getTimeInMillis());
+ // test variable length property
+ assertEquals(msg.getSubject(), "Test Attachment");
+ }
+
+ private POIFSFileSystem rebuildFromAttached(MAPIMessage attachedMsg) throws IOException {
+ // Create new MSG and copy properties.
+ POIFSFileSystem newDoc = new POIFSFileSystem();
+ MessagePropertiesChunk topLevelChunk = new MessagePropertiesChunk(null);
+ // Copy attachments and recipients.
+ int recipientscount = 0;
+ int attachmentscount = 0;
+ for (Entry entry : attachedMsg.getDirectory()) {
+ if (entry.getName().startsWith(RecipientChunks.PREFIX)) {
+ recipientscount++;
+ DirectoryEntry newDir = newDoc.createDirectory(entry.getName());
+ for (Entry e : ((DirectoryEntry) entry)) {
+ EntryUtils.copyNodeRecursively(e, newDir);
+ }
+ } else if (entry.getName().startsWith(AttachmentChunks.PREFIX)) {
+ attachmentscount++;
+ DirectoryEntry newDir = newDoc.createDirectory(entry.getName());
+ for (Entry e : ((DirectoryEntry) entry)) {
+ EntryUtils.copyNodeRecursively(e, newDir);
+ }
+ }
+ }
+ // Copy properties from properties stream.
+ MessagePropertiesChunk mpc = attachedMsg.getMainChunks().getMessageProperties();
+ for (Map.Entry<MAPIProperty, PropertyValue> p : mpc.getRawProperties().entrySet()) {
+ PropertyValue val = p.getValue();
+ if (!(val instanceof ChunkBasedPropertyValue)) {
+ // Reverse data.
+ byte[] bytes = val.getRawValue();
+ for (int idx = 0; idx < bytes.length / 2; idx++) {
+ byte xchg = bytes[bytes.length - 1 - idx];
+ bytes[bytes.length - 1 - idx] = bytes[idx];
+ bytes[idx] = xchg;
+ }
+ MAPIType type = val.getActualType();
+ if (type != null && type != Types.UNKNOWN) {
+ topLevelChunk.setProperty(val);
+ }
+ }
+ }
+ // Create nameid entries.
+ DirectoryEntry nameid = newDoc.getRoot().createDirectory(NameIdChunks.NAME);
+ // GUID stream
+ nameid.createDocument(PropertiesChunk.DEFAULT_NAME_PREFIX + "00020102", new ByteArrayInputStream(new byte[0]));
+ // Entry stream
+ nameid.createDocument(PropertiesChunk.DEFAULT_NAME_PREFIX + "00030102", new ByteArrayInputStream(new byte[0]));
+ // String stream
+ nameid.createDocument(PropertiesChunk.DEFAULT_NAME_PREFIX + "00040102", new ByteArrayInputStream(new byte[0]));
+ // Base properties.
+ // Attachment/Recipient counter.
+ topLevelChunk.setAttachmentCount(attachmentscount);
+ topLevelChunk.setRecipientCount(recipientscount);
+ topLevelChunk.setNextAttachmentId(attachmentscount);
+ topLevelChunk.setNextRecipientId(recipientscount);
+ // Unicode string format.
+ topLevelChunk.setProperty(new PropertyValue(MAPIProperty.STORE_SUPPORT_MASK,
+ MessagePropertiesChunk.PROPERTIES_FLAG_READABLE | MessagePropertiesChunk.PROPERTIES_FLAG_WRITEABLE,
+ ByteBuffer.allocate(4).putInt(0x00040000).array()));
+ topLevelChunk.setProperty(new PropertyValue(MAPIProperty.HASATTACH,
+ MessagePropertiesChunk.PROPERTIES_FLAG_READABLE | MessagePropertiesChunk.PROPERTIES_FLAG_WRITEABLE,
+ attachmentscount == 0 ? new byte[] { 0 } : new byte[] { 1 }));
+ // Copy properties from MSG file system.
+ for (Chunk chunk : attachedMsg.getMainChunks().getChunks()) {
+ if (!(chunk instanceof MessagePropertiesChunk)) {
+ String entryName = chunk.getEntryName();
+ String entryType = entryName.substring(entryName.length() - 4);
+ int iType = Integer.parseInt(entryType, 16);
+ MAPIType type = Types.getById(iType);
+ if (type != null && type != Types.UNKNOWN) {
+ MAPIProperty mprop = MAPIProperty.createCustom(chunk.getChunkId(), type, chunk.getEntryName());
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
+ chunk.writeValue(data);
+ PropertyValue pval = new PropertyValue(mprop, MessagePropertiesChunk.PROPERTIES_FLAG_READABLE
+ | MessagePropertiesChunk.PROPERTIES_FLAG_WRITEABLE, data.toByteArray(), type);
+ topLevelChunk.setProperty(pval);
+ }
+ }
+ }
+ topLevelChunk.writeProperties(newDoc.getRoot());
+ return newDoc;
+ }
+}