dsh-instruction-memory 1.4.7 → 1.5.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/CHANGELOG.md +154 -0
- package/README.md +117 -19
- package/install.mjs +79 -10
- package/lib/client.js +269 -26
- package/lib/index.js +856 -86
- package/lib/mailer.js +50 -9
- package/package.json +1 -1
package/lib/mailer.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { createCipheriv, createDecipheriv, randomBytes, randomUUID, scryptSync } from 'node:crypto'
|
|
28
|
+
import { StringDecoder } from 'node:string_decoder'
|
|
28
29
|
import { connect as netConnect } from 'node:net'
|
|
29
30
|
import { connect as tlsConnect } from 'node:tls'
|
|
30
31
|
import { hostname } from 'node:os'
|
|
@@ -253,7 +254,11 @@ function assertSecret(secret) {
|
|
|
253
254
|
*/
|
|
254
255
|
export function encryptRecipient(address, secret) {
|
|
255
256
|
const clean = textOf(address)
|
|
256
|
-
|
|
257
|
+
// Same verdict the delivery path uses: encrypting an address that SMTP could
|
|
258
|
+
// never deliver (non-ASCII without SMTPUTF8) only surfaced the problem at
|
|
259
|
+
// send time, when the configuration already looked correct.
|
|
260
|
+
const verdict = mailboxVerdict(clean)
|
|
261
|
+
if (verdict !== '') throw new Error('收件邮箱' + verdict + ',无法加密')
|
|
257
262
|
const phrase = textOf(secret)
|
|
258
263
|
assertSecret(phrase)
|
|
259
264
|
const salt = randomBytes(SALT_BYTES)
|
|
@@ -288,7 +293,8 @@ export function decryptRecipient(payload, secret) {
|
|
|
288
293
|
} catch {
|
|
289
294
|
throw new Error('无法解密反馈收件邮箱:密钥不匹配或密文已损坏')
|
|
290
295
|
}
|
|
291
|
-
|
|
296
|
+
const verdict = mailboxVerdict(clear)
|
|
297
|
+
if (verdict !== '') throw new Error('解密后的反馈收件邮箱' + verdict)
|
|
292
298
|
return clear
|
|
293
299
|
}
|
|
294
300
|
|
|
@@ -600,7 +606,14 @@ export function buildRawMail(options = {}) {
|
|
|
600
606
|
const from = assertHeaderSafe(textOf(options.from), '发件邮箱')
|
|
601
607
|
const to = assertHeaderSafe(textOf(options.to), '收件邮箱')
|
|
602
608
|
const now = options.now instanceof Date ? options.now : new Date()
|
|
603
|
-
|
|
609
|
+
// Message-ID is the one header this formatter used to interpolate without
|
|
610
|
+
// validation — and it is exported, so a caller-supplied value could inject
|
|
611
|
+
// CRLF or angle brackets. Only a syntactically id-shaped value survives;
|
|
612
|
+
// anything else falls back to a generated id.
|
|
613
|
+
const candidateId = textOf(options.messageId)
|
|
614
|
+
const messageId = /^[^\s<>@\r\n]+@[^\s<>@\r\n]+$/.test(candidateId)
|
|
615
|
+
? candidateId
|
|
616
|
+
: randomUUID() + '@' + messageDomain(from)
|
|
604
617
|
const subject = oneLine(options.subject)
|
|
605
618
|
const replyTo = oneLine(options.replyTo)
|
|
606
619
|
const body64 = b64(options.text).replace(/(.{76})/g, '$1\r\n')
|
|
@@ -628,6 +641,10 @@ export function buildRawMail(options = {}) {
|
|
|
628
641
|
*/
|
|
629
642
|
function createChannel(socket, timeoutMs) {
|
|
630
643
|
const state = { buffer: '', lines: [], waiters: [], error: null }
|
|
644
|
+
// Decode across chunk boundaries: `chunk.toString('utf8')` corrupts a
|
|
645
|
+
// multi-byte character split between two TCP segments, which turns a server's
|
|
646
|
+
// error text into replacement characters and makes it undiagnosable.
|
|
647
|
+
const decoder = new StringDecoder('utf8')
|
|
631
648
|
|
|
632
649
|
const fail = (err) => {
|
|
633
650
|
if (state.error === null) state.error = err
|
|
@@ -635,7 +652,7 @@ function createChannel(socket, timeoutMs) {
|
|
|
635
652
|
for (const waiter of waiters) waiter.reject(state.error)
|
|
636
653
|
}
|
|
637
654
|
const onData = (chunk) => {
|
|
638
|
-
state.buffer +=
|
|
655
|
+
state.buffer += decoder.write(chunk)
|
|
639
656
|
// A peer that keeps sending bytes without CRLF must not be able to grow this
|
|
640
657
|
// buffer without bound; every other read is line-delimited and small.
|
|
641
658
|
if (state.buffer.length > MAX_RESPONSE_BYTES) {
|
|
@@ -690,7 +707,7 @@ function createChannel(socket, timeoutMs) {
|
|
|
690
707
|
const first = await readLine()
|
|
691
708
|
const match = /^(\d{3})([ -])(.*)$/.exec(first)
|
|
692
709
|
if (match === null) throw new Error('SMTP 响应无法解析:' + cutText(first, 120))
|
|
693
|
-
|
|
710
|
+
let code = Number(match[1])
|
|
694
711
|
const lines = [match[3]]
|
|
695
712
|
if (match[2] === '-') {
|
|
696
713
|
for (;;) {
|
|
@@ -698,6 +715,11 @@ function createChannel(socket, timeoutMs) {
|
|
|
698
715
|
const nextMatch = /^(\d{3})([ -])(.*)$/.exec(next)
|
|
699
716
|
if (nextMatch === null) throw new Error('SMTP 多行响应无法解析:' + cutText(next, 120))
|
|
700
717
|
lines.push(nextMatch[3])
|
|
718
|
+
// A continuation line carrying a DIFFERENT status code is how some
|
|
719
|
+
// servers report the real outcome (e.g. `250-...` then `550 ...`).
|
|
720
|
+
// Keeping the first line's code silently turned a rejection into a
|
|
721
|
+
// success; the last line is the verdict.
|
|
722
|
+
code = Number(nextMatch[1])
|
|
701
723
|
if (nextMatch[2] === ' ') break
|
|
702
724
|
}
|
|
703
725
|
}
|
|
@@ -732,10 +754,22 @@ async function command(channel, line, codes, what) {
|
|
|
732
754
|
return expectCode(channel, codes, what)
|
|
733
755
|
}
|
|
734
756
|
|
|
735
|
-
/**
|
|
757
|
+
/**
|
|
758
|
+
* EHLO; returns the capability lines the server advertised.
|
|
759
|
+
*
|
|
760
|
+
* RFC 5321 §4.1.1.1: a server that does not implement EHLO answers 500, 502 or
|
|
761
|
+
* 504 and expects HELO instead. Without the fallback such a server made the
|
|
762
|
+
* feedback feature unusable with a misleading "EHLO failed (500)" message.
|
|
763
|
+
*/
|
|
736
764
|
async function hello(channel, name) {
|
|
737
|
-
|
|
738
|
-
|
|
765
|
+
await channel.write('EHLO ' + name + '\r\n')
|
|
766
|
+
const response = await channel.readResponse()
|
|
767
|
+
if (response.code === 250) return response.lines.map((line) => line.trim())
|
|
768
|
+
if ([500, 502, 504].includes(response.code)) {
|
|
769
|
+
const helo = await command(channel, 'HELO ' + name, 250, 'HELO')
|
|
770
|
+
return helo.lines.map((line) => line.trim())
|
|
771
|
+
}
|
|
772
|
+
throw smtpError('EHLO', response)
|
|
739
773
|
}
|
|
740
774
|
|
|
741
775
|
/**
|
|
@@ -759,7 +793,14 @@ async function authenticate(channel, smtp, caps) {
|
|
|
759
793
|
+ cutText(response.lines.join(' '), 300)
|
|
760
794
|
+ '。请检查用户名 / 密码;Gmail、QQ 邮箱等服务需要「应用专用密码」,不能用登录密码。')
|
|
761
795
|
|
|
762
|
-
if (canLogin
|
|
796
|
+
if (!canLogin && !canPlain) {
|
|
797
|
+
// Sending AUTH LOGIN to a server that never offered it produced a 504 and
|
|
798
|
+
// an error pointing at the password, which sent users to the wrong place.
|
|
799
|
+
throw new Error('SMTP 服务器不支持 LOGIN / PLAIN 认证(服务器声明的机制:'
|
|
800
|
+
+ (mechanisms.length > 0 ? mechanisms.join('、') : '未声明')
|
|
801
|
+
+ ')。请改用支持 LOGIN 或 PLAIN 的服务器')
|
|
802
|
+
}
|
|
803
|
+
if (canLogin) {
|
|
763
804
|
await command(channel, 'AUTH LOGIN', 334, 'AUTH LOGIN')
|
|
764
805
|
await command(channel, b64(smtp.user), 334, 'AUTH LOGIN 用户名')
|
|
765
806
|
const done = await (async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-instruction-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "User-only instruction memory for DSH: long-term instructions maintained in the settings UI and auto-injected into every conversation — the model has no write access. 指令记忆:只由用户在设置页维护的长期指令,自动注入此后每轮对话;模型没有任何写入口。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|