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.
- package/CHANGELOG.md +16 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account/account-state.js +248 -0
- package/lib/account.js +17 -178
- package/lib/api-routes/account-routes.js +1006 -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 +3 -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 +5 -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 +12 -12
- package/sbom.json +1 -1
- package/static/js/app.js +5 -5
- package/static/licenses.html +91 -21
- package/translations/de.mo +0 -0
- package/translations/de.po +85 -82
- package/translations/en.mo +0 -0
- package/translations/en.po +63 -71
- package/translations/et.mo +0 -0
- package/translations/et.po +84 -82
- package/translations/fr.mo +0 -0
- package/translations/fr.po +85 -82
- package/translations/ja.mo +0 -0
- package/translations/ja.po +84 -82
- package/translations/messages.pot +67 -80
- package/translations/nl.mo +0 -0
- package/translations/nl.po +86 -82
- package/translations/pl.mo +0 -0
- package/translations/pl.po +84 -82
- 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/workers/webhooks.js +6 -0
- 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
|
@@ -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
|
+
};
|