diff --git a/apps/dav/appinfo/app.php b/apps/dav/appinfo/app.php
index 59ebd3eda0d..4b4c0824efe 100644
--- a/apps/dav/appinfo/app.php
+++ b/apps/dav/appinfo/app.php
@@ -29,6 +29,7 @@
  */
 
 use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
 use OCA\DAV\CardDAV\CardDavBackend;
 use Symfony\Component\EventDispatcher\GenericEvent;
 
@@ -61,6 +62,13 @@ $eventDispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createSubscription
 		$jobList = $app->getContainer()->getServer()->getJobList();
 		$subscriptionData = $event->getArgument('subscriptionData');
 
+		/**
+		 * Initial subscription refetch
+		 * @var RefreshWebcalService $refreshWebcalService
+		 */
+		$refreshWebcalService = $app->getContainer()->query(RefreshWebcalService::class);
+		$refreshWebcalService->refreshSubscription($subscriptionData['principaluri'], $subscriptionData['uri']);
+
 		$jobList->add(\OCA\DAV\BackgroundJob\RefreshWebcalJob::class, [
 			'principaluri' => $subscriptionData['principaluri'],
 			'uri' => $subscriptionData['uri']
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index e6948d1151e..7db6f3c1fef 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -79,6 +79,7 @@ return array(
     'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php',
     'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
     'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => $baseDir . '/../lib/CalDAV/WebcalCaching/Plugin.php',
+    'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => $baseDir . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php',
     'OCA\\DAV\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
     'OCA\\DAV\\CardDAV\\AddressBook' => $baseDir . '/../lib/CardDAV/AddressBook.php',
     'OCA\\DAV\\CardDAV\\AddressBookImpl' => $baseDir . '/../lib/CardDAV/AddressBookImpl.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 4a26ceb040f..49f0c4ab82e 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -94,6 +94,7 @@ class ComposerStaticInitDAV
         'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php',
         'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
         'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Plugin.php',
+        'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php',
         'OCA\\DAV\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
         'OCA\\DAV\\CardDAV\\AddressBook' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBook.php',
         'OCA\\DAV\\CardDAV\\AddressBookImpl' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBookImpl.php',
diff --git a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
index 89f941c14d5..710dd09da86 100644
--- a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
+++ b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
@@ -7,6 +7,7 @@ declare(strict_types=1);
  *
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Thomas Citharel <nextcloud@tcit.fr>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -27,36 +28,20 @@ declare(strict_types=1);
 
 namespace OCA\DAV\BackgroundJob;
 
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Middleware;
+use DateInterval;
 use OC\BackgroundJob\Job;
-use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
 use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\Http\Client\IClientService;
-use OCP\IConfig;
 use OCP\ILogger;
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
-use Sabre\DAV\Exception\BadRequest;
-use Sabre\DAV\PropPatch;
-use Sabre\DAV\Xml\Property\Href;
-use Sabre\VObject\Component;
 use Sabre\VObject\DateTimeParser;
 use Sabre\VObject\InvalidDataException;
-use Sabre\VObject\ParseException;
-use Sabre\VObject\Reader;
-use Sabre\VObject\Splitter\ICalendar;
 
 class RefreshWebcalJob extends Job {
 
-	/** @var CalDavBackend */
-	private $calDavBackend;
-
-	/** @var IClientService */
-	private $clientService;
-
-	/** @var IConfig */
-	private $config;
+	/**
+	 * @var RefreshWebcalService
+	 */
+	private $refreshWebcalService;
 
 	/** @var ILogger */
 	private $logger;
@@ -64,27 +49,15 @@ class RefreshWebcalJob extends Job {
 	/** @var ITimeFactory */
 	private $timeFactory;
 
-	/** @var array */
-	private $subscription;
-
-	private const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
-	private const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
-	private const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
-	private const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
-
 	/**
 	 * RefreshWebcalJob constructor.
 	 *
-	 * @param CalDavBackend $calDavBackend
-	 * @param IClientService $clientService
-	 * @param IConfig $config
+	 * @param RefreshWebcalService $refreshWebcalService
 	 * @param ILogger $logger
 	 * @param ITimeFactory $timeFactory
 	 */
-	public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, ILogger $logger, ITimeFactory $timeFactory) {
-		$this->calDavBackend = $calDavBackend;
-		$this->clientService = $clientService;
-		$this->config = $config;
+	public function __construct(RefreshWebcalService $refreshWebcalService, ILogger $logger, ITimeFactory $timeFactory) {
+		$this->refreshWebcalService = $refreshWebcalService;
 		$this->logger = $logger;
 		$this->timeFactory = $timeFactory;
 	}
@@ -95,7 +68,7 @@ class RefreshWebcalJob extends Job {
 	 * @inheritdoc
 	 */
 	public function execute($jobList, ILogger $logger = null) {
-		$subscription = $this->getSubscription($this->argument['principaluri'], $this->argument['uri']);
+		$subscription = $this->refreshWebcalService->getSubscription($this->argument['principaluri'], $this->argument['uri']);
 		if (!$subscription) {
 			return;
 		}
@@ -104,10 +77,10 @@ class RefreshWebcalJob extends Job {
 
 		// if no refresh rate was configured, just refresh once a week
 		$subscriptionId = $subscription['id'];
-		$refreshrate = $subscription[self::REFRESH_RATE] ?? 'P1W';
+		$refreshrate = $subscription[RefreshWebcalService::REFRESH_RATE] ?? 'P1W';
 
 		try {
-			/** @var \DateInterval $dateInterval */
+			/** @var DateInterval $dateInterval */
 			$dateInterval = DateTimeParser::parseDuration($refreshrate);
 		} catch(InvalidDataException $ex) {
 			$this->logger->logException($ex);
@@ -127,243 +100,16 @@ class RefreshWebcalJob extends Job {
 	 * @param array $argument
 	 */
 	protected function run($argument) {
-		$subscription = $this->getSubscription($argument['principaluri'], $argument['uri']);
-		$mutations = [];
-		if (!$subscription) {
-			return;
-		}
-
-		$webcalData = $this->queryWebcalFeed($subscription, $mutations);
-		if (!$webcalData) {
-			return;
-		}
-
-		$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
-		$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
-		$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
-
-		try {
-			$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
-
-			// we wait with deleting all outdated events till we parsed the new ones
-			// in case the new calendar is broken and `new ICalendar` throws a ParseException
-			// the user will still see the old data
-			$this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
-
-			while ($vObject = $splitter->getNext()) {
-				/** @var Component $vObject */
-				$uid = null;
-				$compName = null;
-
-				foreach ($vObject->getComponents() as $component) {
-					if ($component->name === 'VTIMEZONE') {
-						continue;
-					}
-
-					$uid = $component->{'UID'}->getValue();
-					$compName = $component->name;
-
-					if ($stripAlarms) {
-						unset($component->{'VALARM'});
-					}
-					if ($stripAttachments) {
-						unset($component->{'ATTACH'});
-					}
-				}
-
-				if ($stripTodos && $compName === 'VTODO') {
-					continue;
-				}
-
-				$uri = $uid . '.ics';
-				$calendarData = $vObject->serialize();
-				try {
-					$this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
-				} catch(BadRequest $ex) {
-					$this->logger->logException($ex);
-				}
-			}
-
-			$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
-			if ($newRefreshRate) {
-				$mutations[self::REFRESH_RATE] = $newRefreshRate;
-			}
-
-			$this->updateSubscription($subscription, $mutations);
-		} catch(ParseException $ex) {
-			$subscriptionId = $subscription['id'];
-
-			$this->logger->logException($ex);
-			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a parsing error");
-		}
-	}
-
-	/**
-	 * gets webcal feed from remote server
-	 *
-	 * @param array $subscription
-	 * @param array &$mutations
-	 * @return null|string
-	 */
-	private function queryWebcalFeed(array $subscription, array &$mutations) {
-		$client = $this->clientService->newClient();
-
-		$didBreak301Chain = false;
-		$latestLocation = null;
-
-		$handlerStack = HandlerStack::create();
-		$handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
-			return $request
-				->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
-				->withHeader('User-Agent', 'Nextcloud Webcal Crawler');
-		}));
-		$handlerStack->push(Middleware::mapResponse(function(ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
-			if (!$didBreak301Chain) {
-				if ($response->getStatusCode() !== 301) {
-					$didBreak301Chain = true;
-				} else {
-					$latestLocation = $response->getHeader('Location');
-				}
-			}
-			return $response;
-		}));
-
-		$allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
-		$subscriptionId = $subscription['id'];
-		$url = $this->cleanURL($subscription['source']);
-		if ($url === null) {
-			return null;
-		}
-
-		if ($allowLocalAccess !== 'yes') {
-			$host = strtolower(parse_url($url, PHP_URL_HOST));
-			// remove brackets from IPv6 addresses
-			if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
-				$host = substr($host, 1, -1);
-			}
-
-			// Disallow localhost and local network
-			if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
-				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
-				return null;
-			}
-
-			// Disallow hostname only
-			if (substr_count($host, '.') === 0) {
-				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
-				return null;
-			}
-
-			if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
-				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
-				return null;
-			}
-
-			// Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
-			if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
-				$delimiter = strrpos($host, ':'); // Get last colon
-				$ipv4Address = substr($host, $delimiter + 1);
-
-				if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
-					$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
-					return null;
-				}
-			}
-		}
-
-		try {
-			$params = [
-				'allow_redirects' => [
-					'redirects' => 10
-				],
-				'handler' => $handlerStack,
-			];
-
-			$user = parse_url($subscription['source'], PHP_URL_USER);
-			$pass = parse_url($subscription['source'], PHP_URL_PASS);
-			if ($user !== null && $pass !== null) {
-				$params['auth'] = [$user, $pass];
-			}
-
-			$response = $client->get($url, $params);
-			$body = $response->getBody();
-
-			if ($latestLocation) {
-				$mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
-			}
-
-			$contentType = $response->getHeader('Content-Type');
-			$contentType = explode(';', $contentType, 2)[0];
-			switch($contentType) {
-				case 'application/calendar+json':
-					try {
-						$jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
-					} catch(\Exception $ex) {
-						// In case of a parsing error return null
-						$this->logger->debug("Subscription $subscriptionId could not be parsed");
-						return null;
-					}
-					return $jCalendar->serialize();
-
-				case 'application/calendar+xml':
-					try {
-						$xCalendar = Reader::readXML($body);
-					} catch(\Exception $ex) {
-						// In case of a parsing error return null
-						$this->logger->debug("Subscription $subscriptionId could not be parsed");
-						return null;
-					}
-					return $xCalendar->serialize();
-
-				case 'text/calendar':
-				default:
-					try {
-						$vCalendar = Reader::read($body);
-					} catch(\Exception $ex) {
-						// In case of a parsing error return null
-						$this->logger->debug("Subscription $subscriptionId could not be parsed");
-						return null;
-					}
-					return $vCalendar->serialize();
-			}
-		} catch(\Exception $ex) {
-			$this->logger->logException($ex);
-			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error");
-
-			return null;
-		}
-	}
-
-	/**
-	 * loads subscription from backend
-	 *
-	 * @param string $principalUri
-	 * @param string $uri
-	 * @return array|null
-	 */
-	private function getSubscription(string $principalUri, string $uri) {
-		$subscriptions = array_values(array_filter(
-			$this->calDavBackend->getSubscriptionsForUser($principalUri),
-			function($sub) use ($uri) {
-				return $sub['uri'] === $uri;
-			}
-		));
-
-		if (\count($subscriptions) === 0) {
-			return null;
-		}
-
-		$this->subscription = $subscriptions[0];
-		return $this->subscription;
+		$this->refreshWebcalService->refreshSubscription($argument['principaluri'], $argument['uri']);
 	}
 
 	/**
 	 * get total number of seconds from DateInterval object
 	 *
-	 * @param \DateInterval $interval
+	 * @param DateInterval $interval
 	 * @return int
 	 */
-	private function getIntervalFromDateInterval(\DateInterval $interval):int {
+	private function getIntervalFromDateInterval(DateInterval $interval):int {
 		return $interval->s
 			+ ($interval->i * 60)
 			+ ($interval->h * 60 * 60)
@@ -372,103 +118,6 @@ class RefreshWebcalJob extends Job {
 			+ ($interval->y * 60 * 60 * 24 * 365);
 	}
 
-	/**
-	 * check if:
-	 *  - current subscription stores a refreshrate
-	 *  - the webcal feed suggests a refreshrate
-	 *  - return suggested refreshrate if user didn't set a custom one
-	 *
-	 * @param array $subscription
-	 * @param string $webcalData
-	 * @return string|null
-	 */
-	private function checkWebcalDataForRefreshRate($subscription, $webcalData) {
-		// if there is no refreshrate stored in the database, check the webcal feed
-		// whether it suggests any refresh rate and store that in the database
-		if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
-			return null;
-		}
-
-		/** @var Component\VCalendar $vCalendar */
-		$vCalendar = Reader::read($webcalData);
-
-		$newRefreshRate = null;
-		if (isset($vCalendar->{'X-PUBLISHED-TTL'})) {
-			$newRefreshRate = $vCalendar->{'X-PUBLISHED-TTL'}->getValue();
-		}
-		if (isset($vCalendar->{'REFRESH-INTERVAL'})) {
-			$newRefreshRate = $vCalendar->{'REFRESH-INTERVAL'}->getValue();
-		}
-
-		if (!$newRefreshRate) {
-			return null;
-		}
-
-		// check if new refresh rate is even valid
-		try {
-			DateTimeParser::parseDuration($newRefreshRate);
-		} catch(InvalidDataException $ex) {
-			return null;
-		}
-
-		return $newRefreshRate;
-	}
-
-	/**
-	 * update subscription stored in database
-	 * used to set:
-	 *  - refreshrate
-	 *  - source
-	 *
-	 * @param array $subscription
-	 * @param array $mutations
-	 */
-	private function updateSubscription(array $subscription, array $mutations) {
-		if (empty($mutations)) {
-			return;
-		}
-
-		$propPatch = new PropPatch($mutations);
-		$this->calDavBackend->updateSubscription($subscription['id'], $propPatch);
-		$propPatch->commit();
-	}
-
-	/**
-	 * This method will strip authentication information and replace the
-	 * 'webcal' or 'webcals' protocol scheme
-	 *
-	 * @param string $url
-	 * @return string|null
-	 */
-	private function cleanURL(string $url) {
-		$parsed = parse_url($url);
-		if ($parsed === false) {
-			return null;
-		}
-
-		if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
-			$scheme = 'http';
-		} else {
-			$scheme = 'https';
-		}
-
-		$host = $parsed['host'] ?? '';
-		$port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
-		$path = $parsed['path'] ?? '';
-		$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
-		$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
-
-		$cleanURL = "$scheme://$host$port$path$query$fragment";
-		// parse_url is giving some weird results if no url and no :// is given,
-		// so let's test the url again
-		$parsedClean = parse_url($cleanURL);
-		if ($parsedClean === false || !isset($parsedClean['host'])) {
-			return null;
-		}
-
-		return $cleanURL;
-	}
-
 	/**
 	 * Fixes types of rows
 	 *
@@ -478,9 +127,9 @@ class RefreshWebcalJob extends Job {
 		$forceInt = [
 			'id',
 			'lastmodified',
-			self::STRIP_ALARMS,
-			self::STRIP_ATTACHMENTS,
-			self::STRIP_TODOS,
+			RefreshWebcalService::STRIP_ALARMS,
+			RefreshWebcalService::STRIP_ATTACHMENTS,
+			RefreshWebcalService::STRIP_TODOS,
 		];
 
 		foreach($forceInt as $column) {
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
new file mode 100644
index 00000000000..beab0271d8f
--- /dev/null
+++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
@@ -0,0 +1,416 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr>
+ *
+ * @author Georg Ehrke <oc.list@georgehrke.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Thomas Citharel <nextcloud@tcit.fr>
+ *
+ * @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\DAV\CalDAV\WebcalCaching;
+
+use Exception;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Middleware;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\ILogger;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\VObject\Component;
+use Sabre\VObject\DateTimeParser;
+use Sabre\VObject\InvalidDataException;
+use Sabre\VObject\ParseException;
+use Sabre\VObject\Reader;
+use Sabre\VObject\Splitter\ICalendar;
+use function count;
+
+class RefreshWebcalService {
+
+	/** @var CalDavBackend */
+	private $calDavBackend;
+
+	/** @var IClientService */
+	private $clientService;
+
+	/** @var IConfig */
+	private $config;
+
+	/** @var ILogger */
+	private $logger;
+
+	public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
+	public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
+	public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
+	public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
+
+	/**
+	 * RefreshWebcalJob constructor.
+	 *
+	 * @param CalDavBackend $calDavBackend
+	 * @param IClientService $clientService
+	 * @param IConfig $config
+	 * @param ILogger $logger
+	 */
+	public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, ILogger $logger) {
+		$this->calDavBackend = $calDavBackend;
+		$this->clientService = $clientService;
+		$this->config = $config;
+		$this->logger = $logger;
+	}
+
+	/**
+	 * @param string $principalUri
+	 * @param string $uri
+	 */
+	public function refreshSubscription(string $principalUri, string $uri) {
+		$subscription = $this->getSubscription($principalUri, $uri);
+		$mutations = [];
+		if (!$subscription) {
+			return;
+		}
+
+		$webcalData = $this->queryWebcalFeed($subscription, $mutations);
+		if (!$webcalData) {
+			return;
+		}
+
+		$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
+		$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
+		$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
+
+		try {
+			$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
+
+			// we wait with deleting all outdated events till we parsed the new ones
+			// in case the new calendar is broken and `new ICalendar` throws a ParseException
+			// the user will still see the old data
+			$this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
+
+			while ($vObject = $splitter->getNext()) {
+				/** @var Component $vObject */
+				$uid = null;
+				$compName = null;
+
+				foreach ($vObject->getComponents() as $component) {
+					if ($component->name === 'VTIMEZONE') {
+						continue;
+					}
+
+					$uid = $component->{'UID'}->getValue();
+					$compName = $component->name;
+
+					if ($stripAlarms) {
+						unset($component->{'VALARM'});
+					}
+					if ($stripAttachments) {
+						unset($component->{'ATTACH'});
+					}
+				}
+
+				if ($stripTodos && $compName === 'VTODO') {
+					continue;
+				}
+
+				$uri = $uid . '.ics';
+				$calendarData = $vObject->serialize();
+				try {
+					$this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+				} catch(BadRequest $ex) {
+					$this->logger->logException($ex);
+				}
+			}
+
+			$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
+			if ($newRefreshRate) {
+				$mutations[self::REFRESH_RATE] = $newRefreshRate;
+			}
+
+			$this->updateSubscription($subscription, $mutations);
+		} catch(ParseException $ex) {
+			$subscriptionId = $subscription['id'];
+
+			$this->logger->logException($ex);
+			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a parsing error");
+		}
+	}
+
+	/**
+	 * loads subscription from backend
+	 *
+	 * @param string $principalUri
+	 * @param string $uri
+	 * @return array|null
+	 */
+	public function getSubscription(string $principalUri, string $uri) {
+		$subscriptions = array_values(array_filter(
+			$this->calDavBackend->getSubscriptionsForUser($principalUri),
+			function($sub) use ($uri) {
+				return $sub['uri'] === $uri;
+			}
+		));
+
+		if (count($subscriptions) === 0) {
+			return null;
+		}
+
+		return $subscriptions[0];
+	}
+
+	/**
+	 * gets webcal feed from remote server
+	 *
+	 * @param array $subscription
+	 * @param array &$mutations
+	 * @return null|string
+	 */
+	private function queryWebcalFeed(array $subscription, array &$mutations) {
+		$client = $this->clientService->newClient();
+
+		$didBreak301Chain = false;
+		$latestLocation = null;
+
+		$handlerStack = HandlerStack::create();
+		$handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
+			return $request
+				->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
+				->withHeader('User-Agent', 'Nextcloud Webcal Crawler');
+		}));
+		$handlerStack->push(Middleware::mapResponse(function(ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
+			if (!$didBreak301Chain) {
+				if ($response->getStatusCode() !== 301) {
+					$didBreak301Chain = true;
+				} else {
+					$latestLocation = $response->getHeader('Location');
+				}
+			}
+			return $response;
+		}));
+
+		$allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
+		$subscriptionId = $subscription['id'];
+		$url = $this->cleanURL($subscription['source']);
+		if ($url === null) {
+			return null;
+		}
+
+		if ($allowLocalAccess !== 'yes') {
+			$host = strtolower(parse_url($url, PHP_URL_HOST));
+			// remove brackets from IPv6 addresses
+			if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
+				$host = substr($host, 1, -1);
+			}
+
+			// Disallow localhost and local network
+			if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
+				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
+				return null;
+			}
+
+			// Disallow hostname only
+			if (substr_count($host, '.') === 0) {
+				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
+				return null;
+			}
+
+			if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
+				return null;
+			}
+
+			// Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
+			if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
+				$delimiter = strrpos($host, ':'); // Get last colon
+				$ipv4Address = substr($host, $delimiter + 1);
+
+				if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+					$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
+					return null;
+				}
+			}
+		}
+
+		try {
+			$params = [
+				'allow_redirects' => [
+					'redirects' => 10
+				],
+				'handler' => $handlerStack,
+			];
+
+			$user = parse_url($subscription['source'], PHP_URL_USER);
+			$pass = parse_url($subscription['source'], PHP_URL_PASS);
+			if ($user !== null && $pass !== null) {
+				$params['auth'] = [$user, $pass];
+			}
+
+			$response = $client->get($url, $params);
+			$body = $response->getBody();
+
+			if ($latestLocation) {
+				$mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
+			}
+
+			$contentType = $response->getHeader('Content-Type');
+			$contentType = explode(';', $contentType, 2)[0];
+			switch($contentType) {
+				case 'application/calendar+json':
+					try {
+						$jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
+					} catch(Exception $ex) {
+						// In case of a parsing error return null
+						$this->logger->debug("Subscription $subscriptionId could not be parsed");
+						return null;
+					}
+					return $jCalendar->serialize();
+
+				case 'application/calendar+xml':
+					try {
+						$xCalendar = Reader::readXML($body);
+					} catch(Exception $ex) {
+						// In case of a parsing error return null
+						$this->logger->debug("Subscription $subscriptionId could not be parsed");
+						return null;
+					}
+					return $xCalendar->serialize();
+
+				case 'text/calendar':
+				default:
+					try {
+						$vCalendar = Reader::read($body);
+					} catch(Exception $ex) {
+						// In case of a parsing error return null
+						$this->logger->debug("Subscription $subscriptionId could not be parsed");
+						return null;
+					}
+					return $vCalendar->serialize();
+			}
+		} catch(Exception $ex) {
+			$this->logger->logException($ex);
+			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error");
+
+			return null;
+		}
+	}
+
+	/**
+	 * check if:
+	 *  - current subscription stores a refreshrate
+	 *  - the webcal feed suggests a refreshrate
+	 *  - return suggested refreshrate if user didn't set a custom one
+	 *
+	 * @param array $subscription
+	 * @param string $webcalData
+	 * @return string|null
+	 */
+	private function checkWebcalDataForRefreshRate($subscription, $webcalData) {
+		// if there is no refreshrate stored in the database, check the webcal feed
+		// whether it suggests any refresh rate and store that in the database
+		if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
+			return null;
+		}
+
+		/** @var Component\VCalendar $vCalendar */
+		$vCalendar = Reader::read($webcalData);
+
+		$newRefreshRate = null;
+		if (isset($vCalendar->{'X-PUBLISHED-TTL'})) {
+			$newRefreshRate = $vCalendar->{'X-PUBLISHED-TTL'}->getValue();
+		}
+		if (isset($vCalendar->{'REFRESH-INTERVAL'})) {
+			$newRefreshRate = $vCalendar->{'REFRESH-INTERVAL'}->getValue();
+		}
+
+		if (!$newRefreshRate) {
+			return null;
+		}
+
+		// check if new refresh rate is even valid
+		try {
+			DateTimeParser::parseDuration($newRefreshRate);
+		} catch(InvalidDataException $ex) {
+			return null;
+		}
+
+		return $newRefreshRate;
+	}
+
+	/**
+	 * update subscription stored in database
+	 * used to set:
+	 *  - refreshrate
+	 *  - source
+	 *
+	 * @param array $subscription
+	 * @param array $mutations
+	 */
+	private function updateSubscription(array $subscription, array $mutations) {
+		if (empty($mutations)) {
+			return;
+		}
+
+		$propPatch = new PropPatch($mutations);
+		$this->calDavBackend->updateSubscription($subscription['id'], $propPatch);
+		$propPatch->commit();
+	}
+
+	/**
+	 * This method will strip authentication information and replace the
+	 * 'webcal' or 'webcals' protocol scheme
+	 *
+	 * @param string $url
+	 * @return string|null
+	 */
+	private function cleanURL(string $url) {
+		$parsed = parse_url($url);
+		if ($parsed === false) {
+			return null;
+		}
+
+		if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
+			$scheme = 'http';
+		} else {
+			$scheme = 'https';
+		}
+
+		$host = $parsed['host'] ?? '';
+		$port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
+		$path = $parsed['path'] ?? '';
+		$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
+		$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
+
+		$cleanURL = "$scheme://$host$port$path$query$fragment";
+		// parse_url is giving some weird results if no url and no :// is given,
+		// so let's test the url again
+		$parsedClean = parse_url($cleanURL);
+		if ($parsedClean === false || !isset($parsedClean['host'])) {
+			return null;
+		}
+
+		return $cleanURL;
+	}
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
index 255ad21f042..3f95b24661f 100644
--- a/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
@@ -4,6 +4,7 @@
  *
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Thomas Citharel <nextcloud@tcit.fr>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -24,46 +25,33 @@
 
 namespace OCA\DAV\Tests\unit\BackgroundJob;
 
