import type { SzPlatformClient } from '../classes.platformclient.js'; import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; export type TServiceMailTlsMode = 'plain' | 'starttls' | 'implicitTls'; export interface IServiceMailSmtpCredentials { from: string; host: string; port: number; tlsMode: TServiceMailTlsMode; username: string; password: string; addressToken?: string; } export interface IServiceMailTypedCredentials { from: string; typedUrl: string; credentialId: string; credentialSecret: string; addressToken?: string; } export interface IServiceMailStatus { ready: boolean; from?: string; host?: string; port?: number; tlsMode?: TServiceMailTlsMode; typedUrl?: string; transport?: 'typedsocket' | 'smtp' | 'none'; usernameConfigured: boolean; passwordConfigured: boolean; typedCredentialConfigured?: boolean; smtpReady?: boolean; missing: string[]; } export interface IServiceMailAttachment { filename: string; contentType?: string; content: string | Uint8Array; } export interface IServiceMailSendOptions { /** Stable replay key for exact retries during dcrouter's 30-day idempotency window. */ idempotencyKey?: string; from?: string; to: string | string[]; cc?: string | string[]; bcc?: string | string[]; /** One bare mailbox address rendered as the authoritative Reply-To header by dcrouter. */ replyTo?: string; subject: string; text?: string; html?: string; headers?: Record; attachments?: IServiceMailAttachment[]; } export type TServiceMailSubmissionErrorCode = plugins.servezoneInterfaces.data.TMailSubmissionErrorCode; export class ServiceMailSubmissionError extends Error { constructor( public readonly code: TServiceMailSubmissionErrorCode, messageArg: string, ) { super(messageArg); this.name = 'ServiceMailSubmissionError'; } } export interface IServiceMailSendResult { messageId?: string; spoolItemId?: string; accepted?: string[]; rejected?: string[]; response?: string; } export interface IServiceMailInboundMessage extends plugins.servezoneInterfaces.data.IMailInboundMessagePayload { from: string; to: string[]; receivedAtIso: string; } export interface IServiceMailInboundHandlerResult { accepted?: boolean; workAppMessageId?: string; message?: string; } export type IServiceMailInboundHandleResult = plugins.servezoneInterfaces.requests.mail.IReq_DeliverInboundMail['response']; export type TServiceMailInboundHandler = ( messageArg: IServiceMailInboundMessage, ) => Promise | IServiceMailInboundHandlerResult | void; export class SzEmailConnector { public platformClientRef: SzPlatformClient; private mailTypedSocket?: plugins.typedsocket.TypedSocket; private mailTypedSocketUrl?: string; private mailTypedSocketPromise?: Promise; private mailTypedSocketAbortController?: AbortController; private mailTypedSocketStatusSubscription?: { unsubscribe: () => void }; private inboundHandler?: TServiceMailInboundHandler; private inboundTypedHandlerRegistered = false; private inboundEndpointRegistrations = new Set(); private inboundEndpointReplayPromise?: Promise; private readonly inboundEndpointReplayRetryDelaysMs = [500, 1000, 2000, 5000]; constructor(platformClientRefArg: SzPlatformClient) { this.platformClientRef = platformClientRefArg; } /** * Legacy platform-service email path. New service-mail code should use sendMail(). */ public async sendEmail( optionsArg: plugins.servezoneInterfaces.platform.email.IReq_SendEmail['request'] ) { if (this.platformClientRef.debugMode) { logger.log('info', `sent email with subject ${optionsArg.title} to ${optionsArg.to} body: ${optionsArg.body.split('\n').map(line => ` ${line}`).join('\n')} `); } if (this.platformClientRef.debugMode) { return; } const typedRequest = this.platformClientRef.typedsocket.createTypedRequest( 'sendEmail' ); const response = await typedRequest.fire(optionsArg); return response.responseId; } public async getServiceMailStatus(addressArg?: string): Promise { const requestedAddress = addressArg?.trim().toLowerCase(); const token = requestedAddress ? this.getMailEnvToken(requestedAddress) : undefined; const scopedEnv = token ? await this.readMailEnv(token) : undefined; const defaultEnv = token ? await this.readMailEnv() : undefined; const useDefaultEnv = requestedAddress && !this.hasAnyMailEnvValue(scopedEnv) && defaultEnv?.from?.trim().toLowerCase() === requestedAddress; const env = useDefaultEnv ? defaultEnv! : scopedEnv || await this.readMailEnv(); const missingPrefix = useDefaultEnv || !token ? undefined : token; const port = env.smtpPort ? Number(env.smtpPort) : undefined; const tlsMode = this.normalizeTlsMode(env.smtpTlsMode || 'starttls'); const typedMissing: string[] = []; const smtpMissing: string[] = []; if (!env.typedUrl) typedMissing.push(missingPrefix ? `MAIL_${missingPrefix}_TYPED_URL` : 'MAIL_TYPED_URL'); if (!env.apiCredentialId) typedMissing.push(missingPrefix ? `MAIL_${missingPrefix}_API_CREDENTIAL_ID` : 'MAIL_API_CREDENTIAL_ID'); if (!env.apiCredentialSecret) typedMissing.push(missingPrefix ? `MAIL_${missingPrefix}_API_CREDENTIAL_SECRET` : 'MAIL_API_CREDENTIAL_SECRET'); if (!env.smtpHost) smtpMissing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_HOST` : 'SMTP_HOST'); if (!env.smtpPort || !Number.isInteger(port) || port! < 1 || port! > 65535) { smtpMissing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_PORT` : 'SMTP_PORT'); } if (!env.smtpUsername) smtpMissing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_USERNAME` : 'SMTP_USERNAME'); if (!env.smtpPassword) smtpMissing.push(missingPrefix ? `MAIL_${missingPrefix}_SMTP_PASSWORD` : 'SMTP_PASSWORD'); const fromMissing = env.from ? [] : [missingPrefix ? `MAIL_${missingPrefix}_FROM` : 'MAIL_FROM']; const typedReady = fromMissing.length === 0 && typedMissing.length === 0; const smtpReady = fromMissing.length === 0 && smtpMissing.length === 0; return { ready: typedReady, from: env.from, host: env.smtpHost, port, tlsMode, typedUrl: env.typedUrl, transport: typedReady ? 'typedsocket' : smtpReady ? 'smtp' : 'none', usernameConfigured: Boolean(env.smtpUsername), passwordConfigured: Boolean(env.smtpPassword), typedCredentialConfigured: Boolean(env.apiCredentialId && env.apiCredentialSecret), smtpReady, missing: typedReady ? [] : [...fromMissing, ...typedMissing], }; } public async getServiceMailCredentials(addressArg?: string): Promise { const requestedAddress = addressArg?.trim().toLowerCase(); const requestedToken = requestedAddress ? this.getMailEnvToken(requestedAddress) : undefined; const scopedEnv = requestedToken ? await this.readMailEnv(requestedToken) : undefined; const hasScopedConfig = scopedEnv && Object.values(scopedEnv).some(Boolean); const env = hasScopedConfig ? scopedEnv! : await this.readMailEnv(); const from = env.from?.trim().toLowerCase(); if (!from) throw new Error('MAIL_FROM is required for service mail'); if (requestedAddress && from !== requestedAddress) { throw new Error(`No service mail credentials are configured for ${requestedAddress}`); } const port = Number(env.smtpPort); if (!env.smtpHost) throw new Error('SMTP_HOST is required for service mail'); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error('SMTP_PORT must be an integer between 1 and 65535'); } if (!env.smtpUsername) throw new Error('SMTP_USERNAME is required for service mail'); if (!env.smtpPassword) throw new Error('SMTP_PASSWORD is required for service mail'); return { from, host: env.smtpHost, port, tlsMode: this.normalizeTlsMode(env.smtpTlsMode || 'starttls'), username: env.smtpUsername, password: env.smtpPassword, addressToken: hasScopedConfig ? requestedToken : undefined, }; } public async sendText(optionsArg: Omit & { text: string }) { return await this.sendMail(optionsArg); } public async sendHtml(optionsArg: Omit & { html: string }) { return await this.sendMail(optionsArg); } public async sendMail(optionsArg: IServiceMailSendOptions): Promise { if (!optionsArg.text && !optionsArg.html) { throw new Error('Either text or html is required for service mail'); } if (this.platformClientRef.debugMode) { logger.log('info', `service mail debug send: ${optionsArg.subject} to ${this.listToArray(optionsArg.to).join(', ')}`); return { messageId: 'debug' }; } const credentials = await this.getServiceMailTypedCredentials(optionsArg.from); const typedSocket = await this.getMailTypedSocket(credentials.typedUrl); const request = typedSocket.createTypedRequest('enqueueMail'); const result = await request.fire({ auth: { credentialId: credentials.credentialId, credentialSecret: credentials.credentialSecret, }, outboundIdentityId: credentials.credentialId, idempotencyKey: optionsArg.idempotencyKey, message: { from: credentials.from, to: this.listToArray(optionsArg.to), cc: optionsArg.cc ? this.listToArray(optionsArg.cc) : undefined, bcc: optionsArg.bcc ? this.listToArray(optionsArg.bcc) : undefined, ...(optionsArg.replyTo !== undefined ? { replyTo: optionsArg.replyTo } : {}), subject: optionsArg.subject, text: optionsArg.text, html: optionsArg.html, headers: optionsArg.headers, attachments: optionsArg.attachments?.map((attachmentArg) => ({ filename: attachmentArg.filename, contentType: attachmentArg.contentType, binaryAttachmentString: this.normalizeAttachmentContent(attachmentArg.content), })), }, }); if (!result.accepted) { const message = result.message || 'service mail was not accepted by dcrouter'; if (result.errorCode) { throw new ServiceMailSubmissionError(result.errorCode, message); } throw new Error(message); } return { messageId: result.spoolItemId, spoolItemId: result.spoolItemId, accepted: this.listToArray(optionsArg.to), rejected: [], response: result.message, }; } public async getMailDeliveryStatus( spoolItemIdArg: string, addressArg?: string, ): Promise { const spoolItemId = String(spoolItemIdArg || '').trim(); if (!spoolItemId) { throw new Error('spoolItemId is required'); } const credentials = await this.getServiceMailTypedCredentials(addressArg); const typedSocket = await this.getMailTypedSocket(credentials.typedUrl); const request = typedSocket.createTypedRequest('getMailDeliveryStatus'); return await request.fire({ auth: { credentialId: credentials.credentialId, credentialSecret: credentials.credentialSecret, } as plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth, spoolItemId, }); } public async registerInboundHandler( handlerArg: TServiceMailInboundHandler, addressArg?: string, ): Promise { this.inboundHandler = handlerArg; this.ensureInboundTypedHandler(); const credentials = await this.getServiceMailTypedCredentials(addressArg); const typedSocket = await this.getMailTypedSocket(credentials.typedUrl); const result = await this.registerServiceMailEndpoint(typedSocket, credentials); const registeredAddresses = result.addresses?.length ? result.addresses : [credentials.from]; for (const address of registeredAddresses) { this.inboundEndpointRegistrations.add(address.trim().toLowerCase()); } return result; } public async stop(): Promise { await this.stopMailTypedSocket(); } public normalizeInboundMessage(payloadArg: unknown): IServiceMailInboundMessage { const payload = this.asRecord(payloadArg); const deliveryCandidate = this.asRecord(payload.delivery || payload.message || payload); const envelope = this.asRecord(deliveryCandidate.envelope); const rawMessage = typeof deliveryCandidate.rawMessage === 'string' ? deliveryCandidate.rawMessage : ''; const rcptTo = Array.isArray(envelope.rcptTo) ? envelope.rcptTo.map(String) : []; const receivedAt = typeof deliveryCandidate.receivedAt === 'number' ? deliveryCandidate.receivedAt : Date.now(); return { spoolItemId: String(deliveryCandidate.spoolItemId || deliveryCandidate.id || ''), owner: deliveryCandidate.owner as plugins.servezoneInterfaces.data.IMailResourceOwner | undefined, envelope: { mailFrom: String(envelope.mailFrom || deliveryCandidate.from || ''), rcptTo, }, rawMessage, sizeBytes: typeof deliveryCandidate.sizeBytes === 'number' ? deliveryCandidate.sizeBytes : rawMessage.length, messageId: deliveryCandidate.messageId ? String(deliveryCandidate.messageId) : undefined, subject: deliveryCandidate.subject ? String(deliveryCandidate.subject) : this.extractHeader(rawMessage, 'subject'), headers: this.normalizeHeaders(deliveryCandidate.headers), source: deliveryCandidate.source as plugins.servezoneInterfaces.data.IMailConnectionInfo | undefined, receivedAt, from: String(envelope.mailFrom || deliveryCandidate.from || ''), to: rcptTo, receivedAtIso: new Date(receivedAt).toISOString(), }; } public async handleInboundPayload( payloadArg: unknown, handlerArg: TServiceMailInboundHandler, ): Promise { const message = this.normalizeInboundMessage(payloadArg); const result = await handlerArg(message); return { accepted: result?.accepted !== false, workAppMessageId: result?.workAppMessageId, message: result?.message, }; } public createInboundHandler(handlerArg: TServiceMailInboundHandler) { return async (payloadArg: unknown) => await this.handleInboundPayload(payloadArg, handlerArg); } private ensureInboundTypedHandler(): void { if (this.inboundTypedHandlerRegistered) return; this.inboundTypedHandlerRegistered = true; this.platformClientRef.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deliverInboundMail', async (dataArg) => { if (!this.inboundHandler) { return { accepted: false, message: 'No service mail inbound handler registered' }; } return await this.handleInboundPayload(dataArg.delivery, this.inboundHandler); }, ), ); } public async sendNotification(optionsArg: { toArg: string; subject: string; text: string }) {} public async sendActionLink(optionsArg: { toArg: string; subject: string; text: string }) {} private async readMailEnv(tokenArg?: string): Promise<{ from?: string; smtpHost?: string; smtpPort?: string; smtpTlsMode?: string; smtpUsername?: string; smtpPassword?: string; typedUrl?: string; apiCredentialId?: string; apiCredentialSecret?: string; }> { const prefix = tokenArg ? `MAIL_${tokenArg}_` : ''; return { from: await this.readMailEnvVar(tokenArg ? `${prefix}FROM` : 'MAIL_FROM'), smtpHost: await this.readMailEnvVar(`${prefix}SMTP_HOST`), smtpPort: await this.readMailEnvVar(`${prefix}SMTP_PORT`), smtpTlsMode: await this.readMailEnvVar(`${prefix}SMTP_TLS_MODE`), smtpUsername: await this.readMailEnvVar(`${prefix}SMTP_USERNAME`), smtpPassword: await this.readMailEnvVar(`${prefix}SMTP_PASSWORD`), typedUrl: await this.readMailEnvVar(tokenArg ? `${prefix}TYPED_URL` : 'MAIL_TYPED_URL'), apiCredentialId: await this.readMailEnvVar(tokenArg ? `${prefix}API_CREDENTIAL_ID` : 'MAIL_API_CREDENTIAL_ID'), apiCredentialSecret: await this.readMailEnvVar(tokenArg ? `${prefix}API_CREDENTIAL_SECRET` : 'MAIL_API_CREDENTIAL_SECRET'), }; } private async readMailEnvVar(envNameArg: string): Promise { return await this.platformClientRef.getConfiguredEnvVar(envNameArg); } private hasAnyMailEnvValue(envArg?: { from?: string; smtpHost?: string; smtpPort?: string; smtpTlsMode?: string; smtpUsername?: string; smtpPassword?: string; typedUrl?: string; apiCredentialId?: string; apiCredentialSecret?: string; }): boolean { return Boolean(envArg && Object.values(envArg).some(Boolean)); } private async getServiceMailTypedCredentials(addressArg?: string): Promise { const requestedAddress = addressArg?.trim().toLowerCase(); const requestedToken = requestedAddress ? this.getMailEnvToken(requestedAddress) : undefined; const scopedEnv = requestedToken ? await this.readMailEnv(requestedToken) : undefined; const hasScopedConfig = scopedEnv && Object.values(scopedEnv).some(Boolean); const env = hasScopedConfig ? scopedEnv! : await this.readMailEnv(); const from = env.from?.trim().toLowerCase(); if (!from) throw new Error('MAIL_FROM is required for typed service mail'); if (requestedAddress && from !== requestedAddress) { throw new Error(`No typed service mail credentials are configured for ${requestedAddress}`); } if (!env.typedUrl) throw new Error('MAIL_TYPED_URL is required for typed service mail'); if (!env.apiCredentialId) throw new Error('MAIL_API_CREDENTIAL_ID is required for typed service mail'); if (!env.apiCredentialSecret) throw new Error('MAIL_API_CREDENTIAL_SECRET is required for typed service mail'); return { from, typedUrl: this.normalizeTypedSocketUrl(env.typedUrl), credentialId: env.apiCredentialId, credentialSecret: env.apiCredentialSecret, addressToken: hasScopedConfig ? requestedToken : undefined, }; } private async getMailTypedSocket(urlArg: string): Promise { const normalizedUrl = this.normalizeTypedSocketUrl(urlArg); if (this.mailTypedSocket && this.mailTypedSocketUrl === normalizedUrl) { return this.mailTypedSocket; } if (this.mailTypedSocketPromise && this.mailTypedSocketUrl === normalizedUrl) { return await this.mailTypedSocketPromise; } if (this.mailTypedSocket || this.mailTypedSocketPromise) { await this.stopMailTypedSocket(); } this.mailTypedSocketUrl = normalizedUrl; const abortController = new AbortController(); this.mailTypedSocketAbortController = abortController; const socketPromise = plugins.typedsocket.TypedSocket.createClient( this.platformClientRef.typedrouter, normalizedUrl, { autoReconnect: true, abortSignal: abortController.signal }, ); this.mailTypedSocketPromise = socketPromise; try { const socket = await socketPromise; if (this.mailTypedSocketPromise === socketPromise && this.mailTypedSocketUrl === normalizedUrl) { this.mailTypedSocket = socket; this.watchMailTypedSocketStatus(socket); } else { await socket.stop(); } return socket; } catch (error) { if (this.mailTypedSocketPromise === socketPromise) { this.mailTypedSocketPromise = undefined; this.mailTypedSocketUrl = undefined; this.mailTypedSocketAbortController = undefined; } throw error; } } private async stopMailTypedSocket(): Promise { const socket = this.mailTypedSocket; const socketPromise = this.mailTypedSocketPromise; const abortController = this.mailTypedSocketAbortController; const statusSubscription = this.mailTypedSocketStatusSubscription; this.mailTypedSocket = undefined; this.mailTypedSocketPromise = undefined; this.mailTypedSocketUrl = undefined; this.mailTypedSocketAbortController = undefined; this.mailTypedSocketStatusSubscription = undefined; statusSubscription?.unsubscribe(); abortController?.abort(); if (socket) { await socket.stop(); return; } if (socketPromise) { try { const pendingSocket = await socketPromise; await pendingSocket.stop(); } catch { // The pending connection already failed; there is no socket to stop. } } } private watchMailTypedSocketStatus(socketArg: plugins.typedsocket.TypedSocket): void { this.mailTypedSocketStatusSubscription?.unsubscribe(); this.mailTypedSocketStatusSubscription = socketArg.statusSubject.subscribe((statusArg) => { void this.handleMailTypedSocketStatus(socketArg, statusArg); }); } private async handleMailTypedSocketStatus( socketArg: plugins.typedsocket.TypedSocket, statusArg: plugins.typedsocket.TConnectionStatus, ): Promise { if (statusArg !== 'connected' || this.mailTypedSocket !== socketArg) { return; } try { await this.replayInboundEndpointRegistrationsWithRetry(socketArg); } catch (error) { console.warn('Could not re-register service mail endpoint after reconnect:', (error as Error).message); } } private async replayInboundEndpointRegistrationsWithRetry( socketArg: plugins.typedsocket.TypedSocket, ): Promise { if (!this.inboundEndpointRegistrations.size) { return; } if (this.inboundEndpointReplayPromise) { return await this.inboundEndpointReplayPromise; } const replayPromise = this.replayInboundEndpointRegistrationsWithRetryInner(socketArg); this.inboundEndpointReplayPromise = replayPromise; try { await replayPromise; } finally { if (this.inboundEndpointReplayPromise === replayPromise) { this.inboundEndpointReplayPromise = undefined; } } } private async replayInboundEndpointRegistrationsWithRetryInner( socketArg: plugins.typedsocket.TypedSocket, ): Promise { let lastError: unknown; const delays = [0, ...this.inboundEndpointReplayRetryDelaysMs]; for (let attemptIndex = 0; attemptIndex < delays.length; attemptIndex++) { if (this.mailTypedSocket !== socketArg) { return; } if (attemptIndex > 0) { const shouldContinue = await this.waitForInboundEndpointReplayRetry(socketArg, delays[attemptIndex]); if (!shouldContinue) { return; } } try { await this.replayInboundEndpointRegistrationsOnce(socketArg); return; } catch (error) { lastError = error; } } throw lastError instanceof Error ? lastError : new Error('Could not re-register service mail endpoint after reconnect'); } private async replayInboundEndpointRegistrationsOnce( socketArg: plugins.typedsocket.TypedSocket, ): Promise { for (const address of this.inboundEndpointRegistrations.values()) { if (this.mailTypedSocket !== socketArg) { return; } const credentials = await this.getServiceMailTypedCredentials(address); if (this.normalizeTypedSocketUrl(credentials.typedUrl) !== this.mailTypedSocketUrl) { continue; } await this.registerServiceMailEndpoint(socketArg, credentials); } } private async waitForInboundEndpointReplayRetry( socketArg: plugins.typedsocket.TypedSocket, delayMsArg: number, ): Promise { if (this.mailTypedSocket !== socketArg) { return false; } const abortSignal = this.mailTypedSocketAbortController?.signal; if (abortSignal?.aborted) { return false; } return await new Promise((resolve) => { const resolveOnce = (resultArg: boolean) => { clearTimeout(timeout); abortSignal?.removeEventListener('abort', abortHandler); resolve(resultArg && this.mailTypedSocket === socketArg); }; const abortHandler = () => resolveOnce(false); const timeout = setTimeout(() => resolveOnce(true), delayMsArg); abortSignal?.addEventListener('abort', abortHandler, { once: true }); }); } private async registerServiceMailEndpoint( typedSocketArg: plugins.typedsocket.TypedSocket, credentialsArg: IServiceMailTypedCredentials, ): Promise { const request = typedSocketArg.createTypedRequest('registerServiceMailEndpoint'); const result = await request.fire({ auth: { credentialId: credentialsArg.credentialId, credentialSecret: credentialsArg.credentialSecret, }, addresses: [credentialsArg.from], }); if (!result.success) { throw new Error(result.message || 'Could not register service mail endpoint'); } return result; } private normalizeTypedSocketUrl(urlArg: string): string { return urlArg.trim().replace(/\/typedrequest\/?$/, '').replace(/\/+$/, ''); } private normalizeTlsMode(valueArg: string): TServiceMailTlsMode { if (valueArg === 'plain' || valueArg === 'starttls' || valueArg === 'implicitTls') return valueArg; throw new Error(`Invalid SMTP_TLS_MODE: ${valueArg}`); } private getMailEnvToken(addressArg: string): string { const token = addressArg.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').toUpperCase(); return /^\d/.test(token) ? `ADDR_${token}` : token; } private listToArray(valueArg: string | string[]): string[] { return Array.isArray(valueArg) ? valueArg : [valueArg]; } private normalizeAttachmentContent(contentArg: string | Uint8Array): string { return Buffer.from(contentArg).toString('base64'); } private asRecord(valueArg: unknown): Record { return valueArg && typeof valueArg === 'object' ? valueArg as Record : {}; } private normalizeHeaders(headersArg: unknown): Record | undefined { if (!headersArg || typeof headersArg !== 'object') return undefined; const result: Record = {}; for (const [key, value] of Object.entries(headersArg as Record)) { if (Array.isArray(value)) { result[key] = value.map(String); } else if (value !== undefined && value !== null) { result[key] = String(value); } } return result; } private extractHeader(rawMessageArg: string, headerNameArg: string): string | undefined { const lowerName = `${headerNameArg.toLowerCase()}:`; for (const line of rawMessageArg.split(/\r?\n/)) { if (!line) return undefined; if (line.toLowerCase().startsWith(lowerName)) { return line.slice(lowerName.length).trim(); } } } }