1.邮件方法
Go
package mailSend
import (
"bytes"
"encoding/base64"
"fmt"
"html/template"
"io"
"log"
"mime/multipart"
"net/smtp"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/jordan-wright/email"
)
// MailConfig 存储邮件相关的配置信息
type MailConfig struct {
From string
Host string
Port int
Email string
Password string
}
// MailContent 存储邮件的内容信息
type MailContent struct {
To []string
Cc []string
Bcc []string
Subject string
Body string
AttachmentPath []string // 你的附件路径
}
// SendMailBySmtp 发送邮件
func SendMailBySmtp(config *MailConfig, content *MailContent) error {
// 连接到SMTP服务器
auth := smtp.PlainAuth("", config.Email, config.Password, config.Host)
var message bytes.Buffer
writer := multipart.NewWriter(&message)
// 设置邮件头部
headers := map[string]string{
"From": config.From,
"To": strings.Join(content.To, ","),
"Cc": strings.Join(content.Cc, ","),
"Bcc": strings.Join(content.Bcc, ","),
"Subject": content.Subject,
"MIME-Version": "1.0",
"Content-Type": "multipart/mixed; boundary=" + writer.Boundary(),
}
// 设置邮件头部
BuildHeaders(&message, headers)
// 读取HTML模板文件,模板文件与当前文件在同一个目录下
tpl, err := ParseTemplate("./email_template.html")
if err != nil {
log.Printf("Failed to parse template file: %+v", err)
return err
}
imagePath := "/Users/yangge/Pictures/vlcsnap-2024-04-07-20h31m31s596.png"
imageMine, base64Data, err := GetImgMineAndContent(imagePath)
if err != nil {
log.Printf("Failed to detect MIME type for imagePath %s: %+v", imagePath, err)
return err
}
// 创建一个buffer来保存渲染后的HTML内容
var bodyBuffer bytes.Buffer
data := &TemplateData{
Body: content.Body,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
ImageMine: imageMine,
ImageData: base64Data, // 获取图片base64编码
ImageUrl: "",
UserList: nil,
ServiceList: nil,
}
// 渲染HTML模板
if err = tpl.Execute(&bodyBuffer, data); err != nil {
log.Printf("Failed to execute bodyBuffer: %+v", err)
return err
}
// 添加HTML版本的邮件正文
message.WriteString("--" + writer.Boundary() + "\r\n")
message.WriteString("Content-Type:text/html;charset=utf-8\r\n")
message.WriteString("\r\n")
message.WriteString(bodyBuffer.String() + "\r\n")
// 附件部分
for _, attachmentPath := range content.AttachmentPath {
err := AppendAttachToMessage(&message, writer, attachmentPath)
if err != nil {
log.Printf("Failed to attach file %s: %v", attachmentPath, err)
continue
}
}
message.WriteString("--" + writer.Boundary() + "--\r\n")
if err := writer.Close(); err != nil {
log.Printf("Failed to close multipart writer: %v", err)
return err
}
startTime := time.Now()
port := strconv.Itoa(config.Port)
if err := smtp.SendMail(config.Host+":"+port, auth, config.Email, append(content.To, content.Cc...), message.Bytes()); err != nil {
if strings.Contains(err.Error(), "short response") {
log.Printf("⚠️ 服务器短响应但邮件可能已发送成功: %v", err)
// 选择性返回 nil,表示你要"容忍"这类错误
elapsedTime := time.Since(startTime).Seconds()
log.Printf("smtp邮件发送成功!耗时: %s 秒", fmt.Sprintf("%.2f", elapsedTime))
return nil
}
log.Printf("发送邮件失败: %+v", err)
return err
}
elapsedTime := time.Since(startTime).Seconds()
log.Printf("smtp邮件发送成功!耗时: %s 秒", fmt.Sprintf("%.2f", elapsedTime))
return nil
}
func BuildHeaders(w *bytes.Buffer, headers map[string]string) {
for key, value := range headers {
w.WriteString(key + ": " + value + "\r\n")
}
w.WriteString("\r\n")
}
func ParseTemplate(tplPath string) (*template.Template, error) {
tpl, err := template.ParseFiles(tplPath)
if err != nil {
log.Printf("解析模板失败: %v", err)
return nil, err
}
return tpl, nil
}
// AppendAttachToMessage 添加附件到邮箱信息体中
//
// 参数
//
// message (*bytes.Buffer): 消息buff对象
// writer (*multipart.Writer): 附件写入处理器
// filePath (string): 附件路径
func AppendAttachToMessage(message *bytes.Buffer, writer *multipart.Writer, filePath string) (err error) {
mimeType, err := mimetype.DetectFile(filePath)
if err != nil {
message.WriteString("Content-Type:application/octet-stream" + "\r\n")
}
openFile, err := os.Open(filePath)
if err != nil {
log.Printf("Failed to open attachment %s: %v", filePath, err)
return
}
defer openFile.Close()
// 读取附件方式二
var fileContent []byte
buffer := make([]byte, 2048)
for {
n, err := io.ReadFull(openFile, buffer)
if n > 0 {
fileContent = append(fileContent, buffer[:n]...)
}
if err != nil {
if err != io.EOF && err != io.ErrUnexpectedEOF {
log.Printf("Failed to read attachment %s: %v", filePath, err)
}
break
}
}
_, file := filepath.Split(filePath)
encodedFilename := url.QueryEscape(file)
message.WriteString("--" + writer.Boundary() + "\r\n")
message.WriteString("Content-Disposition: attachment; filename*=utf-8''" + encodedFilename + "\r\n") // 使用utf-8编码
message.WriteString("Content-Type: " + mimeType.String() + "\r\n")
message.WriteString("Content-Transfer-Encoding: base64\r\n")
message.WriteString("\r\n")
message.WriteString(base64.StdEncoding.EncodeToString(fileContent))
message.WriteString("\r\n")
return nil
}
2.模板代码
html
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.data-table th {
background: #3498db;
color: white;
text-align: left;
padding: 12px;
font-weight: 500;
}
.data-table td {
padding: 15px;
border-bottom: 1px solid #e0e0e0;
}
.data-table tr:nth-child(even) {
background: #f9f9f9;
}
.data-table tr:hover {
background: #f1f9ff;
}
.badge {
padding: 5px 10px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
}
.badge-success {
background: #e8f7f0;
color: #2ecc71;
}
.badge-warning {
background: #fef7ec;
color: #f39c12;
}
.badge-error {
background: #fdedee;
color: #e74c3c;
}
.progress-bar {
width: 100%;
height: 10px;
background: #ecf0f1;
border-radius: 5px;
margin-top: 5px;
overflow: hidden;
}
.progress-value {
height: 100%;
background: #3498db;
border-radius: 5px;
}
.footer {
margin-top: 20px;
text-align: center;
padding: 20px;
background: #2c3e50;
color: #ecf0f1;
font-size: 14px;
}
.footer a {
color: #3498db;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
{{.Body}}
<h3>{{.Timestamp}}</h3>
<div style="margin: 0 auto">
{{if and .ImageMine .ImageData}}
<div style="margin: 10px;">
<h2>这是使用base64加载的图片</h2>
<img src="data:{{.ImageMine}};base64,{{.ImageData}}" alt="描述图片的文字"/>
</div>
{{end}}
{{if .ImageUrl}}
<div style="margin: 10px;">
<h2>这是从网络加载的图片</h2>
<img src="{{.ImageUrl}}" alt="描述图片的文字"/>
</div>
{{end}}
{{if .UserList}}
<div style="margin: 10px;">
<div class="section-title">
<i>👥</i>
<h3>团队成员</h3>
</div>
<table class="data-table">
<thead>
<tr>
<th>姓名</th>
<th>职位</th>
<th>邮箱</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{{range .UserList}}
<tr>
<td>{{.Name}}</td>
<td>{{.Position}}</td>
<td><a href="mailto:{{.Email}}">{{.Email}}</a></td>
<td>
{{if eq .Status "online"}}
<span class="badge user-online">在线</span>
{{else if eq .Status "busy"}}
<span class="badge user-busy">忙碌</span>
{{else}}
<span class="badge user-offline">离线</span>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{if .ServiceList}}
<div class="section">
<div class="section-title">
<i>📊</i>
<h3>服务数据统计</h3>
</div>
<table class="data-table">
<thead>
<tr>
<th>服务名称</th>
<th>状态</th>
<th>使用率</th>
<th>最后更新</th>
</tr>
</thead>
<tbody>
{{range .ServiceList}}
<tr>
<td>{{.Name}}</td>
<td>
{{if eq .Status "active"}}
<span class="badge service-active">运行中</span>
{{else if eq .Status "warning"}}
<span class="badge service-warning">警告</span>
{{else}}
<span class="badge service-error">停止</span>
{{end}}
</td>
<td>
<div class="progress-bar">
<div class="progress-value" style="width: {{.Usage}}%"></div>
</div>
<div>{{.Usage}}%</div>
</td>
<td>{{.LastUpdate}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<div class="footer">
<p>© 2025 Jordan 邮件服务. 保留所有权利</p>
<p>
<a href="#">隐私政策</a> |
<a href="#">使用条款</a> |
<a href="#">联系我们</a>
</p>
<p>此邮件为系统自动发送,请勿直接回复</p>
</div>
</div>
</body>
</html>
3.邮件发送
Go
package main
import (
"apiProject/email/mailSend"
"log"
)
func main() {
config := &mailSend.MailConfig{
From: "你的QQ邮箱",
Host: "smtp.qq.com",
Port: 587,
Email: "你的QQ邮箱",
Password: "123456789", // 这是QQ邮箱开通SMTP服务授权码
}
content := &mailSend.MailContent{
To: []string{"你的QQ邮箱"},
Cc: []string{"你的QQ邮箱"},
Bcc: []string{},
Subject: "使用smtp发送的邮件",
Body: "这是来自go smtp发送的邮件",
// 这里是macOS的绝对路径,Windows请改成对应的绝对路径
AttachmentPath: []string{
"/Users/xxx/xx/xxxx.jpg",
"/Users/xxx/xx/dsa92c.mp4",
},
}
if err := mailSend.SendMailBySmtp(config, content); err != nil {
log.Fatalf("smtp发送邮件失败===%+v", err)
}
}