diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml
index 33c3e28eeb194552c889899877a7704fc0d7c736..2dd56aa5cb8cc433134bae908c6635793b86650f 100644
--- a/CoreLibs/nbproject/project.xml
+++ b/CoreLibs/nbproject/project.xml
@@ -973,7 +973,7 @@
             </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/dd-plist-1.20.jar</runtime-relative-path>
-                <binary-origin>C:\Users\kelly\Workspace\autopsy\CoreLibs\release\modules\ext\dd-plist-1.20.jar</binary-origin>
+                <binary-origin>release/modules/ext/dd-plist-1.20.jar</binary-origin>
             </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/dom4j-1.6.1.jar</runtime-relative-path>
diff --git a/RecentActivity/ivy.xml b/RecentActivity/ivy.xml
index 290c8371eae064e2c558862744a7107569a06a7a..ca95f14a981e828a78da5828c554a99d2c25dd33 100644
--- a/RecentActivity/ivy.xml
+++ b/RecentActivity/ivy.xml
@@ -6,4 +6,7 @@
         <conf name="recent-activity"/>
       
     </configurations>
+	<dependencies>
+		<dependency conf="recent-activity->default" org="com.googlecode.plist" name="dd-plist" rev="1.20"/>
+	</dependencies>
 </ivy-module>
diff --git a/RecentActivity/nbproject/project.properties b/RecentActivity/nbproject/project.properties
index 9736070e535c751845709a9d241ec4b2a13a5faa..4071f4a54e42ca590d3262ac2cf8b4620b0e1b68 100644
--- a/RecentActivity/nbproject/project.properties
+++ b/RecentActivity/nbproject/project.properties
@@ -1,3 +1,4 @@
+file.reference.dd-plist-1.20.jar=release/modules/ext/dd-plist-1.20.jar
 javac.source=1.8
 javac.compilerargs=-Xlint -Xlint:-serial
 license.file=../LICENSE-2.0.txt
diff --git a/RecentActivity/nbproject/project.xml b/RecentActivity/nbproject/project.xml
index 87619a83562c3fb03faf6788142f610f9d98d025..9584170602fd5b39350d8eba584200fc76ccb469 100644
--- a/RecentActivity/nbproject/project.xml
+++ b/RecentActivity/nbproject/project.xml
@@ -74,6 +74,10 @@
                 </dependency>
             </module-dependencies>
             <public-packages/>
