629 lines
17 KiB
Go
629 lines
17 KiB
Go
package smtp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net"
|
|
"net/smtp"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.yun.ink/pkg/mailx/interfaces"
|
|
)
|
|
|
|
const (
|
|
MaxAttachmentSize = 25 * 1024 * 1024 // 25MB
|
|
DefaultTimeout = 30 * time.Second
|
|
MaxRetries = 3
|
|
)
|
|
|
|
// 邮件发送的封装
|
|
// 1. 支持文本
|
|
// 2. 支持文件
|
|
|
|
type Smtp struct {
|
|
interfaces.DefaultEmail
|
|
auth smtp.Auth
|
|
timeout time.Duration
|
|
}
|
|
|
|
func NewSmtp() *Smtp {
|
|
smtp := &Smtp{
|
|
timeout: DefaultTimeout,
|
|
}
|
|
smtp.Options = interfaces.DefaultOptions()
|
|
smtp.EmailType = interfaces.EmailTypeSmtp
|
|
return smtp
|
|
}
|
|
|
|
func (l *Smtp) SetOption(ctx context.Context, opt ...interfaces.Option) (interfaces.EmailInterface, error) {
|
|
for _, o := range opt {
|
|
o(&l.Options)
|
|
}
|
|
|
|
if l.Options.Smtp == nil {
|
|
return nil, fmt.Errorf("SMTP configuration is required")
|
|
}
|
|
|
|
// 验证配置
|
|
if err := l.validateConfig(); err != nil {
|
|
return nil, fmt.Errorf("invalid SMTP config: %w", err)
|
|
}
|
|
|
|
// 初始化认证
|
|
l.auth = smtp.PlainAuth("", l.Options.Smtp.Username, l.Options.Smtp.Password, l.Options.Smtp.Host)
|
|
|
|
// 安全日志输出
|
|
l.Options.Logger.Infof(ctx, "SMTP configured - Host:%s Port:%s Username:%s",
|
|
l.Options.Smtp.Host, l.Options.Smtp.Port, l.Options.Smtp.Username)
|
|
|
|
return l, nil
|
|
}
|
|
|
|
func (l *Smtp) Send(ctx context.Context, message interfaces.Message) error {
|
|
if l.Options.Smtp == nil {
|
|
return fmt.Errorf("SMTP not initialized")
|
|
}
|
|
|
|
// 验证消息
|
|
if err := l.validateMessage(message); err != nil {
|
|
return fmt.Errorf("invalid message: %w", err)
|
|
}
|
|
|
|
// 重试机制
|
|
var lastErr error
|
|
for i := 0; i < MaxRetries; i++ {
|
|
if i > 0 {
|
|
l.Options.Logger.Infof(ctx, "Retrying email send, attempt %d/%d", i+1, MaxRetries)
|
|
time.Sleep(time.Duration(i) * time.Second)
|
|
}
|
|
|
|
// 根据端口和SSL配置选择连接方式
|
|
switch l.Options.Smtp.Port {
|
|
case "465":
|
|
lastErr = l.sendWithTLS(ctx, message)
|
|
case "587":
|
|
lastErr = l.sendWithSTARTTLS(ctx, message)
|
|
default:
|
|
if l.Options.Smtp.IsSSL {
|
|
lastErr = l.sendWithTLS(ctx, message)
|
|
} else {
|
|
lastErr = l.sendPlain(ctx, message)
|
|
}
|
|
}
|
|
|
|
if lastErr == nil {
|
|
l.Options.Logger.Infof(ctx, "Email sent successfully to %v", message.To)
|
|
return nil
|
|
}
|
|
|
|
l.Options.Logger.Errorf(ctx, "Send attempt %d failed: %v", i+1, lastErr)
|
|
}
|
|
|
|
return fmt.Errorf("failed to send email after %d attempts: %w", MaxRetries, lastErr)
|
|
}
|
|
|
|
func (l *Smtp) sendPlain(ctx context.Context, message interfaces.Message) error {
|
|
// .Auth()
|
|
buffer := bytes.NewBuffer(nil)
|
|
boundary := "YunBoundaryYun"
|
|
|
|
Header := make(map[string]string)
|
|
// Header["From"] = "BOP<" + message.Form + ">"
|
|
|
|
if message.Form != "" {
|
|
Header["From"] = message.Form
|
|
} else {
|
|
Header["From"] = l.Options.Smtp.Username
|
|
}
|
|
|
|
if len(message.To) > 0 {
|
|
Header["To"] = l.formatEmailAddresses(message.To)
|
|
}
|
|
if len(message.Cc) > 0 {
|
|
Header["Cc"] = l.formatEmailAddresses(message.Cc)
|
|
}
|
|
if len(message.Bcc) > 0 {
|
|
Header["Bcc"] = l.formatEmailAddresses(message.Bcc)
|
|
}
|
|
|
|
Header["Subject"] = message.Subject
|
|
Header["Content-Type"] = "multipart/mixed; charset=UTF-8; boundary=" + boundary
|
|
Header["Date"] = time.Now().Format(time.RFC1123Z)
|
|
Header["Reply-To"] = message.ReplyTo
|
|
|
|
Header["X-Priority"] = "3"
|
|
l.writeHeader(buffer, Header)
|
|
|
|
body := "--" + boundary + "\r\n"
|
|
// body += "Content-Type: text/plain; charset=UTF-8 \r\n"
|
|
body += "Content-Type: text/html;charset=utf-8\r\n"
|
|
body += "Content-Transfer-Encoding:quoted-printable\r\n\r\n"
|
|
// body += "<html><body><h1>huang</h1><h2>xin</h2></body></html>\r\n"
|
|
|
|
// body += "<html><body>" + message.Body + "</body></html>\r\n"
|
|
|
|
body += message.Body + "\r\n"
|
|
|
|
// body += "--" + boundary + "--\r\n\r\n"
|
|
buffer.WriteString(body)
|
|
|
|
for _, value := range message.Attachment {
|
|
if err := l.writeAttachment(buffer, boundary, value, ctx); err != nil {
|
|
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", value.Name, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
buffer.WriteString("\r\n--" + boundary + "--\r\n")
|
|
b := buffer.Bytes()
|
|
|
|
// 合并所有收件人
|
|
allRecipients := append(append(message.To, message.Cc...), message.Bcc...)
|
|
err := smtp.SendMail(l.Options.Smtp.Host+":"+l.Options.Smtp.Port, l.auth, l.Options.Smtp.Username, allRecipients, b)
|
|
return err
|
|
}
|
|
|
|
func (l *Smtp) sendWithTLS(ctx context.Context, message interfaces.Message) error {
|
|
// 构建完整邮件内容
|
|
buffer := bytes.NewBuffer(nil)
|
|
boundary := "YunBoundaryYun"
|
|
|
|
// 构建邮件头
|
|
headers := l.buildHeaders(message, boundary)
|
|
l.writeHeader(buffer, headers)
|
|
|
|
// 构建邮件体
|
|
l.writeBody(buffer, boundary, message)
|
|
|
|
// 处理内嵌图片
|
|
for _, inlineImg := range message.InlineImage {
|
|
if err := l.writeAttachment(buffer, boundary, inlineImg, ctx); err != nil {
|
|
l.Options.Logger.Errorf(ctx, "Failed to process inline image %s: %v", inlineImg.Name, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// 处理普通附件
|
|
for _, attachment := range message.Attachment {
|
|
if err := l.writeAttachment(buffer, boundary, attachment, ctx); err != nil {
|
|
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", attachment.Name, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
buffer.WriteString("\r\n--" + boundary + "--\r\n")
|
|
|
|
// 建立TLS连接
|
|
if l.Options.Smtp.Port == "" {
|
|
l.Options.Smtp.Port = "465"
|
|
}
|
|
host := l.Options.Smtp.Host + ":" + l.Options.Smtp.Port
|
|
|
|
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: l.timeout}, "tcp", host, &tls.Config{
|
|
InsecureSkipVerify: false,
|
|
ServerName: l.Options.Smtp.Host,
|
|
MinVersion: tls.VersionTLS12,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("TLS connection failed: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client, err := smtp.NewClient(conn, l.Options.Smtp.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP client creation failed: %w", err)
|
|
}
|
|
defer client.Quit()
|
|
|
|
if err := client.Auth(l.auth); err != nil {
|
|
return fmt.Errorf("authentication failed: %w", err)
|
|
}
|
|
|
|
if err := client.Mail(l.Options.Smtp.Username); err != nil {
|
|
return fmt.Errorf("set sender failed: %w", err)
|
|
}
|
|
|
|
// 添加所有收件人
|
|
allRecipients := append(append(message.To, message.Cc...), message.Bcc...)
|
|
for _, addr := range allRecipients {
|
|
if err := client.Rcpt(addr); err != nil {
|
|
return fmt.Errorf("set recipient %s failed: %w", addr, err)
|
|
}
|
|
}
|
|
|
|
w, err := client.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("data command failed: %w", err)
|
|
}
|
|
|
|
if _, err := w.Write(buffer.Bytes()); err != nil {
|
|
return fmt.Errorf("write message failed: %w", err)
|
|
}
|
|
|
|
return w.Close()
|
|
}
|
|
|
|
// STARTTLS方式发送(587端口)
|
|
func (l *Smtp) sendWithSTARTTLS(ctx context.Context, message interfaces.Message) error {
|
|
// 构建完整邮件内容
|
|
buffer := bytes.NewBuffer(nil)
|
|
boundary := "YunBoundaryYun"
|
|
|
|
// 构建邮件头
|
|
headers := l.buildHeaders(message, boundary)
|
|
l.writeHeader(buffer, headers)
|
|
|
|
// 构建邮件体
|
|
l.writeBody(buffer, boundary, message)
|
|
|
|
// 处理内嵌图片
|
|
for _, inlineImg := range message.InlineImage {
|
|
if err := l.writeAttachment(buffer, boundary, inlineImg, ctx); err != nil {
|
|
l.Options.Logger.Errorf(ctx, "Failed to process inline image %s: %v", inlineImg.Name, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// 处理普通附件
|
|
for _, attachment := range message.Attachment {
|
|
if err := l.writeAttachment(buffer, boundary, attachment, ctx); err != nil {
|
|
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", attachment.Name, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
buffer.WriteString("\r\n--" + boundary + "--\r\n")
|
|
|
|
// 建立普通TCP连接
|
|
if l.Options.Smtp.Port == "" {
|
|
l.Options.Smtp.Port = "587"
|
|
}
|
|
host := l.Options.Smtp.Host + ":" + l.Options.Smtp.Port
|
|
|
|
conn, err := net.DialTimeout("tcp", host, l.timeout)
|
|
if err != nil {
|
|
return fmt.Errorf("TCP connection failed: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client, err := smtp.NewClient(conn, l.Options.Smtp.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP client creation failed: %w", err)
|
|
}
|
|
defer client.Quit()
|
|
|
|
// 启动TLS
|
|
tlsConfig := &tls.Config{
|
|
InsecureSkipVerify: false,
|
|
ServerName: l.Options.Smtp.Host,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
if err := client.StartTLS(tlsConfig); err != nil {
|
|
return fmt.Errorf("STARTTLS failed: %w", err)
|
|
}
|
|
|
|
if err := client.Auth(l.auth); err != nil {
|
|
return fmt.Errorf("authentication failed: %w", err)
|
|
}
|
|
|
|
if err := client.Mail(l.Options.Smtp.Username); err != nil {
|
|
return fmt.Errorf("set sender failed: %w", err)
|
|
}
|
|
|
|
// 添加所有收件人
|
|
allRecipients := append(append(message.To, message.Cc...), message.Bcc...)
|
|
for _, addr := range allRecipients {
|
|
if err := client.Rcpt(addr); err != nil {
|
|
return fmt.Errorf("set recipient %s failed: %w", addr, err)
|
|
}
|
|
}
|
|
|
|
w, err := client.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("data command failed: %w", err)
|
|
}
|
|
|
|
if _, err := w.Write(buffer.Bytes()); err != nil {
|
|
return fmt.Errorf("write message failed: %w", err)
|
|
}
|
|
|
|
return w.Close()
|
|
}
|
|
|
|
// 验证配置
|
|
func (l *Smtp) validateConfig() error {
|
|
if l.Options.Smtp.Host == "" {
|
|
return errors.New("host is required")
|
|
}
|
|
if l.Options.Smtp.Username == "" {
|
|
return errors.New("username is required")
|
|
}
|
|
if l.Options.Smtp.Password == "" {
|
|
return errors.New("password is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 验证消息
|
|
func (l *Smtp) validateMessage(message interfaces.Message) error {
|
|
if len(message.To) == 0 {
|
|
return errors.New("at least one recipient is required")
|
|
}
|
|
if message.Subject == "" {
|
|
return errors.New("subject is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 格式化邮件地址
|
|
func (l *Smtp) formatEmailAddresses(addresses []string) string {
|
|
if len(addresses) == 0 {
|
|
return ""
|
|
}
|
|
var parts []string
|
|
for _, addr := range addresses {
|
|
name := strings.Split(addr, "@")[0]
|
|
parts = append(parts, fmt.Sprintf("%s<%s>", name, addr))
|
|
}
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
// 构建邮件头
|
|
func (l *Smtp) buildHeaders(message interfaces.Message, boundary string) map[string]string {
|
|
headers := make(map[string]string)
|
|
|
|
if message.Form != "" {
|
|
headers["From"] = message.Form
|
|
} else {
|
|
headers["From"] = l.Options.Smtp.Username
|
|
}
|
|
|
|
if len(message.To) > 0 {
|
|
headers["To"] = l.formatEmailAddresses(message.To)
|
|
}
|
|
if len(message.Cc) > 0 {
|
|
headers["Cc"] = l.formatEmailAddresses(message.Cc)
|
|
}
|
|
if len(message.Bcc) > 0 {
|
|
headers["Bcc"] = l.formatEmailAddresses(message.Bcc)
|
|
}
|
|
|
|
headers["Subject"] = message.Subject
|
|
headers["Date"] = time.Now().Format(time.RFC1123Z)
|
|
headers["MIME-Version"] = "1.0"
|
|
|
|
// 根据内容类型和附件情况选择Content-Type
|
|
if len(message.InlineImage) > 0 {
|
|
headers["Content-Type"] = "multipart/related; charset=UTF-8; boundary=" + boundary
|
|
} else if len(message.Attachment) > 0 {
|
|
headers["Content-Type"] = "multipart/mixed; charset=UTF-8; boundary=" + boundary
|
|
} else {
|
|
headers["Content-Type"] = "multipart/alternative; charset=UTF-8; boundary=" + boundary
|
|
}
|
|
|
|
if message.ReplyTo != "" {
|
|
headers["Reply-To"] = message.ReplyTo
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
// 写入邮件头
|
|
func (l *Smtp) writeHeader(buffer *bytes.Buffer, headers map[string]string) {
|
|
for key, value := range headers {
|
|
if value != "" {
|
|
buffer.WriteString(fmt.Sprintf("%s: %s\r\n", key, value))
|
|
}
|
|
}
|
|
buffer.WriteString("\r\n")
|
|
}
|
|
|
|
// 写入邮件体
|
|
func (l *Smtp) writeBody(buffer *bytes.Buffer, boundary string, message interfaces.Message) {
|
|
// 检测内容类型
|
|
bodyType := message.BodyType
|
|
if bodyType == interfaces.ContentTypeAuto {
|
|
bodyType = l.detectContentType(message.Body)
|
|
}
|
|
|
|
// 如果有纯文本版本,创建 multipart/alternative
|
|
if message.TextBody != "" && bodyType == interfaces.ContentTypeHTML {
|
|
altBoundary := "alt-" + boundary
|
|
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
|
buffer.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=%s\r\n\r\n", altBoundary))
|
|
|
|
// 纯文本版本
|
|
buffer.WriteString(fmt.Sprintf("--%s\r\n", altBoundary))
|
|
buffer.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
|
|
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
|
buffer.WriteString(message.TextBody + "\r\n")
|
|
|
|
// HTML版本
|
|
buffer.WriteString(fmt.Sprintf("--%s\r\n", altBoundary))
|
|
buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n")
|
|
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
|
buffer.WriteString(message.Body + "\r\n")
|
|
|
|
buffer.WriteString(fmt.Sprintf("--%s--\r\n", altBoundary))
|
|
} else {
|
|
// 单一内容类型
|
|
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
|
if bodyType == interfaces.ContentTypeHTML {
|
|
buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n")
|
|
} else {
|
|
buffer.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
|
|
}
|
|
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
|
buffer.WriteString(message.Body + "\r\n")
|
|
}
|
|
}
|
|
|
|
// 检测内容类型
|
|
func (l *Smtp) detectContentType(body string) interfaces.ContentType {
|
|
if strings.Contains(body, "<html>") || strings.Contains(body, "<HTML>") ||
|
|
strings.Contains(body, "<body>") || strings.Contains(body, "<BODY>") ||
|
|
strings.Contains(body, "<div>") || strings.Contains(body, "<p>") ||
|
|
strings.Contains(body, "<br>") || strings.Contains(body, "<img") {
|
|
return interfaces.ContentTypeHTML
|
|
}
|
|
return interfaces.ContentTypeText
|
|
}
|
|
|
|
// 写入附件
|
|
func (l *Smtp) writeAttachment(buffer *bytes.Buffer, boundary string, attachment interfaces.Attachment, ctx context.Context) error {
|
|
if attachment.Type == interfaces.AttachmentTypeInline {
|
|
return l.writeInlineImage(buffer, boundary, attachment, ctx)
|
|
}
|
|
return l.writeFileAttachment(buffer, boundary, attachment, ctx)
|
|
}
|
|
|
|
// 写入普通附件
|
|
func (l *Smtp) writeFileAttachment(buffer *bytes.Buffer, boundary string, attachment interfaces.Attachment, ctx context.Context) error {
|
|
var data []byte
|
|
var err error
|
|
var fileName string
|
|
|
|
// 获取数据
|
|
if len(attachment.Data) > 0 {
|
|
data = attachment.Data
|
|
fileName = attachment.Name
|
|
} else {
|
|
data, err = l.readAttachmentFile(attachment.Content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileName = filepath.Base(attachment.Content)
|
|
}
|
|
|
|
if attachment.Name != "" {
|
|
fileName = attachment.Name
|
|
}
|
|
|
|
// 检测 MIME 类型
|
|
mimeType := attachment.ContentType
|
|
if mimeType == "" {
|
|
mimeType = mime.TypeByExtension(filepath.Ext(fileName))
|
|
if mimeType == "" {
|
|
mimeType = "application/octet-stream"
|
|
}
|
|
}
|
|
|
|
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
|
buffer.WriteString("Content-Transfer-Encoding: base64\r\n")
|
|
buffer.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\r\n", fileName))
|
|
buffer.WriteString(fmt.Sprintf("Content-Type: %s; name=%s\r\n\r\n", mimeType, fileName))
|
|
|
|
// base64编码
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
for i := 0; i < len(encoded); i += 76 {
|
|
end := i + 76
|
|
if end > len(encoded) {
|
|
end = len(encoded)
|
|
}
|
|
buffer.WriteString(encoded[i:end] + "\r\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 写入内嵌图片
|
|
func (l *Smtp) writeInlineImage(buffer *bytes.Buffer, boundary string, attachment interfaces.Attachment, ctx context.Context) error {
|
|
var data []byte
|
|
var err error
|
|
var fileName string
|
|
|
|
// 获取数据
|
|
if len(attachment.Data) > 0 {
|
|
data = attachment.Data
|
|
fileName = attachment.Name
|
|
} else {
|
|
data, err = l.readAttachmentFile(attachment.Content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileName = filepath.Base(attachment.Content)
|
|
}
|
|
|
|
if attachment.Name != "" {
|
|
fileName = attachment.Name
|
|
}
|
|
|
|
// 检测 MIME 类型
|
|
mimeType := attachment.ContentType
|
|
if mimeType == "" {
|
|
mimeType = mime.TypeByExtension(filepath.Ext(fileName))
|
|
if mimeType == "" {
|
|
mimeType = "image/jpeg" // 默认图片类型
|
|
}
|
|
}
|
|
|
|
// 生成 Content-ID
|
|
cid := attachment.CID
|
|
if cid == "" {
|
|
cid = fmt.Sprintf("img_%d_%s", time.Now().UnixNano(), strings.ReplaceAll(fileName, ".", "_"))
|
|
}
|
|
|
|
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
|
buffer.WriteString("Content-Transfer-Encoding: base64\r\n")
|
|
buffer.WriteString(fmt.Sprintf("Content-Disposition: inline; filename=%s\r\n", fileName))
|
|
buffer.WriteString(fmt.Sprintf("Content-Type: %s; name=%s\r\n", mimeType, fileName))
|
|
buffer.WriteString(fmt.Sprintf("Content-ID: <%s>\r\n\r\n", cid))
|
|
|
|
// base64编码
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
for i := 0; i < len(encoded); i += 76 {
|
|
end := i + 76
|
|
if end > len(encoded) {
|
|
end = len(encoded)
|
|
}
|
|
buffer.WriteString(encoded[i:end] + "\r\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 读取附件文件
|
|
func (l *Smtp) readAttachmentFile(fileName string) ([]byte, error) {
|
|
stat, err := os.Stat(fileName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("file stat failed: %w", err)
|
|
}
|
|
if stat.Size() > MaxAttachmentSize {
|
|
return nil, fmt.Errorf("attachment too large: %d bytes (max: %d)", stat.Size(), MaxAttachmentSize)
|
|
}
|
|
|
|
return os.ReadFile(fileName)
|
|
}
|
|
|
|
// 行包装器,用于base64编码时的换行
|
|
type lineWrapper struct {
|
|
w io.Writer
|
|
count int
|
|
}
|
|
|
|
func (lw *lineWrapper) Write(p []byte) (n int, err error) {
|
|
for i, b := range p {
|
|
if lw.count == 76 {
|
|
if _, err := lw.w.Write([]byte("\r\n")); err != nil {
|
|
return i, err
|
|
}
|
|
lw.count = 0
|
|
}
|
|
if _, err := lw.w.Write([]byte{b}); err != nil {
|
|
return i, err
|
|
}
|
|
lw.count++
|
|
}
|
|
return len(p), nil
|
|
}
|