From eb2643de931f18622ccde81ec76ddb66f8e281d6 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 10 Feb 2026 22:26:20 +0000 Subject: [PATCH] feat(mailer-smtp): add in-process security pipeline for SMTP delivery (DKIM/SPF/DMARC, content scanning, IP reputation) --- changelog.md | 10 + dist_ts/00_commitinfo_data.js | 2 +- dist_ts/mail/delivery/index.d.ts | 3 +- dist_ts/mail/delivery/index.js | 5 +- .../smtpserver/certificate-utils.d.ts | 45 - .../delivery/smtpserver/certificate-utils.js | 345 ----- .../delivery/smtpserver/command-handler.d.ts | 156 -- .../delivery/smtpserver/command-handler.js | 1163 -------------- .../smtpserver/connection-manager.d.ts | 159 -- .../delivery/smtpserver/connection-manager.js | 918 ----------- .../mail/delivery/smtpserver/constants.d.ts | 130 -- dist_ts/mail/delivery/smtpserver/constants.js | 162 -- .../delivery/smtpserver/create-server.d.ts | 14 - .../mail/delivery/smtpserver/create-server.js | 28 - .../delivery/smtpserver/data-handler.d.ts | 123 -- .../mail/delivery/smtpserver/data-handler.js | 1152 -------------- dist_ts/mail/delivery/smtpserver/index.d.ts | 20 - dist_ts/mail/delivery/smtpserver/index.js | 27 - .../mail/delivery/smtpserver/interfaces.d.ts | 530 ------- .../mail/delivery/smtpserver/interfaces.js | 10 - .../delivery/smtpserver/secure-server.d.ts | 15 - .../mail/delivery/smtpserver/secure-server.js | 79 - .../delivery/smtpserver/security-handler.d.ts | 86 -- .../delivery/smtpserver/security-handler.js | 242 --- .../delivery/smtpserver/session-manager.d.ts | 140 -- .../delivery/smtpserver/session-manager.js | 473 ------ .../mail/delivery/smtpserver/smtp-server.d.ts | 137 -- .../mail/delivery/smtpserver/smtp-server.js | 698 --------- .../delivery/smtpserver/starttls-handler.d.ts | 21 - .../delivery/smtpserver/starttls-handler.js | 207 --- .../mail/delivery/smtpserver/tls-handler.d.ts | 66 - .../mail/delivery/smtpserver/tls-handler.js | 273 ---- .../smtpserver/utils/adaptive-logging.d.ts | 117 -- .../smtpserver/utils/adaptive-logging.js | 413 ----- .../delivery/smtpserver/utils/helpers.d.ts | 78 - .../mail/delivery/smtpserver/utils/helpers.js | 208 --- .../delivery/smtpserver/utils/logging.d.ts | 106 -- .../mail/delivery/smtpserver/utils/logging.js | 181 --- .../delivery/smtpserver/utils/validation.d.ts | 69 - .../delivery/smtpserver/utils/validation.js | 360 ----- .../routing/classes.unified.email.server.d.ts | 11 +- .../routing/classes.unified.email.server.js | 109 +- npmextra.json | 15 +- rust/Cargo.lock | 1 + .../mailer-security/src/content_scanner.rs | 4 +- rust/crates/mailer-smtp/Cargo.toml | 1 + rust/crates/mailer-smtp/src/connection.rs | 289 +++- rust/crates/mailer-smtp/src/server.rs | 23 + test/helpers/server.loader.ts | 275 +--- .../test.cmd-01.ehlo-command.ts | 193 --- .../test.cmd-02.mail-from.ts | 330 ---- .../test.cmd-03.rcpt-to.ts | 296 ---- .../test.cmd-04.data-command.ts | 395 ----- .../test.cmd-05.noop-command.ts | 320 ---- .../test.cmd-06.rset-command.ts | 399 ----- .../test.cmd-07.vrfy-command.ts | 391 ----- .../test.cmd-08.expn-command.ts | 450 ------ .../test.cmd-09.size-extension.ts | 465 ------ .../test.cmd-10.help-command.ts | 454 ------ .../test.cmd-11.command-pipelining.ts | 334 ---- .../test.cmd-12.helo-command.ts | 420 ------ .../test.cmd-13.quit-command.ts | 384 ----- .../test.cm-01.tls-connection.ts | 61 - .../test.cm-02.multiple-connections.ts | 112 -- .../test.cm-03.connection-timeout.ts | 134 -- .../test.cm-04.connection-limits.ts | 374 ----- .../test.cm-05.connection-rejection.ts | 296 ---- .../test.cm-06.starttls-upgrade.ts | 468 ------ .../test.cm-07.abrupt-disconnection.ts | 321 ---- .../test.cm-08.tls-versions.ts | 361 ----- .../test.cm-09.tls-ciphers.ts | 556 ------- .../test.cm-10.plain-connection.ts | 293 ---- .../test.cm-11.keepalive.ts | 382 ----- .../test.edge-01.very-large-email.ts | 239 --- .../test.edge-02.very-small-email.ts | 389 ----- ...test.edge-03.invalid-character-handling.ts | 479 ------ .../test.edge-04.empty-commands.ts | 430 ------ .../test.edge-05.extremely-long-lines.ts | 425 ------ .../test.edge-06.extremely-long-headers.ts | 404 ----- .../test.edge-07.unusual-mime-types.ts | 333 ---- .../test.edge-08.nested-mime-structures.ts | 379 ----- .../test.ep-01.basic-email-sending.ts | 338 ----- .../test.ep-02.invalid-email-addresses.ts | 315 ---- .../test.ep-03.multiple-recipients.ts | 493 ------ .../test.ep-04.large-email.ts | 528 ------- .../test.ep-05.mime-handling.ts | 515 ------- .../test.ep-06.attachment-handling.ts | 629 -------- .../test.ep-07.special-character-handling.ts | 462 ------ .../test.ep-08.email-routing.ts | 527 ------- ...est.ep-09.delivery-status-notifications.ts | 486 ------ .../test.err-01.syntax-errors.ts | 475 ------ .../test.err-02.invalid-sequence.ts | 450 ------ .../test.err-03.temporary-failures.ts | 453 ------ .../test.err-04.permanent-failures.ts | 325 ---- .../test.err-05.resource-exhaustion.ts | 302 ---- .../test.err-06.malformed-mime.ts | 374 ----- .../test.err-07.exception-handling.ts | 333 ---- .../test.err-08.error-logging.ts | 324 ---- .../test.perf-01.throughput.ts | 183 --- .../test.perf-02.concurrency.ts | 388 ----- .../test.perf-03.cpu-utilization.ts | 245 --- .../test.perf-04.memory-usage.ts | 238 --- ...test.perf-05.connection-processing-time.ts | 363 ----- .../test.perf-06.message-processing-time.ts | 252 ---- .../test.perf-07.resource-cleanup.ts | 317 ---- .../test.rel-01.long-running-operation.ts | 344 ----- .../test.rel-02.restart-recovery.ts | 328 ---- .../test.rel-03.resource-leak-detection.ts | 394 ----- .../test.rel-04.error-recovery.ts | 401 ----- .../test.rel-05.dns-resolution-failure.ts | 335 ----- .../test.rel-06.network-interruption.ts | 410 ----- .../test.rfc-01.rfc5321-compliance.ts | 382 ----- .../test.rfc-02.rfc5322-compliance.ts | 428 ------ .../test.rfc-03.rfc7208-spf-compliance.ts | 330 ---- .../test.rfc-04.rfc6376-dkim-compliance.ts | 450 ------ .../test.rfc-05.rfc7489-dmarc-compliance.ts | 408 ----- .../test.rfc-06.rfc8314-tls-compliance.ts | 366 ----- .../test.rfc-07.rfc3461-dsn-compliance.ts | 399 ----- .../test.sec-01.authentication.ts | 218 --- .../test.sec-02.authorization.ts | 286 ---- .../test.sec-03.dkim-processing.ts | 414 ----- .../test.sec-04.spf-checking.ts | 280 ---- .../test.sec-05.dmarc-policy.ts | 374 ----- .../test.sec-06.ip-reputation.ts | 303 ---- .../test.sec-07.content-scanning.ts | 409 ----- .../test.sec-08.rate-limiting.ts | 324 ---- .../test.sec-09.tls-certificate-validation.ts | 312 ---- ...test.sec-10.header-injection-prevention.ts | 332 ---- .../test.sec-11.bounce-management.ts | 363 ----- test/test.smtp.server.ts | 180 --- ts/00_commitinfo_data.ts | 2 +- ts/mail/delivery/index.ts | 3 +- .../delivery/smtpserver/certificate-utils.ts | 398 ----- .../delivery/smtpserver/command-handler.ts | 1340 ----------------- .../delivery/smtpserver/connection-manager.ts | 1061 ------------- ts/mail/delivery/smtpserver/constants.ts | 181 --- ts/mail/delivery/smtpserver/create-server.ts | 31 - ts/mail/delivery/smtpserver/data-handler.ts | 1283 ---------------- ts/mail/delivery/smtpserver/index.ts | 32 - ts/mail/delivery/smtpserver/interfaces.ts | 655 -------- ts/mail/delivery/smtpserver/secure-server.ts | 97 -- .../delivery/smtpserver/security-handler.ts | 345 ----- .../delivery/smtpserver/session-manager.ts | 557 ------- ts/mail/delivery/smtpserver/smtp-server.ts | 804 ---------- .../delivery/smtpserver/starttls-handler.ts | 262 ---- ts/mail/delivery/smtpserver/tls-handler.ts | 346 ----- .../smtpserver/utils/adaptive-logging.ts | 514 ------- ts/mail/delivery/smtpserver/utils/helpers.ts | 246 --- ts/mail/delivery/smtpserver/utils/logging.ts | 246 --- .../delivery/smtpserver/utils/validation.ts | 436 ------ .../routing/classes.unified.email.server.ts | 118 +- 151 files changed, 477 insertions(+), 47531 deletions(-) delete mode 100644 dist_ts/mail/delivery/smtpserver/certificate-utils.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/certificate-utils.js delete mode 100644 dist_ts/mail/delivery/smtpserver/command-handler.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/command-handler.js delete mode 100644 dist_ts/mail/delivery/smtpserver/connection-manager.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/connection-manager.js delete mode 100644 dist_ts/mail/delivery/smtpserver/constants.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/constants.js delete mode 100644 dist_ts/mail/delivery/smtpserver/create-server.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/create-server.js delete mode 100644 dist_ts/mail/delivery/smtpserver/data-handler.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/data-handler.js delete mode 100644 dist_ts/mail/delivery/smtpserver/index.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/index.js delete mode 100644 dist_ts/mail/delivery/smtpserver/interfaces.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/interfaces.js delete mode 100644 dist_ts/mail/delivery/smtpserver/secure-server.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/secure-server.js delete mode 100644 dist_ts/mail/delivery/smtpserver/security-handler.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/security-handler.js delete mode 100644 dist_ts/mail/delivery/smtpserver/session-manager.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/session-manager.js delete mode 100644 dist_ts/mail/delivery/smtpserver/smtp-server.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/smtp-server.js delete mode 100644 dist_ts/mail/delivery/smtpserver/starttls-handler.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/starttls-handler.js delete mode 100644 dist_ts/mail/delivery/smtpserver/tls-handler.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/tls-handler.js delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.js delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/helpers.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/helpers.js delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/logging.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/logging.js delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/validation.d.ts delete mode 100644 dist_ts/mail/delivery/smtpserver/utils/validation.js delete mode 100644 test/suite/smtpserver_commands/test.cmd-01.ehlo-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-02.mail-from.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-03.rcpt-to.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-04.data-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-05.noop-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-06.rset-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-07.vrfy-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-08.expn-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-09.size-extension.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-10.help-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-11.command-pipelining.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-12.helo-command.ts delete mode 100644 test/suite/smtpserver_commands/test.cmd-13.quit-command.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-01.tls-connection.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-02.multiple-connections.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-03.connection-timeout.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-04.connection-limits.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-05.connection-rejection.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-06.starttls-upgrade.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-07.abrupt-disconnection.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-08.tls-versions.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-09.tls-ciphers.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-10.plain-connection.ts delete mode 100644 test/suite/smtpserver_connection/test.cm-11.keepalive.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-01.very-large-email.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-02.very-small-email.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-03.invalid-character-handling.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-04.empty-commands.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-05.extremely-long-lines.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-06.extremely-long-headers.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-07.unusual-mime-types.ts delete mode 100644 test/suite/smtpserver_edge-cases/test.edge-08.nested-mime-structures.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-01.basic-email-sending.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-02.invalid-email-addresses.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-03.multiple-recipients.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-04.large-email.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-05.mime-handling.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-06.attachment-handling.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-07.special-character-handling.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-08.email-routing.ts delete mode 100644 test/suite/smtpserver_email-processing/test.ep-09.delivery-status-notifications.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-01.syntax-errors.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-02.invalid-sequence.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-03.temporary-failures.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-04.permanent-failures.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-05.resource-exhaustion.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-06.malformed-mime.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-07.exception-handling.ts delete mode 100644 test/suite/smtpserver_error-handling/test.err-08.error-logging.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-01.throughput.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-02.concurrency.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-03.cpu-utilization.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-04.memory-usage.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-05.connection-processing-time.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-06.message-processing-time.ts delete mode 100644 test/suite/smtpserver_performance/test.perf-07.resource-cleanup.ts delete mode 100644 test/suite/smtpserver_reliability/test.rel-01.long-running-operation.ts delete mode 100644 test/suite/smtpserver_reliability/test.rel-02.restart-recovery.ts delete mode 100644 test/suite/smtpserver_reliability/test.rel-03.resource-leak-detection.ts delete mode 100644 test/suite/smtpserver_reliability/test.rel-04.error-recovery.ts delete mode 100644 test/suite/smtpserver_reliability/test.rel-05.dns-resolution-failure.ts delete mode 100644 test/suite/smtpserver_reliability/test.rel-06.network-interruption.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-01.rfc5321-compliance.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-02.rfc5322-compliance.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-03.rfc7208-spf-compliance.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-04.rfc6376-dkim-compliance.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-05.rfc7489-dmarc-compliance.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-06.rfc8314-tls-compliance.ts delete mode 100644 test/suite/smtpserver_rfc-compliance/test.rfc-07.rfc3461-dsn-compliance.ts delete mode 100644 test/suite/smtpserver_security/test.sec-01.authentication.ts delete mode 100644 test/suite/smtpserver_security/test.sec-02.authorization.ts delete mode 100644 test/suite/smtpserver_security/test.sec-03.dkim-processing.ts delete mode 100644 test/suite/smtpserver_security/test.sec-04.spf-checking.ts delete mode 100644 test/suite/smtpserver_security/test.sec-05.dmarc-policy.ts delete mode 100644 test/suite/smtpserver_security/test.sec-06.ip-reputation.ts delete mode 100644 test/suite/smtpserver_security/test.sec-07.content-scanning.ts delete mode 100644 test/suite/smtpserver_security/test.sec-08.rate-limiting.ts delete mode 100644 test/suite/smtpserver_security/test.sec-09.tls-certificate-validation.ts delete mode 100644 test/suite/smtpserver_security/test.sec-10.header-injection-prevention.ts delete mode 100644 test/suite/smtpserver_security/test.sec-11.bounce-management.ts delete mode 100644 test/test.smtp.server.ts delete mode 100644 ts/mail/delivery/smtpserver/certificate-utils.ts delete mode 100644 ts/mail/delivery/smtpserver/command-handler.ts delete mode 100644 ts/mail/delivery/smtpserver/connection-manager.ts delete mode 100644 ts/mail/delivery/smtpserver/constants.ts delete mode 100644 ts/mail/delivery/smtpserver/create-server.ts delete mode 100644 ts/mail/delivery/smtpserver/data-handler.ts delete mode 100644 ts/mail/delivery/smtpserver/index.ts delete mode 100644 ts/mail/delivery/smtpserver/interfaces.ts delete mode 100644 ts/mail/delivery/smtpserver/secure-server.ts delete mode 100644 ts/mail/delivery/smtpserver/security-handler.ts delete mode 100644 ts/mail/delivery/smtpserver/session-manager.ts delete mode 100644 ts/mail/delivery/smtpserver/smtp-server.ts delete mode 100644 ts/mail/delivery/smtpserver/starttls-handler.ts delete mode 100644 ts/mail/delivery/smtpserver/tls-handler.ts delete mode 100644 ts/mail/delivery/smtpserver/utils/adaptive-logging.ts delete mode 100644 ts/mail/delivery/smtpserver/utils/helpers.ts delete mode 100644 ts/mail/delivery/smtpserver/utils/logging.ts delete mode 100644 ts/mail/delivery/smtpserver/utils/validation.ts diff --git a/changelog.md b/changelog.md index b7f88ab..af61137 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-10 - 2.3.0 - feat(mailer-smtp) +add in-process security pipeline for SMTP delivery (DKIM/SPF/DMARC, content scanning, IP reputation) + +- Integrate mailer_security verification (DKIM/SPF/DMARC) and IP reputation checks into the Rust SMTP server; run concurrently and wrapped with a 30s timeout. +- Add MIME parsing using mailparse and an extract_mime_parts helper to extract subject, text/html bodies and attachment filenames for content scanning. +- Wire MessageAuthenticator and TokioResolver into server and connection startup; pass them into the delivery pipeline and connection handlers. +- Run content scanning (mailer_security::content_scanner), combine results (dkim/spf/dmarc, contentScan, ipReputation) into a JSON object and attach as security_results on EmailReceived events. +- Update Rust crates (Cargo.toml/Cargo.lock) to include mailparse and resolver usage and add serde::Deserialize where required; add unit tests for MIME extraction. +- Remove the TypeScript SMTP server implementation and many TS tests; replace test helper (server.loader.ts) with a stub that points tests to use the Rust SMTP server and provide small utilities (getAvailablePort/isPortFree). + ## 2026-02-10 - 2.2.1 - fix(readme) Clarify Rust-powered architecture and mandatory Rust bridge; expand README with Rust workspace details and project structure updates diff --git a/dist_ts/00_commitinfo_data.js b/dist_ts/00_commitinfo_data.js index 47b8c58..5c9a05c 100644 --- a/dist_ts/00_commitinfo_data.js +++ b/dist_ts/00_commitinfo_data.js @@ -3,7 +3,7 @@ */ export const commitinfo = { name: '@push.rocks/smartmta', - version: '2.1.0', + version: '2.2.1', description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' }; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlIQUF5SDtDQUN2SSxDQUFBIn0= \ No newline at end of file diff --git a/dist_ts/mail/delivery/index.d.ts b/dist_ts/mail/delivery/index.d.ts index 473fb2a..7faca12 100644 --- a/dist_ts/mail/delivery/index.d.ts +++ b/dist_ts/mail/delivery/index.d.ts @@ -8,5 +8,4 @@ export type { IRateLimitConfig } from './classes.ratelimiter.js'; export * from './classes.unified.rate.limiter.js'; export * from './classes.mta.config.js'; import * as smtpClientMod from './smtpclient/index.js'; -import * as smtpServerMod from './smtpserver/index.js'; -export { smtpClientMod, smtpServerMod }; +export { smtpClientMod }; diff --git a/dist_ts/mail/delivery/index.js b/dist_ts/mail/delivery/index.js index d8037ad..7a2abdf 100644 --- a/dist_ts/mail/delivery/index.js +++ b/dist_ts/mail/delivery/index.js @@ -13,6 +13,5 @@ export * from './classes.unified.rate.limiter.js'; export * from './classes.mta.config.js'; // Import and export SMTP modules as namespaces to avoid conflicts import * as smtpClientMod from './smtpclient/index.js'; -import * as smtpServerMod from './smtpserver/index.js'; -export { smtpClientMod, smtpServerMod }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDRCQUE0QjtBQUM1QixjQUFjLDJCQUEyQixDQUFDO0FBQzFDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyw4QkFBOEIsQ0FBQztBQUU3Qyx1Q0FBdUM7QUFDdkMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQ3pELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUU5RCw2Q0FBNkM7QUFDN0MsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBR3ZELHVCQUF1QjtBQUN2QixjQUFjLG1DQUFtQyxDQUFDO0FBRWxELGdDQUFnQztBQUNoQyxjQUFjLHlCQUF5QixDQUFDO0FBRXhDLGtFQUFrRTtBQUNsRSxPQUFPLEtBQUssYUFBYSxNQUFNLHVCQUF1QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxhQUFhLE1BQU0sdUJBQXVCLENBQUM7QUFFdkQsT0FBTyxFQUFFLGFBQWEsRUFBRSxhQUFhLEVBQUUsQ0FBQyJ9 \ No newline at end of file +export { smtpClientMod }; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDRCQUE0QjtBQUM1QixjQUFjLDJCQUEyQixDQUFDO0FBQzFDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyw4QkFBOEIsQ0FBQztBQUU3Qyx1Q0FBdUM7QUFDdkMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQ3pELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUU5RCw2Q0FBNkM7QUFDN0MsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBR3ZELHVCQUF1QjtBQUN2QixjQUFjLG1DQUFtQyxDQUFDO0FBRWxELGdDQUFnQztBQUNoQyxjQUFjLHlCQUF5QixDQUFDO0FBRXhDLGtFQUFrRTtBQUNsRSxPQUFPLEtBQUssYUFBYSxNQUFNLHVCQUF1QixDQUFDO0FBRXZELE9BQU8sRUFBRSxhQUFhLEVBQUUsQ0FBQyJ9 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/certificate-utils.d.ts b/dist_ts/mail/delivery/smtpserver/certificate-utils.d.ts deleted file mode 100644 index 669df6e..0000000 --- a/dist_ts/mail/delivery/smtpserver/certificate-utils.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Certificate Utilities for SMTP Server - * Provides utilities for managing TLS certificates - */ -import * as tls from 'tls'; -/** - * Certificate data - */ -export interface ICertificateData { - key: Buffer; - cert: Buffer; - ca?: Buffer; -} -/** - * Load certificates from PEM format strings - * @param options - Certificate options - * @returns Certificate data with Buffer format - */ -export declare function loadCertificatesFromString(options: { - key: string | Buffer; - cert: string | Buffer; - ca?: string | Buffer; -}): ICertificateData; -/** - * Load certificates from files - * @param options - Certificate file paths - * @returns Certificate data with Buffer format - */ -export declare function loadCertificatesFromFiles(options: { - keyPath: string; - certPath: string; - caPath?: string; -}): ICertificateData; -/** - * Generate self-signed certificates for testing - * @returns Certificate data with Buffer format - */ -export declare function generateSelfSignedCertificates(): ICertificateData; -/** - * Create TLS options for secure server or STARTTLS - * @param certificates - Certificate data - * @param isServer - Whether this is for server (true) or client (false) - * @returns TLS options - */ -export declare function createTlsOptions(certificates: ICertificateData, isServer?: boolean): tls.TlsOptions; diff --git a/dist_ts/mail/delivery/smtpserver/certificate-utils.js b/dist_ts/mail/delivery/smtpserver/certificate-utils.js deleted file mode 100644 index fecb3c6..0000000 --- a/dist_ts/mail/delivery/smtpserver/certificate-utils.js +++ /dev/null @@ -1,345 +0,0 @@ -/** - * Certificate Utilities for SMTP Server - * Provides utilities for managing TLS certificates - */ -import * as fs from 'fs'; -import * as tls from 'tls'; -import { SmtpLogger } from './utils/logging.js'; -/** - * Normalize a PEM certificate string - * @param str - Certificate string - * @returns Normalized certificate string - */ -function normalizeCertificate(str) { - // Handle different input types - let inputStr; - if (Buffer.isBuffer(str)) { - // Convert Buffer to string using utf8 encoding - inputStr = str.toString('utf8'); - } - else if (typeof str === 'string') { - inputStr = str; - } - else { - throw new Error('Certificate must be a string or Buffer'); - } - if (!inputStr) { - throw new Error('Empty certificate data'); - } - // Remove any whitespace around the string - let normalizedStr = inputStr.trim(); - // Make sure it has proper PEM format - if (!normalizedStr.includes('-----BEGIN ')) { - throw new Error('Invalid certificate format: Missing BEGIN marker'); - } - if (!normalizedStr.includes('-----END ')) { - throw new Error('Invalid certificate format: Missing END marker'); - } - // Normalize line endings (replace Windows-style \r\n with Unix-style \n) - normalizedStr = normalizedStr.replace(/\r\n/g, '\n'); - // Only normalize if the certificate appears to have formatting issues - // Check if the certificate is already properly formatted - const lines = normalizedStr.split('\n'); - let needsReformatting = false; - // Check for common formatting issues: - // 1. Missing line breaks after header/before footer - // 2. Lines that are too long or too short (except header/footer) - // 3. Multiple consecutive blank lines - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith('-----BEGIN ') || line.startsWith('-----END ')) { - continue; // Skip header/footer lines - } - if (line.length === 0) { - continue; // Skip empty lines - } - // Check if content lines are reasonable length (base64 is typically 64 chars per line) - if (line.length > 76) { // Allow some flexibility beyond standard 64 - needsReformatting = true; - break; - } - } - // Only reformat if necessary - if (needsReformatting) { - const beginMatch = normalizedStr.match(/^(-----BEGIN [^-]+-----)(.*)$/s); - const endMatch = normalizedStr.match(/(.*)(-----END [^-]+-----)$/s); - if (beginMatch && endMatch) { - const header = beginMatch[1]; - const footer = endMatch[2]; - let content = normalizedStr.substring(header.length, normalizedStr.length - footer.length); - // Clean up only line breaks and carriage returns, preserve base64 content - content = content.replace(/[\n\r]/g, '').trim(); - // Add proper line breaks (every 64 characters) - let formattedContent = ''; - for (let i = 0; i < content.length; i += 64) { - formattedContent += content.substring(i, Math.min(i + 64, content.length)) + '\n'; - } - // Reconstruct the certificate - return header + '\n' + formattedContent + footer; - } - } - return normalizedStr; -} -/** - * Load certificates from PEM format strings - * @param options - Certificate options - * @returns Certificate data with Buffer format - */ -export function loadCertificatesFromString(options) { - try { - // First try to use certificates without normalization - try { - let keyStr; - let certStr; - let caStr; - // Convert inputs to strings without aggressive normalization - if (Buffer.isBuffer(options.key)) { - keyStr = options.key.toString('utf8'); - } - else { - keyStr = options.key; - } - if (Buffer.isBuffer(options.cert)) { - certStr = options.cert.toString('utf8'); - } - else { - certStr = options.cert; - } - if (options.ca) { - if (Buffer.isBuffer(options.ca)) { - caStr = options.ca.toString('utf8'); - } - else { - caStr = options.ca; - } - } - // Simple cleanup - only normalize line endings - keyStr = keyStr.trim().replace(/\r\n/g, '\n'); - certStr = certStr.trim().replace(/\r\n/g, '\n'); - if (caStr) { - caStr = caStr.trim().replace(/\r\n/g, '\n'); - } - // Convert to buffers - const keyBuffer = Buffer.from(keyStr, 'utf8'); - const certBuffer = Buffer.from(certStr, 'utf8'); - const caBuffer = caStr ? Buffer.from(caStr, 'utf8') : undefined; - // Test the certificates first - const secureContext = tls.createSecureContext({ - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }); - SmtpLogger.info('Successfully validated certificates without normalization'); - return { - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }; - } - catch (simpleError) { - SmtpLogger.warn(`Simple certificate loading failed, trying normalization: ${simpleError instanceof Error ? simpleError.message : String(simpleError)}`); - // DEBUG: Log certificate details when simple loading fails - SmtpLogger.warn('Certificate loading failure details', { - keyType: typeof options.key, - certType: typeof options.cert, - keyIsBuffer: Buffer.isBuffer(options.key), - certIsBuffer: Buffer.isBuffer(options.cert), - keyLength: options.key ? options.key.length : 0, - certLength: options.cert ? options.cert.length : 0, - keyPreview: options.key ? (typeof options.key === 'string' ? options.key.substring(0, 50) : options.key.toString('utf8').substring(0, 50)) : 'null', - certPreview: options.cert ? (typeof options.cert === 'string' ? options.cert.substring(0, 50) : options.cert.toString('utf8').substring(0, 50)) : 'null' - }); - } - // Fallback: Try to fix and normalize certificates - try { - // Normalize certificates (handles both string and Buffer inputs) - const key = normalizeCertificate(options.key); - const cert = normalizeCertificate(options.cert); - const ca = options.ca ? normalizeCertificate(options.ca) : undefined; - // Convert normalized strings to Buffer with explicit utf8 encoding - const keyBuffer = Buffer.from(key, 'utf8'); - const certBuffer = Buffer.from(cert, 'utf8'); - const caBuffer = ca ? Buffer.from(ca, 'utf8') : undefined; - // Log for debugging - SmtpLogger.debug('Certificate properties', { - keyLength: keyBuffer.length, - certLength: certBuffer.length, - caLength: caBuffer ? caBuffer.length : 0 - }); - // Validate the certificates by attempting to create a secure context - try { - const secureContext = tls.createSecureContext({ - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }); - // If createSecureContext doesn't throw, the certificates are valid - SmtpLogger.info('Successfully validated certificate format'); - } - catch (validationError) { - // Log detailed error information for debugging - SmtpLogger.error(`Certificate validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`); - SmtpLogger.debug('Certificate validation details', { - keyPreview: keyBuffer.toString('utf8').substring(0, 100) + '...', - certPreview: certBuffer.toString('utf8').substring(0, 100) + '...', - keyLength: keyBuffer.length, - certLength: certBuffer.length - }); - throw validationError; - } - return { - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }; - } - catch (innerError) { - SmtpLogger.warn(`Certificate normalization failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`); - throw innerError; - } - } - catch (error) { - SmtpLogger.error(`Error loading certificates: ${error instanceof Error ? error.message : String(error)}`); - throw error; - } -} -/** - * Load certificates from files - * @param options - Certificate file paths - * @returns Certificate data with Buffer format - */ -export function loadCertificatesFromFiles(options) { - try { - // Read files directly as Buffers - const key = fs.readFileSync(options.keyPath); - const cert = fs.readFileSync(options.certPath); - const ca = options.caPath ? fs.readFileSync(options.caPath) : undefined; - // Log for debugging - SmtpLogger.debug('Certificate file properties', { - keyLength: key.length, - certLength: cert.length, - caLength: ca ? ca.length : 0 - }); - // Validate the certificates by attempting to create a secure context - try { - const secureContext = tls.createSecureContext({ - key, - cert, - ca - }); - // If createSecureContext doesn't throw, the certificates are valid - SmtpLogger.info('Successfully validated certificate files'); - } - catch (validationError) { - SmtpLogger.error(`Certificate file validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`); - throw validationError; - } - return { - key, - cert, - ca - }; - } - catch (error) { - SmtpLogger.error(`Error loading certificate files: ${error instanceof Error ? error.message : String(error)}`); - throw error; - } -} -/** - * Generate self-signed certificates for testing - * @returns Certificate data with Buffer format - */ -export function generateSelfSignedCertificates() { - // This is for fallback/testing only - log a warning - SmtpLogger.warn('Generating self-signed certificates for testing - DO NOT USE IN PRODUCTION'); - // Create selfsigned certificates using node-forge or similar library - // For now, use hardcoded certificates as a last resort - const key = Buffer.from(`-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEgJW1HdJPACGB -ifoL3PB+HdAVA2nUmMfq43JbIUPXGTxCtzmQhuV04WjITwFw1loPx3ReHh4KR5yJ -BVdzUDocHuauMmBycHAjv7mImR/VkuK/SwT0Q5G/9/M55o6HUNol0UKt+uZuBy1r -ggFTdTDLw86i9UG5CZbWF/Yb/DTRoAkCr7iLnaZhhhqcdh5BGj7JBylIAV5RIW1y -xQxJVJZQT2KgCeCnHRRvYRQ7tVzUQBcSvtW4zYtqK4C39BgRyLUZQVYB7siGT/uP -YJE7R73u0xEgDMFWR1pItUYcVQXHQJ+YsLVCzqI22Mik7URdwxoSHSXRYKn6wnKg -4JYg65JnAgMBAAECggEAM2LlwRhwP0pnLlLHiPE4jJ3Qdz/NUF0hLnRhcUwW1iJ1 -03jzCQ4QZ3etfL9O2hVJg49J+QUG50FNduLq4SE7GZj1dEJ/YNnlk9PpI8GSpLuA -mGTUKofIEJjNy5gKR0c6/rfgP8UXYSbRnTnZwIXVkUYuAUJLJTBVcJlcvCwJ3/zz -C8789JyOO1CNwF3zEIALdW5X5se8V+sw5iHDrHVxkR2xgsYpBBOylFfBxbMvV5o1 -i+QOD1HaXdmIvjBCnHqrjX5SDnAYwHBSB9y6WbwC+Th76QHkRNcHZH86PJVdLEUi -tBPQmQh+SjDRaZzDJvURnOFks+eEsCPVPZnQ4wgnAQKBgQD8oHwGZIZRUjnXULNc -vJoPcjLpvdHRO0kXTJHtG2au2i9jVzL9SFwH1lHQM0XdXPnR2BK4Gmgc2dRnSB9n -YPPvCgyL2RS0Y7W98yEcgBgwVOJHnPQGRNwxUfCTHgmCQ7lXjQKKG51+dBfOYP3j -w8VYbS2pqxZtzzZ5zhk2BrZJdwKBgQDHDZC+NU80f7rLEr5vpwx9epTArwXre8oj -nGgzZ9/lE14qDnITBuZPUHWc4/7U1CCmP0vVH6nFVvhN9ra9QCTJBzQ5aj0l3JM7 -9j8R5QZIPqOu4+aqf0ZFEgmpBK2SAYqNrJ+YVa2T/zLF44Jlr5WiLkPTUyMxV5+k -P4ZK8QP7wQKBgQCbeLuRWCuVKNYgYjm9TA55BbJL82J+MvhcbXUccpUksJQRxMV3 -98PBUW0Qw38WciJxQF4naSKD/jXYndD+wGzpKMIU+tKU+sEYMnuFnx13++K8XrAe -NQPHDsK1wRgXk5ygOHx78xnZbMmwBXNLwQXIhyO8FJpwJHj2CtYvjb+2xwKBgQCn -KW/RiAHvG6GKjCHCOTlx2qLPxUiXYCk2xwvRnNfY5+2PFoqMI/RZLT/41kTda1fA -TDw+j4Uu/fF2ChPadwRiUjXZzZx/UjcMJXTpQ2kpbGJ11U/cL4+Tk0S6wz+HoS7z -w3vXT9UoDyFxDBjuMQJxJWTjmymaYUtNnz4iMuRqwQKBgH+HKbYHCZaIzXRMEO5S -T3xDMYH59dTEKKXEOA1KJ9Zo5XSD8NE9SQ+9etoOcEq8tdYS45OkHD3VyFQa7THu -58awjTdkpSmMPsw3AElOYDYJgD9oxKtTjwkXHqMjDBQZrXqzOImOAJhEVL+XH3LP -lv6RZ47YRC88T+P6n1yg6BPp ------END PRIVATE KEY-----`, 'utf8'); - const cert = Buffer.from(`-----BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIUHxmGQOQoiSbzqh6hIe+7h9xDXIUwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUyMTE2MDAzM1oXDTI2MDUy -MTE2MDAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAxICVtR3STwAhgYn6C9zwfh3QFQNp1JjH6uNyWyFD1xk8 -Qrc5kIbldOFoyE8BcNZaD8d0Xh4eCkeciwOV3FwHR4brjJgcnRwI7+5iJkf1ZLiv -0sE9EORv/fzOeaOh1DaJdFCrfrmbgdgOUm62WNQOB2hq0kggjh/S1K+TBfF+8QFs -XQyW7y7mHecNgCgK/pI5b1irdajRc7nLvzM/U8qNn4jjrLsRoYqBPpn7aLKIBrmN -pNSIe18q8EYWkdmWBcnsZpAYv75SJG8E0lAYpMv9OEUIwsPh7AYUdkZqKtFxVxV5 -bYlA5ZfnVnWrWEwRXaVdFFRXIjP+EFkGYYWThbvAIb0TPQIDAQABo1MwUTAdBgNV -HQ4EFgQUiW1MoYR8YK9KJTyip5oFoUVJoCgwHwYDVR0jBBgwFoAUiW1MoYR8YK9K -JTyip5oFoUVJoCgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA -BToM8SbUQXwJ9rTlQB2QI2GJaFwTpCFoQZwGUOCkwGLM3nOPLEbNPMDoIKGPwenB -P1xL8uJEgYRqP6UG/xy3HsxYsLCxuoxGGP2QjuiQKnFl0n85usZ5flCxmLC5IzYx -FLcR6WPTdj6b5JX0tM8Bi6toQ9Pj3u3dSVPZKRLYvJvZKt1PXI8qsHD/LvNa2wGG -Zi1BQFAr2cScNYa+p6IYDJi9TBNxoBIHNTzQPfWaen4MHRJqUNZCzQXcOnU/NW5G -+QqQSEMmk8yGucEHWUMFrEbABVgYuBslICEEtBZALB2jZJYSaJnPOJCcmFrxUv61 -ORWZbz+8rBL0JIeA7eFxEA== ------END CERTIFICATE-----`, 'utf8'); - return { - key, - cert - }; -} -/** - * Create TLS options for secure server or STARTTLS - * @param certificates - Certificate data - * @param isServer - Whether this is for server (true) or client (false) - * @returns TLS options - */ -export function createTlsOptions(certificates, isServer = true) { - const options = { - key: certificates.key, - cert: certificates.cert, - ca: certificates.ca, - // Support a wider range of TLS versions for better compatibility - minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0) - maxVersion: 'TLSv1.3', // Support latest TLS version (1.3) - // Cipher suites for broad compatibility - ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4', - // For testing, allow unauthorized (self-signed certs) - rejectUnauthorized: false, - // Longer handshake timeout for reliability - handshakeTimeout: 30000, - // TLS renegotiation option (removed - not supported in newer Node.js) - // Increase timeout for better reliability under test conditions - sessionTimeout: 600, - // Let the client choose the cipher for better compatibility - honorCipherOrder: false, - // For debugging - enableTrace: true, - // Disable secure options to allow more flexibility - secureOptions: 0 - }; - // Server-specific options - if (isServer) { - options.ALPNProtocols = ['smtp']; // Accept non-ALPN connections (legacy clients) - } - return options; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2VydGlmaWNhdGUtdXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBzZXJ2ZXIvY2VydGlmaWNhdGUtdXRpbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxLQUFLLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFDekIsT0FBTyxLQUFLLEdBQUcsTUFBTSxLQUFLLENBQUM7QUFDM0IsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBV2hEOzs7O0dBSUc7QUFDSCxTQUFTLG9CQUFvQixDQUFDLEdBQW9CO0lBQ2hELCtCQUErQjtJQUMvQixJQUFJLFFBQWdCLENBQUM7SUFFckIsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDekIsK0NBQStDO1FBQy9DLFFBQVEsR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ2xDLENBQUM7U0FBTSxJQUFJLE9BQU8sR0FBRyxLQUFLLFFBQVEsRUFBRSxDQUFDO1FBQ25DLFFBQVEsR0FBRyxHQUFHLENBQUM7SUFDakIsQ0FBQztTQUFNLENBQUM7UUFDTixNQUFNLElBQUksS0FBSyxDQUFDLHdDQUF3QyxDQUFDLENBQUM7SUFDNUQsQ0FBQztJQUVELElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNkLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUM1QyxDQUFDO0lBRUQsMENBQTBDO0lBQzFDLElBQUksYUFBYSxHQUFHLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUVwQyxxQ0FBcUM7SUFDckMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQztRQUMzQyxNQUFNLElBQUksS0FBSyxDQUFDLGtEQUFrRCxDQUFDLENBQUM7SUFDdEUsQ0FBQztJQUVELElBQUks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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/command-handler.d.ts b/dist_ts/mail/delivery/smtpserver/command-handler.d.ts deleted file mode 100644 index ce658d8..0000000 --- a/dist_ts/mail/delivery/smtpserver/command-handler.d.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * SMTP Command Handler - * Responsible for parsing and handling SMTP commands - */ -import * as plugins from '../../../plugins.js'; -import type { ISmtpSession } from './interfaces.js'; -import type { ICommandHandler, ISmtpServer } from './interfaces.js'; -import { SmtpCommand } from './constants.js'; -/** - * Handles SMTP commands and responses - */ -export declare class CommandHandler implements ICommandHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer; - /** - * Creates a new command handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer); - /** - * Process a command from the client - * @param socket - Client socket - * @param commandLine - Command line from client - */ - processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): Promise; - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response to send - */ - sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void; - /** - * Check if a socket error is potentially recoverable - * @param error - The error that occurred - * @returns Whether the error is potentially recoverable - */ - private isRecoverableSocketError; - /** - * Handle recoverable socket errors with retry logic - * @param socket - Client socket - * @param error - The error that occurred - * @param response - The response that failed to send - */ - private handleSocketError; - /** - * Handle EHLO command - * @param socket - Client socket - * @param clientHostname - Client hostname from EHLO command - */ - handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void; - /** - * Handle MAIL FROM command - * @param socket - Client socket - * @param args - Command arguments - */ - handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void; - /** - * Handle RCPT TO command - * @param socket - Client socket - * @param args - Command arguments - */ - handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void; - /** - * Handle DATA command - * @param socket - Client socket - */ - handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Handle RSET command - * @param socket - Client socket - */ - handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Handle NOOP command - * @param socket - Client socket - */ - handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Handle QUIT command - * @param socket - Client socket - */ - handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket, args?: string): void; - /** - * Handle AUTH command - * @param socket - Client socket - * @param args - Command arguments - */ - private handleAuth; - /** - * Handle AUTH PLAIN authentication - * @param socket - Client socket - * @param session - Session - * @param initialResponse - Optional initial response - */ - private handleAuthPlain; - /** - * Handle AUTH LOGIN authentication - * @param socket - Client socket - * @param session - Session - * @param initialResponse - Optional initial response - */ - private handleAuthLogin; - /** - * Handle AUTH LOGIN response - * @param socket - Client socket - * @param session - Session - * @param response - Response from client - */ - private handleAuthLoginResponse; - /** - * Handle HELP command - * @param socket - Client socket - * @param args - Command arguments - */ - private handleHelp; - /** - * Handle VRFY command (Verify user/mailbox) - * RFC 5321 Section 3.5.1: Server MAY respond with 252 to avoid disclosing sensitive information - * @param socket - Client socket - * @param args - Command arguments (username to verify) - */ - private handleVrfy; - /** - * Handle EXPN command (Expand mailing list) - * RFC 5321 Section 3.5.2: Server MAY disable this for security - * @param socket - Client socket - * @param args - Command arguments (mailing list to expand) - */ - private handleExpn; - /** - * Reset session to after-EHLO state - * @param session - SMTP session to reset - */ - private resetSession; - /** - * Validate command sequence based on current state - * @param command - Command to validate - * @param session - Current session - * @returns Whether the command is valid in the current state - */ - private validateCommandSequence; - /** - * Handle an SMTP command (interface requirement) - */ - handleCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, command: SmtpCommand, args: string, session: ISmtpSession): Promise; - /** - * Get supported commands for current session state (interface requirement) - */ - getSupportedCommands(session: ISmtpSession): SmtpCommand[]; - /** - * Clean up resources - */ - destroy(): void; -} diff --git a/dist_ts/mail/delivery/smtpserver/command-handler.js b/dist_ts/mail/delivery/smtpserver/command-handler.js deleted file mode 100644 index 0bc70a1..0000000 --- a/dist_ts/mail/delivery/smtpserver/command-handler.js +++ /dev/null @@ -1,1163 +0,0 @@ -/** - * SMTP Command Handler - * Responsible for parsing and handling SMTP commands - */ -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import { SmtpCommand, SmtpResponseCode, SMTP_DEFAULTS, SMTP_EXTENSIONS } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { adaptiveLogger } from './utils/adaptive-logging.js'; -import { extractCommandName, extractCommandArgs, formatMultilineResponse } from './utils/helpers.js'; -import { validateEhlo, validateMailFrom, validateRcptTo, isValidCommandSequence } from './utils/validation.js'; -/** - * Handles SMTP commands and responses - */ -export class CommandHandler { - /** - * Reference to the SMTP server instance - */ - smtpServer; - /** - * Creates a new command handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer) { - this.smtpServer = smtpServer; - } - /** - * Process a command from the client - * @param socket - Client socket - * @param commandLine - Command line from client - */ - async processCommand(socket, commandLine) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - SmtpLogger.warn(`No session found for socket from ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - socket.end(); - return; - } - // Check if we're in the middle of an AUTH LOGIN sequence - if (session.authLoginState) { - await this.handleAuthLoginResponse(socket, session, commandLine); - return; - } - // Handle raw data chunks from connection manager during DATA mode - if (commandLine.startsWith('__RAW_DATA__')) { - const rawData = commandLine.substring('__RAW_DATA__'.length); - const dataHandler = this.smtpServer.getDataHandler(); - if (dataHandler) { - // Let the data handler process the raw chunk - dataHandler.handleDataReceived(socket, rawData) - .catch(error => { - SmtpLogger.error(`Error processing raw email data: ${error.message}`, { - sessionId: session.id, - error - }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email data: ${error.message}`); - this.resetSession(session); - }); - } - else { - // No data handler available - SmtpLogger.error('Data handler not available for raw data', { sessionId: session.id }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - data handler not available`); - this.resetSession(session); - } - return; - } - // Handle data state differently - pass to data handler (legacy line-based processing) - if (session.state === SmtpState.DATA_RECEIVING) { - // Check if this looks like an SMTP command - during DATA mode all input should be treated as message content - const looksLikeCommand = /^[A-Z]{4,}( |:)/i.test(commandLine.trim()); - // Special handling for ERR-02 test: handle "MAIL FROM" during DATA mode - // The test expects a 503 response for this case, not treating it as content - if (looksLikeCommand && commandLine.trim().toUpperCase().startsWith('MAIL FROM')) { - // This is the command that ERR-02 test is expecting to fail with 503 - SmtpLogger.debug(`Received MAIL FROM command during DATA mode - responding with sequence error`); - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - const dataHandler = this.smtpServer.getDataHandler(); - if (dataHandler) { - // Let the data handler process the line (legacy mode) - dataHandler.processEmailData(socket, commandLine) - .catch(error => { - SmtpLogger.error(`Error processing email data: ${error.message}`, { - sessionId: session.id, - error - }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email data: ${error.message}`); - this.resetSession(session); - }); - } - else { - // No data handler available - SmtpLogger.error('Data handler not available', { sessionId: session.id }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - data handler not available`); - this.resetSession(session); - } - return; - } - // Handle command pipelining (RFC 2920) - // Multiple commands can be sent in a single TCP packet - if (commandLine.includes('\r\n') || commandLine.includes('\n')) { - // Split the commandLine into individual commands by newline - const commands = commandLine.split(/\r\n|\n/).filter(line => line.trim().length > 0); - if (commands.length > 1) { - SmtpLogger.debug(`Command pipelining detected: ${commands.length} commands`, { - sessionId: session.id, - commandCount: commands.length - }); - // Process each command separately (recursively call processCommand) - for (const cmd of commands) { - await this.processCommand(socket, cmd); - } - return; - } - } - // Log received command using adaptive logger - adaptiveLogger.logCommand(commandLine, socket, session); - // Extract command and arguments - const command = extractCommandName(commandLine); - const args = extractCommandArgs(commandLine); - // For the ERR-01 test, an empty or invalid command is considered a syntax error (500) - if (!command || command.trim().length === 0) { - // Record error for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordError(session.remoteAddress); - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive errors`); - this.sendResponse(socket, `421 Too many errors - connection blocked`); - socket.end(); - } - else { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Command not recognized`); - } - return; - } - // Handle unknown commands - this should happen before sequence validation - // RFC 5321: Use 500 for unrecognized commands, 501 for parameter errors - if (!Object.values(SmtpCommand).includes(command.toUpperCase())) { - // Record error for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordError(session.remoteAddress); - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive errors`); - this.sendResponse(socket, `421 Too many errors - connection blocked`); - socket.end(); - } - else { - // Comply with RFC 5321 section 4.2.4: Use 500 for unrecognized commands - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Command not recognized`); - } - return; - } - // Handle test input "MAIL FROM: missing_brackets@example.com" - specifically check for this case - // This is needed for ERR-01 test to pass - if (command.toUpperCase() === SmtpCommand.MAIL_FROM) { - // Handle "MAIL FROM:" with missing parameter - a special case for ERR-01 test - if (!args || args.trim() === '' || args.trim() === ':') { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Missing email address`); - return; - } - // Handle email without angle brackets - if (args.includes('@') && !args.includes('<') && !args.includes('>')) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid syntax - angle brackets required`); - return; - } - } - // Special handling for the "MAIL FROM:" missing parameter test (ERR-01 Test 3) - // The test explicitly sends "MAIL FROM:" without any address and expects 501 - // We need to catch this EXACT case before the sequence validation - if (commandLine.trim() === 'MAIL FROM:') { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Missing email address`); - return; - } - // Validate command sequence - this must happen after validating that it's a recognized command - // The order matters for ERR-01 and ERR-02 test compliance: - // - Syntax errors (501): Invalid command format or arguments - // - Sequence errors (503): Valid command in wrong sequence - if (!this.validateCommandSequence(command, session)) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - // Process the command - switch (command) { - case SmtpCommand.EHLO: - case SmtpCommand.HELO: - this.handleEhlo(socket, args); - break; - case SmtpCommand.MAIL_FROM: - this.handleMailFrom(socket, args); - break; - case SmtpCommand.RCPT_TO: - this.handleRcptTo(socket, args); - break; - case SmtpCommand.DATA: - this.handleData(socket); - break; - case SmtpCommand.RSET: - this.handleRset(socket); - break; - case SmtpCommand.NOOP: - this.handleNoop(socket); - break; - case SmtpCommand.QUIT: - this.handleQuit(socket, args); - break; - case SmtpCommand.STARTTLS: - const tlsHandler = this.smtpServer.getTlsHandler(); - if (tlsHandler && tlsHandler.isTlsEnabled()) { - await tlsHandler.handleStartTls(socket, session); - } - else { - SmtpLogger.warn('STARTTLS requested but TLS is not enabled', { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort - }); - this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} STARTTLS not available at this time`); - } - break; - case SmtpCommand.AUTH: - this.handleAuth(socket, args); - break; - case SmtpCommand.HELP: - this.handleHelp(socket, args); - break; - case SmtpCommand.VRFY: - this.handleVrfy(socket, args); - break; - case SmtpCommand.EXPN: - this.handleExpn(socket, args); - break; - default: - this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Command not implemented`); - break; - } - } - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response to send - */ - sendResponse(socket, response) { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - adaptiveLogger.logResponse(response, socket); - } - catch (error) { - // Attempt to recover from known transient errors - if (this.isRecoverableSocketError(error)) { - this.handleSocketError(socket, error, response); - } - else { - // Log error and destroy socket for non-recoverable errors - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - socket.destroy(); - } - } - } - /** - * Check if a socket error is potentially recoverable - * @param error - The error that occurred - * @returns Whether the error is potentially recoverable - */ - isRecoverableSocketError(error) { - const recoverableErrorCodes = [ - 'EPIPE', // Broken pipe - 'ECONNRESET', // Connection reset by peer - 'ETIMEDOUT', // Connection timed out - 'ECONNABORTED' // Connection aborted - ]; - return (error instanceof Error && - 'code' in error && - typeof error.code === 'string' && - recoverableErrorCodes.includes(error.code)); - } - /** - * Handle recoverable socket errors with retry logic - * @param socket - Client socket - * @param error - The error that occurred - * @param response - The response that failed to send - */ - handleSocketError(socket, error, response) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - SmtpLogger.error(`Session not found when handling socket error`); - socket.destroy(); - return; - } - // Get error details for logging - const errorMessage = error instanceof Error ? error.message : String(error); - const errorCode = error instanceof Error && 'code' in error ? error.code : 'UNKNOWN'; - SmtpLogger.warn(`Recoverable socket error (${errorCode}): ${errorMessage}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Check if socket is already destroyed - if (socket.destroyed) { - SmtpLogger.info(`Socket already destroyed, cannot retry operation`); - return; - } - // Check if socket is writeable - if (!socket.writable) { - SmtpLogger.info(`Socket no longer writable, aborting recovery attempt`); - socket.destroy(); - return; - } - // Attempt to retry the write operation after a short delay - setTimeout(() => { - try { - if (!socket.destroyed && socket.writable) { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - SmtpLogger.info(`Successfully retried send operation after error`); - } - else { - SmtpLogger.warn(`Socket no longer available for retry`); - if (!socket.destroyed) { - socket.destroy(); - } - } - } - catch (retryError) { - SmtpLogger.error(`Retry attempt failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`); - if (!socket.destroyed) { - socket.destroy(); - } - } - }, 100); // Short delay before retry - } - /** - * Handle EHLO command - * @param socket - Client socket - * @param clientHostname - Client hostname from EHLO command - */ - handleEhlo(socket, clientHostname) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Extract command and arguments from clientHostname - // EHLO/HELO might come with the command itself in the arguments string - let hostname = clientHostname; - if (hostname.toUpperCase().startsWith('EHLO ') || hostname.toUpperCase().startsWith('HELO ')) { - hostname = hostname.substring(5).trim(); - } - // Check for empty hostname - if (!hostname) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Missing domain name`); - return; - } - // Validate EHLO hostname - const validation = validateEhlo(hostname); - if (!validation.isValid) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`); - return; - } - // Update session state and client hostname - session.clientHostname = validation.hostname || hostname; - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO); - // Get options once for this method - const options = this.smtpServer.getOptions(); - // Set up EHLO response lines - const responseLines = [ - `${options.hostname || SMTP_DEFAULTS.HOSTNAME} greets ${session.clientHostname}`, - SMTP_EXTENSIONS.PIPELINING, - SMTP_EXTENSIONS.formatExtension(SMTP_EXTENSIONS.SIZE, options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE), - SMTP_EXTENSIONS.EIGHTBITMIME, - SMTP_EXTENSIONS.ENHANCEDSTATUSCODES - ]; - // Add TLS extension if available and not already using TLS - const tlsHandler = this.smtpServer.getTlsHandler(); - if (tlsHandler && tlsHandler.isTlsEnabled() && !session.useTLS) { - responseLines.push(SMTP_EXTENSIONS.STARTTLS); - } - // Add AUTH extension if configured - if (options.auth && options.auth.methods && options.auth.methods.length > 0) { - responseLines.push(`${SMTP_EXTENSIONS.AUTH} ${options.auth.methods.join(' ')}`); - } - // Send multiline response - this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.OK, responseLines)); - } - /** - * Handle MAIL FROM command - * @param socket - Client socket - * @param args - Command arguments - */ - handleMailFrom(socket, args) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Check if the client has sent EHLO/HELO first - if (session.state === SmtpState.GREETING) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - // For test compatibility - reset state if receiving a new MAIL FROM after previous transaction - if (session.state === SmtpState.MAIL_FROM || session.state === SmtpState.RCPT_TO) { - // Silently reset the transaction state - allow multiple MAIL FROM commands - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - session.envelope = { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }; - } - // Get options once for this method - const options = this.smtpServer.getOptions(); - // Check if authentication is required but not provided - if (options.auth && options.auth.required && !session.authenticated) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_REQUIRED} Authentication required`); - return; - } - // Get rate limiter for message-level checks - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - // Note: Connection-level rate limiting is already handled in ConnectionManager - // Special handling for commands that include "MAIL FROM:" in the args - let processedArgs = args; - // Handle test formats with or without colons and "FROM" parts - if (args.toUpperCase().startsWith('FROM:')) { - processedArgs = args.substring(5).trim(); // Skip "FROM:" - } - else if (args.toUpperCase().startsWith('FROM')) { - processedArgs = args.substring(4).trim(); // Skip "FROM" - } - else if (args.toUpperCase().includes('MAIL FROM:')) { - // The command was already prepended to the args - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - processedArgs = args.substring(colonIndex + 1).trim(); - } - } - else if (args.toUpperCase().includes('MAIL FROM')) { - // Handle case without colon - const fromIndex = args.toUpperCase().indexOf('FROM'); - if (fromIndex !== -1) { - processedArgs = args.substring(fromIndex + 4).trim(); - } - } - // Validate MAIL FROM syntax - for ERR-01 test compliance, this must be BEFORE sequence validation - const validation = validateMailFrom(processedArgs); - if (!validation.isValid) { - // Return 501 for syntax errors - required for ERR-01 test to pass - // This RFC 5321 compliance is critical - syntax errors must be 501 - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`); - return; - } - // Check message rate limits for this sender - const senderAddress = validation.address || ''; - const senderDomain = senderAddress.includes('@') ? senderAddress.split('@')[1] : undefined; - // Check rate limits with domain context if available - const messageResult = rateLimiter.checkMessageLimit(senderAddress, session.remoteAddress, 1, // We don't know recipients yet, check with 1 - undefined, // No pattern matching for now - senderDomain // Pass domain for domain-specific limits - ); - if (!messageResult.allowed) { - SmtpLogger.warn(`Message rate limit exceeded for ${senderAddress} from IP ${session.remoteAddress}: ${messageResult.reason}`); - // Use 421 for temporary rate limiting (client should retry later) - this.sendResponse(socket, `421 ${messageResult.reason} - try again later`); - return; - } - // Enhanced SIZE parameter handling - if (validation.params && validation.params.SIZE) { - const size = parseInt(validation.params.SIZE, 10); - // Check for valid numeric format - if (isNaN(size)) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: not a number`); - return; - } - // Check for negative values - if (size < 0) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: cannot be negative`); - return; - } - // Ensure reasonable minimum size (at least 100 bytes for headers) - if (size < 100) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: too small (minimum 100 bytes)`); - return; - } - // Check against server maximum - const maxSize = options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE; - if (size > maxSize) { - // Generate informative error with the server's limit - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit of ${Math.floor(maxSize / 1024)} KB`); - return; - } - // Log large messages for monitoring - if (size > maxSize * 0.8) { - SmtpLogger.info(`Large message detected (${Math.floor(size / 1024)} KB)`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - sizeBytes: size, - percentOfMax: Math.floor((size / maxSize) * 100) - }); - } - } - // Reset email data and recipients for new transaction - session.mailFrom = validation.address || ''; - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - // Update envelope information - session.envelope = { - mailFrom: { - address: validation.address || '', - args: validation.params || {} - }, - rcptTo: [] - }; - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.MAIL_FROM); - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); - } - /** - * Handle RCPT TO command - * @param socket - Client socket - * @param args - Command arguments - */ - handleRcptTo(socket, args) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Check if MAIL FROM was provided first - if (session.state !== SmtpState.MAIL_FROM && session.state !== SmtpState.RCPT_TO) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - // Special handling for commands that include "RCPT TO:" in the args - let processedArgs = args; - if (args.toUpperCase().startsWith('TO:')) { - processedArgs = args; - } - else if (args.toUpperCase().includes('RCPT TO')) { - // The command was already prepended to the args - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - processedArgs = args.substring(colonIndex + 1).trim(); - } - } - // Validate RCPT TO syntax - const validation = validateRcptTo(processedArgs); - if (!validation.isValid) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`); - return; - } - // Check if we've reached maximum recipients - const options = this.smtpServer.getOptions(); - const maxRecipients = options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS; - if (session.rcptTo.length >= maxRecipients) { - this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Too many recipients`); - return; - } - // Check rate limits for recipients - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const recipientAddress = validation.address || ''; - const recipientDomain = recipientAddress.includes('@') ? recipientAddress.split('@')[1] : undefined; - // Check rate limits with accumulated recipient count - const recipientCount = session.rcptTo.length + 1; // Including this new recipient - const messageResult = rateLimiter.checkMessageLimit(session.mailFrom, session.remoteAddress, recipientCount, undefined, // No pattern matching for now - recipientDomain // Pass recipient domain for domain-specific limits - ); - if (!messageResult.allowed) { - SmtpLogger.warn(`Recipient rate limit exceeded for ${recipientAddress} from IP ${session.remoteAddress}: ${messageResult.reason}`); - // Use 451 for temporary recipient rejection - this.sendResponse(socket, `451 ${messageResult.reason} - try again later`); - return; - } - // Create recipient object - const recipient = { - address: validation.address || '', - args: validation.params || {} - }; - // Add to session data - session.rcptTo.push(validation.address || ''); - session.envelope.rcptTo.push(recipient); - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.RCPT_TO); - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} Recipient ok`); - } - /** - * Handle DATA command - * @param socket - Client socket - */ - handleData(socket) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // For tests, be slightly more permissive - also accept DATA after MAIL FROM - // But ensure we at least have a sender defined - if (session.state !== SmtpState.RCPT_TO && session.state !== SmtpState.MAIL_FROM) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - // Check if we have a sender - if (!session.mailFrom) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} No sender specified`); - return; - } - // Ideally we should have recipients, but for test compatibility, we'll only - // insist on recipients if we're in RCPT_TO state - if (session.state === SmtpState.RCPT_TO && !session.rcptTo.length) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} No recipients specified`); - return; - } - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.DATA_RECEIVING); - // Reset email data storage - session.emailData = ''; - session.emailDataChunks = []; - // Set up timeout for DATA command - const dataTimeout = SMTP_DEFAULTS.DATA_TIMEOUT; - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - session.dataTimeoutId = setTimeout(() => { - if (session.state === SmtpState.DATA_RECEIVING) { - SmtpLogger.warn(`DATA command timeout for session ${session.id}`, { - sessionId: session.id, - timeout: dataTimeout - }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`); - this.resetSession(session); - } - }, dataTimeout); - // Send intermediate response to signal start of data - this.sendResponse(socket, `${SmtpResponseCode.START_MAIL_INPUT} Start mail input; end with .`); - } - /** - * Handle RSET command - * @param socket - Client socket - */ - handleRset(socket) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Reset the transaction state - this.resetSession(session); - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); - } - /** - * Handle NOOP command - * @param socket - Client socket - */ - handleNoop(socket) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); - } - /** - * Handle QUIT command - * @param socket - Client socket - */ - handleQuit(socket, args) { - // QUIT command should not have any parameters - if (args && args.trim().length > 0) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Syntax error in parameters`); - return; - } - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - // Send goodbye message - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_CLOSING} ${this.smtpServer.getOptions().hostname} Service closing transmission channel`); - // End the connection - socket.end(); - // Clean up session if we have one - if (session) { - this.smtpServer.getSessionManager().removeSession(socket); - } - } - /** - * Handle AUTH command - * @param socket - Client socket - * @param args - Command arguments - */ - handleAuth(socket, args) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Check if we have auth config - if (!this.smtpServer.getOptions().auth || !this.smtpServer.getOptions().auth.methods || !this.smtpServer.getOptions().auth.methods.length) { - this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Authentication not supported`); - return; - } - // Check if TLS is required for authentication - if (!session.useTLS) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication requires TLS`); - return; - } - // Parse AUTH command - const parts = args.trim().split(/\s+/); - const method = parts[0]?.toUpperCase(); - const initialResponse = parts[1]; - // Check if method is supported - const supportedMethods = this.smtpServer.getOptions().auth.methods.map(m => m.toUpperCase()); - if (!method || !supportedMethods.includes(method)) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Unsupported authentication method`); - return; - } - // Handle different authentication methods - switch (method) { - case 'PLAIN': - this.handleAuthPlain(socket, session, initialResponse); - break; - case 'LOGIN': - this.handleAuthLogin(socket, session, initialResponse); - break; - default: - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} ${method} authentication not implemented`); - } - } - /** - * Handle AUTH PLAIN authentication - * @param socket - Client socket - * @param session - Session - * @param initialResponse - Optional initial response - */ - async handleAuthPlain(socket, session, initialResponse) { - try { - let credentials; - if (initialResponse) { - // Credentials provided with AUTH PLAIN command - credentials = initialResponse; - } - else { - // Request credentials - this.sendResponse(socket, '334'); - // Wait for credentials - credentials = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Auth response timeout')); - }, 30000); - socket.once('data', (data) => { - clearTimeout(timeout); - resolve(data.toString().trim()); - }); - }); - } - // Decode PLAIN credentials (base64 encoded: authzid\0authcid\0password) - const decoded = Buffer.from(credentials, 'base64').toString('utf8'); - const parts = decoded.split('\0'); - if (parts.length !== 3) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Invalid credentials format`); - return; - } - const [authzid, authcid, password] = parts; - const username = authcid || authzid; // Use authcid if provided, otherwise authzid - // Authenticate using security handler - const authenticated = await this.smtpServer.getSecurityHandler().authenticate({ - username, - password - }); - if (authenticated) { - session.authenticated = true; - session.username = username; - this.sendResponse(socket, `${SmtpResponseCode.AUTHENTICATION_SUCCESSFUL} Authentication successful`); - } - else { - // Record authentication failure for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordAuthFailure(session.remoteAddress); - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive authentication failures`); - this.sendResponse(socket, `421 Too many authentication failures - connection blocked`); - socket.end(); - } - else { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication failed`); - } - } - } - catch (error) { - SmtpLogger.error(`AUTH PLAIN error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`); - } - } - /** - * Handle AUTH LOGIN authentication - * @param socket - Client socket - * @param session - Session - * @param initialResponse - Optional initial response - */ - async handleAuthLogin(socket, session, initialResponse) { - try { - if (initialResponse) { - // Username provided with AUTH LOGIN command - const username = Buffer.from(initialResponse, 'base64').toString('utf8'); - session.authLoginState = 'waiting_password'; - session.authLoginUsername = username; - // Request password - this.sendResponse(socket, '334 UGFzc3dvcmQ6'); // Base64 for "Password:" - } - else { - // Request username - session.authLoginState = 'waiting_username'; - this.sendResponse(socket, '334 VXNlcm5hbWU6'); // Base64 for "Username:" - } - } - catch (error) { - SmtpLogger.error(`AUTH LOGIN error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`); - delete session.authLoginState; - delete session.authLoginUsername; - } - } - /** - * Handle AUTH LOGIN response - * @param socket - Client socket - * @param session - Session - * @param response - Response from client - */ - async handleAuthLoginResponse(socket, session, response) { - const trimmedResponse = response.trim(); - // Check for cancellation - if (trimmedResponse === '*') { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication cancelled`); - delete session.authLoginState; - delete session.authLoginUsername; - return; - } - try { - if (session.authLoginState === 'waiting_username') { - // We received the username - const username = Buffer.from(trimmedResponse, 'base64').toString('utf8'); - session.authLoginUsername = username; - session.authLoginState = 'waiting_password'; - // Request password - this.sendResponse(socket, '334 UGFzc3dvcmQ6'); // Base64 for "Password:" - } - else if (session.authLoginState === 'waiting_password') { - // We received the password - const password = Buffer.from(trimmedResponse, 'base64').toString('utf8'); - const username = session.authLoginUsername; - // Clear auth state - delete session.authLoginState; - delete session.authLoginUsername; - // Authenticate using security handler - const authenticated = await this.smtpServer.getSecurityHandler().authenticate({ - username, - password - }); - if (authenticated) { - session.authenticated = true; - session.username = username; - this.sendResponse(socket, `${SmtpResponseCode.AUTHENTICATION_SUCCESSFUL} Authentication successful`); - } - else { - // Record authentication failure for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordAuthFailure(session.remoteAddress); - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive authentication failures`); - this.sendResponse(socket, `421 Too many authentication failures - connection blocked`); - socket.end(); - } - else { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication failed`); - } - } - } - } - catch (error) { - SmtpLogger.error(`AUTH LOGIN response error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`); - delete session.authLoginState; - delete session.authLoginUsername; - } - } - /** - * Handle HELP command - * @param socket - Client socket - * @param args - Command arguments - */ - handleHelp(socket, args) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - // Provide help information based on arguments - const helpCommand = args.trim().toUpperCase(); - if (!helpCommand) { - // General help - const helpLines = [ - 'Supported commands:', - 'EHLO/HELO domain - Identify yourself to the server', - 'MAIL FROM:
- Start a new mail transaction', - 'RCPT TO:
- Specify recipients for the message', - 'DATA - Start message data input', - 'RSET - Reset the transaction', - 'NOOP - No operation', - 'QUIT - Close the connection', - 'HELP [command] - Show help' - ]; - // Add conditional commands - const tlsHandler = this.smtpServer.getTlsHandler(); - if (tlsHandler && tlsHandler.isTlsEnabled()) { - helpLines.push('STARTTLS - Start TLS negotiation'); - } - if (this.smtpServer.getOptions().auth && this.smtpServer.getOptions().auth.methods.length) { - helpLines.push('AUTH mechanism - Authenticate with the server'); - } - this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.HELP_MESSAGE, helpLines)); - return; - } - // Command-specific help - let helpText; - switch (helpCommand) { - case 'EHLO': - case 'HELO': - helpText = 'EHLO/HELO domain - Identify yourself to the server'; - break; - case 'MAIL': - helpText = 'MAIL FROM:
[SIZE=size] - Start a new mail transaction'; - break; - case 'RCPT': - helpText = 'RCPT TO:
- Specify a recipient for the message'; - break; - case 'DATA': - helpText = 'DATA - Start message data input, end with .'; - break; - case 'RSET': - helpText = 'RSET - Reset the transaction'; - break; - case 'NOOP': - helpText = 'NOOP - No operation'; - break; - case 'QUIT': - helpText = 'QUIT - Close the connection'; - break; - case 'STARTTLS': - helpText = 'STARTTLS - Start TLS negotiation'; - break; - case 'AUTH': - helpText = `AUTH mechanism - Authenticate with the server. Supported methods: ${this.smtpServer.getOptions().auth?.methods.join(', ')}`; - break; - default: - helpText = `Unknown command: ${helpCommand}`; - break; - } - this.sendResponse(socket, `${SmtpResponseCode.HELP_MESSAGE} ${helpText}`); - } - /** - * Handle VRFY command (Verify user/mailbox) - * RFC 5321 Section 3.5.1: Server MAY respond with 252 to avoid disclosing sensitive information - * @param socket - Client socket - * @param args - Command arguments (username to verify) - */ - handleVrfy(socket, args) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - const username = args.trim(); - // Security best practice: Do not confirm or deny user existence - // Instead, respond with 252 "Cannot verify, but will attempt delivery" - // This prevents VRFY from being used for user enumeration attacks - if (!username) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} User name required`); - } - else { - // Log the VRFY attempt - SmtpLogger.info(`VRFY command received for user: ${username}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - useTLS: session.useTLS - }); - // Respond with ambiguous response for security - this.sendResponse(socket, `${SmtpResponseCode.CANNOT_VRFY} Cannot VRFY user, but will accept message and attempt delivery`); - } - } - /** - * Handle EXPN command (Expand mailing list) - * RFC 5321 Section 3.5.2: Server MAY disable this for security - * @param socket - Client socket - * @param args - Command arguments (mailing list to expand) - */ - handleExpn(socket, args) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - const listname = args.trim(); - // Log the EXPN attempt - SmtpLogger.info(`EXPN command received for list: ${listname}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - useTLS: session.useTLS - }); - // Disable EXPN for security (best practice - RFC 5321 Section 3.5.2) - // EXPN allows enumeration of list members, which is a privacy concern - this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} EXPN command is disabled for security reasons`); - } - /** - * Reset session to after-EHLO state - * @param session - SMTP session to reset - */ - resetSession(session) { - // Clear any data timeout - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - // Reset data fields but keep authentication state - session.mailFrom = ''; - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - session.envelope = { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }; - // Reset state to after EHLO - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO); - } - /** - * Validate command sequence based on current state - * @param command - Command to validate - * @param session - Current session - * @returns Whether the command is valid in the current state - */ - validateCommandSequence(command, session) { - // Always allow EHLO to reset the transaction at any state - // This makes tests pass where EHLO is used multiple times - if (command.toUpperCase() === 'EHLO' || command.toUpperCase() === 'HELO') { - return true; - } - // Always allow RSET, NOOP, QUIT, and HELP - if (command.toUpperCase() === 'RSET' || - command.toUpperCase() === 'NOOP' || - command.toUpperCase() === 'QUIT' || - command.toUpperCase() === 'HELP') { - return true; - } - // Always allow STARTTLS after EHLO/HELO (but not in DATA state) - if (command.toUpperCase() === 'STARTTLS' && - (session.state === SmtpState.AFTER_EHLO || - session.state === SmtpState.MAIL_FROM || - session.state === SmtpState.RCPT_TO)) { - return true; - } - // During testing, be more permissive with sequence for MAIL and RCPT commands - // This helps pass tests that may send these commands in unexpected order - if (command.toUpperCase() === 'MAIL' && session.state !== SmtpState.DATA_RECEIVING) { - return true; - } - // Handle RCPT TO during tests - be permissive but not in DATA state - if (command.toUpperCase() === 'RCPT' && session.state !== SmtpState.DATA_RECEIVING) { - return true; - } - // Allow DATA command if in MAIL_FROM or RCPT_TO state for test compatibility - if (command.toUpperCase() === 'DATA' && - (session.state === SmtpState.MAIL_FROM || session.state === SmtpState.RCPT_TO)) { - return true; - } - // Check standard command sequence - return isValidCommandSequence(command, session.state); - } - /** - * Handle an SMTP command (interface requirement) - */ - async handleCommand(socket, command, args, session) { - // Delegate to processCommand for now - this.processCommand(socket, `${command} ${args}`.trim()); - } - /** - * Get supported commands for current session state (interface requirement) - */ - getSupportedCommands(session) { - const commands = [SmtpCommand.NOOP, SmtpCommand.QUIT, SmtpCommand.RSET]; - switch (session.state) { - case SmtpState.GREETING: - commands.push(SmtpCommand.EHLO, SmtpCommand.HELO); - break; - case SmtpState.AFTER_EHLO: - commands.push(SmtpCommand.MAIL_FROM, SmtpCommand.STARTTLS); - if (!session.authenticated) { - commands.push(SmtpCommand.AUTH); - } - break; - case SmtpState.MAIL_FROM: - commands.push(SmtpCommand.RCPT_TO); - break; - case SmtpState.RCPT_TO: - commands.push(SmtpCommand.RCPT_TO, SmtpCommand.DATA); - break; - default: - break; - } - return commands; - } - /** - * Clean up resources - */ - destroy() { - // CommandHandler doesn't have timers or event listeners to clean up - SmtpLogger.debug('CommandHandler destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tbWFuZC1oYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL2NvbW1hbmQtaGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUc1QyxPQUFPLEVBQUUsV0FBVyxFQUFFLGdCQUFnQixFQUFFLGFBQWEsRUFBRSxlQUFlLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMvRixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDaEQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQzdELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxrQkFBa0IsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3JHLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsY0FBYyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFFL0c7O0dBRUc7QUFDSCxNQUFNLE9BQU8sY0FBYztJQUN6Qjs7T0FFRztJQUNLLFVBQVUsQ0FBYztJQUVoQzs7O09BR0c7SUFDSCxZQUFZLFVBQXVCO1FBQ2pDLElBQUksQ0FBQyxVQUFVLEdBQUcsVUFBVSxDQUFDO0lBQy9CLENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksS0FBSyxDQUFDLGNBQWMsQ0FBQyxNQUFrRCxFQUFFLFdBQW1CO1FBQ2pHLGtDQUFrQztRQUNsQyxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLGlCQUFpQixFQUFFLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQ3ZFLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNiLFVBQVUsQ0FBQyxJQUFJLENBQUMsb0NBQW9D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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/connection-manager.d.ts b/dist_ts/mail/delivery/smtpserver/connection-manager.d.ts deleted file mode 100644 index 5bb1c99..0000000 --- a/dist_ts/mail/delivery/smtpserver/connection-manager.d.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * SMTP Connection Manager - * Responsible for managing socket connections to the SMTP server - */ -import * as plugins from '../../../plugins.js'; -import type { IConnectionManager, ISmtpServer } from './interfaces.js'; -/** - * Manager for SMTP connections - * Handles connection setup, event listeners, and lifecycle management - * Provides resource management, connection tracking, and monitoring - */ -export declare class ConnectionManager implements IConnectionManager { - /** - * Reference to the SMTP server instance - */ - private smtpServer; - /** - * Set of active socket connections - */ - private activeConnections; - /** - * Connection tracking for resource management - */ - private connectionStats; - /** - * Per-IP connection tracking for rate limiting - */ - private ipConnections; - /** - * Resource monitoring interval - */ - private resourceCheckInterval; - /** - * Track cleanup timers so we can clear them - */ - private cleanupTimers; - /** - * SMTP server options with enhanced resource controls - */ - private options; - /** - * Creates a new connection manager with enhanced resource management - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer); - /** - * Start resource monitoring interval to check resource usage - */ - private startResourceMonitoring; - /** - * Monitor resource usage and log statistics - */ - private monitorResourceUsage; - /** - * Clean up expired IP rate limits and perform additional resource monitoring - */ - private cleanupIpRateLimits; - /** - * Validate and repair resource tracking to prevent leaks - */ - private validateResourceTracking; - /** - * Handle a new connection with resource management - * @param socket - Client socket - */ - handleNewConnection(socket: plugins.net.Socket): Promise; - /** - * Check if an IP has exceeded the rate limit - * @param ip - Client IP address - * @returns True if rate limited - */ - private isIPRateLimited; - /** - * Track a new connection from an IP - * @param ip - Client IP address - */ - private trackIPConnection; - /** - * Check if an IP has reached its connection limit - * @param ip - Client IP address - * @returns True if limit reached - */ - private hasReachedIPConnectionLimit; - /** - * Handle a new secure TLS connection with resource management - * @param socket - Client TLS socket - */ - handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise; - /** - * Set up event handlers for a socket with enhanced resource management - * @param socket - Client socket - */ - setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Get the current connection count - * @returns Number of active connections - */ - getConnectionCount(): number; - /** - * Check if the server has reached the maximum number of connections - * @returns True if max connections reached - */ - hasReachedMaxConnections(): boolean; - /** - * Close all active connections - */ - closeAllConnections(): void; - /** - * Handle socket close event - * @param socket - Client socket - * @param hadError - Whether the socket was closed due to error - */ - private handleSocketClose; - /** - * Handle socket error event - * @param socket - Client socket - * @param error - Error object - */ - private handleSocketError; - /** - * Handle socket timeout event - * @param socket - Client socket - */ - private handleSocketTimeout; - /** - * Reject a connection - * @param socket - Client socket - * @param reason - Reason for rejection - */ - private rejectConnection; - /** - * Send greeting message - * @param socket - Client socket - */ - private sendGreeting; - /** - * Send service closing notification - * @param socket - Client socket - */ - private sendServiceClosing; - /** - * Send response to client - * @param socket - Client socket - * @param response - Response to send - */ - private sendResponse; - /** - * Handle a new connection (interface requirement) - */ - handleConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): Promise; - /** - * Check if accepting new connections (interface requirement) - */ - canAcceptConnection(): boolean; - /** - * Clean up resources - */ - destroy(): void; -} diff --git a/dist_ts/mail/delivery/smtpserver/connection-manager.js b/dist_ts/mail/delivery/smtpserver/connection-manager.js deleted file mode 100644 index a5b639d..0000000 --- a/dist_ts/mail/delivery/smtpserver/connection-manager.js +++ /dev/null @@ -1,918 +0,0 @@ -/** - * SMTP Connection Manager - * Responsible for managing socket connections to the SMTP server - */ -import * as plugins from '../../../plugins.js'; -import { SmtpResponseCode, SMTP_DEFAULTS, SmtpState } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { adaptiveLogger } from './utils/adaptive-logging.js'; -import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js'; -/** - * Manager for SMTP connections - * Handles connection setup, event listeners, and lifecycle management - * Provides resource management, connection tracking, and monitoring - */ -export class ConnectionManager { - /** - * Reference to the SMTP server instance - */ - smtpServer; - /** - * Set of active socket connections - */ - activeConnections = new Set(); - /** - * Connection tracking for resource management - */ - connectionStats = { - totalConnections: 0, - activeConnections: 0, - peakConnections: 0, - rejectedConnections: 0, - closedConnections: 0, - erroredConnections: 0, - timedOutConnections: 0 - }; - /** - * Per-IP connection tracking for rate limiting - */ - ipConnections = new Map(); - /** - * Resource monitoring interval - */ - resourceCheckInterval = null; - /** - * Track cleanup timers so we can clear them - */ - cleanupTimers = new Set(); - /** - * SMTP server options with enhanced resource controls - */ - options; - /** - * Creates a new connection manager with enhanced resource management - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer) { - this.smtpServer = smtpServer; - // Get options from server - const serverOptions = this.smtpServer.getOptions(); - // Default values for resource management - adjusted for production scalability - const DEFAULT_MAX_CONNECTIONS_PER_IP = 50; // Increased to support high-concurrency scenarios - const DEFAULT_CONNECTION_RATE_LIMIT = 200; // Increased for production load handling - const DEFAULT_CONNECTION_RATE_WINDOW = 60 * 1000; // 60 seconds window - const DEFAULT_BUFFER_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB - const DEFAULT_RESOURCE_CHECK_INTERVAL = 30 * 1000; // 30 seconds - this.options = { - hostname: serverOptions.hostname || SMTP_DEFAULTS.HOSTNAME, - maxConnections: serverOptions.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, - socketTimeout: serverOptions.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, - maxConnectionsPerIP: DEFAULT_MAX_CONNECTIONS_PER_IP, - connectionRateLimit: DEFAULT_CONNECTION_RATE_LIMIT, - connectionRateWindow: DEFAULT_CONNECTION_RATE_WINDOW, - bufferSizeLimit: DEFAULT_BUFFER_SIZE_LIMIT, - resourceCheckInterval: DEFAULT_RESOURCE_CHECK_INTERVAL - }; - // Start resource monitoring - this.startResourceMonitoring(); - } - /** - * Start resource monitoring interval to check resource usage - */ - startResourceMonitoring() { - // Clear any existing interval - if (this.resourceCheckInterval) { - clearInterval(this.resourceCheckInterval); - } - // Set up new interval - this.resourceCheckInterval = setInterval(() => { - this.monitorResourceUsage(); - }, this.options.resourceCheckInterval); - } - /** - * Monitor resource usage and log statistics - */ - monitorResourceUsage() { - // Calculate memory usage - const memoryUsage = process.memoryUsage(); - const memoryUsageMB = { - rss: Math.round(memoryUsage.rss / 1024 / 1024), - heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024), - heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024), - external: Math.round(memoryUsage.external / 1024 / 1024) - }; - // Calculate connection rate metrics - const activeIPs = Array.from(this.ipConnections.entries()) - .filter(([_, data]) => data.count > 0).length; - const highVolumeIPs = Array.from(this.ipConnections.entries()) - .filter(([_, data]) => data.count > this.options.connectionRateLimit / 2).length; - // Log resource usage with more detailed metrics - SmtpLogger.info('Resource usage stats', { - connections: { - active: this.activeConnections.size, - total: this.connectionStats.totalConnections, - peak: this.connectionStats.peakConnections, - rejected: this.connectionStats.rejectedConnections, - closed: this.connectionStats.closedConnections, - errored: this.connectionStats.erroredConnections, - timedOut: this.connectionStats.timedOutConnections - }, - memory: memoryUsageMB, - ipTracking: { - uniqueIPs: this.ipConnections.size, - activeIPs: activeIPs, - highVolumeIPs: highVolumeIPs - }, - resourceLimits: { - maxConnections: this.options.maxConnections, - maxConnectionsPerIP: this.options.maxConnectionsPerIP, - connectionRateLimit: this.options.connectionRateLimit, - bufferSizeLimit: Math.round(this.options.bufferSizeLimit / 1024 / 1024) + 'MB' - } - }); - // Check for potential DoS conditions - if (highVolumeIPs > 3) { - SmtpLogger.warn(`Potential DoS detected: ${highVolumeIPs} IPs with high connection rates`); - } - // Assess memory usage trends - if (memoryUsageMB.heapUsed > 500) { // Over 500MB heap used - SmtpLogger.warn(`High memory usage detected: ${memoryUsageMB.heapUsed}MB heap used`); - } - // Clean up expired IP rate limits and validate resource tracking - this.cleanupIpRateLimits(); - } - /** - * Clean up expired IP rate limits and perform additional resource monitoring - */ - cleanupIpRateLimits() { - const now = Date.now(); - const windowThreshold = now - this.options.connectionRateWindow; - let activeIps = 0; - let removedEntries = 0; - // Iterate through IP connections and manage entries - for (const [ip, data] of this.ipConnections.entries()) { - // If the last connection was before the window threshold + one extra window, remove the entry - if (data.lastConnection < windowThreshold - this.options.connectionRateWindow) { - // Remove stale entries to prevent memory growth - this.ipConnections.delete(ip); - removedEntries++; - } - // If last connection was before the window threshold, reset the count - else if (data.lastConnection < windowThreshold) { - if (data.count > 0) { - // Reset but keep the IP in the map with a zero count - this.ipConnections.set(ip, { - count: 0, - firstConnection: now, - lastConnection: now - }); - } - } - else { - // This IP is still active within the current window - activeIps++; - } - } - // Log cleanup activity if significant changes occurred - if (removedEntries > 0) { - SmtpLogger.debug(`IP rate limit cleanup: removed ${removedEntries} stale entries, ${this.ipConnections.size} remaining, ${activeIps} active in current window`); - } - // Check for memory leaks in connection tracking - if (this.activeConnections.size > 0 && this.connectionStats.activeConnections !== this.activeConnections.size) { - SmtpLogger.warn(`Connection tracking inconsistency detected: stats.active=${this.connectionStats.activeConnections}, actual=${this.activeConnections.size}`); - // Fix the inconsistency - this.connectionStats.activeConnections = this.activeConnections.size; - } - // Validate and clean leaked resources if needed - this.validateResourceTracking(); - } - /** - * Validate and repair resource tracking to prevent leaks - */ - validateResourceTracking() { - // Prepare a detailed report if inconsistencies are found - const inconsistenciesFound = []; - // 1. Check active connections count matches activeConnections set size - if (this.connectionStats.activeConnections !== this.activeConnections.size) { - inconsistenciesFound.push({ - issue: 'Active connection count mismatch', - stats: this.connectionStats.activeConnections, - actual: this.activeConnections.size, - action: 'Auto-corrected' - }); - this.connectionStats.activeConnections = this.activeConnections.size; - } - // 2. Check for destroyed sockets in active connections - let destroyedSocketsCount = 0; - const socketsToRemove = []; - for (const socket of this.activeConnections) { - if (socket.destroyed) { - destroyedSocketsCount++; - socketsToRemove.push(socket); - } - } - // Remove destroyed sockets from tracking - for (const socket of socketsToRemove) { - this.activeConnections.delete(socket); - // Also ensure all listeners are removed - try { - socket.removeAllListeners(); - } - catch { - // Ignore errors from removeAllListeners - } - } - if (destroyedSocketsCount > 0) { - inconsistenciesFound.push({ - issue: 'Destroyed sockets in active list', - count: destroyedSocketsCount, - action: 'Removed from tracking' - }); - // Update active connections count after cleanup - this.connectionStats.activeConnections = this.activeConnections.size; - } - // 3. Check for sessions without corresponding active connections - const sessionCount = this.smtpServer.getSessionManager().getSessionCount(); - if (sessionCount > this.activeConnections.size) { - inconsistenciesFound.push({ - issue: 'Orphaned sessions', - sessions: sessionCount, - connections: this.activeConnections.size, - action: 'Session cleanup recommended' - }); - } - // If any inconsistencies found, log a detailed report - if (inconsistenciesFound.length > 0) { - SmtpLogger.warn('Resource tracking inconsistencies detected and repaired', { inconsistencies: inconsistenciesFound }); - } - } - /** - * Handle a new connection with resource management - * @param socket - Client socket - */ - async handleNewConnection(socket) { - // Update connection stats - this.connectionStats.totalConnections++; - this.connectionStats.activeConnections = this.activeConnections.size + 1; - if (this.connectionStats.activeConnections > this.connectionStats.peakConnections) { - this.connectionStats.peakConnections = this.connectionStats.activeConnections; - } - // Get client IP - const remoteAddress = socket.remoteAddress || '0.0.0.0'; - // Use UnifiedRateLimiter for connection rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - // Check connection limit with UnifiedRateLimiter - const connectionResult = rateLimiter.recordConnection(remoteAddress); - if (!connectionResult.allowed) { - this.rejectConnection(socket, connectionResult.reason || 'Rate limit exceeded'); - this.connectionStats.rejectedConnections++; - return; - } - // Still track IP connections locally for cleanup purposes - this.trackIPConnection(remoteAddress); - // Check if maximum global connections reached - if (this.hasReachedMaxConnections()) { - this.rejectConnection(socket, 'Too many connections'); - this.connectionStats.rejectedConnections++; - return; - } - // Add socket to active connections - this.activeConnections.add(socket); - // Set up socket options - socket.setKeepAlive(true); - socket.setTimeout(this.options.socketTimeout); - // Explicitly set socket buffer sizes to prevent memory issues - socket.setNoDelay(true); // Disable Nagle's algorithm for better responsiveness - // Set limits on socket buffer size if supported by Node.js version - try { - // Here we set reasonable buffer limits to prevent memory exhaustion attacks - const highWaterMark = 64 * 1024; // 64 KB - // Note: Socket high water mark methods can't be set directly in newer Node.js versions - // These would need to be set during socket creation or with a different API - } - catch (error) { - // Ignore errors from older Node.js versions that don't support these methods - SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`); - } - // Set up event handlers - this.setupSocketEventHandlers(socket); - // Create a session for this connection - this.smtpServer.getSessionManager().createSession(socket, false); - // Log the new connection using adaptive logger - const socketDetails = getSocketDetails(socket); - adaptiveLogger.logConnection(socket, 'connect'); - // Update adaptive logger with current connection count - adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections); - // Send greeting - this.sendGreeting(socket); - } - /** - * Check if an IP has exceeded the rate limit - * @param ip - Client IP address - * @returns True if rate limited - */ - isIPRateLimited(ip) { - const now = Date.now(); - const ipData = this.ipConnections.get(ip); - if (!ipData) { - return false; // No previous connections - } - // Check if we're within the rate window - const isWithinWindow = now - ipData.firstConnection <= this.options.connectionRateWindow; - // If within window and count exceeds limit, rate limit is applied - if (isWithinWindow && ipData.count >= this.options.connectionRateLimit) { - SmtpLogger.warn(`Rate limit exceeded for IP ${ip}: ${ipData.count} connections in ${Math.round((now - ipData.firstConnection) / 1000)}s`); - return true; - } - return false; - } - /** - * Track a new connection from an IP - * @param ip - Client IP address - */ - trackIPConnection(ip) { - const now = Date.now(); - const ipData = this.ipConnections.get(ip); - if (!ipData) { - // First connection from this IP - this.ipConnections.set(ip, { - count: 1, - firstConnection: now, - lastConnection: now - }); - } - else { - // Check if we need to reset the window - if (now - ipData.lastConnection > this.options.connectionRateWindow) { - // Reset the window - this.ipConnections.set(ip, { - count: 1, - firstConnection: now, - lastConnection: now - }); - } - else { - // Increment within the current window - this.ipConnections.set(ip, { - count: ipData.count + 1, - firstConnection: ipData.firstConnection, - lastConnection: now - }); - } - } - } - /** - * Check if an IP has reached its connection limit - * @param ip - Client IP address - * @returns True if limit reached - */ - hasReachedIPConnectionLimit(ip) { - let ipConnectionCount = 0; - // Count active connections from this IP - for (const socket of this.activeConnections) { - if (socket.remoteAddress === ip) { - ipConnectionCount++; - } - } - return ipConnectionCount >= this.options.maxConnectionsPerIP; - } - /** - * Handle a new secure TLS connection with resource management - * @param socket - Client TLS socket - */ - async handleNewSecureConnection(socket) { - // Update connection stats - this.connectionStats.totalConnections++; - this.connectionStats.activeConnections = this.activeConnections.size + 1; - if (this.connectionStats.activeConnections > this.connectionStats.peakConnections) { - this.connectionStats.peakConnections = this.connectionStats.activeConnections; - } - // Get client IP - const remoteAddress = socket.remoteAddress || '0.0.0.0'; - // Use UnifiedRateLimiter for connection rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - // Check connection limit with UnifiedRateLimiter - const connectionResult = rateLimiter.recordConnection(remoteAddress); - if (!connectionResult.allowed) { - this.rejectConnection(socket, connectionResult.reason || 'Rate limit exceeded'); - this.connectionStats.rejectedConnections++; - return; - } - // Still track IP connections locally for cleanup purposes - this.trackIPConnection(remoteAddress); - // Check if maximum global connections reached - if (this.hasReachedMaxConnections()) { - this.rejectConnection(socket, 'Too many connections'); - this.connectionStats.rejectedConnections++; - return; - } - // Add socket to active connections - this.activeConnections.add(socket); - // Set up socket options - socket.setKeepAlive(true); - socket.setTimeout(this.options.socketTimeout); - // Explicitly set socket buffer sizes to prevent memory issues - socket.setNoDelay(true); // Disable Nagle's algorithm for better responsiveness - // Set limits on socket buffer size if supported by Node.js version - try { - // Here we set reasonable buffer limits to prevent memory exhaustion attacks - const highWaterMark = 64 * 1024; // 64 KB - // Note: Socket high water mark methods can't be set directly in newer Node.js versions - // These would need to be set during socket creation or with a different API - } - catch (error) { - // Ignore errors from older Node.js versions that don't support these methods - SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`); - } - // Set up event handlers - this.setupSocketEventHandlers(socket); - // Create a session for this connection - this.smtpServer.getSessionManager().createSession(socket, true); - // Log the new secure connection using adaptive logger - adaptiveLogger.logConnection(socket, 'connect'); - // Update adaptive logger with current connection count - adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections); - // Send greeting - this.sendGreeting(socket); - } - /** - * Set up event handlers for a socket with enhanced resource management - * @param socket - Client socket - */ - setupSocketEventHandlers(socket) { - // Store existing socket event handlers before adding new ones - const existingDataHandler = socket.listeners('data')[0]; - const existingCloseHandler = socket.listeners('close')[0]; - const existingErrorHandler = socket.listeners('error')[0]; - const existingTimeoutHandler = socket.listeners('timeout')[0]; - // Remove existing event handlers if they exist - if (existingDataHandler) - socket.removeListener('data', existingDataHandler); - if (existingCloseHandler) - socket.removeListener('close', existingCloseHandler); - if (existingErrorHandler) - socket.removeListener('error', existingErrorHandler); - if (existingTimeoutHandler) - socket.removeListener('timeout', existingTimeoutHandler); - // Data event - process incoming data from the client with resource limits - let buffer = ''; - let totalBytesReceived = 0; - socket.on('data', async (data) => { - try { - // Get current session and update activity timestamp - const session = this.smtpServer.getSessionManager().getSession(socket); - if (session) { - this.smtpServer.getSessionManager().updateSessionActivity(session); - } - // Check if we're in DATA receiving mode - handle differently - if (session && session.state === SmtpState.DATA_RECEIVING) { - // In DATA mode, pass raw chunks directly to command handler with special marker - // Don't line-buffer large email content - try { - const dataString = data.toString('utf8'); - // Use a special prefix to indicate this is raw data, not a command line - // CRITICAL FIX: Must await to prevent async pile-up - await this.smtpServer.getCommandHandler().processCommand(socket, `__RAW_DATA__${dataString}`); - return; - } - catch (dataError) { - SmtpLogger.error(`Data handler error during DATA mode: ${dataError instanceof Error ? dataError.message : String(dataError)}`); - socket.destroy(); - return; - } - } - // For command mode, continue with line-buffered processing - // Check buffer size limits to prevent memory attacks - totalBytesReceived += data.length; - if (buffer.length > this.options.bufferSizeLimit) { - // Buffer is too large, reject the connection - SmtpLogger.warn(`Buffer size limit exceeded: ${buffer.length} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too large, disconnecting`); - socket.destroy(); - return; - } - // Impose a total transfer limit to prevent DoS - if (totalBytesReceived > this.options.bufferSizeLimit * 2) { - SmtpLogger.warn(`Total transfer limit exceeded: ${totalBytesReceived} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Transfer limit exceeded, disconnecting`); - socket.destroy(); - return; - } - // Convert buffer to string safely with explicit encoding - const dataString = data.toString('utf8'); - // Buffer incoming data - buffer += dataString; - // Process complete lines - let lineEndPos; - while ((lineEndPos = buffer.indexOf(SMTP_DEFAULTS.CRLF)) !== -1) { - // Extract a complete line - const line = buffer.substring(0, lineEndPos); - buffer = buffer.substring(lineEndPos + 2); // +2 to skip CRLF - // Check line length to prevent extremely long lines - if (line.length > 4096) { // 4KB line limit is reasonable for SMTP - SmtpLogger.warn(`Line length limit exceeded: ${line.length} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Line too long, disconnecting`); - socket.destroy(); - return; - } - // Process non-empty lines - if (line.length > 0) { - try { - // CRITICAL FIX: Must await processCommand to prevent async pile-up - // This was causing the busy loop with high CPU usage when many empty lines were processed - await this.smtpServer.getCommandHandler().processCommand(socket, line); - } - catch (error) { - // Handle any errors in command processing - SmtpLogger.error(`Command handler error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error`); - // If there's a severe error, close the connection - if (error instanceof Error && - (error.message.includes('fatal') || error.message.includes('critical'))) { - socket.destroy(); - return; - } - } - } - } - // If buffer is getting too large without CRLF, it might be a DoS attempt - if (buffer.length > 10240) { // 10KB is a reasonable limit for a line without CRLF - SmtpLogger.warn(`Incomplete line too large: ${buffer.length} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Incomplete line too large, disconnecting`); - socket.destroy(); - } - } - catch (error) { - // Handle any unexpected errors during data processing - SmtpLogger.error(`Data handler error: ${error instanceof Error ? error.message : String(error)}`); - socket.destroy(); - } - }); - // Add drain event handler to manage flow control - socket.on('drain', () => { - // Socket buffer has been emptied, resume data flow if needed - if (socket.isPaused()) { - socket.resume(); - SmtpLogger.debug(`Resumed socket for ${socket.remoteAddress} after drain`); - } - }); - // Close event - clean up when connection is closed - socket.on('close', (hadError) => { - this.handleSocketClose(socket, hadError); - }); - // Error event - handle socket errors - socket.on('error', (err) => { - this.handleSocketError(socket, err); - }); - // Timeout event - handle socket timeouts - socket.on('timeout', () => { - this.handleSocketTimeout(socket); - }); - } - /** - * Get the current connection count - * @returns Number of active connections - */ - getConnectionCount() { - return this.activeConnections.size; - } - /** - * Check if the server has reached the maximum number of connections - * @returns True if max connections reached - */ - hasReachedMaxConnections() { - return this.activeConnections.size >= this.options.maxConnections; - } - /** - * Close all active connections - */ - closeAllConnections() { - const connectionCount = this.activeConnections.size; - if (connectionCount === 0) { - return; - } - SmtpLogger.info(`Closing all connections (count: ${connectionCount})`); - for (const socket of this.activeConnections) { - try { - // Send service closing notification - this.sendServiceClosing(socket); - // End the socket gracefully - socket.end(); - // Force destroy after a short delay if not already destroyed - const destroyTimer = setTimeout(() => { - if (!socket.destroyed) { - socket.destroy(); - } - this.cleanupTimers.delete(destroyTimer); - }, 100); - this.cleanupTimers.add(destroyTimer); - } - catch (error) { - SmtpLogger.error(`Error closing connection: ${error instanceof Error ? error.message : String(error)}`); - // Force destroy on error - try { - socket.destroy(); - } - catch (e) { - // Ignore destroy errors - } - } - } - // Clear active connections - this.activeConnections.clear(); - // Stop resource monitoring to prevent hanging timers - if (this.resourceCheckInterval) { - clearInterval(this.resourceCheckInterval); - this.resourceCheckInterval = null; - } - } - /** - * Handle socket close event - * @param socket - Client socket - * @param hadError - Whether the socket was closed due to error - */ - handleSocketClose(socket, hadError) { - try { - // Update connection statistics - this.connectionStats.closedConnections++; - this.connectionStats.activeConnections = this.activeConnections.size - 1; - // Get socket details for logging - const socketDetails = getSocketDetails(socket); - const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; - // Log with appropriate level based on whether there was an error - if (hadError) { - SmtpLogger.warn(`Socket closed with error: ${socketId}`); - } - else { - SmtpLogger.debug(`Socket closed normally: ${socketId}`); - } - // Get the session before removing it - const session = this.smtpServer.getSessionManager().getSession(socket); - // Remove from active connections - this.activeConnections.delete(socket); - // Remove from session manager - this.smtpServer.getSessionManager().removeSession(socket); - // Cancel any timeout ID stored in the session - if (session?.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - // Remove all event listeners to prevent memory leaks - socket.removeAllListeners(); - // Log connection close with session details if available - adaptiveLogger.logConnection(socket, 'close', session); - // Update adaptive logger with new connection count - adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections); - } - catch (error) { - // Handle any unexpected errors during cleanup - SmtpLogger.error(`Error in handleSocketClose: ${error instanceof Error ? error.message : String(error)}`); - // Ensure socket is removed from active connections even if an error occurs - this.activeConnections.delete(socket); - // Always try to remove all listeners even on error - try { - socket.removeAllListeners(); - } - catch { - // Ignore errors from removeAllListeners - } - } - } - /** - * Handle socket error event - * @param socket - Client socket - * @param error - Error object - */ - handleSocketError(socket, error) { - try { - // Update connection statistics - this.connectionStats.erroredConnections++; - // Get socket details for context - const socketDetails = getSocketDetails(socket); - const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; - // Get the session - const session = this.smtpServer.getSessionManager().getSession(socket); - // Detailed error logging with context information - SmtpLogger.error(`Socket error for ${socketId}: ${error.message}`, { - errorCode: error.code, - errorStack: error.stack, - sessionId: session?.id, - sessionState: session?.state, - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - // Log the error for connection tracking using adaptive logger - adaptiveLogger.logConnection(socket, 'error', session, error); - // Cancel any timeout ID stored in the session - if (session?.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - // Close the socket if not already closed - if (!socket.destroyed) { - socket.destroy(); - } - // Remove from active connections (cleanup after error) - this.activeConnections.delete(socket); - // Remove from session manager - this.smtpServer.getSessionManager().removeSession(socket); - } - catch (handlerError) { - // Meta-error handling (errors in the error handler) - SmtpLogger.error(`Error in handleSocketError: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`); - // Ensure socket is destroyed and removed from active connections - if (!socket.destroyed) { - socket.destroy(); - } - this.activeConnections.delete(socket); - } - } - /** - * Handle socket timeout event - * @param socket - Client socket - */ - handleSocketTimeout(socket) { - try { - // Update connection statistics - this.connectionStats.timedOutConnections++; - // Get socket details for context - const socketDetails = getSocketDetails(socket); - const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; - // Get the session - const session = this.smtpServer.getSessionManager().getSession(socket); - // Get timing information for better debugging - const now = Date.now(); - const idleTime = session?.lastActivity ? now - session.lastActivity : 'unknown'; - if (session) { - // Log the timeout with extended details - SmtpLogger.warn(`Socket timeout from ${session.remoteAddress}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - state: session.state, - timeout: this.options.socketTimeout, - idleTime: idleTime, - emailState: session.envelope?.mailFrom ? 'has-sender' : 'no-sender', - recipientCount: session.envelope?.rcptTo?.length || 0 - }); - // Cancel any timeout ID stored in the session - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - // Send timeout notification to client - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} Connection timeout - closing connection`); - } - else { - // Log timeout without session context - SmtpLogger.warn(`Socket timeout without session from ${socketId}`); - } - // Close the socket gracefully - try { - socket.end(); - // Set a forced close timeout in case socket.end() doesn't close the connection - const timeoutDestroyTimer = setTimeout(() => { - if (!socket.destroyed) { - SmtpLogger.warn(`Forcing destroy of timed out socket: ${socketId}`); - socket.destroy(); - } - this.cleanupTimers.delete(timeoutDestroyTimer); - }, 5000); // 5 second grace period for socket to end properly - this.cleanupTimers.add(timeoutDestroyTimer); - } - catch (error) { - SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`); - // Ensure socket is destroyed even if end() fails - if (!socket.destroyed) { - socket.destroy(); - } - } - // Clean up resources - this.activeConnections.delete(socket); - this.smtpServer.getSessionManager().removeSession(socket); - } - catch (handlerError) { - // Handle any unexpected errors during timeout handling - SmtpLogger.error(`Error in handleSocketTimeout: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`); - // Ensure socket is destroyed and removed from tracking - if (!socket.destroyed) { - socket.destroy(); - } - this.activeConnections.delete(socket); - } - } - /** - * Reject a connection - * @param socket - Client socket - * @param reason - Reason for rejection - */ - rejectConnection(socket, reason) { - // Log the rejection - const socketDetails = getSocketDetails(socket); - SmtpLogger.warn(`Connection rejected from ${socketDetails.remoteAddress}:${socketDetails.remotePort}: ${reason}`); - // Send rejection message - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} ${this.options.hostname} Service temporarily unavailable - ${reason}`); - // Close the socket - try { - socket.end(); - } - catch (error) { - SmtpLogger.error(`Error ending rejected socket: ${error instanceof Error ? error.message : String(error)}`); - } - } - /** - * Send greeting message - * @param socket - Client socket - */ - sendGreeting(socket) { - const greeting = `${SmtpResponseCode.SERVICE_READY} ${this.options.hostname} ESMTP service ready`; - this.sendResponse(socket, greeting); - } - /** - * Send service closing notification - * @param socket - Client socket - */ - sendServiceClosing(socket) { - const message = `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`; - this.sendResponse(socket, message); - } - /** - * Send response to client - * @param socket - Client socket - * @param response - Response to send - */ - sendResponse(socket, response) { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - adaptiveLogger.logResponse(response, socket); - } - catch (error) { - // Log error and destroy socket - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - socket.destroy(); - } - } - /** - * Handle a new connection (interface requirement) - */ - async handleConnection(socket, secure) { - if (secure) { - this.handleNewSecureConnection(socket); - } - else { - this.handleNewConnection(socket); - } - } - /** - * Check if accepting new connections (interface requirement) - */ - canAcceptConnection() { - return !this.hasReachedMaxConnections(); - } - /** - * Clean up resources - */ - destroy() { - // Clear resource monitoring interval - if (this.resourceCheckInterval) { - clearInterval(this.resourceCheckInterval); - this.resourceCheckInterval = null; - } - // Clear all cleanup timers - for (const timer of this.cleanupTimers) { - clearTimeout(timer); - } - this.cleanupTimers.clear(); - // Close all active connections - this.closeAllConnections(); - // Clear maps - this.activeConnections.clear(); - this.ipConnections.clear(); - // Reset connection stats - this.connectionStats = { - totalConnections: 0, - activeConnections: 0, - peakConnections: 0, - rejectedConnections: 0, - closedConnections: 0, - erroredConnections: 0, - timedOutConnections: 0 - }; - SmtpLogger.debug('ConnectionManager destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29ubmVjdGlvbi1tYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL2Nvbm5lY3Rpb24tbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBRS9DLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxhQUFhLEVBQUUsU0FBUyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDNUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUUvRTs7OztHQUlHO0FBQ0gsTUFBTSxPQUFPLGlCQUFpQjtJQUM1Qjs7T0FFRztJQUNLLFVBQVUsQ0FBYztJQUVoQzs7T0FFRztJQUNLLGlCQUFpQixHQUFvRCxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXZGOztPQUVHO0lBQ0ssZUFBZSxHQUFHO1FBQ3hCLGdCQUFnQixFQUFFLENBQUM7UUFDbkIsaUJBQWlCLEVBQUUsQ0FBQztRQUNwQixlQUFlLEVBQUUsQ0FBQztRQUNsQixtQkFBbUIsRUFBRSxDQUFDO1FBQ3RCLGlCQUFpQixFQUFFLENBQUM7UUFDcEIsa0JBQWtCLEVBQUUsQ0FBQztRQUNyQixtQkFBbUIsRUFBRSxDQUFDO0tBQ3ZCLENBQUM7SUFFRjs7T0FFRztJQUNLLGFBQWEsR0FJaEIsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUVmOztPQUVHO0lBQ0sscUJBQXFCLEdBQTBCLElBQUksQ0FBQztJQUU1RDs7T0FFRztJQUNLLGFBQWEsR0FBd0IsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUV2RDs7T0FFRztJQUNLLE9BQU8sQ0FTYjtJQUVGOzs7T0FHRztJ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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/constants.d.ts b/dist_ts/mail/delivery/smtpserver/constants.d.ts deleted file mode 100644 index 7afec92..0000000 --- a/dist_ts/mail/delivery/smtpserver/constants.d.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * SMTP Server Constants - * This file contains all constants and enums used by the SMTP server - */ -import { SmtpState } from '../interfaces.js'; -export { SmtpState }; -/** - * SMTP Response Codes - * Based on RFC 5321 and common SMTP practice - */ -export declare enum SmtpResponseCode { - SUCCESS = 250,// Requested mail action okay, completed - SYSTEM_STATUS = 211,// System status, or system help reply - HELP_MESSAGE = 214,// Help message - SERVICE_READY = 220,// Service ready - SERVICE_CLOSING = 221,// Service closing transmission channel - AUTHENTICATION_SUCCESSFUL = 235,// Authentication successful - OK = 250,// Requested mail action okay, completed - FORWARD = 251,// User not local; will forward to - CANNOT_VRFY = 252,// Cannot VRFY user, but will accept message and attempt delivery - MORE_INFO_NEEDED = 334,// Server challenge for authentication - START_MAIL_INPUT = 354,// Start mail input; end with . - SERVICE_NOT_AVAILABLE = 421,// Service not available, closing transmission channel - MAILBOX_TEMPORARILY_UNAVAILABLE = 450,// Requested mail action not taken: mailbox unavailable - LOCAL_ERROR = 451,// Requested action aborted: local error in processing - INSUFFICIENT_STORAGE = 452,// Requested action not taken: insufficient system storage - TLS_UNAVAILABLE_TEMP = 454,// TLS not available due to temporary reason - SYNTAX_ERROR = 500,// Syntax error, command unrecognized - SYNTAX_ERROR_PARAMETERS = 501,// Syntax error in parameters or arguments - COMMAND_NOT_IMPLEMENTED = 502,// Command not implemented - BAD_SEQUENCE = 503,// Bad sequence of commands - COMMAND_PARAMETER_NOT_IMPLEMENTED = 504,// Command parameter not implemented - AUTH_REQUIRED = 530,// Authentication required - AUTH_FAILED = 535,// Authentication credentials invalid - MAILBOX_UNAVAILABLE = 550,// Requested action not taken: mailbox unavailable - USER_NOT_LOCAL = 551,// User not local; please try - EXCEEDED_STORAGE = 552,// Requested mail action aborted: exceeded storage allocation - MAILBOX_NAME_INVALID = 553,// Requested action not taken: mailbox name not allowed - TRANSACTION_FAILED = 554,// Transaction failed - MAIL_RCPT_PARAMETERS_INVALID = 555 -} -/** - * SMTP Command Types - */ -export declare enum SmtpCommand { - HELO = "HELO", - EHLO = "EHLO", - MAIL_FROM = "MAIL", - RCPT_TO = "RCPT", - DATA = "DATA", - RSET = "RSET", - NOOP = "NOOP", - QUIT = "QUIT", - STARTTLS = "STARTTLS", - AUTH = "AUTH", - HELP = "HELP", - VRFY = "VRFY", - EXPN = "EXPN" -} -/** - * Security log event types - */ -export declare enum SecurityEventType { - CONNECTION = "connection", - AUTHENTICATION = "authentication", - COMMAND = "command", - DATA = "data", - IP_REPUTATION = "ip_reputation", - TLS_NEGOTIATION = "tls_negotiation", - DKIM = "dkim", - SPF = "spf", - DMARC = "dmarc", - EMAIL_VALIDATION = "email_validation", - SPAM = "spam", - ACCESS_CONTROL = "access_control" -} -/** - * Security log levels - */ -export declare enum SecurityLogLevel { - DEBUG = "debug", - INFO = "info", - WARN = "warn", - ERROR = "error" -} -/** - * SMTP Server Defaults - */ -export declare const SMTP_DEFAULTS: { - CONNECTION_TIMEOUT: number; - SOCKET_TIMEOUT: number; - DATA_TIMEOUT: number; - CLEANUP_INTERVAL: number; - MAX_CONNECTIONS: number; - MAX_RECIPIENTS: number; - MAX_MESSAGE_SIZE: number; - SMTP_PORT: number; - SUBMISSION_PORT: number; - SECURE_PORT: number; - HOSTNAME: string; - CRLF: string; -}; -/** - * SMTP Command Patterns - * Regular expressions for parsing SMTP commands - */ -export declare const SMTP_PATTERNS: { - EHLO: RegExp; - MAIL_FROM: RegExp; - RCPT_TO: RegExp; - PARAM: RegExp; - EMAIL: RegExp; - END_DATA: RegExp; -}; -/** - * SMTP Extension List - * These extensions are advertised in the EHLO response - */ -export declare const SMTP_EXTENSIONS: { - PIPELINING: string; - SIZE: string; - EIGHTBITMIME: string; - STARTTLS: string; - AUTH: string; - ENHANCEDSTATUSCODES: string; - HELP: string; - CHUNKING: string; - DSN: string; - formatExtension(name: string, parameter?: string | number): string; -}; diff --git a/dist_ts/mail/delivery/smtpserver/constants.js b/dist_ts/mail/delivery/smtpserver/constants.js deleted file mode 100644 index 1266db7..0000000 --- a/dist_ts/mail/delivery/smtpserver/constants.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * SMTP Server Constants - * This file contains all constants and enums used by the SMTP server - */ -import { SmtpState } from '../interfaces.js'; -// Re-export SmtpState enum from the main interfaces file -export { SmtpState }; -/** - * SMTP Response Codes - * Based on RFC 5321 and common SMTP practice - */ -export var SmtpResponseCode; -(function (SmtpResponseCode) { - // Success codes (2xx) - SmtpResponseCode[SmtpResponseCode["SUCCESS"] = 250] = "SUCCESS"; - SmtpResponseCode[SmtpResponseCode["SYSTEM_STATUS"] = 211] = "SYSTEM_STATUS"; - SmtpResponseCode[SmtpResponseCode["HELP_MESSAGE"] = 214] = "HELP_MESSAGE"; - SmtpResponseCode[SmtpResponseCode["SERVICE_READY"] = 220] = "SERVICE_READY"; - SmtpResponseCode[SmtpResponseCode["SERVICE_CLOSING"] = 221] = "SERVICE_CLOSING"; - SmtpResponseCode[SmtpResponseCode["AUTHENTICATION_SUCCESSFUL"] = 235] = "AUTHENTICATION_SUCCESSFUL"; - SmtpResponseCode[SmtpResponseCode["OK"] = 250] = "OK"; - SmtpResponseCode[SmtpResponseCode["FORWARD"] = 251] = "FORWARD"; - SmtpResponseCode[SmtpResponseCode["CANNOT_VRFY"] = 252] = "CANNOT_VRFY"; - // Intermediate codes (3xx) - SmtpResponseCode[SmtpResponseCode["MORE_INFO_NEEDED"] = 334] = "MORE_INFO_NEEDED"; - SmtpResponseCode[SmtpResponseCode["START_MAIL_INPUT"] = 354] = "START_MAIL_INPUT"; - // Temporary error codes (4xx) - SmtpResponseCode[SmtpResponseCode["SERVICE_NOT_AVAILABLE"] = 421] = "SERVICE_NOT_AVAILABLE"; - SmtpResponseCode[SmtpResponseCode["MAILBOX_TEMPORARILY_UNAVAILABLE"] = 450] = "MAILBOX_TEMPORARILY_UNAVAILABLE"; - SmtpResponseCode[SmtpResponseCode["LOCAL_ERROR"] = 451] = "LOCAL_ERROR"; - SmtpResponseCode[SmtpResponseCode["INSUFFICIENT_STORAGE"] = 452] = "INSUFFICIENT_STORAGE"; - SmtpResponseCode[SmtpResponseCode["TLS_UNAVAILABLE_TEMP"] = 454] = "TLS_UNAVAILABLE_TEMP"; - // Permanent error codes (5xx) - SmtpResponseCode[SmtpResponseCode["SYNTAX_ERROR"] = 500] = "SYNTAX_ERROR"; - SmtpResponseCode[SmtpResponseCode["SYNTAX_ERROR_PARAMETERS"] = 501] = "SYNTAX_ERROR_PARAMETERS"; - SmtpResponseCode[SmtpResponseCode["COMMAND_NOT_IMPLEMENTED"] = 502] = "COMMAND_NOT_IMPLEMENTED"; - SmtpResponseCode[SmtpResponseCode["BAD_SEQUENCE"] = 503] = "BAD_SEQUENCE"; - SmtpResponseCode[SmtpResponseCode["COMMAND_PARAMETER_NOT_IMPLEMENTED"] = 504] = "COMMAND_PARAMETER_NOT_IMPLEMENTED"; - SmtpResponseCode[SmtpResponseCode["AUTH_REQUIRED"] = 530] = "AUTH_REQUIRED"; - SmtpResponseCode[SmtpResponseCode["AUTH_FAILED"] = 535] = "AUTH_FAILED"; - SmtpResponseCode[SmtpResponseCode["MAILBOX_UNAVAILABLE"] = 550] = "MAILBOX_UNAVAILABLE"; - SmtpResponseCode[SmtpResponseCode["USER_NOT_LOCAL"] = 551] = "USER_NOT_LOCAL"; - SmtpResponseCode[SmtpResponseCode["EXCEEDED_STORAGE"] = 552] = "EXCEEDED_STORAGE"; - SmtpResponseCode[SmtpResponseCode["MAILBOX_NAME_INVALID"] = 553] = "MAILBOX_NAME_INVALID"; - SmtpResponseCode[SmtpResponseCode["TRANSACTION_FAILED"] = 554] = "TRANSACTION_FAILED"; - SmtpResponseCode[SmtpResponseCode["MAIL_RCPT_PARAMETERS_INVALID"] = 555] = "MAIL_RCPT_PARAMETERS_INVALID"; -})(SmtpResponseCode || (SmtpResponseCode = {})); -/** - * SMTP Command Types - */ -export var SmtpCommand; -(function (SmtpCommand) { - SmtpCommand["HELO"] = "HELO"; - SmtpCommand["EHLO"] = "EHLO"; - SmtpCommand["MAIL_FROM"] = "MAIL"; - SmtpCommand["RCPT_TO"] = "RCPT"; - SmtpCommand["DATA"] = "DATA"; - SmtpCommand["RSET"] = "RSET"; - SmtpCommand["NOOP"] = "NOOP"; - SmtpCommand["QUIT"] = "QUIT"; - SmtpCommand["STARTTLS"] = "STARTTLS"; - SmtpCommand["AUTH"] = "AUTH"; - SmtpCommand["HELP"] = "HELP"; - SmtpCommand["VRFY"] = "VRFY"; - SmtpCommand["EXPN"] = "EXPN"; -})(SmtpCommand || (SmtpCommand = {})); -/** - * Security log event types - */ -export var SecurityEventType; -(function (SecurityEventType) { - SecurityEventType["CONNECTION"] = "connection"; - SecurityEventType["AUTHENTICATION"] = "authentication"; - SecurityEventType["COMMAND"] = "command"; - SecurityEventType["DATA"] = "data"; - SecurityEventType["IP_REPUTATION"] = "ip_reputation"; - SecurityEventType["TLS_NEGOTIATION"] = "tls_negotiation"; - SecurityEventType["DKIM"] = "dkim"; - SecurityEventType["SPF"] = "spf"; - SecurityEventType["DMARC"] = "dmarc"; - SecurityEventType["EMAIL_VALIDATION"] = "email_validation"; - SecurityEventType["SPAM"] = "spam"; - SecurityEventType["ACCESS_CONTROL"] = "access_control"; -})(SecurityEventType || (SecurityEventType = {})); -/** - * Security log levels - */ -export var SecurityLogLevel; -(function (SecurityLogLevel) { - SecurityLogLevel["DEBUG"] = "debug"; - SecurityLogLevel["INFO"] = "info"; - SecurityLogLevel["WARN"] = "warn"; - SecurityLogLevel["ERROR"] = "error"; -})(SecurityLogLevel || (SecurityLogLevel = {})); -/** - * SMTP Server Defaults - */ -export const SMTP_DEFAULTS = { - // Default timeouts in milliseconds - CONNECTION_TIMEOUT: 30000, // 30 seconds - SOCKET_TIMEOUT: 300000, // 5 minutes - DATA_TIMEOUT: 60000, // 1 minute - CLEANUP_INTERVAL: 5000, // 5 seconds - // Default limits - MAX_CONNECTIONS: 100, - MAX_RECIPIENTS: 100, - MAX_MESSAGE_SIZE: 10485760, // 10MB - // Default ports - SMTP_PORT: 25, - SUBMISSION_PORT: 587, - SECURE_PORT: 465, - // Default hostname - HOSTNAME: 'mail.lossless.one', - // CRLF line ending required by SMTP protocol - CRLF: '\r\n', -}; -/** - * SMTP Command Patterns - * Regular expressions for parsing SMTP commands - */ -export const SMTP_PATTERNS = { - // Match EHLO/HELO command: "EHLO example.com" - // Made very permissive to handle various client implementations - EHLO: /^(?:EHLO|HELO)\s+(.+)$/i, - // Match MAIL FROM command: "MAIL FROM: [PARAM=VALUE]" - // Made more permissive with whitespace and parameter formats - MAIL_FROM: /^MAIL\s+FROM\s*:\s*<([^>]*)>((?:\s+[a-zA-Z0-9][a-zA-Z0-9\-]*(?:=[^\s]+)?)*)$/i, - // Match RCPT TO command: "RCPT TO: [PARAM=VALUE]" - // Made more permissive with whitespace and parameter formats - RCPT_TO: /^RCPT\s+TO\s*:\s*<([^>]*)>((?:\s+[a-zA-Z0-9][a-zA-Z0-9\-]*(?:=[^\s]+)?)*)$/i, - // Match parameter format: "PARAM=VALUE" - PARAM: /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g, - // Match email address format - basic validation - // This pattern rejects common invalid formats while being permissive for edge cases - // Checks: no spaces, has @, has domain with dot, no double dots, proper domain format - EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - // Match end of DATA marker: \r\n.\r\n or just .\r\n at the start of a line (to handle various client implementations) - END_DATA: /(\r\n\.\r\n$)|(\n\.\r\n$)|(\r\n\.\n$)|(\n\.\n$)|^\.(\r\n|\n)$/, -}; -/** - * SMTP Extension List - * These extensions are advertised in the EHLO response - */ -export const SMTP_EXTENSIONS = { - // Basic extensions (RFC 1869) - PIPELINING: 'PIPELINING', - SIZE: 'SIZE', - EIGHTBITMIME: '8BITMIME', - // Security extensions - STARTTLS: 'STARTTLS', - AUTH: 'AUTH', - // Additional extensions - ENHANCEDSTATUSCODES: 'ENHANCEDSTATUSCODES', - HELP: 'HELP', - CHUNKING: 'CHUNKING', - DSN: 'DSN', - // Format an extension with a parameter - formatExtension(name, parameter) { - return parameter !== undefined ? `${name} ${parameter}` : name; - } -}; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFN0MseURBQXlEO0FBQ3pELE9BQU8sRUFBRSxTQUFTLEVBQUUsQ0FBQztBQUVyQjs7O0dBR0c7QUFDSCxNQUFNLENBQU4sSUFBWSxnQkFxQ1g7QUFyQ0QsV0FBWSxnQkFBZ0I7SUFDMUIsc0JBQXNCO0lBQ3RCLCtEQUFhLENBQUE7SUFDYiwyRUFBbUIsQ0FBQTtJQUNuQix5RUFBa0IsQ0FBQTtJQUNsQiwyRUFBbUIsQ0FBQTtJQUNuQiwrRUFBcUIsQ0FBQTtJQUNyQixtR0FBK0IsQ0FBQTtJQUMvQixxREFBUSxDQUFBO0lBQ1IsK0RBQWEsQ0FBQTtJQUNiLHVFQUFpQixDQUFBO0lBRWpCLDJCQUEyQjtJQUMzQixpRkFBc0IsQ0FBQTtJQUN0QixpRkFBc0IsQ0FBQTtJQUV0Qiw4QkFBOEI7SUFDOUIsMkZBQTJCLENBQUE7SUFDM0IsK0dBQXFDLENBQUE7SUFDckMsdUVBQWlCLENBQUE7SUFDakIseUZBQTBCLENBQUE7SUFDMUIseUZBQTBCLENBQUE7SUFFMUIsOEJBQThCO0lBQzlCLHlFQUFrQixDQUFBO0lBQ2xCLCtGQUE2QixDQUFBO0lBQzdCLCtGQUE2QixDQUFBO0lBQzdCLHlFQUFrQixDQUFBO0lBQ2xCLG1IQUF1QyxDQUFBO0lBQ3ZDLDJFQUFtQixDQUFBO0lBQ25CLHVFQUFpQixDQUFBO0lBQ2pCLHVGQUF5QixDQUFBO0lBQ3pCLDZFQUFvQixDQUFBO0lBQ3BCLGlGQUFzQixDQUFBO0lBQ3RCLHlGQUEwQixDQUFBO0lBQzFCLHFGQUF3QixDQUFBO0lBQ3hCLHlHQUFrQyxDQUFBO0FBQ3BDLENBQUMsRUFyQ1csZ0JBQWdCLEtBQWhCLGdCQUFn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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/create-server.d.ts b/dist_ts/mail/delivery/smtpserver/create-server.d.ts deleted file mode 100644 index e3c1912..0000000 --- a/dist_ts/mail/delivery/smtpserver/create-server.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * SMTP Server Creation Factory - * Provides a simple way to create a complete SMTP server - */ -import { SmtpServer } from './smtp-server.js'; -import type { ISmtpServerOptions } from './interfaces.js'; -import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; -/** - * Create a complete SMTP server with all components - * @param emailServer - Email server reference - * @param options - SMTP server options - * @returns Configured SMTP server instance - */ -export declare function createSmtpServer(emailServer: UnifiedEmailServer, options: ISmtpServerOptions): SmtpServer; diff --git a/dist_ts/mail/delivery/smtpserver/create-server.js b/dist_ts/mail/delivery/smtpserver/create-server.js deleted file mode 100644 index bd29837..0000000 --- a/dist_ts/mail/delivery/smtpserver/create-server.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * SMTP Server Creation Factory - * Provides a simple way to create a complete SMTP server - */ -import { SmtpServer } from './smtp-server.js'; -import { SessionManager } from './session-manager.js'; -import { ConnectionManager } from './connection-manager.js'; -import { CommandHandler } from './command-handler.js'; -import { DataHandler } from './data-handler.js'; -import { TlsHandler } from './tls-handler.js'; -import { SecurityHandler } from './security-handler.js'; -import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; -/** - * Create a complete SMTP server with all components - * @param emailServer - Email server reference - * @param options - SMTP server options - * @returns Configured SMTP server instance - */ -export function createSmtpServer(emailServer, options) { - // First create the SMTP server instance - const smtpServer = new SmtpServer({ - emailServer, - options - }); - // Return the configured server - return smtpServer; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLXNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9jcmVhdGUtc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM5QyxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdEQsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDNUQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUNoRCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDOUMsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRXhELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLCtDQUErQyxDQUFDO0FBRW5GOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGdCQUFnQixDQUFDLFdBQStCLEVBQUUsT0FBMkI7SUFDM0Ysd0NBQXdDO0lBQ3hDLE1BQU0sVUFBVSxHQUFHLElBQUksVUFBVSxDQUFDO1FBQ2hDLFdBQVc7UUFDWCxPQUFPO0tBQ1IsQ0FBQyxDQUFDO0lBRUgsK0JBQStCO0lBQy9CLE9BQU8sVUFBVSxDQUFDO0FBQ3BCLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/data-handler.d.ts b/dist_ts/mail/delivery/smtpserver/data-handler.d.ts deleted file mode 100644 index 418f228..0000000 --- a/dist_ts/mail/delivery/smtpserver/data-handler.d.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * SMTP Data Handler - * Responsible for processing email data during and after DATA command - */ -import * as plugins from '../../../plugins.js'; -import type { ISmtpSession, ISmtpTransactionResult } from './interfaces.js'; -import type { IDataHandler, ISmtpServer } from './interfaces.js'; -import { Email } from '../../core/classes.email.js'; -/** - * Handles SMTP DATA command and email data processing - */ -export declare class DataHandler implements IDataHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer; - /** - * Creates a new data handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer); - /** - * Process incoming email data - * @param socket - Client socket - * @param data - Data chunk - * @returns Promise that resolves when the data is processed - */ - processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise; - /** - * Handle raw data chunks during DATA mode (optimized for large messages) - * @param socket - Client socket - * @param data - Raw data chunk - */ - handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise; - /** - * Process email data chunks efficiently for large messages - * @param chunks - Array of email data chunks - * @returns Processed email data string - */ - private processEmailDataStreaming; - /** - * Process a complete email - * @param rawData - Raw email data - * @param session - SMTP session - * @returns Promise that resolves with the Email object - */ - processEmail(rawData: string, session: ISmtpSession): Promise; - /** - * Parse email from raw data - * @param rawData - Raw email data - * @param session - SMTP session - * @returns Email object - */ - private parseEmailFromData; - /** - * Process a complete email (legacy method) - * @param session - SMTP session - * @returns Promise that resolves with the result of the transaction - */ - processEmailLegacy(session: ISmtpSession): Promise; - /** - * Save an email to disk - * @param session - SMTP session - */ - saveEmail(session: ISmtpSession): void; - /** - * Parse an email into an Email object - * @param session - SMTP session - * @returns Promise that resolves with the parsed Email object - */ - parseEmail(session: ISmtpSession): Promise; - /** - * Basic fallback method for parsing emails - * @param session - SMTP session - * @returns The parsed Email object - */ - private parseEmailBasic; - /** - * Handle multipart content parsing - * @param email - Email object to update - * @param bodyText - Body text to parse - * @param boundary - MIME boundary - */ - private handleMultipartContent; - /** - * Handle end of data marker received - * @param socket - Client socket - * @param session - SMTP session - */ - private handleEndOfData; - /** - * Reset session after email processing - * @param session - SMTP session - */ - private resetSession; - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response message - */ - private sendResponse; - /** - * Check if a socket error is potentially recoverable - * @param error - The error that occurred - * @returns Whether the error is potentially recoverable - */ - private isRecoverableSocketError; - /** - * Handle recoverable socket errors with retry logic - * @param socket - Client socket - * @param error - The error that occurred - * @param response - The response that failed to send - */ - private handleSocketError; - /** - * Handle email data (interface requirement) - */ - handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string, session: ISmtpSession): Promise; - /** - * Clean up resources - */ - destroy(): void; -} diff --git a/dist_ts/mail/delivery/smtpserver/data-handler.js b/dist_ts/mail/delivery/smtpserver/data-handler.js deleted file mode 100644 index 4bd17de..0000000 --- a/dist_ts/mail/delivery/smtpserver/data-handler.js +++ /dev/null @@ -1,1152 +0,0 @@ -/** - * SMTP Data Handler - * Responsible for processing email data during and after DATA command - */ -import * as plugins from '../../../plugins.js'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SmtpState } from './interfaces.js'; -import { SmtpResponseCode, SMTP_PATTERNS, SMTP_DEFAULTS } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { detectHeaderInjection } from './utils/validation.js'; -import { Email } from '../../core/classes.email.js'; -/** - * Handles SMTP DATA command and email data processing - */ -export class DataHandler { - /** - * Reference to the SMTP server instance - */ - smtpServer; - /** - * Creates a new data handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer) { - this.smtpServer = smtpServer; - } - /** - * Process incoming email data - * @param socket - Client socket - * @param data - Data chunk - * @returns Promise that resolves when the data is processed - */ - async processEmailData(socket, data) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Clear any existing timeout and set a new one - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - session.dataTimeoutId = setTimeout(() => { - if (session.state === SmtpState.DATA_RECEIVING) { - SmtpLogger.warn(`DATA timeout for session ${session.id}`, { sessionId: session.id }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`); - this.resetSession(session); - } - }, SMTP_DEFAULTS.DATA_TIMEOUT); - // Update activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - // Store data in chunks for better memory efficiency - if (!session.emailDataChunks) { - session.emailDataChunks = []; - session.emailDataSize = 0; // Track size incrementally - } - session.emailDataChunks.push(data); - session.emailDataSize = (session.emailDataSize || 0) + data.length; - // Check if we've reached the max size (using incremental tracking) - const options = this.smtpServer.getOptions(); - const maxSize = options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE; - if (session.emailDataSize > maxSize) { - SmtpLogger.warn(`Message size exceeds limit for session ${session.id}`, { - sessionId: session.id, - size: session.emailDataSize, - limit: maxSize - }); - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too big, size limit is ${maxSize} bytes`); - this.resetSession(session); - return; - } - // Check for end of data marker efficiently without combining all chunks - // Only check the current chunk and the last chunk for the marker - let hasEndMarker = false; - // Check if current chunk contains end marker - if (data === '.\r\n' || data === '.') { - hasEndMarker = true; - } - else { - // For efficiency with large messages, only check the last few chunks - // Get the last 2 chunks to check for split markers - const lastChunks = session.emailDataChunks.slice(-2).join(''); - hasEndMarker = lastChunks.endsWith('\r\n.\r\n') || - lastChunks.endsWith('\n.\r\n') || - lastChunks.endsWith('\r\n.\n') || - lastChunks.endsWith('\n.\n'); - } - if (hasEndMarker) { - SmtpLogger.debug(`End of data marker found for session ${session.id}`, { sessionId: session.id }); - // End of data marker found - await this.handleEndOfData(socket, session); - } - } - /** - * Handle raw data chunks during DATA mode (optimized for large messages) - * @param socket - Client socket - * @param data - Raw data chunk - */ - async handleDataReceived(socket, data) { - // Get the session - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - // Special handling for ERR-02 test: detect MAIL FROM command during DATA mode - // This needs to work for both raw data chunks and line-based data - const trimmedData = data.trim(); - const looksLikeCommand = /^[A-Z]{4,}( |:)/i.test(trimmedData); - if (looksLikeCommand && trimmedData.toUpperCase().startsWith('MAIL FROM')) { - // This is the command that ERR-02 test is expecting to fail with 503 - SmtpLogger.debug(`Received MAIL FROM command during DATA mode - responding with sequence error`); - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - // For all other data, process normally - return this.processEmailData(socket, data); - } - /** - * Process email data chunks efficiently for large messages - * @param chunks - Array of email data chunks - * @returns Processed email data string - */ - processEmailDataStreaming(chunks) { - // For very large messages, use a more memory-efficient approach - const CHUNK_SIZE = 50; // Process 50 chunks at a time - let result = ''; - // Process chunks in batches to reduce memory pressure - for (let batchStart = 0; batchStart < chunks.length; batchStart += CHUNK_SIZE) { - const batchEnd = Math.min(batchStart + CHUNK_SIZE, chunks.length); - const batchChunks = chunks.slice(batchStart, batchEnd); - // Join this batch - let batchData = batchChunks.join(''); - // Clear references to help GC - for (let i = 0; i < batchChunks.length; i++) { - batchChunks[i] = ''; - } - result += batchData; - batchData = ''; // Clear reference - // Force garbage collection hint (if available) - if (global.gc && batchStart % 200 === 0) { - global.gc(); - } - } - // Remove trailing end-of-data marker: various formats - result = result - .replace(/\r\n\.\r\n$/, '') - .replace(/\n\.\r\n$/, '') - .replace(/\r\n\.\n$/, '') - .replace(/\n\.\n$/, '') - .replace(/^\.$/, ''); // Handle ONLY a lone dot as the entire content (not trailing dots) - // Remove dot-stuffing (RFC 5321, section 4.5.2) - result = result.replace(/\r\n\.\./g, '\r\n.'); - return result; - } - /** - * Process a complete email - * @param rawData - Raw email data - * @param session - SMTP session - * @returns Promise that resolves with the Email object - */ - async processEmail(rawData, session) { - // Clean up the raw email data - let cleanedData = rawData; - // Remove trailing end-of-data marker: various formats - cleanedData = cleanedData - .replace(/\r\n\.\r\n$/, '') - .replace(/\n\.\r\n$/, '') - .replace(/\r\n\.\n$/, '') - .replace(/\n\.\n$/, '') - .replace(/^\.$/, ''); // Handle ONLY a lone dot as the entire content (not trailing dots) - // Remove dot-stuffing (RFC 5321, section 4.5.2) - cleanedData = cleanedData.replace(/\r\n\.\./g, '\r\n.'); - try { - // Parse email into Email object using cleaned data - const email = await this.parseEmailFromData(cleanedData, session); - // Return the parsed email - return email; - } - catch (error) { - SmtpLogger.error(`Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Create a minimal email object on error - const fallbackEmail = new Email({ - from: 'unknown@localhost', - to: 'unknown@localhost', - subject: 'Parse Error', - text: cleanedData - }); - return fallbackEmail; - } - } - /** - * Parse email from raw data - * @param rawData - Raw email data - * @param session - SMTP session - * @returns Email object - */ - async parseEmailFromData(rawData, session) { - // Parse the raw email data to extract headers and body - const lines = rawData.split('\r\n'); - let headerEnd = -1; - // Find where headers end - for (let i = 0; i < lines.length; i++) { - if (lines[i].trim() === '') { - headerEnd = i; - break; - } - } - // Extract headers - let subject = 'No Subject'; - const headers = {}; - if (headerEnd > -1) { - for (let i = 0; i < headerEnd; i++) { - const line = lines[i]; - const colonIndex = line.indexOf(':'); - if (colonIndex > 0) { - const headerName = line.substring(0, colonIndex).trim().toLowerCase(); - const headerValue = line.substring(colonIndex + 1).trim(); - if (headerName === 'subject') { - subject = headerValue; - } - else { - headers[headerName] = headerValue; - } - } - } - } - // Extract body - const body = headerEnd > -1 ? lines.slice(headerEnd + 1).join('\r\n') : rawData; - // Create email with session information - const email = new Email({ - from: session.mailFrom || 'unknown@localhost', - to: session.rcptTo || ['unknown@localhost'], - subject, - text: body, - headers - }); - return email; - } - /** - * Process a complete email (legacy method) - * @param session - SMTP session - * @returns Promise that resolves with the result of the transaction - */ - async processEmailLegacy(session) { - try { - // Use the email data from session - const email = await this.parseEmailFromData(session.emailData || '', session); - // Process the email based on the processing mode - const processingMode = session.processingMode || 'mta'; - let result = { - success: false, - error: 'Email processing failed' - }; - switch (processingMode) { - case 'mta': - // Process through the MTA system - try { - SmtpLogger.debug(`Processing email in MTA mode for session ${session.id}`, { - sessionId: session.id, - messageId: email.getMessageId() - }); - // Generate a message ID since queueEmail is not available - const options = this.smtpServer.getOptions(); - const hostname = options.hostname || SMTP_DEFAULTS.HOSTNAME; - const messageId = `${Date.now()}-${Math.floor(Math.random() * 1000000)}@${hostname}`; - // Process the email through the emailServer - try { - // Process the email via the UnifiedEmailServer - // Pass the email object, session data, and specify the mode (mta, forward, or process) - // This connects SMTP reception to the overall email system - const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session); - SmtpLogger.info(`Email processed through UnifiedEmailServer: ${email.getMessageId()}`, { - sessionId: session.id, - messageId: email.getMessageId(), - recipients: email.to.join(', '), - success: true - }); - result = { - success: true, - messageId, - email - }; - } - catch (emailError) { - SmtpLogger.error(`Failed to process email through UnifiedEmailServer: ${emailError instanceof Error ? emailError.message : String(emailError)}`, { - sessionId: session.id, - error: emailError instanceof Error ? emailError : new Error(String(emailError)), - messageId - }); - // Default to success for now to pass tests, but log the error - result = { - success: true, - messageId, - email - }; - } - } - catch (error) { - SmtpLogger.error(`Failed to queue email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - result = { - success: false, - error: `Failed to queue email: ${error instanceof Error ? error.message : String(error)}` - }; - } - break; - case 'forward': - // Forward email to another server - SmtpLogger.debug(`Processing email in FORWARD mode for session ${session.id}`, { - sessionId: session.id, - messageId: email.getMessageId() - }); - // Process the email via the UnifiedEmailServer in forward mode - try { - const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session); - SmtpLogger.info(`Email forwarded through UnifiedEmailServer: ${email.getMessageId()}`, { - sessionId: session.id, - messageId: email.getMessageId(), - recipients: email.to.join(', '), - success: true - }); - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } - catch (forwardError) { - SmtpLogger.error(`Failed to forward email: ${forwardError instanceof Error ? forwardError.message : String(forwardError)}`, { - sessionId: session.id, - error: forwardError instanceof Error ? forwardError : new Error(String(forwardError)), - messageId: email.getMessageId() - }); - // For testing, still return success - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } - break; - case 'process': - // Process the email immediately - SmtpLogger.debug(`Processing email in PROCESS mode for session ${session.id}`, { - sessionId: session.id, - messageId: email.getMessageId() - }); - // Process the email via the UnifiedEmailServer in process mode - try { - const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session); - SmtpLogger.info(`Email processed directly through UnifiedEmailServer: ${email.getMessageId()}`, { - sessionId: session.id, - messageId: email.getMessageId(), - recipients: email.to.join(', '), - success: true - }); - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } - catch (processError) { - SmtpLogger.error(`Failed to process email directly: ${processError instanceof Error ? processError.message : String(processError)}`, { - sessionId: session.id, - error: processError instanceof Error ? processError : new Error(String(processError)), - messageId: email.getMessageId() - }); - // For testing, still return success - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } - break; - default: - SmtpLogger.warn(`Unknown processing mode: ${processingMode}`, { sessionId: session.id }); - result = { - success: false, - error: `Unknown processing mode: ${processingMode}` - }; - } - return result; - } - catch (error) { - SmtpLogger.error(`Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - return { - success: false, - error: `Failed to parse email: ${error instanceof Error ? error.message : String(error)}` - }; - } - } - /** - * Save an email to disk - * @param session - SMTP session - */ - saveEmail(session) { - // Email saving to disk is currently disabled in the refactored architecture - // This functionality can be re-enabled by adding a tempDir option to ISmtpServerOptions - SmtpLogger.debug(`Email saving to disk is disabled`, { - sessionId: session.id - }); - } - /** - * Parse an email into an Email object - * @param session - SMTP session - * @returns Promise that resolves with the parsed Email object - */ - async parseEmail(session) { - try { - // Store raw data for testing and debugging - const rawData = session.emailData; - // Try to parse with mailparser for better MIME support - const parsed = await plugins.mailparser.simpleParser(rawData); - // Extract headers - const headers = {}; - // Add all headers from the parsed email - if (parsed.headers) { - // Convert headers to a standard object format - for (const [key, value] of parsed.headers.entries()) { - if (typeof value === 'string') { - headers[key.toLowerCase()] = value; - } - else if (Array.isArray(value)) { - headers[key.toLowerCase()] = value.join(', '); - } - } - } - // Get message ID or generate one - const messageId = parsed.messageId || - headers['message-id'] || - `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.smtpServer.getOptions().hostname}>`; - // Get From, To, and Subject from parsed email or envelope - const from = parsed.from?.value?.[0]?.address || - session.envelope.mailFrom.address; - // Handle multiple recipients appropriately - let to = []; - // Try to get recipients from parsed email - if (parsed.to) { - // Handle both array and single object cases - if (Array.isArray(parsed.to)) { - to = parsed.to.map(addr => typeof addr === 'object' && addr !== null && 'address' in addr ? String(addr.address) : ''); - } - else if (typeof parsed.to === 'object' && parsed.to !== null) { - // Handle object with value property (array or single address object) - if ('value' in parsed.to && Array.isArray(parsed.to.value)) { - to = parsed.to.value.map(addr => typeof addr === 'object' && addr !== null && 'address' in addr ? String(addr.address) : ''); - } - else if ('address' in parsed.to) { - to = [String(parsed.to.address)]; - } - } - // Filter out empty strings - to = to.filter(Boolean); - } - // If no recipients found, fall back to envelope - if (to.length === 0) { - to = session.envelope.rcptTo.map(r => r.address); - } - // Handle subject with special care for character encoding - const subject = parsed.subject || headers['subject'] || 'No Subject'; - SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject }); - // Create email object using the parsed content - const email = new Email({ - from: from, - to: to, - subject: subject, - text: parsed.text || '', - html: parsed.html || undefined, - // Include original envelope data as headers for accurate routing - headers: { - 'X-Original-Mail-From': session.envelope.mailFrom.address, - 'X-Original-Rcpt-To': session.envelope.rcptTo.map(r => r.address).join(', '), - 'Message-Id': messageId - } - }); - // Add attachments if any - if (parsed.attachments && parsed.attachments.length > 0) { - SmtpLogger.debug(`Found ${parsed.attachments.length} attachments in email`, { - sessionId: session.id, - attachmentCount: parsed.attachments.length - }); - for (const attachment of parsed.attachments) { - // Enhanced attachment logging for debugging - SmtpLogger.debug(`Processing attachment: ${attachment.filename}`, { - filename: attachment.filename, - contentType: attachment.contentType, - size: attachment.content?.length, - contentId: attachment.contentId || 'none', - contentDisposition: attachment.contentDisposition || 'none' - }); - // Ensure we have valid content - if (!attachment.content || !Buffer.isBuffer(attachment.content)) { - SmtpLogger.warn(`Attachment ${attachment.filename} has invalid content, skipping`); - continue; - } - // Fix up content type if missing but can be inferred from filename - let contentType = attachment.contentType || 'application/octet-stream'; - const filename = attachment.filename || 'attachment'; - if (!contentType || contentType === 'application/octet-stream') { - if (filename.endsWith('.pdf')) { - contentType = 'application/pdf'; - } - else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) { - contentType = 'image/jpeg'; - } - else if (filename.endsWith('.png')) { - contentType = 'image/png'; - } - else if (filename.endsWith('.gif')) { - contentType = 'image/gif'; - } - else if (filename.endsWith('.txt')) { - contentType = 'text/plain'; - } - } - email.attachments.push({ - filename: filename, - content: attachment.content, - contentType: contentType, - contentId: attachment.contentId - }); - SmtpLogger.debug(`Added attachment to email: ${filename}, type: ${contentType}, size: ${attachment.content.length} bytes`); - } - } - else { - SmtpLogger.debug(`No attachments found in email via parser`, { sessionId: session.id }); - // Additional check for attachments that might be missed by the parser - // Look for Content-Disposition headers in the raw data - const rawData = session.emailData; - const hasAttachmentDisposition = rawData.includes('Content-Disposition: attachment'); - if (hasAttachmentDisposition) { - SmtpLogger.debug(`Found potential attachments in raw data, will handle in multipart processing`, { - sessionId: session.id - }); - } - } - // Add received header - const timestamp = new Date().toUTCString(); - const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.smtpServer.getOptions().hostname} with ESMTP id ${session.id}; ${timestamp}`; - email.addHeader('Received', receivedHeader); - // Add all original headers - for (const [name, value] of Object.entries(headers)) { - if (!['from', 'to', 'subject', 'message-id'].includes(name)) { - email.addHeader(name, value); - } - } - // Store raw data for testing and debugging - email.rawData = rawData; - SmtpLogger.debug(`Email parsed successfully: ${messageId}`, { - sessionId: session.id, - messageId, - hasHtml: !!parsed.html, - attachmentCount: parsed.attachments?.length || 0 - }); - return email; - } - catch (error) { - // If parsing fails, fall back to basic parsing - SmtpLogger.warn(`Advanced email parsing failed, falling back to basic parsing: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - return this.parseEmailBasic(session); - } - } - /** - * Basic fallback method for parsing emails - * @param session - SMTP session - * @returns The parsed Email object - */ - parseEmailBasic(session) { - // Parse raw email text to extract headers - const rawData = session.emailData; - const headerEndIndex = rawData.indexOf('\r\n\r\n'); - if (headerEndIndex === -1) { - // No headers/body separation, create basic email - const email = new Email({ - from: session.envelope.mailFrom.address, - to: session.envelope.rcptTo.map(r => r.address), - subject: 'Received via SMTP', - text: rawData - }); - // Store raw data for testing - email.rawData = rawData; - return email; - } - // Extract headers and body - const headersText = rawData.substring(0, headerEndIndex); - const bodyText = rawData.substring(headerEndIndex + 4); // Skip the \r\n\r\n separator - // Parse headers with enhanced injection detection - const headers = {}; - const headerLines = headersText.split('\r\n'); - let currentHeader = ''; - const criticalHeaders = new Set(); // Track critical headers for duplication detection - for (const line of headerLines) { - // Check if this is a continuation of a previous header - if (line.startsWith(' ') || line.startsWith('\t')) { - if (currentHeader) { - headers[currentHeader] += ' ' + line.trim(); - } - continue; - } - // This is a new header - const separatorIndex = line.indexOf(':'); - if (separatorIndex !== -1) { - const name = line.substring(0, separatorIndex).trim().toLowerCase(); - const value = line.substring(separatorIndex + 1).trim(); - // Check for header injection attempts in header values - if (detectHeaderInjection(value, 'email-header')) { - SmtpLogger.warn('Header injection attempt detected in email header', { - headerName: name, - headerValue: value.substring(0, 100) + (value.length > 100 ? '...' : ''), - sessionId: session.id - }); - // Throw error to reject the email completely - throw new Error(`Header injection attempt detected in ${name} header`); - } - // Enhanced security: Check for duplicate critical headers (potential injection) - const criticalHeaderNames = ['from', 'to', 'subject', 'date', 'message-id']; - if (criticalHeaderNames.includes(name)) { - if (criticalHeaders.has(name)) { - SmtpLogger.warn('Duplicate critical header detected - potential header injection', { - headerName: name, - existingValue: headers[name]?.substring(0, 50) + '...', - newValue: value.substring(0, 50) + '...', - sessionId: session.id - }); - // Throw error for duplicate critical headers - throw new Error(`Duplicate ${name} header detected - potential header injection`); - } - criticalHeaders.add(name); - } - // Enhanced security: Check for envelope mismatch (spoofing attempt) - if (name === 'from' && session.envelope?.mailFrom?.address) { - const emailFromHeader = value.match(/<([^>]+)>/)?.[1] || value.trim(); - const envelopeFrom = session.envelope.mailFrom.address; - // Allow some flexibility but detect obvious spoofing attempts - if (emailFromHeader && envelopeFrom && - !emailFromHeader.toLowerCase().includes(envelopeFrom.toLowerCase()) && - !envelopeFrom.toLowerCase().includes(emailFromHeader.toLowerCase())) { - SmtpLogger.warn('Potential sender spoofing detected', { - envelopeFrom: envelopeFrom, - headerFrom: emailFromHeader, - sessionId: session.id - }); - // Note: This is logged but not blocked as legitimate use cases exist - } - } - // Special handling for MIME-encoded headers (especially Subject) - if (name === 'subject' && value.includes('=?')) { - try { - // Use plugins.mailparser to decode the MIME-encoded subject - // This is a simplified approach - in a real system, you'd use a full MIME decoder - // For now, just log it for debugging - SmtpLogger.debug(`Found encoded subject: ${value}`, { encodedSubject: value }); - } - catch (error) { - SmtpLogger.warn(`Failed to decode MIME-encoded subject: ${error instanceof Error ? error.message : String(error)}`); - } - } - headers[name] = value; - currentHeader = name; - } - } - // Look for multipart content - let isMultipart = false; - let boundary = ''; - let contentType = headers['content-type'] || ''; - // Check for multipart content - if (contentType.includes('multipart/')) { - isMultipart = true; - // Extract boundary - const boundaryMatch = contentType.match(/boundary="?([^";\r\n]+)"?/i); - if (boundaryMatch && boundaryMatch[1]) { - boundary = boundaryMatch[1]; - } - } - // Extract common headers - const subject = headers['subject'] || 'No Subject'; - const from = headers['from'] || session.envelope.mailFrom.address; - const to = headers['to'] || session.envelope.rcptTo.map(r => r.address).join(', '); - const messageId = headers['message-id'] || `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.smtpServer.getOptions().hostname}>`; - // Create email object - const email = new Email({ - from: from, - to: to.split(',').map(addr => addr.trim()), - subject: subject, - text: bodyText, - // Add original session envelope data for accurate routing as headers - headers: { - 'X-Original-Mail-From': session.envelope.mailFrom.address, - 'X-Original-Rcpt-To': session.envelope.rcptTo.map(r => r.address).join(', '), - 'Message-Id': messageId - } - }); - // Handle multipart content if needed - if (isMultipart && boundary) { - this.handleMultipartContent(email, bodyText, boundary); - } - // Add received header - const timestamp = new Date().toUTCString(); - const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.smtpServer.getOptions().hostname} with ESMTP id ${session.id}; ${timestamp}`; - email.addHeader('Received', receivedHeader); - // Add all original headers - for (const [name, value] of Object.entries(headers)) { - if (!['from', 'to', 'subject', 'message-id'].includes(name)) { - email.addHeader(name, value); - } - } - // Store raw data for testing - email.rawData = rawData; - return email; - } - /** - * Handle multipart content parsing - * @param email - Email object to update - * @param bodyText - Body text to parse - * @param boundary - MIME boundary - */ - handleMultipartContent(email, bodyText, boundary) { - // Split the body by boundary - const parts = bodyText.split(`--${boundary}`); - SmtpLogger.debug(`Handling multipart content with ${parts.length - 1} parts (boundary: ${boundary})`); - // Process each part - for (let i = 1; i < parts.length; i++) { - const part = parts[i]; - // Skip the end boundary marker - if (part.startsWith('--')) { - SmtpLogger.debug(`Found end boundary marker in part ${i}`); - continue; - } - // Find the headers and content - const partHeaderEndIndex = part.indexOf('\r\n\r\n'); - if (partHeaderEndIndex === -1) { - SmtpLogger.debug(`No header/body separator found in part ${i}`); - continue; - } - const partHeadersText = part.substring(0, partHeaderEndIndex); - const partContent = part.substring(partHeaderEndIndex + 4); - // Parse part headers - const partHeaders = {}; - const partHeaderLines = partHeadersText.split('\r\n'); - let currentHeader = ''; - for (const line of partHeaderLines) { - // Check if this is a continuation of a previous header - if (line.startsWith(' ') || line.startsWith('\t')) { - if (currentHeader) { - partHeaders[currentHeader] += ' ' + line.trim(); - } - continue; - } - // This is a new header - const separatorIndex = line.indexOf(':'); - if (separatorIndex !== -1) { - const name = line.substring(0, separatorIndex).trim().toLowerCase(); - const value = line.substring(separatorIndex + 1).trim(); - partHeaders[name] = value; - currentHeader = name; - } - } - // Get content type - const contentType = partHeaders['content-type'] || ''; - // Get encoding - const encoding = partHeaders['content-transfer-encoding'] || '7bit'; - // Get disposition - const disposition = partHeaders['content-disposition'] || ''; - // Log part information - SmtpLogger.debug(`Processing MIME part ${i}: type=${contentType}, encoding=${encoding}, disposition=${disposition}`); - // Handle text/plain parts - if (contentType.includes('text/plain')) { - try { - // Decode content based on encoding - let decodedContent = partContent; - if (encoding.toLowerCase() === 'base64') { - // Remove line breaks from base64 content before decoding - const cleanBase64 = partContent.replace(/[\r\n]/g, ''); - try { - decodedContent = Buffer.from(cleanBase64, 'base64').toString('utf8'); - } - catch (error) { - SmtpLogger.warn(`Failed to decode base64 text content: ${error instanceof Error ? error.message : String(error)}`); - } - } - else if (encoding.toLowerCase() === 'quoted-printable') { - try { - // Basic quoted-printable decoding - decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => { - return String.fromCharCode(parseInt(hex, 16)); - }); - } - catch (error) { - SmtpLogger.warn(`Failed to decode quoted-printable content: ${error instanceof Error ? error.message : String(error)}`); - } - } - email.text = decodedContent.trim(); - } - catch (error) { - SmtpLogger.warn(`Error processing text/plain part: ${error instanceof Error ? error.message : String(error)}`); - email.text = partContent.trim(); - } - } - // Handle text/html parts - if (contentType.includes('text/html')) { - try { - // Decode content based on encoding - let decodedContent = partContent; - if (encoding.toLowerCase() === 'base64') { - // Remove line breaks from base64 content before decoding - const cleanBase64 = partContent.replace(/[\r\n]/g, ''); - try { - decodedContent = Buffer.from(cleanBase64, 'base64').toString('utf8'); - } - catch (error) { - SmtpLogger.warn(`Failed to decode base64 HTML content: ${error instanceof Error ? error.message : String(error)}`); - } - } - else if (encoding.toLowerCase() === 'quoted-printable') { - try { - // Basic quoted-printable decoding - decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => { - return String.fromCharCode(parseInt(hex, 16)); - }); - } - catch (error) { - SmtpLogger.warn(`Failed to decode quoted-printable HTML content: ${error instanceof Error ? error.message : String(error)}`); - } - } - email.html = decodedContent.trim(); - } - catch (error) { - SmtpLogger.warn(`Error processing text/html part: ${error instanceof Error ? error.message : String(error)}`); - email.html = partContent.trim(); - } - } - // Handle attachments - detect attachments by content disposition or by content-type - const isAttachment = (disposition && disposition.toLowerCase().includes('attachment')) || - (!contentType.includes('text/plain') && !contentType.includes('text/html')); - if (isAttachment) { - try { - // Extract filename from Content-Disposition or generate one based on content type - let filename = 'attachment'; - if (disposition) { - const filenameMatch = disposition.match(/filename="?([^";\r\n]+)"?/i); - if (filenameMatch && filenameMatch[1]) { - filename = filenameMatch[1].trim(); - } - } - else if (contentType) { - // If no filename but we have content type, generate a name with appropriate extension - const mainType = contentType.split(';')[0].trim().toLowerCase(); - if (mainType === 'application/pdf') { - filename = `attachment_${Date.now()}.pdf`; - } - else if (mainType === 'image/jpeg' || mainType === 'image/jpg') { - filename = `image_${Date.now()}.jpg`; - } - else if (mainType === 'image/png') { - filename = `image_${Date.now()}.png`; - } - else if (mainType === 'image/gif') { - filename = `image_${Date.now()}.gif`; - } - else { - filename = `attachment_${Date.now()}.bin`; - } - } - // Decode content based on encoding - let content; - if (encoding.toLowerCase() === 'base64') { - try { - // Remove line breaks from base64 content before decoding - const cleanBase64 = partContent.replace(/[\r\n]/g, ''); - content = Buffer.from(cleanBase64, 'base64'); - SmtpLogger.debug(`Successfully decoded base64 attachment: ${filename}, size: ${content.length} bytes`); - } - catch (error) { - SmtpLogger.warn(`Failed to decode base64 attachment: ${error instanceof Error ? error.message : String(error)}`); - content = Buffer.from(partContent); - } - } - else if (encoding.toLowerCase() === 'quoted-printable') { - try { - // Basic quoted-printable decoding - const decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => { - return String.fromCharCode(parseInt(hex, 16)); - }); - content = Buffer.from(decodedContent); - } - catch (error) { - SmtpLogger.warn(`Failed to decode quoted-printable attachment: ${error instanceof Error ? error.message : String(error)}`); - content = Buffer.from(partContent); - } - } - else { - // Default for 7bit, 8bit, or binary encoding - no decoding needed - content = Buffer.from(partContent); - } - // Determine content type - use the one from headers or infer from filename - let finalContentType = contentType; - if (!finalContentType || finalContentType === 'application/octet-stream') { - if (filename.endsWith('.pdf')) { - finalContentType = 'application/pdf'; - } - else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) { - finalContentType = 'image/jpeg'; - } - else if (filename.endsWith('.png')) { - finalContentType = 'image/png'; - } - else if (filename.endsWith('.gif')) { - finalContentType = 'image/gif'; - } - else if (filename.endsWith('.txt')) { - finalContentType = 'text/plain'; - } - else if (filename.endsWith('.html')) { - finalContentType = 'text/html'; - } - } - // Add attachment to email - email.attachments.push({ - filename, - content, - contentType: finalContentType || 'application/octet-stream' - }); - SmtpLogger.debug(`Added attachment: ${filename}, type: ${finalContentType}, size: ${content.length} bytes`); - } - catch (error) { - SmtpLogger.error(`Failed to process attachment: ${error instanceof Error ? error.message : String(error)}`); - } - } - // Check for nested multipart content - if (contentType.includes('multipart/')) { - try { - // Extract boundary - const nestedBoundaryMatch = contentType.match(/boundary="?([^";\r\n]+)"?/i); - if (nestedBoundaryMatch && nestedBoundaryMatch[1]) { - const nestedBoundary = nestedBoundaryMatch[1].trim(); - SmtpLogger.debug(`Found nested multipart content with boundary: ${nestedBoundary}`); - // Process nested multipart - this.handleMultipartContent(email, partContent, nestedBoundary); - } - } - catch (error) { - SmtpLogger.warn(`Error processing nested multipart content: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - } - /** - * Handle end of data marker received - * @param socket - Client socket - * @param session - SMTP session - */ - async handleEndOfData(socket, session) { - // Clear the data timeout - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - try { - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.FINISHED); - // Optionally save email to disk - this.saveEmail(session); - // Process the email using legacy method - const result = await this.processEmailLegacy(session); - if (result.success) { - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK message queued as ${result.messageId}`); - } - else { - // Send error response - this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Failed to process email: ${result.error}`); - } - // Reset session for new transaction - this.resetSession(session); - } - catch (error) { - SmtpLogger.error(`Error processing email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email: ${error instanceof Error ? error.message : String(error)}`); - this.resetSession(session); - } - } - /** - * Reset session after email processing - * @param session - SMTP session - */ - resetSession(session) { - // Clear any data timeout - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - // Reset data fields but keep authentication state - session.mailFrom = ''; - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - session.emailDataSize = 0; - session.envelope = { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }; - // Reset state to after EHLO - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO); - } - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response message - */ - sendResponse(socket, response) { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - SmtpLogger.logResponse(response, socket); - } - catch (error) { - // Attempt to recover from specific transient errors - if (this.isRecoverableSocketError(error)) { - this.handleSocketError(socket, error, response); - } - else { - // Log error for non-recoverable errors - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - } - } - } - /** - * Check if a socket error is potentially recoverable - * @param error - The error that occurred - * @returns Whether the error is potentially recoverable - */ - isRecoverableSocketError(error) { - const recoverableErrorCodes = [ - 'EPIPE', // Broken pipe - 'ECONNRESET', // Connection reset by peer - 'ETIMEDOUT', // Connection timed out - 'ECONNABORTED' // Connection aborted - ]; - return (error instanceof Error && - 'code' in error && - typeof error.code === 'string' && - recoverableErrorCodes.includes(error.code)); - } - /** - * Handle recoverable socket errors with retry logic - * @param socket - Client socket - * @param error - The error that occurred - * @param response - The response that failed to send - */ - handleSocketError(socket, error, response) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - SmtpLogger.error(`Session not found when handling socket error`); - if (!socket.destroyed) { - socket.destroy(); - } - return; - } - // Get error details for logging - const errorMessage = error instanceof Error ? error.message : String(error); - const errorCode = error instanceof Error && 'code' in error ? error.code : 'UNKNOWN'; - SmtpLogger.warn(`Recoverable socket error during data handling (${errorCode}): ${errorMessage}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Check if socket is already destroyed - if (socket.destroyed) { - SmtpLogger.info(`Socket already destroyed, cannot retry data operation`); - return; - } - // Check if socket is writeable - if (!socket.writable) { - SmtpLogger.info(`Socket no longer writable, aborting data recovery attempt`); - if (!socket.destroyed) { - socket.destroy(); - } - return; - } - // Attempt to retry the write operation after a short delay - setTimeout(() => { - try { - if (!socket.destroyed && socket.writable) { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - SmtpLogger.info(`Successfully retried data send operation after error`); - } - else { - SmtpLogger.warn(`Socket no longer available for data retry`); - if (!socket.destroyed) { - socket.destroy(); - } - } - } - catch (retryError) { - SmtpLogger.error(`Data retry attempt failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`); - if (!socket.destroyed) { - socket.destroy(); - } - } - }, 100); // Short delay before retry - } - /** - * Handle email data (interface requirement) - */ - async handleData(socket, data, session) { - // Delegate to existing method - await this.handleDataReceived(socket, data); - } - /** - * Clean up resources - */ - destroy() { - // DataHandler doesn't have timers or event listeners to clean up - SmtpLogger.debug('DataHandler destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0YS1oYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL2RhdGEtaGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sS0FBSyxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBQ3pCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUc1QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsYUFBYSxFQUFFLGFBQWEsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2hGLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUM5RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFFcEQ7O0dBRUc7QUFDSCxNQUFNLE9BQU8sV0FBVztJQUN0Qjs7T0FFRztJQUNLLFVBQVUsQ0FBYztJQUVoQzs7O09BR0c7SUFDSCxZQUFZLFVBQXVCO1FBQ2pDLElBQUksQ0FBQyxVQUFVLEdBQUcsVUFBVSxDQUFDO0lBQy9CLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNJLEtBQUssQ0FBQyxnQkFBZ0IsQ0FBQyxNQUFrRCxFQUFFLElBQVk7UUFDNUYsa0NBQWtDO1FBQ2xDLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsaUJBQWlCLEVBQUUsQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDdkUsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ2IsSUFBSSxDQUFDLFlBQVksQ0FBQyxNQUFNLEVBQUUsR0FBRyxnQkFBZ0IsQ0FBQyxXQUFXLDRDQUE0QyxDQUFDLENBQUM7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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/index.d.ts b/dist_ts/mail/delivery/smtpserver/index.d.ts deleted file mode 100644 index 9030c5b..0000000 --- a/dist_ts/mail/delivery/smtpserver/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * SMTP Server Module Exports - * This file exports all components of the refactored SMTP server - */ -export * from './interfaces.js'; -export { SmtpServer } from './smtp-server.js'; -export { SessionManager } from './session-manager.js'; -export { ConnectionManager } from './connection-manager.js'; -export { CommandHandler } from './command-handler.js'; -export { DataHandler } from './data-handler.js'; -export { TlsHandler } from './tls-handler.js'; -export { SecurityHandler } from './security-handler.js'; -export * from './constants.js'; -export { SmtpLogger } from './utils/logging.js'; -export * from './utils/validation.js'; -export * from './utils/helpers.js'; -export * from './certificate-utils.js'; -export * from './secure-server.js'; -export * from './starttls-handler.js'; -export { createSmtpServer } from './create-server.js'; diff --git a/dist_ts/mail/delivery/smtpserver/index.js b/dist_ts/mail/delivery/smtpserver/index.js deleted file mode 100644 index ee544bd..0000000 --- a/dist_ts/mail/delivery/smtpserver/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * SMTP Server Module Exports - * This file exports all components of the refactored SMTP server - */ -// Export interfaces -export * from './interfaces.js'; -// Export server classes -export { SmtpServer } from './smtp-server.js'; -export { SessionManager } from './session-manager.js'; -export { ConnectionManager } from './connection-manager.js'; -export { CommandHandler } from './command-handler.js'; -export { DataHandler } from './data-handler.js'; -export { TlsHandler } from './tls-handler.js'; -export { SecurityHandler } from './security-handler.js'; -// Export constants -export * from './constants.js'; -// Export utilities -export { SmtpLogger } from './utils/logging.js'; -export * from './utils/validation.js'; -export * from './utils/helpers.js'; -// Export TLS and certificate utilities -export * from './certificate-utils.js'; -export * from './secure-server.js'; -export * from './starttls-handler.js'; -// Factory function to create a complete SMTP server with default components -export { createSmtpServer } from './create-server.js'; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBzZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsb0JBQW9CO0FBQ3BCLGNBQWMsaUJBQWlCLENBQUM7QUFFaEMsd0JBQXdCO0FBQ3hCLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM5QyxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdEQsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDNUQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUNoRCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDOUMsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRXhELG1CQUFtQjtBQUNuQixjQUFjLGdCQUFnQixDQUFDO0FBRS9CLG1CQUFtQjtBQUNuQixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDaEQsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLG9CQUFvQixDQUFDO0FBRW5DLHVDQUF1QztBQUN2QyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyx1QkFBdUIsQ0FBQztBQUV0Qyw0RUFBNEU7QUFDNUUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0JBQW9CLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/interfaces.d.ts b/dist_ts/mail/delivery/smtpserver/interfaces.d.ts deleted file mode 100644 index d451e41..0000000 --- a/dist_ts/mail/delivery/smtpserver/interfaces.d.ts +++ /dev/null @@ -1,530 +0,0 @@ -/** - * SMTP Server Interfaces - * Defines all the interfaces used by the SMTP server implementation - */ -import * as plugins from '../../../plugins.js'; -import type { Email } from '../../core/classes.email.js'; -import type { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; -import { SmtpState } from '../interfaces.js'; -import { SmtpCommand } from './constants.js'; -export { SmtpState, SmtpCommand }; -export type { IEnvelopeRecipient } from '../interfaces.js'; -/** - * Interface for components that need cleanup - */ -export interface IDestroyable { - /** - * Clean up all resources (timers, listeners, etc) - */ - destroy(): void | Promise; -} -/** - * SMTP authentication credentials - */ -export interface ISmtpAuth { - /** - * Username for authentication - */ - username: string; - /** - * Password for authentication - */ - password: string; -} -/** - * SMTP envelope (sender and recipients) - */ -export interface ISmtpEnvelope { - /** - * Mail from address - */ - mailFrom: { - address: string; - args?: Record; - }; - /** - * Recipients list - */ - rcptTo: Array<{ - address: string; - args?: Record; - }>; -} -/** - * SMTP session representing a client connection - */ -export interface ISmtpSession { - /** - * Unique session identifier - */ - id: string; - /** - * Current state of the SMTP session - */ - state: SmtpState; - /** - * Client's hostname from EHLO/HELO - */ - clientHostname: string | null; - /** - * Whether TLS is active for this session - */ - secure: boolean; - /** - * Authentication status - */ - authenticated: boolean; - /** - * Authentication username if authenticated - */ - username?: string; - /** - * Transaction envelope - */ - envelope: ISmtpEnvelope; - /** - * When the session was created - */ - createdAt: Date; - /** - * Last activity timestamp - */ - lastActivity: number; - /** - * Client's IP address - */ - remoteAddress: string; - /** - * Client's port - */ - remotePort: number; - /** - * Additional session data - */ - data?: Record; - /** - * Message size if SIZE extension is used - */ - messageSize?: number; - /** - * Server capabilities advertised to client - */ - capabilities?: string[]; - /** - * Buffer for incomplete data - */ - dataBuffer?: string; - /** - * Flag to track if we're currently receiving DATA - */ - receivingData?: boolean; - /** - * The raw email data being received - */ - rawData?: string; - /** - * Greeting sent to client - */ - greeting?: string; - /** - * Whether EHLO has been sent - */ - ehloSent?: boolean; - /** - * Whether HELO has been sent - */ - heloSent?: boolean; - /** - * TLS options for this session - */ - tlsOptions?: any; - /** - * Whether TLS is being used - */ - useTLS?: boolean; - /** - * Mail from address for this transaction - */ - mailFrom?: string; - /** - * Recipients for this transaction - */ - rcptTo?: string[]; - /** - * Email data being received - */ - emailData?: string; - /** - * Chunks of email data - */ - emailDataChunks?: string[]; - /** - * Timeout ID for data reception - */ - dataTimeoutId?: NodeJS.Timeout; - /** - * Whether connection has ended - */ - connectionEnded?: boolean; - /** - * Size of email data being received - */ - emailDataSize?: number; - /** - * Processing mode for this session - */ - processingMode?: string; -} -/** - * Session manager interface - */ -export interface ISessionManager extends IDestroyable { - /** - * Create a new session for a socket - */ - createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure?: boolean): ISmtpSession; - /** - * Get session by socket - */ - getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined; - /** - * Update session state - */ - updateSessionState(session: ISmtpSession, newState: SmtpState): void; - /** - * Remove a session - */ - removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Clear all sessions - */ - clearAllSessions(): void; - /** - * Get all active sessions - */ - getAllSessions(): ISmtpSession[]; - /** - * Get session count - */ - getSessionCount(): number; - /** - * Update last activity for a session - */ - updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Check for timed out sessions - */ - checkTimeouts(timeoutMs: number): ISmtpSession[]; - /** - * Update session activity timestamp - */ - updateSessionActivity(session: ISmtpSession): void; - /** - * Replace socket in session (for TLS upgrade) - */ - replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean; -} -/** - * Connection manager interface - */ -export interface IConnectionManager extends IDestroyable { - /** - * Handle a new connection - */ - handleConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): Promise; - /** - * Close all active connections - */ - closeAllConnections(): void; - /** - * Get active connection count - */ - getConnectionCount(): number; - /** - * Check if accepting new connections - */ - canAcceptConnection(): boolean; - /** - * Handle new connection (legacy method name) - */ - handleNewConnection(socket: plugins.net.Socket): Promise; - /** - * Handle new secure connection (legacy method name) - */ - handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise; - /** - * Setup socket event handlers - */ - setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; -} -/** - * Command handler interface - */ -export interface ICommandHandler extends IDestroyable { - /** - * Handle an SMTP command - */ - handleCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, command: SmtpCommand, args: string, session: ISmtpSession): Promise; - /** - * Get supported commands for current session state - */ - getSupportedCommands(session: ISmtpSession): SmtpCommand[]; - /** - * Process command (legacy method name) - */ - processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, command: string): Promise; -} -/** - * Data handler interface - */ -export interface IDataHandler extends IDestroyable { - /** - * Handle email data - */ - handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string, session: ISmtpSession): Promise; - /** - * Process a complete email - */ - processEmail(rawData: string, session: ISmtpSession): Promise; - /** - * Handle data received (legacy method name) - */ - handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise; - /** - * Process email data (legacy method name) - */ - processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise; -} -/** - * TLS handler interface - */ -export interface ITlsHandler extends IDestroyable { - /** - * Handle STARTTLS command - */ - handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise; - /** - * Check if TLS is available - */ - isTlsAvailable(): boolean; - /** - * Get TLS options - */ - getTlsOptions(): plugins.tls.TlsOptions; - /** - * Check if TLS is enabled - */ - isTlsEnabled(): boolean; -} -/** - * Security handler interface - */ -export interface ISecurityHandler extends IDestroyable { - /** - * Check IP reputation - */ - checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise; - /** - * Validate email address - */ - isValidEmail(email: string): boolean; - /** - * Authenticate user - */ - authenticate(auth: ISmtpAuth): Promise; -} -/** - * SMTP server options - */ -export interface ISmtpServerOptions { - /** - * Port to listen on - */ - port: number; - /** - * Hostname of the server - */ - hostname: string; - /** - * Host to bind to (optional, defaults to 0.0.0.0) - */ - host?: string; - /** - * Secure port for TLS connections - */ - securePort?: number; - /** - * TLS/SSL private key (PEM format) - */ - key?: string; - /** - * TLS/SSL certificate (PEM format) - */ - cert?: string; - /** - * CA certificates for TLS (PEM format) - */ - ca?: string; - /** - * Maximum size of messages in bytes - */ - maxSize?: number; - /** - * Maximum number of concurrent connections - */ - maxConnections?: number; - /** - * Authentication options - */ - auth?: { - /** - * Whether authentication is required - */ - required: boolean; - /** - * Allowed authentication methods - */ - methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; - }; - /** - * Socket timeout in milliseconds (default: 5 minutes / 300000ms) - */ - socketTimeout?: number; - /** - * Initial connection timeout in milliseconds (default: 30 seconds / 30000ms) - */ - connectionTimeout?: number; - /** - * Interval for checking idle sessions in milliseconds (default: 5 seconds / 5000ms) - * For testing, can be set lower (e.g. 1000ms) to detect timeouts more quickly - */ - cleanupInterval?: number; - /** - * Maximum number of recipients allowed per message (default: 100) - */ - maxRecipients?: number; - /** - * Maximum message size in bytes (default: 10MB / 10485760 bytes) - * This is advertised in the EHLO SIZE extension - */ - size?: number; - /** - * Timeout for the DATA command in milliseconds (default: 60000ms / 1 minute) - * This controls how long to wait for the complete email data - */ - dataTimeout?: number; -} -/** - * Result of SMTP transaction - */ -export interface ISmtpTransactionResult { - /** - * Whether the transaction was successful - */ - success: boolean; - /** - * Error message if failed - */ - error?: string; - /** - * Message ID if successful - */ - messageId?: string; - /** - * Resulting email if successful - */ - email?: Email; -} -/** - * Interface for SMTP session events - * These events are emitted by the session manager - */ -export interface ISessionEvents { - created: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void; - stateChanged: (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void; - timeout: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void; - completed: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void; - error: (session: ISmtpSession, error: Error) => void; -} -/** - * SMTP Server interface - */ -export interface ISmtpServer extends IDestroyable { - /** - * Start the SMTP server - */ - listen(): Promise; - /** - * Stop the SMTP server - */ - close(): Promise; - /** - * Get the session manager - */ - getSessionManager(): ISessionManager; - /** - * Get the connection manager - */ - getConnectionManager(): IConnectionManager; - /** - * Get the command handler - */ - getCommandHandler(): ICommandHandler; - /** - * Get the data handler - */ - getDataHandler(): IDataHandler; - /** - * Get the TLS handler - */ - getTlsHandler(): ITlsHandler; - /** - * Get the security handler - */ - getSecurityHandler(): ISecurityHandler; - /** - * Get the server options - */ - getOptions(): ISmtpServerOptions; - /** - * Get the email server reference - */ - getEmailServer(): UnifiedEmailServer; -} -/** - * Configuration for creating SMTP server - */ -export interface ISmtpServerConfig { - /** - * Email server instance - */ - emailServer: UnifiedEmailServer; - /** - * Server options - */ - options: ISmtpServerOptions; - /** - * Optional custom session manager - */ - sessionManager?: ISessionManager; - /** - * Optional custom connection manager - */ - connectionManager?: IConnectionManager; - /** - * Optional custom command handler - */ - commandHandler?: ICommandHandler; - /** - * Optional custom data handler - */ - dataHandler?: IDataHandler; - /** - * Optional custom TLS handler - */ - tlsHandler?: ITlsHandler; - /** - * Optional custom security handler - */ - securityHandler?: ISecurityHandler; -} diff --git a/dist_ts/mail/delivery/smtpserver/interfaces.js b/dist_ts/mail/delivery/smtpserver/interfaces.js deleted file mode 100644 index 9fba0bf..0000000 --- a/dist_ts/mail/delivery/smtpserver/interfaces.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * SMTP Server Interfaces - * Defines all the interfaces used by the SMTP server implementation - */ -import * as plugins from '../../../plugins.js'; -// Re-export types from other modules -import { SmtpState } from '../interfaces.js'; -import { SmtpCommand } from './constants.js'; -export { SmtpState, SmtpCommand }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9pbnRlcmZhY2VzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFJL0MsdUNBQXVDO0FBQ3ZDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM3QyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDN0MsT0FBTyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsQ0FBQyJ9 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/secure-server.d.ts b/dist_ts/mail/delivery/smtpserver/secure-server.d.ts deleted file mode 100644 index d2b16ce..0000000 --- a/dist_ts/mail/delivery/smtpserver/secure-server.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Secure SMTP Server Utility Functions - * Provides helper functions for creating and managing secure TLS server - */ -import * as plugins from '../../../plugins.js'; -/** - * Create a secure TLS server for direct TLS connections - * @param options - TLS certificate options - * @returns A configured TLS server or undefined if TLS is not available - */ -export declare function createSecureTlsServer(options: { - key: string; - cert: string; - ca?: string; -}): plugins.tls.Server | undefined; diff --git a/dist_ts/mail/delivery/smtpserver/secure-server.js b/dist_ts/mail/delivery/smtpserver/secure-server.js deleted file mode 100644 index 0d0952e..0000000 --- a/dist_ts/mail/delivery/smtpserver/secure-server.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Secure SMTP Server Utility Functions - * Provides helper functions for creating and managing secure TLS server - */ -import * as plugins from '../../../plugins.js'; -import { loadCertificatesFromString, generateSelfSignedCertificates, createTlsOptions } from './certificate-utils.js'; -import { SmtpLogger } from './utils/logging.js'; -/** - * Create a secure TLS server for direct TLS connections - * @param options - TLS certificate options - * @returns A configured TLS server or undefined if TLS is not available - */ -export function createSecureTlsServer(options) { - try { - // Log the creation attempt - SmtpLogger.info('Creating secure TLS server for direct connections'); - // Load certificates from strings - let certificates; - try { - certificates = loadCertificatesFromString({ - key: options.key, - cert: options.cert, - ca: options.ca - }); - SmtpLogger.info('Successfully loaded TLS certificates for secure server'); - } - catch (certificateError) { - SmtpLogger.warn(`Failed to load certificates, using self-signed: ${certificateError instanceof Error ? certificateError.message : String(certificateError)}`); - certificates = generateSelfSignedCertificates(); - } - // Create server-side TLS options - const tlsOptions = createTlsOptions(certificates, true); - // Log details for debugging - SmtpLogger.debug('Creating secure server with options', { - certificates: { - keyLength: certificates.key.length, - certLength: certificates.cert.length, - caLength: certificates.ca ? certificates.ca.length : 0 - }, - tlsOptions: { - minVersion: tlsOptions.minVersion, - maxVersion: tlsOptions.maxVersion, - ciphers: tlsOptions.ciphers?.substring(0, 50) + '...' // Truncate long cipher list - } - }); - // Create the TLS server - const server = new plugins.tls.Server(tlsOptions); - // Set up error handlers - server.on('error', (err) => { - SmtpLogger.error(`Secure server error: ${err.message}`, { - component: 'secure-server', - error: err, - stack: err.stack - }); - }); - // Log secure connections - server.on('secureConnection', (socket) => { - const protocol = socket.getProtocol(); - const cipher = socket.getCipher(); - SmtpLogger.info('New direct TLS connection established', { - component: 'secure-server', - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - protocol: protocol || 'unknown', - cipher: cipher?.name || 'unknown' - }); - }); - return server; - } - catch (error) { - SmtpLogger.error(`Failed to create secure TLS server: ${error instanceof Error ? error.message : String(error)}`, { - component: 'secure-server', - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - return undefined; - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdXJlLXNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9zZWN1cmUtc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxFQUNMLDBCQUEwQixFQUMxQiw4QkFBOEIsRUFDOUIsZ0JBQWdCLEVBRWpCLE1BQU0sd0JBQXdCLENBQUM7QUFDaEMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRWhEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUscUJBQXFCLENBQUMsT0FJckM7SUFDQyxJQUFJLENBQUM7UUFDSCwyQkFBMkI7UUFDM0IsVUFBVSxDQUFDLElBQUksQ0FBQyxtREFBbUQsQ0FBQyxDQUFDO1FBRXJFLGlDQUFpQztRQUNqQyxJQUFJLFlBQThCLENBQUM7UUFDbkMsSUFBSSxDQUFDO1lBQ0gsWUFBWSxHQUFHLDBCQUEwQixDQUFDO2dCQUN4QyxHQUFHLEVBQUUsT0FBTyxDQUFDLEdBQUc7Z0JBQ2hCLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSTtnQkFDbEIsRUFBRSxFQUFFLE9BQU8sQ0FBQyxFQUFFO2FBQ2YsQ0FBQyxDQUFDO1lBRUgsVUFBVSxDQUFDLElBQUksQ0FBQyx3REFBd0QsQ0FBQyxDQUFDO1FBQzVFLENBQUM7UUFBQyxPQUFPLGdCQUFnQixFQUFFLENBQUM7WUFDMUIsVUFBVSxDQUFDLElBQUksQ0FBQyxtREFBbUQsZ0JBQWdCLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUM5SixZQUFZLEdBQUcsOEJBQThCLEVBQUUsQ0FBQztRQUNsRCxDQUFDO1FBRUQsaUNBQWlDO1FBQ2pDLE1BQU0s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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/security-handler.d.ts b/dist_ts/mail/delivery/smtpserver/security-handler.d.ts deleted file mode 100644 index fb85dda..0000000 --- a/dist_ts/mail/delivery/smtpserver/security-handler.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * SMTP Security Handler - * Responsible for security aspects including IP reputation checking, - * email validation, and authentication - */ -import * as plugins from '../../../plugins.js'; -import type { ISmtpAuth } from './interfaces.js'; -import type { ISecurityHandler, ISmtpServer } from './interfaces.js'; -/** - * Handles security aspects for SMTP server - */ -export declare class SecurityHandler implements ISecurityHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer; - /** - * IP reputation checker service - */ - private ipReputationService; - /** - * Simple in-memory IP denylist - */ - private ipDenylist; - /** - * Cleanup interval timer - */ - private cleanupInterval; - /** - * Creates a new security handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer); - /** - * Check IP reputation for a connection - * @param socket - Client socket - * @returns Promise that resolves to true if IP is allowed, false if blocked - */ - checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise; - /** - * Validate an email address - * @param email - Email address to validate - * @returns Whether the email address is valid - */ - isValidEmail(email: string): boolean; - /** - * Validate authentication credentials - * @param auth - Authentication credentials - * @returns Promise that resolves to true if authenticated - */ - authenticate(auth: ISmtpAuth): Promise; - /** - * Log a security event - * @param event - Event type - * @param level - Log level - * @param details - Event details - */ - logSecurityEvent(event: string, level: string, message: string, details: Record): void; - /** - * Add an IP to the denylist - * @param ip - IP address - * @param reason - Reason for denylisting - * @param duration - Duration in milliseconds (optional, indefinite if not specified) - */ - private addToDenylist; - /** - * Check if an IP is denylisted - * @param ip - IP address - * @returns Whether the IP is denylisted - */ - private isIpDenylisted; - /** - * Get the reason an IP was denylisted - * @param ip - IP address - * @returns Reason for denylisting or undefined if not denylisted - */ - private getDenylistReason; - /** - * Clean expired denylist entries - */ - private cleanExpiredDenylistEntries; - /** - * Clean up resources - */ - destroy(): void; -} diff --git a/dist_ts/mail/delivery/smtpserver/security-handler.js b/dist_ts/mail/delivery/smtpserver/security-handler.js deleted file mode 100644 index a6e6f9d..0000000 --- a/dist_ts/mail/delivery/smtpserver/security-handler.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * SMTP Security Handler - * Responsible for security aspects including IP reputation checking, - * email validation, and authentication - */ -import * as plugins from '../../../plugins.js'; -import { SmtpLogger } from './utils/logging.js'; -import { SecurityEventType, SecurityLogLevel } from './constants.js'; -import { isValidEmail } from './utils/validation.js'; -import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; -import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.js'; -/** - * Handles security aspects for SMTP server - */ -export class SecurityHandler { - /** - * Reference to the SMTP server instance - */ - smtpServer; - /** - * IP reputation checker service - */ - ipReputationService; - /** - * Simple in-memory IP denylist - */ - ipDenylist = []; - /** - * Cleanup interval timer - */ - cleanupInterval = null; - /** - * Creates a new security handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer) { - this.smtpServer = smtpServer; - // Initialize IP reputation checker - this.ipReputationService = new IPReputationChecker(); - // Clean expired denylist entries periodically - this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute - } - /** - * Check IP reputation for a connection - * @param socket - Client socket - * @returns Promise that resolves to true if IP is allowed, false if blocked - */ - async checkIpReputation(socket) { - const socketDetails = getSocketDetails(socket); - const ip = socketDetails.remoteAddress; - // Check local denylist first - if (this.isIpDenylisted(ip)) { - // Log the blocked connection - this.logSecurityEvent(SecurityEventType.IP_REPUTATION, SecurityLogLevel.WARN, `Connection blocked from denylisted IP: ${ip}`, { reason: this.getDenylistReason(ip) }); - return false; - } - // Check with IP reputation service - if (!this.ipReputationService) { - return true; - } - try { - // Check with IP reputation service - const reputationResult = await this.ipReputationService.checkReputation(ip); - // Block if score is below HIGH_RISK threshold (20) or if it's spam/proxy/tor/vpn - const isBlocked = reputationResult.score < 20 || - reputationResult.isSpam || - reputationResult.isTor || - reputationResult.isProxy; - if (isBlocked) { - // Add to local denylist temporarily - const reason = reputationResult.isSpam ? 'spam' : - reputationResult.isTor ? 'tor' : - reputationResult.isProxy ? 'proxy' : - `low reputation score: ${reputationResult.score}`; - this.addToDenylist(ip, reason, 3600000); // 1 hour - // Log the blocked connection - this.logSecurityEvent(SecurityEventType.IP_REPUTATION, SecurityLogLevel.WARN, `Connection blocked by reputation service: ${ip}`, { - reason, - score: reputationResult.score, - isSpam: reputationResult.isSpam, - isTor: reputationResult.isTor, - isProxy: reputationResult.isProxy, - isVPN: reputationResult.isVPN - }); - return false; - } - // Log the allowed connection - this.logSecurityEvent(SecurityEventType.IP_REPUTATION, SecurityLogLevel.INFO, `IP reputation check passed: ${ip}`, { - score: reputationResult.score, - country: reputationResult.country, - org: reputationResult.org - }); - return true; - } - catch (error) { - // Log the error - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - ip, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Allow the connection on error (fail open) - return true; - } - } - /** - * Validate an email address - * @param email - Email address to validate - * @returns Whether the email address is valid - */ - isValidEmail(email) { - return isValidEmail(email); - } - /** - * Validate authentication credentials - * @param auth - Authentication credentials - * @returns Promise that resolves to true if authenticated - */ - async authenticate(auth) { - const { username, password } = auth; - // Get auth options from server - const options = this.smtpServer.getOptions(); - const authOptions = options.auth; - // Check if authentication is enabled - if (!authOptions) { - this.logSecurityEvent(SecurityEventType.AUTHENTICATION, SecurityLogLevel.WARN, 'Authentication attempt when auth is disabled', { username }); - return false; - } - // Note: Method validation and TLS requirement checks would need to be done - // at the caller level since the interface doesn't include session/method info - try { - let authenticated = false; - // Use custom validation function if provided - if (authOptions.validateUser) { - authenticated = await authOptions.validateUser(username, password); - } - else { - // Default behavior - no authentication - authenticated = false; - } - // Log the authentication result - this.logSecurityEvent(SecurityEventType.AUTHENTICATION, authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN, authenticated ? 'Authentication successful' : 'Authentication failed', { username }); - return authenticated; - } - catch (error) { - // Log authentication error - this.logSecurityEvent(SecurityEventType.AUTHENTICATION, SecurityLogLevel.ERROR, `Authentication error: ${error instanceof Error ? error.message : String(error)}`, { username, error: error instanceof Error ? error.message : String(error) }); - return false; - } - } - /** - * Log a security event - * @param event - Event type - * @param level - Log level - * @param details - Event details - */ - logSecurityEvent(event, level, message, details) { - SmtpLogger.logSecurityEvent(level, event, message, details, details.ip, details.domain, details.success); - } - /** - * Add an IP to the denylist - * @param ip - IP address - * @param reason - Reason for denylisting - * @param duration - Duration in milliseconds (optional, indefinite if not specified) - */ - addToDenylist(ip, reason, duration) { - // Remove existing entry if present - this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip); - // Create new entry - const entry = { - ip, - reason, - expiresAt: duration ? Date.now() + duration : undefined - }; - // Add to denylist - this.ipDenylist.push(entry); - // Log the action - this.logSecurityEvent(SecurityEventType.ACCESS_CONTROL, SecurityLogLevel.INFO, `Added IP to denylist: ${ip}`, { - ip, - reason, - duration: duration ? `${duration / 1000} seconds` : 'indefinite' - }); - } - /** - * Check if an IP is denylisted - * @param ip - IP address - * @returns Whether the IP is denylisted - */ - isIpDenylisted(ip) { - const entry = this.ipDenylist.find(e => e.ip === ip); - if (!entry) { - return false; - } - // Check if entry has expired - if (entry.expiresAt && entry.expiresAt < Date.now()) { - // Remove expired entry - this.ipDenylist = this.ipDenylist.filter(e => e !== entry); - return false; - } - return true; - } - /** - * Get the reason an IP was denylisted - * @param ip - IP address - * @returns Reason for denylisting or undefined if not denylisted - */ - getDenylistReason(ip) { - const entry = this.ipDenylist.find(e => e.ip === ip); - return entry?.reason; - } - /** - * Clean expired denylist entries - */ - cleanExpiredDenylistEntries() { - const now = Date.now(); - const initialCount = this.ipDenylist.length; - this.ipDenylist = this.ipDenylist.filter(entry => { - return !entry.expiresAt || entry.expiresAt > now; - }); - const removedCount = initialCount - this.ipDenylist.length; - if (removedCount > 0) { - this.logSecurityEvent(SecurityEventType.ACCESS_CONTROL, SecurityLogLevel.INFO, `Cleaned up ${removedCount} expired denylist entries`, { remainingCount: this.ipDenylist.length }); - } - } - /** - * Clean up resources - */ - destroy() { - // Clear the cleanup interval - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - // Clear the denylist - this.ipDenylist = []; - // Clean up IP reputation service if it has a destroy method - if (this.ipReputationService && typeof this.ipReputationService.destroy === 'function') { - this.ipReputationService.destroy(); - } - SmtpLogger.debug('SecurityHandler destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdXJpdHktaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9zZWN1cml0eS1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7O0dBSUc7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBRy9DLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNyRSxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGFBQWEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3JFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGtEQUFrRCxDQUFDO0FBV3ZGOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGVBQWU7SUFDMUI7O09BRUc7SUFDSyxVQUFVLENBQWM7SUFFaEM7O09BRUc7SUFDSyxtQkFBbUIsQ0FBc0I7SUFFakQ7O09BRUc7SUFDSyxVQUFVLEdBQXVCLEVBQUUsQ0FBQztJQUU1Qzs7T0FFRztJQUNLLGVBQWUsR0FBMEIsSUFBSSxDQUFDO0lBRXREOzs7T0FHRztJQUNILFlBQVksVUFBdUI7UUFDakMsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUM7UUFFN0IsbUNBQW1DO1FBQ25DLElBQUksQ0FBQyxtQkFBbUIsR0FBRyxJQUFJLG1CQUFtQixFQUFFLENBQUM7UUFFckQsOENBQThDO1FBQzlDLElBQUksQ0FBQyxlQUFlLEdBQUcsV0FBVyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQywyQkFBMkIsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUMsZUFBZTtJQUN0RyxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLEtBQUssQ0FBQyxpQkFB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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/session-manager.d.ts b/dist_ts/mail/delivery/smtpserver/session-manager.d.ts deleted file mode 100644 index 4629a23..0000000 --- a/dist_ts/mail/delivery/smtpserver/session-manager.d.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * SMTP Session Manager - * Responsible for creating, managing, and cleaning up SMTP sessions - */ -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import type { ISmtpSession } from './interfaces.js'; -import type { ISessionManager, ISessionEvents } from './interfaces.js'; -/** - * Manager for SMTP sessions - * Handles session creation, tracking, timeout management, and cleanup - */ -export declare class SessionManager implements ISessionManager { - /** - * Map of socket ID to session - */ - private sessions; - /** - * Map of socket to socket ID - */ - private socketIds; - /** - * SMTP server options - */ - private options; - /** - * Event listeners - */ - private eventListeners; - /** - * Timer for cleanup interval - */ - private cleanupTimer; - /** - * Creates a new session manager - * @param options - Session manager options - */ - constructor(options?: { - socketTimeout?: number; - connectionTimeout?: number; - cleanupInterval?: number; - }); - /** - * Creates a new session for a socket connection - * @param socket - Client socket - * @param secure - Whether the connection is secure (TLS) - * @returns New SMTP session - */ - createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession; - /** - * Updates the session state - * @param session - SMTP session - * @param newState - New state - */ - updateSessionState(session: ISmtpSession, newState: SmtpState): void; - /** - * Updates the session's last activity timestamp - * @param session - SMTP session - */ - updateSessionActivity(session: ISmtpSession): void; - /** - * Removes a session - * @param socket - Client socket - */ - removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Gets a session for a socket - * @param socket - Client socket - * @returns SMTP session or undefined if not found - */ - getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined; - /** - * Cleans up idle sessions - */ - cleanupIdleSessions(): void; - /** - * Gets the current number of active sessions - * @returns Number of active sessions - */ - getSessionCount(): number; - /** - * Clears all sessions (used when shutting down) - */ - clearAllSessions(): void; - /** - * Register an event listener - * @param event - Event name - * @param listener - Event listener function - */ - on(event: K, listener: ISessionEvents[K]): void; - /** - * Remove an event listener - * @param event - Event name - * @param listener - Event listener function - */ - off(event: K, listener: ISessionEvents[K]): void; - /** - * Emit an event to registered listeners - * @param event - Event name - * @param args - Event arguments - */ - private emitEvent; - /** - * Start the cleanup timer - */ - private startCleanupTimer; - /** - * Stop the cleanup timer - */ - private stopCleanupTimer; - /** - * Replace socket mapping for STARTTLS upgrades - * @param oldSocket - Original plain socket - * @param newSocket - New TLS socket - * @returns Whether the replacement was successful - */ - replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean; - /** - * Gets a unique key for a socket - * @param socket - Client socket - * @returns Socket key - */ - private getSocketKey; - /** - * Get all active sessions - */ - getAllSessions(): ISmtpSession[]; - /** - * Update last activity for a session by socket - */ - updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Check for timed out sessions - */ - checkTimeouts(timeoutMs: number): ISmtpSession[]; - /** - * Clean up resources - */ - destroy(): void; -} diff --git a/dist_ts/mail/delivery/smtpserver/session-manager.js b/dist_ts/mail/delivery/smtpserver/session-manager.js deleted file mode 100644 index eea9bd8..0000000 --- a/dist_ts/mail/delivery/smtpserver/session-manager.js +++ /dev/null @@ -1,473 +0,0 @@ -/** - * SMTP Session Manager - * Responsible for creating, managing, and cleaning up SMTP sessions - */ -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import { SMTP_DEFAULTS } from './constants.js'; -import { generateSessionId, getSocketDetails } from './utils/helpers.js'; -import { SmtpLogger } from './utils/logging.js'; -/** - * Manager for SMTP sessions - * Handles session creation, tracking, timeout management, and cleanup - */ -export class SessionManager { - /** - * Map of socket ID to session - */ - sessions = new Map(); - /** - * Map of socket to socket ID - */ - socketIds = new Map(); - /** - * SMTP server options - */ - options; - /** - * Event listeners - */ - eventListeners = {}; - /** - * Timer for cleanup interval - */ - cleanupTimer = null; - /** - * Creates a new session manager - * @param options - Session manager options - */ - constructor(options = {}) { - this.options = { - socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, - connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT, - cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL - }; - // Start the cleanup timer - this.startCleanupTimer(); - } - /** - * Creates a new session for a socket connection - * @param socket - Client socket - * @param secure - Whether the connection is secure (TLS) - * @returns New SMTP session - */ - createSession(socket, secure) { - const sessionId = generateSessionId(); - const socketDetails = getSocketDetails(socket); - // Create a new session - const session = { - id: sessionId, - state: SmtpState.GREETING, - clientHostname: '', - mailFrom: '', - rcptTo: [], - emailData: '', - emailDataChunks: [], - emailDataSize: 0, - useTLS: secure || false, - connectionEnded: false, - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - createdAt: new Date(), - secure: secure || false, - authenticated: false, - envelope: { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }, - lastActivity: Date.now() - }; - // Store session with unique ID - const socketKey = this.getSocketKey(socket); - this.socketIds.set(socket, socketKey); - this.sessions.set(socketKey, session); - // Set socket timeout - socket.setTimeout(this.options.socketTimeout); - // Emit session created event - this.emitEvent('created', session, socket); - // Log session creation - SmtpLogger.info(`Created SMTP session ${sessionId}`, { - sessionId, - remoteAddress: session.remoteAddress, - remotePort: socketDetails.remotePort, - secure: session.secure - }); - return session; - } - /** - * Updates the session state - * @param session - SMTP session - * @param newState - New state - */ - updateSessionState(session, newState) { - if (session.state === newState) { - return; - } - const previousState = session.state; - session.state = newState; - // Update activity timestamp - this.updateSessionActivity(session); - // Emit state changed event - this.emitEvent('stateChanged', session, previousState, newState); - // Log state change - SmtpLogger.debug(`Session ${session.id} state changed from ${previousState} to ${newState}`, { - sessionId: session.id, - previousState, - newState, - remoteAddress: session.remoteAddress - }); - } - /** - * Updates the session's last activity timestamp - * @param session - SMTP session - */ - updateSessionActivity(session) { - session.lastActivity = Date.now(); - } - /** - * Removes a session - * @param socket - Client socket - */ - removeSession(socket) { - const socketKey = this.socketIds.get(socket); - if (!socketKey) { - return; - } - const session = this.sessions.get(socketKey); - if (session) { - // Mark the session as ended - session.connectionEnded = true; - // Clear any data timeout if it exists - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - // Emit session completed event - this.emitEvent('completed', session, socket); - // Log session removal - SmtpLogger.info(`Removed SMTP session ${session.id}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - finalState: session.state - }); - } - // Remove from maps - this.sessions.delete(socketKey); - this.socketIds.delete(socket); - } - /** - * Gets a session for a socket - * @param socket - Client socket - * @returns SMTP session or undefined if not found - */ - getSession(socket) { - const socketKey = this.socketIds.get(socket); - if (!socketKey) { - return undefined; - } - return this.sessions.get(socketKey); - } - /** - * Cleans up idle sessions - */ - cleanupIdleSessions() { - const now = Date.now(); - let timedOutCount = 0; - for (const [socketKey, session] of this.sessions.entries()) { - if (session.connectionEnded) { - // Session already marked as ended, but still in map - this.sessions.delete(socketKey); - continue; - } - // Calculate how long the session has been idle - const lastActivity = session.lastActivity || 0; - const idleTime = now - lastActivity; - // Use appropriate timeout based on session state - const timeout = session.state === SmtpState.DATA_RECEIVING - ? this.options.socketTimeout * 2 // Double timeout for data receiving - : session.state === SmtpState.GREETING - ? this.options.connectionTimeout // Initial connection timeout - : this.options.socketTimeout; // Standard timeout for other states - // Check if session has timed out - if (idleTime > timeout) { - // Find the socket for this session - let timedOutSocket; - for (const [socket, key] of this.socketIds.entries()) { - if (key === socketKey) { - timedOutSocket = socket; - break; - } - } - if (timedOutSocket) { - // Emit timeout event - this.emitEvent('timeout', session, timedOutSocket); - // Log timeout - SmtpLogger.warn(`Session ${session.id} timed out after ${Math.round(idleTime / 1000)}s of inactivity`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - state: session.state, - idleTime - }); - // End the socket connection - try { - timedOutSocket.end(); - } - catch (error) { - SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - } - // Remove from maps - this.sessions.delete(socketKey); - this.socketIds.delete(timedOutSocket); - timedOutCount++; - } - } - } - if (timedOutCount > 0) { - SmtpLogger.info(`Cleaned up ${timedOutCount} timed out sessions`, { - totalSessions: this.sessions.size - }); - } - } - /** - * Gets the current number of active sessions - * @returns Number of active sessions - */ - getSessionCount() { - return this.sessions.size; - } - /** - * Clears all sessions (used when shutting down) - */ - clearAllSessions() { - // Log the action - SmtpLogger.info(`Clearing all sessions (count: ${this.sessions.size})`); - // Clear the sessions and socket IDs maps - this.sessions.clear(); - this.socketIds.clear(); - // Stop the cleanup timer - this.stopCleanupTimer(); - } - /** - * Register an event listener - * @param event - Event name - * @param listener - Event listener function - */ - on(event, listener) { - switch (event) { - case 'created': - if (!this.eventListeners.created) { - this.eventListeners.created = new Set(); - } - this.eventListeners.created.add(listener); - break; - case 'stateChanged': - if (!this.eventListeners.stateChanged) { - this.eventListeners.stateChanged = new Set(); - } - this.eventListeners.stateChanged.add(listener); - break; - case 'timeout': - if (!this.eventListeners.timeout) { - this.eventListeners.timeout = new Set(); - } - this.eventListeners.timeout.add(listener); - break; - case 'completed': - if (!this.eventListeners.completed) { - this.eventListeners.completed = new Set(); - } - this.eventListeners.completed.add(listener); - break; - case 'error': - if (!this.eventListeners.error) { - this.eventListeners.error = new Set(); - } - this.eventListeners.error.add(listener); - break; - } - } - /** - * Remove an event listener - * @param event - Event name - * @param listener - Event listener function - */ - off(event, listener) { - switch (event) { - case 'created': - if (this.eventListeners.created) { - this.eventListeners.created.delete(listener); - } - break; - case 'stateChanged': - if (this.eventListeners.stateChanged) { - this.eventListeners.stateChanged.delete(listener); - } - break; - case 'timeout': - if (this.eventListeners.timeout) { - this.eventListeners.timeout.delete(listener); - } - break; - case 'completed': - if (this.eventListeners.completed) { - this.eventListeners.completed.delete(listener); - } - break; - case 'error': - if (this.eventListeners.error) { - this.eventListeners.error.delete(listener); - } - break; - } - } - /** - * Emit an event to registered listeners - * @param event - Event name - * @param args - Event arguments - */ - emitEvent(event, ...args) { - let listeners; - switch (event) { - case 'created': - listeners = this.eventListeners.created; - break; - case 'stateChanged': - listeners = this.eventListeners.stateChanged; - break; - case 'timeout': - listeners = this.eventListeners.timeout; - break; - case 'completed': - listeners = this.eventListeners.completed; - break; - case 'error': - listeners = this.eventListeners.error; - break; - } - if (!listeners) { - return; - } - for (const listener of listeners) { - try { - listener(...args); - } - catch (error) { - SmtpLogger.error(`Error in session event listener for ${String(event)}: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)) - }); - } - } - } - /** - * Start the cleanup timer - */ - startCleanupTimer() { - if (this.cleanupTimer) { - return; - } - this.cleanupTimer = setInterval(() => { - this.cleanupIdleSessions(); - }, this.options.cleanupInterval); - // Prevent the timer from keeping the process alive - if (this.cleanupTimer.unref) { - this.cleanupTimer.unref(); - } - } - /** - * Stop the cleanup timer - */ - stopCleanupTimer() { - if (this.cleanupTimer) { - clearInterval(this.cleanupTimer); - this.cleanupTimer = null; - } - } - /** - * Replace socket mapping for STARTTLS upgrades - * @param oldSocket - Original plain socket - * @param newSocket - New TLS socket - * @returns Whether the replacement was successful - */ - replaceSocket(oldSocket, newSocket) { - const socketKey = this.socketIds.get(oldSocket); - if (!socketKey) { - SmtpLogger.warn('Cannot replace socket - original socket not found in session manager'); - return false; - } - const session = this.sessions.get(socketKey); - if (!session) { - SmtpLogger.warn('Cannot replace socket - session not found for socket key'); - return false; - } - // Remove old socket mapping - this.socketIds.delete(oldSocket); - // Add new socket mapping - this.socketIds.set(newSocket, socketKey); - // Set socket timeout for new socket - newSocket.setTimeout(this.options.socketTimeout); - SmtpLogger.info(`Socket replaced for session ${session.id} (STARTTLS upgrade)`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - oldSocketType: oldSocket.constructor.name, - newSocketType: newSocket.constructor.name - }); - return true; - } - /** - * Gets a unique key for a socket - * @param socket - Client socket - * @returns Socket key - */ - getSocketKey(socket) { - const details = getSocketDetails(socket); - return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`; - } - /** - * Get all active sessions - */ - getAllSessions() { - return Array.from(this.sessions.values()); - } - /** - * Update last activity for a session by socket - */ - updateLastActivity(socket) { - const session = this.getSession(socket); - if (session) { - this.updateSessionActivity(session); - } - } - /** - * Check for timed out sessions - */ - checkTimeouts(timeoutMs) { - const now = Date.now(); - const timedOutSessions = []; - for (const session of this.sessions.values()) { - if (now - session.lastActivity > timeoutMs) { - timedOutSessions.push(session); - } - } - return timedOutSessions; - } - /** - * Clean up resources - */ - destroy() { - // Clear the cleanup timer - if (this.cleanupTimer) { - clearInterval(this.cleanupTimer); - this.cleanupTimer = null; - } - // Clear all sessions - this.clearAllSessions(); - // Clear event listeners - this.eventListeners = {}; - SmtpLogger.debug('SessionManager destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vzc2lvbi1tYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL3Nlc3Npb24tbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUc1QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDL0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDekUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRWhEOzs7R0FHRztBQUNILE1BQU0sT0FBTyxjQUFjO0lBQ3pCOztPQUVHO0lBQ0ssUUFBUSxHQUE4QixJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXhEOztPQUVHO0lBQ0ssU0FBUyxHQUE0RCxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXZGOztPQUVHO0lBQ0ssT0FBTyxDQUliO0lBRUY7O09BRUc7SUFDSyxjQUFjLEdBTWxCLEVBQUUsQ0FBQztJQUVQOztPQUVHO0lBQ0ssWUFBWSxHQUEwQixJQUFJLENBQUM7SUFFbkQ7OztPQUdHO0lBQ0gsWUFBWSxVQUlSLEVBQUU7UUFDSixJQUFJLENBQUMsT0FBTyxHQUFHO1lBQ2IsYUFBYSxFQUFFLE9BQU8sQ0FBQyxhQUFhLElBQUksYUFBYSxDQUFDLGNBQWM7WUFDcEUsaUJBQWlCLEVBQUUsT0FBTyxDQUFDLGlCQUFpQixJQUFJLGFBQWEsQ0FBQyxrQkFBa0I7WUFDaEYsZUFBZSxFQUFFLE9BQU8sQ0FBQyxlQUFlLElBQUksYUFBYSxDQUFDLGdCQUFnQjtTQUMzRSxDQUFDO1FBRUYsMEJBQTBCO1FBQzFCLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO0lBQzNCLENB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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/smtp-server.d.ts b/dist_ts/mail/delivery/smtpserver/smtp-server.d.ts deleted file mode 100644 index 45effce..0000000 --- a/dist_ts/mail/delivery/smtpserver/smtp-server.d.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * SMTP Server - * Core implementation for the refactored SMTP server - */ -import type { ISmtpServerOptions } from './interfaces.js'; -import type { ISmtpServer, ISmtpServerConfig, ISessionManager, IConnectionManager, ICommandHandler, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js'; -import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; -/** - * SMTP Server implementation - * The main server class that coordinates all components - */ -export declare class SmtpServer implements ISmtpServer { - /** - * Email server reference - */ - private emailServer; - /** - * Session manager - */ - private sessionManager; - /** - * Connection manager - */ - private connectionManager; - /** - * Command handler - */ - private commandHandler; - /** - * Data handler - */ - private dataHandler; - /** - * TLS handler - */ - private tlsHandler; - /** - * Security handler - */ - private securityHandler; - /** - * SMTP server options - */ - private options; - /** - * Net server instance - */ - private server; - /** - * Secure server instance - */ - private secureServer; - /** - * Whether the server is running - */ - private running; - /** - * Server recovery state - */ - private recoveryState; - /** - * Creates a new SMTP server - * @param config - Server configuration - */ - constructor(config: ISmtpServerConfig); - /** - * Start the SMTP server - * @returns Promise that resolves when server is started - */ - listen(): Promise; - /** - * Stop the SMTP server - * @returns Promise that resolves when server is stopped - */ - close(): Promise; - /** - * Get the session manager - * @returns Session manager instance - */ - getSessionManager(): ISessionManager; - /** - * Get the connection manager - * @returns Connection manager instance - */ - getConnectionManager(): IConnectionManager; - /** - * Get the command handler - * @returns Command handler instance - */ - getCommandHandler(): ICommandHandler; - /** - * Get the data handler - * @returns Data handler instance - */ - getDataHandler(): IDataHandler; - /** - * Get the TLS handler - * @returns TLS handler instance - */ - getTlsHandler(): ITlsHandler; - /** - * Get the security handler - * @returns Security handler instance - */ - getSecurityHandler(): ISecurityHandler; - /** - * Get the server options - * @returns SMTP server options - */ - getOptions(): ISmtpServerOptions; - /** - * Get the email server reference - * @returns Email server instance - */ - getEmailServer(): UnifiedEmailServer; - /** - * Check if the server is running - * @returns Whether the server is running - */ - isRunning(): boolean; - /** - * Check if we should attempt to recover from an error - * @param error - The error that occurred - * @returns Whether recovery should be attempted - */ - private shouldAttemptRecovery; - /** - * Attempt to recover the server after a critical error - * @param serverType - The type of server to recover ('standard' or 'secure') - * @param error - The error that triggered recovery - */ - private attemptServerRecovery; - /** - * Clean up all component resources - */ - destroy(): Promise; -} diff --git a/dist_ts/mail/delivery/smtpserver/smtp-server.js b/dist_ts/mail/delivery/smtpserver/smtp-server.js deleted file mode 100644 index 2a1e484..0000000 --- a/dist_ts/mail/delivery/smtpserver/smtp-server.js +++ /dev/null @@ -1,698 +0,0 @@ -/** - * SMTP Server - * Core implementation for the refactored SMTP server - */ -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import { SessionManager } from './session-manager.js'; -import { ConnectionManager } from './connection-manager.js'; -import { CommandHandler } from './command-handler.js'; -import { DataHandler } from './data-handler.js'; -import { TlsHandler } from './tls-handler.js'; -import { SecurityHandler } from './security-handler.js'; -import { SMTP_DEFAULTS } from './constants.js'; -import { mergeWithDefaults } from './utils/helpers.js'; -import { SmtpLogger } from './utils/logging.js'; -import { adaptiveLogger } from './utils/adaptive-logging.js'; -import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; -/** - * SMTP Server implementation - * The main server class that coordinates all components - */ -export class SmtpServer { - /** - * Email server reference - */ - emailServer; - /** - * Session manager - */ - sessionManager; - /** - * Connection manager - */ - connectionManager; - /** - * Command handler - */ - commandHandler; - /** - * Data handler - */ - dataHandler; - /** - * TLS handler - */ - tlsHandler; - /** - * Security handler - */ - securityHandler; - /** - * SMTP server options - */ - options; - /** - * Net server instance - */ - server = null; - /** - * Secure server instance - */ - secureServer = null; - /** - * Whether the server is running - */ - running = false; - /** - * Server recovery state - */ - recoveryState = { - /** - * Whether recovery is in progress - */ - recovering: false, - /** - * Number of consecutive connection failures - */ - connectionFailures: 0, - /** - * Last recovery attempt timestamp - */ - lastRecoveryAttempt: 0, - /** - * Recovery cooldown in milliseconds - */ - recoveryCooldown: 5000, - /** - * Maximum recovery attempts before giving up - */ - maxRecoveryAttempts: 3, - /** - * Current recovery attempt - */ - currentRecoveryAttempt: 0 - }; - /** - * Creates a new SMTP server - * @param config - Server configuration - */ - constructor(config) { - this.emailServer = config.emailServer; - this.options = mergeWithDefaults(config.options); - // Create components - all components now receive the SMTP server instance - this.sessionManager = config.sessionManager || new SessionManager({ - socketTimeout: this.options.socketTimeout, - connectionTimeout: this.options.connectionTimeout, - cleanupInterval: this.options.cleanupInterval - }); - this.securityHandler = config.securityHandler || new SecurityHandler(this); - this.tlsHandler = config.tlsHandler || new TlsHandler(this); - this.dataHandler = config.dataHandler || new DataHandler(this); - this.commandHandler = config.commandHandler || new CommandHandler(this); - this.connectionManager = config.connectionManager || new ConnectionManager(this); - } - /** - * Start the SMTP server - * @returns Promise that resolves when server is started - */ - async listen() { - if (this.running) { - throw new Error('SMTP server is already running'); - } - try { - // Create the server - this.server = plugins.net.createServer((socket) => { - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - this.connectionManager.handleNewConnection(socket); - } - else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Allow connection on error (fail open) - this.connectionManager.handleNewConnection(socket); - }); - }); - // Set up error handling with recovery - this.server.on('error', (err) => { - SmtpLogger.error(`SMTP server error: ${err.message}`, { error: err }); - // Try to recover from specific errors - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('standard', err); - } - }); - // Start listening - await new Promise((resolve, reject) => { - if (!this.server) { - reject(new Error('Server not initialized')); - return; - } - this.server.listen(this.options.port, this.options.host, () => { - SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); - resolve(); - }); - this.server.on('error', reject); - }); - // Start secure server if configured - if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { - try { - // Import the secure server creation utility from our new module - // This gives us better certificate handling and error resilience - const { createSecureTlsServer } = await import('./secure-server.js'); - // Create secure server with the certificates - // This uses a more robust approach to certificate loading and validation - this.secureServer = createSecureTlsServer({ - key: this.options.key, - cert: this.options.cert, - ca: this.options.ca - }); - SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort}`); - if (this.secureServer) { - // Use explicit error handling for secure connections - this.secureServer.on('tlsClientError', (err, tlsSocket) => { - SmtpLogger.error(`TLS client error: ${err.message}`, { - error: err, - remoteAddress: tlsSocket.remoteAddress, - remotePort: tlsSocket.remotePort, - stack: err.stack - }); - // No need to destroy, the error event will handle that - }); - // Register the secure connection handler - this.secureServer.on('secureConnection', (socket) => { - SmtpLogger.info(`New secure connection from ${socket.remoteAddress}:${socket.remotePort}`, { - protocol: socket.getProtocol(), - cipher: socket.getCipher()?.name - }); - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - // Pass the connection to the connection manager - this.connectionManager.handleNewSecureConnection(socket); - } - else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - // Allow connection on error (fail open) - this.connectionManager.handleNewSecureConnection(socket); - }); - }); - // Global error handler for the secure server with recovery - this.secureServer.on('error', (err) => { - SmtpLogger.error(`SMTP secure server error: ${err.message}`, { - error: err, - stack: err.stack - }); - // Try to recover from specific errors - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('secure', err); - } - }); - // Start listening on secure port - await new Promise((resolve, reject) => { - if (!this.secureServer) { - reject(new Error('Secure server not initialized')); - return; - } - this.secureServer.listen(this.options.securePort, this.options.host, () => { - SmtpLogger.info(`SMTP secure server listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`); - resolve(); - }); - // Only use error event for startup issues - this.secureServer.once('error', reject); - }); - } - else { - SmtpLogger.warn('Failed to create secure server, TLS may not be properly configured'); - } - } - catch (error) { - SmtpLogger.error(`Error setting up secure server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - } - } - this.running = true; - } - catch (error) { - SmtpLogger.error(`Failed to start SMTP server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)) - }); - // Clean up on error - this.close(); - throw error; - } - } - /** - * Stop the SMTP server - * @returns Promise that resolves when server is stopped - */ - async close() { - if (!this.running) { - return; - } - SmtpLogger.info('Stopping SMTP server'); - try { - // Close all active connections - this.connectionManager.closeAllConnections(); - // Clear all sessions - this.sessionManager.clearAllSessions(); - // Clean up adaptive logger to prevent hanging timers - adaptiveLogger.destroy(); - // Destroy all components to clean up their resources - await this.destroy(); - // Close servers - const closePromises = []; - if (this.server) { - closePromises.push(new Promise((resolve, reject) => { - if (!this.server) { - resolve(); - return; - } - this.server.close((err) => { - if (err) { - reject(err); - } - else { - resolve(); - } - }); - })); - } - if (this.secureServer) { - closePromises.push(new Promise((resolve, reject) => { - if (!this.secureServer) { - resolve(); - return; - } - this.secureServer.close((err) => { - if (err) { - reject(err); - } - else { - resolve(); - } - }); - })); - } - // Add timeout to prevent hanging on close - await Promise.race([ - Promise.all(closePromises), - new Promise((resolve) => { - setTimeout(() => { - SmtpLogger.warn('Server close timed out after 3 seconds, forcing shutdown'); - resolve(); - }, 3000); - }) - ]); - this.server = null; - this.secureServer = null; - this.running = false; - SmtpLogger.info('SMTP server stopped'); - } - catch (error) { - SmtpLogger.error(`Error stopping SMTP server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)) - }); - throw error; - } - } - /** - * Get the session manager - * @returns Session manager instance - */ - getSessionManager() { - return this.sessionManager; - } - /** - * Get the connection manager - * @returns Connection manager instance - */ - getConnectionManager() { - return this.connectionManager; - } - /** - * Get the command handler - * @returns Command handler instance - */ - getCommandHandler() { - return this.commandHandler; - } - /** - * Get the data handler - * @returns Data handler instance - */ - getDataHandler() { - return this.dataHandler; - } - /** - * Get the TLS handler - * @returns TLS handler instance - */ - getTlsHandler() { - return this.tlsHandler; - } - /** - * Get the security handler - * @returns Security handler instance - */ - getSecurityHandler() { - return this.securityHandler; - } - /** - * Get the server options - * @returns SMTP server options - */ - getOptions() { - return this.options; - } - /** - * Get the email server reference - * @returns Email server instance - */ - getEmailServer() { - return this.emailServer; - } - /** - * Check if the server is running - * @returns Whether the server is running - */ - isRunning() { - return this.running; - } - /** - * Check if we should attempt to recover from an error - * @param error - The error that occurred - * @returns Whether recovery should be attempted - */ - shouldAttemptRecovery(error) { - // Skip recovery if we're already in recovery mode - if (this.recoveryState.recovering) { - return false; - } - // Check if we've reached the maximum number of recovery attempts - if (this.recoveryState.currentRecoveryAttempt >= this.recoveryState.maxRecoveryAttempts) { - SmtpLogger.warn('Maximum recovery attempts reached, not attempting further recovery'); - return false; - } - // Check if enough time has passed since the last recovery attempt - const now = Date.now(); - if (now - this.recoveryState.lastRecoveryAttempt < this.recoveryState.recoveryCooldown) { - SmtpLogger.warn('Recovery cooldown period not elapsed, skipping recovery attempt'); - return false; - } - // Recoverable errors include: - // - EADDRINUSE: Address already in use (port conflict) - // - ECONNRESET: Connection reset by peer - // - EPIPE: Broken pipe - // - ETIMEDOUT: Connection timed out - const recoverableErrors = [ - 'EADDRINUSE', - 'ECONNRESET', - 'EPIPE', - 'ETIMEDOUT', - 'ECONNABORTED', - 'EPROTO', - 'EMFILE' // Too many open files - ]; - // Check if this is a recoverable error - const errorCode = error.code; - return recoverableErrors.includes(errorCode); - } - /** - * Attempt to recover the server after a critical error - * @param serverType - The type of server to recover ('standard' or 'secure') - * @param error - The error that triggered recovery - */ - async attemptServerRecovery(serverType, error) { - // Set recovery flag to prevent multiple simultaneous recovery attempts - if (this.recoveryState.recovering) { - SmtpLogger.warn('Recovery already in progress, skipping new recovery attempt'); - return; - } - this.recoveryState.recovering = true; - this.recoveryState.lastRecoveryAttempt = Date.now(); - this.recoveryState.currentRecoveryAttempt++; - SmtpLogger.info(`Attempting server recovery for ${serverType} server after error: ${error.message}`, { - attempt: this.recoveryState.currentRecoveryAttempt, - maxAttempts: this.recoveryState.maxRecoveryAttempts, - errorCode: error.code - }); - try { - // Determine which server to restart - const isStandardServer = serverType === 'standard'; - // Close the affected server - if (isStandardServer && this.server) { - await new Promise((resolve) => { - if (!this.server) { - resolve(); - return; - } - // First try a clean shutdown - this.server.close((err) => { - if (err) { - SmtpLogger.warn(`Error during server close in recovery: ${err.message}`); - } - resolve(); - }); - // Set a timeout to force close - setTimeout(() => { - resolve(); - }, 3000); - }); - this.server = null; - } - else if (!isStandardServer && this.secureServer) { - await new Promise((resolve) => { - if (!this.secureServer) { - resolve(); - return; - } - // First try a clean shutdown - this.secureServer.close((err) => { - if (err) { - SmtpLogger.warn(`Error during secure server close in recovery: ${err.message}`); - } - resolve(); - }); - // Set a timeout to force close - setTimeout(() => { - resolve(); - }, 3000); - }); - this.secureServer = null; - } - // Short delay before restarting - await new Promise((resolve) => setTimeout(resolve, 1000)); - // Clean up any lingering connections - this.connectionManager.closeAllConnections(); - this.sessionManager.clearAllSessions(); - // Restart the affected server - if (isStandardServer) { - // Create and start the standard server - this.server = plugins.net.createServer((socket) => { - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - this.connectionManager.handleNewConnection(socket); - } - else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Allow connection on error (fail open) - this.connectionManager.handleNewConnection(socket); - }); - }); - // Set up error handling with recovery - this.server.on('error', (err) => { - SmtpLogger.error(`SMTP server error after recovery: ${err.message}`, { error: err }); - // Try to recover again if needed - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('standard', err); - } - }); - // Start listening again - await new Promise((resolve, reject) => { - if (!this.server) { - reject(new Error('Server not initialized during recovery')); - return; - } - this.server.listen(this.options.port, this.options.host, () => { - SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); - resolve(); - }); - // Only use error event for startup issues during recovery - this.server.once('error', (err) => { - SmtpLogger.error(`Failed to restart server during recovery: ${err.message}`); - reject(err); - }); - }); - } - else if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { - // Try to recreate the secure server - try { - // Import the secure server creation utility - const { createSecureTlsServer } = await import('./secure-server.js'); - // Create secure server with the certificates - this.secureServer = createSecureTlsServer({ - key: this.options.key, - cert: this.options.cert, - ca: this.options.ca - }); - if (this.secureServer) { - SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort} during recovery`); - // Use explicit error handling for secure connections - this.secureServer.on('tlsClientError', (err, tlsSocket) => { - SmtpLogger.error(`TLS client error after recovery: ${err.message}`, { - error: err, - remoteAddress: tlsSocket.remoteAddress, - remotePort: tlsSocket.remotePort, - stack: err.stack - }); - }); - // Register the secure connection handler - this.secureServer.on('secureConnection', (socket) => { - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - // Pass the connection to the connection manager - this.connectionManager.handleNewSecureConnection(socket); - } - else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error after recovery: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Allow connection on error (fail open) - this.connectionManager.handleNewSecureConnection(socket); - }); - }); - // Global error handler for the secure server with recovery - this.secureServer.on('error', (err) => { - SmtpLogger.error(`SMTP secure server error after recovery: ${err.message}`, { - error: err, - stack: err.stack - }); - // Try to recover again if needed - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('secure', err); - } - }); - // Start listening on secure port again - await new Promise((resolve, reject) => { - if (!this.secureServer) { - reject(new Error('Secure server not initialized during recovery')); - return; - } - this.secureServer.listen(this.options.securePort, this.options.host, () => { - SmtpLogger.info(`SMTP secure server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`); - resolve(); - }); - // Only use error event for startup issues during recovery - this.secureServer.once('error', (err) => { - SmtpLogger.error(`Failed to restart secure server during recovery: ${err.message}`); - reject(err); - }); - }); - } - else { - SmtpLogger.warn('Failed to create secure server during recovery'); - } - } - catch (error) { - SmtpLogger.error(`Error setting up secure server during recovery: ${error instanceof Error ? error.message : String(error)}`); - } - } - // Recovery successful - SmtpLogger.info('Server recovery completed successfully'); - } - catch (recoveryError) { - SmtpLogger.error(`Server recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`, { - error: recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError)), - attempt: this.recoveryState.currentRecoveryAttempt, - maxAttempts: this.recoveryState.maxRecoveryAttempts - }); - } - finally { - // Reset recovery flag - this.recoveryState.recovering = false; - } - } - /** - * Clean up all component resources - */ - async destroy() { - SmtpLogger.info('Destroying SMTP server components'); - // Destroy all components in parallel - const destroyPromises = []; - if (this.sessionManager && typeof this.sessionManager.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.sessionManager.destroy())); - } - if (this.connectionManager && typeof this.connectionManager.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.connectionManager.destroy())); - } - if (this.commandHandler && typeof this.commandHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.commandHandler.destroy())); - } - if (this.dataHandler && typeof this.dataHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.dataHandler.destroy())); - } - if (this.tlsHandler && typeof this.tlsHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.tlsHandler.destroy())); - } - if (this.securityHandler && typeof this.securityHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.securityHandler.destroy())); - } - await Promise.all(destroyPromises); - // Destroy the adaptive logger singleton to clean up its timer - const { adaptiveLogger } = await import('./utils/adaptive-logging.js'); - if (adaptiveLogger && typeof adaptiveLogger.destroy === 'function') { - adaptiveLogger.destroy(); - } - // Clear recovery state - this.recoveryState = { - recovering: false, - connectionFailures: 0, - lastRecoveryAttempt: 0, - recoveryCooldown: 5000, - maxRecoveryAttempts: 3, - currentRecoveryAttempt: 0 - }; - SmtpLogger.info('All SMTP server components destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic210cC1zZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBzZXJ2ZXIvc210cC1zZXJ2ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFHNUMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzVELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUN0RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDaEQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzlDLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUN4RCxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDL0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDdkQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSwrQ0FBK0MsQ0FBQztBQUVuRjs7O0dBR0c7QUFDSCxNQUFNLE9BQU8sVUFBVTtJQUNyQjs7T0FFRztJQUNLLFdBQVcsQ0FBcUI7SUFFeEM7O09BRUc7SUFDSyxjQUFjLENBQWtCO0lBRXhDOztPQUVHO0lBQ0ssaUJBQWlCLENBQXFCO0lBRTlDOztPQUVHO0lBQ0ssY0FBYyxDQUFrQjtJQUV4Qzs7T0FFRztJQUNLLFdBQVcsQ0FBZTtJQUVsQzs7T0FF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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/starttls-handler.d.ts b/dist_ts/mail/delivery/smtpserver/starttls-handler.d.ts deleted file mode 100644 index 6dd99ee..0000000 --- a/dist_ts/mail/delivery/smtpserver/starttls-handler.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * STARTTLS Implementation - * Provides an improved implementation for STARTTLS upgrades - */ -import * as plugins from '../../../plugins.js'; -import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.js'; -import { SmtpState } from '../interfaces.js'; -/** - * Enhanced STARTTLS handler for more reliable TLS upgrades - */ -export declare function performStartTLS(socket: plugins.net.Socket, options: { - key: string; - cert: string; - ca?: string; - session?: ISmtpSession; - sessionManager?: ISessionManager; - connectionManager?: IConnectionManager; - onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void; - onFailure?: (error: Error) => void; - updateSessionState?: (session: ISmtpSession, state: SmtpState) => void; -}): Promise; diff --git a/dist_ts/mail/delivery/smtpserver/starttls-handler.js b/dist_ts/mail/delivery/smtpserver/starttls-handler.js deleted file mode 100644 index 712b1ce..0000000 --- a/dist_ts/mail/delivery/smtpserver/starttls-handler.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * STARTTLS Implementation - * Provides an improved implementation for STARTTLS upgrades - */ -import * as plugins from '../../../plugins.js'; -import { SmtpLogger } from './utils/logging.js'; -import { loadCertificatesFromString, createTlsOptions } from './certificate-utils.js'; -import { getSocketDetails } from './utils/helpers.js'; -import { SmtpState } from '../interfaces.js'; -/** - * Enhanced STARTTLS handler for more reliable TLS upgrades - */ -export async function performStartTLS(socket, options) { - return new Promise((resolve) => { - try { - const socketDetails = getSocketDetails(socket); - SmtpLogger.info('Starting enhanced STARTTLS upgrade process', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - // Create a proper socket cleanup function - const cleanupSocket = () => { - // Remove all listeners to prevent memory leaks - socket.removeAllListeners('data'); - socket.removeAllListeners('error'); - socket.removeAllListeners('close'); - socket.removeAllListeners('end'); - socket.removeAllListeners('drain'); - }; - // Prepare the socket for TLS upgrade - socket.setNoDelay(true); - // Critical: make sure there's no pending data before TLS handshake - socket.pause(); - // Add error handling for the base socket - const handleSocketError = (err) => { - SmtpLogger.error(`Socket error during STARTTLS preparation: ${err.message}`, { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - error: err, - stack: err.stack - }); - if (options.onFailure) { - options.onFailure(err); - } - // Resolve with undefined to indicate failure - resolve(undefined); - }; - socket.once('error', handleSocketError); - // Load certificates - let certificates; - try { - certificates = loadCertificatesFromString({ - key: options.key, - cert: options.cert, - ca: options.ca - }); - } - catch (certError) { - SmtpLogger.error(`Certificate error during STARTTLS: ${certError instanceof Error ? certError.message : String(certError)}`); - if (options.onFailure) { - options.onFailure(certError instanceof Error ? certError : new Error(String(certError))); - } - resolve(undefined); - return; - } - // Create TLS options optimized for STARTTLS - const tlsOptions = createTlsOptions(certificates, true); - // Create secure context - let secureContext; - try { - secureContext = plugins.tls.createSecureContext(tlsOptions); - } - catch (contextError) { - SmtpLogger.error(`Failed to create secure context: ${contextError instanceof Error ? contextError.message : String(contextError)}`); - if (options.onFailure) { - options.onFailure(contextError instanceof Error ? contextError : new Error(String(contextError))); - } - resolve(undefined); - return; - } - // Log STARTTLS upgrade attempt - SmtpLogger.debug('Attempting TLS socket upgrade with options', { - minVersion: tlsOptions.minVersion, - maxVersion: tlsOptions.maxVersion, - handshakeTimeout: tlsOptions.handshakeTimeout - }); - // Use a safer approach to create the TLS socket - const handshakeTimeout = 30000; // 30 seconds timeout for TLS handshake - let handshakeTimeoutId; - // Create the TLS socket using a conservative approach for STARTTLS - const tlsSocket = new plugins.tls.TLSSocket(socket, { - isServer: true, - secureContext, - // Server-side options (simpler is more reliable for STARTTLS) - requestCert: false, - rejectUnauthorized: false - }); - // Set up error handling for the TLS socket - tlsSocket.once('error', (err) => { - if (handshakeTimeoutId) { - clearTimeout(handshakeTimeoutId); - } - SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - error: err, - stack: err.stack - }); - // Clean up socket listeners - cleanupSocket(); - if (options.onFailure) { - options.onFailure(err); - } - // Destroy the socket to ensure we don't have hanging connections - tlsSocket.destroy(); - resolve(undefined); - }); - // Set up handshake timeout manually for extra safety - handshakeTimeoutId = setTimeout(() => { - SmtpLogger.error('TLS handshake timed out', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - // Clean up socket listeners - cleanupSocket(); - if (options.onFailure) { - options.onFailure(new Error('TLS handshake timed out')); - } - // Destroy the socket to ensure we don't have hanging connections - tlsSocket.destroy(); - resolve(undefined); - }, handshakeTimeout); - // Set up handler for successful TLS negotiation - tlsSocket.once('secure', () => { - if (handshakeTimeoutId) { - clearTimeout(handshakeTimeoutId); - } - const protocol = tlsSocket.getProtocol(); - const cipher = tlsSocket.getCipher(); - SmtpLogger.info('TLS upgrade successful via STARTTLS', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - protocol: protocol || 'unknown', - cipher: cipher?.name || 'unknown' - }); - // Update socket mapping in session manager - if (options.sessionManager) { - const socketReplaced = options.sessionManager.replaceSocket(socket, tlsSocket); - if (!socketReplaced) { - SmtpLogger.error('Failed to replace socket in session manager after STARTTLS', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - } - } - // Re-attach event handlers from connection manager - if (options.connectionManager) { - try { - options.connectionManager.setupSocketEventHandlers(tlsSocket); - SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - } - catch (handlerError) { - SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - error: handlerError instanceof Error ? handlerError : new Error(String(handlerError)) - }); - } - } - // Update session if provided - if (options.session) { - // Update session properties to indicate TLS is active - options.session.useTLS = true; - options.session.secure = true; - // Reset session state as required by RFC 3207 - // After STARTTLS, client must issue a new EHLO - if (options.updateSessionState) { - options.updateSessionState(options.session, SmtpState.GREETING); - } - } - // Call success callback if provided - if (options.onSuccess) { - options.onSuccess(tlsSocket); - } - // Success - return the TLS socket - resolve(tlsSocket); - }); - // Resume the socket after we've set up all handlers - // This allows the TLS handshake to proceed - socket.resume(); - } - catch (error) { - SmtpLogger.error(`Unexpected error in STARTTLS: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - if (options.onFailure) { - options.onFailure(error instanceof Error ? error : new Error(String(error))); - } - resolve(undefined); - } - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhcnR0bHMtaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9zdGFydHRscy1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFDTCwwQkFBMEIsRUFDMUIsZ0JBQWdCLEVBRWpCLE1BQU0sd0JBQXdCLENBQUM7QUFDaEMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFdEQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRTdDOztHQUVHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxlQUFlLENBQ25DLE1BQTBCLEVBQzFCLE9BVUM7SUFFRCxPQUFPLElBQUksT0FBTyxDQUFvQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQ2hFLElBQUksQ0FBQztZQUNILE1BQU0sYUFBYSxHQUFHLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBRS9DLFVBQVUsQ0FBQyxJQUFJLENBQUMsNENBQTRDLEVBQUU7Z0JBQzVELGFBQWEsRUFBRSxhQUFhLENBQUMsYUFBYTtnQkFDMUMsVUFBVSxFQUFFLGFBQWEsQ0FBQyxVQUFVO2FBQ3JDLENBQUMsQ0FBQztZQUVILDBDQUEwQztZQUMxQyxNQUFNLGFBQWEsR0FBRyxHQUFHLEVBQUU7Z0JBQ3pCLCtDQUErQztnQkFDL0MsTUFBTSxDQUFDLGtCQUFrQixDQUFDLE1BQU0sQ0FBQyxDQUFDO2dCQUNsQyxNQUFNLENBQUMsa0JBQWtCLENBQUMsT0FBTyxDQUFDLENBQUM7Z0JBQ25DLE1BQU0sQ0FBQyxrQkFBa0IsQ0FBQyxPQUFPLENBQUMsQ0FBQztnQkFDbkMsTUFBTSxDQUFDLGtCQUFrQixD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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/tls-handler.d.ts b/dist_ts/mail/delivery/smtpserver/tls-handler.d.ts deleted file mode 100644 index cd73516..0000000 --- a/dist_ts/mail/delivery/smtpserver/tls-handler.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * SMTP TLS Handler - * Responsible for handling TLS-related SMTP functionality - */ -import * as plugins from '../../../plugins.js'; -import type { ITlsHandler, ISmtpServer, ISmtpSession } from './interfaces.js'; -/** - * Handles TLS functionality for SMTP server - */ -export declare class TlsHandler implements ITlsHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer; - /** - * Certificate data - */ - private certificates; - /** - * TLS options - */ - private options; - /** - * Creates a new TLS handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer); - /** - * Handle STARTTLS command - * @param socket - Client socket - */ - handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise; - /** - * Upgrade a connection to TLS - * @param socket - Client socket - */ - startTLS(socket: plugins.net.Socket): Promise; - /** - * Create a secure server - * @returns TLS server instance or undefined if TLS is not enabled - */ - createSecureServer(): plugins.tls.Server | undefined; - /** - * Check if TLS is enabled - * @returns Whether TLS is enabled - */ - isTlsEnabled(): boolean; - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response message - */ - private sendResponse; - /** - * Check if TLS is available (interface requirement) - */ - isTlsAvailable(): boolean; - /** - * Get TLS options (interface requirement) - */ - getTlsOptions(): plugins.tls.TlsOptions; - /** - * Clean up resources - */ - destroy(): void; -} diff --git a/dist_ts/mail/delivery/smtpserver/tls-handler.js b/dist_ts/mail/delivery/smtpserver/tls-handler.js deleted file mode 100644 index 164c9fb..0000000 --- a/dist_ts/mail/delivery/smtpserver/tls-handler.js +++ /dev/null @@ -1,273 +0,0 @@ -/** - * SMTP TLS Handler - * Responsible for handling TLS-related SMTP functionality - */ -import * as plugins from '../../../plugins.js'; -import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; -import { loadCertificatesFromString, generateSelfSignedCertificates, createTlsOptions } from './certificate-utils.js'; -import { SmtpState } from '../interfaces.js'; -/** - * Handles TLS functionality for SMTP server - */ -export class TlsHandler { - /** - * Reference to the SMTP server instance - */ - smtpServer; - /** - * Certificate data - */ - certificates; - /** - * TLS options - */ - options; - /** - * Creates a new TLS handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer) { - this.smtpServer = smtpServer; - // Initialize certificates - const serverOptions = this.smtpServer.getOptions(); - try { - // Try to load certificates from provided options - this.certificates = loadCertificatesFromString({ - key: serverOptions.key, - cert: serverOptions.cert, - ca: serverOptions.ca - }); - SmtpLogger.info('Successfully loaded TLS certificates'); - } - catch (error) { - SmtpLogger.warn(`Failed to load certificates from options, using self-signed: ${error instanceof Error ? error.message : String(error)}`); - // Fall back to self-signed certificates for testing - this.certificates = generateSelfSignedCertificates(); - } - // Initialize TLS options - this.options = createTlsOptions(this.certificates); - } - /** - * Handle STARTTLS command - * @param socket - Client socket - */ - async handleStartTls(socket, session) { - // Check if already using TLS - if (session.useTLS) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`); - return null; - } - // Check if we have the necessary TLS certificates - if (!this.isTlsEnabled()) { - this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`); - return null; - } - // Send ready for TLS response - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`); - // Upgrade the connection to TLS - try { - const tlsSocket = await this.startTLS(socket); - return tlsSocket; - } - catch (error) { - SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - // Log security event - SmtpLogger.logSecurityEvent(SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS negotiation failed', { error: error instanceof Error ? error.message : String(error) }, session.remoteAddress); - return null; - } - } - /** - * Upgrade a connection to TLS - * @param socket - Client socket - */ - async startTLS(socket) { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - try { - // Import the enhanced STARTTLS handler - // This uses a more robust approach to TLS upgrades - const { performStartTLS } = await import('./starttls-handler.js'); - SmtpLogger.info('Using enhanced STARTTLS implementation'); - // Use the enhanced STARTTLS handler with better error handling and socket management - const serverOptions = this.smtpServer.getOptions(); - const tlsSocket = await performStartTLS(socket, { - key: serverOptions.key, - cert: serverOptions.cert, - ca: serverOptions.ca, - session: session, - sessionManager: this.smtpServer.getSessionManager(), - connectionManager: this.smtpServer.getConnectionManager(), - // Callback for successful upgrade - onSuccess: (secureSocket) => { - SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', { - remoteAddress: secureSocket.remoteAddress, - remotePort: secureSocket.remotePort, - protocol: secureSocket.getProtocol() || 'unknown', - cipher: secureSocket.getCipher()?.name || 'unknown' - }); - // Log security event - SmtpLogger.logSecurityEvent(SecurityLogLevel.INFO, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS successful with enhanced implementation', { - protocol: secureSocket.getProtocol(), - cipher: secureSocket.getCipher()?.name - }, secureSocket.remoteAddress, undefined, true); - }, - // Callback for failed upgrade - onFailure: (error) => { - SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, { - sessionId: session?.id, - remoteAddress: socket.remoteAddress, - error - }); - // Log security event - SmtpLogger.logSecurityEvent(SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Enhanced STARTTLS failed', { error: error.message }, socket.remoteAddress, undefined, false); - }, - // Function to update session state - updateSessionState: this.smtpServer.getSessionManager().updateSessionState?.bind(this.smtpServer.getSessionManager()) - }); - // If STARTTLS failed with the enhanced implementation, log the error - if (!tlsSocket) { - SmtpLogger.warn('Enhanced STARTTLS implementation failed to create TLS socket', { - sessionId: session?.id, - remoteAddress: socket.remoteAddress - }); - throw new Error('Failed to create TLS socket'); - } - return tlsSocket; - } - catch (error) { - // Log STARTTLS failure - SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - // Log security event - SmtpLogger.logSecurityEvent(SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Failed to upgrade connection to TLS', { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }, socket.remoteAddress, undefined, false); - // Destroy the socket on error - socket.destroy(); - throw error; - } - } - /** - * Create a secure server - * @returns TLS server instance or undefined if TLS is not enabled - */ - createSecureServer() { - if (!this.isTlsEnabled()) { - return undefined; - } - try { - SmtpLogger.info('Creating secure TLS server'); - // Log certificate info - SmtpLogger.debug('Using certificates for secure server', { - keyLength: this.certificates.key.length, - certLength: this.certificates.cert.length, - caLength: this.certificates.ca ? this.certificates.ca.length : 0 - }); - // Create TLS options using our certificate utilities - // This ensures proper PEM format handling and protocol negotiation - const tlsOptions = createTlsOptions(this.certificates, true); // Use server options - SmtpLogger.info('Creating TLS server with options', { - minVersion: tlsOptions.minVersion, - maxVersion: tlsOptions.maxVersion, - handshakeTimeout: tlsOptions.handshakeTimeout - }); - // Create a server with wider TLS compatibility - const server = new plugins.tls.Server(tlsOptions); - // Add error handling - server.on('error', (err) => { - SmtpLogger.error(`TLS server error: ${err.message}`, { - error: err, - stack: err.stack - }); - }); - // Log TLS details for each connection - server.on('secureConnection', (socket) => { - SmtpLogger.info('New secure connection established', { - protocol: socket.getProtocol(), - cipher: socket.getCipher()?.name, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort - }); - }); - return server; - } - catch (error) { - SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - return undefined; - } - } - /** - * Check if TLS is enabled - * @returns Whether TLS is enabled - */ - isTlsEnabled() { - const options = this.smtpServer.getOptions(); - return !!(options.key && options.cert); - } - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response message - */ - sendResponse(socket, response) { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - try { - socket.write(`${response}\r\n`); - SmtpLogger.logResponse(response, socket); - } - catch (error) { - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - socket.destroy(); - } - } - /** - * Check if TLS is available (interface requirement) - */ - isTlsAvailable() { - return this.isTlsEnabled(); - } - /** - * Get TLS options (interface requirement) - */ - getTlsOptions() { - return this.options; - } - /** - * Clean up resources - */ - destroy() { - // Clear any cached certificates or TLS contexts - // TlsHandler doesn't have timers but may have cached resources - SmtpLogger.debug('TlsHandler destroyed'); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGxzLWhhbmRsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBzZXJ2ZXIvdGxzLWhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUUvQyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUN2RixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDaEQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGFBQWEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3JFLE9BQU8sRUFDTCwwQkFBMEIsRUFDMUIsOEJBQThCLEVBQzlCLGdCQUFnQixFQUVqQixNQUFNLHdCQUF3QixDQUFDO0FBQ2hDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUU3Qzs7R0FFRztBQUNILE1BQU0sT0FBTyxVQUFVO0lBQ3JCOztPQUVHO0lBQ0ssVUFBVSxDQUFjO0lBRWhDOztPQUVHO0lBQ0ssWUFBWSxDQUFtQjtJQUV2Qzs7T0FFRztJQUNLLE9BQU8sQ0FBeUI7SUFFeEM7OztPQUdHO0lBQ0gsWUFBWSxVQUF1QjtRQUNqQyxJQUFJLENBQUMsVUFBVSxHQUFHLFVBQVUsQ0FBQztRQUU3QiwwQkFBMEI7UUFDMUIsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUNuRCxJQUFJLENBQUM7WUFDSCxpREFBaUQ7WUFDakQsSUFBSSxDQUFDLFlBQVksR0FBRywwQkFBMEIsQ0FBQztnQkFDN0MsR0FBRyxFQUFFLGFBQWEsQ0FBQyxHQUFHO2dCQUN0QixJQUFJLEVBQUUsYUFBYSxDQUFDLElBQUk7Z0JBQ3hCLEVBQUUsRUFBRSxhQUFhLENBQUMsRUFBRTthQUNyQixDQUFDLENB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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.d.ts b/dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.d.ts deleted file mode 100644 index e773035..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.d.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Adaptive SMTP Logging System - * Automatically switches between logging modes based on server load (active connections) - * to maintain performance during high-concurrency scenarios - */ -import * as plugins from '../../../../plugins.js'; -import { SecurityLogLevel, SecurityEventType } from '../constants.js'; -import type { ISmtpSession } from '../interfaces.js'; -import type { LogLevel, ISmtpLogOptions } from './logging.js'; -/** - * Log modes based on server load - */ -export declare enum LogMode { - VERBOSE = "VERBOSE",// < 20 connections: Full detailed logging - REDUCED = "REDUCED",// 20-40 connections: Limited command/response logging, full error logging - MINIMAL = "MINIMAL" -} -/** - * Configuration for adaptive logging thresholds - */ -export interface IAdaptiveLogConfig { - verboseThreshold: number; - reducedThreshold: number; - aggregationInterval: number; - maxAggregatedEntries: number; -} -/** - * Connection metadata for aggregation tracking - */ -interface IConnectionTracker { - activeConnections: number; - peakConnections: number; - totalConnections: number; - connectionsPerSecond: number; - lastConnectionTime: number; -} -/** - * Adaptive SMTP Logger that scales logging based on server load - */ -export declare class AdaptiveSmtpLogger { - private static instance; - private currentMode; - private config; - private aggregatedEntries; - private aggregationTimer; - private connectionTracker; - private constructor(); - /** - * Get singleton instance - */ - static getInstance(config?: Partial): AdaptiveSmtpLogger; - /** - * Update active connection count and adjust log mode if needed - */ - updateConnectionCount(activeConnections: number): void; - /** - * Track new connection for rate calculation - */ - trackConnection(): void; - /** - * Get current logging mode - */ - getCurrentMode(): LogMode; - /** - * Get connection statistics - */ - getConnectionStats(): IConnectionTracker; - /** - * Log a message with adaptive behavior - */ - log(level: LogLevel, message: string, options?: ISmtpLogOptions): void; - /** - * Log command with adaptive behavior - */ - logCommand(command: string, socket: plugins.net.Socket | plugins.tls.TLSSocket, session?: ISmtpSession): void; - /** - * Log response with adaptive behavior - */ - logResponse(response: string, socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Log connection event with adaptive behavior - */ - logConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, eventType: 'connect' | 'close' | 'error', session?: ISmtpSession, error?: Error): void; - /** - * Log security event (always logged regardless of mode) - */ - logSecurityEvent(level: SecurityLogLevel, type: SecurityEventType, message: string, details: Record, ipAddress?: string, domain?: string, success?: boolean): void; - /** - * Determine appropriate log mode based on connection count - */ - private determineLogMode; - /** - * Switch to a new log mode - */ - private switchLogMode; - /** - * Add entry to aggregation buffer - */ - private aggregateEntry; - /** - * Start the aggregation timer - */ - private startAggregationTimer; - /** - * Flush aggregated entries to logs - */ - private flushAggregatedEntries; - /** - * Cleanup resources - */ - destroy(): void; -} -/** - * Default instance for easy access - */ -export declare const adaptiveLogger: AdaptiveSmtpLogger; -export {}; diff --git a/dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.js b/dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.js deleted file mode 100644 index 90077df..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/adaptive-logging.js +++ /dev/null @@ -1,413 +0,0 @@ -/** - * Adaptive SMTP Logging System - * Automatically switches between logging modes based on server load (active connections) - * to maintain performance during high-concurrency scenarios - */ -import * as plugins from '../../../../plugins.js'; -import { logger } from '../../../../logger.js'; -import { SecurityLogLevel, SecurityEventType } from '../constants.js'; -/** - * Log modes based on server load - */ -export var LogMode; -(function (LogMode) { - LogMode["VERBOSE"] = "VERBOSE"; - LogMode["REDUCED"] = "REDUCED"; - LogMode["MINIMAL"] = "MINIMAL"; // 40+ connections: Aggregated logging only, critical errors only -})(LogMode || (LogMode = {})); -/** - * Adaptive SMTP Logger that scales logging based on server load - */ -export class AdaptiveSmtpLogger { - static instance; - currentMode = LogMode.VERBOSE; - config; - aggregatedEntries = new Map(); - aggregationTimer = null; - connectionTracker = { - activeConnections: 0, - peakConnections: 0, - totalConnections: 0, - connectionsPerSecond: 0, - lastConnectionTime: Date.now() - }; - constructor(config) { - this.config = { - verboseThreshold: 20, - reducedThreshold: 40, - aggregationInterval: 30000, // 30 seconds - maxAggregatedEntries: 100, - ...config - }; - this.startAggregationTimer(); - } - /** - * Get singleton instance - */ - static getInstance(config) { - if (!AdaptiveSmtpLogger.instance) { - AdaptiveSmtpLogger.instance = new AdaptiveSmtpLogger(config); - } - return AdaptiveSmtpLogger.instance; - } - /** - * Update active connection count and adjust log mode if needed - */ - updateConnectionCount(activeConnections) { - this.connectionTracker.activeConnections = activeConnections; - this.connectionTracker.peakConnections = Math.max(this.connectionTracker.peakConnections, activeConnections); - const newMode = this.determineLogMode(activeConnections); - if (newMode !== this.currentMode) { - this.switchLogMode(newMode); - } - } - /** - * Track new connection for rate calculation - */ - trackConnection() { - this.connectionTracker.totalConnections++; - const now = Date.now(); - const timeDiff = (now - this.connectionTracker.lastConnectionTime) / 1000; - if (timeDiff > 0) { - this.connectionTracker.connectionsPerSecond = 1 / timeDiff; - } - this.connectionTracker.lastConnectionTime = now; - } - /** - * Get current logging mode - */ - getCurrentMode() { - return this.currentMode; - } - /** - * Get connection statistics - */ - getConnectionStats() { - return { ...this.connectionTracker }; - } - /** - * Log a message with adaptive behavior - */ - log(level, message, options = {}) { - // Always log structured data - const errorInfo = options.error ? { - errorMessage: options.error.message, - errorStack: options.error.stack, - errorName: options.error.name - } : {}; - const logData = { - component: 'smtp-server', - logMode: this.currentMode, - activeConnections: this.connectionTracker.activeConnections, - ...options, - ...errorInfo - }; - if (logData.error) { - delete logData.error; - } - logger.log(level, message, logData); - // Adaptive console logging based on mode - switch (this.currentMode) { - case LogMode.VERBOSE: - // Full console logging - if (level === 'error' || level === 'warn') { - console[level](`[SMTP] ${message}`, logData); - } - break; - case LogMode.REDUCED: - // Only errors and warnings to console - if (level === 'error' || level === 'warn') { - console[level](`[SMTP] ${message}`, logData); - } - break; - case LogMode.MINIMAL: - // Only critical errors to console - if (level === 'error' && (message.includes('critical') || message.includes('security') || message.includes('crash'))) { - console[level](`[SMTP] ${message}`, logData); - } - break; - } - } - /** - * Log command with adaptive behavior - */ - logCommand(command, socket, session) { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - switch (this.currentMode) { - case LogMode.VERBOSE: - this.log('info', `Command received: ${command}`, { - ...clientInfo, - command: command.split(' ')[0]?.toUpperCase() - }); - console.log(`← ${command}`); - break; - case LogMode.REDUCED: - // Aggregate commands instead of logging each one - this.aggregateEntry('command', 'info', `Command: ${command.split(' ')[0]?.toUpperCase()}`, clientInfo); - // Only show error commands - if (command.toUpperCase().startsWith('QUIT') || command.includes('error')) { - console.log(`← ${command}`); - } - break; - case LogMode.MINIMAL: - // Only aggregate, no console output unless it's an error command - this.aggregateEntry('command', 'info', `Command: ${command.split(' ')[0]?.toUpperCase()}`, clientInfo); - break; - } - } - /** - * Log response with adaptive behavior - */ - logResponse(response, socket) { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket - }; - const responseCode = response.substring(0, 3); - const isError = responseCode.startsWith('4') || responseCode.startsWith('5'); - switch (this.currentMode) { - case LogMode.VERBOSE: - if (responseCode.startsWith('2') || responseCode.startsWith('3')) { - this.log('debug', `Response sent: ${response}`, clientInfo); - } - else if (responseCode.startsWith('4')) { - this.log('warn', `Temporary error response: ${response}`, clientInfo); - } - else if (responseCode.startsWith('5')) { - this.log('error', `Permanent error response: ${response}`, clientInfo); - } - console.log(`→ ${response}`); - break; - case LogMode.REDUCED: - // Log errors normally, aggregate success responses - if (isError) { - if (responseCode.startsWith('4')) { - this.log('warn', `Temporary error response: ${response}`, clientInfo); - } - else { - this.log('error', `Permanent error response: ${response}`, clientInfo); - } - console.log(`→ ${response}`); - } - else { - this.aggregateEntry('response', 'debug', `Response: ${responseCode}xx`, clientInfo); - } - break; - case LogMode.MINIMAL: - // Only log critical errors - if (responseCode.startsWith('5')) { - this.log('error', `Permanent error response: ${response}`, clientInfo); - console.log(`→ ${response}`); - } - else { - this.aggregateEntry('response', 'debug', `Response: ${responseCode}xx`, clientInfo); - } - break; - } - } - /** - * Log connection event with adaptive behavior - */ - logConnection(socket, eventType, session, error) { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - if (eventType === 'connect') { - this.trackConnection(); - } - switch (this.currentMode) { - case LogMode.VERBOSE: - // Full connection logging - switch (eventType) { - case 'connect': - this.log('info', `New ${clientInfo.secure ? 'secure ' : ''}connection from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - case 'close': - this.log('info', `Connection closed from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - case 'error': - this.log('error', `Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - break; - } - break; - case LogMode.REDUCED: - // Aggregate normal connections, log errors - if (eventType === 'error') { - this.log('error', `Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - } - else { - this.aggregateEntry('connection', 'info', `Connection ${eventType}`, clientInfo); - } - break; - case LogMode.MINIMAL: - // Only aggregate, except for critical errors - if (eventType === 'error' && error && (error.message.includes('security') || error.message.includes('critical'))) { - this.log('error', `Critical connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - } - else { - this.aggregateEntry('connection', 'info', `Connection ${eventType}`, clientInfo); - } - break; - } - } - /** - * Log security event (always logged regardless of mode) - */ - logSecurityEvent(level, type, message, details, ipAddress, domain, success) { - const logLevel = level === SecurityLogLevel.DEBUG ? 'debug' : - level === SecurityLogLevel.INFO ? 'info' : - level === SecurityLogLevel.WARN ? 'warn' : 'error'; - // Security events are always logged in full detail - this.log(logLevel, message, { - component: 'smtp-security', - eventType: type, - success, - ipAddress, - domain, - ...details - }); - } - /** - * Determine appropriate log mode based on connection count - */ - determineLogMode(activeConnections) { - if (activeConnections >= this.config.reducedThreshold) { - return LogMode.MINIMAL; - } - else if (activeConnections >= this.config.verboseThreshold) { - return LogMode.REDUCED; - } - else { - return LogMode.VERBOSE; - } - } - /** - * Switch to a new log mode - */ - switchLogMode(newMode) { - const oldMode = this.currentMode; - this.currentMode = newMode; - // Log the mode switch - console.log(`[SMTP] Adaptive logging switched from ${oldMode} to ${newMode} (${this.connectionTracker.activeConnections} active connections)`); - this.log('info', `Adaptive logging mode changed to ${newMode}`, { - oldMode, - newMode, - activeConnections: this.connectionTracker.activeConnections, - peakConnections: this.connectionTracker.peakConnections, - totalConnections: this.connectionTracker.totalConnections - }); - // If switching to more verbose mode, flush aggregated entries - if ((oldMode === LogMode.MINIMAL && newMode !== LogMode.MINIMAL) || - (oldMode === LogMode.REDUCED && newMode === LogMode.VERBOSE)) { - this.flushAggregatedEntries(); - } - } - /** - * Add entry to aggregation buffer - */ - aggregateEntry(type, level, message, options) { - const key = `${type}:${message}`; - const now = Date.now(); - if (this.aggregatedEntries.has(key)) { - const entry = this.aggregatedEntries.get(key); - entry.count++; - entry.lastSeen = now; - } - else { - this.aggregatedEntries.set(key, { - type, - count: 1, - firstSeen: now, - lastSeen: now, - sample: { message, level, options } - }); - } - // Force flush if we have too many entries - if (this.aggregatedEntries.size >= this.config.maxAggregatedEntries) { - this.flushAggregatedEntries(); - } - } - /** - * Start the aggregation timer - */ - startAggregationTimer() { - if (this.aggregationTimer) { - clearInterval(this.aggregationTimer); - } - this.aggregationTimer = setInterval(() => { - this.flushAggregatedEntries(); - }, this.config.aggregationInterval); - // Unref the timer so it doesn't keep the process alive - if (this.aggregationTimer && typeof this.aggregationTimer.unref === 'function') { - this.aggregationTimer.unref(); - } - } - /** - * Flush aggregated entries to logs - */ - flushAggregatedEntries() { - if (this.aggregatedEntries.size === 0) { - return; - } - const summary = {}; - let totalAggregated = 0; - for (const [key, entry] of this.aggregatedEntries.entries()) { - summary[entry.type] = (summary[entry.type] || 0) + entry.count; - totalAggregated += entry.count; - // Log a sample of high-frequency entries - if (entry.count >= 10) { - this.log(entry.sample.level, `${entry.sample.message} (aggregated: ${entry.count} occurrences)`, { - ...entry.sample.options, - aggregated: true, - occurrences: entry.count, - timeSpan: entry.lastSeen - entry.firstSeen - }); - } - } - // Log aggregation summary - console.log(`[SMTP] Aggregated ${totalAggregated} log entries: ${JSON.stringify(summary)}`); - this.log('info', 'Aggregated log summary', { - totalEntries: totalAggregated, - breakdown: summary, - logMode: this.currentMode, - activeConnections: this.connectionTracker.activeConnections - }); - // Clear aggregated entries - this.aggregatedEntries.clear(); - } - /** - * Cleanup resources - */ - destroy() { - if (this.aggregationTimer) { - clearInterval(this.aggregationTimer); - this.aggregationTimer = null; - } - this.flushAggregatedEntries(); - } -} -/** - * Default instance for easy access - */ -export const adaptiveLogger = AdaptiveSmtpLogger.getInstance(); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWRhcHRpdmUtbG9nZ2luZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci91dGlscy9hZGFwdGl2ZS1sb2dnaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7O0dBSUc7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHdCQUF3QixDQUFDO0FBQ2xELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUMvQyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUl0RTs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLE9BSVg7QUFKRCxXQUFZLE9BQU87SUFDakIsOEJBQW1CLENBQUE7SUFDbkIsOEJBQW1CLENBQUE7SUFDbkIsOEJBQW1CLENBQUEsQ0FBTSxpRUFBaUU7QUFDNUYsQ0FBQyxFQUpXLE9BQU8sS0FBUCxPQUFPLFFBSWxCO0FBc0NEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGtCQUFrQjtJQUNyQixNQUFNLENBQUMsUUFBUSxDQUFxQjtJQUNwQyxXQUFXLEdBQVksT0FBTyxDQUFDLE9BQU8sQ0FBQztJQUN2QyxNQUFNLENBQXFCO0lBQzNCLGlCQUFpQixHQUFxQyxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBQ2hFLGdCQUFnQixHQUEwQixJQUFJLENBQUM7SUFDL0MsaUJBQWlCLEdBQXVCO1FBQzlDLGlCQUFpQixFQUFFLENBQUM7UUFDcEIsZUFBZSxFQUFFLENBQUM7UUFDbEIsZ0JBQWdCLEVBQUUsQ0FBQztRQUNuQixvQkFBb0IsRUFBRSxDQUFDO1FBQ3ZCLGtCQUFrQixFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUU7S0FDL0IsQ0FBQztJQUVGLFlBQW9CLE1BQW9DO1FBQ3RELElBQUksQ0FBQyxNQUFNLEdBQUc7WUFDWixnQkFBZ0IsRUFBRSxFQUFFO1lBQ3BCLGdCQUFnQixFQUFFLEVBQUU7WUFD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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/utils/helpers.d.ts b/dist_ts/mail/delivery/smtpserver/utils/helpers.d.ts deleted file mode 100644 index ec21f65..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/helpers.d.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * SMTP Helper Functions - * Provides utility functions for SMTP server implementation - */ -import * as plugins from '../../../../plugins.js'; -import type { ISmtpServerOptions } from '../interfaces.js'; -/** - * Formats a multi-line SMTP response according to RFC 5321 - * @param code - Response code - * @param lines - Response lines - * @returns Formatted SMTP response - */ -export declare function formatMultilineResponse(code: number, lines: string[]): string; -/** - * Generates a unique session ID - * @returns Unique session ID - */ -export declare function generateSessionId(): string; -/** - * Safely parses an integer from string with a default value - * @param value - String value to parse - * @param defaultValue - Default value if parsing fails - * @returns Parsed integer or default value - */ -export declare function safeParseInt(value: string | undefined, defaultValue: number): number; -/** - * Safely gets the socket details - * @param socket - Socket to get details from - * @returns Socket details object - */ -export declare function getSocketDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): { - remoteAddress: string; - remotePort: number; - remoteFamily: string; - localAddress: string; - localPort: number; - encrypted: boolean; -}; -/** - * Gets TLS details if socket is TLS - * @param socket - Socket to get TLS details from - * @returns TLS details or undefined if not TLS - */ -export declare function getTlsDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): { - protocol?: string; - cipher?: string; - authorized?: boolean; -} | undefined; -/** - * Merges default options with provided options - * @param options - User provided options - * @returns Merged options with defaults - */ -export declare function mergeWithDefaults(options: Partial): ISmtpServerOptions; -/** - * Creates a text response formatter for the SMTP server - * @param socket - Socket to send responses to - * @returns Function to send formatted response - */ -export declare function createResponseFormatter(socket: plugins.net.Socket | plugins.tls.TLSSocket): (response: string) => void; -/** - * Extracts SMTP command name from a command line - * @param commandLine - Full command line - * @returns Command name in uppercase - */ -export declare function extractCommandName(commandLine: string): string; -/** - * Extracts SMTP command arguments from a command line - * @param commandLine - Full command line - * @returns Arguments string - */ -export declare function extractCommandArgs(commandLine: string): string; -/** - * Sanitizes data for logging (hides sensitive info) - * @param data - Data to sanitize - * @returns Sanitized data - */ -export declare function sanitizeForLogging(data: any): any; diff --git a/dist_ts/mail/delivery/smtpserver/utils/helpers.js b/dist_ts/mail/delivery/smtpserver/utils/helpers.js deleted file mode 100644 index 973fe28..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/helpers.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * SMTP Helper Functions - * Provides utility functions for SMTP server implementation - */ -import * as plugins from '../../../../plugins.js'; -import { SMTP_DEFAULTS } from '../constants.js'; -/** - * Formats a multi-line SMTP response according to RFC 5321 - * @param code - Response code - * @param lines - Response lines - * @returns Formatted SMTP response - */ -export function formatMultilineResponse(code, lines) { - if (!lines || lines.length === 0) { - return `${code} `; - } - if (lines.length === 1) { - return `${code} ${lines[0]}`; - } - let response = ''; - for (let i = 0; i < lines.length - 1; i++) { - response += `${code}-${lines[i]}${SMTP_DEFAULTS.CRLF}`; - } - response += `${code} ${lines[lines.length - 1]}`; - return response; -} -/** - * Generates a unique session ID - * @returns Unique session ID - */ -export function generateSessionId() { - return `${Date.now()}-${Math.floor(Math.random() * 10000)}`; -} -/** - * Safely parses an integer from string with a default value - * @param value - String value to parse - * @param defaultValue - Default value if parsing fails - * @returns Parsed integer or default value - */ -export function safeParseInt(value, defaultValue) { - if (!value) { - return defaultValue; - } - const parsed = parseInt(value, 10); - return isNaN(parsed) ? defaultValue : parsed; -} -/** - * Safely gets the socket details - * @param socket - Socket to get details from - * @returns Socket details object - */ -export function getSocketDetails(socket) { - return { - remoteAddress: socket.remoteAddress || 'unknown', - remotePort: socket.remotePort || 0, - remoteFamily: socket.remoteFamily || 'unknown', - localAddress: socket.localAddress || 'unknown', - localPort: socket.localPort || 0, - encrypted: socket instanceof plugins.tls.TLSSocket - }; -} -/** - * Gets TLS details if socket is TLS - * @param socket - Socket to get TLS details from - * @returns TLS details or undefined if not TLS - */ -export function getTlsDetails(socket) { - if (!(socket instanceof plugins.tls.TLSSocket)) { - return undefined; - } - return { - protocol: socket.getProtocol(), - cipher: socket.getCipher()?.name, - authorized: socket.authorized - }; -} -/** - * Merges default options with provided options - * @param options - User provided options - * @returns Merged options with defaults - */ -export function mergeWithDefaults(options) { - return { - port: options.port || SMTP_DEFAULTS.SMTP_PORT, - key: options.key || '', - cert: options.cert || '', - hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME, - host: options.host, - securePort: options.securePort, - ca: options.ca, - maxSize: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE, - maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, - socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, - connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT, - cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL, - maxRecipients: options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS, - size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE, - dataTimeout: options.dataTimeout || SMTP_DEFAULTS.DATA_TIMEOUT, - auth: options.auth, - }; -} -/** - * Creates a text response formatter for the SMTP server - * @param socket - Socket to send responses to - * @returns Function to send formatted response - */ -export function createResponseFormatter(socket) { - return (response) => { - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - console.log(`→ ${response}`); - } - catch (error) { - console.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`); - socket.destroy(); - } - }; -} -/** - * Extracts SMTP command name from a command line - * @param commandLine - Full command line - * @returns Command name in uppercase - */ -export function extractCommandName(commandLine) { - if (!commandLine || typeof commandLine !== 'string') { - return ''; - } - // Handle specific command patterns first - const ehloMatch = commandLine.match(/^(EHLO|HELO)\b/i); - if (ehloMatch) { - return ehloMatch[1].toUpperCase(); - } - const mailMatch = commandLine.match(/^MAIL\b/i); - if (mailMatch) { - return 'MAIL'; - } - const rcptMatch = commandLine.match(/^RCPT\b/i); - if (rcptMatch) { - return 'RCPT'; - } - // Default handling - const parts = commandLine.trim().split(/\s+/); - return (parts[0] || '').toUpperCase(); -} -/** - * Extracts SMTP command arguments from a command line - * @param commandLine - Full command line - * @returns Arguments string - */ -export function extractCommandArgs(commandLine) { - if (!commandLine || typeof commandLine !== 'string') { - return ''; - } - const command = extractCommandName(commandLine); - if (!command) { - return commandLine.trim(); - } - // Special handling for specific commands - if (command === 'EHLO' || command === 'HELO') { - const match = commandLine.match(/^(?:EHLO|HELO)\s+(.+)$/i); - return match ? match[1].trim() : ''; - } - if (command === 'MAIL') { - return commandLine.replace(/^MAIL\s+/i, ''); - } - if (command === 'RCPT') { - return commandLine.replace(/^RCPT\s+/i, ''); - } - // Default extraction - const firstSpace = commandLine.indexOf(' '); - if (firstSpace === -1) { - return ''; - } - return commandLine.substring(firstSpace + 1).trim(); -} -/** - * Sanitizes data for logging (hides sensitive info) - * @param data - Data to sanitize - * @returns Sanitized data - */ -export function sanitizeForLogging(data) { - if (!data) { - return data; - } - if (typeof data !== 'object') { - return data; - } - const result = Array.isArray(data) ? [] : {}; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - // Sanitize sensitive fields - if (key.toLowerCase().includes('password') || - key.toLowerCase().includes('token') || - key.toLowerCase().includes('secret') || - key.toLowerCase().includes('credential')) { - result[key] = '********'; - } - else if (typeof data[key] === 'object' && data[key] !== null) { - result[key] = sanitizeForLogging(data[key]); - } - else { - result[key] = data[key]; - } - } - } - return result; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVscGVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci91dGlscy9oZWxwZXJzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sS0FBSyxPQUFPLE1BQU0sd0JBQXdCLENBQUM7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBR2hEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLHVCQUF1QixDQUFDLElBQVksRUFBRSxLQUFlO0lBQ25FLElBQUksQ0FBQyxLQUFLLElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNqQyxPQUFPLEdBQUcsSUFBSSxHQUFHLENBQUM7SUFDcEIsQ0FBQztJQUVELElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN2QixPQUFPLEdBQUcsSUFBSSxJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQy9CLENBQUM7SUFFRCxJQUFJLFFBQVEsR0FBRyxFQUFFLENBQUM7SUFDbEIsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDMUMsUUFBUSxJQUFJLEdBQUcsSUFBSSxJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxhQUFhLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDekQsQ0FBQztJQUNELFFBQVEsSUFBSSxHQUFHLElBQUksSUFBSSxLQUFLLENBQUMsS0FBSyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBRWpELE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsaUJBQWlCO0lBQy9CLE9BQU8sR0FBRyxJQUFJLENBQUMs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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/utils/logging.d.ts b/dist_ts/mail/delivery/smtpserver/utils/logging.d.ts deleted file mode 100644 index 5f06cfb..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/logging.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * SMTP Logging Utilities - * Provides structured logging for SMTP server components - */ -import * as plugins from '../../../../plugins.js'; -import { SecurityLogLevel, SecurityEventType } from '../constants.js'; -import type { ISmtpSession } from '../interfaces.js'; -/** - * SMTP connection metadata to include in logs - */ -export interface IConnectionMetadata { - remoteAddress?: string; - remotePort?: number; - socketId?: string; - secure?: boolean; - sessionId?: string; -} -/** - * Log levels for SMTP server - */ -export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; -/** - * Options for SMTP log - */ -export interface ISmtpLogOptions { - level?: LogLevel; - sessionId?: string; - sessionState?: string; - remoteAddress?: string; - remotePort?: number; - command?: string; - error?: Error; - [key: string]: any; -} -/** - * SMTP logger - provides structured logging for SMTP server - */ -export declare class SmtpLogger { - /** - * Log a message with context - * @param level - Log level - * @param message - Log message - * @param options - Additional log options - */ - static log(level: LogLevel, message: string, options?: ISmtpLogOptions): void; - /** - * Log debug level message - * @param message - Log message - * @param options - Additional log options - */ - static debug(message: string, options?: ISmtpLogOptions): void; - /** - * Log info level message - * @param message - Log message - * @param options - Additional log options - */ - static info(message: string, options?: ISmtpLogOptions): void; - /** - * Log warning level message - * @param message - Log message - * @param options - Additional log options - */ - static warn(message: string, options?: ISmtpLogOptions): void; - /** - * Log error level message - * @param message - Log message - * @param options - Additional log options - */ - static error(message: string, options?: ISmtpLogOptions): void; - /** - * Log command received from client - * @param command - The command string - * @param socket - The client socket - * @param session - The SMTP session - */ - static logCommand(command: string, socket: plugins.net.Socket | plugins.tls.TLSSocket, session?: ISmtpSession): void; - /** - * Log response sent to client - * @param response - The response string - * @param socket - The client socket - */ - static logResponse(response: string, socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - /** - * Log client connection event - * @param socket - The client socket - * @param eventType - Type of connection event (connect, close, error) - * @param session - The SMTP session - * @param error - Optional error object for error events - */ - static logConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, eventType: 'connect' | 'close' | 'error', session?: ISmtpSession, error?: Error): void; - /** - * Log security event - * @param level - Security log level - * @param type - Security event type - * @param message - Log message - * @param details - Event details - * @param ipAddress - Client IP address - * @param domain - Optional domain involved - * @param success - Whether the security check was successful - */ - static logSecurityEvent(level: SecurityLogLevel, type: SecurityEventType, message: string, details: Record, ipAddress?: string, domain?: string, success?: boolean): void; -} -/** - * Default instance for backward compatibility - */ -export declare const smtpLogger: typeof SmtpLogger; diff --git a/dist_ts/mail/delivery/smtpserver/utils/logging.js b/dist_ts/mail/delivery/smtpserver/utils/logging.js deleted file mode 100644 index b12d97b..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/logging.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * SMTP Logging Utilities - * Provides structured logging for SMTP server components - */ -import * as plugins from '../../../../plugins.js'; -import { logger } from '../../../../logger.js'; -import { SecurityLogLevel, SecurityEventType } from '../constants.js'; -/** - * SMTP logger - provides structured logging for SMTP server - */ -export class SmtpLogger { - /** - * Log a message with context - * @param level - Log level - * @param message - Log message - * @param options - Additional log options - */ - static log(level, message, options = {}) { - // Extract error information if provided - const errorInfo = options.error ? { - errorMessage: options.error.message, - errorStack: options.error.stack, - errorName: options.error.name - } : {}; - // Structure log data - const logData = { - component: 'smtp-server', - ...options, - ...errorInfo - }; - // Remove error from log data to avoid duplication - if (logData.error) { - delete logData.error; - } - // Log through the main logger - logger.log(level, message, logData); - // Also console log for immediate visibility during development - if (level === 'error' || level === 'warn') { - console[level](`[SMTP] ${message}`, logData); - } - } - /** - * Log debug level message - * @param message - Log message - * @param options - Additional log options - */ - static debug(message, options = {}) { - this.log('debug', message, options); - } - /** - * Log info level message - * @param message - Log message - * @param options - Additional log options - */ - static info(message, options = {}) { - this.log('info', message, options); - } - /** - * Log warning level message - * @param message - Log message - * @param options - Additional log options - */ - static warn(message, options = {}) { - this.log('warn', message, options); - } - /** - * Log error level message - * @param message - Log message - * @param options - Additional log options - */ - static error(message, options = {}) { - this.log('error', message, options); - } - /** - * Log command received from client - * @param command - The command string - * @param socket - The client socket - * @param session - The SMTP session - */ - static logCommand(command, socket, session) { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - this.info(`Command received: ${command}`, { - ...clientInfo, - command: command.split(' ')[0]?.toUpperCase() - }); - // Also log to console for easy debugging - console.log(`← ${command}`); - } - /** - * Log response sent to client - * @param response - The response string - * @param socket - The client socket - */ - static logResponse(response, socket) { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket - }; - // Get the response code from the beginning of the response - const responseCode = response.substring(0, 3); - // Log different levels based on response code - if (responseCode.startsWith('2') || responseCode.startsWith('3')) { - this.debug(`Response sent: ${response}`, clientInfo); - } - else if (responseCode.startsWith('4')) { - this.warn(`Temporary error response: ${response}`, clientInfo); - } - else if (responseCode.startsWith('5')) { - this.error(`Permanent error response: ${response}`, clientInfo); - } - // Also log to console for easy debugging - console.log(`→ ${response}`); - } - /** - * Log client connection event - * @param socket - The client socket - * @param eventType - Type of connection event (connect, close, error) - * @param session - The SMTP session - * @param error - Optional error object for error events - */ - static logConnection(socket, eventType, session, error) { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - switch (eventType) { - case 'connect': - this.info(`New ${clientInfo.secure ? 'secure ' : ''}connection from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - case 'close': - this.info(`Connection closed from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - case 'error': - this.error(`Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - break; - } - } - /** - * Log security event - * @param level - Security log level - * @param type - Security event type - * @param message - Log message - * @param details - Event details - * @param ipAddress - Client IP address - * @param domain - Optional domain involved - * @param success - Whether the security check was successful - */ - static logSecurityEvent(level, type, message, details, ipAddress, domain, success) { - // Map security log level to system log level - const logLevel = level === SecurityLogLevel.DEBUG ? 'debug' : - level === SecurityLogLevel.INFO ? 'info' : - level === SecurityLogLevel.WARN ? 'warn' : 'error'; - // Log the security event - this.log(logLevel, message, { - component: 'smtp-security', - eventType: type, - success, - ipAddress, - domain, - ...details - }); - } -} -/** - * Default instance for backward compatibility - */ -export const smtpLogger = SmtpLogger; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9nZ2luZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci91dGlscy9sb2dnaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sS0FBSyxPQUFPLE1BQU0sd0JBQXdCLENBQUM7QUFDbEQsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQy9DLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBaUN0RTs7R0FFRztBQUNILE1BQU0sT0FBTyxVQUFVO0lBQ3JCOzs7OztPQUtHO0lBQ0ksTUFBTSxDQUFDLEdBQUcsQ0FBQyxLQUFlLEVBQUUsT0FBZSxFQUFFLFVBQTJCLEVBQUU7UUFDL0Usd0NBQXdDO1FBQ3hDLE1BQU0sU0FBUyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO1lBQ2hDLFlBQVksRUFBRSxPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU87WUFDbkMsVUFBVSxFQUFFLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSztZQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEtBQUssQ0FBQyxJQUFJO1NBQzlCLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUVQLHFCQUFxQjtRQUNyQixNQUFNLE9BQU8sR0FBRztZQUNkLFNBQVMsRUFBRSxhQUFhO1lBQ3hCLEdBQUcsT0FBTztZQUNWLEdBQUcsU0FBUztTQUNiLENBQUM7UUFFRixrREFBa0Q7UUFDbEQsSUFBSSxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDbEIsT0FBTyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQ3ZCLENBQUM7UUFFRCw4QkFBOEI7UUFDOUIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBRXBDLCtEQUErRDtRQUMvRCxJQUFJLEtB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 \ No newline at end of file diff --git a/dist_ts/mail/delivery/smtpserver/utils/validation.d.ts b/dist_ts/mail/delivery/smtpserver/utils/validation.d.ts deleted file mode 100644 index 9f74002..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/validation.d.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * SMTP Validation Utilities - * Provides validation functions for SMTP server - */ -import { SmtpState } from '../interfaces.js'; -/** - * Detects header injection attempts in input strings - * @param input - The input string to check - * @param context - The context where this input is being used ('smtp-command' or 'email-header') - * @returns true if header injection is detected, false otherwise - */ -export declare function detectHeaderInjection(input: string, context?: 'smtp-command' | 'email-header'): boolean; -/** - * Sanitizes input by removing or escaping potentially dangerous characters - * @param input - The input string to sanitize - * @returns Sanitized string - */ -export declare function sanitizeInput(input: string): string; -/** - * Validates an email address - * @param email - Email address to validate - * @returns Whether the email address is valid - */ -export declare function isValidEmail(email: string): boolean; -/** - * Validates the MAIL FROM command syntax - * @param args - Arguments string from the MAIL FROM command - * @returns Object with validation result and extracted data - */ -export declare function validateMailFrom(args: string): { - isValid: boolean; - address?: string; - params?: Record; - errorMessage?: string; -}; -/** - * Validates the RCPT TO command syntax - * @param args - Arguments string from the RCPT TO command - * @returns Object with validation result and extracted data - */ -export declare function validateRcptTo(args: string): { - isValid: boolean; - address?: string; - params?: Record; - errorMessage?: string; -}; -/** - * Validates the EHLO command syntax - * @param args - Arguments string from the EHLO command - * @returns Object with validation result and extracted data - */ -export declare function validateEhlo(args: string): { - isValid: boolean; - hostname?: string; - errorMessage?: string; -}; -/** - * Validates command in the current SMTP state - * @param command - SMTP command - * @param currentState - Current SMTP state - * @returns Whether the command is valid in the current state - */ -export declare function isValidCommandSequence(command: string, currentState: SmtpState): boolean; -/** - * Validates if a hostname is valid according to RFC 5321 - * @param hostname - Hostname to validate - * @returns Whether the hostname is valid - */ -export declare function isValidHostname(hostname: string): boolean; diff --git a/dist_ts/mail/delivery/smtpserver/utils/validation.js b/dist_ts/mail/delivery/smtpserver/utils/validation.js deleted file mode 100644 index e9ab70e..0000000 --- a/dist_ts/mail/delivery/smtpserver/utils/validation.js +++ /dev/null @@ -1,360 +0,0 @@ -/** - * SMTP Validation Utilities - * Provides validation functions for SMTP server - */ -import { SmtpState } from '../interfaces.js'; -import { SMTP_PATTERNS } from '../constants.js'; -/** - * Header injection patterns to detect malicious input - * These patterns detect common header injection attempts - */ -const HEADER_INJECTION_PATTERNS = [ - /\r\n/, // CRLF sequence - /\n/, // LF alone - /\r/, // CR alone - /\x00/, // Null byte - /\x0A/, // Line feed hex - /\x0D/, // Carriage return hex - /%0A/i, // URL encoded LF - /%0D/i, // URL encoded CR - /%0a/i, // URL encoded LF lowercase - /%0d/i, // URL encoded CR lowercase - /\\\n/, // Escaped newline - /\\\r/, // Escaped carriage return - /(?:subject|from|to|cc|bcc|reply-to|return-path|received|delivered-to|x-.*?):/i // Email headers -]; -/** - * Detects header injection attempts in input strings - * @param input - The input string to check - * @param context - The context where this input is being used ('smtp-command' or 'email-header') - * @returns true if header injection is detected, false otherwise - */ -export function detectHeaderInjection(input, context = 'smtp-command') { - if (!input || typeof input !== 'string') { - return false; - } - // Check for control characters and CRLF sequences (always dangerous) - const controlCharPatterns = [ - /\r\n/, // CRLF sequence - /\n/, // LF alone - /\r/, // CR alone - /\x00/, // Null byte - /\x0A/, // Line feed hex - /\x0D/, // Carriage return hex - /%0A/i, // URL encoded LF - /%0D/i, // URL encoded CR - /%0a/i, // URL encoded LF lowercase - /%0d/i, // URL encoded CR lowercase - /\\\n/, // Escaped newline - /\\\r/, // Escaped carriage return - ]; - // Check control characters (always dangerous in any context) - if (controlCharPatterns.some(pattern => pattern.test(input))) { - return true; - } - // For email headers, also check for header injection patterns - if (context === 'email-header') { - const headerPatterns = [ - /(?:subject|from|to|cc|bcc|reply-to|return-path|received|delivered-to|x-.*?):/i // Email headers - ]; - return headerPatterns.some(pattern => pattern.test(input)); - } - // For SMTP commands, don't flag normal command syntax like "TO:" as header injection - return false; -} -/** - * Sanitizes input by removing or escaping potentially dangerous characters - * @param input - The input string to sanitize - * @returns Sanitized string - */ -export function sanitizeInput(input) { - if (!input || typeof input !== 'string') { - return ''; - } - // Remove control characters and potential injection sequences - return input - .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars except \t, \n, \r - .replace(/\r\n/g, ' ') // Replace CRLF with space - .replace(/[\r\n]/g, ' ') // Replace individual CR/LF with space - .replace(/%0[aAdD]/gi, '') // Remove URL encoded CRLF - .trim(); -} -import { SmtpLogger } from './logging.js'; -/** - * Validates an email address - * @param email - Email address to validate - * @returns Whether the email address is valid - */ -export function isValidEmail(email) { - if (!email || typeof email !== 'string') { - return false; - } - // Basic pattern check - if (!SMTP_PATTERNS.EMAIL.test(email)) { - return false; - } - // Additional validation for common invalid patterns - const [localPart, domain] = email.split('@'); - // Check for double dots - if (email.includes('..')) { - return false; - } - // Check domain doesn't start or end with dot - if (domain && (domain.startsWith('.') || domain.endsWith('.'))) { - return false; - } - // Check local part length (max 64 chars per RFC) - if (localPart && localPart.length > 64) { - return false; - } - // Check domain length (max 253 chars per RFC - accounting for trailing dot) - if (domain && domain.length > 253) { - return false; - } - return true; -} -/** - * Validates the MAIL FROM command syntax - * @param args - Arguments string from the MAIL FROM command - * @returns Object with validation result and extracted data - */ -export function validateMailFrom(args) { - if (!args) { - return { isValid: false, errorMessage: 'Missing arguments' }; - } - // Check for header injection attempts - if (detectHeaderInjection(args)) { - SmtpLogger.warn('Header injection attempt detected in MAIL FROM command', { args }); - return { isValid: false, errorMessage: 'Invalid syntax - illegal characters detected' }; - } - // Handle "MAIL FROM:" already in the args - let cleanArgs = args; - if (args.toUpperCase().startsWith('MAIL FROM')) { - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - cleanArgs = args.substring(colonIndex + 1).trim(); - } - } - else if (args.toUpperCase().startsWith('FROM:')) { - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - cleanArgs = args.substring(colonIndex + 1).trim(); - } - } - // Handle empty sender case '<>' - if (cleanArgs === '<>') { - return { isValid: true, address: '', params: {} }; - } - // According to test expectations, validate that the address is enclosed in angle brackets - // Check for angle brackets and RFC-compliance - if (cleanArgs.includes('<') && cleanArgs.includes('>')) { - const startBracket = cleanArgs.indexOf('<'); - const endBracket = cleanArgs.indexOf('>', startBracket); - if (startBracket !== -1 && endBracket !== -1 && startBracket < endBracket) { - const emailPart = cleanArgs.substring(startBracket + 1, endBracket).trim(); - const paramsString = cleanArgs.substring(endBracket + 1).trim(); - // Handle empty sender case '<>' again - if (emailPart === '') { - return { isValid: true, address: '', params: {} }; - } - // During testing, we should validate the email format - // Check for basic email format (something@somewhere) - if (!isValidEmail(emailPart)) { - return { isValid: false, errorMessage: 'Invalid email address format' }; - } - // Parse parameters if they exist - const params = {}; - if (paramsString) { - const paramRegex = /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g; - let match; - while ((match = paramRegex.exec(paramsString)) !== null) { - const name = match[1].toUpperCase(); - const value = match[2] || ''; - params[name] = value; - } - } - return { isValid: true, address: emailPart, params }; - } - } - // If no angle brackets, the format is invalid for MAIL FROM - // Tests expect us to reject formats without angle brackets - // For better compliance with tests, check if the argument might contain an email without brackets - if (isValidEmail(cleanArgs)) { - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; - } - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; -} -/** - * Validates the RCPT TO command syntax - * @param args - Arguments string from the RCPT TO command - * @returns Object with validation result and extracted data - */ -export function validateRcptTo(args) { - if (!args) { - return { isValid: false, errorMessage: 'Missing arguments' }; - } - // Check for header injection attempts - if (detectHeaderInjection(args)) { - SmtpLogger.warn('Header injection attempt detected in RCPT TO command', { args }); - return { isValid: false, errorMessage: 'Invalid syntax - illegal characters detected' }; - } - // Handle "RCPT TO:" already in the args - let cleanArgs = args; - if (args.toUpperCase().startsWith('RCPT TO')) { - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - cleanArgs = args.substring(colonIndex + 1).trim(); - } - } - else if (args.toUpperCase().startsWith('TO:')) { - cleanArgs = args.substring(3).trim(); - } - // According to test expectations, validate that the address is enclosed in angle brackets - // Check for angle brackets and RFC-compliance - if (cleanArgs.includes('<') && cleanArgs.includes('>')) { - const startBracket = cleanArgs.indexOf('<'); - const endBracket = cleanArgs.indexOf('>', startBracket); - if (startBracket !== -1 && endBracket !== -1 && startBracket < endBracket) { - const emailPart = cleanArgs.substring(startBracket + 1, endBracket).trim(); - const paramsString = cleanArgs.substring(endBracket + 1).trim(); - // During testing, we should validate the email format - // Check for basic email format (something@somewhere) - if (!isValidEmail(emailPart)) { - return { isValid: false, errorMessage: 'Invalid email address format' }; - } - // Parse parameters if they exist - const params = {}; - if (paramsString) { - const paramRegex = /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g; - let match; - while ((match = paramRegex.exec(paramsString)) !== null) { - const name = match[1].toUpperCase(); - const value = match[2] || ''; - params[name] = value; - } - } - return { isValid: true, address: emailPart, params }; - } - } - // If no angle brackets, the format is invalid for RCPT TO - // Tests expect us to reject formats without angle brackets - // For better compliance with tests, check if the argument might contain an email without brackets - if (isValidEmail(cleanArgs)) { - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; - } - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; -} -/** - * Validates the EHLO command syntax - * @param args - Arguments string from the EHLO command - * @returns Object with validation result and extracted data - */ -export function validateEhlo(args) { - if (!args) { - return { isValid: false, errorMessage: 'Missing domain name' }; - } - // Check for header injection attempts - if (detectHeaderInjection(args)) { - SmtpLogger.warn('Header injection attempt detected in EHLO command', { args }); - return { isValid: false, errorMessage: 'Invalid domain name format' }; - } - // Extract hostname from EHLO command if present in args - let hostname = args; - const match = args.match(/^(?:EHLO|HELO)\s+([^\s]+)$/i); - if (match) { - hostname = match[1]; - } - // Check for empty hostname - if (!hostname || hostname.trim() === '') { - return { isValid: false, errorMessage: 'Missing domain name' }; - } - // Basic validation - Be very permissive with domain names to handle various client implementations - // RFC 5321 allows a broad range of clients to connect, so validation should be lenient - // Only check for characters that would definitely cause issues - const invalidChars = ['<', '>', '"', '\'', '\\', '\n', '\r']; - if (invalidChars.some(char => hostname.includes(char))) { - // During automated testing, we check for invalid character validation - // For production we could consider accepting these with proper cleanup - return { isValid: false, errorMessage: 'Invalid domain name format' }; - } - // Support IP addresses in square brackets (e.g., [127.0.0.1] or [IPv6:2001:db8::1]) - if (hostname.startsWith('[') && hostname.endsWith(']')) { - // Be permissive with IP literals - many clients use non-standard formats - // Just check for closing bracket and basic format - return { isValid: true, hostname }; - } - // RFC 5321 states we should accept anything as a domain name for EHLO - // Clients may send domain literals, IP addresses, or any other identification - // As long as it follows the basic format and doesn't have clearly invalid characters - // we should accept it to be compatible with a wide range of clients - // The test expects us to reject 'invalid@domain', but RFC doesn't strictly require this - // For testing purposes, we'll include a basic check to validate email-like formats - if (hostname.includes('@')) { - // Reject email-like formats for EHLO/HELO command - return { isValid: false, errorMessage: 'Invalid domain name format' }; - } - // Special handling for test with special characters - // The test "EHLO spec!al@#$chars" is expected to pass with either response: - // 1. Accept it (since RFC doesn't prohibit special chars in domain names) - // 2. Reject it with a 501 error (for implementations with stricter validation) - if (/[!@#$%^&*()+=\[\]{}|;:',<>?~`]/.test(hostname)) { - // For test compatibility, let's be permissive and accept special characters - // RFC 5321 doesn't explicitly prohibit these characters, and some implementations accept them - SmtpLogger.debug(`Allowing hostname with special characters for test: ${hostname}`); - return { isValid: true, hostname }; - } - // Hostname validation can be very tricky - many clients don't follow RFCs exactly - // Better to be permissive than to reject valid clients - return { isValid: true, hostname }; -} -/** - * Validates command in the current SMTP state - * @param command - SMTP command - * @param currentState - Current SMTP state - * @returns Whether the command is valid in the current state - */ -export function isValidCommandSequence(command, currentState) { - const upperCommand = command.toUpperCase(); - // Some commands are valid in any state - if (upperCommand === 'QUIT' || upperCommand === 'RSET' || upperCommand === 'NOOP' || upperCommand === 'HELP') { - return true; - } - // State-specific validation - switch (currentState) { - case SmtpState.GREETING: - return upperCommand === 'EHLO' || upperCommand === 'HELO'; - case SmtpState.AFTER_EHLO: - return upperCommand === 'MAIL' || upperCommand === 'STARTTLS' || upperCommand === 'AUTH' || upperCommand === 'EHLO' || upperCommand === 'HELO'; - case SmtpState.MAIL_FROM: - case SmtpState.RCPT_TO: - if (upperCommand === 'RCPT') { - return true; - } - return currentState === SmtpState.RCPT_TO && upperCommand === 'DATA'; - case SmtpState.DATA: - // In DATA state, only the data content is accepted, not commands - return false; - case SmtpState.DATA_RECEIVING: - // In DATA_RECEIVING state, only the data content is accepted, not commands - return false; - case SmtpState.FINISHED: - // After data is received, only new transactions or session end - return upperCommand === 'MAIL' || upperCommand === 'QUIT' || upperCommand === 'RSET'; - default: - return false; - } -} -/** - * Validates if a hostname is valid according to RFC 5321 - * @param hostname - Hostname to validate - * @returns Whether the hostname is valid - */ -export function isValidHostname(hostname) { - if (!hostname || typeof hostname !== 'string') { - return false; - } - // Basic hostname validation - // This is a simplified check, full RFC compliance would be more complex - return /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(hostname); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci91dGlscy92YWxpZGF0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM3QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFaEQ7OztHQUdHO0FBQ0gsTUFBTSx5QkFBeUIsR0FBRztJQUNoQyxNQUFNLEVBQXFCLGdCQUFnQjtJQUMzQyxJQUFJLEVBQXVCLGFBQWE7SUFDeEMsSUFBSSxFQUF1QixXQUFXO0lBQ3RDLE1BQU0sRUFBcUIsWUFBWTtJQUN2QyxNQUFNLEVBQXFCLGdCQUFnQjtJQUMzQyxNQUFNLEVBQXFCLHNCQUFzQjtJQUNqRCxNQUFNLEVBQXFCLGlCQUFpQjtJQUM1QyxNQUFNLEVBQXFCLGlCQUFpQjtJQUM1QyxNQUFNLEVBQXFCLDJCQUEyQjtJQUN0RCxNQUFNLEVBQXFCLDJCQUEyQjtJQUN0RCxNQUFNLEVBQXFCLGtCQUFrQjtJQUM3QyxNQUFNLEVBQXFCLDBCQUEwQjtJQUNyRCwrRUFBK0UsQ0FBRSxnQkFBZ0I7Q0FDbEcsQ0FBQztBQUVGOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLHFCQUFxQixDQUFDLEtBQWEsRUFBRSxVQUEyQyxjQUFjO0lBQzVHLElBQUksQ0FBQyxLQUFLLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7UUFDeEMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRUQscUVBQXFFO0lBQ3JFLE1BQU0sbUJBQW1CLEdBQUc7UUFDMUIsTUFBTSxFQUFxQixnQkFBZ0I7UUFDM0MsSUFBSSxFQUF1QixhQUFhO1FBQ3hDLElBQUksRUFBdUIsV0FBVztRQUN0QyxNQUFNLEVBQXFCLFlBQVk7UUFDdkMsTUFBTSxFQUFxQixnQkFBZ0I7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 \ No newline at end of file diff --git a/dist_ts/mail/routing/classes.unified.email.server.d.ts b/dist_ts/mail/routing/classes.unified.email.server.d.ts index 5144ff9..2db93bb 100644 --- a/dist_ts/mail/routing/classes.unified.email.server.d.ts +++ b/dist_ts/mail/routing/classes.unified.email.server.d.ts @@ -1,4 +1,3 @@ -import * as plugins from '../../plugins.js'; import { EventEmitter } from 'events'; import { DKIMCreator } from '../security/classes.dkimcreator.js'; interface IIPWarmupConfig { @@ -154,12 +153,6 @@ export declare class UnifiedEmailServer extends EventEmitter { * Start the unified email server */ start(): Promise; - /** - * Handle a socket from smartproxy in socket-handler mode - * @param socket The socket to handle - * @param port The port this connection is for (25, 587, 465) - */ - handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise; /** * Stop the unified email server */ @@ -176,8 +169,8 @@ export declare class UnifiedEmailServer extends EventEmitter { */ private handleRustAuthRequest; /** - * Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge. - * Falls back gracefully if the bridge is not running. + * Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results + * or falling back to IPC call if no pre-computed results are available. */ private verifyInboundSecurity; /** diff --git a/dist_ts/mail/routing/classes.unified.email.server.js b/dist_ts/mail/routing/classes.unified.email.server.js index 4b0a574..0da1147 100644 --- a/dist_ts/mail/routing/classes.unified.email.server.js +++ b/dist_ts/mail/routing/classes.unified.email.server.js @@ -11,7 +11,6 @@ import { Email } from '../core/classes.email.js'; import { DomainRegistry } from './classes.domain.registry.js'; import { DnsManager } from './classes.dns.manager.js'; import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js'; -import { createSmtpServer } from '../delivery/smtpserver/index.js'; import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js'; import { MultiModeDeliverySystem } from '../delivery/classes.delivery.system.js'; import { UnifiedDeliveryQueue } from '../delivery/classes.delivery.queue.js'; @@ -195,12 +194,6 @@ export class UnifiedEmailServer extends EventEmitter { // Check and rotate DKIM keys if needed await this.checkAndRotateDkimKeys(); logger.log('info', 'DKIM key rotation check completed'); - // Skip server creation in socket-handler mode - if (this.options.useSocketHandler) { - logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)'); - this.emit('started'); - return; - } // Ensure we have the necessary TLS options const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath; // Prepare the certificate and key if available @@ -283,47 +276,6 @@ export class UnifiedEmailServer extends EventEmitter { throw error; } } - /** - * Handle a socket from smartproxy in socket-handler mode - * @param socket The socket to handle - * @param port The port this connection is for (25, 587, 465) - */ - async handleSocket(socket, port) { - if (!this.options.useSocketHandler) { - logger.log('error', 'handleSocket called but useSocketHandler is not enabled'); - socket.destroy(); - return; - } - logger.log('info', `Handling socket for port ${port}`); - // Create a temporary SMTP server instance for this connection - // We need a full server instance because the SMTP protocol handler needs all components - const smtpServerOptions = { - port, - hostname: this.options.hostname, - key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined, - cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined - }; - // Create the SMTP server instance - const smtpServer = createSmtpServer(this, smtpServerOptions); - // Get the connection manager from the server - const connectionManager = smtpServer.connectionManager; - if (!connectionManager) { - logger.log('error', 'Could not get connection manager from SMTP server'); - socket.destroy(); - return; - } - // Determine if this is a secure connection - // Port 465 uses implicit TLS, so the socket is already secure - const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket; - // Pass the socket to the connection manager - try { - await connectionManager.handleConnection(socket, isSecure); - } - catch (error) { - logger.log('error', `Error handling socket connection: ${error.message}`); - socket.destroy(); - } - } /** * Stop the unified email server */ @@ -422,6 +374,10 @@ export class UnifiedEmailServer extends EventEmitter { if (authenticatedUser) { session.user = { username: authenticatedUser }; } + // Attach pre-computed security results from Rust in-process pipeline + if (data.securityResults) { + session._precomputedSecurityResults = data.securityResults; + } // Process the email through the routing system await this.processEmailByMode(rawMessageBuffer, session); // Send acceptance back to Rust @@ -468,23 +424,33 @@ export class UnifiedEmailServer extends EventEmitter { } } /** - * Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge. - * Falls back gracefully if the bridge is not running. + * Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results + * or falling back to IPC call if no pre-computed results are available. */ async verifyInboundSecurity(email, session) { try { - const rawMessage = session.emailData || email.toRFC822String(); - const result = await this.rustBridge.verifyEmail({ - rawMessage, - ip: session.remoteAddress, - heloDomain: session.clientHostname || '', - hostname: this.options.hostname, - mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '', - }); + // Check for pre-computed results from Rust in-process security pipeline + const precomputed = session._precomputedSecurityResults; + let result; + if (precomputed) { + logger.log('info', 'Using pre-computed security results from Rust in-process pipeline'); + result = precomputed; + } + else { + // Fallback: IPC round-trip to Rust (for backward compat / handleSocket mode) + const rawMessage = session.emailData || email.toRFC822String(); + result = await this.rustBridge.verifyEmail({ + rawMessage, + ip: session.remoteAddress, + heloDomain: session.clientHostname || '', + hostname: this.options.hostname, + mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '', + }); + } // Apply DKIM result headers if (result.dkim && result.dkim.length > 0) { const dkimSummary = result.dkim - .map(d => `${d.status}${d.domain ? ` (${d.domain})` : ''}`) + .map((d) => `${d.status}${d.domain ? ` (${d.domain})` : ''}`) .join(', '); email.addHeader('X-DKIM-Result', dkimSummary); } @@ -509,6 +475,29 @@ export class UnifiedEmailServer extends EventEmitter { logger.log('info', `DMARC quarantine for domain ${result.dmarc.domain} — marking as potential spam`); } } + // Apply content scan results (from pre-computed pipeline) + if (result.contentScan) { + const scan = result.contentScan; + if (scan.threatScore > 0) { + email.addHeader('X-Spam-Score', String(scan.threatScore)); + if (scan.threatType) { + email.addHeader('X-Spam-Type', scan.threatType); + } + if (scan.threatScore >= 50) { + email.mightBeSpam = true; + logger.log('warn', `Content scan threat score ${scan.threatScore} (${scan.threatType}) — marking as potential spam`); + } + } + } + // Apply IP reputation results (from pre-computed pipeline) + if (result.ipReputation) { + const rep = result.ipReputation; + email.addHeader('X-IP-Reputation-Score', String(rep.score)); + if (rep.is_spam) { + email.mightBeSpam = true; + logger.log('warn', `IP ${rep.ip} flagged by reputation check (score=${rep.score}) — marking as potential spam`); + } + } logger.log('info', `Inbound security verified for email from ${session.remoteAddress}: DKIM=${result.dkim?.[0]?.status ?? 'none'}, SPF=${result.spf?.result ?? 'none'}, DMARC=${result.dmarc?.action ?? 'none'}`); } catch (err) { @@ -1563,4 +1552,4 @@ export class UnifiedEmailServer extends EventEmitter { return this.rateLimiter; } } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy51bmlmaWVkLmVtYWlsLnNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvcm91dGluZy9jbGFzc2VzLnVuaWZpZWQuZW1haWwuc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQ0wsY0FBYyxFQUNkLGdCQUFnQixFQUNoQixpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDakUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sK0NBQStDLENBQUM7QUFDcEYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOENBQThDLENBQUM7QUErQmxGLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV4RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDakQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzlELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsYUFBYSxFQUFFLFVBQVUsRUFBRSxjQUFjLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RixPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsdUJBQXVCLEVBQWtDLE1BQU0sd0NBQXdDLENBQUM7QUFD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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy51bmlmaWVkLmVtYWlsLnNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvcm91dGluZy9jbGFzc2VzLnVuaWZpZWQuZW1haWwuc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQ0wsY0FBYyxFQUNkLGdCQUFnQixFQUNoQixpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDakUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sK0NBQStDLENBQUM7QUFDcEYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOENBQThDLENBQUM7QUErQmxGLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV4RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDakQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzlELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsYUFBYSxFQUFFLFVBQVUsRUFBRSxjQUFjLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RixPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsdUJBQXVCLEVBQWtDLE1BQU0sd0NBQXdDLENBQUM7QUFDakgsT0FBTyxFQUFFLG9CQUFvQixFQUFzQixNQUFNLHVDQUF1QyxDQUFDO0FB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 \ No newline at end of file diff --git a/npmextra.json b/npmextra.json index c574154..6fc225e 100644 --- a/npmextra.json +++ b/npmextra.json @@ -1,5 +1,16 @@ { "@git.zone/tsrust": { - "targets": ["linux_amd64", "linux_arm64"] + "targets": [ + "linux_amd64", + "linux_arm64" + ] + }, + "@git.zone/cli": { + "release": { + "registries": [ + "https://verdaccio.lossless.digital" + ], + "accessLevel": "public" + } } -} +} \ No newline at end of file diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 42b669c..5ce0e55 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1075,6 +1075,7 @@ dependencies = [ "hickory-resolver 0.25.2", "mailer-core", "mailer-security", + "mailparse", "regex", "rustls", "rustls-pemfile", diff --git a/rust/crates/mailer-security/src/content_scanner.rs b/rust/crates/mailer-security/src/content_scanner.rs index bf542e1..f20c771 100644 --- a/rust/crates/mailer-security/src/content_scanner.rs +++ b/rust/crates/mailer-security/src/content_scanner.rs @@ -5,14 +5,14 @@ //! script injection, and sensitive data patterns. use regex::Regex; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::sync::LazyLock; // --------------------------------------------------------------------------- // Result types // --------------------------------------------------------------------------- -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContentScanResult { pub threat_score: u32, diff --git a/rust/crates/mailer-smtp/Cargo.toml b/rust/crates/mailer-smtp/Cargo.toml index 9ad39c7..2a7ca6b 100644 --- a/rust/crates/mailer-smtp/Cargo.toml +++ b/rust/crates/mailer-smtp/Cargo.toml @@ -22,3 +22,4 @@ base64.workspace = true rustls-pki-types.workspace = true rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] } rustls-pemfile = "2" +mailparse.workspace = true diff --git a/rust/crates/mailer-smtp/src/connection.rs b/rust/crates/mailer-smtp/src/connection.rs index af9ec33..e17090e 100644 --- a/rust/crates/mailer-smtp/src/connection.rs +++ b/rust/crates/mailer-smtp/src/connection.rs @@ -14,7 +14,10 @@ use crate::validation; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; +use hickory_resolver::TokioResolver; +use mailer_security::MessageAuthenticator; use serde::{Deserialize, Serialize}; +use std::net::IpAddr; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; @@ -152,6 +155,8 @@ pub async fn handle_connection( tls_acceptor: Option>, remote_addr: String, is_secure: bool, + authenticator: Arc, + resolver: Arc, ) { let mut session = SmtpSession::new(remote_addr.clone(), is_secure); @@ -217,6 +222,8 @@ pub async fn handle_connection( &event_tx, callback_register.as_ref(), &tls_acceptor, + &authenticator, + &resolver, ) .await; @@ -327,6 +334,8 @@ async fn process_line( event_tx: &mpsc::Sender, callback_registry: &dyn CallbackRegistry, tls_acceptor: &Option>, + authenticator: &Arc, + resolver: &Arc, ) -> LineResult { // Handle AUTH intermediate states (waiting for username/password) match &session.auth_state { @@ -375,7 +384,7 @@ async fn process_line( } SmtpCommand::Data => { - handle_data(session, stream, config, event_tx, callback_registry).await + handle_data(session, stream, config, event_tx, callback_registry, authenticator, resolver).await } SmtpCommand::Rset => { @@ -558,6 +567,8 @@ async fn handle_data( config: &SmtpServerConfig, event_tx: &mpsc::Sender, callback_registry: &dyn CallbackRegistry, + authenticator: &Arc, + resolver: &Arc, ) -> LineResult { if !session.state.can_data() { return LineResult::Response(SmtpResponse::bad_sequence( @@ -622,6 +633,18 @@ async fn handle_data( let raw_message = accumulator.into_message().unwrap_or_default(); let correlation_id = uuid::Uuid::new_v4().to_string(); + // --- In-process security pipeline (30s timeout) --- + let security_results = run_security_pipeline( + &raw_message, + &session.remote_addr, + session.client_hostname.as_deref().unwrap_or("unknown"), + &config.hostname, + &session.envelope.mail_from, + authenticator, + resolver, + ) + .await; + // Determine transport: inline base64 or temp file let email_data = if raw_message.len() <= 256 * 1024 { EmailData::Inline { @@ -656,7 +679,7 @@ async fn handle_data( client_hostname: session.client_hostname.clone(), secure: session.secure, authenticated_user: session.authenticated_user().map(|s| s.to_string()), - security_results: None, // Will be populated by server.rs when in-process security is added + security_results }; if event_tx.send(event).await.is_err() { @@ -991,6 +1014,166 @@ async fn validate_credentials( } } +/// Extract MIME parts from a raw email message for content scanning. +/// +/// Returns `(subject, text_body, html_body, attachment_filenames)`. +fn extract_mime_parts(raw_message: &[u8]) -> (Option, Option, Option, Vec) { + let parsed = match mailparse::parse_mail(raw_message) { + Ok(p) => p, + Err(e) => { + debug!(error = %e, "Failed to parse MIME for content scanning"); + return (None, None, None, Vec::new()); + } + }; + + // Extract Subject header + let subject = parsed + .headers + .iter() + .find(|h| h.get_key().eq_ignore_ascii_case("subject")) + .map(|h| h.get_value()); + + let mut text_body: Option = None; + let mut html_body: Option = None; + let mut attachments: Vec = Vec::new(); + + // Walk the MIME tree + fn walk_parts( + part: &mailparse::ParsedMail<'_>, + text_body: &mut Option, + html_body: &mut Option, + attachments: &mut Vec, + ) { + let content_type = part.ctype.mimetype.to_lowercase(); + let disposition = part.get_content_disposition(); + + // Check if this is an attachment + if disposition.disposition == mailparse::DispositionType::Attachment { + if let Some(filename) = disposition.params.get("filename") { + attachments.push(filename.clone()); + } + } else if content_type == "text/plain" && text_body.is_none() { + if let Ok(body) = part.get_body() { + *text_body = Some(body); + } + } else if content_type == "text/html" && html_body.is_none() { + if let Ok(body) = part.get_body() { + *html_body = Some(body); + } + } + + // Recurse into subparts + for sub in &part.subparts { + walk_parts(sub, text_body, html_body, attachments); + } + } + + walk_parts(&parsed, &mut text_body, &mut html_body, &mut attachments); + + (subject, text_body, html_body, attachments) +} + +/// Run the full security pipeline: DKIM/SPF/DMARC + content scan + IP reputation. +/// +/// Returns `Some(json_value)` on success or `None` if the pipeline fails or times out. +async fn run_security_pipeline( + raw_message: &[u8], + remote_addr: &str, + helo_domain: &str, + hostname: &str, + mail_from: &str, + authenticator: &Arc, + resolver: &Arc, +) -> Option { + let security_timeout = Duration::from_secs(30); + + match timeout(security_timeout, run_security_pipeline_inner( + raw_message, remote_addr, helo_domain, hostname, mail_from, authenticator, resolver, + )).await { + Ok(Ok(value)) => { + debug!("In-process security pipeline completed"); + Some(value) + } + Ok(Err(e)) => { + warn!(error = %e, "Security pipeline error — emitting event without results"); + None + } + Err(_) => { + warn!("Security pipeline timed out (30s) — emitting event without results"); + None + } + } +} + +/// Inner implementation of the security pipeline (no timeout wrapper). +async fn run_security_pipeline_inner( + raw_message: &[u8], + remote_addr: &str, + helo_domain: &str, + hostname: &str, + mail_from: &str, + authenticator: &Arc, + resolver: &Arc, +) -> std::result::Result> { + // Parse the remote IP address + let ip: IpAddr = remote_addr.parse().unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)); + + // Run DKIM/SPF/DMARC and IP reputation concurrently + let (email_security, reputation) = tokio::join!( + mailer_security::verify_email_security( + raw_message, ip, helo_domain, hostname, mail_from, authenticator, + ), + mailer_security::check_reputation( + ip, mailer_security::DEFAULT_DNSBL_SERVERS, resolver, + ), + ); + + // Extract MIME parts for content scanning (synchronous) + let (subject, text_body, html_body, attachment_names) = extract_mime_parts(raw_message); + + // Run content scan (synchronous) + let content_scan = mailer_security::content_scanner::scan_content( + subject.as_deref(), + text_body.as_deref(), + html_body.as_deref(), + &attachment_names, + ); + + // Build the combined results JSON + let mut results = serde_json::Map::new(); + + // DKIM/SPF/DMARC + match email_security { + Ok(sec) => { + results.insert("dkim".into(), serde_json::to_value(&sec.dkim)?); + results.insert("spf".into(), serde_json::to_value(&sec.spf)?); + results.insert("dmarc".into(), serde_json::to_value(&sec.dmarc)?); + } + Err(e) => { + warn!(error = %e, "Email security verification failed"); + results.insert("dkim".into(), serde_json::Value::Array(vec![])); + results.insert("spf".into(), serde_json::Value::Null); + results.insert("dmarc".into(), serde_json::Value::Null); + } + } + + // Content scan + results.insert("contentScan".into(), serde_json::to_value(&content_scan)?); + + // IP reputation + match reputation { + Ok(rep) => { + results.insert("ipReputation".into(), serde_json::to_value(&rep)?); + } + Err(e) => { + warn!(error = %e, "IP reputation check failed"); + results.insert("ipReputation".into(), serde_json::Value::Null); + } + } + + Ok(serde_json::Value::Object(results)) +} + #[cfg(test)] mod tests { use super::*; @@ -1020,4 +1203,106 @@ mod tests { let json = serde_json::to_string(&result).unwrap(); assert!(json.contains("accepted")); } + + #[test] + fn test_extract_mime_parts_simple() { + let raw = b"From: sender@example.com\r\n\ + To: rcpt@example.com\r\n\ + Subject: Test Subject\r\n\ + Content-Type: text/plain\r\n\ + \r\n\ + Hello, this is a test body.\r\n"; + + let (subject, text, html, attachments) = extract_mime_parts(raw); + assert_eq!(subject.as_deref(), Some("Test Subject")); + assert!(text.is_some()); + assert!(text.unwrap().contains("Hello, this is a test body.")); + assert!(html.is_none()); + assert!(attachments.is_empty()); + } + + #[test] + fn test_extract_mime_parts_multipart() { + let raw = b"From: sender@example.com\r\n\ + To: rcpt@example.com\r\n\ + Subject: Multipart Test\r\n\ + Content-Type: multipart/mixed; boundary=\"boundary123\"\r\n\ + \r\n\ + --boundary123\r\n\ + Content-Type: text/plain\r\n\ + \r\n\ + Plain text body\r\n\ + --boundary123\r\n\ + Content-Type: text/html\r\n\ + \r\n\ + HTML body\r\n\ + --boundary123\r\n\ + Content-Type: application/octet-stream\r\n\ + Content-Disposition: attachment; filename=\"report.pdf\"\r\n\ + \r\n\ + binary data here\r\n\ + --boundary123--\r\n"; + + let (subject, text, html, attachments) = extract_mime_parts(raw); + assert_eq!(subject.as_deref(), Some("Multipart Test")); + assert!(text.is_some()); + assert!(text.unwrap().contains("Plain text body")); + assert!(html.is_some()); + assert!(html.unwrap().contains("HTML body")); + assert_eq!(attachments.len(), 1); + assert_eq!(attachments[0], "report.pdf"); + } + + #[test] + fn test_extract_mime_parts_no_subject() { + let raw = b"From: sender@example.com\r\n\ + To: rcpt@example.com\r\n\ + Content-Type: text/plain\r\n\ + \r\n\ + Body without subject\r\n"; + + let (subject, text, _html, _attachments) = extract_mime_parts(raw); + assert!(subject.is_none()); + assert!(text.is_some()); + } + + #[test] + fn test_extract_mime_parts_invalid() { + let raw = b"this is not a valid email"; + let (subject, text, html, attachments) = extract_mime_parts(raw); + // Should not panic, may or may not parse partially + // The key property is that it doesn't crash + let _ = (subject, text, html, attachments); + } + + #[test] + fn test_extract_mime_parts_multiple_attachments() { + let raw = b"From: sender@example.com\r\n\ + To: rcpt@example.com\r\n\ + Subject: Attachments\r\n\ + Content-Type: multipart/mixed; boundary=\"bound\"\r\n\ + \r\n\ + --bound\r\n\ + Content-Type: text/plain\r\n\ + \r\n\ + See attached\r\n\ + --bound\r\n\ + Content-Type: application/pdf\r\n\ + Content-Disposition: attachment; filename=\"doc1.pdf\"\r\n\ + \r\n\ + pdf data\r\n\ + --bound\r\n\ + Content-Type: application/vnd.ms-excel\r\n\ + Content-Disposition: attachment; filename=\"data.xlsx\"\r\n\ + \r\n\ + excel data\r\n\ + --bound--\r\n"; + + let (subject, text, _html, attachments) = extract_mime_parts(raw); + assert_eq!(subject.as_deref(), Some("Attachments")); + assert!(text.is_some()); + assert_eq!(attachments.len(), 2); + assert!(attachments.contains(&"doc1.pdf".to_string())); + assert!(attachments.contains(&"data.xlsx".to_string())); + } } diff --git a/rust/crates/mailer-smtp/src/server.rs b/rust/crates/mailer-smtp/src/server.rs index d5d3d32..bc2b0d4 100644 --- a/rust/crates/mailer-smtp/src/server.rs +++ b/rust/crates/mailer-smtp/src/server.rs @@ -9,6 +9,8 @@ use crate::connection::{ }; use crate::rate_limiter::{RateLimitConfig, RateLimiter}; +use hickory_resolver::TokioResolver; +use mailer_security::MessageAuthenticator; use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use std::io::BufReader; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -63,6 +65,17 @@ pub async fn start_server( let (event_tx, event_rx) = mpsc::channel::(1024); + // Create shared security resources for in-process email verification + let authenticator: Arc = Arc::new( + mailer_security::default_authenticator() + .map_err(|e| format!("Failed to create MessageAuthenticator: {e}"))? + ); + let resolver: Arc = Arc::new( + TokioResolver::builder_tokio() + .map(|b| b.build()) + .map_err(|e| format!("Failed to create TokioResolver: {e}"))? + ); + // Build TLS acceptor if configured let tls_acceptor = if config.has_tls() { Some(Arc::new(build_tls_acceptor(&config)?)) @@ -87,6 +100,8 @@ pub async fn start_server( callback_registry.clone(), tls_acceptor.clone(), false, // not implicit TLS + authenticator.clone(), + resolver.clone(), )); handles.push(handle); } @@ -108,6 +123,8 @@ pub async fn start_server( callback_registry.clone(), tls_acceptor.clone(), true, // implicit TLS + authenticator.clone(), + resolver.clone(), )); handles.push(handle); } else { @@ -153,6 +170,8 @@ async fn accept_loop( callback_registry: Arc, tls_acceptor: Option>, implicit_tls: bool, + authenticator: Arc, + resolver: Arc, ) { loop { if shutdown.load(Ordering::SeqCst) { @@ -194,6 +213,8 @@ async fn accept_loop( let callback_registry = callback_registry.clone(); let tls_acceptor = tls_acceptor.clone(); let active_connections = active_connections.clone(); + let authenticator = authenticator.clone(); + let resolver = resolver.clone(); active_connections.fetch_add(1, Ordering::SeqCst); @@ -232,6 +253,8 @@ async fn accept_loop( tls_acceptor, remote_addr, implicit_tls, + authenticator, + resolver, ) .await; diff --git a/test/helpers/server.loader.ts b/test/helpers/server.loader.ts index 3931f20..c66fdb5 100644 --- a/test/helpers/server.loader.ts +++ b/test/helpers/server.loader.ts @@ -1,8 +1,4 @@ import * as plugins from '../../ts/plugins.js'; -import { UnifiedEmailServer } from '../../ts/mail/routing/classes.unified.email.server.js'; -import { createSmtpServer } from '../../ts/mail/delivery/smtpserver/index.js'; -import type { ISmtpServerOptions } from '../../ts/mail/delivery/smtpserver/interfaces.js'; -import type { net } from '../../ts/plugins.js'; export interface ITestServerConfig { port: number; @@ -27,165 +23,18 @@ export interface ITestServer { } /** - * Starts a test SMTP server with the given configuration + * Starts a test SMTP server with the given configuration. + * + * NOTE: The TS SMTP server implementation was removed in Phase 7B + * (replaced by the Rust SMTP server). This stub preserves the interface + * for smtpclient tests that import it, but those tests require `node-forge` + * which is not installed (pre-existing issue). */ -export async function startTestServer(config: ITestServerConfig): Promise { - // Find a free port if one wasn't specified - // Using smartnetwork to find an available port in the range 10000-60000 - let port = config.port; - if (port === undefined || port === 0) { - const network = new plugins.smartnetwork.Network(); - port = await network.findFreePort(10000, 60000, { randomize: true }); - if (!port) { - throw new Error('No free ports available in range 10000-60000'); - } - } - - const serverConfig = { - port: port, // Use the found free port - hostname: config.hostname || 'localhost', - tlsEnabled: config.tlsEnabled || false, - authRequired: config.authRequired || false, - timeout: config.timeout || 30000, - maxConnections: config.maxConnections || 100, - size: config.size || 10 * 1024 * 1024, // 10MB default - maxRecipients: config.maxRecipients || 100 - }; - - // Create a mock email server for testing - const mockEmailServer = { - processEmailByMode: async (emailData: any) => { - console.log(`📧 [Test Server] Processing email:`, emailData.subject || 'No subject'); - return emailData; - }, - getRateLimiter: () => { - // Return a mock rate limiter for testing - return { - recordConnection: (_ip: string) => ({ allowed: true, remaining: 100 }), - checkConnectionLimit: async (_ip: string) => ({ allowed: true, remaining: 100 }), - checkMessageLimit: (_senderAddress: string, _ip: string, _recipientCount?: number, _pattern?: string, _domain?: string) => ({ allowed: true, remaining: 1000 }), - checkRecipientLimit: async (_session: any) => ({ allowed: true, remaining: 50 }), - recordAuthenticationFailure: async (_ip: string) => {}, - recordSyntaxError: async (_ip: string) => {}, - recordCommandError: async (_ip: string) => {}, - recordError: (_key: string) => false, // Return false to not block during tests - isBlocked: async (_ip: string) => false, - cleanup: async () => {} - }; - } - } as any; - - // Load test certificates - let key: string; - let cert: string; - - if (serverConfig.tlsEnabled) { - try { - const certPath = config.testCertPath || './test/fixtures/test-cert.pem'; - const keyPath = config.testKeyPath || './test/fixtures/test-key.pem'; - - cert = await plugins.fs.promises.readFile(certPath, 'utf8'); - key = await plugins.fs.promises.readFile(keyPath, 'utf8'); - } catch (error) { - console.warn('⚠️ Failed to load TLS certificates, falling back to self-signed'); - // Generate self-signed certificate for testing - const forge = await import('node-forge'); - const pki = forge.default.pki; - - // Generate key pair - const keys = pki.rsa.generateKeyPair(2048); - - // Create certificate - const certificate = pki.createCertificate(); - certificate.publicKey = keys.publicKey; - certificate.serialNumber = '01'; - certificate.validity.notBefore = new Date(); - certificate.validity.notAfter = new Date(); - certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1); - - const attrs = [{ - name: 'commonName', - value: serverConfig.hostname - }]; - certificate.setSubject(attrs); - certificate.setIssuer(attrs); - certificate.sign(keys.privateKey); - - // Convert to PEM - cert = pki.certificateToPem(certificate); - key = pki.privateKeyToPem(keys.privateKey); - } - } else { - // Always provide a self-signed certificate for non-TLS servers - // This is required by the interface - const forge = await import('node-forge'); - const pki = forge.default.pki; - - // Generate key pair - const keys = pki.rsa.generateKeyPair(2048); - - // Create certificate - const certificate = pki.createCertificate(); - certificate.publicKey = keys.publicKey; - certificate.serialNumber = '01'; - certificate.validity.notBefore = new Date(); - certificate.validity.notAfter = new Date(); - certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1); - - const attrs = [{ - name: 'commonName', - value: serverConfig.hostname - }]; - certificate.setSubject(attrs); - certificate.setIssuer(attrs); - certificate.sign(keys.privateKey); - - // Convert to PEM - cert = pki.certificateToPem(certificate); - key = pki.privateKeyToPem(keys.privateKey); - } - - // SMTP server options - const smtpOptions: ISmtpServerOptions = { - port: serverConfig.port, - hostname: serverConfig.hostname, - key: key, - cert: cert, - maxConnections: serverConfig.maxConnections, - size: serverConfig.size, - maxRecipients: serverConfig.maxRecipients, - socketTimeout: serverConfig.timeout, - connectionTimeout: serverConfig.timeout * 2, - cleanupInterval: 300000, - auth: serverConfig.authRequired ? ({ - required: true, - methods: ['PLAIN', 'LOGIN'] as ('PLAIN' | 'LOGIN' | 'OAUTH2')[], - validateUser: async (username: string, password: string) => { - // Test server accepts these credentials - return username === 'testuser' && password === 'testpass'; - } - } as any) : undefined - }; - - // Create SMTP server - const smtpServer = await createSmtpServer(mockEmailServer, smtpOptions); - - // Start the server - await smtpServer.listen(); - - // Wait for server to be ready - await waitForServerReady(serverConfig.hostname, serverConfig.port); - - console.log(`✅ Test SMTP server started on ${serverConfig.hostname}:${serverConfig.port}`); - - return { - server: mockEmailServer, - smtpServer: smtpServer, - port: serverConfig.port, // Return the port we already know - hostname: serverConfig.hostname, - config: serverConfig, - startTime: Date.now() - }; +export async function startTestServer(_config: ITestServerConfig): Promise { + throw new Error( + 'startTestServer is no longer available — the TS SMTP server was removed in Phase 7B. ' + + 'Use the Rust SMTP server (via UnifiedEmailServer) for integration testing.' + ); } /** @@ -193,94 +42,19 @@ export async function startTestServer(config: ITestServerConfig): Promise { if (!testServer || !testServer.smtpServer) { - console.warn('⚠️ No test server to stop'); return; } - + try { - console.log(`🛑 Stopping test SMTP server on ${testServer.hostname}:${testServer.port}`); - - // Stop the SMTP server if (testServer.smtpServer.close && typeof testServer.smtpServer.close === 'function') { await testServer.smtpServer.close(); } - - // Wait for port to be free - await waitForPortFree(testServer.port); - - console.log(`✅ Test SMTP server stopped`); } catch (error) { - console.error('❌ Error stopping test server:', error); + console.error('Error stopping test server:', error); throw error; } } -/** - * Wait for server to be ready to accept connections - */ -async function waitForServerReady(hostname: string, port: number, timeout: number = 10000): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - try { - await new Promise((resolve, reject) => { - const socket = plugins.net.createConnection({ port, host: hostname }); - - socket.on('connect', () => { - socket.end(); - resolve(); - }); - - socket.on('error', reject); - - setTimeout(() => { - socket.destroy(); - reject(new Error('Connection timeout')); - }, 1000); - }); - - return; // Server is ready - } catch { - // Server not ready yet, wait and retry - await new Promise(resolve => setTimeout(resolve, 100)); - } - } - - throw new Error(`Server did not become ready within ${timeout}ms`); -} - -/** - * Wait for port to be free - */ -async function waitForPortFree(port: number, timeout: number = 5000): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const isFree = await isPortFree(port); - if (isFree) { - return; - } - await new Promise(resolve => setTimeout(resolve, 100)); - } - - console.warn(`⚠️ Port ${port} still in use after ${timeout}ms`); -} - -/** - * Check if a port is free - */ -async function isPortFree(port: number): Promise { - return new Promise((resolve) => { - const server = plugins.net.createServer(); - - server.listen(port, () => { - server.close(() => resolve(true)); - }); - - server.on('error', () => resolve(false)); - }); -} - /** * Get an available port for testing */ @@ -293,6 +67,21 @@ export async function getAvailablePort(startPort: number = 25000): Promise { + return new Promise((resolve) => { + const server = plugins.net.createServer(); + + server.listen(port, () => { + server.close(() => resolve(true)); + }); + + server.on('error', () => resolve(false)); + }); +} + /** * Create test email data */ @@ -332,7 +121,7 @@ export async function createTestServer(options: { }): Promise { const hostname = options.hostname || 'localhost'; const port = options.port || await getAvailablePort(); - + const server = plugins.net.createServer((socket) => { if (options.onConnection) { const result = options.onConnection(socket); @@ -344,7 +133,7 @@ export async function createTestServer(options: { } } }); - + return new Promise((resolve, reject) => { server.listen(port, hostname, () => { resolve({ @@ -353,7 +142,7 @@ export async function createTestServer(options: { port }); }); - + server.on('error', reject); }); -} \ No newline at end of file +} diff --git a/test/suite/smtpserver_commands/test.cmd-01.ehlo-command.ts b/test/suite/smtpserver_commands/test.cmd-01.ehlo-command.ts deleted file mode 100644 index 6b8395b..0000000 --- a/test/suite/smtpserver_commands/test.cmd-01.ehlo-command.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('CMD-01: EHLO Command - server responds with proper capabilities', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; // Clear buffer - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - // Parse response - only lines that start with 250 - const lines = receivedData.split('\r\n') - .filter(line => line.startsWith('250')) - .filter(line => line.length > 0); - - // Check for required ESMTP extensions - const capabilities = lines.map(line => line.substring(4).trim()); - console.log('📋 Server capabilities:', capabilities); - - // Verify essential capabilities - expect(capabilities.some(cap => cap.includes('SIZE'))).toBeTruthy(); - expect(capabilities.some(cap => cap.includes('8BITMIME'))).toBeTruthy(); - - // The last line should be "250 " (without hyphen) - const lastLine = lines[lines.length - 1]; - expect(lastLine.startsWith('250 ')).toBeTruthy(); - - currentStep = 'quit'; - receivedData = ''; // Clear buffer - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - socket.destroy(); - done.resolve(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('CMD-01: EHLO with invalid hostname - server handles gracefully', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let testIndex = 0; - - const invalidHostnames = [ - '', // Empty hostname - ' ', // Whitespace only - 'invalid..hostname', // Double dots - '.invalid', // Leading dot - 'invalid.', // Trailing dot - 'very-long-hostname-that-exceeds-reasonable-limits-' + 'x'.repeat(200) - ]; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'testing'; - receivedData = ''; // Clear buffer - console.log(`Testing invalid hostname: "${invalidHostnames[testIndex]}"`); - socket.write(`EHLO ${invalidHostnames[testIndex]}\r\n`); - } else if (currentStep === 'testing' && (receivedData.includes('250') || receivedData.includes('5'))) { - // Server should either accept with warning or reject with 5xx - expect(receivedData).toMatch(/^(250|5\d\d)/); - - testIndex++; - if (testIndex < invalidHostnames.length) { - currentStep = 'reset'; - receivedData = ''; // Clear buffer - socket.write('RSET\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } else if (currentStep === 'reset' && receivedData.includes('250')) { - currentStep = 'testing'; - receivedData = ''; // Clear buffer - console.log(`Testing invalid hostname: "${invalidHostnames[testIndex]}"`); - socket.write(`EHLO ${invalidHostnames[testIndex]}\r\n`); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('CMD-01: EHLO command pipelining - multiple EHLO commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'first_ehlo'; - receivedData = ''; // Clear buffer - socket.write('EHLO first.example.com\r\n'); - } else if (currentStep === 'first_ehlo' && receivedData.includes('250 ')) { - currentStep = 'second_ehlo'; - receivedData = ''; // Clear buffer - // Second EHLO (should reset session) - socket.write('EHLO second.example.com\r\n'); - } else if (currentStep === 'second_ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; // Clear buffer - // Verify session was reset by trying MAIL FROM - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-02.mail-from.ts b/test/suite/smtpserver_commands/test.cmd-02.mail-from.ts deleted file mode 100644 index 3e59dc3..0000000 --- a/test/suite/smtpserver_commands/test.cmd-02.mail-from.ts +++ /dev/null @@ -1,330 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('CMD-02: MAIL FROM - accepts valid sender addresses', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let testIndex = 0; - - const validAddresses = [ - 'sender@example.com', - 'test.user+tag@example.com', - 'user@[192.168.1.1]', // IP literal - 'user@subdomain.example.com', - 'user@very-long-domain-name-that-is-still-valid.example.com', - 'test_user@example.com' // underscore in local part - ]; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - console.log(`Testing valid address: ${validAddresses[testIndex]}`); - socket.write(`MAIL FROM:<${validAddresses[testIndex]}>\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - testIndex++; - if (testIndex < validAddresses.length) { - currentStep = 'rset'; - receivedData = ''; - socket.write('RSET\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_from'; - receivedData = ''; - console.log(`Testing valid address: ${validAddresses[testIndex]}`); - socket.write(`MAIL FROM:<${validAddresses[testIndex]}>\r\n`); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('CMD-02: MAIL FROM - rejects invalid sender addresses', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let testIndex = 0; - - const invalidAddresses = [ - 'notanemail', // No @ symbol - '@example.com', // Missing local part - 'user@', // Missing domain - 'user@.com', // Invalid domain - 'user@domain..com', // Double dot - 'user with spaces@example.com', // Unquoted spaces - 'user@', // Invalid characters - 'user@@example.com', // Double @ - 'user@localhost' // localhost not valid domain - ]; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - console.log(`Testing invalid address: "${invalidAddresses[testIndex]}"`); - socket.write(`MAIL FROM:<${invalidAddresses[testIndex]}>\r\n`); - } else if (currentStep === 'mail_from' && (receivedData.includes('250') || receivedData.includes('5'))) { - // Server might accept some addresses or reject with 5xx error - // For this test, we just verify the server responds appropriately - console.log(` Response: ${receivedData.trim()}`); - - testIndex++; - if (testIndex < invalidAddresses.length) { - currentStep = 'rset'; - receivedData = ''; - socket.write('RSET\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_from'; - receivedData = ''; - console.log(`Testing invalid address: "${invalidAddresses[testIndex]}"`); - socket.write(`MAIL FROM:<${invalidAddresses[testIndex]}>\r\n`); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('CMD-02: MAIL FROM with SIZE parameter', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from_small'; - receivedData = ''; - // Test small size - socket.write('MAIL FROM: SIZE=1024\r\n'); - } else if (currentStep === 'mail_from_small' && receivedData.includes('250')) { - currentStep = 'rset'; - receivedData = ''; - socket.write('RSET\r\n'); - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_from_large'; - receivedData = ''; - // Test large size (should be rejected if exceeds limit) - socket.write('MAIL FROM: SIZE=99999999\r\n'); - } else if (currentStep === 'mail_from_large') { - // Should get either 250 (accepted) or 552 (message size exceeds limit) - expect(receivedData).toMatch(/^(250|552)/); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('CMD-02: MAIL FROM with parameters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from_8bitmime'; - receivedData = ''; - // Test BODY=8BITMIME - socket.write('MAIL FROM: BODY=8BITMIME\r\n'); - } else if (currentStep === 'mail_from_8bitmime' && receivedData.includes('250')) { - currentStep = 'rset'; - receivedData = ''; - socket.write('RSET\r\n'); - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_from_unknown'; - receivedData = ''; - // Test unknown parameter (should be ignored or rejected) - socket.write('MAIL FROM: UNKNOWN=value\r\n'); - } else if (currentStep === 'mail_from_unknown') { - // Should get either 250 (ignored) or 555 (parameter not recognized) - expect(receivedData).toMatch(/^(250|555|501)/); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('CMD-02: MAIL FROM sequence violations', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'mail_without_ehlo'; - receivedData = ''; - // Try MAIL FROM without EHLO/HELO first - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_without_ehlo' && receivedData.includes('503')) { - // Should get 503 (bad sequence) - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'first_mail'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'first_mail' && receivedData.includes('250')) { - currentStep = 'second_mail'; - receivedData = ''; - // Try second MAIL FROM without RSET - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'second_mail' && (receivedData.includes('503') || receivedData.includes('250'))) { - // Server might accept or reject the second MAIL FROM - // Some servers allow resetting the sender, others require RSET - console.log(`Second MAIL FROM response: ${receivedData.trim()}`); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-03.rcpt-to.ts b/test/suite/smtpserver_commands/test.cmd-03.rcpt-to.ts deleted file mode 100644 index 27132f1..0000000 --- a/test/suite/smtpserver_commands/test.cmd-03.rcpt-to.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('RCPT TO - should accept valid recipient after MAIL FROM', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - expect(receivedData).toInclude('250'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('RCPT TO - should reject without MAIL FROM', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'rcpt_to_without_mail'; - receivedData = ''; - // Try RCPT TO without MAIL FROM - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to_without_mail' && receivedData.includes('503')) { - // Should get 503 (bad sequence) - expect(receivedData).toInclude('503'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('RCPT TO - should accept multiple recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientCount = 0; - const maxRecipients = 3; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write(`RCPT TO:\r\n`); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - recipientCount++; - receivedData = ''; - - if (recipientCount < maxRecipients) { - socket.write(`RCPT TO:\r\n`); - } else { - expect(recipientCount).toEqual(maxRecipients); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('RCPT TO - should reject invalid email format', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let testIndex = 0; - - const invalidRecipients = [ - 'notanemail', - '@example.com', - 'user@', - 'user@.com', - 'user@domain..com' - ]; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - console.log(`Testing invalid recipient: "${invalidRecipients[testIndex]}"`); - socket.write(`RCPT TO:<${invalidRecipients[testIndex]}>\r\n`); - } else if (currentStep === 'rcpt_to' && (receivedData.includes('501') || receivedData.includes('5'))) { - // Should reject with 5xx error - console.log(` Response: ${receivedData.trim()}`); - - testIndex++; - if (testIndex < invalidRecipients.length) { - currentStep = 'rset'; - receivedData = ''; - socket.write('RSET\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('RCPT TO - should handle SIZE parameter', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to_with_size'; - receivedData = ''; - // RCPT TO doesn't typically have SIZE parameter, but test server response - socket.write('RCPT TO: SIZE=1024\r\n'); - } else if (currentStep === 'rcpt_to_with_size') { - // Server might accept or reject the parameter - expect(receivedData).toMatch(/^(250|555|501)/); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-04.data-command.ts b/test/suite/smtpserver_commands/test.cmd-04.data-command.ts deleted file mode 100644 index c9a3fca..0000000 --- a/test/suite/smtpserver_commands/test.cmd-04.data-command.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 15000; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('DATA - should accept email data after RCPT TO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data_command'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_command' && receivedData.includes('354')) { - currentStep = 'message_body'; - receivedData = ''; - // Send email content - socket.write('From: sender@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('Subject: Test message\r\n'); - socket.write('\r\n'); // Empty line to separate headers from body - socket.write('This is a test message.\r\n'); - socket.write('.\r\n'); // End of message - } else if (currentStep === 'message_body' && receivedData.includes('250')) { - expect(receivedData).toInclude('250'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('DATA - should reject without RCPT TO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'data_without_rcpt'; - receivedData = ''; - // Try DATA without MAIL FROM or RCPT TO - socket.write('DATA\r\n'); - } else if (currentStep === 'data_without_rcpt' && receivedData.includes('503')) { - // Should get 503 (bad sequence) - expect(receivedData).toInclude('503'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('DATA - should accept empty message body', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data_command'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_command' && receivedData.includes('354')) { - currentStep = 'empty_message'; - receivedData = ''; - // Send only the terminator - socket.write('.\r\n'); - } else if (currentStep === 'empty_message') { - // Server should accept empty message - expect(receivedData).toMatch(/^(250|5\d\d)/); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('DATA - should handle dot stuffing correctly', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data_command'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_command' && receivedData.includes('354')) { - currentStep = 'dot_stuffed_message'; - receivedData = ''; - // Send message with dots that need stuffing - socket.write('This line is normal.\r\n'); - socket.write('..This line starts with two dots (one will be removed).\r\n'); - socket.write('.This line starts with a single dot.\r\n'); - socket.write('...This line starts with three dots.\r\n'); - socket.write('.\r\n'); // End of message - } else if (currentStep === 'dot_stuffed_message' && receivedData.includes('250')) { - expect(receivedData).toInclude('250'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('DATA - should handle large messages', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data_command'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_command' && receivedData.includes('354')) { - currentStep = 'large_message'; - receivedData = ''; - // Send a large message (100KB) - socket.write('From: sender@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('Subject: Large test message\r\n'); - socket.write('\r\n'); - - // Generate 100KB of data - const lineContent = 'This is a test line that will be repeated many times. '; - const linesNeeded = Math.ceil(100000 / lineContent.length); - - for (let i = 0; i < linesNeeded; i++) { - socket.write(lineContent + '\r\n'); - } - - socket.write('.\r\n'); // End of message - } else if (currentStep === 'large_message' && receivedData.includes('250')) { - expect(receivedData).toInclude('250'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('DATA - should handle binary data in message', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data_command'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_command' && receivedData.includes('354')) { - currentStep = 'binary_message'; - receivedData = ''; - // Send message with binary data (base64 encoded attachment) - socket.write('From: sender@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('Subject: Binary test message\r\n'); - socket.write('MIME-Version: 1.0\r\n'); - socket.write('Content-Type: multipart/mixed; boundary="boundary123"\r\n'); - socket.write('\r\n'); - socket.write('--boundary123\r\n'); - socket.write('Content-Type: text/plain\r\n'); - socket.write('\r\n'); - socket.write('This message contains binary data.\r\n'); - socket.write('--boundary123\r\n'); - socket.write('Content-Type: application/octet-stream\r\n'); - socket.write('Content-Transfer-Encoding: base64\r\n'); - socket.write('\r\n'); - socket.write('SGVsbG8gV29ybGQhIFRoaXMgaXMgYmluYXJ5IGRhdGEu\r\n'); - socket.write('--boundary123--\r\n'); - socket.write('.\r\n'); // End of message - } else if (currentStep === 'binary_message' && receivedData.includes('250')) { - expect(receivedData).toInclude('250'); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-05.noop-command.ts b/test/suite/smtpserver_commands/test.cmd-05.noop-command.ts deleted file mode 100644 index d5de611..0000000 --- a/test/suite/smtpserver_commands/test.cmd-05.noop-command.ts +++ /dev/null @@ -1,320 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic NOOP command -tap.test('NOOP - should accept NOOP command', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'noop'; - socket.write('NOOP\r\n'); - } else if (currentStep === 'noop' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); // NOOP response - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Multiple NOOP commands -tap.test('NOOP - should handle multiple consecutive NOOP commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let noopCount = 0; - const maxNoops = 3; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; // Clear buffer after processing - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'noop'; - receivedData = ''; // Clear buffer after processing - socket.write('NOOP\r\n'); - } else if (currentStep === 'noop' && receivedData.includes('250 OK')) { - noopCount++; - receivedData = ''; // Clear buffer after processing - - if (noopCount < maxNoops) { - // Send another NOOP command - socket.write('NOOP\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(noopCount).toEqual(maxNoops); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: NOOP during transaction -tap.test('NOOP - should work during email transaction', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'noop_after_mail'; - socket.write('NOOP\r\n'); - } else if (currentStep === 'noop_after_mail' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'noop_after_rcpt'; - socket.write('NOOP\r\n'); - } else if (currentStep === 'noop_after_rcpt' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: NOOP with parameter (should be ignored) -tap.test('NOOP - should handle NOOP with parameters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'noop_with_param'; - socket.write('NOOP ignored parameter\r\n'); // Parameters should be ignored - } else if (currentStep === 'noop_with_param' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: NOOP before EHLO/HELO -tap.test('NOOP - should work before EHLO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'noop_before_ehlo'; - socket.write('NOOP\r\n'); - } else if (currentStep === 'noop_before_ehlo' && receivedData.includes('250')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Rapid NOOP commands (stress test) -tap.test('NOOP - should handle rapid NOOP commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let noopsSent = 0; - let noopsReceived = 0; - const rapidNoops = 10; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'rapid_noop'; - // Send multiple NOOPs rapidly - for (let i = 0; i < rapidNoops; i++) { - socket.write('NOOP\r\n'); - noopsSent++; - } - } else if (currentStep === 'rapid_noop') { - // Count 250 responses - const matches = receivedData.match(/250 /g); - if (matches) { - noopsReceived = matches.length - 1; // -1 for EHLO response - } - - if (noopsReceived >= rapidNoops) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(noopsReceived).toBeGreaterThan(rapidNoops - 1); - done.resolve(); - }, 500); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-06.rset-command.ts b/test/suite/smtpserver_commands/test.cmd-06.rset-command.ts deleted file mode 100644 index 292e68b..0000000 --- a/test/suite/smtpserver_commands/test.cmd-06.rset-command.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic RSET command -tap.test('RSET - should reset transaction after MAIL FROM', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rset'; - socket.write('RSET\r\n'); - } else if (currentStep === 'rset' && receivedData.includes('250')) { - // RSET successful, try to send MAIL FROM again to verify reset - currentStep = 'mail_from_after_rset'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from_after_rset' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250 OK'); // RSET response - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RSET after RCPT TO -tap.test('RSET - should reset transaction after RCPT TO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'rset'; - socket.write('RSET\r\n'); - } else if (currentStep === 'rset' && receivedData.includes('250')) { - // After RSET, should need MAIL FROM before RCPT TO - currentStep = 'rcpt_to_after_rset'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to_after_rset' && receivedData.includes('503')) { - // Should get 503 bad sequence - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); // Bad sequence after RSET - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RSET during DATA -tap.test('RSET - should reset transaction during DATA phase', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - // Start sending data but then RSET - currentStep = 'rset_during_data'; - socket.write('Subject: Test\r\n\r\nPartial message...\r\n'); - socket.write('RSET\r\n'); // This should be treated as part of data - socket.write('\r\n.\r\n'); // End data - } else if (currentStep === 'rset_during_data' && receivedData.includes('250')) { - // Message accepted, now send actual RSET - currentStep = 'rset_after_data'; - socket.write('RSET\r\n'); - } else if (currentStep === 'rset_after_data' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Multiple RSET commands -tap.test('RSET - should handle multiple consecutive RSET commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let rsetCount = 0; - const maxRsets = 3; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'multiple_rsets'; - receivedData = ''; - socket.write('RSET\r\n'); - } else if (currentStep === 'multiple_rsets' && receivedData.includes('250')) { - rsetCount++; - receivedData = ''; // Clear buffer after processing - - if (rsetCount < maxRsets) { - socket.write('RSET\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(rsetCount).toEqual(maxRsets); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RSET without transaction -tap.test('RSET - should work without active transaction', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'rset_without_transaction'; - socket.write('RSET\r\n'); - } else if (currentStep === 'rset_without_transaction' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); // RSET should work even without transaction - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RSET with multiple recipients -tap.test('RSET - should clear all recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientCount = 0; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'add_recipients'; - recipientCount++; - socket.write(`RCPT TO:\r\n`); - } else if (currentStep === 'add_recipients' && receivedData.includes('250')) { - if (recipientCount < 3) { - recipientCount++; - receivedData = ''; // Clear buffer - socket.write(`RCPT TO:\r\n`); - } else { - currentStep = 'rset'; - socket.write('RSET\r\n'); - } - } else if (currentStep === 'rset' && receivedData.includes('250')) { - // After RSET, all recipients should be cleared - currentStep = 'data_after_rset'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_after_rset' && receivedData.includes('503')) { - // Should get 503 bad sequence (no recipients) - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RSET with parameter (should be ignored) -tap.test('RSET - should ignore parameters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'rset_with_param'; - socket.write('RSET ignored parameter\r\n'); // Parameters should be ignored - } else if (currentStep === 'rset_with_param' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-07.vrfy-command.ts b/test/suite/smtpserver_commands/test.cmd-07.vrfy-command.ts deleted file mode 100644 index bed30bd..0000000 --- a/test/suite/smtpserver_commands/test.cmd-07.vrfy-command.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic VRFY command -tap.test('VRFY - should respond to VRFY command', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'vrfy'; - receivedData = ''; // Clear buffer before sending VRFY - socket.write('VRFY postmaster\r\n'); - } else if (currentStep === 'vrfy' && receivedData.includes(' ')) { - const lines = receivedData.split('\r\n'); - const vrfyResponse = lines.find(line => line.match(/^\d{3}/)); - const responseCode = vrfyResponse?.substring(0, 3); - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // VRFY may be: - // 250/251 - User found/will forward - // 252 - Cannot verify but will try - // 502 - Command not implemented (common for security) - // 503 - Bad sequence of commands (this server rejects VRFY due to sequence validation) - // 550 - User not found - expect(responseCode).toMatch(/^(250|251|252|502|503|550)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: VRFY multiple users -tap.test('VRFY - should handle multiple VRFY requests', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const testUsers = ['postmaster', 'admin', 'test', 'nonexistent']; - let currentUserIndex = 0; - const vrfyResults: Array<{ user: string; responseCode: string; supported: boolean }> = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'vrfy'; - receivedData = ''; // Clear buffer before sending VRFY - socket.write(`VRFY ${testUsers[currentUserIndex]}\r\n`); - } else if (currentStep === 'vrfy' && receivedData.includes('503') && currentUserIndex < testUsers.length) { - // This server always returns 503 for VRFY - vrfyResults.push({ - user: testUsers[currentUserIndex], - responseCode: '503', - supported: false - }); - - currentUserIndex++; - - if (currentUserIndex < testUsers.length) { - receivedData = ''; // Clear buffer - socket.write(`VRFY ${testUsers[currentUserIndex]}\r\n`); - } else { - currentStep = 'done'; // Change state to prevent processing QUIT response - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Should have results for all users - expect(vrfyResults.length).toEqual(testUsers.length); - - // All responses should be valid SMTP codes - vrfyResults.forEach(result => { - expect(result.responseCode).toMatch(/^(250|251|252|502|503|550)$/); - }); - - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: VRFY without parameter -tap.test('VRFY - should reject VRFY without parameter', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'vrfy_empty'; - receivedData = ''; // Clear buffer before sending VRFY - socket.write('VRFY\r\n'); // No user specified - } else if (currentStep === 'vrfy_empty' && receivedData.includes(' ')) { - const responseCode = receivedData.match(/(\d{3})/)?.[1]; - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Should be 501 (syntax error), 502 (not implemented), or 503 (bad sequence) - expect(responseCode).toMatch(/^(501|502|503)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: VRFY during transaction -tap.test('VRFY - should work during mail transaction', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'vrfy_during_transaction'; - receivedData = ''; // Clear buffer before sending VRFY - socket.write('VRFY test@example.com\r\n'); - } else if (currentStep === 'vrfy_during_transaction' && receivedData.includes('503')) { - const responseCode = '503'; // We know this server always returns 503 - - // VRFY may be rejected with 503 during transaction in this server - expect(responseCode).toMatch(/^(250|251|252|502|503|550)$/); - - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: VRFY special addresses -tap.test('VRFY - should handle special addresses', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const specialAddresses = [ - 'postmaster', - 'postmaster@localhost', - 'abuse', - 'abuse@localhost', - 'noreply', - '' // With angle brackets - ]; - let currentIndex = 0; - const results: Array<{ address: string; responseCode: string }> = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'vrfy_special'; - receivedData = ''; // Clear buffer before sending VRFY - socket.write(`VRFY ${specialAddresses[currentIndex]}\r\n`); - } else if (currentStep === 'vrfy_special' && receivedData.includes('503') && currentIndex < specialAddresses.length) { - // This server always returns 503 for VRFY - results.push({ - address: specialAddresses[currentIndex], - responseCode: '503' - }); - - currentIndex++; - - if (currentIndex < specialAddresses.length) { - receivedData = ''; // Clear buffer - socket.write(`VRFY ${specialAddresses[currentIndex]}\r\n`); - } else { - currentStep = 'done'; // Change state to prevent processing QUIT response - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // All addresses should get valid responses - results.forEach(result => { - expect(result.responseCode).toMatch(/^(250|251|252|502|503|550)$/); - }); - - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: VRFY security considerations -tap.test('VRFY - verify security behavior', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let commandDisabled = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'vrfy_security'; - receivedData = ''; // Clear buffer before sending VRFY - socket.write('VRFY randomuser123\r\n'); - } else if (currentStep === 'vrfy_security' && receivedData.includes(' ')) { - const responseCode = receivedData.match(/(\d{3})/)?.[1]; - - // Check if command is disabled for security or sequence validation - if (responseCode === '502' || responseCode === '252' || responseCode === '503') { - commandDisabled = true; - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Note: Many servers disable VRFY for security reasons - // Both enabled and disabled are valid configurations - // This server rejects VRFY with 503 due to sequence validation - if (responseCode === '503' || commandDisabled) { - expect(responseCode).toMatch(/^(502|252|503)$/); - } else { - expect(responseCode).toMatch(/^(250|251|550)$/); - } - - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-08.expn-command.ts b/test/suite/smtpserver_commands/test.cmd-08.expn-command.ts deleted file mode 100644 index 4db81f9..0000000 --- a/test/suite/smtpserver_commands/test.cmd-08.expn-command.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic EXPN command -tap.test('EXPN - should respond to EXPN command', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'expn'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write('EXPN postmaster\r\n'); - } else if (currentStep === 'expn' && receivedData.includes(' ')) { - const lines = receivedData.split('\r\n'); - const expnResponse = lines.find(line => line.match(/^\d{3}/)); - const responseCode = expnResponse?.substring(0, 3); - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // EXPN may be: - // 250/251 - List expanded - // 252 - Cannot expand but will try to deliver - // 502 - Command not implemented (common for security) - // 503 - Bad sequence of commands (this server rejects EXPN due to sequence validation) - // 550 - List not found - expect(responseCode).toMatch(/^(250|251|252|502|503|550)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EXPN multiple lists -tap.test('EXPN - should handle multiple EXPN requests', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const testLists = ['postmaster', 'admin', 'staff', 'all', 'users']; - let currentListIndex = 0; - const expnResults: Array<{ list: string; responseCode: string; supported: boolean }> = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'expn'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write(`EXPN ${testLists[currentListIndex]}\r\n`); - } else if (currentStep === 'expn' && receivedData.includes('503') && currentListIndex < testLists.length) { - // This server always returns 503 for EXPN - const responseCode = '503'; - expnResults.push({ - list: testLists[currentListIndex], - responseCode: responseCode, - supported: responseCode.startsWith('2') - }); - - currentListIndex++; - - if (currentListIndex < testLists.length) { - receivedData = ''; // Clear buffer - socket.write(`EXPN ${testLists[currentListIndex]}\r\n`); - } else { - currentStep = 'done'; // Change state to prevent processing QUIT response - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Should have results for all lists - expect(expnResults.length).toEqual(testLists.length); - - // All responses should be valid SMTP codes - expnResults.forEach(result => { - expect(result.responseCode).toMatch(/^(250|251|252|502|503|550)$/); - }); - - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EXPN without parameter -tap.test('EXPN - should reject EXPN without parameter', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'expn_empty'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write('EXPN\r\n'); // No list specified - } else if (currentStep === 'expn_empty' && receivedData.includes(' ')) { - const responseCode = receivedData.match(/(\d{3})/)?.[1]; - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Should be 501 (syntax error), 502 (not implemented), or 503 (bad sequence) - expect(responseCode).toMatch(/^(501|502|503)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EXPN during transaction -tap.test('EXPN - should work during mail transaction', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'expn_during_transaction'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write('EXPN admin\r\n'); - } else if (currentStep === 'expn_during_transaction' && receivedData.includes('503')) { - const responseCode = '503'; // We know this server always returns 503 - - // EXPN may be rejected with 503 during transaction in this server - expect(responseCode).toMatch(/^(250|251|252|502|503|550)$/); - - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EXPN special lists -tap.test('EXPN - should handle special mailing lists', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const specialLists = [ - 'postmaster', - 'postmaster@localhost', - 'abuse', - 'webmaster', - 'noreply', - '' // With angle brackets - ]; - let currentIndex = 0; - const results: Array<{ list: string; responseCode: string }> = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'expn_special'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write(`EXPN ${specialLists[currentIndex]}\r\n`); - } else if (currentStep === 'expn_special' && receivedData.includes('503') && currentIndex < specialLists.length) { - // This server always returns 503 for EXPN - results.push({ - list: specialLists[currentIndex], - responseCode: '503' - }); - - currentIndex++; - - if (currentIndex < specialLists.length) { - receivedData = ''; // Clear buffer - socket.write(`EXPN ${specialLists[currentIndex]}\r\n`); - } else { - currentStep = 'done'; // Change state to prevent processing QUIT response - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // All lists should get valid responses - results.forEach(result => { - expect(result.responseCode).toMatch(/^(250|251|252|502|503|550)$/); - }); - - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EXPN security considerations -tap.test('EXPN - verify security behavior', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let commandDisabled = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'expn_security'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write('EXPN randomlist123\r\n'); - } else if (currentStep === 'expn_security' && receivedData.includes(' ')) { - const responseCode = receivedData.match(/(\d{3})/)?.[1]; - - // Check if command is disabled for security or sequence validation - if (responseCode === '502' || responseCode === '252' || responseCode === '503') { - commandDisabled = true; - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Note: Many servers disable EXPN for security reasons - // to prevent email address harvesting - // Both enabled and disabled are valid configurations - // This server rejects EXPN with 503 due to sequence validation - if (responseCode === '503' || commandDisabled) { - expect(responseCode).toMatch(/^(502|252|503)$/); - console.log('EXPN disabled - good security practice'); - } else { - expect(responseCode).toMatch(/^(250|251|550)$/); - } - - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EXPN response format -tap.test('EXPN - verify proper response format when supported', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'expn_format'; - receivedData = ''; // Clear buffer before sending EXPN - socket.write('EXPN postmaster\r\n'); - } else if (currentStep === 'expn_format' && receivedData.includes(' ')) { - const lines = receivedData.split('\r\n'); - - // This server returns 503 for EXPN commands - if (receivedData.includes('503')) { - // Server doesn't support EXPN in the current state - expect(receivedData).toInclude('503'); - } else if (receivedData.includes('250-') || receivedData.includes('250 ')) { - // Multi-line response format check - const expansionLines = lines.filter(l => l.startsWith('250')); - expect(expansionLines.length).toBeGreaterThan(0); - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-09.size-extension.ts b/test/suite/smtpserver_commands/test.cmd-09.size-extension.ts deleted file mode 100644 index 5c5042e..0000000 --- a/test/suite/smtpserver_commands/test.cmd-09.size-extension.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 15000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: SIZE extension advertised in EHLO -tap.test('SIZE Extension - should advertise SIZE in EHLO response', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let sizeSupported = false; - let maxMessageSize: number | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - // Check if SIZE extension is advertised - if (receivedData.includes('SIZE')) { - sizeSupported = true; - - // Extract maximum message size if specified - const sizeMatch = receivedData.match(/SIZE\s+(\d+)/); - if (sizeMatch) { - maxMessageSize = parseInt(sizeMatch[1]); - } - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(sizeSupported).toEqual(true); - if (maxMessageSize !== null) { - expect(maxMessageSize).toBeGreaterThan(0); - } - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: MAIL FROM with SIZE parameter -tap.test('SIZE Extension - should accept MAIL FROM with SIZE parameter', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const messageSize = 1000; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from_size'; - socket.write(`MAIL FROM: SIZE=${messageSize}\r\n`); - } else if (currentStep === 'mail_from_size' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250 OK'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: SIZE parameter with various sizes -tap.test('SIZE Extension - should handle different message sizes', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const testSizes = [1000, 10000, 100000, 1000000]; // 1KB, 10KB, 100KB, 1MB - let currentSizeIndex = 0; - const sizeResults: Array<{ size: number; accepted: boolean; response: string }> = []; - - const testNextSize = () => { - if (currentSizeIndex < testSizes.length) { - receivedData = ''; // Clear buffer - const size = testSizes[currentSizeIndex]; - socket.write(`MAIL FROM: SIZE=${size}\r\n`); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // At least some sizes should be accepted - const acceptedCount = sizeResults.filter(r => r.accepted).length; - expect(acceptedCount).toBeGreaterThan(0); - - // Verify larger sizes may be rejected - const largeRejected = sizeResults - .filter(r => r.size >= 1000000 && !r.accepted) - .length; - expect(largeRejected + acceptedCount).toEqual(sizeResults.length); - - done.resolve(); - }, 100); - } - }; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from_sizes'; - testNextSize(); - } else if (currentStep === 'mail_from_sizes') { - if (receivedData.includes('250')) { - // Size accepted - sizeResults.push({ - size: testSizes[currentSizeIndex], - accepted: true, - response: receivedData.trim() - }); - - socket.write('RSET\r\n'); - currentSizeIndex++; - currentStep = 'rset'; - } else if (receivedData.includes('552') || receivedData.includes('5')) { - // Size rejected - sizeResults.push({ - size: testSizes[currentSizeIndex], - accepted: false, - response: receivedData.trim() - }); - - socket.write('RSET\r\n'); - currentSizeIndex++; - currentStep = 'rset'; - } - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_from_sizes'; - testNextSize(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: SIZE parameter exceeding limit -tap.test('SIZE Extension - should reject SIZE exceeding server limit', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let maxSize: number | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - // Extract max size if advertised - const sizeMatch = receivedData.match(/SIZE\s+(\d+)/); - if (sizeMatch) { - maxSize = parseInt(sizeMatch[1]); - } - - currentStep = 'mail_from_oversized'; - // Try to send a message larger than any reasonable limit - const oversizedValue = maxSize ? maxSize + 1 : 100000000; // 100MB or maxSize+1 - socket.write(`MAIL FROM: SIZE=${oversizedValue}\r\n`); - } else if (currentStep === 'mail_from_oversized') { - if (receivedData.includes('552') || receivedData.includes('5')) { - // Size limit exceeded - expected - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toMatch(/552|5\d{2}/); - done.resolve(); - }, 100); - } else if (receivedData.includes('250')) { - // If accepted, server has very high or no limit - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: SIZE=0 (empty message) -tap.test('SIZE Extension - should handle SIZE=0', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from_zero_size'; - socket.write('MAIL FROM: SIZE=0\r\n'); - } else if (currentStep === 'mail_from_zero_size' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Invalid SIZE parameter -tap.test('SIZE Extension - should reject invalid SIZE values', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const invalidSizes = ['abc', '-1', '1.5', '']; // Invalid size values - let currentIndex = 0; - const results: Array<{ value: string; rejected: boolean }> = []; - - const testNextInvalidSize = () => { - if (currentIndex < invalidSizes.length) { - receivedData = ''; // Clear buffer - const invalidSize = invalidSizes[currentIndex]; - socket.write(`MAIL FROM: SIZE=${invalidSize}\r\n`); - } else { - currentStep = 'done'; // Change state to prevent processing QUIT response - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // This server accepts invalid SIZE values without strict validation - // This is permissive but not necessarily incorrect - // Just verify we got responses for all test cases - expect(results.length).toEqual(invalidSizes.length); - - done.resolve(); - }, 100); - } - }; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'invalid_sizes'; - testNextInvalidSize(); - } else if (currentStep === 'invalid_sizes' && currentIndex < invalidSizes.length) { - if (receivedData.includes('250')) { - // This server accepts invalid size values - results.push({ - value: invalidSizes[currentIndex], - rejected: false - }); - } else if (receivedData.includes('501') || receivedData.includes('552')) { - // Invalid parameter - proper validation - results.push({ - value: invalidSizes[currentIndex], - rejected: true - }); - } - - socket.write('RSET\r\n'); - currentIndex++; - currentStep = 'rset'; - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'invalid_sizes'; - testNextInvalidSize(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: SIZE with actual message data -tap.test('SIZE Extension - should enforce SIZE during DATA phase', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const declaredSize = 100; // Declare 100 bytes - const actualMessage = 'X'.repeat(200); // Send 200 bytes (more than declared) - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write(`MAIL FROM: SIZE=${declaredSize}\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'message'; - // Send message larger than declared size - socket.write(`Subject: Size Test\r\n\r\n${actualMessage}\r\n.\r\n`); - } else if (currentStep === 'message') { - // Server may accept or reject based on enforcement - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Either accepted (250) or rejected (552) - expect(receivedData).toMatch(/250|552/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-10.help-command.ts b/test/suite/smtpserver_commands/test.cmd-10.help-command.ts deleted file mode 100644 index f8575b4..0000000 --- a/test/suite/smtpserver_commands/test.cmd-10.help-command.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic HELP command -tap.test('HELP - should respond to general HELP command', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'help'; - receivedData = ''; // Clear buffer before sending HELP - socket.write('HELP\r\n'); - } else if (currentStep === 'help' && receivedData.includes('214')) { - const lines = receivedData.split('\r\n'); - const helpResponse = lines.find(line => line.match(/^\d{3}/)); - const responseCode = helpResponse?.substring(0, 3); - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // HELP may return: - // 214 - Help message - // 502 - Command not implemented - // 504 - Command parameter not implemented - expect(responseCode).toMatch(/^(214|502|504)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELP with specific topics -tap.test('HELP - should respond to HELP with specific command topics', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const helpTopics = ['EHLO', 'MAIL', 'RCPT', 'DATA', 'QUIT']; - let currentTopicIndex = 0; - const helpResults: Array<{ topic: string; responseCode: string; supported: boolean }> = []; - - const getLastResponse = (data: string): string => { - const lines = data.split('\r\n'); - for (let i = lines.length - 1; i >= 0; i--) { - const line = lines[i].trim(); - if (line && /^\d{3}/.test(line)) { - return line; - } - } - return ''; - }; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'help_topics'; - receivedData = ''; // Clear buffer before sending first HELP topic - socket.write(`HELP ${helpTopics[currentTopicIndex]}\r\n`); - } else if (currentStep === 'help_topics' && (receivedData.includes('214') || receivedData.includes('502') || receivedData.includes('504'))) { - const lastResponse = getLastResponse(receivedData); - - if (lastResponse && lastResponse.match(/^\d{3}/)) { - const responseCode = lastResponse.substring(0, 3); - helpResults.push({ - topic: helpTopics[currentTopicIndex], - responseCode: responseCode, - supported: responseCode === '214' - }); - - currentTopicIndex++; - - if (currentTopicIndex < helpTopics.length) { - receivedData = ''; // Clear buffer - socket.write(`HELP ${helpTopics[currentTopicIndex]}\r\n`); - } else { - currentStep = 'done'; // Change state to prevent processing QUIT response - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Should have results for all topics - expect(helpResults.length).toEqual(helpTopics.length); - - // All responses should be valid - helpResults.forEach(result => { - expect(result.responseCode).toMatch(/^(214|502|504)$/); - }); - - done.resolve(); - }, 100); - } - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELP response format -tap.test('HELP - should return properly formatted help text', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let helpResponse = ''; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'help'; - receivedData = ''; // Clear to capture only HELP response - socket.write('HELP\r\n'); - } else if (currentStep === 'help') { - helpResponse = receivedData; - const responseCode = receivedData.match(/(\d{3})/)?.[1]; - - if (responseCode === '214') { - // Help is supported - check format - const lines = receivedData.split('\r\n'); - const helpLines = lines.filter(l => l.startsWith('214')); - - // Should have at least one help line - expect(helpLines.length).toBeGreaterThan(0); - - // Multi-line help should use 214- prefix - if (helpLines.length > 1) { - const hasMultilineFormat = helpLines.some(l => l.startsWith('214-')); - expect(hasMultilineFormat).toEqual(true); - } - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELP during transaction -tap.test('HELP - should work during mail transaction', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'help_during_transaction'; - receivedData = ''; // Clear buffer before sending HELP - socket.write('HELP RCPT\r\n'); - } else if (currentStep === 'help_during_transaction' && receivedData.includes('214')) { - const responseCode = '214'; // We know HELP works on this server - - // HELP should work even during transaction - expect(responseCode).toMatch(/^(214|502|504)$/); - - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELP with invalid topic -tap.test('HELP - should handle HELP with invalid topic', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'help_invalid'; - receivedData = ''; // Clear buffer before sending HELP - socket.write('HELP INVALID_COMMAND_XYZ\r\n'); - } else if (currentStep === 'help_invalid' && receivedData.includes(' ')) { - const lines = receivedData.split('\r\n'); - const helpResponse = lines.find(line => line.match(/^\d{3}/)); - const responseCode = helpResponse?.substring(0, 3); - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Should return 504 (command parameter not implemented) or - // 214 (general help) or 502 (not implemented) - expect(responseCode).toMatch(/^(214|502|504)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELP availability check -tap.test('HELP - verify HELP command optional status', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let helpSupported = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - // Check if HELP is advertised in EHLO response - if (receivedData.includes('HELP')) { - console.log('HELP command advertised in EHLO response'); - } - - currentStep = 'help_test'; - receivedData = ''; // Clear buffer before sending HELP - socket.write('HELP\r\n'); - } else if (currentStep === 'help_test' && receivedData.includes(' ')) { - const lines = receivedData.split('\r\n'); - const helpResponse = lines.find(line => line.match(/^\d{3}/)); - const responseCode = helpResponse?.substring(0, 3); - - if (responseCode === '214') { - helpSupported = true; - console.log('HELP command is supported'); - } else if (responseCode === '502') { - console.log('HELP command not implemented (optional per RFC 5321)'); - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Both supported and not supported are valid - expect(responseCode).toMatch(/^(214|502)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELP content usefulness -tap.test('HELP - check if help content is useful when supported', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'help_data'; - receivedData = ''; // Clear buffer before sending HELP - socket.write('HELP DATA\r\n'); - } else if (currentStep === 'help_data' && receivedData.includes(' ')) { - const lines = receivedData.split('\r\n'); - const helpResponse = lines.find(line => line.match(/^\d{3}/)); - const responseCode = helpResponse?.substring(0, 3); - - if (responseCode === '214') { - // Check if help text mentions relevant DATA command info - const helpText = receivedData.toLowerCase(); - if (helpText.includes('data') || helpText.includes('message') || helpText.includes('354')) { - console.log('HELP provides relevant information about DATA command'); - } - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-11.command-pipelining.ts b/test/suite/smtpserver_commands/test.cmd-11.command-pipelining.ts deleted file mode 100644 index e51a47c..0000000 --- a/test/suite/smtpserver_commands/test.cmd-11.command-pipelining.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 30000; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('Command Pipelining - should advertise PIPELINING in EHLO response', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - console.log('EHLO response:', ehloResponse); - - // Check if PIPELINING is advertised - const pipeliningAdvertised = ehloResponse.includes('250-PIPELINING') || ehloResponse.includes('250 PIPELINING'); - console.log('PIPELINING advertised:', pipeliningAdvertised); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - // Note: PIPELINING is optional per RFC 2920 - expect(ehloResponse).toInclude('250'); - - } finally { - done.resolve(); - } -}); - -tap.test('Command Pipelining - should handle pipelined MAIL FROM and RCPT TO', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send pipelined commands (all at once) - const pipelinedCommands = - 'MAIL FROM:\r\n' + - 'RCPT TO:\r\n'; - - console.log('Sending pipelined commands...'); - socket.write(pipelinedCommands); - - // Collect responses - const responses = await new Promise((resolve) => { - let data = ''; - let responseCount = 0; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - const lines = data.split('\r\n').filter(line => line.trim()); - - // Count responses that look like complete SMTP responses - const completeResponses = lines.filter(line => /^[0-9]{3}(\s|-)/.test(line)); - - // We expect 2 responses (one for MAIL FROM, one for RCPT TO) - if (completeResponses.length >= 2) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - - // Timeout if we don't get responses - setTimeout(() => { - socket.removeListener('data', handler); - resolve(data); - }, 5000); - }); - - console.log('Pipelined command responses:', responses); - - // Parse responses - const responseLines = responses.split('\r\n').filter(line => line.trim()); - const mailFromResponse = responseLines.find(line => line.match(/^250.*/) && responseLines.indexOf(line) === 0); - const rcptToResponse = responseLines.find(line => line.match(/^250.*/) && responseLines.indexOf(line) === 1); - - // Both commands should succeed - expect(mailFromResponse).toBeDefined(); - expect(rcptToResponse).toBeDefined(); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Command Pipelining - should handle pipelined commands with DATA', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send pipelined MAIL FROM, RCPT TO, and DATA commands - const pipelinedCommands = - 'MAIL FROM:\r\n' + - 'RCPT TO:\r\n' + - 'DATA\r\n'; - - console.log('Sending pipelined commands with DATA...'); - socket.write(pipelinedCommands); - - // Collect responses - const responses = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - - // Look for the DATA prompt (354) - if (data.includes('354')) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - - setTimeout(() => { - socket.removeListener('data', handler); - resolve(data); - }, 5000); - }); - - console.log('Responses including DATA:', responses); - - // Should get 250 for MAIL FROM, 250 for RCPT TO, and 354 for DATA - expect(responses).toInclude('250'); // MAIL FROM OK - expect(responses).toInclude('354'); // Start mail input - - // Send email content - const emailContent = 'Subject: Pipelining Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\nTest email with pipelining.\r\n.\r\n'; - socket.write(emailContent); - - // Get final response - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Final response:', finalResponse); - expect(finalResponse).toInclude('250'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Command Pipelining - should handle pipelined NOOP commands', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send multiple pipelined NOOP commands - const pipelinedNoops = - 'NOOP\r\n' + - 'NOOP\r\n' + - 'NOOP\r\n'; - - console.log('Sending pipelined NOOP commands...'); - socket.write(pipelinedNoops); - - // Collect responses - const responses = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - const responseCount = (data.match(/^250.*OK/gm) || []).length; - - // We expect 3 NOOP responses - if (responseCount >= 3) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - - setTimeout(() => { - socket.removeListener('data', handler); - resolve(data); - }, 5000); - }); - - console.log('NOOP responses:', responses); - - // Count OK responses - const okResponses = (responses.match(/^250.*OK/gm) || []).length; - expect(okResponses).toBeGreaterThanOrEqual(3); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-12.helo-command.ts b/test/suite/smtpserver_commands/test.cmd-12.helo-command.ts deleted file mode 100644 index 270fc40..0000000 --- a/test/suite/smtpserver_commands/test.cmd-12.helo-command.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic HELO command -tap.test('HELO - should accept HELO command', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'helo'; - socket.write('HELO test.example.com\r\n'); - } else if (currentStep === 'helo' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELO without hostname -tap.test('HELO - should reject HELO without hostname', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'helo_no_hostname'; - socket.write('HELO\r\n'); // Missing hostname - } else if (currentStep === 'helo_no_hostname' && receivedData.includes('501')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('501'); // Syntax error - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Multiple HELO commands -tap.test('HELO - should accept multiple HELO commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let heloCount = 0; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'first_helo'; - receivedData = ''; - socket.write('HELO test1.example.com\r\n'); - } else if (currentStep === 'first_helo' && receivedData.includes('250 ')) { - heloCount++; - currentStep = 'second_helo'; - receivedData = ''; // Clear buffer - socket.write('HELO test2.example.com\r\n'); - } else if (currentStep === 'second_helo' && receivedData.includes('250 ')) { - heloCount++; - receivedData = ''; - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(heloCount).toEqual(2); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELO after EHLO -tap.test('HELO - should accept HELO after EHLO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'helo_after_ehlo'; - receivedData = ''; // Clear buffer - socket.write('HELO test.example.com\r\n'); - } else if (currentStep === 'helo_after_ehlo' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELO response format -tap.test('HELO - should return simple 250 response', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let heloResponse = ''; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'helo'; - receivedData = ''; // Clear to capture only HELO response - socket.write('HELO test.example.com\r\n'); - } else if (currentStep === 'helo' && receivedData.includes('250')) { - heloResponse = receivedData.trim(); - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // This server returns multi-line response even for HELO - // (technically incorrect per RFC, but we test actual behavior) - expect(heloResponse).toStartWith('250'); - - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: SMTP commands after HELO -tap.test('HELO - should process SMTP commands after HELO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'helo'; - socket.write('HELO test.example.com\r\n'); - } else if (currentStep === 'helo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELO with special characters -tap.test('HELO - should handle hostnames with special characters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const specialHostnames = [ - 'test-host.example.com', // Hyphen - 'test_host.example.com', // Underscore (technically invalid but common) - '192.168.1.1', // IP address - '[192.168.1.1]', // Bracketed IP - 'localhost', // Single label - 'UPPERCASE.EXAMPLE.COM' // Uppercase - ]; - let currentIndex = 0; - const results: Array<{ hostname: string; accepted: boolean }> = []; - - const testNextHostname = () => { - if (currentIndex < specialHostnames.length) { - receivedData = ''; // Clear buffer - socket.write(`HELO ${specialHostnames[currentIndex]}\r\n`); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - - // Most hostnames should be accepted - const acceptedCount = results.filter(r => r.accepted).length; - expect(acceptedCount).toBeGreaterThan(specialHostnames.length / 2); - - done.resolve(); - }, 100); - } - }; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'helo_special'; - testNextHostname(); - } else if (currentStep === 'helo_special') { - if (receivedData.includes('250')) { - results.push({ - hostname: specialHostnames[currentIndex], - accepted: true - }); - } else if (receivedData.includes('501')) { - results.push({ - hostname: specialHostnames[currentIndex], - accepted: false - }); - } - - currentIndex++; - testNextHostname(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: HELO vs EHLO feature availability -tap.test('HELO - verify no extensions with HELO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'helo'; - socket.write('HELO test.example.com\r\n'); - } else if (currentStep === 'helo' && receivedData.includes('250')) { - // Note: This server returns ESMTP extensions even for HELO commands - // This differs from strict RFC compliance but matches the server's behavior - // expect(receivedData).not.toInclude('SIZE'); - // expect(receivedData).not.toInclude('STARTTLS'); - // expect(receivedData).not.toInclude('AUTH'); - // expect(receivedData).not.toInclude('8BITMIME'); - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_commands/test.cmd-13.quit-command.ts b/test/suite/smtpserver_commands/test.cmd-13.quit-command.ts deleted file mode 100644 index 341920e..0000000 --- a/test/suite/smtpserver_commands/test.cmd-13.quit-command.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; - -let testServer; -const TEST_TIMEOUT = 10000; - -// Setup -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -// Test: Basic QUIT command -tap.test('QUIT - should close connection gracefully', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let connectionClosed = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'quit'; - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - // Don't destroy immediately, wait for server to close connection - setTimeout(() => { - if (!connectionClosed) { - socket.destroy(); - expect(receivedData).toInclude('221'); // Closing connection message - done.resolve(); - } - }, 2000); - } - }); - - socket.on('close', () => { - if (currentStep === 'quit' && receivedData.includes('221')) { - connectionClosed = true; - expect(receivedData).toInclude('221'); - done.resolve(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: QUIT during transaction -tap.test('QUIT - should work during active transaction', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'quit'; - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('221'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: QUIT immediately after connect -tap.test('QUIT - should work immediately after connection', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'quit'; - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('221'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: QUIT with parameters (should be ignored or rejected) -tap.test('QUIT - should handle QUIT with parameters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'quit_with_param'; - receivedData = ''; - socket.write('QUIT unexpected parameter\r\n'); - } else if (currentStep === 'quit_with_param' && (receivedData.includes('221') || receivedData.includes('501'))) { - // Server may accept (221) or reject (501) QUIT with parameters - const responseCode = receivedData.match(/(\d{3})/)?.[1]; - socket.destroy(); - expect(['221', '501']).toInclude(responseCode); - done.resolve(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Multiple QUITs (second should fail) -tap.test('QUIT - second QUIT should fail after connection closed', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let quitSent = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'quit'; - receivedData = ''; - socket.write('QUIT\r\n'); - quitSent = true; - } else if (currentStep === 'quit' && receivedData.includes('221')) { - // Try to send another QUIT - try { - socket.write('QUIT\r\n'); - // If write succeeds, wait a bit to see if we get a response - setTimeout(() => { - socket.destroy(); - done.resolve(); // Test passes either way - }, 500); - } catch (err) { - // Write failed because connection closed - this is expected - done.resolve(); - } - } - }); - - socket.on('close', () => { - if (quitSent) { - done.resolve(); - } - }); - - socket.on('error', (error) => { - if (quitSent && error.message.includes('EPIPE')) { - // Expected error when writing to closed socket - done.resolve(); - } else { - done.reject(error); - } - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: QUIT response format -tap.test('QUIT - should return proper 221 response', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let quitResponse = ''; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'quit'; - receivedData = ''; // Clear buffer to capture only QUIT response - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - quitResponse = receivedData.trim(); - setTimeout(() => { - socket.destroy(); - expect(quitResponse).toStartWith('221'); - expect(quitResponse.toLowerCase()).toInclude('closing'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Connection cleanup after QUIT -tap.test('QUIT - verify clean connection shutdown', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let closeEventFired = false; - let endEventFired = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'quit'; - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - // Wait for clean shutdown - setTimeout(() => { - if (!closeEventFired && !endEventFired) { - socket.destroy(); - done.resolve(); - } - }, 3000); - } - }); - - socket.on('end', () => { - endEventFired = true; - }); - - socket.on('close', () => { - closeEventFired = true; - if (currentStep === 'quit') { - expect(endEventFired || closeEventFired).toEqual(true); - done.resolve(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-01.tls-connection.ts b/test/suite/smtpserver_connection/test.cm-01.tls-connection.ts deleted file mode 100644 index e546da0..0000000 --- a/test/suite/smtpserver_connection/test.cm-01.tls-connection.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -import { connectToSmtp, performSmtpHandshake, closeSmtpConnection } from '../../helpers/utils.js'; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server with TLS support', async () => { - testServer = await startTestServer({ - port: 2525, - tlsEnabled: true // Enable TLS support - }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(testServer.port).toEqual(2525); -}); - -tap.test('CM-01: TLS Connection Test - server should advertise STARTTLS capability', async () => { - const startTime = Date.now(); - - try { - // Connect to SMTP server - const socket = await connectToSmtp(testServer.hostname, testServer.port); - expect(socket).toBeInstanceOf(Object); - - // Perform handshake and get capabilities - const capabilities = await performSmtpHandshake(socket, 'test.example.com'); - expect(capabilities).toBeArray(); - - // Check for STARTTLS support - const supportsStarttls = capabilities.some(cap => cap.toUpperCase().includes('STARTTLS')); - expect(supportsStarttls).toEqual(true); - - // Close connection gracefully - await closeSmtpConnection(socket); - - const duration = Date.now() - startTime; - console.log(`✅ TLS capability test completed in ${duration}ms`); - console.log(`📋 Server capabilities: ${capabilities.join(', ')}`); - - } catch (error) { - const duration = Date.now() - startTime; - console.error(`❌ TLS connection test failed after ${duration}ms:`, error); - throw error; - } -}); - -tap.test('CM-01: TLS Connection Test - verify TLS certificate configuration', async () => { - // This test verifies that the server has TLS certificates configured - expect(testServer.config.tlsEnabled).toEqual(true); - - // The server should have loaded certificates during startup - // In production, this would validate actual certificate properties - console.log('✅ TLS configuration verified'); -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - console.log('✅ Test server stopped'); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-02.multiple-connections.ts b/test/suite/smtpserver_connection/test.cm-02.multiple-connections.ts deleted file mode 100644 index ec99e39..0000000 --- a/test/suite/smtpserver_connection/test.cm-02.multiple-connections.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -import { createConcurrentConnections, performSmtpHandshake, closeSmtpConnection } from '../../helpers/utils.js'; - -let testServer: ITestServer; -const CONCURRENT_COUNT = 10; -const TEST_PORT = 2527; - -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ - port: 2526 - }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(testServer.port).toEqual(2526); -}); - -tap.test('CM-02: Multiple Simultaneous Connections - server handles concurrent connections', async () => { - const startTime = Date.now(); - - try { - // Create multiple concurrent connections - console.log(`🔄 Creating ${CONCURRENT_COUNT} concurrent connections...`); - const sockets = await createConcurrentConnections( - testServer.hostname, - testServer.port, - CONCURRENT_COUNT - ); - - expect(sockets).toBeArray(); - expect(sockets.length).toEqual(CONCURRENT_COUNT); - - // Verify all connections are active - let activeCount = 0; - for (const socket of sockets) { - if (socket && !socket.destroyed) { - activeCount++; - } - } - expect(activeCount).toEqual(CONCURRENT_COUNT); - - // Perform handshake on all connections - console.log('🤝 Performing handshake on all connections...'); - const handshakePromises = sockets.map(socket => - performSmtpHandshake(socket).catch(err => ({ error: err.message })) - ); - - const results = await Promise.all(handshakePromises); - const successCount = results.filter(r => Array.isArray(r)).length; - - expect(successCount).toBeGreaterThan(0); - console.log(`✅ ${successCount}/${CONCURRENT_COUNT} connections completed handshake`); - - // Close all connections - console.log('🔚 Closing all connections...'); - await Promise.all( - sockets.map(socket => closeSmtpConnection(socket).catch(() => {})) - ); - - const duration = Date.now() - startTime; - console.log(`✅ Multiple connection test completed in ${duration}ms`); - - } catch (error) { - console.error('❌ Multiple connection test failed:', error); - throw error; - } -}); - -// TODO: Enable this test when connection limits are implemented in the server -// tap.test('CM-02: Connection limit enforcement - verify max connections', async () => { -// const maxConnections = 5; -// -// // Start a new server with lower connection limit -// const limitedServer = await startTestServer({ port: TEST_PORT }); -// -// await new Promise(resolve => setTimeout(resolve, 1000)); -// -// try { -// // Try to create more connections than allowed -// const attemptCount = maxConnections + 5; -// console.log(`🔄 Attempting ${attemptCount} connections (limit: ${maxConnections})...`); -// -// const connectionPromises = []; -// for (let i = 0; i < attemptCount; i++) { -// connectionPromises.push( -// createConcurrentConnections(limitedServer.hostname, limitedServer.port, 1) -// .then(() => ({ success: true, index: i })) -// .catch(err => ({ success: false, index: i, error: err.message })) -// ); -// } -// -// const results = await Promise.all(connectionPromises); -// const successfulConnections = results.filter(r => r.success).length; -// const failedConnections = results.filter(r => !r.success).length; -// -// console.log(`✅ Successful connections: ${successfulConnections}`); -// console.log(`❌ Failed connections: ${failedConnections}`); -// -// // Some connections should fail due to limit -// expect(failedConnections).toBeGreaterThan(0); -// -// } finally { -// await stopTestServer(limitedServer); -// } -// }); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - console.log('✅ Test server stopped'); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-03.connection-timeout.ts b/test/suite/smtpserver_connection/test.cm-03.connection-timeout.ts deleted file mode 100644 index 4eeef39..0000000 --- a/test/suite/smtpserver_connection/test.cm-03.connection-timeout.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -import * as plugins from '../../../ts/plugins.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -tap.test('setup - start SMTP server with short timeout', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - timeout: 5000 // 5 second timeout for this test - }); - - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('CM-03: Connection Timeout - idle connections are closed after timeout', async (tools) => { - const startTime = Date.now(); - - // Create connection - const socket = await new Promise((resolve, reject) => { - const client = plugins.net.createConnection({ - host: testServer.hostname, - port: testServer.port - }); - - client.on('connect', () => resolve(client)); - client.on('error', reject); - - setTimeout(() => reject(new Error('Connection timeout')), 3000); - }); - - // Wait for greeting - await new Promise((resolve) => { - socket.once('data', (data) => { - const response = data.toString(); - expect(response).toInclude('220'); - resolve(); - }); - }); - - console.log('✅ Connected and received greeting'); - - // Now stay idle and wait for server to timeout the connection - const disconnectPromise = new Promise((resolve) => { - socket.on('close', () => { - const duration = Date.now() - startTime; - resolve(duration); - }); - - socket.on('end', () => { - console.log('📡 Server initiated connection close'); - }); - - socket.on('error', (err) => { - console.log('⚠️ Socket error:', err.message); - }); - }); - - // Wait for timeout (should be around 5 seconds) - const duration = await disconnectPromise; - - console.log(`⏱️ Connection closed after ${duration}ms`); - - // Verify timeout happened within expected range (4-6 seconds) - expect(duration).toBeGreaterThan(4000); - expect(duration).toBeLessThan(7000); - - console.log('✅ Connection timeout test passed'); -}); - -tap.test('CM-03: Active connection should not timeout', async () => { - // Create new connection - const socket = plugins.net.createConnection({ - host: testServer.hostname, - port: testServer.port - }); - - await new Promise((resolve) => { - socket.on('connect', resolve); - }); - - // Wait for greeting - await new Promise((resolve) => { - socket.once('data', resolve); - }); - - // Keep connection active with NOOP commands - let isConnected = true; - socket.on('close', () => { - isConnected = false; - }); - - // Send NOOP every 2 seconds for 8 seconds - for (let i = 0; i < 4; i++) { - if (!isConnected) break; - - socket.write('NOOP\r\n'); - - // Wait for response - await new Promise((resolve) => { - socket.once('data', (data) => { - const response = data.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - console.log(`✅ NOOP ${i + 1}/4 successful`); - - // Wait 2 seconds before next NOOP - await new Promise(resolve => setTimeout(resolve, 2000)); - } - - // Connection should still be active - expect(isConnected).toEqual(true); - - // Close connection gracefully - socket.write('QUIT\r\n'); - await new Promise((resolve) => { - socket.once('data', () => { - socket.end(); - resolve(); - }); - }); - - console.log('✅ Active connection did not timeout'); -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-04.connection-limits.ts b/test/suite/smtpserver_connection/test.cm-04.connection-limits.ts deleted file mode 100644 index ceb54ae..0000000 --- a/test/suite/smtpserver_connection/test.cm-04.connection-limits.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -// Test configuration -const TEST_PORT = 2525; -const TEST_TIMEOUT = 5000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); - -}); - -// Test: Basic connection limit enforcement -tap.test('Connection Limits - should handle multiple connections gracefully', async (tools) => { - const done = tools.defer(); - - const maxConnections = 20; // Test with reasonable number - const testConnections = maxConnections + 5; // Try 5 more than limit - const connections: net.Socket[] = []; - const connectionPromises: Promise<{ index: number; success: boolean; error?: string }>[] = []; - - // Helper to create a connection with index - const createConnectionWithIndex = (index: number): Promise<{ index: number; success: boolean; error?: string }> => { - return new Promise((resolve) => { - let timeoutHandle: NodeJS.Timeout; - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - socket.on('connect', () => { - clearTimeout(timeoutHandle); - connections[index] = socket; - - // Wait for server greeting - socket.on('data', (data) => { - if (data.toString().includes('220')) { - resolve({ index, success: true }); - } - }); - }); - - socket.on('error', (err) => { - clearTimeout(timeoutHandle); - resolve({ index, success: false, error: err.message }); - }); - - timeoutHandle = setTimeout(() => { - socket.destroy(); - resolve({ index, success: false, error: 'Connection timeout' }); - }, TEST_TIMEOUT); - } catch (err: any) { - resolve({ index, success: false, error: err.message }); - } - }); - }; - - // Create connections - for (let i = 0; i < testConnections; i++) { - connectionPromises.push(createConnectionWithIndex(i)); - } - - const results = await Promise.all(connectionPromises); - - // Count successful connections - const successfulConnections = results.filter(r => r.success).length; - const failedConnections = results.filter(r => !r.success).length; - - // Clean up connections - for (const socket of connections) { - if (socket && !socket.destroyed) { - socket.write('QUIT\r\n'); - setTimeout(() => socket.destroy(), 100); - } - } - - // Verify results - expect(successfulConnections).toBeGreaterThan(0); - - // If some connections were rejected, that's good (limit enforced) - // If all connections succeeded, that's also acceptable (high/no limit) - if (failedConnections > 0) { - console.log(`Server enforced connection limit: ${successfulConnections} accepted, ${failedConnections} rejected`); - } else { - console.log(`Server accepted all ${successfulConnections} connections`); - } - - done.resolve(); - - await done.promise; -}); - -// Test: Connection limit recovery -tap.test('Connection Limits - should accept new connections after closing old ones', async (tools) => { - const done = tools.defer(); - - const batchSize = 10; - const firstBatch: net.Socket[] = []; - const secondBatch: net.Socket[] = []; - - // Create first batch of connections - const firstBatchPromises = []; - for (let i = 0; i < batchSize; i++) { - firstBatchPromises.push( - new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - socket.on('connect', () => { - firstBatch.push(socket); - socket.on('data', (data) => { - if (data.toString().includes('220')) { - resolve(true); - } - }); - }); - - socket.on('error', () => resolve(false)); - }) - ); - } - - const firstResults = await Promise.all(firstBatchPromises); - const firstSuccessCount = firstResults.filter(r => r).length; - - // Close first batch - for (const socket of firstBatch) { - if (socket && !socket.destroyed) { - socket.write('QUIT\r\n'); - } - } - - // Wait for connections to close - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Destroy sockets - for (const socket of firstBatch) { - if (socket && !socket.destroyed) { - socket.destroy(); - } - } - - // Create second batch - const secondBatchPromises = []; - for (let i = 0; i < batchSize; i++) { - secondBatchPromises.push( - new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - socket.on('connect', () => { - secondBatch.push(socket); - socket.on('data', (data) => { - if (data.toString().includes('220')) { - resolve(true); - } - }); - }); - - socket.on('error', () => resolve(false)); - }) - ); - } - - const secondResults = await Promise.all(secondBatchPromises); - const secondSuccessCount = secondResults.filter(r => r).length; - - // Clean up second batch - for (const socket of secondBatch) { - if (socket && !socket.destroyed) { - socket.write('QUIT\r\n'); - setTimeout(() => socket.destroy(), 100); - } - } - - // Both batches should have successful connections - expect(firstSuccessCount).toBeGreaterThan(0); - expect(secondSuccessCount).toBeGreaterThan(0); - - done.resolve(); - - await done.promise; -}); - -// Test: Rapid connection attempts -tap.test('Connection Limits - should handle rapid connection attempts', async (tools) => { - const done = tools.defer(); - - const rapidConnections = 50; - const connections: net.Socket[] = []; - let successCount = 0; - let errorCount = 0; - - // Create connections as fast as possible - for (let i = 0; i < rapidConnections; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT - }); - - socket.on('connect', () => { - connections.push(socket); - successCount++; - }); - - socket.on('error', () => { - errorCount++; - }); - } - - // Wait for all connection attempts to settle - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Clean up - for (const socket of connections) { - if (socket && !socket.destroyed) { - socket.destroy(); - } - } - - // Should handle at least some connections - expect(successCount).toBeGreaterThan(0); - console.log(`Rapid connections: ${successCount} succeeded, ${errorCount} failed`); - - done.resolve(); - - await done.promise; -}); - -// Test: Connection limit with different client IPs (simulated) -tap.test('Connection Limits - should track connections per IP or globally', async (tools) => { - const done = tools.defer(); - - // Note: In real test, this would use different source IPs - // For now, we test from same IP but document the behavior - const connectionsPerIP = 5; - const connections: net.Socket[] = []; - const results: boolean[] = []; - - for (let i = 0; i < connectionsPerIP; i++) { - const result = await new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - socket.on('connect', () => { - connections.push(socket); - socket.on('data', (data) => { - if (data.toString().includes('220')) { - resolve(true); - } - }); - }); - - socket.on('error', () => resolve(false)); - }); - - results.push(result); - } - - const successCount = results.filter(r => r).length; - - // Clean up - for (const socket of connections) { - if (socket && !socket.destroyed) { - socket.write('QUIT\r\n'); - setTimeout(() => socket.destroy(), 100); - } - } - - // Should accept connections from same IP - expect(successCount).toBeGreaterThan(0); - console.log(`Per-IP connections: ${successCount} of ${connectionsPerIP} succeeded`); - - done.resolve(); - - await done.promise; -}); - -// Test: Connection limit error messages -tap.test('Connection Limits - should provide meaningful error when limit reached', async (tools) => { - const done = tools.defer(); - - const manyConnections = 100; - const connections: net.Socket[] = []; - const errors: string[] = []; - let rejected = false; - - // Create many connections to try to hit limit - const promises = []; - for (let i = 0; i < manyConnections; i++) { - promises.push( - new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 1000 - }); - - socket.on('connect', () => { - connections.push(socket); - - socket.on('data', (data) => { - const response = data.toString(); - // Check if server sends connection limit message - if (response.includes('421') || response.includes('too many connections')) { - rejected = true; - errors.push(response); - } - resolve(); - }); - }); - - socket.on('error', (err) => { - if (err.message.includes('ECONNREFUSED') || err.message.includes('ECONNRESET')) { - rejected = true; - errors.push(err.message); - } - resolve(); - }); - - socket.on('timeout', () => { - resolve(); - }); - }) - ); - } - - await Promise.all(promises); - - // Clean up - for (const socket of connections) { - if (socket && !socket.destroyed) { - socket.destroy(); - } - } - - // Log results - console.log(`Connection limit test: ${connections.length} connected, ${errors.length} rejected`); - if (rejected) { - console.log(`Sample rejection: ${errors[0]}`); - } - - // Should have handled connections (either accepted or properly rejected) - expect(connections.length + errors.length).toBeGreaterThan(0); - - done.resolve(); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-05.connection-rejection.ts b/test/suite/smtpserver_connection/test.cm-05.connection-rejection.ts deleted file mode 100644 index 584e065..0000000 --- a/test/suite/smtpserver_connection/test.cm-05.connection-rejection.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -const TEST_PORT = 2525; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for connection rejection tests', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('Connection Rejection - should handle suspicious domains', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - - // Send EHLO with suspicious domain - socket.write('EHLO blocked.spammer.com\r\n'); - - const response = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n')) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - - // Timeout after 5 seconds - setTimeout(() => { - socket.removeListener('data', handler); - resolve(data || 'TIMEOUT'); - }, 5000); - }); - - console.log('Response to suspicious domain:', response); - - // Server might reject with 421, 550, or accept (depends on configuration) - // We just verify it responds appropriately - const validResponses = ['250', '421', '550', '501']; - const hasValidResponse = validResponses.some(code => response.includes(code)); - expect(hasValidResponse).toEqual(true); - - // Clean up - if (!socket.destroyed) { - socket.write('QUIT\r\n'); - socket.end(); - } - - } finally { - done.resolve(); - } -}); - -tap.test('Connection Rejection - should handle overload conditions', async (tools) => { - const done = tools.defer(); - - const connections: net.Socket[] = []; - - try { - // Create many connections rapidly - const rapidConnectionCount = 20; // Reduced from 50 to be more reasonable - const connectionPromises: Promise[] = []; - - for (let i = 0; i < rapidConnectionCount; i++) { - connectionPromises.push( - new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT - }); - - socket.on('connect', () => { - connections.push(socket); - resolve(socket); - }); - - socket.on('error', () => { - // Connection rejected - this is OK during overload - resolve(null); - }); - - // Timeout individual connections - setTimeout(() => resolve(null), 2000); - }) - ); - } - - // Wait for all connection attempts - const results = await Promise.all(connectionPromises); - const successfulConnections = results.filter(r => r !== null).length; - - console.log(`Created ${successfulConnections}/${rapidConnectionCount} connections`); - - // Now try one more connection - let overloadRejected = false; - try { - const testSocket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - testSocket.once('connect', () => { - testSocket.end(); - resolve(); - }); - testSocket.once('error', (err) => { - overloadRejected = true; - reject(err); - }); - - setTimeout(() => { - testSocket.destroy(); - resolve(); - }, 5000); - }); - } catch (error) { - console.log('Additional connection was rejected:', error); - overloadRejected = true; - } - - console.log(`Overload test results: - - Successful connections: ${successfulConnections} - - Additional connection rejected: ${overloadRejected} - - Server behavior: ${overloadRejected ? 'Properly rejected under load' : 'Accepted all connections'}`); - - // Either behavior is acceptable - rejection shows overload protection, - // acceptance shows high capacity - expect(true).toEqual(true); - - } finally { - // Clean up all connections - for (const socket of connections) { - try { - if (!socket.destroyed) { - socket.end(); - } - } catch (e) { - // Ignore cleanup errors - } - } - - done.resolve(); - } -}); - -tap.test('Connection Rejection - should reject invalid protocol', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner first - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Got banner:', banner); - - // Send HTTP request instead of SMTP - socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); - - const response = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - }; - socket.on('data', handler); - - // Wait for response or connection close - socket.on('close', () => { - socket.removeListener('data', handler); - resolve(data); - }); - - // Timeout - setTimeout(() => { - socket.removeListener('data', handler); - socket.destroy(); - resolve(data || 'CLOSED_WITHOUT_RESPONSE'); - }, 3000); - }); - - console.log('Response to HTTP request:', response); - - // Server should either: - // - Send error response (500, 501, 502, 421) - // - Close connection immediately - // - Send nothing and close - const errorResponses = ['500', '501', '502', '421']; - const hasErrorResponse = errorResponses.some(code => response.includes(code)); - const closedWithoutResponse = response === 'CLOSED_WITHOUT_RESPONSE' || response === ''; - - expect(hasErrorResponse || closedWithoutResponse).toEqual(true); - - if (hasErrorResponse) { - console.log('Server properly rejected with error response'); - } else if (closedWithoutResponse) { - console.log('Server closed connection without response (also valid)'); - } - - } finally { - done.resolve(); - } -}); - -tap.test('Connection Rejection - should handle invalid commands gracefully', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send completely invalid command - socket.write('INVALID_COMMAND_12345\r\n'); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to invalid command:', response); - - // Should get 500 or 502 error - expect(response).toMatch(/^5\d{2}/); - - // Server should still be responsive - socket.write('NOOP\r\n'); - - const noopResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('NOOP response after error:', noopResponse); - expect(noopResponse).toInclude('250'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-06.starttls-upgrade.ts b/test/suite/smtpserver_connection/test.cm-06.starttls-upgrade.ts deleted file mode 100644 index 5d7a993..0000000 --- a/test/suite/smtpserver_connection/test.cm-06.starttls-upgrade.ts +++ /dev/null @@ -1,468 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as tls from 'tls'; -import * as path from 'path'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; -const TEST_TIMEOUT = 30000; // Increased timeout for TLS handshake - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server with STARTTLS support', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - tlsEnabled: true // Enable TLS to advertise STARTTLS - }); - - - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(testServer.port).toEqual(TEST_PORT); -}); - -// Test: Basic STARTTLS upgrade -tap.test('STARTTLS - should upgrade plain connection to TLS', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let tlsSocket: tls.TLSSocket | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - // Check if STARTTLS is advertised - if (receivedData.includes('STARTTLS')) { - currentStep = 'starttls'; - socket.write('STARTTLS\r\n'); - } else { - socket.destroy(); - done.reject(new Error('STARTTLS not advertised in EHLO response')); - } - } else if (currentStep === 'starttls' && receivedData.includes('220')) { - // Server accepted STARTTLS - upgrade to TLS - currentStep = 'tls_handshake'; - - const tlsOptions: tls.ConnectionOptions = { - socket: socket, - servername: 'localhost', - rejectUnauthorized: false // Accept self-signed certificates for testing - }; - - tlsSocket = tls.connect(tlsOptions); - - tlsSocket.on('secureConnect', () => { - // TLS handshake successful - currentStep = 'tls_ehlo'; - tlsSocket!.write('EHLO test.example.com\r\n'); - }); - - tlsSocket.on('data', (tlsData) => { - const tlsResponse = tlsData.toString(); - - if (currentStep === 'tls_ehlo' && tlsResponse.includes('250')) { - tlsSocket!.write('QUIT\r\n'); - setTimeout(() => { - tlsSocket!.destroy(); - expect(tlsSocket!.encrypted).toEqual(true); - done.resolve(); - }, 100); - } - }); - - tlsSocket.on('error', (error) => { - done.reject(error); - }); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - if (tlsSocket) tlsSocket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: STARTTLS with commands after upgrade -tap.test('STARTTLS - should process SMTP commands after TLS upgrade', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let tlsSocket: tls.TLSSocket | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - if (receivedData.includes('STARTTLS')) { - currentStep = 'starttls'; - socket.write('STARTTLS\r\n'); - } - } else if (currentStep === 'starttls' && receivedData.includes('220')) { - currentStep = 'tls_handshake'; - - tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false - }); - - tlsSocket.on('secureConnect', () => { - currentStep = 'tls_ehlo'; - tlsSocket!.write('EHLO test.example.com\r\n'); - }); - - tlsSocket.on('data', (tlsData) => { - const tlsResponse = tlsData.toString(); - - if (currentStep === 'tls_ehlo' && tlsResponse.includes('250')) { - currentStep = 'tls_mail_from'; - tlsSocket!.write('MAIL FROM:\r\n'); - } else if (currentStep === 'tls_mail_from' && tlsResponse.includes('250')) { - currentStep = 'tls_rcpt_to'; - tlsSocket!.write('RCPT TO:\r\n'); - } else if (currentStep === 'tls_rcpt_to' && tlsResponse.includes('250')) { - currentStep = 'tls_data'; - tlsSocket!.write('DATA\r\n'); - } else if (currentStep === 'tls_data' && tlsResponse.includes('354')) { - currentStep = 'tls_message'; - tlsSocket!.write('Subject: Test over TLS\r\n\r\nSecure message\r\n.\r\n'); - } else if (currentStep === 'tls_message' && tlsResponse.includes('250')) { - tlsSocket!.write('QUIT\r\n'); - setTimeout(() => { - const protocol = tlsSocket!.getProtocol(); - const cipher = tlsSocket!.getCipher(); - tlsSocket!.destroy(); - // Protocol and cipher might be null in some cases - if (protocol) { - expect(typeof protocol).toEqual('string'); - } - if (cipher) { - expect(cipher).toBeDefined(); - expect(cipher.name).toBeDefined(); - } - done.resolve(); - }, 100); - } - }); - - tlsSocket.on('error', (error) => { - done.reject(error); - }); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - if (tlsSocket) tlsSocket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: STARTTLS rejected after MAIL FROM -tap.test('STARTTLS - should reject STARTTLS after transaction started', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'starttls_after_mail'; - socket.write('STARTTLS\r\n'); - } else if (currentStep === 'starttls_after_mail') { - if (receivedData.includes('503')) { - // Server correctly rejected STARTTLS after MAIL FROM - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); // Bad sequence - done.resolve(); - }, 100); - } else if (receivedData.includes('220')) { - // Server incorrectly accepted STARTTLS - this is a bug - // For now, let's accept this behavior but log it - console.log('WARNING: Server accepted STARTTLS after MAIL FROM - this violates RFC 3207'); - socket.destroy(); - done.resolve(); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Multiple STARTTLS attempts -tap.test('STARTTLS - should not allow STARTTLS after TLS is established', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let tlsSocket: tls.TLSSocket | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - if (receivedData.includes('STARTTLS')) { - currentStep = 'starttls'; - socket.write('STARTTLS\r\n'); - } - } else if (currentStep === 'starttls' && receivedData.includes('220')) { - currentStep = 'tls_handshake'; - - tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false - }); - - tlsSocket.on('secureConnect', () => { - currentStep = 'tls_ehlo'; - tlsSocket!.write('EHLO test.example.com\r\n'); - }); - - tlsSocket.on('data', (tlsData) => { - const tlsResponse = tlsData.toString(); - - if (currentStep === 'tls_ehlo') { - // Check that STARTTLS is NOT advertised after TLS upgrade - expect(tlsResponse).not.toInclude('STARTTLS'); - tlsSocket!.write('QUIT\r\n'); - setTimeout(() => { - tlsSocket!.destroy(); - done.resolve(); - }, 100); - } - }); - - tlsSocket.on('error', (error) => { - done.reject(error); - }); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - if (tlsSocket) tlsSocket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: STARTTLS with invalid command -tap.test('STARTTLS - should handle commands during TLS negotiation', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - if (receivedData.includes('STARTTLS')) { - currentStep = 'starttls'; - socket.write('STARTTLS\r\n'); - } - } else if (currentStep === 'starttls' && receivedData.includes('220')) { - // Send invalid data instead of starting TLS handshake - currentStep = 'invalid_after_starttls'; - socket.write('EHLO should.not.work\r\n'); - - setTimeout(() => { - socket.destroy(); - done.resolve(); // Connection should close or timeout - }, 2000); - } - }); - - socket.on('close', () => { - if (currentStep === 'invalid_after_starttls') { - done.resolve(); - } - }); - - socket.on('error', (error) => { - if (currentStep === 'invalid_after_starttls') { - done.resolve(); // Expected error - } else { - done.reject(error); - } - }); - - socket.on('timeout', () => { - socket.destroy(); - if (currentStep === 'invalid_after_starttls') { - done.resolve(); - } else { - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - } - }); - - await done.promise; -}); - -// Test: STARTTLS TLS version and cipher info -tap.test('STARTTLS - should use secure TLS version and ciphers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let tlsSocket: tls.TLSSocket | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - if (receivedData.includes('STARTTLS')) { - currentStep = 'starttls'; - socket.write('STARTTLS\r\n'); - } - } else if (currentStep === 'starttls' && receivedData.includes('220')) { - currentStep = 'tls_handshake'; - - tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false, - minVersion: 'TLSv1.2' // Require at least TLS 1.2 - }); - - tlsSocket.on('secureConnect', () => { - const protocol = tlsSocket!.getProtocol(); - const cipher = tlsSocket!.getCipher(); - - // Verify TLS version - expect(typeof protocol).toEqual('string'); - expect(['TLSv1.2', 'TLSv1.3']).toInclude(protocol!); - - // Verify cipher info - expect(cipher).toBeDefined(); - expect(cipher.name).toBeDefined(); - expect(typeof cipher.name).toEqual('string'); - - tlsSocket!.write('QUIT\r\n'); - setTimeout(() => { - tlsSocket!.destroy(); - done.resolve(); - }, 100); - }); - - tlsSocket.on('error', (error) => { - done.reject(error); - }); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - if (tlsSocket) tlsSocket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - if (testServer) { - await stopTestServer(testServer); - } -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-07.abrupt-disconnection.ts b/test/suite/smtpserver_connection/test.cm-07.abrupt-disconnection.ts deleted file mode 100644 index 56aebdb..0000000 --- a/test/suite/smtpserver_connection/test.cm-07.abrupt-disconnection.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -const TEST_PORT = 2525; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for abrupt disconnection tests', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('Abrupt Disconnection - should handle socket destruction without QUIT', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Abruptly disconnect without QUIT - console.log('Destroying socket without QUIT...'); - socket.destroy(); - - // Wait a moment for server to handle the disconnection - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Test server recovery - try new connection - console.log('Testing server recovery with new connection...'); - const recoverySocket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - const recoveryConnected = await new Promise((resolve) => { - recoverySocket.once('connect', () => resolve(true)); - recoverySocket.once('error', () => resolve(false)); - setTimeout(() => resolve(false), 5000); - }); - - expect(recoveryConnected).toEqual(true); - - if (recoveryConnected) { - // Get banner from recovery connection - const recoveryBanner = await new Promise((resolve) => { - recoverySocket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(recoveryBanner).toInclude('220'); - console.log('Server recovered successfully, accepting new connections'); - - // Clean up recovery connection properly - recoverySocket.write('QUIT\r\n'); - recoverySocket.end(); - } - - } finally { - done.resolve(); - } -}); - -tap.test('Abrupt Disconnection - should handle multiple simultaneous abrupt disconnections', async (tools) => { - const done = tools.defer(); - - try { - const connections = 5; - const sockets: net.Socket[] = []; - - // Create multiple connections - for (let i = 0; i < connections; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - sockets.push(socket); - } - - console.log(`Created ${connections} connections`); - - // Abruptly disconnect all at once - console.log('Destroying all sockets simultaneously...'); - sockets.forEach(socket => socket.destroy()); - - // Wait for server to handle disconnections - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Test that server still accepts new connections - console.log('Testing server stability after multiple abrupt disconnections...'); - const testSocket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - const stillAccepting = await new Promise((resolve) => { - testSocket.once('connect', () => resolve(true)); - testSocket.once('error', () => resolve(false)); - setTimeout(() => resolve(false), 5000); - }); - - expect(stillAccepting).toEqual(true); - - if (stillAccepting) { - const banner = await new Promise((resolve) => { - testSocket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - console.log('Server remained stable after multiple abrupt disconnections'); - - testSocket.write('QUIT\r\n'); - testSocket.end(); - } - - } finally { - done.resolve(); - } -}); - -tap.test('Abrupt Disconnection - should handle disconnection during DATA transfer', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Start DATA - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(dataResponse).toInclude('354'); - - // Send partial email data then disconnect abruptly - socket.write('From: sender@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('Subject: Test '); - - console.log('Disconnecting during DATA transfer...'); - socket.destroy(); - - // Wait for server to handle disconnection - await new Promise(resolve => setTimeout(resolve, 1500)); - - // Verify server can handle new connections - const newSocket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - const canConnect = await new Promise((resolve) => { - newSocket.once('connect', () => resolve(true)); - newSocket.once('error', () => resolve(false)); - setTimeout(() => resolve(false), 5000); - }); - - expect(canConnect).toEqual(true); - - if (canConnect) { - const banner = await new Promise((resolve) => { - newSocket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - console.log('Server recovered from disconnection during DATA transfer'); - - newSocket.write('QUIT\r\n'); - newSocket.end(); - } - - } finally { - done.resolve(); - } -}); - -tap.test('Abrupt Disconnection - should timeout idle connections', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - console.log('Connected, now testing idle timeout...'); - - // Don't send any commands and wait for server to potentially timeout - // Most servers have a timeout of 5-10 minutes, so we'll test shorter - let disconnectedByServer = false; - - socket.on('close', () => { - disconnectedByServer = true; - }); - - socket.on('end', () => { - disconnectedByServer = true; - }); - - // Wait 10 seconds to see if server has a short idle timeout - await new Promise(resolve => setTimeout(resolve, 10000)); - - if (!disconnectedByServer) { - console.log('Server maintains idle connections (no short timeout detected)'); - // Send QUIT to close gracefully - socket.write('QUIT\r\n'); - socket.end(); - } else { - console.log('Server disconnected idle connection'); - } - - // Either behavior is acceptable - expect(true).toEqual(true); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); diff --git a/test/suite/smtpserver_connection/test.cm-08.tls-versions.ts b/test/suite/smtpserver_connection/test.cm-08.tls-versions.ts deleted file mode 100644 index 0971bc6..0000000 --- a/test/suite/smtpserver_connection/test.cm-08.tls-versions.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as tls from 'tls'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -const TEST_PORT = 2525; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server with TLS support for version tests', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - tlsEnabled: true - }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(testServer).toBeDefined(); -}); - -tap.test('TLS Versions - should support STARTTLS capability', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - console.log('EHLO response:', ehloResponse); - - // Check for STARTTLS support - const supportsStarttls = ehloResponse.includes('250-STARTTLS') || ehloResponse.includes('250 STARTTLS'); - console.log('STARTTLS supported:', supportsStarttls); - - if (supportsStarttls) { - // Test STARTTLS upgrade - socket.write('STARTTLS\r\n'); - - const starttlsResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(starttlsResponse).toInclude('220'); - console.log('STARTTLS ready response received'); - - // Would upgrade to TLS here in a real implementation - // For testing, we just verify the capability - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - // STARTTLS is optional but common - expect(true).toEqual(true); - - } finally { - done.resolve(); - } -}); - -tap.test('TLS Versions - should support modern TLS versions via STARTTLS', async (tools) => { - const done = tools.defer(); - - try { - // Test TLS 1.2 via STARTTLS - console.log('Testing TLS 1.2 support via STARTTLS...'); - const tls12Result = await testTlsVersionViaStartTls('TLSv1.2', TEST_PORT); - console.log('TLS 1.2 result:', tls12Result); - - // Test TLS 1.3 via STARTTLS - console.log('Testing TLS 1.3 support via STARTTLS...'); - const tls13Result = await testTlsVersionViaStartTls('TLSv1.3', TEST_PORT); - console.log('TLS 1.3 result:', tls13Result); - - // At least one modern version should be supported - const supportsModernTls = tls12Result.success || tls13Result.success; - expect(supportsModernTls).toEqual(true); - - if (tls12Result.success) { - console.log('TLS 1.2 supported with cipher:', tls12Result.cipher); - } - if (tls13Result.success) { - console.log('TLS 1.3 supported with cipher:', tls13Result.cipher); - } - - } finally { - done.resolve(); - } -}); - -tap.test('TLS Versions - should reject obsolete TLS versions via STARTTLS', async (tools) => { - const done = tools.defer(); - - try { - // Test TLS 1.0 (should be rejected by modern servers) - console.log('Testing TLS 1.0 (obsolete) via STARTTLS...'); - const tls10Result = await testTlsVersionViaStartTls('TLSv1', TEST_PORT); - - // Test TLS 1.1 (should be rejected by modern servers) - console.log('Testing TLS 1.1 (obsolete) via STARTTLS...'); - const tls11Result = await testTlsVersionViaStartTls('TLSv1.1', TEST_PORT); - - // Modern servers should reject these old versions - // But some might still support them for compatibility - console.log(`TLS 1.0 ${tls10Result.success ? 'accepted (legacy support)' : 'rejected (good)'}`); - console.log(`TLS 1.1 ${tls11Result.success ? 'accepted (legacy support)' : 'rejected (good)'}`); - - // Either behavior is acceptable - log the results - expect(true).toEqual(true); - - } finally { - done.resolve(); - } -}); - -tap.test('TLS Versions - should provide cipher information via STARTTLS', async (tools) => { - const done = tools.defer(); - - try { - // Connect to plain SMTP port - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS - if (!ehloResponse.includes('STARTTLS')) { - console.log('Server does not support STARTTLS - skipping cipher info test'); - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - return; - } - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Upgrade to TLS - const tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false - }); - - await new Promise((resolve, reject) => { - tlsSocket.once('secureConnect', () => resolve()); - tlsSocket.once('error', reject); - }); - - // Get connection details - const cipher = tlsSocket.getCipher(); - const protocol = tlsSocket.getProtocol(); - const authorized = tlsSocket.authorized; - - console.log('TLS connection established via STARTTLS:'); - console.log('- Protocol:', protocol); - console.log('- Cipher:', cipher?.name); - console.log('- Key exchange:', cipher?.standardName); - console.log('- Authorized:', authorized); - - if (protocol) { - expect(typeof protocol).toEqual('string'); - } - if (cipher) { - expect(cipher.name).toBeDefined(); - } - - // Clean up - tlsSocket.write('QUIT\r\n'); - tlsSocket.end(); - - } finally { - done.resolve(); - } -}); - -// Helper function to test specific TLS version via STARTTLS -async function testTlsVersionViaStartTls(version: string, port: number): Promise<{success: boolean, cipher?: any, error?: string}> { - return new Promise(async (resolve) => { - try { - // Connect to plain SMTP port - const socket = net.createConnection({ - host: 'localhost', - port: port, - timeout: 5000 - }); - - await new Promise((socketResolve, socketReject) => { - socket.once('connect', () => socketResolve()); - socket.once('error', socketReject); - }); - - // Get banner - await new Promise((bannerResolve) => { - socket.once('data', (chunk) => bannerResolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((ehloResolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - ehloResolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS - if (!ehloResponse.includes('STARTTLS')) { - socket.destroy(); - resolve({ - success: false, - error: 'STARTTLS not supported' - }); - return; - } - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - - await new Promise((starttlsResolve) => { - socket.once('data', (chunk) => starttlsResolve(chunk.toString())); - }); - - // Set up TLS options with version constraints - const tlsOptions: any = { - socket: socket, - servername: 'localhost', - rejectUnauthorized: false - }; - - // Set version constraints based on requested version - switch (version) { - case 'TLSv1': - tlsOptions.minVersion = 'TLSv1'; - tlsOptions.maxVersion = 'TLSv1'; - break; - case 'TLSv1.1': - tlsOptions.minVersion = 'TLSv1.1'; - tlsOptions.maxVersion = 'TLSv1.1'; - break; - case 'TLSv1.2': - tlsOptions.minVersion = 'TLSv1.2'; - tlsOptions.maxVersion = 'TLSv1.2'; - break; - case 'TLSv1.3': - tlsOptions.minVersion = 'TLSv1.3'; - tlsOptions.maxVersion = 'TLSv1.3'; - break; - } - - // Upgrade to TLS - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.once('secureConnect', () => { - const cipher = tlsSocket.getCipher(); - const protocol = tlsSocket.getProtocol(); - - tlsSocket.destroy(); - resolve({ - success: true, - cipher: { - name: cipher?.name, - standardName: cipher?.standardName, - protocol: protocol - } - }); - }); - - tlsSocket.once('error', (error) => { - resolve({ - success: false, - error: error.message - }); - }); - - setTimeout(() => { - tlsSocket.destroy(); - resolve({ - success: false, - error: 'TLS handshake timeout' - }); - }, 5000); - - } catch (error) { - resolve({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }); - } - }); -} - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); diff --git a/test/suite/smtpserver_connection/test.cm-09.tls-ciphers.ts b/test/suite/smtpserver_connection/test.cm-09.tls-ciphers.ts deleted file mode 100644 index cebf9fc..0000000 --- a/test/suite/smtpserver_connection/test.cm-09.tls-ciphers.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as tls from 'tls'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer: ITestServer; -const TEST_TIMEOUT = 30000; - -tap.test('TLS Ciphers - should advertise STARTTLS for cipher negotiation', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: true }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS support - const supportsStarttls = ehloResponse.includes('250-STARTTLS') || ehloResponse.includes('250 STARTTLS'); - console.log('STARTTLS supported:', supportsStarttls); - - if (supportsStarttls) { - console.log('Server supports STARTTLS - cipher negotiation available'); - } else { - console.log('Server does not advertise STARTTLS - direct TLS connections may be required'); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - // Either behavior is acceptable - expect(true).toEqual(true); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('TLS Ciphers - should negotiate secure cipher suites via STARTTLS', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: true }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS - if (!ehloResponse.includes('STARTTLS')) { - console.log('Server does not support STARTTLS - skipping cipher test'); - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - return; - } - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - - const starttlsResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(starttlsResponse).toInclude('220'); - - // Upgrade to TLS - const tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false - }); - - await new Promise((resolve, reject) => { - tlsSocket.once('secureConnect', () => resolve()); - tlsSocket.once('error', reject); - }); - - // Get cipher information - const cipher = tlsSocket.getCipher(); - console.log('Negotiated cipher suite:'); - console.log('- Name:', cipher.name); - console.log('- Standard name:', cipher.standardName); - console.log('- Version:', cipher.version); - - // Check cipher security - const cipherSecurity = checkCipherSecurity(cipher); - console.log('Cipher security analysis:', cipherSecurity); - - expect(cipher.name).toBeDefined(); - expect(cipherSecurity.secure).toEqual(true); - - // Clean up - tlsSocket.write('QUIT\r\n'); - tlsSocket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('TLS Ciphers - should reject weak cipher suites', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: true }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS - if (!ehloResponse.includes('STARTTLS')) { - console.log('Server does not support STARTTLS - skipping weak cipher test'); - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - return; - } - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Try to connect with weak ciphers only - const weakCiphers = [ - 'DES-CBC3-SHA', - 'RC4-MD5', - 'RC4-SHA', - 'NULL-SHA', - 'EXPORT-DES40-CBC-SHA' - ]; - - console.log('Testing connection with weak ciphers only...'); - - const connectionResult = await new Promise<{success: boolean, error?: string}>((resolve) => { - const tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false, - ciphers: weakCiphers.join(':') - }); - - tlsSocket.once('secureConnect', () => { - // If connection succeeds, server accepts weak ciphers - const cipher = tlsSocket.getCipher(); - tlsSocket.destroy(); - resolve({ - success: true, - error: `Server accepted weak cipher: ${cipher.name}` - }); - }); - - tlsSocket.once('error', (err) => { - // Connection failed - good, server rejects weak ciphers - resolve({ - success: false, - error: err.message - }); - }); - - setTimeout(() => { - tlsSocket.destroy(); - resolve({ - success: false, - error: 'Connection timeout' - }); - }, 5000); - }); - - if (!connectionResult.success) { - console.log('Good: Server rejected weak ciphers'); - } else { - console.log('Warning:', connectionResult.error); - } - - // Either behavior is logged - some servers may support legacy ciphers - expect(true).toEqual(true); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('TLS Ciphers - should support forward secrecy', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: true }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS - if (!ehloResponse.includes('STARTTLS')) { - console.log('Server does not support STARTTLS - skipping forward secrecy test'); - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - return; - } - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Prefer ciphers with forward secrecy (ECDHE, DHE) - const forwardSecrecyCiphers = [ - 'ECDHE-RSA-AES128-GCM-SHA256', - 'ECDHE-RSA-AES256-GCM-SHA384', - 'ECDHE-ECDSA-AES128-GCM-SHA256', - 'ECDHE-ECDSA-AES256-GCM-SHA384', - 'DHE-RSA-AES128-GCM-SHA256', - 'DHE-RSA-AES256-GCM-SHA384' - ]; - - const tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false, - ciphers: forwardSecrecyCiphers.join(':') - }); - - await new Promise((resolve, reject) => { - tlsSocket.once('secureConnect', () => resolve()); - tlsSocket.once('error', reject); - setTimeout(() => reject(new Error('TLS connection timeout')), 5000); - }); - - const cipher = tlsSocket.getCipher(); - console.log('Forward secrecy cipher negotiated:', cipher.name); - - // Check if cipher provides forward secrecy - const hasForwardSecrecy = cipher.name.includes('ECDHE') || cipher.name.includes('DHE'); - console.log('Forward secrecy:', hasForwardSecrecy ? 'YES' : 'NO'); - - if (hasForwardSecrecy) { - console.log('Good: Server supports forward secrecy'); - } else { - console.log('Warning: Negotiated cipher does not provide forward secrecy'); - } - - // Clean up - tlsSocket.write('QUIT\r\n'); - tlsSocket.end(); - - // Forward secrecy is recommended but not required - expect(true).toEqual(true); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('TLS Ciphers - should list all supported ciphers', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: true }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - // Get list of ciphers supported by Node.js - const supportedCiphers = tls.getCiphers(); - console.log(`Node.js supports ${supportedCiphers.length} cipher suites`); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check for STARTTLS - if (!ehloResponse.includes('STARTTLS')) { - console.log('Server does not support STARTTLS - skipping cipher list test'); - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - return; - } - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Test connection with default ciphers - const tlsSocket = tls.connect({ - socket: socket, - servername: 'localhost', - rejectUnauthorized: false - }); - - await new Promise((resolve, reject) => { - tlsSocket.once('secureConnect', () => resolve()); - tlsSocket.once('error', reject); - setTimeout(() => reject(new Error('TLS connection timeout')), 5000); - }); - - const negotiatedCipher = tlsSocket.getCipher(); - console.log('\nServer selected cipher:', negotiatedCipher.name); - - // Categorize the cipher - const categories = { - 'AEAD': negotiatedCipher.name.includes('GCM') || negotiatedCipher.name.includes('CCM') || negotiatedCipher.name.includes('POLY1305'), - 'Forward Secrecy': negotiatedCipher.name.includes('ECDHE') || negotiatedCipher.name.includes('DHE'), - 'Strong Encryption': negotiatedCipher.name.includes('AES') && (negotiatedCipher.name.includes('128') || negotiatedCipher.name.includes('256')) - }; - - console.log('Cipher properties:'); - Object.entries(categories).forEach(([property, value]) => { - console.log(`- ${property}: ${value ? 'YES' : 'NO'}`); - }); - - // Clean up - tlsSocket.end(); - - expect(negotiatedCipher.name).toBeDefined(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -// Helper function to check cipher security -function checkCipherSecurity(cipher: any): {secure: boolean, reason?: string, recommendations?: string[]} { - if (!cipher || !cipher.name) { - return { - secure: false, - reason: 'No cipher information available' - }; - } - - const cipherName = cipher.name.toUpperCase(); - const recommendations: string[] = []; - - // Check for insecure ciphers - const insecureCiphers = ['NULL', 'EXPORT', 'DES', '3DES', 'RC4', 'MD5']; - - for (const insecure of insecureCiphers) { - if (cipherName.includes(insecure)) { - return { - secure: false, - reason: `Insecure cipher detected: ${insecure} in ${cipherName}`, - recommendations: ['Use AEAD ciphers like AES-GCM or ChaCha20-Poly1305'] - }; - } - } - - // Check for recommended secure ciphers - const secureCiphers = [ - 'AES128-GCM', 'AES256-GCM', 'CHACHA20-POLY1305', - 'AES128-CCM', 'AES256-CCM' - ]; - - const hasSecureCipher = secureCiphers.some(secure => - cipherName.includes(secure.replace('-', '_')) || cipherName.includes(secure) - ); - - if (hasSecureCipher) { - return { - secure: true, - recommendations: ['Cipher suite is considered secure'] - }; - } - - // Check for acceptable but not ideal ciphers - if (cipherName.includes('AES') && !cipherName.includes('CBC')) { - return { - secure: true, - recommendations: ['Consider upgrading to AEAD ciphers for better security'] - }; - } - - // Check for weak but sometimes acceptable ciphers - if (cipherName.includes('AES') && cipherName.includes('CBC')) { - recommendations.push('CBC mode ciphers are vulnerable to padding oracle attacks'); - recommendations.push('Consider upgrading to GCM or other AEAD modes'); - return { - secure: true, // Still acceptable but not ideal - recommendations: recommendations - }; - } - - // Default to secure if it's a modern cipher we don't recognize - return { - secure: true, - recommendations: [`Unknown cipher ${cipherName} - verify security manually`] - }; -} - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-10.plain-connection.ts b/test/suite/smtpserver_connection/test.cm-10.plain-connection.ts deleted file mode 100644 index 611213c..0000000 --- a/test/suite/smtpserver_connection/test.cm-10.plain-connection.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer: ITestServer; -const TEST_TIMEOUT = 30000; - -tap.test('Plain Connection - should establish basic TCP connection', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - const connected = await new Promise((resolve) => { - socket.once('connect', () => resolve(true)); - socket.once('error', () => resolve(false)); - setTimeout(() => resolve(false), 5000); - }); - - expect(connected).toEqual(true); - - if (connected) { - console.log('Plain connection established:'); - console.log('- Local:', `${socket.localAddress}:${socket.localPort}`); - console.log('- Remote:', `${socket.remoteAddress}:${socket.remotePort}`); - - // Close connection - socket.destroy(); - } - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Plain Connection - should receive SMTP banner on plain connection', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Received banner:', banner.trim()); - - expect(banner).toInclude('220'); - expect(banner).toInclude('ESMTP'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Plain Connection - should complete full SMTP transaction on plain connection', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - expect(ehloResponse).toInclude('250'); - console.log('EHLO successful on plain connection'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(mailResponse).toInclude('250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(rcptResponse).toInclude('250'); - - // Send DATA - socket.write('DATA\r\n'); - - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(dataResponse).toInclude('354'); - - // Send email content - const emailContent = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: Plain Connection Test\r\n' + - '\r\n' + - 'This email was sent over a plain connection.\r\n' + - '.\r\n'; - - socket.write(emailContent); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(finalResponse).toInclude('250'); - console.log('Email sent successfully over plain connection'); - - // Clean up - socket.write('QUIT\r\n'); - - const quitResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(quitResponse).toInclude('221'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Plain Connection - should handle multiple plain connections', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const connectionCount = 3; - const connections: net.Socket[] = []; - - // Create multiple connections - for (let i = 0; i < connectionCount; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => { - connections.push(socket); - resolve(); - }); - socket.once('error', reject); - }); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - console.log(`Connection ${i + 1} established`); - } - - expect(connections.length).toEqual(connectionCount); - console.log(`All ${connectionCount} plain connections established successfully`); - - // Clean up all connections - for (const socket of connections) { - socket.write('QUIT\r\n'); - socket.end(); - } - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Plain Connection - should work on standard SMTP port 25', async (tools) => { - const done = tools.defer(); - - // Test port 25 (standard SMTP port) - const SMTP_PORT = 25; - - // Note: Port 25 might require special permissions or might be blocked - // We'll test the connection but handle failures gracefully - const socket = net.createConnection({ - host: 'localhost', - port: SMTP_PORT, - timeout: 5000 - }); - - const result = await new Promise<{connected: boolean, error?: string}>((resolve) => { - socket.once('connect', () => { - socket.destroy(); - resolve({ connected: true }); - }); - - socket.once('error', (err) => { - resolve({ - connected: false, - error: err.message - }); - }); - - setTimeout(() => { - socket.destroy(); - resolve({ - connected: false, - error: 'Connection timeout' - }); - }, 5000); - }); - - if (result.connected) { - console.log('Successfully connected to port 25 (standard SMTP)'); - } else { - console.log(`Could not connect to port 25: ${result.error}`); - console.log('This is expected if port 25 is blocked or requires privileges'); - } - - // Test passes regardless - port 25 connectivity is environment-dependent - expect(true).toEqual(true); - - done.resolve(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_connection/test.cm-11.keepalive.ts b/test/suite/smtpserver_connection/test.cm-11.keepalive.ts deleted file mode 100644 index bc23c96..0000000 --- a/test/suite/smtpserver_connection/test.cm-11.keepalive.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer: ITestServer; -const TEST_TIMEOUT = 60000; // Longer timeout for keepalive tests - -tap.test('Keepalive - should maintain TCP keepalive', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Enable TCP keepalive - const keepAliveDelay = 1000; // 1 second - socket.setKeepAlive(true, keepAliveDelay); - console.log(`TCP keepalive enabled with ${keepAliveDelay}ms delay`); - - // Get banner - const banner = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(banner).toInclude('220'); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - expect(ehloResponse).toInclude('250'); - - // Wait for keepalive duration + buffer - console.log('Waiting for keepalive period...'); - await new Promise(resolve => setTimeout(resolve, keepAliveDelay + 500)); - - // Verify connection is still alive by sending NOOP - socket.write('NOOP\r\n'); - - const noopResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(noopResponse).toInclude('250'); - console.log('Connection maintained after keepalive period'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Keepalive - should maintain idle connection for extended period', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Enable keepalive - socket.setKeepAlive(true, 1000); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Test multiple keepalive periods - const periods = 3; - const periodDuration = 1000; // 1 second each - - for (let i = 0; i < periods; i++) { - console.log(`Keepalive period ${i + 1}/${periods}...`); - await new Promise(resolve => setTimeout(resolve, periodDuration)); - - // Send NOOP to verify connection - socket.write('NOOP\r\n'); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(response).toInclude('250'); - console.log(`Connection alive after ${(i + 1) * periodDuration}ms`); - } - - console.log(`Connection maintained for ${periods * periodDuration}ms total`); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Keepalive - should detect connection loss', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Enable keepalive with short interval - socket.setKeepAlive(true, 1000); - - // Track connection state - let connectionLost = false; - socket.on('close', () => { - connectionLost = true; - console.log('Connection closed'); - }); - - socket.on('error', (err) => { - connectionLost = true; - console.log('Connection error:', err.message); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - console.log('Connection established, now simulating server shutdown...'); - - // Shutdown server to simulate connection loss - await stopTestServer(testServer); - - // Wait for keepalive to detect connection loss - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Connection should be detected as lost - expect(connectionLost).toEqual(true); - console.log('Keepalive detected connection loss'); - - } finally { - // Server already shutdown, just resolve - done.resolve(); - } -}); - -tap.test('Keepalive - should handle long-running SMTP session', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Enable keepalive - socket.setKeepAlive(true, 2000); - - const sessionStart = Date.now(); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Simulate a long-running session with periodic activity - const activities = [ - { command: 'MAIL FROM:', delay: 500 }, - { command: 'RSET', delay: 500 }, - { command: 'MAIL FROM:', delay: 500 }, - { command: 'RSET', delay: 500 } - ]; - - for (const activity of activities) { - await new Promise(resolve => setTimeout(resolve, activity.delay)); - - console.log(`Sending: ${activity.command}`); - socket.write(`${activity.command}\r\n`); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(response).toInclude('250'); - } - - const sessionDuration = Date.now() - sessionStart; - console.log(`Long-running session maintained for ${sessionDuration}ms`); - - // Clean up - socket.write('QUIT\r\n'); - - const quitResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(quitResponse).toInclude('221'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Keepalive - should handle NOOP as keepalive mechanism', async (tools) => { - const done = tools.defer(); - - // Start test server - testServer = await startTestServer({ port: TEST_PORT }); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Use NOOP as application-level keepalive - const noopInterval = 1000; // 1 second - const noopCount = 3; - - console.log(`Sending ${noopCount} NOOP commands as keepalive...`); - - for (let i = 0; i < noopCount; i++) { - await new Promise(resolve => setTimeout(resolve, noopInterval)); - - socket.write('NOOP\r\n'); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(response).toInclude('250'); - console.log(`NOOP ${i + 1}/${noopCount} successful`); - } - - console.log('Application-level keepalive successful'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-01.very-large-email.ts b/test/suite/smtpserver_edge-cases/test.edge-01.very-large-email.ts deleted file mode 100644 index 962961e..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-01.very-large-email.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -import { connectToSmtp, waitForGreeting, sendSmtpCommand, closeSmtpConnection, generateRandomEmail } from '../../helpers/utils.js'; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server with large size limit', async () => { - testServer = await startTestServer({ - port: 2532, - hostname: 'localhost', - size: 100 * 1024 * 1024 // 100MB limit for testing - }); - expect(testServer).toBeInstanceOf(Object); -}); - -tap.test('EDGE-01: Very Large Email - test size limits and handling', async () => { - const testCases = [ - { size: 1 * 1024 * 1024, label: '1MB', shouldPass: true }, - { size: 10 * 1024 * 1024, label: '10MB', shouldPass: true }, - { size: 50 * 1024 * 1024, label: '50MB', shouldPass: true }, - { size: 101 * 1024 * 1024, label: '101MB', shouldPass: false } // Over limit - ]; - - for (const testCase of testCases) { - console.log(`\n📧 Testing ${testCase.label} email...`); - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Check SIZE extension - await sendSmtpCommand(socket, `MAIL FROM: SIZE=${testCase.size}`, - testCase.shouldPass ? '250' : '552'); - - if (testCase.shouldPass) { - // Continue with transaction - await sendSmtpCommand(socket, 'RCPT TO:', '250'); - await sendSmtpCommand(socket, 'DATA', '354'); - - // Send large content in chunks - const chunkSize = 65536; // 64KB chunks - const totalChunks = Math.ceil(testCase.size / chunkSize); - - console.log(` Sending ${totalChunks} chunks...`); - - // Headers - socket.write('From: large@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write(`Subject: ${testCase.label} Test Email\r\n`); - socket.write('Content-Type: text/plain\r\n'); - socket.write('\r\n'); - - // Body in chunks - let bytesSent = 100; // Approximate header size - const startTime = Date.now(); - - for (let i = 0; i < totalChunks; i++) { - const chunk = generateRandomEmail(Math.min(chunkSize, testCase.size - bytesSent)); - socket.write(chunk); - bytesSent += chunk.length; - - // Progress indicator every 10% - if (i % Math.floor(totalChunks / 10) === 0) { - const progress = (i / totalChunks * 100).toFixed(0); - console.log(` Progress: ${progress}%`); - } - - // Small delay to avoid overwhelming - if (i % 100 === 0) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - } - - // End of data - socket.write('\r\n.\r\n'); - - // Wait for response with longer timeout for large emails - const response = await new Promise((resolve, reject) => { - let buffer = ''; - const timeout = setTimeout(() => reject(new Error('Timeout')), 60000); - - const onData = (data: Buffer) => { - buffer += data.toString(); - if (buffer.includes('250') || buffer.includes('5')) { - clearTimeout(timeout); - socket.removeListener('data', onData); - resolve(buffer); - } - }; - - socket.on('data', onData); - }); - - const duration = Date.now() - startTime; - const throughputMBps = (testCase.size / 1024 / 1024) / (duration / 1000); - - expect(response).toInclude('250'); - console.log(` ✅ ${testCase.label} email accepted in ${duration}ms`); - console.log(` Throughput: ${throughputMBps.toFixed(2)} MB/s`); - - } else { - console.log(` ✅ ${testCase.label} email properly rejected (over size limit)`); - } - - } catch (error) { - if (!testCase.shouldPass && error.message.includes('552')) { - console.log(` ✅ ${testCase.label} email properly rejected: ${error.message}`); - } else { - throw error; - } - } finally { - await closeSmtpConnection(socket).catch(() => {}); - } - } -}); - -tap.test('EDGE-01: Email size enforcement - SIZE parameter', async () => { - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - const ehloResponse = await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Extract SIZE limit from capabilities - const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/); - const sizeLimit = sizeMatch ? parseInt(sizeMatch[1]) : 0; - - console.log(`📏 Server advertises SIZE limit: ${sizeLimit} bytes`); - expect(sizeLimit).toBeGreaterThan(0); - - // Test SIZE parameter enforcement - const testSizes = [ - { size: 1000, shouldPass: true }, - { size: sizeLimit - 1000, shouldPass: true }, - { size: sizeLimit + 1000, shouldPass: false } - ]; - - for (const test of testSizes) { - try { - const response = await sendSmtpCommand( - socket, - `MAIL FROM: SIZE=${test.size}` - ); - - if (test.shouldPass) { - expect(response).toInclude('250'); - console.log(` ✅ SIZE=${test.size} accepted`); - await sendSmtpCommand(socket, 'RSET', '250'); - } else { - expect(response).toInclude('552'); - console.log(` ✅ SIZE=${test.size} rejected`); - } - } catch (error) { - if (!test.shouldPass) { - console.log(` ✅ SIZE=${test.size} rejected: ${error.message}`); - } else { - throw error; - } - } - } - - } finally { - await closeSmtpConnection(socket); - } -}); - -tap.test('EDGE-01: Memory efficiency with large emails', async () => { - // Get initial memory usage - const initialMemory = process.memoryUsage(); - console.log('📊 Initial memory usage:', { - heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`, - rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)} MB` - }); - - // Send a moderately large email - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - await sendSmtpCommand(socket, 'MAIL FROM:', '250'); - await sendSmtpCommand(socket, 'RCPT TO:', '250'); - await sendSmtpCommand(socket, 'DATA', '354'); - - // Send 20MB email - const size = 20 * 1024 * 1024; - const chunkSize = 1024 * 1024; // 1MB chunks - - socket.write('From: memory@test.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('Subject: Memory Test\r\n\r\n'); - - for (let i = 0; i < size / chunkSize; i++) { - socket.write(generateRandomEmail(chunkSize)); - // Force garbage collection if available - if (global.gc) { - global.gc(); - } - } - - socket.write('\r\n.\r\n'); - - // Wait for response - await new Promise((resolve) => { - const onData = (data: Buffer) => { - if (data.toString().includes('250')) { - socket.removeListener('data', onData); - resolve(); - } - }; - socket.on('data', onData); - }); - - // Check memory after processing - const finalMemory = process.memoryUsage(); - const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024; - - console.log('📊 Final memory usage:', { - heapUsed: `${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`, - rss: `${(finalMemory.rss / 1024 / 1024).toFixed(2)} MB`, - increase: `${memoryIncrease.toFixed(2)} MB` - }); - - // Memory increase should be reasonable (not storing entire email in memory) - expect(memoryIncrease).toBeLessThan(50); // Less than 50MB increase for 20MB email - console.log('✅ Memory efficiency test passed'); - - } finally { - await closeSmtpConnection(socket); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - console.log('✅ Test server stopped'); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-02.very-small-email.ts b/test/suite/smtpserver_edge-cases/test.edge-02.very-small-email.ts deleted file mode 100644 index 56e958d..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-02.very-small-email.ts +++ /dev/null @@ -1,389 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30034; -const TEST_TIMEOUT = 30000; - -tap.test('Very Small Email - should handle minimal email with single character body', async (tools) => { - const done = tools.defer(); - - // Start test server - const testServer = await startTestServer({ port: TEST_PORT }); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(mailResponse).toInclude('250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(rcptResponse).toInclude('250'); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(dataResponse).toInclude('354'); - - // Send minimal email - just required headers and single character body - const minimalEmail = 'From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: \r\n\r\nX\r\n.\r\n'; - socket.write(minimalEmail); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(finalResponse).toInclude('250'); - console.log(`Minimal email (${minimalEmail.length} bytes) processed successfully`); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Very Small Email - should handle email with empty body', async (tools) => { - const done = tools.defer(); - - // Start test server - const testServer = await startTestServer({ port: TEST_PORT }); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Complete envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send email with empty body - const emptyBodyEmail = 'From: sender@example.com\r\nTo: recipient@example.com\r\n\r\n.\r\n'; - socket.write(emptyBodyEmail); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(finalResponse).toInclude('250'); - console.log('Email with empty body processed successfully'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Very Small Email - should handle email with minimal headers only', async (tools) => { - const done = tools.defer(); - - // Start test server - const testServer = await startTestServer({ port: TEST_PORT }); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner and send EHLO - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Complete envelope - use valid email addresses - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send absolutely minimal valid email - const minimalHeaders = 'From: a@example.com\r\n\r\n.\r\n'; - socket.write(minimalHeaders); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(finalResponse).toInclude('250'); - console.log(`Ultra-minimal email (${minimalHeaders.length} bytes) processed`); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Very Small Email - should handle single dot line correctly', async (tools) => { - const done = tools.defer(); - - // Start test server - const testServer = await startTestServer({ port: TEST_PORT }); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Complete envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(dataResponse).toInclude('354'); - - // Test edge case: just the terminating dot - socket.write('.\r\n'); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Server should accept this as an email with no headers or body - expect(finalResponse).toMatch(/^[2-5]\d{2}/); - console.log('Single dot terminator handled:', finalResponse.trim()); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -tap.test('Very Small Email - should handle email with empty subject', async (tools) => { - const done = tools.defer(); - - // Start test server - const testServer = await startTestServer({ port: TEST_PORT }); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Complete envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send email with empty subject line - const emptySubjectEmail = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: \r\n' + - 'Date: ' + new Date().toUTCString() + '\r\n' + - '\r\n' + - 'Email with empty subject.\r\n' + - '.\r\n'; - - socket.write(emptySubjectEmail); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(finalResponse).toInclude('250'); - console.log('Email with empty subject processed successfully'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - await stopTestServer(testServer); - done.resolve(); - } -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-03.invalid-character-handling.ts b/test/suite/smtpserver_edge-cases/test.edge-03.invalid-character-handling.ts deleted file mode 100644 index 210a6e2..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-03.invalid-character-handling.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30035; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for invalid character tests', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - hostname: 'localhost' - }); - expect(testServer).toBeDefined(); -}); - -tap.test('Invalid Character Handling - should handle control characters in email', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(dataResponse).toInclude('354'); - - // Test with control characters - const controlChars = [ - '\x00', // NULL - '\x01', // SOH - '\x02', // STX - '\x03', // ETX - '\x7F' // DEL - ]; - - const emailWithControlChars = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - `Subject: Control Character Test ${controlChars.join('')}\r\n` + - '\r\n' + - `This email contains control characters: ${controlChars.join('')}\r\n` + - 'Null byte: \x00\r\n' + - 'Delete char: \x7F\r\n' + - '.\r\n'; - - socket.write(emailWithControlChars); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to control characters:', finalResponse); - - // Server might accept or reject based on security settings - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('550') || finalResponse.includes('554'); - - expect(accepted || rejected).toEqual(true); - - if (rejected) { - console.log('Server rejected control characters (strict security)'); - } else { - console.log('Server accepted control characters (may sanitize internally)'); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Invalid Character Handling - should handle high-byte characters', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Test with high-byte characters - const highByteChars = [ - '\xFF', // 255 - '\xFE', // 254 - '\xFD', // 253 - '\xFC', // 252 - '\xFB' // 251 - ]; - - const emailWithHighBytes = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: High-byte Character Test\r\n' + - '\r\n' + - `High-byte characters: ${highByteChars.join('')}\r\n` + - 'Extended ASCII: \xE0\xE1\xE2\xE3\xE4\r\n' + - '.\r\n'; - - socket.write(emailWithHighBytes); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to high-byte characters:', finalResponse); - - // Both acceptance and rejection are valid - expect(finalResponse).toMatch(/^[2-5]\d{2}/); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Invalid Character Handling - should handle Unicode special characters', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Test with Unicode special characters - const unicodeSpecials = [ - '\u2000', // EN QUAD - '\u2028', // LINE SEPARATOR - '\u2029', // PARAGRAPH SEPARATOR - '\uFEFF', // ZERO WIDTH NO-BREAK SPACE (BOM) - '\u200B', // ZERO WIDTH SPACE - '\u200C', // ZERO WIDTH NON-JOINER - '\u200D' // ZERO WIDTH JOINER - ]; - - const emailWithUnicode = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: Unicode Special Characters Test\r\n' + - 'Content-Type: text/plain; charset=utf-8\r\n' + - '\r\n' + - `Unicode specials: ${unicodeSpecials.join('')}\r\n` + - 'Line separator: \u2028\r\n' + - 'Paragraph separator: \u2029\r\n' + - 'Zero-width space: word\u200Bword\r\n' + - '.\r\n'; - - socket.write(emailWithUnicode); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to Unicode special characters:', finalResponse); - - // Most servers should accept Unicode with proper charset declaration - expect(finalResponse).toMatch(/^[2-5]\d{2}/); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Invalid Character Handling - should handle bare LF and CR', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Test with bare LF and CR (not allowed in SMTP) - const emailWithBareLfCr = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: Bare LF and CR Test\r\n' + - '\r\n' + - 'Line with bare LF:\nThis should not be allowed\r\n' + - 'Line with bare CR:\rThis should also not be allowed\r\n' + - 'Correct line ending\r\n' + - '.\r\n'; - - socket.write(emailWithBareLfCr); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to bare LF/CR:', finalResponse); - - // Servers may accept and fix, or reject - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('550') || finalResponse.includes('554'); - - if (accepted) { - console.log('Server accepted bare LF/CR (may convert to CRLF)'); - } else if (rejected) { - console.log('Server rejected bare LF/CR (strict SMTP compliance)'); - } - - expect(accepted || rejected).toEqual(true); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Invalid Character Handling - should handle long lines without proper folding', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Create a line that exceeds RFC 5322 limit (998 characters) - const longLine = 'X'.repeat(1500); - - const emailWithLongLine = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: Long Line Test\r\n' + - '\r\n' + - 'Normal line\r\n' + - longLine + '\r\n' + - 'Another normal line\r\n' + - '.\r\n'; - - socket.write(emailWithLongLine); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to long line:', finalResponse); - console.log(`Line length: ${longLine.length} characters`); - - // Server should handle this (accept, wrap, or reject) - expect(finalResponse).toMatch(/^[2-5]\d{2}/); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-04.empty-commands.ts b/test/suite/smtpserver_edge-cases/test.edge-04.empty-commands.ts deleted file mode 100644 index 4c423f1..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-04.empty-commands.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30036; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for empty command tests', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - hostname: 'localhost' - }); - expect(testServer).toBeDefined(); -}); - -tap.test('Empty Commands - should reject empty line (just CRLF)', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO first - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send empty line (just CRLF) - socket.write('\r\n'); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - setTimeout(() => resolve('TIMEOUT'), 2000); - }); - - console.log('Response to empty line:', response); - - // Should get syntax error (500, 501, or 502) - if (response !== 'TIMEOUT') { - expect(response).toMatch(/^5\d{2}/); - } else { - // Server might ignore empty lines - console.log('Server ignored empty line'); - expect(true).toEqual(true); - } - - // Test server is still responsive - socket.write('NOOP\r\n'); - const noopResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(noopResponse).toInclude('250'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Empty Commands - should reject commands with only whitespace', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner and send EHLO - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Test various whitespace-only commands - const whitespaceCommands = [ - ' \r\n', // Spaces only - '\t\r\n', // Tab only - ' \t \r\n', // Mixed whitespace - ' \r\n' // Multiple spaces - ]; - - for (const cmd of whitespaceCommands) { - socket.write(cmd); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - setTimeout(() => resolve('TIMEOUT'), 2000); - }); - - console.log(`Response to whitespace "${cmd.trim()}"\\r\\n:`, response); - - if (response !== 'TIMEOUT') { - // Should get syntax error - expect(response).toMatch(/^5\d{2}/); - } - } - - // Verify server still works - socket.write('NOOP\r\n'); - const noopResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(noopResponse).toInclude('250'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Empty Commands - should reject MAIL FROM with empty parameter', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM with empty parameter - socket.write('MAIL FROM:\r\n'); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to empty MAIL FROM:', response); - - // Should get syntax error (501 or 550) - expect(response).toMatch(/^5\d{2}/); - expect(response).toMatch(/syntax|parameter|address/i); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Empty Commands - should reject RCPT TO with empty parameter', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send valid MAIL FROM first - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send RCPT TO with empty parameter - socket.write('RCPT TO:\r\n'); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to empty RCPT TO:', response); - - // Should get syntax error - expect(response).toMatch(/^5\d{2}/); - expect(response).toMatch(/syntax|parameter|address/i); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Empty Commands - should reject EHLO/HELO without hostname', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO without hostname - socket.write('EHLO\r\n'); - - const ehloResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to EHLO without hostname:', ehloResponse); - - // Should get syntax error - expect(ehloResponse).toMatch(/^5\d{2}/); - - // Try HELO without hostname - socket.write('HELO\r\n'); - - const heloResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to HELO without hostname:', heloResponse); - - // Should get syntax error - expect(heloResponse).toMatch(/^5\d{2}/); - - // Send valid EHLO to establish session - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Empty Commands - server should remain stable after empty commands', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send multiple empty/invalid commands - const invalidCommands = [ - '\r\n', - ' \r\n', - 'MAIL FROM:\r\n', - 'RCPT TO:\r\n', - 'EHLO\r\n', - '\t\r\n' - ]; - - for (const cmd of invalidCommands) { - socket.write(cmd); - - // Read response but don't fail if error - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - setTimeout(() => resolve('TIMEOUT'), 1000); - }); - } - - // Now test that server is still functional - socket.write('MAIL FROM:\r\n'); - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(mailResponse).toInclude('250'); - - socket.write('RCPT TO:\r\n'); - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(rcptResponse).toInclude('250'); - - console.log('Server remained stable after multiple empty commands'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-05.extremely-long-lines.ts b/test/suite/smtpserver_edge-cases/test.edge-05.extremely-long-lines.ts deleted file mode 100644 index 5585962..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-05.extremely-long-lines.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30037; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for extremely long lines tests', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - hostname: 'localhost' - }); - expect(testServer).toBeDefined(); -}); - -tap.test('Extremely Long Lines - should handle lines exceeding RFC 5321 limit', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(dataResponse).toInclude('354'); - - // Create line exceeding RFC 5321 limit (1000 chars including CRLF) - const longLine = 'X'.repeat(2000); // 2000 character line - - const emailWithLongLine = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: Long Line Test\r\n' + - '\r\n' + - 'This email contains an extremely long line:\r\n' + - longLine + '\r\n' + - 'End of test.\r\n' + - '.\r\n'; - - socket.write(emailWithLongLine); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log(`Response to ${longLine.length} character line:`, finalResponse); - - // Server should handle gracefully (accept, wrap, or reject) - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('552') || finalResponse.includes('500') || finalResponse.includes('554'); - - expect(accepted || rejected).toEqual(true); - - if (accepted) { - console.log('Server accepted long line (may wrap internally)'); - } else { - console.log('Server rejected long line'); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Lines - should handle extremely long subject header', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Create extremely long subject (3000 characters) - const longSubject = 'A'.repeat(3000); - - const emailWithLongSubject = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - `Subject: ${longSubject}\r\n` + - '\r\n' + - 'Body of email with extremely long subject.\r\n' + - '.\r\n'; - - socket.write(emailWithLongSubject); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log(`Response to ${longSubject.length} character subject:`, finalResponse); - - // Server should handle this - expect(finalResponse).toMatch(/^[2-5]\d{2}/); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Lines - should handle multiple consecutive long lines', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Create multiple long lines - const longLine1 = 'A'.repeat(1500); - const longLine2 = 'B'.repeat(1800); - const longLine3 = 'C'.repeat(2000); - - const emailWithMultipleLongLines = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: Multiple Long Lines Test\r\n' + - '\r\n' + - 'First long line:\r\n' + - longLine1 + '\r\n' + - 'Second long line:\r\n' + - longLine2 + '\r\n' + - 'Third long line:\r\n' + - longLine3 + '\r\n' + - '.\r\n'; - - socket.write(emailWithMultipleLongLines); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to multiple long lines:', finalResponse); - - // Server should handle this - expect(finalResponse).toMatch(/^[2-5]\d{2}/); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Lines - should handle extremely long MAIL FROM parameter', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Create extremely long email address (technically invalid but testing limits) - const longLocalPart = 'a'.repeat(500); - const longDomain = 'b'.repeat(500) + '.com'; - const longEmail = `${longLocalPart}@${longDomain}`; - - socket.write(`MAIL FROM:<${longEmail}>\r\n`); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log(`Response to ${longEmail.length} character email address:`, response); - - // Should get error response - expect(response).toMatch(/^5\d{2}/); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Lines - should handle line exactly at RFC limit', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Setup connection - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send envelope - socket.write('MAIL FROM:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('RCPT TO:\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - socket.write('DATA\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Create line exactly at RFC 5321 limit (998 chars + CRLF = 1000) - const rfcLimitLine = 'X'.repeat(998); - - const emailWithRfcLimitLine = - 'From: sender@example.com\r\n' + - 'To: recipient@example.com\r\n' + - 'Subject: RFC Limit Test\r\n' + - '\r\n' + - 'Line at RFC 5321 limit:\r\n' + - rfcLimitLine + '\r\n' + - 'This should be accepted.\r\n' + - '.\r\n'; - - socket.write(emailWithRfcLimitLine); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log(`Response to ${rfcLimitLine.length} character line (RFC limit):`, finalResponse); - - // This should be accepted - expect(finalResponse).toInclude('250'); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-06.extremely-long-headers.ts b/test/suite/smtpserver_edge-cases/test.edge-06.extremely-long-headers.ts deleted file mode 100644 index 433769f..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-06.extremely-long-headers.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(testServer).toBeDefined(); -}); - -tap.test('Extremely Long Headers - should handle single extremely long header', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(mailResponse).toInclude('250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(rcptResponse).toInclude('250'); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(dataResponse).toInclude('354'); - - // Send email with extremely long header (3000 characters) - const longValue = 'X'.repeat(3000); - const emailContent = [ - `Subject: Test Email`, - `From: sender@example.com`, - `To: recipient@example.com`, - `X-Long-Header: ${longValue}`, - '', - 'This email has an extremely long header.', - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Server might accept or reject - both are valid for extremely long headers - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500'); - - console.log(`Long header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`); - expect(accepted || rejected).toEqual(true); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Headers - should handle multi-line header with many segments', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(mailResponse).toInclude('250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(rcptResponse).toInclude('250'); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(dataResponse).toInclude('354'); - - // Create multi-line header with 50 segments (RFC 5322 folding) - const segments = []; - for (let i = 0; i < 50; i++) { - segments.push(` Segment ${i}: ${' '.repeat(60)}value`); - } - - const emailContent = [ - `Subject: Test Email`, - `From: sender@example.com`, - `To: recipient@example.com`, - `X-Multi-Line: Initial value`, - ...segments, - '', - 'This email has a multi-line header with many segments.', - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500'); - - console.log(`Multi-line header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`); - expect(accepted || rejected).toEqual(true); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Headers - should handle multiple long headers in one email', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(mailResponse).toInclude('250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(rcptResponse).toInclude('250'); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(dataResponse).toInclude('354'); - - // Create multiple long headers - const header1 = 'A'.repeat(1000); - const header2 = 'B'.repeat(1500); - const header3 = 'C'.repeat(2000); - - const emailContent = [ - `Subject: Test Email with Multiple Long Headers`, - `From: sender@example.com`, - `To: recipient@example.com`, - `X-Long-Header-1: ${header1}`, - `X-Long-Header-2: ${header2}`, - `X-Long-Header-3: ${header3}`, - '', - 'This email has multiple long headers.', - '.', - '' - ].join('\r\n'); - - const totalHeaderSize = header1.length + header2.length + header3.length; - console.log(`Total header size: ${totalHeaderSize} bytes`); - - socket.write(emailContent); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500'); - - console.log(`Multiple long headers test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`); - expect(accepted || rejected).toEqual(true); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Extremely Long Headers - should handle header with exactly RFC limit', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(mailResponse).toInclude('250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(rcptResponse).toInclude('250'); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - expect(dataResponse).toInclude('354'); - - // Create header line exactly at RFC 5322 limit (998 chars excluding CRLF) - // Header name and colon take some space - const headerName = 'X-RFC-Limit'; - const colonSpace = ': '; - const remainingSpace = 998 - headerName.length - colonSpace.length; - const headerValue = 'X'.repeat(remainingSpace); - - const emailContent = [ - `Subject: Test Email`, - `From: sender@example.com`, - `To: recipient@example.com`, - `${headerName}${colonSpace}${headerValue}`, - '', - 'This email has a header at exactly the RFC limit.', - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - - const finalResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // This should be accepted since it's exactly at the limit - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500'); - - console.log(`RFC limit header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`); - expect(accepted || rejected).toEqual(true); - - // RFC compliant servers should accept headers exactly at the limit - if (accepted) { - console.log('✓ Server correctly accepts headers at RFC limit'); - } else { - console.log('⚠ Server rejected header at RFC limit (may be overly strict)'); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-07.unusual-mime-types.ts b/test/suite/smtpserver_edge-cases/test.edge-07.unusual-mime-types.ts deleted file mode 100644 index 783d49a..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-07.unusual-mime-types.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30041; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - hostname: 'localhost' - }); - expect(testServer).toBeDefined(); -}); - -tap.test('Unusual MIME Types - should handle email with various unusual MIME types', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - console.log('Server response:', data.toString()); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO testclient\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - // Create multipart email with unusual MIME types - const boundary = '----=_Part_1_' + Date.now(); - const unusualMimeTypes = [ - { type: 'text/plain', content: 'This is plain text content.' }, - { type: 'application/x-custom-unusual-type', content: 'Custom proprietary format data' }, - { type: 'model/vrml', content: '#VRML V2.0 utf8\nShape { geometry Box {} }' }, - { type: 'chemical/x-mdl-molfile', content: 'Molecule data\n -ISIS- 04249412312D\n\n 3 2 0 0 0 0 0 0 0 0999 V2000' }, - { type: 'application/vnd.ms-fontobject', content: 'Font binary data simulation' }, - { type: 'application/x-doom', content: 'IWAD game data simulation' } - ]; - - let emailContent = [ - 'Subject: Email with Unusual MIME Types', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - 'This is a multipart message with unusual MIME types.', - '' - ]; - - // Add each unusual MIME type as a part - unusualMimeTypes.forEach((mime, index) => { - emailContent.push(`--${boundary}`); - emailContent.push(`Content-Type: ${mime.type}`); - emailContent.push(`Content-Disposition: attachment; filename="part${index + 1}"`); - emailContent.push(''); - emailContent.push(mime.content); - emailContent.push(''); - }); - - emailContent.push(`--${boundary}--`); - emailContent.push('.'); - emailContent.push(''); - - const fullEmail = emailContent.join('\r\n'); - console.log(`Sending email with ${unusualMimeTypes.length} unusual MIME types`); - - socket.write(fullEmail); - currentStep = 'waiting_response'; - receivedData = ''; - } else if (currentStep === 'waiting_response' && (receivedData.includes('250 ') || - receivedData.includes('552 ') || - receivedData.includes('554 ') || - receivedData.includes('500 '))) { - // Either accepted or gracefully rejected - const accepted = receivedData.includes('250 '); - console.log(`Unusual MIME types test ${accepted ? 'accepted' : 'rejected'}`); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('timeout', () => { - console.error('Socket timeout'); - socket.destroy(); - done.reject(new Error('Socket timeout')); - }); - - await done.promise; -}); - -tap.test('Unusual MIME Types - should handle email with deeply nested multipart structure', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - console.log('Server response:', data.toString()); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO testclient\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - // Create nested multipart structure - const boundary1 = '----=_Part_Outer_' + Date.now(); - const boundary2 = '----=_Part_Inner_' + Date.now(); - - let emailContent = [ - 'Subject: Nested Multipart Email', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/mixed; boundary="${boundary1}"`, - '', - 'This is a nested multipart message.', - '', - `--${boundary1}`, - 'Content-Type: text/plain', - '', - 'First level plain text.', - '', - `--${boundary1}`, - `Content-Type: multipart/alternative; boundary="${boundary2}"`, - '', - `--${boundary2}`, - 'Content-Type: text/richtext', - '', - 'Rich text content', - '', - `--${boundary2}`, - 'Content-Type: application/rtf', - '', - '{\\rtf1 RTF content}', - '', - `--${boundary2}--`, - '', - `--${boundary1}`, - 'Content-Type: audio/x-aiff', - 'Content-Disposition: attachment; filename="sound.aiff"', - '', - 'AIFF audio data simulation', - '', - `--${boundary1}--`, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - currentStep = 'waiting_response'; - receivedData = ''; - } else if (currentStep === 'waiting_response' && (receivedData.includes('250 ') || - receivedData.includes('552 ') || - receivedData.includes('554 ') || - receivedData.includes('500 '))) { - const accepted = receivedData.includes('250 '); - console.log(`Nested multipart test ${accepted ? 'accepted' : 'rejected'}`); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('timeout', () => { - console.error('Socket timeout'); - socket.destroy(); - done.reject(new Error('Socket timeout')); - }); - - await done.promise; -}); - -tap.test('Unusual MIME Types - should handle email with non-standard charset encodings', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - console.log('Server response:', data.toString()); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - receivedData = ''; - socket.write('EHLO testclient\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { - currentStep = 'mail_from'; - receivedData = ''; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - receivedData = ''; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - receivedData = ''; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - // Create email with various charset encodings - const boundary = '----=_Part_Charset_' + Date.now(); - - let emailContent = [ - 'Subject: Email with Various Charset Encodings', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - 'This email contains various charset encodings.', - '', - `--${boundary}`, - 'Content-Type: text/plain; charset="iso-2022-jp"', - '', - 'Japanese text simulation', - '', - `--${boundary}`, - 'Content-Type: text/plain; charset="windows-1251"', - '', - 'Cyrillic text simulation', - '', - `--${boundary}`, - 'Content-Type: text/plain; charset="koi8-r"', - '', - 'Russian KOI8-R text', - '', - `--${boundary}`, - 'Content-Type: text/plain; charset="gb2312"', - '', - 'Chinese GB2312 text', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - currentStep = 'waiting_response'; - receivedData = ''; - } else if (currentStep === 'waiting_response' && (receivedData.includes('250 ') || - receivedData.includes('552 ') || - receivedData.includes('554 ') || - receivedData.includes('500 '))) { - const accepted = receivedData.includes('250 '); - console.log(`Various charset test ${accepted ? 'accepted' : 'rejected'}`); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('timeout', () => { - console.error('Socket timeout'); - socket.destroy(); - done.reject(new Error('Socket timeout')); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_edge-cases/test.edge-08.nested-mime-structures.ts b/test/suite/smtpserver_edge-cases/test.edge-08.nested-mime-structures.ts deleted file mode 100644 index 6cb1486..0000000 --- a/test/suite/smtpserver_edge-cases/test.edge-08.nested-mime-structures.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -let testServer: ITestServer; -const TEST_PORT = 2525; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('Nested MIME Structures - should handle deeply nested multipart structure', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let state = 'initial'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (dataBuffer.includes('220 ') && state === 'initial') { - // Send EHLO - socket.write('EHLO testclient\r\n'); - state = 'ehlo_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') { - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - state = 'mail_from_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') { - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - state = 'rcpt_to_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') { - // Send DATA - socket.write('DATA\r\n'); - state = 'data_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('354 ') && state === 'data_sent') { - // Create deeply nested MIME structure (4 levels) - const outerBoundary = '----=_Outer_Boundary_' + Date.now(); - const middleBoundary = '----=_Middle_Boundary_' + Date.now(); - const innerBoundary = '----=_Inner_Boundary_' + Date.now(); - const deepBoundary = '----=_Deep_Boundary_' + Date.now(); - - let emailContent = [ - 'Subject: Deeply Nested MIME Structure Test', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/mixed; boundary="${outerBoundary}"`, - '', - 'This is a multipart message with deeply nested structure.', - '', - // Level 1: Outer boundary - `--${outerBoundary}`, - 'Content-Type: text/plain', - '', - 'This is the first part at the outer level.', - '', - `--${outerBoundary}`, - `Content-Type: multipart/alternative; boundary="${middleBoundary}"`, - '', - // Level 2: Middle boundary - `--${middleBoundary}`, - 'Content-Type: text/plain', - '', - 'Alternative plain text version.', - '', - `--${middleBoundary}`, - `Content-Type: multipart/related; boundary="${innerBoundary}"`, - '', - // Level 3: Inner boundary - `--${innerBoundary}`, - 'Content-Type: text/html', - '', - '

HTML with related content

', - '', - `--${innerBoundary}`, - 'Content-Type: image/png', - 'Content-ID: ', - 'Content-Transfer-Encoding: base64', - '', - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', - '', - `--${innerBoundary}`, - `Content-Type: multipart/mixed; boundary="${deepBoundary}"`, - '', - // Level 4: Deep boundary - `--${deepBoundary}`, - 'Content-Type: application/octet-stream', - 'Content-Disposition: attachment; filename="data.bin"', - '', - 'Binary data simulation', - '', - `--${deepBoundary}`, - 'Content-Type: message/rfc822', - '', - 'Subject: Embedded Message', - 'From: embedded@example.com', - 'To: recipient@example.com', - '', - 'This is an embedded email message.', - '', - `--${deepBoundary}--`, - '', - `--${innerBoundary}--`, - '', - `--${middleBoundary}--`, - '', - `--${outerBoundary}`, - 'Content-Type: application/pdf', - 'Content-Disposition: attachment; filename="document.pdf"', - '', - 'PDF document data simulation', - '', - `--${outerBoundary}--`, - '.', - '' - ].join('\r\n'); - - console.log('Sending email with 4-level nested MIME structure'); - socket.write(emailContent); - state = 'email_sent'; - dataBuffer = ''; - } else if ((dataBuffer.includes('250 OK') && state === 'email_sent') || - dataBuffer.includes('552 ') || - dataBuffer.includes('554 ') || - dataBuffer.includes('500 ')) { - // Either accepted or gracefully rejected - const accepted = dataBuffer.includes('250 '); - console.log(`Nested MIME structure test ${accepted ? 'accepted' : 'rejected'}`); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('timeout', () => { - console.error('Socket timeout'); - socket.destroy(); - done.reject(new Error('Socket timeout')); - }); - - await done.promise; -}); - -tap.test('Nested MIME Structures - should handle circular references in multipart structure', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let state = 'initial'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (dataBuffer.includes('220 ') && state === 'initial') { - socket.write('EHLO testclient\r\n'); - state = 'ehlo_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') { - socket.write('MAIL FROM:\r\n'); - state = 'mail_from_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') { - socket.write('RCPT TO:\r\n'); - state = 'rcpt_to_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') { - socket.write('DATA\r\n'); - state = 'data_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('354 ') && state === 'data_sent') { - // Create structure with references between parts - const boundary1 = '----=_Boundary1_' + Date.now(); - const boundary2 = '----=_Boundary2_' + Date.now(); - - let emailContent = [ - 'Subject: Multipart with Cross-References', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/related; boundary="${boundary1}"`, - '', - `--${boundary1}`, - `Content-Type: multipart/alternative; boundary="${boundary2}"`, - 'Content-ID: ', - '', - `--${boundary2}`, - 'Content-Type: text/html', - '', - 'See related part: Link', - '', - `--${boundary2}`, - 'Content-Type: text/plain', - '', - 'Plain text with reference to part2', - '', - `--${boundary2}--`, - '', - `--${boundary1}`, - 'Content-Type: application/xml', - 'Content-ID: ', - '', - '', - '', - `--${boundary1}--`, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - state = 'email_sent'; - dataBuffer = ''; - } else if ((dataBuffer.includes('250 OK') && state === 'email_sent') || - dataBuffer.includes('552 ') || - dataBuffer.includes('554 ') || - dataBuffer.includes('500 ')) { - const accepted = dataBuffer.includes('250 '); - console.log(`Cross-reference test ${accepted ? 'accepted' : 'rejected'}`); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('timeout', () => { - console.error('Socket timeout'); - socket.destroy(); - done.reject(new Error('Socket timeout')); - }); - - await done.promise; -}); - -tap.test('Nested MIME Structures - should handle mixed nesting with various encodings', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let state = 'initial'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (dataBuffer.includes('220 ') && state === 'initial') { - socket.write('EHLO testclient\r\n'); - state = 'ehlo_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') { - socket.write('MAIL FROM:\r\n'); - state = 'mail_from_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') { - socket.write('RCPT TO:\r\n'); - state = 'rcpt_to_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') { - socket.write('DATA\r\n'); - state = 'data_sent'; - dataBuffer = ''; - } else if (dataBuffer.includes('354 ') && state === 'data_sent') { - // Create structure with various encodings - const boundary1 = '----=_Encoding_Outer_' + Date.now(); - const boundary2 = '----=_Encoding_Inner_' + Date.now(); - - let emailContent = [ - 'Subject: Mixed Encodings in Nested Structure', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/mixed; boundary="${boundary1}"`, - '', - `--${boundary1}`, - 'Content-Type: text/plain; charset="utf-8"', - 'Content-Transfer-Encoding: quoted-printable', - '', - 'This is quoted-printable encoded: =C3=A9=C3=A8=C3=AA', - '', - `--${boundary1}`, - `Content-Type: multipart/alternative; boundary="${boundary2}"`, - '', - `--${boundary2}`, - 'Content-Type: text/plain; charset="iso-8859-1"', - 'Content-Transfer-Encoding: 8bit', - '', - 'Text with 8-bit characters: ñáéíóú', - '', - `--${boundary2}`, - 'Content-Type: text/html; charset="utf-16"', - 'Content-Transfer-Encoding: base64', - '', - '//48AGgAdABtAGwAPgA8AGIAbwBkAHkAPgBVAFQARgAtADEANgAgAHQAZQB4AHQAPAAvAGIAbwBkAHkAPgA8AC8AaAB0AG0AbAA+', - '', - `--${boundary2}--`, - '', - `--${boundary1}`, - 'Content-Type: application/octet-stream', - 'Content-Transfer-Encoding: base64', - 'Content-Disposition: attachment; filename="binary.dat"', - '', - 'VGhpcyBpcyBiaW5hcnkgZGF0YQ==', - '', - `--${boundary1}--`, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - state = 'email_sent'; - dataBuffer = ''; - } else if ((dataBuffer.includes('250 OK') && state === 'email_sent') || - dataBuffer.includes('552 ') || - dataBuffer.includes('554 ') || - dataBuffer.includes('500 ')) { - const accepted = dataBuffer.includes('250 '); - console.log(`Mixed encodings test ${accepted ? 'accepted' : 'rejected'}`); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('timeout', () => { - console.error('Socket timeout'); - socket.destroy(); - done.reject(new Error('Socket timeout')); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-01.basic-email-sending.ts b/test/suite/smtpserver_email-processing/test.ep-01.basic-email-sending.ts deleted file mode 100644 index 7d5f3fb..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-01.basic-email-sending.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -// Test configuration -const TEST_PORT = 2525; -const TEST_TIMEOUT = 15000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -// Test: Complete email sending flow -tap.test('Basic Email Sending - should send email through complete SMTP flow', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const fromAddress = 'sender@example.com'; - const toAddress = 'recipient@example.com'; - const emailContent = `Subject: Production Test Email\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nDate: ${new Date().toUTCString()}\r\n\r\nThis is a test email sent during production testing.\r\nTest ID: EP-01\r\nTimestamp: ${Date.now()}\r\n`; - - const steps: string[] = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - steps.push('CONNECT'); - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - steps.push('EHLO'); - currentStep = 'mail_from'; - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - steps.push('MAIL FROM'); - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${toAddress}>\r\n`); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - steps.push('RCPT TO'); - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - steps.push('DATA'); - currentStep = 'email_content'; - socket.write(emailContent); - socket.write('\r\n.\r\n'); // End of data marker - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - steps.push('CONTENT'); - currentStep = 'quit'; - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - steps.push('QUIT'); - socket.destroy(); - - // Verify all steps completed - expect(steps).toInclude('CONNECT'); - expect(steps).toInclude('EHLO'); - expect(steps).toInclude('MAIL FROM'); - expect(steps).toInclude('RCPT TO'); - expect(steps).toInclude('DATA'); - expect(steps).toInclude('CONTENT'); - expect(steps).toInclude('QUIT'); - expect(steps.length).toEqual(7); - - done.resolve(); - } else if (receivedData.match(/\r\n5\d{2}\s/)) { - // Server error (5xx response codes) - socket.destroy(); - done.reject(new Error(`Email sending failed at step ${currentStep}: ${receivedData}`)); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Send email with attachments (MIME) -tap.test('Basic Email Sending - should send email with MIME attachment', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const fromAddress = 'sender@example.com'; - const toAddress = 'recipient@example.com'; - const boundary = '----=_Part_0_1234567890'; - - const emailContent = `Subject: Email with Attachment\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis email contains an attachment.\r\n\r\n--${boundary}\r\nContent-Type: text/plain; name="test.txt"\r\nContent-Disposition: attachment; filename="test.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nVGhpcyBpcyBhIHRlc3QgZmlsZS4=\r\n\r\n--${boundary}--\r\n`; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${toAddress}>\r\n`); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - socket.write(emailContent); - socket.write('\r\n.\r\n'); // End of data marker - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Send HTML email -tap.test('Basic Email Sending - should send HTML email', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const fromAddress = 'sender@example.com'; - const toAddress = 'recipient@example.com'; - const boundary = '----=_Part_0_987654321'; - - const emailContent = `Subject: HTML Email Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis is the plain text version.\r\n\r\n--${boundary}\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n

HTML Email

This is the HTML version.

\r\n\r\n--${boundary}--\r\n`; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${toAddress}>\r\n`); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - socket.write(emailContent); - socket.write('\r\n.\r\n'); // End of data marker - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Send email with custom headers -tap.test('Basic Email Sending - should send email with custom headers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const fromAddress = 'sender@example.com'; - const toAddress = 'recipient@example.com'; - - const emailContent = `Subject: Custom Headers Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nX-Custom-Header: CustomValue\r\nX-Priority: 1\r\nX-Mailer: SMTP Test Suite\r\nReply-To: noreply@example.com\r\nOrganization: Test Organization\r\n\r\nThis email contains custom headers.\r\n`; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${toAddress}>\r\n`); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - socket.write(emailContent); - socket.write('\r\n.\r\n'); // End of data marker - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Minimal email (only required headers) -tap.test('Basic Email Sending - should send minimal email', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const fromAddress = 'sender@example.com'; - const toAddress = 'recipient@example.com'; - - // Minimal email - just a body, no headers - const emailContent = 'This is a minimal email with no headers.\r\n'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${toAddress}>\r\n`); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - socket.write(emailContent); - socket.write('\r\n.\r\n'); // End of data marker - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-02.invalid-email-addresses.ts b/test/suite/smtpserver_email-processing/test.ep-02.invalid-email-addresses.ts deleted file mode 100644 index 8336d90..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-02.invalid-email-addresses.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -// Test configuration -const TEST_PORT = 2525; -const TEST_TIMEOUT = 20000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -// Test: Invalid email address validation -tap.test('Invalid Email Addresses - should reject various invalid email formats', async (tools) => { - const done = tools.defer(); - - const invalidAddresses = [ - 'invalid-email', - '@example.com', - 'user@', - 'user..name@example.com', - 'user@.example.com', - 'user@example..com', - 'user@example.', - 'user name@example.com', - 'user@exam ple.com', - 'user@[invalid]', - 'a'.repeat(65) + '@example.com', // Local part too long - 'user@' + 'a'.repeat(250) + '.com' // Domain too long - ]; - - const results: Array<{ - address: string; - response: string; - responseCode: string; - properlyRejected: boolean; - accepted: boolean; - }> = []; - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let currentIndex = 0; - let state = 'connecting'; - let buffer = ''; - let lastResponseCode = ''; - const fromAddress = 'test@example.com'; - - const processNextAddress = () => { - if (currentIndex < invalidAddresses.length) { - socket.write(`RCPT TO:<${invalidAddresses[currentIndex]}>\r\n`); - state = 'rcpt'; - } else { - socket.write('QUIT\r\n'); - state = 'quit'; - } - }; - - socket.on('data', (data) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Process complete lines - for (let i = 0; i < lines.length - 1; i++) { - const line = lines[i]; - if (line.match(/^\d{3}/)) { - lastResponseCode = line.substring(0, 3); - - if (state === 'connecting' && line.startsWith('220')) { - socket.write('EHLO test.example.com\r\n'); - state = 'ehlo'; - } else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) { - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - state = 'mail'; - } else if (state === 'mail' && line.startsWith('250')) { - processNextAddress(); - } else if (state === 'rcpt') { - // Record result - const rejected = lastResponseCode.startsWith('5') || lastResponseCode.startsWith('4'); - results.push({ - address: invalidAddresses[currentIndex], - response: line, - responseCode: lastResponseCode, - properlyRejected: rejected, - accepted: lastResponseCode.startsWith('2') - }); - - currentIndex++; - - if (currentIndex < invalidAddresses.length) { - // Reset and test next - socket.write('RSET\r\n'); - state = 'rset'; - } else { - socket.write('QUIT\r\n'); - state = 'quit'; - } - } else if (state === 'rset' && line.startsWith('250')) { - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - state = 'mail'; - } else if (state === 'quit' && line.startsWith('221')) { - socket.destroy(); - - // Analyze results - const rejected = results.filter(r => r.properlyRejected).length; - const rate = results.length > 0 ? rejected / results.length : 0; - - // Log results for debugging - results.forEach(r => { - if (!r.properlyRejected) { - console.log(`WARNING: Invalid address accepted: ${r.address}`); - } - }); - - // We expect at least 70% rejection rate for invalid addresses - expect(rate).toBeGreaterThan(0.7); - expect(results.length).toEqual(invalidAddresses.length); - - done.resolve(); - } - } - } - - // Keep incomplete line in buffer - buffer = lines[lines.length - 1]; - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error('Test timeout')); - }); - - socket.on('error', (err) => { - done.reject(err); - }); - - await done.promise; -}); - -// Test: Edge case email addresses that might be valid -tap.test('Invalid Email Addresses - should handle edge case addresses', async (tools) => { - const done = tools.defer(); - - const edgeCaseAddresses = [ - 'user+tag@example.com', // Valid - with plus addressing - 'user.name@example.com', // Valid - with dot - 'user@sub.example.com', // Valid - subdomain - 'user@192.168.1.1', // Valid - IP address - 'user@[192.168.1.1]', // Valid - IP in brackets - '"user name"@example.com', // Valid - quoted local part - 'user\\@name@example.com', // Valid - escaped character - 'user@localhost', // Might be valid depending on server config - ]; - - const results: Array<{ - address: string; - accepted: boolean; - }> = []; - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let currentIndex = 0; - let state = 'connecting'; - let buffer = ''; - const fromAddress = 'test@example.com'; - - socket.on('data', (data) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (let i = 0; i < lines.length - 1; i++) { - const line = lines[i]; - if (line.match(/^\d{3}/)) { - const responseCode = line.substring(0, 3); - - if (state === 'connecting' && line.startsWith('220')) { - socket.write('EHLO test.example.com\r\n'); - state = 'ehlo'; - } else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) { - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - state = 'mail'; - } else if (state === 'mail' && line.startsWith('250')) { - if (currentIndex < edgeCaseAddresses.length) { - socket.write(`RCPT TO:<${edgeCaseAddresses[currentIndex]}>\r\n`); - state = 'rcpt'; - } else { - socket.write('QUIT\r\n'); - state = 'quit'; - } - } else if (state === 'rcpt') { - results.push({ - address: edgeCaseAddresses[currentIndex], - accepted: responseCode.startsWith('2') - }); - - currentIndex++; - - if (currentIndex < edgeCaseAddresses.length) { - socket.write('RSET\r\n'); - state = 'rset'; - } else { - socket.write('QUIT\r\n'); - state = 'quit'; - } - } else if (state === 'rset' && line.startsWith('250')) { - socket.write(`MAIL FROM:<${fromAddress}>\r\n`); - state = 'mail'; - } else if (state === 'quit' && line.startsWith('221')) { - socket.destroy(); - - // Just verify we tested all addresses - expect(results.length).toEqual(edgeCaseAddresses.length); - - done.resolve(); - } - } - } - - buffer = lines[lines.length - 1]; - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error('Test timeout')); - }); - - socket.on('error', (err) => { - done.reject(err); - }); - - await done.promise; -}); - -// Test: Empty and null addresses -tap.test('Invalid Email Addresses - should handle empty addresses', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_empty'; - socket.write('RCPT TO:<>\r\n'); // Empty address - } else if (currentStep === 'rcpt_empty') { - if (receivedData.includes('250')) { - // Empty recipient allowed (for bounces) - currentStep = 'rset'; - socket.write('RSET\r\n'); - } else if (receivedData.match(/[45]\d{2}/)) { - // Empty recipient rejected - currentStep = 'rset'; - socket.write('RSET\r\n'); - } - } else if (currentStep === 'rset' && receivedData.includes('250')) { - currentStep = 'mail_empty'; - socket.write('MAIL FROM:<>\r\n'); // Empty sender (bounce) - } else if (currentStep === 'mail_empty' && receivedData.includes('250')) { - currentStep = 'rcpt_after_empty'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_after_empty' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Empty MAIL FROM should be accepted for bounces - expect(receivedData).toInclude('250'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - await stopTestServer(testServer); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-03.multiple-recipients.ts b/test/suite/smtpserver_email-processing/test.ep-03.multiple-recipients.ts deleted file mode 100644 index 6b69e8a..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-03.multiple-recipients.ts +++ /dev/null @@ -1,493 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 30049; -const TEST_TIMEOUT = 15000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' }); - - expect(testServer).toBeDefined(); - expect(testServer.port).toEqual(TEST_PORT); -}); - -// Test: Basic multiple recipients -tap.test('Multiple Recipients - should accept multiple valid recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientCount = 0; - const recipients = [ - 'recipient1@example.com', - 'recipient2@example.com', - 'recipient3@example.com' - ]; - let acceptedRecipients = 0; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else if (currentStep === 'rcpt_to') { - if (receivedData.includes('250')) { - acceptedRecipients++; - recipientCount++; - - if (recipientCount < recipients.length) { - receivedData = ''; // Clear buffer for next response - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else { - currentStep = 'data'; - socket.write('DATA\r\n'); - } - } - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - const emailContent = `Subject: Multiple Recipients Test\r\nFrom: sender@example.com\r\nTo: ${recipients.join(', ')}\r\n\r\nThis email was sent to ${acceptedRecipients} recipients.\r\n`; - socket.write(emailContent); - socket.write('\r\n.\r\n'); - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(acceptedRecipients).toEqual(recipients.length); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Mixed valid and invalid recipients -tap.test('Multiple Recipients - should handle mix of valid and invalid recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientIndex = 0; - const recipients = [ - 'valid@example.com', - 'invalid-email', // Invalid format - 'another.valid@example.com', - '@example.com', // Invalid format - 'third.valid@example.com' - ]; - const recipientResults: Array<{ email: string, accepted: boolean }> = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`); - } else if (currentStep === 'rcpt_to') { - const lines = receivedData.split('\r\n'); - const lastLine = lines[lines.length - 2] || lines[lines.length - 1]; - - if (lastLine.match(/^\d{3}/)) { - const accepted = lastLine.startsWith('250'); - recipientResults.push({ - email: recipients[recipientIndex], - accepted: accepted - }); - - recipientIndex++; - - if (recipientIndex < recipients.length) { - receivedData = ''; // Clear buffer - socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`); - } else { - const acceptedCount = recipientResults.filter(r => r.accepted).length; - - if (acceptedCount > 0) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(acceptedCount).toEqual(0); - done.resolve(); - }, 100); - } - } - } - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - const acceptedEmails = recipientResults.filter(r => r.accepted).map(r => r.email); - const emailContent = `Subject: Mixed Recipients Test\r\nFrom: sender@example.com\r\nTo: ${acceptedEmails.join(', ')}\r\n\r\nDelivered to ${acceptedEmails.length} valid recipients.\r\n`; - socket.write(emailContent); - socket.write('\r\n.\r\n'); - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - const acceptedCount = recipientResults.filter(r => r.accepted).length; - const rejectedCount = recipientResults.filter(r => !r.accepted).length; - expect(acceptedCount).toEqual(3); // 3 valid recipients - expect(rejectedCount).toEqual(2); // 2 invalid recipients - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Large number of recipients -tap.test('Multiple Recipients - should handle many recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientCount = 0; - const totalRecipients = 10; - const recipients: string[] = []; - for (let i = 1; i <= totalRecipients; i++) { - recipients.push(`recipient${i}@example.com`); - } - let acceptedCount = 0; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else if (currentStep === 'rcpt_to') { - if (receivedData.includes('250')) { - acceptedCount++; - } - - recipientCount++; - - if (recipientCount < recipients.length) { - receivedData = ''; // Clear buffer - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else { - currentStep = 'data'; - socket.write('DATA\r\n'); - } - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - const emailContent = `Subject: Large Recipients Test\r\nFrom: sender@example.com\r\n\r\nSent to ${acceptedCount} recipients.\r\n`; - socket.write(emailContent); - socket.write('\r\n.\r\n'); - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(acceptedCount).toBeGreaterThan(0); - expect(acceptedCount).toBeLessThan(totalRecipients + 1); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Duplicate recipients -tap.test('Multiple Recipients - should handle duplicate recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientCount = 0; - const recipients = [ - 'duplicate@example.com', - 'unique@example.com', - 'duplicate@example.com', // Duplicate - 'another@example.com', - 'duplicate@example.com' // Another duplicate - ]; - const results: boolean[] = []; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else if (currentStep === 'rcpt_to') { - if (receivedData.match(/[245]\d{2}/)) { - results.push(receivedData.includes('250')); - recipientCount++; - - if (recipientCount < recipients.length) { - receivedData = ''; // Clear buffer - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else { - currentStep = 'data'; - socket.write('DATA\r\n'); - } - } - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - const emailContent = `Subject: Duplicate Recipients Test\r\nFrom: sender@example.com\r\n\r\nTesting duplicate recipient handling.\r\n`; - socket.write(emailContent); - socket.write('\r\n.\r\n'); - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(results.length).toEqual(recipients.length); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: No recipients (should fail DATA) -tap.test('Multiple Recipients - DATA should fail with no recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - // Skip RCPT TO, go directly to DATA - currentStep = 'data_no_recipients'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_no_recipients') { - if (receivedData.includes('503')) { - // Expected: bad sequence error - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); // Bad sequence - done.resolve(); - }, 100); - } else if (receivedData.includes('354')) { - // Some servers accept DATA without recipients and fail later - // Send empty data to trigger the error - socket.write('.\r\n'); - currentStep = 'data_sent'; - } - } else if (currentStep === 'data_sent' && receivedData.match(/[45]\d{2}/)) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Should get an error when trying to send without recipients - expect(receivedData).toMatch(/[45]\d{2}/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Recipients with different domains -tap.test('Multiple Recipients - should handle recipients from different domains', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let recipientCount = 0; - const recipients = [ - 'user1@example.com', - 'user2@test.com', - 'user3@localhost', - 'user4@example.org', - 'user5@subdomain.example.com' - ]; - let acceptedCount = 0; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else if (currentStep === 'rcpt_to') { - if (receivedData.includes('250')) { - acceptedCount++; - } - - recipientCount++; - - if (recipientCount < recipients.length) { - receivedData = ''; // Clear buffer - socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); - } else { - if (acceptedCount > 0) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'email_content'; - const emailContent = `Subject: Multi-domain Test\r\nFrom: sender@example.com\r\n\r\nDelivered to ${acceptedCount} recipients across different domains.\r\n`; - socket.write(emailContent); - socket.write('\r\n.\r\n'); - } else if (currentStep === 'email_content' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(acceptedCount).toBeGreaterThan(0); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - if (testServer) { - await stopTestServer(testServer); - } - expect(true).toEqual(true); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-04.large-email.ts b/test/suite/smtpserver_email-processing/test.ep-04.large-email.ts deleted file mode 100644 index 115cd6a..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-04.large-email.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 30048; -const TEST_TIMEOUT = 60000; // Increased for large email handling - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' }); - expect(testServer).toBeDefined(); -}); - -// Test: Moderately large email (1MB) -tap.test('Large Email - should handle 1MB email', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let completed = false; - - // Generate 1MB of content - const largeBody = 'X'.repeat(1024 * 1024); // 1MB - const emailContent = `Subject: 1MB Email Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\n${largeBody}\r\n`; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'sending_large_email'; - - // Send in chunks to avoid overwhelming - const chunkSize = 64 * 1024; // 64KB chunks - let sent = 0; - - const sendChunk = () => { - if (sent < emailContent.length) { - const chunk = emailContent.slice(sent, sent + chunkSize); - socket.write(chunk); - sent += chunk.length; - - // Small delay between chunks - if (sent < emailContent.length) { - setTimeout(sendChunk, 10); - } else { - // End of data - socket.write('.\r\n'); - currentStep = 'sent'; - } - } - }; - - sendChunk(); - } else if (currentStep === 'sent' && (receivedData.includes('250') || receivedData.includes('552'))) { - if (!completed) { - completed = true; - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Either accepted (250) or size exceeded (552) - expect(receivedData).toMatch(/250|552/); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Large email with MIME attachments -tap.test('Large Email - should handle multi-part MIME message', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let completed = false; - - const boundary = '----=_Part_0_123456789'; - const attachment1 = 'A'.repeat(500 * 1024); // 500KB - const attachment2 = 'B'.repeat(300 * 1024); // 300KB - - const emailContent = [ - 'Subject: Large MIME Email Test', - 'From: sender@example.com', - 'To: recipient@example.com', - 'MIME-Version: 1.0', - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - 'This is a multi-part message in MIME format.', - '', - `--${boundary}`, - 'Content-Type: text/plain; charset=utf-8', - '', - 'This email contains large attachments.', - '', - `--${boundary}`, - 'Content-Type: text/plain; charset=utf-8', - 'Content-Disposition: attachment; filename="file1.txt"', - '', - attachment1, - '', - `--${boundary}`, - 'Content-Type: application/octet-stream', - 'Content-Disposition: attachment; filename="file2.bin"', - 'Content-Transfer-Encoding: base64', - '', - Buffer.from(attachment2).toString('base64'), - '', - `--${boundary}--`, - '' - ].join('\r\n'); - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'sending_mime'; - socket.write(emailContent); - socket.write('\r\n.\r\n'); - currentStep = 'sent'; - } else if (currentStep === 'sent' && (receivedData.includes('250') || receivedData.includes('552'))) { - if (!completed) { - completed = true; - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toMatch(/250|552/); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Email size limits with SIZE extension -tap.test('Large Email - should respect SIZE limits if advertised', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let maxSize: number | null = null; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - // Check for SIZE extension - const sizeMatch = receivedData.match(/SIZE\s+(\d+)/); - if (sizeMatch) { - maxSize = parseInt(sizeMatch[1]); - console.log(`Server advertises max size: ${maxSize} bytes`); - } - - currentStep = 'mail_from'; - const emailSize = maxSize ? maxSize + 1000 : 5000000; // Over limit or 5MB - socket.write(`MAIL FROM: SIZE=${emailSize}\r\n`); - } else if (currentStep === 'mail_from') { - if (maxSize && receivedData.includes('552')) { - // Size rejected - expected - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('552'); - done.resolve(); - }, 100); - } else if (receivedData.includes('250')) { - // Size accepted or no limit - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Very large email handling (5MB) -tap.test('Large Email - should handle or reject very large emails gracefully', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let completed = false; - - // Generate 5MB email - const largeContent = 'X'.repeat(5 * 1024 * 1024); // 5MB - const emailContent = `Subject: 5MB Email Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\n${largeContent}\r\n`; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'sending_5mb'; - - console.log('Sending 5MB email...'); - - // Send in larger chunks for efficiency - const chunkSize = 256 * 1024; // 256KB chunks - let sent = 0; - - const sendChunk = () => { - if (sent < emailContent.length) { - const chunk = emailContent.slice(sent, sent + chunkSize); - socket.write(chunk); - sent += chunk.length; - - if (sent < emailContent.length) { - setImmediate(sendChunk); // Use setImmediate for better performance - } else { - socket.write('.\r\n'); - currentStep = 'sent'; - } - } - }; - - sendChunk(); - } else if (currentStep === 'sent' && receivedData.match(/[245]\d{2}/)) { - if (!completed) { - completed = true; - // Extract the last response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - - // Look for the most recent response code - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([245]\d{2})[\s-]/); - if (match) { - responseCode = match[1]; - break; - } - } - - // If we couldn't extract, but we know there's a response, default to 250 - if (!responseCode && receivedData.includes('250 OK message queued')) { - responseCode = '250'; - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Accept various responses: 250 (accepted), 552 (size exceeded), 554 (failed) - expect(responseCode).toMatch(/^(250|552|554|451|452)$/); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - // Connection errors during large transfers are acceptable - if (currentStep === 'sending_5mb' || currentStep === 'sent') { - done.resolve(); - } else { - done.reject(error); - } - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Chunked transfer handling -tap.test('Large Email - should handle chunked transfers properly', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let chunksSent = 0; - let completed = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'chunked_sending'; - - // Send headers - socket.write('Subject: Chunked Transfer Test\r\n'); - socket.write('From: sender@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('\r\n'); - - // Send body in multiple chunks with delays - const chunks = [ - 'First chunk of data\r\n', - 'Second chunk of data\r\n', - 'Third chunk of data\r\n', - 'Fourth chunk of data\r\n', - 'Final chunk of data\r\n' - ]; - - const sendNextChunk = () => { - if (chunksSent < chunks.length) { - socket.write(chunks[chunksSent]); - chunksSent++; - setTimeout(sendNextChunk, 100); // 100ms delay between chunks - } else { - socket.write('.\r\n'); - } - }; - - sendNextChunk(); - } else if (currentStep === 'chunked_sending' && receivedData.includes('250')) { - if (!completed) { - completed = true; - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(chunksSent).toEqual(5); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Email with very long lines -tap.test('Large Email - should handle emails with very long lines', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let completed = false; - - // Create a very long line (10KB) - const veryLongLine = 'A'.repeat(10 * 1024); - const emailContent = `Subject: Long Line Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\n${veryLongLine}\r\nNormal line after long line.\r\n`; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'long_line'; - socket.write(emailContent); - socket.write('.\r\n'); - currentStep = 'sent'; - } else if (currentStep === 'sent') { - // Extract the last response code from the received data - // Look for response codes that are at the beginning of a line - const responseMatches = receivedData.split('\r\n').filter(line => /^\d{3}\s/.test(line)); - const lastResponseLine = responseMatches[responseMatches.length - 1]; - const responseCode = lastResponseLine?.match(/^(\d{3})/)?.[1]; - if (responseCode && !completed) { - completed = true; - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // May accept or reject based on line length limits - expect(responseCode).toMatch(/^(250|500|501|552)$/); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - if (testServer) { - await stopTestServer(testServer); - } - expect(true).toEqual(true); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-05.mime-handling.ts b/test/suite/smtpserver_email-processing/test.ep-05.mime-handling.ts deleted file mode 100644 index 685ead6..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-05.mime-handling.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('MIME Handling - Comprehensive multipart message', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - // Create comprehensive MIME test email - const boundary = 'mime-test-boundary-12345'; - const innerBoundary = 'inner-mime-boundary-67890'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: MIME Handling Test - Comprehensive`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - 'This is a multi-part message in MIME format.', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-8`, - `Content-Transfer-Encoding: 7bit`, - '', - 'This is the plain text part of the email.', - 'It tests basic MIME text handling.', - '', - `--${boundary}`, - `Content-Type: text/html; charset=utf-8`, - `Content-Transfer-Encoding: quoted-printable`, - '', - '', - 'MIME Test', - '', - '

HTML MIME Content

', - '

This tests HTML MIME content handling.

', - '

Special chars: =E2=98=85 =E2=9C=93 =E2=9D=A4

', - '', - '', - '', - `--${boundary}`, - `Content-Type: multipart/alternative; boundary="${innerBoundary}"`, - '', - `--${innerBoundary}`, - `Content-Type: text/plain; charset=iso-8859-1`, - `Content-Transfer-Encoding: base64`, - '', - 'VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCB0ZXh0IGNvbnRlbnQu', - '', - `--${innerBoundary}`, - `Content-Type: application/json; charset=utf-8`, - '', - '{"message": "JSON MIME content", "test": true, "special": "àáâãäå"}', - '', - `--${innerBoundary}--`, - '', - `--${boundary}`, - `Content-Type: image/png`, - `Content-Disposition: attachment; filename="test.png"`, - `Content-Transfer-Encoding: base64`, - '', - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', - '', - `--${boundary}`, - `Content-Type: text/csv`, - `Content-Disposition: attachment; filename="data.csv"`, - '', - 'Name,Age,Email', - 'John,25,john@example.com', - 'Jane,30,jane@example.com', - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="document.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - 'JVBERi0xLjQKJcOkw7zDtsOVDQo=', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - console.log('Sending comprehensive MIME email with multiple parts and encodings'); - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Complex MIME message accepted successfully'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('MIME Handling - Quoted-printable encoding', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: =?UTF-8?Q?Quoted=2DPrintable=20Test=20=F0=9F=8C=9F?=`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: text/plain; charset=utf-8`, - `Content-Transfer-Encoding: quoted-printable`, - '', - 'This is a test of quoted-printable encoding.', - 'Special characters: =C3=A9 =C3=A8 =C3=AA =C3=AB', - 'Long line that needs to be wrapped with soft line breaks at 76 character=', - 's per line to comply with MIME standards for quoted-printable encoding.', - 'Emoji: =F0=9F=98=80 =F0=9F=91=8D =F0=9F=8C=9F', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Quoted-printable encoded email accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('MIME Handling - Base64 encoding', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'base64-test-boundary'; - const textContent = 'This is a test of base64 encoding with various content types.\nSpecial chars: éèêë\nEmoji: 😀 👍 🌟'; - const base64Content = Buffer.from(textContent).toString('base64'); - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Base64 Encoding Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-8`, - `Content-Transfer-Encoding: base64`, - '', - base64Content, - '', - `--${boundary}`, - `Content-Type: application/octet-stream`, - `Content-Disposition: attachment; filename="binary.dat"`, - `Content-Transfer-Encoding: base64`, - '', - 'VGhpcyBpcyBiaW5hcnkgZGF0YSBmb3IgdGVzdGluZw==', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Base64 encoded email accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('MIME Handling - Content-Disposition headers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'disposition-test-boundary'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Content-Disposition Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: inline`, - '', - 'This is inline text content.', - '', - `--${boundary}`, - `Content-Type: image/jpeg`, - `Content-Disposition: attachment; filename="photo.jpg"`, - `Content-Transfer-Encoding: base64`, - '', - '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAEBAQ==', - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="report.pdf"; size=1234`, - `Content-Description: Monthly Report`, - '', - 'PDF content here', - '', - `--${boundary}`, - `Content-Type: text/html`, - `Content-Disposition: inline; filename="content.html"`, - '', - 'Inline HTML content', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with various Content-Disposition headers accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('MIME Handling - International character sets', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'intl-charset-boundary'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: International Character Sets`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-8`, - '', - 'UTF-8: Français, Español, Deutsch, 中文, 日本語, 한국어, العربية', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=iso-8859-1`, - '', - 'ISO-8859-1: Français, Español, Português', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=windows-1252`, - '', - 'Windows-1252: Special chars: €‚ƒ„…†‡', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=shift_jis`, - '', - 'Shift-JIS: Japanese text', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with international character sets accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-06.attachment-handling.ts b/test/suite/smtpserver_email-processing/test.ep-06.attachment-handling.ts deleted file mode 100644 index 50b3039..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-06.attachment-handling.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as fs from 'fs'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -const TEST_PORT = 2525; -const SAMPLE_FILES_DIR = path.join(process.cwd(), '.nogit', 'sample-files'); - -let testServer: ITestServer; - -// Helper function to read and encode files -function readFileAsBase64(filePath: string): string { - try { - const fileContent = fs.readFileSync(filePath); - return fileContent.toString('base64'); - } catch (err) { - console.error(`Failed to read file ${filePath}:`, err); - return ''; - } -} - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('Attachment Handling - Multiple file types', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'attachment-test-boundary-12345'; - - // Create various attachments - const textAttachment = 'This is a text attachment content.\nIt has multiple lines.\nAnd special chars: åäö'; - const jsonAttachment = JSON.stringify({ - name: 'test', - data: [1, 2, 3], - unicode: 'ñoño', - special: '∑∆≈' - }, null, 2); - - // Read real files from sample directory - const sampleImage = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '003-pdflatex-image/image.jpg')); - const minimalPdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '001-trivial/minimal-document.pdf')); - const multiPagePdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '004-pdflatex-4-pages/pdflatex-4-pages.pdf')); - const pdfWithAttachment = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '025-attachment/with-attachment.pdf')); - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Attachment Handling Test - Multiple Types`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - 'This is a multi-part message with various attachments.', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-8`, - '', - 'This email tests attachment handling capabilities.', - 'The server should properly process all attached files.', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-8`, - `Content-Disposition: attachment; filename="document.txt"`, - `Content-Transfer-Encoding: 7bit`, - '', - textAttachment, - '', - `--${boundary}`, - `Content-Type: application/json; charset=utf-8`, - `Content-Disposition: attachment; filename="data.json"`, - '', - jsonAttachment, - '', - `--${boundary}`, - `Content-Type: image/jpeg`, - `Content-Disposition: attachment; filename="sample-image.jpg"`, - `Content-Transfer-Encoding: base64`, - '', - sampleImage, - '', - `--${boundary}`, - `Content-Type: application/octet-stream`, - `Content-Disposition: attachment; filename="binary.bin"`, - `Content-Transfer-Encoding: base64`, - '', - Buffer.from('Binary file content with null bytes\0\0\0').toString('base64'), - '', - `--${boundary}`, - `Content-Type: text/csv`, - `Content-Disposition: attachment; filename="spreadsheet.csv"`, - '', - 'Name,Age,Country', - 'Alice,25,Sweden', - 'Bob,30,Norway', - 'Charlie,35,Denmark', - '', - `--${boundary}`, - `Content-Type: application/xml; charset=utf-8`, - `Content-Disposition: attachment; filename="config.xml"`, - '', - '', - '', - ' value', - ' ñoño ∑∆≈', - '', - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="minimal-document.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - minimalPdf, - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="multi-page-document.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - multiPagePdf, - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="pdf-with-embedded-attachment.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - pdfWithAttachment, - '', - `--${boundary}`, - `Content-Type: text/html; charset=utf-8`, - `Content-Disposition: attachment; filename="webpage.html"`, - '', - '', - 'Test', - '

HTML Attachment

Content with markup

', - '', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - console.log('Sending email with 10 different attachment types including real PDFs'); - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with multiple attachments accepted successfully'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Attachment Handling - Large attachment', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'large-attachment-boundary'; - - // Use a real large PDF file - const largePdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '009-pdflatex-geotopo/GeoTopo.pdf')); - const largePdfSize = Buffer.from(largePdf, 'base64').length; - console.log(`Large PDF size: ${(largePdfSize / 1024).toFixed(2)}KB`); - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Large Attachment Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain`, - '', - 'This email contains a large attachment.', - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="large-geotopo.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - largePdf, - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - console.log(`Sending email with large PDF attachment (${(largePdfSize / 1024).toFixed(2)}KB)`); - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && (dataBuffer.includes('250 ') || dataBuffer.includes('552 '))) { - if (!completed) { - completed = true; - const accepted = dataBuffer.includes('250'); - const rejected = dataBuffer.includes('552'); // Size exceeded - - console.log(`Large attachment: ${accepted ? 'accepted' : 'rejected (size limit)'}`); - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Attachment Handling - Inline vs attachment disposition', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'inline-attachment-boundary'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Inline vs Attachment Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/related; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/html`, - '', - '', - '

This email has inline images:

', - '', - '', - '', - '', - `--${boundary}`, - `Content-Type: image/png`, - `Content-ID: `, - `Content-Disposition: inline; filename="inline1.png"`, - `Content-Transfer-Encoding: base64`, - '', - readFileAsBase64(path.join(SAMPLE_FILES_DIR, '008-reportlab-inline-image/smile.png')) || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', - '', - `--${boundary}`, - `Content-Type: image/png`, - `Content-ID: `, - `Content-Disposition: inline; filename="inline2.png"`, - `Content-Transfer-Encoding: base64`, - '', - readFileAsBase64(path.join(SAMPLE_FILES_DIR, '019-grayscale-image/page-0-X0.png')) || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="document.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - readFileAsBase64(path.join(SAMPLE_FILES_DIR, '013-reportlab-overlay/reportlab-overlay.pdf')) || 'JVBERi0xLjQKJcOkw7zDtsOVDQo=', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with inline and attachment dispositions accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Attachment Handling - Filename encoding', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'filename-encoding-boundary'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Filename Encoding Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain`, - '', - 'Testing various filename encodings.', - '', - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: attachment; filename="simple.txt"`, - '', - 'Simple ASCII filename', - '', - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: attachment; filename="åäö-nordic.txt"`, - '', - 'Nordic characters in filename', - '', - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: attachment; filename*=UTF-8''%C3%A5%C3%A4%C3%B6-encoded.txt`, - '', - 'RFC 2231 encoded filename', - '', - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: attachment; filename="=?UTF-8?B?8J+YgC1lbW9qaS50eHQ=?="`, - '', - 'MIME encoded filename with emoji', - '', - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: attachment; filename="very long filename that exceeds normal limits and should be handled properly by the server.txt"`, - '', - 'Very long filename', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with various filename encodings accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Attachment Handling - Empty and malformed attachments', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'malformed-boundary'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Empty and Malformed Attachments`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain`, - '', - 'Testing empty and malformed attachments.', - '', - `--${boundary}`, - `Content-Type: application/octet-stream`, - `Content-Disposition: attachment; filename="empty.dat"`, - '', - '', // Empty attachment - `--${boundary}`, - `Content-Type: text/plain`, - `Content-Disposition: attachment`, // Missing filename - '', - 'Attachment without filename', - '', - `--${boundary}`, - `Content-Type: application/pdf`, - `Content-Disposition: attachment; filename="broken.pdf"`, - `Content-Transfer-Encoding: base64`, - '', - 'NOT-VALID-BASE64-@#$%', // Invalid base64 - '', - `--${boundary}`, - `Content-Disposition: attachment; filename="no-content-type.txt"`, // Missing Content-Type - '', - 'Attachment without Content-Type header', - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && (dataBuffer.includes('250 ') || dataBuffer.includes('550 '))) { - if (!completed) { - completed = true; - const result = dataBuffer.includes('250') ? 'accepted' : 'rejected'; - console.log(`Email with malformed attachments ${result}`); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-07.special-character-handling.ts b/test/suite/smtpserver_email-processing/test.ep-07.special-character-handling.ts deleted file mode 100644 index fd84553..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-07.special-character-handling.ts +++ /dev/null @@ -1,462 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30050; - -let testServer: ITestServer; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' }); - expect(testServer).toBeDefined(); -}); - -tap.test('Special Character Handling - Comprehensive Unicode test', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Special Character Test - Unicode & Symbols ñáéíóú`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: text/plain; charset=utf-8`, - `Content-Transfer-Encoding: 8bit`, - '', - 'This email tests special character handling:', - '', - '=== UNICODE CHARACTERS ===', - 'Accented letters: àáâãäåæçèéêëìíîïñòóôõöøùúûüý', - 'German umlauts: äöüÄÖÜß', - 'Scandinavian: åäöÅÄÖ', - 'French: àâéèêëïîôœùûüÿç', - 'Spanish: ñáéíóúü¿¡', - 'Polish: ąćęłńóśźż', - 'Russian: абвгдеёжзийклмнопрстуфхцчшщъыьэюя', - 'Greek: αβγδεζηθικλμνξοπρστυφχψω', - 'Arabic: العربية', - 'Hebrew: עברית', - 'Chinese: 中文测试', - 'Japanese: 日本語テスト', - 'Korean: 한국어 테스트', - 'Thai: ภาษาไทย', - '', - '=== MATHEMATICAL SYMBOLS ===', - 'Math: ∑∏∫∆∇∂∞±×÷≠≤≥≈∝∪∩⊂⊃∈∀∃', - 'Greek letters: αβγδεζηθικλμνξοπρστυφχψω', - 'Arrows: ←→↑↓↔↕⇐⇒⇑⇓⇔⇕', - '', - '=== CURRENCY & SYMBOLS ===', - 'Currency: $€£¥¢₹₽₩₪₫₨₦₡₵₴₸₼₲₱', - 'Symbols: ©®™§¶†‡•…‰‱°℃℉№', - `Punctuation: «»""''‚„‹›–—―‖‗''""‚„…‰′″‴‵‶‷‸‹›※‼‽⁇⁈⁉⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞`, - '', - '=== EMOJI & SYMBOLS ===', - 'Common: ☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷', - 'Smileys: ☺☻☹☿♀♁♂♃♄♅♆♇', - 'Hearts: ♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯', - '', - '=== SPECIAL FORMATTING ===', - 'Zero-width chars: ​‌‍‎‏', - 'Combining: e̊åa̋o̧ç', - 'Ligatures: fffiflffifflſtst', - 'Fractions: ½⅓⅔¼¾⅛⅜⅝⅞', - 'Superscript: ⁰¹²³⁴⁵⁶⁷⁸⁹', - 'Subscript: ₀₁₂₃₄₅₆₇₈₉', - '', - 'End of special character test.', - '.', - '' - ].join('\r\n'); - - console.log('Sending email with comprehensive Unicode characters'); - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with special characters accepted successfully'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Special Character Handling - Control characters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Control Character Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: text/plain; charset=utf-8`, - '', - '=== CONTROL CHARACTERS TEST ===', - 'Tab character: (between words)', - 'Non-breaking space: word word', - 'Soft hyphen: super­cali­fragi­listic­expi­ali­docious', - 'Vertical tab: word\x0Bword', - 'Form feed: word\x0Cword', - 'Backspace: word\x08word', - '', - '=== LINE ENDING TESTS ===', - 'Unix LF: Line1\nLine2', - 'Windows CRLF: Line3\r\nLine4', - 'Mac CR: Line5\rLine6', - '', - '=== BOUNDARY CHARACTERS ===', - 'SMTP boundary test: . (dot at start)', - 'Double dots: .. (escaped in SMTP)', - 'CRLF.CRLF sequence test', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with control characters accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Special Character Handling - Subject header encoding', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: =?UTF-8?B?8J+YgCBFbW9qaSBpbiBTdWJqZWN0IOKcqCDwn4yI?=`, - `Subject: =?UTF-8?Q?Quoted=2DPrintable=20Subject=20=C3=A1=C3=A9=C3=AD=C3=B3=C3=BA?=`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Testing encoded subject headers with special characters.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with encoded subject headers accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Special Character Handling - Address headers with special chars', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: "José García" `, - `To: "François Müller" , "北京用户" `, - `Cc: =?UTF-8?B?IkFubmEgw4XDpMO2Ig==?= `, - `Reply-To: "Søren Ñoño" `, - `Subject: Special names in address headers`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Testing special characters in email addresses and display names.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with special characters in addresses accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Special Character Handling - Mixed encodings', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const boundary = 'mixed-encoding-boundary'; - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Mixed Encoding Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `MIME-Version: 1.0`, - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-8`, - `Content-Transfer-Encoding: 8bit`, - '', - 'UTF-8 part: ñáéíóú 中文 日本語', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=iso-8859-1`, - `Content-Transfer-Encoding: quoted-printable`, - '', - 'ISO-8859-1 part: =F1=E1=E9=ED=F3=FA', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=windows-1252`, - '', - 'Windows-1252 part: €‚ƒ„…†‡', - '', - `--${boundary}`, - `Content-Type: text/plain; charset=utf-16`, - `Content-Transfer-Encoding: base64`, - '', - Buffer.from('UTF-16 text: ñoño', 'utf16le').toString('base64'), - '', - `--${boundary}--`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with mixed character encodings accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-08.email-routing.ts b/test/suite/smtpserver_email-processing/test.ep-08.email-routing.ts deleted file mode 100644 index 6928ccb..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-08.email-routing.ts +++ /dev/null @@ -1,527 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -const TEST_PORT = 2525; - -let testServer: ITestServer; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('Email Routing - Local domain routing', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO localhost\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - // Local sender - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - // Local recipient - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - console.log(`Local domain routing: ${accepted ? 'accepted' : 'rejected'}`); - - if (accepted) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else { - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: test@example.com`, - `To: local@localhost`, - `Subject: Local Domain Routing Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests local domain routing.', - 'The server should route this email locally.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Local domain email routed successfully'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Email Routing - External domain routing', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO localhost\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - // External recipient - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - console.log(`External domain routing: ${accepted ? 'accepted' : 'rejected'}`); - - if (accepted) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else { - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@external.com`, - `Subject: External Domain Routing Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests external domain routing.', - 'The server should accept this for relay.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('External domain email accepted for relay'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Email Routing - Multiple recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let recipientCount = 0; - const totalRecipients = 5; - let completed = false; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO localhost\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - recipientCount++; - socket.write(`RCPT TO:\r\n`); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - if (recipientCount < totalRecipients) { - recipientCount++; - socket.write(`RCPT TO:\r\n`); - dataBuffer = ''; - } else { - console.log(`All ${totalRecipients} recipients accepted`); - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } - } else if (step === 'data' && dataBuffer.includes('354')) { - const recipients = Array.from({length: totalRecipients}, (_, i) => `recipient${i+1}@example.com`); - const email = [ - `From: sender@example.com`, - `To: ${recipients.join(', ')}`, - `Subject: Multiple Recipients Routing Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests routing to multiple recipients.', - `Total recipients: ${totalRecipients}`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with multiple recipients routed successfully'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Email Routing - Invalid domain handling', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let testType = 'invalid-tld'; - const testCases = [ - { email: 'user@invalid-tld', type: 'invalid-tld' }, - { email: 'user@.com', type: 'missing-domain' }, - { email: 'user@domain..com', type: 'double-dot' }, - { email: 'user@-domain.com', type: 'leading-dash' }, - { email: 'user@domain-.com', type: 'trailing-dash' } - ]; - let currentTest = 0; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO localhost\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - testType = testCases[currentTest].type; - socket.write(`RCPT TO:<${testCases[currentTest].email}>\r\n`); - dataBuffer = ''; - } else if (step === 'rcpt') { - const rejected = dataBuffer.includes('550') || dataBuffer.includes('553') || dataBuffer.includes('501'); - console.log(`Invalid domain test (${testType}): ${rejected ? 'properly rejected' : 'unexpectedly accepted'}`); - - currentTest++; - if (currentTest < testCases.length) { - // Reset for next test - socket.write('RSET\r\n'); - step = 'rset'; - dataBuffer = ''; - } else { - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'rset' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Email Routing - Mixed local and external recipients', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - const recipients = [ - 'local@localhost', - 'external@example.com', - 'another@localhost', - 'remote@external.com' - ]; - let currentRecipient = 0; - let acceptedRecipients: string[] = []; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO localhost\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write(`RCPT TO:<${recipients[currentRecipient]}>\r\n`); - dataBuffer = ''; - } else if (step === 'rcpt') { - if (dataBuffer.includes('250')) { - acceptedRecipients.push(recipients[currentRecipient]); - console.log(`Recipient ${recipients[currentRecipient]} accepted`); - } else { - console.log(`Recipient ${recipients[currentRecipient]} rejected`); - } - - currentRecipient++; - if (currentRecipient < recipients.length) { - socket.write(`RCPT TO:<${recipients[currentRecipient]}>\r\n`); - dataBuffer = ''; - } else if (acceptedRecipients.length > 0) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else { - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: ${acceptedRecipients.join(', ')}`, - `Subject: Mixed Recipients Routing Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests routing to mixed local and external recipients.', - `Accepted recipients: ${acceptedRecipients.length}`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with mixed recipients routed successfully'); - expect(acceptedRecipients.length).toBeGreaterThan(0); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Email Routing - Subdomain routing', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - const subdomainTests = [ - 'user@mail.example.com', - 'user@smtp.corp.example.com', - 'user@deep.sub.domain.example.com' - ]; - let currentTest = 0; - - socket.on('data', (data) => { - if (completed) return; - - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO localhost\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write(`RCPT TO:<${subdomainTests[currentTest]}>\r\n`); - dataBuffer = ''; - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - console.log(`Subdomain routing test (${subdomainTests[currentTest]}): ${accepted ? 'accepted' : 'rejected'}`); - - currentTest++; - if (currentTest < subdomainTests.length) { - socket.write('RSET\r\n'); - step = 'rset'; - dataBuffer = ''; - } else { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } - } else if (step === 'rset' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: ${subdomainTests[subdomainTests.length - 1]}`, - `Subject: Subdomain Routing Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests subdomain routing.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - step = 'sent'; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Subdomain routing test completed'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_email-processing/test.ep-09.delivery-status-notifications.ts b/test/suite/smtpserver_email-processing/test.ep-09.delivery-status-notifications.ts deleted file mode 100644 index 84d01fd..0000000 --- a/test/suite/smtpserver_email-processing/test.ep-09.delivery-status-notifications.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; -const TEST_PORT = 2525; - -let testServer: ITestServer; - -tap.test('setup - start test server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 1000)); -}); - -tap.test('DSN - Extension advertised in EHLO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (dataBuffer.includes('220 ') && !dataBuffer.includes('EHLO')) { - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (dataBuffer.includes('250')) { - // Check if DSN extension is advertised - const dsnSupported = dataBuffer.toLowerCase().includes('dsn'); - console.log('DSN extension advertised:', dsnSupported); - - // Parse extensions - const lines = dataBuffer.split('\r\n'); - const extensions = lines - .filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0)) - .map(line => line.substring(4).split(' ')[0].toUpperCase()); - - console.log('Server extensions:', extensions); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('DSN - Success notification request', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - // MAIL FROM with DSN parameters - const envId = `dsn-success-${Date.now()}`; - socket.write(`MAIL FROM: RET=FULL ENVID=${envId}\r\n`); - dataBuffer = ''; - } else if (step === 'mail') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - console.log(`MAIL FROM with DSN: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`); - - if (accepted || notSupported) { - step = 'rcpt'; - // Plain MAIL FROM if DSN not supported - if (notSupported) { - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else { - // RCPT TO with NOTIFY parameter - socket.write('RCPT TO: NOTIFY=SUCCESS\r\n'); - dataBuffer = ''; - } - } - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - if (notSupported) { - // DSN not supported, try plain RCPT TO - socket.write('RCPT TO:\r\n'); - step = 'rcpt_plain'; - dataBuffer = ''; - } else if (accepted) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } - } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DSN Test - Success Notification`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests DSN success notification.', - 'The server should send a success DSN if supported.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with DSN success request accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('DSN - Multiple notification types', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - // Request multiple notification types - socket.write('RCPT TO: NOTIFY=SUCCESS,FAILURE,DELAY\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - console.log(`Multiple NOTIFY types: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`); - - if (notSupported) { - // Try plain RCPT TO - socket.write('RCPT TO:\r\n'); - step = 'rcpt_plain'; - dataBuffer = ''; - } else if (accepted) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } - } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DSN Test - Multiple Notifications`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Testing multiple DSN notification types.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with multiple DSN types accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('DSN - Never notify', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - // Request no notifications - socket.write('RCPT TO: NOTIFY=NEVER\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - console.log(`NOTIFY=NEVER: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`); - expect(accepted || notSupported).toEqual(true); - - if (notSupported) { - socket.write('RCPT TO:\r\n'); - step = 'rcpt_plain'; - dataBuffer = ''; - } else if (accepted) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } - } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DSN Test - Never Notify`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email should not generate any DSN.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with NOTIFY=NEVER accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('DSN - Original recipient tracking', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - let completed = false; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - // Include original recipient for tracking - socket.write('RCPT TO: NOTIFY=FAILURE ORCPT=rfc822;original@example.com\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - console.log(`ORCPT parameter: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`); - - if (notSupported) { - socket.write('RCPT TO:\r\n'); - step = 'rcpt_plain'; - dataBuffer = ''; - } else if (accepted) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } - } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DSN Test - Original Recipient`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests ORCPT parameter for tracking.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - step = 'sent'; - dataBuffer = ''; - } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) { - if (!completed) { - completed = true; - console.log('Email with ORCPT tracking accepted'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('DSN - Return parameter handling', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail_hdrs'; - // Test RET=HDRS - socket.write('MAIL FROM: RET=HDRS\r\n'); - dataBuffer = ''; - } else if (step === 'mail_hdrs') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - console.log(`RET=HDRS: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`); - - if (accepted || notSupported) { - // Reset and test RET=FULL - socket.write('RSET\r\n'); - step = 'reset'; - dataBuffer = ''; - } - } else if (step === 'reset' && dataBuffer.includes('250')) { - step = 'mail_full'; - socket.write('MAIL FROM: RET=FULL\r\n'); - dataBuffer = ''; - } else if (step === 'mail_full') { - const accepted = dataBuffer.includes('250'); - const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555'); - - console.log(`RET=FULL: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`); - expect(accepted || notSupported).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-01.syntax-errors.ts b/test/suite/smtpserver_error-handling/test.err-01.syntax-errors.ts deleted file mode 100644 index 08a70c2..0000000 --- a/test/suite/smtpserver_error-handling/test.err-01.syntax-errors.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; -const TEST_TIMEOUT = 10000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - tlsEnabled: false, - hostname: 'localhost' - }); - - expect(testServer).toBeDefined(); - expect(testServer.port).toEqual(TEST_PORT); -}); - -// Test: Invalid command -tap.test('Syntax Errors - should reject invalid command', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'invalid_command'; - socket.write('INVALID_COMMAND\r\n'); - } else if (currentStep === 'invalid_command' && receivedData.match(/[45]\d{2}/)) { - // Extract response code immediately after receiving error response - const lines = receivedData.split('\r\n'); - // Find the last line that starts with 4xx or 5xx - let errorCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([45]\d{2})\s/); - if (match) { - errorCode = match[1]; - break; - } - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Expect 500 (syntax error) or 502 (command not implemented) - expect(errorCode).toMatch(/^(500|502)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: MAIL FROM without brackets -tap.test('Syntax Errors - should reject MAIL FROM without brackets', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from_no_brackets'; - socket.write('MAIL FROM:test@example.com\r\n'); // Missing angle brackets - } else if (currentStep === 'mail_from_no_brackets' && receivedData.match(/[45]\d{2}/)) { - // Extract the most recent error response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([45]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Expect 501 (syntax error in parameters) - expect(responseCode).toEqual('501'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RCPT TO without brackets -tap.test('Syntax Errors - should reject RCPT TO without brackets', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to_no_brackets'; - socket.write('RCPT TO:recipient@example.com\r\n'); // Missing angle brackets - } else if (currentStep === 'rcpt_to_no_brackets' && receivedData.match(/[45]\d{2}/)) { - // Extract the most recent error response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([45]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Expect 501 (syntax error in parameters) - expect(responseCode).toEqual('501'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: EHLO without hostname -tap.test('Syntax Errors - should reject EHLO without hostname', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo_no_hostname'; - socket.write('EHLO\r\n'); // Missing hostname - } else if (currentStep === 'ehlo_no_hostname' && receivedData.match(/[45]\d{2}/)) { - // Extract the most recent error response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([45]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Expect 501 (syntax error in parameters) - expect(responseCode).toEqual('501'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Command with extra parameters -tap.test('Syntax Errors - should handle commands with extra parameters', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'quit_extra'; - socket.write('QUIT extra parameters\r\n'); // QUIT doesn't take parameters - } else if (currentStep === 'quit_extra') { - // Extract the most recent response code (could be 221 or error) - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([2-5]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.destroy(); - // Some servers might accept it (221) or reject it (501) - expect(responseCode).toMatch(/^(221|501)$/); - done.resolve(); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Malformed addresses -tap.test('Syntax Errors - should reject malformed email addresses', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from_malformed'; - socket.write('MAIL FROM:\r\n'); // Malformed address - } else if (currentStep === 'mail_from_malformed' && receivedData.match(/[45]\d{2}/)) { - // Extract the most recent error response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([45]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Expect 501 or 553 (bad address) - expect(responseCode).toMatch(/^(501|553)$/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Commands in wrong order -tap.test('Syntax Errors - should reject commands in wrong sequence', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'data_without_rcpt'; - socket.write('DATA\r\n'); // DATA without MAIL FROM/RCPT TO - } else if (currentStep === 'data_without_rcpt' && receivedData.match(/[45]\d{2}/)) { - // Extract the most recent error response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([45]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Expect 503 (bad sequence of commands) - expect(responseCode).toEqual('503'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Long commands -tap.test('Syntax Errors - should handle excessively long commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - const longString = 'A'.repeat(1000); // Very long string - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'long_command'; - socket.write(`EHLO ${longString}\r\n`); // Excessively long hostname - } else if (currentStep === 'long_command') { - // Wait for complete response (including all continuation lines) - if (receivedData.includes('250 ') || receivedData.match(/[45]\d{2}\s/)) { - currentStep = 'done'; - - // The server accepted the long EHLO command with 250 - // Some servers might reject with 500/501 - // Since we see 250 in the logs, the server accepts it - const hasError = receivedData.match(/([45]\d{2})\s/); - const hasSuccess = receivedData.includes('250 '); - - // Determine the response code - let responseCode = ''; - if (hasError) { - responseCode = hasError[1]; - } else if (hasSuccess) { - responseCode = '250'; - } - - // Some servers accept long hostnames, others reject them - // Accept either 250 (ok), 500 (syntax error), or 501 (line too long) - expect(responseCode).toMatch(/^(250|500|501)$/); - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - if (testServer) { - await stopTestServer(testServer); - } -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-02.invalid-sequence.ts b/test/suite/smtpserver_error-handling/test.err-02.invalid-sequence.ts deleted file mode 100644 index 73f7a8a..0000000 --- a/test/suite/smtpserver_error-handling/test.err-02.invalid-sequence.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 30051; -const TEST_TIMEOUT = 10000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - tlsEnabled: false, - hostname: 'localhost' - }); - - expect(testServer).toBeDefined(); - expect(testServer.port).toEqual(TEST_PORT); -}); - -// Test: MAIL FROM before EHLO/HELO -tap.test('Invalid Sequence - should reject MAIL FROM before EHLO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'mail_from_without_ehlo'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from_without_ehlo' && receivedData.includes('503')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); // Bad sequence of commands - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RCPT TO before MAIL FROM -tap.test('Invalid Sequence - should reject RCPT TO before MAIL FROM', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'rcpt_without_mail'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_without_mail' && receivedData.includes('503')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: DATA before RCPT TO -tap.test('Invalid Sequence - should reject DATA before RCPT TO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'data_without_rcpt'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_without_rcpt') { - if (receivedData.includes('503')) { - // Expected: bad sequence error - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); - done.resolve(); - }, 100); - } else if (receivedData.includes('354')) { - // Some servers accept DATA without recipients - // Send empty data to trigger error - socket.write('.\r\n'); - currentStep = 'data_sent'; - } - } else if (currentStep === 'data_sent' && receivedData.match(/[45]\d{2}/)) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Should get an error when trying to send without recipients - expect(receivedData).toMatch(/[45]\d{2}/); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Multiple EHLO commands (should be allowed) -tap.test('Invalid Sequence - should allow multiple EHLO commands', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let commandsSent = false; - - socket.on('data', async (data) => { - receivedData += data.toString(); - - // Wait for server greeting and only send commands once - if (!commandsSent && receivedData.includes('220 localhost ESMTP')) { - commandsSent = true; - - // Send all 3 EHLO commands sequentially - socket.write('EHLO test1.example.com\r\n'); - - // Wait for response before sending next - await new Promise(resolve => setTimeout(resolve, 100)); - socket.write('EHLO test2.example.com\r\n'); - - // Wait for response before sending next - await new Promise(resolve => setTimeout(resolve, 100)); - socket.write('EHLO test3.example.com\r\n'); - - // Wait for all responses - await new Promise(resolve => setTimeout(resolve, 200)); - - // Check that we got 3 successful EHLO responses - const ehloResponses = (receivedData.match(/250-localhost greets test\d+\.example\.com/g) || []).length; - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(ehloResponses).toEqual(3); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error('Connection timeout')); - }); - - await done.promise; -}); - -// Test: Multiple MAIL FROM without RSET -tap.test('Invalid Sequence - should reject second MAIL FROM without RSET', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'first_mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'first_mail_from' && receivedData.includes('250')) { - currentStep = 'second_mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'second_mail_from') { - // Check if we get either 503 (expected) or 250 (current behavior) - if (receivedData.includes('503') || receivedData.includes('250 OK')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Accept either behavior for now - expect(receivedData).toMatch(/503|250 OK/); - done.resolve(); - }, 100); - } - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: DATA without MAIL FROM -tap.test('Invalid Sequence - should reject DATA without MAIL FROM', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'data_without_mail'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data_without_mail' && receivedData.includes('503')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('503'); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Commands after QUIT -tap.test('Invalid Sequence - should reject commands after QUIT', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let quitResponseReceived = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'quit'; - socket.write('QUIT\r\n'); - } else if (currentStep === 'quit' && receivedData.includes('221')) { - quitResponseReceived = true; - // Try to send command after QUIT - try { - socket.write('EHLO test.example.com\r\n'); - // If write succeeds, wait to see if we get a response - setTimeout(() => { - socket.destroy(); - done.resolve(); // No response expected after QUIT - }, 1000); - } catch (err) { - // Write failed - connection already closed - done.resolve(); - } - } - }); - - socket.on('close', () => { - if (quitResponseReceived) { - done.resolve(); - } - }); - - socket.on('error', (error) => { - if (quitResponseReceived && error.message.includes('EPIPE')) { - done.resolve(); - } else { - done.reject(error); - } - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: RCPT TO without proper email brackets -tap.test('Invalid Sequence - should handle commands with wrong syntax in sequence', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'bad_rcpt'; - // RCPT TO with wrong syntax - socket.write('RCPT TO:recipient@example.com\r\n'); // Missing brackets - } else if (currentStep === 'bad_rcpt' && receivedData.includes('501')) { - // After syntax error, try valid command - currentStep = 'valid_rcpt'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'valid_rcpt' && receivedData.includes('250')) { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(receivedData).toInclude('501'); // Syntax error - expect(receivedData).toInclude('250'); // Valid command worked - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - if (testServer) { - await stopTestServer(testServer); - } - expect(true).toEqual(true); -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-03.temporary-failures.ts b/test/suite/smtpserver_error-handling/test.err-03.temporary-failures.ts deleted file mode 100644 index 1ab2801..0000000 --- a/test/suite/smtpserver_error-handling/test.err-03.temporary-failures.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import * as path from 'path'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -// Test configuration -const TEST_PORT = 2525; -const TEST_TIMEOUT = 10000; - -let testServer: ITestServer; - -// Setup -tap.test('setup - start SMTP server', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - tlsEnabled: false, - hostname: 'localhost' - }); - - expect(testServer).toBeDefined(); - expect(testServer.port).toEqual(TEST_PORT); -}); - -// Test: Temporary failure response codes -tap.test('Temporary Failures - should handle 4xx response codes properly', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - // Use a special address that might trigger temporary failure - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.match(/[245]\d{2}/)) { - // Extract the most recent response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([245]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - - if (responseCode?.startsWith('4')) { - // Temporary failure - expected for special addresses - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - expect(responseCode).toMatch(/^4\d{2}$/); - done.resolve(); - }, 100); - } else if (responseCode === '250') { - // Server accepts the address - this is also valid behavior - // Continue with the flow to test normal operation - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } - } else if (currentStep === 'rcpt_to') { - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Test passed - server handled the flow - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Retry after temporary failure -tap.test('Temporary Failures - should allow retry after temporary failure', async (tools) => { - const done = tools.defer(); - - const attemptConnection = async (attemptNumber: number): Promise<{ success: boolean; responseCode?: string }> => { - return new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - // Include attempt number to potentially vary server response - socket.write(`MAIL FROM:\r\n`); - } else if (currentStep === 'mail_from' && receivedData.match(/[245]\d{2}/)) { - // Extract the most recent response code - const lines = receivedData.split('\r\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - const match = lines[i].match(/^([245]\d{2})\s/); - if (match) { - responseCode = match[1]; - break; - } - } - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - resolve({ success: responseCode === '250' || responseCode?.startsWith('4'), responseCode }); - }, 100); - } - }); - - socket.on('error', () => { - resolve({ success: false }); - }); - - socket.on('timeout', () => { - socket.destroy(); - resolve({ success: false }); - }); - }); - }; - - // Try multiple attempts - const attempt1 = await attemptConnection(1); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait before retry - const attempt2 = await attemptConnection(2); - - // At least one attempt should work - expect(attempt1.success || attempt2.success).toEqual(true); - - done.resolve(); - - await done.promise; -}); - -// Test: Temporary failure during DATA -tap.test('Temporary Failures - should handle temporary failure during DATA phase', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from' && receivedData.includes('250')) { - currentStep = 'rcpt_to'; - socket.write('RCPT TO:\r\n'); - } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { - currentStep = 'data'; - socket.write('DATA\r\n'); - } else if (currentStep === 'data' && receivedData.includes('354')) { - currentStep = 'message'; - // Send a message that might trigger temporary failure - const message = 'Subject: Temporary Failure Test\r\n' + - 'X-Test-Header: temporary-failure\r\n' + - '\r\n' + - 'This message tests temporary failure handling.\r\n' + - '.\r\n'; - socket.write(message); - } else if (currentStep === 'message' && receivedData.match(/[245]\d{2}/)) { - currentStep = 'done'; // Prevent further processing - - // Extract the most recent response code - handle both plain and log format - const lines = receivedData.split('\n'); - let responseCode = ''; - for (let i = lines.length - 1; i >= 0; i--) { - // Try to match response codes in different formats - const plainMatch = lines[i].match(/^([245]\d{2})\s/); - const logMatch = lines[i].match(/→\s*([245]\d{2})\s/); - const embeddedMatch = lines[i].match(/\b([245]\d{2})\s+OK/); - - if (plainMatch) { - responseCode = plainMatch[1]; - break; - } else if (logMatch) { - responseCode = logMatch[1]; - break; - } else if (embeddedMatch) { - responseCode = embeddedMatch[1]; - break; - } - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Either accepted (250) or temporary failure (4xx) - if (responseCode) { - console.log(`Response code found: '${responseCode}'`); - // Ensure the response code is trimmed and valid - const trimmedCode = responseCode.trim(); - if (trimmedCode === '250' || trimmedCode.match(/^4\d{2}$/)) { - expect(true).toEqual(true); - } else { - console.error(`Unexpected response code: '${trimmedCode}'`); - expect(true).toEqual(true); // Pass anyway to avoid blocking - } - } else { - // If no response code found, just pass the test - expect(true).toEqual(true); - } - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Common temporary failure codes -tap.test('Temporary Failures - verify proper temporary failure codes', async (tools) => { - const done = tools.defer(); - - // Common temporary failure codes and their meanings - const temporaryFailureCodes = { - '421': 'Service not available, closing transmission channel', - '450': 'Requested mail action not taken: mailbox unavailable', - '451': 'Requested action aborted: local error in processing', - '452': 'Requested action not taken: insufficient system storage' - }; - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - let foundTemporaryCode = false; - - socket.on('data', (data) => { - receivedData += data.toString(); - - // Check for any temporary failure codes - for (const code of Object.keys(temporaryFailureCodes)) { - if (receivedData.includes(code)) { - foundTemporaryCode = true; - console.log(`Found temporary failure code: ${code} - ${temporaryFailureCodes[code as keyof typeof temporaryFailureCodes]}`); - } - } - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'testing'; - // Try various commands that might trigger temporary failures - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'testing') { - // Continue with normal flow - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - // Test passes whether we found temporary codes or not - // (server may not expose them in normal operation) - done.resolve(); - }, 500); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Test: Server overload simulation -tap.test('Temporary Failures - should handle server overload gracefully', async (tools) => { - const done = tools.defer(); - - const connections: net.Socket[] = []; - const results: Array<{ connected: boolean; responseCode?: string }> = []; - - // Create multiple rapid connections to simulate load - const connectionPromises = []; - for (let i = 0; i < 10; i++) { - connectionPromises.push( - new Promise((resolve) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 2000 - }); - - socket.on('connect', () => { - connections.push(socket); - - socket.on('data', (data) => { - const response = data.toString(); - const responseCode = response.match(/(\d{3})/)?.[1]; - - if (responseCode?.startsWith('4')) { - // Temporary failure due to load - results.push({ connected: true, responseCode }); - } else if (responseCode === '220') { - // Normal greeting - results.push({ connected: true, responseCode }); - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - resolve(); - }, 100); - }); - }); - - socket.on('error', () => { - results.push({ connected: false }); - resolve(); - }); - - socket.on('timeout', () => { - socket.destroy(); - results.push({ connected: false }); - resolve(); - }); - }) - ); - } - - await Promise.all(connectionPromises); - - // Clean up any remaining connections - for (const socket of connections) { - if (socket && !socket.destroyed) { - socket.destroy(); - } - } - - // Should handle connections (either accept or temporary failure) - const handled = results.filter(r => r.connected).length; - expect(handled).toBeGreaterThan(0); - - done.resolve(); - - await done.promise; -}); - -// Test: Temporary failure with retry header -tap.test('Temporary Failures - should provide retry information if available', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - let receivedData = ''; - let currentStep = 'connecting'; - - socket.on('data', (data) => { - receivedData += data.toString(); - - if (currentStep === 'connecting' && receivedData.includes('220')) { - currentStep = 'ehlo'; - socket.write('EHLO test.example.com\r\n'); - } else if (currentStep === 'ehlo' && receivedData.includes('250')) { - currentStep = 'mail_from'; - // Try to trigger a temporary failure - socket.write('MAIL FROM:\r\n'); - } else if (currentStep === 'mail_from') { - const response = receivedData; - - // Check if response includes retry information - if (response.includes('try again') || response.includes('retry') || response.includes('later')) { - console.log('Server provided retry guidance in temporary failure'); - } - - socket.write('QUIT\r\n'); - setTimeout(() => { - socket.destroy(); - done.resolve(); - }, 100); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); - - socket.on('timeout', () => { - socket.destroy(); - done.reject(new Error(`Connection timeout at step: ${currentStep}`)); - }); - - await done.promise; -}); - -// Teardown -tap.test('teardown - stop SMTP server', async () => { - if (testServer) { - await stopTestServer(testServer); - } -}); - -// Start the test -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-04.permanent-failures.ts b/test/suite/smtpserver_error-handling/test.err-04.permanent-failures.ts deleted file mode 100644 index 6146a30..0000000 --- a/test/suite/smtpserver_error-handling/test.err-04.permanent-failures.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30028; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for permanent failure tests', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - hostname: 'localhost' - }); - expect(testServer).toBeDefined(); -}); - -tap.test('Permanent Failures - should return 5xx for invalid recipient syntax', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(mailResponse).toInclude('250'); - - // Send RCPT TO with invalid syntax (double @) - socket.write('RCPT TO:\r\n'); - - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to invalid recipient:', rcptResponse); - - // Should get a permanent failure (5xx) - const permanentFailureCodes = ['550', '551', '552', '553', '554', '501']; - const isPermanentFailure = permanentFailureCodes.some(code => rcptResponse.includes(code)); - - expect(isPermanentFailure).toEqual(true); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Permanent Failures - should handle non-existent domain', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(mailResponse).toInclude('250'); - - // Send RCPT TO with non-existent domain - socket.write('RCPT TO:\r\n'); - - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to non-existent domain:', rcptResponse); - - // Server might: - // 1. Accept it (250) and handle bounces later - // 2. Reject with permanent failure (5xx) - // Both are valid approaches - const acceptedOrRejected = rcptResponse.includes('250') || /^5\d{2}/.test(rcptResponse); - expect(acceptedOrRejected).toEqual(true); - - if (rcptResponse.includes('250')) { - console.log('Server accepts unknown domains (will handle bounces later)'); - } else { - console.log('Server rejects unknown domains immediately'); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Permanent Failures - should reject oversized messages', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // Check if SIZE is advertised - const sizeMatch = ehloResponse.match(/250[- ]SIZE\s+(\d+)/); - const maxSize = sizeMatch ? parseInt(sizeMatch[1]) : null; - - console.log('Server max size:', maxSize || 'not advertised'); - - // Send MAIL FROM with SIZE parameter exceeding limit - const oversizeAmount = maxSize ? maxSize + 1000000 : 100000000; // 100MB if no limit advertised - socket.write(`MAIL FROM: SIZE=${oversizeAmount}\r\n`); - - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Response to oversize MAIL FROM:', mailResponse); - - if (maxSize && oversizeAmount > maxSize) { - // Server should reject with 552 but currently accepts - this is a bug - // TODO: Fix server to properly enforce SIZE limits - // For now, accept both behaviors - if (mailResponse.match(/^5\d{2}/)) { - // Correct behavior - server rejects oversized message - expect(mailResponse.toLowerCase()).toMatch(/size|too.*large|exceed/); - } else { - // Current behavior - server incorrectly accepts oversized message - expect(mailResponse).toMatch(/^250/); - console.log('WARNING: Server not enforcing SIZE limit - accepting oversized message'); - } - } else { - // No size limit advertised, server might accept - expect(mailResponse).toMatch(/^[2-5]\d{2}/); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('Permanent Failures - should persist after RSET', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - // First attempt with invalid syntax - socket.write('MAIL FROM:\r\n'); - - const firstMailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('First MAIL FROM response:', firstMailResponse); - const firstWasRejected = /^5\d{2}/.test(firstMailResponse); - - if (firstWasRejected) { - // Try RSET - socket.write('RSET\r\n'); - - const rsetResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - expect(rsetResponse).toInclude('250'); - - // Try same invalid syntax again - socket.write('MAIL FROM:\r\n'); - - const secondMailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - console.log('Second MAIL FROM response after RSET:', secondMailResponse); - - // Should still get permanent failure - expect(secondMailResponse).toMatch(/^5\d{2}/); - console.log('Permanent failures persist correctly after RSET'); - } else { - console.log('Server accepts invalid syntax in MAIL FROM (lenient parsing)'); - expect(true).toEqual(true); - } - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-05.resource-exhaustion.ts b/test/suite/smtpserver_error-handling/test.err-05.resource-exhaustion.ts deleted file mode 100644 index fadfc37..0000000 --- a/test/suite/smtpserver_error-handling/test.err-05.resource-exhaustion.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30052; - -let testServer: ITestServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' }); - expect(testServer).toBeDefined(); -}); - -tap.test('ERR-05: Resource exhaustion handling - Connection limit', async (tools) => { - const done = tools.defer(); - const connections: net.Socket[] = []; - const maxAttempts = 50; // Reduced from 150 to speed up test - let exhaustionDetected = false; - let connectionsEstablished = 0; - let lastError: string | null = null; - - // Set a timeout for the entire test - const testTimeout = setTimeout(() => { - console.log('Test timeout reached, cleaning up...'); - exhaustionDetected = true; // Consider timeout as resource protection - }, 20000); // 20 second timeout - - try { - for (let i = 0; i < maxAttempts; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => { - connections.push(socket); - connectionsEstablished++; - resolve(); - }); - socket.once('error', (err) => { - reject(err); - }); - }); - - // Try EHLO on each connection - const response = await new Promise((resolve) => { - let data = ''; - socket.once('data', (chunk) => { - data += chunk.toString(); - if (data.includes('\r\n')) { - resolve(data); - } - }); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - const ehloResponse = await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(data); - } - }; - socket.on('data', handleData); - }); - - // Check for resource exhaustion indicators - if (ehloResponse.includes('421') || - ehloResponse.includes('too many') || - ehloResponse.includes('limit') || - ehloResponse.includes('resource')) { - exhaustionDetected = true; - break; - } - - // Don't keep all connections open - close older ones to prevent timeout - if (connections.length > 10) { - const oldSocket = connections.shift(); - if (oldSocket && !oldSocket.destroyed) { - oldSocket.write('QUIT\r\n'); - oldSocket.destroy(); - } - } - - // Small delay every 10 connections to avoid overwhelming - if (i % 10 === 0 && i > 0) { - await new Promise(resolve => setTimeout(resolve, 50)); - } - - } catch (err) { - const error = err as Error; - lastError = error.message; - - // Connection refused or resource errors indicate exhaustion handling - if (error.message.includes('ECONNREFUSED') || - error.message.includes('EMFILE') || - error.message.includes('ENFILE') || - error.message.includes('too many') || - error.message.includes('resource')) { - exhaustionDetected = true; - break; - } - - // For other errors, continue trying - } - } - - // Clean up connections - for (const socket of connections) { - try { - if (!socket.destroyed) { - socket.write('QUIT\r\n'); - socket.end(); - } - } catch (e) { - // Ignore cleanup errors - } - } - - // Wait for connections to close - await new Promise(resolve => setTimeout(resolve, 500)); - - // Test passes if we either: - // 1. Detected resource exhaustion (server properly limits connections) - // 2. Established fewer connections than attempted (server has limits) - // 3. Server handled all connections gracefully (no crashes) - const hasResourceProtection = exhaustionDetected || connectionsEstablished < maxAttempts; - const handledGracefully = connectionsEstablished === maxAttempts && !lastError; - - console.log(`Connections established: ${connectionsEstablished}/${maxAttempts}`); - console.log(`Exhaustion detected: ${exhaustionDetected}`); - if (lastError) console.log(`Last error: ${lastError}`); - - clearTimeout(testTimeout); // Clear the timeout - - // Pass if server either has protection OR handles many connections gracefully - expect(hasResourceProtection || handledGracefully).toEqual(true); - - if (handledGracefully) { - console.log('Server handled all connections gracefully without resource limits'); - } - done.resolve(); - } catch (error) { - console.error('Test error:', error); - clearTimeout(testTimeout); // Clear the timeout - done.reject(error); - } -}); - -tap.test('ERR-05: Resource exhaustion handling - Memory limits', async (tools) => { - const done = tools.defer(); - - // Set a timeout for this test - const testTimeout = setTimeout(() => { - console.log('Memory test timeout reached'); - done.resolve(); // Just pass the test on timeout - }, 15000); // 15 second timeout - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 10000 // Reduced from 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Try to send a very large email that might exhaust memory - socket.write('MAIL FROM:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - socket.write('RCPT TO:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - socket.write('DATA\r\n'); - - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(dataResponse).toInclude('354'); - - // Try to send extremely large headers to test memory limits - const largeHeader = 'X-Test-Header: ' + 'A'.repeat(1024 * 100) + '\r\n'; - let resourceError = false; - - try { - // Send multiple large headers - for (let i = 0; i < 100; i++) { - socket.write(largeHeader); - - // Check if socket is still writable - if (!socket.writable) { - resourceError = true; - break; - } - } - - socket.write('\r\n.\r\n'); - - const endResponse = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout waiting for response')); - }, 10000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - - socket.once('error', (err) => { - clearTimeout(timeout); - // Connection errors during large data handling indicate resource protection - resourceError = true; - resolve(''); - }); - }); - - // Check for resource protection responses - if (endResponse.includes('552') || // Message too large - endResponse.includes('451') || // Temporary failure - endResponse.includes('421') || // Service unavailable - endResponse.includes('resource') || - endResponse.includes('memory') || - endResponse.includes('limit')) { - resourceError = true; - } - - // Resource protection is working if we got an error or protective response - expect(resourceError || endResponse.includes('552') || endResponse.includes('451')).toEqual(true); - - } catch (err) { - // Errors during large data transmission indicate resource protection - console.log('Expected resource protection error:', err); - expect(true).toEqual(true); - } - - socket.write('QUIT\r\n'); - socket.end(); - clearTimeout(testTimeout); - done.resolve(); - } catch (error) { - socket.end(); - clearTimeout(testTimeout); - done.reject(error); - } - }); - - socket.on('error', (error) => { - clearTimeout(testTimeout); - done.reject(error); - }); -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-06.malformed-mime.ts b/test/suite/smtpserver_error-handling/test.err-06.malformed-mime.ts deleted file mode 100644 index 860773a..0000000 --- a/test/suite/smtpserver_error-handling/test.err-06.malformed-mime.ts +++ /dev/null @@ -1,374 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('ERR-06: Malformed MIME handling - Invalid boundary', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Send DATA - socket.write('DATA\r\n'); - - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(dataResponse).toInclude('354'); - - // Send malformed MIME with invalid boundary - const malformedMime = [ - 'From: sender@example.com', - 'To: recipient@example.com', - 'Subject: Malformed MIME Test', - 'MIME-Version: 1.0', - 'Content-Type: multipart/mixed; boundary=invalid-boundary', - '', - '--invalid-boundary', - 'Content-Type: text/plain', - 'Content-Transfer-Encoding: invalid-encoding', - '', - 'This is malformed MIME content.', - '--invalid-boundary', - 'Content-Type: application/octet-stream', - 'Content-Disposition: attachment; filename="malformed.txt', // Missing closing quote - '', - 'Malformed attachment content without proper boundary.', - '--invalid-boundary--missing-final-boundary', // Malformed closing boundary - '.', - '' - ].join('\r\n'); - - socket.write(malformedMime); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - // Server should either: - // 1. Accept the message (250) - tolerant handling - // 2. Reject with error (550/552) - strict MIME validation - // 3. Return temporary failure (4xx) - processing error - const validResponse = response.includes('250') || - response.includes('550') || - response.includes('552') || - response.includes('451') || - response.includes('mime') || - response.includes('malformed'); - - console.log('Malformed MIME response:', response.substring(0, 100)); - expect(validResponse).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('ERR-06: Malformed MIME handling - Missing headers', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Send DATA - socket.write('DATA\r\n'); - - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(dataResponse).toInclude('354'); - - // Send MIME with missing required headers - const malformedMime = [ - 'Subject: Missing MIME headers', - 'Content-Type: multipart/mixed', // Missing boundary parameter - '', - '--boundary', - // Missing Content-Type for part - '', - 'This part has no Content-Type header.', - '--boundary', - 'Content-Type: text/plain', - // Missing blank line between headers and body - 'This part has no separator line.', - '--boundary--', - '.', - '' - ].join('\r\n'); - - socket.write(malformedMime); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - // Server should handle this gracefully - const validResponse = response.includes('250') || - response.includes('550') || - response.includes('552') || - response.includes('451'); - - console.log('Missing headers response:', response.substring(0, 100)); - expect(validResponse).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('ERR-06: Malformed MIME handling - Nested multipart errors', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Send DATA - socket.write('DATA\r\n'); - - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(dataResponse).toInclude('354'); - - // Send deeply nested multipart with errors - const malformedMime = [ - 'From: sender@example.com', - 'To: recipient@example.com', - 'Subject: Nested multipart errors', - 'MIME-Version: 1.0', - 'Content-Type: multipart/mixed; boundary="outer"', - '', - '--outer', - 'Content-Type: multipart/alternative; boundary="inner"', - '', - '--inner', - 'Content-Type: multipart/related; boundary="nested"', // Too deeply nested - '', - '--nested', - 'Content-Type: text/plain', - 'Content-Transfer-Encoding: base64', - '', - 'NOT-VALID-BASE64-CONTENT!!!', // Invalid base64 - '--nested', // Missing closing -- - '--inner--', // Improper nesting - '--outer', // Missing part content - '--outer--', - '.', - '' - ].join('\r\n'); - - socket.write(malformedMime); - - const response = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - // Server should handle complex MIME errors gracefully - const validResponse = response.includes('250') || - response.includes('550') || - response.includes('552') || - response.includes('451'); - - console.log('Nested multipart response:', response.substring(0, 100)); - expect(validResponse).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-07.exception-handling.ts b/test/suite/smtpserver_error-handling/test.err-07.exception-handling.ts deleted file mode 100644 index 5c97e04..0000000 --- a/test/suite/smtpserver_error-handling/test.err-07.exception-handling.ts +++ /dev/null @@ -1,333 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; -const activeSockets = new Set(); - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('ERR-07: Exception handling - Invalid commands', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - activeSockets.add(socket); - socket.on('close', () => activeSockets.delete(socket)); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Test various exception-triggering commands - const invalidCommands = [ - 'INVALID_COMMAND_THAT_SHOULD_TRIGGER_EXCEPTION', - 'MAIL FROM:<>', // Empty address - 'RCPT TO:<>', // Empty address - '\x00\x01\x02INVALID_BYTES', // Binary data - 'VERY_LONG_COMMAND_' + 'X'.repeat(1000), // Excessively long command - 'MAIL FROM', // Missing parameter - 'RCPT TO', // Missing parameter - 'DATA DATA DATA' // Invalid syntax - ]; - - let exceptionHandled = false; - let serverStillResponding = true; - - for (const command of invalidCommands) { - try { - socket.write(command + '\r\n'); - - const response = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout waiting for response')); - }, 5000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - }); - - console.log(`Command: "${command.substring(0, 50)}..." -> Response: ${response.substring(0, 50)}`); - - // Check if server handled the exception properly - if (response.includes('500') || // Command not recognized - response.includes('501') || // Syntax error - response.includes('502') || // Command not implemented - response.includes('503') || // Bad sequence - response.includes('error') || - response.includes('invalid')) { - exceptionHandled = true; - } - - // Small delay between commands - await new Promise(resolve => setTimeout(resolve, 100)); - - } catch (err) { - console.log('Error with command:', command, err); - // Connection might be closed by server - that's ok for some commands - serverStillResponding = false; - break; - } - } - - // If still connected, verify server is still responsive - if (serverStillResponding) { - try { - socket.write('NOOP\r\n'); - const noopResponse = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout on NOOP')); - }, 5000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - }); - - if (noopResponse.includes('250')) { - serverStillResponding = true; - } - } catch (err) { - serverStillResponding = false; - } - } - - console.log('Exception handled:', exceptionHandled); - console.log('Server still responding:', serverStillResponding); - - // Test passes if exceptions were handled OR server is still responding - expect(exceptionHandled || serverStillResponding).toEqual(true); - - if (socket.writable) { - socket.write('QUIT\r\n'); - } - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('ERR-07: Exception handling - Malformed protocol', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - activeSockets.add(socket); - socket.on('close', () => activeSockets.delete(socket)); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send commands with protocol violations - const protocolViolations = [ - 'EHLO', // No hostname - 'MAIL FROM: SIZE=', // Incomplete SIZE - 'RCPT TO: NOTIFY=', // Incomplete NOTIFY - 'AUTH PLAIN', // No credentials - 'STARTTLS EXTRA', // Extra parameters - 'MAIL FROM:\r\nRCPT TO:', // Multiple commands in one line - ]; - - let violationsHandled = 0; - - for (const violation of protocolViolations) { - try { - socket.write(violation + '\r\n'); - - const response = await new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve('TIMEOUT'); - }, 3000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - }); - - if (response !== 'TIMEOUT' && - (response.includes('500') || - response.includes('501') || - response.includes('503'))) { - violationsHandled++; - } - - await new Promise(resolve => setTimeout(resolve, 100)); - - } catch (err) { - // Error is ok - server might close connection - } - } - - console.log(`Protocol violations handled: ${violationsHandled}/${protocolViolations.length}`); - - // Server should handle at least some violations properly - expect(violationsHandled).toBeGreaterThan(0); - - if (socket.writable) { - socket.write('QUIT\r\n'); - } - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('ERR-07: Exception handling - Recovery after errors', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - activeSockets.add(socket); - socket.on('close', () => activeSockets.delete(socket)); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Trigger an error - socket.write('INVALID_COMMAND\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toMatch(/50[0-3]/); - resolve(); - }); - }); - - // Now try a valid command sequence to ensure recovery - socket.write('MAIL FROM:\r\n'); - - const mailResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(mailResponse).toInclude('250'); - - socket.write('RCPT TO:\r\n'); - - const rcptResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(rcptResponse).toInclude('250'); - - // Server recovered successfully after exception - socket.write('RSET\r\n'); - - const rsetResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(rsetResponse).toInclude('250'); - - console.log('Server recovered successfully after exception'); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('cleanup server', async () => { - // Close any remaining sockets - for (const socket of activeSockets) { - if (!socket.destroyed) { - socket.destroy(); - } - } - - // Wait for all sockets to be fully closed - await new Promise(resolve => setTimeout(resolve, 500)); - - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_error-handling/test.err-08.error-logging.ts b/test/suite/smtpserver_error-handling/test.err-08.error-logging.ts deleted file mode 100644 index c60f7ec..0000000 --- a/test/suite/smtpserver_error-handling/test.err-08.error-logging.ts +++ /dev/null @@ -1,324 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('ERR-08: Error logging - Command errors', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Test various error conditions that should be logged - const errorTests = [ - { command: 'INVALID_COMMAND', expectedCode: '500', description: 'Invalid command' }, - { command: 'MAIL FROM:', expectedCode: '501', description: 'Invalid email syntax' }, - { command: 'RCPT TO:', expectedCode: '501', description: 'Invalid recipient syntax' }, - { command: 'VRFY nonexistent@domain.com', expectedCode: '550', description: 'User verification failed' }, - { command: 'EXPN invalidlist', expectedCode: '550', description: 'List expansion failed' } - ]; - - let errorsDetected = 0; - let totalTests = errorTests.length; - - for (const test of errorTests) { - try { - socket.write(test.command + '\r\n'); - - const response = await new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve('TIMEOUT'); - }, 5000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - }); - - console.log(`${test.description}: ${test.command} -> ${response.substring(0, 50)}`); - - // Check if appropriate error code was returned - if (response.includes(test.expectedCode) || - response.includes('500') || // General error - response.includes('501') || // Syntax error - response.includes('502') || // Not implemented - response.includes('550')) { // Action not taken - errorsDetected++; - } - - // Small delay between commands - await new Promise(resolve => setTimeout(resolve, 100)); - - } catch (err) { - console.log('Error during test:', test.description, err); - // Connection errors also count as detected errors - errorsDetected++; - } - } - - const detectionRate = errorsDetected / totalTests; - console.log(`Error detection rate: ${errorsDetected}/${totalTests} (${Math.round(detectionRate * 100)}%)`); - - // Expect at least 80% of errors to be properly detected and responded to - expect(detectionRate).toBeGreaterThanOrEqual(0.8); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('ERR-08: Error logging - Protocol violations', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Test protocol violations that should trigger error logging - const violations = [ - { - sequence: ['RCPT TO:'], // RCPT before MAIL - description: 'RCPT before MAIL FROM' - }, - { - sequence: ['MAIL FROM:', 'DATA'], // DATA before RCPT - description: 'DATA before RCPT TO' - }, - { - sequence: ['EHLO testhost', 'EHLO testhost', 'MAIL FROM:', 'MAIL FROM:'], // Double MAIL FROM - description: 'Multiple MAIL FROM commands' - } - ]; - - let violationsDetected = 0; - - for (const violation of violations) { - // Reset connection state - socket.write('RSET\r\n'); - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - console.log(`Testing: ${violation.description}`); - - for (const cmd of violation.sequence) { - socket.write(cmd + '\r\n'); - - const response = await new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve('TIMEOUT'); - }, 5000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - }); - - // Check for error responses - if (response.includes('503') || // Bad sequence - response.includes('501') || // Syntax error - response.includes('500')) { // Error - violationsDetected++; - console.log(` Violation detected: ${response.substring(0, 50)}`); - break; // Move to next violation test - } - } - - await new Promise(resolve => setTimeout(resolve, 100)); - } - - console.log(`Protocol violations detected: ${violationsDetected}/${violations.length}`); - - // Expect all protocol violations to be detected - expect(violationsDetected).toBeGreaterThan(0); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('ERR-08: Error logging - Data transmission errors', async (tools) => { - const done = tools.defer(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('connect', async () => { - try { - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ') && data.includes('\r\n')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Set up valid email transaction - socket.write('MAIL FROM:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - socket.write('RCPT TO:\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - socket.write('DATA\r\n'); - - const dataResponse = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(dataResponse).toInclude('354'); - - // Test various data transmission errors - const dataErrors = [ - { - data: 'From: sender@example.com\r\n.\r\n', // Premature termination - description: 'Premature dot termination' - }, - { - data: 'Subject: Test\r\n\r\n' + '\x00\x01\x02\x03', // Binary data - description: 'Binary data in message' - }, - { - data: 'X-Long-Line: ' + 'A'.repeat(2000) + '\r\n', // Excessively long line - description: 'Excessively long header line' - } - ]; - - for (const errorData of dataErrors) { - console.log(`Testing: ${errorData.description}`); - socket.write(errorData.data); - } - - // Terminate the data - socket.write('\r\n.\r\n'); - - const finalResponse = await new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve('TIMEOUT'); - }, 10000); - - socket.once('data', (chunk) => { - clearTimeout(timeout); - resolve(chunk.toString()); - }); - }); - - console.log('Data transmission response:', finalResponse.substring(0, 100)); - - // Server should either accept (250) or reject (5xx) but must respond - const hasResponse = finalResponse !== 'TIMEOUT' && - (finalResponse.includes('250') || - finalResponse.includes('5')); - - expect(hasResponse).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - socket.end(); - done.reject(error); - } - }); - - socket.on('error', (error) => { - done.reject(error); - }); -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-01.throughput.ts b/test/suite/smtpserver_performance/test.perf-01.throughput.ts deleted file mode 100644 index b4fcd25..0000000 --- a/test/suite/smtpserver_performance/test.perf-01.throughput.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -// import { createTestSmtpClient, sendConcurrentEmails, measureClientThroughput } from '../../helpers/smtp.client.js'; -import { connectToSmtp, sendSmtpCommand, waitForGreeting, createMimeMessage, closeSmtpConnection } from '../../helpers/utils.js'; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for performance testing', async () => { - testServer = await startTestServer({ - port: 2531, - hostname: 'localhost', - maxConnections: 1000, - size: 50 * 1024 * 1024 // 50MB for performance testing - }); - expect(testServer).toBeInstanceOf(Object); -}); - -// TODO: Enable these tests when the helper functions are implemented -/* -tap.test('PERF-01: Throughput Testing - measure emails per second', async () => { - const client = createTestSmtpClient({ - host: testServer.hostname, - port: testServer.port, - maxConnections: 10 - }); - - try { - // Warm up the connection pool - console.log('🔥 Warming up connection pool...'); - await sendConcurrentEmails(client, 5); - - // Measure throughput for 10 seconds - console.log('📊 Measuring throughput for 10 seconds...'); - const startTime = Date.now(); - const testDuration = 10000; // 10 seconds - - const result = await measureClientThroughput(client, testDuration, { - from: 'perf-test@example.com', - to: 'recipient@example.com', - subject: 'Performance Test Email', - text: 'This is a performance test email to measure throughput.' - }); - - const actualDuration = (Date.now() - startTime) / 1000; - - console.log('📈 Throughput Test Results:'); - console.log(` Total emails sent: ${result.totalSent}`); - console.log(` Successful: ${result.successCount}`); - console.log(` Failed: ${result.errorCount}`); - console.log(` Duration: ${actualDuration.toFixed(2)}s`); - console.log(` Throughput: ${result.throughput.toFixed(2)} emails/second`); - - // Performance expectations - expect(result.throughput).toBeGreaterThan(10); // At least 10 emails/second - expect(result.errorCount).toBeLessThan(result.totalSent * 0.05); // Less than 5% errors - - console.log('✅ Throughput test passed'); - - } finally { - if (client.close) { - await client.close(); - } - } -}); - -tap.test('PERF-01: Burst throughput - handle sudden load spikes', async () => { - const client = createTestSmtpClient({ - host: testServer.hostname, - port: testServer.port, - maxConnections: 20 - }); - - try { - // Send burst of emails - const burstSize = 100; - console.log(`💥 Sending burst of ${burstSize} emails...`); - - const startTime = Date.now(); - const results = await sendConcurrentEmails(client, burstSize, { - from: 'burst-test@example.com', - to: 'recipient@example.com', - subject: 'Burst Test Email', - text: 'Testing burst performance.' - }); - - const duration = Date.now() - startTime; - const successCount = results.filter(r => r && !r.rejected).length; - const throughput = (successCount / duration) * 1000; - - console.log(`✅ Burst completed in ${duration}ms`); - console.log(` Success rate: ${successCount}/${burstSize} (${(successCount/burstSize*100).toFixed(1)}%)`); - console.log(` Burst throughput: ${throughput.toFixed(2)} emails/second`); - - expect(successCount).toBeGreaterThan(burstSize * 0.95); // 95% success rate - - } finally { - if (client.close) { - await client.close(); - } - } -}); -*/ - -tap.test('PERF-01: Large message throughput - measure with varying sizes', async () => { - const messageSizes = [ - { size: 1024, label: '1KB' }, - { size: 100 * 1024, label: '100KB' }, - { size: 1024 * 1024, label: '1MB' }, - { size: 5 * 1024 * 1024, label: '5MB' } - ]; - - for (const { size, label } of messageSizes) { - console.log(`\n📧 Testing throughput with ${label} messages...`); - - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Send a few messages of this size - const messageCount = 5; - const timings: number[] = []; - - for (let i = 0; i < messageCount; i++) { - const startTime = Date.now(); - - await sendSmtpCommand(socket, 'MAIL FROM:', '250'); - await sendSmtpCommand(socket, 'RCPT TO:', '250'); - await sendSmtpCommand(socket, 'DATA', '354'); - - // Create message with padding to reach target size - const padding = 'X'.repeat(Math.max(0, size - 200)); // Account for headers - const emailContent = createMimeMessage({ - from: 'size-test@example.com', - to: 'recipient@example.com', - subject: `${label} Performance Test`, - text: padding - }); - - socket.write(emailContent); - socket.write('\r\n.\r\n'); - - // Wait for acceptance - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout')), 30000); - const onData = (data: Buffer) => { - if (data.toString().includes('250')) { - clearTimeout(timeout); - socket.removeListener('data', onData); - resolve(); - } - }; - socket.on('data', onData); - }); - - const duration = Date.now() - startTime; - timings.push(duration); - - // Reset for next message - await sendSmtpCommand(socket, 'RSET', '250'); - } - - const avgTime = timings.reduce((a, b) => a + b, 0) / timings.length; - const throughputMBps = (size / 1024 / 1024) / (avgTime / 1000); - - console.log(` Average time: ${avgTime.toFixed(0)}ms`); - console.log(` Throughput: ${throughputMBps.toFixed(2)} MB/s`); - - } finally { - await closeSmtpConnection(socket); - } - } - - console.log('\n✅ Large message throughput test completed'); -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - console.log('✅ Test server stopped'); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-02.concurrency.ts b/test/suite/smtpserver_performance/test.perf-02.concurrency.ts deleted file mode 100644 index 4c3c2d1..0000000 --- a/test/suite/smtpserver_performance/test.perf-02.concurrency.ts +++ /dev/null @@ -1,388 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('PERF-02: Concurrency testing - Multiple simultaneous connections', async (tools) => { - const done = tools.defer(); - const concurrentCount = 20; - const connectionResults: Array<{ - connectionId: number; - success: boolean; - duration: number; - error?: string; - }> = []; - - const createConcurrentConnection = (connectionId: number): Promise => { - return new Promise((resolve) => { - const startTime = Date.now(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 10000 - }); - - let state = 'connecting'; - let receivedData = ''; - - const timeoutHandle = setTimeout(() => { - socket.destroy(); - connectionResults.push({ - connectionId, - success: false, - duration: Date.now() - startTime, - error: 'Connection timeout' - }); - resolve(); - }, 10000); - - socket.on('connect', () => { - state = 'connected'; - }); - - socket.on('data', (chunk) => { - receivedData += chunk.toString(); - const lines = receivedData.split('\r\n'); - - for (const line of lines) { - if (!line.trim()) continue; - - if (state === 'connected' && line.startsWith('220')) { - state = 'ehlo'; - socket.write(`EHLO testhost-${connectionId}\r\n`); - } else if (state === 'ehlo' && line.includes('250 ') && !line.includes('250-')) { - // Final 250 response received - state = 'quit'; - socket.write('QUIT\r\n'); - } else if (state === 'quit' && line.startsWith('221')) { - clearTimeout(timeoutHandle); - socket.end(); - connectionResults.push({ - connectionId, - success: true, - duration: Date.now() - startTime - }); - resolve(); - } - } - }); - - socket.on('error', (error) => { - clearTimeout(timeoutHandle); - connectionResults.push({ - connectionId, - success: false, - duration: Date.now() - startTime, - error: error.message - }); - resolve(); - }); - - socket.on('close', () => { - clearTimeout(timeoutHandle); - if (!connectionResults.find(r => r.connectionId === connectionId)) { - connectionResults.push({ - connectionId, - success: false, - duration: Date.now() - startTime, - error: 'Connection closed unexpectedly' - }); - } - resolve(); - }); - }); - }; - - try { - // Create all concurrent connections - const promises: Promise[] = []; - console.log(`Creating ${concurrentCount} concurrent connections...`); - - for (let i = 0; i < concurrentCount; i++) { - promises.push(createConcurrentConnection(i)); - // Small stagger to avoid overwhelming the system - if (i % 5 === 0) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - } - - // Wait for all connections to complete - await Promise.all(promises); - - // Analyze results - const successful = connectionResults.filter(r => r.success).length; - const failed = connectionResults.filter(r => !r.success).length; - const successRate = successful / concurrentCount; - const avgDuration = connectionResults - .filter(r => r.success) - .reduce((sum, r) => sum + r.duration, 0) / successful || 0; - - console.log(`\nConcurrency Test Results:`); - console.log(`Total connections: ${concurrentCount}`); - console.log(`Successful: ${successful} (${(successRate * 100).toFixed(1)}%)`); - console.log(`Failed: ${failed}`); - console.log(`Average duration: ${avgDuration.toFixed(0)}ms`); - - if (failed > 0) { - const errors = connectionResults - .filter(r => !r.success) - .map(r => r.error) - .filter((v, i, a) => a.indexOf(v) === i); // unique errors - console.log(`Unique errors: ${errors.join(', ')}`); - } - - // Success if at least 80% of connections succeed - expect(successRate).toBeGreaterThanOrEqual(0.8); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('PERF-02: Concurrency testing - Concurrent transactions', async (tools) => { - const done = tools.defer(); - const transactionCount = 10; - const transactionResults: Array<{ - transactionId: number; - success: boolean; - duration: number; - error?: string; - }> = []; - - const performConcurrentTransaction = (transactionId: number): Promise => { - return new Promise((resolve) => { - const startTime = Date.now(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 15000 - }); - - let state = 'connecting'; - - const timeoutHandle = setTimeout(() => { - socket.destroy(); - transactionResults.push({ - transactionId, - success: false, - duration: Date.now() - startTime, - error: 'Transaction timeout' - }); - resolve(); - }, 15000); - - const processResponse = async () => { - try { - // Read greeting - await new Promise((res) => { - let greeting = ''; - const handleGreeting = (chunk: Buffer) => { - greeting += chunk.toString(); - if (greeting.includes('220') && greeting.includes('\r\n')) { - socket.removeListener('data', handleGreeting); - res(); - } - }; - socket.on('data', handleGreeting); - }); - - // Send EHLO - socket.write(`EHLO testhost-tx-${transactionId}\r\n`); - - await new Promise((res) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - // Look for the end of EHLO response (250 without dash) - if (data.includes('250 ')) { - socket.removeListener('data', handleData); - res(); - } - }; - socket.on('data', handleData); - }); - - // Complete email transaction - socket.write(`MAIL FROM:\r\n`); - - await new Promise((res, rej) => { - let mailResponse = ''; - const handleMailResponse = (chunk: Buffer) => { - mailResponse += chunk.toString(); - if (mailResponse.includes('\r\n')) { - socket.removeListener('data', handleMailResponse); - if (!mailResponse.includes('250')) { - rej(new Error('MAIL FROM failed')); - } else { - res(); - } - } - }; - socket.on('data', handleMailResponse); - }); - - socket.write(`RCPT TO:\r\n`); - - await new Promise((res, rej) => { - let rcptResponse = ''; - const handleRcptResponse = (chunk: Buffer) => { - rcptResponse += chunk.toString(); - if (rcptResponse.includes('\r\n')) { - socket.removeListener('data', handleRcptResponse); - if (!rcptResponse.includes('250')) { - rej(new Error('RCPT TO failed')); - } else { - res(); - } - } - }; - socket.on('data', handleRcptResponse); - }); - - socket.write('DATA\r\n'); - - await new Promise((res, rej) => { - let dataResponse = ''; - const handleDataResponse = (chunk: Buffer) => { - dataResponse += chunk.toString(); - if (dataResponse.includes('\r\n')) { - socket.removeListener('data', handleDataResponse); - if (!dataResponse.includes('354')) { - rej(new Error('DATA command failed')); - } else { - res(); - } - } - }; - socket.on('data', handleDataResponse); - }); - - // Send email content - const emailContent = [ - `From: sender${transactionId}@example.com`, - `To: recipient${transactionId}@example.com`, - `Subject: Concurrent test ${transactionId}`, - '', - `This is concurrent test message ${transactionId}`, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - - await new Promise((res, rej) => { - let submitResponse = ''; - const handleSubmitResponse = (chunk: Buffer) => { - submitResponse += chunk.toString(); - if (submitResponse.includes('\r\n') && submitResponse.includes('250')) { - socket.removeListener('data', handleSubmitResponse); - res(); - } else if (submitResponse.includes('\r\n') && (submitResponse.includes('4') || submitResponse.includes('5'))) { - socket.removeListener('data', handleSubmitResponse); - rej(new Error('Message submission failed')); - } - }; - socket.on('data', handleSubmitResponse); - }); - - socket.write('QUIT\r\n'); - - await new Promise((res) => { - socket.once('data', () => res()); - }); - - clearTimeout(timeoutHandle); - socket.end(); - - transactionResults.push({ - transactionId, - success: true, - duration: Date.now() - startTime - }); - resolve(); - - } catch (error) { - clearTimeout(timeoutHandle); - socket.end(); - const errorMsg = error instanceof Error ? error.message : 'Unknown error'; - console.log(`Transaction ${transactionId} failed: ${errorMsg}`); - transactionResults.push({ - transactionId, - success: false, - duration: Date.now() - startTime, - error: errorMsg - }); - resolve(); - } - }; - - socket.on('connect', () => { - state = 'connected'; - processResponse(); - }); - - socket.on('error', (error) => { - clearTimeout(timeoutHandle); - if (!transactionResults.find(r => r.transactionId === transactionId)) { - transactionResults.push({ - transactionId, - success: false, - duration: Date.now() - startTime, - error: error.message - }); - } - resolve(); - }); - }); - }; - - try { - // Create concurrent transactions - const promises: Promise[] = []; - console.log(`\nStarting ${transactionCount} concurrent email transactions...`); - - for (let i = 0; i < transactionCount; i++) { - promises.push(performConcurrentTransaction(i)); - // Small stagger - await new Promise(resolve => setTimeout(resolve, 50)); - } - - // Wait for all transactions - await Promise.all(promises); - - // Analyze results - const successful = transactionResults.filter(r => r.success).length; - const failed = transactionResults.filter(r => !r.success).length; - const successRate = successful / transactionCount; - const avgDuration = transactionResults - .filter(r => r.success) - .reduce((sum, r) => sum + r.duration, 0) / successful || 0; - - console.log(`\nConcurrent Transaction Results:`); - console.log(`Total transactions: ${transactionCount}`); - console.log(`Successful: ${successful} (${(successRate * 100).toFixed(1)}%)`); - console.log(`Failed: ${failed}`); - console.log(`Average duration: ${avgDuration.toFixed(0)}ms`); - - // Success if at least 80% of transactions complete - expect(successRate).toBeGreaterThanOrEqual(0.8); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-03.cpu-utilization.ts b/test/suite/smtpserver_performance/test.perf-03.cpu-utilization.ts deleted file mode 100644 index 3e3fc79..0000000 --- a/test/suite/smtpserver_performance/test.perf-03.cpu-utilization.ts +++ /dev/null @@ -1,245 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('PERF-03: CPU utilization - Load test', async (tools) => { - const done = tools.defer(); - const monitoringDuration = 3000; // 3 seconds (reduced from 5) - const connectionCount = 5; // Reduced from 10 - const connections: net.Socket[] = []; - - // Add timeout to prevent hanging - const testTimeout = setTimeout(() => { - console.log('CPU test timeout reached, cleaning up...'); - for (const socket of connections) { - if (!socket.destroyed) socket.destroy(); - } - done.resolve(); - }, 30000); // 30 second timeout - - try { - // Record initial CPU usage - const initialCpuUsage = process.cpuUsage(); - const startTime = Date.now(); - - // Create multiple connections and send emails - console.log(`Creating ${connectionCount} connections for CPU load test...`); - - for (let i = 0; i < connectionCount; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - connections.push(socket); - - await new Promise((resolve, reject) => { - socket.once('connect', () => { - resolve(); - }); - socket.once('error', reject); - }); - - // Process greeting - await new Promise((resolve) => { - let greeting = ''; - const handleGreeting = (chunk: Buffer) => { - greeting += chunk.toString(); - if (greeting.includes('220') && greeting.includes('\r\n')) { - socket.removeListener('data', handleGreeting); - resolve(); - } - }; - socket.on('data', handleGreeting); - }); - - // Send EHLO - socket.write(`EHLO testhost-cpu-${i}\r\n`); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Keep connection active, don't send full transaction to avoid timeout - } - - // Keep connections active during monitoring period - console.log(`Monitoring CPU usage for ${monitoringDuration}ms...`); - - // Send periodic NOOP commands to keep connections active - const noopInterval = setInterval(() => { - connections.forEach((socket, idx) => { - if (socket.writable) { - socket.write('NOOP\r\n'); - } - }); - }, 1000); - - await new Promise(resolve => setTimeout(resolve, monitoringDuration)); - clearInterval(noopInterval); - - // Calculate CPU usage - const finalCpuUsage = process.cpuUsage(initialCpuUsage); - const totalCpuTimeMs = (finalCpuUsage.user + finalCpuUsage.system) / 1000; - const elapsedTime = Date.now() - startTime; - const cpuUtilizationPercent = (totalCpuTimeMs / elapsedTime) * 100; - - console.log(`\nCPU Utilization Results:`); - console.log(`Total CPU time: ${totalCpuTimeMs.toFixed(0)}ms`); - console.log(`Elapsed time: ${elapsedTime}ms`); - console.log(`CPU utilization: ${cpuUtilizationPercent.toFixed(1)}%`); - console.log(`User CPU: ${(finalCpuUsage.user / 1000).toFixed(0)}ms`); - console.log(`System CPU: ${(finalCpuUsage.system / 1000).toFixed(0)}ms`); - - // Clean up connections - for (const socket of connections) { - if (socket.writable) { - socket.write('QUIT\r\n'); - socket.end(); - } - } - - // Test passes if CPU usage is reasonable (less than 80%) - expect(cpuUtilizationPercent).toBeLessThan(80); - clearTimeout(testTimeout); - done.resolve(); - } catch (error) { - // Clean up on error - connections.forEach(socket => socket.destroy()); - clearTimeout(testTimeout); - done.reject(error); - } -}); - -tap.test('PERF-03: CPU utilization - Stress test', async (tools) => { - const done = tools.defer(); - const testDuration = 2000; // 2 seconds (reduced from 3) - let requestCount = 0; - - // Add timeout to prevent hanging - const testTimeout = setTimeout(() => { - console.log('Stress test timeout reached, completing...'); - done.resolve(); - }, 15000); // 15 second timeout - - try { - const initialCpuUsage = process.cpuUsage(); - const startTime = Date.now(); - - console.log(`\nRunning CPU stress test for ${testDuration}ms...`); - - // Create a single connection for rapid requests - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await new Promise((resolve) => { - let greeting = ''; - const handleGreeting = (chunk: Buffer) => { - greeting += chunk.toString(); - if (greeting.includes('220') && greeting.includes('\r\n')) { - socket.removeListener('data', handleGreeting); - resolve(); - } - }; - socket.on('data', handleGreeting); - }); - - // Send EHLO - socket.write('EHLO stresstest\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Rapid command loop - const endTime = Date.now() + testDuration; - const commands = ['NOOP', 'RSET', 'VRFY test@example.com', 'HELP']; - let commandIndex = 0; - - while (Date.now() < endTime) { - const command = commands[commandIndex % commands.length]; - socket.write(`${command}\r\n`); - - await new Promise((resolve) => { - socket.once('data', () => { - requestCount++; - resolve(); - }); - }); - - commandIndex++; - - // Small delay to avoid overwhelming - if (requestCount % 20 === 0) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - } - - // Calculate final CPU usage - const finalCpuUsage = process.cpuUsage(initialCpuUsage); - const totalCpuTimeMs = (finalCpuUsage.user + finalCpuUsage.system) / 1000; - const elapsedTime = Date.now() - startTime; - const cpuUtilizationPercent = (totalCpuTimeMs / elapsedTime) * 100; - const requestsPerSecond = (requestCount / elapsedTime) * 1000; - - console.log(`\nStress Test Results:`); - console.log(`Requests processed: ${requestCount}`); - console.log(`Requests per second: ${requestsPerSecond.toFixed(1)}`); - console.log(`CPU utilization: ${cpuUtilizationPercent.toFixed(1)}%`); - console.log(`CPU time per request: ${(totalCpuTimeMs / requestCount).toFixed(2)}ms`); - - socket.write('QUIT\r\n'); - socket.end(); - - // Test passes if CPU usage per request is reasonable - const cpuPerRequest = totalCpuTimeMs / requestCount; - expect(cpuPerRequest).toBeLessThan(10); // Less than 10ms CPU per request - clearTimeout(testTimeout); - done.resolve(); - } catch (error) { - clearTimeout(testTimeout); - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-04.memory-usage.ts b/test/suite/smtpserver_performance/test.perf-04.memory-usage.ts deleted file mode 100644 index 51209d9..0000000 --- a/test/suite/smtpserver_performance/test.perf-04.memory-usage.ts +++ /dev/null @@ -1,238 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('PERF-04: Memory usage - Connection memory test', async (tools) => { - const done = tools.defer(); - const connectionCount = 10; // Reduced from 20 to make test faster - const connections: net.Socket[] = []; - - try { - // Force garbage collection if available - if (global.gc) { - global.gc(); - } - - // Record initial memory usage - const initialMemory = process.memoryUsage(); - console.log(`Initial memory usage: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`); - - // Create multiple connections with large email content - console.log(`Creating ${connectionCount} connections with large emails...`); - - for (let i = 0; i < connectionCount; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - connections.push(socket); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write(`EHLO testhost-mem-${i}\r\n`); - await waitForResponse(socket, '250'); - - // Send email transaction - socket.write(`MAIL FROM:\r\n`); - await waitForResponse(socket, '250'); - - socket.write(`RCPT TO:\r\n`); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Send large email content - const largeContent = 'This is a large email content for memory testing. '.repeat(100); - const emailContent = [ - `From: sender${i}@example.com`, - `To: recipient${i}@example.com`, - `Subject: Memory Usage Test ${i}`, - '', - largeContent, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - await waitForResponse(socket, '250'); - - // Pause every 5 connections - if (i > 0 && i % 5 === 0) { - await new Promise(resolve => setTimeout(resolve, 100)); - const intermediateMemory = process.memoryUsage(); - console.log(`Memory after ${i} connections: ${Math.round(intermediateMemory.heapUsed / (1024 * 1024))}MB`); - } - } - - // Wait to let memory stabilize - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Record final memory usage - const finalMemory = process.memoryUsage(); - const memoryIncreaseMB = (finalMemory.heapUsed - initialMemory.heapUsed) / (1024 * 1024); - const memoryPerConnectionKB = (memoryIncreaseMB * 1024) / connectionCount; - - console.log(`\nMemory Usage Results:`); - console.log(`Initial heap: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Final heap: ${Math.round(finalMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`); - console.log(`Memory per connection: ${memoryPerConnectionKB.toFixed(2)}KB`); - console.log(`RSS increase: ${Math.round((finalMemory.rss - initialMemory.rss) / (1024 * 1024))}MB`); - - // Clean up connections - for (const socket of connections) { - if (socket.writable) { - socket.write('QUIT\r\n'); - socket.end(); - } - } - - // Test passes if memory increase is reasonable (less than 30MB for 10 connections) - expect(memoryIncreaseMB).toBeLessThan(30); - done.resolve(); - } catch (error) { - // Clean up on error - connections.forEach(socket => socket.destroy()); - done.reject(error); - } -}); - -tap.test('PERF-04: Memory usage - Memory leak detection', async (tools) => { - const done = tools.defer(); - const iterations = 3; // Reduced from 5 - const connectionsPerIteration = 3; // Reduced from 5 - - try { - // Force GC if available - if (global.gc) { - global.gc(); - } - - const initialMemory = process.memoryUsage(); - const memorySnapshots: number[] = []; - - console.log(`\nRunning memory leak detection (${iterations} iterations)...`); - - for (let iteration = 0; iteration < iterations; iteration++) { - const sockets: net.Socket[] = []; - - // Create and close connections - for (let i = 0; i < connectionsPerIteration; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Quick transaction - await waitForResponse(socket, '220'); - - socket.write('EHLO leaktest\r\n'); - await waitForResponse(socket, '250'); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - sockets.push(socket); - } - - // Wait for sockets to close - await new Promise(resolve => setTimeout(resolve, 500)); - - // Force cleanup - sockets.forEach(s => s.destroy()); - - // Force GC if available - if (global.gc) { - global.gc(); - } - - // Record memory after each iteration - const currentMemory = process.memoryUsage(); - const memoryMB = currentMemory.heapUsed / (1024 * 1024); - memorySnapshots.push(memoryMB); - - console.log(`Iteration ${iteration + 1}: ${memoryMB.toFixed(2)}MB`); - - await new Promise(resolve => setTimeout(resolve, 500)); - } - - // Check for memory leak pattern - const firstSnapshot = memorySnapshots[0]; - const lastSnapshot = memorySnapshots[memorySnapshots.length - 1]; - const memoryGrowth = lastSnapshot - firstSnapshot; - const avgGrowthPerIteration = memoryGrowth / (iterations - 1); - - console.log(`\nMemory Leak Detection Results:`); - console.log(`First snapshot: ${firstSnapshot.toFixed(2)}MB`); - console.log(`Last snapshot: ${lastSnapshot.toFixed(2)}MB`); - console.log(`Total growth: ${memoryGrowth.toFixed(2)}MB`); - console.log(`Average growth per iteration: ${avgGrowthPerIteration.toFixed(2)}MB`); - - // Test passes if average growth per iteration is less than 2MB - expect(avgGrowthPerIteration).toBeLessThan(2); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-05.connection-processing-time.ts b/test/suite/smtpserver_performance/test.perf-05.connection-processing-time.ts deleted file mode 100644 index 2dfd4ad..0000000 --- a/test/suite/smtpserver_performance/test.perf-05.connection-processing-time.ts +++ /dev/null @@ -1,363 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('PERF-05: Connection processing time - Connection establishment', async (tools) => { - const done = tools.defer(); - const testConnections = 10; - const connectionTimes: number[] = []; - - try { - console.log(`Testing connection establishment time for ${testConnections} connections...`); - - for (let i = 0; i < testConnections; i++) { - const connectionStart = Date.now(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => { - const connectionTime = Date.now() - connectionStart; - connectionTimes.push(connectionTime); - resolve(); - }); - socket.once('error', reject); - }); - - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Clean close - socket.write('QUIT\r\n'); - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - socket.end(); - - // Small delay between connections - await new Promise(resolve => setTimeout(resolve, 50)); - } - - // Calculate statistics - const avgConnectionTime = connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length; - const minConnectionTime = Math.min(...connectionTimes); - const maxConnectionTime = Math.max(...connectionTimes); - - console.log(`\nConnection Establishment Results:`); - console.log(`Average: ${avgConnectionTime.toFixed(0)}ms`); - console.log(`Min: ${minConnectionTime}ms`); - console.log(`Max: ${maxConnectionTime}ms`); - console.log(`All times: ${connectionTimes.join(', ')}ms`); - - // Test passes if average connection time is less than 1000ms - expect(avgConnectionTime).toBeLessThan(1000); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('PERF-05: Connection processing time - Transaction processing', async (tools) => { - const done = tools.defer(); - const testTransactions = 10; - const processingTimes: number[] = []; - const fullTransactionTimes: number[] = []; - - // Add a timeout to prevent test from hanging - const testTimeout = setTimeout(() => { - console.log('Test timeout reached, moving on...'); - done.resolve(); - }, 30000); // 30 second timeout - - try { - console.log(`\nTesting transaction processing time for ${testTransactions} transactions...`); - - for (let i = 0; i < testTransactions; i++) { - const fullTransactionStart = Date.now(); - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - const processingStart = Date.now(); - - // Send EHLO - socket.write(`EHLO testhost-perf-${i}\r\n`); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - // Look for the end of EHLO response (250 without dash) - if (data.includes('250 ')) { - socket.removeListener('data', handleData); - resolve(); - } - }; - socket.on('data', handleData); - }); - - // Send MAIL FROM - socket.write(`MAIL FROM:\r\n`); - - await new Promise((resolve, reject) => { - let mailResponse = ''; - const handleMailResponse = (chunk: Buffer) => { - mailResponse += chunk.toString(); - if (mailResponse.includes('\r\n')) { - socket.removeListener('data', handleMailResponse); - if (mailResponse.includes('250')) { - resolve(); - } else { - reject(new Error(`MAIL FROM failed: ${mailResponse}`)); - } - } - }; - socket.on('data', handleMailResponse); - }); - - // Send RCPT TO - socket.write(`RCPT TO:\r\n`); - - await new Promise((resolve, reject) => { - let rcptResponse = ''; - const handleRcptResponse = (chunk: Buffer) => { - rcptResponse += chunk.toString(); - if (rcptResponse.includes('\r\n')) { - socket.removeListener('data', handleRcptResponse); - if (rcptResponse.includes('250')) { - resolve(); - } else { - reject(new Error(`RCPT TO failed: ${rcptResponse}`)); - } - } - }; - socket.on('data', handleRcptResponse); - }); - - // Send DATA - socket.write('DATA\r\n'); - - await new Promise((resolve, reject) => { - let dataResponse = ''; - const handleDataResponse = (chunk: Buffer) => { - dataResponse += chunk.toString(); - if (dataResponse.includes('\r\n')) { - socket.removeListener('data', handleDataResponse); - if (dataResponse.includes('354')) { - resolve(); - } else { - reject(new Error(`DATA failed: ${dataResponse}`)); - } - } - }; - socket.on('data', handleDataResponse); - }); - - // Send email content - const emailContent = [ - `From: sender${i}@example.com`, - `To: recipient${i}@example.com`, - `Subject: Connection Processing Test ${i}`, - '', - 'Connection processing time test.', - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - - await new Promise((resolve, reject) => { - let submitResponse = ''; - const handleSubmitResponse = (chunk: Buffer) => { - submitResponse += chunk.toString(); - if (submitResponse.includes('\r\n') && submitResponse.includes('250')) { - socket.removeListener('data', handleSubmitResponse); - resolve(); - } else if (submitResponse.includes('\r\n') && (submitResponse.includes('4') || submitResponse.includes('5'))) { - socket.removeListener('data', handleSubmitResponse); - reject(new Error(`Message submission failed: ${submitResponse}`)); - } - }; - socket.on('data', handleSubmitResponse); - }); - - const processingTime = Date.now() - processingStart; - processingTimes.push(processingTime); - - // Send QUIT - socket.write('QUIT\r\n'); - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - socket.end(); - - const fullTransactionTime = Date.now() - fullTransactionStart; - fullTransactionTimes.push(fullTransactionTime); - - // Small delay between transactions - await new Promise(resolve => setTimeout(resolve, 50)); - } - - // Calculate statistics - const avgProcessingTime = processingTimes.reduce((a, b) => a + b, 0) / processingTimes.length; - const minProcessingTime = Math.min(...processingTimes); - const maxProcessingTime = Math.max(...processingTimes); - - const avgFullTime = fullTransactionTimes.reduce((a, b) => a + b, 0) / fullTransactionTimes.length; - - console.log(`\nTransaction Processing Results:`); - console.log(`Average processing: ${avgProcessingTime.toFixed(0)}ms`); - console.log(`Min processing: ${minProcessingTime}ms`); - console.log(`Max processing: ${maxProcessingTime}ms`); - console.log(`Average full transaction: ${avgFullTime.toFixed(0)}ms`); - - // Test passes if average processing time is less than 2000ms - expect(avgProcessingTime).toBeLessThan(2000); - clearTimeout(testTimeout); - done.resolve(); - } catch (error) { - clearTimeout(testTimeout); - done.reject(error); - } -}); - -tap.test('PERF-05: Connection processing time - Command response times', async (tools) => { - const done = tools.defer(); - const commandTimings: { [key: string]: number[] } = { - EHLO: [], - NOOP: [] - }; - - // Add a timeout to prevent test from hanging - const testTimeout = setTimeout(() => { - console.log('Command timing test timeout reached, moving on...'); - done.resolve(); - }, 20000); // 20 second timeout - - try { - console.log(`\nMeasuring individual command response times...`); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await new Promise((resolve) => { - let greeting = ''; - const handleGreeting = (chunk: Buffer) => { - greeting += chunk.toString(); - if (greeting.includes('220') && greeting.includes('\r\n')) { - socket.removeListener('data', handleGreeting); - resolve(); - } - }; - socket.on('data', handleGreeting); - }); - - // Measure EHLO response times - for (let i = 0; i < 3; i++) { - const start = Date.now(); - socket.write('EHLO testhost\r\n'); - - await new Promise((resolve) => { - let data = ''; - const handleData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('250 ')) { - socket.removeListener('data', handleData); - commandTimings.EHLO.push(Date.now() - start); - resolve(); - } - }; - socket.on('data', handleData); - }); - } - - // Measure NOOP response times - for (let i = 0; i < 3; i++) { - const start = Date.now(); - socket.write('NOOP\r\n'); - - await new Promise((resolve) => { - let noopResponse = ''; - const handleNoop = (chunk: Buffer) => { - noopResponse += chunk.toString(); - if (noopResponse.includes('\r\n')) { - socket.removeListener('data', handleNoop); - commandTimings.NOOP.push(Date.now() - start); - resolve(); - } - }; - socket.on('data', handleNoop); - }); - } - - // Close connection - socket.write('QUIT\r\n'); - await new Promise((resolve) => { - socket.once('data', () => { - socket.end(); - resolve(); - }); - }); - - // Calculate and display results - console.log(`\nCommand Response Times (ms):`); - for (const [command, times] of Object.entries(commandTimings)) { - if (times.length > 0) { - const avg = times.reduce((a, b) => a + b, 0) / times.length; - console.log(`${command}: avg=${avg.toFixed(0)}, samples=[${times.join(', ')}]`); - - // All commands should respond in less than 500ms on average - expect(avg).toBeLessThan(500); - } - } - - clearTimeout(testTimeout); - done.resolve(); - } catch (error) { - clearTimeout(testTimeout); - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-06.message-processing-time.ts b/test/suite/smtpserver_performance/test.perf-06.message-processing-time.ts deleted file mode 100644 index b9442b3..0000000 --- a/test/suite/smtpserver_performance/test.perf-06.message-processing-time.ts +++ /dev/null @@ -1,252 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('PERF-06: Message processing time - Various message sizes', async (tools) => { - const done = tools.defer(); - const messageSizes = [1000, 5000, 10000, 25000, 50000]; // bytes - const messageProcessingTimes: number[] = []; - const processingRates: number[] = []; - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await waitForResponse(socket, '250'); - - console.log('Testing message processing times for various sizes...\n'); - - for (let i = 0; i < messageSizes.length; i++) { - const messageSize = messageSizes[i]; - const messageContent = 'A'.repeat(messageSize); - - const messageStart = Date.now(); - - // Send MAIL FROM - socket.write(`MAIL FROM:\r\n`); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write(`RCPT TO:\r\n`); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Send email content - const emailContent = [ - `From: sender${i}@example.com`, - `To: recipient${i}@example.com`, - `Subject: Message Processing Test ${i} (${messageSize} bytes)`, - '', - messageContent, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - await waitForResponse(socket, '250'); - - const messageProcessingTime = Date.now() - messageStart; - messageProcessingTimes.push(messageProcessingTime); - - const processingRateKBps = (messageSize / 1024) / (messageProcessingTime / 1000); - processingRates.push(processingRateKBps); - - console.log(`${messageSize} bytes: ${messageProcessingTime}ms (${processingRateKBps.toFixed(1)} KB/s)`); - - // Send RSET - socket.write('RSET\r\n'); - - await new Promise((resolve) => { - socket.once('data', (chunk) => { - const response = chunk.toString(); - expect(response).toInclude('250'); - resolve(); - }); - }); - - // Small delay between tests - await new Promise(resolve => setTimeout(resolve, 100)); - } - - // Calculate statistics - const avgProcessingTime = messageProcessingTimes.reduce((a, b) => a + b, 0) / messageProcessingTimes.length; - const avgProcessingRate = processingRates.reduce((a, b) => a + b, 0) / processingRates.length; - const minProcessingTime = Math.min(...messageProcessingTimes); - const maxProcessingTime = Math.max(...messageProcessingTimes); - - console.log(`\nMessage Processing Results:`); - console.log(`Average processing time: ${avgProcessingTime.toFixed(0)}ms`); - console.log(`Min/Max processing time: ${minProcessingTime}ms / ${maxProcessingTime}ms`); - console.log(`Average processing rate: ${avgProcessingRate.toFixed(1)} KB/s`); - - socket.write('QUIT\r\n'); - socket.end(); - - // Test passes if average processing time is less than 3000ms and rate > 10KB/s - expect(avgProcessingTime).toBeLessThan(3000); - expect(avgProcessingRate).toBeGreaterThan(10); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('PERF-06: Message processing time - Large message handling', async (tools) => { - const done = tools.defer(); - const largeSizes = [100000, 250000, 500000]; // 100KB, 250KB, 500KB - const results: Array<{ size: number; time: number; rate: number }> = []; - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 60000 // Longer timeout for large messages - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testhost-large\r\n'); - await waitForResponse(socket, '250'); - - console.log('\nTesting large message processing...\n'); - - for (let i = 0; i < largeSizes.length; i++) { - const messageSize = largeSizes[i]; - - const messageStart = Date.now(); - - // Send MAIL FROM - socket.write(`MAIL FROM:\r\n`); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write(`RCPT TO:\r\n`); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Send large email content in chunks to avoid buffer issues - socket.write(`From: largesender${i}@example.com\r\n`); - socket.write(`To: largerecipient${i}@example.com\r\n`); - socket.write(`Subject: Large Message Test ${i} (${messageSize} bytes)\r\n\r\n`); - - // Send content in 10KB chunks - const chunkSize = 10000; - let remaining = messageSize; - while (remaining > 0) { - const currentChunk = Math.min(remaining, chunkSize); - socket.write('B'.repeat(currentChunk)); - remaining -= currentChunk; - - // Small delay to avoid overwhelming buffers - if (remaining > 0) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - } - - socket.write('\r\n.\r\n'); - - const response = await waitForResponse(socket, '250', 30000); - expect(response).toInclude('250'); - - const messageProcessingTime = Date.now() - messageStart; - const processingRateMBps = (messageSize / (1024 * 1024)) / (messageProcessingTime / 1000); - - results.push({ - size: messageSize, - time: messageProcessingTime, - rate: processingRateMBps - }); - - console.log(`${(messageSize/1024).toFixed(0)}KB: ${messageProcessingTime}ms (${processingRateMBps.toFixed(2)} MB/s)`); - - // Send RSET - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - - // Delay between large tests - await new Promise(resolve => setTimeout(resolve, 500)); - } - - const avgRate = results.reduce((sum, r) => sum + r.rate, 0) / results.length; - console.log(`\nAverage large message rate: ${avgRate.toFixed(2)} MB/s`); - - socket.write('QUIT\r\n'); - socket.end(); - - // Test passes if we can process at least 0.5 MB/s - expect(avgRate).toBeGreaterThan(0.5); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_performance/test.perf-07.resource-cleanup.ts b/test/suite/smtpserver_performance/test.perf-07.resource-cleanup.ts deleted file mode 100644 index 1b82a07..0000000 --- a/test/suite/smtpserver_performance/test.perf-07.resource-cleanup.ts +++ /dev/null @@ -1,317 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('PERF-07: Resource cleanup - Connection cleanup efficiency', async (tools) => { - const done = tools.defer(); - const testConnections = 20; // Reduced from 50 - const connections: net.Socket[] = []; - const cleanupTimes: number[] = []; - - try { - // Force GC if available - if (global.gc) { - global.gc(); - } - - const initialMemory = process.memoryUsage(); - console.log(`Initial memory: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Creating ${testConnections} connections for resource cleanup test...`); - - // Create many connections and process emails - for (let i = 0; i < testConnections; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - connections.push(socket); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write(`EHLO testhost-cleanup-${i}\r\n`); - - await waitForResponse(socket, '250'); - - // Complete email transaction - socket.write(`MAIL FROM:\r\n`); - await waitForResponse(socket, '250'); - - socket.write(`RCPT TO:\r\n`); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - const emailContent = [ - `From: sender${i}@example.com`, - `To: recipient${i}@example.com`, - `Subject: Resource Cleanup Test ${i}`, - '', - 'Testing resource cleanup.', - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - await waitForResponse(socket, '250'); - - // Pause every 10 connections - if (i > 0 && i % 10 === 0) { - await new Promise(resolve => setTimeout(resolve, 50)); - } - } - - const midTestMemory = process.memoryUsage(); - console.log(`Memory after creating connections: ${Math.round(midTestMemory.heapUsed / (1024 * 1024))}MB`); - - // Clean up all connections and measure cleanup time - console.log('\nCleaning up connections...'); - - for (let i = 0; i < connections.length; i++) { - const socket = connections[i]; - const cleanupStart = Date.now(); - - try { - if (socket.writable) { - socket.write('QUIT\r\n'); - try { - await waitForResponse(socket, '221', 1000); - } catch (e) { - // Ignore timeout on QUIT - } - } - - socket.end(); - await new Promise((resolve) => { - socket.once('close', () => resolve()); - setTimeout(() => resolve(), 100); // Fallback timeout - }); - - cleanupTimes.push(Date.now() - cleanupStart); - } catch (error) { - cleanupTimes.push(Date.now() - cleanupStart); - } - } - - // Wait for cleanup to complete - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Force GC if available - if (global.gc) { - global.gc(); - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - const finalMemory = process.memoryUsage(); - const memoryIncreaseMB = (finalMemory.heapUsed - initialMemory.heapUsed) / (1024 * 1024); - const avgCleanupTime = cleanupTimes.reduce((a, b) => a + b, 0) / cleanupTimes.length; - const maxCleanupTime = Math.max(...cleanupTimes); - - console.log(`\nResource Cleanup Results:`); - console.log(`Initial memory: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Mid-test memory: ${Math.round(midTestMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Final memory: ${Math.round(finalMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`); - console.log(`Average cleanup time: ${avgCleanupTime.toFixed(0)}ms`); - console.log(`Max cleanup time: ${maxCleanupTime}ms`); - - // Test passes if memory increase is less than 10MB and cleanup is fast - expect(memoryIncreaseMB).toBeLessThan(10); - expect(avgCleanupTime).toBeLessThan(100); - done.resolve(); - } catch (error) { - // Emergency cleanup - connections.forEach(socket => socket.destroy()); - done.reject(error); - } -}); - -tap.test('PERF-07: Resource cleanup - File descriptor management', async (tools) => { - const done = tools.defer(); - const rapidConnections = 20; - let successfulCleanups = 0; - - try { - console.log(`\nTesting rapid connection open/close cycles...`); - - for (let i = 0; i < rapidConnections; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 10000 - }); - - try { - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Quick EHLO/QUIT - socket.write('EHLO rapidtest\r\n'); - await waitForResponse(socket, '250'); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - - await new Promise((resolve) => { - socket.once('close', () => { - successfulCleanups++; - resolve(); - }); - }); - - } catch (error) { - socket.destroy(); - console.log(`Connection ${i} failed:`, error); - } - - // Very short delay - await new Promise(resolve => setTimeout(resolve, 20)); - } - - console.log(`Successful cleanups: ${successfulCleanups}/${rapidConnections}`); - - // Test passes if at least 90% of connections cleaned up successfully - const cleanupRate = successfulCleanups / rapidConnections; - expect(cleanupRate).toBeGreaterThanOrEqual(0.9); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('PERF-07: Resource cleanup - Memory recovery after load', async (tools) => { - const done = tools.defer(); - - try { - // Force GC if available - if (global.gc) { - global.gc(); - } - - const baselineMemory = process.memoryUsage(); - console.log(`\nBaseline memory: ${Math.round(baselineMemory.heapUsed / (1024 * 1024))}MB`); - - // Create load - const loadConnections = 10; - const sockets: net.Socket[] = []; - - console.log('Creating load...'); - for (let i = 0; i < loadConnections; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - sockets.push(socket); - - // Just connect, don't send anything - await waitForResponse(socket, '220'); - } - - const loadMemory = process.memoryUsage(); - console.log(`Memory under load: ${Math.round(loadMemory.heapUsed / (1024 * 1024))}MB`); - - // Clean up all at once - console.log('Cleaning up all connections...'); - sockets.forEach(socket => { - socket.destroy(); - }); - - // Wait for cleanup - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Force GC multiple times - if (global.gc) { - for (let i = 0; i < 3; i++) { - global.gc(); - await new Promise(resolve => setTimeout(resolve, 500)); - } - } - - const recoveredMemory = process.memoryUsage(); - const memoryIncrease = loadMemory.heapUsed - baselineMemory.heapUsed; - const memoryRecovered = loadMemory.heapUsed - recoveredMemory.heapUsed; - const recoveryPercent = memoryIncrease > 0 ? (memoryRecovered / memoryIncrease) * 100 : 100; - - console.log(`Memory after cleanup: ${Math.round(recoveredMemory.heapUsed / (1024 * 1024))}MB`); - console.log(`Memory recovered: ${Math.round(memoryRecovered / (1024 * 1024))}MB`); - console.log(`Recovery percentage: ${recoveryPercent.toFixed(1)}%`); - - // Test passes if memory is stable (no significant increase) or we recover at least 50% - if (memoryIncrease < 1024 * 1024) { // Less than 1MB increase - console.log('Memory usage was stable during test - good resource management!'); - expect(true).toEqual(true); - } else { - expect(recoveryPercent).toBeGreaterThan(50); - } - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_reliability/test.rel-01.long-running-operation.ts b/test/suite/smtpserver_reliability/test.rel-01.long-running-operation.ts deleted file mode 100644 index 4948b1d..0000000 --- a/test/suite/smtpserver_reliability/test.rel-01.long-running-operation.ts +++ /dev/null @@ -1,344 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('REL-01: Long-running operation - Continuous email sending', async (tools) => { - const done = tools.defer(); - const testDuration = 30000; // 30 seconds - const operationInterval = 2000; // 2 seconds between operations - const startTime = Date.now(); - const endTime = startTime + testDuration; - - let operations = 0; - let successful = 0; - let errors = 0; - let connectionIssues = 0; - const operationResults: Array<{ - operation: number; - success: boolean; - duration: number; - error?: string; - timestamp: number; - }> = []; - - console.log(`Running long-duration test for ${testDuration/1000} seconds...`); - - const performOperation = async (operationId: number): Promise => { - const operationStart = Date.now(); - operations++; - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 10000 - }); - - const result = await new Promise<{ success: boolean; error?: string; connectionIssue?: boolean }>((resolve) => { - let step = 'connecting'; - let receivedData = ''; - - const timeout = setTimeout(() => { - socket.destroy(); - resolve({ - success: false, - error: `Timeout in step ${step}`, - connectionIssue: true - }); - }, 10000); - - socket.on('connect', () => { - step = 'connected'; - }); - - socket.on('data', (chunk) => { - receivedData += chunk.toString(); - const lines = receivedData.split('\r\n'); - - for (const line of lines) { - if (!line.trim()) continue; - - // Check for errors - if (line.match(/^[45]\d\d\s/)) { - clearTimeout(timeout); - socket.destroy(); - resolve({ - success: false, - error: `SMTP error in ${step}: ${line}`, - connectionIssue: false - }); - return; - } - - // Process responses - if (step === 'connected' && line.startsWith('220')) { - step = 'ehlo'; - socket.write(`EHLO longrun-${operationId}\r\n`); - } else if (step === 'ehlo' && line.includes('250 ') && !line.includes('250-')) { - step = 'mail_from'; - socket.write(`MAIL FROM:\r\n`); - } else if (step === 'mail_from' && line.startsWith('250')) { - step = 'rcpt_to'; - socket.write(`RCPT TO:\r\n`); - } else if (step === 'rcpt_to' && line.startsWith('250')) { - step = 'data'; - socket.write('DATA\r\n'); - } else if (step === 'data' && line.startsWith('354')) { - step = 'email_content'; - const emailContent = [ - `From: sender${operationId}@example.com`, - `To: recipient${operationId}@example.com`, - `Subject: Long Running Test Operation ${operationId}`, - `Date: ${new Date().toUTCString()}`, - '', - `This is test operation ${operationId} for long-running reliability testing.`, - `Timestamp: ${Date.now()}`, - '.', - '' - ].join('\r\n'); - socket.write(emailContent); - } else if (step === 'email_content' && line.startsWith('250')) { - step = 'quit'; - socket.write('QUIT\r\n'); - } else if (step === 'quit' && line.startsWith('221')) { - clearTimeout(timeout); - socket.end(); - resolve({ - success: true - }); - return; - } - } - }); - - socket.on('error', (error) => { - clearTimeout(timeout); - resolve({ - success: false, - error: error.message, - connectionIssue: true - }); - }); - - socket.on('close', () => { - if (step !== 'quit') { - clearTimeout(timeout); - resolve({ - success: false, - error: 'Connection closed unexpectedly', - connectionIssue: true - }); - } - }); - }); - - const duration = Date.now() - operationStart; - - if (result.success) { - successful++; - } else { - errors++; - if (result.connectionIssue) { - connectionIssues++; - } - } - - operationResults.push({ - operation: operationId, - success: result.success, - duration, - error: result.error, - timestamp: operationStart - }); - - } catch (error) { - errors++; - operationResults.push({ - operation: operationId, - success: false, - duration: Date.now() - operationStart, - error: error instanceof Error ? error.message : 'Unknown error', - timestamp: operationStart - }); - } - }; - - try { - // Run operations continuously until end time - while (Date.now() < endTime) { - const operationStart = Date.now(); - - await performOperation(operations + 1); - - // Calculate wait time for next operation - const nextOperation = operationStart + operationInterval; - const waitTime = nextOperation - Date.now(); - - if (waitTime > 0 && Date.now() < endTime) { - await new Promise(resolve => setTimeout(resolve, waitTime)); - } - - // Progress update every 5 operations - if (operations % 5 === 0) { - console.log(`Progress: ${operations} operations, ${successful} successful, ${errors} errors`); - } - } - - // Calculate results - const totalDuration = Date.now() - startTime; - const successRate = successful / operations; - const connectionIssueRate = connectionIssues / operations; - const avgOperationTime = operationResults.reduce((sum, r) => sum + r.duration, 0) / operations; - - console.log(`\nLong-Running Operation Results:`); - console.log(`Total duration: ${(totalDuration/1000).toFixed(1)}s`); - console.log(`Total operations: ${operations}`); - console.log(`Successful: ${successful} (${(successRate * 100).toFixed(1)}%)`); - console.log(`Errors: ${errors}`); - console.log(`Connection issues: ${connectionIssues} (${(connectionIssueRate * 100).toFixed(1)}%)`); - console.log(`Average operation time: ${avgOperationTime.toFixed(0)}ms`); - - // Show last few operations for debugging - console.log('\nLast 5 operations:'); - operationResults.slice(-5).forEach(op => { - console.log(` Op ${op.operation}: ${op.success ? 'success' : 'failed'} (${op.duration}ms)${op.error ? ' - ' + op.error : ''}`); - }); - - // Test passes with 85% success rate and max 10% connection issues - expect(successRate).toBeGreaterThanOrEqual(0.85); - expect(connectionIssueRate).toBeLessThanOrEqual(0.1); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-01: Long-running operation - Server stability check', async (tools) => { - const done = tools.defer(); - const checkDuration = 15000; // 15 seconds - const checkInterval = 3000; // 3 seconds between checks - const startTime = Date.now(); - const endTime = startTime + checkDuration; - - const stabilityChecks: Array<{ - timestamp: number; - responseTime: number; - success: boolean; - error?: string; - }> = []; - - console.log(`\nRunning server stability checks for ${checkDuration/1000} seconds...`); - - try { - while (Date.now() < endTime) { - const checkStart = Date.now(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - const checkResult = await new Promise<{ success: boolean; responseTime: number; error?: string }>((resolve) => { - const connectTime = Date.now(); - let greetingReceived = false; - - const timeout = setTimeout(() => { - socket.destroy(); - resolve({ - success: false, - responseTime: Date.now() - connectTime, - error: 'Timeout waiting for greeting' - }); - }, 5000); - - socket.on('connect', () => { - // Connected - }); - - socket.once('data', (chunk) => { - const response = chunk.toString(); - clearTimeout(timeout); - greetingReceived = true; - - if (response.startsWith('220')) { - socket.write('QUIT\r\n'); - socket.end(); - resolve({ - success: true, - responseTime: Date.now() - connectTime - }); - } else { - socket.end(); - resolve({ - success: false, - responseTime: Date.now() - connectTime, - error: `Unexpected greeting: ${response.substring(0, 50)}` - }); - } - }); - - socket.on('error', (error) => { - clearTimeout(timeout); - resolve({ - success: false, - responseTime: Date.now() - connectTime, - error: error.message - }); - }); - }); - - stabilityChecks.push({ - timestamp: checkStart, - responseTime: checkResult.responseTime, - success: checkResult.success, - error: checkResult.error - }); - - console.log(`Stability check ${stabilityChecks.length}: ${checkResult.success ? 'OK' : 'FAILED'} (${checkResult.responseTime}ms)`); - - // Wait for next check - const nextCheck = checkStart + checkInterval; - const waitTime = nextCheck - Date.now(); - if (waitTime > 0 && Date.now() < endTime) { - await new Promise(resolve => setTimeout(resolve, waitTime)); - } - } - - // Analyze stability - const successfulChecks = stabilityChecks.filter(c => c.success).length; - const avgResponseTime = stabilityChecks - .filter(c => c.success) - .reduce((sum, c) => sum + c.responseTime, 0) / successfulChecks || 0; - const maxResponseTime = Math.max(...stabilityChecks.filter(c => c.success).map(c => c.responseTime)); - - console.log(`\nStability Check Results:`); - console.log(`Total checks: ${stabilityChecks.length}`); - console.log(`Successful: ${successfulChecks} (${(successfulChecks/stabilityChecks.length * 100).toFixed(1)}%)`); - console.log(`Average response time: ${avgResponseTime.toFixed(0)}ms`); - console.log(`Max response time: ${maxResponseTime}ms`); - - // All checks should succeed for stable server - expect(successfulChecks).toEqual(stabilityChecks.length); - expect(avgResponseTime).toBeLessThan(1000); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_reliability/test.rel-02.restart-recovery.ts b/test/suite/smtpserver_reliability/test.rel-02.restart-recovery.ts deleted file mode 100644 index 6cb78eb..0000000 --- a/test/suite/smtpserver_reliability/test.rel-02.restart-recovery.ts +++ /dev/null @@ -1,328 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('REL-02: Restart recovery - Server state after restart', async (tools) => { - const done = tools.defer(); - - try { - console.log('Testing server state and recovery capabilities...'); - - // First, establish that server is working normally - const socket1 = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket1.once('connect', resolve); - socket1.once('error', reject); - }); - - // Read greeting - const greeting1 = await waitForResponse(socket1, '220'); - expect(greeting1).toInclude('220'); - console.log('Initial connection successful'); - - // Send EHLO - socket1.write('EHLO testhost\r\n'); - await waitForResponse(socket1, '250'); - - // Complete a transaction - socket1.write('MAIL FROM:\r\n'); - const mailResp1 = await waitForResponse(socket1, '250'); - expect(mailResp1).toInclude('250'); - - socket1.write('RCPT TO:\r\n'); - const rcptResp1 = await waitForResponse(socket1, '250'); - expect(rcptResp1).toInclude('250'); - - socket1.write('DATA\r\n'); - const dataResp1 = await waitForResponse(socket1, '354'); - expect(dataResp1).toInclude('354'); - - const emailContent = [ - 'From: sender@example.com', - 'To: recipient@example.com', - 'Subject: Pre-restart test', - '', - 'Testing server state before restart.', - '.', - '' - ].join('\r\n'); - - socket1.write(emailContent); - const sendResp1 = await waitForResponse(socket1, '250'); - expect(sendResp1).toInclude('250'); - - socket1.write('QUIT\r\n'); - await waitForResponse(socket1, '221'); - socket1.end(); - - console.log('Pre-restart transaction completed successfully'); - - // Simulate server restart by closing and reopening connections - console.log('\nSimulating server restart scenario...'); - - // Wait a moment to simulate restart time - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Test recovery after simulated restart - const socket2 = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket2.once('connect', resolve); - socket2.once('error', reject); - }); - - // Read greeting after "restart" - const greeting2 = await new Promise((resolve) => { - socket2.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - expect(greeting2).toInclude('220'); - console.log('Post-restart connection successful'); - - // Verify server is fully functional after restart - socket2.write('EHLO testhost-postrestart\r\n'); - await waitForResponse(socket2, '250'); - - // Complete another transaction to verify full recovery - socket2.write('MAIL FROM:\r\n'); - const mailResp2 = await waitForResponse(socket2, '250'); - expect(mailResp2).toInclude('250'); - - socket2.write('RCPT TO:\r\n'); - const rcptResp2 = await waitForResponse(socket2, '250'); - expect(rcptResp2).toInclude('250'); - - socket2.write('DATA\r\n'); - const dataResp2 = await waitForResponse(socket2, '354'); - expect(dataResp2).toInclude('354'); - - const postRestartEmail = [ - 'From: sender2@example.com', - 'To: recipient2@example.com', - 'Subject: Post-restart recovery test', - '', - 'Testing server recovery after restart.', - '.', - '' - ].join('\r\n'); - - socket2.write(postRestartEmail); - const sendResp2 = await waitForResponse(socket2, '250'); - expect(sendResp2).toInclude('250'); - - socket2.write('QUIT\r\n'); - await waitForResponse(socket2, '221'); - socket2.end(); - - console.log('Post-restart transaction completed successfully'); - console.log('Server recovered successfully from restart'); - - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-02: Restart recovery - Multiple rapid reconnections', async (tools) => { - const done = tools.defer(); - const rapidConnections = 10; - let successfulReconnects = 0; - - try { - console.log(`\nTesting rapid reconnection after disruption (${rapidConnections} attempts)...`); - - for (let i = 0; i < rapidConnections; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - socket.destroy(); - reject(new Error('Connection timeout')); - }, 5000); - - socket.once('connect', () => { - clearTimeout(timeout); - resolve(); - }); - socket.once('error', (err) => { - clearTimeout(timeout); - reject(err); - }); - }); - - // Read greeting - try { - const greeting = await waitForResponse(socket, '220', 3000); - if (greeting.includes('220')) { - successfulReconnects++; - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221', 1000).catch(() => {}); - socket.end(); - } else { - socket.destroy(); - } - } catch (error) { - socket.destroy(); - throw error; - } - - // Very short delay between attempts - await new Promise(resolve => setTimeout(resolve, 100)); - - } catch (error) { - console.log(`Reconnection ${i + 1} failed:`, error.message); - } - } - - const reconnectRate = successfulReconnects / rapidConnections; - console.log(`Successful reconnections: ${successfulReconnects}/${rapidConnections} (${(reconnectRate * 100).toFixed(1)}%)`); - - // Expect high success rate for good recovery - expect(reconnectRate).toBeGreaterThanOrEqual(0.8); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-02: Restart recovery - State persistence check', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting server state persistence across connections...'); - - // Create initial connection and start transaction - const socket1 = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket1.once('connect', resolve); - socket1.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket1, '220'); - - // Send EHLO - socket1.write('EHLO persistence-test\r\n'); - await waitForResponse(socket1, '250'); - - // Start transaction but don't complete it - socket1.write('MAIL FROM:\r\n'); - const mailResp = await waitForResponse(socket1, '250'); - expect(mailResp).toInclude('250'); - - // Abruptly close connection - socket1.destroy(); - console.log('Abruptly closed connection with incomplete transaction'); - - // Wait briefly - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Create new connection and verify server recovered - const socket2 = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket2.once('connect', resolve); - socket2.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket2, '220'); - - // Send EHLO - socket2.write('EHLO recovery-test\r\n'); - await waitForResponse(socket2, '250'); - - // Try new transaction - should work without issues from previous incomplete one - socket2.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket2, '250'); - expect(mailResponse).toInclude('250'); - console.log('Server recovered successfully - new transaction started without issues'); - - socket2.write('QUIT\r\n'); - await waitForResponse(socket2, '221'); - socket2.end(); - - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_reliability/test.rel-03.resource-leak-detection.ts b/test/suite/smtpserver_reliability/test.rel-03.resource-leak-detection.ts deleted file mode 100644 index 8d49ae5..0000000 --- a/test/suite/smtpserver_reliability/test.rel-03.resource-leak-detection.ts +++ /dev/null @@ -1,394 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -interface ResourceMetrics { - timestamp: number; - memoryUsage: { - rss: number; - heapTotal: number; - heapUsed: number; - external: number; - }; - processInfo: { - pid: number; - uptime: number; - cpuUsage: NodeJS.CpuUsage; - }; -} - -interface LeakAnalysis { - memoryGrowthMB: number; - memoryTrend: number; - stabilityScore: number; - memoryLeakDetected: boolean; - resourcesStable: boolean; - samplesAnalyzed: number; - initialMemoryMB: number; - finalMemoryMB: number; -} - -const captureResourceMetrics = async (): Promise => { - // Force GC if available before measurement - if (global.gc) { - global.gc(); - await new Promise(resolve => setTimeout(resolve, 100)); - } - - const memUsage = process.memoryUsage(); - - return { - timestamp: Date.now(), - memoryUsage: { - rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100, - heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100, - heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100, - external: Math.round(memUsage.external / 1024 / 1024 * 100) / 100 - }, - processInfo: { - pid: process.pid, - uptime: process.uptime(), - cpuUsage: process.cpuUsage() - } - }; -}; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -const analyzeResourceLeaks = (initial: ResourceMetrics, samples: Array<{ operation: number; metrics: ResourceMetrics }>, final: ResourceMetrics): LeakAnalysis => { - const memoryGrowthMB = final.memoryUsage.heapUsed - initial.memoryUsage.heapUsed; - - // Analyze memory trend over samples - let memoryTrend = 0; - if (samples.length > 1) { - const firstSample = samples[0].metrics.memoryUsage.heapUsed; - const lastSample = samples[samples.length - 1].metrics.memoryUsage.heapUsed; - memoryTrend = lastSample - firstSample; - } - - // Calculate stability score based on memory variance - let stabilityScore = 1.0; - if (samples.length > 2) { - const memoryValues = samples.map(s => s.metrics.memoryUsage.heapUsed); - const average = memoryValues.reduce((a, b) => a + b, 0) / memoryValues.length; - const variance = memoryValues.reduce((acc, val) => acc + Math.pow(val - average, 2), 0) / memoryValues.length; - const stdDev = Math.sqrt(variance); - stabilityScore = Math.max(0, 1 - (stdDev / average)); - } - - return { - memoryGrowthMB: Math.round(memoryGrowthMB * 100) / 100, - memoryTrend: Math.round(memoryTrend * 100) / 100, - stabilityScore: Math.round(stabilityScore * 100) / 100, - memoryLeakDetected: memoryGrowthMB > 50, - resourcesStable: stabilityScore > 0.8 && memoryGrowthMB < 25, - samplesAnalyzed: samples.length, - initialMemoryMB: initial.memoryUsage.heapUsed, - finalMemoryMB: final.memoryUsage.heapUsed - }; -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('REL-03: Resource leak detection - Memory leak analysis', async (tools) => { - const done = tools.defer(); - const operationCount = 20; - const connections: net.Socket[] = []; - const samples: Array<{ operation: number; metrics: ResourceMetrics }> = []; - - try { - const initialMetrics = await captureResourceMetrics(); - console.log(`📊 Initial memory: ${initialMetrics.memoryUsage.heapUsed}MB`); - - for (let i = 0; i < operationCount; i++) { - console.log(`🔄 Operation ${i + 1}/${operationCount}...`); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - connections.push(socket); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write(`EHLO leaktest-${i}\r\n`); - await waitForResponse(socket, '250'); - - // Complete email transaction - socket.write(`MAIL FROM:\r\n`); - const mailResp = await waitForResponse(socket, '250'); - expect(mailResp).toInclude('250'); - - socket.write(`RCPT TO:\r\n`); - const rcptResp = await waitForResponse(socket, '250'); - expect(rcptResp).toInclude('250'); - - socket.write('DATA\r\n'); - const dataResp = await waitForResponse(socket, '354'); - expect(dataResp).toInclude('354'); - - const emailContent = [ - `From: sender${i}@example.com`, - `To: recipient${i}@example.com`, - `Subject: Resource Leak Test ${i + 1}`, - `Message-ID: `, - '', - `This is resource leak test iteration ${i + 1}.`, - '.', - '' - ].join('\r\n'); - - socket.write(emailContent); - const sendResp = await waitForResponse(socket, '250'); - expect(sendResp).toInclude('250'); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - socket.end(); - - // Capture metrics every 5 operations - if ((i + 1) % 5 === 0) { - const metrics = await captureResourceMetrics(); - samples.push({ - operation: i + 1, - metrics - }); - console.log(`📈 Sample ${samples.length}: Memory ${metrics.memoryUsage.heapUsed}MB`); - } - - // Small delay between operations - await new Promise(resolve => setTimeout(resolve, 100)); - } - - // Clean up all connections - connections.forEach(conn => { - if (!conn.destroyed) { - conn.destroy(); - } - }); - - // Wait for cleanup - await new Promise(resolve => setTimeout(resolve, 2000)); - - const finalMetrics = await captureResourceMetrics(); - const leakAnalysis = analyzeResourceLeaks(initialMetrics, samples, finalMetrics); - - console.log('\n📊 Resource Leak Analysis:'); - console.log(`Initial memory: ${leakAnalysis.initialMemoryMB}MB`); - console.log(`Final memory: ${leakAnalysis.finalMemoryMB}MB`); - console.log(`Memory growth: ${leakAnalysis.memoryGrowthMB}MB`); - console.log(`Memory trend: ${leakAnalysis.memoryTrend}MB`); - console.log(`Stability score: ${leakAnalysis.stabilityScore}`); - console.log(`Memory leak detected: ${leakAnalysis.memoryLeakDetected}`); - console.log(`Resources stable: ${leakAnalysis.resourcesStable}`); - - expect(leakAnalysis.memoryLeakDetected).toEqual(false); - expect(leakAnalysis.resourcesStable).toEqual(true); - done.resolve(); - } catch (error) { - connections.forEach(conn => conn.destroy()); - done.reject(error); - } -}); - -tap.test('REL-03: Resource leak detection - Connection leak test', async (tools) => { - const done = tools.defer(); - const abandonedConnections: net.Socket[] = []; - - try { - console.log('\nTesting for connection resource leaks...'); - - // Create connections that are abandoned without proper cleanup - for (let i = 0; i < 10; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - abandonedConnections.push(socket); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting but don't complete transaction - await new Promise((resolve) => { - socket.once('data', () => resolve()); - }); - - // Start but don't complete EHLO - socket.write(`EHLO abandoned-${i}\r\n`); - - // Don't wait for response, just move to next - await new Promise(resolve => setTimeout(resolve, 50)); - } - - console.log('Created 10 abandoned connections'); - - // Wait a bit - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Try to create new connections - should still work - let newConnectionsSuccessful = 0; - for (let i = 0; i < 5; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - socket.destroy(); - reject(new Error('Connection timeout')); - }, 5000); - - socket.once('connect', () => { - clearTimeout(timeout); - resolve(); - }); - socket.once('error', (err) => { - clearTimeout(timeout); - reject(err); - }); - }); - - // Verify connection works - const greeting = await new Promise((resolve) => { - socket.once('data', (chunk) => { - resolve(chunk.toString()); - }); - }); - - if (greeting.includes('220')) { - newConnectionsSuccessful++; - socket.write('QUIT\r\n'); - socket.end(); - } - } catch (error) { - console.log(`New connection ${i + 1} failed:`, error.message); - } - } - - // Clean up abandoned connections - abandonedConnections.forEach(conn => conn.destroy()); - - console.log(`New connections successful: ${newConnectionsSuccessful}/5`); - - // Server should still accept new connections despite abandoned ones - expect(newConnectionsSuccessful).toBeGreaterThanOrEqual(4); - done.resolve(); - } catch (error) { - abandonedConnections.forEach(conn => conn.destroy()); - done.reject(error); - } -}); - -tap.test('REL-03: Resource leak detection - Rapid create/destroy cycles', async (tools) => { - const done = tools.defer(); - const cycles = 30; - const initialMetrics = await captureResourceMetrics(); - - try { - console.log('\nTesting rapid connection create/destroy cycles...'); - - for (let i = 0; i < cycles; i++) { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Immediately destroy after connect - socket.destroy(); - - // Very short delay - await new Promise(resolve => setTimeout(resolve, 20)); - - if ((i + 1) % 10 === 0) { - console.log(`Completed ${i + 1} cycles`); - } - } - - // Wait for resources to be released - await new Promise(resolve => setTimeout(resolve, 3000)); - - const finalMetrics = await captureResourceMetrics(); - const memoryGrowth = finalMetrics.memoryUsage.heapUsed - initialMetrics.memoryUsage.heapUsed; - - console.log(`Memory growth after ${cycles} cycles: ${memoryGrowth.toFixed(2)}MB`); - - // Memory growth should be minimal - expect(memoryGrowth).toBeLessThan(10); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_reliability/test.rel-04.error-recovery.ts b/test/suite/smtpserver_reliability/test.rel-04.error-recovery.ts deleted file mode 100644 index ecd0c81..0000000 --- a/test/suite/smtpserver_reliability/test.rel-04.error-recovery.ts +++ /dev/null @@ -1,401 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -const createConnection = async (): Promise => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - return socket; -}; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -const getResponse = waitForResponse; - -const testBasicSmtpFlow = async (socket: net.Socket): Promise => { - try { - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO recovery-test\r\n'); - const ehloResp = await waitForResponse(socket, '250'); - if (!ehloResp.includes('250')) return false; - - socket.write('MAIL FROM:\r\n'); - const mailResp = await waitForResponse(socket, '250'); - if (!mailResp.includes('250')) return false; - - socket.write('RCPT TO:\r\n'); - const rcptResp = await waitForResponse(socket, '250'); - if (!rcptResp.includes('250')) return false; - - socket.write('DATA\r\n'); - const dataResp = await waitForResponse(socket, '354'); - if (!dataResp.includes('354')) return false; - - const testEmail = [ - 'From: sender@example.com', - 'To: recipient@example.com', - 'Subject: Recovery Test Email', - '', - 'This email tests server recovery.', - '.', - '' - ].join('\r\n'); - - socket.write(testEmail); - const finalResp = await waitForResponse(socket, '250'); - - socket.write('QUIT\r\n'); - socket.end(); - - return finalResp.includes('250'); - } catch (error) { - console.log('Basic SMTP flow error:', error); - return false; - } -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('REL-04: Error recovery - Invalid command recovery', async (tools) => { - const done = tools.defer(); - - try { - console.log('Testing recovery from invalid commands...'); - - // Phase 1: Send invalid commands - const socket1 = await createConnection(); - await waitForResponse(socket1, '220'); - - // Send multiple invalid commands - socket1.write('INVALID_COMMAND\r\n'); - const response1 = await waitForResponse(socket1); - expect(response1).toMatch(/50[0-3]/); // Should get error response - - socket1.write('ANOTHER_INVALID\r\n'); - const response2 = await waitForResponse(socket1); - expect(response2).toMatch(/50[0-3]/); - - socket1.write('YET_ANOTHER_BAD_CMD\r\n'); - const response3 = await waitForResponse(socket1); - expect(response3).toMatch(/50[0-3]/); - - socket1.end(); - - // Phase 2: Test recovery - server should still work normally - await new Promise(resolve => setTimeout(resolve, 500)); - - const socket2 = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket2); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from invalid commands'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-04: Error recovery - Malformed data recovery', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting recovery from malformed data...'); - - // Phase 1: Send malformed data - const socket1 = await createConnection(); - await waitForResponse(socket1, '220'); - - socket1.write('EHLO testhost\r\n'); - await waitForResponse(socket1, '250'); - - // Send malformed MAIL FROM - socket1.write('MAIL FROM: invalid-format\r\n'); - const response1 = await waitForResponse(socket1); - expect(response1).toMatch(/50[0-3]/); - - // Send malformed RCPT TO - socket1.write('RCPT TO: also-invalid\r\n'); - const response2 = await waitForResponse(socket1); - expect(response2).toMatch(/50[0-3]/); - - // Send malformed DATA with binary - socket1.write('DATA\x00\x01\x02CORRUPTED\r\n'); - const response3 = await waitForResponse(socket1); - expect(response3).toMatch(/50[0-3]/); - - socket1.end(); - - // Phase 2: Test recovery - await new Promise(resolve => setTimeout(resolve, 500)); - - const socket2 = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket2); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from malformed data'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-04: Error recovery - Premature disconnection recovery', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting recovery from premature disconnection...'); - - // Phase 1: Create incomplete transactions - for (let i = 0; i < 3; i++) { - const socket = await createConnection(); - await waitForResponse(socket, '220'); - - socket.write('EHLO abrupt-test\r\n'); - await waitForResponse(socket, '250'); - - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Abruptly close connection during transaction - socket.destroy(); - console.log(` Abruptly closed connection ${i + 1}`); - - await new Promise(resolve => setTimeout(resolve, 200)); - } - - // Phase 2: Test recovery - await new Promise(resolve => setTimeout(resolve, 1000)); - - const socket2 = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket2); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from premature disconnections'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-04: Error recovery - Data corruption recovery', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting recovery from data corruption...'); - - const socket1 = await createConnection(); - await waitForResponse(socket1, '220'); - - socket1.write('EHLO corruption-test\r\n'); - await waitForResponse(socket1, '250'); - - socket1.write('MAIL FROM:\r\n'); - await waitForResponse(socket1, '250'); - - socket1.write('RCPT TO:\r\n'); - await waitForResponse(socket1, '250'); - - socket1.write('DATA\r\n'); - const dataResp = await waitForResponse(socket1, '354'); - expect(dataResp).toInclude('354'); - - // Send corrupted email data with null bytes and invalid characters - socket1.write('From: test\r\n\0\0\0CORRUPTED DATA\xff\xfe\r\n'); - socket1.write('Subject: \x01\x02\x03Invalid\r\n'); - socket1.write('\r\n'); - socket1.write('Body with \0null bytes\r\n'); - socket1.write('.\r\n'); - - try { - const response = await waitForResponse(socket1); - console.log(' Server response to corrupted data:', response.substring(0, 50)); - } catch (error) { - console.log(' Server rejected corrupted data (expected)'); - } - - socket1.end(); - - // Phase 2: Test recovery - await new Promise(resolve => setTimeout(resolve, 1000)); - - const socket2 = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket2); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from data corruption'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-04: Error recovery - Connection flooding recovery', async (tools) => { - const done = tools.defer(); - const connections: net.Socket[] = []; - - try { - console.log('\nTesting recovery from connection flooding...'); - - // Phase 1: Create multiple rapid connections - console.log(' Creating 15 rapid connections...'); - for (let i = 0; i < 15; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 2000 - }); - connections.push(socket); - - // Don't wait for connection to complete - await new Promise(resolve => setTimeout(resolve, 50)); - } catch (error) { - // Some connections might fail - that's expected - console.log(` Connection ${i + 1} failed (expected during flooding)`); - } - } - - console.log(` Created ${connections.length} connections`); - - // Close all connections - connections.forEach(conn => { - try { - conn.destroy(); - } catch (e) { - // Ignore errors - } - }); - - // Phase 2: Test recovery - console.log(' Waiting for server to recover...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - - const socket2 = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket2); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from connection flooding'); - done.resolve(); - } catch (error) { - connections.forEach(conn => conn.destroy()); - done.reject(error); - } -}); - -tap.test('REL-04: Error recovery - Mixed error scenario', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting recovery from mixed error scenarios...'); - - // Create multiple error conditions simultaneously - const errorPromises = []; - - // Invalid command connection - errorPromises.push((async () => { - const socket = await createConnection(); - await waitForResponse(socket, '220'); - socket.write('TOTALLY_WRONG\r\n'); - await waitForResponse(socket); - socket.destroy(); - })()); - - // Malformed data connection - errorPromises.push((async () => { - const socket = await createConnection(); - await waitForResponse(socket, '220'); - socket.write('MAIL FROM:<<>>\r\n'); - try { - await waitForResponse(socket); - } catch (e) { - // Expected - } - socket.destroy(); - })()); - - // Abrupt disconnection - errorPromises.push((async () => { - const socket = await createConnection(); - socket.destroy(); - })()); - - // Wait for all errors to execute - await Promise.allSettled(errorPromises); - - console.log(' All error scenarios executed'); - - // Test recovery - await new Promise(resolve => setTimeout(resolve, 2000)); - - const socket = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from mixed error scenarios'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_reliability/test.rel-05.dns-resolution-failure.ts b/test/suite/smtpserver_reliability/test.rel-05.dns-resolution-failure.ts deleted file mode 100644 index 4d93fa9..0000000 --- a/test/suite/smtpserver_reliability/test.rel-05.dns-resolution-failure.ts +++ /dev/null @@ -1,335 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -interface DnsTestResult { - scenario: string; - domain: string; - expectedBehavior: string; - mailFromSuccess: boolean; - rcptToSuccess: boolean; - mailFromResponse: string; - rcptToResponse: string; - handledGracefully: boolean; -} - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('REL-05: DNS resolution failure handling - Non-existent domains', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO dns-test\r\n'); - await waitForResponse(socket, '250'); - - console.log('Testing DNS resolution for non-existent domains...'); - - // Test 1: Non-existent domain in MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - console.log(' MAIL FROM response:', mailResponse.trim()); - - // Server should either accept (and defer later) or reject immediately - const mailFromHandled = mailResponse.includes('250') || - mailResponse.includes('450') || - mailResponse.includes('550'); - expect(mailFromHandled).toEqual(true); - - // Reset if needed - if (mailResponse.includes('250')) { - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - } - - // Test 2: Non-existent domain in RCPT TO - socket.write('MAIL FROM:\r\n'); - const mailFromResp = await waitForResponse(socket, '250'); - expect(mailFromResp).toInclude('250'); - - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - console.log(' RCPT TO response:', rcptResponse.trim()); - - // Server may accept (and defer validation) or reject immediately - const rcptToHandled = rcptResponse.includes('250') || // Accepted (for later validation) - rcptResponse.includes('450') || // Temporary failure - rcptResponse.includes('550') || // Permanent failure - rcptResponse.includes('553'); // Address error - expect(rcptToHandled).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-05: DNS resolution failure handling - Malformed domains', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO malformed-test\r\n'); - await waitForResponse(socket, '250'); - - console.log('\nTesting malformed domain handling...'); - - const malformedDomains = [ - 'malformed..domain..test', - 'invalid-.domain.com', - 'domain.with.spaces .com', - '.leading-dot.com', - 'trailing-dot.com.', - 'domain@with@at.com', - 'a'.repeat(255) + '.toolong.com' // Domain too long - ]; - - for (const domain of malformedDomains) { - console.log(` Testing: ${domain.substring(0, 50)}${domain.length > 50 ? '...' : ''}`); - - socket.write(`MAIL FROM:\r\n`); - const response = await waitForResponse(socket); - - // Server should reject malformed domains or accept for later validation - const properlyHandled = response.includes('250') || // Accepted (may validate later) - response.includes('501') || // Syntax error - response.includes('550') || // Rejected - response.includes('553'); // Address error - - console.log(` Response: ${response.trim().substring(0, 50)}`); - expect(properlyHandled).toEqual(true); - - // Reset if needed - if (!response.includes('5')) { - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - } - } - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-05: DNS resolution failure handling - Special cases', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO special-test\r\n'); - await waitForResponse(socket, '250'); - - console.log('\nTesting special DNS cases...'); - - // Test 1: Localhost (may be accepted or rejected) - socket.write('MAIL FROM:\r\n'); - const localhostResponse = await waitForResponse(socket); - - console.log(' Localhost response:', localhostResponse.trim()); - const localhostHandled = localhostResponse.includes('250') || localhostResponse.includes('501'); - expect(localhostHandled).toEqual(true); - - // Only reset if transaction was started - if (localhostResponse.includes('250')) { - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - } - - // Test 2: IP address (should work) - socket.write('MAIL FROM:\r\n'); - const ipResponse = await waitForResponse(socket); - - console.log(' IP address response:', ipResponse.trim()); - const ipHandled = ipResponse.includes('250') || ipResponse.includes('501'); - expect(ipHandled).toEqual(true); - - // Only reset if transaction was started - if (ipResponse.includes('250')) { - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - } - - // Test 3: Empty domain - socket.write('MAIL FROM:\r\n'); - const emptyResponse = await waitForResponse(socket); - - console.log(' Empty domain response:', emptyResponse.trim()); - expect(emptyResponse).toMatch(/50[1-3]/); // Should reject - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-05: DNS resolution failure handling - Mixed valid/invalid recipients', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO mixed-test\r\n'); - await waitForResponse(socket, '250'); - - console.log('\nTesting mixed valid/invalid recipients...'); - - // Start transaction - socket.write('MAIL FROM:\r\n'); - const mailFromResp = await waitForResponse(socket, '250'); - expect(mailFromResp).toInclude('250'); - - // Add valid recipient - socket.write('RCPT TO:\r\n'); - const validRcptResponse = await waitForResponse(socket, '250'); - - console.log(' Valid recipient:', validRcptResponse.trim()); - expect(validRcptResponse).toInclude('250'); - - // Add invalid recipient - socket.write('RCPT TO:\r\n'); - const invalidRcptResponse = await waitForResponse(socket); - - console.log(' Invalid recipient:', invalidRcptResponse.trim()); - - // Server may accept (for later validation) or reject invalid domain - const invalidHandled = invalidRcptResponse.includes('250') || // Accepted (for later validation) - invalidRcptResponse.includes('450') || - invalidRcptResponse.includes('550') || - invalidRcptResponse.includes('553'); - expect(invalidHandled).toEqual(true); - - // Try to send data (should work if at least one valid recipient) - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket); - - if (dataResponse.includes('354')) { - socket.write('Subject: Mixed recipient test\r\n\r\nTest\r\n.\r\n'); - await waitForResponse(socket, '250'); - console.log(' Message accepted with valid recipient'); - } else { - console.log(' Server rejected DATA (acceptable behavior)'); - } - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_reliability/test.rel-06.network-interruption.ts b/test/suite/smtpserver_reliability/test.rel-06.network-interruption.ts deleted file mode 100644 index cce98ed..0000000 --- a/test/suite/smtpserver_reliability/test.rel-06.network-interruption.ts +++ /dev/null @@ -1,410 +0,0 @@ -import * as plugins from '@git.zone/tstest/tapbundle'; -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; - -let testServer; - -const createConnection = async (): Promise => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 5000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - return socket; -}; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -const getResponse = waitForResponse; - -const testBasicSmtpFlow = async (socket: net.Socket): Promise => { - try { - await waitForResponse(socket, '220'); - - socket.write('EHLO test.example.com\r\n'); - const ehloResp = await waitForResponse(socket, '250'); - if (!ehloResp.includes('250')) return false; - - socket.write('MAIL FROM:\r\n'); - const mailResp = await waitForResponse(socket, '250'); - if (!mailResp.includes('250')) return false; - - socket.write('RCPT TO:\r\n'); - const rcptResp = await waitForResponse(socket, '250'); - if (!rcptResp.includes('250')) return false; - - socket.write('DATA\r\n'); - const dataResp = await waitForResponse(socket, '354'); - if (!dataResp.includes('354')) return false; - - const testEmail = [ - 'From: test@example.com', - 'To: recipient@example.com', - 'Subject: Interruption Recovery Test', - '', - 'This email tests server recovery after network interruption.', - '.', - '' - ].join('\r\n'); - - socket.write(testEmail); - const finalResp = await waitForResponse(socket, '250'); - - socket.write('QUIT\r\n'); - socket.end(); - - return finalResp.includes('250'); - } catch (error) { - return false; - } -}; - -tap.test('prepare server', async () => { - testServer = await startTestServer({ port: TEST_PORT }); - await new Promise(resolve => setTimeout(resolve, 100)); -}); - -tap.test('REL-06: Network interruption - Sudden connection drop', async (tools) => { - const done = tools.defer(); - - try { - console.log('Testing sudden connection drop during session...'); - - // Phase 1: Create connection and drop it mid-session - const socket1 = await createConnection(); - await waitForResponse(socket1, '220'); - - socket1.write('EHLO testhost\r\n'); - await waitForResponse(socket1, '250'); - - socket1.write('MAIL FROM:\r\n'); - await waitForResponse(socket1, '250'); - - // Abruptly close connection during active session - socket1.destroy(); - console.log(' Connection abruptly closed'); - - // Phase 2: Test recovery - await new Promise(resolve => setTimeout(resolve, 1000)); - - const socket2 = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(socket2); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from sudden connection drop'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-06: Network interruption - Data transfer interruption', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting connection interruption during data transfer...'); - - const socket = await createConnection(); - await waitForResponse(socket, '220'); - - socket.write('EHLO datatest\r\n'); - await waitForResponse(socket, '250'); - - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - const dataResp = await waitForResponse(socket, '354'); - expect(dataResp).toInclude('354'); - - // Start sending data but interrupt midway - socket.write('From: sender@example.com\r\n'); - socket.write('To: recipient@example.com\r\n'); - socket.write('Subject: Interruption Test\r\n\r\n'); - socket.write('This email will be interrupted...\r\n'); - - // Wait briefly then destroy connection (simulating network loss) - await new Promise(resolve => setTimeout(resolve, 500)); - socket.destroy(); - console.log(' Connection interrupted during data transfer'); - - // Test recovery - await new Promise(resolve => setTimeout(resolve, 1500)); - - const newSocket = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(newSocket); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from data transfer interruption'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-06: Network interruption - Rapid reconnection attempts', async (tools) => { - const done = tools.defer(); - const connections: net.Socket[] = []; - - try { - console.log('\nTesting rapid reconnection after interruptions...'); - - // Create and immediately destroy multiple connections - console.log(' Creating 5 unstable connections...'); - for (let i = 0; i < 5; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 2000 - }); - - connections.push(socket); - - // Destroy after short random delay to simulate instability - setTimeout(() => socket.destroy(), 50 + Math.random() * 150); - - await new Promise(resolve => setTimeout(resolve, 50)); - } catch (error) { - // Expected - some connections might fail - } - } - - // Wait for cleanup - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Now test if server can handle normal connections - let successfulConnections = 0; - console.log(' Testing recovery with stable connections...'); - - for (let i = 0; i < 3; i++) { - try { - const socket = await createConnection(); - const success = await testBasicSmtpFlow(socket); - - if (success) { - successfulConnections++; - } - } catch (error) { - console.log(` Connection ${i + 1} failed:`, error.message); - } - - await new Promise(resolve => setTimeout(resolve, 500)); - } - - const recoveryRate = successfulConnections / 3; - console.log(` Recovery rate: ${successfulConnections}/3 (${(recoveryRate * 100).toFixed(0)}%)`); - - expect(recoveryRate).toBeGreaterThanOrEqual(0.66); // At least 2/3 should succeed - console.log('✓ Server recovered from rapid reconnection attempts'); - done.resolve(); - } catch (error) { - connections.forEach(conn => conn.destroy()); - done.reject(error); - } -}); - -tap.test('REL-06: Network interruption - Partial command interruption', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting partial command transmission interruption...'); - - const socket = await createConnection(); - await waitForResponse(socket, '220'); - - // Send partial EHLO command and interrupt - socket.write('EH'); - console.log(' Sent partial command "EH"'); - - await new Promise(resolve => setTimeout(resolve, 100)); - socket.destroy(); - console.log(' Connection destroyed with incomplete command'); - - // Test recovery - await new Promise(resolve => setTimeout(resolve, 1000)); - - const newSocket = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(newSocket); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered from partial command interruption'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-06: Network interruption - Multiple interruption types', async (tools) => { - const done = tools.defer(); - const results: Array<{ type: string; recovered: boolean }> = []; - - try { - console.log('\nTesting recovery from multiple interruption types...'); - - // Test 1: Interrupt after greeting - try { - const socket = await createConnection(); - await waitForResponse(socket, '220'); - socket.destroy(); - results.push({ type: 'after-greeting', recovered: false }); - } catch (e) { - results.push({ type: 'after-greeting', recovered: false }); - } - - await new Promise(resolve => setTimeout(resolve, 500)); - - // Test 2: Interrupt during EHLO - try { - const socket = await createConnection(); - await waitForResponse(socket, '220'); - socket.write('EHLO te'); - socket.destroy(); - results.push({ type: 'during-ehlo', recovered: false }); - } catch (e) { - results.push({ type: 'during-ehlo', recovered: false }); - } - - await new Promise(resolve => setTimeout(resolve, 500)); - - // Test 3: Interrupt with invalid data - try { - const socket = await createConnection(); - await waitForResponse(socket, '220'); - socket.write('\x00\x01\x02\x03'); - socket.destroy(); - results.push({ type: 'invalid-data', recovered: false }); - } catch (e) { - results.push({ type: 'invalid-data', recovered: false }); - } - - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Test final recovery - try { - const socket = await createConnection(); - const success = await testBasicSmtpFlow(socket); - - if (success) { - // Mark all previous tests as recovered - results.forEach(r => r.recovered = true); - } - } catch (error) { - console.log('Final recovery failed:', error.message); - } - - const recoveredCount = results.filter(r => r.recovered).length; - console.log(`\nInterruption recovery summary:`); - results.forEach(r => { - console.log(` ${r.type}: ${r.recovered ? 'recovered' : 'failed'}`); - }); - - expect(recoveredCount).toBeGreaterThan(0); - console.log(`✓ Server recovered from ${recoveredCount}/${results.length} interruption scenarios`); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('REL-06: Network interruption - Long delay recovery', async (tools) => { - const done = tools.defer(); - - try { - console.log('\nTesting recovery after long network interruption...'); - - // Create connection and start transaction - const socket = await createConnection(); - await waitForResponse(socket, '220'); - - socket.write('EHLO longdelay\r\n'); - await waitForResponse(socket, '250'); - - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Simulate long network interruption - socket.pause(); - console.log(' Connection paused (simulating network freeze)'); - - await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second "freeze" - - // Try to continue - should fail - socket.resume(); - socket.write('RCPT TO:\r\n'); - - let continuationFailed = false; - try { - await waitForResponse(socket, '250', 3000); - } catch (error) { - continuationFailed = true; - console.log(' Continuation failed as expected'); - } - - socket.destroy(); - - // Test recovery with new connection - const newSocket = await createConnection(); - const recoverySuccess = await testBasicSmtpFlow(newSocket); - - expect(recoverySuccess).toEqual(true); - console.log('✓ Server recovered after long network interruption'); - done.resolve(); - } catch (error) { - done.reject(error); - } -}); - -tap.test('cleanup server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-01.rfc5321-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-01.rfc5321-compliance.ts deleted file mode 100644 index 2229c5c..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-01.rfc5321-compliance.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 5321 - Server greeting format', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for initial greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server greeting:', greeting.trim()); - - // RFC 5321: Server must provide proper 220 greeting - const greetingLine = greeting.trim(); - const validGreeting = greetingLine.startsWith('220') && greetingLine.length > 10; - - expect(validGreeting).toEqual(true); - expect(greetingLine).toMatch(/^220\s+\S+/); // Should have hostname after 220 - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5321 - EHLO response format', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // RFC 5321: EHLO must return 250 with hostname and extensions - const ehloLines = ehloResponse.split('\r\n').filter(line => line.startsWith('250')); - - expect(ehloLines.length).toBeGreaterThan(0); - expect(ehloLines[0]).toMatch(/^250[\s-]\S+/); // First line should have hostname - - // Check for common extensions - const extensions = ehloLines.slice(1).map(line => line.substring(4).trim()); - console.log('Extensions:', extensions); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5321 - Command case insensitivity', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Test lowercase command - socket.write('ehlo testclient\r\n'); - await waitForResponse(socket, '250'); - - // Test mixed case command - socket.write('MaIl FrOm:\r\n'); - await waitForResponse(socket, '250'); - - // Test uppercase command - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // All case variations worked - console.log('All case variations accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5321 - Line length limits', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // RFC 5321: Command line limit is 512 chars including CRLF - // Test with a long MAIL FROM command (but within limit) - const longDomain = 'a'.repeat(400); - socket.write(`MAIL FROM:\r\n`); - const response = await waitForResponse(socket); - - // Should either accept (if within server limits) or reject gracefully - const accepted = response.includes('250'); - const rejected = response.includes('501') || response.includes('500'); - - expect(accepted || rejected).toEqual(true); - console.log(`Long line test ${accepted ? 'accepted' : 'rejected'}`); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5321 - Standard SMTP verb compliance', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - const supportedVerbs: string[] = []; - - // Wait for greeting - await waitForResponse(socket, '220'); - - // Try HELP command to see supported verbs - socket.write('HELP\r\n'); - const helpResponse = await waitForResponse(socket); - - // Parse HELP response for supported commands - if (helpResponse.includes('214') || helpResponse.includes('502')) { - // Either help text or command not implemented - } - - // Test NOOP - socket.write('NOOP\r\n'); - const noopResponse = await waitForResponse(socket); - if (noopResponse.includes('250')) { - supportedVerbs.push('NOOP'); - } - - // Test RSET - socket.write('RSET\r\n'); - const rsetResponse = await waitForResponse(socket); - if (rsetResponse.includes('250')) { - supportedVerbs.push('RSET'); - } - - // Test VRFY - socket.write('VRFY test@example.com\r\n'); - const vrfyResponse = await waitForResponse(socket); - // VRFY may be disabled for security (252 or 502) - if (vrfyResponse.includes('250') || vrfyResponse.includes('252')) { - supportedVerbs.push('VRFY'); - } - - // Check minimum required verbs - const requiredVerbs = ['NOOP', 'RSET']; - const hasRequired = requiredVerbs.every(verb => - supportedVerbs.includes(verb) || verb === 'VRFY' // VRFY is optional - ); - - console.log('Supported verbs:', supportedVerbs); - expect(hasRequired).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5321 - Required minimum extensions', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - - // Check for extensions - const lines = ehloResponse.split('\r\n'); - const extensions = lines - .filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0)) - .map(line => line.substring(4).split(' ')[0].toUpperCase()); - - console.log('Server extensions:', extensions); - - // RFC 5321 recommends these extensions - const recommendedExtensions = ['8BITMIME', 'SIZE', 'PIPELINING']; - const hasRecommended = recommendedExtensions.filter(ext => extensions.includes(ext)); - - console.log('Recommended extensions present:', hasRecommended); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-02.rfc5322-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-02.rfc5322-compliance.ts deleted file mode 100644 index 6ec0450..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-02.rfc5322-compliance.ts +++ /dev/null @@ -1,428 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 5322 - Message format with required headers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // RFC 5322 compliant email with all required headers - const messageId = ``; - const date = new Date().toUTCString(); - - const rfc5322Email = [ - `Date: ${date}`, - `From: "Test Sender" `, - `To: "Test Recipient" `, - `Subject: RFC 5322 Compliance Test`, - `Message-ID: ${messageId}`, - `MIME-Version: 1.0`, - `Content-Type: text/plain; charset=UTF-8`, - `Content-Transfer-Encoding: 7bit`, - '', - 'This is a test message for RFC 5322 compliance verification.', - 'It includes proper headers according to RFC 5322 specifications.', - '', - 'Best regards,', - 'Test System', - '.', - '' - ].join('\r\n'); - - socket.write(rfc5322Email); - const response = await waitForResponse(socket, '250'); - - console.log('RFC 5322 compliant message accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5322 - Folded header lines', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Test folded header lines (RFC 5322 section 2.2.3) - const email = [ - `Date: ${new Date().toUTCString()}`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: This is a very long subject line that needs to be`, - ` folded according to RFC 5322 specifications for proper`, - ` email header formatting`, - `Message-ID: <${Date.now()}@example.com>`, - `References: `, - ` `, - ` `, - '', - 'Email with folded headers.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Folded headers message accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5322 - Multiple recipient formats', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send multiple RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Test various recipient formats allowed by RFC 5322 - const email = [ - `Date: ${new Date().toUTCString()}`, - `From: "Sender Name" `, - `To: recipient1@example.com, "Recipient Two" `, - `Cc: "Carbon Copy" `, - `Bcc: bcc@example.com`, - `Reply-To: "Reply Address" `, - `Subject: Multiple recipient formats test`, - `Message-ID: <${Date.now()}@example.com>`, - '', - 'Testing various recipient header formats.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Multiple recipient formats accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5322 - Comments in headers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // RFC 5322 allows comments in headers using parentheses - const email = [ - `Date: ${new Date().toUTCString()} (generated by test system)`, - `From: sender@example.com (Test Sender)`, - `To: recipient@example.com (Primary Recipient)`, - `Subject: Testing comments (RFC 5322 section 3.2.2)`, - `Message-ID: <${Date.now()}@example.com>`, - `X-Custom-Header: value (with comment)`, - '', - 'Email with comments in headers.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Headers with comments accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 5322 - Resent headers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // RFC 5322 resent headers for forwarded messages - const email = [ - `Resent-Date: ${new Date().toUTCString()}`, - `Resent-From: resender@example.com`, - `Resent-To: newrecipient@example.com`, - `Resent-Message-ID: `, - `Date: ${new Date(Date.now() - 86400000).toUTCString()}`, // Original date (yesterday) - `From: original@example.com`, - `To: oldrecipient@example.com`, - `Subject: Forwarded: Original Subject`, - `Message-ID: `, - '', - 'This is a forwarded message with resent headers.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Resent headers message accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-03.rfc7208-spf-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-03.rfc7208-spf-compliance.ts deleted file mode 100644 index 9f00d20..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-03.rfc7208-spf-compliance.ts +++ /dev/null @@ -1,330 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 7208 SPF - Server handles SPF checks', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - const spfResults: any[] = []; - - // Test domains simulating different SPF scenarios - const spfTestDomains = [ - 'spf-pass.example.com', // Should have valid SPF record allowing sender - 'spf-fail.example.com', // Should have SPF record that fails - 'spf-neutral.example.com', // Should have neutral SPF record - 'no-spf.example.com' // Should have no SPF record - ]; - - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - - // Check if server advertises SPF support - const advertisesSpf = ehloResponse.toLowerCase().includes('spf'); - console.log('Server advertises SPF:', advertisesSpf); - - // Test each domain - for (let i = 0; i < spfTestDomains.length; i++) { - const domain = spfTestDomains[i]; - const testEmail = `spf-test@${domain}`; - - spfResults[i] = { - domain: domain, - email: testEmail, - mailFromAccepted: false, - rcptAccepted: false, - spfFailed: false - }; - - console.log(`Testing SPF for domain: ${domain}`); - socket.write(`MAIL FROM:<${testEmail}>\r\n`); - const mailResponse = await waitForResponse(socket); - - spfResults[i].mailFromResponse = mailResponse.trim(); - - if (mailResponse.includes('250')) { - // MAIL FROM accepted - spfResults[i].mailFromAccepted = true; - - socket.write(`RCPT TO:\r\n`); - const rcptResponse = await waitForResponse(socket); - - if (rcptResponse.includes('250')) { - spfResults[i].rcptAccepted = true; - } - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - // SPF failure (expected for some domains) - spfResults[i].spfFailed = true; - } - - // Reset for next test - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - } - - // All tests complete - console.log('SPF test results:', spfResults); - - // Check that server handled all domains - const allDomainsHandled = spfResults.every(result => - result.mailFromResponse !== undefined && result.mailFromResponse !== 'pending' - ); - - expect(allDomainsHandled).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 7208 SPF - SPF record syntax handling', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Test with domain that might have complex SPF record - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - // Server should handle this appropriately (accept or reject based on SPF) - const handled = mailResponse.includes('250') || - mailResponse.includes('550') || - mailResponse.includes('553'); - - expect(handled).toEqual(true); - console.log('SPF handling response:', mailResponse.trim()); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 7208 SPF - Received-SPF header', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Send email to check if server adds Received-SPF header - const email = [ - `Date: ${new Date().toUTCString()}`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: SPF Header Test`, - `Message-ID: <${Date.now()}@example.com>`, - '', - 'Testing if server adds Received-SPF header.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email accepted - server should process SPF'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 7208 SPF - IPv4 and IPv6 mechanism support', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Test with IPv6 address representation - socket.write('EHLO [::1]\r\n'); - await waitForResponse(socket, '250'); - - // Test domain with IP-based SPF mechanisms - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - // Server should handle IP-based SPF mechanisms - const handled = mailResponse.includes('250') || - mailResponse.includes('550') || - mailResponse.includes('553'); - - expect(handled).toEqual(true); - console.log('IP mechanism SPF response:', mailResponse.trim()); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-04.rfc6376-dkim-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-04.rfc6376-dkim-compliance.ts deleted file mode 100644 index b79bf69..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-04.rfc6376-dkim-compliance.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 6376 DKIM - Server accepts email with DKIM signature', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Create email with DKIM signature - const dkimSignature = [ - 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=default;', - ' h=from:to:subject:date:message-id;', - ' bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;', - ' b=Kt1zLCYmUVYJKEOVL9nGF2JVPJ5/k5l6yOkNBJGCrZn4E5z9Qn7TlYrG8QfBgJ4', - ' CzYVLjKm5xOhUoEaDzTJ1E6C9A4hL8sKfBxQjN8oWv4kP3GdE6mFqS0wKcRjT+', - ' NxOz2VcJP4LmKjFsG8XqBhYoEfCvSr3UwNmEkP6RjT9WlQzA4kJe2VoMsJ=' - ].join('\r\n'); - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DKIM RFC 6376 Compliance Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - dkimSignature, - '', - 'This email tests RFC 6376 DKIM compliance.', - 'The server should properly handle DKIM signatures.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email with DKIM signature accepted'); - expect(true).toEqual(true); // Server accepts DKIM headers - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 6376 DKIM - Multiple DKIM signatures', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with multiple DKIM signatures (common in forwarding scenarios) - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Multiple DKIM Signatures Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=selector1;', - ' h=from:to:subject:date;', - ' bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;', - ' b=signature1data', - 'DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple;', - ' d=forwarder.com; s=selector2;', - ' h=from:to:subject:date:message-id;', - ' bh=differentbodyhash=;', - ' b=signature2data', - '', - 'Email with multiple DKIM signatures.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email with multiple DKIM signatures accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 6376 DKIM - Various canonicalization methods', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Test different canonicalization methods - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DKIM Canonicalization Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - 'DKIM-Signature: v=1; a=rsa-sha256; c=simple/relaxed;', - ' d=example.com; s=default;', - ' h=from:to:subject;', - ' bh=bodyhash=;', - ' b=signature', - '', - 'Testing different canonicalization methods.', - 'Simple header canonicalization preserves whitespace.', - 'Relaxed body canonicalization normalizes whitespace.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email with different canonicalization accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 6376 DKIM - Long header fields and folding', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // DKIM signature with long fields that require folding - const longSignature = 'b=' + 'A'.repeat(200); - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DKIM Long Fields Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=default; t=' + Math.floor(Date.now() / 1000) + ';', - ' h=from:to:subject:date:message-id:content-type:mime-version;', - ' bh=verylongbodyhashvalueherethatexceedsnormallength1234567890=;', - ' ' + longSignature.substring(0, 70), - ' ' + longSignature.substring(70, 140), - ' ' + longSignature.substring(140), - '', - 'Testing DKIM with long header fields.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email with long DKIM fields accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 6376 DKIM - Authentication-Results header', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - - // Check if server advertises DKIM support - const advertisesDkim = ehloResponse.toLowerCase().includes('dkim'); - console.log('Server advertises DKIM:', advertisesDkim); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email to test if server adds Authentication-Results header - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Authentication-Results Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=default;', - ' h=from:to:subject;', - ' bh=simplehash=;', - ' b=simplesignature', - '', - 'Testing if server adds Authentication-Results header.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email accepted - server should process DKIM and potentially add Authentication-Results'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-05.rfc7489-dmarc-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-05.rfc7489-dmarc-compliance.ts deleted file mode 100644 index a3c6a0c..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-05.rfc7489-dmarc-compliance.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 7489 DMARC - Server handles DMARC policies', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - const dmarcResults: any[] = []; - - // Test domains simulating different DMARC policies - const dmarcTestScenarios = [ - { - domain: 'dmarc-reject.example.com', - policy: 'reject', - alignment: 'strict' - }, - { - domain: 'dmarc-quarantine.example.com', - policy: 'quarantine', - alignment: 'relaxed' - }, - { - domain: 'dmarc-none.example.com', - policy: 'none', - alignment: 'relaxed' - } - ]; - - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - - // Check if server advertises DMARC support - const advertisesDmarc = ehloResponse.toLowerCase().includes('dmarc'); - console.log('Server advertises DMARC:', advertisesDmarc); - - // Test each scenario - for (let i = 0; i < dmarcTestScenarios.length; i++) { - const scenario = dmarcTestScenarios[i]; - const testFromAddress = `dmarc-test@${scenario.domain}`; - - dmarcResults[i] = { - domain: scenario.domain, - policy: scenario.policy, - mailFromAccepted: false, - rcptAccepted: false - }; - - console.log(`Testing DMARC policy: ${scenario.policy} for domain: ${scenario.domain}`); - socket.write(`MAIL FROM:<${testFromAddress}>\r\n`); - const mailResponse = await waitForResponse(socket); - - dmarcResults[i].mailFromResponse = mailResponse.trim(); - - if (mailResponse.includes('250')) { - dmarcResults[i].mailFromAccepted = true; - - socket.write(`RCPT TO:\r\n`); - const rcptResponse = await waitForResponse(socket); - - if (rcptResponse.includes('250')) { - dmarcResults[i].rcptAccepted = true; - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Send email with DMARC-relevant headers - const email = [ - `From: dmarc-test@${scenario.domain}`, - `To: recipient@example.com`, - `Subject: DMARC RFC 7489 Compliance Test - ${scenario.policy}`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=${scenario.domain}; s=default;`, - ` h=from:to:subject:date; bh=testbodyhash; b=testsignature`, - `Authentication-Results: example.org; spf=pass smtp.mailfrom=${scenario.domain}`, - '', - `This email tests DMARC ${scenario.policy} policy compliance.`, - 'The server should handle DMARC policies according to RFC 7489.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket, '250'); - - dmarcResults[i].emailAccepted = true; - console.log(`DMARC ${scenario.policy} policy email accepted`); - } - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - // DMARC policy rejection (expected for some scenarios) - dmarcResults[i].dmarcRejected = true; - dmarcResults[i].rejectionResponse = mailResponse.trim(); - console.log(`DMARC ${scenario.policy} policy rejected as expected`); - } - - // Reset for next test - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - } - - // All tests complete - console.log('DMARC test results:', dmarcResults); - - // Check that server handled all scenarios - const allScenariosHandled = dmarcResults.every(result => - result.mailFromResponse !== undefined - ); - - expect(allScenariosHandled).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 7489 DMARC - Alignment testing', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Test misaligned domain (envelope vs header) - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with different header From domain (testing alignment) - const email = [ - `From: sender@header-domain.com`, - `To: recipient@example.com`, - `Subject: DMARC Alignment Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=header-domain.com; s=default;`, - ` h=from:to:subject:date; bh=alignmenthash; b=alignmentsig`, - '', - 'Testing DMARC domain alignment (envelope vs header From).', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const response = await waitForResponse(socket); - - const accepted = response.includes('250'); - console.log(`Alignment test ${accepted ? 'accepted' : 'rejected due to alignment failure'}`); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 7489 DMARC - Subdomain policy', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Test subdomain policy inheritance - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email from subdomain to test policy inheritance - const email = [ - `From: sender@subdomain.dmarc-policy.com`, - `To: recipient@example.com`, - `Subject: DMARC Subdomain Policy Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=subdomain.dmarc-policy.com; s=default;`, - ` h=from:to:subject:date; bh=subdomainhash; b=subdomainsig`, - '', - 'Testing DMARC subdomain policy inheritance.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const response = await waitForResponse(socket); - - const accepted = response.includes('250'); - console.log(`Subdomain policy test ${accepted ? 'accepted' : 'rejected'}`); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 7489 DMARC - Report generation hint', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with DMARC report request headers - const email = [ - `From: dmarc-report@example.com`, - `To: recipient@example.com`, - `Subject: DMARC Report Generation Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=default;`, - ` h=from:to:subject:date; bh=reporthash; b=reportsig`, - `Authentication-Results: mta.example.com;`, - ` dmarc=pass (p=none dis=none) header.from=example.com`, - '', - 'Testing DMARC report generation capabilities.', - 'Server should log DMARC results for reporting.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('DMARC report test email accepted'); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-06.rfc8314-tls-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-06.rfc8314-tls-compliance.ts deleted file mode 100644 index e073401..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-06.rfc8314-tls-compliance.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import * as tls from 'tls'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 8314 TLS - STARTTLS advertised in EHLO', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - - // Check if STARTTLS is advertised (RFC 8314 requirement) - const advertisesStarttls = ehloResponse.toLowerCase().includes('starttls'); - - console.log('STARTTLS advertised:', advertisesStarttls); - expect(advertisesStarttls).toEqual(true); - - // Parse other extensions - const lines = ehloResponse.split('\r\n'); - const extensions = lines - .filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0)) - .map(line => line.substring(4).split(' ')[0].toUpperCase()); - - console.log('Server extensions:', extensions); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 8314 TLS - STARTTLS command functionality', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - - const advertisesStarttls = ehloResponse.toLowerCase().includes('starttls'); - - if (advertisesStarttls) { - // Send STARTTLS - socket.write('STARTTLS\r\n'); - const starttlsResponse = await waitForResponse(socket, '220'); - - console.log('STARTTLS command accepted, ready to upgrade'); - - // In a real test, we would upgrade to TLS here - // For this test, we just verify the command is accepted - expect(true).toEqual(true); - } else { - console.log('STARTTLS not advertised, skipping upgrade'); - } - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 8314 TLS - Commands before STARTTLS', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Try MAIL FROM before STARTTLS (server may require TLS first) - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - // Server may accept or reject based on TLS policy - if (mailResponse.includes('250')) { - console.log('Server allows MAIL FROM before STARTTLS'); - } else if (mailResponse.includes('530') || mailResponse.includes('554')) { - console.log('Server requires STARTTLS before MAIL FROM (RFC 8314 compliant)'); - expect(true).toEqual(true); // This is actually good for security - } - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 8314 TLS - TLS version support', async (tools) => { - const done = tools.defer(); - - // First establish plain connection to get STARTTLS - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send STARTTLS - socket.write('STARTTLS\r\n'); - const starttlsResponse = await waitForResponse(socket, '220'); - - console.log('Ready to upgrade to TLS'); - - // Upgrade connection to TLS - const tlsOptions = { - socket: socket, - rejectUnauthorized: false, // For testing - minVersion: 'TLSv1.2' as any // RFC 8314 recommends TLS 1.2 or higher - }; - - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.on('secureConnect', () => { - console.log('TLS connection established'); - console.log('Protocol:', tlsSocket.getProtocol()); - console.log('Cipher:', tlsSocket.getCipher()); - - // Verify TLS 1.2 or higher - const protocol = tlsSocket.getProtocol(); - if (protocol) { - expect(['TLSv1.2', 'TLSv1.3']).toContain(protocol); - } - - tlsSocket.write('EHLO testclient\r\n'); - }); - - tlsSocket.on('data', (data) => { - const response = data.toString(); - console.log('TLS response:', response); - - if (response.includes('250')) { - console.log('EHLO after STARTTLS successful'); - tlsSocket.write('QUIT\r\n'); - setTimeout(() => { - tlsSocket.end(); - done.resolve(); - }, 100); - } - }); - - tlsSocket.on('error', (err) => { - console.error('TLS error:', err); - // If TLS upgrade fails, still pass the test as server accepted STARTTLS - done.resolve(); - }); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 8314 TLS - Email submission after STARTTLS', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // For this test, proceed without STARTTLS to test basic functionality - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.includes('250')) { - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - const email = [ - `Date: ${new Date().toUTCString()}`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: RFC 8314 TLS Compliance Test`, - `Message-ID: `, - '', - 'Testing email submission with TLS requirements.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('Email accepted (server allows non-TLS or we are testing on TLS port)'); - } else { - // Server may require STARTTLS first - console.log('Server requires STARTTLS for mail submission'); - } - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_rfc-compliance/test.rfc-07.rfc3461-dsn-compliance.ts b/test/suite/smtpserver_rfc-compliance/test.rfc-07.rfc3461-dsn-compliance.ts deleted file mode 100644 index ea9a242..0000000 --- a/test/suite/smtpserver_rfc-compliance/test.rfc-07.rfc3461-dsn-compliance.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper function to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - // Check if we have a complete response - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Any complete response line - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('RFC 3461 DSN - DSN extension advertised', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Check if DSN extension is advertised - const advertisesDsn = ehloResponse.toLowerCase().includes('dsn'); - console.log('DSN extension advertised:', advertisesDsn); - - // Parse extensions - const lines = ehloResponse.split('\r\n'); - const extensions = lines - .filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0)) - .map(line => line.substring(4).split(' ')[0].toUpperCase()); - - console.log('Server extensions:', extensions); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - socket.end(); - done.resolve(); - } catch (error) { - console.error('Socket error:', error); - done.reject(error); - } -}); - -tap.test('RFC 3461 DSN - MAIL FROM with DSN parameters', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Test MAIL FROM with DSN parameters (RFC 3461) - socket.write('MAIL FROM: RET=FULL ENVID=test-envelope-123\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - // Server should either accept (250) or reject with proper error - const accepted = mailResponse.includes('250'); - const properlyRejected = mailResponse.includes('501') || mailResponse.includes('555'); - - expect(accepted || properlyRejected).toEqual(true); - console.log(`DSN parameters in MAIL FROM ${accepted ? 'accepted' : 'rejected'}`); - - if (accepted) { - // Reset to test other parameters - socket.write('RSET\r\n'); - const resetResponse = await waitForResponse(socket, '250'); - console.log('Server response:', resetResponse); - - // Test with RET=HDRS - socket.write('MAIL FROM: RET=HDRS\r\n'); - const mailHdrsResponse = await waitForResponse(socket); - console.log('Server response:', mailHdrsResponse); - - const hdrsAccepted = mailHdrsResponse.includes('250'); - console.log(`RET=HDRS parameter ${hdrsAccepted ? 'accepted' : 'rejected'}`); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - socket.end(); - done.resolve(); - } catch (error) { - console.error('Socket error:', error); - done.reject(error); - } -}); - -tap.test('RFC 3461 DSN - RCPT TO with DSN parameters', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - - // Read greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Test RCPT TO with DSN parameters - socket.write('RCPT TO: NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;recipient@example.com\r\n'); - const rcptResponse = await waitForResponse(socket); - console.log('Server response:', rcptResponse); - - // Server should either accept (250) or reject with proper error - const accepted = rcptResponse.includes('250'); - const properlyRejected = rcptResponse.includes('501') || rcptResponse.includes('555'); - - expect(accepted || properlyRejected).toEqual(true); - console.log(`DSN parameters in RCPT TO ${accepted ? 'accepted' : 'rejected'}`); - - if (accepted) { - // Reset to test other notify values - socket.write('RSET\r\n'); - const resetResponse = await waitForResponse(socket, '250'); - console.log('Server response:', resetResponse); - - // Send MAIL FROM again - socket.write('MAIL FROM:\r\n'); - const mail2Response = await waitForResponse(socket, '250'); - console.log('Server response:', mail2Response); - - // Test NOTIFY=NEVER - socket.write('RCPT TO: NOTIFY=NEVER\r\n'); - const rcptNeverResponse = await waitForResponse(socket); - console.log('Server response:', rcptNeverResponse); - - const neverAccepted = rcptNeverResponse.includes('250'); - console.log(`NOTIFY=NEVER parameter ${neverAccepted ? 'accepted' : 'rejected'}`); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - socket.end(); - done.resolve(); - } catch (error) { - console.error('Socket error:', error); - done.reject(error); - } -}); - -tap.test('RFC 3461 DSN - Complete DSN-enabled email', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Try with DSN parameters - socket.write('MAIL FROM: RET=FULL ENVID=test123\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.includes('250')) { - // DSN parameters accepted, continue with DSN RCPT - socket.write('RCPT TO: NOTIFY=SUCCESS,FAILURE,DELAY\r\n'); - const rcptResponse = await waitForResponse(socket); - - if (!rcptResponse.includes('250')) { - // Fallback to plain RCPT if DSN parameters not supported - console.log('DSN RCPT parameters not supported, using plain RCPT TO'); - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - } - } else if (mailResponse.includes('501') || mailResponse.includes('555')) { - // DSN not supported, use plain MAIL FROM - console.log('DSN parameters not supported, using plain MAIL FROM'); - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - } - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Send email content - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: RFC 3461 DSN Compliance Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email tests RFC 3461 DSN (Delivery Status Notification) compliance.', - 'The server should handle DSN parameters according to RFC 3461.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - await waitForResponse(socket, '250'); - - console.log('DSN-enabled email accepted'); - - // Quit - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('RFC 3461 DSN - Invalid DSN parameter handling', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - socket.on('connect', async () => { - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Test with invalid RET value - socket.write('MAIL FROM: RET=INVALID\r\n'); - const mailResponse = await waitForResponse(socket); - - // Should reject with 501 or similar - const properlyRejected = mailResponse.includes('501') || - mailResponse.includes('555') || - mailResponse.includes('500'); - - if (properlyRejected) { - console.log('Invalid RET parameter properly rejected'); - expect(true).toEqual(true); - } else if (mailResponse.includes('250')) { - // Server ignores unknown parameters (also acceptable) - console.log('Server ignores invalid DSN parameters'); - } - - // Reset and test invalid NOTIFY - socket.write('RSET\r\n'); - await waitForResponse(socket, '250'); - - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Test with invalid NOTIFY value - socket.write('RCPT TO: NOTIFY=INVALID\r\n'); - const rcptResponse = await waitForResponse(socket); - - const rcptRejected = rcptResponse.includes('501') || - rcptResponse.includes('555') || - rcptResponse.includes('500'); - - if (rcptRejected) { - console.log('Invalid NOTIFY parameter properly rejected'); - } else if (rcptResponse.includes('250')) { - console.log('Server ignores invalid NOTIFY parameter'); - } - - // Quit - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - - socket.end(); - done.resolve(); - } catch (err) { - console.error('Test error:', err); - socket.end(); - done.reject(err); - } - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-01.authentication.ts b/test/suite/smtpserver_security/test.sec-01.authentication.ts deleted file mode 100644 index 1a4f5d4..0000000 --- a/test/suite/smtpserver_security/test.sec-01.authentication.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; -import { connectToSmtp, waitForGreeting, sendSmtpCommand, closeSmtpConnection } from '../../helpers/utils.js'; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server with authentication', async () => { - testServer = await startTestServer({ - port: 2530, - hostname: 'localhost', - authRequired: true - }); - expect(testServer).toBeInstanceOf(Object); -}); - -tap.test('SEC-01: Authentication - server advertises AUTH capability', async () => { - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - - // Send EHLO to get capabilities - const ehloResponse = await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Parse capabilities - const lines = ehloResponse.split('\r\n').filter(line => line.length > 0); - const capabilities = lines.map(line => line.substring(4).trim()); - - // Check for AUTH capability - const authCapability = capabilities.find(cap => cap.startsWith('AUTH')); - expect(authCapability).toBeDefined(); - - // Extract supported mechanisms - const supportedMechanisms = authCapability?.substring(5).split(' ') || []; - console.log('📋 Supported AUTH mechanisms:', supportedMechanisms); - - // Common mechanisms should be supported - expect(supportedMechanisms).toContain('PLAIN'); - expect(supportedMechanisms).toContain('LOGIN'); - - console.log('✅ AUTH capability test passed'); - - } finally { - await closeSmtpConnection(socket); - } -}); - -tap.test('SEC-01: AUTH PLAIN mechanism - correct credentials', async () => { - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Create AUTH PLAIN credentials - // Format: base64(NULL + username + NULL + password) - const username = 'testuser'; - const password = 'testpass'; - const authString = Buffer.from(`\0${username}\0${password}`).toString('base64'); - - // Send AUTH PLAIN command - try { - const authResponse = await sendSmtpCommand(socket, `AUTH PLAIN ${authString}`); - // Server might accept (235) or reject (535) based on configuration - expect(authResponse).toMatch(/^(235|535)/); - - if (authResponse.startsWith('235')) { - console.log('✅ AUTH PLAIN accepted (test mode)'); - } else { - console.log('✅ AUTH PLAIN properly rejected (production mode)'); - } - } catch (error) { - // Auth failure is expected in test environment - console.log('✅ AUTH PLAIN handled:', error.message); - } - - } finally { - await closeSmtpConnection(socket); - } -}); - -tap.test('SEC-01: AUTH LOGIN mechanism - interactive authentication', async () => { - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Start AUTH LOGIN - try { - const authStartResponse = await sendSmtpCommand(socket, 'AUTH LOGIN', '334'); - expect(authStartResponse).toInclude('334'); - - // Server should prompt for username (base64 "Username:") - const usernamePrompt = Buffer.from( - authStartResponse.substring(4).trim(), - 'base64' - ).toString(); - console.log('Server prompt:', usernamePrompt); - - // Send username - const username = Buffer.from('testuser').toString('base64'); - const passwordPromptResponse = await sendSmtpCommand(socket, username, '334'); - - // Send password - const password = Buffer.from('testpass').toString('base64'); - const authResult = await sendSmtpCommand(socket, password); - - // Check result (235 = success, 535 = failure) - expect(authResult).toMatch(/^(235|535)/); - - } catch (error) { - // Auth failure is expected in test environment - console.log('✅ AUTH LOGIN handled:', error.message); - } - - } finally { - await closeSmtpConnection(socket); - } -}); - -tap.test('SEC-01: Authentication required - reject commands without auth', async () => { - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Try to send email without authentication - try { - const mailResponse = await sendSmtpCommand(socket, 'MAIL FROM:'); - - // Server should reject with 530 (authentication required) or similar - if (mailResponse.startsWith('530') || mailResponse.startsWith('503')) { - console.log('✅ Server properly requires authentication'); - } else if (mailResponse.startsWith('250')) { - console.log('⚠️ Server accepted mail without auth (test mode)'); - } - - } catch (error) { - // Command rejection is expected - console.log('✅ Server rejected unauthenticated command:', error.message); - } - - } finally { - await closeSmtpConnection(socket); - } -}); - -tap.test('SEC-01: Invalid authentication attempts - rate limiting', async () => { - const socket = await connectToSmtp(testServer.hostname, testServer.port); - - try { - await waitForGreeting(socket); - await sendSmtpCommand(socket, 'EHLO test.example.com', '250'); - - // Try multiple failed authentication attempts - const maxAttempts = 5; - let failedAttempts = 0; - let requiresTLS = false; - - for (let i = 0; i < maxAttempts; i++) { - try { - // Send invalid credentials - const invalidAuth = Buffer.from('\0invalid\0wrong').toString('base64'); - const response = await sendSmtpCommand(socket, `AUTH PLAIN ${invalidAuth}`); - - // Check if authentication failed - if (response.startsWith('535')) { - failedAttempts++; - console.log(`Failed attempt ${i + 1}: ${response.trim()}`); - - // Check if server requires TLS (common security practice) - if (response.includes('TLS')) { - requiresTLS = true; - console.log('✅ Server enforces TLS requirement for authentication'); - break; - } - } else if (response.startsWith('503')) { - // Too many failed attempts - failedAttempts++; - console.log('✅ Server enforces auth attempt limits'); - break; - } - } catch (error) { - // Handle connection errors - failedAttempts++; - console.log(`Failed attempt ${i + 1}: ${error.message}`); - - // Check if server closed connection or rate limited - if (error.message.includes('closed') || error.message.includes('timeout')) { - console.log('✅ Server enforces auth attempt limits by closing connection'); - break; - } - } - } - - // Either TLS is required or we had failed attempts - expect(failedAttempts).toBeGreaterThan(0); - if (requiresTLS) { - console.log('✅ Authentication properly protected by TLS requirement'); - } else { - console.log(`✅ Handled ${failedAttempts} failed auth attempts`); - } - - } finally { - if (!socket.destroyed) { - socket.destroy(); - } - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - console.log('✅ Test server stopped'); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-02.authorization.ts b/test/suite/smtpserver_security/test.sec-02.authorization.ts deleted file mode 100644 index a09765a..0000000 --- a/test/suite/smtpserver_security/test.sec-02.authorization.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('Authorization - Valid sender domain', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO test.example.com\r\n'); - await waitForResponse(socket, '250'); - - // Use valid sender domain with proper format - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.startsWith('250')) { - // Try recipient - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - // Valid sender should be accepted or require auth - const accepted = rcptResponse.startsWith('250'); - const authRequired = rcptResponse.startsWith('530'); - console.log(`Valid sender domain: ${accepted ? 'accepted' : authRequired ? 'auth required' : 'rejected'}`); - - expect(accepted || authRequired).toEqual(true); - } else { - // Mail from rejected - could be due to auth requirement - const authRequired = mailResponse.startsWith('530'); - console.log(`MAIL FROM requires auth: ${authRequired}`); - expect(authRequired || mailResponse.startsWith('250')).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Authorization - External sender domain', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO external.com\r\n'); - await waitForResponse(socket, '250'); - - // Use external sender domain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.startsWith('250')) { - // Try recipient - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - // Check response - const accepted = rcptResponse.startsWith('250'); - const authRequired = rcptResponse.startsWith('530'); - const rejected = rcptResponse.startsWith('550') || rcptResponse.startsWith('553'); - - console.log(`External sender: accepted=${accepted}, authRequired=${authRequired}, rejected=${rejected}`); - expect(accepted || authRequired || rejected).toEqual(true); - } else { - // Check if auth required or rejected - const authRequired = mailResponse.startsWith('530'); - const rejected = mailResponse.startsWith('550') || mailResponse.startsWith('553'); - - console.log(`External sender ${authRequired ? 'requires authentication' : rejected ? 'rejected by policy' : 'error'}`); - expect(authRequired || rejected).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Authorization - Relay attempt rejection', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO external.com\r\n'); - await waitForResponse(socket, '250'); - - // External sender - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.startsWith('250')) { - // Try to relay to another external domain (should be rejected) - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - // Relay attempt should be rejected or accepted (test mode) - const rejected = rcptResponse.startsWith('550') || - rcptResponse.startsWith('553') || - rcptResponse.startsWith('530') || - rcptResponse.startsWith('554'); - const accepted = rcptResponse.startsWith('250'); - - console.log(`Relay attempt ${rejected ? 'properly rejected' : accepted ? 'accepted (test mode)' : 'error'}`); - // In production, relay should be rejected. In test mode, it might be accepted - expect(rejected || accepted).toEqual(true); - - if (accepted) { - console.log('⚠️ WARNING: Server accepted relay attempt - ensure relay restrictions are properly configured in production'); - } - } else { - // MAIL FROM already rejected - console.log('External sender rejected at MAIL FROM'); - expect(mailResponse.startsWith('530') || mailResponse.startsWith('550')).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Authorization - IP-based restrictions', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Use IP address in EHLO - socket.write('EHLO [127.0.0.1]\r\n'); - await waitForResponse(socket, '250'); - - // Use proper email format - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.startsWith('250')) { - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - // Localhost IP should typically be accepted - const accepted = rcptResponse.startsWith('250'); - const rejected = rcptResponse.startsWith('550') || rcptResponse.startsWith('553'); - const authRequired = rcptResponse.startsWith('530'); - - console.log(`IP-based authorization: ${accepted ? 'accepted' : rejected ? 'rejected' : 'auth required'}`); - expect(accepted || rejected || authRequired).toEqual(true); // Any is valid based on server config - } else { - // Check if auth required - const authRequired = mailResponse.startsWith('530'); - console.log(`MAIL FROM ${authRequired ? 'requires auth' : 'rejected'}`); - expect(authRequired || mailResponse.startsWith('250')).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Authorization - Case sensitivity in addresses', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO test.example.com\r\n'); - await waitForResponse(socket, '250'); - - // Use mixed case in email address with proper domain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.startsWith('250')) { - // Mixed case recipient - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - // Email addresses should be case-insensitive - const accepted = rcptResponse.startsWith('250'); - const authRequired = rcptResponse.startsWith('530'); - console.log(`Mixed case addresses ${accepted ? 'accepted' : authRequired ? 'auth required' : 'rejected'}`); - - expect(accepted || authRequired).toEqual(true); - } else { - // Check if auth required - const authRequired = mailResponse.startsWith('530'); - console.log(`MAIL FROM ${authRequired ? 'requires auth' : 'rejected'}`); - expect(authRequired || mailResponse.startsWith('250')).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-03.dkim-processing.ts b/test/suite/smtpserver_security/test.sec-03.dkim-processing.ts deleted file mode 100644 index 29ed4c2..0000000 --- a/test/suite/smtpserver_security/test.sec-03.dkim-processing.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('DKIM Processing - Valid DKIM signature', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Generate valid DKIM signature - const timestamp = Math.floor(Date.now() / 1000); - const dkimSignature = [ - 'v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=default;', - ' t=' + timestamp + ';', - ' h=from:to:subject:date:message-id;', - ' bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=;', - ' b=AMGNaJ3BliF0KSLD0wTfJd1eJhYbhP8YD2z9BPwAoeh6nKzfQ8wktB9Iwml3GKKj', - ' V6zJSGxJClQAoqJnO7oiIzPvHZTMGTbMvV9YBQcw5uvxLa2mRNkRT3FQ5vKFzfVQ', - ' OlHnZ8qZJDxYO4JmReCBnHQcC8W9cNJJh9ZQ4A=' - ].join(''); - - const email = [ - `DKIM-Signature: ${dkimSignature}`, - `Subject: DKIM Test - Valid Signature`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This is a DKIM test email with a valid signature.', - `Timestamp: ${Date.now()}`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - const emailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', emailResponse); - console.log('Email with valid DKIM signature accepted'); - expect(emailResponse).toInclude('250'); - expect(emailResponse.startsWith('250')).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DKIM Processing - Invalid DKIM signature', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Generate invalid DKIM signature (wrong domain, bad signature) - const timestamp = Math.floor(Date.now() / 1000); - const dkimSignature = [ - 'v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=wrong-domain.com; s=invalid;', - ' t=' + timestamp + ';', - ' h=from:to:subject:date;', - ' bh=INVALID-BODY-HASH;', - ' b=INVALID-SIGNATURE-DATA' - ].join(''); - - const email = [ - `DKIM-Signature: ${dkimSignature}`, - `Subject: DKIM Test - Invalid Signature`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This is a DKIM test email with an invalid signature.', - `Timestamp: ${Date.now()}`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - const emailResponse = await waitForResponse(socket); - console.log('Server response:', emailResponse); - - const accepted = emailResponse.includes('250'); - console.log(`Email with invalid DKIM signature ${accepted ? 'accepted' : 'rejected'}`); - // Either response is valid - server may accept and mark as failed, or reject - expect(emailResponse.match(/250|550/)).toBeTruthy(); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DKIM Processing - Missing DKIM signature', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Email without DKIM signature - const email = [ - `Subject: DKIM Test - No Signature`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This is a DKIM test email without any signature.', - `Timestamp: ${Date.now()}`, - '.', - '' - ].join('\r\n'); - - socket.write(email); - const emailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', emailResponse); - console.log('Email without DKIM signature accepted (neutral)'); - expect(emailResponse).toInclude('250'); - expect(emailResponse.startsWith('250')).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DKIM Processing - Multiple DKIM signatures', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Email with multiple DKIM signatures (common in forwarding) - const timestamp = Math.floor(Date.now() / 1000); - - const email = [ - 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=selector1;', - ' t=' + timestamp + ';', - ' h=from:to:subject;', - ' bh=first-body-hash;', - ' b=first-signature', - 'DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple;', - ' d=forwarder.com; s=selector2;', - ' t=' + (timestamp + 60) + ';', - ' h=from:to:subject:date:message-id;', - ' bh=second-body-hash;', - ' b=second-signature', - `Subject: DKIM Test - Multiple Signatures`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email has multiple DKIM signatures.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const emailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', emailResponse); - console.log('Email with multiple DKIM signatures accepted'); - expect(emailResponse).toInclude('250'); - expect(emailResponse.startsWith('250')).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DKIM Processing - Expired DKIM signature', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // DKIM signature with expired timestamp - const expiredTimestamp = Math.floor(Date.now() / 1000) - 2592000; // 30 days ago - const expirationTime = expiredTimestamp + 86400; // Expired 29 days ago - - const dkimSignature = [ - 'v=1; a=rsa-sha256; c=relaxed/relaxed;', - ' d=example.com; s=default;', - ' t=' + expiredTimestamp + '; x=' + expirationTime + ';', - ' h=from:to:subject:date;', - ' bh=expired-body-hash;', - ' b=expired-signature' - ].join(''); - - const email = [ - `DKIM-Signature: ${dkimSignature}`, - `Subject: DKIM Test - Expired Signature`, - `From: sender@example.com`, - `To: recipient@example.com`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email has an expired DKIM signature.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const emailResponse = await waitForResponse(socket); - console.log('Server response:', emailResponse); - - const accepted = emailResponse.includes('250'); - console.log(`Email with expired DKIM signature ${accepted ? 'accepted' : 'rejected'}`); - // Either response is valid - expect(emailResponse.match(/250|550/)).toBeTruthy(); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-04.spf-checking.ts b/test/suite/smtpserver_security/test.sec-04.spf-checking.ts deleted file mode 100644 index d8f8b94..0000000 --- a/test/suite/smtpserver_security/test.sec-04.spf-checking.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('SPF Checking - Authorized IP from local domain', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO localhost\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with example.com domain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - if (mailResponse.includes('250')) { - console.log('Local domain sender accepted (SPF pass or neutral)'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - console.log('Server response:', rcptResponse); - - if (rcptResponse.includes('250')) { - console.log('Email accepted - SPF likely passed or neutral'); - expect(true).toEqual(true); - } - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - console.log('Local domain sender rejected (SPF fail)'); - expect(true).toEqual(true); // Either result shows SPF processing - } - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('SPF Checking - External domain sender', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with well-known external domain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - if (mailResponse.includes('250')) { - console.log('External domain sender accepted'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - console.log('Server response:', rcptResponse); - - const accepted = rcptResponse.includes('250'); - const rejected = rcptResponse.includes('550') || rcptResponse.includes('553'); - - console.log(`External domain: accepted=${accepted}, rejected=${rejected}`); - expect(accepted || rejected).toEqual(true); - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - console.log('External domain sender rejected (SPF fail)'); - expect(true).toEqual(true); // Shows SPF is working - } - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('SPF Checking - Known SPF fail domain', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with domain that should fail SPF - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - if (mailResponse.includes('250')) { - console.log('SPF fail domain accepted (server may not enforce SPF)'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - console.log('Server response:', rcptResponse); - - // Either accepted or rejected is valid - const response = rcptResponse.includes('250') || rcptResponse.includes('550') || rcptResponse.includes('553'); - expect(response).toEqual(true); - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - console.log('SPF fail domain properly rejected'); - expect(true).toEqual(true); - } - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('SPF Checking - IPv4 literal in HELO', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO with IP literal - socket.write('EHLO [127.0.0.1]\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with IP literal - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - // Server should handle IP literals appropriately - const accepted = mailResponse.includes('250'); - const rejected = mailResponse.includes('550') || mailResponse.includes('553'); - - console.log(`IP literal sender: accepted=${accepted}, rejected=${rejected}`); - expect(accepted || rejected).toEqual(true); - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('SPF Checking - Subdomain sender', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO subdomain.example.com\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with subdomain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - if (mailResponse.includes('250')) { - console.log('Subdomain sender accepted'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - console.log('Server response:', rcptResponse); - - const accepted = rcptResponse.includes('250'); - console.log(`Subdomain SPF test: ${accepted ? 'passed' : 'failed'}`); - expect(true).toEqual(true); - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - console.log('Subdomain sender rejected'); - expect(true).toEqual(true); - } - - // Send QUIT - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-05.dmarc-policy.ts b/test/suite/smtpserver_security/test.sec-05.dmarc-policy.ts deleted file mode 100644 index dcb1a02..0000000 --- a/test/suite/smtpserver_security/test.sec-05.dmarc-policy.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('DMARC Policy - Reject policy enforcement', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Check if server advertises DMARC support - const advertisesDmarc = ehloResponse.toLowerCase().includes('dmarc'); - console.log('DMARC advertised:', advertisesDmarc); - - // Send MAIL FROM with domain that has reject policy - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - if (mailResponse.includes('550') || mailResponse.includes('553')) { - // DMARC reject policy enforced at MAIL FROM - console.log('DMARC reject policy enforced at MAIL FROM'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } else if (mailResponse.includes('250')) { - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Send email with DMARC-relevant headers - const email = [ - `From: test@dmarc-reject.example.com`, - `To: recipient@example.com`, - `Subject: DMARC Policy Test - Reject`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=dmarc-reject.example.com; s=default;`, - ` h=from:to:subject:date; bh=test; b=test`, - '', - 'Testing DMARC reject policy enforcement.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const finalResponse = await waitForResponse(socket); - console.log('Server response:', finalResponse); - - const accepted = finalResponse.includes('250'); - const rejected = finalResponse.includes('550'); - - console.log(`DMARC reject policy: accepted=${accepted}, rejected=${rejected}`); - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } - } finally { - socket.destroy(); - } -}); - -tap.test('DMARC Policy - Quarantine policy', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with domain that has quarantine policy - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Send email with DMARC-relevant headers - const email = [ - `From: test@dmarc-quarantine.example.com`, - `To: recipient@example.com`, - `Subject: DMARC Policy Test - Quarantine`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Testing DMARC quarantine policy.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const finalResponse = await waitForResponse(socket); - console.log('Server response:', finalResponse); - - const accepted = finalResponse.includes('250'); - console.log(`DMARC quarantine policy: ${accepted ? 'accepted (may be quarantined)' : 'rejected'}`); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DMARC Policy - None policy', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with domain that has none policy (monitoring only) - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Send email with DMARC-relevant headers - const email = [ - `From: test@dmarc-none.example.com`, - `To: recipient@example.com`, - `Subject: DMARC Policy Test - None`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Testing DMARC none policy (monitoring only).', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const finalResponse = await waitForResponse(socket, '250'); - console.log('Server response:', finalResponse); - - console.log('DMARC none policy: email accepted (monitoring only)'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DMARC Policy - Alignment testing', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with envelope domain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Send email with different header From (tests alignment) - const email = [ - `From: test@header-domain.com`, - `To: recipient@example.com`, - `Subject: DMARC Alignment Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=header-domain.com; s=default;`, - ` h=from:to:subject:date; bh=test; b=test`, - '', - 'Testing DMARC domain alignment (envelope vs header From).', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const finalResponse = await waitForResponse(socket); - console.log('Server response:', finalResponse); - - const result = finalResponse.includes('250') ? 'accepted' : 'rejected'; - console.log(`DMARC alignment test: ${result}`); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('DMARC Policy - Percentage testing', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM with domain that has percentage-based DMARC policy - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Send email with DMARC-relevant headers - const email = [ - `From: test@dmarc-pct.example.com`, - `To: recipient@example.com`, - `Subject: DMARC Percentage Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Testing DMARC with percentage-based policy application.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const finalResponse = await waitForResponse(socket); - console.log('Server response:', finalResponse); - - const result = finalResponse.includes('250') ? 'accepted' : 'rejected'; - console.log(`DMARC percentage policy: ${result} (may vary based on percentage)`); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-06.ip-reputation.ts b/test/suite/smtpserver_security/test.sec-06.ip-reputation.ts deleted file mode 100644 index ff61837..0000000 --- a/test/suite/smtpserver_security/test.sec-06.ip-reputation.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('IP Reputation - Suspicious hostname in EHLO', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Use suspicious hostname - socket.write('EHLO suspicious-host.badreputation.com\r\n'); - const ehloResponse = await waitForResponse(socket); - console.log('Server response:', ehloResponse); - - const accepted = ehloResponse.includes('250'); - const rejected = ehloResponse.includes('550') || ehloResponse.includes('521'); - - console.log(`Suspicious hostname: accepted=${accepted}, rejected=${rejected}`); - expect(accepted || rejected).toEqual(true); - - if (rejected) { - console.log('IP reputation check working - suspicious host rejected at EHLO'); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('IP Reputation - Blacklisted sender domain', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Use known spam/blacklisted domain - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket); - console.log('Server response:', mailResponse); - - if (mailResponse.includes('250')) { - console.log('Blacklisted sender accepted at MAIL FROM'); - - // Try RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - console.log('Server response:', rcptResponse); - - const accepted = rcptResponse.includes('250'); - const rejected = rcptResponse.includes('550') || rcptResponse.includes('553'); - - console.log(`Blacklisted domain at RCPT: accepted=${accepted}, rejected=${rejected}`); - expect(accepted || rejected).toEqual(true); - } else if (mailResponse.includes('550') || mailResponse.includes('553')) { - console.log('Blacklisted sender rejected - IP reputation check working'); - expect(true).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('IP Reputation - Known good sender', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO localhost\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Use legitimate sender - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket, '250'); - console.log('Server response:', rcptResponse); - - console.log('Good sender accepted - IP reputation allows legitimate senders'); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('IP Reputation - Multiple connections from same IP', async (tools) => { - const connections: net.Socket[] = []; - const totalConnections = 3; - const connectionResults: Promise[] = []; - - // Create multiple connections rapidly - for (let i = 0; i < totalConnections; i++) { - const connectionPromise = (async () => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - connections.push(socket); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log(`Connection ${i + 1} response:`, greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket); - console.log(`Connection ${i + 1} response:`, ehloResponse); - - if (ehloResponse.includes('250')) { - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } else if (ehloResponse.includes('421') || ehloResponse.includes('550')) { - // Connection rejected due to rate limiting or reputation - console.log(`Connection ${i + 1} rejected - IP reputation/rate limiting active`); - } - } catch (err: any) { - console.error(`Connection ${i + 1} error:`, err.message); - } finally { - socket.destroy(); - } - })(); - - connectionResults.push(connectionPromise); - - // Small delay between connections - if (i < totalConnections - 1) { - await tools.delayFor(100); - } - } - - // Wait for all connections to complete - await Promise.all(connectionResults); - console.log('All connections completed'); - expect(true).toEqual(true); -}); - -tap.test('IP Reputation - Suspicious patterns in email', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - const greeting = await waitForResponse(socket, '220'); - console.log('Server response:', greeting); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - const ehloResponse = await waitForResponse(socket, '250'); - console.log('Server response:', ehloResponse); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - const mailResponse = await waitForResponse(socket, '250'); - console.log('Server response:', mailResponse); - - // Multiple recipients (spam pattern) - socket.write('RCPT TO:\r\n'); - const rcpt1Response = await waitForResponse(socket, '250'); - console.log('Server response:', rcpt1Response); - - socket.write('RCPT TO:\r\n'); - const rcpt2Response = await waitForResponse(socket, '250'); - console.log('Server response:', rcpt2Response); - - socket.write('RCPT TO:\r\n'); - const rcpt3Response = await waitForResponse(socket); - console.log('Server response:', rcpt3Response); - - if (rcpt3Response.includes('250')) { - // Send DATA - socket.write('DATA\r\n'); - const dataResponse = await waitForResponse(socket, '354'); - console.log('Server response:', dataResponse); - - // Email with spam-like content - const email = [ - `From: sender@example.com`, - `To: recipient1@example.com, recipient2@example.com, recipient3@example.com`, - `Subject: URGENT!!! You've won $1,000,000!!!`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'CLICK HERE NOW!!! Limited time offer!!!', - 'Visit http://suspicious-link.com/win-money', - 'Act NOW before it\'s too late!!!', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const emailResponse = await waitForResponse(socket); - console.log('Server response:', emailResponse); - - const result = emailResponse.includes('250') ? 'accepted' : 'rejected'; - console.log(`Suspicious content email ${result}`); - expect(true).toEqual(true); - } else if (rcpt3Response.includes('452') || rcpt3Response.includes('550')) { - console.log('Multiple recipients limited - reputation control active'); - expect(true).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221'); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-07.content-scanning.ts b/test/suite/smtpserver_security/test.sec-07.content-scanning.ts deleted file mode 100644 index 9e36806..0000000 --- a/test/suite/smtpserver_security/test.sec-07.content-scanning.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('Content Scanning - Suspicious content patterns', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with suspicious content - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Content Scanning Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email contains suspicious content that should trigger content scanning:', - 'VIRUS_TEST_STRING', - 'SUSPICIOUS_ATTACHMENT_PATTERN', - 'MALWARE_SIGNATURE_TEST', - 'Click here for FREE MONEY!!!', - 'Visit http://phishing-site.com/steal-data', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket); - - const accepted = dataResponse.startsWith('250'); - const rejected = dataResponse.startsWith('550'); - - console.log(`Suspicious content: accepted=${accepted}, rejected=${rejected}`); - - if (rejected) { - console.log('Content scanning active - suspicious content detected'); - } else { - console.log('Content scanning operational - email processed'); - } - - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Content Scanning - Malware patterns', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with malware-like patterns - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Important Security Update`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - 'Content-Type: multipart/mixed; boundary="malware-boundary"', - '', - '--malware-boundary', - 'Content-Type: text/plain', - '', - 'Please run the attached file to update your security software.', - '', - '--malware-boundary', - 'Content-Type: application/x-msdownload; name="update.exe"', - 'Content-Transfer-Encoding: base64', - 'Content-Disposition: attachment; filename="update.exe"', - '', - 'TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - 'AAAA4AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1v', - '', - '--malware-boundary--', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket); - - const accepted = dataResponse.startsWith('250'); - const rejected = dataResponse.startsWith('550'); - - console.log(`Malware pattern email: ${accepted ? 'accepted' : 'rejected'}`); - - if (rejected) { - console.log('Content scanning active - malware patterns detected'); - } else { - console.log('Content scanning operational - email processed'); - } - - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Content Scanning - Spam keywords', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with spam keywords - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: URGENT!!! Act NOW!!! Limited Time OFFER!!!`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'CONGRATULATIONS!!! You have WON!!!', - 'FREE FREE FREE!!!', - 'VIAGRA CIALIS CHEAP MEDS!!!', - 'MAKE $$$ FAST!!!', - 'WORK FROM HOME!!!', - 'NO CREDIT CHECK!!!', - 'GUARANTEED WINNER!!!', - 'CLICK HERE NOW!!!', - 'This is NOT SPAM!!!', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket); - - const accepted = dataResponse.startsWith('250'); - const rejected = dataResponse.startsWith('550'); - - console.log(`Spam keyword email: ${accepted ? 'accepted' : 'rejected (spam detected)'}`); - - if (rejected) { - console.log('Content scanning active - spam keywords detected'); - } else { - console.log('Content scanning operational - email processed'); - } - - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Content Scanning - Clean legitimate email', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Clean legitimate email - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Meeting Tomorrow`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'Hi,', - '', - 'Just wanted to confirm our meeting for tomorrow at 2 PM.', - 'Please let me know if you need to reschedule.', - '', - 'Best regards,', - 'John', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket, '250'); - - console.log('Clean email accepted - content scanning allows legitimate emails'); - expect(dataResponse.startsWith('250')).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Content Scanning - Large attachment', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with large attachment pattern - const largeData = 'A'.repeat(10000); // 10KB of data - - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Large Attachment Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - 'Content-Type: multipart/mixed; boundary="boundary123"', - '', - '--boundary123', - 'Content-Type: text/plain', - '', - 'Please find the attached file.', - '', - '--boundary123', - 'Content-Type: application/octet-stream; name="largefile.dat"', - 'Content-Transfer-Encoding: base64', - 'Content-Disposition: attachment; filename="largefile.dat"', - '', - Buffer.from(largeData).toString('base64'), - '', - '--boundary123--', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket); - - const accepted = dataResponse.startsWith('250'); - const rejected = dataResponse.startsWith('550') || dataResponse.startsWith('552'); - - console.log(`Large attachment: ${accepted ? 'accepted' : 'rejected (size or content issue)'}`); - - if (rejected) { - console.log('Content scanning active - large attachment blocked'); - } else { - console.log('Content scanning operational - email processed'); - } - - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-08.rate-limiting.ts b/test/suite/smtpserver_security/test.sec-08.rate-limiting.ts deleted file mode 100644 index a736eaa..0000000 --- a/test/suite/smtpserver_security/test.sec-08.rate-limiting.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 30025; -const TEST_TIMEOUT = 30000; - -let testServer: ITestServer; - -tap.test('setup - start SMTP server for rate limiting tests', async () => { - testServer = await startTestServer({ - port: TEST_PORT, - hostname: 'localhost' - }); - expect(testServer).toBeInstanceOf(Object); -}); - -tap.test('Rate Limiting - should limit rapid consecutive connections', async (tools) => { - const done = tools.defer(); - - try { - const connections: net.Socket[] = []; - let rateLimitTriggered = false; - let successfulConnections = 0; - const maxAttempts = 10; - - for (let i = 0; i < maxAttempts; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - connections.push(socket); - - // Try EHLO - socket.write('EHLO testhost\r\n'); - - const response = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^[0-9]{3} /m) || data.match(/^[0-9]{3}-.*\r\n[0-9]{3} /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - if (response.includes('421') || response.toLowerCase().includes('rate') || response.toLowerCase().includes('limit')) { - rateLimitTriggered = true; - console.log(`Rate limit triggered at connection ${i + 1}`); - break; - } - - if (response.includes('250')) { - successfulConnections++; - } - - // Small delay between connections - await new Promise(resolve => setTimeout(resolve, 100)); - - } catch (error) { - const errorMsg = error instanceof Error ? error.message.toLowerCase() : ''; - if (errorMsg.includes('rate') || errorMsg.includes('limit') || errorMsg.includes('too many')) { - rateLimitTriggered = true; - console.log(`Rate limit error at connection ${i + 1}: ${errorMsg}`); - break; - } - // Connection refused might also indicate rate limiting - if (errorMsg.includes('econnrefused')) { - rateLimitTriggered = true; - console.log(`Connection refused at attempt ${i + 1} - possible rate limiting`); - break; - } - } - } - - // Clean up connections - for (const socket of connections) { - try { - if (!socket.destroyed) { - socket.write('QUIT\r\n'); - socket.end(); - } - } catch (e) { - // Ignore cleanup errors - } - } - - // Rate limiting is working if either: - // 1. We got explicit rate limit responses - // 2. We couldn't make all connections (some were refused/limited) - const rateLimitWorking = rateLimitTriggered || successfulConnections < maxAttempts; - - console.log(`Rate limiting test results: - - Successful connections: ${successfulConnections}/${maxAttempts} - - Rate limit triggered: ${rateLimitTriggered} - - Rate limiting effective: ${rateLimitWorking}`); - - // Note: We consider the test passed if rate limiting is either working OR not configured - // Many SMTP servers don't have rate limiting, which is also valid - expect(true).toEqual(true); - - } finally { - done.resolve(); - } -}); - -tap.test('Rate Limiting - should allow connections after rate limit period', async (tools) => { - const done = tools.defer(); - - try { - // First, try to trigger rate limiting - const connections: net.Socket[] = []; - let rateLimitTriggered = false; - - // Make rapid connections - for (let i = 0; i < 5; i++) { - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - connections.push(socket); - - socket.write('EHLO testhost\r\n'); - - const response = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^[0-9]{3} /m) || data.match(/^[0-9]{3}-.*\r\n[0-9]{3} /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - if (response.includes('421') || response.toLowerCase().includes('rate')) { - rateLimitTriggered = true; - break; - } - } catch (error) { - // Rate limit might cause connection errors - rateLimitTriggered = true; - break; - } - } - - // Clean up initial connections - for (const socket of connections) { - try { - if (!socket.destroyed) { - socket.end(); - } - } catch (e) { - // Ignore - } - } - - if (rateLimitTriggered) { - console.log('Rate limit was triggered, waiting before retry...'); - - // Wait a bit for rate limit to potentially reset - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Try a new connection - try { - const retrySocket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - retrySocket.once('connect', () => resolve()); - retrySocket.once('error', reject); - }); - - retrySocket.write('EHLO testhost\r\n'); - - const retryResponse = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^[0-9]{3} /m) || data.match(/^[0-9]{3}-.*\r\n[0-9]{3} /ms))) { - retrySocket.removeListener('data', handler); - resolve(data); - } - }; - retrySocket.on('data', handler); - }); - - console.log('Retry connection response:', retryResponse.trim()); - - // Clean up - retrySocket.write('QUIT\r\n'); - retrySocket.end(); - - // If we got a normal response, rate limiting reset worked - expect(retryResponse).toInclude('250'); - } catch (error) { - console.log('Retry connection failed:', error); - // Some servers might have longer rate limit periods - expect(true).toEqual(true); - } - } else { - console.log('Rate limiting not triggered or not configured'); - expect(true).toEqual(true); - } - - } finally { - done.resolve(); - } -}); - -tap.test('Rate Limiting - should limit rapid MAIL FROM commands', async (tools) => { - const done = tools.defer(); - - try { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: TEST_TIMEOUT - }); - - await new Promise((resolve, reject) => { - socket.once('connect', () => resolve()); - socket.once('error', reject); - }); - - // Get banner - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - - // Send EHLO - socket.write('EHLO testhost\r\n'); - await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - let commandRateLimitTriggered = false; - let successfulCommands = 0; - - // Try rapid MAIL FROM commands - for (let i = 0; i < 10; i++) { - socket.write(`MAIL FROM:\r\n`); - - const response = await new Promise((resolve) => { - let data = ''; - const handler = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes('\r\n')) { - socket.removeListener('data', handler); - resolve(data); - } - }; - socket.on('data', handler); - }); - - if (response.includes('421') || response.toLowerCase().includes('rate') || response.toLowerCase().includes('limit')) { - commandRateLimitTriggered = true; - console.log(`Command rate limit triggered at command ${i + 1}`); - break; - } - - if (response.includes('250')) { - successfulCommands++; - // Need to reset after each MAIL FROM - socket.write('RSET\r\n'); - await new Promise((resolve) => { - socket.once('data', (chunk) => resolve(chunk.toString())); - }); - } - } - - console.log(`Command rate limiting results: - - Successful commands: ${successfulCommands}/10 - - Rate limit triggered: ${commandRateLimitTriggered}`); - - // Clean up - socket.write('QUIT\r\n'); - socket.end(); - - // Test passes regardless - rate limiting is optional - expect(true).toEqual(true); - - } finally { - done.resolve(); - } -}); - -tap.test('cleanup - stop SMTP server', async () => { - await stopTestServer(testServer); - expect(true).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-09.tls-certificate-validation.ts b/test/suite/smtpserver_security/test.sec-09.tls-certificate-validation.ts deleted file mode 100644 index 92d393c..0000000 --- a/test/suite/smtpserver_security/test.sec-09.tls-certificate-validation.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import * as tls from 'tls'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('TLS Certificate Validation - STARTTLS certificate check', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - const supportsStarttls = dataBuffer.toLowerCase().includes('starttls'); - console.log('STARTTLS supported:', supportsStarttls); - - if (supportsStarttls) { - step = 'starttls'; - socket.write('STARTTLS\r\n'); - dataBuffer = ''; - } else { - console.log('STARTTLS not supported, testing plain connection'); - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'starttls' && dataBuffer.includes('220')) { - console.log('Ready to start TLS'); - - // Upgrade to TLS - const tlsOptions = { - socket: socket, - rejectUnauthorized: false, // For self-signed certificates in testing - requestCert: true - }; - - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.on('secureConnect', () => { - console.log('TLS connection established'); - - // Get certificate information - const cert = tlsSocket.getPeerCertificate(); - console.log('Certificate present:', !!cert); - - if (cert && Object.keys(cert).length > 0) { - console.log('Certificate subject:', cert.subject); - console.log('Certificate issuer:', cert.issuer); - console.log('Certificate valid from:', cert.valid_from); - console.log('Certificate valid to:', cert.valid_to); - - // Check certificate validity - const now = new Date(); - const validFrom = new Date(cert.valid_from); - const validTo = new Date(cert.valid_to); - const isValid = now >= validFrom && now <= validTo; - - console.log('Certificate currently valid:', isValid); - expect(true).toEqual(true); // Certificate present - } - - // Test EHLO over TLS - tlsSocket.write('EHLO testclient\r\n'); - }); - - tlsSocket.on('data', (data) => { - const response = data.toString(); - console.log('TLS response:', response); - - if (response.includes('250')) { - console.log('EHLO over TLS successful'); - expect(true).toEqual(true); - - tlsSocket.write('QUIT\r\n'); - tlsSocket.end(); - done.resolve(); - } - }); - - tlsSocket.on('error', (err) => { - console.error('TLS error:', err); - done.reject(err); - }); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('TLS Certificate Validation - Direct TLS connection', async (tools) => { - const done = tools.defer(); - - // Try connecting with TLS directly (implicit TLS) - const tlsOptions = { - host: 'localhost', - port: TEST_PORT, - rejectUnauthorized: false, - timeout: 30000 - }; - - const socket = tls.connect(tlsOptions); - - socket.on('secureConnect', () => { - console.log('Direct TLS connection established'); - - const cert = socket.getPeerCertificate(); - if (cert && Object.keys(cert).length > 0) { - console.log('Certificate found on direct TLS connection'); - expect(true).toEqual(true); - } - - socket.end(); - done.resolve(); - }); - - socket.on('error', (err) => { - // Direct TLS might not be supported, try plain connection - console.log('Direct TLS not supported, this is expected for STARTTLS servers'); - expect(true).toEqual(true); - done.resolve(); - }); - - socket.on('timeout', () => { - console.log('Direct TLS connection timeout'); - socket.destroy(); - done.resolve(); - }); - - await done.promise; -}); - -tap.test('TLS Certificate Validation - Certificate verification with strict mode', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - if (dataBuffer.toLowerCase().includes('starttls')) { - step = 'starttls'; - socket.write('STARTTLS\r\n'); - dataBuffer = ''; - } else { - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'starttls' && dataBuffer.includes('220')) { - // Try with strict certificate verification - const tlsOptions = { - socket: socket, - rejectUnauthorized: true, // Strict mode - servername: 'localhost' // For SNI - }; - - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.on('secureConnect', () => { - console.log('TLS connection with strict verification successful'); - const authorized = tlsSocket.authorized; - console.log('Certificate authorized:', authorized); - - if (!authorized) { - console.log('Authorization error:', tlsSocket.authorizationError); - } - - expect(true).toEqual(true); // Connection established - tlsSocket.write('QUIT\r\n'); - tlsSocket.end(); - done.resolve(); - }); - - tlsSocket.on('error', (err) => { - console.log('Certificate verification error (expected for self-signed):', err.message); - expect(true).toEqual(true); // Error is expected for self-signed certificates - socket.end(); - done.resolve(); - }); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('TLS Certificate Validation - Cipher suite information', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - if (dataBuffer.toLowerCase().includes('starttls')) { - step = 'starttls'; - socket.write('STARTTLS\r\n'); - dataBuffer = ''; - } else { - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - } else if (step === 'starttls' && dataBuffer.includes('220')) { - const tlsOptions = { - socket: socket, - rejectUnauthorized: false - }; - - const tlsSocket = tls.connect(tlsOptions); - - tlsSocket.on('secureConnect', () => { - console.log('TLS connection established'); - - // Get cipher information - const cipher = tlsSocket.getCipher(); - if (cipher) { - console.log('Cipher name:', cipher.name); - console.log('Cipher version:', cipher.version); - console.log('Cipher standardName:', cipher.standardName); - } - - // Get protocol version - const protocol = tlsSocket.getProtocol(); - console.log('TLS Protocol:', protocol); - - // Verify modern TLS version - expect(['TLSv1.2', 'TLSv1.3']).toContain(protocol); - - tlsSocket.write('QUIT\r\n'); - tlsSocket.end(); - done.resolve(); - }); - - tlsSocket.on('error', (err) => { - console.error('TLS error:', err); - done.reject(err); - }); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-10.header-injection-prevention.ts b/test/suite/smtpserver_security/test.sec-10.header-injection-prevention.ts deleted file mode 100644 index eebcf0b..0000000 --- a/test/suite/smtpserver_security/test.sec-10.header-injection-prevention.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('Header Injection Prevention - CRLF injection in headers', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - // Attempt header injection with CRLF sequences - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: Test\r\nBcc: hidden@attacker.com`, // CRLF injection attempt - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `X-Custom: normal\r\nX-Injected: malicious`, // Another injection attempt - '', - 'This email tests header injection prevention.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') || dataBuffer.includes('550 ')) { - const accepted = dataBuffer.includes('250'); - const rejected = dataBuffer.includes('550'); - - console.log(`Header injection attempt: ${accepted ? 'accepted' : 'rejected'}`); - - if (rejected) { - console.log('Header injection prevention active - malicious headers detected'); - } - - expect(accepted || rejected).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Header Injection Prevention - Command injection in MAIL FROM', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - // Attempt command injection in MAIL FROM - socket.write('MAIL FROM: SIZE=1000\r\nRCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'mail') { - // Server should reject or handle this properly - const properResponse = dataBuffer.includes('250') || - dataBuffer.includes('501') || - dataBuffer.includes('500'); - - console.log('Command injection attempt handled'); - expect(properResponse).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Header Injection Prevention - HTML/Script injection in body', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - // Email with HTML/Script content - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: HTML Injection Test`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `Content-Type: text/html`, - '', - '', - '

Test Email

', - '', - '', - 'Injected-Header: malicious-value', // Attempted header injection in body - '', - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') || dataBuffer.includes('550 ')) { - const accepted = dataBuffer.includes('250'); - console.log(`HTML/Script content: ${accepted ? 'accepted (may be sanitized)' : 'rejected'}`); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Header Injection Prevention - Null byte injection', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - // Attempt null byte injection - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail') { - // Should be rejected or sanitized - const handled = dataBuffer.includes('250') || - dataBuffer.includes('501') || - dataBuffer.includes('550'); - - console.log('Null byte injection attempt handled'); - expect(handled).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('Header Injection Prevention - Unicode and encoding attacks', async (tools) => { - const done = tools.defer(); - - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - let dataBuffer = ''; - let step = 'greeting'; - - socket.on('data', (data) => { - dataBuffer += data.toString(); - console.log('Server response:', data.toString()); - - if (step === 'greeting' && dataBuffer.includes('220 ')) { - step = 'ehlo'; - socket.write('EHLO testclient\r\n'); - dataBuffer = ''; - } else if (step === 'ehlo' && dataBuffer.includes('250')) { - step = 'mail'; - socket.write('MAIL FROM:\r\n'); - dataBuffer = ''; - } else if (step === 'mail' && dataBuffer.includes('250')) { - step = 'rcpt'; - socket.write('RCPT TO:\r\n'); - dataBuffer = ''; - } else if (step === 'rcpt' && dataBuffer.includes('250')) { - step = 'data'; - socket.write('DATA\r\n'); - dataBuffer = ''; - } else if (step === 'data' && dataBuffer.includes('354')) { - // Unicode tricks and encoding attacks - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: =?UTF-8?B?${Buffer.from('Test\r\nBcc: hidden@attacker.com').toString('base64')}?=`, // Encoded injection - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `X-Test: \u000D\u000AX-Injected: true`, // Unicode CRLF - '', - 'Testing unicode and encoding attacks.', - '\x00\x0D\x0AExtra-Header: injected', // Null byte + CRLF - '.', - '' - ].join('\r\n'); - - socket.write(email); - dataBuffer = ''; - } else if (dataBuffer.includes('250 ') || dataBuffer.includes('550 ')) { - const result = dataBuffer.includes('250') ? 'accepted' : 'rejected'; - console.log(`Unicode/encoding attack: ${result}`); - expect(true).toEqual(true); - - socket.write('QUIT\r\n'); - socket.end(); - done.resolve(); - } - }); - - socket.on('error', (err) => { - console.error('Socket error:', err); - done.reject(err); - }); - - await done.promise; -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/suite/smtpserver_security/test.sec-11.bounce-management.ts b/test/suite/smtpserver_security/test.sec-11.bounce-management.ts deleted file mode 100644 index 813a758..0000000 --- a/test/suite/smtpserver_security/test.sec-11.bounce-management.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../../../ts/plugins.js'; -import * as net from 'net'; -import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts' -import type { ITestServer } from '../../helpers/server.loader.js'; - -const TEST_PORT = 2525; -let testServer: ITestServer; - -// Helper to wait for SMTP response -const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { - return new Promise((resolve, reject) => { - let buffer = ''; - const timer = setTimeout(() => { - socket.removeListener('data', handler); - reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); - }, timeout); - - const handler = (data: Buffer) => { - buffer += data.toString(); - const lines = buffer.split('\r\n'); - - for (const line of lines) { - if (expectedCode) { - if (line.startsWith(expectedCode + ' ')) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } else { - // Look for any complete response - if (line.match(/^\d{3} /)) { - clearTimeout(timer); - socket.removeListener('data', handler); - resolve(buffer); - return; - } - } - } - }; - - socket.on('data', handler); - }); -}; - -tap.test('setup - start test server', async (toolsArg) => { - testServer = await startTestServer({ port: TEST_PORT }); - await toolsArg.delayFor(1000); -}); - -tap.test('Bounce Management - Invalid recipient domain', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send to non-existent domain - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - if (rcptResponse.startsWith('550') || rcptResponse.startsWith('551') || rcptResponse.startsWith('553')) { - console.log('Bounce management active - invalid recipient properly rejected'); - expect(true).toEqual(true); - } else if (rcptResponse.startsWith('250')) { - // Server accepted, may generate bounce later - console.log('Invalid recipient accepted - bounce may be generated later'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - const email = [ - `From: sender@example.com`, - `To: nonexistent@invalid-domain-that-does-not-exist.com`, - `Subject: Bounce Management Test`, - `Return-Path: `, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email is designed to test bounce management functionality.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket, '250'); - - console.log('Email accepted for processing - bounce will be generated'); - expect(dataResponse.startsWith('250')).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Bounce Management - Empty return path (null sender)', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Empty return path (null sender) - used for bounce messages - socket.write('MAIL FROM:<>\r\n'); - const mailResponse = await waitForResponse(socket); - - if (mailResponse.startsWith('250')) { - console.log('Null sender accepted (for bounce messages)'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - const dataCommandResponse = await waitForResponse(socket); - - if (dataCommandResponse.startsWith('354')) { - // Bounce message format - const email = [ - `From: MAILER-DAEMON@example.com`, - `To: recipient@example.com`, - `Subject: Mail delivery failed: returning message to sender`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `Auto-Submitted: auto-replied`, - '', - 'This message was created automatically by mail delivery software.', - '', - 'A message that you sent could not be delivered to one or more recipients.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket, '250'); - - console.log('Bounce message with null sender accepted'); - expect(dataResponse.startsWith('250')).toEqual(true); - } else if (dataCommandResponse.startsWith('503')) { - // Server rejects DATA for null sender - console.log('Server rejects DATA command for null sender (strict policy)'); - expect(dataCommandResponse.startsWith('503')).toEqual(true); - } - } else { - console.log('Null sender rejected'); - expect(true).toEqual(true); - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Bounce Management - DSN headers', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - // Email with DSN request headers - const email = [ - `From: sender@example.com`, - `To: recipient@example.com`, - `Subject: DSN Test`, - `Return-Path: `, - `Disposition-Notification-To: sender@example.com`, - `Return-Receipt-To: sender@example.com`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This email requests delivery status notifications.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket, '250'); - - console.log('Email with DSN headers accepted'); - expect(dataResponse.startsWith('250')).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Bounce Management - Bounce loop prevention', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Null sender (bounce message) - socket.write('MAIL FROM:<>\r\n'); - await waitForResponse(socket, '250'); - - // To another mailer-daemon (potential loop) - socket.write('RCPT TO:\r\n'); - const rcptResponse = await waitForResponse(socket); - - if (rcptResponse.startsWith('550') || rcptResponse.startsWith('553')) { - console.log('Bounce loop prevented - mailer-daemon recipient rejected'); - expect(true).toEqual(true); - } else if (rcptResponse.startsWith('250')) { - console.log('Mailer-daemon recipient accepted - check for loop prevention'); - - // Send DATA - socket.write('DATA\r\n'); - const dataCommandResponse = await waitForResponse(socket); - - if (dataCommandResponse.startsWith('354')) { - const email = [ - `From: MAILER-DAEMON@example.com`, - `To: mailer-daemon@another-server.com`, - `Subject: Delivery Status Notification (Failure)`, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - `Auto-Submitted: auto-replied`, - `X-Loop: example.com`, - '', - 'This is a bounce of a bounce - potential loop.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket); - - const result = dataResponse.startsWith('250') ? 'accepted' : 'rejected'; - console.log(`Bounce loop test: ${result}`); - expect(true).toEqual(true); - } else if (dataCommandResponse.startsWith('503')) { - // Server rejects DATA for null sender - console.log('Bounce loop prevented at DATA stage (null sender rejection)'); - expect(dataCommandResponse.startsWith('503')).toEqual(true); - } - } - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('Bounce Management - Valid email (control test)', async (tools) => { - const socket = net.createConnection({ - host: 'localhost', - port: TEST_PORT, - timeout: 30000 - }); - - try { - // Wait for greeting - await waitForResponse(socket, '220'); - - // Send EHLO - socket.write('EHLO testclient\r\n'); - await waitForResponse(socket, '250'); - - // Send MAIL FROM - socket.write('MAIL FROM:\r\n'); - await waitForResponse(socket, '250'); - - // Send RCPT TO - socket.write('RCPT TO:\r\n'); - await waitForResponse(socket, '250'); - - // Send DATA - socket.write('DATA\r\n'); - await waitForResponse(socket, '354'); - - const email = [ - `From: sender@example.com`, - `To: valid@example.com`, - `Subject: Valid Email Test`, - `Return-Path: `, - `Date: ${new Date().toUTCString()}`, - `Message-ID: `, - '', - 'This is a valid email that should not trigger bounce.', - '.', - '' - ].join('\r\n'); - - socket.write(email); - const dataResponse = await waitForResponse(socket, '250'); - - console.log('Valid email accepted - no bounce expected'); - expect(dataResponse.startsWith('250')).toEqual(true); - - socket.write('QUIT\r\n'); - await waitForResponse(socket, '221').catch(() => {}); - } finally { - socket.destroy(); - } -}); - -tap.test('cleanup - stop test server', async () => { - await stopTestServer(testServer); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.smtp.server.ts b/test/test.smtp.server.ts deleted file mode 100644 index 75bae07..0000000 --- a/test/test.smtp.server.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.js'; -import * as paths from '../ts/paths.js'; -import { createSmtpServer } from '../ts/mail/delivery/smtpserver/index.js'; -import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js'; -import { Email } from '../ts/mail/core/classes.email.js'; -import type { ISmtpServerOptions } from '../ts/mail/delivery/interfaces.js'; - -/** - * Tests for the SMTP server class - */ -tap.test('verify SMTP server initialization', async () => { - // Mock email server - const mockEmailServer = { - processEmailByMode: async () => new Email({ - from: 'test@example.com', - to: 'recipient@example.com', - subject: 'Test Email', - text: 'This is a test email' - }) - } as any; - - // Create test configuration - const options: ISmtpServerOptions = { - port: 2525, // Use a high port for testing - hostname: 'test.example.com', - key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxzYIwlfnr7AK2v6E+c2oYD7nAIXIIvDuvVvZ8R9kyxXIzTXB\nj5D1AgntqKS3bFR1XT8hCVeXjuLKPBvXbhVjG15gXlXxpNiFi1ZcphJvs4zB/Vh7\nZv2ALt3anSIwsJ2rZA/R/GqdJPkHvYf/GMTDLw0YllR0YOevErnRIIM5S58Lj2nT\nCr5v5hK1Gl9mWwRkFQKkWVl2UXt/JX6C7Z6UyJXMZSnoG0Kw6GQje41K5r0Zdzrh\nrGfmb9wSDUn9sZGX6il+oMiYz7UgQkPEzGUZEJxKJwxy8ZgPdSgbvYq4WwPwbBUJ\nlpw0gt5i6HOS7CphRama+zAf5LvfSLoLXSP5JwIDAQABAoIBAQC8C5Ge6wS4LuH9\ntbZFPwjdGHXL+QT2fOFxPBrE7PkeY8UXD7G5Yei6iqqCxJh8nhLQ3DoayhZM69hO\nePOV1Z/LDERCnGel15WKQ1QJ1HZ+JQXnfQrE1Mi9QrXO5bVFtnXIr0mZ+AzwoUmn\nK5fYCvaL3xDZPDzOYL5kZG2hQKgbywGKZoQx16G0dSEhlAHbK9z6XmPRrbUKGzB8\nqV7QGbL7BUTQs5JW/8LpkYr5C0q5THtUVb9mHNR3jPf9WTPQ0D3lxcbLS4PQ8jQ/\nL/GcuHGmsXhe2Unw3w2wpuJKPeHKz4rBNIvaSjIZl9/dIKM88JYQTiIGKErxsC0e\nkczQMp6BAoGBAO0zUN8H7ynXGNNtK/tJo0lI3qg1ZKgr+0CU2L5eU8Bn1oJ1JkCI\nWD3p36NdECx5tGexm9U6MN+HzKYUjnQ6LKzbHQGLZqzF5IL5axXgCn8w4BM+6Ixm\ny8kQgsTKlKRMXIn8RZCmXNnc7v0FhBgpDxPmm7ZUuOPrInd8Ph4mEsePAoGBANb4\n3/izAHnLEp3/sTOZpfWBnDcvEHCG7/JAX0TDRW1FpXiTHpvDV1j3XU3EvLl7WRJ1\nB+B8h/Z6kQtUUxQ3I+zxuQIkQYI8qPu+xhQ8gb5AIO5CMX09+xKUgYjQtm7kYs7W\nL0LD9u3hkGsJk2wfVvMJKb3OSIHeTwRzFCzGX995AoGADkLB8eu/FKAIfwRPCHVE\nsfwMtqjkj2XJ9FeNcRQ5g/Tf8OGnCGEzBwXb05wJVrXUgXp4dBaqYTdAKj8uLEvd\nmi9t/LzR+33cGUdAQHItxcKbsMv00TyNRQUvZFZ7ZEY8aBkv5uZfvJHZ5iQ8C7+g\nHGXNfVGXGPutz/KN6X25CLECgYEAjVLK0MkXzLxCYJRDIhB1TpQVXjpxYUP2Vxls\nSSxfeYqkJPgNvYiHee33xQ8+TP1y9WzkWh+g2AbGmwTuKKL6CvQS9gKVvqqaFB7y\nKrkR13MTPJKvHHdQYKGQqQGgHKh0kGFCC0+PoVwtYs/XU1KpZCE16nNgXrOvTYNN\nHxESa+kCgYB7WOcawTp3WdKP8JbolxIfxax7Kd4QkZhY7dEb4JxBBYXXXpv/NHE9\npcJw4eKDyY+QE2AHPu3+fQYzXopaaTGRpB+ynEfYfD2hW+HnOWfWu/lFJbiwBn/S\nwRsYzSWiLtNplKNFRrsSoMWlh8GOTUpZ7FMLXWhE4rE9NskQBbYq8g==\n-----END RSA PRIVATE KEY-----', - cert: '-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUcmAewXEYwtzbZmZAJ5inMogKSbowDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjAwODM4MzRaFw0yNTAy\nMTkwODM4MzRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDHNgjCV+evsAra/oT5zahgPucAhcgi8O69W9nxH2TL\nFcjNNcGPkPUCCe2opLdsVHVdPyEJV5eO4so8G9duFWMbXmBeVfGk2IWLVlymEm+z\njMH9WHtm/YAu3dqdIjCwnatED9H8ap0k+Qd9h/8YxMMvDRiWVHRg568SudEggzlL\nnwuPadMKvm/mErUaX2ZbBGQVAqRZWXZRe38lfoLtnpTIlcxlKegbQrDoZCN7jUrm\nvRl3OuGsZ+Zv3BINSf2xkZfqKX6gyJjPtSBCQ8TMZRkQnEonDHLxmA91KBu9irhb\nA/BsFQmWnDSC3mLoc5LsKmFFqZr7MB/ku99IugtdI/knAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQryyWLuN22OqU1r9HIt2tMLBk42DAfBgNVHSMEGDAWgBQryyWLuN22OqU1\nr9HIt2tMLBk42DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAe\nCeXQZlXJ2xLnDoOoKY3BpodErNmAwygGYxwDCU0xPbpUMPrQhLI80JlZmfy58gT/\n0ZbULS+srShfEsFnBLmzWLGXDvA/IKCQyTmCQwbPeELGXF6h4URMb+lQL7WL9tY0\nuUg2dA+7CtYokIrOkGqUitPK3yvVhxugkf51WIgKMACZDibOQSWrV5QO2vHOAaO9\nePzRGGl3+Ebmcs3+5w1fI6OLsIZH10lfEnC83C0lO8tIJlGsXMQkCjAcX22rT0rc\nAcxLm07H4EwMwgOAJUkuDjD3y4+KH91jKWF8bhaLZooFB8lccNnaCRiuZRnXlvmf\nM7uVlLGwlj5R9iHd+0dP\n-----END CERTIFICATE-----' - }; - - // Create SMTP server instance - const smtpServer = createSmtpServer(mockEmailServer, options); - - // Verify instance was created correctly - expect(smtpServer).toBeTruthy(); - - // Test that the listen method exists and is callable - expect(typeof smtpServer.listen === 'function').toBeTruthy(); - - // Test that the close method exists - expect(typeof smtpServer.close === 'function').toBeTruthy(); -}); - -tap.test('verify SMTP server listen method - skipping test that accesses private properties', async (tools) => { - tools.skip('Skipping test that accesses private properties'); - // Mock email server - const mockEmailServer = { - processEmailByMode: async () => new Email({ - from: 'test@example.com', - to: 'recipient@example.com', - subject: 'Test Email', - text: 'This is a test email' - }) - } as any; - - // Create test configuration without certificates (will use self-signed) - const options: ISmtpServerOptions = { - port: 2526, // Use a different port for this test - hostname: 'test.example.com', - connectionTimeout: 5000 // Short timeout for tests - }; - - // Create SMTP server instance - const smtpServer = createSmtpServer(mockEmailServer, options); - - // Test that server was created - expect(smtpServer).toBeTruthy(); - expect(smtpServer).toHaveProperty('server'); - - // Mock server methods to avoid actual networking - let listenCalled = false; - let closeCalled = false; - - if (smtpServer.server) { - const originalListen = smtpServer.server.listen; - const originalClose = smtpServer.server.close; - - smtpServer.server.listen = function(port, callback) { - listenCalled = true; - if (callback) callback(); - return this; - }; - - smtpServer.server.close = function(callback) { - closeCalled = true; - if (callback) callback(null); - return this; - }; - - try { - // Test listen method - await smtpServer.listen(); - expect(listenCalled).toBeTruthy(); - - // Test close method - await smtpServer.close(); - expect(closeCalled).toBeTruthy(); - } finally { - // Restore original methods - smtpServer.server.listen = originalListen; - smtpServer.server.close = originalClose; - } - } -}); - -tap.test('verify SMTP server error handling - skipping test that accesses private properties', async (tools) => { - tools.skip('Skipping test that accesses private properties'); - // Mock email server - const mockEmailServer = { - processEmailByMode: async () => new Email({ - from: 'test@example.com', - to: 'recipient@example.com', - subject: 'Test Email', - text: 'This is a test email' - }) - } as any; - - // Create test configuration without certificates - const options: ISmtpServerOptions = { - port: 2527, // Use port that should work - hostname: 'test.example.com' - }; - - // Create SMTP server instance - const smtpServer = createSmtpServer(mockEmailServer, options); - - // Test error handling by mocking the server's error event - if (smtpServer.server) { - const originalListen = smtpServer.server.listen; - const originalOn = smtpServer.server.on; - const originalOnce = smtpServer.server.once; - - let errorCallback: (err: Error) => void; - let listeningCallback: () => void; - - smtpServer.server.listen = function(port, callback) { - // Simulate error after a delay - setTimeout(() => { - if (errorCallback) { - errorCallback(new Error('EACCES: Permission denied')); - } - }, 10); - return this; - }; - - smtpServer.server.on = function(event: string, callback: any) { - if (event === 'error') { - errorCallback = callback; - } - return originalOn.call(this, event, callback); - }; - - smtpServer.server.once = function(event: string, callback: any) { - if (event === 'listening') { - listeningCallback = callback; - } - return originalOnce.call(this, event, callback); - }; - - try { - // This should fail with an error - await smtpServer.listen().catch(error => { - // Expect an error - expect(error).toBeTruthy(); - expect(error.message).toContain('EACCES'); - }); - } finally { - // Restore original methods - smtpServer.server.listen = originalListen; - smtpServer.server.on = originalOn as any; - smtpServer.server.once = originalOnce as any; - } - } -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index fa8491d..7e87997 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartmta', - version: '2.2.1', + version: '2.3.0', description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' } diff --git a/ts/mail/delivery/index.ts b/ts/mail/delivery/index.ts index dc87825..e3066a9 100644 --- a/ts/mail/delivery/index.ts +++ b/ts/mail/delivery/index.ts @@ -19,6 +19,5 @@ export * from './classes.mta.config.js'; // Import and export SMTP modules as namespaces to avoid conflicts import * as smtpClientMod from './smtpclient/index.js'; -import * as smtpServerMod from './smtpserver/index.js'; -export { smtpClientMod, smtpServerMod }; \ No newline at end of file +export { smtpClientMod }; \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/certificate-utils.ts b/ts/mail/delivery/smtpserver/certificate-utils.ts deleted file mode 100644 index 1bf372f..0000000 --- a/ts/mail/delivery/smtpserver/certificate-utils.ts +++ /dev/null @@ -1,398 +0,0 @@ -/** - * Certificate Utilities for SMTP Server - * Provides utilities for managing TLS certificates - */ - -import * as fs from 'fs'; -import * as tls from 'tls'; -import { SmtpLogger } from './utils/logging.js'; - -/** - * Certificate data - */ -export interface ICertificateData { - key: Buffer; - cert: Buffer; - ca?: Buffer; -} - -/** - * Normalize a PEM certificate string - * @param str - Certificate string - * @returns Normalized certificate string - */ -function normalizeCertificate(str: string | Buffer): string { - // Handle different input types - let inputStr: string; - - if (Buffer.isBuffer(str)) { - // Convert Buffer to string using utf8 encoding - inputStr = str.toString('utf8'); - } else if (typeof str === 'string') { - inputStr = str; - } else { - throw new Error('Certificate must be a string or Buffer'); - } - - if (!inputStr) { - throw new Error('Empty certificate data'); - } - - // Remove any whitespace around the string - let normalizedStr = inputStr.trim(); - - // Make sure it has proper PEM format - if (!normalizedStr.includes('-----BEGIN ')) { - throw new Error('Invalid certificate format: Missing BEGIN marker'); - } - - if (!normalizedStr.includes('-----END ')) { - throw new Error('Invalid certificate format: Missing END marker'); - } - - // Normalize line endings (replace Windows-style \r\n with Unix-style \n) - normalizedStr = normalizedStr.replace(/\r\n/g, '\n'); - - // Only normalize if the certificate appears to have formatting issues - // Check if the certificate is already properly formatted - const lines = normalizedStr.split('\n'); - let needsReformatting = false; - - // Check for common formatting issues: - // 1. Missing line breaks after header/before footer - // 2. Lines that are too long or too short (except header/footer) - // 3. Multiple consecutive blank lines - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith('-----BEGIN ') || line.startsWith('-----END ')) { - continue; // Skip header/footer lines - } - if (line.length === 0) { - continue; // Skip empty lines - } - // Check if content lines are reasonable length (base64 is typically 64 chars per line) - if (line.length > 76) { // Allow some flexibility beyond standard 64 - needsReformatting = true; - break; - } - } - - // Only reformat if necessary - if (needsReformatting) { - const beginMatch = normalizedStr.match(/^(-----BEGIN [^-]+-----)(.*)$/s); - const endMatch = normalizedStr.match(/(.*)(-----END [^-]+-----)$/s); - - if (beginMatch && endMatch) { - const header = beginMatch[1]; - const footer = endMatch[2]; - let content = normalizedStr.substring(header.length, normalizedStr.length - footer.length); - - // Clean up only line breaks and carriage returns, preserve base64 content - content = content.replace(/[\n\r]/g, '').trim(); - - // Add proper line breaks (every 64 characters) - let formattedContent = ''; - for (let i = 0; i < content.length; i += 64) { - formattedContent += content.substring(i, Math.min(i + 64, content.length)) + '\n'; - } - - // Reconstruct the certificate - return header + '\n' + formattedContent + footer; - } - } - - return normalizedStr; -} - -/** - * Load certificates from PEM format strings - * @param options - Certificate options - * @returns Certificate data with Buffer format - */ -export function loadCertificatesFromString(options: { - key: string | Buffer; - cert: string | Buffer; - ca?: string | Buffer; -}): ICertificateData { - try { - // First try to use certificates without normalization - try { - let keyStr: string; - let certStr: string; - let caStr: string | undefined; - - // Convert inputs to strings without aggressive normalization - if (Buffer.isBuffer(options.key)) { - keyStr = options.key.toString('utf8'); - } else { - keyStr = options.key; - } - - if (Buffer.isBuffer(options.cert)) { - certStr = options.cert.toString('utf8'); - } else { - certStr = options.cert; - } - - if (options.ca) { - if (Buffer.isBuffer(options.ca)) { - caStr = options.ca.toString('utf8'); - } else { - caStr = options.ca; - } - } - - // Simple cleanup - only normalize line endings - keyStr = keyStr.trim().replace(/\r\n/g, '\n'); - certStr = certStr.trim().replace(/\r\n/g, '\n'); - if (caStr) { - caStr = caStr.trim().replace(/\r\n/g, '\n'); - } - - // Convert to buffers - const keyBuffer = Buffer.from(keyStr, 'utf8'); - const certBuffer = Buffer.from(certStr, 'utf8'); - const caBuffer = caStr ? Buffer.from(caStr, 'utf8') : undefined; - - // Test the certificates first - const secureContext = tls.createSecureContext({ - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }); - - SmtpLogger.info('Successfully validated certificates without normalization'); - - return { - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }; - - } catch (simpleError) { - SmtpLogger.warn(`Simple certificate loading failed, trying normalization: ${simpleError instanceof Error ? simpleError.message : String(simpleError)}`); - - // DEBUG: Log certificate details when simple loading fails - SmtpLogger.warn('Certificate loading failure details', { - keyType: typeof options.key, - certType: typeof options.cert, - keyIsBuffer: Buffer.isBuffer(options.key), - certIsBuffer: Buffer.isBuffer(options.cert), - keyLength: options.key ? options.key.length : 0, - certLength: options.cert ? options.cert.length : 0, - keyPreview: options.key ? (typeof options.key === 'string' ? options.key.substring(0, 50) : options.key.toString('utf8').substring(0, 50)) : 'null', - certPreview: options.cert ? (typeof options.cert === 'string' ? options.cert.substring(0, 50) : options.cert.toString('utf8').substring(0, 50)) : 'null' - }); - } - - // Fallback: Try to fix and normalize certificates - try { - // Normalize certificates (handles both string and Buffer inputs) - const key = normalizeCertificate(options.key); - const cert = normalizeCertificate(options.cert); - const ca = options.ca ? normalizeCertificate(options.ca) : undefined; - - // Convert normalized strings to Buffer with explicit utf8 encoding - const keyBuffer = Buffer.from(key, 'utf8'); - const certBuffer = Buffer.from(cert, 'utf8'); - const caBuffer = ca ? Buffer.from(ca, 'utf8') : undefined; - - // Log for debugging - SmtpLogger.debug('Certificate properties', { - keyLength: keyBuffer.length, - certLength: certBuffer.length, - caLength: caBuffer ? caBuffer.length : 0 - }); - - // Validate the certificates by attempting to create a secure context - try { - const secureContext = tls.createSecureContext({ - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }); - - // If createSecureContext doesn't throw, the certificates are valid - SmtpLogger.info('Successfully validated certificate format'); - } catch (validationError) { - // Log detailed error information for debugging - SmtpLogger.error(`Certificate validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`); - SmtpLogger.debug('Certificate validation details', { - keyPreview: keyBuffer.toString('utf8').substring(0, 100) + '...', - certPreview: certBuffer.toString('utf8').substring(0, 100) + '...', - keyLength: keyBuffer.length, - certLength: certBuffer.length - }); - throw validationError; - } - - return { - key: keyBuffer, - cert: certBuffer, - ca: caBuffer - }; - } catch (innerError) { - SmtpLogger.warn(`Certificate normalization failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`); - throw innerError; - } - } catch (error) { - SmtpLogger.error(`Error loading certificates: ${error instanceof Error ? error.message : String(error)}`); - throw error; - } -} - -/** - * Load certificates from files - * @param options - Certificate file paths - * @returns Certificate data with Buffer format - */ -export function loadCertificatesFromFiles(options: { - keyPath: string; - certPath: string; - caPath?: string; -}): ICertificateData { - try { - // Read files directly as Buffers - const key = fs.readFileSync(options.keyPath); - const cert = fs.readFileSync(options.certPath); - const ca = options.caPath ? fs.readFileSync(options.caPath) : undefined; - - // Log for debugging - SmtpLogger.debug('Certificate file properties', { - keyLength: key.length, - certLength: cert.length, - caLength: ca ? ca.length : 0 - }); - - // Validate the certificates by attempting to create a secure context - try { - const secureContext = tls.createSecureContext({ - key, - cert, - ca - }); - - // If createSecureContext doesn't throw, the certificates are valid - SmtpLogger.info('Successfully validated certificate files'); - } catch (validationError) { - SmtpLogger.error(`Certificate file validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`); - throw validationError; - } - - return { - key, - cert, - ca - }; - } catch (error) { - SmtpLogger.error(`Error loading certificate files: ${error instanceof Error ? error.message : String(error)}`); - throw error; - } -} - -/** - * Generate self-signed certificates for testing - * @returns Certificate data with Buffer format - */ -export function generateSelfSignedCertificates(): ICertificateData { - // This is for fallback/testing only - log a warning - SmtpLogger.warn('Generating self-signed certificates for testing - DO NOT USE IN PRODUCTION'); - - // Create selfsigned certificates using node-forge or similar library - // For now, use hardcoded certificates as a last resort - const key = Buffer.from(`-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEgJW1HdJPACGB -ifoL3PB+HdAVA2nUmMfq43JbIUPXGTxCtzmQhuV04WjITwFw1loPx3ReHh4KR5yJ -BVdzUDocHuauMmBycHAjv7mImR/VkuK/SwT0Q5G/9/M55o6HUNol0UKt+uZuBy1r -ggFTdTDLw86i9UG5CZbWF/Yb/DTRoAkCr7iLnaZhhhqcdh5BGj7JBylIAV5RIW1y -xQxJVJZQT2KgCeCnHRRvYRQ7tVzUQBcSvtW4zYtqK4C39BgRyLUZQVYB7siGT/uP -YJE7R73u0xEgDMFWR1pItUYcVQXHQJ+YsLVCzqI22Mik7URdwxoSHSXRYKn6wnKg -4JYg65JnAgMBAAECggEAM2LlwRhwP0pnLlLHiPE4jJ3Qdz/NUF0hLnRhcUwW1iJ1 -03jzCQ4QZ3etfL9O2hVJg49J+QUG50FNduLq4SE7GZj1dEJ/YNnlk9PpI8GSpLuA -mGTUKofIEJjNy5gKR0c6/rfgP8UXYSbRnTnZwIXVkUYuAUJLJTBVcJlcvCwJ3/zz -C8789JyOO1CNwF3zEIALdW5X5se8V+sw5iHDrHVxkR2xgsYpBBOylFfBxbMvV5o1 -i+QOD1HaXdmIvjBCnHqrjX5SDnAYwHBSB9y6WbwC+Th76QHkRNcHZH86PJVdLEUi -tBPQmQh+SjDRaZzDJvURnOFks+eEsCPVPZnQ4wgnAQKBgQD8oHwGZIZRUjnXULNc -vJoPcjLpvdHRO0kXTJHtG2au2i9jVzL9SFwH1lHQM0XdXPnR2BK4Gmgc2dRnSB9n -YPPvCgyL2RS0Y7W98yEcgBgwVOJHnPQGRNwxUfCTHgmCQ7lXjQKKG51+dBfOYP3j -w8VYbS2pqxZtzzZ5zhk2BrZJdwKBgQDHDZC+NU80f7rLEr5vpwx9epTArwXre8oj -nGgzZ9/lE14qDnITBuZPUHWc4/7U1CCmP0vVH6nFVvhN9ra9QCTJBzQ5aj0l3JM7 -9j8R5QZIPqOu4+aqf0ZFEgmpBK2SAYqNrJ+YVa2T/zLF44Jlr5WiLkPTUyMxV5+k -P4ZK8QP7wQKBgQCbeLuRWCuVKNYgYjm9TA55BbJL82J+MvhcbXUccpUksJQRxMV3 -98PBUW0Qw38WciJxQF4naSKD/jXYndD+wGzpKMIU+tKU+sEYMnuFnx13++K8XrAe -NQPHDsK1wRgXk5ygOHx78xnZbMmwBXNLwQXIhyO8FJpwJHj2CtYvjb+2xwKBgQCn -KW/RiAHvG6GKjCHCOTlx2qLPxUiXYCk2xwvRnNfY5+2PFoqMI/RZLT/41kTda1fA -TDw+j4Uu/fF2ChPadwRiUjXZzZx/UjcMJXTpQ2kpbGJ11U/cL4+Tk0S6wz+HoS7z -w3vXT9UoDyFxDBjuMQJxJWTjmymaYUtNnz4iMuRqwQKBgH+HKbYHCZaIzXRMEO5S -T3xDMYH59dTEKKXEOA1KJ9Zo5XSD8NE9SQ+9etoOcEq8tdYS45OkHD3VyFQa7THu -58awjTdkpSmMPsw3AElOYDYJgD9oxKtTjwkXHqMjDBQZrXqzOImOAJhEVL+XH3LP -lv6RZ47YRC88T+P6n1yg6BPp ------END PRIVATE KEY-----`, 'utf8'); - - const cert = Buffer.from(`-----BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIUHxmGQOQoiSbzqh6hIe+7h9xDXIUwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUyMTE2MDAzM1oXDTI2MDUy -MTE2MDAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAxICVtR3STwAhgYn6C9zwfh3QFQNp1JjH6uNyWyFD1xk8 -Qrc5kIbldOFoyE8BcNZaD8d0Xh4eCkeciwOV3FwHR4brjJgcnRwI7+5iJkf1ZLiv -0sE9EORv/fzOeaOh1DaJdFCrfrmbgdgOUm62WNQOB2hq0kggjh/S1K+TBfF+8QFs -XQyW7y7mHecNgCgK/pI5b1irdajRc7nLvzM/U8qNn4jjrLsRoYqBPpn7aLKIBrmN -pNSIe18q8EYWkdmWBcnsZpAYv75SJG8E0lAYpMv9OEUIwsPh7AYUdkZqKtFxVxV5 -bYlA5ZfnVnWrWEwRXaVdFFRXIjP+EFkGYYWThbvAIb0TPQIDAQABo1MwUTAdBgNV -HQ4EFgQUiW1MoYR8YK9KJTyip5oFoUVJoCgwHwYDVR0jBBgwFoAUiW1MoYR8YK9K -JTyip5oFoUVJoCgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA -BToM8SbUQXwJ9rTlQB2QI2GJaFwTpCFoQZwGUOCkwGLM3nOPLEbNPMDoIKGPwenB -P1xL8uJEgYRqP6UG/xy3HsxYsLCxuoxGGP2QjuiQKnFl0n85usZ5flCxmLC5IzYx -FLcR6WPTdj6b5JX0tM8Bi6toQ9Pj3u3dSVPZKRLYvJvZKt1PXI8qsHD/LvNa2wGG -Zi1BQFAr2cScNYa+p6IYDJi9TBNxoBIHNTzQPfWaen4MHRJqUNZCzQXcOnU/NW5G -+QqQSEMmk8yGucEHWUMFrEbABVgYuBslICEEtBZALB2jZJYSaJnPOJCcmFrxUv61 -ORWZbz+8rBL0JIeA7eFxEA== ------END CERTIFICATE-----`, 'utf8'); - - return { - key, - cert - }; -} - -/** - * Create TLS options for secure server or STARTTLS - * @param certificates - Certificate data - * @param isServer - Whether this is for server (true) or client (false) - * @returns TLS options - */ -export function createTlsOptions( - certificates: ICertificateData, - isServer: boolean = true -): tls.TlsOptions { - const options: tls.TlsOptions = { - key: certificates.key, - cert: certificates.cert, - ca: certificates.ca, - // Support a wider range of TLS versions for better compatibility - minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0) - maxVersion: 'TLSv1.3', // Support latest TLS version (1.3) - // Cipher suites for broad compatibility - ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4', - // For testing, allow unauthorized (self-signed certs) - rejectUnauthorized: false, - // Longer handshake timeout for reliability - handshakeTimeout: 30000, - // TLS renegotiation option (removed - not supported in newer Node.js) - // Increase timeout for better reliability under test conditions - sessionTimeout: 600, - // Let the client choose the cipher for better compatibility - honorCipherOrder: false, - // For debugging - enableTrace: true, - // Disable secure options to allow more flexibility - secureOptions: 0 - }; - - // Server-specific options - if (isServer) { - options.ALPNProtocols = ['smtp']; // Accept non-ALPN connections (legacy clients) - } - - return options; -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/command-handler.ts b/ts/mail/delivery/smtpserver/command-handler.ts deleted file mode 100644 index 7f9a267..0000000 --- a/ts/mail/delivery/smtpserver/command-handler.ts +++ /dev/null @@ -1,1340 +0,0 @@ -/** - * SMTP Command Handler - * Responsible for parsing and handling SMTP commands - */ - -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import type { ISmtpSession, IEnvelopeRecipient } from './interfaces.js'; -import type { ICommandHandler, ISmtpServer } from './interfaces.js'; -import { SmtpCommand, SmtpResponseCode, SMTP_DEFAULTS, SMTP_EXTENSIONS } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { adaptiveLogger } from './utils/adaptive-logging.js'; -import { extractCommandName, extractCommandArgs, formatMultilineResponse } from './utils/helpers.js'; -import { validateEhlo, validateMailFrom, validateRcptTo, isValidCommandSequence } from './utils/validation.js'; - -/** - * Handles SMTP commands and responses - */ -export class CommandHandler implements ICommandHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer: ISmtpServer; - - /** - * Creates a new command handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer) { - this.smtpServer = smtpServer; - } - - /** - * Process a command from the client - * @param socket - Client socket - * @param commandLine - Command line from client - */ - public async processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): Promise { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - SmtpLogger.warn(`No session found for socket from ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - socket.end(); - return; - } - - // Check if we're in the middle of an AUTH LOGIN sequence - if ((session as any).authLoginState) { - await this.handleAuthLoginResponse(socket, session, commandLine); - return; - } - - // Handle raw data chunks from connection manager during DATA mode - if (commandLine.startsWith('__RAW_DATA__')) { - const rawData = commandLine.substring('__RAW_DATA__'.length); - - const dataHandler = this.smtpServer.getDataHandler(); - if (dataHandler) { - // Let the data handler process the raw chunk - dataHandler.handleDataReceived(socket, rawData) - .catch(error => { - SmtpLogger.error(`Error processing raw email data: ${error.message}`, { - sessionId: session.id, - error - }); - - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email data: ${error.message}`); - this.resetSession(session); - }); - } else { - // No data handler available - SmtpLogger.error('Data handler not available for raw data', { sessionId: session.id }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - data handler not available`); - this.resetSession(session); - } - return; - } - - // Handle data state differently - pass to data handler (legacy line-based processing) - if (session.state === SmtpState.DATA_RECEIVING) { - // Check if this looks like an SMTP command - during DATA mode all input should be treated as message content - const looksLikeCommand = /^[A-Z]{4,}( |:)/i.test(commandLine.trim()); - - // Special handling for ERR-02 test: handle "MAIL FROM" during DATA mode - // The test expects a 503 response for this case, not treating it as content - if (looksLikeCommand && commandLine.trim().toUpperCase().startsWith('MAIL FROM')) { - // This is the command that ERR-02 test is expecting to fail with 503 - SmtpLogger.debug(`Received MAIL FROM command during DATA mode - responding with sequence error`); - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - - const dataHandler = this.smtpServer.getDataHandler(); - if (dataHandler) { - // Let the data handler process the line (legacy mode) - dataHandler.processEmailData(socket, commandLine) - .catch(error => { - SmtpLogger.error(`Error processing email data: ${error.message}`, { - sessionId: session.id, - error - }); - - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email data: ${error.message}`); - this.resetSession(session); - }); - } else { - // No data handler available - SmtpLogger.error('Data handler not available', { sessionId: session.id }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - data handler not available`); - this.resetSession(session); - } - return; - } - - // Handle command pipelining (RFC 2920) - // Multiple commands can be sent in a single TCP packet - if (commandLine.includes('\r\n') || commandLine.includes('\n')) { - // Split the commandLine into individual commands by newline - const commands = commandLine.split(/\r\n|\n/).filter(line => line.trim().length > 0); - - if (commands.length > 1) { - SmtpLogger.debug(`Command pipelining detected: ${commands.length} commands`, { - sessionId: session.id, - commandCount: commands.length - }); - - // Process each command separately (recursively call processCommand) - for (const cmd of commands) { - await this.processCommand(socket, cmd); - } - return; - } - } - - // Log received command using adaptive logger - adaptiveLogger.logCommand(commandLine, socket, session); - - // Extract command and arguments - const command = extractCommandName(commandLine); - const args = extractCommandArgs(commandLine); - - // For the ERR-01 test, an empty or invalid command is considered a syntax error (500) - if (!command || command.trim().length === 0) { - // Record error for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordError(session.remoteAddress); - - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive errors`); - this.sendResponse(socket, `421 Too many errors - connection blocked`); - socket.end(); - } else { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Command not recognized`); - } - return; - } - - // Handle unknown commands - this should happen before sequence validation - // RFC 5321: Use 500 for unrecognized commands, 501 for parameter errors - if (!Object.values(SmtpCommand).includes(command.toUpperCase() as SmtpCommand)) { - // Record error for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordError(session.remoteAddress); - - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive errors`); - this.sendResponse(socket, `421 Too many errors - connection blocked`); - socket.end(); - } else { - // Comply with RFC 5321 section 4.2.4: Use 500 for unrecognized commands - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Command not recognized`); - } - return; - } - - // Handle test input "MAIL FROM: missing_brackets@example.com" - specifically check for this case - // This is needed for ERR-01 test to pass - if (command.toUpperCase() === SmtpCommand.MAIL_FROM) { - // Handle "MAIL FROM:" with missing parameter - a special case for ERR-01 test - if (!args || args.trim() === '' || args.trim() === ':') { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Missing email address`); - return; - } - - // Handle email without angle brackets - if (args.includes('@') && !args.includes('<') && !args.includes('>')) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid syntax - angle brackets required`); - return; - } - } - - // Special handling for the "MAIL FROM:" missing parameter test (ERR-01 Test 3) - // The test explicitly sends "MAIL FROM:" without any address and expects 501 - // We need to catch this EXACT case before the sequence validation - if (commandLine.trim() === 'MAIL FROM:') { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Missing email address`); - return; - } - - // Validate command sequence - this must happen after validating that it's a recognized command - // The order matters for ERR-01 and ERR-02 test compliance: - // - Syntax errors (501): Invalid command format or arguments - // - Sequence errors (503): Valid command in wrong sequence - if (!this.validateCommandSequence(command, session)) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - - // Process the command - switch (command) { - case SmtpCommand.EHLO: - case SmtpCommand.HELO: - this.handleEhlo(socket, args); - break; - - case SmtpCommand.MAIL_FROM: - this.handleMailFrom(socket, args); - break; - - case SmtpCommand.RCPT_TO: - this.handleRcptTo(socket, args); - break; - - case SmtpCommand.DATA: - this.handleData(socket); - break; - - case SmtpCommand.RSET: - this.handleRset(socket); - break; - - case SmtpCommand.NOOP: - this.handleNoop(socket); - break; - - case SmtpCommand.QUIT: - this.handleQuit(socket, args); - break; - - case SmtpCommand.STARTTLS: - const tlsHandler = this.smtpServer.getTlsHandler(); - if (tlsHandler && tlsHandler.isTlsEnabled()) { - await tlsHandler.handleStartTls(socket, session); - } else { - SmtpLogger.warn('STARTTLS requested but TLS is not enabled', { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort - }); - this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} STARTTLS not available at this time`); - } - break; - - case SmtpCommand.AUTH: - this.handleAuth(socket, args); - break; - - case SmtpCommand.HELP: - this.handleHelp(socket, args); - break; - - case SmtpCommand.VRFY: - this.handleVrfy(socket, args); - break; - - case SmtpCommand.EXPN: - this.handleExpn(socket, args); - break; - - default: - this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Command not implemented`); - break; - } - } - - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response to send - */ - public sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - adaptiveLogger.logResponse(response, socket); - } catch (error) { - // Attempt to recover from known transient errors - if (this.isRecoverableSocketError(error)) { - this.handleSocketError(socket, error, response); - } else { - // Log error and destroy socket for non-recoverable errors - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - - socket.destroy(); - } - } - } - - /** - * Check if a socket error is potentially recoverable - * @param error - The error that occurred - * @returns Whether the error is potentially recoverable - */ - private isRecoverableSocketError(error: unknown): boolean { - const recoverableErrorCodes = [ - 'EPIPE', // Broken pipe - 'ECONNRESET', // Connection reset by peer - 'ETIMEDOUT', // Connection timed out - 'ECONNABORTED' // Connection aborted - ]; - - return ( - error instanceof Error && - 'code' in error && - typeof (error as any).code === 'string' && - recoverableErrorCodes.includes((error as any).code) - ); - } - - /** - * Handle recoverable socket errors with retry logic - * @param socket - Client socket - * @param error - The error that occurred - * @param response - The response that failed to send - */ - private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - SmtpLogger.error(`Session not found when handling socket error`); - socket.destroy(); - return; - } - - // Get error details for logging - const errorMessage = error instanceof Error ? error.message : String(error); - const errorCode = error instanceof Error && 'code' in error ? (error as any).code : 'UNKNOWN'; - - SmtpLogger.warn(`Recoverable socket error (${errorCode}): ${errorMessage}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Check if socket is already destroyed - if (socket.destroyed) { - SmtpLogger.info(`Socket already destroyed, cannot retry operation`); - return; - } - - // Check if socket is writeable - if (!socket.writable) { - SmtpLogger.info(`Socket no longer writable, aborting recovery attempt`); - socket.destroy(); - return; - } - - // Attempt to retry the write operation after a short delay - setTimeout(() => { - try { - if (!socket.destroyed && socket.writable) { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - SmtpLogger.info(`Successfully retried send operation after error`); - } else { - SmtpLogger.warn(`Socket no longer available for retry`); - if (!socket.destroyed) { - socket.destroy(); - } - } - } catch (retryError) { - SmtpLogger.error(`Retry attempt failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`); - if (!socket.destroyed) { - socket.destroy(); - } - } - }, 100); // Short delay before retry - } - - /** - * Handle EHLO command - * @param socket - Client socket - * @param clientHostname - Client hostname from EHLO command - */ - public handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Extract command and arguments from clientHostname - // EHLO/HELO might come with the command itself in the arguments string - let hostname = clientHostname; - if (hostname.toUpperCase().startsWith('EHLO ') || hostname.toUpperCase().startsWith('HELO ')) { - hostname = hostname.substring(5).trim(); - } - - // Check for empty hostname - if (!hostname) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Missing domain name`); - return; - } - - // Validate EHLO hostname - const validation = validateEhlo(hostname); - - if (!validation.isValid) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`); - return; - } - - // Update session state and client hostname - session.clientHostname = validation.hostname || hostname; - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO); - - // Get options once for this method - const options = this.smtpServer.getOptions(); - - // Set up EHLO response lines - const responseLines = [ - `${options.hostname || SMTP_DEFAULTS.HOSTNAME} greets ${session.clientHostname}`, - SMTP_EXTENSIONS.PIPELINING, - SMTP_EXTENSIONS.formatExtension(SMTP_EXTENSIONS.SIZE, options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE), - SMTP_EXTENSIONS.EIGHTBITMIME, - SMTP_EXTENSIONS.ENHANCEDSTATUSCODES - ]; - - // Add TLS extension if available and not already using TLS - const tlsHandler = this.smtpServer.getTlsHandler(); - if (tlsHandler && tlsHandler.isTlsEnabled() && !session.useTLS) { - responseLines.push(SMTP_EXTENSIONS.STARTTLS); - } - - // Add AUTH extension if configured - if (options.auth && options.auth.methods && options.auth.methods.length > 0) { - responseLines.push(`${SMTP_EXTENSIONS.AUTH} ${options.auth.methods.join(' ')}`); - } - - // Send multiline response - this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.OK, responseLines)); - } - - /** - * Handle MAIL FROM command - * @param socket - Client socket - * @param args - Command arguments - */ - public handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Check if the client has sent EHLO/HELO first - if (session.state === SmtpState.GREETING) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - - // For test compatibility - reset state if receiving a new MAIL FROM after previous transaction - if (session.state === SmtpState.MAIL_FROM || session.state === SmtpState.RCPT_TO) { - // Silently reset the transaction state - allow multiple MAIL FROM commands - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - session.envelope = { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }; - } - - // Get options once for this method - const options = this.smtpServer.getOptions(); - - // Check if authentication is required but not provided - if (options.auth && options.auth.required && !session.authenticated) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_REQUIRED} Authentication required`); - return; - } - - // Get rate limiter for message-level checks - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - - // Note: Connection-level rate limiting is already handled in ConnectionManager - - // Special handling for commands that include "MAIL FROM:" in the args - let processedArgs = args; - - // Handle test formats with or without colons and "FROM" parts - if (args.toUpperCase().startsWith('FROM:')) { - processedArgs = args.substring(5).trim(); // Skip "FROM:" - } else if (args.toUpperCase().startsWith('FROM')) { - processedArgs = args.substring(4).trim(); // Skip "FROM" - } else if (args.toUpperCase().includes('MAIL FROM:')) { - // The command was already prepended to the args - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - processedArgs = args.substring(colonIndex + 1).trim(); - } - } else if (args.toUpperCase().includes('MAIL FROM')) { - // Handle case without colon - const fromIndex = args.toUpperCase().indexOf('FROM'); - if (fromIndex !== -1) { - processedArgs = args.substring(fromIndex + 4).trim(); - } - } - - // Validate MAIL FROM syntax - for ERR-01 test compliance, this must be BEFORE sequence validation - const validation = validateMailFrom(processedArgs); - - if (!validation.isValid) { - // Return 501 for syntax errors - required for ERR-01 test to pass - // This RFC 5321 compliance is critical - syntax errors must be 501 - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`); - return; - } - - // Check message rate limits for this sender - const senderAddress = validation.address || ''; - const senderDomain = senderAddress.includes('@') ? senderAddress.split('@')[1] : undefined; - - // Check rate limits with domain context if available - const messageResult = rateLimiter.checkMessageLimit( - senderAddress, - session.remoteAddress, - 1, // We don't know recipients yet, check with 1 - undefined, // No pattern matching for now - senderDomain // Pass domain for domain-specific limits - ); - - if (!messageResult.allowed) { - SmtpLogger.warn(`Message rate limit exceeded for ${senderAddress} from IP ${session.remoteAddress}: ${messageResult.reason}`); - // Use 421 for temporary rate limiting (client should retry later) - this.sendResponse(socket, `421 ${messageResult.reason} - try again later`); - return; - } - - // Enhanced SIZE parameter handling - if (validation.params && validation.params.SIZE) { - const size = parseInt(validation.params.SIZE, 10); - - // Check for valid numeric format - if (isNaN(size)) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: not a number`); - return; - } - - // Check for negative values - if (size < 0) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: cannot be negative`); - return; - } - - // Ensure reasonable minimum size (at least 100 bytes for headers) - if (size < 100) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: too small (minimum 100 bytes)`); - return; - } - - // Check against server maximum - const maxSize = options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE; - if (size > maxSize) { - // Generate informative error with the server's limit - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit of ${Math.floor(maxSize / 1024)} KB`); - return; - } - - // Log large messages for monitoring - if (size > maxSize * 0.8) { - SmtpLogger.info(`Large message detected (${Math.floor(size / 1024)} KB)`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - sizeBytes: size, - percentOfMax: Math.floor((size / maxSize) * 100) - }); - } - } - - // Reset email data and recipients for new transaction - session.mailFrom = validation.address || ''; - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - - // Update envelope information - session.envelope = { - mailFrom: { - address: validation.address || '', - args: validation.params || {} - }, - rcptTo: [] - }; - - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.MAIL_FROM); - - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); - } - - /** - * Handle RCPT TO command - * @param socket - Client socket - * @param args - Command arguments - */ - public handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Check if MAIL FROM was provided first - if (session.state !== SmtpState.MAIL_FROM && session.state !== SmtpState.RCPT_TO) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - - // Special handling for commands that include "RCPT TO:" in the args - let processedArgs = args; - if (args.toUpperCase().startsWith('TO:')) { - processedArgs = args; - } else if (args.toUpperCase().includes('RCPT TO')) { - // The command was already prepended to the args - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - processedArgs = args.substring(colonIndex + 1).trim(); - } - } - - // Validate RCPT TO syntax - const validation = validateRcptTo(processedArgs); - - if (!validation.isValid) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`); - return; - } - - // Check if we've reached maximum recipients - const options = this.smtpServer.getOptions(); - const maxRecipients = options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS; - if (session.rcptTo.length >= maxRecipients) { - this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Too many recipients`); - return; - } - - // Check rate limits for recipients - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const recipientAddress = validation.address || ''; - const recipientDomain = recipientAddress.includes('@') ? recipientAddress.split('@')[1] : undefined; - - // Check rate limits with accumulated recipient count - const recipientCount = session.rcptTo.length + 1; // Including this new recipient - const messageResult = rateLimiter.checkMessageLimit( - session.mailFrom, - session.remoteAddress, - recipientCount, - undefined, // No pattern matching for now - recipientDomain // Pass recipient domain for domain-specific limits - ); - - if (!messageResult.allowed) { - SmtpLogger.warn(`Recipient rate limit exceeded for ${recipientAddress} from IP ${session.remoteAddress}: ${messageResult.reason}`); - // Use 451 for temporary recipient rejection - this.sendResponse(socket, `451 ${messageResult.reason} - try again later`); - return; - } - - // Create recipient object - const recipient: IEnvelopeRecipient = { - address: validation.address || '', - args: validation.params || {} - }; - - // Add to session data - session.rcptTo.push(validation.address || ''); - session.envelope.rcptTo.push(recipient); - - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.RCPT_TO); - - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} Recipient ok`); - } - - /** - * Handle DATA command - * @param socket - Client socket - */ - public handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // For tests, be slightly more permissive - also accept DATA after MAIL FROM - // But ensure we at least have a sender defined - if (session.state !== SmtpState.RCPT_TO && session.state !== SmtpState.MAIL_FROM) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - - // Check if we have a sender - if (!session.mailFrom) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} No sender specified`); - return; - } - - // Ideally we should have recipients, but for test compatibility, we'll only - // insist on recipients if we're in RCPT_TO state - if (session.state === SmtpState.RCPT_TO && !session.rcptTo.length) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} No recipients specified`); - return; - } - - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.DATA_RECEIVING); - - // Reset email data storage - session.emailData = ''; - session.emailDataChunks = []; - - // Set up timeout for DATA command - const dataTimeout = SMTP_DEFAULTS.DATA_TIMEOUT; - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - - session.dataTimeoutId = setTimeout(() => { - if (session.state === SmtpState.DATA_RECEIVING) { - SmtpLogger.warn(`DATA command timeout for session ${session.id}`, { - sessionId: session.id, - timeout: dataTimeout - }); - - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`); - this.resetSession(session); - } - }, dataTimeout); - - // Send intermediate response to signal start of data - this.sendResponse(socket, `${SmtpResponseCode.START_MAIL_INPUT} Start mail input; end with .`); - } - - /** - * Handle RSET command - * @param socket - Client socket - */ - public handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Reset the transaction state - this.resetSession(session); - - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); - } - - /** - * Handle NOOP command - * @param socket - Client socket - */ - public handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); - } - - /** - * Handle QUIT command - * @param socket - Client socket - */ - public handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket, args?: string): void { - // QUIT command should not have any parameters - if (args && args.trim().length > 0) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Syntax error in parameters`); - return; - } - - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - - // Send goodbye message - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_CLOSING} ${this.smtpServer.getOptions().hostname} Service closing transmission channel`); - - // End the connection - socket.end(); - - // Clean up session if we have one - if (session) { - this.smtpServer.getSessionManager().removeSession(socket); - } - } - - /** - * Handle AUTH command - * @param socket - Client socket - * @param args - Command arguments - */ - private handleAuth(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Check if we have auth config - if (!this.smtpServer.getOptions().auth || !this.smtpServer.getOptions().auth.methods || !this.smtpServer.getOptions().auth.methods.length) { - this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Authentication not supported`); - return; - } - - // Check if TLS is required for authentication - if (!session.useTLS) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication requires TLS`); - return; - } - - // Parse AUTH command - const parts = args.trim().split(/\s+/); - const method = parts[0]?.toUpperCase(); - const initialResponse = parts[1]; - - // Check if method is supported - const supportedMethods = this.smtpServer.getOptions().auth.methods.map(m => m.toUpperCase()); - if (!method || !supportedMethods.includes(method)) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Unsupported authentication method`); - return; - } - - // Handle different authentication methods - switch (method) { - case 'PLAIN': - this.handleAuthPlain(socket, session, initialResponse); - break; - case 'LOGIN': - this.handleAuthLogin(socket, session, initialResponse); - break; - default: - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} ${method} authentication not implemented`); - } - } - - /** - * Handle AUTH PLAIN authentication - * @param socket - Client socket - * @param session - Session - * @param initialResponse - Optional initial response - */ - private async handleAuthPlain(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession, initialResponse?: string): Promise { - try { - let credentials: string; - - if (initialResponse) { - // Credentials provided with AUTH PLAIN command - credentials = initialResponse; - } else { - // Request credentials - this.sendResponse(socket, '334'); - - // Wait for credentials - credentials = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Auth response timeout')); - }, 30000); - - socket.once('data', (data: Buffer) => { - clearTimeout(timeout); - resolve(data.toString().trim()); - }); - }); - } - - // Decode PLAIN credentials (base64 encoded: authzid\0authcid\0password) - const decoded = Buffer.from(credentials, 'base64').toString('utf8'); - const parts = decoded.split('\0'); - - if (parts.length !== 3) { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Invalid credentials format`); - return; - } - - const [authzid, authcid, password] = parts; - const username = authcid || authzid; // Use authcid if provided, otherwise authzid - - // Authenticate using security handler - const authenticated = await this.smtpServer.getSecurityHandler().authenticate({ - username, - password - }); - - if (authenticated) { - session.authenticated = true; - session.username = username; - this.sendResponse(socket, `${SmtpResponseCode.AUTHENTICATION_SUCCESSFUL} Authentication successful`); - } else { - // Record authentication failure for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordAuthFailure(session.remoteAddress); - - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive authentication failures`); - this.sendResponse(socket, `421 Too many authentication failures - connection blocked`); - socket.end(); - } else { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication failed`); - } - } - } catch (error) { - SmtpLogger.error(`AUTH PLAIN error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`); - } - } - - /** - * Handle AUTH LOGIN authentication - * @param socket - Client socket - * @param session - Session - * @param initialResponse - Optional initial response - */ - private async handleAuthLogin(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession, initialResponse?: string): Promise { - try { - if (initialResponse) { - // Username provided with AUTH LOGIN command - const username = Buffer.from(initialResponse, 'base64').toString('utf8'); - (session as any).authLoginState = 'waiting_password'; - (session as any).authLoginUsername = username; - // Request password - this.sendResponse(socket, '334 UGFzc3dvcmQ6'); // Base64 for "Password:" - } else { - // Request username - (session as any).authLoginState = 'waiting_username'; - this.sendResponse(socket, '334 VXNlcm5hbWU6'); // Base64 for "Username:" - } - } catch (error) { - SmtpLogger.error(`AUTH LOGIN error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`); - delete (session as any).authLoginState; - delete (session as any).authLoginUsername; - } - } - - /** - * Handle AUTH LOGIN response - * @param socket - Client socket - * @param session - Session - * @param response - Response from client - */ - private async handleAuthLoginResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession, response: string): Promise { - const trimmedResponse = response.trim(); - - // Check for cancellation - if (trimmedResponse === '*') { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication cancelled`); - delete (session as any).authLoginState; - delete (session as any).authLoginUsername; - return; - } - - try { - if ((session as any).authLoginState === 'waiting_username') { - // We received the username - const username = Buffer.from(trimmedResponse, 'base64').toString('utf8'); - (session as any).authLoginUsername = username; - (session as any).authLoginState = 'waiting_password'; - // Request password - this.sendResponse(socket, '334 UGFzc3dvcmQ6'); // Base64 for "Password:" - } else if ((session as any).authLoginState === 'waiting_password') { - // We received the password - const password = Buffer.from(trimmedResponse, 'base64').toString('utf8'); - const username = (session as any).authLoginUsername; - - // Clear auth state - delete (session as any).authLoginState; - delete (session as any).authLoginUsername; - - // Authenticate using security handler - const authenticated = await this.smtpServer.getSecurityHandler().authenticate({ - username, - password - }); - - if (authenticated) { - session.authenticated = true; - session.username = username; - this.sendResponse(socket, `${SmtpResponseCode.AUTHENTICATION_SUCCESSFUL} Authentication successful`); - } else { - // Record authentication failure for rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - const shouldBlock = rateLimiter.recordAuthFailure(session.remoteAddress); - - if (shouldBlock) { - SmtpLogger.warn(`IP ${session.remoteAddress} blocked due to excessive authentication failures`); - this.sendResponse(socket, `421 Too many authentication failures - connection blocked`); - socket.end(); - } else { - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication failed`); - } - } - } - } catch (error) { - SmtpLogger.error(`AUTH LOGIN response error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`); - delete (session as any).authLoginState; - delete (session as any).authLoginUsername; - } - } - - /** - * Handle HELP command - * @param socket - Client socket - * @param args - Command arguments - */ - private handleHelp(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - - // Provide help information based on arguments - const helpCommand = args.trim().toUpperCase(); - - if (!helpCommand) { - // General help - const helpLines = [ - 'Supported commands:', - 'EHLO/HELO domain - Identify yourself to the server', - 'MAIL FROM:
- Start a new mail transaction', - 'RCPT TO:
- Specify recipients for the message', - 'DATA - Start message data input', - 'RSET - Reset the transaction', - 'NOOP - No operation', - 'QUIT - Close the connection', - 'HELP [command] - Show help' - ]; - - // Add conditional commands - const tlsHandler = this.smtpServer.getTlsHandler(); - if (tlsHandler && tlsHandler.isTlsEnabled()) { - helpLines.push('STARTTLS - Start TLS negotiation'); - } - - if (this.smtpServer.getOptions().auth && this.smtpServer.getOptions().auth.methods.length) { - helpLines.push('AUTH mechanism - Authenticate with the server'); - } - - this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.HELP_MESSAGE, helpLines)); - return; - } - - // Command-specific help - let helpText: string; - - switch (helpCommand) { - case 'EHLO': - case 'HELO': - helpText = 'EHLO/HELO domain - Identify yourself to the server'; - break; - - case 'MAIL': - helpText = 'MAIL FROM:
[SIZE=size] - Start a new mail transaction'; - break; - - case 'RCPT': - helpText = 'RCPT TO:
- Specify a recipient for the message'; - break; - - case 'DATA': - helpText = 'DATA - Start message data input, end with .'; - break; - - case 'RSET': - helpText = 'RSET - Reset the transaction'; - break; - - case 'NOOP': - helpText = 'NOOP - No operation'; - break; - - case 'QUIT': - helpText = 'QUIT - Close the connection'; - break; - - case 'STARTTLS': - helpText = 'STARTTLS - Start TLS negotiation'; - break; - - case 'AUTH': - helpText = `AUTH mechanism - Authenticate with the server. Supported methods: ${this.smtpServer.getOptions().auth?.methods.join(', ')}`; - break; - - default: - helpText = `Unknown command: ${helpCommand}`; - break; - } - - this.sendResponse(socket, `${SmtpResponseCode.HELP_MESSAGE} ${helpText}`); - } - - /** - * Handle VRFY command (Verify user/mailbox) - * RFC 5321 Section 3.5.1: Server MAY respond with 252 to avoid disclosing sensitive information - * @param socket - Client socket - * @param args - Command arguments (username to verify) - */ - private handleVrfy(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - - const username = args.trim(); - - // Security best practice: Do not confirm or deny user existence - // Instead, respond with 252 "Cannot verify, but will attempt delivery" - // This prevents VRFY from being used for user enumeration attacks - if (!username) { - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} User name required`); - } else { - // Log the VRFY attempt - SmtpLogger.info(`VRFY command received for user: ${username}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - useTLS: session.useTLS - }); - - // Respond with ambiguous response for security - this.sendResponse(socket, `${SmtpResponseCode.CANNOT_VRFY} Cannot VRFY user, but will accept message and attempt delivery`); - } - } - - /** - * Handle EXPN command (Expand mailing list) - * RFC 5321 Section 3.5.2: Server MAY disable this for security - * @param socket - Client socket - * @param args - Command arguments (mailing list to expand) - */ - private handleExpn(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Update session activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - - const listname = args.trim(); - - // Log the EXPN attempt - SmtpLogger.info(`EXPN command received for list: ${listname}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - useTLS: session.useTLS - }); - - // Disable EXPN for security (best practice - RFC 5321 Section 3.5.2) - // EXPN allows enumeration of list members, which is a privacy concern - this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} EXPN command is disabled for security reasons`); - } - - /** - * Reset session to after-EHLO state - * @param session - SMTP session to reset - */ - private resetSession(session: ISmtpSession): void { - // Clear any data timeout - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - - // Reset data fields but keep authentication state - session.mailFrom = ''; - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - session.envelope = { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }; - - // Reset state to after EHLO - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO); - } - - /** - * Validate command sequence based on current state - * @param command - Command to validate - * @param session - Current session - * @returns Whether the command is valid in the current state - */ - private validateCommandSequence(command: string, session: ISmtpSession): boolean { - // Always allow EHLO to reset the transaction at any state - // This makes tests pass where EHLO is used multiple times - if (command.toUpperCase() === 'EHLO' || command.toUpperCase() === 'HELO') { - return true; - } - - // Always allow RSET, NOOP, QUIT, and HELP - if (command.toUpperCase() === 'RSET' || - command.toUpperCase() === 'NOOP' || - command.toUpperCase() === 'QUIT' || - command.toUpperCase() === 'HELP') { - return true; - } - - // Always allow STARTTLS after EHLO/HELO (but not in DATA state) - if (command.toUpperCase() === 'STARTTLS' && - (session.state === SmtpState.AFTER_EHLO || - session.state === SmtpState.MAIL_FROM || - session.state === SmtpState.RCPT_TO)) { - return true; - } - - // During testing, be more permissive with sequence for MAIL and RCPT commands - // This helps pass tests that may send these commands in unexpected order - if (command.toUpperCase() === 'MAIL' && session.state !== SmtpState.DATA_RECEIVING) { - return true; - } - - // Handle RCPT TO during tests - be permissive but not in DATA state - if (command.toUpperCase() === 'RCPT' && session.state !== SmtpState.DATA_RECEIVING) { - return true; - } - - // Allow DATA command if in MAIL_FROM or RCPT_TO state for test compatibility - if (command.toUpperCase() === 'DATA' && - (session.state === SmtpState.MAIL_FROM || session.state === SmtpState.RCPT_TO)) { - return true; - } - - // Check standard command sequence - return isValidCommandSequence(command, session.state); - } - - /** - * Handle an SMTP command (interface requirement) - */ - public async handleCommand( - socket: plugins.net.Socket | plugins.tls.TLSSocket, - command: SmtpCommand, - args: string, - session: ISmtpSession - ): Promise { - // Delegate to processCommand for now - this.processCommand(socket, `${command} ${args}`.trim()); - } - - /** - * Get supported commands for current session state (interface requirement) - */ - public getSupportedCommands(session: ISmtpSession): SmtpCommand[] { - const commands: SmtpCommand[] = [SmtpCommand.NOOP, SmtpCommand.QUIT, SmtpCommand.RSET]; - - switch (session.state) { - case SmtpState.GREETING: - commands.push(SmtpCommand.EHLO, SmtpCommand.HELO); - break; - case SmtpState.AFTER_EHLO: - commands.push(SmtpCommand.MAIL_FROM, SmtpCommand.STARTTLS); - if (!session.authenticated) { - commands.push(SmtpCommand.AUTH); - } - break; - case SmtpState.MAIL_FROM: - commands.push(SmtpCommand.RCPT_TO); - break; - case SmtpState.RCPT_TO: - commands.push(SmtpCommand.RCPT_TO, SmtpCommand.DATA); - break; - default: - break; - } - - return commands; - } - - /** - * Clean up resources - */ - public destroy(): void { - // CommandHandler doesn't have timers or event listeners to clean up - SmtpLogger.debug('CommandHandler destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/connection-manager.ts b/ts/mail/delivery/smtpserver/connection-manager.ts deleted file mode 100644 index 02b27c9..0000000 --- a/ts/mail/delivery/smtpserver/connection-manager.ts +++ /dev/null @@ -1,1061 +0,0 @@ -/** - * SMTP Connection Manager - * Responsible for managing socket connections to the SMTP server - */ - -import * as plugins from '../../../plugins.js'; -import type { IConnectionManager, ISmtpServer } from './interfaces.js'; -import { SmtpResponseCode, SMTP_DEFAULTS, SmtpState } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { adaptiveLogger } from './utils/adaptive-logging.js'; -import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js'; - -/** - * Manager for SMTP connections - * Handles connection setup, event listeners, and lifecycle management - * Provides resource management, connection tracking, and monitoring - */ -export class ConnectionManager implements IConnectionManager { - /** - * Reference to the SMTP server instance - */ - private smtpServer: ISmtpServer; - - /** - * Set of active socket connections - */ - private activeConnections: Set = new Set(); - - /** - * Connection tracking for resource management - */ - private connectionStats = { - totalConnections: 0, - activeConnections: 0, - peakConnections: 0, - rejectedConnections: 0, - closedConnections: 0, - erroredConnections: 0, - timedOutConnections: 0 - }; - - /** - * Per-IP connection tracking for rate limiting - */ - private ipConnections: Map = new Map(); - - /** - * Resource monitoring interval - */ - private resourceCheckInterval: NodeJS.Timeout | null = null; - - /** - * Track cleanup timers so we can clear them - */ - private cleanupTimers: Set = new Set(); - - /** - * SMTP server options with enhanced resource controls - */ - private options: { - hostname: string; - maxConnections: number; - socketTimeout: number; - maxConnectionsPerIP: number; - connectionRateLimit: number; - connectionRateWindow: number; - bufferSizeLimit: number; - resourceCheckInterval: number; - }; - - /** - * Creates a new connection manager with enhanced resource management - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer) { - this.smtpServer = smtpServer; - - // Get options from server - const serverOptions = this.smtpServer.getOptions(); - - // Default values for resource management - adjusted for production scalability - const DEFAULT_MAX_CONNECTIONS_PER_IP = 50; // Increased to support high-concurrency scenarios - const DEFAULT_CONNECTION_RATE_LIMIT = 200; // Increased for production load handling - const DEFAULT_CONNECTION_RATE_WINDOW = 60 * 1000; // 60 seconds window - const DEFAULT_BUFFER_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB - const DEFAULT_RESOURCE_CHECK_INTERVAL = 30 * 1000; // 30 seconds - - this.options = { - hostname: serverOptions.hostname || SMTP_DEFAULTS.HOSTNAME, - maxConnections: serverOptions.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, - socketTimeout: serverOptions.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, - maxConnectionsPerIP: DEFAULT_MAX_CONNECTIONS_PER_IP, - connectionRateLimit: DEFAULT_CONNECTION_RATE_LIMIT, - connectionRateWindow: DEFAULT_CONNECTION_RATE_WINDOW, - bufferSizeLimit: DEFAULT_BUFFER_SIZE_LIMIT, - resourceCheckInterval: DEFAULT_RESOURCE_CHECK_INTERVAL - }; - - // Start resource monitoring - this.startResourceMonitoring(); - } - - /** - * Start resource monitoring interval to check resource usage - */ - private startResourceMonitoring(): void { - // Clear any existing interval - if (this.resourceCheckInterval) { - clearInterval(this.resourceCheckInterval); - } - - // Set up new interval - this.resourceCheckInterval = setInterval(() => { - this.monitorResourceUsage(); - }, this.options.resourceCheckInterval); - } - - /** - * Monitor resource usage and log statistics - */ - private monitorResourceUsage(): void { - // Calculate memory usage - const memoryUsage = process.memoryUsage(); - const memoryUsageMB = { - rss: Math.round(memoryUsage.rss / 1024 / 1024), - heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024), - heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024), - external: Math.round(memoryUsage.external / 1024 / 1024) - }; - - // Calculate connection rate metrics - const activeIPs = Array.from(this.ipConnections.entries()) - .filter(([_, data]) => data.count > 0).length; - - const highVolumeIPs = Array.from(this.ipConnections.entries()) - .filter(([_, data]) => data.count > this.options.connectionRateLimit / 2).length; - - // Log resource usage with more detailed metrics - SmtpLogger.info('Resource usage stats', { - connections: { - active: this.activeConnections.size, - total: this.connectionStats.totalConnections, - peak: this.connectionStats.peakConnections, - rejected: this.connectionStats.rejectedConnections, - closed: this.connectionStats.closedConnections, - errored: this.connectionStats.erroredConnections, - timedOut: this.connectionStats.timedOutConnections - }, - memory: memoryUsageMB, - ipTracking: { - uniqueIPs: this.ipConnections.size, - activeIPs: activeIPs, - highVolumeIPs: highVolumeIPs - }, - resourceLimits: { - maxConnections: this.options.maxConnections, - maxConnectionsPerIP: this.options.maxConnectionsPerIP, - connectionRateLimit: this.options.connectionRateLimit, - bufferSizeLimit: Math.round(this.options.bufferSizeLimit / 1024 / 1024) + 'MB' - } - }); - - // Check for potential DoS conditions - if (highVolumeIPs > 3) { - SmtpLogger.warn(`Potential DoS detected: ${highVolumeIPs} IPs with high connection rates`); - } - - // Assess memory usage trends - if (memoryUsageMB.heapUsed > 500) { // Over 500MB heap used - SmtpLogger.warn(`High memory usage detected: ${memoryUsageMB.heapUsed}MB heap used`); - } - - // Clean up expired IP rate limits and validate resource tracking - this.cleanupIpRateLimits(); - } - - /** - * Clean up expired IP rate limits and perform additional resource monitoring - */ - private cleanupIpRateLimits(): void { - const now = Date.now(); - const windowThreshold = now - this.options.connectionRateWindow; - let activeIps = 0; - let removedEntries = 0; - - // Iterate through IP connections and manage entries - for (const [ip, data] of this.ipConnections.entries()) { - // If the last connection was before the window threshold + one extra window, remove the entry - if (data.lastConnection < windowThreshold - this.options.connectionRateWindow) { - // Remove stale entries to prevent memory growth - this.ipConnections.delete(ip); - removedEntries++; - } - // If last connection was before the window threshold, reset the count - else if (data.lastConnection < windowThreshold) { - if (data.count > 0) { - // Reset but keep the IP in the map with a zero count - this.ipConnections.set(ip, { - count: 0, - firstConnection: now, - lastConnection: now - }); - } - } else { - // This IP is still active within the current window - activeIps++; - } - } - - // Log cleanup activity if significant changes occurred - if (removedEntries > 0) { - SmtpLogger.debug(`IP rate limit cleanup: removed ${removedEntries} stale entries, ${this.ipConnections.size} remaining, ${activeIps} active in current window`); - } - - // Check for memory leaks in connection tracking - if (this.activeConnections.size > 0 && this.connectionStats.activeConnections !== this.activeConnections.size) { - SmtpLogger.warn(`Connection tracking inconsistency detected: stats.active=${this.connectionStats.activeConnections}, actual=${this.activeConnections.size}`); - // Fix the inconsistency - this.connectionStats.activeConnections = this.activeConnections.size; - } - - // Validate and clean leaked resources if needed - this.validateResourceTracking(); - } - - /** - * Validate and repair resource tracking to prevent leaks - */ - private validateResourceTracking(): void { - // Prepare a detailed report if inconsistencies are found - const inconsistenciesFound = []; - - // 1. Check active connections count matches activeConnections set size - if (this.connectionStats.activeConnections !== this.activeConnections.size) { - inconsistenciesFound.push({ - issue: 'Active connection count mismatch', - stats: this.connectionStats.activeConnections, - actual: this.activeConnections.size, - action: 'Auto-corrected' - }); - this.connectionStats.activeConnections = this.activeConnections.size; - } - - // 2. Check for destroyed sockets in active connections - let destroyedSocketsCount = 0; - const socketsToRemove: Array = []; - - for (const socket of this.activeConnections) { - if (socket.destroyed) { - destroyedSocketsCount++; - socketsToRemove.push(socket); - } - } - - // Remove destroyed sockets from tracking - for (const socket of socketsToRemove) { - this.activeConnections.delete(socket); - // Also ensure all listeners are removed - try { - socket.removeAllListeners(); - } catch { - // Ignore errors from removeAllListeners - } - } - - if (destroyedSocketsCount > 0) { - inconsistenciesFound.push({ - issue: 'Destroyed sockets in active list', - count: destroyedSocketsCount, - action: 'Removed from tracking' - }); - // Update active connections count after cleanup - this.connectionStats.activeConnections = this.activeConnections.size; - } - - // 3. Check for sessions without corresponding active connections - const sessionCount = this.smtpServer.getSessionManager().getSessionCount(); - if (sessionCount > this.activeConnections.size) { - inconsistenciesFound.push({ - issue: 'Orphaned sessions', - sessions: sessionCount, - connections: this.activeConnections.size, - action: 'Session cleanup recommended' - }); - } - - // If any inconsistencies found, log a detailed report - if (inconsistenciesFound.length > 0) { - SmtpLogger.warn('Resource tracking inconsistencies detected and repaired', { inconsistencies: inconsistenciesFound }); - } - } - - /** - * Handle a new connection with resource management - * @param socket - Client socket - */ - public async handleNewConnection(socket: plugins.net.Socket): Promise { - // Update connection stats - this.connectionStats.totalConnections++; - this.connectionStats.activeConnections = this.activeConnections.size + 1; - - if (this.connectionStats.activeConnections > this.connectionStats.peakConnections) { - this.connectionStats.peakConnections = this.connectionStats.activeConnections; - } - - // Get client IP - const remoteAddress = socket.remoteAddress || '0.0.0.0'; - - // Use UnifiedRateLimiter for connection rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - - // Check connection limit with UnifiedRateLimiter - const connectionResult = rateLimiter.recordConnection(remoteAddress); - if (!connectionResult.allowed) { - this.rejectConnection(socket, connectionResult.reason || 'Rate limit exceeded'); - this.connectionStats.rejectedConnections++; - return; - } - - // Still track IP connections locally for cleanup purposes - this.trackIPConnection(remoteAddress); - - // Check if maximum global connections reached - if (this.hasReachedMaxConnections()) { - this.rejectConnection(socket, 'Too many connections'); - this.connectionStats.rejectedConnections++; - return; - } - - // Add socket to active connections - this.activeConnections.add(socket); - - // Set up socket options - socket.setKeepAlive(true); - socket.setTimeout(this.options.socketTimeout); - - // Explicitly set socket buffer sizes to prevent memory issues - socket.setNoDelay(true); // Disable Nagle's algorithm for better responsiveness - - // Set limits on socket buffer size if supported by Node.js version - try { - // Here we set reasonable buffer limits to prevent memory exhaustion attacks - const highWaterMark = 64 * 1024; // 64 KB - // Note: Socket high water mark methods can't be set directly in newer Node.js versions - // These would need to be set during socket creation or with a different API - } catch (error) { - // Ignore errors from older Node.js versions that don't support these methods - SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`); - } - - // Set up event handlers - this.setupSocketEventHandlers(socket); - - // Create a session for this connection - this.smtpServer.getSessionManager().createSession(socket, false); - - // Log the new connection using adaptive logger - const socketDetails = getSocketDetails(socket); - adaptiveLogger.logConnection(socket, 'connect'); - - // Update adaptive logger with current connection count - adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections); - - // Send greeting - this.sendGreeting(socket); - } - - /** - * Check if an IP has exceeded the rate limit - * @param ip - Client IP address - * @returns True if rate limited - */ - private isIPRateLimited(ip: string): boolean { - const now = Date.now(); - const ipData = this.ipConnections.get(ip); - - if (!ipData) { - return false; // No previous connections - } - - // Check if we're within the rate window - const isWithinWindow = now - ipData.firstConnection <= this.options.connectionRateWindow; - - // If within window and count exceeds limit, rate limit is applied - if (isWithinWindow && ipData.count >= this.options.connectionRateLimit) { - SmtpLogger.warn(`Rate limit exceeded for IP ${ip}: ${ipData.count} connections in ${Math.round((now - ipData.firstConnection) / 1000)}s`); - return true; - } - - return false; - } - - /** - * Track a new connection from an IP - * @param ip - Client IP address - */ - private trackIPConnection(ip: string): void { - const now = Date.now(); - const ipData = this.ipConnections.get(ip); - - if (!ipData) { - // First connection from this IP - this.ipConnections.set(ip, { - count: 1, - firstConnection: now, - lastConnection: now - }); - } else { - // Check if we need to reset the window - if (now - ipData.lastConnection > this.options.connectionRateWindow) { - // Reset the window - this.ipConnections.set(ip, { - count: 1, - firstConnection: now, - lastConnection: now - }); - } else { - // Increment within the current window - this.ipConnections.set(ip, { - count: ipData.count + 1, - firstConnection: ipData.firstConnection, - lastConnection: now - }); - } - } - } - - /** - * Check if an IP has reached its connection limit - * @param ip - Client IP address - * @returns True if limit reached - */ - private hasReachedIPConnectionLimit(ip: string): boolean { - let ipConnectionCount = 0; - - // Count active connections from this IP - for (const socket of this.activeConnections) { - if (socket.remoteAddress === ip) { - ipConnectionCount++; - } - } - - return ipConnectionCount >= this.options.maxConnectionsPerIP; - } - - /** - * Handle a new secure TLS connection with resource management - * @param socket - Client TLS socket - */ - public async handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise { - // Update connection stats - this.connectionStats.totalConnections++; - this.connectionStats.activeConnections = this.activeConnections.size + 1; - - if (this.connectionStats.activeConnections > this.connectionStats.peakConnections) { - this.connectionStats.peakConnections = this.connectionStats.activeConnections; - } - - // Get client IP - const remoteAddress = socket.remoteAddress || '0.0.0.0'; - - // Use UnifiedRateLimiter for connection rate limiting - const emailServer = this.smtpServer.getEmailServer(); - const rateLimiter = emailServer.getRateLimiter(); - - // Check connection limit with UnifiedRateLimiter - const connectionResult = rateLimiter.recordConnection(remoteAddress); - if (!connectionResult.allowed) { - this.rejectConnection(socket, connectionResult.reason || 'Rate limit exceeded'); - this.connectionStats.rejectedConnections++; - return; - } - - // Still track IP connections locally for cleanup purposes - this.trackIPConnection(remoteAddress); - - // Check if maximum global connections reached - if (this.hasReachedMaxConnections()) { - this.rejectConnection(socket, 'Too many connections'); - this.connectionStats.rejectedConnections++; - return; - } - - // Add socket to active connections - this.activeConnections.add(socket); - - // Set up socket options - socket.setKeepAlive(true); - socket.setTimeout(this.options.socketTimeout); - - // Explicitly set socket buffer sizes to prevent memory issues - socket.setNoDelay(true); // Disable Nagle's algorithm for better responsiveness - - // Set limits on socket buffer size if supported by Node.js version - try { - // Here we set reasonable buffer limits to prevent memory exhaustion attacks - const highWaterMark = 64 * 1024; // 64 KB - // Note: Socket high water mark methods can't be set directly in newer Node.js versions - // These would need to be set during socket creation or with a different API - } catch (error) { - // Ignore errors from older Node.js versions that don't support these methods - SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`); - } - - // Set up event handlers - this.setupSocketEventHandlers(socket); - - // Create a session for this connection - this.smtpServer.getSessionManager().createSession(socket, true); - - // Log the new secure connection using adaptive logger - adaptiveLogger.logConnection(socket, 'connect'); - - // Update adaptive logger with current connection count - adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections); - - // Send greeting - this.sendGreeting(socket); - } - - /** - * Set up event handlers for a socket with enhanced resource management - * @param socket - Client socket - */ - public setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - // Store existing socket event handlers before adding new ones - const existingDataHandler = socket.listeners('data')[0] as (...args: any[]) => void; - const existingCloseHandler = socket.listeners('close')[0] as (...args: any[]) => void; - const existingErrorHandler = socket.listeners('error')[0] as (...args: any[]) => void; - const existingTimeoutHandler = socket.listeners('timeout')[0] as (...args: any[]) => void; - - // Remove existing event handlers if they exist - if (existingDataHandler) socket.removeListener('data', existingDataHandler); - if (existingCloseHandler) socket.removeListener('close', existingCloseHandler); - if (existingErrorHandler) socket.removeListener('error', existingErrorHandler); - if (existingTimeoutHandler) socket.removeListener('timeout', existingTimeoutHandler); - - // Data event - process incoming data from the client with resource limits - let buffer = ''; - let totalBytesReceived = 0; - - socket.on('data', async (data) => { - try { - // Get current session and update activity timestamp - const session = this.smtpServer.getSessionManager().getSession(socket); - if (session) { - this.smtpServer.getSessionManager().updateSessionActivity(session); - } - - // Check if we're in DATA receiving mode - handle differently - if (session && session.state === SmtpState.DATA_RECEIVING) { - // In DATA mode, pass raw chunks directly to command handler with special marker - // Don't line-buffer large email content - try { - const dataString = data.toString('utf8'); - // Use a special prefix to indicate this is raw data, not a command line - // CRITICAL FIX: Must await to prevent async pile-up - await this.smtpServer.getCommandHandler().processCommand(socket, `__RAW_DATA__${dataString}`); - return; - } catch (dataError) { - SmtpLogger.error(`Data handler error during DATA mode: ${dataError instanceof Error ? dataError.message : String(dataError)}`); - socket.destroy(); - return; - } - } - - // For command mode, continue with line-buffered processing - // Check buffer size limits to prevent memory attacks - totalBytesReceived += data.length; - - if (buffer.length > this.options.bufferSizeLimit) { - // Buffer is too large, reject the connection - SmtpLogger.warn(`Buffer size limit exceeded: ${buffer.length} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too large, disconnecting`); - socket.destroy(); - return; - } - - // Impose a total transfer limit to prevent DoS - if (totalBytesReceived > this.options.bufferSizeLimit * 2) { - SmtpLogger.warn(`Total transfer limit exceeded: ${totalBytesReceived} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Transfer limit exceeded, disconnecting`); - socket.destroy(); - return; - } - - // Convert buffer to string safely with explicit encoding - const dataString = data.toString('utf8'); - - // Buffer incoming data - buffer += dataString; - - // Process complete lines - let lineEndPos; - while ((lineEndPos = buffer.indexOf(SMTP_DEFAULTS.CRLF)) !== -1) { - // Extract a complete line - const line = buffer.substring(0, lineEndPos); - buffer = buffer.substring(lineEndPos + 2); // +2 to skip CRLF - - // Check line length to prevent extremely long lines - if (line.length > 4096) { // 4KB line limit is reasonable for SMTP - SmtpLogger.warn(`Line length limit exceeded: ${line.length} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Line too long, disconnecting`); - socket.destroy(); - return; - } - - // Process non-empty lines - if (line.length > 0) { - try { - // CRITICAL FIX: Must await processCommand to prevent async pile-up - // This was causing the busy loop with high CPU usage when many empty lines were processed - await this.smtpServer.getCommandHandler().processCommand(socket, line); - } catch (error) { - // Handle any errors in command processing - SmtpLogger.error(`Command handler error: ${error instanceof Error ? error.message : String(error)}`); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error`); - - // If there's a severe error, close the connection - if (error instanceof Error && - (error.message.includes('fatal') || error.message.includes('critical'))) { - socket.destroy(); - return; - } - } - } - } - - // If buffer is getting too large without CRLF, it might be a DoS attempt - if (buffer.length > 10240) { // 10KB is a reasonable limit for a line without CRLF - SmtpLogger.warn(`Incomplete line too large: ${buffer.length} bytes for ${socket.remoteAddress}`); - this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Incomplete line too large, disconnecting`); - socket.destroy(); - } - } catch (error) { - // Handle any unexpected errors during data processing - SmtpLogger.error(`Data handler error: ${error instanceof Error ? error.message : String(error)}`); - socket.destroy(); - } - }); - - // Add drain event handler to manage flow control - socket.on('drain', () => { - // Socket buffer has been emptied, resume data flow if needed - if (socket.isPaused()) { - socket.resume(); - SmtpLogger.debug(`Resumed socket for ${socket.remoteAddress} after drain`); - } - }); - - // Close event - clean up when connection is closed - socket.on('close', (hadError) => { - this.handleSocketClose(socket, hadError); - }); - - // Error event - handle socket errors - socket.on('error', (err) => { - this.handleSocketError(socket, err); - }); - - // Timeout event - handle socket timeouts - socket.on('timeout', () => { - this.handleSocketTimeout(socket); - }); - } - - /** - * Get the current connection count - * @returns Number of active connections - */ - public getConnectionCount(): number { - return this.activeConnections.size; - } - - /** - * Check if the server has reached the maximum number of connections - * @returns True if max connections reached - */ - public hasReachedMaxConnections(): boolean { - return this.activeConnections.size >= this.options.maxConnections; - } - - /** - * Close all active connections - */ - public closeAllConnections(): void { - const connectionCount = this.activeConnections.size; - if (connectionCount === 0) { - return; - } - - SmtpLogger.info(`Closing all connections (count: ${connectionCount})`); - - for (const socket of this.activeConnections) { - try { - // Send service closing notification - this.sendServiceClosing(socket); - - // End the socket gracefully - socket.end(); - - // Force destroy after a short delay if not already destroyed - const destroyTimer = setTimeout(() => { - if (!socket.destroyed) { - socket.destroy(); - } - this.cleanupTimers.delete(destroyTimer); - }, 100); - this.cleanupTimers.add(destroyTimer); - } catch (error) { - SmtpLogger.error(`Error closing connection: ${error instanceof Error ? error.message : String(error)}`); - // Force destroy on error - try { - socket.destroy(); - } catch (e) { - // Ignore destroy errors - } - } - } - - // Clear active connections - this.activeConnections.clear(); - - // Stop resource monitoring to prevent hanging timers - if (this.resourceCheckInterval) { - clearInterval(this.resourceCheckInterval); - this.resourceCheckInterval = null; - } - } - - /** - * Handle socket close event - * @param socket - Client socket - * @param hadError - Whether the socket was closed due to error - */ - private handleSocketClose(socket: plugins.net.Socket | plugins.tls.TLSSocket, hadError: boolean): void { - try { - // Update connection statistics - this.connectionStats.closedConnections++; - this.connectionStats.activeConnections = this.activeConnections.size - 1; - - // Get socket details for logging - const socketDetails = getSocketDetails(socket); - const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; - - // Log with appropriate level based on whether there was an error - if (hadError) { - SmtpLogger.warn(`Socket closed with error: ${socketId}`); - } else { - SmtpLogger.debug(`Socket closed normally: ${socketId}`); - } - - // Get the session before removing it - const session = this.smtpServer.getSessionManager().getSession(socket); - - // Remove from active connections - this.activeConnections.delete(socket); - - // Remove from session manager - this.smtpServer.getSessionManager().removeSession(socket); - - // Cancel any timeout ID stored in the session - if (session?.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - - // Remove all event listeners to prevent memory leaks - socket.removeAllListeners(); - - // Log connection close with session details if available - adaptiveLogger.logConnection(socket, 'close', session); - - // Update adaptive logger with new connection count - adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections); - } catch (error) { - // Handle any unexpected errors during cleanup - SmtpLogger.error(`Error in handleSocketClose: ${error instanceof Error ? error.message : String(error)}`); - - // Ensure socket is removed from active connections even if an error occurs - this.activeConnections.delete(socket); - - // Always try to remove all listeners even on error - try { - socket.removeAllListeners(); - } catch { - // Ignore errors from removeAllListeners - } - } - } - - /** - * Handle socket error event - * @param socket - Client socket - * @param error - Error object - */ - private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: Error): void { - try { - // Update connection statistics - this.connectionStats.erroredConnections++; - - // Get socket details for context - const socketDetails = getSocketDetails(socket); - const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; - - // Get the session - const session = this.smtpServer.getSessionManager().getSession(socket); - - // Detailed error logging with context information - SmtpLogger.error(`Socket error for ${socketId}: ${error.message}`, { - errorCode: (error as any).code, - errorStack: error.stack, - sessionId: session?.id, - sessionState: session?.state, - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - - // Log the error for connection tracking using adaptive logger - adaptiveLogger.logConnection(socket, 'error', session, error); - - // Cancel any timeout ID stored in the session - if (session?.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - - // Close the socket if not already closed - if (!socket.destroyed) { - socket.destroy(); - } - - // Remove from active connections (cleanup after error) - this.activeConnections.delete(socket); - - // Remove from session manager - this.smtpServer.getSessionManager().removeSession(socket); - } catch (handlerError) { - // Meta-error handling (errors in the error handler) - SmtpLogger.error(`Error in handleSocketError: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`); - - // Ensure socket is destroyed and removed from active connections - if (!socket.destroyed) { - socket.destroy(); - } - this.activeConnections.delete(socket); - } - } - - /** - * Handle socket timeout event - * @param socket - Client socket - */ - private handleSocketTimeout(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - try { - // Update connection statistics - this.connectionStats.timedOutConnections++; - - // Get socket details for context - const socketDetails = getSocketDetails(socket); - const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; - - // Get the session - const session = this.smtpServer.getSessionManager().getSession(socket); - - // Get timing information for better debugging - const now = Date.now(); - const idleTime = session?.lastActivity ? now - session.lastActivity : 'unknown'; - - if (session) { - // Log the timeout with extended details - SmtpLogger.warn(`Socket timeout from ${session.remoteAddress}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - state: session.state, - timeout: this.options.socketTimeout, - idleTime: idleTime, - emailState: session.envelope?.mailFrom ? 'has-sender' : 'no-sender', - recipientCount: session.envelope?.rcptTo?.length || 0 - }); - - // Cancel any timeout ID stored in the session - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - - // Send timeout notification to client - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} Connection timeout - closing connection`); - } else { - // Log timeout without session context - SmtpLogger.warn(`Socket timeout without session from ${socketId}`); - } - - // Close the socket gracefully - try { - socket.end(); - - // Set a forced close timeout in case socket.end() doesn't close the connection - const timeoutDestroyTimer = setTimeout(() => { - if (!socket.destroyed) { - SmtpLogger.warn(`Forcing destroy of timed out socket: ${socketId}`); - socket.destroy(); - } - this.cleanupTimers.delete(timeoutDestroyTimer); - }, 5000); // 5 second grace period for socket to end properly - this.cleanupTimers.add(timeoutDestroyTimer); - } catch (error) { - SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`); - - // Ensure socket is destroyed even if end() fails - if (!socket.destroyed) { - socket.destroy(); - } - } - - // Clean up resources - this.activeConnections.delete(socket); - this.smtpServer.getSessionManager().removeSession(socket); - } catch (handlerError) { - // Handle any unexpected errors during timeout handling - SmtpLogger.error(`Error in handleSocketTimeout: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`); - - // Ensure socket is destroyed and removed from tracking - if (!socket.destroyed) { - socket.destroy(); - } - this.activeConnections.delete(socket); - } - } - - /** - * Reject a connection - * @param socket - Client socket - * @param reason - Reason for rejection - */ - private rejectConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, reason: string): void { - // Log the rejection - const socketDetails = getSocketDetails(socket); - SmtpLogger.warn(`Connection rejected from ${socketDetails.remoteAddress}:${socketDetails.remotePort}: ${reason}`); - - // Send rejection message - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} ${this.options.hostname} Service temporarily unavailable - ${reason}`); - - // Close the socket - try { - socket.end(); - } catch (error) { - SmtpLogger.error(`Error ending rejected socket: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Send greeting message - * @param socket - Client socket - */ - private sendGreeting(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - const greeting = `${SmtpResponseCode.SERVICE_READY} ${this.options.hostname} ESMTP service ready`; - this.sendResponse(socket, greeting); - } - - /** - * Send service closing notification - * @param socket - Client socket - */ - private sendServiceClosing(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - const message = `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`; - this.sendResponse(socket, message); - } - - /** - * Send response to client - * @param socket - Client socket - * @param response - Response to send - */ - private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - adaptiveLogger.logResponse(response, socket); - } catch (error) { - // Log error and destroy socket - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - - socket.destroy(); - } - } - - /** - * Handle a new connection (interface requirement) - */ - public async handleConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): Promise { - if (secure) { - this.handleNewSecureConnection(socket as plugins.tls.TLSSocket); - } else { - this.handleNewConnection(socket as plugins.net.Socket); - } - } - - /** - * Check if accepting new connections (interface requirement) - */ - public canAcceptConnection(): boolean { - return !this.hasReachedMaxConnections(); - } - - /** - * Clean up resources - */ - public destroy(): void { - // Clear resource monitoring interval - if (this.resourceCheckInterval) { - clearInterval(this.resourceCheckInterval); - this.resourceCheckInterval = null; - } - - // Clear all cleanup timers - for (const timer of this.cleanupTimers) { - clearTimeout(timer); - } - this.cleanupTimers.clear(); - - // Close all active connections - this.closeAllConnections(); - - // Clear maps - this.activeConnections.clear(); - this.ipConnections.clear(); - - // Reset connection stats - this.connectionStats = { - totalConnections: 0, - activeConnections: 0, - peakConnections: 0, - rejectedConnections: 0, - closedConnections: 0, - erroredConnections: 0, - timedOutConnections: 0 - }; - - SmtpLogger.debug('ConnectionManager destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/constants.ts b/ts/mail/delivery/smtpserver/constants.ts deleted file mode 100644 index a11b98d..0000000 --- a/ts/mail/delivery/smtpserver/constants.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * SMTP Server Constants - * This file contains all constants and enums used by the SMTP server - */ - -import { SmtpState } from '../interfaces.js'; - -// Re-export SmtpState enum from the main interfaces file -export { SmtpState }; - -/** - * SMTP Response Codes - * Based on RFC 5321 and common SMTP practice - */ -export enum SmtpResponseCode { - // Success codes (2xx) - SUCCESS = 250, // Requested mail action okay, completed - SYSTEM_STATUS = 211, // System status, or system help reply - HELP_MESSAGE = 214, // Help message - SERVICE_READY = 220, // Service ready - SERVICE_CLOSING = 221, // Service closing transmission channel - AUTHENTICATION_SUCCESSFUL = 235, // Authentication successful - OK = 250, // Requested mail action okay, completed - FORWARD = 251, // User not local; will forward to - CANNOT_VRFY = 252, // Cannot VRFY user, but will accept message and attempt delivery - - // Intermediate codes (3xx) - MORE_INFO_NEEDED = 334, // Server challenge for authentication - START_MAIL_INPUT = 354, // Start mail input; end with . - - // Temporary error codes (4xx) - SERVICE_NOT_AVAILABLE = 421, // Service not available, closing transmission channel - MAILBOX_TEMPORARILY_UNAVAILABLE = 450, // Requested mail action not taken: mailbox unavailable - LOCAL_ERROR = 451, // Requested action aborted: local error in processing - INSUFFICIENT_STORAGE = 452, // Requested action not taken: insufficient system storage - TLS_UNAVAILABLE_TEMP = 454, // TLS not available due to temporary reason - - // Permanent error codes (5xx) - SYNTAX_ERROR = 500, // Syntax error, command unrecognized - SYNTAX_ERROR_PARAMETERS = 501, // Syntax error in parameters or arguments - COMMAND_NOT_IMPLEMENTED = 502, // Command not implemented - BAD_SEQUENCE = 503, // Bad sequence of commands - COMMAND_PARAMETER_NOT_IMPLEMENTED = 504, // Command parameter not implemented - AUTH_REQUIRED = 530, // Authentication required - AUTH_FAILED = 535, // Authentication credentials invalid - MAILBOX_UNAVAILABLE = 550, // Requested action not taken: mailbox unavailable - USER_NOT_LOCAL = 551, // User not local; please try - EXCEEDED_STORAGE = 552, // Requested mail action aborted: exceeded storage allocation - MAILBOX_NAME_INVALID = 553, // Requested action not taken: mailbox name not allowed - TRANSACTION_FAILED = 554, // Transaction failed - MAIL_RCPT_PARAMETERS_INVALID = 555, // MAIL FROM/RCPT TO parameters not recognized or not implemented -} - -/** - * SMTP Command Types - */ -export enum SmtpCommand { - HELO = 'HELO', - EHLO = 'EHLO', - MAIL_FROM = 'MAIL', - RCPT_TO = 'RCPT', - DATA = 'DATA', - RSET = 'RSET', - NOOP = 'NOOP', - QUIT = 'QUIT', - STARTTLS = 'STARTTLS', - AUTH = 'AUTH', - HELP = 'HELP', - VRFY = 'VRFY', - EXPN = 'EXPN', -} - -/** - * Security log event types - */ -export enum SecurityEventType { - CONNECTION = 'connection', - AUTHENTICATION = 'authentication', - COMMAND = 'command', - DATA = 'data', - IP_REPUTATION = 'ip_reputation', - TLS_NEGOTIATION = 'tls_negotiation', - DKIM = 'dkim', - SPF = 'spf', - DMARC = 'dmarc', - EMAIL_VALIDATION = 'email_validation', - SPAM = 'spam', - ACCESS_CONTROL = 'access_control', -} - -/** - * Security log levels - */ -export enum SecurityLogLevel { - DEBUG = 'debug', - INFO = 'info', - WARN = 'warn', - ERROR = 'error', -} - -/** - * SMTP Server Defaults - */ -export const SMTP_DEFAULTS = { - // Default timeouts in milliseconds - CONNECTION_TIMEOUT: 30000, // 30 seconds - SOCKET_TIMEOUT: 300000, // 5 minutes - DATA_TIMEOUT: 60000, // 1 minute - CLEANUP_INTERVAL: 5000, // 5 seconds - - // Default limits - MAX_CONNECTIONS: 100, - MAX_RECIPIENTS: 100, - MAX_MESSAGE_SIZE: 10485760, // 10MB - - // Default ports - SMTP_PORT: 25, - SUBMISSION_PORT: 587, - SECURE_PORT: 465, - - // Default hostname - HOSTNAME: 'mail.lossless.one', - - // CRLF line ending required by SMTP protocol - CRLF: '\r\n', -}; - -/** - * SMTP Command Patterns - * Regular expressions for parsing SMTP commands - */ -export const SMTP_PATTERNS = { - // Match EHLO/HELO command: "EHLO example.com" - // Made very permissive to handle various client implementations - EHLO: /^(?:EHLO|HELO)\s+(.+)$/i, - - // Match MAIL FROM command: "MAIL FROM: [PARAM=VALUE]" - // Made more permissive with whitespace and parameter formats - MAIL_FROM: /^MAIL\s+FROM\s*:\s*<([^>]*)>((?:\s+[a-zA-Z0-9][a-zA-Z0-9\-]*(?:=[^\s]+)?)*)$/i, - - // Match RCPT TO command: "RCPT TO: [PARAM=VALUE]" - // Made more permissive with whitespace and parameter formats - RCPT_TO: /^RCPT\s+TO\s*:\s*<([^>]*)>((?:\s+[a-zA-Z0-9][a-zA-Z0-9\-]*(?:=[^\s]+)?)*)$/i, - - // Match parameter format: "PARAM=VALUE" - PARAM: /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g, - - // Match email address format - basic validation - // This pattern rejects common invalid formats while being permissive for edge cases - // Checks: no spaces, has @, has domain with dot, no double dots, proper domain format - EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - - // Match end of DATA marker: \r\n.\r\n or just .\r\n at the start of a line (to handle various client implementations) - END_DATA: /(\r\n\.\r\n$)|(\n\.\r\n$)|(\r\n\.\n$)|(\n\.\n$)|^\.(\r\n|\n)$/, -}; - -/** - * SMTP Extension List - * These extensions are advertised in the EHLO response - */ -export const SMTP_EXTENSIONS = { - // Basic extensions (RFC 1869) - PIPELINING: 'PIPELINING', - SIZE: 'SIZE', - EIGHTBITMIME: '8BITMIME', - - // Security extensions - STARTTLS: 'STARTTLS', - AUTH: 'AUTH', - - // Additional extensions - ENHANCEDSTATUSCODES: 'ENHANCEDSTATUSCODES', - HELP: 'HELP', - CHUNKING: 'CHUNKING', - DSN: 'DSN', - - // Format an extension with a parameter - formatExtension(name: string, parameter?: string | number): string { - return parameter !== undefined ? `${name} ${parameter}` : name; - } -}; \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/create-server.ts b/ts/mail/delivery/smtpserver/create-server.ts deleted file mode 100644 index 0f56764..0000000 --- a/ts/mail/delivery/smtpserver/create-server.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * SMTP Server Creation Factory - * Provides a simple way to create a complete SMTP server - */ - -import { SmtpServer } from './smtp-server.js'; -import { SessionManager } from './session-manager.js'; -import { ConnectionManager } from './connection-manager.js'; -import { CommandHandler } from './command-handler.js'; -import { DataHandler } from './data-handler.js'; -import { TlsHandler } from './tls-handler.js'; -import { SecurityHandler } from './security-handler.js'; -import type { ISmtpServerOptions } from './interfaces.js'; -import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; - -/** - * Create a complete SMTP server with all components - * @param emailServer - Email server reference - * @param options - SMTP server options - * @returns Configured SMTP server instance - */ -export function createSmtpServer(emailServer: UnifiedEmailServer, options: ISmtpServerOptions): SmtpServer { - // First create the SMTP server instance - const smtpServer = new SmtpServer({ - emailServer, - options - }); - - // Return the configured server - return smtpServer; -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/data-handler.ts b/ts/mail/delivery/smtpserver/data-handler.ts deleted file mode 100644 index 2e5368e..0000000 --- a/ts/mail/delivery/smtpserver/data-handler.ts +++ /dev/null @@ -1,1283 +0,0 @@ -/** - * SMTP Data Handler - * Responsible for processing email data during and after DATA command - */ - -import * as plugins from '../../../plugins.js'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SmtpState } from './interfaces.js'; -import type { ISmtpSession, ISmtpTransactionResult } from './interfaces.js'; -import type { IDataHandler, ISmtpServer } from './interfaces.js'; -import { SmtpResponseCode, SMTP_PATTERNS, SMTP_DEFAULTS } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { detectHeaderInjection } from './utils/validation.js'; -import { Email } from '../../core/classes.email.js'; - -/** - * Handles SMTP DATA command and email data processing - */ -export class DataHandler implements IDataHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer: ISmtpServer; - - /** - * Creates a new data handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer) { - this.smtpServer = smtpServer; - } - - /** - * Process incoming email data - * @param socket - Client socket - * @param data - Data chunk - * @returns Promise that resolves when the data is processed - */ - public async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Clear any existing timeout and set a new one - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - } - - session.dataTimeoutId = setTimeout(() => { - if (session.state === SmtpState.DATA_RECEIVING) { - SmtpLogger.warn(`DATA timeout for session ${session.id}`, { sessionId: session.id }); - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`); - this.resetSession(session); - } - }, SMTP_DEFAULTS.DATA_TIMEOUT); - - // Update activity timestamp - this.smtpServer.getSessionManager().updateSessionActivity(session); - - // Store data in chunks for better memory efficiency - if (!session.emailDataChunks) { - session.emailDataChunks = []; - session.emailDataSize = 0; // Track size incrementally - } - - session.emailDataChunks.push(data); - session.emailDataSize = (session.emailDataSize || 0) + data.length; - - // Check if we've reached the max size (using incremental tracking) - const options = this.smtpServer.getOptions(); - const maxSize = options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE; - if (session.emailDataSize > maxSize) { - SmtpLogger.warn(`Message size exceeds limit for session ${session.id}`, { - sessionId: session.id, - size: session.emailDataSize, - limit: maxSize - }); - - this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too big, size limit is ${maxSize} bytes`); - this.resetSession(session); - return; - } - - // Check for end of data marker efficiently without combining all chunks - // Only check the current chunk and the last chunk for the marker - let hasEndMarker = false; - - // Check if current chunk contains end marker - if (data === '.\r\n' || data === '.') { - hasEndMarker = true; - } else { - // For efficiency with large messages, only check the last few chunks - // Get the last 2 chunks to check for split markers - const lastChunks = session.emailDataChunks.slice(-2).join(''); - - hasEndMarker = lastChunks.endsWith('\r\n.\r\n') || - lastChunks.endsWith('\n.\r\n') || - lastChunks.endsWith('\r\n.\n') || - lastChunks.endsWith('\n.\n'); - } - - if (hasEndMarker) { - - SmtpLogger.debug(`End of data marker found for session ${session.id}`, { sessionId: session.id }); - - // End of data marker found - await this.handleEndOfData(socket, session); - } - } - - /** - * Handle raw data chunks during DATA mode (optimized for large messages) - * @param socket - Client socket - * @param data - Raw data chunk - */ - public async handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise { - // Get the session - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); - return; - } - - // Special handling for ERR-02 test: detect MAIL FROM command during DATA mode - // This needs to work for both raw data chunks and line-based data - const trimmedData = data.trim(); - const looksLikeCommand = /^[A-Z]{4,}( |:)/i.test(trimmedData); - - if (looksLikeCommand && trimmedData.toUpperCase().startsWith('MAIL FROM')) { - // This is the command that ERR-02 test is expecting to fail with 503 - SmtpLogger.debug(`Received MAIL FROM command during DATA mode - responding with sequence error`); - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`); - return; - } - - // For all other data, process normally - return this.processEmailData(socket, data); - } - - /** - * Process email data chunks efficiently for large messages - * @param chunks - Array of email data chunks - * @returns Processed email data string - */ - private processEmailDataStreaming(chunks: string[]): string { - // For very large messages, use a more memory-efficient approach - const CHUNK_SIZE = 50; // Process 50 chunks at a time - let result = ''; - - // Process chunks in batches to reduce memory pressure - for (let batchStart = 0; batchStart < chunks.length; batchStart += CHUNK_SIZE) { - const batchEnd = Math.min(batchStart + CHUNK_SIZE, chunks.length); - const batchChunks = chunks.slice(batchStart, batchEnd); - - // Join this batch - let batchData = batchChunks.join(''); - - // Clear references to help GC - for (let i = 0; i < batchChunks.length; i++) { - batchChunks[i] = ''; - } - - result += batchData; - batchData = ''; // Clear reference - - // Force garbage collection hint (if available) - if (global.gc && batchStart % 200 === 0) { - global.gc(); - } - } - - // Remove trailing end-of-data marker: various formats - result = result - .replace(/\r\n\.\r\n$/, '') - .replace(/\n\.\r\n$/, '') - .replace(/\r\n\.\n$/, '') - .replace(/\n\.\n$/, '') - .replace(/^\.$/, ''); // Handle ONLY a lone dot as the entire content (not trailing dots) - - // Remove dot-stuffing (RFC 5321, section 4.5.2) - result = result.replace(/\r\n\.\./g, '\r\n.'); - - return result; - } - - /** - * Process a complete email - * @param rawData - Raw email data - * @param session - SMTP session - * @returns Promise that resolves with the Email object - */ - public async processEmail(rawData: string, session: ISmtpSession): Promise { - // Clean up the raw email data - let cleanedData = rawData; - - // Remove trailing end-of-data marker: various formats - cleanedData = cleanedData - .replace(/\r\n\.\r\n$/, '') - .replace(/\n\.\r\n$/, '') - .replace(/\r\n\.\n$/, '') - .replace(/\n\.\n$/, '') - .replace(/^\.$/, ''); // Handle ONLY a lone dot as the entire content (not trailing dots) - - // Remove dot-stuffing (RFC 5321, section 4.5.2) - cleanedData = cleanedData.replace(/\r\n\.\./g, '\r\n.'); - - try { - // Parse email into Email object using cleaned data - const email = await this.parseEmailFromData(cleanedData, session); - - // Return the parsed email - return email; - } catch (error) { - SmtpLogger.error(`Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Create a minimal email object on error - const fallbackEmail = new Email({ - from: 'unknown@localhost', - to: 'unknown@localhost', - subject: 'Parse Error', - text: cleanedData - }); - return fallbackEmail; - } - } - - /** - * Parse email from raw data - * @param rawData - Raw email data - * @param session - SMTP session - * @returns Email object - */ - private async parseEmailFromData(rawData: string, session: ISmtpSession): Promise { - // Parse the raw email data to extract headers and body - const lines = rawData.split('\r\n'); - let headerEnd = -1; - - // Find where headers end - for (let i = 0; i < lines.length; i++) { - if (lines[i].trim() === '') { - headerEnd = i; - break; - } - } - - // Extract headers - let subject = 'No Subject'; - const headers: Record = {}; - - if (headerEnd > -1) { - for (let i = 0; i < headerEnd; i++) { - const line = lines[i]; - const colonIndex = line.indexOf(':'); - if (colonIndex > 0) { - const headerName = line.substring(0, colonIndex).trim().toLowerCase(); - const headerValue = line.substring(colonIndex + 1).trim(); - - if (headerName === 'subject') { - subject = headerValue; - } else { - headers[headerName] = headerValue; - } - } - } - } - - // Extract body - const body = headerEnd > -1 ? lines.slice(headerEnd + 1).join('\r\n') : rawData; - - // Create email with session information - const email = new Email({ - from: session.mailFrom || 'unknown@localhost', - to: session.rcptTo || ['unknown@localhost'], - subject, - text: body, - headers - }); - - return email; - } - - /** - * Process a complete email (legacy method) - * @param session - SMTP session - * @returns Promise that resolves with the result of the transaction - */ - public async processEmailLegacy(session: ISmtpSession): Promise { - try { - // Use the email data from session - const email = await this.parseEmailFromData(session.emailData || '', session); - - // Process the email based on the processing mode - const processingMode = session.processingMode || 'mta'; - - let result: ISmtpTransactionResult = { - success: false, - error: 'Email processing failed' - }; - - switch (processingMode) { - case 'mta': - // Process through the MTA system - try { - SmtpLogger.debug(`Processing email in MTA mode for session ${session.id}`, { - sessionId: session.id, - messageId: email.getMessageId() - }); - - // Generate a message ID since queueEmail is not available - const options = this.smtpServer.getOptions(); - const hostname = options.hostname || SMTP_DEFAULTS.HOSTNAME; - const messageId = `${Date.now()}-${Math.floor(Math.random() * 1000000)}@${hostname}`; - - // Process the email through the emailServer - try { - // Process the email via the UnifiedEmailServer - // Pass the email object, session data, and specify the mode (mta, forward, or process) - // This connects SMTP reception to the overall email system - const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session as any); - - SmtpLogger.info(`Email processed through UnifiedEmailServer: ${email.getMessageId()}`, { - sessionId: session.id, - messageId: email.getMessageId(), - recipients: email.to.join(', '), - success: true - }); - - result = { - success: true, - messageId, - email - }; - } catch (emailError) { - SmtpLogger.error(`Failed to process email through UnifiedEmailServer: ${emailError instanceof Error ? emailError.message : String(emailError)}`, { - sessionId: session.id, - error: emailError instanceof Error ? emailError : new Error(String(emailError)), - messageId - }); - - // Default to success for now to pass tests, but log the error - result = { - success: true, - messageId, - email - }; - } - } catch (error) { - SmtpLogger.error(`Failed to queue email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - - result = { - success: false, - error: `Failed to queue email: ${error instanceof Error ? error.message : String(error)}` - }; - } - break; - - case 'forward': - // Forward email to another server - SmtpLogger.debug(`Processing email in FORWARD mode for session ${session.id}`, { - sessionId: session.id, - messageId: email.getMessageId() - }); - - // Process the email via the UnifiedEmailServer in forward mode - try { - const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session as any); - - SmtpLogger.info(`Email forwarded through UnifiedEmailServer: ${email.getMessageId()}`, { - sessionId: session.id, - messageId: email.getMessageId(), - recipients: email.to.join(', '), - success: true - }); - - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } catch (forwardError) { - SmtpLogger.error(`Failed to forward email: ${forwardError instanceof Error ? forwardError.message : String(forwardError)}`, { - sessionId: session.id, - error: forwardError instanceof Error ? forwardError : new Error(String(forwardError)), - messageId: email.getMessageId() - }); - - // For testing, still return success - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } - break; - - case 'process': - // Process the email immediately - SmtpLogger.debug(`Processing email in PROCESS mode for session ${session.id}`, { - sessionId: session.id, - messageId: email.getMessageId() - }); - - // Process the email via the UnifiedEmailServer in process mode - try { - const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session as any); - - SmtpLogger.info(`Email processed directly through UnifiedEmailServer: ${email.getMessageId()}`, { - sessionId: session.id, - messageId: email.getMessageId(), - recipients: email.to.join(', '), - success: true - }); - - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } catch (processError) { - SmtpLogger.error(`Failed to process email directly: ${processError instanceof Error ? processError.message : String(processError)}`, { - sessionId: session.id, - error: processError instanceof Error ? processError : new Error(String(processError)), - messageId: email.getMessageId() - }); - - // For testing, still return success - result = { - success: true, - messageId: email.getMessageId(), - email - }; - } - break; - - default: - SmtpLogger.warn(`Unknown processing mode: ${processingMode}`, { sessionId: session.id }); - result = { - success: false, - error: `Unknown processing mode: ${processingMode}` - }; - } - - return result; - } catch (error) { - SmtpLogger.error(`Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - - return { - success: false, - error: `Failed to parse email: ${error instanceof Error ? error.message : String(error)}` - }; - } - } - - /** - * Save an email to disk - * @param session - SMTP session - */ - public saveEmail(session: ISmtpSession): void { - // Email saving to disk is currently disabled in the refactored architecture - // This functionality can be re-enabled by adding a tempDir option to ISmtpServerOptions - SmtpLogger.debug(`Email saving to disk is disabled`, { - sessionId: session.id - }); - } - - /** - * Parse an email into an Email object - * @param session - SMTP session - * @returns Promise that resolves with the parsed Email object - */ - public async parseEmail(session: ISmtpSession): Promise { - try { - // Store raw data for testing and debugging - const rawData = session.emailData; - - // Try to parse with mailparser for better MIME support - const parsed = await plugins.mailparser.simpleParser(rawData); - - // Extract headers - const headers: Record = {}; - - // Add all headers from the parsed email - if (parsed.headers) { - // Convert headers to a standard object format - for (const [key, value] of parsed.headers.entries()) { - if (typeof value === 'string') { - headers[key.toLowerCase()] = value; - } else if (Array.isArray(value)) { - headers[key.toLowerCase()] = value.join(', '); - } - } - } - - // Get message ID or generate one - const messageId = parsed.messageId || - headers['message-id'] || - `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.smtpServer.getOptions().hostname}>`; - - // Get From, To, and Subject from parsed email or envelope - const from = parsed.from?.value?.[0]?.address || - session.envelope.mailFrom.address; - - // Handle multiple recipients appropriately - let to: string[] = []; - - // Try to get recipients from parsed email - if (parsed.to) { - // Handle both array and single object cases - if (Array.isArray(parsed.to)) { - to = parsed.to.map(addr => typeof addr === 'object' && addr !== null && 'address' in addr ? String(addr.address) : ''); - } else if (typeof parsed.to === 'object' && parsed.to !== null) { - // Handle object with value property (array or single address object) - if ('value' in parsed.to && Array.isArray(parsed.to.value)) { - to = parsed.to.value.map(addr => typeof addr === 'object' && addr !== null && 'address' in addr ? String(addr.address) : ''); - } else if ('address' in parsed.to) { - to = [String(parsed.to.address)]; - } - } - - // Filter out empty strings - to = to.filter(Boolean); - } - - // If no recipients found, fall back to envelope - if (to.length === 0) { - to = session.envelope.rcptTo.map(r => r.address); - } - - // Handle subject with special care for character encoding -const subject = parsed.subject || headers['subject'] || 'No Subject'; -SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject }); - - // Create email object using the parsed content - const email = new Email({ - from: from, - to: to, - subject: subject, - text: parsed.text || '', - html: parsed.html || undefined, - // Include original envelope data as headers for accurate routing - headers: { - 'X-Original-Mail-From': session.envelope.mailFrom.address, - 'X-Original-Rcpt-To': session.envelope.rcptTo.map(r => r.address).join(', '), - 'Message-Id': messageId - } - }); - - // Add attachments if any - if (parsed.attachments && parsed.attachments.length > 0) { - SmtpLogger.debug(`Found ${parsed.attachments.length} attachments in email`, { - sessionId: session.id, - attachmentCount: parsed.attachments.length - }); - - for (const attachment of parsed.attachments) { - // Enhanced attachment logging for debugging - SmtpLogger.debug(`Processing attachment: ${attachment.filename}`, { - filename: attachment.filename, - contentType: attachment.contentType, - size: attachment.content?.length, - contentId: attachment.contentId || 'none', - contentDisposition: attachment.contentDisposition || 'none' - }); - - // Ensure we have valid content - if (!attachment.content || !Buffer.isBuffer(attachment.content)) { - SmtpLogger.warn(`Attachment ${attachment.filename} has invalid content, skipping`); - continue; - } - - // Fix up content type if missing but can be inferred from filename - let contentType = attachment.contentType || 'application/octet-stream'; - const filename = attachment.filename || 'attachment'; - - if (!contentType || contentType === 'application/octet-stream') { - if (filename.endsWith('.pdf')) { - contentType = 'application/pdf'; - } else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) { - contentType = 'image/jpeg'; - } else if (filename.endsWith('.png')) { - contentType = 'image/png'; - } else if (filename.endsWith('.gif')) { - contentType = 'image/gif'; - } else if (filename.endsWith('.txt')) { - contentType = 'text/plain'; - } - } - - email.attachments.push({ - filename: filename, - content: attachment.content, - contentType: contentType, - contentId: attachment.contentId - }); - - SmtpLogger.debug(`Added attachment to email: ${filename}, type: ${contentType}, size: ${attachment.content.length} bytes`); - } - } else { - SmtpLogger.debug(`No attachments found in email via parser`, { sessionId: session.id }); - - // Additional check for attachments that might be missed by the parser - // Look for Content-Disposition headers in the raw data - const rawData = session.emailData; - const hasAttachmentDisposition = rawData.includes('Content-Disposition: attachment'); - - if (hasAttachmentDisposition) { - SmtpLogger.debug(`Found potential attachments in raw data, will handle in multipart processing`, { - sessionId: session.id - }); - } - } - - // Add received header - const timestamp = new Date().toUTCString(); - const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.smtpServer.getOptions().hostname} with ESMTP id ${session.id}; ${timestamp}`; - email.addHeader('Received', receivedHeader); - - // Add all original headers - for (const [name, value] of Object.entries(headers)) { - if (!['from', 'to', 'subject', 'message-id'].includes(name)) { - email.addHeader(name, value); - } - } - - // Store raw data for testing and debugging - (email as any).rawData = rawData; - - SmtpLogger.debug(`Email parsed successfully: ${messageId}`, { - sessionId: session.id, - messageId, - hasHtml: !!parsed.html, - attachmentCount: parsed.attachments?.length || 0 - }); - - return email; - } catch (error) { - // If parsing fails, fall back to basic parsing - SmtpLogger.warn(`Advanced email parsing failed, falling back to basic parsing: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - - return this.parseEmailBasic(session); - } - } - - /** - * Basic fallback method for parsing emails - * @param session - SMTP session - * @returns The parsed Email object - */ - private parseEmailBasic(session: ISmtpSession): Email { - // Parse raw email text to extract headers - const rawData = session.emailData; - const headerEndIndex = rawData.indexOf('\r\n\r\n'); - - if (headerEndIndex === -1) { - // No headers/body separation, create basic email - const email = new Email({ - from: session.envelope.mailFrom.address, - to: session.envelope.rcptTo.map(r => r.address), - subject: 'Received via SMTP', - text: rawData - }); - - // Store raw data for testing - (email as any).rawData = rawData; - - return email; - } - - // Extract headers and body - const headersText = rawData.substring(0, headerEndIndex); - const bodyText = rawData.substring(headerEndIndex + 4); // Skip the \r\n\r\n separator - - // Parse headers with enhanced injection detection - const headers: Record = {}; - const headerLines = headersText.split('\r\n'); - let currentHeader = ''; - const criticalHeaders = new Set(); // Track critical headers for duplication detection - - for (const line of headerLines) { - // Check if this is a continuation of a previous header - if (line.startsWith(' ') || line.startsWith('\t')) { - if (currentHeader) { - headers[currentHeader] += ' ' + line.trim(); - } - continue; - } - - // This is a new header - const separatorIndex = line.indexOf(':'); - if (separatorIndex !== -1) { - const name = line.substring(0, separatorIndex).trim().toLowerCase(); - const value = line.substring(separatorIndex + 1).trim(); - - // Check for header injection attempts in header values - if (detectHeaderInjection(value, 'email-header')) { - SmtpLogger.warn('Header injection attempt detected in email header', { - headerName: name, - headerValue: value.substring(0, 100) + (value.length > 100 ? '...' : ''), - sessionId: session.id - }); - // Throw error to reject the email completely - throw new Error(`Header injection attempt detected in ${name} header`); - } - - // Enhanced security: Check for duplicate critical headers (potential injection) - const criticalHeaderNames = ['from', 'to', 'subject', 'date', 'message-id']; - if (criticalHeaderNames.includes(name)) { - if (criticalHeaders.has(name)) { - SmtpLogger.warn('Duplicate critical header detected - potential header injection', { - headerName: name, - existingValue: headers[name]?.substring(0, 50) + '...', - newValue: value.substring(0, 50) + '...', - sessionId: session.id - }); - // Throw error for duplicate critical headers - throw new Error(`Duplicate ${name} header detected - potential header injection`); - } - criticalHeaders.add(name); - } - - // Enhanced security: Check for envelope mismatch (spoofing attempt) - if (name === 'from' && session.envelope?.mailFrom?.address) { - const emailFromHeader = value.match(/<([^>]+)>/)?.[1] || value.trim(); - const envelopeFrom = session.envelope.mailFrom.address; - // Allow some flexibility but detect obvious spoofing attempts - if (emailFromHeader && envelopeFrom && - !emailFromHeader.toLowerCase().includes(envelopeFrom.toLowerCase()) && - !envelopeFrom.toLowerCase().includes(emailFromHeader.toLowerCase())) { - SmtpLogger.warn('Potential sender spoofing detected', { - envelopeFrom: envelopeFrom, - headerFrom: emailFromHeader, - sessionId: session.id - }); - // Note: This is logged but not blocked as legitimate use cases exist - } - } - - // Special handling for MIME-encoded headers (especially Subject) - if (name === 'subject' && value.includes('=?')) { - try { - // Use plugins.mailparser to decode the MIME-encoded subject - // This is a simplified approach - in a real system, you'd use a full MIME decoder - // For now, just log it for debugging - SmtpLogger.debug(`Found encoded subject: ${value}`, { encodedSubject: value }); - } catch (error) { - SmtpLogger.warn(`Failed to decode MIME-encoded subject: ${error instanceof Error ? error.message : String(error)}`); - } - } - - headers[name] = value; - currentHeader = name; - } - } - - // Look for multipart content - let isMultipart = false; - let boundary = ''; - let contentType = headers['content-type'] || ''; - - // Check for multipart content - if (contentType.includes('multipart/')) { - isMultipart = true; - - // Extract boundary - const boundaryMatch = contentType.match(/boundary="?([^";\r\n]+)"?/i); - if (boundaryMatch && boundaryMatch[1]) { - boundary = boundaryMatch[1]; - } - } - - // Extract common headers - const subject = headers['subject'] || 'No Subject'; - const from = headers['from'] || session.envelope.mailFrom.address; - const to = headers['to'] || session.envelope.rcptTo.map(r => r.address).join(', '); - const messageId = headers['message-id'] || `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.smtpServer.getOptions().hostname}>`; - - // Create email object - const email = new Email({ - from: from, - to: to.split(',').map(addr => addr.trim()), - subject: subject, - text: bodyText, - // Add original session envelope data for accurate routing as headers - headers: { - 'X-Original-Mail-From': session.envelope.mailFrom.address, - 'X-Original-Rcpt-To': session.envelope.rcptTo.map(r => r.address).join(', '), - 'Message-Id': messageId - } - }); - - // Handle multipart content if needed - if (isMultipart && boundary) { - this.handleMultipartContent(email, bodyText, boundary); - } - - // Add received header - const timestamp = new Date().toUTCString(); - const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.smtpServer.getOptions().hostname} with ESMTP id ${session.id}; ${timestamp}`; - email.addHeader('Received', receivedHeader); - - // Add all original headers - for (const [name, value] of Object.entries(headers)) { - if (!['from', 'to', 'subject', 'message-id'].includes(name)) { - email.addHeader(name, value); - } - } - - // Store raw data for testing - (email as any).rawData = rawData; - - return email; - } - - /** - * Handle multipart content parsing - * @param email - Email object to update - * @param bodyText - Body text to parse - * @param boundary - MIME boundary - */ - private handleMultipartContent(email: Email, bodyText: string, boundary: string): void { - // Split the body by boundary - const parts = bodyText.split(`--${boundary}`); - - SmtpLogger.debug(`Handling multipart content with ${parts.length - 1} parts (boundary: ${boundary})`); - - // Process each part - for (let i = 1; i < parts.length; i++) { - const part = parts[i]; - - // Skip the end boundary marker - if (part.startsWith('--')) { - SmtpLogger.debug(`Found end boundary marker in part ${i}`); - continue; - } - - // Find the headers and content - const partHeaderEndIndex = part.indexOf('\r\n\r\n'); - if (partHeaderEndIndex === -1) { - SmtpLogger.debug(`No header/body separator found in part ${i}`); - continue; - } - - const partHeadersText = part.substring(0, partHeaderEndIndex); - const partContent = part.substring(partHeaderEndIndex + 4); - - // Parse part headers - const partHeaders: Record = {}; - const partHeaderLines = partHeadersText.split('\r\n'); - let currentHeader = ''; - - for (const line of partHeaderLines) { - // Check if this is a continuation of a previous header - if (line.startsWith(' ') || line.startsWith('\t')) { - if (currentHeader) { - partHeaders[currentHeader] += ' ' + line.trim(); - } - continue; - } - - // This is a new header - const separatorIndex = line.indexOf(':'); - if (separatorIndex !== -1) { - const name = line.substring(0, separatorIndex).trim().toLowerCase(); - const value = line.substring(separatorIndex + 1).trim(); - partHeaders[name] = value; - currentHeader = name; - } - } - - // Get content type - const contentType = partHeaders['content-type'] || ''; - - // Get encoding - const encoding = partHeaders['content-transfer-encoding'] || '7bit'; - - // Get disposition - const disposition = partHeaders['content-disposition'] || ''; - - // Log part information - SmtpLogger.debug(`Processing MIME part ${i}: type=${contentType}, encoding=${encoding}, disposition=${disposition}`); - - // Handle text/plain parts - if (contentType.includes('text/plain')) { - try { - // Decode content based on encoding - let decodedContent = partContent; - - if (encoding.toLowerCase() === 'base64') { - // Remove line breaks from base64 content before decoding - const cleanBase64 = partContent.replace(/[\r\n]/g, ''); - try { - decodedContent = Buffer.from(cleanBase64, 'base64').toString('utf8'); - } catch (error) { - SmtpLogger.warn(`Failed to decode base64 text content: ${error instanceof Error ? error.message : String(error)}`); - } - } else if (encoding.toLowerCase() === 'quoted-printable') { - try { - // Basic quoted-printable decoding - decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => { - return String.fromCharCode(parseInt(hex, 16)); - }); - } catch (error) { - SmtpLogger.warn(`Failed to decode quoted-printable content: ${error instanceof Error ? error.message : String(error)}`); - } - } - - email.text = decodedContent.trim(); - } catch (error) { - SmtpLogger.warn(`Error processing text/plain part: ${error instanceof Error ? error.message : String(error)}`); - email.text = partContent.trim(); - } - } - - // Handle text/html parts - if (contentType.includes('text/html')) { - try { - // Decode content based on encoding - let decodedContent = partContent; - - if (encoding.toLowerCase() === 'base64') { - // Remove line breaks from base64 content before decoding - const cleanBase64 = partContent.replace(/[\r\n]/g, ''); - try { - decodedContent = Buffer.from(cleanBase64, 'base64').toString('utf8'); - } catch (error) { - SmtpLogger.warn(`Failed to decode base64 HTML content: ${error instanceof Error ? error.message : String(error)}`); - } - } else if (encoding.toLowerCase() === 'quoted-printable') { - try { - // Basic quoted-printable decoding - decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => { - return String.fromCharCode(parseInt(hex, 16)); - }); - } catch (error) { - SmtpLogger.warn(`Failed to decode quoted-printable HTML content: ${error instanceof Error ? error.message : String(error)}`); - } - } - - email.html = decodedContent.trim(); - } catch (error) { - SmtpLogger.warn(`Error processing text/html part: ${error instanceof Error ? error.message : String(error)}`); - email.html = partContent.trim(); - } - } - - // Handle attachments - detect attachments by content disposition or by content-type - const isAttachment = - (disposition && disposition.toLowerCase().includes('attachment')) || - (!contentType.includes('text/plain') && !contentType.includes('text/html')); - - if (isAttachment) { - try { - // Extract filename from Content-Disposition or generate one based on content type - let filename = 'attachment'; - - if (disposition) { - const filenameMatch = disposition.match(/filename="?([^";\r\n]+)"?/i); - if (filenameMatch && filenameMatch[1]) { - filename = filenameMatch[1].trim(); - } - } else if (contentType) { - // If no filename but we have content type, generate a name with appropriate extension - const mainType = contentType.split(';')[0].trim().toLowerCase(); - - if (mainType === 'application/pdf') { - filename = `attachment_${Date.now()}.pdf`; - } else if (mainType === 'image/jpeg' || mainType === 'image/jpg') { - filename = `image_${Date.now()}.jpg`; - } else if (mainType === 'image/png') { - filename = `image_${Date.now()}.png`; - } else if (mainType === 'image/gif') { - filename = `image_${Date.now()}.gif`; - } else { - filename = `attachment_${Date.now()}.bin`; - } - } - - // Decode content based on encoding - let content: Buffer; - - if (encoding.toLowerCase() === 'base64') { - try { - // Remove line breaks from base64 content before decoding - const cleanBase64 = partContent.replace(/[\r\n]/g, ''); - content = Buffer.from(cleanBase64, 'base64'); - SmtpLogger.debug(`Successfully decoded base64 attachment: ${filename}, size: ${content.length} bytes`); - } catch (error) { - SmtpLogger.warn(`Failed to decode base64 attachment: ${error instanceof Error ? error.message : String(error)}`); - content = Buffer.from(partContent); - } - } else if (encoding.toLowerCase() === 'quoted-printable') { - try { - // Basic quoted-printable decoding - const decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => { - return String.fromCharCode(parseInt(hex, 16)); - }); - content = Buffer.from(decodedContent); - } catch (error) { - SmtpLogger.warn(`Failed to decode quoted-printable attachment: ${error instanceof Error ? error.message : String(error)}`); - content = Buffer.from(partContent); - } - } else { - // Default for 7bit, 8bit, or binary encoding - no decoding needed - content = Buffer.from(partContent); - } - - // Determine content type - use the one from headers or infer from filename - let finalContentType = contentType; - - if (!finalContentType || finalContentType === 'application/octet-stream') { - if (filename.endsWith('.pdf')) { - finalContentType = 'application/pdf'; - } else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) { - finalContentType = 'image/jpeg'; - } else if (filename.endsWith('.png')) { - finalContentType = 'image/png'; - } else if (filename.endsWith('.gif')) { - finalContentType = 'image/gif'; - } else if (filename.endsWith('.txt')) { - finalContentType = 'text/plain'; - } else if (filename.endsWith('.html')) { - finalContentType = 'text/html'; - } - } - - // Add attachment to email - email.attachments.push({ - filename, - content, - contentType: finalContentType || 'application/octet-stream' - }); - - SmtpLogger.debug(`Added attachment: ${filename}, type: ${finalContentType}, size: ${content.length} bytes`); - } catch (error) { - SmtpLogger.error(`Failed to process attachment: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // Check for nested multipart content - if (contentType.includes('multipart/')) { - try { - // Extract boundary - const nestedBoundaryMatch = contentType.match(/boundary="?([^";\r\n]+)"?/i); - if (nestedBoundaryMatch && nestedBoundaryMatch[1]) { - const nestedBoundary = nestedBoundaryMatch[1].trim(); - SmtpLogger.debug(`Found nested multipart content with boundary: ${nestedBoundary}`); - - // Process nested multipart - this.handleMultipartContent(email, partContent, nestedBoundary); - } - } catch (error) { - SmtpLogger.warn(`Error processing nested multipart content: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - } - - /** - * Handle end of data marker received - * @param socket - Client socket - * @param session - SMTP session - */ - private async handleEndOfData(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession): Promise { - // Clear the data timeout - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - - try { - // Update session state - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.FINISHED); - - // Optionally save email to disk - this.saveEmail(session); - - // Process the email using legacy method - const result = await this.processEmailLegacy(session); - - if (result.success) { - // Send success response - this.sendResponse(socket, `${SmtpResponseCode.OK} OK message queued as ${result.messageId}`); - } else { - // Send error response - this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Failed to process email: ${result.error}`); - } - - // Reset session for new transaction - this.resetSession(session); - } catch (error) { - SmtpLogger.error(`Error processing email: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - error: error instanceof Error ? error : new Error(String(error)) - }); - - this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email: ${error instanceof Error ? error.message : String(error)}`); - this.resetSession(session); - } - } - - /** - * Reset session after email processing - * @param session - SMTP session - */ - private resetSession(session: ISmtpSession): void { - // Clear any data timeout - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - - // Reset data fields but keep authentication state - session.mailFrom = ''; - session.rcptTo = []; - session.emailData = ''; - session.emailDataChunks = []; - session.emailDataSize = 0; - session.envelope = { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }; - - // Reset state to after EHLO - this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO); - } - - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response message - */ - private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - SmtpLogger.logResponse(response, socket); - } catch (error) { - // Attempt to recover from specific transient errors - if (this.isRecoverableSocketError(error)) { - this.handleSocketError(socket, error, response); - } else { - // Log error for non-recoverable errors - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - } - } - } - - /** - * Check if a socket error is potentially recoverable - * @param error - The error that occurred - * @returns Whether the error is potentially recoverable - */ - private isRecoverableSocketError(error: unknown): boolean { - const recoverableErrorCodes = [ - 'EPIPE', // Broken pipe - 'ECONNRESET', // Connection reset by peer - 'ETIMEDOUT', // Connection timed out - 'ECONNABORTED' // Connection aborted - ]; - - return ( - error instanceof Error && - 'code' in error && - typeof (error as any).code === 'string' && - recoverableErrorCodes.includes((error as any).code) - ); - } - - /** - * Handle recoverable socket errors with retry logic - * @param socket - Client socket - * @param error - The error that occurred - * @param response - The response that failed to send - */ - private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - if (!session) { - SmtpLogger.error(`Session not found when handling socket error`); - if (!socket.destroyed) { - socket.destroy(); - } - return; - } - - // Get error details for logging - const errorMessage = error instanceof Error ? error.message : String(error); - const errorCode = error instanceof Error && 'code' in error ? (error as any).code : 'UNKNOWN'; - - SmtpLogger.warn(`Recoverable socket error during data handling (${errorCode}): ${errorMessage}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Check if socket is already destroyed - if (socket.destroyed) { - SmtpLogger.info(`Socket already destroyed, cannot retry data operation`); - return; - } - - // Check if socket is writeable - if (!socket.writable) { - SmtpLogger.info(`Socket no longer writable, aborting data recovery attempt`); - if (!socket.destroyed) { - socket.destroy(); - } - return; - } - - // Attempt to retry the write operation after a short delay - setTimeout(() => { - try { - if (!socket.destroyed && socket.writable) { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - SmtpLogger.info(`Successfully retried data send operation after error`); - } else { - SmtpLogger.warn(`Socket no longer available for data retry`); - if (!socket.destroyed) { - socket.destroy(); - } - } - } catch (retryError) { - SmtpLogger.error(`Data retry attempt failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`); - if (!socket.destroyed) { - socket.destroy(); - } - } - }, 100); // Short delay before retry - } - - /** - * Handle email data (interface requirement) - */ - public async handleData( - socket: plugins.net.Socket | plugins.tls.TLSSocket, - data: string, - session: ISmtpSession - ): Promise { - // Delegate to existing method - await this.handleDataReceived(socket, data); - } - - /** - * Clean up resources - */ - public destroy(): void { - // DataHandler doesn't have timers or event listeners to clean up - SmtpLogger.debug('DataHandler destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/index.ts b/ts/mail/delivery/smtpserver/index.ts deleted file mode 100644 index 7a0c448..0000000 --- a/ts/mail/delivery/smtpserver/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * SMTP Server Module Exports - * This file exports all components of the refactored SMTP server - */ - -// Export interfaces -export * from './interfaces.js'; - -// Export server classes -export { SmtpServer } from './smtp-server.js'; -export { SessionManager } from './session-manager.js'; -export { ConnectionManager } from './connection-manager.js'; -export { CommandHandler } from './command-handler.js'; -export { DataHandler } from './data-handler.js'; -export { TlsHandler } from './tls-handler.js'; -export { SecurityHandler } from './security-handler.js'; - -// Export constants -export * from './constants.js'; - -// Export utilities -export { SmtpLogger } from './utils/logging.js'; -export * from './utils/validation.js'; -export * from './utils/helpers.js'; - -// Export TLS and certificate utilities -export * from './certificate-utils.js'; -export * from './secure-server.js'; -export * from './starttls-handler.js'; - -// Factory function to create a complete SMTP server with default components -export { createSmtpServer } from './create-server.js'; \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/interfaces.ts b/ts/mail/delivery/smtpserver/interfaces.ts deleted file mode 100644 index a4afa61..0000000 --- a/ts/mail/delivery/smtpserver/interfaces.ts +++ /dev/null @@ -1,655 +0,0 @@ -/** - * SMTP Server Interfaces - * Defines all the interfaces used by the SMTP server implementation - */ - -import * as plugins from '../../../plugins.js'; -import type { Email } from '../../core/classes.email.js'; -import type { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; - -// Re-export types from other modules -import { SmtpState } from '../interfaces.js'; -import { SmtpCommand } from './constants.js'; -export { SmtpState, SmtpCommand }; -export type { IEnvelopeRecipient } from '../interfaces.js'; - -/** - * Interface for components that need cleanup - */ -export interface IDestroyable { - /** - * Clean up all resources (timers, listeners, etc) - */ - destroy(): void | Promise; -} - -/** - * SMTP authentication credentials - */ -export interface ISmtpAuth { - /** - * Username for authentication - */ - username: string; - - /** - * Password for authentication - */ - password: string; -} - -/** - * SMTP envelope (sender and recipients) - */ -export interface ISmtpEnvelope { - /** - * Mail from address - */ - mailFrom: { - address: string; - args?: Record; - }; - - /** - * Recipients list - */ - rcptTo: Array<{ - address: string; - args?: Record; - }>; -} - -/** - * SMTP session representing a client connection - */ -export interface ISmtpSession { - /** - * Unique session identifier - */ - id: string; - - /** - * Current state of the SMTP session - */ - state: SmtpState; - - /** - * Client's hostname from EHLO/HELO - */ - clientHostname: string | null; - - /** - * Whether TLS is active for this session - */ - secure: boolean; - - /** - * Authentication status - */ - authenticated: boolean; - - /** - * Authentication username if authenticated - */ - username?: string; - - /** - * Transaction envelope - */ - envelope: ISmtpEnvelope; - - /** - * When the session was created - */ - createdAt: Date; - - /** - * Last activity timestamp - */ - lastActivity: number; - - /** - * Client's IP address - */ - remoteAddress: string; - - /** - * Client's port - */ - remotePort: number; - - /** - * Additional session data - */ - data?: Record; - - /** - * Message size if SIZE extension is used - */ - messageSize?: number; - - /** - * Server capabilities advertised to client - */ - capabilities?: string[]; - - /** - * Buffer for incomplete data - */ - dataBuffer?: string; - - /** - * Flag to track if we're currently receiving DATA - */ - receivingData?: boolean; - - /** - * The raw email data being received - */ - rawData?: string; - - /** - * Greeting sent to client - */ - greeting?: string; - - /** - * Whether EHLO has been sent - */ - ehloSent?: boolean; - - /** - * Whether HELO has been sent - */ - heloSent?: boolean; - - /** - * TLS options for this session - */ - tlsOptions?: any; - - /** - * Whether TLS is being used - */ - useTLS?: boolean; - - /** - * Mail from address for this transaction - */ - mailFrom?: string; - - /** - * Recipients for this transaction - */ - rcptTo?: string[]; - - /** - * Email data being received - */ - emailData?: string; - - /** - * Chunks of email data - */ - emailDataChunks?: string[]; - - /** - * Timeout ID for data reception - */ - dataTimeoutId?: NodeJS.Timeout; - - /** - * Whether connection has ended - */ - connectionEnded?: boolean; - - /** - * Size of email data being received - */ - emailDataSize?: number; - - /** - * Processing mode for this session - */ - processingMode?: string; -} - -/** - * Session manager interface - */ -export interface ISessionManager extends IDestroyable { - /** - * Create a new session for a socket - */ - createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure?: boolean): ISmtpSession; - - /** - * Get session by socket - */ - getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined; - - /** - * Update session state - */ - updateSessionState(session: ISmtpSession, newState: SmtpState): void; - - /** - * Remove a session - */ - removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - - /** - * Clear all sessions - */ - clearAllSessions(): void; - - /** - * Get all active sessions - */ - getAllSessions(): ISmtpSession[]; - - /** - * Get session count - */ - getSessionCount(): number; - - /** - * Update last activity for a session - */ - updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; - - /** - * Check for timed out sessions - */ - checkTimeouts(timeoutMs: number): ISmtpSession[]; - - /** - * Update session activity timestamp - */ - updateSessionActivity(session: ISmtpSession): void; - - /** - * Replace socket in session (for TLS upgrade) - */ - replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean; -} - -/** - * Connection manager interface - */ -export interface IConnectionManager extends IDestroyable { - /** - * Handle a new connection - */ - handleConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): Promise; - - /** - * Close all active connections - */ - closeAllConnections(): void; - - /** - * Get active connection count - */ - getConnectionCount(): number; - - /** - * Check if accepting new connections - */ - canAcceptConnection(): boolean; - - /** - * Handle new connection (legacy method name) - */ - handleNewConnection(socket: plugins.net.Socket): Promise; - - /** - * Handle new secure connection (legacy method name) - */ - handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise; - - /** - * Setup socket event handlers - */ - setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void; -} - -/** - * Command handler interface - */ -export interface ICommandHandler extends IDestroyable { - /** - * Handle an SMTP command - */ - handleCommand( - socket: plugins.net.Socket | plugins.tls.TLSSocket, - command: SmtpCommand, - args: string, - session: ISmtpSession - ): Promise; - - /** - * Get supported commands for current session state - */ - getSupportedCommands(session: ISmtpSession): SmtpCommand[]; - - /** - * Process command (legacy method name) - */ - processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, command: string): Promise; -} - -/** - * Data handler interface - */ -export interface IDataHandler extends IDestroyable { - /** - * Handle email data - */ - handleData( - socket: plugins.net.Socket | plugins.tls.TLSSocket, - data: string, - session: ISmtpSession - ): Promise; - - /** - * Process a complete email - */ - processEmail( - rawData: string, - session: ISmtpSession - ): Promise; - - /** - * Handle data received (legacy method name) - */ - handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise; - - /** - * Process email data (legacy method name) - */ - processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise; -} - -/** - * TLS handler interface - */ -export interface ITlsHandler extends IDestroyable { - /** - * Handle STARTTLS command - */ - handleStartTls( - socket: plugins.net.Socket, - session: ISmtpSession - ): Promise; - - /** - * Check if TLS is available - */ - isTlsAvailable(): boolean; - - /** - * Get TLS options - */ - getTlsOptions(): plugins.tls.TlsOptions; - - /** - * Check if TLS is enabled - */ - isTlsEnabled(): boolean; -} - -/** - * Security handler interface - */ -export interface ISecurityHandler extends IDestroyable { - /** - * Check IP reputation - */ - checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise; - - /** - * Validate email address - */ - isValidEmail(email: string): boolean; - - /** - * Authenticate user - */ - authenticate(auth: ISmtpAuth): Promise; -} - -/** - * SMTP server options - */ -export interface ISmtpServerOptions { - /** - * Port to listen on - */ - port: number; - - /** - * Hostname of the server - */ - hostname: string; - - /** - * Host to bind to (optional, defaults to 0.0.0.0) - */ - host?: string; - - /** - * Secure port for TLS connections - */ - securePort?: number; - - /** - * TLS/SSL private key (PEM format) - */ - key?: string; - - /** - * TLS/SSL certificate (PEM format) - */ - cert?: string; - - /** - * CA certificates for TLS (PEM format) - */ - ca?: string; - - /** - * Maximum size of messages in bytes - */ - maxSize?: number; - - /** - * Maximum number of concurrent connections - */ - maxConnections?: number; - - /** - * Authentication options - */ - auth?: { - /** - * Whether authentication is required - */ - required: boolean; - - /** - * Allowed authentication methods - */ - methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; - }; - - /** - * Socket timeout in milliseconds (default: 5 minutes / 300000ms) - */ - socketTimeout?: number; - - /** - * Initial connection timeout in milliseconds (default: 30 seconds / 30000ms) - */ - connectionTimeout?: number; - - /** - * Interval for checking idle sessions in milliseconds (default: 5 seconds / 5000ms) - * For testing, can be set lower (e.g. 1000ms) to detect timeouts more quickly - */ - cleanupInterval?: number; - - /** - * Maximum number of recipients allowed per message (default: 100) - */ - maxRecipients?: number; - - /** - * Maximum message size in bytes (default: 10MB / 10485760 bytes) - * This is advertised in the EHLO SIZE extension - */ - size?: number; - - /** - * Timeout for the DATA command in milliseconds (default: 60000ms / 1 minute) - * This controls how long to wait for the complete email data - */ - dataTimeout?: number; -} - -/** - * Result of SMTP transaction - */ -export interface ISmtpTransactionResult { - /** - * Whether the transaction was successful - */ - success: boolean; - - /** - * Error message if failed - */ - error?: string; - - /** - * Message ID if successful - */ - messageId?: string; - - /** - * Resulting email if successful - */ - email?: Email; -} - -/** - * Interface for SMTP session events - * These events are emitted by the session manager - */ -export interface ISessionEvents { - created: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void; - stateChanged: (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void; - timeout: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void; - completed: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void; - error: (session: ISmtpSession, error: Error) => void; -} - -/** - * SMTP Server interface - */ -export interface ISmtpServer extends IDestroyable { - /** - * Start the SMTP server - */ - listen(): Promise; - - /** - * Stop the SMTP server - */ - close(): Promise; - - /** - * Get the session manager - */ - getSessionManager(): ISessionManager; - - /** - * Get the connection manager - */ - getConnectionManager(): IConnectionManager; - - /** - * Get the command handler - */ - getCommandHandler(): ICommandHandler; - - /** - * Get the data handler - */ - getDataHandler(): IDataHandler; - - /** - * Get the TLS handler - */ - getTlsHandler(): ITlsHandler; - - /** - * Get the security handler - */ - getSecurityHandler(): ISecurityHandler; - - /** - * Get the server options - */ - getOptions(): ISmtpServerOptions; - - /** - * Get the email server reference - */ - getEmailServer(): UnifiedEmailServer; -} - -/** - * Configuration for creating SMTP server - */ -export interface ISmtpServerConfig { - /** - * Email server instance - */ - emailServer: UnifiedEmailServer; - - /** - * Server options - */ - options: ISmtpServerOptions; - - /** - * Optional custom session manager - */ - sessionManager?: ISessionManager; - - /** - * Optional custom connection manager - */ - connectionManager?: IConnectionManager; - - /** - * Optional custom command handler - */ - commandHandler?: ICommandHandler; - - /** - * Optional custom data handler - */ - dataHandler?: IDataHandler; - - /** - * Optional custom TLS handler - */ - tlsHandler?: ITlsHandler; - - /** - * Optional custom security handler - */ - securityHandler?: ISecurityHandler; -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/secure-server.ts b/ts/mail/delivery/smtpserver/secure-server.ts deleted file mode 100644 index f4b62e5..0000000 --- a/ts/mail/delivery/smtpserver/secure-server.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Secure SMTP Server Utility Functions - * Provides helper functions for creating and managing secure TLS server - */ - -import * as plugins from '../../../plugins.js'; -import { - loadCertificatesFromString, - generateSelfSignedCertificates, - createTlsOptions, - type ICertificateData -} from './certificate-utils.js'; -import { SmtpLogger } from './utils/logging.js'; - -/** - * Create a secure TLS server for direct TLS connections - * @param options - TLS certificate options - * @returns A configured TLS server or undefined if TLS is not available - */ -export function createSecureTlsServer(options: { - key: string; - cert: string; - ca?: string; -}): plugins.tls.Server | undefined { - try { - // Log the creation attempt - SmtpLogger.info('Creating secure TLS server for direct connections'); - - // Load certificates from strings - let certificates: ICertificateData; - try { - certificates = loadCertificatesFromString({ - key: options.key, - cert: options.cert, - ca: options.ca - }); - - SmtpLogger.info('Successfully loaded TLS certificates for secure server'); - } catch (certificateError) { - SmtpLogger.warn(`Failed to load certificates, using self-signed: ${certificateError instanceof Error ? certificateError.message : String(certificateError)}`); - certificates = generateSelfSignedCertificates(); - } - - // Create server-side TLS options - const tlsOptions = createTlsOptions(certificates, true); - - // Log details for debugging - SmtpLogger.debug('Creating secure server with options', { - certificates: { - keyLength: certificates.key.length, - certLength: certificates.cert.length, - caLength: certificates.ca ? certificates.ca.length : 0 - }, - tlsOptions: { - minVersion: tlsOptions.minVersion, - maxVersion: tlsOptions.maxVersion, - ciphers: tlsOptions.ciphers?.substring(0, 50) + '...' // Truncate long cipher list - } - }); - - // Create the TLS server - const server = new plugins.tls.Server(tlsOptions); - - // Set up error handlers - server.on('error', (err) => { - SmtpLogger.error(`Secure server error: ${err.message}`, { - component: 'secure-server', - error: err, - stack: err.stack - }); - }); - - // Log secure connections - server.on('secureConnection', (socket) => { - const protocol = socket.getProtocol(); - const cipher = socket.getCipher(); - - SmtpLogger.info('New direct TLS connection established', { - component: 'secure-server', - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - protocol: protocol || 'unknown', - cipher: cipher?.name || 'unknown' - }); - }); - - return server; - } catch (error) { - SmtpLogger.error(`Failed to create secure TLS server: ${error instanceof Error ? error.message : String(error)}`, { - component: 'secure-server', - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - - return undefined; - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/security-handler.ts b/ts/mail/delivery/smtpserver/security-handler.ts deleted file mode 100644 index 6963c78..0000000 --- a/ts/mail/delivery/smtpserver/security-handler.ts +++ /dev/null @@ -1,345 +0,0 @@ -/** - * SMTP Security Handler - * Responsible for security aspects including IP reputation checking, - * email validation, and authentication - */ - -import * as plugins from '../../../plugins.js'; -import type { ISmtpSession, ISmtpAuth } from './interfaces.js'; -import type { ISecurityHandler, ISmtpServer } from './interfaces.js'; -import { SmtpLogger } from './utils/logging.js'; -import { SecurityEventType, SecurityLogLevel } from './constants.js'; -import { isValidEmail } from './utils/validation.js'; -import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; -import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.js'; - -/** - * Interface for IP denylist entry - */ -interface IIpDenylistEntry { - ip: string; - reason: string; - expiresAt?: number; -} - -/** - * Handles security aspects for SMTP server - */ -export class SecurityHandler implements ISecurityHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer: ISmtpServer; - - /** - * IP reputation checker service - */ - private ipReputationService: IPReputationChecker; - - /** - * Simple in-memory IP denylist - */ - private ipDenylist: IIpDenylistEntry[] = []; - - /** - * Cleanup interval timer - */ - private cleanupInterval: NodeJS.Timeout | null = null; - - /** - * Creates a new security handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer) { - this.smtpServer = smtpServer; - - // Initialize IP reputation checker - this.ipReputationService = new IPReputationChecker(); - - // Clean expired denylist entries periodically - this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute - } - - /** - * Check IP reputation for a connection - * @param socket - Client socket - * @returns Promise that resolves to true if IP is allowed, false if blocked - */ - public async checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise { - const socketDetails = getSocketDetails(socket); - const ip = socketDetails.remoteAddress; - - // Check local denylist first - if (this.isIpDenylisted(ip)) { - // Log the blocked connection - this.logSecurityEvent( - SecurityEventType.IP_REPUTATION, - SecurityLogLevel.WARN, - `Connection blocked from denylisted IP: ${ip}`, - { reason: this.getDenylistReason(ip) } - ); - - return false; - } - - // Check with IP reputation service - if (!this.ipReputationService) { - return true; - } - - try { - // Check with IP reputation service - const reputationResult = await this.ipReputationService.checkReputation(ip); - - // Block if score is below HIGH_RISK threshold (20) or if it's spam/proxy/tor/vpn - const isBlocked = reputationResult.score < 20 || - reputationResult.isSpam || - reputationResult.isTor || - reputationResult.isProxy; - - if (isBlocked) { - // Add to local denylist temporarily - const reason = reputationResult.isSpam ? 'spam' : - reputationResult.isTor ? 'tor' : - reputationResult.isProxy ? 'proxy' : - `low reputation score: ${reputationResult.score}`; - this.addToDenylist(ip, reason, 3600000); // 1 hour - - // Log the blocked connection - this.logSecurityEvent( - SecurityEventType.IP_REPUTATION, - SecurityLogLevel.WARN, - `Connection blocked by reputation service: ${ip}`, - { - reason, - score: reputationResult.score, - isSpam: reputationResult.isSpam, - isTor: reputationResult.isTor, - isProxy: reputationResult.isProxy, - isVPN: reputationResult.isVPN - } - ); - - return false; - } - - // Log the allowed connection - this.logSecurityEvent( - SecurityEventType.IP_REPUTATION, - SecurityLogLevel.INFO, - `IP reputation check passed: ${ip}`, - { - score: reputationResult.score, - country: reputationResult.country, - org: reputationResult.org - } - ); - - return true; - } catch (error) { - // Log the error - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - ip, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Allow the connection on error (fail open) - return true; - } - } - - /** - * Validate an email address - * @param email - Email address to validate - * @returns Whether the email address is valid - */ - public isValidEmail(email: string): boolean { - return isValidEmail(email); - } - - /** - * Validate authentication credentials - * @param auth - Authentication credentials - * @returns Promise that resolves to true if authenticated - */ - public async authenticate(auth: ISmtpAuth): Promise { - const { username, password } = auth; - // Get auth options from server - const options = this.smtpServer.getOptions(); - const authOptions = options.auth; - - // Check if authentication is enabled - if (!authOptions) { - this.logSecurityEvent( - SecurityEventType.AUTHENTICATION, - SecurityLogLevel.WARN, - 'Authentication attempt when auth is disabled', - { username } - ); - - return false; - } - - // Note: Method validation and TLS requirement checks would need to be done - // at the caller level since the interface doesn't include session/method info - - try { - let authenticated = false; - - // Use custom validation function if provided - if ((authOptions as any).validateUser) { - authenticated = await (authOptions as any).validateUser(username, password); - } else { - // Default behavior - no authentication - authenticated = false; - } - - // Log the authentication result - this.logSecurityEvent( - SecurityEventType.AUTHENTICATION, - authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN, - authenticated ? 'Authentication successful' : 'Authentication failed', - { username } - ); - - return authenticated; - } catch (error) { - // Log authentication error - this.logSecurityEvent( - SecurityEventType.AUTHENTICATION, - SecurityLogLevel.ERROR, - `Authentication error: ${error instanceof Error ? error.message : String(error)}`, - { username, error: error instanceof Error ? error.message : String(error) } - ); - - return false; - } - } - - /** - * Log a security event - * @param event - Event type - * @param level - Log level - * @param details - Event details - */ - public logSecurityEvent(event: string, level: string, message: string, details: Record): void { - SmtpLogger.logSecurityEvent( - level as SecurityLogLevel, - event as SecurityEventType, - message, - details, - details.ip, - details.domain, - details.success - ); - } - - /** - * Add an IP to the denylist - * @param ip - IP address - * @param reason - Reason for denylisting - * @param duration - Duration in milliseconds (optional, indefinite if not specified) - */ - private addToDenylist(ip: string, reason: string, duration?: number): void { - // Remove existing entry if present - this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip); - - // Create new entry - const entry: IIpDenylistEntry = { - ip, - reason, - expiresAt: duration ? Date.now() + duration : undefined - }; - - // Add to denylist - this.ipDenylist.push(entry); - - // Log the action - this.logSecurityEvent( - SecurityEventType.ACCESS_CONTROL, - SecurityLogLevel.INFO, - `Added IP to denylist: ${ip}`, - { - ip, - reason, - duration: duration ? `${duration / 1000} seconds` : 'indefinite' - } - ); - } - - /** - * Check if an IP is denylisted - * @param ip - IP address - * @returns Whether the IP is denylisted - */ - private isIpDenylisted(ip: string): boolean { - const entry = this.ipDenylist.find(e => e.ip === ip); - - if (!entry) { - return false; - } - - // Check if entry has expired - if (entry.expiresAt && entry.expiresAt < Date.now()) { - // Remove expired entry - this.ipDenylist = this.ipDenylist.filter(e => e !== entry); - return false; - } - - return true; - } - - /** - * Get the reason an IP was denylisted - * @param ip - IP address - * @returns Reason for denylisting or undefined if not denylisted - */ - private getDenylistReason(ip: string): string | undefined { - const entry = this.ipDenylist.find(e => e.ip === ip); - return entry?.reason; - } - - /** - * Clean expired denylist entries - */ - private cleanExpiredDenylistEntries(): void { - const now = Date.now(); - const initialCount = this.ipDenylist.length; - - this.ipDenylist = this.ipDenylist.filter(entry => { - return !entry.expiresAt || entry.expiresAt > now; - }); - - const removedCount = initialCount - this.ipDenylist.length; - - if (removedCount > 0) { - this.logSecurityEvent( - SecurityEventType.ACCESS_CONTROL, - SecurityLogLevel.INFO, - `Cleaned up ${removedCount} expired denylist entries`, - { remainingCount: this.ipDenylist.length } - ); - } - } - - /** - * Clean up resources - */ - public destroy(): void { - // Clear the cleanup interval - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - - // Clear the denylist - this.ipDenylist = []; - - // Clean up IP reputation service if it has a destroy method - if (this.ipReputationService && typeof (this.ipReputationService as any).destroy === 'function') { - (this.ipReputationService as any).destroy(); - } - - SmtpLogger.debug('SecurityHandler destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/session-manager.ts b/ts/mail/delivery/smtpserver/session-manager.ts deleted file mode 100644 index b7c1cb2..0000000 --- a/ts/mail/delivery/smtpserver/session-manager.ts +++ /dev/null @@ -1,557 +0,0 @@ -/** - * SMTP Session Manager - * Responsible for creating, managing, and cleaning up SMTP sessions - */ - -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import type { ISmtpSession, ISmtpEnvelope } from './interfaces.js'; -import type { ISessionManager, ISessionEvents } from './interfaces.js'; -import { SMTP_DEFAULTS } from './constants.js'; -import { generateSessionId, getSocketDetails } from './utils/helpers.js'; -import { SmtpLogger } from './utils/logging.js'; - -/** - * Manager for SMTP sessions - * Handles session creation, tracking, timeout management, and cleanup - */ -export class SessionManager implements ISessionManager { - /** - * Map of socket ID to session - */ - private sessions: Map = new Map(); - - /** - * Map of socket to socket ID - */ - private socketIds: Map = new Map(); - - /** - * SMTP server options - */ - private options: { - socketTimeout: number; - connectionTimeout: number; - cleanupInterval: number; - }; - - /** - * Event listeners - */ - private eventListeners: { - created?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>; - stateChanged?: Set<(session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void>; - timeout?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>; - completed?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>; - error?: Set<(session: ISmtpSession, error: Error) => void>; - } = {}; - - /** - * Timer for cleanup interval - */ - private cleanupTimer: NodeJS.Timeout | null = null; - - /** - * Creates a new session manager - * @param options - Session manager options - */ - constructor(options: { - socketTimeout?: number; - connectionTimeout?: number; - cleanupInterval?: number; - } = {}) { - this.options = { - socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, - connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT, - cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL - }; - - // Start the cleanup timer - this.startCleanupTimer(); - } - - /** - * Creates a new session for a socket connection - * @param socket - Client socket - * @param secure - Whether the connection is secure (TLS) - * @returns New SMTP session - */ - public createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession { - const sessionId = generateSessionId(); - const socketDetails = getSocketDetails(socket); - - // Create a new session - const session: ISmtpSession = { - id: sessionId, - state: SmtpState.GREETING, - clientHostname: '', - mailFrom: '', - rcptTo: [], - emailData: '', - emailDataChunks: [], - emailDataSize: 0, - useTLS: secure || false, - connectionEnded: false, - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - createdAt: new Date(), - secure: secure || false, - authenticated: false, - envelope: { - mailFrom: { address: '', args: {} }, - rcptTo: [] - }, - lastActivity: Date.now() - }; - - // Store session with unique ID - const socketKey = this.getSocketKey(socket); - this.socketIds.set(socket, socketKey); - this.sessions.set(socketKey, session); - - // Set socket timeout - socket.setTimeout(this.options.socketTimeout); - - // Emit session created event - this.emitEvent('created', session, socket); - - // Log session creation - SmtpLogger.info(`Created SMTP session ${sessionId}`, { - sessionId, - remoteAddress: session.remoteAddress, - remotePort: socketDetails.remotePort, - secure: session.secure - }); - - return session; - } - - /** - * Updates the session state - * @param session - SMTP session - * @param newState - New state - */ - public updateSessionState(session: ISmtpSession, newState: SmtpState): void { - if (session.state === newState) { - return; - } - - const previousState = session.state; - session.state = newState; - - // Update activity timestamp - this.updateSessionActivity(session); - - // Emit state changed event - this.emitEvent('stateChanged', session, previousState, newState); - - // Log state change - SmtpLogger.debug(`Session ${session.id} state changed from ${previousState} to ${newState}`, { - sessionId: session.id, - previousState, - newState, - remoteAddress: session.remoteAddress - }); - } - - /** - * Updates the session's last activity timestamp - * @param session - SMTP session - */ - public updateSessionActivity(session: ISmtpSession): void { - session.lastActivity = Date.now(); - } - - /** - * Removes a session - * @param socket - Client socket - */ - public removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - const socketKey = this.socketIds.get(socket); - if (!socketKey) { - return; - } - - const session = this.sessions.get(socketKey); - if (session) { - // Mark the session as ended - session.connectionEnded = true; - - // Clear any data timeout if it exists - if (session.dataTimeoutId) { - clearTimeout(session.dataTimeoutId); - session.dataTimeoutId = undefined; - } - - // Emit session completed event - this.emitEvent('completed', session, socket); - - // Log session removal - SmtpLogger.info(`Removed SMTP session ${session.id}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - finalState: session.state - }); - } - - // Remove from maps - this.sessions.delete(socketKey); - this.socketIds.delete(socket); - } - - /** - * Gets a session for a socket - * @param socket - Client socket - * @returns SMTP session or undefined if not found - */ - public getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined { - const socketKey = this.socketIds.get(socket); - if (!socketKey) { - return undefined; - } - - return this.sessions.get(socketKey); - } - - /** - * Cleans up idle sessions - */ - public cleanupIdleSessions(): void { - const now = Date.now(); - let timedOutCount = 0; - - for (const [socketKey, session] of this.sessions.entries()) { - if (session.connectionEnded) { - // Session already marked as ended, but still in map - this.sessions.delete(socketKey); - continue; - } - - // Calculate how long the session has been idle - const lastActivity = session.lastActivity || 0; - const idleTime = now - lastActivity; - - // Use appropriate timeout based on session state - const timeout = session.state === SmtpState.DATA_RECEIVING - ? this.options.socketTimeout * 2 // Double timeout for data receiving - : session.state === SmtpState.GREETING - ? this.options.connectionTimeout // Initial connection timeout - : this.options.socketTimeout; // Standard timeout for other states - - // Check if session has timed out - if (idleTime > timeout) { - // Find the socket for this session - let timedOutSocket: plugins.net.Socket | plugins.tls.TLSSocket | undefined; - - for (const [socket, key] of this.socketIds.entries()) { - if (key === socketKey) { - timedOutSocket = socket; - break; - } - } - - if (timedOutSocket) { - // Emit timeout event - this.emitEvent('timeout', session, timedOutSocket); - - // Log timeout - SmtpLogger.warn(`Session ${session.id} timed out after ${Math.round(idleTime / 1000)}s of inactivity`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - state: session.state, - idleTime - }); - - // End the socket connection - try { - timedOutSocket.end(); - } catch (error) { - SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - } - - // Remove from maps - this.sessions.delete(socketKey); - this.socketIds.delete(timedOutSocket); - timedOutCount++; - } - } - } - - if (timedOutCount > 0) { - SmtpLogger.info(`Cleaned up ${timedOutCount} timed out sessions`, { - totalSessions: this.sessions.size - }); - } - } - - /** - * Gets the current number of active sessions - * @returns Number of active sessions - */ - public getSessionCount(): number { - return this.sessions.size; - } - - /** - * Clears all sessions (used when shutting down) - */ - public clearAllSessions(): void { - // Log the action - SmtpLogger.info(`Clearing all sessions (count: ${this.sessions.size})`); - - // Clear the sessions and socket IDs maps - this.sessions.clear(); - this.socketIds.clear(); - - // Stop the cleanup timer - this.stopCleanupTimer(); - } - - /** - * Register an event listener - * @param event - Event name - * @param listener - Event listener function - */ - public on(event: K, listener: ISessionEvents[K]): void { - switch (event) { - case 'created': - if (!this.eventListeners.created) { - this.eventListeners.created = new Set(); - } - this.eventListeners.created.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); - break; - case 'stateChanged': - if (!this.eventListeners.stateChanged) { - this.eventListeners.stateChanged = new Set(); - } - this.eventListeners.stateChanged.add(listener as (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void); - break; - case 'timeout': - if (!this.eventListeners.timeout) { - this.eventListeners.timeout = new Set(); - } - this.eventListeners.timeout.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); - break; - case 'completed': - if (!this.eventListeners.completed) { - this.eventListeners.completed = new Set(); - } - this.eventListeners.completed.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); - break; - case 'error': - if (!this.eventListeners.error) { - this.eventListeners.error = new Set(); - } - this.eventListeners.error.add(listener as (session: ISmtpSession, error: Error) => void); - break; - } - } - - /** - * Remove an event listener - * @param event - Event name - * @param listener - Event listener function - */ - public off(event: K, listener: ISessionEvents[K]): void { - switch (event) { - case 'created': - if (this.eventListeners.created) { - this.eventListeners.created.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); - } - break; - case 'stateChanged': - if (this.eventListeners.stateChanged) { - this.eventListeners.stateChanged.delete(listener as (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void); - } - break; - case 'timeout': - if (this.eventListeners.timeout) { - this.eventListeners.timeout.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); - } - break; - case 'completed': - if (this.eventListeners.completed) { - this.eventListeners.completed.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); - } - break; - case 'error': - if (this.eventListeners.error) { - this.eventListeners.error.delete(listener as (session: ISmtpSession, error: Error) => void); - } - break; - } - } - - /** - * Emit an event to registered listeners - * @param event - Event name - * @param args - Event arguments - */ - private emitEvent(event: K, ...args: any[]): void { - let listeners: Set | undefined; - - switch (event) { - case 'created': - listeners = this.eventListeners.created; - break; - case 'stateChanged': - listeners = this.eventListeners.stateChanged; - break; - case 'timeout': - listeners = this.eventListeners.timeout; - break; - case 'completed': - listeners = this.eventListeners.completed; - break; - case 'error': - listeners = this.eventListeners.error; - break; - } - - if (!listeners) { - return; - } - - for (const listener of listeners) { - try { - (listener as Function)(...args); - } catch (error) { - SmtpLogger.error(`Error in session event listener for ${String(event)}: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)) - }); - } - } - } - - /** - * Start the cleanup timer - */ - private startCleanupTimer(): void { - if (this.cleanupTimer) { - return; - } - - this.cleanupTimer = setInterval(() => { - this.cleanupIdleSessions(); - }, this.options.cleanupInterval); - - // Prevent the timer from keeping the process alive - if (this.cleanupTimer.unref) { - this.cleanupTimer.unref(); - } - } - - /** - * Stop the cleanup timer - */ - private stopCleanupTimer(): void { - if (this.cleanupTimer) { - clearInterval(this.cleanupTimer); - this.cleanupTimer = null; - } - } - - /** - * Replace socket mapping for STARTTLS upgrades - * @param oldSocket - Original plain socket - * @param newSocket - New TLS socket - * @returns Whether the replacement was successful - */ - public replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean { - const socketKey = this.socketIds.get(oldSocket); - if (!socketKey) { - SmtpLogger.warn('Cannot replace socket - original socket not found in session manager'); - return false; - } - - const session = this.sessions.get(socketKey); - if (!session) { - SmtpLogger.warn('Cannot replace socket - session not found for socket key'); - return false; - } - - // Remove old socket mapping - this.socketIds.delete(oldSocket); - - // Add new socket mapping - this.socketIds.set(newSocket, socketKey); - - // Set socket timeout for new socket - newSocket.setTimeout(this.options.socketTimeout); - - SmtpLogger.info(`Socket replaced for session ${session.id} (STARTTLS upgrade)`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - oldSocketType: oldSocket.constructor.name, - newSocketType: newSocket.constructor.name - }); - - return true; - } - - /** - * Gets a unique key for a socket - * @param socket - Client socket - * @returns Socket key - */ - private getSocketKey(socket: plugins.net.Socket | plugins.tls.TLSSocket): string { - const details = getSocketDetails(socket); - return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`; - } - - /** - * Get all active sessions - */ - public getAllSessions(): ISmtpSession[] { - return Array.from(this.sessions.values()); - } - - /** - * Update last activity for a session by socket - */ - public updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - const session = this.getSession(socket); - if (session) { - this.updateSessionActivity(session); - } - } - - /** - * Check for timed out sessions - */ - public checkTimeouts(timeoutMs: number): ISmtpSession[] { - const now = Date.now(); - const timedOutSessions: ISmtpSession[] = []; - - for (const session of this.sessions.values()) { - if (now - session.lastActivity > timeoutMs) { - timedOutSessions.push(session); - } - } - - return timedOutSessions; - } - - /** - * Clean up resources - */ - public destroy(): void { - // Clear the cleanup timer - if (this.cleanupTimer) { - clearInterval(this.cleanupTimer); - this.cleanupTimer = null; - } - - // Clear all sessions - this.clearAllSessions(); - - // Clear event listeners - this.eventListeners = {}; - - SmtpLogger.debug('SessionManager destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/smtp-server.ts b/ts/mail/delivery/smtpserver/smtp-server.ts deleted file mode 100644 index 27f1dc8..0000000 --- a/ts/mail/delivery/smtpserver/smtp-server.ts +++ /dev/null @@ -1,804 +0,0 @@ -/** - * SMTP Server - * Core implementation for the refactored SMTP server - */ - -import * as plugins from '../../../plugins.js'; -import { SmtpState } from './interfaces.js'; -import type { ISmtpServerOptions } from './interfaces.js'; -import type { ISmtpServer, ISmtpServerConfig, ISessionManager, IConnectionManager, ICommandHandler, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js'; -import { SessionManager } from './session-manager.js'; -import { ConnectionManager } from './connection-manager.js'; -import { CommandHandler } from './command-handler.js'; -import { DataHandler } from './data-handler.js'; -import { TlsHandler } from './tls-handler.js'; -import { SecurityHandler } from './security-handler.js'; -import { SMTP_DEFAULTS } from './constants.js'; -import { mergeWithDefaults } from './utils/helpers.js'; -import { SmtpLogger } from './utils/logging.js'; -import { adaptiveLogger } from './utils/adaptive-logging.js'; -import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; - -/** - * SMTP Server implementation - * The main server class that coordinates all components - */ -export class SmtpServer implements ISmtpServer { - /** - * Email server reference - */ - private emailServer: UnifiedEmailServer; - - /** - * Session manager - */ - private sessionManager: ISessionManager; - - /** - * Connection manager - */ - private connectionManager: IConnectionManager; - - /** - * Command handler - */ - private commandHandler: ICommandHandler; - - /** - * Data handler - */ - private dataHandler: IDataHandler; - - /** - * TLS handler - */ - private tlsHandler: ITlsHandler; - - /** - * Security handler - */ - private securityHandler: ISecurityHandler; - - /** - * SMTP server options - */ - private options: ISmtpServerOptions; - - /** - * Net server instance - */ - private server: plugins.net.Server | null = null; - - /** - * Secure server instance - */ - private secureServer: plugins.tls.Server | null = null; - - /** - * Whether the server is running - */ - private running = false; - - /** - * Server recovery state - */ - private recoveryState = { - /** - * Whether recovery is in progress - */ - recovering: false, - - /** - * Number of consecutive connection failures - */ - connectionFailures: 0, - - /** - * Last recovery attempt timestamp - */ - lastRecoveryAttempt: 0, - - /** - * Recovery cooldown in milliseconds - */ - recoveryCooldown: 5000, - - /** - * Maximum recovery attempts before giving up - */ - maxRecoveryAttempts: 3, - - /** - * Current recovery attempt - */ - currentRecoveryAttempt: 0 - }; - - /** - * Creates a new SMTP server - * @param config - Server configuration - */ - constructor(config: ISmtpServerConfig) { - this.emailServer = config.emailServer; - this.options = mergeWithDefaults(config.options); - - // Create components - all components now receive the SMTP server instance - this.sessionManager = config.sessionManager || new SessionManager({ - socketTimeout: this.options.socketTimeout, - connectionTimeout: this.options.connectionTimeout, - cleanupInterval: this.options.cleanupInterval - }); - - this.securityHandler = config.securityHandler || new SecurityHandler(this); - this.tlsHandler = config.tlsHandler || new TlsHandler(this); - this.dataHandler = config.dataHandler || new DataHandler(this); - this.commandHandler = config.commandHandler || new CommandHandler(this); - this.connectionManager = config.connectionManager || new ConnectionManager(this); - } - - /** - * Start the SMTP server - * @returns Promise that resolves when server is started - */ - public async listen(): Promise { - if (this.running) { - throw new Error('SMTP server is already running'); - } - - try { - // Create the server - this.server = plugins.net.createServer((socket) => { - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - this.connectionManager.handleNewConnection(socket); - } else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Allow connection on error (fail open) - this.connectionManager.handleNewConnection(socket); - }); - }); - - // Set up error handling with recovery - this.server.on('error', (err) => { - SmtpLogger.error(`SMTP server error: ${err.message}`, { error: err }); - - // Try to recover from specific errors - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('standard', err); - } - }); - - // Start listening - await new Promise((resolve, reject) => { - if (!this.server) { - reject(new Error('Server not initialized')); - return; - } - - this.server.listen(this.options.port, this.options.host, () => { - SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); - resolve(); - }); - - this.server.on('error', reject); - }); - - // Start secure server if configured - if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { - try { - // Import the secure server creation utility from our new module - // This gives us better certificate handling and error resilience - const { createSecureTlsServer } = await import('./secure-server.js'); - - // Create secure server with the certificates - // This uses a more robust approach to certificate loading and validation - this.secureServer = createSecureTlsServer({ - key: this.options.key, - cert: this.options.cert, - ca: this.options.ca - }); - - SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort}`); - - if (this.secureServer) { - // Use explicit error handling for secure connections - this.secureServer.on('tlsClientError', (err, tlsSocket) => { - SmtpLogger.error(`TLS client error: ${err.message}`, { - error: err, - remoteAddress: tlsSocket.remoteAddress, - remotePort: tlsSocket.remotePort, - stack: err.stack - }); - // No need to destroy, the error event will handle that - }); - - // Register the secure connection handler - this.secureServer.on('secureConnection', (socket) => { - SmtpLogger.info(`New secure connection from ${socket.remoteAddress}:${socket.remotePort}`, { - protocol: socket.getProtocol(), - cipher: socket.getCipher()?.name - }); - - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - // Pass the connection to the connection manager - this.connectionManager.handleNewSecureConnection(socket); - } else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - - // Allow connection on error (fail open) - this.connectionManager.handleNewSecureConnection(socket); - }); - }); - - // Global error handler for the secure server with recovery - this.secureServer.on('error', (err) => { - SmtpLogger.error(`SMTP secure server error: ${err.message}`, { - error: err, - stack: err.stack - }); - - // Try to recover from specific errors - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('secure', err); - } - }); - - // Start listening on secure port - await new Promise((resolve, reject) => { - if (!this.secureServer) { - reject(new Error('Secure server not initialized')); - return; - } - - this.secureServer.listen(this.options.securePort, this.options.host, () => { - SmtpLogger.info(`SMTP secure server listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`); - resolve(); - }); - - // Only use error event for startup issues - this.secureServer.once('error', reject); - }); - } else { - SmtpLogger.warn('Failed to create secure server, TLS may not be properly configured'); - } - } catch (error) { - SmtpLogger.error(`Error setting up secure server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - } - } - - this.running = true; - } catch (error) { - SmtpLogger.error(`Failed to start SMTP server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Clean up on error - this.close(); - - throw error; - } - } - - /** - * Stop the SMTP server - * @returns Promise that resolves when server is stopped - */ - public async close(): Promise { - if (!this.running) { - return; - } - - SmtpLogger.info('Stopping SMTP server'); - - try { - // Close all active connections - this.connectionManager.closeAllConnections(); - - // Clear all sessions - this.sessionManager.clearAllSessions(); - - // Clean up adaptive logger to prevent hanging timers - adaptiveLogger.destroy(); - - // Destroy all components to clean up their resources - await this.destroy(); - - // Close servers - const closePromises: Promise[] = []; - - if (this.server) { - closePromises.push( - new Promise((resolve, reject) => { - if (!this.server) { - resolve(); - return; - } - - this.server.close((err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }) - ); - } - - if (this.secureServer) { - closePromises.push( - new Promise((resolve, reject) => { - if (!this.secureServer) { - resolve(); - return; - } - - this.secureServer.close((err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }) - ); - } - - // Add timeout to prevent hanging on close - await Promise.race([ - Promise.all(closePromises), - new Promise((resolve) => { - setTimeout(() => { - SmtpLogger.warn('Server close timed out after 3 seconds, forcing shutdown'); - resolve(); - }, 3000); - }) - ]); - - this.server = null; - this.secureServer = null; - this.running = false; - - SmtpLogger.info('SMTP server stopped'); - } catch (error) { - SmtpLogger.error(`Error stopping SMTP server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)) - }); - - throw error; - } - } - - /** - * Get the session manager - * @returns Session manager instance - */ - public getSessionManager(): ISessionManager { - return this.sessionManager; - } - - /** - * Get the connection manager - * @returns Connection manager instance - */ - public getConnectionManager(): IConnectionManager { - return this.connectionManager; - } - - /** - * Get the command handler - * @returns Command handler instance - */ - public getCommandHandler(): ICommandHandler { - return this.commandHandler; - } - - /** - * Get the data handler - * @returns Data handler instance - */ - public getDataHandler(): IDataHandler { - return this.dataHandler; - } - - /** - * Get the TLS handler - * @returns TLS handler instance - */ - public getTlsHandler(): ITlsHandler { - return this.tlsHandler; - } - - /** - * Get the security handler - * @returns Security handler instance - */ - public getSecurityHandler(): ISecurityHandler { - return this.securityHandler; - } - - /** - * Get the server options - * @returns SMTP server options - */ - public getOptions(): ISmtpServerOptions { - return this.options; - } - - /** - * Get the email server reference - * @returns Email server instance - */ - public getEmailServer(): UnifiedEmailServer { - return this.emailServer; - } - - /** - * Check if the server is running - * @returns Whether the server is running - */ - public isRunning(): boolean { - return this.running; - } - - /** - * Check if we should attempt to recover from an error - * @param error - The error that occurred - * @returns Whether recovery should be attempted - */ - private shouldAttemptRecovery(error: Error): boolean { - // Skip recovery if we're already in recovery mode - if (this.recoveryState.recovering) { - return false; - } - - // Check if we've reached the maximum number of recovery attempts - if (this.recoveryState.currentRecoveryAttempt >= this.recoveryState.maxRecoveryAttempts) { - SmtpLogger.warn('Maximum recovery attempts reached, not attempting further recovery'); - return false; - } - - // Check if enough time has passed since the last recovery attempt - const now = Date.now(); - if (now - this.recoveryState.lastRecoveryAttempt < this.recoveryState.recoveryCooldown) { - SmtpLogger.warn('Recovery cooldown period not elapsed, skipping recovery attempt'); - return false; - } - - // Recoverable errors include: - // - EADDRINUSE: Address already in use (port conflict) - // - ECONNRESET: Connection reset by peer - // - EPIPE: Broken pipe - // - ETIMEDOUT: Connection timed out - const recoverableErrors = [ - 'EADDRINUSE', - 'ECONNRESET', - 'EPIPE', - 'ETIMEDOUT', - 'ECONNABORTED', - 'EPROTO', - 'EMFILE' // Too many open files - ]; - - // Check if this is a recoverable error - const errorCode = (error as any).code; - return recoverableErrors.includes(errorCode); - } - - /** - * Attempt to recover the server after a critical error - * @param serverType - The type of server to recover ('standard' or 'secure') - * @param error - The error that triggered recovery - */ - private async attemptServerRecovery(serverType: 'standard' | 'secure', error: Error): Promise { - // Set recovery flag to prevent multiple simultaneous recovery attempts - if (this.recoveryState.recovering) { - SmtpLogger.warn('Recovery already in progress, skipping new recovery attempt'); - return; - } - - this.recoveryState.recovering = true; - this.recoveryState.lastRecoveryAttempt = Date.now(); - this.recoveryState.currentRecoveryAttempt++; - - SmtpLogger.info(`Attempting server recovery for ${serverType} server after error: ${error.message}`, { - attempt: this.recoveryState.currentRecoveryAttempt, - maxAttempts: this.recoveryState.maxRecoveryAttempts, - errorCode: (error as any).code - }); - - try { - // Determine which server to restart - const isStandardServer = serverType === 'standard'; - - // Close the affected server - if (isStandardServer && this.server) { - await new Promise((resolve) => { - if (!this.server) { - resolve(); - return; - } - - // First try a clean shutdown - this.server.close((err) => { - if (err) { - SmtpLogger.warn(`Error during server close in recovery: ${err.message}`); - } - resolve(); - }); - - // Set a timeout to force close - setTimeout(() => { - resolve(); - }, 3000); - }); - - this.server = null; - } else if (!isStandardServer && this.secureServer) { - await new Promise((resolve) => { - if (!this.secureServer) { - resolve(); - return; - } - - // First try a clean shutdown - this.secureServer.close((err) => { - if (err) { - SmtpLogger.warn(`Error during secure server close in recovery: ${err.message}`); - } - resolve(); - }); - - // Set a timeout to force close - setTimeout(() => { - resolve(); - }, 3000); - }); - - this.secureServer = null; - } - - // Short delay before restarting - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Clean up any lingering connections - this.connectionManager.closeAllConnections(); - this.sessionManager.clearAllSessions(); - - // Restart the affected server - if (isStandardServer) { - // Create and start the standard server - this.server = plugins.net.createServer((socket) => { - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - this.connectionManager.handleNewConnection(socket); - } else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Allow connection on error (fail open) - this.connectionManager.handleNewConnection(socket); - }); - }); - - // Set up error handling with recovery - this.server.on('error', (err) => { - SmtpLogger.error(`SMTP server error after recovery: ${err.message}`, { error: err }); - - // Try to recover again if needed - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('standard', err); - } - }); - - // Start listening again - await new Promise((resolve, reject) => { - if (!this.server) { - reject(new Error('Server not initialized during recovery')); - return; - } - - this.server.listen(this.options.port, this.options.host, () => { - SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); - resolve(); - }); - - // Only use error event for startup issues during recovery - this.server.once('error', (err) => { - SmtpLogger.error(`Failed to restart server during recovery: ${err.message}`); - reject(err); - }); - }); - } else if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { - // Try to recreate the secure server - try { - // Import the secure server creation utility - const { createSecureTlsServer } = await import('./secure-server.js'); - - // Create secure server with the certificates - this.secureServer = createSecureTlsServer({ - key: this.options.key, - cert: this.options.cert, - ca: this.options.ca - }); - - if (this.secureServer) { - SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort} during recovery`); - - // Use explicit error handling for secure connections - this.secureServer.on('tlsClientError', (err, tlsSocket) => { - SmtpLogger.error(`TLS client error after recovery: ${err.message}`, { - error: err, - remoteAddress: tlsSocket.remoteAddress, - remotePort: tlsSocket.remotePort, - stack: err.stack - }); - }); - - // Register the secure connection handler - this.secureServer.on('secureConnection', (socket) => { - // Check IP reputation before handling connection - this.securityHandler.checkIpReputation(socket) - .then(allowed => { - if (allowed) { - // Pass the connection to the connection manager - this.connectionManager.handleNewSecureConnection(socket); - } else { - // Close connection if IP is not allowed - socket.destroy(); - } - }) - .catch(error => { - SmtpLogger.error(`IP reputation check error after recovery: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Allow connection on error (fail open) - this.connectionManager.handleNewSecureConnection(socket); - }); - }); - - // Global error handler for the secure server with recovery - this.secureServer.on('error', (err) => { - SmtpLogger.error(`SMTP secure server error after recovery: ${err.message}`, { - error: err, - stack: err.stack - }); - - // Try to recover again if needed - if (this.shouldAttemptRecovery(err)) { - this.attemptServerRecovery('secure', err); - } - }); - - // Start listening on secure port again - await new Promise((resolve, reject) => { - if (!this.secureServer) { - reject(new Error('Secure server not initialized during recovery')); - return; - } - - this.secureServer.listen(this.options.securePort, this.options.host, () => { - SmtpLogger.info(`SMTP secure server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`); - resolve(); - }); - - // Only use error event for startup issues during recovery - this.secureServer.once('error', (err) => { - SmtpLogger.error(`Failed to restart secure server during recovery: ${err.message}`); - reject(err); - }); - }); - } else { - SmtpLogger.warn('Failed to create secure server during recovery'); - } - } catch (error) { - SmtpLogger.error(`Error setting up secure server during recovery: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // Recovery successful - SmtpLogger.info('Server recovery completed successfully'); - - } catch (recoveryError) { - SmtpLogger.error(`Server recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`, { - error: recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError)), - attempt: this.recoveryState.currentRecoveryAttempt, - maxAttempts: this.recoveryState.maxRecoveryAttempts - }); - } finally { - // Reset recovery flag - this.recoveryState.recovering = false; - } - } - - /** - * Clean up all component resources - */ - public async destroy(): Promise { - SmtpLogger.info('Destroying SMTP server components'); - - // Destroy all components in parallel - const destroyPromises: Promise[] = []; - - if (this.sessionManager && typeof this.sessionManager.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.sessionManager.destroy())); - } - - if (this.connectionManager && typeof this.connectionManager.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.connectionManager.destroy())); - } - - if (this.commandHandler && typeof this.commandHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.commandHandler.destroy())); - } - - if (this.dataHandler && typeof this.dataHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.dataHandler.destroy())); - } - - if (this.tlsHandler && typeof this.tlsHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.tlsHandler.destroy())); - } - - if (this.securityHandler && typeof this.securityHandler.destroy === 'function') { - destroyPromises.push(Promise.resolve(this.securityHandler.destroy())); - } - - await Promise.all(destroyPromises); - - // Destroy the adaptive logger singleton to clean up its timer - const { adaptiveLogger } = await import('./utils/adaptive-logging.js'); - if (adaptiveLogger && typeof adaptiveLogger.destroy === 'function') { - adaptiveLogger.destroy(); - } - - // Clear recovery state - this.recoveryState = { - recovering: false, - connectionFailures: 0, - lastRecoveryAttempt: 0, - recoveryCooldown: 5000, - maxRecoveryAttempts: 3, - currentRecoveryAttempt: 0 - }; - - SmtpLogger.info('All SMTP server components destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/starttls-handler.ts b/ts/mail/delivery/smtpserver/starttls-handler.ts deleted file mode 100644 index 3baa82b..0000000 --- a/ts/mail/delivery/smtpserver/starttls-handler.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * STARTTLS Implementation - * Provides an improved implementation for STARTTLS upgrades - */ - -import * as plugins from '../../../plugins.js'; -import { SmtpLogger } from './utils/logging.js'; -import { - loadCertificatesFromString, - createTlsOptions, - type ICertificateData -} from './certificate-utils.js'; -import { getSocketDetails } from './utils/helpers.js'; -import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.js'; -import { SmtpState } from '../interfaces.js'; - -/** - * Enhanced STARTTLS handler for more reliable TLS upgrades - */ -export async function performStartTLS( - socket: plugins.net.Socket, - options: { - key: string; - cert: string; - ca?: string; - session?: ISmtpSession; - sessionManager?: ISessionManager; - connectionManager?: IConnectionManager; - onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void; - onFailure?: (error: Error) => void; - updateSessionState?: (session: ISmtpSession, state: SmtpState) => void; - } -): Promise { - return new Promise((resolve) => { - try { - const socketDetails = getSocketDetails(socket); - - SmtpLogger.info('Starting enhanced STARTTLS upgrade process', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - - // Create a proper socket cleanup function - const cleanupSocket = () => { - // Remove all listeners to prevent memory leaks - socket.removeAllListeners('data'); - socket.removeAllListeners('error'); - socket.removeAllListeners('close'); - socket.removeAllListeners('end'); - socket.removeAllListeners('drain'); - }; - - // Prepare the socket for TLS upgrade - socket.setNoDelay(true); - - // Critical: make sure there's no pending data before TLS handshake - socket.pause(); - - // Add error handling for the base socket - const handleSocketError = (err: Error) => { - SmtpLogger.error(`Socket error during STARTTLS preparation: ${err.message}`, { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - error: err, - stack: err.stack - }); - - if (options.onFailure) { - options.onFailure(err); - } - - // Resolve with undefined to indicate failure - resolve(undefined); - }; - - socket.once('error', handleSocketError); - - // Load certificates - let certificates: ICertificateData; - try { - certificates = loadCertificatesFromString({ - key: options.key, - cert: options.cert, - ca: options.ca - }); - } catch (certError) { - SmtpLogger.error(`Certificate error during STARTTLS: ${certError instanceof Error ? certError.message : String(certError)}`); - - if (options.onFailure) { - options.onFailure(certError instanceof Error ? certError : new Error(String(certError))); - } - - resolve(undefined); - return; - } - - // Create TLS options optimized for STARTTLS - const tlsOptions = createTlsOptions(certificates, true); - - // Create secure context - let secureContext; - try { - secureContext = plugins.tls.createSecureContext(tlsOptions); - } catch (contextError) { - SmtpLogger.error(`Failed to create secure context: ${contextError instanceof Error ? contextError.message : String(contextError)}`); - - if (options.onFailure) { - options.onFailure(contextError instanceof Error ? contextError : new Error(String(contextError))); - } - - resolve(undefined); - return; - } - - // Log STARTTLS upgrade attempt - SmtpLogger.debug('Attempting TLS socket upgrade with options', { - minVersion: tlsOptions.minVersion, - maxVersion: tlsOptions.maxVersion, - handshakeTimeout: tlsOptions.handshakeTimeout - }); - - // Use a safer approach to create the TLS socket - const handshakeTimeout = 30000; // 30 seconds timeout for TLS handshake - let handshakeTimeoutId: NodeJS.Timeout | undefined; - - // Create the TLS socket using a conservative approach for STARTTLS - const tlsSocket = new plugins.tls.TLSSocket(socket, { - isServer: true, - secureContext, - // Server-side options (simpler is more reliable for STARTTLS) - requestCert: false, - rejectUnauthorized: false - }); - - // Set up error handling for the TLS socket - tlsSocket.once('error', (err) => { - if (handshakeTimeoutId) { - clearTimeout(handshakeTimeoutId); - } - - SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - error: err, - stack: err.stack - }); - - // Clean up socket listeners - cleanupSocket(); - - if (options.onFailure) { - options.onFailure(err); - } - - // Destroy the socket to ensure we don't have hanging connections - tlsSocket.destroy(); - resolve(undefined); - }); - - // Set up handshake timeout manually for extra safety - handshakeTimeoutId = setTimeout(() => { - SmtpLogger.error('TLS handshake timed out', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - - // Clean up socket listeners - cleanupSocket(); - - if (options.onFailure) { - options.onFailure(new Error('TLS handshake timed out')); - } - - // Destroy the socket to ensure we don't have hanging connections - tlsSocket.destroy(); - resolve(undefined); - }, handshakeTimeout); - - // Set up handler for successful TLS negotiation - tlsSocket.once('secure', () => { - if (handshakeTimeoutId) { - clearTimeout(handshakeTimeoutId); - } - - const protocol = tlsSocket.getProtocol(); - const cipher = tlsSocket.getCipher(); - - SmtpLogger.info('TLS upgrade successful via STARTTLS', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - protocol: protocol || 'unknown', - cipher: cipher?.name || 'unknown' - }); - - // Update socket mapping in session manager - if (options.sessionManager) { - const socketReplaced = options.sessionManager.replaceSocket(socket, tlsSocket); - if (!socketReplaced) { - SmtpLogger.error('Failed to replace socket in session manager after STARTTLS', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - } - } - - // Re-attach event handlers from connection manager - if (options.connectionManager) { - try { - options.connectionManager.setupSocketEventHandlers(tlsSocket); - SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort - }); - } catch (handlerError) { - SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', { - remoteAddress: socketDetails.remoteAddress, - remotePort: socketDetails.remotePort, - error: handlerError instanceof Error ? handlerError : new Error(String(handlerError)) - }); - } - } - - // Update session if provided - if (options.session) { - // Update session properties to indicate TLS is active - options.session.useTLS = true; - options.session.secure = true; - - // Reset session state as required by RFC 3207 - // After STARTTLS, client must issue a new EHLO - if (options.updateSessionState) { - options.updateSessionState(options.session, SmtpState.GREETING); - } - } - - // Call success callback if provided - if (options.onSuccess) { - options.onSuccess(tlsSocket); - } - - // Success - return the TLS socket - resolve(tlsSocket); - }); - - // Resume the socket after we've set up all handlers - // This allows the TLS handshake to proceed - socket.resume(); - - } catch (error) { - SmtpLogger.error(`Unexpected error in STARTTLS: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - - if (options.onFailure) { - options.onFailure(error instanceof Error ? error : new Error(String(error))); - } - - resolve(undefined); - } - }); -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/tls-handler.ts b/ts/mail/delivery/smtpserver/tls-handler.ts deleted file mode 100644 index 20007e0..0000000 --- a/ts/mail/delivery/smtpserver/tls-handler.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * SMTP TLS Handler - * Responsible for handling TLS-related SMTP functionality - */ - -import * as plugins from '../../../plugins.js'; -import type { ITlsHandler, ISmtpServer, ISmtpSession } from './interfaces.js'; -import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js'; -import { SmtpLogger } from './utils/logging.js'; -import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; -import { - loadCertificatesFromString, - generateSelfSignedCertificates, - createTlsOptions, - type ICertificateData -} from './certificate-utils.js'; -import { SmtpState } from '../interfaces.js'; - -/** - * Handles TLS functionality for SMTP server - */ -export class TlsHandler implements ITlsHandler { - /** - * Reference to the SMTP server instance - */ - private smtpServer: ISmtpServer; - - /** - * Certificate data - */ - private certificates: ICertificateData; - - /** - * TLS options - */ - private options: plugins.tls.TlsOptions; - - /** - * Creates a new TLS handler - * @param smtpServer - SMTP server instance - */ - constructor(smtpServer: ISmtpServer) { - this.smtpServer = smtpServer; - - // Initialize certificates - const serverOptions = this.smtpServer.getOptions(); - try { - // Try to load certificates from provided options - this.certificates = loadCertificatesFromString({ - key: serverOptions.key, - cert: serverOptions.cert, - ca: serverOptions.ca - }); - - SmtpLogger.info('Successfully loaded TLS certificates'); - } catch (error) { - SmtpLogger.warn(`Failed to load certificates from options, using self-signed: ${error instanceof Error ? error.message : String(error)}`); - - // Fall back to self-signed certificates for testing - this.certificates = generateSelfSignedCertificates(); - } - - // Initialize TLS options - this.options = createTlsOptions(this.certificates); - } - - /** - * Handle STARTTLS command - * @param socket - Client socket - */ - public async handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise { - - // Check if already using TLS - if (session.useTLS) { - this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`); - return null; - } - - // Check if we have the necessary TLS certificates - if (!this.isTlsEnabled()) { - this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`); - return null; - } - - // Send ready for TLS response - this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`); - - // Upgrade the connection to TLS - try { - const tlsSocket = await this.startTLS(socket); - return tlsSocket; - } catch (error) { - SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, { - sessionId: session.id, - remoteAddress: session.remoteAddress, - error: error instanceof Error ? error : new Error(String(error)) - }); - - // Log security event - SmtpLogger.logSecurityEvent( - SecurityLogLevel.ERROR, - SecurityEventType.TLS_NEGOTIATION, - 'STARTTLS negotiation failed', - { error: error instanceof Error ? error.message : String(error) }, - session.remoteAddress - ); - - return null; - } - } - - /** - * Upgrade a connection to TLS - * @param socket - Client socket - */ - public async startTLS(socket: plugins.net.Socket): Promise { - // Get the session for this socket - const session = this.smtpServer.getSessionManager().getSession(socket); - - try { - // Import the enhanced STARTTLS handler - // This uses a more robust approach to TLS upgrades - const { performStartTLS } = await import('./starttls-handler.js'); - - SmtpLogger.info('Using enhanced STARTTLS implementation'); - - // Use the enhanced STARTTLS handler with better error handling and socket management - const serverOptions = this.smtpServer.getOptions(); - const tlsSocket = await performStartTLS(socket, { - key: serverOptions.key, - cert: serverOptions.cert, - ca: serverOptions.ca, - session: session, - sessionManager: this.smtpServer.getSessionManager(), - connectionManager: this.smtpServer.getConnectionManager(), - // Callback for successful upgrade - onSuccess: (secureSocket) => { - SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', { - remoteAddress: secureSocket.remoteAddress, - remotePort: secureSocket.remotePort, - protocol: secureSocket.getProtocol() || 'unknown', - cipher: secureSocket.getCipher()?.name || 'unknown' - }); - - // Log security event - SmtpLogger.logSecurityEvent( - SecurityLogLevel.INFO, - SecurityEventType.TLS_NEGOTIATION, - 'STARTTLS successful with enhanced implementation', - { - protocol: secureSocket.getProtocol(), - cipher: secureSocket.getCipher()?.name - }, - secureSocket.remoteAddress, - undefined, - true - ); - }, - // Callback for failed upgrade - onFailure: (error) => { - SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, { - sessionId: session?.id, - remoteAddress: socket.remoteAddress, - error - }); - - // Log security event - SmtpLogger.logSecurityEvent( - SecurityLogLevel.ERROR, - SecurityEventType.TLS_NEGOTIATION, - 'Enhanced STARTTLS failed', - { error: error.message }, - socket.remoteAddress, - undefined, - false - ); - }, - // Function to update session state - updateSessionState: this.smtpServer.getSessionManager().updateSessionState?.bind(this.smtpServer.getSessionManager()) - }); - - // If STARTTLS failed with the enhanced implementation, log the error - if (!tlsSocket) { - SmtpLogger.warn('Enhanced STARTTLS implementation failed to create TLS socket', { - sessionId: session?.id, - remoteAddress: socket.remoteAddress - }); - throw new Error('Failed to create TLS socket'); - } - - return tlsSocket; - } catch (error) { - // Log STARTTLS failure - SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - - // Log security event - SmtpLogger.logSecurityEvent( - SecurityLogLevel.ERROR, - SecurityEventType.TLS_NEGOTIATION, - 'Failed to upgrade connection to TLS', - { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }, - socket.remoteAddress, - undefined, - false - ); - - // Destroy the socket on error - socket.destroy(); - throw error; - } - } - - /** - * Create a secure server - * @returns TLS server instance or undefined if TLS is not enabled - */ - public createSecureServer(): plugins.tls.Server | undefined { - if (!this.isTlsEnabled()) { - return undefined; - } - - try { - SmtpLogger.info('Creating secure TLS server'); - - // Log certificate info - SmtpLogger.debug('Using certificates for secure server', { - keyLength: this.certificates.key.length, - certLength: this.certificates.cert.length, - caLength: this.certificates.ca ? this.certificates.ca.length : 0 - }); - - // Create TLS options using our certificate utilities - // This ensures proper PEM format handling and protocol negotiation - const tlsOptions = createTlsOptions(this.certificates, true); // Use server options - - SmtpLogger.info('Creating TLS server with options', { - minVersion: tlsOptions.minVersion, - maxVersion: tlsOptions.maxVersion, - handshakeTimeout: tlsOptions.handshakeTimeout - }); - - // Create a server with wider TLS compatibility - const server = new plugins.tls.Server(tlsOptions); - - // Add error handling - server.on('error', (err) => { - SmtpLogger.error(`TLS server error: ${err.message}`, { - error: err, - stack: err.stack - }); - }); - - // Log TLS details for each connection - server.on('secureConnection', (socket) => { - SmtpLogger.info('New secure connection established', { - protocol: socket.getProtocol(), - cipher: socket.getCipher()?.name, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort - }); - }); - - return server; - } catch (error) { - SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, { - error: error instanceof Error ? error : new Error(String(error)), - stack: error instanceof Error ? error.stack : 'No stack trace available' - }); - - return undefined; - } - } - - /** - * Check if TLS is enabled - * @returns Whether TLS is enabled - */ - public isTlsEnabled(): boolean { - const options = this.smtpServer.getOptions(); - return !!(options.key && options.cert); - } - - /** - * Send a response to the client - * @param socket - Client socket - * @param response - Response message - */ - private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { - // Check if socket is still writable before attempting to write - if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { - SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - destroyed: socket.destroyed, - readyState: socket.readyState, - writable: socket.writable - }); - return; - } - - try { - socket.write(`${response}\r\n`); - SmtpLogger.logResponse(response, socket); - } catch (error) { - SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { - response, - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - error: error instanceof Error ? error : new Error(String(error)) - }); - - socket.destroy(); - } - } - - /** - * Check if TLS is available (interface requirement) - */ - public isTlsAvailable(): boolean { - return this.isTlsEnabled(); - } - - /** - * Get TLS options (interface requirement) - */ - public getTlsOptions(): plugins.tls.TlsOptions { - return this.options; - } - - /** - * Clean up resources - */ - public destroy(): void { - // Clear any cached certificates or TLS contexts - // TlsHandler doesn't have timers but may have cached resources - SmtpLogger.debug('TlsHandler destroyed'); - } -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/utils/adaptive-logging.ts b/ts/mail/delivery/smtpserver/utils/adaptive-logging.ts deleted file mode 100644 index 24880fb..0000000 --- a/ts/mail/delivery/smtpserver/utils/adaptive-logging.ts +++ /dev/null @@ -1,514 +0,0 @@ -/** - * Adaptive SMTP Logging System - * Automatically switches between logging modes based on server load (active connections) - * to maintain performance during high-concurrency scenarios - */ - -import * as plugins from '../../../../plugins.js'; -import { logger } from '../../../../logger.js'; -import { SecurityLogLevel, SecurityEventType } from '../constants.js'; -import type { ISmtpSession } from '../interfaces.js'; -import type { LogLevel, ISmtpLogOptions } from './logging.js'; - -/** - * Log modes based on server load - */ -export enum LogMode { - VERBOSE = 'VERBOSE', // < 20 connections: Full detailed logging - REDUCED = 'REDUCED', // 20-40 connections: Limited command/response logging, full error logging - MINIMAL = 'MINIMAL' // 40+ connections: Aggregated logging only, critical errors only -} - -/** - * Configuration for adaptive logging thresholds - */ -export interface IAdaptiveLogConfig { - verboseThreshold: number; // Switch to REDUCED mode above this connection count - reducedThreshold: number; // Switch to MINIMAL mode above this connection count - aggregationInterval: number; // How often to flush aggregated logs (ms) - maxAggregatedEntries: number; // Max entries to hold before forced flush -} - -/** - * Aggregated log entry for batching similar events - */ -interface IAggregatedLogEntry { - type: 'connection' | 'command' | 'response' | 'error'; - count: number; - firstSeen: number; - lastSeen: number; - sample: { - message: string; - level: LogLevel; - options?: ISmtpLogOptions; - }; -} - -/** - * Connection metadata for aggregation tracking - */ -interface IConnectionTracker { - activeConnections: number; - peakConnections: number; - totalConnections: number; - connectionsPerSecond: number; - lastConnectionTime: number; -} - -/** - * Adaptive SMTP Logger that scales logging based on server load - */ -export class AdaptiveSmtpLogger { - private static instance: AdaptiveSmtpLogger; - private currentMode: LogMode = LogMode.VERBOSE; - private config: IAdaptiveLogConfig; - private aggregatedEntries: Map = new Map(); - private aggregationTimer: NodeJS.Timeout | null = null; - private connectionTracker: IConnectionTracker = { - activeConnections: 0, - peakConnections: 0, - totalConnections: 0, - connectionsPerSecond: 0, - lastConnectionTime: Date.now() - }; - - private constructor(config?: Partial) { - this.config = { - verboseThreshold: 20, - reducedThreshold: 40, - aggregationInterval: 30000, // 30 seconds - maxAggregatedEntries: 100, - ...config - }; - - this.startAggregationTimer(); - } - - /** - * Get singleton instance - */ - public static getInstance(config?: Partial): AdaptiveSmtpLogger { - if (!AdaptiveSmtpLogger.instance) { - AdaptiveSmtpLogger.instance = new AdaptiveSmtpLogger(config); - } - return AdaptiveSmtpLogger.instance; - } - - /** - * Update active connection count and adjust log mode if needed - */ - public updateConnectionCount(activeConnections: number): void { - this.connectionTracker.activeConnections = activeConnections; - this.connectionTracker.peakConnections = Math.max( - this.connectionTracker.peakConnections, - activeConnections - ); - - const newMode = this.determineLogMode(activeConnections); - if (newMode !== this.currentMode) { - this.switchLogMode(newMode); - } - } - - /** - * Track new connection for rate calculation - */ - public trackConnection(): void { - this.connectionTracker.totalConnections++; - const now = Date.now(); - const timeDiff = (now - this.connectionTracker.lastConnectionTime) / 1000; - if (timeDiff > 0) { - this.connectionTracker.connectionsPerSecond = 1 / timeDiff; - } - this.connectionTracker.lastConnectionTime = now; - } - - /** - * Get current logging mode - */ - public getCurrentMode(): LogMode { - return this.currentMode; - } - - /** - * Get connection statistics - */ - public getConnectionStats(): IConnectionTracker { - return { ...this.connectionTracker }; - } - - /** - * Log a message with adaptive behavior - */ - public log(level: LogLevel, message: string, options: ISmtpLogOptions = {}): void { - // Always log structured data - const errorInfo = options.error ? { - errorMessage: options.error.message, - errorStack: options.error.stack, - errorName: options.error.name - } : {}; - - const logData = { - component: 'smtp-server', - logMode: this.currentMode, - activeConnections: this.connectionTracker.activeConnections, - ...options, - ...errorInfo - }; - - if (logData.error) { - delete logData.error; - } - - logger.log(level, message, logData); - - // Adaptive console logging based on mode - switch (this.currentMode) { - case LogMode.VERBOSE: - // Full console logging - if (level === 'error' || level === 'warn') { - console[level](`[SMTP] ${message}`, logData); - } - break; - - case LogMode.REDUCED: - // Only errors and warnings to console - if (level === 'error' || level === 'warn') { - console[level](`[SMTP] ${message}`, logData); - } - break; - - case LogMode.MINIMAL: - // Only critical errors to console - if (level === 'error' && (message.includes('critical') || message.includes('security') || message.includes('crash'))) { - console[level](`[SMTP] ${message}`, logData); - } - break; - } - } - - /** - * Log command with adaptive behavior - */ - public logCommand(command: string, socket: plugins.net.Socket | plugins.tls.TLSSocket, session?: ISmtpSession): void { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - - switch (this.currentMode) { - case LogMode.VERBOSE: - this.log('info', `Command received: ${command}`, { - ...clientInfo, - command: command.split(' ')[0]?.toUpperCase() - }); - console.log(`← ${command}`); - break; - - case LogMode.REDUCED: - // Aggregate commands instead of logging each one - this.aggregateEntry('command', 'info', `Command: ${command.split(' ')[0]?.toUpperCase()}`, clientInfo); - // Only show error commands - if (command.toUpperCase().startsWith('QUIT') || command.includes('error')) { - console.log(`← ${command}`); - } - break; - - case LogMode.MINIMAL: - // Only aggregate, no console output unless it's an error command - this.aggregateEntry('command', 'info', `Command: ${command.split(' ')[0]?.toUpperCase()}`, clientInfo); - break; - } - } - - /** - * Log response with adaptive behavior - */ - public logResponse(response: string, socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket - }; - - const responseCode = response.substring(0, 3); - const isError = responseCode.startsWith('4') || responseCode.startsWith('5'); - - switch (this.currentMode) { - case LogMode.VERBOSE: - if (responseCode.startsWith('2') || responseCode.startsWith('3')) { - this.log('debug', `Response sent: ${response}`, clientInfo); - } else if (responseCode.startsWith('4')) { - this.log('warn', `Temporary error response: ${response}`, clientInfo); - } else if (responseCode.startsWith('5')) { - this.log('error', `Permanent error response: ${response}`, clientInfo); - } - console.log(`→ ${response}`); - break; - - case LogMode.REDUCED: - // Log errors normally, aggregate success responses - if (isError) { - if (responseCode.startsWith('4')) { - this.log('warn', `Temporary error response: ${response}`, clientInfo); - } else { - this.log('error', `Permanent error response: ${response}`, clientInfo); - } - console.log(`→ ${response}`); - } else { - this.aggregateEntry('response', 'debug', `Response: ${responseCode}xx`, clientInfo); - } - break; - - case LogMode.MINIMAL: - // Only log critical errors - if (responseCode.startsWith('5')) { - this.log('error', `Permanent error response: ${response}`, clientInfo); - console.log(`→ ${response}`); - } else { - this.aggregateEntry('response', 'debug', `Response: ${responseCode}xx`, clientInfo); - } - break; - } - } - - /** - * Log connection event with adaptive behavior - */ - public logConnection( - socket: plugins.net.Socket | plugins.tls.TLSSocket, - eventType: 'connect' | 'close' | 'error', - session?: ISmtpSession, - error?: Error - ): void { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - - if (eventType === 'connect') { - this.trackConnection(); - } - - switch (this.currentMode) { - case LogMode.VERBOSE: - // Full connection logging - switch (eventType) { - case 'connect': - this.log('info', `New ${clientInfo.secure ? 'secure ' : ''}connection from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - case 'close': - this.log('info', `Connection closed from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - case 'error': - this.log('error', `Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - break; - } - break; - - case LogMode.REDUCED: - // Aggregate normal connections, log errors - if (eventType === 'error') { - this.log('error', `Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - } else { - this.aggregateEntry('connection', 'info', `Connection ${eventType}`, clientInfo); - } - break; - - case LogMode.MINIMAL: - // Only aggregate, except for critical errors - if (eventType === 'error' && error && (error.message.includes('security') || error.message.includes('critical'))) { - this.log('error', `Critical connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - } else { - this.aggregateEntry('connection', 'info', `Connection ${eventType}`, clientInfo); - } - break; - } - } - - /** - * Log security event (always logged regardless of mode) - */ - public logSecurityEvent( - level: SecurityLogLevel, - type: SecurityEventType, - message: string, - details: Record, - ipAddress?: string, - domain?: string, - success?: boolean - ): void { - const logLevel: LogLevel = level === SecurityLogLevel.DEBUG ? 'debug' : - level === SecurityLogLevel.INFO ? 'info' : - level === SecurityLogLevel.WARN ? 'warn' : 'error'; - - // Security events are always logged in full detail - this.log(logLevel, message, { - component: 'smtp-security', - eventType: type, - success, - ipAddress, - domain, - ...details - }); - } - - /** - * Determine appropriate log mode based on connection count - */ - private determineLogMode(activeConnections: number): LogMode { - if (activeConnections >= this.config.reducedThreshold) { - return LogMode.MINIMAL; - } else if (activeConnections >= this.config.verboseThreshold) { - return LogMode.REDUCED; - } else { - return LogMode.VERBOSE; - } - } - - /** - * Switch to a new log mode - */ - private switchLogMode(newMode: LogMode): void { - const oldMode = this.currentMode; - this.currentMode = newMode; - - // Log the mode switch - console.log(`[SMTP] Adaptive logging switched from ${oldMode} to ${newMode} (${this.connectionTracker.activeConnections} active connections)`); - - this.log('info', `Adaptive logging mode changed to ${newMode}`, { - oldMode, - newMode, - activeConnections: this.connectionTracker.activeConnections, - peakConnections: this.connectionTracker.peakConnections, - totalConnections: this.connectionTracker.totalConnections - }); - - // If switching to more verbose mode, flush aggregated entries - if ((oldMode === LogMode.MINIMAL && newMode !== LogMode.MINIMAL) || - (oldMode === LogMode.REDUCED && newMode === LogMode.VERBOSE)) { - this.flushAggregatedEntries(); - } - } - - /** - * Add entry to aggregation buffer - */ - private aggregateEntry( - type: 'connection' | 'command' | 'response' | 'error', - level: LogLevel, - message: string, - options?: ISmtpLogOptions - ): void { - const key = `${type}:${message}`; - const now = Date.now(); - - if (this.aggregatedEntries.has(key)) { - const entry = this.aggregatedEntries.get(key)!; - entry.count++; - entry.lastSeen = now; - } else { - this.aggregatedEntries.set(key, { - type, - count: 1, - firstSeen: now, - lastSeen: now, - sample: { message, level, options } - }); - } - - // Force flush if we have too many entries - if (this.aggregatedEntries.size >= this.config.maxAggregatedEntries) { - this.flushAggregatedEntries(); - } - } - - /** - * Start the aggregation timer - */ - private startAggregationTimer(): void { - if (this.aggregationTimer) { - clearInterval(this.aggregationTimer); - } - - this.aggregationTimer = setInterval(() => { - this.flushAggregatedEntries(); - }, this.config.aggregationInterval); - - // Unref the timer so it doesn't keep the process alive - if (this.aggregationTimer && typeof this.aggregationTimer.unref === 'function') { - this.aggregationTimer.unref(); - } - } - - /** - * Flush aggregated entries to logs - */ - private flushAggregatedEntries(): void { - if (this.aggregatedEntries.size === 0) { - return; - } - - const summary: Record = {}; - let totalAggregated = 0; - - for (const [key, entry] of this.aggregatedEntries.entries()) { - summary[entry.type] = (summary[entry.type] || 0) + entry.count; - totalAggregated += entry.count; - - // Log a sample of high-frequency entries - if (entry.count >= 10) { - this.log(entry.sample.level, `${entry.sample.message} (aggregated: ${entry.count} occurrences)`, { - ...entry.sample.options, - aggregated: true, - occurrences: entry.count, - timeSpan: entry.lastSeen - entry.firstSeen - }); - } - } - - // Log aggregation summary - console.log(`[SMTP] Aggregated ${totalAggregated} log entries: ${JSON.stringify(summary)}`); - - this.log('info', 'Aggregated log summary', { - totalEntries: totalAggregated, - breakdown: summary, - logMode: this.currentMode, - activeConnections: this.connectionTracker.activeConnections - }); - - // Clear aggregated entries - this.aggregatedEntries.clear(); - } - - /** - * Cleanup resources - */ - public destroy(): void { - if (this.aggregationTimer) { - clearInterval(this.aggregationTimer); - this.aggregationTimer = null; - } - this.flushAggregatedEntries(); - } -} - -/** - * Default instance for easy access - */ -export const adaptiveLogger = AdaptiveSmtpLogger.getInstance(); \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/utils/helpers.ts b/ts/mail/delivery/smtpserver/utils/helpers.ts deleted file mode 100644 index ea8906f..0000000 --- a/ts/mail/delivery/smtpserver/utils/helpers.ts +++ /dev/null @@ -1,246 +0,0 @@ -/** - * SMTP Helper Functions - * Provides utility functions for SMTP server implementation - */ - -import * as plugins from '../../../../plugins.js'; -import { SMTP_DEFAULTS } from '../constants.js'; -import type { ISmtpSession, ISmtpServerOptions } from '../interfaces.js'; - -/** - * Formats a multi-line SMTP response according to RFC 5321 - * @param code - Response code - * @param lines - Response lines - * @returns Formatted SMTP response - */ -export function formatMultilineResponse(code: number, lines: string[]): string { - if (!lines || lines.length === 0) { - return `${code} `; - } - - if (lines.length === 1) { - return `${code} ${lines[0]}`; - } - - let response = ''; - for (let i = 0; i < lines.length - 1; i++) { - response += `${code}-${lines[i]}${SMTP_DEFAULTS.CRLF}`; - } - response += `${code} ${lines[lines.length - 1]}`; - - return response; -} - -/** - * Generates a unique session ID - * @returns Unique session ID - */ -export function generateSessionId(): string { - return `${Date.now()}-${Math.floor(Math.random() * 10000)}`; -} - -/** - * Safely parses an integer from string with a default value - * @param value - String value to parse - * @param defaultValue - Default value if parsing fails - * @returns Parsed integer or default value - */ -export function safeParseInt(value: string | undefined, defaultValue: number): number { - if (!value) { - return defaultValue; - } - - const parsed = parseInt(value, 10); - return isNaN(parsed) ? defaultValue : parsed; -} - -/** - * Safely gets the socket details - * @param socket - Socket to get details from - * @returns Socket details object - */ -export function getSocketDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): { - remoteAddress: string; - remotePort: number; - remoteFamily: string; - localAddress: string; - localPort: number; - encrypted: boolean; -} { - return { - remoteAddress: socket.remoteAddress || 'unknown', - remotePort: socket.remotePort || 0, - remoteFamily: socket.remoteFamily || 'unknown', - localAddress: socket.localAddress || 'unknown', - localPort: socket.localPort || 0, - encrypted: socket instanceof plugins.tls.TLSSocket - }; -} - -/** - * Gets TLS details if socket is TLS - * @param socket - Socket to get TLS details from - * @returns TLS details or undefined if not TLS - */ -export function getTlsDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): { - protocol?: string; - cipher?: string; - authorized?: boolean; -} | undefined { - if (!(socket instanceof plugins.tls.TLSSocket)) { - return undefined; - } - - return { - protocol: socket.getProtocol(), - cipher: socket.getCipher()?.name, - authorized: socket.authorized - }; -} - -/** - * Merges default options with provided options - * @param options - User provided options - * @returns Merged options with defaults - */ -export function mergeWithDefaults(options: Partial): ISmtpServerOptions { - return { - port: options.port || SMTP_DEFAULTS.SMTP_PORT, - key: options.key || '', - cert: options.cert || '', - hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME, - host: options.host, - securePort: options.securePort, - ca: options.ca, - maxSize: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE, - maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, - socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, - connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT, - cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL, - maxRecipients: options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS, - size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE, - dataTimeout: options.dataTimeout || SMTP_DEFAULTS.DATA_TIMEOUT, - auth: options.auth, - }; -} - -/** - * Creates a text response formatter for the SMTP server - * @param socket - Socket to send responses to - * @returns Function to send formatted response - */ -export function createResponseFormatter(socket: plugins.net.Socket | plugins.tls.TLSSocket): (response: string) => void { - return (response: string): void => { - try { - socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); - console.log(`→ ${response}`); - } catch (error) { - console.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`); - socket.destroy(); - } - }; -} - -/** - * Extracts SMTP command name from a command line - * @param commandLine - Full command line - * @returns Command name in uppercase - */ -export function extractCommandName(commandLine: string): string { - if (!commandLine || typeof commandLine !== 'string') { - return ''; - } - - // Handle specific command patterns first - const ehloMatch = commandLine.match(/^(EHLO|HELO)\b/i); - if (ehloMatch) { - return ehloMatch[1].toUpperCase(); - } - - const mailMatch = commandLine.match(/^MAIL\b/i); - if (mailMatch) { - return 'MAIL'; - } - - const rcptMatch = commandLine.match(/^RCPT\b/i); - if (rcptMatch) { - return 'RCPT'; - } - - // Default handling - const parts = commandLine.trim().split(/\s+/); - return (parts[0] || '').toUpperCase(); -} - -/** - * Extracts SMTP command arguments from a command line - * @param commandLine - Full command line - * @returns Arguments string - */ -export function extractCommandArgs(commandLine: string): string { - if (!commandLine || typeof commandLine !== 'string') { - return ''; - } - - const command = extractCommandName(commandLine); - if (!command) { - return commandLine.trim(); - } - - // Special handling for specific commands - if (command === 'EHLO' || command === 'HELO') { - const match = commandLine.match(/^(?:EHLO|HELO)\s+(.+)$/i); - return match ? match[1].trim() : ''; - } - - if (command === 'MAIL') { - return commandLine.replace(/^MAIL\s+/i, ''); - } - - if (command === 'RCPT') { - return commandLine.replace(/^RCPT\s+/i, ''); - } - - // Default extraction - const firstSpace = commandLine.indexOf(' '); - if (firstSpace === -1) { - return ''; - } - - return commandLine.substring(firstSpace + 1).trim(); -} - -/** - * Sanitizes data for logging (hides sensitive info) - * @param data - Data to sanitize - * @returns Sanitized data - */ -export function sanitizeForLogging(data: any): any { - if (!data) { - return data; - } - - if (typeof data !== 'object') { - return data; - } - - const result: any = Array.isArray(data) ? [] : {}; - - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - // Sanitize sensitive fields - if (key.toLowerCase().includes('password') || - key.toLowerCase().includes('token') || - key.toLowerCase().includes('secret') || - key.toLowerCase().includes('credential')) { - result[key] = '********'; - } else if (typeof data[key] === 'object' && data[key] !== null) { - result[key] = sanitizeForLogging(data[key]); - } else { - result[key] = data[key]; - } - } - } - - return result; -} \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/utils/logging.ts b/ts/mail/delivery/smtpserver/utils/logging.ts deleted file mode 100644 index e45b398..0000000 --- a/ts/mail/delivery/smtpserver/utils/logging.ts +++ /dev/null @@ -1,246 +0,0 @@ -/** - * SMTP Logging Utilities - * Provides structured logging for SMTP server components - */ - -import * as plugins from '../../../../plugins.js'; -import { logger } from '../../../../logger.js'; -import { SecurityLogLevel, SecurityEventType } from '../constants.js'; -import type { ISmtpSession } from '../interfaces.js'; - -/** - * SMTP connection metadata to include in logs - */ -export interface IConnectionMetadata { - remoteAddress?: string; - remotePort?: number; - socketId?: string; - secure?: boolean; - sessionId?: string; -} - -/** - * Log levels for SMTP server - */ -export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; - -/** - * Options for SMTP log - */ -export interface ISmtpLogOptions { - level?: LogLevel; - sessionId?: string; - sessionState?: string; - remoteAddress?: string; - remotePort?: number; - command?: string; - error?: Error; - [key: string]: any; -} - -/** - * SMTP logger - provides structured logging for SMTP server - */ -export class SmtpLogger { - /** - * Log a message with context - * @param level - Log level - * @param message - Log message - * @param options - Additional log options - */ - public static log(level: LogLevel, message: string, options: ISmtpLogOptions = {}): void { - // Extract error information if provided - const errorInfo = options.error ? { - errorMessage: options.error.message, - errorStack: options.error.stack, - errorName: options.error.name - } : {}; - - // Structure log data - const logData = { - component: 'smtp-server', - ...options, - ...errorInfo - }; - - // Remove error from log data to avoid duplication - if (logData.error) { - delete logData.error; - } - - // Log through the main logger - logger.log(level, message, logData); - - // Also console log for immediate visibility during development - if (level === 'error' || level === 'warn') { - console[level](`[SMTP] ${message}`, logData); - } - } - - /** - * Log debug level message - * @param message - Log message - * @param options - Additional log options - */ - public static debug(message: string, options: ISmtpLogOptions = {}): void { - this.log('debug', message, options); - } - - /** - * Log info level message - * @param message - Log message - * @param options - Additional log options - */ - public static info(message: string, options: ISmtpLogOptions = {}): void { - this.log('info', message, options); - } - - /** - * Log warning level message - * @param message - Log message - * @param options - Additional log options - */ - public static warn(message: string, options: ISmtpLogOptions = {}): void { - this.log('warn', message, options); - } - - /** - * Log error level message - * @param message - Log message - * @param options - Additional log options - */ - public static error(message: string, options: ISmtpLogOptions = {}): void { - this.log('error', message, options); - } - - /** - * Log command received from client - * @param command - The command string - * @param socket - The client socket - * @param session - The SMTP session - */ - public static logCommand(command: string, socket: plugins.net.Socket | plugins.tls.TLSSocket, session?: ISmtpSession): void { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - - this.info(`Command received: ${command}`, { - ...clientInfo, - command: command.split(' ')[0]?.toUpperCase() - }); - - // Also log to console for easy debugging - console.log(`← ${command}`); - } - - /** - * Log response sent to client - * @param response - The response string - * @param socket - The client socket - */ - public static logResponse(response: string, socket: plugins.net.Socket | plugins.tls.TLSSocket): void { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket - }; - - // Get the response code from the beginning of the response - const responseCode = response.substring(0, 3); - - // Log different levels based on response code - if (responseCode.startsWith('2') || responseCode.startsWith('3')) { - this.debug(`Response sent: ${response}`, clientInfo); - } else if (responseCode.startsWith('4')) { - this.warn(`Temporary error response: ${response}`, clientInfo); - } else if (responseCode.startsWith('5')) { - this.error(`Permanent error response: ${response}`, clientInfo); - } - - // Also log to console for easy debugging - console.log(`→ ${response}`); - } - - /** - * Log client connection event - * @param socket - The client socket - * @param eventType - Type of connection event (connect, close, error) - * @param session - The SMTP session - * @param error - Optional error object for error events - */ - public static logConnection( - socket: plugins.net.Socket | plugins.tls.TLSSocket, - eventType: 'connect' | 'close' | 'error', - session?: ISmtpSession, - error?: Error - ): void { - const clientInfo = { - remoteAddress: socket.remoteAddress, - remotePort: socket.remotePort, - secure: socket instanceof plugins.tls.TLSSocket, - sessionId: session?.id, - sessionState: session?.state - }; - - switch (eventType) { - case 'connect': - this.info(`New ${clientInfo.secure ? 'secure ' : ''}connection from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - - case 'close': - this.info(`Connection closed from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo); - break; - - case 'error': - this.error(`Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, { - ...clientInfo, - error - }); - break; - } - } - - /** - * Log security event - * @param level - Security log level - * @param type - Security event type - * @param message - Log message - * @param details - Event details - * @param ipAddress - Client IP address - * @param domain - Optional domain involved - * @param success - Whether the security check was successful - */ - public static logSecurityEvent( - level: SecurityLogLevel, - type: SecurityEventType, - message: string, - details: Record, - ipAddress?: string, - domain?: string, - success?: boolean - ): void { - // Map security log level to system log level - const logLevel: LogLevel = level === SecurityLogLevel.DEBUG ? 'debug' : - level === SecurityLogLevel.INFO ? 'info' : - level === SecurityLogLevel.WARN ? 'warn' : 'error'; - - // Log the security event - this.log(logLevel, message, { - component: 'smtp-security', - eventType: type, - success, - ipAddress, - domain, - ...details - }); - } -} - -/** - * Default instance for backward compatibility - */ -export const smtpLogger = SmtpLogger; \ No newline at end of file diff --git a/ts/mail/delivery/smtpserver/utils/validation.ts b/ts/mail/delivery/smtpserver/utils/validation.ts deleted file mode 100644 index 5aae12b..0000000 --- a/ts/mail/delivery/smtpserver/utils/validation.ts +++ /dev/null @@ -1,436 +0,0 @@ -/** - * SMTP Validation Utilities - * Provides validation functions for SMTP server - */ - -import { SmtpState } from '../interfaces.js'; -import { SMTP_PATTERNS } from '../constants.js'; - -/** - * Header injection patterns to detect malicious input - * These patterns detect common header injection attempts - */ -const HEADER_INJECTION_PATTERNS = [ - /\r\n/, // CRLF sequence - /\n/, // LF alone - /\r/, // CR alone - /\x00/, // Null byte - /\x0A/, // Line feed hex - /\x0D/, // Carriage return hex - /%0A/i, // URL encoded LF - /%0D/i, // URL encoded CR - /%0a/i, // URL encoded LF lowercase - /%0d/i, // URL encoded CR lowercase - /\\\n/, // Escaped newline - /\\\r/, // Escaped carriage return - /(?:subject|from|to|cc|bcc|reply-to|return-path|received|delivered-to|x-.*?):/i // Email headers -]; - -/** - * Detects header injection attempts in input strings - * @param input - The input string to check - * @param context - The context where this input is being used ('smtp-command' or 'email-header') - * @returns true if header injection is detected, false otherwise - */ -export function detectHeaderInjection(input: string, context: 'smtp-command' | 'email-header' = 'smtp-command'): boolean { - if (!input || typeof input !== 'string') { - return false; - } - - // Check for control characters and CRLF sequences (always dangerous) - const controlCharPatterns = [ - /\r\n/, // CRLF sequence - /\n/, // LF alone - /\r/, // CR alone - /\x00/, // Null byte - /\x0A/, // Line feed hex - /\x0D/, // Carriage return hex - /%0A/i, // URL encoded LF - /%0D/i, // URL encoded CR - /%0a/i, // URL encoded LF lowercase - /%0d/i, // URL encoded CR lowercase - /\\\n/, // Escaped newline - /\\\r/, // Escaped carriage return - ]; - - // Check control characters (always dangerous in any context) - if (controlCharPatterns.some(pattern => pattern.test(input))) { - return true; - } - - // For email headers, also check for header injection patterns - if (context === 'email-header') { - const headerPatterns = [ - /(?:subject|from|to|cc|bcc|reply-to|return-path|received|delivered-to|x-.*?):/i // Email headers - ]; - return headerPatterns.some(pattern => pattern.test(input)); - } - - // For SMTP commands, don't flag normal command syntax like "TO:" as header injection - return false; -} - -/** - * Sanitizes input by removing or escaping potentially dangerous characters - * @param input - The input string to sanitize - * @returns Sanitized string - */ -export function sanitizeInput(input: string): string { - if (!input || typeof input !== 'string') { - return ''; - } - - // Remove control characters and potential injection sequences - return input - .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars except \t, \n, \r - .replace(/\r\n/g, ' ') // Replace CRLF with space - .replace(/[\r\n]/g, ' ') // Replace individual CR/LF with space - .replace(/%0[aAdD]/gi, '') // Remove URL encoded CRLF - .trim(); -} -import { SmtpLogger } from './logging.js'; - -/** - * Validates an email address - * @param email - Email address to validate - * @returns Whether the email address is valid - */ -export function isValidEmail(email: string): boolean { - if (!email || typeof email !== 'string') { - return false; - } - - // Basic pattern check - if (!SMTP_PATTERNS.EMAIL.test(email)) { - return false; - } - - // Additional validation for common invalid patterns - const [localPart, domain] = email.split('@'); - - // Check for double dots - if (email.includes('..')) { - return false; - } - - // Check domain doesn't start or end with dot - if (domain && (domain.startsWith('.') || domain.endsWith('.'))) { - return false; - } - - // Check local part length (max 64 chars per RFC) - if (localPart && localPart.length > 64) { - return false; - } - - // Check domain length (max 253 chars per RFC - accounting for trailing dot) - if (domain && domain.length > 253) { - return false; - } - - return true; -} - -/** - * Validates the MAIL FROM command syntax - * @param args - Arguments string from the MAIL FROM command - * @returns Object with validation result and extracted data - */ -export function validateMailFrom(args: string): { - isValid: boolean; - address?: string; - params?: Record; - errorMessage?: string; -} { - if (!args) { - return { isValid: false, errorMessage: 'Missing arguments' }; - } - - // Check for header injection attempts - if (detectHeaderInjection(args)) { - SmtpLogger.warn('Header injection attempt detected in MAIL FROM command', { args }); - return { isValid: false, errorMessage: 'Invalid syntax - illegal characters detected' }; - } - - // Handle "MAIL FROM:" already in the args - let cleanArgs = args; - if (args.toUpperCase().startsWith('MAIL FROM')) { - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - cleanArgs = args.substring(colonIndex + 1).trim(); - } - } else if (args.toUpperCase().startsWith('FROM:')) { - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - cleanArgs = args.substring(colonIndex + 1).trim(); - } - } - - // Handle empty sender case '<>' - if (cleanArgs === '<>') { - return { isValid: true, address: '', params: {} }; - } - - // According to test expectations, validate that the address is enclosed in angle brackets - // Check for angle brackets and RFC-compliance - if (cleanArgs.includes('<') && cleanArgs.includes('>')) { - const startBracket = cleanArgs.indexOf('<'); - const endBracket = cleanArgs.indexOf('>', startBracket); - - if (startBracket !== -1 && endBracket !== -1 && startBracket < endBracket) { - const emailPart = cleanArgs.substring(startBracket + 1, endBracket).trim(); - const paramsString = cleanArgs.substring(endBracket + 1).trim(); - - // Handle empty sender case '<>' again - if (emailPart === '') { - return { isValid: true, address: '', params: {} }; - } - - // During testing, we should validate the email format - // Check for basic email format (something@somewhere) - if (!isValidEmail(emailPart)) { - return { isValid: false, errorMessage: 'Invalid email address format' }; - } - - // Parse parameters if they exist - const params: Record = {}; - if (paramsString) { - const paramRegex = /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g; - let match; - - while ((match = paramRegex.exec(paramsString)) !== null) { - const name = match[1].toUpperCase(); - const value = match[2] || ''; - params[name] = value; - } - } - - return { isValid: true, address: emailPart, params }; - } - } - - // If no angle brackets, the format is invalid for MAIL FROM - // Tests expect us to reject formats without angle brackets - - // For better compliance with tests, check if the argument might contain an email without brackets - if (isValidEmail(cleanArgs)) { - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; - } - - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; -} - -/** - * Validates the RCPT TO command syntax - * @param args - Arguments string from the RCPT TO command - * @returns Object with validation result and extracted data - */ -export function validateRcptTo(args: string): { - isValid: boolean; - address?: string; - params?: Record; - errorMessage?: string; -} { - if (!args) { - return { isValid: false, errorMessage: 'Missing arguments' }; - } - - // Check for header injection attempts - if (detectHeaderInjection(args)) { - SmtpLogger.warn('Header injection attempt detected in RCPT TO command', { args }); - return { isValid: false, errorMessage: 'Invalid syntax - illegal characters detected' }; - } - - // Handle "RCPT TO:" already in the args - let cleanArgs = args; - if (args.toUpperCase().startsWith('RCPT TO')) { - const colonIndex = args.indexOf(':'); - if (colonIndex !== -1) { - cleanArgs = args.substring(colonIndex + 1).trim(); - } - } else if (args.toUpperCase().startsWith('TO:')) { - cleanArgs = args.substring(3).trim(); - } - - // According to test expectations, validate that the address is enclosed in angle brackets - // Check for angle brackets and RFC-compliance - if (cleanArgs.includes('<') && cleanArgs.includes('>')) { - const startBracket = cleanArgs.indexOf('<'); - const endBracket = cleanArgs.indexOf('>', startBracket); - - if (startBracket !== -1 && endBracket !== -1 && startBracket < endBracket) { - const emailPart = cleanArgs.substring(startBracket + 1, endBracket).trim(); - const paramsString = cleanArgs.substring(endBracket + 1).trim(); - - // During testing, we should validate the email format - // Check for basic email format (something@somewhere) - if (!isValidEmail(emailPart)) { - return { isValid: false, errorMessage: 'Invalid email address format' }; - } - - // Parse parameters if they exist - const params: Record = {}; - if (paramsString) { - const paramRegex = /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g; - let match; - - while ((match = paramRegex.exec(paramsString)) !== null) { - const name = match[1].toUpperCase(); - const value = match[2] || ''; - params[name] = value; - } - } - - return { isValid: true, address: emailPart, params }; - } - } - - // If no angle brackets, the format is invalid for RCPT TO - // Tests expect us to reject formats without angle brackets - - // For better compliance with tests, check if the argument might contain an email without brackets - if (isValidEmail(cleanArgs)) { - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; - } - - return { isValid: false, errorMessage: 'Invalid syntax - angle brackets required' }; -} - -/** - * Validates the EHLO command syntax - * @param args - Arguments string from the EHLO command - * @returns Object with validation result and extracted data - */ -export function validateEhlo(args: string): { - isValid: boolean; - hostname?: string; - errorMessage?: string; -} { - if (!args) { - return { isValid: false, errorMessage: 'Missing domain name' }; - } - - // Check for header injection attempts - if (detectHeaderInjection(args)) { - SmtpLogger.warn('Header injection attempt detected in EHLO command', { args }); - return { isValid: false, errorMessage: 'Invalid domain name format' }; - } - - // Extract hostname from EHLO command if present in args - let hostname = args; - const match = args.match(/^(?:EHLO|HELO)\s+([^\s]+)$/i); - if (match) { - hostname = match[1]; - } - - // Check for empty hostname - if (!hostname || hostname.trim() === '') { - return { isValid: false, errorMessage: 'Missing domain name' }; - } - - // Basic validation - Be very permissive with domain names to handle various client implementations - // RFC 5321 allows a broad range of clients to connect, so validation should be lenient - - // Only check for characters that would definitely cause issues - const invalidChars = ['<', '>', '"', '\'', '\\', '\n', '\r']; - if (invalidChars.some(char => hostname.includes(char))) { - // During automated testing, we check for invalid character validation - // For production we could consider accepting these with proper cleanup - return { isValid: false, errorMessage: 'Invalid domain name format' }; - } - - // Support IP addresses in square brackets (e.g., [127.0.0.1] or [IPv6:2001:db8::1]) - if (hostname.startsWith('[') && hostname.endsWith(']')) { - // Be permissive with IP literals - many clients use non-standard formats - // Just check for closing bracket and basic format - return { isValid: true, hostname }; - } - - // RFC 5321 states we should accept anything as a domain name for EHLO - // Clients may send domain literals, IP addresses, or any other identification - // As long as it follows the basic format and doesn't have clearly invalid characters - // we should accept it to be compatible with a wide range of clients - - // The test expects us to reject 'invalid@domain', but RFC doesn't strictly require this - // For testing purposes, we'll include a basic check to validate email-like formats - if (hostname.includes('@')) { - // Reject email-like formats for EHLO/HELO command - return { isValid: false, errorMessage: 'Invalid domain name format' }; - } - - // Special handling for test with special characters - // The test "EHLO spec!al@#$chars" is expected to pass with either response: - // 1. Accept it (since RFC doesn't prohibit special chars in domain names) - // 2. Reject it with a 501 error (for implementations with stricter validation) - if (/[!@#$%^&*()+=\[\]{}|;:',<>?~`]/.test(hostname)) { - // For test compatibility, let's be permissive and accept special characters - // RFC 5321 doesn't explicitly prohibit these characters, and some implementations accept them - SmtpLogger.debug(`Allowing hostname with special characters for test: ${hostname}`); - return { isValid: true, hostname }; - } - - // Hostname validation can be very tricky - many clients don't follow RFCs exactly - // Better to be permissive than to reject valid clients - return { isValid: true, hostname }; -} - -/** - * Validates command in the current SMTP state - * @param command - SMTP command - * @param currentState - Current SMTP state - * @returns Whether the command is valid in the current state - */ -export function isValidCommandSequence(command: string, currentState: SmtpState): boolean { - const upperCommand = command.toUpperCase(); - - // Some commands are valid in any state - if (upperCommand === 'QUIT' || upperCommand === 'RSET' || upperCommand === 'NOOP' || upperCommand === 'HELP') { - return true; - } - - // State-specific validation - switch (currentState) { - case SmtpState.GREETING: - return upperCommand === 'EHLO' || upperCommand === 'HELO'; - - case SmtpState.AFTER_EHLO: - return upperCommand === 'MAIL' || upperCommand === 'STARTTLS' || upperCommand === 'AUTH' || upperCommand === 'EHLO' || upperCommand === 'HELO'; - - case SmtpState.MAIL_FROM: - case SmtpState.RCPT_TO: - if (upperCommand === 'RCPT') { - return true; - } - return currentState === SmtpState.RCPT_TO && upperCommand === 'DATA'; - - case SmtpState.DATA: - // In DATA state, only the data content is accepted, not commands - return false; - - case SmtpState.DATA_RECEIVING: - // In DATA_RECEIVING state, only the data content is accepted, not commands - return false; - - case SmtpState.FINISHED: - // After data is received, only new transactions or session end - return upperCommand === 'MAIL' || upperCommand === 'QUIT' || upperCommand === 'RSET'; - - default: - return false; - } -} - -/** - * Validates if a hostname is valid according to RFC 5321 - * @param hostname - Hostname to validate - * @returns Whether the hostname is valid - */ -export function isValidHostname(hostname: string): boolean { - if (!hostname || typeof hostname !== 'string') { - return false; - } - - // Basic hostname validation - // This is a simplified check, full RFC compliance would be more complex - return /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(hostname); -} \ No newline at end of file diff --git a/ts/mail/routing/classes.unified.email.server.ts b/ts/mail/routing/classes.unified.email.server.ts index 21a370d..0df0dbf 100644 --- a/ts/mail/routing/classes.unified.email.server.ts +++ b/ts/mail/routing/classes.unified.email.server.ts @@ -46,7 +46,6 @@ import { Email } from '../core/classes.email.js'; import { DomainRegistry } from './classes.domain.registry.js'; import { DnsManager } from './classes.dns.manager.js'; import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js'; -import { createSmtpServer } from '../delivery/smtpserver/index.js'; import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js'; import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js'; import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js'; @@ -391,13 +390,6 @@ export class UnifiedEmailServer extends EventEmitter { await this.checkAndRotateDkimKeys(); logger.log('info', 'DKIM key rotation check completed'); - // Skip server creation in socket-handler mode - if (this.options.useSocketHandler) { - logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)'); - this.emit('started'); - return; - } - // Ensure we have the necessary TLS options const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath; @@ -485,54 +477,6 @@ export class UnifiedEmailServer extends EventEmitter { } } - /** - * Handle a socket from smartproxy in socket-handler mode - * @param socket The socket to handle - * @param port The port this connection is for (25, 587, 465) - */ - public async handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise { - if (!this.options.useSocketHandler) { - logger.log('error', 'handleSocket called but useSocketHandler is not enabled'); - socket.destroy(); - return; - } - - logger.log('info', `Handling socket for port ${port}`); - - // Create a temporary SMTP server instance for this connection - // We need a full server instance because the SMTP protocol handler needs all components - const smtpServerOptions = { - port, - hostname: this.options.hostname, - key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined, - cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined - }; - - // Create the SMTP server instance - const smtpServer = createSmtpServer(this, smtpServerOptions); - - // Get the connection manager from the server - const connectionManager = (smtpServer as any).connectionManager; - - if (!connectionManager) { - logger.log('error', 'Could not get connection manager from SMTP server'); - socket.destroy(); - return; - } - - // Determine if this is a secure connection - // Port 465 uses implicit TLS, so the socket is already secure - const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket; - - // Pass the socket to the connection manager - try { - await connectionManager.handleConnection(socket, isSecure); - } catch (error) { - logger.log('error', `Error handling socket connection: ${error.message}`); - socket.destroy(); - } - } - /** * Stop the unified email server */ @@ -639,6 +583,11 @@ export class UnifiedEmailServer extends EventEmitter { session.user = { username: authenticatedUser }; } + // Attach pre-computed security results from Rust in-process pipeline + if (data.securityResults) { + (session as any)._precomputedSecurityResults = data.securityResults; + } + // Process the email through the routing system await this.processEmailByMode(rawMessageBuffer, session); @@ -691,24 +640,34 @@ export class UnifiedEmailServer extends EventEmitter { } /** - * Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge. - * Falls back gracefully if the bridge is not running. + * Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results + * or falling back to IPC call if no pre-computed results are available. */ private async verifyInboundSecurity(email: Email, session: IExtendedSmtpSession): Promise { try { - const rawMessage = session.emailData || email.toRFC822String(); - const result = await this.rustBridge.verifyEmail({ - rawMessage, - ip: session.remoteAddress, - heloDomain: session.clientHostname || '', - hostname: this.options.hostname, - mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '', - }); + // Check for pre-computed results from Rust in-process security pipeline + const precomputed = (session as any)._precomputedSecurityResults; + let result: any; + + if (precomputed) { + logger.log('info', 'Using pre-computed security results from Rust in-process pipeline'); + result = precomputed; + } else { + // Fallback: IPC round-trip to Rust (for backward compat / handleSocket mode) + const rawMessage = session.emailData || email.toRFC822String(); + result = await this.rustBridge.verifyEmail({ + rawMessage, + ip: session.remoteAddress, + heloDomain: session.clientHostname || '', + hostname: this.options.hostname, + mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '', + }); + } // Apply DKIM result headers if (result.dkim && result.dkim.length > 0) { const dkimSummary = result.dkim - .map(d => `${d.status}${d.domain ? ` (${d.domain})` : ''}`) + .map((d: any) => `${d.status}${d.domain ? ` (${d.domain})` : ''}`) .join(', '); email.addHeader('X-DKIM-Result', dkimSummary); } @@ -737,6 +696,31 @@ export class UnifiedEmailServer extends EventEmitter { } } + // Apply content scan results (from pre-computed pipeline) + if (result.contentScan) { + const scan = result.contentScan; + if (scan.threatScore > 0) { + email.addHeader('X-Spam-Score', String(scan.threatScore)); + if (scan.threatType) { + email.addHeader('X-Spam-Type', scan.threatType); + } + if (scan.threatScore >= 50) { + email.mightBeSpam = true; + logger.log('warn', `Content scan threat score ${scan.threatScore} (${scan.threatType}) — marking as potential spam`); + } + } + } + + // Apply IP reputation results (from pre-computed pipeline) + if (result.ipReputation) { + const rep = result.ipReputation; + email.addHeader('X-IP-Reputation-Score', String(rep.score)); + if (rep.is_spam) { + email.mightBeSpam = true; + logger.log('warn', `IP ${rep.ip} flagged by reputation check (score=${rep.score}) — marking as potential spam`); + } + } + logger.log('info', `Inbound security verified for email from ${session.remoteAddress}: DKIM=${result.dkim?.[0]?.status ?? 'none'}, SPF=${result.spf?.result ?? 'none'}, DMARC=${result.dmarc?.action ?? 'none'}`); } catch (err) { logger.log('warn', `Inbound security verification failed: ${(err as Error).message} — accepting email`);