+            <class-path-extension>
+                <runtime-relative-path>ext/dd-plist-1.20.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/dd-plist-1.20.jar</binary-origin>
+            </class-path-extension>
         </data>
     </configuration>
 </project>
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java
new file mode 100755
index 0000000000000000000000000000000000000000..3e24a1fe5d940e4e9451eddb806d4331b7555c9d
--- /dev/null
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java
@@ -0,0 +1,434 @@
+/*
+ *
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2019 Basis Technology Corp.
+ *
+ * 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.autopsy.recentactivity;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.logging.Level;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.recentactivity.BinaryCookieReader.Cookie;
+
+/**
+ * The binary cookie reader encapsulates all the knowledge of how to read the mac
+ * .binarycookie files into one class.
+ *
+ * The binarycookie file has a header which describes how many pages of cookies
+ * and where they are located. Each cookie page has a header and a list of
+ * cookies.
+ *
+ */
+public final class BinaryCookieReader implements Iterable<Cookie> {
+
+    private static final int MAGIC_SIZE = 4;
+    private static final int SIZEOF_INT_BYTES = 4;
+    private static final int PAGE_HEADER_VALUE = 256;
+
+    private static final String COOKIE_MAGIC = "cook"; //NON-NLS
+
+    private static final int MAC_EPOC_FIX = 978307200;
+
+    private final int[] pageSizeArray;
+    private final File cookieFile;
+
+    private static final Logger LOG = Logger.getLogger(BinaryCookieReader.class.getName());
+
+    /**
+     * The binary cookie reader encapsulates all the knowledge of how to read the mac
+     * .binarycookie files into one class.
+     *
+     */
+    private BinaryCookieReader(File cookieFile, int[] pageSizeArray) {
+        this.cookieFile = cookieFile;
+        this.pageSizeArray = pageSizeArray;
+    }
+
+    /**
+     * initalizeReader opens the given file, reads the header and checks that
+     * the file is a binarycookie file. This function does not keep the file
+     * open.
+     *
+     * @param file binarycookie file
+     * @return An instance of the reader
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public static BinaryCookieReader initalizeReader(File cookieFile) throws FileNotFoundException, IOException {
+        BinaryCookieReader reader = null;
+        try (DataInputStream dataStream = new DataInputStream(new FileInputStream(cookieFile))) {
+
+            byte[] magic = new byte[MAGIC_SIZE];
+            if (dataStream.read(magic) != MAGIC_SIZE) {
+                throw new IOException("Failed to read header, invalid file size (" + cookieFile.getName() + ")"); //NON-NLS
+            }
+
+            if (!(new String(magic)).equals(COOKIE_MAGIC)) {
+                throw new IOException(cookieFile.getName() + " is not a cookie file"); //NON-NLS
+            }
+
+            int[] sizeArray = null;
+            int pageCount = dataStream.readInt();
+            if (pageCount != 0) {
+                sizeArray = new int[pageCount];
+
+                for (int cnt = 0; cnt < pageCount; cnt++) {
+                    sizeArray[cnt] = dataStream.readInt();
+                }
+                
+                LOG.log(Level.INFO, "No cookies found in {0}", cookieFile.getName()); //NON-NLS
+            }
+
+            reader = new BinaryCookieReader(cookieFile, sizeArray);
+        }
+
+        return reader;
+    }
+
+    /**
+     * Creates and returns a instance of CookiePageIterator.
+     *
+     * @return CookiePageIterator
+     */
+    @Override
+    public Iterator<Cookie> iterator() {
+        return new CookiePageIterator();
+    }
+
+    /**
+     * The cookiePageIterator iterates the binarycookie file by page.
+     */
+    private class CookiePageIterator implements Iterator<Cookie> {
+
+        int pageIndex = 0;
+        CookiePage currentPage = null;
+        Iterator<Cookie> currentIterator = null;
+        DataInputStream dataStream = null;
+
+        /**
+         * The cookiePageIterator iterates the binarycookie file by page.
+         */
+        CookiePageIterator() {
+            if(pageSizeArray == null || pageSizeArray.length == 0) {
+                return;
+            }
+            
+            try {
+                dataStream = new DataInputStream(new FileInputStream(cookieFile));
+                // skip to the first page
+                dataStream.skipBytes((2 * SIZEOF_INT_BYTES) + (pageSizeArray.length * SIZEOF_INT_BYTES));
+            } catch (IOException ex) {
+                
+                String errorMessage = String.format("An error occurred creating an input stream for %s", cookieFile.getName());
+                LOG.log(Level.WARNING, errorMessage, ex); //NON-NLS
+                closeStream(); // Just incase the error was from skip
+            }
+        }
+
+        /**
+         * Returns true if there are more cookies in the binarycookie file.  
+         *
+         * @return True if there are more cookies
+         */
+        @Override
+        public boolean hasNext() {
+
+            if (dataStream == null) {
+                return false;
+            }
+            
+            if (currentIterator == null || !currentIterator.hasNext()) {
+                try {
+
+                    if (pageIndex < pageSizeArray.length) {
+                        byte[] nextPage = new byte[pageSizeArray[pageIndex]];
+                        dataStream.read(nextPage);
+
+                        currentPage = new CookiePage(nextPage);
+                        currentIterator = currentPage.iterator();
+                    } else {
+                        closeStream();
+                        return false;
+                    }
+
+                    pageIndex++;
+                } catch (IOException ex) {
+                    closeStream();
+                    String errorMessage = String.format("A read error occured for file %s (pageIndex = %d)", cookieFile.getName(), pageIndex);
+                    LOG.log(Level.WARNING, errorMessage, ex); //NON-NLS
+                    return false;
+                }
+            }
+
+            return currentIterator.hasNext();
+        }
+
+        /**
+         * Get the next cookie from the current CookieIterator.
+         *
+         * @return The next cookie
+         */
+        @Override
+        public Cookie next() {
+            // Just in case someone uses next without hasNext, this check will
+            // make sure there are more elements and that we iterate properly 
+            // through the pages.
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            return currentIterator.next();
+        }
+
+        /**
+         * Close the DataInputStream
+         */
+        private void closeStream() {
+            if (dataStream != null) {
+                try {
+                    dataStream.close();
+                    dataStream = null;
+                } catch (IOException ex) {
+                    String errorMessage = String.format("An error occurred trying to close stream for file %s", cookieFile.getName());
+                    LOG.log(Level.WARNING, errorMessage, ex); //NON-NLS
+                }
+            }
+        }
+    }
+
+    /**
+     * Wrapper class for an instance of a CookiePage in the binarycookie file.
+     */
+    private class CookiePage implements Iterable<Cookie> {
+
+        int[] cookieOffSets;
+        ByteBuffer pageBuffer;
+
+        /**
+         * Setup the CookiePage object. Calidates that the page bytes are in the
+         * correct format by checking for the header value of 0x0100.
+         *
+         * @param page byte array representing a cookie page
+         * @throws IOException
+         */
+        CookiePage(byte[] page) throws IOException {
+            if (page == null || page.length == 0) {
+                throw new IllegalArgumentException("Invalid value for page passed to CookiePage constructor"); //NON-NLS
+            }
+
+            pageBuffer = ByteBuffer.wrap(page);
+
+            if (pageBuffer.getInt() != PAGE_HEADER_VALUE) {
+                pageBuffer = null;
+                throw new IOException("Invalid file format, bad page head value found"); //NON-NLS
+            }
+
+            pageBuffer.order(ByteOrder.LITTLE_ENDIAN);
+            int count = pageBuffer.getInt();
+            cookieOffSets = new int[count];
+
+            for (int cnt = 0; cnt < count; cnt++) {
+                cookieOffSets[cnt] = pageBuffer.getInt();
+            }
+
+            pageBuffer.getInt(); // All 0, not needed
+        }
+
+        /**
+         * Returns an instance of a CookieIterator.
+         *
+         * @return CookieIterator
+         */
+        @Override
+        public Iterator<Cookie> iterator() {
+            return new CookieIterator();
+        }
+
+        /**
+         * Implements Iterator to iterate over the cookies in the page.
+         */
+        private class CookieIterator implements Iterator<Cookie> {
+
+            int index = 0;
+
+            /**
+             * Checks to see if there are more cookies.
+             *
+             * @return True if there are more cookies, false if there are not
+             */
+            @Override
+            public boolean hasNext() {
+                if (pageBuffer == null) {
+                    return false;
+                }
+
+                return index < cookieOffSets.length;
+            }
+
+            /**
+             * Gets the next cookie from the page.
+             *
+             * @return Next cookie
+             */
+            @Override
+            public Cookie next() {
+                if (!hasNext()) {
+                    throw new NoSuchElementException();
+                }
+
+                int offset = cookieOffSets[index];
+                int size = pageBuffer.getInt(offset);
+                byte[] cookieBytes = new byte[size];
+                pageBuffer.get(cookieBytes, 0, size);
+                index++;
+
+                return new Cookie(cookieBytes);
+            }
+        }
+    }
+
+    /**
+     * Represents an instance of a cookie from the binarycookie file.
+     */
+    public class Cookie {
+
+        private final static int COOKIE_HEAD_SKIP = 16;
+
+        private final double expirationDate;
+        private final double creationDate;
+
+        private final String name;
+        private final String url;
+        private final String path;
+        private final String value;
+
+        /**
+         * Creates a cookie object from the given array of bytes.
+         *
+         * @param cookieBytes Byte array for the cookie
+         */
+        protected Cookie(byte[] cookieBytes) {
+            if (cookieBytes == null || cookieBytes.length == 0) {
+                throw new IllegalArgumentException("Invalid value for cookieBytes passed to Cookie constructor"); //NON-NLS
+            }
+
+            ByteBuffer byteBuffer = ByteBuffer.wrap(cookieBytes);
+            byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+            // Skip past the four int values that we are not interested in
+            byteBuffer.position(byteBuffer.position() + COOKIE_HEAD_SKIP);
+
+            int urlOffset = byteBuffer.getInt();
+            int nameOffset = byteBuffer.getInt();
+            int pathOffset = byteBuffer.getInt();
+            int valueOffset = byteBuffer.getInt();
+            byteBuffer.getLong(); // 8 bytes of not needed
+
+            expirationDate = byteBuffer.getDouble();
+            creationDate = byteBuffer.getDouble();
+
+            url = decodeString(cookieBytes, urlOffset);
+            name = decodeString(cookieBytes, nameOffset);
+            path = decodeString(cookieBytes, pathOffset);
+            value = decodeString(cookieBytes, valueOffset);
+        }
+
+        /**
+         * Returns the expiration date of the cookie represented by this cookie
+         * object.
+         *
+         * @return Cookie expiration date in milliseconds with java epoch
+         */
+        public final Long getExpirationDate() {
+            return ((long)expirationDate) + MAC_EPOC_FIX;
+        }
+
+        /**
+         * Returns the creation date of the cookie represented by this cookie
+         * object.
+         *
+         * @return Cookie creation date in milliseconds with java epoch
+         */
+        public final Long getCreationDate() {
+            return ((long)creationDate) + MAC_EPOC_FIX;
+        }
+
+        /**
+         * Returns the url of the cookie represented by this cookie object.
+         *
+         * @return the cookie URL
+         */
+        public final String getURL() {
+            return url;
+        }
+
+        /**
+         * Returns the name of the cookie represented by this cookie object.
+         *
+         * @return The cookie name
+         */
+        public final String getName() {
+            return name;
+        }
+
+        /**
+         * Returns the path of the cookie represented by this cookie object.
+         *
+         * @return The cookie path
+         */
+        public final String getPath() {
+            return path;
+        }
+
+        /**
+         * Returns the value of the cookie represented by this cookie object.
+         *
+         * @return The cookie value
+         */
+        public final String getValue() {
+            return value;
+        }
+
+        /**
+         * Creates an ascii string from the bytes in byteArray starting at
+         * offset ending at the first null terminator found.
+         *
+         * @param byteArray Array of bytes
+         * @param offset starting offset in the array
+         * @return String with bytes converted to ascii
+         */
+        private String decodeString(byte[] byteArray, int offset) {
+            byte[] stringBytes = new byte[byteArray.length - offset];
+            for (int index = 0; index < stringBytes.length; index++) {
+                byte nibble = byteArray[offset + index];
+                if (nibble != '\0') { //NON-NLS
+                    stringBytes[index] = nibble;
+                } else {
+                    break;
+                }
+            }
+
+            return new String(stringBytes);
+        }
+    }
+}
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java
index 7ab22b4850799296b5622767bd63c1511434d198..b4f04c79cd02b382033cbd6ca47245e80236c889 100644
--- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java
@@ -22,10 +22,18 @@
  */
 package org.sleuthkit.autopsy.recentactivity;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
 import java.util.logging.Level;
 import org.openide.util.NbBundle;
 import org.openide.util.NbBundle.Messages;
