<?php
	declare(strict_types = 1);
	namespace Kimendisch\Sbgg_Jetzt;
	use Flake\Dat;
	use Flake\Id64;
	use Flake\Project;
	use Flake\Error;
	use Flake\Request;
	use PHPMailer\PHPMailer\PHPMailer;
	
	class Newsletter {
		/**
		* Static content.
		*/
		private const STATIC_CONTENT = [
			"text_sourcecode" => [
				"de" => "Quellcode",
				"en" => "Source Code"
			],
			
			"text_unsubscribe" => [
				"de" => "Abbestellen",
				"en" => "Unsubscribe"
			],
			"text_verify" => [
				"de" => "Verifizieren",
				"en" => "Verify"
			],
			
			"text_imprint" => [
				"de" => "Impressum",
				"en" => "Imprint"
			],
			"link_imprint" => [
				"de" => "https://www.tjdev.de/impressum",
				"en" => "https://www.tjdev.de/imprint"
			],

			"text_privacy_policy" => [
				"de" => "Datenschutz&shy;erklärung",
				"en" => "Privacy Policy"
			],
			"link_privacy_policy" => [
				"de" => "https://www.tjdev.de/datenschutz",
				"en" => "https://www.tjdev.de/privacy"
			],
		];
		
		
		
		/**
		* @var array $pending_render		List of templates which are currently rendering.
		*/
		private static array $pending_render = [];
		
		
		
		/**
		* Start a newsletter subscription verification process.
		* 
		* @param string $mail_address		Recipient mail address.
		* @param string $language			Desired newsletter language.
		*/
		public static function verify(string $mail_address, string $language): void {
			// OPEN DATABASE //
			// acquire runlock
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			
			// obtain handle
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
			
			
			// MAYBE INITIALIZE DATABASE //
			self::db_init(db: $newsletter_db);
			
			
			// VERIFY LOGIC //
			// cleanup of verify list
			self::verify_list_cleanup(db: $newsletter_db);
			
			// decide whether verification should be attempted
			$do_verify = self::verify_decide(db: $newsletter_db, mail_address: $mail_address, language: $language);
			
			// read current verify key
			if($do_verify) $verify_key = $newsletter_db->get(["verify_list", sha1($mail_address), "verify_key"]);
			
			
			// FINALIZE //
			// close database
			$newsletter_db->write_close();
			
			// maybe send verify mail
			if($do_verify){
				$content = self::content_file_read("0000-00-00-verify");
				self::send(mail_address: $mail_address, content: $content, dataset: [
					"language" => $language,
					"verify_key" => $verify_key
				]);
			}
			
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
		}
		
		
		
		/**
		* HELPER: Remove stale entries in verify list.
		* 
		* @param Dat $db		Newsletter database object.
		*/
		private static function verify_list_cleanup(Dat $db): void {
			// GET FULL DATABASE //
			$db_verify_list = $db->get("verify_list");
			$time = time();
			
			
			// REMOVE STALE ATTEMPT TIMESTAMPS //
			foreach($db_verify_list as $one_key => $one_dataset){
				$db_verify_list[$one_key]["attempt_list"] = array_values(array_filter($one_dataset["attempt_list"], function($one_attempt) use ($time): bool {
					// only keep if younger than 24h
					return ($one_attempt + (24 * 3600) > $time);
				}));
			}
			
			
			// REMOVE STALE DATASETS //
			$db_verify_list = array_filter($db_verify_list, function($one_dataset): bool {
				// check whether there are recent attempts
				if(sizeof($one_dataset["attempt_list"]) > 0) return true;
				
				// no reason for this dataset to stay
				return false;
			});
			
			
			// SET NEW VALUES //
			$db->set("verify_list", $db_verify_list);
		}
		
		
		
		/**
		* HELPER: Manage verify list and decide whether a verify attempt should be made.
		* 
		* @param Dat $db					Newsletter database object.
		* @param string $mail_address		Recipient mail address.
		* @param string $language			Desired newsletter language.
		* 
		* @return bool						Whether verify attempt should be made.
		*/
		private static function verify_decide(Dat $db, string $mail_address, string $language): bool {
			// CHECK MEMBER STATUS //
			if($db->get(["list", $mail_address]) !== null) return false;
			
			
			// MAYBE CREATE DATASET //
			$verify_list_entry_key = ["verify_list", sha1($mail_address)];
			if($db->get($verify_list_entry_key) === null){
				$db->set($verify_list_entry_key, [
					"attempt_list" => [],
				]);
			}
			
			
			// CHECK COOLDOWN //
			// get previous attempts
			$attempt_list = $db->get($verify_list_entry_key)["attempt_list"];
			$attempt_count = sizeof($attempt_list);
			
			// get last attempt timestamp
			$last_attempt_timestamp = end($attempt_list);
			
			// get cooldown duration from attempt count
			$cooldown_duration = ([
				0 => 0,
				1 => (5 * 60),
				2 => (30 * 60),
				3 => (2 * 3600),
				4 => (6 * 3600),
			])[$attempt_count] ?? (12 * 3600);
			
			// check whether still within cooldown
			$time = time();
			if($last_attempt_timestamp + $cooldown_duration > $time) return false;
			
			
			// UPDATE DATASET //
			// get old values
			$dataset = $db->get($verify_list_entry_key);
			
			// generate new verify key
			$dataset["verify_key"] = Id64::new(length: 16);
			
			// add attempt
			$dataset["attempt_list"][] = $time;
			
			// update language
			$dataset["language"] = $language;
			
			// set new values
			$db->set($verify_list_entry_key, $dataset);
			
			
			// SEND VERIFY MAIL //
			return true;
		}
		
		
		
		/**
		* Check a verify key and add a member to the newsletter.
		* 
		* @param string $mail_address		Member mail address.
		* @param string $verify_key			User-provided verify key.
		* 
		* @return bool						Whether member has successfully been subscribed.
		*/
		public static function subscribe(string $mail_address, string $verify_key): bool {
			// OPEN DATABASE //
			// acquire runlock
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			
			// obtain handle
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
			
			
			// MAYBE INITIALIZE DATABASE //
			self::db_init(db: $newsletter_db);
			
			
			// VERIFY LOGIC //
			// cleanup of verify list
			self::verify_list_cleanup(db: $newsletter_db);
			
			// find dataset in verify list
			$verify_list_dataset = $newsletter_db->get(["verify_list", sha1($mail_address)]);
			$success = ($verify_list_dataset !== null);
			
			// maybe check verify key
			$success = ($success and $verify_list_dataset["verify_key"] === $verify_key);
			
			
			// MAYBE ADD MEMBER //
			if($success){
				$entry_key = ["list", $mail_address];
				
				// remove from verify list
				$newsletter_db->unset(["verify_list", sha1($mail_address)]);
				
				// generate unsubscribe key
				$unsubscribe_key = Id64::new(length: 16);
				
				// only allow new entries
				$success = ($newsletter_db->get($entry_key) === null);
				
				// maybe add to database
				if($success){
					$newsletter_db->set($entry_key, [
						"unsubscribe_key" => $unsubscribe_key,
						"language" => $verify_list_dataset["language"],
					]);
				}
			}
			
			
			// FINALIZE //
			// close database
			$newsletter_db->write_close();
			
			// maybe send welcome mail
			if($success) self::send_one(mail_address: $mail_address, content_name: "0000-00-00-welcome");
			
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
			
			// return
			return $success;
		}
		
		
		
		/**
		* Remove a mail address from the mailing list.
		* 
		* @param string $mail_address			Member mail address.
		* @param string $unsubscribe_key		Unsubscribe key (used for authorization).
		* 
		* @return bool							Whether a dataset has successfully been removed.
		*/
		public static function unsubscribe(string $mail_address, string $unsubscribe_key): bool {
			// OPEN DATABASE //
			// acquire runlock
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			
			// obtain handle
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
			
			
			// MAYBE INITIALIZE DATABASE //
			self::db_init(db: $newsletter_db);
			
			
			// UPDATE DATABASE //
			$entry_key = ["list", $mail_address];
			
			// check whether mail address is member of newsletter
			$dataset = $newsletter_db->get($entry_key);
			$is_member = ($dataset !== null);
			
			// validate unsubscribe key
			$unsubscribe_key_valid = (($dataset["unsubscribe_key"] ?? null) === $unsubscribe_key);
			
			// maybe remove from database
			$has_removed = false;
			if($is_member and $unsubscribe_key_valid){
				$newsletter_db->unset($entry_key);
				$has_removed = true;
			}
			
			
			// FINALIZE //
			// close database
			$newsletter_db->write_close();
			
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
			
			// return
			return $has_removed;
		}
		
		
		
		/**
		* Check whether a mail address is contained in the newsletter list.
		* 
		* @param string $mail_address		Mail address to search for.
		* 
		* @return bool						Whether this mail address is a newsletter member.
		*/
		public static function is_member(string $mail_address): bool {
			// TRY LOADING FROM DATABASE //
			// open
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
			
			// read
			$dataset = $newsletter_db->get(["list", $mail_address]);
			
			// close
			$newsletter_db->close();
			ignore_user_abort($old_ignore_user_abort);
			
			
			// CHECK //
			return ($dataset !== null);
		}
		
		
		
		/**
		* Determine number of members.
		* 
		* @return int		Number of members in newsletter list.
		*/
		public static function member_count(): int {
			// LOAD LIST FROM DATABASE //
			// open
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
			
			// read
			$member_list = $newsletter_db->get("list") ?? [];
			
			// close
			$newsletter_db->close();
			ignore_user_abort($old_ignore_user_abort);
			
			
			// RETURN SIZE //
			return sizeof($member_list);
		}
		
		
		
		/**
		* Send content to one newsletter member.
		* 
		* @param string $mail_address		Member mail address.
		* @param string $content_name		Name of content to send.
		*/
		public static function send_one(string $mail_address, string $content_name): void {
			// acquire runlock
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			
			// read content
			$content = self::content_file_read($content_name);
			
			// send content
			self::send(mail_address: $mail_address, content: $content);
			
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
		}
		
		
		
		/**
		* Send content to all newsletter members.
		* 
		* @param string $content_name		Name of content to send.
		*/
		public static function send_all(string $content_name): void {
			// LOAD LIST FROM DATABASE //
			// open database
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
			
			// read list
			$member_list = $newsletter_db->get("list") ?? [];
			
			// close database
			$newsletter_db->close();
			
			
			// SEND CONTENT //
			// read content
			$content = self::content_file_read($content_name);
			
			// iterate over all list members
			foreach($member_list as $one_member_mail_address => $one_member_dataset){
				// send to this member
				self::send(mail_address: $one_member_mail_address, content: $content, dataset: $one_member_dataset);
			}
			
			
			// FINALIZE //
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
		}
		
		
		
		/**
		* HELPER: Initialize the database with empty values.
		* 
		* @param Dat $db		Newsletter database object.
		*/
		private static function db_init(Dat $db): void {
			// verify list
			if(!is_array($db->get("verify_list"))){
				$db->set("verify_list", []);
			}
			
			// member list
			if(!is_array($db->get("list"))){
				$db->set("list", []);
			}
			
			// job queue
			if(!is_array($db->get("queue"))){
				$db->set("queue", []);
			}
		}
		
		
		
		/**
		* HELPER: List all available content files.
		* 
		* @return array		List of content file names.
		*/
		private static function content_file_list(): array {
			// GET AVAILABLE CONTENT FILES //
			$indir = scandir("./newsletter/content");
			$content_file_list = [];
			foreach($indir as $one_element){
				// only get files
				if($one_element === "." or $one_element === "..") continue;
				$path = "./newsletter/content/" . $one_element;
				if(!is_file($path)) continue;
				
				// check file name format
				if(!preg_match("/^\d{4}-\d{2}-\d{2}(-[a-z0-9]+)+\.data\.php$/", $one_element)) continue;
				
				// add to list
				$content_file_list[] = $one_element;
			}
			
			// return list
			return $content_file_list;
		}
		
		
		
		/**
		* HELPER: List all available content names.
		* 
		* @return array		List of content names.
		*/
		public static function content_list(): array {
			// get available content files
			$content_file_list = self::content_file_list();
			
			// remove file extension
			$content_name_list = [];
			foreach($content_file_list as $one_content_file){
				$content_name_list[] = substr($one_content_file, 0, strlen($one_content_file) - strlen(".data.php"));
			}
			
			// return list
			return $content_name_list;
		}
		
		
		
		/**
		* HELPER: Read one content data from file.
		* 
		* @param string $name		Name of the content to read.
		* 
		* @return array				Content data.
		*/
		public static function content_file_read(string $name): array {
			// GET AVAILABLE CONTENT FILES //
			$content_file_list = self::content_file_list();
			
			
			// PARSE FILE //
			// build content file name
			$file_name = $name . ".data.php";
			
			// make sure it exists
			if(!in_array($file_name, $content_file_list)) Error::error(message: "Unknown content file name", data: ["content_file_list" => $content_file_list, "name" => $name, "file_name" => $file_name]);
			
			// build full file path
			$path = "./newsletter/content/" . $file_name;
			
			// run isolated in own function scope
			$content_from_file = (function(){
				return require(func_get_arg(0));
			})($path);
			
			// add content name
			$content_from_file["name"] = [
				"de" => $name,
				"en" => $name,
			];
			
			// add static content
			return array_merge(self::STATIC_CONTENT, $content_from_file);
		}
		
		
		
		/**
		* HELPER: Render content definition to html body.
		* 
		* @param array $content		Content data.
		* @param array $dataset		Member dataset.
		* @param string $root		Root template.
		* 
		* @return string			Html body.
		*/
		private static function content_render(array $content, array $dataset, string $root = "base"): string {
			// START WITH RAW TEMPLATE //
			// read
			$html = self::template_read(name: $root);
			
			// remember that our template is not done rendering yet
			self::$pending_render[] = $root;
			
			
			// SUBSTITUTE PLACEHOLDERS //
			$callback = function(array $matches) use ($content, $dataset): string {
				$key = $matches["key"];
				$value = $matches["value"];
				$value2 = $matches["value2"] ?? "";
				
				// content value
				if($key === ""){
					if(!isset($content[$value][$dataset["language"]])) Error::error(message: "Unable to substitute content placeholder", data: ["matches" => $matches, "content" => $content, "dataset" => $dataset]);
					return $content[$value][$dataset["language"]];
				}
				
				// template
				if($key === "template"){
					if(in_array($value, self::$pending_render)) Error::error(message: "Recursive template rendering", data: ["pending_render" => self::$pending_render, "template" => $value]);
					return self::content_render(content: $content, dataset: $dataset, root: $value);
				}
				
				// conditional template
				if($key === "template-if-isset-dataset"){
					if(!isset($dataset[$value])) return "";
					if(in_array($value2, self::$pending_render)) Error::error(message: "Recursive template rendering", data: ["pending_render" => self::$pending_render, "template" => $value2]);
					return self::content_render(content: $content, dataset: $dataset, root: $value2);
				}
				
				// dataset value
				if($key === "dataset"){
					if(!isset($dataset[$value])) Error::error(message: "Unable to substitute dataset placeholder", data: ["matches" => $matches, "dataset" => $dataset]);
					return $dataset[$value];
				}
				
				// constant
				if($key === "const"){
					if($value === "version") return Project::version();
					if($value === "url_prefix") return ("http" . (Request::has_ssl() ? "s" : "") . "://" . Request::domain_raw_full());
					Error::error(message: "Unknown constant name", data: ["value" => $value]);
				}
				
				// unknown key
				Error::error(message: "Unknown placeholder key", data: ["key" => $key, "value" => $value]);
			};
			$html = preg_replace_callback("/(?:{{)(?:(?<key>[0-9a-z-_\.]+)\:)?(?<value>[0-9a-z-_\.]+)(?:\,(?<value2>[0-9a-z-_\.]+))?(?:}})/", $callback, $html);
			
			
			// DONE //
			// remove from waiting list
			unset(self::$pending_render[array_search($root, self::$pending_render)]);
			
			// return
			return $html;
		}
		
		
		
		/**
		* HELPER: Render content definition to html body as preview.
		* 
		* @param array $content			Content data.
		* @param string $language		Preview language.
		* 
		* @return string				Html body.
		*/
		public static function content_render_preview(array $content, string $language): string {
			// PREPARE A FAKE DATASET //
			// defaults
			$dataset = [
				"language" => $language,
				"unsubscribe_key" => Id64::new(length: 16),
				"mail_address" => "mail@example.com",
				"mail_address_urlencoded" => urlencode("mail@example.com"),
			];
			
			// content: `verify`
			if($content["name"]["de"] === "0000-00-00-verify"){
				unset($dataset["unsubscribe_key"]);
				$dataset["verify_key"] = Id64::new(length: 16);
			}
			
			
			// RENDER //
			return self::content_render(content: $content, dataset: $dataset);
		}
		
		
		
		/**
		* HELPER: Read template file.
		* 
		* @param string $name		Name of template to read.
		* 
		* @return string			Template.
		*/
		private static function template_read(string $name): string {
			// GET AVAILABLE TEMPLATE FILES //
			$indir = scandir("./newsletter/template");
			$template_file_list = [];
			foreach($indir as $one_element){
				// only get files
				if($one_element === "." or $one_element === "..") continue;
				$path = "./newsletter/template/" . $one_element;
				if(!is_file($path)) continue;
				
				// check extension
				if(pathinfo($path)["extension"] !== "html") continue;
				
				// add to list
				$template_file_list[] = $one_element;
			}
			
			
			// READ FILE //
			// build template file name
			$file_name = $name . ".html";
			
			// make sure it exists
			if(!in_array($file_name, $template_file_list)){
				Error::error(message: "Unknown template file name", data: ["template_file_list" => $template_file_list, "name" => $name, "file_name" => $file_name]);
			}
			
			// build full file path
			$path = "./newsletter/template/" . $file_name;
			
			// return contents
			return file_get_contents($path);
		}
		
		
		
		/**
		* HELPER: Send a mail to one newsletter member.
		* 
		* @param string 
		* @param array $content			Mail content.
		* @param ?array $dataset		Previously read dataset for this member
		*								(this makes it possible to use one database read for multiple sends).
		*/
		private static function send(string $mail_address, array $content, ?array $dataset = null): void {
			// MAYBE OBTAIN MEMBER DATASET //
			if($dataset === null){
				// open
				$old_ignore_user_abort = (bool)ignore_user_abort(true);
				$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
				
				// read
				$dataset = $newsletter_db->get(["list", $mail_address]);
				
				// close
				$newsletter_db->close();
				ignore_user_abort($old_ignore_user_abort);
			}
			
			// validate
			if(!is_array($dataset)){
				Error::error(message: "Mail address not contained in database", data: ["mail_address" => $mail_address, "dataset" => $dataset]);
			}
			
			// add mail address to dataset
			$dataset["mail_address"] = $mail_address;
			$dataset["mail_address_urlencoded"] = urlencode($mail_address);
			
			
			// VALIDATE CONTENT //
			// check whether subject is set
			if(!isset($content["subject"])){
				Error::error(message: "Missing content key: 'subject'", data: ["content" => $content]);
			}
			
			// check whether main is set
			if(!isset($content["main"])){
				Error::error(message: "Missing content key: 'main'", data: ["content" => $content]);
			}
			
			
			// RENDER CONTENT //
			$body = self::content_render(content: $content, dataset: $dataset);
			
			
			// ADD JOB //
			// get subject
			$subject = $content["subject"][$dataset["language"]];
			
			// add to queue
			self::queue_job_add(type: "mail_send_html", data: [
				"mail_address" => $mail_address,
				"subject" => $subject,
				"body" => $body,
			]);
		}
		
		
		
		/**
		* HELPER: Add a job to the queue.
		* 
		* @param string $type		Job type.
		* @param array $data		Job data.
		*/
		private static function queue_job_add(string $type, array $data = []): void {
			// OPEN DATABASE //
			// acquire runlock
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			
			// obtain handle
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
			
			
			// MAYBE INITIALIZE DATABASE //
			self::db_init(db: $newsletter_db);
			
			
			// UPDATE DATABASE //
			// get existing job ids
			$used_job_id_list = array_keys($newsletter_db->get("queue"));
			
			// generate new unique job id
			$job_id = Id64::unique(length: 32, list: $used_job_id_list);
			
			// add new job
			$newsletter_db->set(["queue", $job_id], [
				"type" => $type,
				"data" => $data,
				"retry_count" => 0,
				"lock" => [
					"held" => false,
					"time" => 0,
				],
			]);
			
			
			// FINALIZE //
			// close database
			$newsletter_db->write_close();
			
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
		}
		
		
		
		/**
		* HELPER: Close the http connection to the client.
		*/
		public static function api_helper_http_close_connection(): void {
			// make sure cgi process is not killed
			ignore_user_abort(true);
			
			// make sure client stops listening
			header("Connection: close");
			header("Content-Encoding: none");
			
			// current buffer is full content
			header("Content-Length: " . ob_get_length());
			
			// flush ob to cgi buffer
			ob_end_flush();
			
			// flush cgi buffer to client
			flush();
		}
		
		
		
		/**
		* CALLBACK: Client connection has been closed, execute queued jobs now.
		*/
		public static function queue_work(): void {
			// INITIALIZE //
			// acquire runlock
			$old_ignore_user_abort = (bool)ignore_user_abort(true);
			
			// get list of available job ids
			$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
			$job_id_list = array_keys($newsletter_db->get("queue") ?? []);
			$newsletter_db->close();
			
			
			foreach($job_id_list as $one_job_id){
				// TRY ACQUIRING A JOB LOCK //
				// reset timeout
				set_time_limit(150);
				
				// open database
				$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
				
				// make sure job still exists
				$one_job_data = $newsletter_db->get(["queue", $one_job_id]);
				if($one_job_data === null){
					$newsletter_db->close();
					continue;
				}
				
				// check whether lock is either not held or stale
				$time = time();
				if($one_job_data["lock"]["held"] and $one_job_data["lock"]["time"] + 180 >= $time){
					$newsletter_db->close();
					continue;
				}
				
				// acquire lock
				$newsletter_db->set(["queue", $one_job_id, "lock"], [
					"held" => true,
					"time" => $time,
				]);
				
				// close database
				$newsletter_db->write_close();
				
				
				// TRY EXECUTING JOB //
				$success = false;
				if($one_job_data["type"] === "mail_send_html"){
					// `mail_send_html`
					$success = self::mail_send_html(...$one_job_data["data"]);
					
				} else {
					// unknown job type
					Error::warn(message: "Unknown job type", data: ["type" => $one_job_data["type"], "one_job_id" => $one_job_id, "one_job_data" => $one_job_data]);
				}
				
				
				// FINISH THIS JOB //
				// open database
				$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
				
				// make sure job still exists
				$one_job_data = $newsletter_db->get(["queue", $one_job_id]);
				if($one_job_data === null){
					$newsletter_db->close();
					continue;
				}
				
				// maybe remove successful job
				if($success){
					$newsletter_db->unset(["queue", $one_job_id]);
					$newsletter_db->write_close();
					continue;
				}
				
				// maybe remove failed job with high retry count
				if(($one_job_data["retry_count"] + 1) >= 3){
					Error::warn(message: "Multiple failed job execute attempts", data: ["one_job_id" => $one_job_id, "one_job_data" => $one_job_data]);
					$newsletter_db->unset(["queue", $one_job_id]);
					$newsletter_db->write_close();
					continue;
				}
				
				// increase retry count
				$newsletter_db->set(["queue", $one_job_id, "retry_count"], $one_job_data["retry_count"] + 1);
				
				// release job lock
				$newsletter_db->set(["queue", $one_job_id, "lock"], [
					"held" => false,
					"time" => 0,
				]);
				
				// close database
				$newsletter_db->write_close();
			}
			
			
			// FINALIZE //
			// release runlock
			ignore_user_abort($old_ignore_user_abort);
		}
		
		
		
		/**
		* HELPER: Actually send html mail using the phpmailer library.
		* 
		* @param string $mail_address		Recipient mail address.
		* @param string $subject			Mail subject.
		* @param string $body				Mail html body content.
		* 
		* @return bool						Whether sending was successful.
		*/
		private static function mail_send_html(string $mail_address, string $subject, string $body): bool {
			// GET NEW PHPMAILER OBJECT //
			$phpmailer = new PHPMailer();
			
			
			// GENERAL SETTINGS //
			// make timeout more aggressive
			$phpmailer->Timeout = 30;
			
			// we use smtp to send our mail
			$phpmailer->IsSMTP();
			
			// we use utf-8 encoding
			$phpmailer->CharSet = "UTF-8";
			
			
			// SMTP SETTINGS //
			// host and smtp port
			$phpmailer->Host = Env::MAIL["host"];
			$phpmailer->Port = Env::MAIL["port"];
			
			// we need to authenticate
			$phpmailer->SMTPAuth = true;
			
			// username and password
			$phpmailer->Username = Env::MAIL["username"];
			$phpmailer->Password = Env::MAIL["password"];
			
			// encryption
			$phpmailer->SMTPSecure = (Env::MAIL["starttls"] ? PHPMailer::ENCRYPTION_STARTTLS : PHPMailer::ENCRYPTION_SMTPS);
			
			
			// FROM AND TO //
			// from
			$phpmailer->setFrom(Env::MAIL["username"], "SBGG.jetzt");
			
			// recipient
			$phpmailer->addAddress($mail_address);
			
			
			// META STUFF //
			// replace x-mailer header
			$phpmailer->XMailer = Project::name() . " v" . Project::version();
			
			// set hostname to use for message-id header
			$phpmailer->Hostname = "sbgg.jetzt";
			
			
			// CONTENT //
			// this mail has html content
			$phpmailer->isHTML(true);
			
			// set subject
			$phpmailer->Subject = $subject;
			
			// html body
			$phpmailer->Body = $body;
			
			
			// SEND //
			return($phpmailer->send());
		}
	}
?>
