odac 0.9.0
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/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- package/web/view/header/main.html +14 -0
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
const {log, error} = Candy.core('Log', false).init('Mail', 'IMAP')
|
|
2
|
+
|
|
3
|
+
// IMAP Constants
|
|
4
|
+
const CONSTANTS = {
|
|
5
|
+
UIDVALIDITY: 123456789,
|
|
6
|
+
MAX_AUTH_DATA_SIZE: 1024,
|
|
7
|
+
MAX_COMMAND_SIZE: 8192,
|
|
8
|
+
TIMEOUT_INTERVAL: 30000,
|
|
9
|
+
DEFAULT_BOXES: ['INBOX', 'Drafts', 'Sent', 'Spam', 'Trash'],
|
|
10
|
+
PERMANENT_FLAGS: ['\\Answered', '\\Flagged', '\\Deleted', '\\Seen', '\\Draft', '\\*'],
|
|
11
|
+
CAPABILITIES: ['IMAP4rev1', 'AUTH=PLAIN', 'STARTTLS', 'IDLE'],
|
|
12
|
+
MAX_CONNECTIONS_PER_IP: 10
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Rate limiting store
|
|
16
|
+
const rateLimitStore = new Map()
|
|
17
|
+
|
|
18
|
+
class Connection {
|
|
19
|
+
#auth
|
|
20
|
+
#actions = {
|
|
21
|
+
APPEND: () => this.#append(),
|
|
22
|
+
AUTHENTICATE: () => this.#authenticate(),
|
|
23
|
+
CAPABILITY: () => this.#capability(),
|
|
24
|
+
CLOSE: () => this.#close(),
|
|
25
|
+
COPY: () => this.#copy(),
|
|
26
|
+
CREATE: () => this.#create(),
|
|
27
|
+
DELETE: () => this.#delete(),
|
|
28
|
+
EXAMINE: () => this.#examine(),
|
|
29
|
+
EXPUNGE: () => this.#expunge(),
|
|
30
|
+
FETCH: () => this.#fetch(),
|
|
31
|
+
IDLE: () => this.#idle(),
|
|
32
|
+
LIST: () => this.#list(),
|
|
33
|
+
LSUB: () => this.#lsub(),
|
|
34
|
+
LOGIN: () => this.#login(),
|
|
35
|
+
LOGOUT: () => this.#logout(),
|
|
36
|
+
NOOP: () => this.#noop(),
|
|
37
|
+
RENAME: () => this.#rename(),
|
|
38
|
+
SEARCH: () => this.#search(),
|
|
39
|
+
SELECT: () => this.#select(),
|
|
40
|
+
STARTTLS: () => this.#starttls(),
|
|
41
|
+
STATUS: () => this.#status(),
|
|
42
|
+
STORE: () => this.#store()
|
|
43
|
+
}
|
|
44
|
+
#box = 'INBOX'
|
|
45
|
+
#commands
|
|
46
|
+
#end = false
|
|
47
|
+
#idleInterval
|
|
48
|
+
#options
|
|
49
|
+
#request
|
|
50
|
+
#socket
|
|
51
|
+
#timeout
|
|
52
|
+
#wait = false
|
|
53
|
+
|
|
54
|
+
constructor(socket, self) {
|
|
55
|
+
this.#socket = socket
|
|
56
|
+
this.#options = self.options
|
|
57
|
+
this.#setupRateLimit()
|
|
58
|
+
this.#setupTimeout()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#setupRateLimit() {
|
|
62
|
+
const clientIP = this.#getClientIP()
|
|
63
|
+
const now = Date.now()
|
|
64
|
+
|
|
65
|
+
if (!rateLimitStore.has(clientIP)) {
|
|
66
|
+
rateLimitStore.set(clientIP, {count: 1, firstRequest: now})
|
|
67
|
+
} else {
|
|
68
|
+
const clientData = rateLimitStore.get(clientIP)
|
|
69
|
+
if (now - clientData.firstRequest > 60000) {
|
|
70
|
+
// Reset after 1 minute
|
|
71
|
+
rateLimitStore.set(clientIP, {count: 1, firstRequest: now})
|
|
72
|
+
} else {
|
|
73
|
+
clientData.count++
|
|
74
|
+
if (clientData.count > CONSTANTS.MAX_CONNECTIONS_PER_IP) {
|
|
75
|
+
this.#socket.end()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#setupTimeout() {
|
|
83
|
+
this.#timeout = setTimeout(() => {
|
|
84
|
+
if (!this.#end && this.#socket && !this.#socket.destroyed) {
|
|
85
|
+
try {
|
|
86
|
+
this.#socket.write('* BYE Server timeout\r\n')
|
|
87
|
+
this.#end = true
|
|
88
|
+
this.#socket.end()
|
|
89
|
+
} catch (timeoutError) {
|
|
90
|
+
// Socket might already be closed, just cleanup
|
|
91
|
+
error('Timeout cleanup error:', timeoutError.message)
|
|
92
|
+
this.#cleanup()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, CONSTANTS.TIMEOUT_INTERVAL)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#getClientIP() {
|
|
99
|
+
return this.#socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#authenticate() {
|
|
103
|
+
try {
|
|
104
|
+
const authMechanism = this.#commands[2]?.toUpperCase()
|
|
105
|
+
log('Authenticate request from: ' + this.#getClientIP() + ' using mechanism: ' + authMechanism)
|
|
106
|
+
|
|
107
|
+
// Send appropriate challenge based on auth mechanism
|
|
108
|
+
if (authMechanism === 'PLAIN') {
|
|
109
|
+
this.#write('+ \r\n') // Empty challenge for PLAIN
|
|
110
|
+
} else if (authMechanism === 'LOGIN') {
|
|
111
|
+
this.#write('+ VXNlcm5hbWU6\r\n') // Base64 encoded "Username:"
|
|
112
|
+
} else {
|
|
113
|
+
this.#write('+ Ready for authentication\r\n')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.#wait = true
|
|
117
|
+
|
|
118
|
+
// Set a timeout for authentication data
|
|
119
|
+
const authTimeout = setTimeout(() => {
|
|
120
|
+
if (this.#wait) {
|
|
121
|
+
this.#wait = false
|
|
122
|
+
this.#write(`${this.#request.id} NO Authentication timeout\r\n`)
|
|
123
|
+
log('Authentication timeout from: ' + this.#getClientIP())
|
|
124
|
+
}
|
|
125
|
+
}, 10000) // 10 second timeout
|
|
126
|
+
|
|
127
|
+
this.#socket.once('data', data => {
|
|
128
|
+
clearTimeout(authTimeout)
|
|
129
|
+
this.#wait = false
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const dataStr = data.toString().trim()
|
|
133
|
+
log('Authentication data received from ' + this.#getClientIP() + ': ' + dataStr.substring(0, 50) + '...')
|
|
134
|
+
|
|
135
|
+
if (!dataStr || dataStr.length > CONSTANTS.MAX_AUTH_DATA_SIZE) {
|
|
136
|
+
this.#write(`${this.#request.id} NO Authentication data too large\r\n`)
|
|
137
|
+
log('Authentication data too large from: ' + this.#getClientIP())
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle AUTHENTICATE CANCEL
|
|
142
|
+
if (dataStr === '*') {
|
|
143
|
+
this.#write(`${this.#request.id} BAD Authentication cancelled\r\n`)
|
|
144
|
+
log('Authentication cancelled by client: ' + this.#getClientIP())
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (authMechanism === 'PLAIN') {
|
|
149
|
+
let auth
|
|
150
|
+
try {
|
|
151
|
+
auth = Buffer.from(dataStr, 'base64').toString('utf8').split('\0')
|
|
152
|
+
} catch {
|
|
153
|
+
this.#write(`${this.#request.id} NO Authentication data invalid\r\n`)
|
|
154
|
+
log('Base64 decode failed from: ' + this.#getClientIP())
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (auth.length !== 3 || !auth[1] || !auth[2]) {
|
|
159
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
160
|
+
this.#auth = false
|
|
161
|
+
log('Authentication failed - invalid format from: ' + this.#getClientIP())
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!this.#options.onAuth || typeof this.#options.onAuth !== 'function') {
|
|
166
|
+
this.#write(`${this.#request.id} NO Authentication not available\r\n`)
|
|
167
|
+
log('onAuth handler not available')
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.#options.onAuth(
|
|
172
|
+
{
|
|
173
|
+
username: auth[1],
|
|
174
|
+
password: auth[2]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
remoteAddress: this.#getClientIP()
|
|
178
|
+
},
|
|
179
|
+
err => {
|
|
180
|
+
if (err) {
|
|
181
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
182
|
+
log('Authentication failed for: ' + auth[1])
|
|
183
|
+
this.#auth = false
|
|
184
|
+
} else {
|
|
185
|
+
this.#write(`${this.#request.id} OK Authentication successful\r\n`)
|
|
186
|
+
log('Authentication successful for: ' + auth[1])
|
|
187
|
+
this.#auth = auth[1]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
} else if (authMechanism === 'LOGIN') {
|
|
192
|
+
// LOGIN mechanism expects username first, then password in separate responses
|
|
193
|
+
const username = Buffer.from(dataStr, 'base64').toString('utf8')
|
|
194
|
+
this.#write('+ UGFzc3dvcmQ6\r\n') // Base64 encoded "Password:"
|
|
195
|
+
|
|
196
|
+
this.#socket.once('data', passwordData => {
|
|
197
|
+
const password = Buffer.from(passwordData.toString().trim(), 'base64').toString('utf8')
|
|
198
|
+
|
|
199
|
+
if (!this.#options.onAuth || typeof this.#options.onAuth !== 'function') {
|
|
200
|
+
this.#write(`${this.#request.id} NO Authentication not available\r\n`)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.#options.onAuth(
|
|
205
|
+
{
|
|
206
|
+
username: username,
|
|
207
|
+
password: password
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
remoteAddress: this.#getClientIP()
|
|
211
|
+
},
|
|
212
|
+
err => {
|
|
213
|
+
if (err) {
|
|
214
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
215
|
+
log('Authentication failed for: ' + username)
|
|
216
|
+
this.#auth = false
|
|
217
|
+
} else {
|
|
218
|
+
this.#write(`${this.#request.id} OK Authentication successful\r\n`)
|
|
219
|
+
log('Authentication successful for: ' + username)
|
|
220
|
+
this.#auth = username
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
} else {
|
|
226
|
+
this.#write(`${this.#request.id} NO Unsupported authentication mechanism\r\n`)
|
|
227
|
+
log('Unsupported auth mechanism: ' + authMechanism)
|
|
228
|
+
this.#auth = false
|
|
229
|
+
}
|
|
230
|
+
} catch (authError) {
|
|
231
|
+
error('Authentication error:', authError.message)
|
|
232
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
233
|
+
this.#auth = false
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Also listen for socket errors during authentication
|
|
238
|
+
const errorHandler = err => {
|
|
239
|
+
clearTimeout(authTimeout)
|
|
240
|
+
this.#wait = false
|
|
241
|
+
error('Socket error during authentication:', err.message)
|
|
242
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
243
|
+
this.#auth = false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.#socket.once('error', errorHandler)
|
|
247
|
+
this.#socket.once('close', () => {
|
|
248
|
+
clearTimeout(authTimeout)
|
|
249
|
+
this.#wait = false
|
|
250
|
+
log('Socket closed during authentication from: ' + this.#getClientIP())
|
|
251
|
+
})
|
|
252
|
+
} catch (err) {
|
|
253
|
+
error('Authenticate method error:', err.message)
|
|
254
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
255
|
+
this.#wait = false
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#safeParse(jsonString, defaultValue = null) {
|
|
260
|
+
try {
|
|
261
|
+
return JSON.parse(jsonString)
|
|
262
|
+
} catch (parseError) {
|
|
263
|
+
error('JSON parse error:', parseError.message)
|
|
264
|
+
return defaultValue
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#append() {
|
|
269
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
270
|
+
if (!this.#options.onAppend || typeof this.#options.onAppend != 'function')
|
|
271
|
+
return this.#write(`${this.#request.id} NO APPEND failed\r\n`)
|
|
272
|
+
let mailbox = this.#commands[2]
|
|
273
|
+
let flags = this.#commands[3]
|
|
274
|
+
let size = this.#commands[4]
|
|
275
|
+
if (size.startsWith('{') && size.endsWith('}')) size = size.substr(1, size.length - 2)
|
|
276
|
+
this.#write('+ Ready for literal data\r\n')
|
|
277
|
+
this.#wait = true
|
|
278
|
+
this.#socket.once('data', data => {
|
|
279
|
+
this.#options.onAppend({address: this.#auth, mailbox: mailbox, flags: flags, message: data.toString()}, err => {
|
|
280
|
+
if (err) return this.#write(`${this.#request.id} NO APPEND failed\r\n`)
|
|
281
|
+
this.#write(`${this.#request.id} OK APPEND completed\r
|
|
282
|
+
`)
|
|
283
|
+
})
|
|
284
|
+
this.#wait = false
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#bad() {
|
|
289
|
+
error('Unknown command', this.#request.action)
|
|
290
|
+
this.#write(`${this.#request.id} BAD Unknown command\r\n`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#capability() {
|
|
294
|
+
this.#write(`* CAPABILITY ${CONSTANTS.CAPABILITIES.join(' ')}\r\n`)
|
|
295
|
+
this.#write(`${this.#request.id} OK CAPABILITY completed\r\n`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#close() {
|
|
299
|
+
if (this.#box) this.#expunge()
|
|
300
|
+
this.#write(`${this.#request.id} OK CLOSE completed\r\n`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#create() {
|
|
304
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
305
|
+
if (!this.#options.onCreate || typeof this.#options.onCreate != 'function')
|
|
306
|
+
return this.#write(`${this.#request.id} NO CREATE failed\r\n`)
|
|
307
|
+
let mailbox = this.#commands.slice(2).join(' ')
|
|
308
|
+
this.#options.onCreate({address: this.#auth, mailbox: mailbox}, err => {
|
|
309
|
+
if (err) return this.#write(`${this.#request.id} NO CREATE failed\r\n`)
|
|
310
|
+
this.#write(`${this.#request.id} OK CREATE completed\r\n`)
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#delete() {
|
|
315
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
316
|
+
if (!this.#options.onDelete || typeof this.#options.onDelete != 'function')
|
|
317
|
+
return this.#write(`${this.#request.id} NO DELETE failed\r\n`)
|
|
318
|
+
let mailbox = this.#commands.slice(2).join(' ')
|
|
319
|
+
this.#options.onDelete({address: this.#auth, mailbox: mailbox}, err => {
|
|
320
|
+
if (err) return this.#write(`${this.#request.id} NO DELETE failed\r\n`)
|
|
321
|
+
this.#write(`${this.#request.id} OK DELETE completed\r\n`)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#data(data) {
|
|
326
|
+
try {
|
|
327
|
+
log('Data received from: ' + this.#getClientIP(), data.toString().trim())
|
|
328
|
+
if (this.#wait || !data || data.toString().trim().length === 0) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const dataStr = data.toString().trim()
|
|
333
|
+
if (dataStr.length > CONSTANTS.MAX_COMMAND_SIZE) {
|
|
334
|
+
this.#write(`* BAD Command too large\r\n`)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.#commands = dataStr.split(' ')
|
|
339
|
+
this.#request = {}
|
|
340
|
+
const dataParts = dataStr.split(' ')
|
|
341
|
+
this.#request.id = dataParts.shift()
|
|
342
|
+
this.#request.action = dataParts.filter(item => Object.keys(this.#actions).includes(item.toUpperCase())).join(' ')
|
|
343
|
+
|
|
344
|
+
log('Incoming IMAP command: ' + this.#request.action)
|
|
345
|
+
|
|
346
|
+
const index = dataParts.indexOf(this.#request.action)
|
|
347
|
+
dataParts.splice(dataParts.indexOf(this.#request.action), 1)
|
|
348
|
+
this.#request.action = this.#request.action.toUpperCase()
|
|
349
|
+
|
|
350
|
+
if (dataParts.includes('UID') && dataParts.indexOf('UID') < index) {
|
|
351
|
+
this.#request.uid = dataParts[dataParts.indexOf('UID') + 1]
|
|
352
|
+
dataParts.splice(dataParts.indexOf('UID'), 2)
|
|
353
|
+
if (!dataParts.includes('UID') && !dataParts.includes('(UID')) {
|
|
354
|
+
dataParts.splice(0, 0, 'UID')
|
|
355
|
+
}
|
|
356
|
+
} else if (index === 0) {
|
|
357
|
+
this.#request.uid = dataParts[0]
|
|
358
|
+
dataParts.shift()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.#request.requests = this.#export(dataParts)
|
|
362
|
+
|
|
363
|
+
if (this.#actions[this.#request.action]) {
|
|
364
|
+
this.#actions[this.#request.action]()
|
|
365
|
+
} else {
|
|
366
|
+
this.#bad()
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
error('Data processing error:', err.message)
|
|
370
|
+
this.#bad()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#examine() {
|
|
375
|
+
try {
|
|
376
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
377
|
+
if (!this.#options.onSelect || typeof this.#options.onSelect !== 'function') {
|
|
378
|
+
return this.#write(`${this.#request.id} NO EXAMINE failed\r\n`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.#box = this.#commands[2]
|
|
382
|
+
this.#options.onSelect(this.#auth, this.#options, data => {
|
|
383
|
+
const flagsList = CONSTANTS.PERMANENT_FLAGS.join(' ')
|
|
384
|
+
this.#write(`* FLAGS (${flagsList})\r\n`)
|
|
385
|
+
this.#write(`* OK [PERMANENTFLAGS (${flagsList})] Flags permitted\r\n`)
|
|
386
|
+
if (data.exists !== undefined) this.#write('* ' + data.exists + ' EXISTS\r\n')
|
|
387
|
+
this.#write('* ' + (data.recent ?? data.exists ?? 0) + ' RECENT\r\n')
|
|
388
|
+
if (data.unseen !== undefined) {
|
|
389
|
+
this.#write('* OK [UNSEEN ' + data.unseen + '] Message ' + (data.unseen ?? 0) + ' is first unseen\r\n')
|
|
390
|
+
}
|
|
391
|
+
if (data.uidvalidity !== undefined) {
|
|
392
|
+
this.#write('* OK [UIDVALIDITY ' + data.uidvalidity + '] UIDs valid\r\n')
|
|
393
|
+
}
|
|
394
|
+
if (data.uidnext !== undefined) {
|
|
395
|
+
this.#write('* OK [UIDNEXT ' + data.uidnext + '] Predicted next UID\r\n')
|
|
396
|
+
}
|
|
397
|
+
this.#write(`${this.#request.id} OK [READ-ONLY] EXAMINE completed\r\n`)
|
|
398
|
+
})
|
|
399
|
+
} catch (err) {
|
|
400
|
+
error('EXAMINE command failed:', err.message)
|
|
401
|
+
this.#write(`${this.#request.id} NO EXAMINE failed\r\n`)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#expunge() {
|
|
406
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
407
|
+
if (!this.#options.onExpunge || typeof this.#options.onExpunge != 'function')
|
|
408
|
+
return this.#write(`${this.#request.id} NO EXPUNGE failed\r\n`)
|
|
409
|
+
this.#options.onExpunge({address: this.#auth, mailbox: this.#box}, (err, uids) => {
|
|
410
|
+
if (err) return this.#write(`${this.#request.id} NO EXPUNGE failed\r\n`)
|
|
411
|
+
for (let uid of uids) this.#write(`* ${uid} EXPUNGE\r\n`)
|
|
412
|
+
this.#write(`${this.#request.id} OK EXPUNGE completed\r\n`)
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#export(data) {
|
|
417
|
+
let result = []
|
|
418
|
+
while (data.length) {
|
|
419
|
+
let item = data.shift()
|
|
420
|
+
let fields = []
|
|
421
|
+
let index = data.indexOf(item)
|
|
422
|
+
if (item.includes('[]')) item = item.split('[]')[0]
|
|
423
|
+
if (item.startsWith('(') || item.startsWith('[')) item = item.substring(1)
|
|
424
|
+
if (!data[index + 1]?.startsWith('(BODY')) {
|
|
425
|
+
if (
|
|
426
|
+
item.includes('[') ||
|
|
427
|
+
item.includes('(') ||
|
|
428
|
+
(data[index + 1] ?? '').startsWith('[') ||
|
|
429
|
+
(data[index + 1] ?? '').startsWith('(')
|
|
430
|
+
) {
|
|
431
|
+
let next = true
|
|
432
|
+
if (item.includes('[') || item.includes('(')) {
|
|
433
|
+
item = item.split(item.includes('[') ? '[' : '(')
|
|
434
|
+
fields.push(item[1].split(']')[0])
|
|
435
|
+
next = !item[1].includes(']') && !item[1].includes(')')
|
|
436
|
+
item = item[0]
|
|
437
|
+
}
|
|
438
|
+
if (next)
|
|
439
|
+
while (data.length && item[1] && !item[1].includes(']') && !item[1].includes(')')) {
|
|
440
|
+
fields.push(data.shift())
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
while (item.endsWith(']') || item.endsWith(')')) item = item.substring(0, item.length - 1)
|
|
445
|
+
let peek = item.includes('.')
|
|
446
|
+
if (peek) {
|
|
447
|
+
peek = item.split('.')[1]
|
|
448
|
+
item = item.split('.')[0]
|
|
449
|
+
}
|
|
450
|
+
result.push({
|
|
451
|
+
value: item,
|
|
452
|
+
peek: peek,
|
|
453
|
+
fields: this.#export(fields)
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
return result
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async #fetch() {
|
|
460
|
+
try {
|
|
461
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
462
|
+
if (!this.#box) return this.#write(`${this.#request.id} NO Mailbox required\r\n`)
|
|
463
|
+
if (!this.#options.onFetch || typeof this.#options.onFetch !== 'function') {
|
|
464
|
+
return this.#write(`${this.#request.id} NO FETCH failed\r\n`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const ids = this.#request.uid ? this.#request.uid.split(',') : ['ALL']
|
|
468
|
+
for (const id of ids) {
|
|
469
|
+
await new Promise(resolve => {
|
|
470
|
+
this.#options.onFetch(
|
|
471
|
+
{
|
|
472
|
+
email: this.#auth,
|
|
473
|
+
mailbox: this.#box,
|
|
474
|
+
limit: id === 'ALL' ? null : id.includes(':') ? id.split(':') : [id, id]
|
|
475
|
+
},
|
|
476
|
+
this.#commands,
|
|
477
|
+
data => {
|
|
478
|
+
if (data === false) {
|
|
479
|
+
this.#write(`${this.#request.id} NO FETCH failed\r\n`)
|
|
480
|
+
return resolve()
|
|
481
|
+
}
|
|
482
|
+
for (const row of data) {
|
|
483
|
+
this.#write('* ' + row.uid + ' FETCH (')
|
|
484
|
+
this.#prepare(this.#request.requests, row)
|
|
485
|
+
this.#write(')\r\n')
|
|
486
|
+
}
|
|
487
|
+
return resolve()
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
this.#write(`${this.#request.id} OK FETCH completed\r\n`)
|
|
493
|
+
} catch (err) {
|
|
494
|
+
error('FETCH command failed:', err.message)
|
|
495
|
+
this.#write(`${this.#request.id} NO FETCH failed\r\n`)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#search() {
|
|
500
|
+
try {
|
|
501
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
502
|
+
if (!this.#box) return this.#write(`${this.#request.id} NO Mailbox required\r\n`)
|
|
503
|
+
if (!this.#options.onSearch || typeof this.#options.onSearch !== 'function') {
|
|
504
|
+
return this.#write(`${this.#request.id} NO SEARCH not implemented\r\n`)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const criteria = this.#commands.slice(2)
|
|
508
|
+
this.#options.onSearch(
|
|
509
|
+
{
|
|
510
|
+
address: this.#auth,
|
|
511
|
+
mailbox: this.#box,
|
|
512
|
+
criteria: criteria
|
|
513
|
+
},
|
|
514
|
+
(err, uids) => {
|
|
515
|
+
if (err) return this.#write(`${this.#request.id} NO SEARCH failed\r\n`)
|
|
516
|
+
this.#write(`* SEARCH ${uids.join(' ')}\r\n`)
|
|
517
|
+
this.#write(`${this.#request.id} OK SEARCH completed\r\n`)
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
} catch (err) {
|
|
521
|
+
error('SEARCH command failed:', err.message)
|
|
522
|
+
this.#write(`${this.#request.id} NO SEARCH failed\r\n`)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#copy() {
|
|
527
|
+
try {
|
|
528
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
529
|
+
if (!this.#box) return this.#write(`${this.#request.id} NO Mailbox required\r\n`)
|
|
530
|
+
if (!this.#options.onCopy || typeof this.#options.onCopy !== 'function') {
|
|
531
|
+
return this.#write(`${this.#request.id} NO COPY not implemented\r\n`)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const uids = this.#request.uid
|
|
535
|
+
const targetMailbox = this.#commands[this.#commands.length - 1]
|
|
536
|
+
|
|
537
|
+
this.#options.onCopy(
|
|
538
|
+
{
|
|
539
|
+
address: this.#auth,
|
|
540
|
+
sourceMailbox: this.#box,
|
|
541
|
+
targetMailbox: targetMailbox,
|
|
542
|
+
uids: uids
|
|
543
|
+
},
|
|
544
|
+
err => {
|
|
545
|
+
if (err) return this.#write(`${this.#request.id} NO COPY failed\r\n`)
|
|
546
|
+
this.#write(`${this.#request.id} OK COPY completed\r\n`)
|
|
547
|
+
}
|
|
548
|
+
)
|
|
549
|
+
} catch (err) {
|
|
550
|
+
error('COPY command failed:', err.message)
|
|
551
|
+
this.#write(`${this.#request.id} NO COPY failed\r\n`)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
#idle() {
|
|
556
|
+
try {
|
|
557
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
558
|
+
if (!this.#box) return this.#write(`${this.#request.id} NO Mailbox required\r\n`)
|
|
559
|
+
|
|
560
|
+
this.#write('+ idling\r\n')
|
|
561
|
+
this.#wait = true
|
|
562
|
+
|
|
563
|
+
// Event listener for IDLE state
|
|
564
|
+
this.#idleInterval = setInterval(() => {
|
|
565
|
+
if (this.#options.onIdle && typeof this.#options.onIdle === 'function') {
|
|
566
|
+
this.#options.onIdle(
|
|
567
|
+
{
|
|
568
|
+
address: this.#auth,
|
|
569
|
+
mailbox: this.#box
|
|
570
|
+
},
|
|
571
|
+
updates => {
|
|
572
|
+
if (updates) {
|
|
573
|
+
for (const update of updates) {
|
|
574
|
+
this.#write(`* ${update}\r\n`)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
}, 5000)
|
|
581
|
+
|
|
582
|
+
this.#socket.once('data', data => {
|
|
583
|
+
const command = data.toString().trim().toUpperCase()
|
|
584
|
+
if (command === 'DONE') {
|
|
585
|
+
clearInterval(this.#idleInterval)
|
|
586
|
+
this.#wait = false
|
|
587
|
+
this.#write(`${this.#request.id} OK IDLE terminated\r\n`)
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
} catch (err) {
|
|
591
|
+
error('IDLE command failed:', err.message)
|
|
592
|
+
this.#write(`${this.#request.id} NO IDLE failed\r\n`)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
#starttls() {
|
|
597
|
+
try {
|
|
598
|
+
if (this.#socket.encrypted) {
|
|
599
|
+
return this.#write(`${this.#request.id} NO TLS already active\r\n`)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.#write(`${this.#request.id} OK Begin TLS negotiation now\r\n`)
|
|
603
|
+
|
|
604
|
+
// TLS upgrade implementation would go here
|
|
605
|
+
// This is a placeholder for actual TLS implementation
|
|
606
|
+
if (this.#options.onStartTLS && typeof this.#options.onStartTLS === 'function') {
|
|
607
|
+
this.#options.onStartTLS(this.#socket, err => {
|
|
608
|
+
if (err) {
|
|
609
|
+
error('STARTTLS failed:', err.message)
|
|
610
|
+
this.#socket.end()
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
} catch (err) {
|
|
615
|
+
error('STARTTLS command failed:', err.message)
|
|
616
|
+
this.#write(`${this.#request.id} NO STARTTLS failed\r\n`)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#list() {
|
|
621
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
622
|
+
if (!this.#options.onList || typeof this.#options.onList != 'function') return this.#write(`${this.#request.id} NO LIST failed\r\n`)
|
|
623
|
+
this.#options.onList({address: this.#auth}, (err, boxes) => {
|
|
624
|
+
if (err) return this.#write(`${this.#request.id} NO LIST failed\r\n`)
|
|
625
|
+
for (let box of boxes) this.#write(`* LIST (\\HasNoChildren) "/" ${box}\r\n`)
|
|
626
|
+
this.#write(`${this.#request.id} OK LIST completed\r\n`)
|
|
627
|
+
})
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
listen() {
|
|
631
|
+
try {
|
|
632
|
+
this.#socket.on('data', data => {
|
|
633
|
+
try {
|
|
634
|
+
this.#data(data)
|
|
635
|
+
} catch (err) {
|
|
636
|
+
error('Data handling error:', err.message)
|
|
637
|
+
this.#bad()
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
this.#socket.on('end', () => {
|
|
642
|
+
this.#end = true
|
|
643
|
+
this.#cleanup()
|
|
644
|
+
if (!this.#socket.destroyed) {
|
|
645
|
+
this.#socket.end()
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
this.#socket.on('error', err => {
|
|
650
|
+
error('Socket error:', err.message)
|
|
651
|
+
this.#end = true
|
|
652
|
+
if (this.#options.onError) {
|
|
653
|
+
this.#options.onError(err)
|
|
654
|
+
}
|
|
655
|
+
this.#cleanup()
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
this.#socket.on('close', () => {
|
|
659
|
+
this.#end = true
|
|
660
|
+
this.#cleanup()
|
|
661
|
+
})
|
|
662
|
+
} catch (err) {
|
|
663
|
+
error('Listen setup error:', err.message)
|
|
664
|
+
this.#cleanup()
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
#cleanup() {
|
|
669
|
+
try {
|
|
670
|
+
// Set end flag first to prevent any further writes
|
|
671
|
+
this.#end = true
|
|
672
|
+
|
|
673
|
+
// Clear timeout
|
|
674
|
+
if (this.#timeout) {
|
|
675
|
+
clearTimeout(this.#timeout)
|
|
676
|
+
this.#timeout = null
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Clear idle interval
|
|
680
|
+
if (this.#idleInterval) {
|
|
681
|
+
clearInterval(this.#idleInterval)
|
|
682
|
+
this.#idleInterval = null
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Remove from rate limit store
|
|
686
|
+
const clientIP = this.#getClientIP()
|
|
687
|
+
if (rateLimitStore.has(clientIP)) {
|
|
688
|
+
const clientData = rateLimitStore.get(clientIP)
|
|
689
|
+
clientData.count--
|
|
690
|
+
if (clientData.count <= 0) {
|
|
691
|
+
rateLimitStore.delete(clientIP)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Remove all listeners safely
|
|
696
|
+
if (this.#socket && !this.#socket.destroyed) {
|
|
697
|
+
this.#socket.removeAllListeners()
|
|
698
|
+
}
|
|
699
|
+
} catch (cleanupError) {
|
|
700
|
+
error('Cleanup error:', cleanupError.message)
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#lsub() {
|
|
705
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
706
|
+
if (!this.#options.onLsub || typeof this.#options.onLsub != 'function') return this.#write(`${this.#request.id} NO LSUB failed\r\n`)
|
|
707
|
+
this.#options.onLsub({address: this.#auth}, (err, boxes) => {
|
|
708
|
+
if (err) return this.#write(`${this.#request.id} NO LSUB failed\r\n`)
|
|
709
|
+
for (let box of boxes) this.#write(`* LSUB (\\HasNoChildren) "/" "${box}"\r\n`)
|
|
710
|
+
this.#write(`${this.#request.id} OK LSUB completed\r\n`)
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
#login() {
|
|
715
|
+
if (this.#options.onAuth && typeof this.#options.onAuth == 'function') {
|
|
716
|
+
if (this.#commands[2].startsWith('"') && this.#commands[2].endsWith('"'))
|
|
717
|
+
this.#commands[2] = this.#commands[2].substr(1, this.#commands[2].length - 2)
|
|
718
|
+
if (this.#commands[3].startsWith('"') && this.#commands[3].endsWith('"'))
|
|
719
|
+
this.#commands[3] = this.#commands[3].substr(1, this.#commands[3].length - 2)
|
|
720
|
+
this.#options.onAuth(
|
|
721
|
+
{
|
|
722
|
+
username: this.#commands[2],
|
|
723
|
+
password: this.#commands[3]
|
|
724
|
+
},
|
|
725
|
+
this.#commands,
|
|
726
|
+
err => {
|
|
727
|
+
if (err) {
|
|
728
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
729
|
+
this.#auth = false
|
|
730
|
+
} else {
|
|
731
|
+
this.#write(`${this.#request.id} OK Authentication successful\r\n`)
|
|
732
|
+
this.#auth = this.#commands[2]
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
)
|
|
736
|
+
} else {
|
|
737
|
+
this.#write(`${this.#request.id} NO Authentication failed\r\n`)
|
|
738
|
+
this.#auth = false
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
#logout() {
|
|
743
|
+
try {
|
|
744
|
+
if (!this.#end && this.#socket && !this.#socket.destroyed && !this.#socket.writableEnded) {
|
|
745
|
+
this.#write('* BYE IMAP4rev1 Server logging out\r\n')
|
|
746
|
+
this.#write(`${this.#request.id} OK LOGOUT completed\r\n`)
|
|
747
|
+
}
|
|
748
|
+
this.#end = true
|
|
749
|
+
this.#cleanup()
|
|
750
|
+
if (this.#socket && !this.#socket.destroyed) {
|
|
751
|
+
this.#socket.end()
|
|
752
|
+
}
|
|
753
|
+
} catch (err) {
|
|
754
|
+
error('Logout error:', err.message)
|
|
755
|
+
this.#cleanup()
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#noop() {
|
|
760
|
+
this.#write(`${this.#request.id} OK NOOP completed\r\n`)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
#prepareBody(request, data, boundary) {
|
|
764
|
+
let body = {keys: '', header: '', content: ''}
|
|
765
|
+
for (let obj of request.fields.length ? request.fields : [{value: 'HEADER'}, {value: 'TEXT'}]) {
|
|
766
|
+
let fields = obj.fields ? obj.fields.map(field => field.value.toLowerCase()) : []
|
|
767
|
+
if (request.fields.length) body.keys += obj.value + (obj.peek ? '.' + obj.peek : '')
|
|
768
|
+
if (fields.length > 0) body.keys += ' ('
|
|
769
|
+
if (obj.value == 'HEADER') {
|
|
770
|
+
for (let line of data.headerLines) {
|
|
771
|
+
let include = true
|
|
772
|
+
if (obj.peek)
|
|
773
|
+
if (obj.peek == 'FIELDS') include = fields.includes(line.key)
|
|
774
|
+
else if (obj.peek == 'FIELDS.NOT') include = !fields.includes(line.key)
|
|
775
|
+
if (include) {
|
|
776
|
+
if (fields.length > 0) body.keys += line.key + ' '
|
|
777
|
+
if (line.key.toLowerCase() == 'content-type') {
|
|
778
|
+
if (data.attachments.length > 0) {
|
|
779
|
+
body.header += 'Content-Type: multipart/mixed; boundary="' + boundary + '"\r\n'
|
|
780
|
+
} else if (data.html && data.html.length > 1 && data.text && data.text.length > 1) {
|
|
781
|
+
body.header += 'Content-Type: multipart/alternative; boundary="' + boundary + '_alt"\r\n'
|
|
782
|
+
} else if (!data.text || data.text.length < 1) {
|
|
783
|
+
body.header += 'Content-Type: text/html; charset=utf-8\r\n'
|
|
784
|
+
} else if (!data.html || data.html.length < 1) {
|
|
785
|
+
body.header += 'Content-Type: text/plain; charset=utf-8\r\n'
|
|
786
|
+
}
|
|
787
|
+
} else body.header += line.line + '\r\n'
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if ((obj.peek ?? '') !== 'FIELDS.NOT') {
|
|
791
|
+
for (let field of fields) {
|
|
792
|
+
if (!data.headerLines.find(line => line.key == field)) {
|
|
793
|
+
if (fields.length > 0) body.keys += field + ' '
|
|
794
|
+
else body.header += field + ': \r\n'
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
body.header = body.header.trim()
|
|
799
|
+
if (fields.length > 0) body.keys = body.keys.trim() + ')'
|
|
800
|
+
} else if (obj.value == 'TEXT') {
|
|
801
|
+
if (body.header.length) body.content += body.header + '\r\n\r\n'
|
|
802
|
+
if (data.html.length > 1 || data.attachments.length) {
|
|
803
|
+
if (data.attachments.length && data.html && data.html.length && data.text && data.text.length) {
|
|
804
|
+
body.content += '--' + boundary + '\r\n'
|
|
805
|
+
body.content += 'Content-Type: multipart/alternative; boundary="' + boundary + '_alt"\r\n'
|
|
806
|
+
}
|
|
807
|
+
if (data.text && data.text.length) {
|
|
808
|
+
body.content += '\r\n--' + boundary + '_alt\r\n'
|
|
809
|
+
body.content += 'Content-Type: text/plain; charset=utf-8\r\n'
|
|
810
|
+
body.content += 'Content-Transfer-Encoding: quoted-printable\r\n\r\n'
|
|
811
|
+
body.content += data.text
|
|
812
|
+
body.content += '\r\n--' + boundary + '_alt\r\n'
|
|
813
|
+
}
|
|
814
|
+
if (data.html.length) {
|
|
815
|
+
if (data.text && data.text.length) {
|
|
816
|
+
body.content += 'Content-Type: text/html; charset=utf-8\r\n'
|
|
817
|
+
body.content += 'Content-Transfer-Encoding: quoted-printable\r\n\r\n'
|
|
818
|
+
}
|
|
819
|
+
body.content += data.html
|
|
820
|
+
if (data.text && data.text.length) body.content += '\r\n--' + boundary + '_alt--\r\n'
|
|
821
|
+
}
|
|
822
|
+
for (let attachment of data.attachments) {
|
|
823
|
+
body.content += '\r\n--' + boundary + '\r\n'
|
|
824
|
+
body.content += 'Content-Type: ' + attachment.contentType + '; name="' + attachment.filename + '"\r\n'
|
|
825
|
+
body.content += 'Content-Transfer-Encoding: base64\r\n'
|
|
826
|
+
body.content += 'Content-Disposition: attachment; filename="' + attachment.filename + '"\r\n\r\n'
|
|
827
|
+
body.content += Buffer.from(attachment.content.data).toString('base64')
|
|
828
|
+
}
|
|
829
|
+
if (data.attachments.length) body.content += '--' + boundary + '--\r\n'
|
|
830
|
+
} else body.content += data.text
|
|
831
|
+
} else if (!isNaN(obj.value)) {
|
|
832
|
+
obj.value = parseInt(obj.value)
|
|
833
|
+
if (obj.value === 1 || (obj.value === 2 && !data.attachments.length)) {
|
|
834
|
+
if (obj.peek === 2 || obj.value === 2) body.content += data.html
|
|
835
|
+
else body.content += data.text
|
|
836
|
+
} else if (obj.value > 1 && data.attachments[obj.value - 2])
|
|
837
|
+
body.content += Buffer.from(data.attachments[obj.value - 2].content.data).toString('base64') + '\r\n'
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (body.content == '') body.content = body.header
|
|
841
|
+
body.content = body.content.replace(/\r\n/g, '\n')
|
|
842
|
+
this.#write('BODY[' + body.keys + '] {' + Buffer.byteLength(body.content, 'utf8') + '}\r\n')
|
|
843
|
+
this.#write(body.content)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
#prepareBodyStructure(data, boundary) {
|
|
847
|
+
let structure = ''
|
|
848
|
+
if (data.text && data.text.length && data.html && data.html.length) structure += '('
|
|
849
|
+
if (data.text && data.text.length)
|
|
850
|
+
structure +=
|
|
851
|
+
'("TEXT" "PLAIN" ("CHARSET" "UTF-8") NIL NIL "QUOTED-PRINTABLE" ' +
|
|
852
|
+
Buffer.byteLength(data.text, 'utf8') +
|
|
853
|
+
' ' +
|
|
854
|
+
data.text.split('\n').length +
|
|
855
|
+
' NIL NIL NIL NIL)'
|
|
856
|
+
if (data.html && data.html.length)
|
|
857
|
+
structure +=
|
|
858
|
+
'("TEXT" "HTML" ("CHARSET" "UTF-8") NIL NIL "QUOTED-PRINTABLE" ' +
|
|
859
|
+
Buffer.byteLength(data.html, 'utf8') +
|
|
860
|
+
' ' +
|
|
861
|
+
data.html.split('\n').length +
|
|
862
|
+
')'
|
|
863
|
+
if (data.text && data.text.length && data.html && data.html.length)
|
|
864
|
+
structure += ' "ALTERNATIVE" ("BOUNDARY" "' + boundary + '_alt") NIL NIL NIL'
|
|
865
|
+
if (data.text && data.text.length && data.html && data.html.length) structure += ')'
|
|
866
|
+
for (let attachment of data.attachments)
|
|
867
|
+
structure +=
|
|
868
|
+
'("APPLICATION" "' +
|
|
869
|
+
attachment.contentType.split('/')[1].toUpperCase() +
|
|
870
|
+
'" ("NAME" "' +
|
|
871
|
+
attachment.filename +
|
|
872
|
+
'") NIL NIL "BASE64" ' +
|
|
873
|
+
Buffer.from(attachment.content.data).toString('base64').length +
|
|
874
|
+
' NIL ("ATTACHMENT" ("FILENAME" "' +
|
|
875
|
+
attachment.filename +
|
|
876
|
+
'")) NIL NIL)'
|
|
877
|
+
if (data.attachments.length) structure += ' "MIXED" ("BOUNDARY" "' + boundary + '") NIL NIL NIL'
|
|
878
|
+
this.#write('BODYSTRUCTURE ' + structure)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
#prepareEnvelope(data) {
|
|
882
|
+
try {
|
|
883
|
+
let envelope = ''
|
|
884
|
+
let from = this.#safeParse(data.from, {value: {address: 'unknown@unknown.com'}})
|
|
885
|
+
envelope += '"' + (data.date || '') + '" '
|
|
886
|
+
envelope += '"' + (data.subject || '') + '" '
|
|
887
|
+
envelope += '"<' + from.value.address + '>" '
|
|
888
|
+
this.#write('ENVELOPE (' + envelope + ') ')
|
|
889
|
+
} catch (err) {
|
|
890
|
+
error('PrepareEnvelope error:', err.message)
|
|
891
|
+
this.#write('ENVELOPE ("" "" "" "") ')
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#prepareInternalDate(data) {
|
|
896
|
+
let date = new Date(data.date)
|
|
897
|
+
this.#write('INTERNALDATE "' + date.toUTCString() + '" ')
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
#prepareFlags(data) {
|
|
901
|
+
let flags = []
|
|
902
|
+
try {
|
|
903
|
+
flags = data.flags ? this.#safeParse(data.flags, []) : []
|
|
904
|
+
} catch (err) {
|
|
905
|
+
error('Error parsing flags:', err.message)
|
|
906
|
+
flags = []
|
|
907
|
+
}
|
|
908
|
+
flags = flags.map(flag => '\\' + flag)
|
|
909
|
+
this.#write('FLAGS (' + flags.join(' ') + ') ')
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
#prepareRfc822(data) {
|
|
913
|
+
this.#write('RFC822.SIZE ' + data.html.length + ' ')
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
#prepareUid(data) {
|
|
917
|
+
this.#write('UID ' + data.uid + ' ')
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
#rename() {
|
|
921
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
922
|
+
if (!this.#options.onRename || typeof this.#options.onRename != 'function')
|
|
923
|
+
return this.#write(`${this.#request.id} NO RENAME failed\r\n`)
|
|
924
|
+
let oldMailbox = this.#commands[2]
|
|
925
|
+
let newMailbox = this.#commands[3]
|
|
926
|
+
this.#options.onRename({address: this.#auth, oldMailbox: oldMailbox, newMailbox: newMailbox}, err => {
|
|
927
|
+
if (err) return this.#write(`${this.#request.id} NO RENAME failed\r\n`)
|
|
928
|
+
this.#write(`${this.#request.id} OK RENAME completed\r\n`)
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
#prepare(requests, data) {
|
|
933
|
+
try {
|
|
934
|
+
data.attachments = data.attachments ? this.#safeParse(data.attachments, []) : []
|
|
935
|
+
for (let request of requests) {
|
|
936
|
+
if (typeof data.headerLines === 'string') {
|
|
937
|
+
data.headerLines = this.#safeParse(data.headerLines, [])
|
|
938
|
+
}
|
|
939
|
+
let boundary = data.headerLines.find(line => line.key && line.key.toLowerCase() === 'content-type')
|
|
940
|
+
if (boundary) boundary = boundary.line.replace(/"/g, '').split('boundary=')[1]
|
|
941
|
+
if (!boundary) boundary = 'boundary' + (data.id || Date.now())
|
|
942
|
+
|
|
943
|
+
switch (request.value) {
|
|
944
|
+
case 'BODY':
|
|
945
|
+
this.#prepareBody(request, data, boundary)
|
|
946
|
+
break
|
|
947
|
+
case 'BODYSTRUCTURE':
|
|
948
|
+
this.#prepareBodyStructure(data, boundary)
|
|
949
|
+
break
|
|
950
|
+
case 'ENVELOPE':
|
|
951
|
+
this.#prepareEnvelope(data)
|
|
952
|
+
break
|
|
953
|
+
case 'INTERNALDATE':
|
|
954
|
+
this.#prepareInternalDate(data)
|
|
955
|
+
break
|
|
956
|
+
case 'FLAGS':
|
|
957
|
+
this.#prepareFlags(data)
|
|
958
|
+
break
|
|
959
|
+
case 'RFC822':
|
|
960
|
+
this.#prepareRfc822(data)
|
|
961
|
+
break
|
|
962
|
+
case 'UID':
|
|
963
|
+
this.#prepareUid(data)
|
|
964
|
+
break
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
} catch (err) {
|
|
968
|
+
error('Prepare error:', err.message)
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
#select() {
|
|
973
|
+
try {
|
|
974
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
975
|
+
if (!this.#options.onSelect || typeof this.#options.onSelect !== 'function') {
|
|
976
|
+
return this.#write(`${this.#request.id} NO SELECT failed\r\n`)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
let box = this.#commands[2]
|
|
980
|
+
if (box && box.startsWith('"') && box.endsWith('"')) {
|
|
981
|
+
box = box.substr(1, box.length - 2)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (!box) {
|
|
985
|
+
return this.#write(`${this.#request.id} NO Mailbox name required\r\n`)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (!CONSTANTS.DEFAULT_BOXES.includes(box.toUpperCase())) {
|
|
989
|
+
box = 'INBOX'
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
this.#box = box
|
|
993
|
+
this.#options.onSelect({address: this.#auth, mailbox: this.#box}, this.#options, data => {
|
|
994
|
+
const flagsList = CONSTANTS.PERMANENT_FLAGS.join(' ')
|
|
995
|
+
this.#write(`* FLAGS (${flagsList})\r\n`)
|
|
996
|
+
this.#write(`* OK [PERMANENTFLAGS (${flagsList})] Flags permitted\r\n`)
|
|
997
|
+
this.#write('* ' + ((data.uidnext ?? 1) - 1) + ' EXISTS\r\n')
|
|
998
|
+
this.#write('* ' + (data.recent ?? data.unseen ?? 0) + ' RECENT\r\n')
|
|
999
|
+
this.#write('* OK [UNSEEN ' + (data.unseen ?? 0) + '] Message ' + (data.unseen ?? 0) + ' is first unseen\r\n')
|
|
1000
|
+
this.#write('* OK [UIDVALIDITY ' + (data.uidvalidity ?? CONSTANTS.UIDVALIDITY) + '] UIDs valid\r\n')
|
|
1001
|
+
this.#write('* OK [UIDNEXT ' + (data.uidnext ?? 1) + '] Predicted next UID\r\n')
|
|
1002
|
+
this.#write(`${this.#request.id} OK [READ-WRITE] SELECT completed\r\n`)
|
|
1003
|
+
})
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
error('SELECT command failed:', err.message)
|
|
1006
|
+
this.#write(`${this.#request.id} NO SELECT failed\r\n`)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
#status() {
|
|
1011
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
1012
|
+
if (!this.#options.onSelect || typeof this.#options.onSelect != 'function')
|
|
1013
|
+
return this.#write(`${this.#request.id} NO STATUS failed\r\n`)
|
|
1014
|
+
let mailbox = this.#commands[2]
|
|
1015
|
+
let fields = this.#commands.slice(3).map(field => field.toUpperCase().replace('(', '').replace(')', ''))
|
|
1016
|
+
if (fields.length === 0) fields = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN']
|
|
1017
|
+
this.#options.onSelect({address: this.#auth, mailbox: mailbox}, this.#options, data => {
|
|
1018
|
+
if (data.exists && !data.messages) data.messages = data.exists
|
|
1019
|
+
this.#write('* STATUS ' + mailbox + ' (')
|
|
1020
|
+
for (let field of fields)
|
|
1021
|
+
if (data[field.toLowerCase()] !== undefined) this.#write(field.toUpperCase() + ' ' + (data[field.toLowerCase()] ?? 0) + ' ')
|
|
1022
|
+
this.#write(')\r\n')
|
|
1023
|
+
this.#write(`${this.#request.id} OK STATUS completed\r\n`)
|
|
1024
|
+
})
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#store() {
|
|
1028
|
+
try {
|
|
1029
|
+
if (!this.#auth) return this.#write(`${this.#request.id} NO Authentication required\r\n`)
|
|
1030
|
+
if (!this.#request.uid) return this.#write(`${this.#request.id} NO UID required\r\n`)
|
|
1031
|
+
|
|
1032
|
+
const uids = this.#request.uid.split(',')
|
|
1033
|
+
for (const field of this.#request.requests) {
|
|
1034
|
+
let action
|
|
1035
|
+
switch (field.value) {
|
|
1036
|
+
case '+FLAGS':
|
|
1037
|
+
action = 'add'
|
|
1038
|
+
break
|
|
1039
|
+
case '-FLAGS':
|
|
1040
|
+
action = 'remove'
|
|
1041
|
+
break
|
|
1042
|
+
case 'FLAGS':
|
|
1043
|
+
action = 'set'
|
|
1044
|
+
break
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (action && this.#options.onStore && typeof this.#options.onStore === 'function') {
|
|
1048
|
+
const flags = field.fields.map(flag => flag.value.replace('\\', '').toLowerCase())
|
|
1049
|
+
this.#options.onStore(
|
|
1050
|
+
{
|
|
1051
|
+
address: this.#auth,
|
|
1052
|
+
uids: uids,
|
|
1053
|
+
action: action,
|
|
1054
|
+
flags: flags
|
|
1055
|
+
},
|
|
1056
|
+
this.#options,
|
|
1057
|
+
data => {
|
|
1058
|
+
if (field.peek !== 'SILENT') {
|
|
1059
|
+
for (const uid of uids) {
|
|
1060
|
+
this.#write('* ' + uid + ' FETCH (FLAGS (' + (data.flags || []).join(' ') + '))\r\n')
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
this.#write(`${this.#request.id} OK STORE completed\r\n`)
|
|
1064
|
+
}
|
|
1065
|
+
)
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
error('STORE command failed:', err.message)
|
|
1070
|
+
this.#write(`${this.#request.id} NO STORE failed\r\n`)
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
#write(data) {
|
|
1075
|
+
try {
|
|
1076
|
+
if (!this.#end && this.#socket && !this.#socket.destroyed && !this.#socket.writableEnded) {
|
|
1077
|
+
this.#socket.write(data)
|
|
1078
|
+
// Reset timeout on activity
|
|
1079
|
+
if (this.#timeout) {
|
|
1080
|
+
clearTimeout(this.#timeout)
|
|
1081
|
+
this.#setupTimeout()
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
error('Write error:', err.message)
|
|
1086
|
+
this.#cleanup()
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
module.exports = Connection
|