@@ -35,9 +43,17 @@
 import org.sleuthkit.autopsy.coreutils.Logger;
 import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
 import org.sleuthkit.autopsy.coreutils.SQLiteDBConnect;
+import org.sleuthkit.autopsy.datamodel.ContentUtils;
 import org.sleuthkit.autopsy.ingest.IngestJobContext;
 import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException;
-import org.sleuthkit.datamodel.*;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.TskException;
+
 
 abstract class Extract {
 
@@ -216,4 +232,207 @@ public boolean foundData() {
     protected void setFoundData(boolean foundData){
         dataFound = foundData;
     }
+    
+    /**
+     * Returns the current case instance
+     * @return Current case instance
+     */
+    protected Case getCurrentCase(){
+        return this.currentCase;
+    }
+    
+    /**
+     * Creates a list of attributes for a history artifact.
+     *
+     * @param url 
+     * @param accessTime Time url was accessed
+     * @param referrer referred url
+     * @param title title of the page
+     * @param programName module name
+     * @param domain domain of the url
+     * @param user user that accessed url
+     * @return List of BlackboardAttributes for giving attributes
+     * @throws TskCoreException
+     */
+    protected Collection<BlackboardAttribute> createHistoryAttribute(String url, Long accessTime,
+            String referrer, String title, String programName, String domain, String user) throws TskCoreException {
+
+        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (url != null) ? url : "")); //NON-NLS
+
+        if (accessTime != null) {
+            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
+                    RecentActivityExtracterModuleFactory.getModuleName(), accessTime));
+        }
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_REFERRER,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (referrer != null) ? referrer : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (title != null) ? title : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (programName != null) ? programName : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (domain != null) ? domain : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_NAME,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (user != null) ? user : "")); //NON-NLS
+
+        return bbattributes;
+    }
+    
+    /**
+     * Creates a list of attributes for a cookie.
+     *
+     * @param url cookie url
+     * @param creationTime cookie creation time 
+     * @param name cookie name
+     * @param value cookie value
+     * @param programName Name of the module creating the attribute
+     * @param domain Domain of the URL
+     * @return List of BlackboarAttributes for the passed in attributes
+     */
+    protected Collection<BlackboardAttribute> createCookieAttributes(String url,
+            Long creationTime, String name, String value, String programName, String domain) {
+
+        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (url != null) ? url : "")); //NON-NLS
+
+        if (creationTime != null) {
+            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+                    RecentActivityExtracterModuleFactory.getModuleName(), creationTime));
+        }
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (name != null) ? name : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_VALUE,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (value != null) ? value : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (programName != null) ? programName : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (domain != null) ? domain : "")); //NON-NLS
+
+        return bbattributes;
+    }
+
+    /**
+     * Creates a list of bookmark attributes from the passed in parameters.
+     *
+     * @param url Bookmark url
+     * @param title Title of the bookmarked page
+     * @param creationTime Date & time at which the bookmark was created
+     * @param programName Name of the module creating the attribute
+     * @param domain The domain of the bookmark's url
+     * @return A collection of bookmark attributes
+     */
+    protected Collection<BlackboardAttribute> createBookmarkAttributes(String url, String title, Long creationTime, String programName, String domain) {
+        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (url != null) ? url : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (title != null) ? title : "")); //NON-NLS
+
+        if (creationTime != null) {
+            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
+                    RecentActivityExtracterModuleFactory.getModuleName(), creationTime));
+        }
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (programName != null) ? programName : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (domain != null) ? domain : "")); //NON-NLS
+
+        return bbattributes;
+    }
+
+     /**
+     * Creates a list of the attributes of a downloaded file
+     *
+     * @param path
+     * @param url URL of the downloaded file
+     * @param accessTime Time the download occurred
+     * @param domain Domain of the URL
+     * @param programName Name of the module creating the attribute
+     * @return A collection of attributed of a downloaded file
+     */
+    protected Collection<BlackboardAttribute> createDownloadAttributes(String path, Long pathID, String url, Long accessTime, String domain, String programName) {
+        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (path != null) ? path : "")); //NON-NLS
+
+        if (pathID != null && pathID != -1) {
+            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
+                    RecentActivityExtracterModuleFactory.getModuleName(),
+                    pathID));
+        }
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (url != null) ? url : "")); //NON-NLS
+
+        if (accessTime != null) {
+            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
+                    RecentActivityExtracterModuleFactory.getModuleName(), accessTime));
+        }
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (domain != null) ? domain : "")); //NON-NLS
+
+        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
+                RecentActivityExtracterModuleFactory.getModuleName(),
+                (programName != null) ? programName : "")); //NON-NLS
+
+        return bbattributes;
+    }
+    
+    /**
+     * Create temporary file for the given AbstractFile.  The new file will be 
+     * created in the temp directory for the module with a unique file name.
+     * 
+     * @param context
+     * @param file
+     * @return Newly created copy of the AbstractFile
+     * @throws IOException 
+     */
+    protected File createTemporaryFile(IngestJobContext context, AbstractFile file) throws IOException{
+        Path tempFilePath = Paths.get(RAImageIngestModule.getRATempPath(
+                getCurrentCase(), getName()), file.getName() + file.getId() + file.getNameExtension());
+        java.io.File tempFile = tempFilePath.toFile();
+        
+        try {
+            ContentUtils.writeToFile(file, tempFile, context::dataSourceIngestIsCancelled);
+        } catch (IOException ex) {
+            throw new IOException("Error writingToFile: " + file, ex); //NON-NLS
+        }
+         
+        return tempFile;
+    }
 }
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java
index 2264a8385078af7622e286d108bf255fccc4edec..ff793703b1db4dda9c7d5c94525222c10c99813f 100755
--- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java
@@ -48,7 +48,6 @@
 import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
 import org.sleuthkit.datamodel.AbstractFile;
 import org.sleuthkit.datamodel.BlackboardArtifact;
