feat: implement provider #5

Merged
Sebastian merged 1 commits from feat/implement-provider into main 2026-03-28 16:44:09 +00:00
39 changed files with 2267 additions and 113 deletions
Showing only changes of commit 4c948d177a - Show all commits

View File

@@ -1,42 +0,0 @@
# IMAP Mail Provider Module
This module provides an implementation of an IMAP mail provider using the `gricob/imap` library. It is designed to facilitate email operations such as managing mailboxes and messages through the IMAP protocol.
## Features
- **Service Location**: Configures connection details including host, port, and encryption type.
- **Service Identity**: Manages user credentials securely.
- **Mailbox Management**: Supports operations for listing, fetching, creating, modifying, and deleting mailboxes and messages.
- **Autodiscovery**: Implements methods for discovering IMAP services automatically.
## Installation
To install the module, run the following command in the module directory:
```bash
composer install
```
This will install the required dependencies, including `gricob/imap`.
## Usage
1. **Service Test**: Use the `serviceTest()` method to check connectivity to the IMAP server.
2. **Discover Services**: Call `serviceDiscover()` to find available mail services.
3. **Mailbox Operations**: Utilize the `RemoteMailService` to perform operations such as listing mailboxes and managing messages.
## Example
```php
$provider = new KTXM\ProviderImapMail\Providers\Provider();
$provider->serviceTest();
$mailboxes = $provider->serviceDiscover();
```
## Contributing
Contributions are welcome! Please submit a pull request or open an issue for any enhancements or bug fixes.
## License
This project is licensed under the MIT License. See the LICENSE file for details.

View File

