From f23608c1617b87f2478519a84563020611a90e2e Mon Sep 17 00:00:00 2001
From: Raman Arora <raman@basistech.com>
Date: Thu, 31 Oct 2019 13:39:34 -0400
Subject: [PATCH] 5706: handle message attachments

---
 bindings/java/ivy.xml                         |   1 +
 bindings/java/nbproject/project.xml           |   2 +-
 .../org/sleuthkit/datamodel/Blackboard.java   |   3 +-
 .../datamodel/BlackboardAttribute.java        |  33 +++-
 .../org/sleuthkit/datamodel/Bundle.properties |   1 +
 .../sleuthkit/datamodel/SleuthkitCase.java    |   2 +
 .../CommunicationArtifactsHelper.java         |  80 ++++++----
 .../blackboardutils/FileAttachment.java       | 148 ++++++++++++++++++
 .../blackboardutils/MessageAttachments.java   |  65 ++++++++
 .../blackboardutils/URLAttachment.java        |  47 ++++++
 10 files changed, 346 insertions(+), 36 deletions(-)
 create mode 100644 bindings/java/src/org/sleuthkit/datamodel/blackboardutils/FileAttachment.java
 create mode 100644 bindings/java/src/org/sleuthkit/datamodel/blackboardutils/MessageAttachments.java
 create mode 100644 bindings/java/src/org/sleuthkit/datamodel/blackboardutils/URLAttachment.java

diff --git a/bindings/java/ivy.xml b/bindings/java/ivy.xml
index 3b75f4575..14073535f 100644
--- a/bindings/java/ivy.xml
+++ b/bindings/java/ivy.xml
@@ -5,6 +5,7 @@
 		<dependency org="com.google.guava" name="guava" rev="19.0"/>
 		<dependency org="org.apache.commons" name="commons-lang3" rev="3.0"/>
 		
+		<dependency org="com.google.code.gson" name="gson" rev="2.8.5"/>
 		
 		<dependency org="junit" name="junit" rev="4.8.2"/>
 		<dependency org="com.googlecode.java-diff-utils" name="diffutils" rev="1.2.1"/>
diff --git a/bindings/java/nbproject/project.xml b/bindings/java/nbproject/project.xml
index 3fb5ec577..ccbcee3fd 100755
--- a/bindings/java/nbproject/project.xml
+++ b/bindings/java/nbproject/project.xml
@@ -114,7 +114,7 @@
         <java-data xmlns="http://www.netbeans.org/ns/freeform-project-java/4">
             <compilation-unit>
                 <package-root>src</package-root>
-                <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.8.2.jar;lib/postgresql-9.4-1201.jdbc41.jar;lib/c3p0-0.9.5.jar;lib/mchange-commons-java-0.2.9.jar;lib/c3p0-0.9.5-sources.jar;lib/c3p0-0.9.5-javadoc.jar;lib/joda-time-2.4.jar;lib/commons-lang3-3.0.jar;lib/guava-19.0.jar;lib/SparseBitSet-1.1.jar</classpath>
+                <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.8.2.jar;lib/postgresql-9.4-1201.jdbc41.jar;lib/c3p0-0.9.5.jar;lib/mchange-commons-java-0.2.9.jar;lib/c3p0-0.9.5-sources.jar;lib/c3p0-0.9.5-javadoc.jar;lib/joda-time-2.4.jar;lib/commons-lang3-3.0.jar;lib/guava-19.0.jar;lib/SparseBitSet-1.1.jar;lib/gson-2.8.5.jar</classpath>
                 <built-to>build</built-to>
                 <source-level>1.8</source-level>
             </compilation-unit>
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java b/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java
index 0745f70be..6497f1260 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java
@@ -344,7 +344,8 @@ private boolean attributesMatch(Collection<BlackboardAttribute> fileAttributesLi
 						fileAttributeValue = fileAttribute.getValueLong();
 						expectedAttributeValue = expectedAttribute.getValueLong();
 						break;
-					case STRING:
+					case STRING: // Fall-thru
+					case JSON:
 						fileAttributeValue = fileAttribute.getValueString();
 						expectedAttributeValue = expectedAttribute.getValueString();
 						break;
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
index a0501eabd..32ee4acd8 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
@@ -239,10 +239,13 @@ public BlackboardAttribute(Type attributeType, String source, double valueDouble
 	 *
 	 * @throws IllegalArgumentException If the value type of the specified
 	 *                                  standard attribute type is not
-	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
+	 *                                  or
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON
 	 */
 	public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, String valueString) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING) {
+		if  (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
+		     && attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) 		{
 			throw new IllegalArgumentException("Value types do not match");
 		}
 		this.artifactID = 0;
@@ -273,7 +276,8 @@ public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, String v
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.
 	 */
 	public BlackboardAttribute(Type attributeType, String source, String valueString) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING) {
+		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
+			&& attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
 			throw new IllegalArgumentException("Type mismatched with value type");
 		}
 		this.artifactID = 0;
