diff --git a/aliasvault.sln b/aliasvault.sln
index 40426bb89..512cdb7b9 100644
--- a/aliasvault.sln
+++ b/aliasvault.sln
@@ -67,6 +67,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DD359F
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Shared.Core", "src\Shared\AliasVault.Shared.Core\AliasVault.Shared.Core.csproj", "{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.TaskRunner", "src\Services\AliasVault.TaskRunner\AliasVault.TaskRunner.csproj", "{D631A936-DD1C-40CC-B735-BD0A5D4F46A1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -169,6 +171,10 @@ Global
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -197,6 +203,7 @@ Global
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
+ {D631A936-DD1C-40CC-B735-BD0A5D4F46A1} = {8A477241-B96C-4174-968D-D40CB77F1ECD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FEE82475-C009-4762-8113-A6563D9DC49E}
diff --git a/src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj b/src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj
new file mode 100644
index 000000000..a17c2c2ac
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net9.0
+ enable
+ enable
+ dotnet-AliasVault.TaskRunner-eaac287e-32a7-4ff9-bbf9-1925c446ef73
+ Linux
+ ..\..\..
+ 13
+
+
+
+ true
+ bin\Debug\net9.0\AliasVault.TaskRunner.xml
+
+
+
+ true
+ bin\Release\net9.0\AliasVault.TaskRunner.xml
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/src/Services/AliasVault.TaskRunner/Config.cs b/src/Services/AliasVault.TaskRunner/Config.cs
new file mode 100644
index 000000000..b60048ff6
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/Config.cs
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.TaskRunner;
+
+///
+/// Configuration class for the TaskRunner with values loaded from appsettings.json file.
+///
+public class Config
+{
+ ///
+ /// TODO: update config properties to only use the ones that are needed for TaskRunner.
+ /// TOOD: If none are needed, remove this class.
+ /// Gets or sets whether TLS is enabled for the SMTP service.
+ ///
+ public string SmtpTlsEnabled { get; set; } = "false";
+
+ ///
+ /// Gets or sets the domains that the SMTP service is listening for.
+ /// Domains not in this list will be rejected.
+ ///
+ public List AllowedToDomains { get; set; } = [];
+}
diff --git a/src/Services/AliasVault.TaskRunner/Dockerfile b/src/Services/AliasVault.TaskRunner/Dockerfile
new file mode 100644
index 000000000..a587a7b41
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/Dockerfile
@@ -0,0 +1,20 @@
+FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
+WORKDIR /app
+
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+
+# Copy the project files and restore dependencies
+COPY ["src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj", "src/Services/AliasVault.TaskRunner/"]
+RUN dotnet restore "./src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj"
+COPY . .
+
+# Build and publish the application
+WORKDIR "/src/src/Services/AliasVault.TaskRunner"
+RUN dotnet publish "./AliasVault.TaskRunner.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=build /app/publish .
+ENTRYPOINT ["dotnet", "AliasVault.TaskRunner.dll"]
diff --git a/src/Services/AliasVault.TaskRunner/Program.cs b/src/Services/AliasVault.TaskRunner/Program.cs
new file mode 100644
index 000000000..85ae6e807
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/Program.cs
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+using System.Reflection;
+using AliasServerDb;
+using AliasServerDb.Configuration;
+using AliasVault.Logging;
+using AliasVault.TaskRunner.Workers;
+using AliasVault.WorkerStatus.ServiceExtensions;
+using Microsoft.EntityFrameworkCore;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
+builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
+builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAssembly().GetName().Name!, "../../../logs");
+
+// Create global config object, get values from environment variables.
+Config config = new Config();
+var emailDomains = Environment.GetEnvironmentVariable("PRIVATE_EMAIL_DOMAINS")
+ ?? throw new KeyNotFoundException("PRIVATE_EMAIL_DOMAINS environment variable is not set.");
+config.AllowedToDomains = emailDomains.Split(',').ToList();
+
+var tlsEnabled = Environment.GetEnvironmentVariable("SMTP_TLS_ENABLED")
+ ?? throw new KeyNotFoundException("SMTP_TLS_ENABLED environment variable is not set.");
+config.SmtpTlsEnabled = tlsEnabled;
+builder.Services.AddSingleton(config);
+
+builder.Services.AddAliasVaultSqliteConfiguration();
+
+// -----------------------------------------------------------------------
+// Register hosted services via Status library wrapper in order to monitor and control (start/stop) them via the database.
+// -----------------------------------------------------------------------
+builder.Services.AddStatusHostedService(Assembly.GetExecutingAssembly().GetName().Name!);
+
+var host = builder.Build();
+
+using (var scope = host.Services.CreateScope())
+{
+ var container = scope.ServiceProvider;
+ var factory = container.GetRequiredService>();
+ await using var context = await factory.CreateDbContextAsync();
+ await context.Database.MigrateAsync();
+}
+
+await host.RunAsync();
diff --git a/src/Services/AliasVault.TaskRunner/Properties/launchSettings.json b/src/Services/AliasVault.TaskRunner/Properties/launchSettings.json
new file mode 100644
index 000000000..0e12b8544
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/Properties/launchSettings.json
@@ -0,0 +1,17 @@
+{
+ "profiles": {
+ "AliasVault.TaskRunner": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development",
+ "PRIVATE_EMAIL_DOMAINS": "example.tld",
+ "SMTP_TLS_ENABLED": "false"
+ },
+ "dotnetRunMessages": true
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker"
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json"
+}
diff --git a/src/Services/AliasVault.TaskRunner/Workers/TaskRunnerWorker.cs b/src/Services/AliasVault.TaskRunner/Workers/TaskRunnerWorker.cs
new file mode 100644
index 000000000..d139f03fa
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/Workers/TaskRunnerWorker.cs
@@ -0,0 +1,21 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.TaskRunner.Workers;
+
+///
+/// A worker for the TaskRunner.
+///
+/// ILogger instance.
+public class TaskRunnerWorker(ILogger logger) : BackgroundService
+{
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ logger.LogWarning("AliasVault.TaskRunnerWorker started at: {Time}", DateTimeOffset.Now);
+ }
+}
diff --git a/src/Services/AliasVault.TaskRunner/appsettings.Development.json b/src/Services/AliasVault.TaskRunner/appsettings.Development.json
new file mode 100644
index 000000000..df591876d
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.EntityFrameworkCore": "Warning"
+ }
+ }
+}
diff --git a/src/Services/AliasVault.TaskRunner/appsettings.json b/src/Services/AliasVault.TaskRunner/appsettings.json
new file mode 100644
index 000000000..ddbf1678f
--- /dev/null
+++ b/src/Services/AliasVault.TaskRunner/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.EntityFrameworkCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "AliasServerDbContext": "Data Source=../../../database/AliasServerDb.sqlite"
+ }
+}