@@ -1,5 +1,5 @@
{
"name": "ktxm/provider-imap-mail",
"name": "ktxm/provider-imap",
"description": "IMAP Mail Provider Module",
"type": "library",
"minimum-stability": "stable",
@@ -9,7 +9,7 @@
"platform": {
"php": "8.2"
},
"autoloader-suffix": "ProviderImapMail",
"autoloader-suffix": "ProviderImap",
"vendor-dir": "lib/vendor",
"allow-plugins": {
"bamarni/composer-bin-plugin": true
@@ -27,13 +27,13 @@
},
"autoload": {
"psr-4": {
"KTXM\\ProviderImapMail\\": "lib/",
"KTXM\\ProviderImap\\": "lib/",
"Gricob\\IMAP\\": "lib/Client"
}
},
"autoload-dev": {
"psr-4": {
"KTXT\\ProviderImapMail\\": "tests/php/"
"KTXM\\ProviderImap\\": "tests/php/"
}
},
"scripts": {

View File

@@ -35,8 +35,8 @@ final readonly class TraceableResponseStream implements ResponseStream
private function debug(string $data): void
{
$data = addslashes($data);
$data = str_replace("\r\n", "\\r\\n", $data);
// $data = addslashes($data);
// $data = str_replace("\r\n", "\\r\\n", $data);
$this->logger->debug($data);
}

View File

@@ -7,12 +7,12 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Console;
namespace KTXM\ProviderImap\Console;
use KTXM\ProviderImapMail\Providers\Provider;
use KTXM\ProviderImapMail\Providers\Service;
use KTXM\ProviderImapMail\Providers\ServiceIdentityBasic;
use KTXM\ProviderImapMail\Providers\ServiceLocation;
use KTXM\ProviderImap\Providers\Provider;
use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
use KTXM\ProviderImap\Providers\ServiceLocation;
use KTXC\SessionTenant;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

View File

@@ -7,10 +7,10 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Console;
namespace KTXM\ProviderImap\Console;
use KTXM\ProviderImapMail\Providers\Provider;
use KTXM\ProviderImapMail\Providers\Service;
use KTXM\ProviderImap\Providers\Provider;
use KTXM\ProviderImap\Providers\Service;
use KTXC\SessionTenant;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

View File

@@ -7,14 +7,14 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Console;
namespace KTXM\ProviderImap\Console;
use KTXM\ProviderImapMail\Providers\Provider;
use KTXM\ProviderImapMail\Providers\Service;
use KTXM\ProviderImapMail\Providers\ServiceIdentityBasic;
use KTXM\ProviderImapMail\Providers\ServiceLocation;
use KTXM\ProviderImapMail\Service\Discovery;
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
use KTXM\ProviderImap\Providers\Provider;
use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
use KTXM\ProviderImap\Providers\ServiceLocation;
use KTXM\ProviderImap\Service\Discovery;
use KTXM\ProviderImap\Service\Remote\RemoteService;
use KTXC\SessionTenant;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

View File

@@ -7,11 +7,11 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Console;
namespace KTXM\ProviderImap\Console;
use KTXM\ProviderImapMail\Providers\Provider;
use KTXM\ProviderImapMail\Providers\Service;
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
use KTXM\ProviderImap\Providers\Provider;
use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Service\Remote\RemoteService;
use KTXC\SessionTenant;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

View File

@@ -7,18 +7,18 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail;
namespace KTXM\ProviderImap;
use KTXC\Resource\ProviderManager;
use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\ModuleConsoleInterface;
use KTXF\Module\ModuleInstanceAbstract;
use KTXF\Resource\Provider\ProviderInterface;
use KTXM\ProviderImapMail\Console\ServiceConnectCommand;
use KTXM\ProviderImapMail\Console\ServiceDiscoverCommand;
use KTXM\ProviderImapMail\Console\ServiceDisconnectCommand;
use KTXM\ProviderImapMail\Console\ServiceTestCommand;
use KTXM\ProviderImapMail\Providers\Provider as MailProvider;
use KTXM\ProviderImap\Console\ServiceConnectCommand;
use KTXM\ProviderImap\Console\ServiceDiscoverCommand;
use KTXM\ProviderImap\Console\ServiceDisconnectCommand;
use KTXM\ProviderImap\Console\ServiceTestCommand;
use KTXM\ProviderImap\Providers\Provider as MailProvider;
/**
* IMAP Mail Provider Module
@@ -33,7 +33,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
public function handle(): string
{
return 'provider_imap_mail';
return 'provider_imap';
}
public function label(): string
@@ -59,7 +59,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
public function permissions(): array
{
return [
'provider_imap_mail' => [
'provider_imap' => [
'label' => 'Access IMAP Mail Provider',
'description' => 'View and access the IMAP mail provider module',
'group' => 'Providers',
@@ -86,7 +86,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
{
return [
'handle' => $this->handle(),
'namespace' => 'ProviderImapMail',
'namespace' => 'ProviderImap',
'version' => $this->version(),
'label' => $this->label(),
'author' => $this->author(),

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use Gricob\IMAP\Mailbox;
use KTXF\Mail\Collection\CollectionPropertiesMutableAbstract;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use Gricob\IMAP\Mailbox;
use KTXF\Mail\Collection\CollectionMutableAbstract;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use DateTimeInterface;
use Gricob\IMAP\Mime\Part\Part;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
/**
* Mail Attachment Object

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\MultiPart;
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\Part;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use DateTimeImmutable;
use DateTimeInterface;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use KTXF\Mail\Provider\ProviderBaseInterface;
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
@@ -16,9 +16,9 @@ use KTXF\Mail\Provider\ProviderServiceTestInterface;
use KTXF\Mail\Service\ServiceBaseInterface;
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
use KTXF\Resource\Provider\ResourceServiceMutateInterface;
use KTXM\ProviderImapMail\Service\Discovery;
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
use KTXM\ProviderImapMail\Stores\ServiceStore;
use KTXM\ProviderImap\Service\Discovery;
use KTXM\ProviderImap\Service\Remote\RemoteService;
use KTXM\ProviderImap\Stores\ServiceStore;
/**
* IMAP Mail Provider

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use Generator;
use KTXF\Mail\Collection\CollectionBaseInterface;
@@ -29,10 +29,10 @@ use KTXF\Resource\Range\RangeTally;
use KTXF\Resource\Range\RangeType;
use KTXF\Resource\Sort\ISort;
use KTXF\Resource\Sort\Sort;
use KTXM\ProviderImapMail\Providers\ServiceIdentityBasic;
use KTXM\ProviderImapMail\Providers\ServiceLocation;
use KTXM\ProviderImapMail\Service\Remote\RemoteMailService;
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
use KTXM\ProviderImap\Providers\ServiceLocation;
use KTXM\ProviderImap\Service\Remote\RemoteMailService;
use KTXM\ProviderImap\Service\Remote\RemoteService;
/**
* IMAP Mail Service

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use KTXF\Resource\Provider\ResourceServiceIdentityBasic;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
use Gricob\IMAP\Configuration;
use KTXF\Resource\Provider\ResourceServiceLocationInterface;

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
namespace KTXM\ProviderImap\Providers;
enum ServiceMode: string
{

View File

@@ -7,11 +7,11 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Service\Cache;
namespace KTXM\ProviderImap\Service\Cache;
use KTXM\ProviderImapMail\Providers\CollectionResource;
use KTXM\ProviderImapMail\Providers\EntityResource;
use KTXM\ProviderImapMail\Stores\MessageStore;
use KTXM\ProviderImap\Providers\CollectionResource;
use KTXM\ProviderImap\Providers\EntityResource;
use KTXM\ProviderImap\Stores\MessageStore;
/**
* Cache Mail Service

View File

@@ -7,12 +7,12 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Service\Cache;
namespace KTXM\ProviderImap\Service\Cache;
use KTXM\ProviderImapMail\Providers\CollectionResource;
use KTXM\ProviderImapMail\Providers\EntityResource;
use KTXM\ProviderImapMail\Service\Remote\RemoteMailService;
use KTXM\ProviderImapMail\Stores\MessageStore;
use KTXM\ProviderImap\Providers\CollectionResource;
use KTXM\ProviderImap\Providers\EntityResource;
use KTXM\ProviderImap\Service\Remote\RemoteMailService;
use KTXM\ProviderImap\Stores\MessageStore;
/**
* Cache Service — IMAP Sync Orchestrator

View File

@@ -7,9 +7,9 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Service;
namespace KTXM\ProviderImap\Service;
use KTXM\ProviderImapMail\Providers\ServiceLocation;
use KTXM\ProviderImap\Providers\ServiceLocation;
/**
* IMAP Service Discovery

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Service\Remote;
namespace KTXM\ProviderImap\Service\Remote;
use DateTimeImmutable;
use Generator;
@@ -28,8 +28,8 @@ use KTXF\Resource\Filter\IFilter;
use KTXF\Resource\Range\IRange;
use KTXF\Resource\Range\RangeAnchorType;
use KTXF\Resource\Range\RangeTally;
use KTXM\ProviderImapMail\Providers\CollectionResource;
use KTXM\ProviderImapMail\Providers\EntityResource;
use KTXM\ProviderImap\Providers\CollectionResource;
use KTXM\ProviderImap\Providers\EntityResource;
/**
* IMAP Remote Mail Service

View File

@@ -7,12 +7,12 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Service\Remote;
namespace KTXM\ProviderImap\Service\Remote;
use Gricob\IMAP\Client;
use KTXC\Server;
use KTXC\Logger\PlainFileLogger;
use KTXM\ProviderImapMail\Providers\Service;
use KTXM\ProviderImap\Providers\Service;
/**
* Static factory for IMAP remote service objects.

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Stores;
namespace KTXM\ProviderImap\Stores;
use KTXC\Db\DataStore;

View File

@@ -7,12 +7,12 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Stores;
namespace KTXM\ProviderImap\Stores;
use KTXC\Db\DataStore;
use KTXF\Security\Crypto;
use KTXF\Utile\UUID;
use KTXM\ProviderImapMail\Providers\Service;
use KTXM\ProviderImap\Providers\Service;
/**
* IMAP Service Store

1599
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "provider_imap",
"description": "Ktrix IMAP Provider Module",
"version": "1.0.0",
"private": true,
"license": "AGPL-3.0-or-later",
"author": "Ktrix",
"type": "module",
"scripts": {
"build": "vite build --mode production --config vite.config.ts",
"dev": "vite build --mode development --config vite.config.ts",
"watch": "vite build --mode development --watch --config vite.config.ts",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"pinia": "^2.3.1",
"vue": "^3.5.18",
"vue-router": "^4.5.1",
"vuetify": "^3.10.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3",
"vite": "^7.1.2",
"vue-tsc": "^3.0.5"
}
}

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
const props = defineProps<ProviderAuthPanelProps>()
const emit = defineEmits<ProviderAuthPanelEmits>()
const identity = ref(props.prefilledIdentity || props.emailAddress || '')
const secret = ref(props.prefilledSecret || '')
const rules = {
required: (value: unknown) => !!value || 'This field is required'
}
const isValid = computed(() => !!identity.value && !!secret.value)
const currentIdentity = computed((): ServiceIdentity | null => {
if (!isValid.value) {
return null
}
return {
type: 'BA',
identity: identity.value,
secret: secret.value,
}
})
watch(
currentIdentity,
value => {
if (value) {
emit('update:modelValue', value)
}
},
{ immediate: true, deep: true }
)
watch(
isValid,
value => {
emit('valid', value)
},
{ immediate: true }
)
watch(
() => props.modelValue,
value => {
if (value?.type === 'BA') {
identity.value = value.identity || ''
secret.value = value.secret || ''
}
}
)
watch(
() => props.emailAddress,
value => {
if (value && !identity.value) {
identity.value = value
}
},
{ immediate: true }
)
</script>
<template>
<div class="imap-auth-panel">
<h3 class="text-h6 mb-4">Authentication</h3>
<p class="text-body-2 mb-6">Provide the username and password your IMAP server expects.</p>
<v-alert type="info" variant="tonal" class="mb-4">
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-caption">
Most IMAP servers use your full email address as the username. Use an app password if your mail host requires one.
</div>
</v-alert>
<v-text-field
v-model="identity"
label="Username / Email"
hint="Account login used by the IMAP server"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-account"
class="mb-4"
autocomplete="username"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-text-field
v-model="secret"
type="password"
label="Password"
hint="Password or app-specific password"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-lock"
class="mb-4"
autocomplete="current-password"
:rules="[rules.required]"
/>
</div>
</template>
<style scoped>
.imap-auth-panel {
max-width: 800px;
}
.text-h6 {
font-size: 1.25rem;
font-weight: 500;
line-height: 2rem;
letter-spacing: 0.0125em;
}
.text-body-2 {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
letter-spacing: 0.0178571429em;
color: rgba(var(--v-theme-on-surface), 0.7);
}
</style>

View File

@@ -0,0 +1,277 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type {
ServiceLocation,
ServiceLocationSocketSole,
ServiceLocationUri,
} from '@KTXM/MailManager/types/service'
import type { ProviderConfigPanelProps, ProviderConfigPanelEmits } from '@KTXM/MailManager/types/integration'
type ImapEncryption = 'none' | 'ssl' | 'tls' | 'starttls'
type ImapLocation = ServiceLocationSocketSole & {
type: 'SOCKET_SOLE' | 'URI'
verifyPeerName?: boolean
allowSelfSigned?: boolean
}
const props = defineProps<ProviderConfigPanelProps>()
const emit = defineEmits<ProviderConfigPanelEmits>()
function asImapLocation(location?: ServiceLocation): ImapLocation | null {
if (!location) {
return null
}
if (location.type === 'SOCKET_SOLE') {
return location as ImapLocation
}
if (location.type === 'URI') {
const uriLocation = location as ServiceLocationUri & {
encryption?: ImapEncryption
verifyPeerName?: boolean
allowSelfSigned?: boolean
}
return {
type: 'URI',
host: uriLocation.host || '',
port: uriLocation.port || 993,
encryption: uriLocation.encryption || 'ssl',
verifyPeer: uriLocation.verifyPeer ?? true,
verifyHost: uriLocation.verifyPeerName ?? uriLocation.verifyHost ?? true,
verifyPeerName: uriLocation.verifyPeerName ?? uriLocation.verifyHost ?? true,
allowSelfSigned: uriLocation.allowSelfSigned ?? false,
}
}
return null
}
function defaultPortFor(encryption: ImapEncryption): number {
return encryption === 'ssl' || encryption === 'tls' ? 993 : 143
}
const sourceLocation = computed(() => asImapLocation(props.modelValue || props.discoveredLocation))
const host = ref(sourceLocation.value?.host || '')
const encryption = ref<ImapEncryption>(sourceLocation.value?.encryption || 'ssl')
const port = ref(String(sourceLocation.value?.port || defaultPortFor(encryption.value)))
const verifyPeer = ref(sourceLocation.value?.verifyPeer ?? true)
const verifyHost = ref(sourceLocation.value?.verifyPeerName ?? sourceLocation.value?.verifyHost ?? true)
const allowSelfSigned = ref(sourceLocation.value?.allowSelfSigned ?? false)
const encryptionOptions = [
{ title: 'Implicit TLS (SSL)', value: 'ssl' },
{ title: 'TLS', value: 'tls' },
{ title: 'STARTTLS', value: 'starttls' },
{ title: 'None', value: 'none' },
]
const rules = {
required: (value: unknown) => !!value || 'This field is required',
port: (value: string) => {
const numericValue = Number(value)
return Number.isInteger(numericValue) && numericValue >= 1 && numericValue <= 65535
? true
: 'Port must be between 1 and 65535'
}
}
const isValid = computed(() => !!host.value && rules.port(port.value) === true)
const currentLocation = computed((): ServiceLocation | null => {
if (!isValid.value) {
return null
}
const numericPort = Number(port.value)
const location: ImapLocation = {
type: 'SOCKET_SOLE',
host: host.value,
port: numericPort,
encryption: encryption.value,
verifyPeer: verifyPeer.value,
verifyHost: verifyHost.value,
verifyPeerName: verifyHost.value,
allowSelfSigned: allowSelfSigned.value,
}
return location as ServiceLocation
})
watch(
currentLocation,
value => {
if (value) {
emit('update:modelValue', value)
}
},
{ immediate: true, deep: true }
)
watch(
isValid,
value => {
emit('valid', value)
},
{ immediate: true }
)
watch(
() => props.modelValue,
value => {
const next = asImapLocation(value)
if (!next) {
return
}
host.value = next.host || ''
encryption.value = next.encryption || 'ssl'
port.value = String(next.port || defaultPortFor(encryption.value))
verifyPeer.value = next.verifyPeer ?? true
verifyHost.value = next.verifyPeerName ?? next.verifyHost ?? true
allowSelfSigned.value = next.allowSelfSigned ?? false
}
)
watch(
() => props.discoveredLocation,
value => {
if (props.modelValue) {
return
}
const next = asImapLocation(value)
if (!next) {
return
}
host.value = next.host || ''
encryption.value = next.encryption || 'ssl'
port.value = String(next.port || defaultPortFor(encryption.value))
verifyPeer.value = next.verifyPeer ?? true
verifyHost.value = next.verifyPeerName ?? next.verifyHost ?? true
allowSelfSigned.value = next.allowSelfSigned ?? false
},
{ immediate: true }
)
watch(encryption, (next, previous) => {
const previousDefault = defaultPortFor(previous)
if (!port.value || Number(port.value) === previousDefault) {
port.value = String(defaultPortFor(next))
}
})
</script>
<template>
<div class="imap-config-panel">
<h3 class="text-h6 mb-4">IMAP Connection Settings</h3>
<p class="text-body-2 mb-6">Configure the server address, transport security, and certificate verification for your IMAP mailbox.</p>
<v-text-field
v-model="host"
label="Server Host"
hint="For example: imap.example.com"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-server"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-select
v-model="encryption"
:items="encryptionOptions"
label="Security"
variant="outlined"
prepend-inner-icon="mdi-shield-lock"
class="mb-4"
/>
<v-text-field
v-model="port"
label="Port"
hint="Defaults to 993 for TLS/SSL and 143 for plain or STARTTLS"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-numeric"
class="mb-4"
type="number"
min="1"
max="65535"
:rules="[rules.required, rules.port]"
/>
<v-expansion-panels class="mt-4">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-cog</v-icon>
Security Options
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-switch
v-model="verifyPeer"
label="Verify TLS certificate"
color="primary"
hint="Disable only for trusted internal or test environments"
persistent-hint
class="mb-4"
/>
<v-switch
v-model="verifyHost"
label="Verify certificate hostname"
color="primary"
hint="Checks that the certificate matches the IMAP host"
persistent-hint
class="mb-4"
/>
<v-switch
v-model="allowSelfSigned"
label="Allow self-signed certificates"
color="primary"
hint="Use only when your server is intentionally deployed with a self-signed certificate"
persistent-hint
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<v-alert type="info" variant="tonal" density="compact" class="mt-4">
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-caption">
STARTTLS is accepted for compatibility, but the current IMAP client transport does not perform STARTTLS negotiation. Prefer TLS on port 993 when available.
</div>
</v-alert>
</div>
</template>
<style scoped>
.imap-config-panel {
max-width: 800px;
}
.text-h6 {
font-size: 1.25rem;
font-weight: 500;
line-height: 2rem;
letter-spacing: 0.0125em;
}
.text-body-2 {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
letter-spacing: 0.0178571429em;
color: rgba(var(--v-theme-on-surface), 0.7);
}
</style>

38
src/integrations.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { ModuleIntegrations } from '@KTXC/types/moduleTypes'
import type { ServiceInterface } from '@KTXM/MailManager/types/service'
import { ServiceObject } from '@KTXM/MailManager/models/service'
const integrations: ModuleIntegrations = {
mail_account_config_panels: [
{
id: 'imap',
label: 'IMAP',
icon: 'mdi-email',
caption: 'Internet Message Access Protocol',
component: () => import('@/components/ImapConfigPanel.vue'),
priority: 20,
}
],
mail_account_auth_panels: [
{
id: 'imap',
component: () => import('@/components/ImapAuthPanel.vue'),
}
],
mail_service_factory: [
{
id: 'imap',
factory: (data: ServiceInterface) => new ServiceObject().fromJson(data)
}
],
mail_provider_metadata: [
{
id: 'imap',
label: 'IMAP',
description: 'Classic mailbox access over IMAP',
icon: 'mdi-email',
}
]
}
export default integrations

12
src/main.ts Normal file
View File

@@ -0,0 +1,12 @@
import routes from '@/routes'
import integrations from '@/integrations'
import type { App as VueApp } from 'vue'
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
export { routes, integrations }
export default {
install(_app: VueApp) {
}
}

3
src/routes.ts Normal file
View File

@@ -0,0 +1,3 @@
const routes = []
export default routes

1
src/style.css Normal file
View File

@@ -0,0 +1 @@
/* imap provider module styles */

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

18
tsconfig.app.json Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"],
"@KTXC/*": ["../../core/src/*"],
"@KTXM/MailManager/*": ["../mail_manager/src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

21
tsconfig.node.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

59
vite.config.ts Normal file
View File

@@ -0,0 +1,59 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [
vue(),
{
name: 'inject-css-filename',
enforce: 'post',
generateBundle(_options, bundle) {
const cssFile = Object.keys(bundle).find(name => name.endsWith('.css'))
if (!cssFile) {
return
}
for (const fileName of Object.keys(bundle)) {
const chunk = bundle[fileName]
if (chunk.type === 'chunk' && chunk.code.includes('__CSS_FILENAME_PLACEHOLDER__')) {
chunk.code = chunk.code.replace(/__CSS_FILENAME_PLACEHOLDER__/g, `static/${cssFile}`)
}
}
}
}
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@KTXC': path.resolve(__dirname, '../../core/src'),
'@KTXM/MailManager': path.resolve(__dirname, '../mail_manager/src'),
},
},
build: {
outDir: 'static',
emptyOutDir: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, 'src/main.ts'),
formats: ['es'],
fileName: () => 'module.mjs',
},
rollupOptions: {
external: [
'vue',
'vue-router',
'pinia',
],
output: {
assetFileNames: assetInfo => {
if (assetInfo.name?.endsWith('.css')) {
return 'provider_imap-[hash].css'
}
return '[name]-[hash][extname]'
}
}
},
},
})