dsh-instruction-memory 1.4.8 → 1.5.1

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/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
- if (!EMAIL_RE.test(clean)) throw new Error('收件邮箱格式不正确,无法加密')
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
- if (!EMAIL_RE.test(clear)) throw new Error('解密后的反馈收件邮箱格式不正确')
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
- const messageId = textOf(options.messageId) || (randomUUID() + '@' + messageDomain(from))
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 += chunk.toString('utf8')
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
- const code = Number(match[1])
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
- /** EHLO; returns the capability lines the server advertised. */
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
- const response = await command(channel, 'EHLO ' + name, 250, 'EHLO')
738
- return response.lines.map((line) => line.trim())
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 || !canPlain) {
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.4.8",
3
+ "version": "1.5.1",
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",