Initial commit: WpywMail 自建邮件系统:.NET 8 原生 SMTP/IMAP 服务端(DKIM 签名、SPF/DKIM/DMARC 入站校验、SQLite 存储、完整账号体系)、Node 服务端、WinUI 3 客户端与 Web 前端

This commit is contained in:
WpyQwq
2026-09-19 11:19:40 +08:00
commit b8814a7615
84 changed files with 21197 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
using System.Text;
namespace WpywMail.Native;
/// <summary>
/// 字节级 SMTP 行读取器。
///
/// 为什么不用 StreamReader:
/// 1. StreamReader 只能返回字符串,8bit 正文会被字符集转换破坏;
/// 2. StreamReader 会预读缓冲,若之后改从原始流直接读 DATA,属于正文的字节
/// 可能已经被吞进它的缓冲区,造成命令/正文错位。
/// 这里让命令与 DATA 共用同一份缓冲区,从根本上避免这两个问题。
/// </summary>
internal sealed class SmtpReader
{
private const int MaxCommandLine = 8192;
private static readonly byte[] Crlf = [13, 10];
private readonly Stream stream;
private readonly byte[] buffer = new byte[8192];
private readonly MemoryStream line = new(MaxCommandLine);
private int start;
private int end;
public SmtpReader(Stream stream) => this.stream = stream;
/// <summary>读一行命令(不含 CRLF),连接关闭返回 null。</summary>
public async Task<string?> ReadLineAsync(CancellationToken token)
{
while (true)
{
if (start >= end)
{
end = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token);
start = 0;
if (end <= 0) return null;
}
var current = buffer[start++];
if (current == (byte)'\n')
{
var bytes = line.ToArray();
line.SetLength(0);
var length = bytes.Length;
if (length > 0 && bytes[length - 1] == (byte)'\r') length--;
return Encoding.ASCII.GetString(bytes, 0, length);
}
line.WriteByte(current);
if (line.Length > MaxCommandLine) throw new InvalidOperationException("命令行过长,已断开。");
}
}
/// <summary>从同一缓冲区读取恰好 length 个字节(IMAP 的 literal 需要)。</summary>
public async Task<byte[]> ReadExactlyAsync(int length, CancellationToken token)
{
if (length < 0) throw new InvalidOperationException("长度非法。");
var result = new byte[length];
var written = 0;
while (written < length)
{
if (start >= end)
{
end = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token);
start = 0;
if (end <= 0) throw new IOException("连接在读取 literals 途中关闭。");
}
var take = Math.Min(length - written, end - start);
Buffer.BlockCopy(buffer, start, result, written, take);
start += take;
written += take;
}
return result;
}
/// <summary>
/// 读取 DATA 段直到单独一行的 "."。按字节忠实处理并做 dot-unstuffing,
/// 行尾统一为 CRLF。超过 maxBytes 抛 InvalidOperationException。
/// </summary>
public async Task<byte[]> ReadDataAsync(int maxBytes, CancellationToken token)
{
var message = new MemoryStream();
line.SetLength(0);
while (true)
{
if (start >= end)
{
end = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token);
start = 0;
if (end <= 0) throw new IOException("连接在 DATA 中途关闭。");
}
var current = buffer[start++];
if (current != (byte)'\n')
{
line.WriteByte(current);
if (line.Length > maxBytes) throw new InvalidOperationException("单行过长,已拒绝。");
continue;
}
var text = line.ToArray();
line.SetLength(0);
if (text.Length > 0 && text[^1] == (byte)'\r') text = text[..^1];
if (text.Length == 1 && text[0] == (byte)'.') break; // DATA 结束
if (text.Length > 1 && text[0] == (byte)'.') text = text[1..]; // dot-unstuffing
message.Write(text);
message.Write(Crlf);
if (message.Length > maxBytes)
throw new InvalidOperationException($"邮件超过 {maxBytes / 1024 / 1024} MB 上限,已拒绝。");
}
return message.ToArray();
}
}