-import org.sleuthkit.datamodel.BlackboardAttribute;
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.TskCoreException;
 
@@ -698,180 +697,7 @@ private BlackboardArtifact getBookmarkArtifact(AbstractFile origFile, List<Strin
                 this.getName(), NetworkUtils.extractDomain(url)));
         return bbart;
     }
-
-    /**
-     * Creates a list of attributes for a history artifact.
-     *
-     * @param url 
-     * @param accessTime Time url was accessed
-     * @param referrer referred url
-     * @param title title of the page
-     * @param programName module name
-     * @param domain domain of the url
-     * @param user user that accessed url
-     * @return List of BlackboardAttributes for giving attributes
-     * @throws TskCoreException
-     */
-    private Collection<BlackboardAttribute> createHistoryAttribute(String url, Long accessTime,
-            String referrer, String title, String programName, String domain, String user) throws TskCoreException {
-
-        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (url != null) ? url : ""));
-
-        if (accessTime != null) {
-            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
-                    RecentActivityExtracterModuleFactory.getModuleName(), accessTime));
-        }
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_REFERRER,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (referrer != null) ? referrer : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (title != null) ? title : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (programName != null) ? programName : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (domain != null) ? domain : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_NAME,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (user != null) ? user : ""));
-
-        return bbattributes;
-    }
-
-    /**
-     * Creates a list of attributes for a cookie.
-     *
-     * @param url cookie url
-     * @param creationTime cookie creation time 
-     * @param name cookie name
-     * @param value cookie value
-     * @param programName Name of the module creating the attribute
-     * @param domain Domain of the URL
-     * @return List of BlackboarAttributes for the passed in attributes
-     */
-    private Collection<BlackboardAttribute> createCookieAttributes(String url,
-            Long creationTime, String name, String value, String programName, String domain) {
-
-        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (url != null) ? url : ""));
-
-        if (creationTime != null) {
-            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
-                    RecentActivityExtracterModuleFactory.getModuleName(), creationTime));
-        }
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (name != null) ? name : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_VALUE,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (value != null) ? value : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (programName != null) ? programName : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (domain != null) ? domain : ""));
-
-        return bbattributes;
-    }
-
-    /**
-     * Creates a list of the attributes of a downloaded file
-     *
-     * @param path
-     * @param url URL of the downloaded file
-     * @param accessTime Time the download occurred
-     * @param domain Domain of the URL
-     * @param programName Name of the module creating the attribute
-     * @return A collection of attributed of a downloaded file
-     */
-    private Collection<BlackboardAttribute> createDownloadAttributes(String path, String url, Long accessTime, String domain, String programName) {
-        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (path != null) ? path : ""));
-
-        long pathID = Util.findID(dataSource, path);
-        if (pathID != -1) {
-            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
-                    RecentActivityExtracterModuleFactory.getModuleName(),
-                    pathID));
-        }
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (url != null) ? url : ""));
-
-        if (accessTime != null) {
-            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
-                    RecentActivityExtracterModuleFactory.getModuleName(), accessTime));
-        }
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (domain != null) ? domain : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (programName != null) ? programName : ""));
-
-        return bbattributes;
-    }
-
-    /**
-     * Creates a list of bookmark attributes from the passed in parameters.
-     *
-     * @param url Bookmark url
-     * @param title Title of the bookmarked page
-     * @param creationTime Date & time at which the bookmark was created
-     * @param programName Name of the module creating the attribute
-     * @param domain The domain of the bookmark's url
-     * @return A collection of bookmark attributes
-     */
-    private Collection<BlackboardAttribute> createBookmarkAttributes(String url, String title, Long creationTime, String programName, String domain) {
-        Collection<BlackboardAttribute> bbattributes = new ArrayList<>();
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (url != null) ? url : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (title != null) ? title : ""));
-
-        if (creationTime != null) {
-            bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
-                    RecentActivityExtracterModuleFactory.getModuleName(), creationTime));
-        }
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (programName != null) ? programName : ""));
-
-        bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
-                RecentActivityExtracterModuleFactory.getModuleName(),
-                (domain != null) ? domain : ""));
-
-        return bbattributes;
-    }
-
+    
     /**
      * Converts a space separated string of hex values to ascii characters.
      *
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java
new file mode 100755
index 0000000000000000000000000000000000000000..afb52654b2012170d618df289ddfa0b4517c2d41
--- /dev/null
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java
@@ -0,0 +1,633 @@
+/*
+ *
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2019 Basis Technology Corp.
+ *
+ * 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.autopsy.recentactivity;
+
+import com.dd.plist.NSArray;
+import com.dd.plist.NSDate;
+import com.dd.plist.NSDictionary;
+import com.dd.plist.NSObject;
+import com.dd.plist.NSString;
+import com.dd.plist.PropertyListFormatException;
+import com.dd.plist.PropertyListParser;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+import javax.xml.parsers.ParserConfigurationException;
+import org.openide.util.NbBundle.Messages;
+import org.sleuthkit.autopsy.casemodule.services.FileManager;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.NetworkUtils;
+import org.sleuthkit.autopsy.datamodel.ContentUtils;
+import org.sleuthkit.autopsy.ingest.IngestJobContext;
+import org.sleuthkit.autopsy.ingest.IngestServices;
+import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
+import org.sleuthkit.autopsy.recentactivity.BinaryCookieReader.Cookie;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.xml.sax.SAXException;
+
+/**
+ * Extract the bookmarks, cookies, downloads and history from Safari
+ *
+ */
+final class ExtractSafari extends Extract {
+
+    private final IngestServices services = IngestServices.getInstance();
+
+    // visit_time uses an epoch of Jan 1, 2001 thus the addition of 978307200
+    private static final String HISTORY_QUERY = "SELECT url, title, visit_time + 978307200 as time FROM 'history_items' JOIN history_visits ON history_item = history_items.id;"; //NON-NLS
+
+    private static final String HISTORY_FILE_NAME = "History.db"; //NON-NLS
+    private static final String BOOKMARK_FILE_NAME = "Bookmarks.plist"; //NON-NLS
+    private static final String DOWNLOAD_FILE_NAME = "Downloads.plist"; //NON-NLS
+    private static final String COOKIE_FILE_NAME = "Cookies.binarycookies"; //NON-NLS
+    private static final String COOKIE_FOLDER = "Cookies";
+    private static final String SAFARI_FOLDER = "Safari";
+
+    private static final String HEAD_URL = "url"; //NON-NLS
+    private static final String HEAD_TITLE = "title"; //NON-NLS
+    private static final String HEAD_TIME = "time"; //NON-NLS
+
+    private static final String PLIST_KEY_CHILDREN = "Children"; //NON-NLS
+    private static final String PLIST_KEY_URL = "URLString"; //NON-NLS
+    private static final String PLIST_KEY_URI = "URIDictionary"; //NON-NLS
+    private static final String PLIST_KEY_TITLE = "title"; //NON-NLS
+    private static final String PLIST_KEY_DOWNLOAD_URL = "DownloadEntryURL"; //NON-NLS
+    private static final String PLIST_KEY_DOWNLOAD_DATE = "DownloadEntryDateAddedKey"; //NON-NLS
+    private static final String PLIST_KEY_DOWNLOAD_PATH = "DownloadEntryPath"; //NON-NLS
+    private static final String PLIST_KEY_DOWNLOAD_HISTORY = "DownloadHistory"; //NON-NLS
+
+    private static final Logger LOG = Logger.getLogger(ExtractSafari.class.getName());
+
+    @Messages({
+        "ExtractSafari_Module_Name=Safari",
+        "ExtractSafari_Error_Getting_History=An error occurred while processing Safari history files.",
+        "ExtractSafari_Error_Parsing_Bookmark=An error occured while processing Safari Bookmark files",
+        "ExtractSafari_Error_Parsing_Cookies=An error occured while processing Safari Cookies files",
+    })
+
+    /**
+     * Extract the bookmarks, cookies, downloads and history from Safari.
+     *
+     */
+    ExtractSafari() {
+
+    }
+
+    @Override
+    protected String getName() {
+        return Bundle.ExtractSafari_Module_Name();
+    }
+
+    @Override
+    void process(Content dataSource, IngestJobContext context) {
+        setFoundData(false);
+
+        try {
+            processHistoryDB(dataSource, context);
+
+        } catch (IOException | TskCoreException ex) {
+            this.addErrorMessage(Bundle.ExtractSafari_Error_Getting_History());
+            LOG.log(Level.SEVERE, "Exception thrown while processing history file: {0}", ex); //NON-NLS
+        }
+
+        try {
+            processBookmarkPList(dataSource, context);
+        } catch (IOException | TskCoreException | SAXException | PropertyListFormatException | ParseException | ParserConfigurationException ex) {
+            this.addErrorMessage(Bundle.ExtractSafari_Error_Parsing_Bookmark());
+            LOG.log(Level.SEVERE, "Exception thrown while parsing Safari Bookmarks file: {0}", ex); //NON-NLS
+        }
+        
+         try {
+            processDownloadsPList(dataSource, context);
+        } catch (IOException | TskCoreException | SAXException | PropertyListFormatException | ParseException | ParserConfigurationException ex) {
+            this.addErrorMessage(Bundle.ExtractSafari_Error_Parsing_Bookmark());
+            LOG.log(Level.SEVERE, "Exception thrown while parsing Safari Download.plist file: {0}", ex); //NON-NLS
+        }
+
+        try {
+            processBinaryCookieFile(dataSource, context);
+        } catch (IOException | TskCoreException ex) {
+            this.addErrorMessage(Bundle.ExtractSafari_Error_Parsing_Cookies());
+            LOG.log(Level.SEVERE, "Exception thrown while processing Safari cookies file: {0}", ex); //NON-NLS
+        }
+    }
+
+    /**
+     * Finds the all of the history.db files in the case looping through them to
+     * find all of the history artifacts.
+     *
+     * @throws TskCoreException
+     * @throws IOException
+     */
+    private void processHistoryDB(Content dataSource, IngestJobContext context) throws TskCoreException, IOException {
+        FileManager fileManager = getCurrentCase().getServices().getFileManager();
+
+        List<AbstractFile> historyFiles = fileManager.findFiles(dataSource, HISTORY_FILE_NAME, SAFARI_FOLDER);
+
+        if (historyFiles == null || historyFiles.isEmpty()) {
+            return;
+        }
+
+        setFoundData(true);
+
+        for (AbstractFile historyFile : historyFiles) {
+            if (context.dataSourceIngestIsCancelled()) {
+                break;
+            }
+
+            getHistory(context, historyFile);
+        }
+    }
+
+    /**
+     * Finds all Bookmark.plist files and looks for bookmark entries.
+     * @param dataSource
+     * @param context
+     * @throws TskCoreException
+     * @throws IOException
+     * @throws SAXException
+     * @throws PropertyListFormatException
+     * @throws ParseException
+     * @throws ParserConfigurationException
+     */
+    private void processBookmarkPList(Content dataSource, IngestJobContext context) throws TskCoreException, IOException, SAXException, PropertyListFormatException, ParseException, ParserConfigurationException {
+        FileManager fileManager = getCurrentCase().getServices().getFileManager();
+
+        List<AbstractFile> files = fileManager.findFiles(dataSource, BOOKMARK_FILE_NAME, SAFARI_FOLDER);
+
+        if (files == null || files.isEmpty()) {
+            return;
+        }
+
+        setFoundData(true);
+
+        for (AbstractFile file : files) {
+            if (context.dataSourceIngestIsCancelled()) {
+                break;
+            }
+
+            getBookmarks(context, file);
+        }
+    }
+    
+    /**
+     * Process the safari download.plist file.
+     * 
+     * @param dataSource
+     * @param context
+     * @throws TskCoreException
+     * @throws IOException
+     * @throws SAXException
+     * @throws PropertyListFormatException
+     * @throws ParseException
+     * @throws ParserConfigurationException
+     */
+    private void processDownloadsPList(Content dataSource, IngestJobContext context) throws TskCoreException, IOException, SAXException, PropertyListFormatException, ParseException, ParserConfigurationException {
+        FileManager fileManager = getCurrentCase().getServices().getFileManager();
+
+        List<AbstractFile> files = fileManager.findFiles(dataSource, DOWNLOAD_FILE_NAME, SAFARI_FOLDER);
+
+        if (files == null || files.isEmpty()) {
+            return;
+        }
+
+        setFoundData(true);
+
+        for (AbstractFile file : files) {
+            if (context.dataSourceIngestIsCancelled()) {
+                break;
+            }
+
+            getDownloads(dataSource, context, file);
+        }
+    }
+    
+    /**
+     * Process the Safari Cookie file.
+     * @param dataSource
+     * @param context
+     * @throws TskCoreException
+     * @throws IOException
+     */
+    private void processBinaryCookieFile(Content dataSource, IngestJobContext context) throws TskCoreException, IOException {
+        FileManager fileManager = getCurrentCase().getServices().getFileManager();
+
+        List<AbstractFile> files = fileManager.findFiles(dataSource, COOKIE_FILE_NAME, COOKIE_FOLDER);
+
+        if (files == null || files.isEmpty()) {
+            return;
+        }
+
+        setFoundData(true);
+
+        for (AbstractFile file : files) {
+            if (context.dataSourceIngestIsCancelled()) {
+                break;
+            }
+            getCookies(context, file);
+        }
+    }
+
+    /**
+     * Creates a temporary copy of historyFile and creates a list of
+     * BlackboardArtifacts for the history information in the file.
+     *
+     * @param historyFile AbstractFile version of the history file from the case
+     * @throws TskCoreException
+     * @throws IOException
+     */
+    private void getHistory(IngestJobContext context, AbstractFile historyFile) throws TskCoreException, IOException {
+        if (historyFile.getSize() == 0) {
+            return;
+        }
+
+        File tempHistoryFile = createTemporaryFile(context, historyFile);
+
+        try {
+            ContentUtils.writeToFile(historyFile, tempHistoryFile, context::dataSourceIngestIsCancelled);
+        } catch (IOException ex) {
+            throw new IOException("Error writingToFile: " + historyFile, ex); //NON-NLS
+        }
+
+        try {
+            Collection<BlackboardArtifact> bbartifacts = getHistoryArtifacts(historyFile, tempHistoryFile.toPath());
+            if (!bbartifacts.isEmpty()) {
+                services.fireModuleDataEvent(new ModuleDataEvent(
+                        RecentActivityExtracterModuleFactory.getModuleName(),
+                        BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY, bbartifacts));
+            }
+        } finally {
+            tempHistoryFile.delete();
+        }
+    }
+
+    /**
+     * Creates a temporary bookmark file from the AbstractFile and creates
+     * BlackboardArtifacts for the any bookmarks found.
+     *
+     * @param context IngestJobContext object
+     * @param file AbstractFile from case
+     * @throws TskCoreException
+     * @throws IOException
+     * @throws SAXException
+     * @throws PropertyListFormatException
+     * @throws ParseException
+     * @throws ParserConfigurationException
+     */
+    private void getBookmarks(IngestJobContext context, AbstractFile file) throws TskCoreException, IOException, SAXException, PropertyListFormatException, ParseException, ParserConfigurationException {
+        if (file.getSize() == 0) {
+            return;
+        }
+
+        File tempFile = createTemporaryFile(context, file);
+
+        try {
+            Collection<BlackboardArtifact> bbartifacts = getBookmarkArtifacts(file, tempFile);
+            if (!bbartifacts.isEmpty()) {
+                services.fireModuleDataEvent(new ModuleDataEvent(
+                        RecentActivityExtracterModuleFactory.getModuleName(),
+                        BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK, bbartifacts));
+            }
+        } finally {
+            tempFile.delete();
+        }
+
+    }
+    
+    /**
+     * Creates a temporary downloads file from the AbstractFile and creates
+     * BlackboardArtifacts for the any downloads found.
+     *
+     * @param context IngestJobContext object
+     * @param file AbstractFile from case
+     * @throws TskCoreException
+     * @throws IOException
+     * @throws SAXException
+     * @throws PropertyListFormatException
+     * @throws ParseException
+     * @throws ParserConfigurationException
+     */
+    private void getDownloads(Content dataSource, IngestJobContext context, AbstractFile file) throws TskCoreException, IOException, SAXException, PropertyListFormatException, ParseException, ParserConfigurationException {
+        if (file.getSize() == 0) {
+            return;
+        }
+
+        File tempFile = createTemporaryFile(context, file);
+
+        try {
+            Collection<BlackboardArtifact> bbartifacts = getDownloadArtifacts(dataSource, file, tempFile);
+            if (!bbartifacts.isEmpty()) {
+                services.fireModuleDataEvent(new ModuleDataEvent(
+                        RecentActivityExtracterModuleFactory.getModuleName(),
+                        BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD, bbartifacts));
+            }
+        } finally {
+            if (tempFile != null) {
+                tempFile.delete();
+            }
+        }
+
+    }
+    
+    /**
+     * Creates a temporary copy of the Cookie file and creates a list of cookie 
+     * BlackboardArtifacts.
+     * 
+     * @param context IngetstJobContext
+     * @param file Original Cookie file from the case
+     * @throws TskCoreException
+     * @throws IOException
+     */
+    private void getCookies(IngestJobContext context, AbstractFile file) throws TskCoreException, IOException {
+        if (file.getSize() == 0) {
+            return;
+        }
+
+        File tempFile = null;
+
+        try {
+            tempFile = createTemporaryFile(context, file);
+
+            Collection<BlackboardArtifact> bbartifacts = getCookieArtifacts(file, tempFile);
+
+            if (!bbartifacts.isEmpty()) {
+                services.fireModuleDataEvent(new ModuleDataEvent(
+                        RecentActivityExtracterModuleFactory.getModuleName(),
+                        BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE, bbartifacts));
+            }
+        } finally {
+            if (tempFile != null) {
+                tempFile.delete();
+            }
+        }
+    }
+
+    /**
+     * Queries the history db for the history information creating a list of
+     * BlackBoardArtifact for each row returned from the db.
+     *
+     * @param origFile AbstractFile of the history file from the case
+     * @param tempFilePath Path to temporary copy of the history db
+     * @return Blackboard Artifacts for the history db or null if there are no
+     * history artifacts
+     * @throws TskCoreException
+     */
+    private Collection<BlackboardArtifact> getHistoryArtifacts(AbstractFile origFile, Path tempFilePath) throws TskCoreException {
+        List<HashMap<String, Object>> historyList = this.dbConnect(tempFilePath.toString(), HISTORY_QUERY);
+
+        if (historyList == null || historyList.isEmpty()) {
+            return null;
+        }
+
+        Collection<BlackboardArtifact> bbartifacts = new ArrayList<>();
+        for (HashMap<String, Object> row : historyList) {
+            String url = row.get(HEAD_URL).toString();
+            String title = row.get(HEAD_TITLE).toString();
+            Long time = (Double.valueOf(row.get(HEAD_TIME).toString())).longValue();
+
+            BlackboardArtifact bbart = origFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY);
+            bbart.addAttributes(createHistoryAttribute(url, time, null, title,
+                    this.getName(), NetworkUtils.extractDomain(url), null));
+            bbartifacts.add(bbart);
+        }
+
+        return bbartifacts;
+    }
+
+    /**
+     * Parses the temporary version of bookmarks.plist and creates
+     *
+     * @param origFile The origFile Bookmark.plist file from the case
+     * @param tempFile The temporary local version of Bookmark.plist
+     * @return Collection of BlackboardArtifacts for the bookmarks in origFile
+     * @throws IOException
+     * @throws PropertyListFormatException
+     * @throws ParseException
+     * @throws ParserConfigurationException
+     * @throws SAXException
+     * @throws TskCoreException
+     */
+    private Collection<BlackboardArtifact> getBookmarkArtifacts(AbstractFile origFile, File tempFile) throws IOException, PropertyListFormatException, ParseException, ParserConfigurationException, SAXException, TskCoreException {
+        Collection<BlackboardArtifact> bbartifacts = new ArrayList<>();
+
+        try {
+            NSDictionary root = (NSDictionary) PropertyListParser.parse(tempFile);
+
+            parseBookmarkDictionary(bbartifacts, origFile, root);
+        } catch (PropertyListFormatException ex) {
+            PropertyListFormatException plfe = new PropertyListFormatException(origFile.getName() + ": " + ex.getMessage());
+            plfe.setStackTrace(ex.getStackTrace());
+            throw plfe;
+        } catch (ParseException ex) {
+            ParseException pe = new ParseException(origFile.getName() + ": " + ex.getMessage(), ex.getErrorOffset());
+            pe.setStackTrace(ex.getStackTrace());
+            throw pe;
+        } catch (ParserConfigurationException ex) {
+            ParserConfigurationException pce = new ParserConfigurationException(origFile.getName() + ": " + ex.getMessage());
+            pce.setStackTrace(ex.getStackTrace());
+            throw pce;
+        } catch (SAXException ex) {
+            SAXException se = new SAXException(origFile.getName() + ": " + ex.getMessage());
+            se.setStackTrace(ex.getStackTrace());
+            throw se;
+        }
+
+        return bbartifacts;
+    }
+    
+    /**
+     * Finds the download entries in the tempFile and creates a list of artifacts from them.
+     * 
+     * @param origFile Download.plist file from case
+     * @param tempFile Temporary copy of download.plist file
+     * @return Collection of BlackboardArtifacts for the downloads in origFile
+     * @throws IOException
+     * @throws PropertyListFormatException
+     * @throws ParseException
+     * @throws ParserConfigurationException
+     * @throws SAXException
+     * @throws TskCoreException 
+     */
+    private Collection<BlackboardArtifact> getDownloadArtifacts(Content dataSource, AbstractFile origFile, File tempFile)throws IOException, PropertyListFormatException, ParseException, ParserConfigurationException, SAXException, TskCoreException {
+         Collection<BlackboardArtifact> bbartifacts = null;
+
+        try {
+            while(true){
+                NSDictionary root = (NSDictionary)PropertyListParser.parse(tempFile);
+
+                if(root == null)
+                    break;
+
+                NSArray nsArray = (NSArray)root.get(PLIST_KEY_DOWNLOAD_HISTORY);
+
+                if(nsArray == null)
+                    break;
+           
+                NSObject[] objectArray = nsArray.getArray();
+                bbartifacts = new ArrayList<>();
+
+                for(NSObject obj: objectArray){
+                    if(obj instanceof NSDictionary){
+                        bbartifacts.add(parseDownloadDictionary(dataSource, origFile, (NSDictionary)obj));
+                    }
+                }
+                break;
+            }
+            
+        } catch (PropertyListFormatException ex) {
+            PropertyListFormatException plfe = new PropertyListFormatException(origFile.getName() + ": " + ex.getMessage());
+            plfe.setStackTrace(ex.getStackTrace());
+            throw plfe;
+        } catch (ParseException ex) {
+            ParseException pe = new ParseException(origFile.getName() + ": " + ex.getMessage(), ex.getErrorOffset());
+            pe.setStackTrace(ex.getStackTrace());
+            throw pe;
+        } catch (ParserConfigurationException ex) {
+            ParserConfigurationException pce = new ParserConfigurationException(origFile.getName() + ": " + ex.getMessage());
+            pce.setStackTrace(ex.getStackTrace());
+            throw pce;
+        } catch (SAXException ex) {
+            SAXException se = new SAXException(origFile.getName() + ": " + ex.getMessage());
+            se.setStackTrace(ex.getStackTrace());
+            throw se;
+        }
+
+        return bbartifacts;
+    }
+    
+    /**
+     * Finds the cookies in the tempFile creating a list of BlackboardArtifacts
+     * each representing one cookie.
+     *
+     * @param origFile Original Cookies.binarycookie file from case
+     * @param tempFile Temporary copy of the cookies file
+     * @return List of Blackboard Artifacts, one for each cookie
+     * @throws TskCoreException
+     * @throws IOException
+     */
+    private Collection<BlackboardArtifact> getCookieArtifacts(AbstractFile origFile, File tempFile) throws TskCoreException, IOException {
+        Collection<BlackboardArtifact> bbartifacts = null;
+        BinaryCookieReader reader = BinaryCookieReader.initalizeReader(tempFile);
+
+        if (reader != null) {
+            bbartifacts = new ArrayList<>();
+
+            Iterator<Cookie> iter = reader.iterator();
+            while (iter.hasNext()) {
+                Cookie cookie = iter.next();
+
+                BlackboardArtifact bbart = origFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE);
+                bbart.addAttributes(createCookieAttributes(cookie.getURL(), cookie.getCreationDate(), cookie.getName(), cookie.getValue(), this.getName(), NetworkUtils.extractDomain(cookie.getURL())));
+                bbartifacts.add(bbart);
+            }
+        }
+
+        return bbartifacts;
+    }
+
+    /**
+     * Parses the plist object to find the bookmark child objects, then creates
+     * an artifact with the bookmark information.
+     *
+     * @param bbartifacts BlackboardArtifact list to add new the artifacts to
+     * @param origFile The origFile Bookmark.plist file from the case
+     * @param root NSDictionary object to parse
+     * @throws TskCoreException
+     */
+    private void parseBookmarkDictionary(Collection<BlackboardArtifact> bbartifacts, AbstractFile origFile, NSDictionary root) throws TskCoreException {
+        if (root.containsKey(PLIST_KEY_CHILDREN)) {
+            NSArray children = (NSArray) root.objectForKey(PLIST_KEY_CHILDREN);
+
+            if (children != null) {
+                for (NSObject obj : children.getArray()) {
+                    parseBookmarkDictionary(bbartifacts, origFile, (NSDictionary) obj);
+                }
+            }
+        } else if (root.containsKey(PLIST_KEY_URL)) {
+            String url = null;
+            String title = null;
+
+            NSString nsstr = (NSString) root.objectForKey(PLIST_KEY_URL);
+            if (nsstr != null) {
+                url = nsstr.toString();
+            }
+
+            NSDictionary dic = (NSDictionary) root.get(PLIST_KEY_URI);
+
+            nsstr = (NSString) root.objectForKey(PLIST_KEY_TITLE);
+
+            if (nsstr != null) {
+                title = ((NSString) dic.get(PLIST_KEY_TITLE)).toString();
+            }
+
+            if (url != null || title != null) {
+                BlackboardArtifact bbart = origFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK);
+                bbart.addAttributes(createBookmarkAttributes(url, title, null, getName(), NetworkUtils.extractDomain(url)));
+                bbartifacts.add(bbart);
+            }
+        }
+    }
+    
+    /**
+     * Parse the NSDictionary object that represents one download.
+     *
+     * @param origFile Download.plist file from the case
+     * @param entry One NSDictionary Object that represents one download
+     * instance
+     * @return a Blackboard Artifact for the download.
+     * @throws TskCoreException
+     */
+    private BlackboardArtifact parseDownloadDictionary(Content dataSource, AbstractFile origFile, NSDictionary entry) throws TskCoreException {
+        String url = null;
+        String path = null;
+        Long time = null;
+        Long pathID = null;
+
+        NSString nsstring = (NSString) entry.get(PLIST_KEY_DOWNLOAD_URL);
+        if (nsstring != null) {
+            url = nsstring.toString();
+        }
+
+        nsstring = (NSString) entry.get(PLIST_KEY_DOWNLOAD_PATH);
+        if (nsstring != null) {
+            path = nsstring.toString();
+            pathID = Util.findID(dataSource, path);
+        }
+
+        NSDate date = (NSDate) entry.get(PLIST_KEY_DOWNLOAD_DATE);
+        if (date != null) {
+            time = date.getDate().getTime();
+        }
+
+        BlackboardArtifact bbart = origFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD);
+        bbart.addAttributes(this.createDownloadAttributes(path, pathID, url, time, NetworkUtils.extractDomain(url), getName()));
+
+        return bbart;
+    }
+}
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java
index 7e959660f492a5ec8b049fc8c7ff97e73b0e6268..434cf968ded361373ed5f33ecee4d198cc4a7553 100644
--- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java
@@ -74,11 +74,13 @@ public void startUp(IngestJobContext context) throws IngestModuleException {
         Extract SEUQA = new SearchEngineURLQueryAnalyzer();
         Extract osExtract = new ExtractOs();
         Extract dataSourceAnalyzer = new DataSourceUsageAnalyzer();
+        Extract safari = new ExtractSafari();
 
         extractors.add(chrome);
         extractors.add(firefox);
         extractors.add(iexplore);
         extractors.add(edge);
+        extractors.add(safari);
         extractors.add(recentDocuments);
         extractors.add(SEUQA); // this needs to run after the web browser modules
         extractors.add(registry); // this should run after quicker modules like the browser modules and needs to run before the DataSourceUsageAnalyzer
@@ -89,6 +91,7 @@ public void startUp(IngestJobContext context) throws IngestModuleException {
         browserExtractors.add(firefox);
         browserExtractors.add(iexplore);
         browserExtractors.add(edge);
+        browserExtractors.add(safari);
 
         for (Extract extractor : extractors) {
             extractor.init();