定时邮件发送器在很多场景中都能派上用场,比如每周发送项目进度汇总、每天提醒待办任务、监控系统在检测到异常时自动通知管理员。使用Golang实现这一功能并不复杂,因为Go标准库提供了net/smtp用于SMTP协议通信,配合社区成熟的cron库就能完成精准的定时调度。本文会从零开始,带你搭建一个可运行的定时邮件发送器,包括环境准备、定时任务配置、邮件内容构造以及部署运行等环节。

一、项目初始化与依赖选择
先创建一个新的Go模块。在终端中执行go mod init mail-sender,然后引入定时任务库github.com/robfig/cron/v3。这个库支持标准的cron表达式,也可以使用秒级字段,并且允许通过cron.WithLocation设置时区,避免因为服务器默认UTC时间导致任务在错误的时间触发。安装命令如下:
go get github.com/robfig/cron/v3
cron表达式由5个字段组成,分别表示分钟、小时、日、月、星期。例如0 9 * * *表示每天上午9点执行,0 9 * * 1表示每周一上午9点执行。如果你需要更灵活的调度,比如每隔5分钟执行一次,可以写成*/5 * * * *。下面是一个最小化的调度示例,它会在控制台打印日志,后续我们会把打印替换成真正的邮件发送调用。
package main
import (
"log"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
_, err := c.AddFunc("0 9 * * *", func() {
log.Println("触发定时任务:发送提醒邮件")
})
if err != nil {
log.Fatal(err)
}
c.Start()
select {}
}
上面的代码中,select {}会永久阻塞主goroutine,让调度器在后台持续运行。在实际项目里,你可以把调度器和邮件发送逻辑分开到不同的函数或包中,保持代码结构清晰。
二、通过net/smtp发送第一封邮件
Go标准库自带的net/smtp包已经能够完成SMTP协议的大部分工作。你只需要准备发件邮箱、授权码、SMTP服务器地址和端口,然后调用smtp.PlainAuth和smtp.SendMail即可。需要注意的是,很多免费邮箱(如QQ邮箱、163邮箱)出于安全考虑,不允许直接使用登录密码进行SMTP认证,必须先在邮箱设置中开启SMTP服务并获取一个独立的授权码。
package main
import (
"fmt"
"net/smtp"
)
func sendEmail(to, subject, body string) error {
from := "sender@ipipp.com"
password := "your-smtp-password"
smtpHost := "smtp.qq.com"
smtpPort := "587"
auth := smtp.PlainAuth("", from, password, smtpHost)
msg := []byte("To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
body)
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
return smtp.SendMail(addr, auth, from, []string{to}, msg)
}
smtp.PlainAuth的第二个参数是身份标识,通常留空即可。第三个参数是发件邮箱地址,第四个参数是授权码或密码,第五个参数是SMTP服务器主机名,不需要带端口。端口在SendMail的地址参数中指定。如果使用465端口,通常需要SSL连接,而标准库smtp.SendMail默认使用STARTTLS,因此更推荐使用587端口。不同服务商的SMTP地址和端口会略有差异,需要根据实际情况调整。
上面的例子发送的是纯文本邮件。对于提醒类邮件,纯文本已经够用,但如果需要展示表格、按钮或品牌样式,就必须使用HTML格式。下一节会介绍如何构造HTML邮件,并添加附件支持。
三、构造HTML邮件与附件内容
SMTP协议本身并不区分邮件内容是纯文本还是HTML,关键在于邮件头中的Content-Type字段。如果想让邮件客户端以HTML形式渲染正文,需要把Content-Type设置为text/html; charset=UTF-8,然后把HTML字符串作为邮件正文。下面这个函数可以帮你快速生成一封HTML格式的提醒邮件。
func buildHTMLMail(to, subject, htmlBody string) []byte {
header := "From: sender@ipipp.com\r\n" +
"To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"\r\n"
return []byte(header + htmlBody)
}
func main() {
htmlBody := "<html><body><h1>待办提醒</h1><p>您有3项任务即将到期,请及时处理。</p></body></html>"
msg := buildHTMLMail("receiver@ipipp.com", "每日待办提醒", htmlBody)
_ = msg
}
需要特别提醒的是,如果你在HTML正文中使用了图片或样式,要确保引用的资源是收件人可以访问的公网地址,否则邮件客户端会阻止加载。另外,邮件客户端对CSS的支持不如浏览器完整,建议使用内联样式和简单的表格布局。
如果要发送附件,邮件整体需要改成multipart/mixed结构,把HTML正文和附件文件分别作为不同部分写入。可以使用标准库mime/multipart来简化构造过程。基本思路是:创建一个bytes.Buffer,用multipart.NewWriter写入邮件头,先添加HTML正文部分,再添加文件部分,最后关闭writer并把缓冲区内容交给smtp.SendMail。附件通常是二进制文件,读取后使用w.CreatePart并指定Content-Disposition为attachment; filename="report.csv"。这里不再展开完整代码,但原理与手动拼接一致,只是用标准库避免了手工设置边界字符串的麻烦。
四、整合定时调度与邮件发送
接下来把定时任务和邮件发送组合到一起。你可以把sendEmail函数放到独立的包中,然后在cron.AddFunc的回调里调用它。为了在邮件发送失败时能够及时排查问题,建议在回调中记录错误日志。下面的完整代码展示了如何设置上海时区、每天9点发送提醒邮件,并在失败时输出详细错误。
package main
import (
"fmt"
"log"
"net/smtp"
"time"
"github.com/robfig/cron/v3"
)
func sendEmail(to, subject, body string) error {
from := "sender@ipipp.com"
password := "your-smtp-password"
smtpHost := "smtp.qq.com"
smtpPort := "587"
auth := smtp.PlainAuth("", from, password, smtpHost)
msg := []byte("To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
body)
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
return smtp.SendMail(addr, auth, from, []string{to}, msg)
}
func main() {
loc, _ := time.LoadLocation("Asia/Shanghai")
c := cron.New(cron.WithLocation(loc))
_, err := c.AddFunc("0 9 * * *", func() {
err := sendEmail("receiver@ipipp.com", "每日提醒", "请检查今日待办任务。")
if err != nil {
log.Printf("邮件发送失败: %v", err)
return
}
log.Println("提醒邮件已发送")
})
if err != nil {
log.Fatal(err)
}
c.Start()
select {}
}
如果使用cron.WithLocation设置了时区,cron表达式中的时间就按该时区计算。对于跨时区部署的服务,这一点非常重要,否则很容易出现任务在半夜触发的情况。你还可以在回调中加入重试逻辑,比如发送失败后等待5秒再尝试一次,连续失败3次后再记录错误。
另一种常见的做法是使用time.Ticker配合time.Sleep实现简单定时,但这种方式不够灵活,修改执行时间需要重新编译程序。cron库的好处在于调度规则独立于代码,修改表达式后无需改动业务逻辑,也更符合生产环境对定时任务的管理习惯。
五、部署与安全注意事项
开发完成后,可以在项目根目录下执行go build -o mail-sender .生成单一可执行文件。将这个文件上传到Linux服务器的/usr/local/bin/目录,然后配合systemd守护进程运行。下面是一个简单的systemd服务单元示例。
[Unit] Description=Golang Mail Sender After=network.target [Service] ExecStart=/usr/local/bin/mail-sender Restart=on-failure Environment=SMTP_PASSWORD=yourpassword [Install] WantedBy=multi-user.target
把该文件保存为/etc/systemd/system/mail-sender.service,然后依次执行systemctl daemon-reload和systemctl enable --now mail-sender即可。这样即使服务器重启,程序也会自动拉起。
安全方面,强烈建议不要把SMTP密码硬编码在Go源码里。你可以从环境变量SMTP_PASSWORD中读取,或者使用配置文件配合权限控制。发信邮箱最好单独申请一个专用账号,不要使用个人日常邮箱,以免程序出现异常时泄露敏感信息。另外,如果发送量较大,需要关注邮件服务商的每日发送上限和频率限制,必要时可以加入发送队列和速率控制。
最后,定时邮件发送器虽然实现简单,但可以扩展出很多实用功能,比如从数据库读取收件人列表、根据模板渲染个性化内容、记录每次发送的状态等。掌握了cron调度和SMTP发送这两个核心模块,你就可以按自己的需求灵活定制。