/** * Location implementation classes for Mail Manager services */ import type { ServiceLocation, ServiceLocationUri, ServiceLocationSocketSole, ServiceLocationSocketSplit, ServiceLocationFile } from '@/types/service'; import { MutationProxy } from './mutation-proxy'; import { clonePlain } from './clone-plain'; /** * Base Location class */ export abstract class Location { protected _original: T; protected _mutated: Partial; protected _mutationProxy: MutationProxy; protected _data: T; protected constructor(initial: T) { this._original = clonePlain(initial); this._mutated = {}; this._mutationProxy = new MutationProxy(() => this._original, () => this._mutated); this._data = this._mutationProxy.create(); } protected load(data: T): this { this._original = clonePlain(data); this._mutated = {}; this._data = this._mutationProxy.create(); return this; } toJson(): T; toJson(delta: true): Partial; toJson(delta?: boolean): T | Partial { if (delta) { return clonePlain(this._mutated); } return { ...clonePlain(this._original), ...clonePlain(this._mutated), }; } abstract clone(): Location; mutated(): boolean { return Reflect.ownKeys(this._mutated).length > 0; } static fromJson(data: ServiceLocation): Location { switch (data.type) { case 'URI': return LocationUri.fromJson(data); case 'SOCKET_SOLE': return LocationSocketSole.fromJson(data); case 'SOCKET_SPLIT': return LocationSocketSplit.fromJson(data); case 'FILE': return LocationFile.fromJson(data); default: throw new Error(`Unknown location type: ${(data as any).type}`); } } } /** * URI-based service location for API and web services * Used by: JMAP, Gmail API, etc. */ export class LocationUri extends Location { constructor( scheme: string = 'https', host: string = '', port: number = 443, path?: string, verifyPeer: boolean = true, verifyHost: boolean = true ) { super({ type: 'URI', scheme, host, port, ...(path !== undefined && { path }), verifyPeer, verifyHost, }); } static fromJson(data: ServiceLocationUri): LocationUri { return new LocationUri().load(data); } getUrl(): string { const path = this.path || ''; return `${this.scheme}://${this.host}:${this.port}${path}`; } clone(): LocationUri { return LocationUri.fromJson(structuredClone(this.toJson())); } get type(): 'URI' { return this._data.type; } get scheme(): string { return this._data.scheme; } set scheme(value: string) { this._data.scheme = value; } get host(): string { return this._data.host; } set host(value: string) { this._data.host = value; } get port(): number { return this._data.port; } set port(value: number) { this._data.port = value; } get path(): string | undefined { return this._data.path; } set path(value: string | undefined) { this._data.path = value; } get verifyPeer(): boolean { return this._data.verifyPeer ?? true; } set verifyPeer(value: boolean) { this._data.verifyPeer = value; } get verifyHost(): boolean { return this._data.verifyHost ?? true; } set verifyHost(value: boolean) { this._data.verifyHost = value; } } /** * Single socket-based service location * Used by: services using a single host/port combination */ export class LocationSocketSole extends Location { constructor( host: string = '', port: number = 993, encryption: 'none' | 'ssl' | 'tls' | 'starttls' = 'ssl', verifyPeer: boolean = true, verifyHost: boolean = true ) { super({ type: 'SOCKET_SOLE', host, port, encryption, verifyPeer, verifyHost, }); } static fromJson(data: ServiceLocationSocketSole): LocationSocketSole { return new LocationSocketSole().load(data); } clone(): LocationSocketSole { return LocationSocketSole.fromJson(structuredClone(this.toJson())); } get type(): 'SOCKET_SOLE' { return this._data.type; } get host(): string { return this._data.host; } set host(value: string) { this._data.host = value; } get port(): number { return this._data.port; } set port(value: number) { this._data.port = value; } get encryption(): 'none' | 'ssl' | 'tls' | 'starttls' { return this._data.encryption; } set encryption(value: 'none' | 'ssl' | 'tls' | 'starttls') { this._data.encryption = value; } get verifyPeer(): boolean { return this._data.verifyPeer ?? true; } set verifyPeer(value: boolean) { this._data.verifyPeer = value; } get verifyHost(): boolean { return this._data.verifyHost ?? true; } set verifyHost(value: boolean) { this._data.verifyHost = value; } } /** * Split socket-based service location * Used by: traditional IMAP/SMTP configurations */ export class LocationSocketSplit extends Location { constructor( inboundHost: string = '', inboundPort: number = 993, inboundEncryption: 'none' | 'ssl' | 'tls' | 'starttls' = 'ssl', outboundHost: string = '', outboundPort: number = 465, outboundEncryption: 'none' | 'ssl' | 'tls' | 'starttls' = 'ssl', inboundVerifyPeer: boolean = true, inboundVerifyHost: boolean = true, outboundVerifyPeer: boolean = true, outboundVerifyHost: boolean = true ) { super({ type: 'SOCKET_SPLIT', inboundHost, inboundPort, inboundEncryption, outboundHost, outboundPort, outboundEncryption, inboundVerifyPeer, inboundVerifyHost, outboundVerifyPeer, outboundVerifyHost, }); } static fromJson(data: ServiceLocationSocketSplit): LocationSocketSplit { return new LocationSocketSplit().load(data); } clone(): LocationSocketSplit { return LocationSocketSplit.fromJson(structuredClone(this.toJson())); } get type(): 'SOCKET_SPLIT' { return this._data.type; } get inboundHost(): string { return this._data.inboundHost; } set inboundHost(value: string) { this._data.inboundHost = value; } get inboundPort(): number { return this._data.inboundPort; } set inboundPort(value: number) { this._data.inboundPort = value; } get inboundEncryption(): 'none' | 'ssl' | 'tls' | 'starttls' { return this._data.inboundEncryption; } set inboundEncryption(value: 'none' | 'ssl' | 'tls' | 'starttls') { this._data.inboundEncryption = value; } get outboundHost(): string { return this._data.outboundHost; } set outboundHost(value: string) { this._data.outboundHost = value; } get outboundPort(): number { return this._data.outboundPort; } set outboundPort(value: number) { this._data.outboundPort = value; } get outboundEncryption(): 'none' | 'ssl' | 'tls' | 'starttls' { return this._data.outboundEncryption; } set outboundEncryption(value: 'none' | 'ssl' | 'tls' | 'starttls') { this._data.outboundEncryption = value; } get inboundVerifyPeer(): boolean { return this._data.inboundVerifyPeer ?? true; } set inboundVerifyPeer(value: boolean) { this._data.inboundVerifyPeer = value; } get inboundVerifyHost(): boolean { return this._data.inboundVerifyHost ?? true; } set inboundVerifyHost(value: boolean) { this._data.inboundVerifyHost = value; } get outboundVerifyPeer(): boolean { return this._data.outboundVerifyPeer ?? true; } set outboundVerifyPeer(value: boolean) { this._data.outboundVerifyPeer = value; } get outboundVerifyHost(): boolean { return this._data.outboundVerifyHost ?? true; } set outboundVerifyHost(value: boolean) { this._data.outboundVerifyHost = value; } } /** * File-based service location * Used by: local file system providers */ export class LocationFile extends Location { constructor(path: string = '') { super({ type: 'FILE', path, }); } static fromJson(data: ServiceLocationFile): LocationFile { return new LocationFile().load(data); } clone(): LocationFile { return LocationFile.fromJson(structuredClone(this.toJson())); } get type(): 'FILE' { return this._data.type; } get path(): string { return this._data.path; } set path(value: string) { this._data.path = value; } }