backblaze-backup/internal/services/email/email.go

183 lines
4.3 KiB
Go

package email
import (
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net/smtp"
"strings"
)
const (
mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
subjectReport = "B2 vs Local Files"
templateSuccess = "success.html"
templateConfirm = "confirm.html"
templateForgotPassword = "forgot_password.html"
templateRegister = "register.html"
delimeter = "**=myohmy689407924327"
)
type EmailService struct {
auth smtp.Auth
host string
port string
from string
tlsconfig *tls.Config
}
type EmailWithAttachments struct {
To string
Bucket string
BackupDir string
Count map[string]int
Attachments []EmailAttachment
}
type EmailAttachment struct {
File io.Reader
Title string
}
func (e EmailAttachment) ReadContent() ([]byte, error) {
bts, err := io.ReadAll(e.File)
if err != nil {
return nil, fmt.Errorf("error loading attachment: %s", err)
}
return bts, nil
}
type MailServiceConfig struct {
Auth smtp.Auth
Host string
Port string
From string // Sender email address
}
func NewMailService(config MailServiceConfig) *EmailService {
return &EmailService{
auth: config.Auth,
host: config.Host,
port: config.Port,
from: config.From,
tlsconfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: config.Host,
},
}
}
func (e *EmailService) SendOK(emailData EmailWithAttachments) error {
template := strings.Replace(htmlTemplate, "{{bucket}}", emailData.Bucket, -1)
template = strings.Replace(template, "{{local_backup_path}}", emailData.BackupDir, -1)
for k, v := range emailData.Count {
template = strings.Replace(template, "{{"+k+"}}", fmt.Sprint(v), -1)
}
msg, err := newMessage(e.from, emailData.To, subjectReport).
withAttachments(template, emailData.Attachments)
if err != nil {
return fmt.Errorf("error while upload attachments %w", err)
}
return e.send(emailData.To, msg)
}
func (e *EmailService) send(to string, msg []byte) error {
c, err := smtp.Dial(e.host + ":" + e.port)
if err != nil {
return fmt.Errorf("DIAL: %s", err)
}
if err = c.StartTLS(e.tlsconfig); err != nil {
return fmt.Errorf("c.StartTLS: %s", err)
}
// Auth
if err = c.Auth(e.auth); err != nil {
return fmt.Errorf("c.Auth: %s", err)
}
// To && From
if err = c.Mail(e.from); err != nil {
return fmt.Errorf("c.Mail: %s", err)
}
if err = c.Rcpt(to); err != nil {
return fmt.Errorf("c.Rcpt: %s", err)
}
// Data
w, err := c.Data()
if err != nil {
return fmt.Errorf("c.Data: %s", err)
}
written, err := w.Write(msg)
if err != nil {
return fmt.Errorf("w.Write: %s", err)
}
if written <= 0 {
return fmt.Errorf("%d bytes written", written)
}
if err = w.Close(); err != nil {
return fmt.Errorf("w.Close: %s", err)
}
if err = c.Quit(); err != nil {
return fmt.Errorf("w.Quit: %s", err)
}
return nil
}
type message struct {
from string
to string
subject string
}
func newMessage(from, to, subject string) message {
return message{from: from, to: to, subject: subject}
}
func (m message) withAttachments(body string, attachments []EmailAttachment) ([]byte, error) {
headers := make(map[string]string)
headers["From"] = m.from
headers["To"] = m.to
headers["Subject"] = m.subject
headers["MIME-Version"] = "1.0"
var message bytes.Buffer
for k, v := range headers {
message.WriteString(k)
message.WriteString(": ")
message.WriteString(v)
message.WriteString("\r\n")
}
message.WriteString("Content-Type: " + fmt.Sprintf("multipart/mixed; boundary=\"%s\"\r\n", delimeter))
message.WriteString("--" + delimeter + "\r\n")
message.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n\r\n")
message.WriteString(body + "\r\n\r\n")
for _, attachment := range attachments {
attachmentRawFile, err := attachment.ReadContent()
if err != nil {
return nil, err
}
message.WriteString("--" + delimeter + "\r\n")
message.WriteString("Content-Disposition: attachment; filename=\"" + attachment.Title + "\"\r\n")
message.WriteString("Content-Type: application/octet-stream\r\n")
message.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
message.WriteString(base64.StdEncoding.EncodeToString(attachmentRawFile) + "\r\n")
}
message.WriteString("--" + delimeter + "--") // End the message
return message.Bytes(), nil
}