emailengine-app 2.71.0 → 2.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,233 @@
1
+ # EmailEngine - Feature Map & Prioritized Test Plan
2
+
3
+ Generated 2026-06-16. Read-only audit across five feature areas (email sync engine, REST API, admin UI, background workers/queues, core libraries). No source or tests were modified.
4
+
5
+ Baseline: ~70k LOC source (lib + workers + server.js) vs ~16.1k LOC unit tests, ~1.8k integration, 180 e2e.
6
+
7
+ ---
8
+
9
+ ## 1. Feature & functionality map
10
+
11
+ EmailEngine is a multi-threaded (Worker Threads), Redis-backed email sync platform exposing a REST API + admin UI.
12
+
13
+ ### Email sync engine (lib/email-client/*, workers/imap.js, lib/account*)
14
+ - Three providers behind a shared `BaseClient`: IMAP (ImapFlow + IDLE), Gmail API (Pub/Sub + 10-min poll fallback), Outlook/Graph (subscription webhooks, 3-day renewal).
15
+ - Message ops: list/get/getText/getRawMessage/getAttachment/update/move/delete/upload (+ bulk variants).
16
+ - Mailbox ops: list/create/modify/delete; special-use resolution; provider path encoding (Outlook `/`,`%`; Gmail label<->folder).
17
+ - IMAP sync: `determineSyncStrategy` -> none/partial(condstore|simple)/full; UIDValidity change -> full resync + index reseed; lost-index silent reseed; batched fetch with retry/backoff.
18
+ - Reliability: exponential reconnect/backoff; auth-failure tracking + auto-disable (`setErrorState`); error classification -> account state (authenticationError vs connectError); state machine (account-state.js).
19
+ - Send path: `queueMessage`/`submitMessage` (SMTP + Gmail/Outlook API), idempotency keys, post-delivery Sent-folder upload + Answered/Forwarded flags + gateway stats.
20
+
21
+ ### REST API (lib/api-routes/*, ~8.1k LOC, 20 modules)
22
+ - Account + message + mailbox CRUD/ops; submit; outbox; export; gateways; templates; tokens; settings; oauth2-apps; webhook-routes; blocklist; chat (doc-store-gated); delivery-test; license; pubsub; stats; bull-board.
23
+ - Single `api-token` bearer strategy: scope derived from route tags (all REST modules use `api`); account-token binding restricts to own `{account}`; IP/referrer allowlist + per-token rate limit; `disableTokens` open-API bypass.
24
+
25
+ ### Admin UI (lib/ui-routes/*, lib/routes-ui.js, workers/api.js; ~128 routes)
26
+ - Auth: session cookie (`ee`), TOTP, passkeys/WebAuthn (intentionally bypasses TOTP), OKTA OAuth2, pbkdf2 password, `passwordVersion` force-logout.
27
+ - CSRF via @hapi/crumb (skip-list: api/metrics/static/external tags).
28
+ - Config screens: webhooks, service, AI, logging/Sentry, license, OAuth apps, network/proxy, SMTP/IMAP-proxy servers, document-store (deprecated, self-gates off).
29
+ - Public (auth:false) flows: add-account wizard (HMAC-signed form), OAuth start/redirect (account creation), unsubscribe.
30
+
31
+ ### Background workers & servers (workers/*, server.js, lib/webhooks.js, lib/export.js)
32
+ - Submit worker: BullMQ delivery, retry/backoff classification (`NON_RETRYABLE_CODES`, 5xx-not-503 -> discard).
33
+ - Webhooks worker: BullMQ delivery (10 attempts, HMAC sig), custom routes with sandboxed `fn`/`map` (SubScript/vm).
34
+ - Export worker: phased NDJSON export, gzip + optional AES-256-GCM, recovery, atomic concurrency Lua.
35
+ - SMTP server (MSA) + IMAP proxy: three auth modes each (global password / scoped hex token / header or passthrough).
36
+ - Documents worker: deprecated Elasticsearch indexing, gated off by default.
37
+ - server.js main process: worker spawn/health/restart, account assignment (load-aware round-robin + rendezvous reassignment), RPC with timeout.
38
+
39
+ ### Core libraries (lib/*)
40
+ - oauth2-apps.js (OAuth2 app config + token orchestration, 5 providers, WIF, Pub/Sub IAM), oauth/* providers.
41
+ - tokens.js (API + session tokens, hashed storage), encrypt.js (AES-256-GCM), settings.js (encrypted keys).
42
+ - tools.js (binary sync-state serialize/unserialize, license verify, signed-form HMAC, glob matcher, redaction).
43
+ - bounce-detect.js / arf-detect.js (DSN + FBL parsing), add-trackers.js, get-raw-email.js, autodetect-imap-settings.js, rewrite-text-nodes.js, templates.js, redis-operations.js.
44
+
45
+ ---
46
+
47
+ ## 2. Current coverage assessment
48
+
49
+ Strong (real source imported, good cases):
50
+ - IMAP sync-strategy helpers, lost-index recovery, processChanges, null/event guards, mailbox-listing diff, Outlook folder encoding, label search filters, account-state helpers (email sync).
51
+ - Gmail Pub/Sub manager recovery, WIF external-account signer, OAuth token-request encoding + scope/nonce/error-status, tokens.js, encrypt.js, stream-encrypt, export.js internals + concurrency Lua, IMAP-proxy protocol hardening, redis-operations helpers, bounce/ARF structured (RFC 3464) parsing.
52
+
53
+ Thin or absent:
54
+ - Entire api-routes/ and ui-routes/ handler execution: exercised only via integration smoke (401-without-token + 200-with-`*`-token) and one e2e happy path. No scoped/cross-tenant token tests, no CSRF assertion, no POST mutation handlers, no parameterized routes.
55
+ - Send/submit pipeline, auth auto-disable, reconnect/backoff, Gmail history sync, Outlook subscription lifecycle: only the flaky non-hermetic live integration suite.
56
+ - SubScript sandbox, server.js account assignment/reassignment, SMTP/IMAP-proxy auth layers: zero tests.
57
+ - Large files barely touched: outlook-client.js (4584), gmail-client.js (3079), base-client.js (3279), imap/mailbox.js (3255), workers/api.js (3185), schemas.js (2497), tools.js (2372, ~27% of exports + all binary/crypto/async untested), oauth2-apps.js handler class, autodetect-imap-settings.js (779, 0 tests), get-raw-email.js (552, 0).
58
+
59
+ ---
60
+
61
+ ## 3. Cross-cutting findings
62
+
63
+ ### 3a. "Illusory coverage" - tests that re-implement source instead of importing it
64
+ These pass green even when the shipping code breaks. Highest ROI to fix:
65
+ - `worker-not-available-test.js` - copies the submit discard predicate; omits the `NON_RETRYABLE_CODES` (EAUTH/EOAUTH2/ETLS/...) branch entirely.
66
+ - `autoreply-test.js` - re-defines `isAutoreply`; real `BaseClient.isAutoreply` (base-client.js:2173) untested.
67
+ - `complaint-test.js` - re-implements `mightBeAComplaint`; real wrapper untested (underlying arf-detect IS tested).
68
+ - `email-client-test.js` - defines its own PageCursor + constants; real Gmail `PageCursor` untested.
69
+ - `retry-logic-test.js` - defines its own `calculateBackoffDelay`/`simulateRequestWithRetry`; real Outlook `requestWithRetry` (outlook-client.js:222) untested.
70
+
71
+ ### 3b. Auth boundaries asserted only at "401 without any token"
72
+ Scope enforcement, cross-account binding, CSRF, session invalidation, and the admin auth gate are never positively tested with real (scoped / cross-tenant / crumb-less) requests. The test prepared token decodes to `scopes:["*"]`, so every integration/e2e run is a full-root token.
73
+
74
+ ### 3c. Latent bugs surfaced during the audit (not tests - fix separately)
75
+ - arf-detect.js:112 - reads `report['source-ip']` but writes `report.arf['source-ip']` (source IP never populated).
76
+ - worker-not-available-test.js silently omits a whole discard branch (see 3a).
77
+ - `mightBeABounce`/`mightBeAComplaint`/`mightBeDSNResponse` are duplicated in both base-client.js (2832-2890) and imap/mailbox.js (3056-3204) - drift risk; consolidate.
78
+
79
+ ---
80
+
81
+ ## 4. Prioritized test backlog
82
+
83
+ Priority = blast radius x regression likelihood x security sensitivity, weighted by cost-to-implement (cheap high-value first).
84
+
85
+ ### P0 - Do first
86
+
87
+ | # | Test | Why | Type |
88
+ |---|------|-----|------|
89
+ | 1 | Fix the 5 "illusory coverage" tests to import real exports (submit discard incl. NON_RETRYABLE_CODES, isAutoreply, mightBeAComplaint, Gmail PageCursor, Outlook requestWithRetry) | False green today; cheapest possible win; unblocks trust in the suite | unit |
90
+ | 2 | SubScript sandbox (lib/sub-script.js) - timeout enforcement, no process/require/global escape, env/scriptEnv injection, payload isolation, compile-error caching | Arbitrary user JS runs in-process on every webhook; zero tests | unit |
91
+ | 3 | tools.serialize / unserialize round-trip - UID+flags+modseq+msgpack, D/N sentinels, BigInt modseq | Corrupts sync state for ALL accounts (dup/dropped messages + webhooks); zero tests; pure/cheap | unit |
92
+ | 4 | API token scope enforcement + cross-account binding (workers/api.js:1106,1121-1170) - smtp/imap-proxy-only token must 403 on /v1/*; account token must 403 on a different account; template special-casing | Core security boundaries, tested only at 401-without-token; provision tokens via lib/tokens.provision() in existing smoke harness | integration |
93
+ | 5 | Admin UI auth gate + CSRF + passwordVersion - boot server WITH admin password: anonymous /admin/* -> 302 login; crumb-less POST -> 403; passwordVersion bump invalidates old cookie | Closes the 3 highest-blast-radius UI gaps at once; a regression making admin world-readable passes all current tests | integration |
94
+
95
+ ### P1 - High
96
+
97
+ | # | Test | Why | Type |
98
+ |---|------|-----|------|
99
+ | 6 | Send/submit pipeline + idempotency (base-client.js submitMessage/queueMessageEntry/handleSubmitError, checkIdempotencyKey) - permanent-5xx vs transient, Sent-folder upload, reference flags, gateway stats | Data loss / duplicate sends; only happy-path live coverage | unit + 1 integration error path |
100
+ | 7 | server.js account assignment & reassignment (assignAccounts, rendezvous reassign trigger, call-failure rollback, worker-exit -> unassigned, 10s failsafe, health -> restart) | Highest orchestration blast radius (one routing bug hits all accounts); zero tests | unit (extract) / integration with fake workers |
101
+ | 8 | setErrorState auth-failure auto-disable (base-client.js:383) - error-count, same-error dedupe, MAX_IMAP_AUTH_FAILURE_TIME threshold, disable+close | Safety mechanism vs reconnect storms; untested | unit (mock redis txn + clock) |
102
+ | 9 | SMTP server + IMAP-proxy AUTH layers (workers/smtp.js:112-246, imapproxy/imap-server.js:153-247) - global password, scoped hex token, account==username, IP restriction, API-only rejection | Auth-bypass surface; protocol hardening is tested but auth is not | integration |
103
+ | 10 | Bulk message mutate/delete REST (message-routes.js PUT /messages, /messages/move, /messages/delete?force) | Highest data-loss blast radius among untested endpoints; matches prior Outlook bulk-delete finding | integration |
104
+ | 11 | bounce-detect text-heuristic fallback (~13 vendor matchers: Exim/Postfix/Google/KDDI/Verizon/James/generic 5xx, bounce-detect.js:417-832) | Largest real-world regression surface; regex-fragile (ReDoS-hardened, no regression test) | unit (fixtures) |
105
+ | 12 | OAuth2AppsHandler stateful core (oauth2-apps.js create/update/del encryption, getServiceAccessToken caching/lock:1641, getClient per-provider:1422) + Gmail/Outlook checkForFlags error->flag mapping | Mis-stored/plaintext creds, token stampede, wrong auth flags; only leaf helpers tested today | unit |
106
+ | 13 | Gmail history sync (processHistory/processHistoryEntry, gmail-client.js:2737-3001) + Outlook subscription lifecycle (renewSubscription/ensureSubscription/syncMissedMessages, outlook-client.js:3224-3971) | Core real-time correctness; only flaky live coverage; silent event loss | unit (mock request) |
107
+ | 14 | IMAP reconnect/backoff + error->state classification (imap-client.js reconnect:910, start() catch:1557) | Timing-sensitive; high regression likelihood | unit (fake timers) |
108
+
109
+ ### P2 - Medium
110
+
111
+ | # | Test | Why | Type |
112
+ |---|------|-----|------|
113
+ | 15 | bull-board auth (/admin/bull-board/* relies only on default session guard) | Queue job payloads (recipients/message data) leak if guard regresses | integration |
114
+ | 16 | chat / unified-search REST doc-store gating (/v1/chat 404 + /v1/unified/search runtime-404 when feature off) | doc-store-disabled test covers UI only | route-table + integration |
115
+ | 17 | Secret masking on read (GET /v1/account, /v1/oauth2 -> ******) | Assert secrets never round-trip cleartext | integration |
116
+ | 18 | Public account-setup HMAC verification (/accounts/new/imap/server, POST /v1/authentication/form) + signed-form replay/TTL/nonce | Unauth account creation + credential write if sig check regresses | integration + unit |
117
+ | 19 | settings.js secret encrypt/decrypt + POST /v1/settings write broadcast | Silent corruption of stored OAuth secrets/serviceSecret; global blast radius | unit + integration |
118
+ | 20 | Security redaction units: formatPartialSecretKey, add-trackers redirect rewriting (open-redirect via /redirect), get-raw-email X-EE-*/BCC stripping | Header/secret leakage on every outbound message | unit |
119
+ | 21 | UIDVALIDITY-change full-resync branch (mailbox.js onOpen:2102-2127) | Distinct from lost-index reseed; wrong handling re-floods or drops messages | unit |
120
+ | 22 | Export recovery + encryption end-to-end (markInterruptedAsFailed, gzip->encrypt->decryptable artifact, lease extension) | Classifiers tested in isolation; recovery flow not | integration |
121
+ | 23 | Webhook custom-route fn/map filter/transform end-to-end + payload formatting/truncation + route-disable-on-compile-error + HMAC sig value | Handler cache tested; actual filter/transform behavior not | unit/integration |
122
+ | 24 | autodetect-imap-settings.js (779 LOC, 0 tests) - provider table, autoconfig XML substitution, escapeXml autodiscover injection, SRV parsing | Every new-account config; high churn; injection surface | unit (stub dns/undici) |
123
+ | 25 | smtp-pool-manager generatePoolKey (credential isolation) + cleanupIdlePools | Pool-key collision could cross-send via wrong account | unit |
124
+ | 26 | Broad POST mutation + parameterized route execution (token provision/delete, account delete/edit, gateway/oauth/webhook/template CRUD, network config) with a prepared session | ~20 handlers only snapshot-checked, never run | integration / e2e |
125
+ | 27 | gateway CRUD + delivery-stats hincrby + encrypted-pass roundtrip | Holds SMTP creds; only route-name smoke today | unit/integration |
126
+
127
+ ### P3 - Lower / opportunistic
128
+
129
+ - message-builder.js + notification-handler.js builders (synchronous, cheap; payload contract).
130
+ - export-routes / outbox / blocklist / template / license HTTP behavior (404 + force semantics + download stream).
131
+ - redis-url parsing, redis-scan-delete batching, reconnection-manager backoff cap, rate-limit bucket math, capa CSV parser, templates CRUD.
132
+ - arf-detect edge branches (and fix the source-ip read/write bug, 3c).
133
+ - OKTA login flow, disableTokens open-API mode, sess_ browse round-trip, requireTotp path-allowlist correctness.
134
+ - mergeObjects prototype-pollution guard, comparePattern/matcher glob+LRU, checkLicense ECDSA verify.
135
+ - Documents/ES enabled path (deprecated, off by default - lowest priority).
136
+
137
+ ---
138
+
139
+ ## 5. Suggested sequencing
140
+
141
+ 1. Land P0 #1 (illusory-coverage fixes) immediately - cheapest, removes false confidence, and #1's submit/autoreply/complaint cases overlap with P1 #6/#11.
142
+ 2. P0 #2/#3 (SubScript, serialize) - pure units, high blast radius, no infra needed.
143
+ 3. P0 #4/#5 + P2 #15/#16/#17/#26 share one new integration harness: boot the test server with an admin password and provision scoped tokens via lib/tokens.provision(). Build that harness once, then the auth/scope/CSRF/masking/gating tests are incremental.
144
+ 4. P1 #6-#14 are the email-engine correctness core; most are unit-testable with mocked redis/transport/request and benefit from extracting pure logic out of the giant client files as you go.
145
+
146
+ ---
147
+
148
+ ## 6. Implementation status (2026-06-16)
149
+
150
+ Branch `test-coverage-p0-p1`. Each test file was verified to pass individually
151
+ (the full local suite cannot run all files in parallel on a dev machine due to
152
+ process-isolation + Redis connection accumulation; CI runs the unit tier in a
153
+ clean dedicated job). Source changes are additive exports or behavior-preserving
154
+ extractions, verified not to break existing consumers.
155
+
156
+ Done (committed):
157
+ - P0 #1 - illusory-coverage tests now exercise real code (submit discard via new
158
+ lib/delivery-error.js incl. NON_RETRYABLE_CODES; isAutoreply; Mailbox
159
+ mightBeAComplaint + base-client drift; Gmail PageCursor; Outlook
160
+ requestWithRetry incl. transient-network branch).
161
+ - P0 #2 - SubScript sandbox (timeout, context isolation, env, compile cache).
162
+ - P0 #3 - tools.serialize/unserialize round-trip.
163
+ - P0 #4 - API token scope enforcement + cross-account binding (integration).
164
+ - P0 #5 - admin CSRF crumb enforcement (integration). [partial]
165
+ - P1 #8 - setErrorState auth-failure tracking + IMAP auto-disable.
166
+ - P1 #11 - bounce-detect text-heuristic fallback.
167
+ - P1 #12 - Gmail/Outlook checkForFlags/checkForUserFlags error mapping. [partial]
168
+ - P1 #14 - ReconnectionManager backoff/jitter/reset. [partial]
169
+ - P1 #6 - idempotency-key handling (duplicate-submission guard). [partial]
170
+
171
+ Remaining (deferred, with reason):
172
+ - P0 #5 remainder - anonymous /admin/* -> 302 redirect and passwordVersion
173
+ session invalidation need an isolated server WITH an admin password; belongs
174
+ in the Playwright e2e suite (cannot verify locally without Chromium + the
175
+ external trial endpoint). The shared integration server is password-less by
176
+ design, so these must not mutate its global auth state.
177
+ - P1 #6 remainder - full submitMessage pipeline (permanent-vs-transient retry,
178
+ Sent-folder upload, reference flags, gateway stats): large surface in a
179
+ 3.3k-line client, heavy transport/Redis mocking.
180
+ - P1 #7 - server.js account assignment/reassignment: highest blast radius but
181
+ the orchestration is entangled with module state; needs logic extraction (a
182
+ refactor of a critical orchestrator) or a fake-worker harness.
183
+ - P1 #9 - SMTP server + IMAP-proxy auth: those servers are not enabled in the
184
+ shared test config; needs a dedicated server boot with them enabled.
185
+ - P1 #10 - bulk message mutate/delete REST: needs a connected mailbox with
186
+ messages; not meaningfully testable against the credential-less test server.
187
+ - P1 #12 remainder - OAuth2AppsHandler CRUD encryption / getServiceAccessToken
188
+ caching / getClient: stateful, needs Redis + secret + provider mocking.
189
+ - P1 #13 - Gmail history sync + Outlook subscription lifecycle: heavy mocking of
190
+ the 3k-4.5k-line client classes.
191
+ - P1 #14 remainder - imap-client reconnect() method + start() error->state
192
+ classification: needs an IMAPClient instance with a mocked ImapFlow + fake
193
+ timers.
194
+
195
+ Guiding constraint: no unverified or fragile tests were committed. The deferred
196
+ items each require infrastructure (extra server, live mailbox, orchestrator
197
+ refactor, or large-class mocking) beyond what a test can assert reliably in this
198
+ environment.
199
+
200
+ ## 7. Deferred-item follow-up (2026-06-16, same branch)
201
+
202
+ A second pass converted most of the deferred items into verified tests by
203
+ extracting testable units (leaf workers only - never server.js) and using the
204
+ real handlers against the test Redis:
205
+
206
+ Done (committed):
207
+ - P1 #12 remainder - OAuth2AppsHandler create/get/update/list/del with
208
+ credential encryption at rest (oauth2-apps-crud-test.js).
209
+ - P1 #9 - SMTP auth (extracted lib/smtp-auth.js) and IMAP-proxy auth (extracted
210
+ lib/imap-proxy-auth.js): rejection paths (password/token account/scope/IP) and
211
+ accept paths, plus the IMAP-proxy API-only-account ACCOUNTDISABLED rejection.
212
+ - P1 #14 remainder - IMAPClient.reconnect() guard logic (no double-connect /
213
+ reconnect-storm), via the prototype with start() stubbed.
214
+ - P1 #13 (partial) - Gmail flag/label mapping (flagToLabel/flagsToLabelIds incl.
215
+ inverse \Seen<->UNREAD and set precedence) and Outlook parseExpirationDate.
216
+ - P1 #6 (decision logic) - message-builder send-pipeline deciders
217
+ (SentMailCopyDecider, ProviderMessageIdHandler, SmtpErrorBuilder,
218
+ NetworkRoutingBuilder, NotificationBuilder).
219
+
220
+ Still remaining (genuinely blocked):
221
+ - P1 #7 - server.js account assignment: the assignAccounts loop is entangled
222
+ with module state and per-iteration rollback; a faithful extraction risks
223
+ changing assignment behavior on worker-crash recovery (highest blast radius).
224
+ Not done autonomously - needs a reviewed refactor. The consistency-critical
225
+ half (rendezvous reassignment) is already covered by tools-test.js.
226
+ - P1 #6 remainder - the full submitMessage orchestration (actual SMTP send +
227
+ retry loop) - decision logic is now covered; the glue needs integration/e2e.
228
+ - P1 #13 remainder - Gmail processHistoryEntry / Outlook renew-subscription
229
+ orchestration: multi-step async over 3k-4.5k-line client classes.
230
+ - P0 #5 remainder / P1 #10 - e2e auth-gate + passwordVersion and bulk-message
231
+ REST: need the Playwright e2e instance (isolated server with a password and a
232
+ live mailbox) which depends on Chromium + external endpoints not reliably
233
+ reachable here.
@@ -1,7 +1,7 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Content-Type: text/plain; charset=ascii\n"
4
- "POT-Creation-Date: 2026-06-15 18:35+0000\n"
4
+ "POT-Creation-Date: 2026-06-19 17:14+0000\n"
5
5
 
