emailengine-app 2.61.1 → 2.61.3
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.
- package/CHANGELOG.md +17 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account/account-state.js +248 -0
- package/lib/account.js +45 -193
- package/lib/api-routes/account-routes.js +1023 -0
- package/lib/api-routes/message-routes.js +1377 -0
- package/lib/consts.js +12 -2
- package/lib/email-client/base-client.js +282 -771
- package/lib/email-client/gmail/gmail-api.js +243 -0
- package/lib/email-client/gmail-client.js +145 -53
- package/lib/email-client/imap/mailbox.js +24 -698
- package/lib/email-client/imap/sync-operations.js +812 -0
- package/lib/email-client/imap-client.js +1 -1
- package/lib/email-client/message-builder.js +566 -0
- package/lib/email-client/notification-handler.js +314 -0
- package/lib/email-client/outlook/graph-api.js +326 -0
- package/lib/email-client/outlook-client.js +159 -113
- package/lib/email-client/smtp-pool-manager.js +196 -0
- package/lib/imapproxy/imap-server.js +3 -12
- package/lib/oauth/gmail.js +4 -4
- package/lib/oauth/mail-ru.js +30 -5
- package/lib/oauth/outlook.js +57 -3
- package/lib/oauth/pubsub/google.js +30 -11
- package/lib/oauth/scope-checker.js +202 -0
- package/lib/oauth2-apps.js +8 -4
- package/lib/redis-operations.js +484 -0
- package/lib/routes-ui.js +283 -2582
- package/lib/tools.js +4 -196
- package/lib/ui-routes/account-routes.js +1931 -0
- package/lib/ui-routes/admin-config-routes.js +1233 -0
- package/lib/ui-routes/admin-entities-routes.js +2367 -0
- package/lib/ui-routes/oauth-routes.js +992 -0
- package/lib/utils/network.js +237 -0
- package/package.json +10 -10
- package/sbom.json +1 -1
- package/static/js/app.js +5 -5
- package/static/licenses.html +79 -19
- package/translations/de.mo +0 -0
- package/translations/de.po +97 -86
- package/translations/en.mo +0 -0
- package/translations/en.po +80 -75
- package/translations/et.mo +0 -0
- package/translations/et.po +96 -86
- package/translations/fr.mo +0 -0
- package/translations/fr.po +97 -86
- package/translations/ja.mo +0 -0
- package/translations/ja.po +96 -86
- package/translations/messages.pot +105 -91
- package/translations/nl.mo +0 -0
- package/translations/nl.po +98 -86
- package/translations/pl.mo +0 -0
- package/translations/pl.po +96 -86
- package/views/account/security.hbs +4 -4
- package/views/accounts/account.hbs +13 -13
- package/views/accounts/register/imap-server.hbs +12 -12
- package/views/config/document-store/pre-processing/index.hbs +4 -2
- package/views/config/oauth/app.hbs +6 -7
- package/views/config/oauth/index.hbs +2 -2
- package/views/config/service.hbs +3 -4
- package/views/dashboard.hbs +5 -7
- package/views/error.hbs +22 -7
- package/views/gateways/gateway.hbs +2 -2
- package/views/partials/add_account_modal.hbs +7 -10
- package/views/partials/document_store_header.hbs +1 -1
- package/views/partials/editor_scope_info.hbs +0 -1
- package/views/partials/oauth_config_header.hbs +1 -1
- package/views/partials/side_menu.hbs +3 -3
- package/views/partials/webhook_form.hbs +2 -2
- package/views/templates/index.hbs +1 -1
- package/views/templates/template.hbs +8 -8
- package/views/tokens/index.hbs +6 -6
- package/views/tokens/new.hbs +1 -1
- package/views/webhooks/index.hbs +4 -4
- package/views/webhooks/webhook.hbs +7 -7
- package/workers/api.js +148 -2436
- package/workers/smtp.js +2 -1
- package/lib/imapproxy/imap-core/test/client.js +0 -46
- package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
- package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
- package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
- package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
- package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
- package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
- package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
- package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
- package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
- package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
- package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
- package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
- package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
- package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
- package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
- package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
- package/lib/imapproxy/imap-core/test/test-client.js +0 -152
- package/lib/imapproxy/imap-core/test/test-server.js +0 -623
- package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
- package/test/api-test.js +0 -899
- package/test/autoreply-test.js +0 -327
- package/test/bounce-test.js +0 -151
- package/test/complaint-test.js +0 -256
- package/test/fixtures/autoreply/LICENSE +0 -27
- package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
- package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
- package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
- package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
- package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
- package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
- package/test/fixtures/bounces/163.eml +0 -2521
- package/test/fixtures/bounces/fastmail.eml +0 -242
- package/test/fixtures/bounces/gmail.eml +0 -252
- package/test/fixtures/bounces/hotmail.eml +0 -655
- package/test/fixtures/bounces/mailru.eml +0 -121
- package/test/fixtures/bounces/outlook.eml +0 -1107
- package/test/fixtures/bounces/postfix.eml +0 -101
- package/test/fixtures/bounces/rambler.eml +0 -116
- package/test/fixtures/bounces/workmail.eml +0 -142
- package/test/fixtures/bounces/yahoo.eml +0 -139
- package/test/fixtures/bounces/zoho.eml +0 -83
- package/test/fixtures/bounces/zonemta.eml +0 -100
- package/test/fixtures/complaints/LICENSE +0 -27
- package/test/fixtures/complaints/amazonses.eml +0 -72
- package/test/fixtures/complaints/dmarc.eml +0 -59
- package/test/fixtures/complaints/hotmail.eml +0 -49
- package/test/fixtures/complaints/optout.eml +0 -40
- package/test/fixtures/complaints/standard-arf.eml +0 -68
- package/test/fixtures/complaints/yahoo.eml +0 -68
- package/test/oauth2-apps-test.js +0 -301
- package/test/sendonly-test.js +0 -160
- package/test/test-config.js +0 -34
- package/test/webhooks-server.js +0 -39
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { threadId: workerThreadId } = require('worker_threads');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const logger = require('../logger');
|
|
6
|
-
const { webhooks: Webhooks } = require('../webhooks');
|
|
7
|
-
const { getESClient } = require('../document-store');
|
|
8
|
-
const { getThread } = require('../threads');
|
|
9
6
|
const settings = require('../settings');
|
|
10
7
|
const msgpack = require('msgpack5')();
|
|
11
8
|
const { templates } = require('../templates');
|
|
@@ -19,6 +16,7 @@ const { getRawEmail } = require('../get-raw-email');
|
|
|
19
16
|
const { getTemplate } = require('@postalsys/templates');
|
|
20
17
|
const { deepEqual } = require('assert');
|
|
21
18
|
const { arfDetect } = require('../arf-detect');
|
|
19
|
+
const { createTransaction } = require('../redis-operations');
|
|
22
20
|
const simpleParser = require('mailparser').simpleParser;
|
|
23
21
|
const libmime = require('libmime');
|
|
24
22
|
const { bounceDetect } = require('../bounce-detect');
|
|
@@ -26,15 +24,20 @@ const ical = require('ical.js');
|
|
|
26
24
|
const { llmPreProcess } = require('../llm-pre-process');
|
|
27
25
|
const { oauth2Apps } = require('../oauth2-apps');
|
|
28
26
|
const { Account } = require('../account');
|
|
29
|
-
const util = require('util');
|
|
30
|
-
const socks = require('socks');
|
|
31
|
-
const nodemailer = require('nodemailer');
|
|
32
27
|
const { removeBcc } = require('../get-raw-email');
|
|
33
|
-
const {
|
|
28
|
+
const {
|
|
29
|
+
SmtpConfigBuilder,
|
|
30
|
+
NetworkRoutingBuilder,
|
|
31
|
+
NotificationBuilder,
|
|
32
|
+
ProviderMessageIdHandler,
|
|
33
|
+
SmtpErrorBuilder,
|
|
34
|
+
SentMailCopyDecider
|
|
35
|
+
} = require('./message-builder');
|
|
36
|
+
const { NotificationHandler, postMetrics } = require('./notification-handler');
|
|
37
|
+
const { getMailTransport } = require('./smtp-pool-manager');
|
|
34
38
|
|
|
35
39
|
// Import various utility functions and constants
|
|
36
40
|
const {
|
|
37
|
-
getLocalAddress,
|
|
38
41
|
getSignedFormDataSync,
|
|
39
42
|
getServiceSecret,
|
|
40
43
|
convertDataUrisToAttachments,
|
|
@@ -44,7 +47,6 @@ const {
|
|
|
44
47
|
readEnvValue,
|
|
45
48
|
emitChangeEvent,
|
|
46
49
|
filterEmptyObjectValues,
|
|
47
|
-
resolveCredentials,
|
|
48
50
|
getDateBuckets
|
|
49
51
|
} = require('../tools');
|
|
50
52
|
|
|
@@ -54,17 +56,13 @@ const {
|
|
|
54
56
|
ACCOUNT_INITIALIZED_NOTIFY,
|
|
55
57
|
REDIS_PREFIX,
|
|
56
58
|
MESSAGE_NEW_NOTIFY,
|
|
57
|
-
MESSAGE_DELETED_NOTIFY,
|
|
58
|
-
MESSAGE_UPDATED_NOTIFY,
|
|
59
59
|
EMAIL_BOUNCE_NOTIFY,
|
|
60
|
-
MAILBOX_DELETED_NOTIFY,
|
|
61
60
|
DEFAULT_DELIVERY_ATTEMPTS,
|
|
62
61
|
MIME_BOUNDARY_PREFIX,
|
|
63
62
|
DEFAULT_DOWNLOAD_CHUNK_SIZE,
|
|
64
63
|
EMAIL_COMPLAINT_NOTIFY,
|
|
65
64
|
MAX_INLINE_ATTACHMENT_SIZE,
|
|
66
65
|
DEFAULT_MAX_IMAP_AUTH_FAILURE_TIME,
|
|
67
|
-
TLS_DEFAULTS,
|
|
68
66
|
EMAIL_DELIVERY_ERROR_NOTIFY,
|
|
69
67
|
EMAIL_SENT_NOTIFY
|
|
70
68
|
} = require('../consts');
|
|
@@ -75,154 +73,9 @@ const DOWNLOAD_CHUNK_SIZE = getByteSize(readEnvValue('EENGINE_CHUNK_SIZE')) || D
|
|
|
75
73
|
// Configure maximum time to wait before disabling IMAP on authentication failures
|
|
76
74
|
const MAX_IMAP_AUTH_FAILURE_TIME = getDuration(readEnvValue('EENGINE_MAX_IMAP_AUTH_FAILURE_TIME')) || DEFAULT_MAX_IMAP_AUTH_FAILURE_TIME;
|
|
77
75
|
|
|
78
|
-
/**
|
|
79
|
-
* Sends metrics data to the parent thread for aggregation
|
|
80
|
-
* @param {Object} meta - Metadata to include with the metric
|
|
81
|
-
* @param {Object} logger - Logger instance
|
|
82
|
-
* @param {string} key - Metric key identifier
|
|
83
|
-
* @param {string} method - Metric method (e.g., 'inc', 'dec')
|
|
84
|
-
* @param {...any} args - Additional arguments for the metric
|
|
85
|
-
*/
|
|
86
|
-
async function metricsMeta(meta, logger, key, method, ...args) {
|
|
87
|
-
try {
|
|
88
|
-
parentPort.postMessage({
|
|
89
|
-
cmd: 'metrics',
|
|
90
|
-
key,
|
|
91
|
-
method,
|
|
92
|
-
args,
|
|
93
|
-
meta: meta || {}
|
|
94
|
-
});
|
|
95
|
-
} catch (err) {
|
|
96
|
-
logger.error({ msg: 'Failed to post metrics to parent', err });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
76
|
// Map to track pending idempotency operations to prevent duplicate processing
|
|
101
77
|
const pendingIdempotencyOperations = new Map();
|
|
102
78
|
|
|
103
|
-
// Cache for SMTP connection pools to reuse connections
|
|
104
|
-
const SMTP_POOLS = new Map();
|
|
105
|
-
// Track last usage time for each pool to enable LRU-based cleanup
|
|
106
|
-
const SMTP_POOL_LAST_USED = new Map();
|
|
107
|
-
|
|
108
|
-
// Maximum idle time for SMTP pool connections (10 minutes of inactivity)
|
|
109
|
-
const SMTP_POOL_MAX_IDLE = 10 * 60 * 1000;
|
|
110
|
-
// Cleanup interval for idle SMTP pools (2 minutes)
|
|
111
|
-
const SMTP_POOL_CLEANUP_INTERVAL = 2 * 60 * 1000;
|
|
112
|
-
|
|
113
|
-
// Periodic cleanup of idle SMTP pool connections
|
|
114
|
-
let smtpPoolCleanupTimer = setInterval(() => {
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
const idleKeys = [];
|
|
117
|
-
|
|
118
|
-
for (const [poolKey, lastUsed] of SMTP_POOL_LAST_USED.entries()) {
|
|
119
|
-
if (now - lastUsed > SMTP_POOL_MAX_IDLE) {
|
|
120
|
-
idleKeys.push(poolKey);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const poolKey of idleKeys) {
|
|
125
|
-
const transporter = SMTP_POOLS.get(poolKey);
|
|
126
|
-
if (transporter) {
|
|
127
|
-
// Check if the transporter has active connections
|
|
128
|
-
if (transporter._connectionPool && transporter._connectionPool.size > 0) {
|
|
129
|
-
// Still has active connections, update last used time
|
|
130
|
-
SMTP_POOL_LAST_USED.set(poolKey, now);
|
|
131
|
-
logger.trace({ msg: 'SMTP pool still has active connections, skipping cleanup', poolKey });
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
logger.debug({ msg: 'Cleaning up idle SMTP pool connection', poolKey, idleTime: now - SMTP_POOL_LAST_USED.get(poolKey) });
|
|
136
|
-
try {
|
|
137
|
-
transporter.close();
|
|
138
|
-
} catch (err) {
|
|
139
|
-
logger.error({ msg: 'Failed to close idle SMTP transporter', poolKey, err });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
SMTP_POOLS.delete(poolKey);
|
|
143
|
-
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (idleKeys.length > 0) {
|
|
147
|
-
logger.info({ msg: 'SMTP pool cleanup completed', cleaned: idleKeys.length, remaining: SMTP_POOLS.size });
|
|
148
|
-
}
|
|
149
|
-
}, SMTP_POOL_CLEANUP_INTERVAL);
|
|
150
|
-
|
|
151
|
-
// Prevent the timer from keeping the process alive
|
|
152
|
-
smtpPoolCleanupTimer.unref();
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Gets or creates a reusable SMTP transport for the given configuration
|
|
156
|
-
* Connection pooling improves performance by reusing SMTP connections
|
|
157
|
-
* @param {Object} smtpSettings - SMTP configuration settings
|
|
158
|
-
* @returns {Object} Nodemailer transport instance
|
|
159
|
-
*/
|
|
160
|
-
function getMailTransport(smtpSettings) {
|
|
161
|
-
// Extract only the settings that affect connection identity
|
|
162
|
-
let limitedSettings = {};
|
|
163
|
-
for (let key of ['name', 'localAddress', 'auth', 'host', 'port', 'secure', 'transactionLog', 'proxy']) {
|
|
164
|
-
limitedSettings[key] = smtpSettings[key];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Create a unique key for this SMTP configuration
|
|
168
|
-
let serializedSettings = JSON.stringify(limitedSettings);
|
|
169
|
-
let poolKey = crypto.createHash('sha256').update(serializedSettings).digest('hex');
|
|
170
|
-
|
|
171
|
-
// Return existing transport if available
|
|
172
|
-
let transporter;
|
|
173
|
-
if (SMTP_POOLS.has(poolKey)) {
|
|
174
|
-
transporter = SMTP_POOLS.get(poolKey);
|
|
175
|
-
// Update last used time for LRU tracking
|
|
176
|
-
SMTP_POOL_LAST_USED.set(poolKey, Date.now());
|
|
177
|
-
return transporter;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Configure connection pooling settings
|
|
181
|
-
smtpSettings.pool = true;
|
|
182
|
-
smtpSettings.maxConnections = 1;
|
|
183
|
-
smtpSettings.maxMessages = 100;
|
|
184
|
-
smtpSettings.socketTimeout = 2 * 60 * 1000; // 2 minute timeout
|
|
185
|
-
|
|
186
|
-
// Create new transport with pooling enabled
|
|
187
|
-
transporter = nodemailer.createTransport(smtpSettings);
|
|
188
|
-
transporter.set('proxy_socks_module', socks);
|
|
189
|
-
|
|
190
|
-
// Handle connection pool cleanup when idle
|
|
191
|
-
transporter.once('clear', () => {
|
|
192
|
-
// all emails processed and connection timed out
|
|
193
|
-
logger.trace({ msg: 'Clearing disconnected SMTP pool', poolKey });
|
|
194
|
-
SMTP_POOLS.delete(poolKey);
|
|
195
|
-
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
196
|
-
try {
|
|
197
|
-
transporter.close();
|
|
198
|
-
} catch (closeErr) {
|
|
199
|
-
logger.error({ msg: 'Failed to close transporter', err: closeErr });
|
|
200
|
-
}
|
|
201
|
-
transporter = null;
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Handle transport errors by removing from pool
|
|
205
|
-
transporter.once('error', err => {
|
|
206
|
-
// not sure what happned, but do not re-use this transporter object anymore
|
|
207
|
-
logger.error({ msg: 'Transporter failed', err });
|
|
208
|
-
SMTP_POOLS.delete(poolKey);
|
|
209
|
-
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
210
|
-
try {
|
|
211
|
-
transporter.close();
|
|
212
|
-
} catch (closeErr) {
|
|
213
|
-
logger.error({ msg: 'Failed to close transporter', err: closeErr });
|
|
214
|
-
}
|
|
215
|
-
transporter = null;
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Cache the transport for reuse
|
|
219
|
-
SMTP_POOLS.set(poolKey, transporter);
|
|
220
|
-
SMTP_POOL_LAST_USED.set(poolKey, Date.now());
|
|
221
|
-
logger.trace({ msg: 'Created SMTP pool', poolKey });
|
|
222
|
-
|
|
223
|
-
return transporter;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
79
|
/**
|
|
227
80
|
* Check if an error is a transient error that should be retried
|
|
228
81
|
* @param {Error} err - The error to check
|
|
@@ -329,6 +182,14 @@ class BaseClient {
|
|
|
329
182
|
|
|
330
183
|
// Track sub-connections (e.g., for IMAP IDLE)
|
|
331
184
|
this.subconnections = [];
|
|
185
|
+
|
|
186
|
+
// Initialize notification handler for webhook and event delivery
|
|
187
|
+
this.notificationHandler = new NotificationHandler({
|
|
188
|
+
account: this.account,
|
|
189
|
+
logger: this.logger,
|
|
190
|
+
flowProducer: this.flowProducer,
|
|
191
|
+
documentsQueue: this.documentsQueue
|
|
192
|
+
});
|
|
332
193
|
}
|
|
333
194
|
|
|
334
195
|
// Stub methods to be implemented by subclasses
|
|
@@ -463,18 +324,17 @@ class BaseClient {
|
|
|
463
324
|
* Sends notification on first successful connection
|
|
464
325
|
*/
|
|
465
326
|
async setStateVal() {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
327
|
+
const txn = createTransaction(this.redis, { logger: this.logger });
|
|
328
|
+
txn.add('hSetExists', this.getAccountKey(), 'state', this.state);
|
|
329
|
+
txn.add('hSetBigger', this.getAccountKey(), 'runIndex', this.runIndex.toString());
|
|
330
|
+
txn.add('hget', this.getAccountKey(), `state:count:${this.state}`);
|
|
331
|
+
txn.add('hIncrbyExists', this.getAccountKey(), `state:count:${this.state}`, 1);
|
|
332
|
+
txn.add('hget', this.getAccountKey(), 'state');
|
|
333
|
+
|
|
334
|
+
const values = await txn.execOrThrow();
|
|
335
|
+
const prevVal = values[2];
|
|
336
|
+
const incrVal = values[3];
|
|
337
|
+
const stateVal = values[4];
|
|
478
338
|
|
|
479
339
|
// Detect first successful connection
|
|
480
340
|
if (stateVal === 'connected' && incrVal === 1 && prevVal === '0') {
|
|
@@ -532,31 +392,30 @@ class BaseClient {
|
|
|
532
392
|
|
|
533
393
|
// Track error occurrences
|
|
534
394
|
if (isFirstOccurrence) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.exec();
|
|
395
|
+
const txn = createTransaction(this.redis, { logger: this.logger });
|
|
396
|
+
txn.add('hSetExists', this.getAccountKey(), 'lastError:errorCount', 1);
|
|
397
|
+
txn.add('hSetExists', this.getAccountKey(), 'lastError:first', new Date().toISOString());
|
|
398
|
+
await txn.exec();
|
|
540
399
|
} else {
|
|
541
400
|
let errorCount;
|
|
542
401
|
let firstError;
|
|
543
402
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
.exec();
|
|
403
|
+
const txn = createTransaction(this.redis, { logger: this.logger });
|
|
404
|
+
txn.add('hIncrbyExists', this.getAccountKey(), 'lastError:errorCount', 1);
|
|
405
|
+
txn.add('hget', this.getAccountKey(), 'lastError:first');
|
|
406
|
+
const { error, values } = await txn.exec();
|
|
549
407
|
|
|
550
|
-
if (!
|
|
551
|
-
errorCount =
|
|
408
|
+
if (!error) {
|
|
409
|
+
errorCount = values[0] || 0;
|
|
410
|
+
const fe = values[1];
|
|
552
411
|
if (fe) {
|
|
553
|
-
|
|
554
|
-
if (
|
|
555
|
-
firstError =
|
|
412
|
+
const parsedDate = new Date(fe);
|
|
413
|
+
if (parsedDate.toString() !== 'Invalid Date') {
|
|
414
|
+
firstError = parsedDate;
|
|
556
415
|
}
|
|
557
416
|
}
|
|
558
417
|
} else {
|
|
559
|
-
this.logger.error({ msg: 'Redis error while checking error state counters',
|
|
418
|
+
this.logger.error({ msg: 'Redis error while checking error state counters', error });
|
|
560
419
|
}
|
|
561
420
|
|
|
562
421
|
// Handle repeated authentication errors by disabling IMAP
|
|
@@ -577,19 +436,19 @@ class BaseClient {
|
|
|
577
436
|
imapData.disabled = true;
|
|
578
437
|
|
|
579
438
|
// Disable IMAP and reset error counters
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
439
|
+
const disableTxn = createTransaction(this.redis, { logger: this.logger });
|
|
440
|
+
disableTxn.add('hSetExists', this.getAccountKey(), 'imap', JSON.stringify(imapData));
|
|
441
|
+
disableTxn.add('hdel', this.getAccountKey(), 'lastError:errorCount', 'lastError:first');
|
|
442
|
+
disableTxn.add(
|
|
443
|
+
'hSetExists',
|
|
444
|
+
this.getAccountKey(),
|
|
445
|
+
'lastErrorState',
|
|
446
|
+
JSON.stringify({
|
|
447
|
+
description: 'IMAP was disabled for the account due to exceeding the authentication error threshold',
|
|
448
|
+
response: data.response
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
await disableTxn.exec();
|
|
593
452
|
|
|
594
453
|
this.logger.info({
|
|
595
454
|
msg: 'IMAP was disabled for the account due to exceeding the authentication error threshold',
|
|
@@ -614,22 +473,16 @@ class BaseClient {
|
|
|
614
473
|
|
|
615
474
|
/**
|
|
616
475
|
* Sends notifications for various email events through webhooks and queues
|
|
617
|
-
* Handles
|
|
476
|
+
* Handles error state tracking for connection/auth errors before delegating
|
|
477
|
+
* to the notification handler for actual delivery
|
|
618
478
|
* @param {Object} mailbox - Mailbox information
|
|
619
479
|
* @param {string} event - Event type constant
|
|
620
480
|
* @param {Object} data - Event data payload
|
|
621
481
|
* @param {Object} extraOpts - Additional options
|
|
622
482
|
*/
|
|
623
483
|
async notify(mailbox, event, data, extraOpts) {
|
|
624
|
-
extraOpts = extraOpts || {};
|
|
625
|
-
const { skipWebhook, canSync = true } = extraOpts;
|
|
626
|
-
|
|
627
|
-
// Track event metrics
|
|
628
|
-
metricsMeta({ account: this.account }, this.logger, 'events', 'inc', {
|
|
629
|
-
event
|
|
630
|
-
});
|
|
631
|
-
|
|
632
484
|
// Handle error state tracking for connection/auth errors
|
|
485
|
+
// This is BaseClient-specific logic that must run before notification
|
|
633
486
|
switch (event) {
|
|
634
487
|
case 'connectError':
|
|
635
488
|
case 'authenticationError': {
|
|
@@ -643,133 +496,8 @@ class BaseClient {
|
|
|
643
496
|
}
|
|
644
497
|
}
|
|
645
498
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// Build notification payload
|
|
649
|
-
let payload = {
|
|
650
|
-
serviceUrl,
|
|
651
|
-
account: this.account,
|
|
652
|
-
date: new Date().toISOString()
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
let path = (mailbox && mailbox.path) || (data && data.path);
|
|
656
|
-
if (path) {
|
|
657
|
-
payload.path = path;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (mailbox && mailbox.listingEntry && mailbox.listingEntry.specialUse) {
|
|
661
|
-
payload.specialUse = mailbox.listingEntry.specialUse;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (event) {
|
|
665
|
-
payload.event = event;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (data) {
|
|
669
|
-
payload.data = data;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
let queueKeep = (await settings.get('queueKeep')) || true;
|
|
673
|
-
|
|
674
|
-
// Determine if we need to sync with document store (ElasticSearch)
|
|
675
|
-
let addDocumentQueueJob =
|
|
676
|
-
canSync &&
|
|
677
|
-
this.documentsQueue &&
|
|
678
|
-
[MESSAGE_NEW_NOTIFY, MESSAGE_DELETED_NOTIFY, MESSAGE_UPDATED_NOTIFY, EMAIL_BOUNCE_NOTIFY, MAILBOX_DELETED_NOTIFY].includes(event) &&
|
|
679
|
-
(await settings.get('documentStoreEnabled'));
|
|
680
|
-
|
|
681
|
-
// Generate thread ID for new messages if needed
|
|
682
|
-
if (addDocumentQueueJob && payload.data && event === MESSAGE_NEW_NOTIFY && !payload.data.threadId) {
|
|
683
|
-
// Generate a thread ID for the email. This is also stored in ElasticSearch.
|
|
684
|
-
const { index, client } = await getESClient(logger);
|
|
685
|
-
try {
|
|
686
|
-
if (client) {
|
|
687
|
-
let thread = await getThread(client, index, this.account, payload.data, logger);
|
|
688
|
-
if (thread) {
|
|
689
|
-
payload.data.threadId = thread;
|
|
690
|
-
logger.info({
|
|
691
|
-
msg: 'Provisioned thread ID for a message',
|
|
692
|
-
account: this.account,
|
|
693
|
-
message: payload.data.id,
|
|
694
|
-
threadId: payload.data.threadId
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
} catch (err) {
|
|
699
|
-
if (logger.notifyError) {
|
|
700
|
-
logger.notifyError(err, event => {
|
|
701
|
-
event.setUser(this.account);
|
|
702
|
-
event.addMetadata('ee', {
|
|
703
|
-
index
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
logger.error({ msg: 'Failed to resolve thread', account: this.account, message: payload.data.id, err });
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Configure job options for queuing
|
|
712
|
-
const defaultJobOptions = {
|
|
713
|
-
removeOnComplete: queueKeep,
|
|
714
|
-
removeOnFail: queueKeep,
|
|
715
|
-
attempts: 10,
|
|
716
|
-
backoff: {
|
|
717
|
-
type: 'exponential',
|
|
718
|
-
delay: 5000
|
|
719
|
-
}
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
// use more attempts for ElasticSearch updates
|
|
723
|
-
const documentJobOptions = Object.assign(structuredClone(defaultJobOptions), { attempts: 16 });
|
|
724
|
-
|
|
725
|
-
// Process notifications with flow (webhook + document store) or separately
|
|
726
|
-
if (!skipWebhook && addDocumentQueueJob) {
|
|
727
|
-
// add both jobs as a Flow
|
|
728
|
-
|
|
729
|
-
let notifyPayload = await Webhooks.formatPayload(event, payload);
|
|
730
|
-
|
|
731
|
-
const queueFlow = [
|
|
732
|
-
{
|
|
733
|
-
name: event,
|
|
734
|
-
data: payload,
|
|
735
|
-
queueName: 'documents'
|
|
736
|
-
}
|
|
737
|
-
];
|
|
738
|
-
|
|
739
|
-
await Webhooks.pushToQueue(event, notifyPayload, {
|
|
740
|
-
routesOnly: true,
|
|
741
|
-
queueFlow
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
await this.flowProducer.add(
|
|
745
|
-
{
|
|
746
|
-
name: event,
|
|
747
|
-
data: notifyPayload,
|
|
748
|
-
queueName: 'notify',
|
|
749
|
-
children: queueFlow
|
|
750
|
-
},
|
|
751
|
-
{
|
|
752
|
-
queuesOptions: {
|
|
753
|
-
notify: {
|
|
754
|
-
defaultJobOptions
|
|
755
|
-
},
|
|
756
|
-
documents: {
|
|
757
|
-
defaultJobOptions: documentJobOptions
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
);
|
|
762
|
-
} else {
|
|
763
|
-
// add to queues as normal jobs
|
|
764
|
-
|
|
765
|
-
if (!skipWebhook) {
|
|
766
|
-
await Webhooks.pushToQueue(event, await Webhooks.formatPayload(event, payload));
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (addDocumentQueueJob) {
|
|
770
|
-
await this.documentsQueue.add(event, payload, documentJobOptions);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
499
|
+
// Delegate to notification handler for webhook and document store delivery
|
|
500
|
+
await this.notificationHandler.notify(mailbox, event, data, extraOpts);
|
|
773
501
|
}
|
|
774
502
|
|
|
775
503
|
/**
|
|
@@ -801,7 +529,7 @@ class BaseClient {
|
|
|
801
529
|
}
|
|
802
530
|
|
|
803
531
|
// Check if token needs refresh (with 30 second buffer)
|
|
804
|
-
if (!accountData.oauth2.accessToken || !accountData.oauth2.expires || accountData.oauth2.expires < new Date(now
|
|
532
|
+
if (!accountData.oauth2.accessToken || !accountData.oauth2.expires || accountData.oauth2.expires < new Date(now + 30 * 1000)) {
|
|
805
533
|
// renew access token
|
|
806
534
|
try {
|
|
807
535
|
accountData = await accountObject.renewAccessToken({
|
|
@@ -3176,210 +2904,66 @@ class BaseClient {
|
|
|
3176
2904
|
* @returns {Object} Delivery result with response and messageId
|
|
3177
2905
|
*/
|
|
3178
2906
|
async submitMessage(data) {
|
|
3179
|
-
|
|
2907
|
+
const accountData = await this.accountObject.loadAccountData();
|
|
3180
2908
|
|
|
3181
2909
|
// Verify SMTP capability
|
|
3182
2910
|
if (!accountData.smtp && !accountData.oauth2 && !data.gateway) {
|
|
3183
|
-
|
|
3184
|
-
let err = new Error('SMTP configuration not found');
|
|
2911
|
+
const err = new Error('SMTP configuration not found');
|
|
3185
2912
|
err.code = 'SMTPUnavailable';
|
|
3186
2913
|
err.statusCode = 404;
|
|
3187
2914
|
throw err;
|
|
3188
2915
|
}
|
|
3189
2916
|
|
|
3190
|
-
//
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
gatewayData = await gatewayObject.loadGatewayData();
|
|
3197
|
-
} catch (err) {
|
|
3198
|
-
this.logger.info({ msg: 'Failed to load gateway data', messageId: data.messageId, gateway: data.gateway, err });
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
|
-
let smtpConnectionConfig;
|
|
3203
|
-
|
|
3204
|
-
// Configure SMTP connection based on authentication type
|
|
3205
|
-
if (gatewayData) {
|
|
3206
|
-
// Use gateway configuration
|
|
3207
|
-
smtpConnectionConfig = {
|
|
3208
|
-
host: gatewayData.host,
|
|
3209
|
-
port: gatewayData.port,
|
|
3210
|
-
secure: gatewayData.secure
|
|
3211
|
-
};
|
|
3212
|
-
if (gatewayData.user || gatewayData.pass) {
|
|
3213
|
-
smtpConnectionConfig.auth = {
|
|
3214
|
-
user: gatewayData.user || '',
|
|
3215
|
-
pass: gatewayData.pass || ''
|
|
3216
|
-
};
|
|
3217
|
-
}
|
|
3218
|
-
} else if (accountData.oauth2 && accountData.oauth2.auth) {
|
|
3219
|
-
// load OAuth2 tokens
|
|
3220
|
-
const { oauth2User, accessToken, oauth2App } = await this.loadOAuth2AccountCredentials(accountData, this, 'smtp');
|
|
3221
|
-
const providerData = oauth2ProviderData(oauth2App.provider, oauth2App.cloud);
|
|
3222
|
-
|
|
3223
|
-
smtpConnectionConfig = Object.assign(
|
|
3224
|
-
{
|
|
3225
|
-
auth: {
|
|
3226
|
-
user: oauth2User,
|
|
3227
|
-
accessToken
|
|
3228
|
-
},
|
|
3229
|
-
resyncDelay: 900
|
|
3230
|
-
},
|
|
3231
|
-
providerData.smtp || {}
|
|
3232
|
-
);
|
|
3233
|
-
} else {
|
|
3234
|
-
// deep copy of smtp settings
|
|
3235
|
-
smtpConnectionConfig = JSON.parse(JSON.stringify(accountData.smtp));
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
let { raw, hasBcc, envelope, messageId, queueId, reference, job: jobData } = data;
|
|
3239
|
-
|
|
3240
|
-
let smtpAuth = smtpConnectionConfig.auth;
|
|
3241
|
-
// If authentication server is set then it overrides authentication data
|
|
3242
|
-
if (smtpConnectionConfig.useAuthServer) {
|
|
3243
|
-
try {
|
|
3244
|
-
smtpAuth = await resolveCredentials(this.account, 'smtp');
|
|
3245
|
-
} catch (err) {
|
|
3246
|
-
err.authenticationFailed = true;
|
|
3247
|
-
this.logger.error({
|
|
3248
|
-
account: this.account,
|
|
3249
|
-
err
|
|
3250
|
-
});
|
|
3251
|
-
throw err;
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
// Select local address for outbound connection
|
|
3256
|
-
let { localAddress: address, name, addressSelector: selector } = await getLocalAddress(this.redis, 'smtp', this.account, data.localAddress);
|
|
3257
|
-
this.logger.info({
|
|
3258
|
-
msg: 'Selected local address',
|
|
3259
|
-
account: this.account,
|
|
3260
|
-
proto: 'SMTP',
|
|
3261
|
-
address,
|
|
3262
|
-
name,
|
|
3263
|
-
selector,
|
|
3264
|
-
requestedLocalAddress: data.localAddress
|
|
2917
|
+
// Build SMTP configuration using SmtpConfigBuilder
|
|
2918
|
+
const smtpConfigBuilder = new SmtpConfigBuilder({
|
|
2919
|
+
redis: this.redis,
|
|
2920
|
+
secret: this.secret,
|
|
2921
|
+
logger: this.logger,
|
|
2922
|
+
account: this.account
|
|
3265
2923
|
});
|
|
3266
2924
|
|
|
3267
|
-
//
|
|
3268
|
-
|
|
3269
|
-
let smtpSettings = Object.assign(
|
|
3270
|
-
{
|
|
3271
|
-
name,
|
|
3272
|
-
localAddress: address,
|
|
3273
|
-
transactionLog: true,
|
|
3274
|
-
logger: smtpLogger
|
|
3275
|
-
},
|
|
3276
|
-
smtpConnectionConfig
|
|
3277
|
-
);
|
|
2925
|
+
// Load gateway if specified
|
|
2926
|
+
const { gatewayData, gatewayObject } = await smtpConfigBuilder.loadGateway(data.gateway, data.messageId);
|
|
3278
2927
|
|
|
3279
|
-
//
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
smtpSettings.auth.type = 'OAuth2';
|
|
3287
|
-
smtpSettings.auth.accessToken = smtpAuth.accessToken;
|
|
3288
|
-
} else {
|
|
3289
|
-
smtpSettings.auth.pass = smtpAuth.pass;
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
2928
|
+
// Build connection configuration
|
|
2929
|
+
const smtpConnectionConfig = await smtpConfigBuilder.buildConnectionConfig({
|
|
2930
|
+
gatewayData,
|
|
2931
|
+
accountData,
|
|
2932
|
+
loadOAuth2Credentials: this.loadOAuth2AccountCredentials.bind(this),
|
|
2933
|
+
context: this
|
|
2934
|
+
});
|
|
3292
2935
|
|
|
3293
|
-
//
|
|
3294
|
-
|
|
3295
|
-
smtpSettings.tls = {};
|
|
3296
|
-
}
|
|
3297
|
-
for (let key of Object.keys(TLS_DEFAULTS)) {
|
|
3298
|
-
if (!(key in smtpSettings.tls)) {
|
|
3299
|
-
smtpSettings.tls[key] = TLS_DEFAULTS[key];
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
2936
|
+
// Resolve auth server credentials if configured
|
|
2937
|
+
const smtpAuth = await smtpConfigBuilder.resolveAuthServer(smtpConnectionConfig);
|
|
3302
2938
|
|
|
3303
|
-
//
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
data.sub = 'nodemailer';
|
|
3311
|
-
if (typeof this.logger[level] === 'function') {
|
|
3312
|
-
this.logger[level](data);
|
|
3313
|
-
} else {
|
|
3314
|
-
this.logger.debug(data);
|
|
3315
|
-
}
|
|
3316
|
-
};
|
|
3317
|
-
}
|
|
2939
|
+
// Build complete SMTP settings
|
|
2940
|
+
const smtpSettings = await smtpConfigBuilder.buildSmtpSettings({
|
|
2941
|
+
smtpConnectionConfig,
|
|
2942
|
+
smtpAuth,
|
|
2943
|
+
accountData,
|
|
2944
|
+
data
|
|
2945
|
+
});
|
|
3318
2946
|
|
|
3319
|
-
//
|
|
3320
|
-
|
|
3321
|
-
smtpSettings.proxy = data.proxy;
|
|
3322
|
-
} else if (accountData.proxy) {
|
|
3323
|
-
smtpSettings.proxy = accountData.proxy;
|
|
3324
|
-
} else {
|
|
3325
|
-
let proxyUrl = await settings.get('proxyUrl');
|
|
3326
|
-
let proxyEnabled = await settings.get('proxyEnabled');
|
|
3327
|
-
if (proxyEnabled && proxyUrl && !smtpSettings.proxy) {
|
|
3328
|
-
smtpSettings.proxy = proxyUrl;
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
2947
|
+
// Build network routing info for notifications
|
|
2948
|
+
const networkRouting = NetworkRoutingBuilder.build(smtpSettings, data);
|
|
3331
2949
|
|
|
3332
|
-
//
|
|
3333
|
-
|
|
3334
|
-
smtpSettings.name = accountData.smtpEhloName;
|
|
3335
|
-
}
|
|
2950
|
+
// Extract message data
|
|
2951
|
+
let { raw, hasBcc, envelope, messageId, queueId, reference, job: jobData } = data;
|
|
3336
2952
|
|
|
3337
2953
|
// Verify job still exists
|
|
3338
2954
|
const submitJobEntry = await this.submitQueue.getJob(jobData.id);
|
|
3339
2955
|
if (!submitJobEntry) {
|
|
3340
|
-
|
|
3341
|
-
this.logger.error({
|
|
3342
|
-
msg: 'Submit job was not found',
|
|
3343
|
-
job: jobData.id
|
|
3344
|
-
});
|
|
2956
|
+
this.logger.error({ msg: 'Submit job was not found', job: jobData.id });
|
|
3345
2957
|
return false;
|
|
3346
2958
|
}
|
|
3347
2959
|
|
|
3348
|
-
//
|
|
3349
|
-
const ignoreMailCertErrors = await settings.get('ignoreMailCertErrors');
|
|
3350
|
-
if (ignoreMailCertErrors && smtpSettings?.tls?.rejectUnauthorized !== false) {
|
|
3351
|
-
smtpSettings.tls = smtpSettings.tls || {};
|
|
3352
|
-
smtpSettings.tls.rejectUnauthorized = false;
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
// Prepare network routing info for notifications
|
|
3356
|
-
const networkRouting = smtpSettings.localAddress || smtpSettings.proxy ? {} : null;
|
|
3357
|
-
|
|
3358
|
-
if (networkRouting && smtpSettings.localAddress) {
|
|
3359
|
-
networkRouting.localAddress = smtpSettings.localAddress;
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
if (networkRouting && smtpSettings.proxy) {
|
|
3363
|
-
networkRouting.proxy = smtpSettings.proxy;
|
|
3364
|
-
}
|
|
3365
|
-
|
|
3366
|
-
if (networkRouting && smtpSettings.name) {
|
|
3367
|
-
networkRouting.name = smtpSettings.name;
|
|
3368
|
-
}
|
|
3369
|
-
|
|
3370
|
-
if (networkRouting && data.localAddress && data.localAddress !== networkRouting.localAddress) {
|
|
3371
|
-
networkRouting.requestedLocalAddress = data.localAddress;
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
// Get or create SMTP transport from pool
|
|
2960
|
+
// Get SMTP transport from pool
|
|
3375
2961
|
const transporter = getMailTransport(smtpSettings);
|
|
3376
2962
|
|
|
3377
2963
|
try {
|
|
2964
|
+
// Update job progress
|
|
3378
2965
|
try {
|
|
3379
|
-
|
|
3380
|
-
await submitJobEntry.updateProgress({
|
|
3381
|
-
status: 'smtp-starting'
|
|
3382
|
-
});
|
|
2966
|
+
await submitJobEntry.updateProgress({ status: 'smtp-starting' });
|
|
3383
2967
|
} catch (err) {
|
|
3384
2968
|
// ignore
|
|
3385
2969
|
}
|
|
@@ -3388,7 +2972,6 @@ class BaseClient {
|
|
|
3388
2972
|
const info = await transporter.sendMail({
|
|
3389
2973
|
envelope,
|
|
3390
2974
|
messageId,
|
|
3391
|
-
// make sure that Bcc line is removed from the version sent to SMTP
|
|
3392
2975
|
raw: !hasBcc ? raw : await removeBcc(raw),
|
|
3393
2976
|
dsn: data.dsn || null
|
|
3394
2977
|
});
|
|
@@ -3398,40 +2981,13 @@ class BaseClient {
|
|
|
3398
2981
|
await this.redis.hSetExists(this.getAccountKey(), 'smtpServerEhlo', JSON.stringify(info.ehlo));
|
|
3399
2982
|
}
|
|
3400
2983
|
|
|
3401
|
-
//
|
|
3402
|
-
|
|
3403
|
-
let originalMessageId;
|
|
3404
|
-
|
|
3405
|
-
// Hotmail - extract actual Message-ID from response
|
|
3406
|
-
let hotmailMessageIdMatch = (info.response || '').toString().match(/^250 2.0.0 OK (<[^>]+\.prod\.outlook\.com>)/);
|
|
3407
|
-
if (hotmailMessageIdMatch && hotmailMessageIdMatch[1] !== info.messageId) {
|
|
3408
|
-
// MessageId was overridden
|
|
3409
|
-
originalMessageId = info.messageId;
|
|
3410
|
-
info.messageId = hotmailMessageIdMatch[1];
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
// AWS SES - construct Message-ID from response
|
|
3414
|
-
let awsSesHostMatch = (smtpSettings.host || '').toString().match(/\.([^.]+)\.(amazonaws\.com|awsapps\.com)$/i);
|
|
3415
|
-
let awsSesMessageIdMatch = (info.response || '').toString().match(/^250 Ok ([0-9a-f-]+)$/);
|
|
3416
|
-
if (awsSesHostMatch && awsSesMessageIdMatch) {
|
|
3417
|
-
let region = awsSesHostMatch[1].toLowerCase().trim();
|
|
3418
|
-
let messageId = awsSesMessageIdMatch[1].toLowerCase().trim();
|
|
3419
|
-
if (region === 'us-east-1') {
|
|
3420
|
-
region = 'email';
|
|
3421
|
-
}
|
|
3422
|
-
|
|
3423
|
-
// MessageId was overridden
|
|
3424
|
-
originalMessageId = info.messageId;
|
|
3425
|
-
info.messageId = '<' + messageId + (!/@/.test(messageId) ? '@' + region + '.amazonses.com' : '') + '>';
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
// done
|
|
2984
|
+
// Handle provider-specific message ID extraction
|
|
2985
|
+
const originalMessageId = ProviderMessageIdHandler.processResponse(info, smtpSettings.host);
|
|
3429
2986
|
|
|
2987
|
+
// Update job progress
|
|
3430
2988
|
try {
|
|
3431
|
-
// try to update job progress
|
|
3432
2989
|
await submitJobEntry.updateProgress({
|
|
3433
2990
|
status: 'smtp-completed',
|
|
3434
|
-
|
|
3435
2991
|
response: info.response,
|
|
3436
2992
|
messageId: info.messageId,
|
|
3437
2993
|
originalMessageId
|
|
@@ -3441,269 +2997,223 @@ class BaseClient {
|
|
|
3441
2997
|
}
|
|
3442
2998
|
|
|
3443
2999
|
// Send success notification
|
|
3444
|
-
await this.notify(
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
envelope,
|
|
3450
|
-
networkRouting
|
|
3451
|
-
});
|
|
3000
|
+
await this.notify(
|
|
3001
|
+
false,
|
|
3002
|
+
EMAIL_SENT_NOTIFY,
|
|
3003
|
+
NotificationBuilder.buildSuccessPayload({ info, originalMessageId, queueId, envelope, networkRouting })
|
|
3004
|
+
);
|
|
3452
3005
|
|
|
3453
|
-
//
|
|
3006
|
+
// Clear cached SMTP error
|
|
3454
3007
|
try {
|
|
3455
|
-
await this.redis.hset(
|
|
3456
|
-
this.getAccountKey(),
|
|
3457
|
-
'smtpStatus',
|
|
3458
|
-
JSON.stringify({
|
|
3459
|
-
created: Date.now(),
|
|
3460
|
-
status: 'ok',
|
|
3461
|
-
response: info.response
|
|
3462
|
-
})
|
|
3463
|
-
);
|
|
3008
|
+
await this.redis.hset(this.getAccountKey(), 'smtpStatus', JSON.stringify({ created: Date.now(), status: 'ok', response: info.response }));
|
|
3464
3009
|
} catch (err) {
|
|
3465
|
-
// ignore
|
|
3466
|
-
}
|
|
3467
|
-
|
|
3468
|
-
// The default is to copy message to Sent Mail folder
|
|
3469
|
-
let shouldCopy = !Object.prototype.hasOwnProperty.call(accountData, 'copy');
|
|
3470
|
-
|
|
3471
|
-
// Account specific setting
|
|
3472
|
-
if (typeof accountData.copy === 'boolean') {
|
|
3473
|
-
shouldCopy = accountData.copy;
|
|
3474
|
-
}
|
|
3475
|
-
|
|
3476
|
-
// Suppress uploads for Gmail and Outlook
|
|
3477
|
-
// Unfortunately, previous default schema for all added accounts was copy=true, so can't prefer account specific setting here
|
|
3478
|
-
|
|
3479
|
-
// Emails for delegated accounts will be uploaded as the sender is different.
|
|
3480
|
-
// SMTP is disabled for shared mailboxes, so we need to send using the main account.
|
|
3481
|
-
let skipIfOutlook = this.isOutlook && (!accountData.oauth2 || !accountData.oauth2.auth || !accountData.oauth2.auth.delegatedUser);
|
|
3482
|
-
|
|
3483
|
-
if ((this.isGmail || skipIfOutlook) && !gatewayData) {
|
|
3484
|
-
shouldCopy = false;
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3487
|
-
// Message specific setting, overrides all other settings
|
|
3488
|
-
if (typeof data.copy === 'boolean') {
|
|
3489
|
-
shouldCopy = data.copy;
|
|
3010
|
+
// ignore
|
|
3490
3011
|
}
|
|
3491
3012
|
|
|
3492
|
-
//
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3013
|
+
// Handle sent mail copy to folder
|
|
3014
|
+
const shouldCopy = SentMailCopyDecider.shouldCopy({
|
|
3015
|
+
accountData,
|
|
3016
|
+
data,
|
|
3017
|
+
isGmail: this.isGmail,
|
|
3018
|
+
isOutlook: this.isOutlook,
|
|
3019
|
+
gatewayData
|
|
3020
|
+
});
|
|
3497
3021
|
|
|
3498
|
-
|
|
3022
|
+
const connectionOptions = { allowSecondary: true };
|
|
3499
3023
|
|
|
3500
3024
|
if (shouldCopy) {
|
|
3501
|
-
|
|
3502
|
-
// Upload the message to Sent Mail folder
|
|
3503
|
-
|
|
3504
|
-
try {
|
|
3505
|
-
this.checkIMAPConnection(connectionOptions);
|
|
3506
|
-
|
|
3507
|
-
// Find or use specified sent folder
|
|
3508
|
-
let sentMailbox =
|
|
3509
|
-
data.sentMailPath && typeof data.sentMailPath === 'string'
|
|
3510
|
-
? {
|
|
3511
|
-
path: data.sentMailPath
|
|
3512
|
-
}
|
|
3513
|
-
: await this.getSpecialUseMailbox('\\Sent');
|
|
3514
|
-
|
|
3515
|
-
if (sentMailbox) {
|
|
3516
|
-
if (raw.buffer) {
|
|
3517
|
-
// convert from a Uint8Array to a Buffer
|
|
3518
|
-
raw = Buffer.from(raw);
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
|
-
const connectionClient = await this.getImapConnection(connectionOptions, 'submitMessage');
|
|
3522
|
-
|
|
3523
|
-
// Upload message with Seen flag
|
|
3524
|
-
await connectionClient.append(sentMailbox.path, raw, ['\\Seen']);
|
|
3525
|
-
|
|
3526
|
-
// Return to IDLE if using primary connection
|
|
3527
|
-
if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
3528
|
-
// force back to IDLE
|
|
3529
|
-
this.imapClient.idle().catch(err => {
|
|
3530
|
-
this.logger.error({ msg: 'IDLE error', err });
|
|
3531
|
-
});
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
} catch (err) {
|
|
3535
|
-
this.logger.error({ msg: 'Failed to upload Sent mail', queueId, messageId, err });
|
|
3536
|
-
}
|
|
3025
|
+
await this.uploadToSentFolder(raw, data, queueId, messageId, connectionOptions);
|
|
3537
3026
|
}
|
|
3538
3027
|
|
|
3539
|
-
//
|
|
3028
|
+
// Update reference flags if needed
|
|
3540
3029
|
if (reference && reference.update) {
|
|
3541
|
-
|
|
3542
|
-
this.checkIMAPConnection(connectionOptions);
|
|
3543
|
-
await this.updateMessage(
|
|
3544
|
-
reference.message,
|
|
3545
|
-
{
|
|
3546
|
-
flags: {
|
|
3547
|
-
add: ['\\Answered'].concat(reference.action === 'forward' ? '$Forwarded' : [])
|
|
3548
|
-
}
|
|
3549
|
-
},
|
|
3550
|
-
connectionOptions
|
|
3551
|
-
);
|
|
3552
|
-
} catch (err) {
|
|
3553
|
-
this.logger.error({ msg: 'Failed to update reference flags', queueId, messageId, reference, err });
|
|
3554
|
-
}
|
|
3030
|
+
await this.updateReferenceFlags(reference, queueId, messageId, connectionOptions);
|
|
3555
3031
|
}
|
|
3556
3032
|
|
|
3557
3033
|
// Update feedback key if provided
|
|
3558
3034
|
if (data.feedbackKey) {
|
|
3559
|
-
await this.
|
|
3560
|
-
.multi()
|
|
3561
|
-
.hset(data.feedbackKey, 'success', 'true')
|
|
3562
|
-
.expire(1 * 60 * 60);
|
|
3035
|
+
await this.updateFeedbackKey(data.feedbackKey, true);
|
|
3563
3036
|
}
|
|
3564
3037
|
|
|
3565
3038
|
// Update gateway usage stats
|
|
3566
3039
|
if (gatewayData) {
|
|
3567
|
-
|
|
3568
|
-
await gatewayObject.update({
|
|
3569
|
-
lastError: null,
|
|
3570
|
-
lastUse: new Date(),
|
|
3571
|
-
deliveries: { inc: 1 }
|
|
3572
|
-
});
|
|
3573
|
-
} catch (err) {
|
|
3574
|
-
this.logger.error({ msg: 'Failed to update gateway', queueId, messageId, reference, gateway: gatewayData.gateway, err });
|
|
3575
|
-
}
|
|
3040
|
+
await this.updateGatewayStats(gatewayObject, queueId, messageId, reference);
|
|
3576
3041
|
}
|
|
3577
3042
|
|
|
3578
|
-
return {
|
|
3579
|
-
response: info.response,
|
|
3580
|
-
messageId: info.messageId
|
|
3581
|
-
};
|
|
3043
|
+
return { response: info.response, messageId: info.messageId };
|
|
3582
3044
|
} catch (err) {
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
}
|
|
3597
|
-
break;
|
|
3598
|
-
case 'EMESSAGE':
|
|
3599
|
-
case 'ESTREAM':
|
|
3600
|
-
case 'EENVELOPE':
|
|
3601
|
-
// Ignore. Too generic or message related
|
|
3602
|
-
break;
|
|
3603
|
-
case 'ETIMEDOUT':
|
|
3604
|
-
// firewall?
|
|
3605
|
-
smtpStatus = {
|
|
3606
|
-
description: `Request timed out. Possibly a firewall issue or a wrong hostname/port (${smtpSettings.host}:${smtpSettings.port}).`
|
|
3607
|
-
};
|
|
3608
|
-
break;
|
|
3609
|
-
case 'ETLS':
|
|
3610
|
-
smtpStatus = {
|
|
3611
|
-
description: `EmailEngine failed to set up TLS session with ${smtpSettings.host}:${smtpSettings.port}`
|
|
3612
|
-
};
|
|
3613
|
-
break;
|
|
3614
|
-
case 'EDNS':
|
|
3615
|
-
smtpStatus = {
|
|
3616
|
-
description: `EmailEngine failed to resolve DNS record for ${smtpSettings.host}`
|
|
3617
|
-
};
|
|
3618
|
-
break;
|
|
3619
|
-
case 'ECONNECTION':
|
|
3620
|
-
smtpStatus = {
|
|
3621
|
-
description: `EmailEngine failed to establish TCP connection against ${smtpSettings.host}`
|
|
3622
|
-
};
|
|
3623
|
-
break;
|
|
3624
|
-
case 'EPROTOCOL':
|
|
3625
|
-
smtpStatus = {
|
|
3626
|
-
description: `Unexpected response from ${smtpSettings.host}`
|
|
3627
|
-
};
|
|
3628
|
-
break;
|
|
3629
|
-
case 'EAUTH':
|
|
3630
|
-
smtpStatus = {
|
|
3631
|
-
description: `Authentication failed`
|
|
3632
|
-
};
|
|
3633
|
-
break;
|
|
3634
|
-
}
|
|
3045
|
+
await this.handleSubmitError(err, {
|
|
3046
|
+
smtpSettings,
|
|
3047
|
+
networkRouting,
|
|
3048
|
+
gatewayData,
|
|
3049
|
+
gatewayObject,
|
|
3050
|
+
data,
|
|
3051
|
+
jobData,
|
|
3052
|
+
queueId,
|
|
3053
|
+
envelope
|
|
3054
|
+
});
|
|
3055
|
+
throw err;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3635
3058
|
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
smtpStatus
|
|
3648
|
-
);
|
|
3059
|
+
/**
|
|
3060
|
+
* Uploads sent message to the Sent folder via IMAP
|
|
3061
|
+
* @param {Buffer} raw - Raw message content
|
|
3062
|
+
* @param {Object} data - Message data
|
|
3063
|
+
* @param {string} queueId - Queue ID for logging
|
|
3064
|
+
* @param {string} messageId - Message ID for logging
|
|
3065
|
+
* @param {Object} connectionOptions - IMAP connection options
|
|
3066
|
+
*/
|
|
3067
|
+
async uploadToSentFolder(raw, data, queueId, messageId, connectionOptions) {
|
|
3068
|
+
try {
|
|
3069
|
+
this.checkIMAPConnection(connectionOptions);
|
|
3649
3070
|
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3071
|
+
const sentMailbox =
|
|
3072
|
+
data.sentMailPath && typeof data.sentMailPath === 'string' ? { path: data.sentMailPath } : await this.getSpecialUseMailbox('\\Sent');
|
|
3073
|
+
|
|
3074
|
+
if (sentMailbox) {
|
|
3075
|
+
if (raw.buffer) {
|
|
3076
|
+
raw = Buffer.from(raw);
|
|
3655
3077
|
}
|
|
3656
3078
|
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
} catch (err) {
|
|
3665
|
-
// ignore?
|
|
3666
|
-
}
|
|
3079
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'submitMessage');
|
|
3080
|
+
await connectionClient.append(sentMailbox.path, raw, ['\\Seen']);
|
|
3081
|
+
|
|
3082
|
+
if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
3083
|
+
this.imapClient.idle().catch(err => {
|
|
3084
|
+
this.logger.error({ msg: 'IDLE error', err });
|
|
3085
|
+
});
|
|
3667
3086
|
}
|
|
3668
3087
|
}
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
this.logger.error({ msg: 'Failed to upload Sent mail', queueId, messageId, err });
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3669
3092
|
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3093
|
+
/**
|
|
3094
|
+
* Updates flags on the referenced message (for replies/forwards)
|
|
3095
|
+
* @param {Object} reference - Reference message info
|
|
3096
|
+
* @param {string} queueId - Queue ID for logging
|
|
3097
|
+
* @param {string} messageId - Message ID for logging
|
|
3098
|
+
* @param {Object} connectionOptions - IMAP connection options
|
|
3099
|
+
*/
|
|
3100
|
+
async updateReferenceFlags(reference, queueId, messageId, connectionOptions) {
|
|
3101
|
+
try {
|
|
3102
|
+
this.checkIMAPConnection(connectionOptions);
|
|
3103
|
+
await this.updateMessage(
|
|
3104
|
+
reference.message,
|
|
3105
|
+
{
|
|
3106
|
+
flags: {
|
|
3107
|
+
add: ['\\Answered'].concat(reference.action === 'forward' ? '$Forwarded' : [])
|
|
3108
|
+
}
|
|
3109
|
+
},
|
|
3110
|
+
connectionOptions
|
|
3111
|
+
);
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
this.logger.error({ msg: 'Failed to update reference flags', queueId, messageId, reference, err });
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
/**
|
|
3118
|
+
* Updates feedback key with success/failure status
|
|
3119
|
+
* @param {string} feedbackKey - Redis key for feedback
|
|
3120
|
+
* @param {boolean} success - Whether the operation succeeded
|
|
3121
|
+
* @param {string} [errorMessage] - Error message if failed
|
|
3122
|
+
*/
|
|
3123
|
+
async updateFeedbackKey(feedbackKey, success, errorMessage) {
|
|
3124
|
+
const feedbackTxn = createTransaction(this.redis, { logger: this.logger });
|
|
3125
|
+
feedbackTxn.add('hset', feedbackKey, 'success', success ? 'true' : 'false');
|
|
3126
|
+
if (!success && errorMessage) {
|
|
3127
|
+
feedbackTxn.add('hset', feedbackKey, 'error', errorMessage);
|
|
3128
|
+
}
|
|
3129
|
+
feedbackTxn.add('expire', feedbackKey, 1 * 60 * 60);
|
|
3130
|
+
await feedbackTxn.exec();
|
|
3131
|
+
}
|
|
3679
3132
|
|
|
3680
|
-
|
|
3681
|
-
|
|
3133
|
+
/**
|
|
3134
|
+
* Updates gateway usage statistics after successful send
|
|
3135
|
+
* @param {Object} gatewayObject - Gateway object
|
|
3136
|
+
* @param {string} queueId - Queue ID for logging
|
|
3137
|
+
* @param {string} messageId - Message ID for logging
|
|
3138
|
+
* @param {Object} reference - Reference message info
|
|
3139
|
+
*/
|
|
3140
|
+
async updateGatewayStats(gatewayObject, queueId, messageId, reference) {
|
|
3141
|
+
try {
|
|
3142
|
+
await gatewayObject.update({
|
|
3143
|
+
lastError: null,
|
|
3144
|
+
lastUse: new Date(),
|
|
3145
|
+
deliveries: { inc: 1 }
|
|
3146
|
+
});
|
|
3147
|
+
} catch (err) {
|
|
3148
|
+
this.logger.error({
|
|
3149
|
+
msg: 'Failed to update gateway',
|
|
3682
3150
|
queueId,
|
|
3683
|
-
|
|
3151
|
+
messageId,
|
|
3152
|
+
reference,
|
|
3153
|
+
gateway: gatewayObject.gateway,
|
|
3154
|
+
err
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3684
3158
|
|
|
3685
|
-
|
|
3159
|
+
/**
|
|
3160
|
+
* Handles errors during message submission
|
|
3161
|
+
* @param {Error} err - The error that occurred
|
|
3162
|
+
* @param {Object} context - Error handling context
|
|
3163
|
+
*/
|
|
3164
|
+
async handleSubmitError(err, context) {
|
|
3165
|
+
const { smtpSettings, networkRouting, gatewayData, gatewayObject, data, jobData, queueId, envelope } = context;
|
|
3686
3166
|
|
|
3687
|
-
|
|
3688
|
-
|
|
3167
|
+
// Handle permanent failures
|
|
3168
|
+
if (err.responseCode >= 500 && jobData.opts?.attempts <= jobData.attemptsMade) {
|
|
3169
|
+
jobData.nextAttempt = false;
|
|
3170
|
+
}
|
|
3689
3171
|
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
smtpCommand: err.command,
|
|
3172
|
+
// Build SMTP status from error
|
|
3173
|
+
const smtpStatus = SmtpErrorBuilder.buildStatus(err, smtpSettings, networkRouting);
|
|
3693
3174
|
|
|
3694
|
-
|
|
3175
|
+
if (smtpStatus) {
|
|
3176
|
+
// Store SMTP error for the account
|
|
3177
|
+
try {
|
|
3178
|
+
await this.redis.hset(this.getAccountKey(), 'smtpStatus', JSON.stringify(smtpStatus));
|
|
3179
|
+
} catch (storeErr) {
|
|
3180
|
+
// ignore
|
|
3181
|
+
}
|
|
3695
3182
|
|
|
3696
|
-
|
|
3697
|
-
|
|
3183
|
+
// Update gateway error status
|
|
3184
|
+
if (gatewayData) {
|
|
3185
|
+
try {
|
|
3186
|
+
await gatewayObject.update({ lastError: smtpStatus, lastUse: new Date() });
|
|
3187
|
+
} catch (updateErr) {
|
|
3188
|
+
// ignore
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3698
3192
|
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3193
|
+
// Update feedback key with failure status
|
|
3194
|
+
if (data.feedbackKey && !jobData.nextAttempt) {
|
|
3195
|
+
const errorMessage = (smtpStatus && smtpStatus.description) || 'Failed to send email';
|
|
3196
|
+
await this.updateFeedbackKey(data.feedbackKey, false, errorMessage);
|
|
3197
|
+
}
|
|
3702
3198
|
|
|
3703
|
-
|
|
3199
|
+
// Send delivery error notification
|
|
3200
|
+
await this.notify(
|
|
3201
|
+
false,
|
|
3202
|
+
EMAIL_DELIVERY_ERROR_NOTIFY,
|
|
3203
|
+
NotificationBuilder.buildErrorPayload({
|
|
3204
|
+
error: err,
|
|
3205
|
+
queueId,
|
|
3206
|
+
envelope,
|
|
3207
|
+
messageId: data.messageId,
|
|
3208
|
+
networkRouting,
|
|
3209
|
+
jobData
|
|
3210
|
+
})
|
|
3211
|
+
);
|
|
3704
3212
|
|
|
3705
|
-
|
|
3706
|
-
|
|
3213
|
+
// Enhance error with additional context
|
|
3214
|
+
err.code = err.code || 'SubmitFail';
|
|
3215
|
+
err.statusCode = Number(err.responseCode) || null;
|
|
3216
|
+
err.info = { networkRouting };
|
|
3707
3217
|
}
|
|
3708
3218
|
|
|
3709
3219
|
// stub - to be implemented by subclasses
|
|
@@ -3712,4 +3222,5 @@ class BaseClient {
|
|
|
3712
3222
|
}
|
|
3713
3223
|
}
|
|
3714
3224
|
|
|
3715
|
-
|
|
3225
|
+
// Re-export postMetrics from notification-handler for backward compatibility
|
|
3226
|
+
module.exports = { BaseClient, metricsMeta: postMetrics };
|