import { DyFM_EnvironmentFlag, DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_DataService } from '../../../_services/base/data.service'; import { DyNTS_testEmailPattern } from '../_collections/test-email-pattern.const'; import { DyNTS_SentTestMail, DyNTS_sentTestMail_dataParams } from '../_models/data-models/dynts-sent-test-mail.data-model'; /** * BFR-MASTERPROMPTER-015 (MP-19) — test-email SINK statikus util-service. * * A `DyNTS_EmailService.sendEmail` a kuldes ELOTT megkerdezi (`shouldSink`): ha a kornyezet * local/test ES a cimzett(ek) a teszt-cim-mintara matchelnek, a kuldes NEM SMTP-re megy, hanem a * `dynts_sent_test_mails` kollekcioba (`storeSentMail`). Minden mas eset VALTOZATLAN (zero * regresszio a valos emailekre; prod-on a sink SOHA nem aktiv — fail-closed env-gate). * * Konfiguracio (opcionalis): `DyNTS_global_settings.testEmailSink = { enabled?, pattern?, ttlHours? }` * — default: enabled local/test-en, pattern = `DyNTS_testEmailPattern`, TTL 24h. */ export class DyNTS_TestEmailSink_Service { /** A TTL-index ensure csak egyszer fusson processzenkent (idempotens a Mongo-oldalon is). */ private static ttlIndexEnsured: boolean = false; /** A sink aktiv-e: explicit setting-override VAGY default env-gate (CSAK local/test — prod fail-closed). */ static isSinkActive(): boolean { const settings = DyNTS_global_settings.testEmailSink; const envIsTestLike: boolean = [ DyFM_EnvironmentFlag.local, DyFM_EnvironmentFlag.test ] .includes(DyNTS_global_settings.env_settings.environment); // A prod/dev kornyezet HARD-gate: settings.enabled=true SEM kapcsolhatja be (fail-closed). if (!envIsTestLike) { return false; } return settings?.enabled !== false; // default: local/test-en aktiv } /** Az ervenyes teszt-cim-minta (setting-override vagy a kanonikus default). */ static getPattern(): RegExp { return DyNTS_global_settings.testEmailSink?.pattern ?? DyNTS_testEmailPattern; } /** * SINK-elendo-e a kuldes: aktiv a sink ES MINDEN cimzett a teszt-mintara matchel. * Kevert cimzett-lista (teszt + valos) → NEM sink-elunk (a kuldes a mai uton megy) — igy egy valos * cimzett SOHA nem veszithet emailt a sink miatt. */ static shouldSink(to: string | string[] | undefined): boolean { if (!to || !this.isSinkActive()) { return false; } const recipients: string[] = Array.isArray(to) ? to : String(to).split(',').map((r: string): string => r.trim()); if (!recipients.length) { return false; } const pattern: RegExp = this.getPattern(); return recipients.every((recipient: string): boolean => pattern.test(recipient)); } /** A sink-elt email eltarolasa + (elso hivaskor) a TTL-index best-effort felhuzasa. */ static async storeSentMail(set: { to: string | string[]; subject?: string; html?: string; issuer: string; }): Promise { const mail_DS = new DyNTS_DataService( new DyNTS_SentTestMail(), DyNTS_sentTestMail_dataParams, set.issuer, ); await mail_DS.dataDBService.createData(new DyNTS_SentTestMail({ to: Array.isArray(set.to) ? set.to.join(',') : set.to, subject: set.subject, html: set.html, issuer: set.issuer, }), set.issuer); DyFM_Log.info(`[TEST-EMAIL-SINK] sunk mail to ${set.to} (subject: "${set.subject}") — DB-be, SMTP kihagyva`); this.ensureTtlIndex(mail_DS); } /** * Sink-elt emailek lekerdezese (a read-endpoint + a tesztek hasznaljak). * BFR-008 query-options: sort desc + limit DB-szinten (nincs full-scan). */ static async findSentMails(filter: { to?: string; sinceIso?: string; limit?: number; }, issuer: string): Promise { const mail_DS = new DyNTS_DataService( new DyNTS_SentTestMail(), DyNTS_sentTestMail_dataParams, issuer, ); const dbFilter: Record = {}; if (filter.to) { dbFilter.to = filter.to; } if (filter.sinceIso) { dbFilter.__created = { $gte: new Date(filter.sinceIso) }; } return mail_DS.findDataList( dbFilter as never, true, { sort: { __created: -1 }, limit: Math.min(filter.limit ?? 20, 100) }, ); } /** * Link-kinyero a sink-elt HTML-bol (pl. verifikacios link / reset-token URL) — a fogyaszto tesztek * kenyelmi utilja. Minden href-et visszaad, sorrendben. */ static extractLinks(html: string | undefined): string[] { if (!html) { return []; } const links: string[] = []; const hrefRegex: RegExp = /href="([^"]+)"/gi; let match: RegExpExecArray | null = hrefRegex.exec(html); while (match) { links.push(match[1]); match = hrefRegex.exec(html); } return links; } /** Best-effort TTL-index a __created-en (default 24h; settings.ttlHours override). Sosem dob. */ private static ensureTtlIndex(mail_DS: DyNTS_DataService): void { if (this.ttlIndexEnsured) { return; } this.ttlIndexEnsured = true; const ttlHours: number = DyNTS_global_settings.testEmailSink?.ttlHours ?? 24; try { const model = mail_DS.dataDBService.dataModel as { collection?: { createIndex?: (spec: object, opts: object) => Promise }; }; void model.collection?.createIndex?.({ __created: 1 }, { expireAfterSeconds: ttlHours * 3600 }) ?.catch((error: Error): void => { DyFM_Log.warn(`[TEST-EMAIL-SINK] TTL-index ensure failed (non-fatal): ${error.message}`); }); } catch (error) { DyFM_Log.warn(`[TEST-EMAIL-SINK] TTL-index ensure failed (non-fatal): ${(error as Error).message}`); } } }