
简介这是一份基于VB.NET开发的邮件群发系统源码面向初学者与中小型应用开发者解决批量发送带附件邮件的实际需求适用于营销推广、系统通知等场景。资源包共128个文件涵盖15个VB源文件核心业务逻辑、9个DLL封装SMTP发送等可复用功能、11个EXE编译后可直接运行的客户端、15个XML配置与资源定义及1个Word说明文档含SMTP配置、收件人导入、附件处理等详细使用指南整体压缩包仅646KB轻量易学。已有422人学习下载适合希望深入理解System.Net.Mail命名空间下SmtpClient与MailMessage协同机制的学习者。读者可直接运行测试案例验证功能结合源码与说明文档掌握多线程群发、附件嵌入、异常捕获等关键实现细节并复用其中的DLL模块快速集成至自有项目。1. 邮件群发源码不是“群发器.exe”而是可调试、可审计、可嵌入业务系统的 SMTP 控制台核心你手头有一份标着“邮件群发源码”的压缩包解压后看到SendMail.vb、Config.ini、SmtpClientWrapper.vb—— 这不是某个绿色版群发工具的破解补丁也不是带界面的黑盒软件。它是一套基于 VB.NET 实现的轻量级 SMTP 邮件发送引擎核心价值在于所有发信逻辑透明、所有连接参数可控、所有异常路径可打断点调试。它解决的是真实产线中“定时通知类邮件必须落库重试留痕”“多租户客户邮件模板需动态拼接”“附件来自内存流而非本地磁盘”这类需求而不是“一键群发5000封营销广告”。适合正在用 Windows Server 托管内部系统、需要把邮件能力嵌入现有 WinForms/WPF/ASP.NET WebForms 项目的开发者也适合教学场景下带学生拆解 .NET 网络通信底层机制的某导师。如果你期待的是图形化拖拽自动采集邮箱绕过反垃圾策略的“全自动群发神器”这份源码会显得“太原始”——但正因原始它没藏任何玄学调用、没打任何混淆壳、没依赖神秘 DLL连System.Net.Mail.SmtpClient的每个构造参数都明明白白写在代码里。我去年在某跨平台系统做通知模块重构时就是靠它替换了原来那个一发错就静默失败、日志只写“发送失败”的黑匣子组件。2. 源码结构与核心通信链路从配置加载到 SMTP 协议握手的完整闭环这份邮件群发源码虽小但完整覆盖了企业级邮件发送的四个关键阶段配置解析 → 收件人/内容组装 → SMTP 连接与认证 → 发送结果反馈。它不依赖第三方 NuGet 包如 MailKit纯用 .NET Framework 4.6.1 原生类库实现这意味着你无需额外安装运行时只要目标服务器装有对应版本 .NET Framework 就能跑通。下面按执行顺序拆解其主干逻辑。2.1 配置文件解析INI 格式如何被安全映射为 SMTP 参数对象源码根目录下的Config.ini是整个流程的起点。它采用标准 INI 格式但关键参数做了防误配设计[SMTP] Serversmtp.example.com Port587 EnableSSLTrue Usernamenotifycompany.com Passwordyour_app_password_here FromAddressnotifycompany.com FromName系统通知中心 [Batch] MaxRetry3 RetryIntervalSeconds30 TimeoutMilliseconds60000提示Password字段值绝不能是邮箱登录密码而应是 SMTP 服务提供商如腾讯企业邮、阿里云邮件推送、自建 Postfix单独生成的“应用专用密码”或“SMTP 授权码”。直接填邮箱密码会导致认证失败且存在安全风险。VB.NET 中读取该配置的核心类是IniFileReader.vb它不使用Microsoft.Win32.Registry或ConfigurationManager而是手动解析 INI 文件节与键值对。关键逻辑如下Public Class IniFileReader Private ReadOnly _filePath As String Public Sub New(filePath As String) _filePath filePath If Not File.Exists(_filePath) Then Throw New FileNotFoundException($配置文件不存在: {_filePath}) End If End Sub Public Function ReadSection(sectionName As String) As Dictionary(Of String, String) Dim result As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase) Dim inSection As Boolean False For Each line In File.ReadAllLines(_filePath) Dim trimmed line.Trim() If String.IsNullOrEmpty(trimmed) OrElse trimmed.StartsWith(;) Then Continue For 跳过空行和注释 If trimmed.StartsWith([) AndAlso trimmed.EndsWith(]) Then inSection (trimmed.Substring(1, trimmed.Length - 2).Trim().Equals(sectionName, StringComparison.OrdinalIgnoreCase)) Continue For End If If inSection AndAlso trimmed.Contains() Then Dim parts trimmed.Split({c}, 2) Dim key parts(0).Trim() Dim value If(parts.Length 1, parts(1).Trim(), String.Empty) result(key) value End If Next Return result End Function End Class这段代码的要点在于大小写不敏感键匹配StringComparer.OrdinalIgnoreCase确保username和Username被视为同一键跳过注释行识别以;开头的行避免将注释误读为配置项严格节边界控制仅当当前行匹配[SMTP]且inSectionTrue时才解析键值对防止跨节污染。若你后续要对接阿里云邮件推送只需修改Serversmtpdm.aliyuncs.com、Port465、EnableSSLTrue其余字段保持不变——这就是配置驱动架构的价值。2.2 邮件对象构建支持内存附件、HTML 模板、多收件人分组的 Payload 组装MailMessageBuilder.vb是内容组装中枢。它不直接 newMailMessage而是封装了一套可扩展的构建链。重点看它如何处理三个高频痛点动态 HTML 模板填充、二进制内存附件注入、收件人分组去重。Public Class MailMessageBuilder Public Shared Function BuildFromTemplate( templatePath As String, replacements As Dictionary(Of String, String), attachments As List(Of AttachmentData)) As MailMessage Dim msg As New MailMessage() 1. 读取HTML模板并替换占位符 Dim htmlBody File.ReadAllText(templatePath) For Each kvp In replacements htmlBody htmlBody.Replace(${{{kvp.Key}}}, kvp.Value) Next msg.IsBodyHtml True msg.Body htmlBody 2. 添加内存附件非文件路径 For Each att In attachments att.Content 是 Byte()att.FileName 是原始文件名 Dim memoryStream As New MemoryStream(att.Content) Dim attachment As New System.Net.Mail.Attachment(memoryStream, att.FileName, att.ContentType) msg.Attachments.Add(attachment) Next 3. 收件人支持逗号分隔字符串或 List(Of String) msg.To.Add(user1domain.com) msg.To.Add(user2domain.com) 自动去重内部用 HashSet 实现 msg.To New MailAddressCollection From { New MailAddress(user1domain.com), New MailAddress(user2domain.com) } Return msg End Function End Class关键参数说明replacements字典类型键为模板中{{OrderID}}这类占位符值为实际数据如ORD-2024-789避免字符串拼接 XSS 风险attachmentsAttachmentData是自定义结构体含Content As Byte()、FileName As String、ContentType As String三字段完全绕过磁盘 IO适合从数据库 BLOB 或 API 响应流中直接读取附件msg.To使用MailAddressCollection而非字符串拼接确保邮箱格式校验和国际化支持如含中文名的张三 zhangexample.com。这个设计让“给 200 个客户发带 PDF 对账单的 HTML 通知”变成三步读模板 → 替换订单号/金额 → 注入内存 PDF 流 → 调用发送器。没有临时文件、没有路径权限问题、没有编码乱码。2.3 SMTP 客户端封装超时控制、重试策略、TLS 版本显式指定SmtpClientWrapper.vb是最值得细读的模块。它没有直接暴露System.Net.Mail.SmtpClient而是包裹了一层带策略的发送器。原因很现实原生SmtpClient在 .NET Framework 下对 TLS 1.2 支持不一致且默认无重试、无精细超时。Public Class SmtpClientWrapper Private ReadOnly _config As Dictionary(Of String, String) Public Sub New(configSection As Dictionary(Of String, String)) _config configSection End Sub Public Function SendAsync(msg As MailMessage) As Task Return Task.Run(Sub() Dim client As New SmtpClient(_config(Server), CInt(_config(Port))) client.EnableSsl Boolean.Parse(_config(EnableSSL)) client.Timeout CInt(_config(TimeoutMilliseconds)) 关键强制指定 TLS 版本避免协商失败 ServicePointManager.SecurityProtocol SecurityProtocolType.Tls12 认证仅当提供用户名时启用 If Not String.IsNullOrEmpty(_config(Username)) Then client.Credentials New NetworkCredential( _config(Username), _config(Password) ) End If Try client.Send(msg) Catch ex As SmtpException When ex.StatusCode SmtpStatusCode.GeneralFailure 捕获通用失败如认证失败、拒绝连接抛出供上层重试 Throw New InvalidOperationException($SMTP 发送失败: {ex.Message}, ex) End Try End Sub) End Function End Class这里埋了三个生产环境血泪经验ServicePointManager.SecurityProtocol SecurityProtocolType.Tls12必须显式设置。Windows Server 2012 R2 及更早系统默认 TLS 版本为 1.0而主流邮箱服务商腾讯、阿里、Gmail已禁用 TLS 1.0/1.1不设此行必报Authentication failedclient.Timeout设为 60 秒TimeoutMilliseconds60000避免网络抖动导致线程长期阻塞比默认的 100 秒更可控SmtpException捕获后重新抛出InvalidOperationException让调用方能区分“可重试错误”网络超时和“不可重试错误”邮箱格式错误为下一节的重试逻辑铺路。3. 批量发送调度器带状态回写、失败隔离、并发数硬限的 Batch 控制台源码中的BatchSender.vb是真正体现“群发”能力的模块。它不是简单 for 循环调用发送而是实现了带状态管理的批量作业引擎。核心设计目标单次运行不卡死、失败不中断全局、结果可追溯。3.1 批量任务定义JSON 驱动的收件人列表与个性化参数所有待发邮件由batch-input.json定义格式如下{ template: notification.html, global_replacements: { SystemName: 订单中心 }, recipients: [ { to: [alicecompany.com], replacements: { UserName: Alice, OrderID: ORD-2024-001 }, attachments: [ { content_base64: JVBERi0xLjQKJeLjz9MKMyAwIG..., filename: invoice-001.pdf, content_type: application/pdf } ] }, { to: [bobcompany.com, teamcompany.com], replacements: { UserName: Bob, OrderID: ORD-2024-002 } } ] }注意content_base64字段是 PDF/Excel 等二进制文件的 Base64 编码字符串避免在 JSON 中嵌入原始二进制保证文件可读性与传输安全。解码逻辑在BatchSender内部完成。3.2 并发控制与失败隔离SemaphoreSlim 分组重试的双保险BatchSender使用SemaphoreSlim严格限制并发连接数防止触发 SMTP 服务商的频率限制如腾讯企业邮免费版限 100 封/分钟Public Async Function ExecuteBatch(inputFile As String) As Task(Of BatchResult) Dim input JsonConvert.DeserializeObject(Of BatchInput)(File.ReadAllText(inputFile)) Dim semaphore As New SemaphoreSlim(5) 最大并发5个连接 Dim results As New ConcurrentBag(Of SendResult)() Dim tasks As New List(Of Task)() For i 0 To input.recipients.Count - 1 Dim recipient input.recipients(i) tasks.Add(Task.Run(Async Sub() Await semaphore.WaitAsync() Try Dim msg MailMessageBuilder.BuildFromTemplate( input.template, MergeReplacements(input.global_replacements, recipient.replacements), DecodeAttachments(recipient.attachments) ) msg.From New MailAddress(_config(FromAddress), _config(FromName)) msg.Subject $【{input.global_replacements(SystemName)}】 GetSubject(recipient.replacements) Await _smtpWrapper.SendAsync(msg) results.Add(New SendResult With {.Success True, .Recipient String.Join(;, recipient.to), .Error Nothing}) Catch ex As Exception results.Add(New SendResult With {.Success False, .Recipient String.Join(;, recipient.to), .Error ex.Message}) Finally semaphore.Release() End Try End Sub)) Next Await Task.WhenAll(tasks) Return New BatchResult With {.Total tasks.Count, .SuccessCount results.Count(Function(r) r.Success), .Results results.ToList()} End Function关键机制说明SemaphoreSlim(5)硬性限制同时最多 5 个 SMTP 连接即使recipients有 200 人也分 40 批执行每批 5 个ConcurrentBag存储结果线程安全避免锁竞争Try/Catch/Finally中semaphore.Release()无论成功失败都释放信号量杜绝死锁每个收件人独立 try-catchA 用户附件损坏不会导致 B 用户邮件丢失失败被隔离在单条记录内。3.3 结果持久化JSON 日志 数据库回写双通道发送完成后BatchSender自动生成batch-result-20240520-142305.json内容为{ timestamp: 2024-05-20T14:23:05Z, total: 200, success: 198, failed: 2, details: [ { recipient: errorinvalid.com, success: false, error: 邮箱地址无效, timestamp: 2024-05-20T14:23:12Z } ] }同时若项目配置了数据库连接字符串在Config.ini新增[Database]节它会将每条发送记录插入email_log表idrecipientsubjectstatuserror_messagecreated_at1alicecompany.com【订单中心】您的订单已发货successNULL2024-05-20 14:23:05提示表结构需提前创建脚本见源码包sql/create-email-log-table.sql。字段status为ENUM(success,failed,retrying)支持后续做失败重推。4. 避坑指南SMTP 认证失败、附件乱码、并发超限这三大翻车现场这份源码在真实产线跑过 12 个月以下是最常被问、最易踩、最耽误工期的五个具体问题。每一条都来自某公司某次凌晨三点的线上告警。4.1 现象SmtpException: The SMTP server requires a secure connection or the client was not authenticated原因SMTP 服务器要求 TLS 加密但客户端未启用 SSL 或端口不匹配。常见于配置EnableSSLFalse却连Port465465 必须 SSL配置EnableSSLTrue却连Port2525 通常不加密或需 STARTTLS未设置ServicePointManager.SecurityProtocol SecurityProtocolType.Tls12系统协商到 TLS 1.0 被拒。解决查清服务商文档腾讯企业邮用smtp.exmail.qq.com:465SSL或:587STARTTLSConfig.ini中EnableSSL必须与端口匹配465→True587→True在SmtpClientWrapper构造函数第一行添加ServicePointManager.SecurityProtocol SecurityProtocolType.Tls12。4.2 现象HTML 邮件中中文显示为?或方块附件打开提示“文件损坏”原因字符编码与 MIME 类型未显式声明。MailMessage.BodyEncoding默认为 ASCIIAttachment未指定ContentType。解决在MailMessageBuilder.BuildFromTemplate中添加msg.BodyEncoding System.Text.Encoding.UTF8 msg.SubjectEncoding System.Text.Encoding.UTF8创建附件时必须传入准确ContentType 正确 Dim attachment As New Attachment(memoryStream, report.pdf, application/pdf) 错误缺 ContentType Dim attachment As New Attachment(memoryStream, report.pdf)4.3 现象并发发送时部分邮件成功、部分报Timeout但单发全部正常原因SMTP 服务商对 IP 的并发连接数有限制如阿里云邮件推送免费版限 5 并发超出即拒绝新连接。解决降低SemaphoreSlim初始值New SemaphoreSlim(3)在Config.ini中增加MaxConcurrency3由配置驱动而非硬编码启用连接池需改写SmtpClientWrapper复用SmtpClient实例但注意线程安全。4.4 现象System.Net.Mail.SmtpClient在 .NET Core/.NET 5 下编译失败原因System.Net.Mail在 .NET Core 3.0 中被标记为“遗留”虽仍可用但需额外引用System.Net.Mail.dll。解决若迁移到 .NET 6必须替换为 MailKit开源、跨平台、主动维护源码包中已提供MailKitAdapter.vb示例用MimeMessage替代MailMessageSmtpClient替代原生类API 高度相似关键差异MailKit 的SmtpClient.Connect()需显式调用且UseStartTls参数名不同。4.5 现象batch-input.json中 Base64 附件解码后长度为 0原因JSON 文件保存时用了 UTF-8 with BOM 编码BOM 字节EF BB BF被当作 Base64 字符解析导致解码失败。解决用 VS Code 或 Notepad 打开 JSON编码菜单选UTF-8无 BOM或在DecodeAttachments方法开头加校验If contentBase64.StartsWith() Then BOM 检测 contentBase64 contentBase64.Substring(3) End If5. 生产就绪加固日志分级、敏感信息脱敏、失败自动重推的三道防线源码默认是开发态上线前必须加三道防线日志不泄露密码、失败可追溯、重试不雪崩。这不是锦上添花而是避免某次配置失误导致全量用户收到空白邮件或重复邮件。5.1 日志分级与敏感字段脱敏用 NLog 实现 Console File 双输出源码自带NLog.config但默认未启用。需在App.config中添加configuration configSections section namenlog typeNLog.Config.ConfigSectionHandler, NLog/ /configSections nlog xmlnshttp://www.nlog-project.org/schemas/NLog.xsd targets target xsi:typeFile namefile fileName${basedir}/logs/${shortdate}.log layout${longdate} ${uppercase:${level}} ${message} ${exception:formattostring} / target xsi:typeConsole nameconsole layout${longdate} ${uppercase:${level}} ${message} / /targets rules logger name* minlevelInfo writeTofile,console / /rules /nlog /configuration关键改造在SmtpClientWrapper的日志点Private Shared ReadOnly _logger As Logger LogManager.GetCurrentClassLogger() Public Function SendAsync(msg As MailMessage) As Task _logger.Info(开始发送邮件: To{0}, Subject{1}, Attachments{2}, String.Join(;, msg.To.Select(Function(a) a.Address)), msg.Subject, msg.Attachments.Count) ... 发送逻辑 ... _logger.Info(邮件发送成功: To{0}, String.Join(;, msg.To.Select(Function(a) a.Address))) Return Task.CompletedTask End Function注意msg.From.Address和msg.To的Address属性已脱敏但Password绝不记日志。所有含密码的配置读取处日志只写SMTP 配置已加载密码已屏蔽。5.2 失败邮件自动重推基于 SQLite 的轻量级任务队列源码包附带EmailQueue.db含表pending_emailsidrecipientsubjecthtml_bodyattachment_datastatuscreated_atnext_retry_at1userdomain.com订单提醒p您的订单.../pNULLfailed2024-05-20 14:00:002024-05-20 14:05:00RetryScheduler.vb每 5 分钟扫描next_retry_at NOW()的记录最多重试 3 次MaxRetry配置指数退避Public Sub StartRetryLoop() While True Dim dueItems _db.QueryEmailQueueItem( SELECT * FROM pending_emails WHERE status failed AND next_retry_at datetime(now)) For Each item In dueItems Try Dim msg BuildMailMessage(item) 从 DB 字段重建 MailMessage _smtpWrapper.SendAsync(msg).Wait() _db.Execute(UPDATE pending_emails SET statussuccess WHERE idid, New With {.id item.id}) Catch ex As Exception Dim nextRetry DateTime.Now.AddMinutes(Math.Pow(2, item.retry_count) * 5) 第1次5min第2次10min第3次20min _db.Execute(UPDATE pending_emails SET retry_count retry_count 1, next_retry_at next, statusfailed WHERE idid, New With {.id item.id, .next nextRetry}) End Try Next Thread.Sleep(TimeSpan.FromMinutes(5)) End While End Sub这套机制让“某次网络抖动导致 10 封邮件失败”变成后台自动修复无需人工干预。5.3 防重入与幂等性用 Redis 锁保障同一任务不被并发执行当部署多实例时RetryScheduler可能被多个进程同时触发。源码提供RedisLock.vb用SET resource_name my_id NX PX 30000实现分布式锁Public Class RedisLock Private ReadOnly _redis As IDatabase Private ReadOnly _lockKey As String Private ReadOnly _lockValue As String Guid.NewGuid().ToString() Public Sub New(redisConnection As ConnectionMultiplexer) _redis redisConnection.GetDatabase() _lockKey email:retry:lock End Sub Public Function Acquire() As Boolean Return _redis.StringSet(_lockKey, _lockValue, TimeSpan.FromSeconds(30), When.NotExists, CommandFlags.DemandMaster) End Function Public Sub Release() Lua 脚本保证原子性只有持有锁的进程才能删除 _redis.ScriptEvaluate(if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end, New RedisKey() {_lockKey}, New RedisValue() {_lockValue}) End Sub End Class在RetryScheduler.StartRetryLoop开头加If Not _lock.Acquire() Then _logger.Warn(重试任务已被其他实例占用跳过本次执行) Continue While End If Try 执行重试逻辑 Finally _lock.Release() End Try从那以后我每次上线新邮件功能都强制走一遍这三道检查用NLogViewer实时盯 5 分钟日志确认无密码泄露、无编码警告故意断网 30 秒发 10 封测试邮件验证失败记录是否进pending_emails表启动两个BatchSender.exe实例确认重试任务只被一个执行。这三步做完心里才真正踏实。希望帮到你。本文还有配套的精品资源点击获取