6
6
  #: views/error.hbs:4
7
7
  #: workers/api.js:2957
package/workers/smtp.js CHANGED
@@ -7,7 +7,7 @@ const config = require('@zone-eu/wild-config');
7
7
  const logger = require('../lib/logger');
8
8
 
9
9
  const { getDuration, emitChangeEvent, readEnvValue, threadStats, loadTlsConfig, getByteSize } = require('../lib/tools');
10
- const { matchIp } = require('../lib/utils/network');
10
+ const { createSmtpAuthHandler } = require('../lib/smtp-auth');
11
11
 
12
12
  const { initSentry } = require('../lib/sentry');
13
13
  initSentry('smtp');
@@ -20,7 +20,6 @@ const getSecret = require('../lib/get-secret');
20
20
  const { Splitter, Joiner } = require('@zone-eu/mailsplit');
21
21
  const { HeadersRewriter } = require('../lib/headers-rewriter');
22
22
  const settings = require('../lib/settings');
23
- const tokens = require('../lib/tokens');
24
23
 
25
24
  const { encrypt, decrypt } = require('../lib/encrypt');
26
25
  const { Certs } = require('@postalsys/certs');
@@ -109,89 +108,10 @@ for (let level of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
109
108
  };
110
109
  }
111
110
 
112
- async function onAuth(auth, session) {
113
- if (!session.eeAuthEnabled) {
114
- throw new Error('Authentication not enabled');
115
- }
116
-
117
- let account = auth.username;
118
-
119
- let smtpPassword = await settings.get('smtpServerPassword');
120
- let authPass = false;
121
-
122
- if (!smtpPassword || auth.password !== smtpPassword) {
123
- if (/^[0-9a-f]{64}$/i.test(auth.password)) {
124
- // fallback to tokens
125
- let tokenData;
126
- try {
127
- tokenData = await tokens.get(auth.password, false, { log: true, remoteAddress: session.remoteAddress });
128
- } catch (err) {
129
- // ignore?
130
- }
131
-
132
- if (tokenData) {
133
- if (tokenData.account && tokenData.account !== auth.username) {
134
- throw new Error('Access denied, invalid username');
135
- }
136
-
137
- if (tokenData.scopes && !tokenData.scopes.includes('smtp') && !tokenData.scopes.includes('*')) {
138
- logger.error({
139
- msg: 'Trying to use invalid scope for a token',
140
- tokenAccount: tokenData.account,
141
- tokenId: tokenData.id,
142
- account,
143
- requestedScope: 'smtp',
144
- scopes: tokenData.scopes
145
- });
146
-
147
- throw new Error('Access denied, invalid scope');
148
- }
149
-
150
- if (tokenData.restrictions && tokenData.restrictions.addresses && !matchIp(session.remoteAddress, tokenData.restrictions.addresses)) {
151
- logger.error({
152
- msg: 'Trying to use invalid IP for a token',
153
- tokenAccount: tokenData.account,
154
- tokenId: tokenData.id,
155
- account,
156
- remoteAddress: session.remoteAddress,
157
- addressAllowlist: tokenData.restrictions.addresses
158
- });
159
-
160
- throw new Error('Access denied, traffic not accepted from this IP');
161
- }
162
-
163
- authPass = true;
164
- }
165
- }
166
-
167
- if (!authPass) {
168
- throw new Error('Failed to authenticate user');
169
- }
170
- }
171
-
172
- let accountObject = new Account({ account, redis, call, secret: await getSecret() });
173
- let accountData;
174
- try {
175
- accountData = await accountObject.loadAccountData();
176
- } catch (err) {
177
- let respErr = new Error('Failed to authenticate user');
178
-
179
- if (!err.output || err.output.statusCode !== 404) {
180
- // only log non-obvious errors
181
- logger.error({ msg: 'Failed to load account data', account: auth.username, err });
182
- respErr.statusCode = 454;
183
- }
184
-
185
- throw respErr;
186
- }
187
-
188
- if (!accountData) {
189
- throw new Error('Failed to authenticate user');
190
- }
191
-
192
- ACCOUNT_CACHE.set(session, accountObject);
193
- return { user: accountData.account };
194
- }
111
+ // Authentication logic lives in lib/smtp-auth.js so it can be unit tested
112
+ // without booting this worker. The shared ACCOUNT_CACHE and call() are injected
113
+ // so onAuth caches the Account for later processing steps.
114
+ const onAuth = createSmtpAuthHandler({ accountCache: ACCOUNT_CACHE, call });
195
115
 
