diff --git a/.gitignore b/.gitignore
index 4e6967c19d17ec5d6a26347e06e1d1855926812f..2030c73a9ea767223857669fa1f1b6c9c1fe7f85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
 !/apps/accessibility
 !/apps/cloud_federation_api
 !/apps/comments
+!/apps/contactsinteraction
 !/apps/dav
 !/apps/files
 !/apps/federation
diff --git a/apps/contactsinteraction/appinfo/app.php b/apps/contactsinteraction/appinfo/app.php
new file mode 100644
index 0000000000000000000000000000000000000000..7bc55c958dd812a3469cc7ecee0eab78eb6e80e8
--- /dev/null
+++ b/apps/contactsinteraction/appinfo/app.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+\OC::$server->query(\OCA\ContactsInteraction\AppInfo\Application::class);
diff --git a/apps/contactsinteraction/appinfo/info.xml b/apps/contactsinteraction/appinfo/info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f4e18611150aed848e5a48533dc4ca2979cf2c09
--- /dev/null
+++ b/apps/contactsinteraction/appinfo/info.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
+	  xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
+	<id>contactsinteraction</id>
+	<name>Contacts Interaction</name>
+	<summary>Manages interaction between users and contacts</summary>
+	<description>Collect data about user and contacts interactions and provide an address book for the data</description>
+	<version>1.0.0</version>
+	<licence>agpl</licence>
+	<author>Christoph Wurst</author>
+	<namespace>ContactsInteraction</namespace>
+	<types>
+		<dav/>
+	</types>
+	<default_enable/>
+	<category>integration</category>
+	<category>social</category>
+	<bugs>https://github.com/nextcloud/server/issues</bugs>
+	<dependencies>
+		<nextcloud min-version="19" max-version="19"/>
+	</dependencies>
+	<background-jobs>
+		<job>OCA\ContactsInteraction\BackgroundJob\CleanupJob</job>
+	</background-jobs>
+	<sabre>
+		<address-book-plugins>
+			<plugin>OCA\ContactsInteraction\AddressBookProvider</plugin>
+		</address-book-plugins>
+	</sabre>
+</info>
diff --git a/apps/contactsinteraction/composer/autoload.php b/apps/contactsinteraction/composer/autoload.php
new file mode 100644
index 0000000000000000000000000000000000000000..7bf597cd9cddd1afd7ba0e004f5b1bc57a2e5c2c
--- /dev/null
+++ b/apps/contactsinteraction/composer/autoload.php
@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitContactsInteraction::getLoader();
diff --git a/apps/contactsinteraction/composer/composer.json b/apps/contactsinteraction/composer/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..232fef13e8178219a0472259580a049595d9cfbc
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer.json
@@ -0,0 +1,13 @@
+{
+    "config" : {
+        "vendor-dir": ".",
+        "optimize-autoloader": true,
+        "classmap-authoritative": true,
+        "autoloader-suffix": "ContactsInteraction"
+    },
+    "autoload" : {
+        "psr-4": {
+            "OCA\\ContactsInteraction\\": "../lib/"
+        }
+    }
+}
diff --git a/apps/contactsinteraction/composer/composer/ClassLoader.php b/apps/contactsinteraction/composer/composer/ClassLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..fce8549f0781bafdc7da2301b84d048286757445
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/ClassLoader.php
@@ -0,0 +1,445 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    http://www.php-fig.org/psr/psr-0/
+ * @see    http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
+    private $useIncludePath = false;
+    private $classMap = array();
+    private $classMapAuthoritative = false;
+    private $missingClasses = array();
+    private $apcuPrefix;
+
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', $this->prefixesPsr0);
+        }
+
+        return array();
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array $classMap Class to filename map
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    (array) $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix The prefix/namespace, with trailing '\\'
+     * @param array|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return bool|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            includeFile($file);
+
+            return true;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+    include $file;
+}
diff --git a/apps/contactsinteraction/composer/composer/LICENSE b/apps/contactsinteraction/composer/composer/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f27399a042d95c4708af3a8c74d35d338763cf8f
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/apps/contactsinteraction/composer/composer/autoload_classmap.php b/apps/contactsinteraction/composer/composer/autoload_classmap.php
new file mode 100644
index 0000000000000000000000000000000000000000..d66c04af714e0f7a6dc2fbfcc093384684f21bb8
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/autoload_classmap.php
@@ -0,0 +1,19 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = $vendorDir;
+
+return array(
+    'OCA\\ContactsInteraction\\AddressBook' => $baseDir . '/../lib/AddressBook.php',
+    'OCA\\ContactsInteraction\\AddressBookProvider' => $baseDir . '/../lib/AddressBookProvider.php',
+    'OCA\\ContactsInteraction\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
+    'OCA\\ContactsInteraction\\BackgroundJob\\CleanupJob' => $baseDir . '/../lib/BackgroundJob/CleanupJob.php',
+    'OCA\\ContactsInteraction\\Card' => $baseDir . '/../lib/Card.php',
+    'OCA\\ContactsInteraction\\Db\\CardSearchDao' => $baseDir . '/../lib/Db/CardSearchDao.php',
+    'OCA\\ContactsInteraction\\Db\\RecentContact' => $baseDir . '/../lib/Db/RecentContact.php',
+    'OCA\\ContactsInteraction\\Db\\RecentContactMapper' => $baseDir . '/../lib/Db/RecentContactMapper.php',
+    'OCA\\ContactsInteraction\\Listeners\\ContactInteractionListener' => $baseDir . '/../lib/Listeners/ContactInteractionListener.php',
+    'OCA\\ContactsInteraction\\Migration\\Version010000Date20200304152605' => $baseDir . '/../lib/Migration/Version010000Date20200304152605.php',
+);
diff --git a/apps/contactsinteraction/composer/composer/autoload_namespaces.php b/apps/contactsinteraction/composer/composer/autoload_namespaces.php
new file mode 100644
index 0000000000000000000000000000000000000000..71c9e91858d8e1304b3bac8426d9457257afa03c
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = $vendorDir;
+
+return array(
+);
diff --git a/apps/contactsinteraction/composer/composer/autoload_psr4.php b/apps/contactsinteraction/composer/composer/autoload_psr4.php
new file mode 100644
index 0000000000000000000000000000000000000000..945013a79f5e9072282a923f3c2ae32429916164
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/autoload_psr4.php
@@ -0,0 +1,10 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = $vendorDir;
+
+return array(
+    'OCA\\ContactsInteraction\\' => array($baseDir . '/../lib'),
+);
diff --git a/apps/contactsinteraction/composer/composer/autoload_real.php b/apps/contactsinteraction/composer/composer/autoload_real.php
new file mode 100644
index 0000000000000000000000000000000000000000..efb953248d55917cb78cf766667308ffec596050
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/autoload_real.php
@@ -0,0 +1,46 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitContactsInteraction
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInitContactsInteraction', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+        spl_autoload_unregister(array('ComposerAutoloaderInitContactsInteraction', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require_once __DIR__ . '/autoload_static.php';
+
+            call_user_func(\Composer\Autoload\ComposerStaticInitContactsInteraction::getInitializer($loader));
+        } else {
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }
+
+        $loader->setClassMapAuthoritative(true);
+        $loader->register(true);
+
+        return $loader;
+    }
+}
diff --git a/apps/contactsinteraction/composer/composer/autoload_static.php b/apps/contactsinteraction/composer/composer/autoload_static.php
new file mode 100644
index 0000000000000000000000000000000000000000..892ed8d92f2aa1cd0fbe8fb39f97ecd652a0a711
--- /dev/null
+++ b/apps/contactsinteraction/composer/composer/autoload_static.php
@@ -0,0 +1,45 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitContactsInteraction
+{
+    public static $prefixLengthsPsr4 = array (
+        'O' => 
+        array (
+            'OCA\\ContactsInteraction\\' => 24,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'OCA\\ContactsInteraction\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/../lib',
+        ),
+    );
+
+    public static $classMap = array (
+        'OCA\\ContactsInteraction\\AddressBook' => __DIR__ . '/..' . '/../lib/AddressBook.php',
+        'OCA\\ContactsInteraction\\AddressBookProvider' => __DIR__ . '/..' . '/../lib/AddressBookProvider.php',
+        'OCA\\ContactsInteraction\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
+        'OCA\\ContactsInteraction\\BackgroundJob\\CleanupJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupJob.php',
+        'OCA\\ContactsInteraction\\Card' => __DIR__ . '/..' . '/../lib/Card.php',
+        'OCA\\ContactsInteraction\\Db\\CardSearchDao' => __DIR__ . '/..' . '/../lib/Db/CardSearchDao.php',
+        'OCA\\ContactsInteraction\\Db\\RecentContact' => __DIR__ . '/..' . '/../lib/Db/RecentContact.php',
+        'OCA\\ContactsInteraction\\Db\\RecentContactMapper' => __DIR__ . '/..' . '/../lib/Db/RecentContactMapper.php',
+        'OCA\\ContactsInteraction\\Listeners\\ContactInteractionListener' => __DIR__ . '/..' . '/../lib/Listeners/ContactInteractionListener.php',
+        'OCA\\ContactsInteraction\\Migration\\Version010000Date20200304152605' => __DIR__ . '/..' . '/../lib/Migration/Version010000Date20200304152605.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInitContactsInteraction::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitContactsInteraction::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInitContactsInteraction::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}
diff --git a/apps/contactsinteraction/lib/AddressBook.php b/apps/contactsinteraction/lib/AddressBook.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e015780378c8e66433e44cb44df462386ae2e4e
--- /dev/null
+++ b/apps/contactsinteraction/lib/AddressBook.php
@@ -0,0 +1,178 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction;
+
+use Exception;
+use OCA\ContactsInteraction\AppInfo\Application;
+use OCA\ContactsInteraction\Db\RecentContact;
+use OCA\ContactsInteraction\Db\RecentContactMapper;
+use OCA\DAV\CardDAV\Integration\ExternalAddressBook;
+use OCA\DAV\DAV\Sharing\Plugin;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\IL10N;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\NotImplemented;
+use Sabre\DAV\PropPatch;
+use Sabre\DAVACL\ACLTrait;
+use Sabre\DAVACL\IACL;
+
+class AddressBook extends ExternalAddressBook implements IACL {
+
+	public const URI = 'recent';
+
+	use ACLTrait;
+
+	/** @var RecentContactMapper */
+	private $mapper;
+
+	/** @var IL10N */
+	private $l10n;
+
+	/** @var string */
+	private $principalUri;
+
+	public function __construct(RecentContactMapper $mapper,
+								IL10N $l10n,
+								string $principalUri) {
+		parent::__construct(Application::APP_ID, self::URI);
+
+		$this->mapper = $mapper;
+		$this->l10n = $l10n;
+		$this->principalUri = $principalUri;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function delete(): void {
+		throw new Exception("This addressbook is immutable");
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function createFile($name, $data = null) {
+		throw new Exception("This addressbook is immutable");
+	}
+
+	/**
+	 * @inheritDoc
+	 * @throws NotFound
+	 */
+	public function getChild($name) {
+		try {
+			return new Card(
+				$this->mapper->find(
+					$this->getUid(),
+					(int)$name
+				),
+				$this->principalUri,
+				$this->getACL()
+			);
+		} catch (DoesNotExistException $ex) {
+			throw new NotFound("Contact does not exist: " . $ex->getMessage(), 0, $ex);
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getChildren(): array {
+		return array_map(
+			function (RecentContact $contact) {
+				return new Card(
+					$contact,
+					$this->principalUri,
+					$this->getACL()
+				);
+			},
+			$this->mapper->findAll($this->getUid())
+		);
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function childExists($name) {
+		try {
+			$this->mapper->find(
+				$this->getUid(),
+				(int)$name
+			);
+			return true;
+		} catch (DoesNotExistException $e) {
+			return false;
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getLastModified() {
+		throw new NotImplemented();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function propPatch(PropPatch $propPatch) {
+		throw new Exception("This addressbook is immutable");
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getProperties($properties) {
+		return [
+			'principaluri' => $this->principalUri,
+			'{DAV:}displayname' => $this->l10n->t('Recently contacted'),
+			'{' . Plugin::NS_OWNCLOUD . '}read-only' => true,
+		];
+	}
+
+	public function getOwner(): string {
+		return $this->principalUri;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getACL() {
+		return [
+			[
+				'privilege' => '{DAV:}read',
+				'principal' => $this->getOwner(),
+				'protected' => true,
+			],
+		];
+	}
+
+	private function getUid(): string {
+		list(, $uid) = \Sabre\Uri\split($this->principalUri);
+		return $uid;
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/AddressBookProvider.php b/apps/contactsinteraction/lib/AddressBookProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d16d1da0a5ccd9229044754d43f2c5feec12c09
--- /dev/null
+++ b/apps/contactsinteraction/lib/AddressBookProvider.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction;
+
+use OCA\ContactsInteraction\AppInfo\Application;
+use OCA\ContactsInteraction\Db\RecentContactMapper;
+use OCA\DAV\CardDAV\Integration\ExternalAddressBook;
+use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
+use OCP\IL10N;
+
+class AddressBookProvider implements IAddressBookProvider {
+
+	/** @var RecentContactMapper */
+	private $mapper;
+
+	/** @var IL10N */
+	private $l10n;
+
+	public function __construct(RecentContactMapper $mapper, IL10N $l10n) {
+		$this->mapper = $mapper;
+		$this->l10n = $l10n;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getAppId(): string {
+		return Application::APP_ID;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function fetchAllForAddressBookHome(string $principalUri): array {
+		return [
+			new AddressBook($this->mapper, $this->l10n, $principalUri)
+		];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function hasAddressBookInAddressBookHome(string $principalUri, string $uri): bool {
+		return $uri === AddressBook::URI;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getAddressBookInAddressBookHome(string $principalUri, string $uri): ?ExternalAddressBook {
+		if ($uri === AddressBook::URI) {
+			return new AddressBook($this->mapper, $this->l10n, $principalUri);
+		}
+
+		return null;
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/AppInfo/Application.php b/apps/contactsinteraction/lib/AppInfo/Application.php
new file mode 100644
index 0000000000000000000000000000000000000000..674034cbe48e601991bd67a73bb260398c64760e
--- /dev/null
+++ b/apps/contactsinteraction/lib/AppInfo/Application.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\AppInfo;
+
+use OCA\ContactsInteraction\AddressBook;
+use OCA\ContactsInteraction\Listeners\ContactInteractionListener;
+use OCA\ContactsInteraction\Store;
+use OCP\AppFramework\App;
+use OCP\AppFramework\IAppContainer;
+use OCP\Contacts\Events\ContactInteractedWithEvent;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IL10N;
+
+class Application extends App {
+
+	public const APP_ID = 'contactsinteraction';
+
+	public function __construct() {
+		parent::__construct(self::APP_ID);
+
+		$this->registerListeners($this->getContainer()->query(IEventDispatcher::class));
+	}
+
+	private function registerListeners(IEventDispatcher $dispatcher): void {
+		$dispatcher->addServiceListener(ContactInteractedWithEvent::class, ContactInteractionListener::class);
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php b/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..0efc9d54e81fbc4769379e05e3cce831e3e4089c
--- /dev/null
+++ b/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\BackgroundJob;
+
+use OCA\ContactsInteraction\Db\RecentContactMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+
+class CleanupJob extends TimedJob {
+
+	/** @var RecentContactMapper */
+	private $mapper;
+
+	public function __construct(ITimeFactory $time,
+								RecentContactMapper $mapper) {
+		parent::__construct($time);
+
+		$this->setInterval(12 * 60 * 60);
+
+		$this->mapper = $mapper;
+	}
+
+	protected function run($argument) {
+		$time = $this->time->getDateTime();
+		$time->modify('-7days');
+		$this->mapper->cleanUp($time->getTimestamp());
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/Card.php b/apps/contactsinteraction/lib/Card.php
new file mode 100644
index 0000000000000000000000000000000000000000..264f0ebe96f5b83f6b725e163fa0fde1a970e189
--- /dev/null
+++ b/apps/contactsinteraction/lib/Card.php
@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction;
+
+use OCA\ContactsInteraction\Db\RecentContact;
+use Sabre\CardDAV\ICard;
+use Sabre\DAV\Exception\NotImplemented;
+use Sabre\DAVACL\ACLTrait;
+use Sabre\DAVACL\IACL;
+
+class Card implements ICard, IACL {
+
+	use ACLTrait;
+
+	/** @var RecentContact */
+	private $contact;
+
+	/** @var string */
+	private $principal;
+
+	/** @var array */
+	private $acls;
+
+	public function __construct(RecentContact $contact, string $principal, array $acls) {
+		$this->contact = $contact;
+		$this->principal = $principal;
+		$this->acls = $acls;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getOwner(): ?string {
+		$this->principal;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getACL(): array {
+		return $this->acls;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function setAcls(array $acls): void {
+		throw new NotImplemented();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function put($data): ?string {
+		throw new NotImplemented();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function get() {
+		return $this->contact->getCard();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getContentType(): ?string {
+		return 'text/vcard; charset=utf-8';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getETag(): ?string {
+		return null;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getSize(): int {
+		throw new NotImplemented();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function delete(): void {
+		throw new NotImplemented();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getName(): string {
+		return (string) $this->contact->getId();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function setName($name): void {
+		throw new NotImplemented();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	function getLastModified(): ?int {
+		return $this->contact->getLastContact();
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/Db/CardSearchDao.php b/apps/contactsinteraction/lib/Db/CardSearchDao.php
new file mode 100644
index 0000000000000000000000000000000000000000..8370203bb9e0ecf3620a83ba34f0c0a62b758232
--- /dev/null
+++ b/apps/contactsinteraction/lib/Db/CardSearchDao.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\Db;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\IUser;
+
+class CardSearchDao {
+
+	/** @var IDBConnection */
+	private $db;
+
+	public function __construct(IDBConnection $db) {
+		$this->db = $db;
+	}
+
+	public function findExisting(IUser $user,
+								 ?string $uid,
+								 ?string $email,
+								 ?string $cloudId): ?string {
+		$addressbooksQuery = $this->db->getQueryBuilder();
+		$cardQuery = $this->db->getQueryBuilder();
+		$propQuery = $this->db->getQueryBuilder();
+
+		$propOr = $propQuery->expr()->orX();
+		if ($uid !== null) {
+			$propOr->add($propQuery->expr()->andX(
+				$propQuery->expr()->eq('name', $cardQuery->createNamedParameter('UID')),
+				$propQuery->expr()->eq('value', $cardQuery->createNamedParameter($uid))
+			));
+		}
+		if ($email !== null) {
+			$propOr->add($propQuery->expr()->andX(
+				$propQuery->expr()->eq('name', $cardQuery->createNamedParameter('EMAIL')),
+				$propQuery->expr()->eq('value', $cardQuery->createNamedParameter($email))
+			));
+		}
+		if ($cloudId !== null) {
+			$propOr->add($propQuery->expr()->andX(
+				$propQuery->expr()->eq('name', $cardQuery->createNamedParameter('CLOUD')),
+				$propQuery->expr()->eq('value', $cardQuery->createNamedParameter($cloudId))
+			));
+		}
+		$addressbooksQuery->selectDistinct('id')
+			->from('addressbooks')
+			->where($addressbooksQuery->expr()->eq('principaluri', $cardQuery->createNamedParameter("principals/users/" . $user->getUID())));
+		$propQuery->selectDistinct('cardid')
+			->from('cards_properties')
+			->where($propQuery->expr()->in('addressbookid', $propQuery->createFunction($addressbooksQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY))
+			->andWhere($propOr)
+			->groupBy('cardid');
+		$cardQuery->select('carddata')
+			->from('cards')
+			->where($cardQuery->expr()->in('id', $cardQuery->createFunction($propQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY))
+			->andWhere($cardQuery->expr()->in('addressbookid', $cardQuery->createFunction($addressbooksQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY))
+			->setMaxResults(1);
+		$result = $cardQuery->execute();
+		/** @var string|false $card */
+		$card = $result->fetchColumn(0);
+
+		if ($card === false) {
+			return null;
+		}
+
+		return $card;
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/Db/RecentContact.php b/apps/contactsinteraction/lib/Db/RecentContact.php
new file mode 100644
index 0000000000000000000000000000000000000000..71b58353efb734eee36420b30df2380715c60115
--- /dev/null
+++ b/apps/contactsinteraction/lib/Db/RecentContact.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method void setActorUid(string $uid)
+ * @method string|null getActorUid()
+ * @method void setUid(string $uid)
+ * @method string|null getUid()
+ * @method void setEmail(string $email)
+ * @method string|null getEmail()
+ * @method void setFederatedCloudId(string $federatedCloudId)
+ * @method string|null getFederatedCloudId()
+ * @method void setCard(string $card)
+ * @method string getCard()
+ * @method void setLastContact(int $lastContact)
+ * @method int getLastContact()
+ */
+class RecentContact extends Entity {
+
+	/** @var string */
+	protected $actorUid;
+
+	/** @var string|null */
+	protected $uid;
+
+	/** @var string|null */
+	protected $email;
+
+	/** @var string|null */
+	protected $federatedCloudId;
+
+	/** @var string */
+	protected $card;
+
+	/** @var int */
+	protected $lastContact;
+
+	public function __construct() {
+		$this->addType('actorUid', 'string');
+		$this->addType('uid', 'string');
+		$this->addType('email', 'string');
+		$this->addType('federatedCloudId', 'string');
+		$this->addType('card', 'string');
+		$this->addType('lastContact', 'int');
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/Db/RecentContactMapper.php b/apps/contactsinteraction/lib/Db/RecentContactMapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..7fe98e6e4ecafb14578aee6c909af54fd18623fb
--- /dev/null
+++ b/apps/contactsinteraction/lib/Db/RecentContactMapper.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\Db;
+
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\IDBConnection;
+use OCP\IUser;
+
+class RecentContactMapper extends QBMapper {
+
+	public const TABLE_NAME = 'recent_contact';
+
+	public function __construct(IDBConnection $db) {
+		parent::__construct($db, self::TABLE_NAME);
+	}
+
+	/**
+	 * @return RecentContact[]
+	 */
+	public function findAll(string $uid): array {
+		$qb = $this->db->getQueryBuilder();
+
+		$select = $qb
+			->select('*')
+			->from($this->getTableName())
+			->where($qb->expr()->eq('actor_uid', $qb->createNamedParameter($uid)));
+
+		return $this->findEntities($select);
+	}
+
+	/**
+	 * @param string $uid
+	 * @param int $id
+	 *
+	 * @return RecentContact
+	 * @throws DoesNotExistException
+	 */
+	public function find(string $uid, int $id): RecentContact {
+		$qb = $this->db->getQueryBuilder();
+
+		$select = $qb
+			->select('*')
+			->from($this->getTableName())
+			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, $qb::PARAM_INT)))
+			->andWhere($qb->expr()->eq('actor_uid', $qb->createNamedParameter($uid)));
+
+		return $this->findEntity($select);
+	}
+
+	/**
+	 * @param IUser $user
+	 * @param string|null $uid
+	 * @param string|null $email
+	 * @param string|null $cloudId
+	 *
+	 * @return RecentContact[]
+	 */
+	public function findMatch(IUser $user,
+							  ?string $uid,
+							  ?string $email,
+							  ?string $cloudId): array {
+		$qb = $this->db->getQueryBuilder();
+
+		$or = $qb->expr()->orX();
+		if ($uid !== null) {
+			$or->add($qb->expr()->eq('uid', $qb->createNamedParameter($uid)));
+		}
+		if ($email !== null) {
+			$or->add($qb->expr()->eq('email', $qb->createNamedParameter($email)));
+		}
+		if ($cloudId !== null) {
+			$or->add($qb->expr()->eq('federated_cloud_id', $qb->createNamedParameter($cloudId)));
+		}
+
+		$select = $qb
+			->select('*')
+			->from($this->getTableName())
+			->where($or)
+			->andWhere($qb->expr()->eq('actor_uid', $qb->createNamedParameter($user->getUID())));
+
+		return $this->findEntities($select);
+	}
+
+	public function cleanUp(int $olderThan): void {
+		$qb = $this->db->getQueryBuilder();
+
+		$delete = $qb
+			->delete($this->getTableName())
+			->where($qb->expr()->lt('last_contact', $qb->createNamedParameter($olderThan)));
+
+		$delete->execute();
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php b/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php
new file mode 100644
index 0000000000000000000000000000000000000000..8e801f2e76ebfa44c786ceb88f57ce1142705c33
--- /dev/null
+++ b/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php
@@ -0,0 +1,171 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\Listeners;
+
+use OCA\ContactsInteraction\Db\CardSearchDao;
+use OCA\ContactsInteraction\Db\RecentContact;
+use OCA\ContactsInteraction\Db\RecentContactMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Contacts\Events\ContactInteractedWithEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IL10N;
+use OCP\ILogger;
+use OCP\IUserManager;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Reader;
+use Sabre\VObject\UUIDUtil;
+use Throwable;
+
+class ContactInteractionListener implements IEventListener {
+
+	/** @var RecentContactMapper */
+	private $mapper;
+
+	/** @var CardSearchDao */
+	private $cardSearchDao;
+
+	/** @var IUserManager */
+	private $userManager;
+
+	/** @var ITimeFactory */
+	private $timeFactory;
+
+	/** @var IL10N */
+	private $l10n;
+
+	/** @var ILogger */
+	private $logger;
+
+	public function __construct(RecentContactMapper $mapper,
+								CardSearchDao $cardSearchDao,
+								IUserManager $userManager,
+								ITimeFactory $timeFactory,
+								IL10N $l10nFactory,
+								ILogger $logger) {
+		$this->mapper = $mapper;
+		$this->cardSearchDao = $cardSearchDao;
+		$this->userManager = $userManager;
+		$this->timeFactory = $timeFactory;
+		$this->l10n = $l10nFactory;
+		$this->logger = $logger;
+	}
+
+	public function handle(Event $event): void {
+		if (!($event instanceof ContactInteractedWithEvent)) {
+			return;
+		}
+
+		if ($event->getUid() === null && $event->getEmail() === null && $event->getFederatedCloudId() === null) {
+			$this->logger->warning("Contact interaction event has no user identifier set");
+			return;
+		}
+
+		$existing = $this->mapper->findMatch(
+			$event->getActor(),
+			$event->getUid(),
+			$event->getEmail(),
+			$event->getFederatedCloudId()
+		);
+		if (!empty($existing)) {
+			$now = $this->timeFactory->getTime();
+			foreach ($existing as $c) {
+				$c->setLastContact($now);
+				$this->mapper->update($c);
+			}
+
+			return;
+		}
+
+		$contact = new RecentContact();
+		$contact->setActorUid($event->getActor()->getUID());
+		if ($event->getUid() !== null) {
+			$contact->setUid($event->getUid());
+		}
+		if ($event->getEmail() !== null) {
+			$contact->setEmail($event->getEmail());
+		}
+		if ($event->getFederatedCloudId() !== null) {
+			$contact->setFederatedCloudId($event->getFederatedCloudId());
+		}
+		$contact->setLastContact($this->timeFactory->getTime());
+
+		$copy = $this->cardSearchDao->findExisting(
+			$event->getActor(),
+			$event->getUid(),
+			$event->getEmail(),
+			$event->getFederatedCloudId()
+		);
+		if ($copy !== null) {
+			try {
+				$parsed = Reader::read($copy, Reader::OPTION_FORGIVING);
+				$parsed->CATEGORIES = $this->l10n->t('Recently contacted');
+				$contact->setCard($parsed->serialize());
+			} catch (Throwable $e) {
+				$this->logger->logException($e, [
+					'message' => 'Could not parse card to add recent category: ' . $e->getMessage(),
+					'level' => ILogger::WARN,
+				]);
+				$contact->setCard($copy);
+			}
+		} else {
+			$contact->setCard($this->generateCard($contact));
+		}
+		$this->mapper->insert($contact);
+	}
+
+	private function getDisplayName(?string $uid): ?string {
+		if ($uid === null) {
+			return null;
+		}
+		if (($user = $this->userManager->get($uid)) === null) {
+			return null;
+		}
+
+		return $user->getDisplayName();
+	}
+
+	private function generateCard(RecentContact $contact): string {
+		$props = [
+			'URI' => UUIDUtil::getUUID(),
+			'FN' => $this->getDisplayName($contact->getUid()) ?? $contact->getEmail() ?? $contact->getFederatedCloudId(),
+			'CATEGORIES' => $this->l10n->t('Recently contacted'),
+		];
+
+		if ($contact->getUid() !== null) {
+			$props['X-NEXTCLOUD-UID'] = $contact->getUid();
+		}
+		if ($contact->getEmail() !== null) {
+			$props['EMAIL'] = $contact->getEmail();
+		}
+		if ($contact->getFederatedCloudId() !== null) {
+			$props['CLOUD'] = $contact->getFederatedCloudId();
+		}
+
+		return (new VCard($props))->serialize();
+	}
+
+}
diff --git a/apps/contactsinteraction/lib/Migration/Version010000Date20200304152605.php b/apps/contactsinteraction/lib/Migration/Version010000Date20200304152605.php
new file mode 100644
index 0000000000000000000000000000000000000000..fea763106ae4935c333c3b4f71a1651ba95d43a7
--- /dev/null
+++ b/apps/contactsinteraction/lib/Migration/Version010000Date20200304152605.php
@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\ContactsInteraction\Migration;
+
+use Closure;
+use OCA\ContactsInteraction\Db\RecentContactMapper;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version010000Date20200304152605 extends SimpleMigrationStep {
+
+	/**
+	 * @param IOutput $output
+	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+	 * @param array $options
+	 *
+	 * @return ISchemaWrapper
+	 */
+	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper {
+		/** @var ISchemaWrapper $schema */
+		$schema = $schemaClosure();
+
+		$table = $schema->createTable(RecentContactMapper::TABLE_NAME);
+		$table->addColumn('id', 'integer', [
+			'autoincrement' => true,
+			'notnull' => true,
+			'length' => 4,
+		]);
+		$table->addColumn('actor_uid', 'string', [
+			'notnull' => true,
+			'length' => 64,
+		]);
+		$table->addColumn('uid', 'string', [
+			'notnull' => false,
+			'length' => 64,
+		]);
+		$table->addColumn('email', 'string', [
+			'notnull' => false,
+			'length' => 255,
+		]);
+		$table->addColumn('federated_cloud_id', 'string', [
+			'notnull' => false,
+			'length' => 255,
+		]);
+		$table->addColumn('card', 'blob', [
+			'notnull' => true,
+		]);
+		$table->addColumn('last_contact', 'integer', [
+			'notnull' => true,
+			'length' => 4,
+		]);
+		$table->setPrimaryKey(['id']);
+		// To find all recent entries
+		$table->addIndex(['actor_uid'], RecentContactMapper::TABLE_NAME . '_actor_uid');
+		// To find a specific entry
+		$table->addIndex(['id', 'actor_uid'], RecentContactMapper::TABLE_NAME . '_id_uid');
+		// To find all recent entries with a given UID
+		$table->addIndex(['uid'], RecentContactMapper::TABLE_NAME . '_uid');
+		// To find all recent entries with a given email address
+		$table->addIndex(['email'], RecentContactMapper::TABLE_NAME . '_email');
+		// To find all recent entries with a give federated cloud id
+		$table->addIndex(['federated_cloud_id'], RecentContactMapper::TABLE_NAME . '_fed_id');
+		// For the cleanup
+		$table->addIndex(['last_contact'], RecentContactMapper::TABLE_NAME . '_last_contact');
+
+		return $schema;
+	}
+
+}
diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php
index 8b9e22db5ac7cbf50934133d84cd2ef45be2365d..625eb2c0b80d9070d1c8e108b3310fa35739b5d7 100644
--- a/apps/dav/lib/CardDAV/UserAddressBooks.php
+++ b/apps/dav/lib/CardDAV/UserAddressBooks.php
@@ -28,11 +28,14 @@ declare(strict_types=1);
 namespace OCA\DAV\CardDAV;
 
 use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
 use OCA\DAV\CardDAV\Integration\ExternalAddressBook;
 use OCP\IConfig;
 use OCP\IL10N;
 use Sabre\CardDAV\Backend;
 use Sabre\DAV\Exception\MethodNotAllowed;
+use Sabre\CardDAV\IAddressBook;
+use function array_map;
 use Sabre\DAV\MkCol;
 
 class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
@@ -56,7 +59,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
 	/**
 	 * Returns a list of address books
 	 *
-	 * @return array
+	 * @return IAddressBook[]
 	 */
 	function getChildren() {
 		if ($this->l10n === null) {
@@ -67,6 +70,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
 		}
 
 		$addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
+		/** @var IAddressBook[] $objects */
 		$objects = array_map(function(array $addressBook) {
 			if ($addressBook['principaluri'] === 'principals/system/system') {
 				return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config);
@@ -74,11 +78,12 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
 
 			return new AddressBook($this->carddavBackend, $addressBook, $this->l10n);
 		}, $addressBooks);
-		foreach ($this->pluginManager->getAddressBookPlugins() as $plugin) {
-			$plugin->fetchAllForAddressBookHome($this->principalUri);
-		}
-		return $objects;
+		/** @var IAddressBook[][] $objectsFromPlugins */
+		$objectsFromPlugins = array_map(function(IAddressBookProvider $plugin): array {
+			return $plugin->fetchAllForAddressBookHome($this->principalUri);
+		}, $this->pluginManager->getAddressBookPlugins());
 
+		return array_merge($objects, ...$objectsFromPlugins);
 	}
 
 	public function createExtendedCollection($name, MkCol $mkCol) {
diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php
index da355a48ce60f721d928953682f1391a5c2b934c..046f626d04f8f76ad3cd89855174790b6c3e9fb8 100644
--- a/apps/files_sharing/composer/composer/autoload_classmap.php
+++ b/apps/files_sharing/composer/composer/autoload_classmap.php
@@ -50,6 +50,7 @@ return array(
     'OCA\\Files_Sharing\\ISharedStorage' => $baseDir . '/../lib/ISharedStorage.php',
     'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
     'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
+    'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
     'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',
     'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => $baseDir . '/../lib/Listener/UserShareAcceptanceListener.php',
     'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => $baseDir . '/../lib/Middleware/OCSShareAPIMiddleware.php',
diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php
index 08e7a502f7928044d06cc9eb4bacf1b9e7ec3f8f..c1e887445823ad5ae7ac78b5cdf62198c76d7cd3 100644
--- a/apps/files_sharing/composer/composer/autoload_static.php
+++ b/apps/files_sharing/composer/composer/autoload_static.php
@@ -65,6 +65,7 @@ class ComposerStaticInitFiles_Sharing
         'OCA\\Files_Sharing\\ISharedStorage' => __DIR__ . '/..' . '/../lib/ISharedStorage.php',
         'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
         'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
+        'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
         'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',
         'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => __DIR__ . '/..' . '/../lib/Listener/UserShareAcceptanceListener.php',
         'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/OCSShareAPIMiddleware.php',
diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php
index 40c024eea45884e97953b00d30f7eedf36d5d296..51f7ab75a119f18b4d1fb481a54d299a05956c45 100644
--- a/apps/files_sharing/lib/AppInfo/Application.php
+++ b/apps/files_sharing/lib/AppInfo/Application.php
@@ -36,9 +36,9 @@ use OCA\Files_Sharing\Capabilities;
 use OCA\Files_Sharing\Controller\ExternalSharesController;
 use OCA\Files_Sharing\Controller\ShareController;
 use OCA\Files_Sharing\External\Manager;
-use OCA\Files_Sharing\Listener\GlobalShareAcceptanceListener;
 use OCA\Files_Sharing\Listener\LoadAdditionalListener;
 use OCA\Files_Sharing\Listener\LoadSidebarListener;
+use OCA\Files_Sharing\Listener\ShareInteractionListener;
 use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
 use OCA\Files_Sharing\Listener\UserShareAcceptanceListener;
 use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware;
@@ -213,6 +213,7 @@ class Application extends App {
 		// sidebar and files scripts
 		$dispatcher->addServiceListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
 		$dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class);
+		$dispatcher->addServiceListener(ShareCreatedEvent::class, ShareInteractionListener::class);
 		$dispatcher->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function() {
 			\OCP\Util::addScript('files_sharing', 'dist/collaboration');
 		});
diff --git a/apps/files_sharing/lib/Listener/ShareInteractionListener.php b/apps/files_sharing/lib/Listener/ShareInteractionListener.php
new file mode 100644
index 0000000000000000000000000000000000000000..de4753d3da893488d8ec50fa56746de303d918b0
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/ShareInteractionListener.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Files_Sharing\Listener;
+
+use OCP\Contacts\Events\ContactInteractedWithEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
+use OCP\ILogger;
+use OCP\IUserManager;
+use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\IShare;
+use function in_array;
+
+class ShareInteractionListener implements IEventListener {
+
+	private const SUPPORTED_SHARE_TYPES = [
+		IShare::TYPE_USER,
+		IShare::TYPE_EMAIL,
+		IShare::TYPE_REMOTE,
+	];
+
+	/** @var IEventDispatcher */
+	private $dispatcher;
+
+	/** @var IUserManager */
+	private $userManager;
+
+	/** @var ILogger */
+	private $logger;
+
+	public function __construct(IEventDispatcher $dispatcher,
+								IUserManager $userManager,
+								ILogger $logger) {
+		$this->dispatcher = $dispatcher;
+		$this->userManager = $userManager;
+		$this->logger = $logger;
+	}
+
+	public function handle(Event $event): void {
+		if (!($event instanceof ShareCreatedEvent)) {
+			// Unrelated
+			return;
+		}
+
+		$share = $event->getShare();
+		if (!in_array($share->getShareType(), self::SUPPORTED_SHARE_TYPES, true)) {
+			$this->logger->debug('Share type does not allow to emit interaction event');
+			return;
+		}
+		$actor = $this->userManager->get($share->getSharedBy());
+		if ($actor === null) {
+			$this->logger->warning('Share was not created by a user, can\'t emit interaction event');
+			return;
+		}
+		$interactionEvent = new ContactInteractedWithEvent($actor);
+		switch ($share->getShareType()) {
+			case IShare::TYPE_USER:
+				$interactionEvent->setUid($share->getSharedWith());
+				break;
+			case IShare::TYPE_EMAIL:
+				$interactionEvent->setEmail($share->getSharedWith());
+				break;
+			case IShare::TYPE_REMOTE:
+				$interactionEvent->setFederatedCloudId($share->getSharedWith());
+				break;
+		}
+
+		$this->dispatcher->dispatchTyped($interactionEvent);
+	}
+
+}
diff --git a/core/Command/App/ListApps.php b/core/Command/App/ListApps.php
index 5ee575f60d9996b4679442e7a067cef6c341c952..6c3d4bb743db9e75159457bd5b06d383e9867e83 100644
--- a/core/Command/App/ListApps.php
+++ b/core/Command/App/ListApps.php
@@ -66,7 +66,7 @@ class ListApps extends Base {
 		} else {
 			$shippedFilter = null;
 		}
-		
+
 		$apps = \OC_App::getAllApps();
 		$enabledApps = $disabledApps = [];
 		$versions = \OC_App::getAppVersions();
diff --git a/core/shipped.json b/core/shipped.json
index da408d5a347ddf183baebafe8ca7533e322805bb..b700b3fab37562db245aea909f9472f4be5b0a87 100644
--- a/core/shipped.json
+++ b/core/shipped.json
@@ -5,6 +5,7 @@
     "admin_audit",
     "cloud_federation_api",
     "comments",
+    "contactsinteraction",
     "dav",
     "encryption",
     "federatedfilesharing",
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 94addec59c6b4edcc0aac526c1736fd8d51373c2..388c7906eb81c7ae2311d76ec6e7aa0b3b93d4d5 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -147,6 +147,7 @@ return array(
     'OCP\\Contacts\\ContactsMenu\\IEntry' => $baseDir . '/lib/public/Contacts/ContactsMenu/IEntry.php',
     'OCP\\Contacts\\ContactsMenu\\ILinkAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/ILinkAction.php',
     'OCP\\Contacts\\ContactsMenu\\IProvider' => $baseDir . '/lib/public/Contacts/ContactsMenu/IProvider.php',
+    'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => $baseDir . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php',
     'OCP\\Contacts\\IManager' => $baseDir . '/lib/public/Contacts/IManager.php',
     'OCP\\DB\\ISchemaWrapper' => $baseDir . '/lib/public/DB/ISchemaWrapper.php',
     'OCP\\DB\\QueryBuilder\\ICompositeExpression' => $baseDir . '/lib/public/DB/QueryBuilder/ICompositeExpression.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index d8f68ccd5ea388b5ba6385ba86e6e13dbc39cc82..cfc6d9842dfa765524222b1cc4f785a648ffd098 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -176,6 +176,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OCP\\Contacts\\ContactsMenu\\IEntry' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IEntry.php',
         'OCP\\Contacts\\ContactsMenu\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/ILinkAction.php',
         'OCP\\Contacts\\ContactsMenu\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IProvider.php',
+        'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => __DIR__ . '/../../..' . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php',
         'OCP\\Contacts\\IManager' => __DIR__ . '/../../..' . '/lib/public/Contacts/IManager.php',
         'OCP\\DB\\ISchemaWrapper' => __DIR__ . '/../../..' . '/lib/public/DB/ISchemaWrapper.php',
         'OCP\\DB\\QueryBuilder\\ICompositeExpression' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ICompositeExpression.php',
diff --git a/lib/public/Contacts/Events/ContactInteractedWithEvent.php b/lib/public/Contacts/Events/ContactInteractedWithEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..a21e17124c87fd6c880759c76d3f9f10b1a1d7d3
--- /dev/null
+++ b/lib/public/Contacts/Events/ContactInteractedWithEvent.php
@@ -0,0 +1,136 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\Contacts\Events;
+
+use OCP\EventDispatcher\Event;
+use OCP\IUser;
+
+/**
+ * An event that allows apps to notify other components about an interaction
+ * between two users. This can be used to build better recommendations and
+ * suggestions in user interfaces.
+ *
+ * Emitters should add at least one identifier (uid, email, federated cloud ID)
+ * of the recipient of the interaction.
+ *
+ * @since 19.0.0
+ */
+class ContactInteractedWithEvent extends Event {
+
+	/** @var IUser */
+	private $actor;
+
+	/** @var string|null */
+	private $uid;
+
+	/** @var string|null */
+	private $email;
+
+	/** @var string|null */
+	private $federatedCloudId;
+
+	/**
+	 * @param IUser $actor the user who started the interaction
+	 *
+	 * @since 19.0.0
+	 */
+	public function __construct(IUser $actor) {
+		parent::__construct();
+		$this->actor = $actor;
+	}
+
+	/**
+	 * @return IUser
+	 * @since 19.0.0
+	 */
+	public function getActor(): IUser {
+		return $this->actor;
+	}
+
+	/**
+	 * @return string|null
+	 * @since 19.0.0
+	 */
+	public function getUid(): ?string {
+		return $this->uid;
+	}
+
+	/**
+	 * Set the uid of the person interacted with, if known
+	 *
+	 * @param string $uid
+	 *
+	 * @return self
+	 * @since 19.0.0
+	 */
+	public function setUid(string $uid): self {
+		$this->uid = $uid;
+		return $this;
+	}
+
+	/**
+	 * @return string|null
+	 * @since 19.0.0
+	 */
+	public function getEmail(): ?string {
+		return $this->email;
+	}
+
+	/**
+	 * Set the email of the person interacted with, if known
+	 *
+	 * @param string $email
+	 *
+	 * @return self
+	 * @since 19.0.0
+	 */
+	public function setEmail(string $email): self {
+		$this->email = $email;
+		return $this;
+	}
+
+	/**
+	 * @return string|null
+	 * @since 19.0.0
+	 */
+	public function getFederatedCloudId(): ?string {
+		return $this->federatedCloudId;
+	}
+
+	/**
+	 * Set the federated cloud of the person interacted with, if known
+	 *
+	 * @param string $federatedCloudId
+	 *
+	 * @return self
+	 * @since 19.0.0
+	 */
+	public function setFederatedCloudId(string $federatedCloudId): self {
+		$this->federatedCloudId = $federatedCloudId;
+		return $this;
+	}
+
+}