go邮件发送——附件与图片显示

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>&copy; 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)
	}
}
相关推荐
向葭奔赴♡3 小时前
Spring Boot 分模块:从数据库到前端接口
数据库·spring boot·后端
lly2024063 小时前
Linux 文件与目录管理
开发语言
计算机毕业设计木哥3 小时前
计算机毕业设计选题推荐:基于SpringBoot和Vue的爱心公益网站
java·开发语言·vue.js·spring boot·后端·课程设计
一晌小贪欢3 小时前
Python爬虫第7课:多线程与异步爬虫技术
开发语言·爬虫·python·网络爬虫·python爬虫·python3
IT_陈寒3 小时前
Redis 性能翻倍的 5 个隐藏技巧,99% 的开发者都不知道第3点!
前端·人工智能·后端
JaguarJack3 小时前
PHP 桌面端框架NativePHP for Desktop v2 发布!
后端·php·laravel
ftpeak3 小时前
《Cargo 参考手册》第二十二章:发布命令
开发语言·rust
luckyPian4 小时前
学习go语言
开发语言·学习·golang
自由的疯4 小时前
Java 怎么学习Kubernetes
java·后端·架构