diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..ce35d93
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,26 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+/StalwartSDK/StalwartClient.cs
\ No newline at end of file
diff --git a/.forgejo/workflows/package-debug.yaml b/.forgejo/workflows/package-debug.yaml
new file mode 100644
index 0000000..30b183a
--- /dev/null
+++ b/.forgejo/workflows/package-debug.yaml
@@ -0,0 +1,40 @@
+on:
+ push:
+ branches:
+ - "**"
+ pull_request:
+ types: [ opened, synchronize, reopened ]
+jobs:
+ publish_debug:
+ runs-on: docker
+ container:
+ image: ghcr.io/catthehacker/ubuntu:act-22.04
+ steps:
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ git.spgrn.com/${{ env.GITHUB_REPOSITORY }}-web
+ tags: |
+ type=ref,event=pr
+ type=ref,event=branch
+ type=sha
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Login to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: git.spgrn.com
+ username: seang96
+ password: ${{ secrets.TOKEN }}
+ - name: Build Web Application
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./StalwartSimpleLoginMiddleware/Dockerfile
+ platforms: linux/amd64
+ push: true
+ BUILD_CONFIGURATION=Debug
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.forgejo/workflows/package.yaml b/.forgejo/workflows/package.yaml
new file mode 100644
index 0000000..02a52e7
--- /dev/null
+++ b/.forgejo/workflows/package.yaml
@@ -0,0 +1,40 @@
+on:
+ push:
+ tags:
+ - "v*.*.*"
+jobs:
+ publish:
+ runs-on: docker
+ container:
+ image: ghcr.io/catthehacker/ubuntu:act-22.04
+ steps:
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ git.spgrn.com/${{ env.GITHUB_REPOSITORY }}-web
+ tags: |
+ type=ref,event=tag
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+ type=raw,value=latest
+ type=sha
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Login to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: git.spgrn.com
+ username: seang96
+ password: ${{ secrets.TOKEN }}
+ - name: Build Web Application
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./StalwartSimpleLoginMiddleware/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.gitignore b/.gitignore
index 57940fd..9e1873e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,3 +77,10 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
+/StalwartSDK/StalwartClient.cs
+launchSettings.json
\ No newline at end of file
diff --git a/Stalwart Simple Login Middleware.sln b/Stalwart Simple Login Middleware.sln
new file mode 100644
index 0000000..82c743b
--- /dev/null
+++ b/Stalwart Simple Login Middleware.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StalwartSimpleLoginMiddleware", "StalwartSimpleLoginMiddleware\StalwartSimpleLoginMiddleware.csproj", "{AFC6627E-CA72-440D-B405-384AB62F12C2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StalwartSDK", "StalwartSDK\StalwartSDK.csproj", "{AD805E73-59A5-402C-8EE3-41113DDE86E7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AFC6627E-CA72-440D-B405-384AB62F12C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AFC6627E-CA72-440D-B405-384AB62F12C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AFC6627E-CA72-440D-B405-384AB62F12C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AFC6627E-CA72-440D-B405-384AB62F12C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD805E73-59A5-402C-8EE3-41113DDE86E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD805E73-59A5-402C-8EE3-41113DDE86E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD805E73-59A5-402C-8EE3-41113DDE86E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD805E73-59A5-402C-8EE3-41113DDE86E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/StalwartSDK/StalwartClientFactory.cs b/StalwartSDK/StalwartClientFactory.cs
new file mode 100644
index 0000000..70a1a77
--- /dev/null
+++ b/StalwartSDK/StalwartClientFactory.cs
@@ -0,0 +1,23 @@
+using System.Net.Http.Headers;
+
+namespace AdOrbitSDK;
+
+public partial class Client
+{
+ private readonly string _apiKey;
+
+ public Client(HttpClient httpClient, string apiKey)
+ : this(httpClient)
+ {
+ _apiKey = apiKey;
+ }
+
+ partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
+ {
+ if (client.BaseAddress == null) throw new NullReferenceException("Base address cannot be null");
+
+ var authHeader = new AuthenticationHeaderValue("Bearer", _apiKey);
+
+ request.Headers.Authorization = authHeader;
+ }
+}
\ No newline at end of file
diff --git a/StalwartSDK/StalwartSDK.csproj b/StalwartSDK/StalwartSDK.csproj
new file mode 100644
index 0000000..2e7d275
--- /dev/null
+++ b/StalwartSDK/StalwartSDK.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StalwartSDK/nswag.json b/StalwartSDK/nswag.json
new file mode 100644
index 0000000..9a13066
--- /dev/null
+++ b/StalwartSDK/nswag.json
@@ -0,0 +1,103 @@
+{
+ "runtime": "Net80",
+ "defaultVariables": null,
+ "documentGenerator": {
+ "fromDocument": {
+ "url": "openapi.yml",
+ "output": null,
+ "newLineBehavior": "Auto"
+ }
+ },
+ "codeGenerators": {
+ "openApiToCSharpClient": {
+ "clientBaseClass": null,
+ "configurationClass": null,
+ "generateClientClasses": true,
+ "suppressClientClassesOutput": false,
+ "generateClientInterfaces": false,
+ "suppressClientInterfacesOutput": false,
+ "clientBaseInterface": null,
+ "injectHttpClient": true,
+ "disposeHttpClient": true,
+ "protectedMethods": [],
+ "generateExceptionClasses": true,
+ "exceptionClass": "ApiException",
+ "wrapDtoExceptions": true,
+ "useHttpClientCreationMethod": false,
+ "httpClientType": "System.Net.Http.HttpClient",
+ "useHttpRequestMessageCreationMethod": false,
+ "useBaseUrl": false,
+ "generateBaseUrlProperty": true,
+ "generateSyncMethods": false,
+ "generatePrepareRequestAndProcessResponseAsAsyncMethods": false,
+ "exposeJsonSerializerSettings": false,
+ "clientClassAccessModifier": "public",
+ "typeAccessModifier": "public",
+ "propertySetterAccessModifier": "",
+ "generateNativeRecords": false,
+ "generateContractsOutput": false,
+ "contractsNamespace": null,
+ "contractsOutputFilePath": null,
+ "parameterDateTimeFormat": "yyyy-MM-dd HH:mm:ss",
+ "parameterDateFormat": "yyyy-MM-dd",
+ "generateUpdateJsonSerializerSettingsMethod": true,
+ "useRequestAndResponseSerializationSettings": false,
+ "serializeTypeInformation": false,
+ "queryNullValue": "",
+ "className": "{controller}Client",
+ "operationGenerationMode": "MultipleClientsFromOperationId",
+ "additionalNamespaceUsages": [],
+ "additionalContractNamespaceUsages": [],
+ "generateOptionalParameters": true,
+ "generateJsonMethods": false,
+ "enforceFlagEnums": false,
+ "parameterArrayType": "System.Collections.Generic.IEnumerable",
+ "parameterDictionaryType": "System.Collections.Generic.IDictionary",
+ "responseArrayType": "System.Collections.Generic.ICollection",
+ "responseDictionaryType": "System.Collections.Generic.IDictionary",
+ "wrapResponses": false,
+ "wrapResponseMethods": [],
+ "generateResponseClasses": true,
+ "responseClass": "SwaggerResponse",
+ "namespace": "AdOrbitSDK",
+ "requiredPropertiesMustBeDefined": true,
+ "dateType": "System.DateTimeOffset",
+ "jsonConverters": null,
+ "anyType": "object",
+ "dateTimeType": "System.DateTimeOffset",
+ "timeType": "System.TimeSpan",
+ "timeSpanType": "System.TimeSpan",
+ "arrayType": "System.Collections.Generic.ICollection",
+ "arrayInstanceType": "System.Collections.ObjectModel.Collection",
+ "dictionaryType": "System.Collections.Generic.IDictionary",
+ "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
+ "arrayBaseType": "System.Collections.ObjectModel.Collection",
+ "dictionaryBaseType": "System.Collections.Generic.Dictionary",
+ "classStyle": "Poco",
+ "jsonLibrary": "NewtonsoftJson",
+ "generateDefaultValues": true,
+ "generateDataAnnotations": true,
+ "excludedTypeNames": [],
+ "excludedParameterNames": [
+ "Accept"
+ ],
+ "handleReferences": false,
+ "generateImmutableArrayProperties": false,
+ "generateImmutableDictionaryProperties": false,
+ "jsonSerializerSettingsTransformationMethod": null,
+ "inlineNamedArrays": false,
+ "inlineNamedDictionaries": false,
+ "inlineNamedTuples": true,
+ "inlineNamedAny": false,
+ "generateDtoTypes": true,
+ "generateOptionalPropertiesAsNullable": true,
+ "generateNullableReferenceTypes": false,
+ "templateDirectory": null,
+ "serviceHost": null,
+ "serviceSchemes": null,
+ "output": "StalwartClient.cs",
+ "newLineBehavior": "Auto",
+ "AllowAdditionalProperties": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/StalwartSDK/openapi.yml b/StalwartSDK/openapi.yml
new file mode 100644
index 0000000..6c0896a
--- /dev/null
+++ b/StalwartSDK/openapi.yml
@@ -0,0 +1,2590 @@
+openapi: 3.0.0
+info:
+ title: Stalwart Mail Server API
+ version: 1.0.0
+servers:
+ - url: https://mail.example.org/api
+ description: Sample server
+paths:
+ /oauth:
+ post:
+ summary: Obtain OAuth token
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ code:
+ type: string
+ permissions:
+ type: array
+ items:
+ type: string
+ version:
+ type: string
+ isEnterprise:
+ type: boolean
+ example:
+ data:
+ code: 4YmRFLu9Df1t4JO7Iffnuney4B8tVLAxjimdRxEg
+ permissions:
+ - webadmin-update
+ - spam-filter-update
+ - dkim-signature-get
+ - dkim-signature-create
+ - undelete
+ - fts-reindex
+ - purge-account
+ - purge-in-memory-store
+ - purge-data-store
+ - purge-blob-store
+ version: 0.11.0
+ isEnterprise: true
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ status:
+ type: number
+ title:
+ type: string
+ detail:
+ type: string
+ example:
+ type: about:blank
+ status: 401
+ title: Unauthorized
+ detail: You have to authenticate first.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ client_id:
+ type: string
+ redirect_uri:
+ type: string
+ nonce:
+ type: string
+ example:
+ type: code
+ client_id: webadmin
+ redirect_uri: stalwart://auth
+ nonce: ttsaXca3qx
+ /telemetry/metrics:
+ get:
+ summary: Fetch Telemetry Metrics
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ details:
+ type: string
+ reason:
+ type: string
+ example:
+ error: other
+ details: No metrics store has been defined
+ reason:
+ You need to configure a metrics store in order to use this
+ feature.
+ parameters:
+ - name: after
+ in: query
+ required: false
+ schema:
+ type: string
+ /telemetry/live/metrics-token:
+ get:
+ summary: Obtain Metrics Telemetry token
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: string
+ example:
+ data: 2GO4RahIkSAms6S00R9BRsroo97ZdYTz4QVxFCOwGrGkr7zguP0AVyTMA/iha3Vz/////w8DhZi1+ALBmLX4AndlYg==
+ /telemetry/metrics/live:
+ get:
+ summary: Live Metrics
+ responses:
+ "200":
+ description: OK
+ content: { }
+ parameters:
+ - name: metrics
+ in: query
+ required: false
+ schema:
+ type: string
+ - name: interval
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: token
+ in: query
+ required: false
+ schema:
+ type: string
+ /principal:
+ get:
+ summary: List Principals
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ example:
+ data:
+ items: [ ]
+ total: 0
+ parameters:
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: types
+ in: query
+ required: false
+ schema:
+ type: string
+ post:
+ summary: Create Principal
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: number
+ example:
+ data: 50
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ quota:
+ type: number
+ name:
+ type: string
+ description:
+ type: string
+ secrets:
+ type: array
+ items: { }
+ emails:
+ type: array
+ items: { }
+ urls:
+ type: array
+ items: { }
+ memberOf:
+ type: array
+ items: { }
+ roles:
+ type: array
+ items: { }
+ lists:
+ type: array
+ items: { }
+ members:
+ type: array
+ items: { }
+ enabledPermissions:
+ type: array
+ items: { }
+ disabledPermissions:
+ type: array
+ items: { }
+ externalMembers:
+ type: array
+ items: { }
+ example:
+ type: domain
+ quota: 0
+ name: example.org
+ description: Example domain
+ secrets: [ ]
+ emails: [ ]
+ urls: [ ]
+ memberOf: [ ]
+ roles: [ ]
+ lists: [ ]
+ members: [ ]
+ enabledPermissions: [ ]
+ disabledPermissions: [ ]
+ externalMembers: [ ]
+ /dkim:
+ post:
+ summary: Create DKIM Signature
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: object
+ nullable: true
+ algorithm:
+ type: string
+ domain:
+ type: string
+ selector:
+ type: object
+ nullable: true
+ example:
+ id:
+ algorithm: Ed25519
+ domain: example.org
+ selector:
+ /principal/{principal_id}:
+ get:
+ summary: Fetch Principal
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ id:
+ type: number
+ type:
+ type: string
+ secrets:
+ type: string
+ name:
+ type: string
+ quota:
+ type: number
+ description:
+ type: string
+ emails:
+ type: string
+ roles:
+ type: array
+ items:
+ type: string
+ lists:
+ type: array
+ items:
+ type: string
+ example:
+ data:
+ id: 90
+ type: individual
+ secrets: $6$ONjGT6nQtmPNaxw0$NNF5DXtPfOay2mfVnPJ0uQ77C.L3LNxXO/QMyphP/DzpODqbDBBGd4/gCnckYPQj3st6pqwY8/KeBsCJ.oe1Y1
+ name: jane
+ quota: 0
+ description: Jane Doe
+ emails: jane@example.org
+ roles:
+ - user
+ lists:
+ - all
+ parameters:
+ - name: principal_id
+ in: path
+ required: true
+ schema:
+ type: string
+ patch:
+ summary: Update Principal
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ parameters:
+ - name: principal_id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ action:
+ type: string
+ field:
+ type: string
+ value:
+ type: string
+ example:
+ - action: set
+ field: name
+ value: jane.doe
+ - action: set
+ field: description
+ value: Jane Mary Doe
+ - action: addItem
+ field: emails
+ value: jane-doe@example.org
+ - action: removeItem
+ field: emails
+ value: jane@example.org
+ delete:
+ summary: Delete Principal
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ parameters:
+ - name: principal_id
+ in: path
+ required: true
+ schema:
+ type: string
+ /queue/messages:
+ get:
+ summary: List Queued Messages
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ status:
+ type: boolean
+ example:
+ data:
+ items: [ ]
+ total: 0
+ status: true
+ parameters:
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: max-total
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: values
+ in: query
+ required: false
+ schema:
+ type: number
+ patch:
+ summary: Reschedule Queued Messages
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: true
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ schema:
+ type: string
+ delete:
+ summary: Delete Queued Messages
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: true
+ parameters:
+ - name: text
+ in: query
+ required: false
+ schema:
+ type: string
+ /queue/reports:
+ get:
+ summary: List Queued Reports
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ example:
+ data:
+ items: [ ]
+ total: 0
+ parameters:
+ - name: max-total
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ /reports/dmarc:
+ get:
+ summary: List Incoming DMARC Reports
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ example:
+ data:
+ items: [ ]
+ total: 0
+ parameters:
+ - name: max-total
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ /reports/tls:
+ get:
+ summary: List Incoming TLS Reports
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ example:
+ data:
+ items: [ ]
+ total: 0
+ parameters:
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: max-total
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ /reports/arf:
+ get:
+ summary: List Incoming ARF Reports
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ example:
+ data:
+ items: [ ]
+ total: 0
+ parameters:
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: max-total
+ in: query
+ required: false
+ schema:
+ type: number
+ /telemetry/traces:
+ get:
+ summary: List Stored Traces
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ details:
+ type: string
+ example:
+ error: unsupported
+ details: No tracing store has been configured
+ parameters:
+ - name: type
+ in: query
+ required: false
+ schema:
+ type: string
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: values
+ in: query
+ required: false
+ schema:
+ type: number
+ /logs:
+ get:
+ summary: Quere Log Files
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ type: object
+ properties:
+ timestamp:
+ type: string
+ level:
+ type: string
+ event:
+ type: string
+ event_id:
+ type: string
+ details:
+ type: string
+ total:
+ type: number
+ example:
+ data:
+ items:
+ - timestamp: "2025-01-05T14:06:29Z"
+ level: TRACE
+ event: HTTP request body
+ event_id: http.request-body
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, contents = "", size = 0
+ - timestamp: "2025-01-05T14:06:29Z"
+ level: TRACE
+ event: Write batch operation
+ event_id: store.data-write
+ details: elapsed = 0ms, total = 2
+ - timestamp: "2025-01-05T14:06:29Z"
+ level: TRACE
+ event: Expression evaluation result
+ event_id: eval.result
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, id = "server.http.allowed-endpoint", result
+ = "Integer(200)"
+ - timestamp: "2025-01-05T14:06:29Z"
+ level: DEBUG
+ event: HTTP request URL
+ event_id: http.request-url
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, url = "/api/logs?page=1&limit=50&"
+ - timestamp: "2025-01-05T14:06:23Z"
+ level: TRACE
+ event: HTTP response body
+ event_id: http.response-body
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, contents = "{"error":"unsupported","details":"No
+ tracing store has been configured"}", code = 200, size = 72
+ - timestamp: "2025-01-05T14:06:23Z"
+ level: DEBUG
+ event: Management operation not supported
+ event_id: manage.not-supported
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, details = No tracing store has been configured
+ - timestamp: "2025-01-05T14:06:23Z"
+ level: TRACE
+ event: HTTP request body
+ event_id: http.request-body
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, contents = "", size = 0
+ - timestamp: "2025-01-05T14:06:23Z"
+ level: TRACE
+ event: Write batch operation
+ event_id: store.data-write
+ details: elapsed = 0ms, total = 2
+ - timestamp: "2025-01-05T14:06:23Z"
+ level: TRACE
+ event: Expression evaluation result
+ event_id: eval.result
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, id = "server.http.allowed-endpoint", result
+ = "Integer(200)"
+ - timestamp: "2025-01-05T14:06:23Z"
+ level: DEBUG
+ event: HTTP request URL
+ event_id: http.request-url
+ details:
+ listenerId = "http", localPort = 1443, remoteIp = ::1,
+ remotePort = 57223, url = "/api/telemetry/traces?page=1&type=delivery.attempt-start&limit=10&values=1&"
+ total: 100
+ parameters:
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ /spam-filter/train/spam:
+ post:
+ summary: Train Spam Filter as Spam
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: string
+ example: "From: john@example.org\nTo: list@example.org\nSubject: Testing, please ignore\nContent-Type: text/plain; charset"
+ /spam-filter/train/ham:
+ post:
+ summary: Train Spam Filter as Ham
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: string
+ example: "From: john@example.org\nTo: list@example.org\nSubject: Testing, please ignore\nContent-Type: text/plain; charset"
+ /spam-filter/train/spam/{account_id}:
+ post:
+ summary: Train Account's Spam Filter as Spam
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: string
+ example: "From: john@example.org\nTo: list@example.org\nSubject: Testing, please ignore\nContent-Type: text/plain; charset"
+ /spam-filter/train/ham/{account_id}:
+ post:
+ summary: Train Account's Spam Filter as Ham
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: string
+ example: "From: john@example.org\nTo: list@example.org\nSubject: Testing, please ignore\nContent-Type: text/plain; charset"
+ /spam-filter/classify:
+ post:
+ summary: Test Spam Filter Classification
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ score:
+ type: number
+ tags:
+ type: object
+ properties:
+ FROM_NO_DN:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ SOURCE_ASN_15169:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ SOURCE_COUNTRY_US:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ MISSING_DATE:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ FROMHOST_NORES_A_OR_MX:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ MISSING_MIME_VERSION:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ FORGED_SENDER:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ SPF_NA:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ X_HDR_TO:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ HELO_IPREV_MISMATCH:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ X_HDR_CONTENT_TYPE:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ AUTH_NA:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ FORGED_RECIPIENTS:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ RBL_SENDERSCORE_REPUT_BLOCKED:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ RCVD_COUNT_ZERO:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ X_HDR_SUBJECT:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ X_HDR_FROM:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ RCPT_COUNT_ONE:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ MISSING_MID:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ TO_DOM_EQ_FROM_DOM:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ ARC_NA:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ RCVD_TLS_LAST:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ X_HDR_CONTENT_TRANSFER_ENCODING:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ HELO_NORES_A_OR_MX:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ TO_DN_NONE:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ FROM_NEQ_ENV_FROM:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ DMARC_NA:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ SINGLE_SHORT_PART:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ DKIM_NA:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: number
+ disposition:
+ type: object
+ properties:
+ action:
+ type: string
+ value:
+ type: string
+ example:
+ data:
+ score: 12.7
+ tags:
+ FROM_NO_DN:
+ action: allow
+ value: 0.0
+ SOURCE_ASN_15169:
+ action: allow
+ value: 0.0
+ SOURCE_COUNTRY_US:
+ action: allow
+ value: 0.0
+ MISSING_DATE:
+ action: allow
+ value: 1.0
+ FROMHOST_NORES_A_OR_MX:
+ action: allow
+ value: 1.5
+ MISSING_MIME_VERSION:
+ action: allow
+ value: 2.0
+ FORGED_SENDER:
+ action: allow
+ value: 0.3
+ SPF_NA:
+ action: allow
+ value: 0.0
+ X_HDR_TO:
+ action: allow
+ value: 0.0
+ HELO_IPREV_MISMATCH:
+ action: allow
+ value: 1.0
+ X_HDR_CONTENT_TYPE:
+ action: allow
+ value: 0.0
+ AUTH_NA:
+ action: allow
+ value: 1.0
+ FORGED_RECIPIENTS:
+ action: allow
+ value: 2.0
+ RBL_SENDERSCORE_REPUT_BLOCKED:
+ action: allow
+ value: 0.0
+ RCVD_COUNT_ZERO:
+ action: allow
+ value: 0.1
+ X_HDR_SUBJECT:
+ action: allow
+ value: 0.0
+ X_HDR_FROM:
+ action: allow
+ value: 0.0
+ RCPT_COUNT_ONE:
+ action: allow
+ value: 0.0
+ MISSING_MID:
+ action: allow
+ value: 2.5
+ TO_DOM_EQ_FROM_DOM:
+ action: allow
+ value: 0.0
+ ARC_NA:
+ action: allow
+ value: 0.0
+ RCVD_TLS_LAST:
+ action: allow
+ value: 0.0
+ X_HDR_CONTENT_TRANSFER_ENCODING:
+ action: allow
+ value: 0.0
+ HELO_NORES_A_OR_MX:
+ action: allow
+ value: 0.3
+ TO_DN_NONE:
+ action: allow
+ value: 0.0
+ FROM_NEQ_ENV_FROM:
+ action: allow
+ value: 0.0
+ DMARC_NA:
+ action: allow
+ value: 1.0
+ SINGLE_SHORT_PART:
+ action: allow
+ value: 0.0
+ DKIM_NA:
+ action: allow
+ value: 0.0
+ disposition:
+ action: allow
+ value:
+ "X-Spam-Result: ARC_NA (0.00),\r\n\tDKIM_NA (0.00),\r\n
+ \tFROM_NEQ_ENV_FROM (0.00),\r\n\tFROM_NO_DN (0.00),\r\n\tRBL_SENDERSCORE_REPUT_BLOCKED
+ (0.00),\r\n\tRCPT_COUNT_ONE (0.00),\r\n\tRCVD_TLS_LAST (0.00),\r
+ \n\tSINGLE_SHORT_PART (0.00),\r\n\tSPF_NA (0.00),\r\n\tTO_DN_NONE
+ (0.00),\r\n\tTO_DOM_EQ_FROM_DOM (0.00),\r\n\tRCVD_COUNT_ZERO
+ (0.10),\r\n\tFORGED_SENDER (0.30),\r\n\tHELO_NORES_A_OR_MX (0.30),\r
+ \n\tAUTH_NA (1.00),\r\n\tDMARC_NA (1.00),\r\n\tHELO_IPREV_MISMATCH
+ (1.00),\r\n\tMISSING_DATE (1.00),\r\n\tFROMHOST_NORES_A_OR_MX
+ (1.50),\r\n\tFORGED_RECIPIENTS (2.00),\r\n\tMISSING_MIME_VERSION
+ (2.00),\r\n\tMISSING_MID (2.50)\r\nX-Spam-Status: Yes, score=12.70\r\
+ \n"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ remoteIp:
+ type: string
+ ehloDomain:
+ type: string
+ authenticatedAs:
+ type: object
+ nullable: true
+ isTls:
+ type: boolean
+ envFrom:
+ type: string
+ envFromFlags:
+ type: number
+ envRcptTo:
+ type: array
+ items:
+ type: string
+ example:
+ message:
+ "From: john@example.org\nTo: list@example.org\nSubject: Testing,
+ please ignore\nContent-Type: text/plain; charset=\"utf-8\"\nContent-Transfer-Encoding:
+ 8bit\n\nTesting 1, 2, 3\n"
+ remoteIp: 8.8.8.8
+ ehloDomain: foo.org
+ authenticatedAs:
+ isTls: true
+ envFrom: bill@foo.org
+ envFromFlags: 0
+ envRcptTo:
+ - john@example.org
+ /troubleshoot/token:
+ get:
+ summary: Obtain a Troubleshooting Token
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: string
+ example:
+ data: +bS1rCUcrjoEtl9f7Vz1P6daqVs4nywxa56bHltPIASijRFrj1JrwvHxJCWphPKs/////w8E8p21+AKunrX4AndlYg==
+ /troubleshoot/delivery/{recipient}:
+ get:
+ summary: Run Delivery Troubleshooting
+ responses:
+ "200":
+ description: OK
+ content: { }
+ parameters:
+ - name: recipient
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: token
+ in: query
+ required: false
+ schema:
+ type: string
+ /troubleshoot/dmarc:
+ post:
+ summary: Run DMARC Troubleshooting
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ spfEhloDomain:
+ type: string
+ spfEhloResult:
+ type: object
+ properties:
+ type:
+ type: string
+ spfMailFromDomain:
+ type: string
+ spfMailFromResult:
+ type: object
+ properties:
+ type:
+ type: string
+ details:
+ type: object
+ nullable: true
+ ipRevResult:
+ type: object
+ properties:
+ type:
+ type: string
+ ipRevPtr:
+ type: array
+ items:
+ type: string
+ dkimResults:
+ type: array
+ items: { }
+ dkimPass:
+ type: boolean
+ arcResult:
+ type: object
+ properties:
+ type:
+ type: string
+ dmarcResult:
+ type: object
+ properties:
+ type:
+ type: string
+ dmarcPass:
+ type: boolean
+ dmarcPolicy:
+ type: string
+ elapsed:
+ type: number
+ example:
+ data:
+ spfEhloDomain: mx.google.com
+ spfEhloResult:
+ type: none
+ spfMailFromDomain: google.com
+ spfMailFromResult:
+ type: softFail
+ details:
+ ipRevResult:
+ type: pass
+ ipRevPtr:
+ - dns.google.
+ dkimResults: [ ]
+ dkimPass: false
+ arcResult:
+ type: none
+ dmarcResult:
+ type: none
+ dmarcPass: false
+ dmarcPolicy: reject
+ elapsed: 200
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ remoteIp:
+ type: string
+ ehloDomain:
+ type: string
+ mailFrom:
+ type: string
+ body:
+ type: string
+ example:
+ remoteIp: 8.8.8.8
+ ehloDomain: mx.google.com
+ mailFrom: john@google.com
+ body:
+ "From: john@example.org\nTo: list@example.org\nSubject: Testing,
+ please ignore\nContent-Type: text/plain; charset=\"utf-8\"\nContent-Transfer-Encoding:
+ 8bit\n\nTesting 1, 2, 3\n"
+ /reload:
+ get:
+ summary: Reload Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ warnings:
+ type: object
+ properties: { }
+ errors:
+ type: object
+ properties: { }
+ example:
+ data:
+ warnings: { }
+ errors: { }
+ parameters:
+ - name: dry-run
+ in: query
+ required: false
+ schema:
+ type: string
+ /update/spam-filter:
+ get:
+ summary: Update Spam Filter
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /update/webadmin:
+ get:
+ summary: Update WebAdmin
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /store/reindex:
+ get:
+ summary: Request FTS Reindex
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /store/purge/in-memory/default/bayes-global:
+ get:
+ summary: Delete Global Bayes Model
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /settings/keys:
+ get:
+ summary: List Settings by Key
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ lookup.default.hostname:
+ type: string
+ example:
+ data:
+ lookup.default.hostname: mx.fr.email
+ parameters:
+ - name: prefixes
+ in: query
+ required: false
+ schema:
+ type: string
+ - name: keys
+ in: query
+ required: false
+ schema:
+ type: string
+ /settings/group:
+ get:
+ summary: List Settings by Group
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ total:
+ type: number
+ items:
+ type: array
+ items:
+ type: object
+ properties:
+ _id:
+ type: string
+ bind:
+ type: string
+ protocol:
+ type: string
+ example:
+ data:
+ total: 11
+ items:
+ - _id: http
+ bind: "[::]:1443"
+ protocol: http
+ - bind: "[::]:443"
+ _id: https
+ protocol: http
+ tls.implicit: "true"
+ - protocol: imap
+ bind: "[::]:143"
+ _id: imap
+ - bind: "[::]:1143"
+ tls.implicit: "false"
+ _id: imapnotls
+ protocol: imap
+ proxy.override: "false"
+ tls.override: "false"
+ tls.enable: "false"
+ socket.override: "false"
+ - bind: "[::]:993"
+ tls.implicit: "true"
+ protocol: imap
+ _id: imaptls
+ - bind: "[::]:110"
+ protocol: pop3
+ _id: pop3
+ - tls.implicit: "true"
+ _id: pop3s
+ protocol: pop3
+ bind: "[::]:995"
+ - protocol: managesieve
+ _id: sieve
+ bind: "[::]:4190"
+ - bind: "[::]:25"
+ _id: smtp
+ protocol: smtp
+ - _id: submission
+ bind: "[::]:587"
+ protocol: smtp
+ parameters:
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: suffix
+ in: query
+ required: false
+ schema:
+ type: string
+ - name: prefix
+ in: query
+ required: false
+ schema:
+ type: string
+ /settings/list:
+ get:
+ summary: List Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ total:
+ type: number
+ items:
+ type: object
+ properties:
+ enable:
+ type: string
+ format:
+ type: string
+ limits.entries:
+ type: string
+ limits.entry-size:
+ type: string
+ limits.size:
+ type: string
+ refresh:
+ type: string
+ retry:
+ type: string
+ timeout:
+ type: string
+ url:
+ type: string
+ example:
+ data:
+ total: 9
+ items:
+ enable: "true"
+ format: list
+ limits.entries: "100000"
+ limits.entry-size: "512"
+ limits.size: "104857600"
+ refresh: 12h
+ retry: 1h
+ timeout: 30s
+ url: https://openphish.com/feed.txt
+ parameters:
+ - name: prefix
+ in: query
+ required: false
+ schema:
+ type: string
+ /settings:
+ post:
+ summary: Update Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ type:
+ type: string
+ prefix:
+ type: string
+ example:
+ - type: clear
+ prefix: spam-filter.rule.stwt_arc_signed.
+ /account/crypto:
+ get:
+ summary: Obtain Encryption-at-Rest Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ type:
+ type: string
+ example:
+ data:
+ type: disabled
+ post:
+ summary: Update Encryption-at-Rest Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: number
+ example:
+ data: 1
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ algo:
+ type: string
+ certs:
+ type: string
+ example:
+ type: pGP
+ algo: Aes256
+ certs:
+ "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=0TDv\n-----END PGP
+ PUBLIC KEY BLOCK-----\n\n\n"
+ /account/auth:
+ get:
+ summary: Obtain Account Authentication Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ otpEnabled:
+ type: boolean
+ appPasswords:
+ type: array
+ items: { }
+ example:
+ data:
+ otpEnabled: false
+ appPasswords: [ ]
+ post:
+ summary: Update Account Authentication Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ details:
+ type: string
+ reason:
+ type: object
+ nullable: true
+ example:
+ error: other
+ details: Fallback administrator accounts do not support 2FA or AppPasswords
+ reason:
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ status:
+ type: number
+ title:
+ type: string
+ detail:
+ type: string
+ example:
+ type: about:blank
+ status: 401
+ title: Unauthorized
+ detail: You have to authenticate first.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ type:
+ type: string
+ name:
+ type: string
+ password:
+ type: string
+ example:
+ - type: addAppPassword
+ name: dGVzdCQyMDI1LTAxLTA1VDE0OjEyOjUxLjg0NyswMDowMA==
+ password: $6$4M/5LmG7b13r0cdE$6zb.i6wJ3pAQHA2MRHkKg0t8bgSYb2IeqiIU115t.NugwW6VXifE0VKI5n2BQUNwdeDMUzaX82TmhuVVgC0Gx1
+ /reload/:
+ get:
+ summary: Reload Settings
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ warnings:
+ type: object
+ properties: { }
+ errors:
+ type: object
+ properties: { }
+ example:
+ data:
+ warnings: { }
+ errors: { }
+ /queue/status/stop:
+ patch:
+ summary: Stop Queue Processing
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: true
+ /queue/status/start:
+ patch:
+ summary: Resume Queue Processing
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: false
+ /queue/messages/{message_id}:
+ get:
+ summary: Obtain Queued Message Details
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ id:
+ type: number
+ return_path:
+ type: string
+ domains:
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ status:
+ type: string
+ recipients:
+ type: array
+ items:
+ type: object
+ properties:
+ address:
+ type: string
+ status:
+ type: string
+ retry_num:
+ type: number
+ next_retry:
+ type: string
+ next_notify:
+ type: string
+ expires:
+ type: string
+ created:
+ type: string
+ size:
+ type: number
+ blob_hash:
+ type: string
+ example:
+ data:
+ id: 217700302698266624
+ return_path: pepe@pepe.com
+ domains:
+ - name: example.org
+ status: scheduled
+ recipients:
+ - address: john@example.org
+ status: scheduled
+ retry_num: 0
+ next_retry: "2025-01-05T14:33:15Z"
+ next_notify: "2025-01-06T14:33:15Z"
+ expires: "2025-01-10T14:33:15Z"
+ created: "2025-01-05T14:33:15Z"
+ size: 1451
+ blob_hash: ykrZ_KghvdG2AdjH4AZajkSvZvcsxP_oI2HEZvw-tS0
+ "404":
+ description: Not Found
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ status:
+ type: number
+ title:
+ type: string
+ detail:
+ type: string
+ example:
+ type: about:blank
+ status: 404
+ title: Not Found
+ detail: The requested resource does not exist on this server.
+ parameters:
+ - name: message_id
+ in: path
+ required: true
+ schema:
+ type: string
+ patch:
+ summary: Reschedule Delivery of Queued Message
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: true
+ parameters:
+ - name: message_id
+ in: path
+ required: true
+ schema:
+ type: string
+ delete:
+ summary: Cancel Delivery of Queued Message
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: true
+ parameters:
+ - name: message_id
+ in: path
+ required: true
+ schema:
+ type: string
+ /store/blobs/{blob_id}:
+ get:
+ summary: Fetch Blob by ID
+ responses:
+ "200":
+ description: OK
+ content: { }
+ parameters:
+ - name: blob_id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ /telemetry/trace/{trace_id}:
+ get:
+ summary: Obtain Trace Details
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ properties:
+ text:
+ type: string
+ details:
+ type: string
+ createdAt:
+ type: string
+ type:
+ type: string
+ data:
+ type: object
+ properties:
+ listenerId:
+ type: string
+ localPort:
+ type: number
+ remoteIp:
+ type: string
+ remotePort:
+ type: number
+ example:
+ data:
+ - text: SMTP connection started
+ details: A new SMTP connection was started
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.connection-start
+ data:
+ listenerId: smtp
+ localPort: 25
+ remoteIp: ::1
+ remotePort: 57513
+ - text: SMTP EHLO command
+ details: The remote server sent an EHLO command
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.ehlo
+ data:
+ domain: test.eml
+ - text: SPF EHLO check failed
+ details: EHLO identity failed SPF check
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.spf-ehlo-fail
+ data:
+ domain: test.eml
+ result:
+ type: spf.none
+ text: No SPF record
+ details: No SPF record was found
+ data: { }
+ elapsed: 24
+ - text: IPREV check passed
+ details: Reverse IP check passed
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.iprev-pass
+ data:
+ domain: test.eml
+ result:
+ type: iprev.pass
+ text: IPREV check passed
+ details: The IPREV check has passed
+ data:
+ details:
+ - localhost.
+ elapsed: 0
+ - text: SPF From check failed
+ details: MAIL FROM identity failed SPF check
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.spf-from-fail
+ data:
+ domain: test.eml
+ from: pepe@pepe.com
+ result:
+ type: spf.none
+ text: No SPF record
+ details: No SPF record was found
+ data: { }
+ elapsed: 18
+ - text: SMTP MAIL FROM command
+ details: The remote client sent a MAIL FROM command
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.mail-from
+ data:
+ from: pepe@pepe.com
+ - text: SMTP RCPT TO command
+ details: The remote client sent an RCPT TO command
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.rcpt-to
+ data:
+ to: john@example.org
+ - text: DKIM verification failed
+ details: Failed to verify DKIM signature
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.dkim-fail
+ data:
+ strict: false
+ result: [ ]
+ elapsed: 0
+ - text: ARC verification passed
+ details: Successful ARC verification
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.arc-pass
+ data:
+ strict: false
+ result:
+ type: dkim.none
+ text: No DKIM signature
+ details: No DKIM signature was found
+ data: { }
+ elapsed: 0
+ - text: DMARC check failed
+ details: Failed to verify DMARC policy
+ createdAt: "2025-01-05T14:34:50Z"
+ type: smtp.dmarc-fail
+ data:
+ strict: false
+ domain: example.org
+ policy: reject
+ result:
+ type: dmarc.none
+ text: No DMARC record
+ details: No DMARC record was found
+ data: { }
+ elapsed: 0
+ parameters:
+ - name: trace_id
+ in: path
+ required: true
+ schema:
+ type: string
+ /telemetry/live/tracing-token:
+ get:
+ summary: Request a Tracing Token
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: string
+ example:
+ data: VLxkixOwgDF8Frj0wi8kPhx3SpzKqtsDvbo25wgKw2tBIz/O8La0dwioQw9pN11c/////w8Ctau1+ALxq7X4AndlYg==
+ /telemetry/traces/live:
+ get:
+ summary: Start Live Tracing
+ responses:
+ "200":
+ description: OK
+ content: { }
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ schema:
+ type: string
+ - name: token
+ in: query
+ required: false
+ schema:
+ type: string
+ /dns/records/{domain}:
+ get:
+ summary: Obtain DNS Records for Domain
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ properties:
+ type:
+ type: string
+ name:
+ type: string
+ content:
+ type: string
+ example:
+ data:
+ - type: MX
+ name: example.org.
+ content: 10 mx.fr.email.
+ - type: CNAME
+ name: mail.example.org.
+ content: mx.fr.email.
+ - type: TXT
+ name: 202501e._domainkey.example.org.
+ content: v=DKIM1; k=ed25519; h=sha256; p=82LqzMGRHEBI2HGDogjojWGz+Crrv0TAi8pcaOBd1vw=
+ - type: TXT
+ name: 202501r._domainkey.example.org.
+ content: v=DKIM1; k=rsa; h=sha256;
+ p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1qtCbIlrZffIqm7gHqpihPUlxOq1zD6K3j1RO/enhkZRp5dEdCqcLbyFk5d+rqRsVIWwUZiU4HXHWqMTN1hlKojUlzmU1JYtlHRMwtM5vN4mzG4x1KA0i8ZHxkahE8ITsP+kPByDF9x0vAySHXpyErNXq3BeFyu/VW+6X+fmUW6x39PfWq7kQQTcwU0Ogo447oJfmAX9H4Z+/cD5WJVNiLgvLY6faVgoXm0mJJjRU5xoEStXoUcKwrwbl7G3K7JfxtmWsgEn97auV6v4he2LRRfTxbY9smkqUtcJs61E9iyyYroJv0iRda2pv71qg8e4wTb2sqBloZv/F2FZQhM+wIDAQAB
+ - type: TXT
+ name: example.org.
+ content: v=spf1 mx ra=postmaster -all
+ - type: SRV
+ name: _jmap._tcp.example.org.
+ content: 0 1 443 mx.fr.email.
+ - type: SRV
+ name: _imaps._tcp.example.org.
+ content: 0 1 993 mx.fr.email.
+ - type: SRV
+ name: _imap._tcp.example.org.
+ content: 0 1 143 mx.fr.email.
+ - type: SRV
+ name: _imap._tcp.example.org.
+ content: 0 1 1143 mx.fr.email.
+ - type: SRV
+ name: _pop3s._tcp.example.org.
+ content: 0 1 995 mx.fr.email.
+ parameters:
+ - name: domain
+ in: path
+ required: true
+ schema:
+ type: string
+ /store/purge/account/{account_id}:
+ get:
+ summary: Purge Account
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
+ /store/purge/in-memory/default/bayes-account/{account_id}:
+ get:
+ summary: Delete Bayes Model for Account
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
+ /store/undelete/{account_id}:
+ get:
+ summary: List Deleted Messages
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ items:
+ type: array
+ items: { }
+ total:
+ type: number
+ example:
+ data:
+ items: [ ]
+ total: 0
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: number
+ post:
+ summary: Undelete Messages
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ properties:
+ type:
+ type: string
+ example:
+ data:
+ - type: success
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ hash:
+ type: string
+ collection:
+ type: string
+ restoreTime:
+ type: string
+ cancelDeletion:
+ type: string
+ example:
+ - hash: 9pDYGrkDlLYuBNl062qhi0wStnDYyq4ZWalnj2vXbLY
+ collection: email
+ restoreTime: "2025-01-05T14:50:13Z"
+ cancelDeletion: "2025-02-04T14:50:13Z"
+ /queue/status:
+ get:
+ summary: Obtain Queue Status
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: boolean
+ example:
+ data: true
+ /store/purge/blob:
+ get:
+ summary: Purge Blob Store
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /store/purge/data:
+ get:
+ summary: Purge Data Store
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /store/purge/in-memory:
+ get:
+ summary: Purge In-Memory Store
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /store/purge/account:
+ get:
+ summary: Purge All Accounts
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: object
+ nullable: true
+ example:
+ data:
+ /store/uids/{account_id}:
+ delete:
+ summary: Reset IMAP UIDs for Account
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ type: number
+ example:
+ data:
+ - 0
+ - 0
+ parameters:
+ - name: account_id
+ in: path
+ required: true
+ schema:
+ type: string
diff --git a/StalwartSimpleLoginMiddleware/Constants/EnvironmentVariable.cs b/StalwartSimpleLoginMiddleware/Constants/EnvironmentVariable.cs
new file mode 100644
index 0000000..69e7c84
--- /dev/null
+++ b/StalwartSimpleLoginMiddleware/Constants/EnvironmentVariable.cs
@@ -0,0 +1,10 @@
+namespace StalwartSimpleLoginMiddleware.Constants;
+
+public class EnvironmentVariable
+{
+ public const string MailServerUri = "MAIL_SERVER_URI";
+ public const string MailServerApiKey = "MAIL_SERVER_API_KEY";
+ public const string PostgresUrl = "POSTGRES_URL";
+ public const string PostgresMinPoolSize = "POSTGRES_MIN_POOL_SIZE";
+ public const string PostgresMaxPoolSize = "POSTGRES_MAX_POOL_SIZE";
+}
\ No newline at end of file
diff --git a/StalwartSimpleLoginMiddleware/Contexts/ApiKeyContext.cs b/StalwartSimpleLoginMiddleware/Contexts/ApiKeyContext.cs
new file mode 100644
index 0000000..83d3ddc
--- /dev/null
+++ b/StalwartSimpleLoginMiddleware/Contexts/ApiKeyContext.cs
@@ -0,0 +1,146 @@
+using Microsoft.EntityFrameworkCore;
+using Npgsql;
+using StalwartSimpleLoginMiddleware.Constants;
+using StalwartSimpleLoginMiddleware.Entities;
+using StalwartSimpleLoginMiddleware.Utilities;
+
+namespace StalwartSimpleLoginMiddleware.Contexts;
+
+public class ApiKeyContext : DbContext
+{
+ public DbSet ApiKeys { get; set; }
+ public DbSet Members { get; set; }
+
+ public ApiKeyContext()
+ {
+ }
+
+ public ApiKeyContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ ConfigureOptions(optionsBuilder);
+ }
+
+ public static DbContextOptionsBuilder ConfigureOptions(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (optionsBuilder.IsConfigured) return optionsBuilder;
+
+ var connectionString = Environment.GetEnvironmentVariable(EnvironmentVariable.PostgresUrl);
+
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ // Default connection
+ var defaultConnectionString = "Host=localhost;Port=5432;Database=postgres;";
+ optionsBuilder.UseNpgsql(defaultConnectionString,
+ nsqlOptions => nsqlOptions.MigrationsAssembly("StalwartSimpleLoginMiddleware"));
+ return optionsBuilder;
+ }
+
+ var pgConnectionString = ConnectionHelper.GetPostgresConnectionString(connectionString);
+
+ // Configure pooling
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(pgConnectionString)
+ {
+ MinPoolSize = 10
+ };
+ if (int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariable.PostgresMinPoolSize),
+ out var minPoolSize))
+ {
+ connectionStringBuilder.MinPoolSize = minPoolSize;
+ }
+
+ if (int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariable.PostgresMaxPoolSize),
+ out var maxPoolSize))
+ {
+ connectionStringBuilder.MaxPoolSize = maxPoolSize;
+ }
+
+ var pooledConnectionString = connectionStringBuilder.ToString();
+
+ optionsBuilder.UseLazyLoadingProxies()
+ .UseNpgsql(pooledConnectionString,
+ nsqlOptions => nsqlOptions.MigrationsAssembly("StalwartSimpleLoginMiddleware"));
+ return optionsBuilder;
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(a => a.Key); // Primary key
+
+ // Members navigation
+ entity.HasMany(a => a.Members)
+ .WithOne(m => m.ApiKey)
+ .HasForeignKey(m => m.ApiKeyId)
+ .IsRequired()
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure Member entity
+ modelBuilder.Entity(entity =>
+ {
+ // Composite key: ApiKeyId + Email
+ entity.HasKey(m => new { m.ApiKeyId, m.Email });
+
+ // Configure foreign key relationship with ApiKey
+ entity.HasOne(m => m.ApiKey)
+ .WithMany(a => a.Members)
+ .HasForeignKey(m => m.ApiKeyId)
+ .IsRequired()
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+ }
+
+ // Method to check and apply migrations
+ public async Task EnsureDatabaseMigrated(ILogger logger)
+ {
+ logger.LogInformation("Checking available migrations...");
+
+ // Get all migrations and log them
+ var migrations = Database.GetMigrations().ToList();
+ logger.LogInformation($"Total migrations found: {migrations.Count}");
+
+ foreach (var migration in migrations)
+ {
+ logger.LogInformation($"Migration: {migration}");
+ }
+
+ // Check applied migrations
+ var pendingMigrations = (await Database.GetPendingMigrationsAsync()).ToArray();
+ var appliedMigrations = (await Database.GetAppliedMigrationsAsync()).ToArray();
+
+ logger.LogInformation($"Applied migrations: {string.Join(", ", appliedMigrations)}");
+
+ if (pendingMigrations.Any())
+ {
+ logger.LogInformation($"Applying migrations: {string.Join(", ", pendingMigrations)}");
+ await Database.MigrateAsync();
+ logger.LogInformation("Database migrated.");
+
+ if (appliedMigrations.Length == 0)
+ {
+ // Create Admin key on first migration
+ var apiKey = ApiKeyHelper.GenerateKey();
+ ApiKeys.Add(new ApiKey
+ {
+ Key = apiKey,
+ OwnerEmail = "admin@domain.tld",
+ IsAdmin = true
+ });
+ await SaveChangesAsync();
+
+ Console.WriteLine($"Generated Admin API Key: {apiKey}");
+ }
+ }
+ else
+ {
+ logger.LogInformation("No pending migrations found.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/StalwartSimpleLoginMiddleware/Controllers/AdminController.cs b/StalwartSimpleLoginMiddleware/Controllers/AdminController.cs
new file mode 100644
index 0000000..0d1fc29
--- /dev/null
+++ b/StalwartSimpleLoginMiddleware/Controllers/AdminController.cs
@@ -0,0 +1,109 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using StalwartSimpleLoginMiddleware.Contexts;
+using StalwartSimpleLoginMiddleware.Entities;
+using StalwartSimpleLoginMiddleware.Models;
+using StalwartSimpleLoginMiddleware.Utilities;
+
+namespace StalwartSimpleLoginMiddleware.Controllers;
+
+[ApiController]
+[Authorize(Roles = "Admin")]
+[Route("api/[controller]/[action]")]
+public class AdminController : ControllerBase
+{
+ private readonly ApiKeyContext context;
+
+ public AdminController(ApiKeyContext context)
+ {
+ this.context = context;
+ }
+
+ [HttpGet]
+ public async Task ListApiKeys([FromQuery] int page = 0, [FromQuery] int limit = 100)
+ {
+ return await context.ApiKeys
+ .Include(apiKey => apiKey.Members)
+ .Skip(page * limit)
+ .Take(limit).ToArrayAsync();
+ }
+
+ [HttpGet]
+ public async Task GetApiKey([FromQuery] string key)
+ {
+ var apiKey = await context.ApiKeys
+ .Include(apiKey => apiKey.Members)
+ .Select(apiKey => new ApiKey
+ {
+ Key = apiKey.Key,
+ OwnerEmail = apiKey.OwnerEmail,
+ IsAdmin = apiKey.IsAdmin,
+ Members = apiKey.Members.ToArray()
+ })
+ .FirstOrDefaultAsync(apiKey => apiKey.Key == key);
+ if (apiKey == null) throw new BadHttpRequestException("API Key is invalid.");
+
+ return apiKey;
+ }
+
+ [HttpPost]
+ public async Task UpdateApiKeyOwnerEmail([FromBody] UpdateOwnerEmailInput input)
+ {
+ var rows = await context.ApiKeys.Where(apiKey => apiKey.Key == input.ApiKey)
+ .ExecuteUpdateAsync(apiKey => apiKey.SetProperty(p => p.OwnerEmail, input.OwnerEmail));
+ if (rows == 0) return NotFound();
+ return Ok();
+ }
+
+ [HttpPost]
+ public async Task CreateApiKey([FromBody] ApiKeyInput newApiKeyInput)
+ {
+ if (string.IsNullOrEmpty(ApiKeyHelper.GetEmailDomain(newApiKeyInput.OwnerEmail)))
+ return BadRequest("Owner Email must be a valid email address.");
+ var apiKey = new ApiKey
+ {
+ Key = ApiKeyHelper.GenerateKey(),
+ OwnerEmail = newApiKeyInput.OwnerEmail,
+ IsAdmin = newApiKeyInput.IsAdmin,
+ Members = newApiKeyInput.Members.Select(m => new Member { Email = m.Email, IsExternal = m.IsExternal })
+ .ToArray()
+ };
+ context.ApiKeys.Add(apiKey);
+ await context.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetApiKey), new { key = apiKey.Key }, apiKey);
+ }
+
+ [HttpPost]
+ public async Task CreateApiKeyMember([FromBody] AddApiKeyMemberInput input)
+ {
+ var member = new Member
+ {
+ ApiKeyId = input.ApiKey,
+ Email = input.Member.Email,
+ IsExternal = input.Member.IsExternal
+ };
+ context.Members.Add(member);
+ await context.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetApiKey), new { key = input.ApiKey });
+ }
+
+ [HttpDelete]
+ public async Task DeleteApiKey([FromQuery] string key)
+ {
+ var rows = await context.ApiKeys.Where(apiKey => apiKey.Key == key)
+ .ExecuteDeleteAsync();
+ if (rows == 0) return NotFound();
+ return Ok();
+ }
+
+ [HttpDelete]
+ public async Task DeleteApiKeyMemberEmail([FromQuery] string key, [FromQuery] string email)
+ {
+ var rows = await context.Members.Where(member => member.ApiKeyId == key)
+ .Where(member => member.Email == email)
+ .ExecuteDeleteAsync();
+ if (rows == 0) return NotFound();
+ return Ok();
+ }
+}
\ No newline at end of file
diff --git a/StalwartSimpleLoginMiddleware/Controllers/AliasController.cs b/StalwartSimpleLoginMiddleware/Controllers/AliasController.cs
new file mode 100644
index 0000000..39459e9
--- /dev/null
+++ b/StalwartSimpleLoginMiddleware/Controllers/AliasController.cs
@@ -0,0 +1,56 @@
+using AdOrbitSDK;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using StalwartSimpleLoginMiddleware.Models;
+using StalwartSimpleLoginMiddleware.Services;
+
+namespace StalwartSimpleLoginMiddleware.Controllers;
+
+[ApiController]
+[Authorize]
+[Route("api/[controller]")]
+public class AliasController(StalwartClient stalwartClient) : ControllerBase
+{
+ [Route("random/new")]
+ [HttpPost]
+ public async Task NewRandomAlias([FromQuery] string? hostname,
+ [FromQuery] string? mode,
+ [FromBody] NewRandomAliasInput body)
+ {
+ var apiKeyAccessor = HttpContext.RequestServices.GetRequiredService();
+ var randomAlias = Get8CharacterRandomString();
+ var client = await stalwartClient.GetClient();
+ var requestBody = new Body2
+ {
+ Type = "list",
+ Name = randomAlias,
+ Description = body.Note,
+ Emails = new List