| /* ==================================================================== |
| 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.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import org.apache.poi.hmef.Attachment; |
| import org.apache.poi.hmef.HMEFMessage; |
| import org.apache.poi.hsmf.datatypes.MAPIProperty; |
| import org.apache.poi.hsmf.datatypes.Types; |
| import org.apache.poi.hsmf.datatypes.Types.MAPIType; |
| import org.apache.poi.util.HexDump; |
| import org.apache.poi.util.IOUtils; |
| import org.apache.poi.util.LittleEndian; |
| import org.apache.poi.util.StringUtil; |
| |
| /** |
| * A pure-MAPI attribute which applies to a {@link HMEFMessage} |
| * or one of its {@link Attachment}s. |
| */ |
| public class MAPIAttribute { |
| |
| //arbitrarily selected; may need to increase |
| private static final int MAX_RECORD_LENGTH = 1_000_000; |
| |
| private final MAPIProperty property; |
| private final int type; |
| private final byte[] data; |
| |
| /** |
| * Constructs a single new attribute from |
| * the contents of the stream |
| */ |
| public MAPIAttribute(MAPIProperty property, int type, byte[] data) { |
| this.property = property; |
| this.type = type; |
| this.data = data.clone(); |
| } |
| |
| public MAPIProperty getProperty() { |
| return property; |
| } |
| |
| public int getType() { |
| return type; |
| } |
| |
| public byte[] getData() { |
| return data; |
| } |
| |
| public String toString() { |
| String hex; |
| if(data.length <= 16) { |
| hex = HexDump.toHex(data); |
| } else { |
| byte[] d = Arrays.copyOf(data, 16); |
| hex = HexDump.toHex(d); |
| hex = hex.substring(0, hex.length()-1) + ", ....]"; |
| } |
| |
| return property + " " + hex; |
| } |
| |
| /** |
| * Parses a MAPI Properties TNEF Attribute, and returns |
| * the list of MAPI Attributes contained within it |
| */ |
| public static List<MAPIAttribute> create(TNEFAttribute parent) throws IOException { |
| if(parent.getProperty() == TNEFProperty.ID_MAPIPROPERTIES) { |
| // Regular MAPI Properties, normally on the message |
| } |
| else if(parent.getProperty() == TNEFProperty.ID_ATTACHMENT) { |
| // MAPI Properties for an attachment |
| } |
| else { |
| // Something else, oh dear... |
| throw new IllegalArgumentException( |
| "Can only create from a MAPIProperty attribute, " + |
| "instead received a " + parent.getProperty() + " one" |
| ); |
| } |
| ByteArrayInputStream inp = new ByteArrayInputStream(parent.getData()); |
| |
| // First up, get the number of attributes |
| int count = LittleEndian.readInt(inp); |
| List<MAPIAttribute> attrs = new ArrayList<>(); |
| |
| // Now, read each one in in turn |
| for(int i=0; i<count; i++) { |
| int typeAndMV = LittleEndian.readUShort(inp); |
| int id = LittleEndian.readUShort(inp); |
| |
| // Is it either Multi-Valued or Variable-Length? |
| boolean isMV = false; |
| boolean isVL = false; |
| int typeId = typeAndMV; |
| if( (typeAndMV & Types.MULTIVALUED_FLAG) != 0 ) { |
| isMV = true; |
| typeId -= Types.MULTIVALUED_FLAG; |
| } |
| if(typeId == Types.ASCII_STRING.getId() || typeId == Types.UNICODE_STRING.getId() || |
| typeId == Types.BINARY.getId() || typeId == Types.DIRECTORY.getId()) { |
| isVL = true; |
| } |
| |
| // Turn the type ID into a strongly typed thing |
| MAPIType type = Types.getById(typeId); |
| if (type == null) { |
| type = Types.createCustom(typeId); |
| } |
| |
| // If it's a named property, rather than a standard |
| // MAPI property, grab the details of it |
| MAPIProperty prop = MAPIProperty.get(id); |
| if(id >= 0x8000 && id <= 0xFFFF) { |
| byte[] guid = new byte[16]; |
| IOUtils.readFully(inp, guid); |
| int mptype = LittleEndian.readInt(inp); |
| |
| // Get the name of it |
| String name; |
| if(mptype == 0) { |
| // It's based on a normal one |
| int mpid = LittleEndian.readInt(inp); |
| MAPIProperty base = MAPIProperty.get(mpid); |
| name = base.name; |
| } else { |
| // Custom name was stored |
| int mplen = LittleEndian.readInt(inp); |
| byte[] mpdata = IOUtils.safelyAllocate(mplen, MAX_RECORD_LENGTH); |
| IOUtils.readFully(inp, mpdata); |
| name = StringUtil.getFromUnicodeLE(mpdata, 0, (mplen/2)-1); |
| skipToBoundary(mplen, inp); |
| } |
| |
| // Now create |
| prop = MAPIProperty.createCustom(id, type, name); |
| } |
| if(prop == MAPIProperty.UNKNOWN) { |
| prop = MAPIProperty.createCustom(id, type, "(unknown " + Integer.toHexString(id) + ")"); |
| } |
| |
| // Now read in the value(s) |
| int values = 1; |
| if(isMV || isVL) { |
| values = LittleEndian.readInt(inp); |
| } |
| |
| if (type == Types.NULL && values > 1) { |
| throw new IOException("Placeholder/NULL arrays aren't supported."); |
| } |
| |
| for(int j=0; j<values; j++) { |
| int len = getLength(type, inp); |
| byte[] data = IOUtils.safelyAllocate(len, MAX_RECORD_LENGTH); |
| IOUtils.readFully(inp, data); |
| skipToBoundary(len, inp); |
| |
| // Create |
| MAPIAttribute attr; |
| if(type == Types.UNICODE_STRING || type == Types.ASCII_STRING) { |
| attr = new MAPIStringAttribute(prop, typeId, data); |
| } else if(type == Types.APP_TIME || type == Types.TIME) { |
| attr = new MAPIDateAttribute(prop, typeId, data); |
| } else if(id == MAPIProperty.RTF_COMPRESSED.id) { |
| attr = new MAPIRtfAttribute(prop, typeId, data); |
| } else { |
| attr = new MAPIAttribute(prop, typeId, data); |
| } |
| attrs.add(attr); |
| } |
| } |
| |
| // All done |
| return attrs; |
| } |
| private static int getLength(MAPIType type, InputStream inp) throws IOException { |
| if (type.isFixedLength()) { |
| return type.getLength(); |
| } |
| if (type == Types.ASCII_STRING || |
| type == Types.UNICODE_STRING || |
| type == Types.DIRECTORY || |
| type == Types.BINARY) { |
| // Need to read the length, as it varies |
| return LittleEndian.readInt(inp); |
| } else { |
| throw new IllegalArgumentException("Unknown type " + type); |
| } |
| } |
| private static void skipToBoundary(int length, InputStream inp) throws IOException { |
| // Data is always padded out to a 4 byte boundary |
| if(length % 4 != 0) { |
| int toSkip = 4 - (length % 4); |
| long skipped = IOUtils.skipFully(inp, toSkip); |
| if (skipped != toSkip) { |
| throw new IOException("tried to skip "+toSkip +" but only skipped:"+skipped); |
| } |
| } |
| } |
| } |