[github-103] hsmf: support writing properties. Thanks to Dominik Hölzl. This closes #103
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1827173 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
47fb3691f7
commit
aea347d94a
@ -25,6 +25,9 @@ public class ChunkBasedPropertyValue extends PropertyValue {
|
|||||||
public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData) {
|
public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData) {
|
||||||
super(property, flags, offsetData);
|
super(property, flags, offsetData);
|
||||||
}
|
}
|
||||||
|
public ChunkBasedPropertyValue(MAPIProperty property, long flags, byte[] offsetData, Types.MAPIType actualType) {
|
||||||
|
super(property, flags, offsetData, actualType);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Chunk getValue() {
|
public Chunk getValue() {
|
||||||
|
@ -20,6 +20,7 @@ package org.apache.poi.hsmf.datatypes;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ import org.apache.poi.util.LittleEndian;
|
|||||||
* byte header
|
* byte header
|
||||||
*/
|
*/
|
||||||
public class MessagePropertiesChunk extends PropertiesChunk {
|
public class MessagePropertiesChunk extends PropertiesChunk {
|
||||||
|
private boolean isEmbedded;
|
||||||
private long nextRecipientId;
|
private long nextRecipientId;
|
||||||
private long nextAttachmentId;
|
private long nextAttachmentId;
|
||||||
private long recipientCount;
|
private long recipientCount;
|
||||||
@ -37,6 +39,11 @@ public class MessagePropertiesChunk extends PropertiesChunk {
|
|||||||
super(parentGroup);
|
super(parentGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MessagePropertiesChunk(ChunkGroup parentGroup, boolean isEmbedded) {
|
||||||
|
super(parentGroup);
|
||||||
|
this.isEmbedded = isEmbedded;
|
||||||
|
}
|
||||||
|
|
||||||
public long getNextRecipientId() {
|
public long getNextRecipientId() {
|
||||||
return nextRecipientId;
|
return nextRecipientId;
|
||||||
}
|
}
|
||||||
@ -53,8 +60,24 @@ public class MessagePropertiesChunk extends PropertiesChunk {
|
|||||||
return attachmentCount;
|
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
|
@Override
|
||||||
public void readValue(InputStream stream) throws IOException {
|
protected void readProperties(InputStream stream) throws IOException {
|
||||||
// 8 bytes of reserved zeros
|
// 8 bytes of reserved zeros
|
||||||
LittleEndian.readLong(stream);
|
LittleEndian.readLong(stream);
|
||||||
|
|
||||||
@ -64,28 +87,44 @@ public class MessagePropertiesChunk extends PropertiesChunk {
|
|||||||
recipientCount = LittleEndian.readUInt(stream);
|
recipientCount = LittleEndian.readUInt(stream);
|
||||||
attachmentCount = LittleEndian.readUInt(stream);
|
attachmentCount = LittleEndian.readUInt(stream);
|
||||||
|
|
||||||
// 8 bytes of reserved zeros
|
if (!isEmbedded) {
|
||||||
|
// 8 bytes of reserved zeros (top level properties stream only)
|
||||||
LittleEndian.readLong(stream);
|
LittleEndian.readLong(stream);
|
||||||
|
}
|
||||||
|
|
||||||
// Now properties
|
// Now properties
|
||||||
readProperties(stream);
|
super.readProperties(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
// 8 bytes of reserved zeros
|
||||||
out.write(new byte[8]);
|
LittleEndian.putLong(0, stream);
|
||||||
|
|
||||||
// Nexts and counts
|
// Nexts and counts
|
||||||
LittleEndian.putUInt(nextRecipientId, out);
|
LittleEndian.putUInt(nextRecipientId, stream);
|
||||||
LittleEndian.putUInt(nextAttachmentId, out);
|
LittleEndian.putUInt(nextAttachmentId, stream);
|
||||||
LittleEndian.putUInt(recipientCount, out);
|
LittleEndian.putUInt(recipientCount, stream);
|
||||||
LittleEndian.putUInt(attachmentCount, out);
|
LittleEndian.putUInt(attachmentCount, stream);
|
||||||
|
|
||||||
// 8 bytes of reserved zeros
|
if (!isEmbedded) {
|
||||||
out.write(new byte[8]);
|
// 8 bytes of reserved zeros (top level properties stream only)
|
||||||
|
LittleEndian.putLong(0, stream);
|
||||||
|
}
|
||||||
|
|
||||||
// Now properties
|
// Now properties.
|
||||||
writeProperties(out);
|
return super.writeProperties(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeValue(OutputStream stream) throws IOException {
|
||||||
|
// write properties without variable length properties
|
||||||
|
writeProperties(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,18 @@
|
|||||||
|
|
||||||
package org.apache.poi.hsmf.datatypes;
|
package org.apache.poi.hsmf.datatypes;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import org.apache.poi.hsmf.datatypes.PropertyValue.BooleanPropertyValue;
|
import org.apache.poi.hsmf.datatypes.PropertyValue.BooleanPropertyValue;
|
||||||
import org.apache.poi.hsmf.datatypes.PropertyValue.CurrencyPropertyValue;
|
import org.apache.poi.hsmf.datatypes.PropertyValue.CurrencyPropertyValue;
|
||||||
@ -35,6 +40,7 @@ import org.apache.poi.hsmf.datatypes.PropertyValue.NullPropertyValue;
|
|||||||
import org.apache.poi.hsmf.datatypes.PropertyValue.ShortPropertyValue;
|
import org.apache.poi.hsmf.datatypes.PropertyValue.ShortPropertyValue;
|
||||||
import org.apache.poi.hsmf.datatypes.PropertyValue.TimePropertyValue;
|
import org.apache.poi.hsmf.datatypes.PropertyValue.TimePropertyValue;
|
||||||
import org.apache.poi.hsmf.datatypes.Types.MAPIType;
|
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.IOUtils;
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
import org.apache.poi.util.LittleEndian.BufferUnderrunException;
|
import org.apache.poi.util.LittleEndian.BufferUnderrunException;
|
||||||
@ -54,6 +60,13 @@ public abstract class PropertiesChunk extends Chunk {
|
|||||||
// arbitrarily selected; may need to increase
|
// arbitrarily selected; may need to increase
|
||||||
private static final int MAX_RECORD_LENGTH = 1_000_000;
|
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 */
|
/** For logging problems we spot with the file */
|
||||||
private POILogger logger = POILogFactory.getLogger(PropertiesChunk.class);
|
private POILogger logger = POILogFactory.getLogger(PropertiesChunk.class);
|
||||||
|
|
||||||
@ -105,6 +118,13 @@ public abstract class PropertiesChunk extends Chunk {
|
|||||||
return props;
|
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
|
* Returns all values for the given property, looking up chunk based ones as
|
||||||
* required, of null if none exist
|
* required, of null if none exist
|
||||||
@ -239,7 +259,7 @@ public abstract class PropertiesChunk extends Chunk {
|
|||||||
PropertyValue propVal = null;
|
PropertyValue propVal = null;
|
||||||
if (isPointer) {
|
if (isPointer) {
|
||||||
// We'll match up the chunk later
|
// 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) {
|
} else if (type == Types.NULL) {
|
||||||
propVal = new NullPropertyValue(prop, flags, data);
|
propVal = new NullPropertyValue(prop, flags, data);
|
||||||
} else if (type == Types.BOOLEAN) {
|
} else if (type == Types.BOOLEAN) {
|
||||||
@ -261,7 +281,7 @@ public abstract class PropertiesChunk extends Chunk {
|
|||||||
}
|
}
|
||||||
// TODO Add in the rest of the types
|
// TODO Add in the rest of the types
|
||||||
else {
|
else {
|
||||||
propVal = new PropertyValue(prop, flags, data);
|
propVal = new PropertyValue(prop, flags, data, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties.get(prop) != null) {
|
if (properties.get(prop) != null) {
|
||||||
@ -276,7 +296,130 @@ public abstract class PropertiesChunk extends Chunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import java.util.Calendar;
|
|||||||
|
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
import org.apache.poi.util.LocaleUtil;
|
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
|
* An instance of a {@link MAPIProperty} inside a {@link PropertiesChunk}. Where
|
||||||
@ -32,13 +33,18 @@ import org.apache.poi.util.LocaleUtil;
|
|||||||
*/
|
*/
|
||||||
public class PropertyValue {
|
public class PropertyValue {
|
||||||
private MAPIProperty property;
|
private MAPIProperty property;
|
||||||
|
private MAPIType actualType;
|
||||||
private long flags;
|
private long flags;
|
||||||
protected byte[] data;
|
protected byte[] data;
|
||||||
|
|
||||||
public PropertyValue(MAPIProperty property, long flags, 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.property = property;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
this.actualType = actualType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MAPIProperty getProperty() {
|
public MAPIProperty getProperty() {
|
||||||
@ -56,6 +62,14 @@ public class PropertyValue {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getRawValue() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MAPIType getActualType() {
|
||||||
|
return actualType;
|
||||||
|
}
|
||||||
|
|
||||||
public void setRawValue(byte[] value) {
|
public void setRawValue(byte[] value) {
|
||||||
this.data = value;
|
this.data = value;
|
||||||
}
|
}
|
||||||
@ -78,7 +92,7 @@ public class PropertyValue {
|
|||||||
public static class NullPropertyValue extends PropertyValue {
|
public static class NullPropertyValue extends PropertyValue {
|
||||||
public NullPropertyValue(MAPIProperty property, long flags,
|
public NullPropertyValue(MAPIProperty property, long flags,
|
||||||
byte[] data) {
|
byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -90,7 +104,7 @@ public class PropertyValue {
|
|||||||
public static class BooleanPropertyValue extends PropertyValue {
|
public static class BooleanPropertyValue extends PropertyValue {
|
||||||
public BooleanPropertyValue(MAPIProperty property, long flags,
|
public BooleanPropertyValue(MAPIProperty property, long flags,
|
||||||
byte[] data) {
|
byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.BOOLEAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -112,7 +126,7 @@ public class PropertyValue {
|
|||||||
public static class ShortPropertyValue extends PropertyValue {
|
public static class ShortPropertyValue extends PropertyValue {
|
||||||
public ShortPropertyValue(MAPIProperty property, long flags,
|
public ShortPropertyValue(MAPIProperty property, long flags,
|
||||||
byte[] data) {
|
byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.SHORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -130,7 +144,7 @@ public class PropertyValue {
|
|||||||
|
|
||||||
public static class LongPropertyValue extends PropertyValue {
|
public static class LongPropertyValue extends PropertyValue {
|
||||||
public LongPropertyValue(MAPIProperty property, long flags, byte[] data) {
|
public LongPropertyValue(MAPIProperty property, long flags, byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.LONG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -149,7 +163,7 @@ public class PropertyValue {
|
|||||||
public static class LongLongPropertyValue extends PropertyValue {
|
public static class LongLongPropertyValue extends PropertyValue {
|
||||||
public LongLongPropertyValue(MAPIProperty property, long flags,
|
public LongLongPropertyValue(MAPIProperty property, long flags,
|
||||||
byte[] data) {
|
byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.LONG_LONG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -168,7 +182,7 @@ public class PropertyValue {
|
|||||||
public static class FloatPropertyValue extends PropertyValue {
|
public static class FloatPropertyValue extends PropertyValue {
|
||||||
public FloatPropertyValue(MAPIProperty property, long flags,
|
public FloatPropertyValue(MAPIProperty property, long flags,
|
||||||
byte[] data) {
|
byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.FLOAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -186,7 +200,7 @@ public class PropertyValue {
|
|||||||
|
|
||||||
public static class DoublePropertyValue extends PropertyValue {
|
public static class DoublePropertyValue extends PropertyValue {
|
||||||
public DoublePropertyValue(MAPIProperty property, long flags, byte[] data) {
|
public DoublePropertyValue(MAPIProperty property, long flags, byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.DOUBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -210,7 +224,7 @@ public class PropertyValue {
|
|||||||
private static final BigInteger SHIFT = BigInteger.valueOf(10000);
|
private static final BigInteger SHIFT = BigInteger.valueOf(10000);
|
||||||
|
|
||||||
public CurrencyPropertyValue(MAPIProperty property, long flags, byte[] data) {
|
public CurrencyPropertyValue(MAPIProperty property, long flags, byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.CURRENCY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -236,7 +250,7 @@ public class PropertyValue {
|
|||||||
* (365L * 369L + 89L);
|
* (365L * 369L + 89L);
|
||||||
|
|
||||||
public TimePropertyValue(MAPIProperty property, long flags, byte[] data) {
|
public TimePropertyValue(MAPIProperty property, long flags, byte[] data) {
|
||||||
super(property, flags, data);
|
super(property, flags, data, org.apache.poi.hsmf.datatypes.Types.TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -129,7 +129,8 @@ public final class POIFSChunkParser {
|
|||||||
if (entryName.equals(PropertiesChunk.NAME)) {
|
if (entryName.equals(PropertiesChunk.NAME)) {
|
||||||
if (grouping instanceof Chunks) {
|
if (grouping instanceof Chunks) {
|
||||||
// These should be the properties for the message itself
|
// 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 {
|
} else {
|
||||||
// Will be properties on an attachment or recipient
|
// Will be properties on an attachment or recipient
|
||||||
chunk = new StoragePropertiesChunk(grouping);
|
chunk = new StoragePropertiesChunk(grouping);
|
||||||
|
@ -38,7 +38,8 @@ import org.junit.runners.Suite;
|
|||||||
TestOutlookTextExtractor.class,
|
TestOutlookTextExtractor.class,
|
||||||
TestPOIFSChunkParser.class,
|
TestPOIFSChunkParser.class,
|
||||||
TestMessageSubmissionChunkY2KRead.class,
|
TestMessageSubmissionChunkY2KRead.class,
|
||||||
TestMessageSubmissionChunk.class
|
TestMessageSubmissionChunk.class,
|
||||||
|
TestExtractEmbeddedMSG.class
|
||||||
})
|
})
|
||||||
public class AllHSMFTests {
|
public class AllHSMFTests {
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,179 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user