Initial commit: WpywMail 自建邮件系统:.NET 8 原生 SMTP/IMAP 服务端(DKIM 签名、SPF/DKIM/DMARC 入站校验、SQLite 存储、完整账号体系)、Node 服务端、WinUI 3 客户端与 Web 前端
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
namespace WpywMail.Native;
|
||||
|
||||
/// <summary>
|
||||
/// SMTP DATA 段的线上编码与还原。
|
||||
///
|
||||
/// 为什么要单独抽出来:
|
||||
/// DKIM 是对「签名那一刻的确切字节」做的哈希。任何在签名之后改写报文的行为
|
||||
/// (例如把裸 LF 换成 CRLF)都会让接收方算出的正文哈希对不上,签名直接失效。
|
||||
/// 因此约定:**先规范化行尾 → 再签名 → 传输阶段除 dot-stuffing 外不得改动任何字节**。
|
||||
/// 本类同时被发送侧与自检使用,保证两边行为一致。
|
||||
/// </summary>
|
||||
internal static class SmtpDataEncoder
|
||||
{
|
||||
private static readonly byte[] Crlf = [13, 10];
|
||||
|
||||
/// <summary>把报文规范化为统一的 CRLF 行尾形式(不加密、不加终止行)。应在 DKIM 签名之前调用。</summary>
|
||||
public static byte[] Normalize(byte[] message)
|
||||
{
|
||||
using var output = new MemoryStream(message.Length + 16);
|
||||
var index = 0;
|
||||
while (index < message.Length)
|
||||
{
|
||||
var current = message[index];
|
||||
if (current == (byte)'\r')
|
||||
{
|
||||
output.Write(Crlf);
|
||||
index += index + 1 < message.Length && message[index + 1] == (byte)'\n' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (current == (byte)'\n')
|
||||
{
|
||||
output.Write(Crlf);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
output.WriteByte(current);
|
||||
index++;
|
||||
}
|
||||
var bytes = output.ToArray();
|
||||
if (bytes.Length == 0 || !EndsWithCrlf(bytes))
|
||||
{
|
||||
var padded = new byte[bytes.Length + 2];
|
||||
Buffer.BlockCopy(bytes, 0, padded, 0, bytes.Length);
|
||||
padded[^2] = 13;
|
||||
padded[^1] = 10;
|
||||
return padded;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 DATA 段实际要写出的字节:行首的点做 dot-stuffing,结尾补 CRLF 与单独一行的 "."。
|
||||
/// 除 dot-stuffing 外不改动任何字节,以保证与 DKIM 签名一致。
|
||||
/// </summary>
|
||||
public static byte[] Encode(byte[] normalizedMessage)
|
||||
{
|
||||
using var output = new MemoryStream(normalizedMessage.Length + 16);
|
||||
var atLineStart = true;
|
||||
foreach (var current in normalizedMessage)
|
||||
{
|
||||
if (atLineStart && current == (byte)'.') output.WriteByte((byte)'.'); // dot-stuffing
|
||||
output.WriteByte(current);
|
||||
atLineStart = current == (byte)'\n';
|
||||
}
|
||||
if (output.Length == 0 || output.GetBuffer()[output.Length - 1] != (byte)'\n') output.Write(Crlf);
|
||||
output.Write([(byte)'.', 13, 10]);
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>接收侧还原:去掉终止行并做 dot-unstuffing(自检用于模拟收件端)。</summary>
|
||||
public static byte[] Decode(byte[] wire)
|
||||
{
|
||||
// 去掉结尾的 ".\r\n"
|
||||
var end = wire.Length;
|
||||
if (end >= 3 && wire[end - 3] == (byte)'.' && wire[end - 2] == 13 && wire[end - 1] == 10)
|
||||
end -= 3;
|
||||
else if (end >= 2 && wire[end - 2] == (byte)'.' && wire[end - 1] == 10)
|
||||
end -= 2;
|
||||
|
||||
using var output = new MemoryStream(end);
|
||||
var atLineStart = true;
|
||||
for (var index = 0; index < end; index++)
|
||||
{
|
||||
var current = wire[index];
|
||||
if (atLineStart && current == (byte)'.' && index + 1 < end && wire[index + 1] == (byte)'.')
|
||||
{
|
||||
index++; // 去掉填充的点
|
||||
}
|
||||
output.WriteByte(wire[index]);
|
||||
atLineStart = wire[index] == (byte)'\n';
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static bool EndsWithCrlf(byte[] bytes) =>
|
||||
bytes.Length >= 2 && bytes[^2] == 13 && bytes[^1] == 10;
|
||||
}
|
||||
Reference in New Issue
Block a user