Initial commit: WpywMail 自建邮件系统:.NET 8 原生 SMTP/IMAP 服务端(DKIM 签名、SPF/DKIM/DMARC 入站校验、SQLite 存储、完整账号体系)、Node 服务端、WinUI 3 客户端与 Web 前端
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
namespace WpywMail.Native;
|
||||
|
||||
/// <summary>
|
||||
/// 一次性数据迁移(用法:WpywMail.Native.exe --migrate)。
|
||||
///
|
||||
/// 1.x 版本有两个存储层缺陷:
|
||||
/// · Mime.Parse 不解码 RFC 2047 编码字,也不解 base64/QP 正文 ——
|
||||
/// 导致 messages.json 里的主题是「=?utf-8?b?...?=」、正文是 base64 乱码;
|
||||
/// · 换域名后历史邮件的 ownerEmail 仍指向旧域名,登录新账号后看不到。
|
||||
/// 本迁移用新解析器重新解析 raw/*.eml 修好字段,并把旧域名归到当前配置的账号下。
|
||||
/// </summary>
|
||||
public static class Migration
|
||||
{
|
||||
public static int Run(AppConfig config, IMailStore store)
|
||||
{
|
||||
Console.WriteLine("=== 数据迁移 ===");
|
||||
|
||||
// 旧域名 → 当前域名的账号映射
|
||||
var users = store.AllUsers();
|
||||
var oldAdmin = users.FirstOrDefault(x => x.Email.Equals(config.AdminEmail, StringComparison.OrdinalIgnoreCase));
|
||||
var currentEmail = oldAdmin?.Email ?? config.AdminEmail;
|
||||
|
||||
var messages = store.AllMessages();
|
||||
var fixedSubjects = 0;
|
||||
var reParsed = 0;
|
||||
var reOwnered = 0;
|
||||
var missingRaw = 0;
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// ---- 1) 重新解析原始报文 ----
|
||||
byte[]? raw = null;
|
||||
if (!string.IsNullOrWhiteSpace(message.RawPath))
|
||||
{
|
||||
try { raw = store.ReadRaw(message.RawPath); }
|
||||
catch { missingRaw++; }
|
||||
}
|
||||
|
||||
if (raw is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsed = Mime.Parse(raw);
|
||||
var changed = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parsed.Subject) && parsed.Subject != "(无主题)" && parsed.Subject != message.Subject)
|
||||
{
|
||||
message.Subject = parsed.Subject;
|
||||
changed = true;
|
||||
}
|
||||
if (parsed.Text.Length > 0 && parsed.Text != message.Text)
|
||||
{
|
||||
message.Text = parsed.Text;
|
||||
changed = true;
|
||||
}
|
||||
if (parsed.Html.Length > 0 && parsed.Html != message.Html)
|
||||
{
|
||||
message.Html = parsed.Html;
|
||||
changed = true;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(message.MessageId) && parsed.MessageId.Length > 0)
|
||||
{
|
||||
message.MessageId = parsed.MessageId;
|
||||
changed = true;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(message.From) && parsed.From.Length > 0)
|
||||
{
|
||||
message.From = parsed.From;
|
||||
changed = true;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(message.Cc) && parsed.Cc.Length > 0)
|
||||
{
|
||||
message.Cc = parsed.Cc;
|
||||
changed = true;
|
||||
}
|
||||
// 附件补齐(旧版本没有存附件)
|
||||
if (message.Attachments.Count == 0 && parsed.Attachments.Count > 0)
|
||||
{
|
||||
foreach (var attachment in parsed.Attachments)
|
||||
{
|
||||
if (attachment.Data.Length == 0) continue;
|
||||
message.Attachments.Add(new Attachment
|
||||
{
|
||||
FileName = attachment.FileName,
|
||||
ContentType = attachment.ContentType,
|
||||
Size = attachment.Data.Length,
|
||||
StoredAs = store.SaveAttachment(attachment.Data, attachment.FileName),
|
||||
ContentId = attachment.ContentId,
|
||||
Inline = attachment.Inline,
|
||||
});
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
reParsed++;
|
||||
if (message.Subject.Length > 0) fixedSubjects++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" [跳过] {message.Id}:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 2) 旧域名归属改到当前账号 ----
|
||||
if (!string.IsNullOrWhiteSpace(message.OwnerEmail) &&
|
||||
!message.OwnerEmail.EndsWith("@" + config.Domain, StringComparison.OrdinalIgnoreCase) &&
|
||||
store.FindUser(message.OwnerEmail) is null)
|
||||
{
|
||||
var localPart = message.OwnerEmail.Split('@')[0];
|
||||
var target = users.FirstOrDefault(x => x.Email.StartsWith(localPart + "@", StringComparison.OrdinalIgnoreCase))?.Email
|
||||
?? currentEmail;
|
||||
message.OwnerEmail = target;
|
||||
// 归属改了以后,发件人地址也一并按新域名改写,避免列表里显示成不存在的账号
|
||||
if (message.From.Equals(message.OwnerEmail, StringComparison.OrdinalIgnoreCase) == false &&
|
||||
message.From.Contains('@') && !message.From.Contains('@' + config.Domain, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 仅当 From 就是旧账号本身时才改写
|
||||
}
|
||||
reOwnered++;
|
||||
}
|
||||
}
|
||||
|
||||
store.Persist();
|
||||
|
||||
Console.WriteLine($" 重新解析并修正:{reParsed} 封");
|
||||
Console.WriteLine($" 归属域名改写 :{reOwnered} 封");
|
||||
if (missingRaw > 0) Console.WriteLine($" 原始文件缺失 :{missingRaw} 封(仅修正索引字段)");
|
||||
Console.WriteLine("迁移完成。");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user