emailengine-app 2.61.0 → 2.61.2

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account/account-state.js +248 -0
  4. package/lib/account.js +17 -178
  5. package/lib/api-routes/account-routes.js +1006 -0
  6. package/lib/api-routes/message-routes.js +1377 -0
  7. package/lib/consts.js +12 -2
  8. package/lib/email-client/base-client.js +282 -771
  9. package/lib/email-client/gmail/gmail-api.js +243 -0
  10. package/lib/email-client/gmail-client.js +145 -53
  11. package/lib/email-client/imap/mailbox.js +24 -698
  12. package/lib/email-client/imap/sync-operations.js +812 -0
  13. package/lib/email-client/imap-client.js +3 -1
  14. package/lib/email-client/message-builder.js +566 -0
  15. package/lib/email-client/notification-handler.js +314 -0
  16. package/lib/email-client/outlook/graph-api.js +326 -0
  17. package/lib/email-client/outlook-client.js +159 -113
  18. package/lib/email-client/smtp-pool-manager.js +196 -0
  19. package/lib/imapproxy/imap-server.js +3 -12
  20. package/lib/oauth/gmail.js +4 -4
  21. package/lib/oauth/mail-ru.js +30 -5
  22. package/lib/oauth/outlook.js +57 -3
  23. package/lib/oauth/pubsub/google.js +30 -11
  24. package/lib/oauth/scope-checker.js +202 -0
  25. package/lib/oauth2-apps.js +8 -4
  26. package/lib/redis-operations.js +484 -0
  27. package/lib/routes-ui.js +283 -2582
  28. package/lib/tools.js +5 -196
  29. package/lib/ui-routes/account-routes.js +1931 -0
  30. package/lib/ui-routes/admin-config-routes.js +1233 -0
  31. package/lib/ui-routes/admin-entities-routes.js +2367 -0
  32. package/lib/ui-routes/oauth-routes.js +992 -0
  33. package/lib/utils/network.js +237 -0
  34. package/package.json +12 -12
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +91 -21
  38. package/translations/de.mo +0 -0
  39. package/translations/de.po +85 -82
  40. package/translations/en.mo +0 -0
  41. package/translations/en.po +63 -71
  42. package/translations/et.mo +0 -0
  43. package/translations/et.po +84 -82
  44. package/translations/fr.mo +0 -0
  45. package/translations/fr.po +85 -82
  46. package/translations/ja.mo +0 -0
  47. package/translations/ja.po +84 -82
  48. package/translations/messages.pot +67 -80
  49. package/translations/nl.mo +0 -0
  50. package/translations/nl.po +86 -82
  51. package/translations/pl.mo +0 -0
  52. package/translations/pl.po +84 -82
  53. package/views/account/security.hbs +4 -4
  54. package/views/accounts/account.hbs +13 -13
  55. package/views/accounts/register/imap-server.hbs +12 -12
  56. package/views/config/document-store/pre-processing/index.hbs +4 -2
  57. package/views/config/oauth/app.hbs +6 -7
  58. package/views/config/oauth/index.hbs +2 -2
  59. package/views/config/service.hbs +3 -4
  60. package/views/dashboard.hbs +5 -7
  61. package/views/error.hbs +22 -7
  62. package/views/gateways/gateway.hbs +2 -2
  63. package/views/partials/add_account_modal.hbs +7 -10
  64. package/views/partials/document_store_header.hbs +1 -1
  65. package/views/partials/editor_scope_info.hbs +0 -1
  66. package/views/partials/oauth_config_header.hbs +1 -1
  67. package/views/partials/side_menu.hbs +3 -3
  68. package/views/partials/webhook_form.hbs +2 -2
  69. package/views/templates/index.hbs +1 -1
  70. package/views/templates/template.hbs +8 -8
  71. package/views/tokens/index.hbs +6 -6
  72. package/views/tokens/new.hbs +1 -1
  73. package/views/webhooks/index.hbs +4 -4
  74. package/views/webhooks/webhook.hbs +7 -7
  75. package/workers/api.js +148 -2436
  76. package/workers/smtp.js +2 -1
  77. package/workers/webhooks.js +6 -0
  78. package/lib/imapproxy/imap-core/test/client.js +0 -46
  79. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  80. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  81. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  82. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  83. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  84. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  88. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  89. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  90. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  92. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  93. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  94. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  95. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  96. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  97. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  98. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  99. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  100. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  101. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  102. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  103. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  104. package/test/api-test.js +0 -899
  105. package/test/autoreply-test.js +0 -327
  106. package/test/bounce-test.js +0 -151
  107. package/test/complaint-test.js +0 -256
  108. package/test/fixtures/autoreply/LICENSE +0 -27
  109. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  110. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  111. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  112. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  113. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  114. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  115. package/test/fixtures/bounces/163.eml +0 -2521
  116. package/test/fixtures/bounces/fastmail.eml +0 -242
  117. package/test/fixtures/bounces/gmail.eml +0 -252
  118. package/test/fixtures/bounces/hotmail.eml +0 -655
  119. package/test/fixtures/bounces/mailru.eml +0 -121
  120. package/test/fixtures/bounces/outlook.eml +0 -1107
  121. package/test/fixtures/bounces/postfix.eml +0 -101
  122. package/test/fixtures/bounces/rambler.eml +0 -116
  123. package/test/fixtures/bounces/workmail.eml +0 -142
  124. package/test/fixtures/bounces/yahoo.eml +0 -139
  125. package/test/fixtures/bounces/zoho.eml +0 -83
  126. package/test/fixtures/bounces/zonemta.eml +0 -100
  127. package/test/fixtures/complaints/LICENSE +0 -27
  128. package/test/fixtures/complaints/amazonses.eml +0 -72
  129. package/test/fixtures/complaints/dmarc.eml +0 -59
  130. package/test/fixtures/complaints/hotmail.eml +0 -49
  131. package/test/fixtures/complaints/optout.eml +0 -40
  132. package/test/fixtures/complaints/standard-arf.eml +0 -68
  133. package/test/fixtures/complaints/yahoo.eml +0 -68
  134. package/test/oauth2-apps-test.js +0 -301
  135. package/test/sendonly-test.js +0 -160
  136. package/test/test-config.js +0 -34
  137. package/test/webhooks-server.js +0 -39