196
116
  function processMessage(stream, session, meta) {
197
117
  meta = meta || {};
package/workers/submit.js CHANGED
@@ -42,15 +42,7 @@ const SUBMIT_QC = (readEnvValue('EENGINE_SUBMIT_QC') && Number(readEnvValue('EEN
42
42
 
43
43
  const SUBMIT_DELAY = getDuration(readEnvValue('EENGINE_SUBMIT_DELAY') || config.submitDelay) || null;
44
44
 
45
- const NON_RETRYABLE_CODES = new Set([
46
- 'EAUTH', // authentication failed
47
- 'ENOAUTH', // no credentials provided
48
- 'EOAUTH2', // OAuth2 token failure
49
- 'ETLS', // TLS handshake failed
50
- 'EENVELOPE', // invalid sender/recipients
51
- 'EMESSAGE', // message content error
52
- 'EPROTOCOL' // SMTP protocol mismatch
53
- ]);
45
+ const { shouldDiscardJob } = require('../lib/delivery-error');
54
46
 
55
47
  let callQueue = new Map();
56
48
  let mids = 0;
@@ -288,9 +280,7 @@ const submitWorker = new Worker(
288
280
  // ignore
289
281
  }
290
282
 
291
- const isPermanentSmtp = err.statusCode >= 500 && err.statusCode !== 503;
292
- const isPermanentCode = NON_RETRYABLE_CODES.has(err.code);
293
- if ((isPermanentSmtp || isPermanentCode) && job.attemptsMade < job.opts.attempts) {
283
+ if (shouldDiscardJob(err, job)) {
294
284
  try {
295
285
  // do not retry after 5xx error (except 503 which is transient)
296
286
  await job.discard();