@@ -414,7 +418,8 @@ public double getValueDouble() {
 
 	/**
 	 * Gets the value of this attribute. The value is only valid if the
-	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.
+	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING or
+	 * TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
 	 *
 	 * @return The attribute value.
 	 */
@@ -539,6 +544,12 @@ public String getDisplayString() {
 					return TimeUtilities.epochToTime(getValueLong());
 				}
 			}
+			break;
+			case JSON: {
+				// @TODO 5726: convert JSON string to multilevel bulleted lists 
+				// for display
+				return getValueString();
+			}
 		}
 		return "";
 	}
@@ -795,7 +806,11 @@ public enum TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE {
 		 * The value type of the attribute is a long representing seconds from
 		 * January 1, 1970.
 		 */
-		DATETIME(5, "DateTime");
+		DATETIME(5, "DateTime"),
+		/**
+		 * The value type of the attribute is a JSON string.
+		 */
+		JSON(6, "Json" );
 
 		private final long typeId;
 		private final String typeName;
@@ -1354,7 +1369,13 @@ public enum ATTRIBUTE_TYPE {
 		
 		TSK_GROUPS (140, "TSK_GROUPS", 
 				bundle.getString("BlackboardAttribute.tskgroups.text"),
-				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING);
+				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING),
+		
+		TSK_ATTACHMENTS (141, "TSK_ATTACHMENTS", 
+				bundle.getString("BlackboardAttribute.tskattachments.text"),
+				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON);
+		
+		;
 
 		private final int typeID;
 		private final String typeName;
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
index 9e0e01c75..eecf0cb0e 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
@@ -186,6 +186,7 @@ BlackboardAttribute.tskpasswordsettings.text=Password Settings
 BlackboardAttribute.tskaccountsettings.text=Account Settings
 BlackboardAttribute.tskpasswordhint.text=Password Hint
 BlackboardAttribute.tskgroups.text=Groups
+BlackboardAttribute.tskattachments.text=Message Attachments
 AbstractFile.readLocal.exception.msg4.text=Error reading local file\: {0}
 AbstractFile.readLocal.exception.msg1.text=Error reading local file, local path is not set
 AbstractFile.readLocal.exception.msg2.text=Error reading local file, it does not exist at local path\: {0}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 4b311acc6..c9898814d 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -3567,6 +3567,7 @@ private void addBlackBoardAttribute(BlackboardAttribute attr, int artifactTypeId
 		PreparedStatement statement;
 		switch (attr.getAttributeType().getValueType()) {
 			case STRING:
+			case JSON:
 				statement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_STRING_ATTRIBUTE);
 				statement.clearParameters();
 				statement.setString(7, attr.getValueString());
@@ -3642,6 +3643,7 @@ String addSourceToArtifactAttribute(BlackboardAttribute attr, String source) thr
 			if (BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE != valueType) {
 				switch (valueType) {
 					case STRING:
+					case JSON:
 						valueClause = " value_text = '" + escapeSingleQuotes(attr.getValueString()) + "'";
 						break;
 					case INTEGER:
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
index 267082450..704d18b32 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.datamodel.blackboardutils;
 
+import com.google.gson.Gson;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -122,7 +123,7 @@ public String getDisplayName() {
 	private final AccountFileInstance selfAccountInstance;
 
 	// Type of accounts to be created for the module using this helper.
-	private final Account.Type accountsType;
+	private final Account.Type moduleAccountsType;
 
 	/**
 	 * Constructs a communications artifacts helper for the given source file.
@@ -146,7 +147,7 @@ public CommunicationArtifactsHelper(SleuthkitCase caseDb,
 
 		super(caseDb, moduleName, srcFile);
 
-		this.accountsType = accountsType;
+		this.moduleAccountsType = accountsType;
 		this.selfAccountInstance = getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE, ((DataSource) getAbstractFile().getDataSource()).getDeviceId(), moduleName, getAbstractFile());
 	}
 
@@ -173,7 +174,7 @@ public CommunicationArtifactsHelper(SleuthkitCase caseDb, String moduleName, Abs
 
 		super(caseDb, moduleName, srcFile);
 
-		this.accountsType = accountsType;
+		this.moduleAccountsType = accountsType;
 		this.selfAccountInstance = getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(selfAccountType, selfAccountId, moduleName, getAbstractFile());
 	}
 
@@ -219,8 +220,8 @@ public BlackboardArtifact addContact(String contactName,
 	 * @param emailAddr            Email address for the contact, may be empty
 	 *                             or null.
 	 *
-	 * At least one phone number or email address or an Id is required.
-	 * An Id may be passed in as a TSK_ID attribute in additionalAttributes.
+	 * At least one phone number or email address or an Id is required. An Id
+	 * may be passed in as a TSK_ID attribute in additionalAttributes.
 	 *
 	 * @param additionalAttributes Additional attributes for contact, may be an
 	 *                             empty list.
@@ -245,11 +246,11 @@ public BlackboardArtifact addContact(String contactName,
 		boolean hasAnyIdAttribute = false;
 		if (additionalAttributes != null) {
 			for (BlackboardAttribute attr : additionalAttributes) {
-				if ((attr.getAttributeType().getTypeName().startsWith("TSK_PHONE")) ||
-					(attr.getAttributeType().getTypeName().startsWith("TSK_EMAIL"))	||
-					(attr.getAttributeType().getTypeName().startsWith("TSK_ID")))  {
-						hasAnyIdAttribute = true;
-						break;
+				if ((attr.getAttributeType().getTypeName().startsWith("TSK_PHONE"))
+						|| (attr.getAttributeType().getTypeName().startsWith("TSK_EMAIL"))
+						|| (attr.getAttributeType().getTypeName().startsWith("TSK_ID"))) {
+					hasAnyIdAttribute = true;
+					break;
 				}
 			}
 		}
@@ -289,16 +290,16 @@ public BlackboardArtifact addContact(String contactName,
 		// if the additional attribute list has any phone/email/id attributes, create accounts & relationships for those. 
 		if ((additionalAttributes != null) && hasAnyIdAttribute) {
 			for (BlackboardAttribute bba : additionalAttributes) {
-                if (bba.getAttributeType().getTypeName().startsWith("TSK_PHONE")) {
+				if (bba.getAttributeType().getTypeName().startsWith("TSK_PHONE")) {
 					createContactMethodAccountAndRelationship(Account.Type.PHONE, bba.getValueString(), contactArtifact, 0);
-                } else if (bba.getAttributeType().getTypeName().startsWith("TSK_EMAIL")) {
-                    createContactMethodAccountAndRelationship(Account.Type.EMAIL, bba.getValueString(), contactArtifact, 0);
-                } else if (bba.getAttributeType().getTypeName().startsWith("TSK_ID")) {
-                    createContactMethodAccountAndRelationship(this.accountsType, bba.getValueString(), contactArtifact, 0);
-                } 
-            }
+				} else if (bba.getAttributeType().getTypeName().startsWith("TSK_EMAIL")) {
+					createContactMethodAccountAndRelationship(Account.Type.EMAIL, bba.getValueString(), contactArtifact, 0);
+				} else if (bba.getAttributeType().getTypeName().startsWith("TSK_ID")) {
+					createContactMethodAccountAndRelationship(this.moduleAccountsType, bba.getValueString(), contactArtifact, 0);
+				}
+			}
 		}
-		
+
 		// post artifact 
 		getSleuthkitCase().getBlackboard().postArtifact(contactArtifact, getModuleName());
 
@@ -506,7 +507,7 @@ public BlackboardArtifact addMessage(String messageType,
 			senderAccountInstance = selfAccountInstance;
 			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, selfAccountInstance.getAccount().getTypeSpecificID(), attributes);
 		} else {
-			senderAccountInstance = createAccountInstance(accountsType, senderId);
+			senderAccountInstance = createAccountInstance(moduleAccountsType, senderId);
 			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, senderId, attributes);
 		}
 
@@ -516,7 +517,7 @@ public BlackboardArtifact addMessage(String messageType,
 		if (recipientIdsList != null) {
 			for (String recipient : recipientIdsList) {
 				if (!StringUtils.isEmpty(recipient)) {
-					recipientAccountsList.add(createAccountInstance(accountsType, recipient));
+					recipientAccountsList.add(createAccountInstance(moduleAccountsType, recipient));
 				}
 			}
 			// Create a comma separated string of recipients
@@ -706,20 +707,20 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 				throw new IllegalArgumentException("Caller Id not provided for incoming call.");
 			}
 		} else {
-			callerAccountInstance = createAccountInstance(accountsType, callerId);
+			callerAccountInstance = createAccountInstance(moduleAccountsType, callerId);
 			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, callerId, attributes);
 		}
 
 		// Create a comma separated string of callee
 		List<AccountFileInstance> recipientAccountsList = new ArrayList();
 		String calleesStr = "";
-		if (! isEffectivelyEmpty(calleeIdsList)) {
+		if (!isEffectivelyEmpty(calleeIdsList)) {
 			calleesStr = addressListToString(calleeIdsList);
 			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, calleesStr, attributes);
 
 			for (String callee : calleeIdsList) {
 				if (!StringUtils.isEmpty(callee)) {
-					recipientAccountsList.add(createAccountInstance(accountsType, callee));
+					recipientAccountsList.add(createAccountInstance(moduleAccountsType, callee));
 				}
 			}
 		} else {
@@ -752,6 +753,27 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 		return callLogArtifact;
 	}
 
+	/**
+	 * Adds attachments to a message.
+	 *
+	 * @param message     Message artifact
+	 * @param attachments Attachments to add to the message.
+	 *
+	 * @throws TskCoreException If there is an error in adding attachments
+	 */
+	public void addAttachments(BlackboardArtifact message, MessageAttachments attachments) throws TskCoreException {
+
+		// Convert the MessageAttachments object to JSON string
+		Gson gson = new Gson();
+		String attachmentsJson = gson.toJson(attachments);
+
+		// Create attribute 
+		message.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ATTACHMENTS, getModuleName(), attachmentsJson));
+
+		// @TODO 5698: create a TSK_ASSOCIATED_OBJECT artifact for each fileAttachment 
+		//             to associate the file with the message.
+	}
+
 	/**
 	 * Converts a list of ids into a single comma separated string.
 	 */
@@ -785,15 +807,17 @@ private boolean isEffectivelyEmpty(Collection<String> idList) {
 		if (idList == null || idList.isEmpty()) {
 			return true;
 		}
-		
-		for (String id: idList) {
-			if (!StringUtils.isEmpty(id))
+
+		for (String id : idList) {
+			if (!StringUtils.isEmpty(id)) {
 				return false;
+			}
 		}
-		
+
 		return true;
-				
+
 	}
+
 	/**
 	 * Adds communication direction attribute to the list, if it is not unknown.
 	 */
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/FileAttachment.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/FileAttachment.java
new file mode 100644
index 000000000..327ac296e
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/FileAttachment.java
@@ -0,0 +1,148 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed 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.sleuthkit.datamodel.blackboardutils;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.TskData;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.DerivedFile;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * File attachment to a message.
+ *
+ * The file may or may not have been downloaded, and hence may or may not be
+ * part of the data data source.
+ *
+ * A file attachment may also be created for a blob that is added as a derived
+ * file.
+ *
+ */
+public final class FileAttachment {
+
+	private final String filePathName;
+	private final long objId;
+
+	// Mobile phones often create mount points to refer to SD Cards or other 
+	// fixed/removable storage media.
+	//
+	// Applications use these mount points when referring files. But they may
+	// not exist physically in the data source.
+	//
+	// Common, wellknown mount points are stripped from the file paths to
+	// accurately search for the file in the image.
+	transient private static final List<String> KNOWN_MOUNTPOINTS
+			= ImmutableList.of(
+					"/data/", // NON-NLS
+					"/storage/emulated/"); //NON-NLS
+
+	/**
+	 * Creates a file attachment from a file path.
+	 *
+	 * Searches the specified data source for the give file name and path, and
+	 * if found, saves the object Id of the file. If no match is found, then
+	 * just the pathName is remembered.
+	 *
+	 * @param caseDb     Case database.
+	 * @param dataSource Data source to search in.
+	 * @param pathName   Full path name of the attachment file.
+	 *
+	 * @throws TskCoreException IF there is an error in finding the attached
+	 *                          file.
+	 */
+	public FileAttachment(SleuthkitCase caseDb, Content dataSource, String pathName) throws TskCoreException {
+
+		//normalize the slashes.
+		this.filePathName = normalizePath(pathName);
+
+		String fileName = filePathName.substring(filePathName.lastIndexOf('/') + 1);
+		String parentPathSubString = filePathName.substring(0, filePathName.lastIndexOf('/'));
+
+		long matchedFileObjId = -1;
+		List<AbstractFile> matchedFiles = caseDb.findFiles(dataSource, fileName, parentPathSubString);
+		for (AbstractFile file : matchedFiles) {
+			if (file.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) {
+				matchedFileObjId = file.getId();
+				break;
+			}
+		}
+		objId = matchedFileObjId;
+
+	}
+
+	/**
+	 * Creates a file attachments from a derived file.
+	 *
+	 * Occasionally the contents of an attachment may be stored as a blob in an
+	 * application database. In that case, the ingest module must write out the contents 
+	 * to a local file in the case, and create a corresponding DerivedFile object.
+	 *
+	 * @param derivedFile Derived file for the attachment.
+	 */
+	public FileAttachment(DerivedFile derivedFile) {
+		objId = derivedFile.getId();
+		filePathName = derivedFile.getLocalAbsPath() + "/" + derivedFile.getName();
+	}
+
+
+	/**
+	 * Returns the full path name of the file.
+	 *
+	 * @return full path name.
+	 */
+	public String getPathName() {
+		return filePathName;
+	}
+
+	/**
+	 * Returns the objId of the attachment file, if the file was found in the
+	 * data source.
+	 *
+	 * @return object id of the file. -1 if no matching file is found.
+	 */
+	public long getObjectId() {
+		return objId;
+	}
+
+	/**
+	 * Normalizes the input path - convert all slashes to TSK convention, 
+	 * and checks for any well know mount point prefixes that need stripped.
+	 * 
+	 * @param path path to normalize
+	 * 
+	 * @return normalized path.
+	 */
+	private String normalizePath(String path) {
+		//normalize the slashes.
+		String adjustedPath = path.replace("\\", "/");
+
+		// Strip common known mountpoints.
+		for (String mountPoint : KNOWN_MOUNTPOINTS) {
+			if (adjustedPath.toLowerCase().startsWith(mountPoint)) {
+				adjustedPath = ("/").concat(adjustedPath.substring(mountPoint.length()));
+				break;
+			}
+		}
+
+		return adjustedPath;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/MessageAttachments.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/MessageAttachments.java
new file mode 100644
index 000000000..174d3c322
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/MessageAttachments.java
@@ -0,0 +1,65 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed 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.sleuthkit.datamodel.blackboardutils;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Class to represent attachments to a message.
+ *
+ * Attachments can be URL attachments or file attachments.
+ *
+ */
+public final class MessageAttachments {
+
+	private final Collection<FileAttachment> fileAttachments;
+	private final Collection<URLAttachment> urlAttachments;
+
+	/**
+	 * Builds Message attachments from the given file attachments and URL
+	 * attachments.
+	 *
+	 * @param fileAttachments Collection of file attachments.
+	 * @param urlAttachments  Collection of URL attachments.
+	 */
+	public MessageAttachments(Collection<FileAttachment> fileAttachments, Collection<URLAttachment> urlAttachments) {
+		this.fileAttachments = fileAttachments;
+		this.urlAttachments = urlAttachments;
+	}
+
+	/**
+	 * Returns collection of file attachments.
+	 *
+	 * @return Collection of File attachments.
+	 */
+	public Collection<FileAttachment> getFileAttachments() {
+		return Collections.unmodifiableCollection(fileAttachments);
+	}
+
+	/**
+	 * Returns collection of URL attachments.
+	 *
+	 * @return Collection of URL attachments.
+	 */
+	public Collection<URLAttachment> getUrlAttachments() {
+		return Collections.unmodifiableCollection(urlAttachments);
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/URLAttachment.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/URLAttachment.java
new file mode 100644
index 000000000..0cecda68c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/URLAttachment.java
@@ -0,0 +1,47 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed 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.sleuthkit.datamodel.blackboardutils;
+
+/**
+ * Represents a message attachment where a URL of the attachment is available.
+ *
+ */
+public class URLAttachment {
+
+	private final String url;
+
+	/**
+	 * Creates URL attachment.
+	 *
+	 * @param url URL of attachment.
+	 */
+	public URLAttachment(String url) {
+		this.url = url;
+	}
+
+	/**
+	 * Returns attachment URL.
+	 *
+	 * @return attachment URL.
+	 */
+	public String getURL() {
+		return url;
+	}
+
+}
-- 
GitLab