@@ -15,7 +15,6 @@ const { oauth2Apps } = require('../oauth2-apps');
15
15
  const { Subconnection } = require('./imap/subconnection');
16
16
 
17
17
  const {
18
- getLocalAddress,
19
18
  normalizePath,
20
19
  resolveCredentials,
21
20
  emitChangeEvent,
@@ -26,6 +25,7 @@ const {
26
25
  getDuration,
27
26
  LRUCache
28
27
  } = require('../tools');
28
+ const { getLocalAddress } = require('../utils/network');
29
29
 
30
30
  // Time to wait between mailbox resync operations (15 minutes)
31
31
  const RESYNC_DELAY = 15 * 60;
@@ -1501,6 +1501,7 @@ class IMAPClient extends BaseClient {
1501
1501
  clearTimeout(this.untaggedExpungeTimer);
1502
1502
  clearTimeout(this.resyncTimer);
1503
1503
  clearTimeout(this.completedTimer);
1504
+ clearTimeout(this.reconnectTimer);
1504
1505
 
1505
1506
  try {
1506
1507
  // Clean up all mailboxes
@@ -1544,6 +1545,7 @@ class IMAPClient extends BaseClient {
1544
1545
  clearTimeout(this.untaggedExpungeTimer);
1545
1546
  clearTimeout(this.resyncTimer);
1546
1547
  clearTimeout(this.completedTimer);
1548
+ clearTimeout(this.reconnectTimer);
1547
1549
 
1548
1550
  this.isClosing = false;
1549
1551
  this.isClosed = true;
@@ -0,0 +1,566 @@
1
+ 'use strict';
2
+
3
+ const { Gateway } = require('../gateway');
4
+ const { oauth2ProviderData } = require('../oauth2-apps');
5
+ const { resolveCredentials } = require('../tools');
6
+ const { getLocalAddress } = require('../utils/network');
7
+ const settings = require('../settings');
8
+ const { TLS_DEFAULTS } = require('../consts');
9
+ const util = require('util');
10
+
11
+ /**
12
+ * Builds SMTP transport configuration from various authentication sources
13
+ */
14
+ class SmtpConfigBuilder {
15
+ /**
16
+ * Creates a new SmtpConfigBuilder
17
+ * @param {Object} options - Builder options
18
+ * @param {Object} options.redis - Redis client instance
19
+ * @param {string} options.secret - Secret key for decryption
20
+ * @param {Object} options.logger - Logger instance
21
+ * @param {string} options.account - Account identifier
22
+ */
23
+ constructor(options) {
24
+ this.redis = options.redis;
25
+ this.secret = options.secret;
26
+ this.logger = options.logger;
27
+ this.account = options.account;
28
+ }
29
+
30
+ /**
31
+ * Loads gateway data if a gateway is specified
32
+ * @param {string} gatewayId - Gateway identifier
33
+ * @param {string} messageId - Message ID for logging
34
+ * @returns {Promise<Object|null>} Gateway data and object, or null
35
+ */
36
+ async loadGateway(gatewayId, messageId) {
37
+ if (!gatewayId) {
38
+ return { gatewayData: null, gatewayObject: null };
39
+ }
40
+
41
+ const gatewayObject = new Gateway({
42
+ gateway: gatewayId,
43
+ redis: this.redis,
44
+ secret: this.secret
45
+ });
46
+
47
+ try {
48
+ const gatewayData = await gatewayObject.loadGatewayData();
49
+ return { gatewayData, gatewayObject };
50
+ } catch (err) {
51
+ this.logger.info({
52
+ msg: 'Failed to load gateway data',
53
+ messageId,
54
+ gateway: gatewayId,
55
+ err
56
+ });
57
+ return { gatewayData: null, gatewayObject };
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Builds base SMTP connection configuration
63
+ * @param {Object} options - Configuration options
64
+ * @param {Object} options.gatewayData - Gateway configuration data
65
+ * @param {Object} options.accountData - Account data
66
+ * @param {Function} options.loadOAuth2Credentials - OAuth2 credential loader
67
+ * @param {Object} options.context - Context object for OAuth2 loading
68
+ * @returns {Promise<Object>} SMTP connection configuration
69
+ */
70
+ async buildConnectionConfig(options) {
71
+ const { gatewayData, accountData, loadOAuth2Credentials, context } = options;
72
+
73
+ if (gatewayData) {
74
+ return this.buildGatewayConfig(gatewayData);
75
+ }
76
+
77
+ if (accountData.oauth2 && accountData.oauth2.auth) {
78
+ return this.buildOAuth2Config(accountData, loadOAuth2Credentials, context);
79
+ }
80
+
81
+ // Deep copy of SMTP settings
82
+ return JSON.parse(JSON.stringify(accountData.smtp));
83
+ }
84
+
85
+ /**
86
+ * Builds configuration from gateway data
87
+ * @param {Object} gatewayData - Gateway configuration
88
+ * @returns {Object} SMTP connection config
89
+ */
90
+ buildGatewayConfig(gatewayData) {
91
+ const config = {
92
+ host: gatewayData.host,
93
+ port: gatewayData.port,
94
+ secure: gatewayData.secure
95
+ };
96
+
97
+ if (gatewayData.user || gatewayData.pass) {
98
+ config.auth = {
99
+ user: gatewayData.user || '',
100
+ pass: gatewayData.pass || ''
101
+ };
102
+ }
103
+
104
+ return config;
105
+ }
106
+
107
+ /**
108
+ * Builds OAuth2-based SMTP configuration
109
+ * @param {Object} accountData - Account data with OAuth2 settings
110
+ * @param {Function} loadOAuth2Credentials - Credential loader function
111
+ * @param {Object} context - Context for credential loading
112
+ * @returns {Promise<Object>} SMTP connection config with OAuth2
113
+ */
114
+ async buildOAuth2Config(accountData, loadOAuth2Credentials, context) {
115
+ const { oauth2User, accessToken, oauth2App } = await loadOAuth2Credentials(accountData, context, 'smtp');
116
+ const providerData = oauth2ProviderData(oauth2App.provider, oauth2App.cloud);
117
+
118
+ return Object.assign(
119
+ {
120
+ auth: {
121
+ user: oauth2User,
122
+ accessToken
123
+ },
124
+ resyncDelay: 900
125
+ },
126
+ providerData.smtp || {}
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Resolves authentication from auth server if configured
132
+ * @param {Object} smtpConnectionConfig - Current SMTP config
133
+ * @returns {Promise<Object|null>} Resolved auth credentials
134
+ */
135
+ async resolveAuthServer(smtpConnectionConfig) {
136
+ if (!smtpConnectionConfig.useAuthServer) {
137
+ return smtpConnectionConfig.auth;
138
+ }
139
+
140
+ try {
141
+ return await resolveCredentials(this.account, 'smtp');
142
+ } catch (err) {
143
+ err.authenticationFailed = true;
144
+ this.logger.error({
145
+ account: this.account,
146
+ err
147
+ });
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Builds complete SMTP settings with all configuration applied
154
+ * @param {Object} options - Configuration options
155
+ * @param {Object} options.smtpConnectionConfig - Base SMTP config
156
+ * @param {Object} options.smtpAuth - Authentication credentials
157
+ * @param {Object} options.accountData - Account data
158
+ * @param {Object} options.data - Request data
159
+ * @returns {Promise<Object>} Complete SMTP settings
160
+ */
161
+ async buildSmtpSettings(options) {
162
+ const { smtpConnectionConfig, smtpAuth, accountData, data } = options;
163
+
164
+ // Get local address for outbound connection
165
+ const { localAddress: address, name, addressSelector: selector } = await getLocalAddress(this.redis, 'smtp', this.account, data.localAddress);
166
+
167
+ this.logger.info({
168
+ msg: 'Selected local address',
169
+ account: this.account,
170
+ proto: 'SMTP',
171
+ address,
172
+ name,
173
+ selector,
174
+ requestedLocalAddress: data.localAddress
175
+ });
176
+
177
+ // Build SMTP logger wrapper
178
+ const smtpLogger = this.buildSmtpLogger();
179
+
180
+ // Create settings object
181
+ const smtpSettings = Object.assign(
182
+ {
183
+ name,
184
+ localAddress: address,
185
+ transactionLog: true,
186
+ logger: smtpLogger
187
+ },
188
+ smtpConnectionConfig
189
+ );
190
+
191
+ // Apply authentication
192
+ if (smtpAuth) {
193
+ smtpSettings.auth = { user: smtpAuth.user };
194
+ if (smtpAuth.accessToken) {
195
+ smtpSettings.auth.type = 'OAuth2';
196
+ smtpSettings.auth.accessToken = smtpAuth.accessToken;
197
+ } else {
198
+ smtpSettings.auth.pass = smtpAuth.pass;
199
+ }
200
+ }
201
+
202
+ // Apply TLS defaults
203
+ this.applyTlsDefaults(smtpSettings);
204
+
205
+ // Apply proxy configuration
206
+ await this.applyProxyConfig(smtpSettings, accountData, data);
207
+
208
+ // Override EHLO hostname if configured
209
+ if (accountData.smtpEhloName) {
210
+ smtpSettings.name = accountData.smtpEhloName;
211
+ }
212
+
213
+ // Handle certificate error configuration
214
+ const ignoreMailCertErrors = await settings.get('ignoreMailCertErrors');
215
+ if (ignoreMailCertErrors && smtpSettings?.tls?.rejectUnauthorized !== false) {
216
+ smtpSettings.tls = smtpSettings.tls || {};
217
+ smtpSettings.tls.rejectUnauthorized = false;
218
+ }
219
+
220
+ return smtpSettings;
221
+ }
222
+
223
+ /**
224
+ * Creates SMTP logger wrapper that forwards to main logger
225
+ * @returns {Object} Logger object with level methods
226
+ */
227
+ buildSmtpLogger() {
228
+ const smtpLogger = {};
229
+ const logger = this.logger;
230
+
231
+ for (const level of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
232
+ smtpLogger[level] = (data, message, ...args) => {
233
+ if (args && args.length) {
234
+ message = util.format(message, ...args);
235
+ }
236
+ data.msg = message;
237
+ data.sub = 'nodemailer';
238
+ if (typeof logger[level] === 'function') {
239
+ logger[level](data);
240
+ } else {
241
+ logger.debug(data);
242
+ }
243
+ };
244
+ }
245
+
246
+ return smtpLogger;
247
+ }
248
+
249
+ /**
250
+ * Applies TLS defaults to SMTP settings
251
+ * @param {Object} smtpSettings - SMTP settings to modify
252
+ */
253
+ applyTlsDefaults(smtpSettings) {
254
+ if (!smtpSettings.tls) {
255
+ smtpSettings.tls = {};
256
+ }
257
+ for (const key of Object.keys(TLS_DEFAULTS)) {
258
+ if (!(key in smtpSettings.tls)) {
259
+ smtpSettings.tls[key] = TLS_DEFAULTS[key];
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Applies proxy configuration from various sources
266
+ * @param {Object} smtpSettings - SMTP settings to modify
267
+ * @param {Object} accountData - Account data
268
+ * @param {Object} data - Request data
269
+ */
270
+ async applyProxyConfig(smtpSettings, accountData, data) {
271
+ if (data.proxy) {
272
+ smtpSettings.proxy = data.proxy;
273
+ } else if (accountData.proxy) {
274
+ smtpSettings.proxy = accountData.proxy;
275
+ } else {
276
+ const proxyUrl = await settings.get('proxyUrl');
277
+ const proxyEnabled = await settings.get('proxyEnabled');
278
+ if (proxyEnabled && proxyUrl && !smtpSettings.proxy) {
279
+ smtpSettings.proxy = proxyUrl;
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Builds network routing information for notifications
287
+ */
288
+ class NetworkRoutingBuilder {
289
+ /**
290
+ * Builds network routing info from SMTP settings
291
+ * @param {Object} smtpSettings - SMTP settings
292
+ * @param {Object} data - Request data with optional localAddress
293
+ * @returns {Object|null} Network routing info or null
294
+ */
295
+ static build(smtpSettings, data) {
296
+ const hasRoutingInfo = smtpSettings.localAddress || smtpSettings.proxy;
297
+ if (!hasRoutingInfo) {
298
+ return null;
299
+ }
300
+
301
+ const networkRouting = {};
302
+
303
+ if (smtpSettings.localAddress) {
304
+ networkRouting.localAddress = smtpSettings.localAddress;
305
+ }
306
+
307
+ if (smtpSettings.proxy) {
308
+ networkRouting.proxy = smtpSettings.proxy;
309
+ }
310
+
311
+ if (smtpSettings.name) {
312
+ networkRouting.name = smtpSettings.name;
313
+ }
314
+
315
+ if (data.localAddress && data.localAddress !== networkRouting.localAddress) {
316
+ networkRouting.requestedLocalAddress = data.localAddress;
317
+ }
318
+
319
+ return networkRouting;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Builds notification payloads for email delivery events
325
+ */
326
+ class NotificationBuilder {
327
+ /**
328
+ * Builds success notification payload
329
+ * @param {Object} options - Notification options
330
+ * @param {Object} options.info - SMTP send result info
331
+ * @param {string} options.originalMessageId - Original message ID if overridden
332
+ * @param {string} options.queueId - Queue ID
333
+ * @param {Object} options.envelope - Message envelope
334
+ * @param {Object} options.networkRouting - Network routing info
335
+ * @returns {Object} Success notification payload
336
+ */
337
+ static buildSuccessPayload(options) {
338
+ const { info, originalMessageId, queueId, envelope, networkRouting } = options;
339
+
340
+ return {
341
+ messageId: info.messageId,
342
+ originalMessageId,
343
+ response: info.response,
344
+ queueId,
345
+ envelope,
346
+ networkRouting
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Builds error notification payload
352
+ * @param {Object} options - Notification options
353
+ * @param {Error} options.error - The error that occurred
354
+ * @param {string} options.queueId - Queue ID
355
+ * @param {Object} options.envelope - Message envelope
356
+ * @param {string} options.messageId - Original message ID
357
+ * @param {Object} options.networkRouting - Network routing info
358
+ * @param {Object} options.jobData - Job data
359
+ * @returns {Object} Error notification payload
360
+ */
361
+ static buildErrorPayload(options) {
362
+ const { error, queueId, envelope, messageId, networkRouting, jobData } = options;
363
+
364
+ return {
365
+ queueId,
366
+ envelope,
367
+ messageId,
368
+ error: error.message,
369
+ errorCode: error.code,
370
+ smtpResponse: error.response,
371
+ smtpResponseCode: error.responseCode,
372
+ smtpCommand: error.command,
373
+ networkRouting,
374
+ job: jobData
375
+ };
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Handles provider-specific message ID extraction and transformation
381
+ */
382
+ class ProviderMessageIdHandler {
383
+ /**
384
+ * Extracts actual message ID from Hotmail/Outlook response
385
+ * The server may override the message ID in its response
386
+ * @param {Object} info - SMTP send result info
387
+ * @returns {string|undefined} Original message ID if overridden
388
+ */
389
+ static handleHotmail(info) {
390
+ const response = (info.response || '').toString();
391
+ const match = response.match(/^250 2.0.0 OK (<[^>]+\.prod\.outlook\.com>)/);
392
+
393
+ if (match && match[1] !== info.messageId) {
394
+ const originalMessageId = info.messageId;
395
+ info.messageId = match[1];
396
+ return originalMessageId;
397
+ }
398
+
399
+ return undefined;
400
+ }
401
+
402
+ /**
403
+ * Constructs message ID from AWS SES response
404
+ * SES returns a message ID in the response that should be used
405
+ * @param {Object} info - SMTP send result info
406
+ * @param {string} smtpHost - SMTP host name
407
+ * @returns {string|undefined} Original message ID if overridden
408
+ */
409
+ static handleAwsSes(info, smtpHost) {
410
+ const hostMatch = (smtpHost || '').toString().match(/\.([^.]+)\.(amazonaws\.com|awsapps\.com)$/i);
411
+ const responseMatch = (info.response || '').toString().match(/^250 Ok ([0-9a-f-]+)$/);
412
+
413
+ if (hostMatch && responseMatch) {
414
+ let region = hostMatch[1].toLowerCase().trim();
415
+ const messageIdPart = responseMatch[1].toLowerCase().trim();
416
+
417
+ if (region === 'us-east-1') {
418
+ region = 'email';
419
+ }
420
+
421
+ const originalMessageId = info.messageId;
422
+ info.messageId = '<' + messageIdPart + (!/@/.test(messageIdPart) ? '@' + region + '.amazonses.com' : '') + '>';
423
+ return originalMessageId;
424
+ }
425
+
426
+ return undefined;
427
+ }
428
+
429
+ /**
430
+ * Processes SMTP response to extract provider-specific message ID
431
+ * @param {Object} info - SMTP send result info
432
+ * @param {string} smtpHost - SMTP host name
433
+ * @returns {string|undefined} Original message ID if it was overridden
434
+ */
435
+ static processResponse(info, smtpHost) {
436
+ // Try Hotmail first
437
+ let originalMessageId = this.handleHotmail(info);
438
+ if (originalMessageId) {
439
+ return originalMessageId;
440
+ }
441
+
442
+ // Try AWS SES
443
+ originalMessageId = this.handleAwsSes(info, smtpHost);
444
+ if (originalMessageId) {
445
+ return originalMessageId;
446
+ }
447
+
448
+ return undefined;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Error code to description mapping for SMTP errors
454
+ */
455
+ const SMTP_ERROR_DESCRIPTIONS = {
456
+ ESOCKET: (settings, err) => {
457
+ if (err.cert && err.reason) {
458
+ return `Certificate check for ${settings.host}:${settings.port} failed. ${err.reason}`;
459
+ }
460
+ return null;
461
+ },
462
+ EMESSAGE: () => null,
463
+ ESTREAM: () => null,
464
+ EENVELOPE: () => null,
465
+ ETIMEDOUT: settings => `Request timed out. Possibly a firewall issue or a wrong hostname/port (${settings.host}:${settings.port}).`,
466
+ ETLS: settings => `EmailEngine failed to set up TLS session with ${settings.host}:${settings.port}`,
467
+ EDNS: settings => `EmailEngine failed to resolve DNS record for ${settings.host}`,
468
+ ECONNECTION: settings => `EmailEngine failed to establish TCP connection against ${settings.host}`,
469
+ EPROTOCOL: settings => `Unexpected response from ${settings.host}`,
470
+ EAUTH: () => 'Authentication failed'
471
+ };
472
+
473
+ /**
474
+ * Builds SMTP error status information for tracking and notifications
475
+ */
476
+ class SmtpErrorBuilder {
477
+ /**
478
+ * Builds SMTP status object from error
479
+ * @param {Error} err - The error that occurred
480
+ * @param {Object} smtpSettings - SMTP settings for context
481
+ * @param {Object} networkRouting - Network routing info
482
+ * @returns {Object|null} SMTP status object or null
483
+ */
484
+ static buildStatus(err, smtpSettings, networkRouting) {
485
+ const descriptionBuilder = SMTP_ERROR_DESCRIPTIONS[err.code];
486
+ if (!descriptionBuilder) {
487
+ return null;
488
+ }
489
+
490
+ const description = descriptionBuilder(smtpSettings, err);
491
+ if (!description) {
492
+ return null;
493
+ }
494
+
495
+ return {
496
+ created: Date.now(),
497
+ status: 'error',
498
+ response: err.response,
499
+ responseCode: err.responseCode,
500
+ code: err.code,
501
+ command: err.command,
502
+ networkRouting,
503
+ description
504
+ };
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Determines whether to copy sent message to Sent folder
510
+ */
511
+ class SentMailCopyDecider {
512
+ /**
513
+ * Determines if sent mail should be copied to Sent folder
514
+ * @param {Object} options - Decision options
515
+ * @param {Object} options.accountData - Account data
516
+ * @param {Object} options.data - Request data
517
+ * @param {boolean} options.isGmail - Whether account is Gmail
518
+ * @param {boolean} options.isOutlook - Whether account is Outlook
519
+ * @param {Object} options.gatewayData - Gateway data if using gateway
520
+ * @returns {boolean} Whether to copy to Sent folder
521
+ */
522
+ static shouldCopy(options) {
523
+ const { accountData, data, isGmail, isOutlook, gatewayData } = options;
524
+
525
+ // The default is to copy message to Sent Mail folder
526
+ let shouldCopy = !Object.prototype.hasOwnProperty.call(accountData, 'copy');
527
+
528
+ // Account specific setting
529
+ if (typeof accountData.copy === 'boolean') {
530
+ shouldCopy = accountData.copy;
531
+ }
532
+
533
+ // Suppress uploads for Gmail and Outlook
534
+ // Unfortunately, previous default schema for all added accounts was copy=true,
535
+ // so can't prefer account specific setting here
536
+ // Emails for delegated accounts will be uploaded as the sender is different.
537
+ // SMTP is disabled for shared mailboxes, so we need to send using the main account.
538
+ const skipIfOutlook = isOutlook && (!accountData.oauth2 || !accountData.oauth2.auth || !accountData.oauth2.auth.delegatedUser);
539
+
540
+ if ((isGmail || skipIfOutlook) && !gatewayData) {
541
+ shouldCopy = false;
542
+ }
543
+
544
+ // Message specific setting, overrides all other settings
545
+ if (typeof data.copy === 'boolean') {
546
+ shouldCopy = data.copy;
547
+ }
548
+
549
+ // Check if IMAP is available
550
+ if ((!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled)) {
551
+ // IMAP is disabled for this account
552
+ shouldCopy = false;
553
+ }
554
+
555
+ return shouldCopy;
556
+ }
557
+ }
558
+
559
+ module.exports = {
560
+ SmtpConfigBuilder,
561
+ NetworkRoutingBuilder,
562
+ NotificationBuilder,
563
+ ProviderMessageIdHandler,
564
+ SmtpErrorBuilder,
565
+ SentMailCopyDecider
566
+ };