diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml
new file mode 100644
index 0000000..fac4d2d
--- /dev/null
+++ b/.github/workflows/docfx.yml
@@ -0,0 +1,26 @@
+name: docfx
+
+on:
+ push:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+ - name: Setup docfx
+ run: dotnet tool update -g docfx
+ - name: Copy README
+ run: cp README.md "./src/wan24-AutoDiscover Docs/index.md"
+ - name: Build docs
+ run: docfx "./src/wan24-AutoDiscover Docs/docfx.json" -t default,templates/singulinkfx
+ - name: Commit
+ uses: stefanzweifel/git-auto-commit-action@v5
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..8882759
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,26 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: .NET
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore dependencies
+ run: dotnet restore ./src/wan24-AutoDiscover.sln --ignore-failed-sources
+ - name: Build lib
+ run: dotnet build "./src/wan24-AutoDiscover/wan24-AutoDiscover.csproj" --no-restore
diff --git a/README.md b/README.md
index b9b1c32..b4cb92a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,197 @@
# wan24-AutoDiscover
- Mailserver autodiscovery
+
+This is a micro-webservice which supports a small part of the Microsoft
+Exchange POX autodiscover standard, which allows email clients to receive
+automatic configuration information for an email account.
+
+It was created using .NET 8 and ASP.NET. You find a published release build
+for each published release on GitHub as ZIP download for self-hosting.
+
+## Usage
+
+### `appsettings.json`
+
+The `appsettings.json` file contains the webservice configuration. The
+`DiscoveryConfig` is a `wan24.AutoDiscovery.Models.DiscoveryConfig` object. An
+example:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Kestrel": {
+ "Endpoints": {
+ "AutoDiscover": {
+ "Url": "http://localhost:5000"
+ }
+ }
+ },
+ "AllowedHosts": "*",
+ "DiscoveryConfig": {
+ "PreForkResponses": 10,
+ "Discovery": {
+ "localhost": {
+ "AcceptedDomains": [
+ "wan24.de",
+ "wan-solutions.de"
+ ],
+ "Protocols": [
+ {
+ "Type": "IMAP",
+ "Server": "imap.wan24.de",
+ "Port": 993
+ },
+ {
+ "Type": "SMTP",
+ "Server": "smtp.wan24.de",
+ "Port": 587
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
+Since the webservice should only listen local and be proxied by a real
+webserver (like Apache2), there is a `wan24.AutoDiscover.Models.DomainConfig`
+for `localhost`, which produces POX response for the allowed domains
+`wan24.de` and `wan-solutions.de` in this example (you should use your own
+domain names instead).
+
+The email client configuration will get an `IMAP` and a `SMTP` server pre-
+configuration, which contains the alias of the requested email address as
+login name and has all the other defaults from a
+`wan24.AutoDiscover.Models.Protocol` instance.
+
+With the `PreForkResponses` value you can define a number of pre-forked POX
+response XML documents to serve faster responses.
+
+Any change to this file will cause an automatic reload of the `DomainConfig`
+section.
+
+For serving a request, the `DomainConfig` will be looked up
+
+1. by the email address domain part
+1. by the served request hostname
+1. by any `DomainConfig` which has the email address domain part in the
+`AcceptedDomains` property, which contains a list of accepted domain names
+1. by the `DomainConfig` with an empty domain name as key
+
+Any unmatched `DomainConfig` will cause a `Bad request` http response.
+
+### Apache2 proxy setup
+
+Create the file `/etc/apache2/sites-available/autodiscover.conf`:
+
+```txt
+
+ ServerName [DOMAIN]
+ SSLEngine on
+ SSLCertificateFile /etc/letsencrypt/live/[DOMAIN]/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/[DOMAIN]/privkey.pem
+ ProxyPreserveHost On
+ ProxyPass / http://localhost:5000/
+ ProxyPassReverse / http://localhost:5000/
+ RewriteEngine on
+ RewriteCond %{SERVER_NAME} =[DOMAIN]
+ RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
+
+```
+
+Replace `[IP]` with your servers public IP address and `[DOMAIN]` with your
+domain name which you'd like to use for serving autodiscover.
+
+Then activate the proxy:
+
+```bash
+a2enmod rewrite proxy
+a2ensite autodiscover
+systemctl restart apache2
+```
+
+### Run as systemd service
+
+On a Debian Linux host you can run the `wan24-AutoDiscover` microservice using
+systemd:
+
+```bash
+dotnet wan24AutoDiscover.dll autodiscover systemd > /etc/systemd/system/autodiscover.service
+systemctl enable autodiscover
+systemctl start autodiscover
+systemctl status autodiscover
+```
+
+### Required DNS configuration
+
+In order to make autodiscover working in an email client, you'll need to
+create a SRV record for your email domain - example:
+
+```txt
+_autodiscover._tcp 1D IN SRV 0 0 443 [MTA-DOMAIN].
+```
+
+The domain `wan24.de` uses this record, for example:
+
+```txt
+_autodiscover._tcp 1D IN SRV 0 0 443 mail.wan24.de.
+```
+
+### POX request and response
+
+This is an example POX request to `/autodiscover/autodiscover.xml`:
+
+```xml
+
+
+ alias@wan24.de
+ https://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
+
+
+```
+
+The response with the demo `appsettings.json`:
+
+```xml
+
+
+
+ email
+ settings
+
+ IMAP
+ imap.wan24.de
+ 993
+ alias
+ off
+ on
+ on
+
+
+ SMTP
+ smtp.wan24.de
+ 587
+ alias
+ off
+ on
+ on
+
+
+
+
+```
+
+### CLI API
+
+The `wan24-AutoDiscover` has a small built in CLI API, which can do some
+things for you:
+
+#### Create a systemd service file
+
+```bash
+dotnet wan24AutoDiscover.dll autodiscover systemd > /etc/systemd/system/autodiscover.service
+```
diff --git a/src/wan24-AutoDiscover Docs/.gitignore b/src/wan24-AutoDiscover Docs/.gitignore
new file mode 100644
index 0000000..4378419
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/.gitignore
@@ -0,0 +1,9 @@
+###############
+# folder #
+###############
+/**/DROP/
+/**/TEMP/
+/**/packages/
+/**/bin/
+/**/obj/
+_site
diff --git a/src/wan24-AutoDiscover Docs/api/.gitignore b/src/wan24-AutoDiscover Docs/api/.gitignore
new file mode 100644
index 0000000..e8079a3
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/api/.gitignore
@@ -0,0 +1,5 @@
+###############
+# temp file #
+###############
+*.yml
+.manifest
diff --git a/src/wan24-AutoDiscover Docs/api/index.md b/src/wan24-AutoDiscover Docs/api/index.md
new file mode 100644
index 0000000..50e7266
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/api/index.md
@@ -0,0 +1,3 @@
+# API reference
+
+Choose a type from the left to start browsing.
diff --git a/src/wan24-AutoDiscover Docs/articles/intro.md b/src/wan24-AutoDiscover Docs/articles/intro.md
new file mode 100644
index 0000000..c0478ce
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/articles/intro.md
@@ -0,0 +1 @@
+# Add your introductions here!
diff --git a/src/wan24-AutoDiscover Docs/articles/toc.yml b/src/wan24-AutoDiscover Docs/articles/toc.yml
new file mode 100644
index 0000000..ff89ef1
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/articles/toc.yml
@@ -0,0 +1,2 @@
+- name: Introduction
+ href: intro.md
diff --git a/src/wan24-AutoDiscover Docs/docfx.json b/src/wan24-AutoDiscover Docs/docfx.json
new file mode 100644
index 0000000..0abcea9
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/docfx.json
@@ -0,0 +1,80 @@
+{
+ "metadata": [
+ {
+ "src": [
+ {
+ "files": [
+ "**.csproj"
+ ],
+ "exclude": [
+ "**/*Tests.csproj",
+ "**/*Demo.csproj"
+ ],
+ "src": ".."
+ }
+ ],
+ "dest": "api",
+ "disableGitFeatures": false,
+ "disableDefaultFilter": false
+ }
+ ],
+ "build": {
+ "content": [
+ {
+ "files": [
+ "api/**.yml",
+ "api/index.md"
+ ]
+ },
+ {
+ "files": [
+ "articles/**.md",
+ "articles/**/toc.yml",
+ "toc.yml",
+ "*.md"
+ ]
+ }
+ ],
+ "resource": [
+ {
+ "files": [
+ "images/**"
+ ]
+ }
+ ],
+ "overwrite": [
+ {
+ "files": [
+ ],
+ "exclude": [
+ "obj/**"
+ ]
+ }
+ ],
+ "dest": "../../docs/",
+ "globalMetadataFiles": [],
+ "globalMetadata": {
+ "_appTitle": "wan24-AutoDiscover",
+ "_appFooter": "(c) 2024 Andreas Zimmermann, wan24.de",
+ "_copyrightFooter": "(c) 2024 Andreas Zimmermann, wan24.de",
+ //"_appLogoPath": "custom/logo.png",
+ //"_appFaviconPath": "custom/favicon.ico",
+ "_enableSearch": true,
+ "_disableSideFilter": false,
+ "_enableNewTab": true,
+ "_disableContribution": false,
+ "_disableBreadcrumb": false,
+ },
+ "fileMetadataFiles": [],
+ "template": [
+ "default",
+ "templates/singulinkfx"
+ ],
+ "postProcessors": [],
+ "markdownEngineName": "markdig",
+ "noLangKeyword": false,
+ "keepFileLink": false,
+ "cleanupCacheHistory": false,
+ "disableGitFeatures": false
+ }
+}
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/layout/_master.tmpl b/src/wan24-AutoDiscover Docs/templates/singulinkfx/layout/_master.tmpl
new file mode 100644
index 0000000..b78b10d
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/layout/_master.tmpl
@@ -0,0 +1,74 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+{{!include(/^styles/.*/)}}
+{{!include(/^fonts/.*/)}}
+{{!include(favicon.ico)}}
+{{!include(logo.svg)}}
+{{!include(search-stopwords.json)}}
+
+
+
+ {{>partials/head}}
+
+
+
+
+
+
+
+
+
+ {{>partials/logo}}
+
+
+
+
+
+
+
+
+ {{#_enableSearch}}
+ {{>partials/searchResults}}
+ {{/_enableSearch}}
+
+
+
+
+ {{^_disableBreadcrumb}}
+ {{>partials/breadcrumb}}
+ {{/_disableBreadcrumb}}
+
+ {{^_disableContribution}}
+
+ {{/_disableContribution}}
+
+
+ {{!body}}
+
+
+
+ {{#_copyrightFooter}}
+
+ {{/_copyrightFooter}}
+
+
+
+ {{>partials/scripts}}
+
+
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/footer.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/footer.tmpl.partial
new file mode 100644
index 0000000..dd601a9
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/footer.tmpl.partial
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/head.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/head.tmpl.partial
new file mode 100644
index 0000000..2b0dd67
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/head.tmpl.partial
@@ -0,0 +1,23 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+
+
+ {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}
+
+
+
+ {{#_description}} {{/_description}}
+
+
+
+
+
+
+
+
+
+ {{#_noindex}} {{/_noindex}}
+ {{#_enableSearch}} {{/_enableSearch}}
+ {{#_enableNewTab}} {{/_enableNewTab}}
+
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/li.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/li.tmpl.partial
new file mode 100644
index 0000000..2c8a3d0
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/li.tmpl.partial
@@ -0,0 +1,31 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+ {{#items}}
+ {{^dropdown}}
+
+ {{^leaf}}
+
+ {{/leaf}}
+ {{#topicHref}}
+
+ {{/topicHref}}
+ {{^topicHref}}
+ {{{name}}}
+ {{/topicHref}}
+
+ {{^leaf}}
+ {{>partials/li}}
+ {{/leaf}}
+
+ {{/dropdown}}
+ {{#dropdown}}
+
+ {{name}}
+
+
+ {{/dropdown}}
+ {{/items}}
+
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/logo.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/logo.tmpl.partial
new file mode 100644
index 0000000..738ab5b
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/logo.tmpl.partial
@@ -0,0 +1,6 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+
+ {{_appName}}
+
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/namespace.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/namespace.tmpl.partial
new file mode 100644
index 0000000..42d64e6
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/namespace.tmpl.partial
@@ -0,0 +1,13 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+{{>partials/title}}
+{{{summary}}}
+{{{conceptual}}}
+
+{{#children}}
+ {{>partials/namespaceSubtitle}}
+ {{#children}}
+
+
+ {{/children}}
+{{/children}}
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/navbar.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/navbar.tmpl.partial
new file mode 100644
index 0000000..cfddfd8
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/navbar.tmpl.partial
@@ -0,0 +1,19 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+
+ {{>partials/logo}}
+
+
+ {{#_enableSearch}}
+
+
+
+ {{/_enableSearch}}
+
+
+
+
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/scripts.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/scripts.tmpl.partial
new file mode 100644
index 0000000..8ef7f26
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/scripts.tmpl.partial
@@ -0,0 +1,13 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/searchResults.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/searchResults.tmpl.partial
new file mode 100644
index 0000000..9f08c90
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/searchResults.tmpl.partial
@@ -0,0 +1,9 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+
{{__global.searchResults}}
+
+
+
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/toc.tmpl.partial b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/toc.tmpl.partial
new file mode 100644
index 0000000..c660966
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/partials/toc.tmpl.partial
@@ -0,0 +1,5 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/config.css b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/config.css
new file mode 100644
index 0000000..cb39b51
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/config.css
@@ -0,0 +1,124 @@
+/* Theme Configuration Options */
+
+:root
+{
+ /* General */
+
+ --base-font-size: 16px;
+ --smalldevice-base-font-size: 14px; /* Base font size for devices < 1024px */
+
+ --main-bg-color: #1f1f23;
+ --footer-bg-color: rgba(0,0,0,.4);
+ --separator-color: #42474f;
+
+ --table-strip-bg-color: #151515;
+ --table-header-bg-color: black;
+ --table-header-color: hsla(0,0%,100%,.8);
+ --table-header-border-color: #040405;
+
+ /* Text */
+
+ --appname-color: white;
+
+ --h1-color: white;
+ --h2-color: #f2f2f2;
+ --h3-color: #e3e3e3;
+ --h4-color: #ffffff;
+ --h5-color: #e0e0e0;
+
+ --text-color: #e1e1e1;
+ --link-color: #00b0f4;
+ --link-hover-color: #2ec4ff;
+
+ /* Mobile Topbar */
+
+ --topbar-bg-color: #18191c;
+
+ /* Button */
+
+ --button-color: #747f8d;
+
+ /* Sidebar */
+
+ --sidebar-width: 400px;
+ --sidebar-bg-color: #292B30;
+
+ --search-color: #bdbdbd;
+ --search-bg-color: #1b1e21;
+ --search-searchicon-color: #e3e3e3;
+ --search-border-color: black;
+
+ --sidebar-item-color: white;
+ --sidebar-active-item-color: #00b0f4;
+ --sidebar-level1-item-bg-color: #222429;
+ --sidebar-level1-item-hover-bg-color: #1D1F22;
+
+ --toc-filter-color: #bdbdbd;
+ --toc-filter-bg-color: #1b1e21;
+ --toc-filter-filtericon-color: #e3e3e3;
+ --toc-filter-clearicon-color: #e68585;
+ --toc-filter-border-color: black;
+
+ /* Scrollbars */
+
+ --scrollbar-bg-color: transparent;
+ --scrollbar-thumb-bg-color: rgba(0,0,0,.4);
+ --scrollbar-thumb-border-color: transparent;
+
+ /* Alerts and Blocks */
+
+ --alert-info-border-color: rgba(114,137,218,.5);
+ --alert-info-bg-color: rgba(114,137,218,.1);
+
+ --alert-warning-border-color: rgba(250,166,26,.5);
+ --alert-warning-bg-color: rgba(250,166,26,.1);
+
+ --alert-danger-border-color: rgba(240,71,71,.5);
+ --alert-danger-bg-color: rgba(240,71,71,.1);
+
+ --alert-tip-border-color: rgba(255,255,255,.5);
+ --alert-tip-bg-color: rgba(255,255,255,.1);
+
+ --blockquote-border-color: rgba(255,255,255,.5);
+ --blockquote-bg-color: rgba(255,255,255,.1);
+
+ --breadcrumb-bg-color: #2f3136;
+
+ /* Tabs */
+
+ --nav-tabs-border-width: 1px;
+ --nav-tabs-border-color: #495057;
+ --nav-tabs-border-radius: .375rem;
+ --nav-tabs-link-hover-border-color: #303336 #303336 transparent;
+ --nav-tabs-link-active-color: white;
+ --nav-tabs-link-active-bg: var(--main-bg-color);
+ --nav-tabs-link-active-border-color: var(--nav-tabs-border-color) var(--nav-tabs-border-color) var(--main-bg-color);
+
+ /* Inline Code */
+
+ --ref-bg-color: black;
+ --ref-color: #89d4f1;
+
+ /* Code Blocks */
+
+ --code-bg-color: #151515;
+ --code-color: #d6deeb;
+ --code-keyword-color: #569cd6;
+ --code-comment-color: #57a64a;
+ --code-macro-color: #beb7ff;
+ --code-string-color: #d69d85;
+ --code-string-escape-color: #ffd68f;
+ --code-field-color: #c8c8c8;
+ --code-function-color: #dcdcaa;
+ --code-control-color: #d8a0df;
+ --code-class-color: #4ec9b0;
+ --code-number-color: #b5cea8;
+ --code-params-color: #9a9a9a;
+ --code-breakpoint-color: #8c2f2f;
+}
+
+/* Code Block Overrides */
+
+pre, legend {
+ --scrollbar-thumb-bg-color: #333;
+}
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/down-arrow.svg b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/down-arrow.svg
new file mode 100644
index 0000000..e086126
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/down-arrow.svg
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/jquery.twbsPagination.js b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/jquery.twbsPagination.js
new file mode 100644
index 0000000..332c01c
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/jquery.twbsPagination.js
@@ -0,0 +1,317 @@
+/*!
+ * jQuery pagination plugin v1.4.1
+ * http://esimakin.github.io/twbs-pagination/
+ *
+ * Copyright 2014-2016, Eugene Simakin
+ * Released under Apache 2.0 license
+ * http://apache.org/licenses/LICENSE-2.0.html
+ */
+(function ($, window, document, undefined) {
+
+ 'use strict';
+
+ var old = $.fn.twbsPagination;
+
+ // PROTOTYPE AND CONSTRUCTOR
+
+ var TwbsPagination = function (element, options) {
+ this.$element = $(element);
+ this.options = $.extend({}, $.fn.twbsPagination.defaults, options);
+
+ if (this.options.startPage < 1 || this.options.startPage > this.options.totalPages) {
+ throw new Error('Start page option is incorrect');
+ }
+
+ this.options.totalPages = parseInt(this.options.totalPages);
+ if (isNaN(this.options.totalPages)) {
+ throw new Error('Total pages option is not correct!');
+ }
+
+ this.options.visiblePages = parseInt(this.options.visiblePages);
+ if (isNaN(this.options.visiblePages)) {
+ throw new Error('Visible pages option is not correct!');
+ }
+
+ if (this.options.onPageClick instanceof Function) {
+ this.$element.first().on('page', this.options.onPageClick);
+ }
+
+ // hide if only one page exists
+ if (this.options.hideOnlyOnePage && this.options.totalPages == 1) {
+ this.$element.trigger('page', 1);
+ return this;
+ }
+
+ if (this.options.totalPages < this.options.visiblePages) {
+ this.options.visiblePages = this.options.totalPages;
+ }
+
+ if (this.options.href) {
+ this.options.startPage = this.getPageFromQueryString();
+ if (!this.options.startPage) {
+ this.options.startPage = 1;
+ }
+ }
+
+ var tagName = (typeof this.$element.prop === 'function') ?
+ this.$element.prop('tagName') : this.$element.attr('tagName');
+
+ if (tagName === 'UL') {
+ this.$listContainer = this.$element;
+ } else {
+ this.$listContainer = $('');
+ }
+
+ this.$listContainer.addClass(this.options.paginationClass);
+
+ if (tagName !== 'UL') {
+ this.$element.append(this.$listContainer);
+ }
+
+ if (this.options.initiateStartPageClick) {
+ this.show(this.options.startPage);
+ } else {
+ this.render(this.getPages(this.options.startPage));
+ this.setupEvents();
+ }
+
+ return this;
+ };
+
+ TwbsPagination.prototype = {
+
+ constructor: TwbsPagination,
+
+ destroy: function () {
+ this.$element.empty();
+ this.$element.removeData('twbs-pagination');
+ this.$element.off('page');
+
+ return this;
+ },
+
+ show: function (page) {
+ if (page < 1 || page > this.options.totalPages) {
+ throw new Error('Page is incorrect.');
+ }
+ this.currentPage = page;
+
+ this.render(this.getPages(page));
+ this.setupEvents();
+
+ this.$element.trigger('page', page);
+
+ return this;
+ },
+
+ buildListItems: function (pages) {
+ var listItems = [];
+
+ if (this.options.first) {
+ listItems.push(this.buildItem('first', 1));
+ }
+
+ if (this.options.prev) {
+ var prev = pages.currentPage > 1 ? pages.currentPage - 1 : this.options.loop ? this.options.totalPages : 1;
+ listItems.push(this.buildItem('prev', prev));
+ }
+
+ for (var i = 0; i < pages.numeric.length; i++) {
+ listItems.push(this.buildItem('page', pages.numeric[i]));
+ }
+
+ if (this.options.next) {
+ var next = pages.currentPage < this.options.totalPages ? pages.currentPage + 1 : this.options.loop ? 1 : this.options.totalPages;
+ listItems.push(this.buildItem('next', next));
+ }
+
+ if (this.options.last) {
+ listItems.push(this.buildItem('last', this.options.totalPages));
+ }
+
+ return listItems;
+ },
+
+ buildItem: function (type, page) {
+ var $itemContainer = $(' '),
+ $itemContent = $(' '),
+ itemText = this.options[type] ? this.makeText(this.options[type], page) : page;
+
+ $itemContainer.addClass(this.options[type + 'Class']);
+ $itemContainer.data('page', page);
+ $itemContainer.data('page-type', type);
+ $itemContainer.append($itemContent.attr('href', this.makeHref(page)).addClass(this.options.anchorClass).html(itemText));
+
+ return $itemContainer;
+ },
+
+ getPages: function (currentPage) {
+ var pages = [];
+
+ var half = Math.floor(this.options.visiblePages / 2);
+ var start = currentPage - half + 1 - this.options.visiblePages % 2;
+ var end = currentPage + half;
+
+ // handle boundary case
+ if (start <= 0) {
+ start = 1;
+ end = this.options.visiblePages;
+ }
+ if (end > this.options.totalPages) {
+ start = this.options.totalPages - this.options.visiblePages + 1;
+ end = this.options.totalPages;
+ }
+
+ var itPage = start;
+ while (itPage <= end) {
+ pages.push(itPage);
+ itPage++;
+ }
+
+ return {"currentPage": currentPage, "numeric": pages};
+ },
+
+ render: function (pages) {
+ var _this = this;
+ this.$listContainer.children().remove();
+ var items = this.buildListItems(pages);
+ jQuery.each(items, function(key, item){
+ _this.$listContainer.append(item);
+ });
+
+ this.$listContainer.children().each(function () {
+ var $this = $(this),
+ pageType = $this.data('page-type');
+
+ switch (pageType) {
+ case 'page':
+ if ($this.data('page') === pages.currentPage) {
+ $this.addClass(_this.options.activeClass);
+ }
+ break;
+ case 'first':
+ $this.toggleClass(_this.options.disabledClass, pages.currentPage === 1);
+ break;
+ case 'last':
+ $this.toggleClass(_this.options.disabledClass, pages.currentPage === _this.options.totalPages);
+ break;
+ case 'prev':
+ $this.toggleClass(_this.options.disabledClass, !_this.options.loop && pages.currentPage === 1);
+ break;
+ case 'next':
+ $this.toggleClass(_this.options.disabledClass,
+ !_this.options.loop && pages.currentPage === _this.options.totalPages);
+ break;
+ default:
+ break;
+ }
+
+ });
+ },
+
+ setupEvents: function () {
+ var _this = this;
+ this.$listContainer.off('click').on('click', 'li', function (evt) {
+ var $this = $(this);
+ if ($this.hasClass(_this.options.disabledClass) || $this.hasClass(_this.options.activeClass)) {
+ return false;
+ }
+ // Prevent click event if href is not set.
+ !_this.options.href && evt.preventDefault();
+ _this.show(parseInt($this.data('page')));
+ });
+ },
+
+ makeHref: function (page) {
+ return this.options.href ? this.generateQueryString(page) : "#";
+ },
+
+ makeText: function (text, page) {
+ return text.replace(this.options.pageVariable, page)
+ .replace(this.options.totalPagesVariable, this.options.totalPages)
+ },
+ getPageFromQueryString: function (searchStr) {
+ var search = this.getSearchString(searchStr),
+ regex = new RegExp(this.options.pageVariable + '(=([^]*)|&|#|$)'),
+ page = regex.exec(search);
+ if (!page || !page[2]) {
+ return null;
+ }
+ page = decodeURIComponent(page[2]);
+ page = parseInt(page);
+ if (isNaN(page)) {
+ return null;
+ }
+ return page;
+ },
+ generateQueryString: function (pageNumber, searchStr) {
+ var search = this.getSearchString(searchStr),
+ regex = new RegExp(this.options.pageVariable + '=*[^]*');
+ if (!search) return '';
+ return '?' + search.replace(regex, this.options.pageVariable + '=' + pageNumber);
+
+ },
+ getSearchString: function (searchStr) {
+ var search = searchStr || window.location.search;
+ if (search === '') {
+ return null;
+ }
+ if (search.indexOf('?') === 0) search = search.substr(1);
+ return search;
+ }
+
+ };
+
+ // PLUGIN DEFINITION
+
+ $.fn.twbsPagination = function (option) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var methodReturn;
+
+ var $this = $(this);
+ var data = $this.data('twbs-pagination');
+ var options = typeof option === 'object' ? option : {};
+
+ if (!data) $this.data('twbs-pagination', (data = new TwbsPagination(this, options) ));
+ if (typeof option === 'string') methodReturn = data[ option ].apply(data, args);
+
+ return ( methodReturn === undefined ) ? $this : methodReturn;
+ };
+
+ $.fn.twbsPagination.defaults = {
+ totalPages: 1,
+ startPage: 1,
+ visiblePages: 5,
+ initiateStartPageClick: true,
+ hideOnlyOnePage: false,
+ href: false,
+ pageVariable: '{{page}}',
+ totalPagesVariable: '{{total_pages}}',
+ page: null,
+ first: 'First',
+ prev: 'Previous',
+ next: 'Next',
+ last: 'Last',
+ loop: false,
+ onPageClick: null,
+ paginationClass: 'pagination',
+ nextClass: 'page-item next',
+ prevClass: 'page-item prev',
+ lastClass: 'page-item last',
+ firstClass: 'page-item first',
+ pageClass: 'page-item',
+ activeClass: 'active',
+ disabledClass: 'disabled',
+ anchorClass: 'page-link'
+ };
+
+ $.fn.twbsPagination.Constructor = TwbsPagination;
+
+ $.fn.twbsPagination.noConflict = function () {
+ $.fn.twbsPagination = old;
+ return this;
+ };
+
+ $.fn.twbsPagination.version = "1.4.1";
+
+})(window.jQuery, window, document);
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/jquery.twbsPagination.min.js b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/jquery.twbsPagination.min.js
new file mode 100644
index 0000000..a9e11c6
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/jquery.twbsPagination.min.js
@@ -0,0 +1,8 @@
+/*!
+ * jQuery pagination plugin v1.4.1
+ * http://esimakin.github.io/twbs-pagination/
+ *
+ * Copyright 2014-2016, Eugene Simakin
+ * Released under Apache 2.0 license
+ * http://apache.org/licenses/LICENSE-2.0.html
+ */ !function(t,s,i,e){"use strict";var a=t.fn.twbsPagination,o=function(s,i){if(this.$element=t(s),this.options=t.extend({},t.fn.twbsPagination.defaults,i),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw Error("Visible pages option is not correct!");if(this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.$element.trigger("page",1),this;this.options.totalPages"),this.$listContainer.addClass(this.options.paginationClass),"UL"!==e&&this.$element.append(this.$listContainer),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};o.prototype={constructor:o,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(t){if(t<1||t>this.options.totalPages)throw Error("Page is incorrect.");return this.currentPage=t,this.render(this.getPages(t)),this.setupEvents(),this.$element.trigger("page",t),this},buildListItems:function(t){var s=[];if(this.options.first&&s.push(this.buildItem("first",1)),this.options.prev){var i=t.currentPage>1?t.currentPage-1:this.options.loop?this.options.totalPages:1;s.push(this.buildItem("prev",i))}for(var e=0;e"),a=t(" "),o=this.options[s]?this.makeText(this.options[s],i):i;return e.addClass(this.options[s+"Class"]),e.data("page",i),e.data("page-type",s),e.append(a.attr("href",this.makeHref(i)).addClass(this.options.anchorClass).html(o)),e},getPages:function(t){var s=[],i=Math.floor(this.options.visiblePages/2),e=t-i+1-this.options.visiblePages%2,a=t+i;e<=0&&(e=1,a=this.options.visiblePages),a>this.options.totalPages&&(e=this.options.totalPages-this.options.visiblePages+1,a=this.options.totalPages);for(var o=e;o<=a;)s.push(o),o++;return{currentPage:t,numeric:s}},render:function(s){var i=this;this.$listContainer.children().remove();var e=this.buildListItems(s);jQuery.each(e,function(t,s){i.$listContainer.append(s)}),this.$listContainer.children().each(function(){var e=t(this),a=e.data("page-type");switch(a){case"page":e.data("page")===s.currentPage&&e.addClass(i.options.activeClass);break;case"first":e.toggleClass(i.options.disabledClass,1===s.currentPage);break;case"last":e.toggleClass(i.options.disabledClass,s.currentPage===i.options.totalPages);break;case"prev":e.toggleClass(i.options.disabledClass,!i.options.loop&&1===s.currentPage);break;case"next":e.toggleClass(i.options.disabledClass,!i.options.loop&&s.currentPage===i.options.totalPages)}})},setupEvents:function(){var s=this;this.$listContainer.off("click").on("click","li",function(i){var e=t(this);if(e.hasClass(s.options.disabledClass)||e.hasClass(s.options.activeClass))return!1;s.options.href||i.preventDefault(),s.show(parseInt(e.data("page")))})},makeHref:function(t){return this.options.href?this.generateQueryString(t):"#"},makeText:function(t,s){return t.replace(this.options.pageVariable,s).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(t){var s=this.getSearchString(t),i=RegExp(this.options.pageVariable+"(=([^]*)|&|#|$)").exec(s);return i&&i[2]?(i=parseInt(i=decodeURIComponent(i[2])),isNaN(i))?null:i:null},generateQueryString:function(t,s){var i=this.getSearchString(s),e=RegExp(this.options.pageVariable+"=*[^]*");return i?"?"+i.replace(e,this.options.pageVariable+"="+t):""},getSearchString:function(t){var i=t||s.location.search;return""===i?null:(0===i.indexOf("?")&&(i=i.substr(1)),i)}},t.fn.twbsPagination=function(s){var i,e=Array.prototype.slice.call(arguments,1),a=t(this),n=a.data("twbs-pagination");return n||a.data("twbs-pagination",n=new o(this,"object"==typeof s?s:{})),"string"==typeof s&&(i=n[s].apply(n,e)),void 0===i?a:i},t.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},t.fn.twbsPagination.Constructor=o,t.fn.twbsPagination.noConflict=function(){return t.fn.twbsPagination=a,this},t.fn.twbsPagination.version="1.4.1"}(window.jQuery,window,document);
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/main.css b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/main.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/main.js b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/main.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/singulink.css b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/singulink.css
new file mode 100644
index 0000000..1a23c1e
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/singulink.css
@@ -0,0 +1,1046 @@
+@keyframes showThat {
+ 0% {
+ opacity: 0;
+ visibility: hidden
+ }
+
+ 1% {
+ opacity: 0;
+ visibility: visible
+ }
+
+ to {
+ opacity: 1;
+ visibility: visible
+ }
+}
+
+@keyframes hideThat {
+ 0% {
+ opacity: 1;
+ visibility: visible
+ }
+
+ 99% {
+ opacity: 0;
+ visibility: visible
+ }
+
+ to {
+ opacity: 0;
+ visibility: hidden
+ }
+}
+
+::-webkit-scrollbar {
+ width: 10px
+}
+
+::-webkit-scrollbar-track {
+ background: var(--scrollbar-bg-color)
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb-bg-color);
+ border-color: var(--scrollbar-thumb-border-color);
+ border-radius: 5px
+}
+
+::marker {
+ unicode-bidi: isolate;
+ font-variant-numeric: tabular-nums;
+ text-transform: none;
+ text-indent: 0!important;
+ text-align: start!important;
+ text-align-last: start!important
+}
+
+*,:after,:before {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box
+}
+
+body,html {
+ padding: 0;
+ margin: 0;
+ font: 15px/150%"Roboto",sans-serif;
+ color: var(--text-color);
+ background-color: var(--main-bg-color)
+}
+
+img {
+ max-width: 100%
+}
+
+ol>li,ul>li {
+ display: list-item
+}
+
+h1 {
+ color: var(--link-active-color)
+}
+
+h1,h2,h3,h4,h5 {
+ position: relative
+}
+
+h1,h2 {
+ margin-block-start: 2em
+}
+
+h3 {
+ color: var(--h3-color)
+}
+
+h4 {
+ opacity: 1;
+ color: var(--h4-color);
+ border-bottom: 2px solid var(--separator-color);
+ margin: 20px 0 0
+}
+
+h5 {
+ margin-block-end: .8em;
+ margin-block-start: 1em;
+ font-weight: 500;
+ color: var(--h5-color)
+}
+
+h6 {
+ font-size: .75em;
+ margin: 0
+}
+
+p {
+ font-weight: 400
+}
+
+ul {
+ position: relative
+}
+
+ol,ul {
+ padding-inline-start: 3em
+}
+
+ul.level1 {
+ list-style-type: none;
+ padding-inline-start: 0
+}
+
+ul.level2,ul.level3 {
+ list-style-type: none;
+ font-size: .9em
+}
+
+a {
+ color: var(--link-color);
+ text-decoration: none;
+ transition: color .25s
+}
+
+a:focus,a:hover {
+ color: var(--link-hover-color);
+ text-decoration: underline
+}
+
+a.anchorjs-link:hover {
+ text-decoration: none
+}
+
+a.active,a:active {
+ color: var(--link-active-color)
+}
+
+.body-content {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: hidden
+}
+
+.footer>h4,.page-title {
+ margin-block-start: 0
+}
+
+nav {
+ transition: left .5s ease-out;
+ position: fixed;
+ left: -350px;
+ top: 40px;
+ bottom: 0;
+ background-color: var(--sidebar-bg-color);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ z-index: 1000
+}
+
+h1:first-child {
+ margin-top: 1.1em
+}
+
+.sidebar {
+ flex: 1
+}
+
+.sidebar-item {
+ font-size: 1em;
+ font-weight: 400;
+ display: block;
+ padding: 4px 16px;
+ color: var(--sidebar-item-color)
+}
+
+#navbar .sidebar-item,.sidebar-item.large {
+ padding: 8px 16px
+}
+
+a.sidebar-item:focus,a.sidebar-item:hover {
+ color: var(--link-active-color);
+ text-decoration: none
+}
+
+a.sidebar-item.active {
+ color: var(--link-active-color)
+}
+
+ul.level1>li>a.sidebar-item {
+ background-color: transparent;
+ border-radius: 4px
+}
+
+#toc ul.level1>li>a.sidebar-item.active {
+ background-color: var(--link-active-bg-color)
+}
+
+.sidebar-item-separator {
+ height: 2px;
+ width: 100%;
+ background-color: var(--separator-color);
+ opacity: .8
+}
+
+span.sidebar-item {
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: .8em;
+ color: var(--text-color);
+ margin-block-start: 1.25em
+}
+
+.main-panel {
+ background-color: var(--main-bg-color);
+ flex: 1;
+ overflow-x: hidden;
+}
+
+.top-navbar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background-color: var(--topbar-bg-color)
+}
+
+.burger-icon {
+ color: var(--button-color)
+}
+
+.burger-icon:focus,.burger-icon:hover {
+ color: var(--link-active-color)
+}
+
+.burger-icon.active,.burger-icon:active {
+ color: var(--link-active-color)
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ justify-content: start
+}
+
+.logomark {
+ height: 28px
+}
+
+.brand-title {
+ padding: 0 .5em;
+ font-size: .9em;
+ color: var(--link-active-color)
+}
+
+.blackout,.footer {
+ background-color: var(--footer-bg-color)
+}
+
+.footer {
+ margin: 0 20px 20px;
+ border-radius: 8px
+}
+
+.blackout {
+ display: block;
+ visibility: hidden;
+ position: absolute;
+ z-index: 100;
+ bottom: 0;
+ left: 0;
+ right: 0
+}
+
+.showThat {
+ animation: showThat .5s forwards
+}
+
+.hideThat {
+ animation: hideThat .5s forwards
+}
+
+@media (min-width:1024px) {
+ nav {
+ position: fixed;
+ left: 0!important;
+ top: 0;
+ bottom: 0
+ }
+
+ .blackout,.top-navbar {
+ display: none
+ }
+}
+
+.table-responsive {
+ overflow-x: auto
+}
+
+table {
+ background-color: var(--code-bg-color);
+ border-collapse: collapse;
+ width: 100%;
+ table-layout: auto
+}
+
+table.table-striped tbody tr:nth-child(2n) {
+ background-color: var(--table-strip-bg-color)
+}
+
+table thead {
+ background: var(--table-header-bg-color)
+}
+
+table th {
+ color: var(--table-header-color);
+ text-transform: uppercase;
+ line-height: 15px;
+ border-bottom: 1px solid var(--table-header-border-color);
+ font-size: 14px;
+ padding: 9px 10px
+}
+
+.table-condensed th {
+ text-align: left
+}
+
+table td {
+ padding: 6px 10px
+}
+
+.mainContainer[dir=rtl] main ul[role=tablist],table td>p {
+ margin: 0
+}
+
+.alert,blockquote {
+ border-radius: 4px;
+ padding: 8px;
+ margin: 25px 0
+}
+
+.alert>h5 {
+ display: none;
+ margin: 0
+}
+
+.alert>p,blockquote>p {
+ margin: 0;
+ font-size: 13px
+}
+
+.alert>p,table td {
+ font-weight: 300
+}
+
+.alert.alert-info {
+ border: 2px solid var(--alert-info-border-color);
+ background: var(--alert-info-bg-color)
+}
+
+.alert.alert-warning {
+ border: 2px solid var(--alert-warning-border-color);
+ background: var(--alert-warning-bg-color)
+}
+
+.alert.alert-danger {
+ border: 2px solid var(--alert-danger-border-color);
+ background: var(--alert-danger-bg-color)
+}
+
+.TIP.alert.alert-info {
+ border: 2px solid var(--alert-tip-border-color);
+ background: var(--alert-tip-bg-color)
+}
+
+blockquote {
+ margin: 8px 0;
+ border-left: 4px solid var(--blockquote-border-color);
+ background: var(--blockquote-bg-color)
+}
+
+blockquote>p {
+ font-style: italic
+}
+
+#breadcrumb {
+ padding: 8px 16px;
+ background: var(--breadcrumb-bg-color);
+ border-radius: 4px;
+ overflow: scroll;
+ margin-bottom: 0
+}
+
+#breadcrumb:empty {
+ display: none
+}
+
+ul.breadcrumb {
+ display: flex;
+ flex-direction: row;
+ margin: 0
+}
+
+ul.breadcrumb>li {
+ margin-right: 6px
+}
+
+ul.breadcrumb>li::before {
+ content: "/";
+ margin-right: 5px
+}
+
+ul.breadcrumb>li:first-child::before {
+ content: "";
+ margin: 0
+}
+
+legend,pre {
+ display: block;
+ background-color: var(--code-bg-color);
+ border-radius: 4px;
+ padding: 6px
+}
+
+.hljs {
+ background: 0 0
+}
+
+.small {
+ font-size: .9em
+}
+
+.pull-right {
+ float: right
+}
+
+#breadcrumb wbr,.hide,.sidetoc li>ul,ul.level1>li>.expand-stub {
+ display: none
+}
+
+@media (max-width:1023.98px) {
+ .mobile-hide {
+ display: none
+ }
+}
+
+li {
+ position: relative
+}
+
+.expand-stub {
+ cursor: pointer;
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ left: -10px
+}
+
+.toc .nav>li.active>.expand-stub::before,.toc .nav>li>.expand-stub::before {
+ content: " ";
+ position: absolute;
+ transform: rotate(-90deg);
+ background-repeat: no-repeat;
+ background: url(down-arrow.svg)
+}
+
+.toc .nav>li.active>.expand-stub::before,.toc .nav>li.filtered>.expand-stub::before,.toc .nav>li.in.active>.expand-stub::before,.toc .nav>li.in>.expand-stub::before {
+ transform: none
+}
+
+li,li.in>ul {
+ display: block
+}
+
+ul.level2>li>a.sidebar-item {
+ font-size: .95em;
+ padding: 0
+}
+
+ul.level3>li>a.sidebar-item {
+ padding: 0
+}
+
+ul.level2>li>a.sidebar-item:focus,ul.level2>li>a.sidebar-item:hover,ul.level3>li>a.sidebar-item:focus,ul.level3>li>a.sidebar-item:hover {
+ color: var(--link-active-color);
+ text-decoration: underline
+}
+
+ul.level2>li>a.sidebar-item.active,ul.level3>li>a.sidebar-item.active {
+ color: var(--link-active-color)
+}
+
+.inheritance .level0:before,.inheritance .level1:before,.inheritance .level2:before,.inheritance .level3:before,.inheritance .level4:before,.inheritance .level5:before {
+ content: "↳";
+ margin-right: 5px
+}
+
+.inheritance .level0 {
+ margin-left: 0
+}
+
+.inheritance .level1 {
+ margin-left: 1em
+}
+
+.inheritance .level2 {
+ margin-left: 2em
+}
+
+.inheritance .level3 {
+ margin-left: 3em
+}
+
+.inheritance .level4 {
+ margin-left: 4em
+}
+
+.inheritance .level5 {
+ margin-left: 5em
+}
+
+body {
+ font-size: var(--base-font-size)
+}
+
+@media (max-width:1024px) {
+ body {
+ font-size: var(--smalldevice-base-font-size)
+ }
+}
+
+h1,h2,h3,h4,h5 {
+ line-height: initial
+}
+
+h1,h1:first-child {
+ font-size: 2.25em;
+ letter-spacing: .5px;
+ color: var(--h1-color);
+ margin-block-start: 1em;
+ margin-block-end: -.05em
+}
+
+.article h1 {
+ margin-block-end: -.2em
+}
+
+h2 {
+ font-size: 2.1em;
+ color: var(--h2-color)
+}
+
+.article h2 {
+ margin-block-start: 1.3em;
+ padding-bottom: 6px;
+ border-bottom: 1px solid var(--separator-color)
+}
+
+.article h3,h3 {
+ font-size: 1.95em;
+ font-weight: 500;
+ margin-block-start: 1.7em
+}
+
+.article h3 {
+ font-size: 1.85em;
+ margin-block-start: 1.2em;
+ margin-block-end: .9em
+}
+
+h4 {
+ font-size: 1.8em;
+ font-weight: 400;
+ margin-block-start: 2em;
+ padding-bottom: 10px
+}
+
+.article h4 {
+ font-size: 1.5em;
+ font-weight: 300;
+ margin-block-start: 1em;
+ margin-block-end: 1em;
+ padding-bottom: 0;
+ border-bottom: none
+}
+
+h5 {
+ font-size: 1.1em
+}
+
+.article h5 {
+ font-size: 1.13em;
+ font-weight: 400;
+ text-decoration: underline;
+ margin-block-start: 1.5em;
+ margin-block-end: 1em
+}
+
+a.brand:hover {
+ text-decoration: none
+}
+
+a.brand .brand-title {
+ font-size: 1.4em;
+ font-weight: 500;
+ letter-spacing: .5px;
+ color: var(--appname-color);
+ margin-top: 1px;
+ padding: 0 0 0 .4em
+}
+
+@media (min-width:1024px) {
+ a.brand .brand-title {
+ font-size: 1.55em
+ }
+}
+
+a.brand .logomark {
+ height: 35px
+}
+
+.top-navbar {
+ height: 60px;
+ padding: 0 0 0 10px
+}
+
+.burger-icon {
+ margin-right: 10px
+}
+
+.sidebar {
+ padding: 25px 17px 32px
+}
+
+.blackout {
+ top: 60px
+}
+
+@media (max-width:1023.98px) {
+ .navbar-nav {
+ margin-top: 0
+ }
+}
+
+nav {
+ width: 94%;
+ max-width: var(--sidebar-width);
+ left: calc(var(--sidebar-width)*-1)
+}
+
+@media (max-width:1023.98px) {
+ nav {
+ top: 60px
+ }
+}
+
+nav ul {
+ list-style-type: none
+}
+
+nav .nav a,nav .nav a:hover {
+ text-decoration: none;
+ cursor: pointer;
+ display: block
+}
+
+nav a.sidebar-item {
+ padding: 4px 0 4px 10px;
+ cursor: pointer
+}
+
+.footer a:focus,.footer a:hover,nav a.sidebar-item:focus,nav a.sidebar-item:hover,nav a:focus {
+ text-decoration: underline
+}
+
+nav a,nav a:focus,nav a:hover {
+ color: var(--sidebar-item-color)!important
+}
+
+nav a.active,nav a.active:focus,nav a.active:hover {
+ color: var(--sidebar-active-item-color)!important
+}
+
+.sidebar-item-separator {
+ margin: 20px 0
+}
+
+#toc ul li a {
+ padding: 0 0 0 10px
+}
+
+.search {
+ background: var(--search-bg-color);
+ border: 1px solid var(--search-border-color);
+ border-radius: 5px;
+ position: relative;
+ margin-block-start: 25px
+}
+
+@media (max-width:1023.98px) {
+ .search {
+ margin-block-start: 0;
+ margin-block-end: 15px
+ }
+}
+
+.search>input {
+ font-size: .95em;
+ color: var(--search-color);
+ border: 0;
+ background: 0 0;
+ padding: 11px 30px 10px 37px;
+ width: 100%
+}
+
+.search>input:focus,.toc-filter>input:focus {
+ outline: 0
+}
+
+.search>.search-icon,.toc-filter>.filter-icon {
+ font-size: 1.2em;
+ color: var(--search-searchicon-color);
+ position: absolute;
+ top: 9px;
+ left: 9px
+}
+
+.toc-filter {
+ background: var(--toc-filter-bg-color);
+ border: 1px solid var(--toc-filter-border-color);
+ border-radius: 5px;
+ position: relative
+}
+
+.toc-filter>input {
+ font-size: .95em;
+ color: var(--toc-filter-color);
+ border: 0;
+ background: 0 0;
+ padding: 11px 30px 10px 37px;
+ width: 100%
+}
+
+.toc-filter>.filter-icon {
+ color: var(--toc-filter-filtericon-color)
+}
+
+.toc-filter>.clear-icon {
+ color: var(--toc-filter-clearicon-color);
+ position: absolute;
+ top: 9px;
+ right: 9px;
+ cursor: pointer
+}
+
+.toc .nav>li.active>.expand-stub::before,.toc .nav>li>.expand-stub::before {
+ width: 8px;
+ height: 8px;
+ top: 6px;
+ left: 6px
+}
+
+#toc ul.level2 {
+ margin-bottom: 20px
+}
+
+#toc ul.level1>li>a {
+ font-weight: 500;
+ margin-bottom: 10px;
+ padding: 5px 10px
+}
+
+#toc ul.level1>li>a,#toc ul.level1>li>a.active {
+ background-color: var(--sidebar-level1-item-bg-color)!important;
+ border-radius: 2px
+}
+
+#toc ul.level1>li>a.active:focus,#toc ul.level1>li>a.active:hover,#toc ul.level1>li>a:focus,#toc ul.level1>li>a:hover {
+ background-color: var(--sidebar-level1-item-hover-bg-color)!important;
+ text-decoration: none
+}
+
+ul.level2 {
+ padding-inline-start: .7em
+}
+
+ul.level2 .expand-stub {
+ top: 1px
+}
+
+ul.level2>li>a,ul.level2>li>a.sidebar-item {
+ font-weight: 400;
+ color: var(--sidebar-item-color);
+ margin: 4px 0
+}
+
+ul.level3 {
+ padding-inline-start: 1em
+}
+
+ul.level3>li>a,ul.level3>li>a.sidebar-item,ul.level4>li>a,ul.level4>li>a.sidebar-item {
+ font-size: 1.05em;
+ color: var(--sidebar-item-color);
+ margin: 4px 0
+}
+
+ul.level4 {
+ padding-inline-start: 0;
+ margin-bottom: 12px
+}
+
+ul.level4>li>a,ul.level4>li>a.sidebar-item {
+ margin: 5px 0 5px 10px
+}
+
+.subnav.navbar {
+ margin: 0-15px
+}
+
+#breadcrumb::-webkit-scrollbar {
+ display: none
+}
+
+#breadcrumb a {
+ white-space: nowrap
+}
+
+#search-results h1 {
+ margin-block-start: .5em
+}
+
+#search-results .item-title {
+ font-size: 1.3em;
+ margin-top: 1.5em
+}
+
+#search-results .item-href {
+ font-size: .85em
+}
+
+#search-results .item-brief {
+ margin-top: .7em
+}
+
+#search-results ul.pagination {
+ text-align: center;
+ padding: 10px 0 0;
+ margin-block-start: 40px;
+ border-top: 1px solid var(--separator-color)
+}
+
+#search-results ul.pagination>li {
+ display: inline-block;
+ margin: 0 10px
+}
+
+#search-results ul.pagination>li.disabled a,#search-results ul.pagination>li.disabled a:hover {
+ color: var(--text-color);
+ cursor: txt;
+ text-decoration: none
+}
+
+.main-panel {
+ margin-bottom: 60px;
+ padding: 20px
+}
+
+@media (min-width:1024px) {
+ .main-panel {
+ margin-bottom: 0;
+ margin-left: var(--sidebar-width);
+ padding: 20px 40px
+ }
+}
+
+.pull-right {
+ margin-top: 70px;
+ position: relative;
+ z-index: 1
+}
+
+.divider {
+ margin-left: 4px
+}
+
+article ol li,article ul li {
+ margin-bottom: 10px
+}
+
+.hljs {
+ color: var(--code-color)
+}
+
+.hljs::-webkit-scrollbar {
+ height: 6px
+}
+
+.hljs-built_in,.hljs-keyword,.hljs-title {
+ font-style: normal
+}
+
+code,p .xref {
+ background-color: var(--ref-bg-color);
+ color: var(--ref-color);
+ padding: 2px 3px;
+ font-size: .95em;
+ border-radius: 6px
+}
+
+code,p .xref,span.parametername {
+ font-family: monospace
+}
+
+.table {
+ width: auto
+}
+
+.table-responsive {
+ margin-bottom: 0
+}
+
+table td p {
+ font-weight: 300
+}
+
+.footer {
+ text-align: center;
+ color: var(--text-color);
+ padding: 10px
+}
+
+.copyright-footer {
+ font-size: .85em;
+ font-weight: 700;
+ text-align: center;
+ margin-block-start: 30px
+}
+
+#contribution {
+ float: right;
+ margin-top: 1.1em;
+ position: relative;
+ z-index: 999
+}
+
+.tabGroup {
+ margin-top: 1rem;
+ margin-bottom: 1rem
+}
+
+.tabGroup ul[role=tablist] {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ border-bottom: var(--nav-tabs-border-width) solid var(--nav-tabs-border-color)
+}
+
+.tabGroup ul[role=tablist]>li {
+ list-style: none;
+ display: inline-block;
+ margin-bottom: 0
+}
+
+.tabGroup ul[role=tablist] .dropdown-menu {
+ margin-top: calc(-1*var(--nav-tabs-border-width));
+ border-top-left-radius: 0;
+ border-top-right-radius: 0
+}
+
+.tabGroup a[role=tab] {
+ display: block;
+ text-decoration: none;
+ padding: .5rem 1rem;
+ margin-bottom: calc(-1*var(--nav-tabs-border-width));
+ border: var(--nav-tabs-border-width) solid transparent;
+ border-top-left-radius: var(--nav-tabs-border-radius);
+ border-top-right-radius: var(--nav-tabs-border-radius)
+}
+
+.tabGroup a[role=tab]:focus,.tabGroup a[role=tab]:hover {
+ isolation: isolate;
+ border-color: var(--nav-tabs-link-hover-border-color)
+}
+
+.tabGroup a[role=tab][aria-selected=true] {
+ color: var(--nav-tabs-link-active-color);
+ background-color: var(--nav-tabs-link-active-bg);
+ border-color: var(--nav-tabs-link-active-border-color)
+}
+
+.tabGroup a[role=tab]:disabled {
+ color: var(--nav-link-disabled-color);
+ background-color: transparent;
+ border-color: transparent
+}
+
+.tabGroup section[role=tabpanel] {
+ padding: 15px;
+ margin: 0;
+ overflow: hidden;
+ border: var(--nav-tabs-border-width) solid var(--nav-tabs-border-color);
+ border-top: none;
+ border-bottom-left-radius: var(--nav-tabs-border-radius);
+ border-bottom-right-radius: var(--nav-tabs-border-radius)
+}
+
+.tabGroup section[role=tabpanel]>.codeHeader,.tabGroup section[role=tabpanel]>pre {
+ margin-left: -16px;
+ margin-right: -16px
+}
+
+.tabGroup section[role=tabpanel]>:first-child {
+ margin-top: 0
+}
+
+.tabGroup section[role=tabpanel]>pre:last-child {
+ display: block;
+ margin-bottom: -16px
+}
+
+.tabGroup>section {
+ margin: 0;
+ padding: 1rem;
+ border-top: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0
+}
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/singulink.js b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/singulink.js
new file mode 100644
index 0000000..07cda60
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/singulink.js
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+function toggleMenu() {
+
+ var sidebar = document.getElementById("sidebar");
+ var blackout = document.getElementById("blackout");
+
+ if (sidebar.style.left === "0px")
+ {
+ sidebar.style.left = "-" + sidebar.getBoundingClientRect().width + "px";
+ blackout.classList.remove("showThat");
+ blackout.classList.add("hideThat");
+ }
+ else
+ {
+ sidebar.style.left = "0px";
+ blackout.classList.remove("hideThat");
+ blackout.classList.add("showThat");
+ }
+}
+
+// jQuery .deepest(): https://gist.github.com/geraldfullam/3a151078b55599277da4
+
+(function ($) {
+ $.fn.deepest = function (selector) {
+ var deepestLevel = 0,
+ $deepestChild,
+ $deepestChildSet;
+
+ this.each(function () {
+ $parent = $(this);
+ $parent
+ .find((selector || '*'))
+ .each(function () {
+ if (!this.firstChild || this.firstChild.nodeType !== 1) {
+ var levelsToParent = $(this).parentsUntil($parent).length;
+ if (levelsToParent > deepestLevel) {
+ deepestLevel = levelsToParent;
+ $deepestChild = $(this);
+ } else if (levelsToParent === deepestLevel) {
+ $deepestChild = !$deepestChild ? $(this) : $deepestChild.add(this);
+ }
+ }
+ });
+ $deepestChildSet = !$deepestChildSet ? $deepestChild : $deepestChildSet.add($deepestChild);
+ });
+
+ return this.pushStack($deepestChildSet || [], 'deepest', selector || '');
+ };
+}(jQuery));
+
+$(function() {
+ $('table').each(function(a, tbl) {
+ var currentTableRows = $(tbl).find('tbody tr').length;
+ $(tbl).find('th').each(function(i) {
+ var remove = 0;
+ var currentTable = $(this).parents('table');
+
+ var tds = currentTable.find('tr td:nth-child(' + (i + 1) + ')');
+ tds.each(function(j) { if ($(this).text().trim() === '') remove++; });
+
+ if (remove == currentTableRows) {
+ $(this).hide();
+ tds.hide();
+ }
+ });
+ });
+
+ function scrollToc() {
+ var activeTocItem = $('.sidebar').deepest('.sidebar-item.active')[0]
+
+ if (activeTocItem) {
+ activeTocItem.scrollIntoView({ block: "center" });
+ }
+ else{
+ setTimeout(scrollToc, 500);
+ }
+ }
+
+ setTimeout(scrollToc, 500);
+});
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/url.min.js b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/url.min.js
new file mode 100644
index 0000000..8057e0a
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/styles/url.min.js
@@ -0,0 +1 @@
+/*! url - v1.8.6 - 2013-11-22 */window.url=function(){function a(a){return!isNaN(parseFloat(a))&&isFinite(a)}return function(b,c){var d=c||window.location.toString();if(!b)return d;b=b.toString(),"//"===d.substring(0,2)?d="http:"+d:1===d.split("://").length&&(d="http://"+d),c=d.split("/");var e={auth:""},f=c[2].split("@");1===f.length?f=f[0].split(":"):(e.auth=f[0],f=f[1].split(":")),e.protocol=c[0],e.hostname=f[0],e.port=f[1]||("https"===e.protocol.split(":")[0].toLowerCase()?"443":"80"),e.pathname=(c.length>3?"/":"")+c.slice(3,c.length).join("/").split("?")[0].split("#")[0];var g=e.pathname;"/"===g.charAt(g.length-1)&&(g=g.substring(0,g.length-1));var h=e.hostname,i=h.split("."),j=g.split("/");if("hostname"===b)return h;if("domain"===b)return/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(h)?h:i.slice(-2).join(".");if("sub"===b)return i.slice(0,i.length-2).join(".");if("port"===b)return e.port;if("protocol"===b)return e.protocol.split(":")[0];if("auth"===b)return e.auth;if("user"===b)return e.auth.split(":")[0];if("pass"===b)return e.auth.split(":")[1]||"";if("path"===b)return e.pathname;if("."===b.charAt(0)){if(b=b.substring(1),a(b))return b=parseInt(b,10),i[0>b?i.length+b:b-1]||""}else{if(a(b))return b=parseInt(b,10),j[0>b?j.length+b:b]||"";if("file"===b)return j.slice(-1)[0];if("filename"===b)return j.slice(-1)[0].split(".")[0];if("fileext"===b)return j.slice(-1)[0].split(".")[1]||"";if("?"===b.charAt(0)||"#"===b.charAt(0)){var k=d,l=null;if("?"===b.charAt(0)?k=(k.split("?")[1]||"").split("#")[0]:"#"===b.charAt(0)&&(k=k.split("#")[1]||""),!b.charAt(1))return k;b=b.substring(1),k=k.split("&");for(var m=0,n=k.length;n>m;m++)if(l=k[m].split("="),l[0]===b)return l[1]||"";return null}}return""}}(),"undefined"!=typeof jQuery&&jQuery.extend({url:function(a,b){return window.url(a,b)}});
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover Docs/templates/singulinkfx/toc.html.primary.tmpl b/src/wan24-AutoDiscover Docs/templates/singulinkfx/toc.html.primary.tmpl
new file mode 100644
index 0000000..f4deb5e
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/templates/singulinkfx/toc.html.primary.tmpl
@@ -0,0 +1,22 @@
+{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
+
+
+
+ {{^_disableSideFilter}}
+
+
+
+ {{/_disableSideFilter}}
+
+
+ {{^leaf}}
+ {{>partials/li}}
+ {{/leaf}}
+
+
+
+
diff --git a/src/wan24-AutoDiscover Docs/toc.yml b/src/wan24-AutoDiscover Docs/toc.yml
new file mode 100644
index 0000000..55d7e96
--- /dev/null
+++ b/src/wan24-AutoDiscover Docs/toc.yml
@@ -0,0 +1,5 @@
+#- name: Articles
+# href: articles/
+- name: Api Documentation
+ href: api/
+ homepage: api/index.md
diff --git a/src/wan24-AutoDiscover Shared/Constants.cs b/src/wan24-AutoDiscover Shared/Constants.cs
new file mode 100644
index 0000000..d26c6a7
--- /dev/null
+++ b/src/wan24-AutoDiscover Shared/Constants.cs
@@ -0,0 +1,13 @@
+namespace wan24.AutoDiscover
+{
+ ///
+ /// Constants
+ ///
+ public static class Constants
+ {
+ ///
+ /// POX response node XML namespace
+ ///
+ public const string RESPONSE_NS = "https://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
+ }
+}
diff --git a/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs b/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs
new file mode 100644
index 0000000..d98a9a3
--- /dev/null
+++ b/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs
@@ -0,0 +1,79 @@
+using Microsoft.Extensions.Configuration;
+using System.Collections;
+using System.Collections.Frozen;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using wan24.Core;
+
+namespace wan24.AutoDiscover.Models
+{
+ ///
+ /// Discovery configuration
+ ///
+ public record class DiscoveryConfig
+ {
+ ///
+ /// Constructor
+ ///
+ public DiscoveryConfig() { }
+
+ ///
+ /// Logfile path
+ ///
+ [StringLength(short.MaxValue, MinimumLength = 1)]
+ public string? LogFile { get; init; }
+
+ ///
+ /// Number of POX XML responses to pre-fork
+ ///
+ [Range(1, int.MaxValue)]
+ public int PreForkResponses { get; init; } = 10;
+
+ ///
+ /// Dicovery configuration type name
+ ///
+ [StringLength(byte.MaxValue, MinimumLength = 1)]
+ public string? DiscoveryTypeName { get; init; }
+
+ ///
+ /// Discovery configuration type
+ ///
+ [JsonIgnore]
+ public Type DiscoveryType => DiscoveryTypeName is null
+ ? typeof(Dictionary)
+ : TypeHelper.Instance.GetType(DiscoveryTypeName)
+ ?? throw new InvalidDataException($"Discovery type {DiscoveryTypeName.ToQuotedLiteral()} not found");
+
+ ///
+ /// Get the discovery configuration
+ ///
+ /// Configuration
+ /// Discovery configuration
+ public FrozenDictionary GetDiscoveryConfig(IConfigurationRoot config)
+ {
+ Type discoveryType = DiscoveryType;
+ if (!typeof(IDictionary).IsAssignableFrom(discoveryType))
+ throw new InvalidDataException($"Discovery type must be an {typeof(IDictionary)}");
+ if (!discoveryType.IsGenericType)
+ throw new InvalidDataException($"Discovery type must be a generic type");
+ // Validate discovery configuration type generic type arguments
+ Type[] gt = discoveryType.GetGenericArguments();
+ if (gt.Length != 2)
+ throw new InvalidDataException($"Discovery type must be a generic type with two type arguments");
+ if (gt[0] != typeof(string))
+ throw new InvalidDataException($"Discovery types first generic type argument must be a {typeof(string)}");
+ if (!typeof(DomainConfig).IsAssignableFrom(gt[1]))
+ throw new InvalidDataException($"Discovery types second generic type argument must be a {typeof(DomainConfig)}");
+ // Parse the discovery configuration
+ IDictionary discovery = config.GetRequiredSection("DiscoveryConfig:Discovery").Get(discoveryType) as IDictionary
+ ?? throw new InvalidDataException("Failed to get discovery configuration");
+ object[] keys = new object[discovery.Count],
+ values = new object[discovery.Count];
+ discovery.Keys.CopyTo(keys, index: 0);
+ discovery.Values.CopyTo(values, index: 0);
+ return new Dictionary(
+ Enumerable.Range(0, discovery.Count).Select(i => new KeyValuePair((string)keys[i], (DomainConfig)values[i]))
+ ).ToFrozenDictionary();
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover Shared/Models/DomainConfig.cs b/src/wan24-AutoDiscover Shared/Models/DomainConfig.cs
new file mode 100644
index 0000000..49c4feb
--- /dev/null
+++ b/src/wan24-AutoDiscover Shared/Models/DomainConfig.cs
@@ -0,0 +1,45 @@
+using System.Collections.Frozen;
+using System.Xml;
+using wan24.ObjectValidation;
+
+namespace wan24.AutoDiscover.Models
+{
+ ///
+ /// Domain configuration
+ ///
+ public record class DomainConfig
+ {
+ ///
+ /// Constructor
+ ///
+ public DomainConfig() { }
+
+ ///
+ /// Registered domains (key is the served domain name)
+ ///
+ public static FrozenDictionary Registered { get; set; } = null!;
+
+ ///
+ /// Accepted domain names
+ ///
+ [ItemRegularExpression(@"^[a-z|-|\.]{1,256}$")]
+ public HashSet? AcceptedDomains { get; init; }
+
+ ///
+ /// Protocols
+ ///
+ [CountLimit(1, int.MaxValue)]
+ public required virtual HashSet Protocols { get; init; }
+
+ ///
+ /// Create XML
+ ///
+ /// XML
+ /// Account node
+ /// Splitted email parts
+ public virtual void CreateXml(XmlDocument xml, XmlNode account, string[] emailParts)
+ {
+ foreach (Protocol protocol in Protocols) protocol.CreateXml(xml, account, emailParts);
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover Shared/Models/Protocol.cs b/src/wan24-AutoDiscover Shared/Models/Protocol.cs
new file mode 100644
index 0000000..4dad89e
--- /dev/null
+++ b/src/wan24-AutoDiscover Shared/Models/Protocol.cs
@@ -0,0 +1,136 @@
+using System.ComponentModel.DataAnnotations;
+using System.Xml;
+
+// https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
+
+namespace wan24.AutoDiscover.Models
+{
+ ///
+ /// Protocol (POX)
+ ///
+ public record class Protocol
+ {
+ ///
+ /// Protocol node name
+ ///
+ private const string PROTOCOL_NODE_NAME = "Protocol";
+ ///
+ /// Type node name
+ ///
+ private const string TYPE_NODE_NAME = "Type";
+ ///
+ /// Server node name
+ ///
+ private const string SERVER_NODE_NAME = "Server";
+ ///
+ /// Port node name
+ ///
+ private const string PORT_NODE_NAME = "Port";
+ ///
+ /// LoginName node name
+ ///
+ private const string LOGINNAME_NODE_NAME = "LoginName";
+ ///
+ /// SPA node name
+ ///
+ private const string SPA_NODE_NAME = "SPA";
+ ///
+ /// SSL node name
+ ///
+ private const string SSL_NODE_NAME = "SSL";
+ ///
+ /// AuthRequired node name
+ ///
+ private const string AUTHREQUIRED_NODE_NAME = "AuthRequired";
+ ///
+ /// ON
+ ///
+ private const string ON = "on";
+ ///
+ /// OFF
+ ///
+ private const string OFF = "off";
+
+ ///
+ /// Constructor
+ ///
+ public Protocol() { }
+
+ ///
+ /// Login name getter delegate
+ ///
+ public static LoginName_Delegate LoginName { get; set; } = (xml, account, emailParts, protocol) => protocol.LoginNameIsEmailAlias
+ ? emailParts[0]
+ : string.Join('@', emailParts);
+
+ ///
+ /// Type
+ ///
+ [Required]
+ public required string Type { get; init; }
+
+ ///
+ /// Server
+ ///
+ [RegularExpression(@"^[a-z|-|\.]{1,256}$")]
+ public required string Server { get; init; }
+
+ ///
+ /// Port
+ ///
+ [Range(1, ushort.MaxValue)]
+ public int Port { get; init; }
+
+ ///
+ /// If the login name is the alias of the email address
+ ///
+ public bool LoginNameIsEmailAlias { get; init; } = true;
+
+ ///
+ /// Secure password authentication
+ ///
+ public bool SPA { get; init; }
+
+ ///
+ /// SSL
+ ///
+ public bool SSL { get; init; } = true;
+
+ ///
+ /// Authentication required
+ ///
+ public bool AuthRequired { get; init; } = true;
+
+ ///
+ /// Create XML
+ ///
+ /// XML
+ /// Account node
+ /// Splitted email parts
+ public virtual void CreateXml(XmlDocument xml, XmlNode account, string[] emailParts)
+ {
+ XmlNode protocol = account.AppendChild(xml.CreateElement(PROTOCOL_NODE_NAME, Constants.RESPONSE_NS))!;
+ foreach (KeyValuePair kvp in new Dictionary()
+ {
+ {TYPE_NODE_NAME, Type },
+ {SERVER_NODE_NAME, Server },
+ {PORT_NODE_NAME, Port.ToString() },
+ {LOGINNAME_NODE_NAME, LoginName(xml, account, emailParts, this) },
+ {SPA_NODE_NAME, SPA ? ON : OFF },
+ {SSL_NODE_NAME, SSL ? ON : OFF },
+ {AUTHREQUIRED_NODE_NAME, AuthRequired ? ON : OFF }
+ })
+ protocol.AppendChild(xml.CreateElement(kvp.Key, Constants.RESPONSE_NS))!.InnerText = kvp.Value;
+ }
+
+ ///
+ /// Delegate for a login name getter
+ ///
+ /// XML
+ /// Account node
+ /// Splitted email parts
+ /// Protocol
+ /// Login name
+ public delegate string LoginName_Delegate(XmlDocument xml, XmlNode account, string[] emailParts, Protocol protocol);
+ }
+}
diff --git a/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj b/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj
new file mode 100644
index 0000000..85a7e98
--- /dev/null
+++ b/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ wan24.AutoDiscover
+ enable
+ enable
+ wan24AutoDiscoverShared
+ True
+
+
+
+
+
+
+
+
+
diff --git a/src/wan24-AutoDiscover.sln b/src/wan24-AutoDiscover.sln
new file mode 100644
index 0000000..6849790
--- /dev/null
+++ b/src/wan24-AutoDiscover.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.34707.107
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "wan24-AutoDiscover", "wan24-AutoDiscover\wan24-AutoDiscover.csproj", "{91087847-7A9C-4120-8A91-27CDF44E21E7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "wan24-AutoDiscover Shared", "wan24-AutoDiscover Shared\wan24-AutoDiscover Shared.csproj", "{610B6034-2404-4EBA-80E1-92102CE9E5B4}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {91087847-7A9C-4120-8A91-27CDF44E21E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91087847-7A9C-4120-8A91-27CDF44E21E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91087847-7A9C-4120-8A91-27CDF44E21E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91087847-7A9C-4120-8A91-27CDF44E21E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {610B6034-2404-4EBA-80E1-92102CE9E5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {610B6034-2404-4EBA-80E1-92102CE9E5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {610B6034-2404-4EBA-80E1-92102CE9E5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {610B6034-2404-4EBA-80E1-92102CE9E5B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {EA8C3992-DDF7-4778-9ED7-670279F08583}
+ EndGlobalSection
+EndGlobal
diff --git a/src/wan24-AutoDiscover/.config/dotnet-tools.json b/src/wan24-AutoDiscover/.config/dotnet-tools.json
new file mode 100644
index 0000000..d9d129c
--- /dev/null
+++ b/src/wan24-AutoDiscover/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "8.0.3",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs b/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs
new file mode 100644
index 0000000..3254461
--- /dev/null
+++ b/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs
@@ -0,0 +1,131 @@
+using Microsoft.AspNetCore.Mvc;
+using System.Net;
+using System.Net.Mail;
+using System.Text;
+using System.Xml;
+using wan24.AutoDiscover.Models;
+using wan24.AutoDiscover.Services;
+using wan24.Core;
+
+namespace wan24.AutoDiscover.Controllers
+{
+ ///
+ /// Discovery controller
+ ///
+ ///
+ /// Constructor
+ ///
+ /// Responses
+ [ApiController, Route("autodiscover")]
+ public class DiscoveryController(XmlDocumentInstances responses) : ControllerBase()
+ {
+ ///
+ /// Request XML email address node XPath selector
+ ///
+ private const string EMAIL_NODE_XPATH = "//*[local-name()='EMailAddress']";
+ ///
+ /// Request XML acceptable response node XPath selector
+ ///
+ private const string ACCEPTABLE_RESPONSE_SCHEMA_NODE_XPATH = "//*[local-name()='AcceptableResponseSchema']";
+ ///
+ /// Response XML account node XPath selector
+ ///
+ private const string ACCOUNT_NODE_XPATH = $"//*[local-name()='{ACCOUNT_NODE_NAME}']";
+ ///
+ /// XML response MIME type
+ ///
+ private const string XML_MIME_TYPE = "application/xml";
+ ///
+ /// Auto discovery XML namespace
+ ///
+ public const string AUTO_DISCOVER_NS = "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006";
+ ///
+ /// Autodiscover node name
+ ///
+ public const string AUTODISCOVER_NODE_NAME = "Autodiscover";
+ ///
+ /// Response node name
+ ///
+ public const string RESPONSE_NODE_NAME = "Response";
+ ///
+ /// Account node name
+ ///
+ public const string ACCOUNT_NODE_NAME = "Account";
+ ///
+ /// AccountType node name
+ ///
+ public const string ACCOUNTTYPE_NODE_NAME = "AccountType";
+ ///
+ /// Account type
+ ///
+ public const string ACCOUNTTYPE = "email";
+ ///
+ /// Action node name
+ ///
+ public const string ACTION_NODE_NAME = "Action";
+ ///
+ /// Action
+ ///
+ public const string ACTION = "settings";
+
+ ///
+ /// Responses
+ ///
+ private readonly XmlDocumentInstances Responses = responses;
+
+ ///
+ /// Auto discovery (POX)
+ ///
+ /// XML response
+ [HttpPost, Route("autodiscover.xml")]
+ public async Task AutoDiscoverAsync()
+ {
+ // Try getting the requested email address from the request
+ XmlDocument requestXml = new();
+ Stream requestBody = HttpContext.Request.Body;
+ await using (requestBody.DynamicContext())
+ using (StreamReader reader = new(requestBody, Encoding.UTF8, leaveOpen: true))
+ {
+ string requestXmlString = await reader.ReadToEndAsync(HttpContext.RequestAborted).DynamicContext();
+ if (Logging.Debug)
+ Logging.WriteDebug($"POX request XML body from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort}: {requestXmlString.ToQuotedLiteral()}");
+ requestXml.LoadXml(requestXmlString);
+ }
+ if (
+ requestXml.SelectSingleNode(ACCEPTABLE_RESPONSE_SCHEMA_NODE_XPATH) is XmlNode acceptableResponseSchema &&
+ acceptableResponseSchema.InnerText.Trim() != Constants.RESPONSE_NS
+ )
+ throw new BadHttpRequestException($"Unsupported response schema {acceptableResponseSchema.InnerText.ToQuotedLiteral()}");
+ if (requestXml.SelectSingleNode(EMAIL_NODE_XPATH) is not XmlNode emailNode)
+ throw new BadHttpRequestException("Missing email address in request");
+ string emailAddress = emailNode.InnerText.Trim().ToLower();
+ if (Logging.Debug)
+ Logging.WriteDebug($"POX request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort} email address {emailAddress.ToQuotedLiteral()}");
+ string[] emailParts = emailAddress.Split('@', 2);
+ if (emailParts.Length != 2 || !MailAddress.TryCreate(emailAddress, out _))
+ throw new BadHttpRequestException("Invalid email address");
+ // Generate discovery response
+ if (Logging.Debug)
+ Logging.WriteDebug($"Creating POX response for {emailAddress.ToQuotedLiteral()} request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort}");
+ XmlDocument xml = await Responses.GetOneAsync(HttpContext.RequestAborted).DynamicContext();
+ if (
+ !DomainConfig.Registered.TryGetValue(emailParts[1], out DomainConfig? config) &&
+ !DomainConfig.Registered.TryGetValue(HttpContext.Request.Host.Host, out config) &&
+ !DomainConfig.Registered.TryGetValue(
+ DomainConfig.Registered.Where(kvp => kvp.Value.AcceptedDomains?.Contains(emailParts[1], StringComparer.OrdinalIgnoreCase) ?? false)
+ .Select(kvp => kvp.Key)
+ .FirstOrDefault() ?? string.Empty,
+ out config
+ )
+ )
+ throw new BadHttpRequestException($"Unknown request domain name \"{HttpContext.Request.Host.Host}\"/{emailParts[1].ToQuotedLiteral()}");
+ config.CreateXml(xml, xml.SelectSingleNode(ACCOUNT_NODE_XPATH) ?? throw new InvalidProgramException("Missing response XML account node"), emailParts);
+ return new()
+ {
+ Content = xml.OuterXml,
+ ContentType = XML_MIME_TYPE,
+ StatusCode = (int)HttpStatusCode.OK
+ };
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover/Program.cs b/src/wan24-AutoDiscover/Program.cs
new file mode 100644
index 0000000..b13489f
--- /dev/null
+++ b/src/wan24-AutoDiscover/Program.cs
@@ -0,0 +1,129 @@
+using wan24.AutoDiscover.Models;
+using wan24.AutoDiscover.Services;
+using wan24.CLI;
+using wan24.Core;
+
+// Run the CLI API
+if (args.Length > 0)
+{
+ await Bootstrap.Async().DynamicContext();
+ Translation.Current = Translation.Dummy;
+ Settings.AppId = "wan24-AutoDiscover";
+ Settings.ProcessId = "webservice";
+ Logging.Logger = new VividConsoleLogger();
+ CliApi.HelpHeader = "wan24-AutoDiscover";
+ return await CliApi.RunAsync(args, exportedApis: [typeof(CliHelpApi), typeof(CommandLineInterface)]).DynamicContext();
+}
+
+// Load the configuration
+string configFile = Path.Combine(ENV.AppFolder, "appsettings.json");
+(IConfigurationRoot Config, DiscoveryConfig Discovery) LoadConfig()
+{
+ ConfigurationBuilder configBuilder = new();
+ configBuilder.AddJsonFile(configFile, optional: false);
+ IConfigurationRoot config = configBuilder.Build();
+ DiscoveryConfig discovery = config.GetRequiredSection("DiscoveryConfig").Get()
+ ?? throw new InvalidDataException($"Failed to get a {typeof(DiscoveryConfig)} from the \"DiscoveryConfig\" section");
+ DomainConfig.Registered = discovery.GetDiscoveryConfig(config);
+ return (config, discovery);
+}
+(IConfigurationRoot config, DiscoveryConfig discovery) = LoadConfig();
+
+// Initialize wan24-Core
+await Bootstrap.Async().DynamicContext();
+Translation.Current = Translation.Dummy;
+Settings.AppId = "wan24-AutoDiscover";
+Settings.ProcessId = "webservice";
+Settings.LogLevel = config.GetValue("Logging:LogLevel:Default");
+Logging.Logger = discovery.LogFile is string logFile && !string.IsNullOrWhiteSpace(logFile)
+ ? await FileLogger.CreateAsync(logFile).DynamicContext()
+ : new VividConsoleLogger();
+ErrorHandling.ErrorHandler = (e) => Logging.WriteError($"{e.Info}: {e.Exception}");
+Logging.WriteInfo($"Using configuration \"{configFile}\"");
+
+// Watch configuration changes
+using ConfigChangeEventThrottle fswThrottle = new();
+ConfigChangeEventThrottle.OnConfigChange += () =>
+{
+ try
+ {
+ Logging.WriteDebug("Handling configuration change");
+ if (File.Exists(configFile))
+ {
+ Logging.WriteInfo($"Auto-reloading changed configuration from \"{configFile}\"");
+ LoadConfig();
+ }
+ else
+ {
+ Logging.WriteTrace($"Configuration file \"{configFile}\" doesn't exist");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.WriteWarning($"Failed to reload configuration from \"{configFile}\": {ex}");
+ }
+};
+void ReloadConfig(object sender, FileSystemEventArgs e)
+{
+ try
+ {
+ Logging.WriteDebug($"Detected configuration change {e.ChangeType}");
+ if (File.Exists(configFile))
+ {
+ if (fswThrottle.IsThrottling)
+ {
+ Logging.WriteTrace("Skipping configuration change event due too many events");
+ }
+ else if (fswThrottle.Raise())
+ {
+ Logging.WriteTrace("Configuration change event has been raised");
+ }
+ }
+ else
+ {
+ Logging.WriteTrace($"Configuration file \"{configFile}\" doesn't exist");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.WriteWarning($"Failed to handle configuration change of \"{configFile}\": {ex}");
+ }
+}
+using FileSystemWatcher fsw = new(ENV.AppFolder, "appsettings.json")
+{
+ NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
+ IncludeSubdirectories = false,
+ EnableRaisingEvents = true
+};
+fsw.Changed += ReloadConfig;
+fsw.Created += ReloadConfig;
+
+// Build and run the app
+Logging.WriteInfo("Autodiscovery service app startup");
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+builder.Services.AddControllers();
+builder.Services.AddSingleton(typeof(XmlDocumentInstances), services => new XmlDocumentInstances(capacity: discovery.PreForkResponses))
+ .AddHostedService(services => services.GetRequiredService())
+ .AddExceptionHandler();
+WebApplication app = builder.Build();
+try
+{
+ await using (app.DynamicContext())
+ {
+ app.UseExceptionHandler(b => { });// .NET 8 bugfix :(
+ app.MapControllers();
+ Logging.WriteInfo("Autodiscovery service app starting");
+ await app.RunAsync().DynamicContext();
+ Logging.WriteInfo("Autodiscovery service app quitting");
+ }
+}
+catch(Exception ex)
+{
+ Logging.WriteError($"Autodiscovery service app error: {ex}");
+ return 1;
+}
+finally
+{
+ Logging.WriteInfo("Autodiscovery service app exit");
+}
+return 0;
diff --git a/src/wan24-AutoDiscover/Properties/launchSettings.json b/src/wan24-AutoDiscover/Properties/launchSettings.json
new file mode 100644
index 0000000..29278b5
--- /dev/null
+++ b/src/wan24-AutoDiscover/Properties/launchSettings.json
@@ -0,0 +1,32 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "commandLineArgs": "autodiscover systemd",
+ "launchBrowser": true,
+ "launchUrl": "autodiscover/autodiscover.xml",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5196"
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:23052",
+ "sslPort": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/wan24-AutoDiscover/Services/CommandLineInterface.cs b/src/wan24-AutoDiscover/Services/CommandLineInterface.cs
new file mode 100644
index 0000000..c50f620
--- /dev/null
+++ b/src/wan24-AutoDiscover/Services/CommandLineInterface.cs
@@ -0,0 +1,31 @@
+using System.Text;
+using wan24.CLI;
+using wan24.Core;
+
+namespace wan24.AutoDiscover.Services
+{
+ ///
+ /// CLI API
+ ///
+ [CliApi("autodiscover")]
+ public class CommandLineInterface
+ {
+ ///
+ /// Constructor
+ ///
+ public CommandLineInterface() { }
+
+ ///
+ /// Create service information
+ ///
+ [CliApi("systemd", IsDefault = true)]
+ [StdOut("/etc/systemd/system/autodiscover.service")]
+ public static async Task CreateSystemdServiceAsync()
+ {
+ Stream stdOut = Console.OpenStandardOutput();
+ await using (stdOut.DynamicContext())
+ using (StreamWriter writer = new(stdOut, Encoding.UTF8, leaveOpen: true))
+ await writer.WriteLineAsync(new SystemdServiceFile().ToString().Trim()).DynamicContext();
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs b/src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs
new file mode 100644
index 0000000..bf523d3
--- /dev/null
+++ b/src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs
@@ -0,0 +1,31 @@
+using wan24.Core;
+
+namespace wan24.AutoDiscover.Services
+{
+ ///
+ /// Configuration changed event throttle
+ ///
+ public sealed class ConfigChangeEventThrottle : EventThrottle
+ {
+ ///
+ /// Constructor
+ ///
+ public ConfigChangeEventThrottle() : base(timeout: 300) { }
+
+ ///
+ protected override void HandleEvent(in DateTime raised, in int raisedCount) => RaiseOnConfigChange();
+
+ ///
+ /// Delegate for the event
+ ///
+ public delegate void ConfigChange_Delegate();
+ ///
+ /// Raised on configuration changes
+ ///
+ public static event ConfigChange_Delegate? OnConfigChange;
+ ///
+ /// Raise the event
+ ///
+ private static void RaiseOnConfigChange() => OnConfigChange?.Invoke();
+ }
+}
diff --git a/src/wan24-AutoDiscover/Services/ExceptionHandler.cs b/src/wan24-AutoDiscover/Services/ExceptionHandler.cs
new file mode 100644
index 0000000..e688946
--- /dev/null
+++ b/src/wan24-AutoDiscover/Services/ExceptionHandler.cs
@@ -0,0 +1,34 @@
+using Microsoft.AspNetCore.Diagnostics;
+using System.Net;
+using wan24.Core;
+
+namespace wan24.AutoDiscover.Services
+{
+ ///
+ /// Exception handler
+ ///
+ public sealed class ExceptionHandler : IExceptionHandler
+ {
+ ///
+ /// Constructor
+ ///
+ public ExceptionHandler() { }
+
+ ///
+ public ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
+ {
+ if (exception is BadHttpRequestException badRequest)
+ {
+ if (Logging.Trace)
+ Logging.WriteTrace($"http handling bas request exception for {httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort} request to \"{httpContext.Request.Method} {httpContext.Request.Path}\": {exception}");
+ httpContext.Response.StatusCode = badRequest.StatusCode;
+ }
+ else
+ {
+ Logging.WriteError($"http handling exception for {httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort} request to \"{httpContext.Request.Method} {httpContext.Request.Path}\": {exception}");
+ httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ }
+ return ValueTask.FromResult(true);
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover/Services/XmlDocumentInstances.cs b/src/wan24-AutoDiscover/Services/XmlDocumentInstances.cs
new file mode 100644
index 0000000..5d20da4
--- /dev/null
+++ b/src/wan24-AutoDiscover/Services/XmlDocumentInstances.cs
@@ -0,0 +1,38 @@
+using System.Xml;
+using wan24.AutoDiscover.Controllers;
+using wan24.Core;
+
+namespace wan24.AutoDiscover.Services
+{
+ ///
+ /// response instance pool
+ ///
+ ///
+ /// Constructor
+ ///
+ /// Capacity
+ public sealed class XmlDocumentInstances(in int capacity) : InstancePool(capacity, CreateXmlDocument)
+ {
+ ///
+ /// Constructor
+ ///
+ public XmlDocumentInstances() : this(capacity: 100) { }
+
+ ///
+ /// Create an
+ ///
+ /// Pool
+ ///
+ private static XmlDocument CreateXmlDocument(IInstancePool pool)
+ {
+ if (Logging.Trace) Logging.WriteTrace("Pre-forking a new POX XML response");
+ XmlDocument xml = new();
+ XmlNode account = xml.AppendChild(xml.CreateNode(XmlNodeType.Element, DiscoveryController.AUTODISCOVER_NODE_NAME, DiscoveryController.AUTO_DISCOVER_NS))!
+ .AppendChild(xml.CreateNode(XmlNodeType.Element, DiscoveryController.RESPONSE_NODE_NAME, Constants.RESPONSE_NS))!
+ .AppendChild(xml.CreateElement(DiscoveryController.ACCOUNT_NODE_NAME, Constants.RESPONSE_NS))!;
+ account.AppendChild(xml.CreateElement(DiscoveryController.ACCOUNTTYPE_NODE_NAME, Constants.RESPONSE_NS))!.InnerText = DiscoveryController.ACCOUNTTYPE;
+ account.AppendChild(xml.CreateElement(DiscoveryController.ACTION_NODE_NAME, Constants.RESPONSE_NS))!.InnerText = DiscoveryController.ACTION;
+ return xml;
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover/appsettings.Development.json b/src/wan24-AutoDiscover/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/wan24-AutoDiscover/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover/appsettings.json b/src/wan24-AutoDiscover/appsettings.json
new file mode 100644
index 0000000..c3b0868
--- /dev/null
+++ b/src/wan24-AutoDiscover/appsettings.json
@@ -0,0 +1,41 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Kestrel": {
+ "Endpoints": {
+ "AutoDiscover": {
+ "Url": "http://localhost:5000"
+ }
+ }
+ },
+ "AllowedHosts": "*",
+ "DiscoveryConfig": {
+ "LogFile": null,
+ "PreForkResponses": 10,
+ "DiscoveryType": null,
+ "Discovery": {
+ "localhost": {
+ "AcceptedDomains": [
+ "wan24.de",
+ "wan-solutions.de"
+ ],
+ "Protocols": [
+ {
+ "Type": "IMAP",
+ "Server": "imap.wan24.de",
+ "Port": 993
+ },
+ {
+ "Type": "SMTP",
+ "Server": "smtp.wan24.de",
+ "Port": 587
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj b/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj
new file mode 100644
index 0000000..dbad146
--- /dev/null
+++ b/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ wan24.AutoDiscover
+ wan24AutoDiscover
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+