在.NET应用程序里,定时发送邮件是常见的后台需求,比如每日对账通知、每周数据周报等。实现方式并不唯一,选择哪种取决于项目规模与可靠性要求。本文给出两种典型实现:基于System.Timers.Timer的进程内定时,以及基于Quartz.NET的调度框架方案。

一、使用System.Timers.Timer实现定时发邮件
Timer是.NET基础类库提供的轻量定时器,适合在Windows服务或长驻控制台程序中执行简单周期任务。它的Elapsed事件会在独立线程池线程上触发,因此不会阻塞主线程。需要注意的是,SmtpClient在.Net Framework中同步发送可能超时,建议用异步方法SendMailAsync。
下面示例演示每十分钟检查一次并发送测试邮件。我们在Elapsed事件里构造MailMessage,并设置SMTP服务器信息。为避免事件重入,用Interlocked做简单标记。
using System;
using System.Net;
using System.Net.Mail;
using System.Threading;
class TimerMailSender
{
private static Timer _timer;
private static int _running = 0;
static void Main()
{
// 间隔十分钟
_timer = new Timer(10 * 60 * 1000);
_timer.Elapsed += async (s, e) =>
{
if (Interlocked.Exchange(ref _running, 1) == 1) return;
try
{
using (var msg = new MailMessage())
{
msg.From = new MailAddress("notify@ipipp.com");
msg.To.Add("user@ipipp.com");
msg.Subject = "定时邮件测试";
msg.Body = "这是Timer触发的邮件";
using (var client = new SmtpClient("smtp.ipipp.com", 25))
{
client.Credentials = new NetworkCredential("notify@ipipp.com", "pwd");
client.EnableSsl = true;
await client.SendMailAsync(msg);
}
}
}
catch (Exception ex)
{
Console.WriteLine("发送失败:" + ex.Message);
}
finally
{
Interlocked.Exchange(ref _running, 0);
}
};
_timer.AutoReset = true;
_timer.Start();
Console.WriteLine("按回车退出");
Console.ReadLine();
}
}
这种方案的优点是没有外部依赖,部署简单;缺点是如果进程重启,任务就中断,且不支持Cron这类复杂周期。对于要求不高的内部工具足够用。
另外要注意Timer的精度受系统时钟分辨率影响,不适合秒级严苛调度。若程序跑在IIS中,还要小心应用池回收导致定时器被销毁,此时应改用Windows任务计划或独立服务。
二、使用Quartz.NET实现更灵活的调度
当业务需要“每天9点”“每月1号”这类表达式周期,或要求任务持久化、集群不重复执行时,Quartz.NET是更专业的选择。它以内置调度器管理Job与Trigger,支持ADO.NET或内存存储。
下面代码定义邮件Job,并用CronScheduleBuilder配置每天上午9点触发。Quartz会自动从线程池取线程执行,我们同样用异步发信避免阻塞。
using System;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
using Quartz;
public class EmailJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
using (var msg = new MailMessage())
{
msg.From = new MailAddress("notify@ipipp.com");
msg.To.Add("user@ipipp.com");
msg.Subject = "Quartz定时邮件";
msg.Body = "由Quartz调度发出";
using (var client = new SmtpClient("smtp.ipipp.com", 25))
{
client.Credentials = new NetworkCredential("notify@ipipp.com", "pwd");
client.EnableSsl = true;
await client.SendMailAsync(msg);
}
}
await Task.CompletedTask;
}
}
class QuartzDemo
{
public static async Task Run()
{
var factory = new Quartz.Impl.StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
await scheduler.Start();
var job = JobBuilder.Create<EmailJob>()
.WithIdentity("mailJob", "group1")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("mailTrigger", "group1")
.WithCronSchedule("0 0 9 * * ?") // 每天9点
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
Quartz方案的强项在于调度语义清晰、可监控、可持久化。如果配了数据库存储,即使服务重启也能恢复触发器状态。它还支持 misfire 策略,处理服务宕机期间的漏发问题。
不过引入Quartz会增加包体积与学习成本,小项目未必划算。如果仅偶尔发信,第一种Timer方式更直观。实际选型应综合运维能力与业务严苛度。
三、两种方案对比与注意点
我们从部署、周期表达、可靠性三个维度比较。Timer胜在零依赖,Quartz胜在表达力与韧性。
| 维度 | Timer方案 | Quartz方案 |
|---|---|---|
| 外部依赖 | 无 | NuGet包Quartz |
| 周期描述 | 仅间隔毫秒 | Cron/日历 |
| 宕机恢复 | 不支持 | 可配置持久化 |
无论哪种方式,邮件发送都应加超时与重试。SmtpClient默认超时100秒,高并发时可调小。另外密码等敏感信息不要硬编码,应从配置或密钥库读取。
最后提醒,.NET Core之后SmtpClient虽仍可用,但官方标记为非弃用但建议评估第三方库如MailKit。若用MailKit,上述逻辑只需替换客户端类,定时器或调度器部分完全不变。