-use GuzzleHttp\HandlerStack;
 use OCA\DAV\BackgroundJob\RefreshWebcalJob;
-use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
 use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\BackgroundJob\IJobList;
-use OCP\Http\Client\IClient;
-use OCP\Http\Client\IClientService;
-use OCP\Http\Client\IResponse;
-use OCP\IConfig;
 use OCP\ILogger;
-use Sabre\VObject;
+use PHPUnit\Framework\MockObject\MockObject;
 
 use Test\TestCase;
 
 class RefreshWebcalJobTest extends TestCase {
 
-	/** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject */
-	private $caldavBackend;
+	/** @var RefreshWebcalService | MockObject */
+	private $refreshWebcalService;
 
-	/** @var IClientService | \PHPUnit_Framework_MockObject_MockObject */
-	private $clientService;
-
-	/** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */
-	private $config;
-
-	/** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */
+	/** @var ILogger | MockObject */
 	private $logger;
 
-	/** @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject */
+	/** @var ITimeFactory | MockObject */
 	private $timeFactory;
 
-	/** @var IJobList | \PHPUnit_Framework_MockObject_MockObject */
+	/** @var IJobList | MockObject */
 	private $jobList;
 
 	protected function setUp(): void {
 		parent::setUp();
 
-		$this->caldavBackend = $this->createMock(CalDavBackend::class);
-		$this->clientService = $this->createMock(IClientService::class);
-		$this->config = $this->createMock(IConfig::class);
+		$this->refreshWebcalService = $this->createMock(RefreshWebcalService::class);
 		$this->logger = $this->createMock(ILogger::class);
 		$this->timeFactory = $this->createMock(ITimeFactory::class);
 
@@ -71,86 +59,48 @@ class RefreshWebcalJobTest extends TestCase {
 	}
 
 	/**
-	 * @param string $body
-	 * @param string $contentType
-	 * @param string $result
+	 *
+	 * @param int $lastRun
+	 * @param int $time
+	 * @param bool $process
 	 *
 	 * @dataProvider runDataProvider
 	 */
-	public function testRun(string $body, string $contentType, string $result) {
-		$backgroundJob = new RefreshWebcalJob($this->caldavBackend,
-			$this->clientService, $this->config, $this->logger, $this->timeFactory);
+	public function testRun(int $lastRun, int $time, bool $process) {
+		$backgroundJob = new RefreshWebcalJob($this->refreshWebcalService, $this->logger, $this->timeFactory);
 
 		$backgroundJob->setArgument([
 			'principaluri' => 'principals/users/testuser',
 			'uri' => 'sub123',
 		]);
-		$backgroundJob->setLastRun(0);
+		$backgroundJob->setLastRun($lastRun);
+
+		$this->refreshWebcalService->expects($this->once())
+			->method('getSubscription')
+			->with('principals/users/testuser', 'sub123')
+			->willReturn([
+				'id' => '99',
+				'uri' => 'sub456',
+				'{http://apple.com/ns/ical/}refreshrate' => 'P1D',
+				'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
+				'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
+				'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
+				'source' => 'webcal://foo.bar/bla'
+			]);
 
 		$this->timeFactory->expects($this->once())
 			->method('getTime')
-			->with()
-			->will($this->returnValue(1000000000));
+			->willReturn($time);
 
-		$this->caldavBackend->expects($this->exactly(2))
-			->method('getSubscriptionsForUser')
-			->with('principals/users/testuser')
-			->will($this->returnValue([
-				[
-					'id' => '99',
-					'uri' => 'sub456',
-					'{http://apple.com/ns/ical/}refreshrate' => 'P1D',
-					'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
-					'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
-					'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
-					'source' => 'webcal://foo.bar/bla'
-				],
-				[
-					'id' => '42',
-					'uri' => 'sub123',
-					'{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
-					'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
-					'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
-					'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
-					'source' => 'webcal://foo.bar/bla2'
-				],
-			]));
-
-		$client = $this->createMock(IClient::class);
-		$response = $this->createMock(IResponse::class);
-		$this->clientService->expects($this->once())
-			->method('newClient')
-			->with()
-			->will($this->returnValue($client));
-
-		$this->config->expects($this->once())
-			->method('getAppValue')
-			->with('dav', 'webcalAllowLocalAccess', 'no')
-			->will($this->returnValue('no'));
-
-		$client->expects($this->once())
-			->method('get')
-			->with('https://foo.bar/bla2', $this->callback(function($obj) {
-				return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
-			}))
-			->will($this->returnValue($response));
-
-		$response->expects($this->once())
-			->method('getBody')
-			->with()
-			->will($this->returnValue($body));
-		$response->expects($this->once())
-			->method('getHeader')
-			->with('Content-Type')
-			->will($this->returnValue($contentType));
-
-		$this->caldavBackend->expects($this->once())
-			->method('purgeAllCachedEventsForSubscription')
-			->with(42);
-
-		$this->caldavBackend->expects($this->once())
-			->method('createCalendarObject')
-			->with(42, '12345.ics', $result, 1);
+		if ($process) {
+			$this->refreshWebcalService->expects($this->once())
+				->method('refreshSubscription')
+				->with('principals/users/testuser', 'sub123');
+		} else {
+			$this->refreshWebcalService->expects($this->never())
+				->method('refreshSubscription')
+				->with('principals/users/testuser', 'sub123');
+		}
 
 		$backgroundJob->execute($this->jobList, $this->logger);
 	}
@@ -160,93 +110,8 @@ class RefreshWebcalJobTest extends TestCase {
 	 */
 	public function runDataProvider():array {
 		return [
-			[
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
-				'text/calendar;charset=utf8',
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
-			],
-			[
-				'["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
-				'application/calendar+json',
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VTIMEZONE\r\nLAST-MODIFIED:20040110T032845Z\r\nTZID:US/Eastern\r\nBEGIN:DAYLIGHT\r\nDTSTART:20000404T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20001026T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060102T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
-			],
-			[
-				'<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
-				'application/calendar+xml',
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060104T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2 bis\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
-			]
-		];
-	}
-
-	/**
-	 * @dataProvider runLocalURLDataProvider
-	 *
-	 * @param string $source
-	 */
-	public function testRunLocalURL($source) {
-		$backgroundJob = new RefreshWebcalJob($this->caldavBackend,
-			$this->clientService, $this->config, $this->logger, $this->timeFactory);
-
-		$backgroundJob->setArgument([
-			'principaluri' => 'principals/users/testuser',
-			'uri' => 'sub123',
-		]);
-		$backgroundJob->setLastRun(0);
-
-		$this->timeFactory->expects($this->once())
-			->method('getTime')
-			->with()
-			->will($this->returnValue(1000000000));
-
-		$this->caldavBackend->expects($this->exactly(2))
-			->method('getSubscriptionsForUser')
-			->with('principals/users/testuser')
-			->will($this->returnValue([
-				[
-					'id' => 42,
-					'uri' => 'sub123',
-					'refreshreate' => 'P1H',
-					'striptodos' => 1,
-					'stripalarms' => 1,
-					'stripattachments' => 1,
-					'source' => $source
-				],
-			]));
-
-		$client = $this->createMock(IClient::class);
-		$this->clientService->expects($this->once())
-			->method('newClient')
-			->with()
-			->will($this->returnValue($client));
-
-		$this->config->expects($this->once())
-			->method('getAppValue')
-			->with('dav', 'webcalAllowLocalAccess', 'no')
-			->will($this->returnValue('no'));
-
-		$client->expects($this->never())
-			->method('get');
-
-		$backgroundJob->execute($this->jobList, $this->logger);
-	}
-
-	public function runLocalURLDataProvider():array {
-		return [
-			['localhost/foo.bar'],
-			['localHost/foo.bar'],
-			['random-host/foo.bar'],
-			['[::1]/bla.blub'],
-			['[::]/bla.blub'],
-			['192.168.0.1'],
-			['172.16.42.1'],
-			['[fdf8:f53b:82e4::53]/secret.ics'],
-			['[fe80::200:5aee:feaa:20a2]/secret.ics'],
-			['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
-			['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
-			['10.0.0.1'],
-			['another-host.local'],
-			['service.localhost'],
-			['!@#$'], // test invalid url
+			[0, 100000, true],
+			[100000, 100000, false]
 		];
 	}
 }
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
new file mode 100644
index 00000000000..01e541ec20e
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
@@ -0,0 +1,221 @@
+<?php
+/**
+ * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr>
+ *
+ * @author Georg Ehrke <oc.list@georgehrke.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Thomas Citharel <nextcloud@tcit.fr>
+ *
+ * @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\DAV\Tests\unit\BackgroundJob;
+
+use GuzzleHttp\HandlerStack;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IResponse;
+use OCP\IConfig;
+use OCP\ILogger;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject;
+
+use Test\TestCase;
+
+class RefreshWebcalServiceTest extends TestCase {
+
+	/** @var CalDavBackend | MockObject */
+	private $caldavBackend;
+
+	/** @var IClientService | MockObject */
+	private $clientService;
+
+	/** @var IConfig | MockObject */
+	private $config;
+
+	/** @var ILogger | MockObject */
+	private $logger;
+
+	protected function setUp(): void {
+		parent::setUp();
+
+		$this->caldavBackend = $this->createMock(CalDavBackend::class);
+		$this->clientService = $this->createMock(IClientService::class);
+		$this->config = $this->createMock(IConfig::class);
+		$this->logger = $this->createMock(ILogger::class);
+	}
+
+	/**
+	 * @param string $body
+	 * @param string $contentType
+	 * @param string $result
+	 *
+	 * @dataProvider runDataProvider
+	 */
+	public function testRun(string $body, string $contentType, string $result) {
+		$refreshWebcalService = new RefreshWebcalService($this->caldavBackend,
+			$this->clientService, $this->config, $this->logger);
+
+		$this->caldavBackend->expects($this->once())
+			->method('getSubscriptionsForUser')
+			->with('principals/users/testuser')
+			->will($this->returnValue([
+				[
+					'id' => '99',
+					'uri' => 'sub456',
+					'{http://apple.com/ns/ical/}refreshrate' => 'P1D',
+					'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
+					'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
+					'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
+					'source' => 'webcal://foo.bar/bla'
+				],
+				[
+					'id' => '42',
+					'uri' => 'sub123',
+					'{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
+					'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
+					'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
+					'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
+					'source' => 'webcal://foo.bar/bla2'
+				],
+			]));
+
+		$client = $this->createMock(IClient::class);
+		$response = $this->createMock(IResponse::class);
+		$this->clientService->expects($this->once())
+			->method('newClient')
+			->with()
+			->will($this->returnValue($client));
+
+		$this->config->expects($this->once())
+			->method('getAppValue')
+			->with('dav', 'webcalAllowLocalAccess', 'no')
+			->will($this->returnValue('no'));
+
+		$client->expects($this->once())
+			->method('get')
+			->with('https://foo.bar/bla2', $this->callback(function($obj) {
+				return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
+			}))
+			->will($this->returnValue($response));
+
+		$response->expects($this->once())
+			->method('getBody')
+			->with()
+			->will($this->returnValue($body));
+		$response->expects($this->once())
+			->method('getHeader')
+			->with('Content-Type')
+			->will($this->returnValue($contentType));
+
+		$this->caldavBackend->expects($this->once())
+			->method('purgeAllCachedEventsForSubscription')
+			->with(42);
+
+		$this->caldavBackend->expects($this->once())
+			->method('createCalendarObject')
+			->with(42, '12345.ics', $result, 1);
+
+		$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+	}
+
+	/**
+	 * @return array
+	 */
+	public function runDataProvider():array {
+		return [
+			[
+				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+				'text/calendar;charset=utf8',
+				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+			],
+			[
+				'["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
+				'application/calendar+json',
+				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VTIMEZONE\r\nLAST-MODIFIED:20040110T032845Z\r\nTZID:US/Eastern\r\nBEGIN:DAYLIGHT\r\nDTSTART:20000404T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20001026T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060102T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
+			],
+			[
+				'<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
+				'application/calendar+xml',
+				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060104T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2 bis\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
+			]
+		];
+	}
+
+	/**
+	 * @dataProvider runLocalURLDataProvider
+	 *
+	 * @param string $source
+	 */
+	public function testRunLocalURL($source) {
+		$refreshWebcalService = new RefreshWebcalService($this->caldavBackend,
+			$this->clientService, $this->config, $this->logger);
+
+		$this->caldavBackend->expects($this->once())
+			->method('getSubscriptionsForUser')
+			->with('principals/users/testuser')
+			->will($this->returnValue([
+				[
+					'id' => 42,
+					'uri' => 'sub123',
+					'refreshreate' => 'P1H',
+					'striptodos' => 1,
+					'stripalarms' => 1,
+					'stripattachments' => 1,
+					'source' => $source
+				],
+			]));
+
+		$client = $this->createMock(IClient::class);
+		$this->clientService->expects($this->once())
+			->method('newClient')
+			->with()
+			->will($this->returnValue($client));
+
+		$this->config->expects($this->once())
+			->method('getAppValue')
+			->with('dav', 'webcalAllowLocalAccess', 'no')
+			->will($this->returnValue('no'));
+
+		$client->expects($this->never())
+			->method('get');
+
+		$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+	}
+
+	public function runLocalURLDataProvider():array {
+		return [
+			['localhost/foo.bar'],
+			['localHost/foo.bar'],
+			['random-host/foo.bar'],
+			['[::1]/bla.blub'],
+			['[::]/bla.blub'],
+			['192.168.0.1'],
+			['172.16.42.1'],
+			['[fdf8:f53b:82e4::53]/secret.ics'],
+			['[fe80::200:5aee:feaa:20a2]/secret.ics'],
+			['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
+			['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
+			['10.0.0.1'],
+			['another-host.local'],
+			['service.localhost'],
+			['!@#$'], // test invalid url
+		];
+	}
+}