139 lines
4.1 KiB
Go
139 lines
4.1 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"mime/quotedprintable"
|
|
"strings"
|
|
|
|
sestypes "github.com/aws/aws-sdk-go-v2/service/sesv2/types"
|
|
|
|
"wucher/internal/queue"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
func buildSESRawContent(from string, to, cc, replyTo []string, job queue.EmailJob) (*sestypes.EmailContent, error) {
|
|
mixedBoundary := randomMIMEBoundary("mixed")
|
|
relatedBoundary := randomMIMEBoundary("related")
|
|
alternativeBoundary := randomMIMEBoundary("alt")
|
|
|
|
var buf bytes.Buffer
|
|
writeMIMEHeader(&buf, "From", from)
|
|
if len(to) > 0 {
|
|
writeMIMEHeader(&buf, "To", strings.Join(to, ", "))
|
|
}
|
|
if len(cc) > 0 {
|
|
writeMIMEHeader(&buf, "Cc", strings.Join(cc, ", "))
|
|
}
|
|
if len(replyTo) > 0 {
|
|
writeMIMEHeader(&buf, "Reply-To", strings.Join(replyTo, ", "))
|
|
}
|
|
writeMIMEHeader(&buf, "Subject", sanitizeSubject(job.Subject))
|
|
writeMIMEHeader(&buf, "MIME-Version", "1.0")
|
|
writeMIMEHeader(&buf, "Content-Type", fmt.Sprintf(`multipart/mixed; boundary="%s"`, mixedBoundary))
|
|
buf.WriteString("\r\n")
|
|
|
|
writeBoundary(&buf, mixedBoundary)
|
|
writeMIMEHeader(&buf, "Content-Type", fmt.Sprintf(`multipart/related; boundary="%s"`, relatedBoundary))
|
|
buf.WriteString("\r\n")
|
|
|
|
writeBoundary(&buf, relatedBoundary)
|
|
writeMIMEHeader(&buf, "Content-Type", fmt.Sprintf(`multipart/alternative; boundary="%s"`, alternativeBoundary))
|
|
buf.WriteString("\r\n")
|
|
|
|
if textBody := strings.TrimSpace(job.TextBodyValue()); textBody != "" {
|
|
writeBoundary(&buf, alternativeBoundary)
|
|
writeTextPart(&buf, "text/plain", textBody)
|
|
}
|
|
if htmlBody := strings.TrimSpace(job.HTMLBody); htmlBody != "" {
|
|
writeBoundary(&buf, alternativeBoundary)
|
|
writeTextPart(&buf, "text/html", htmlBody)
|
|
}
|
|
writeClosingBoundary(&buf, alternativeBoundary)
|
|
|
|
for i := range job.Attachments {
|
|
writeBoundary(&buf, relatedBoundary)
|
|
writeAttachmentPart(&buf, job.Attachments[i])
|
|
}
|
|
writeClosingBoundary(&buf, relatedBoundary)
|
|
writeClosingBoundary(&buf, mixedBoundary)
|
|
|
|
return &sestypes.EmailContent{
|
|
Raw: &sestypes.RawMessage{Data: buf.Bytes()},
|
|
}, nil
|
|
}
|
|
|
|
func writeTextPart(buf *bytes.Buffer, contentType, body string) {
|
|
writeMIMEHeader(buf, "Content-Type", contentType+`; charset="UTF-8"`)
|
|
writeMIMEHeader(buf, "Content-Transfer-Encoding", "quoted-printable")
|
|
buf.WriteString("\r\n")
|
|
qp := quotedprintable.NewWriter(buf)
|
|
_, _ = qp.Write([]byte(body))
|
|
_ = qp.Close()
|
|
buf.WriteString("\r\n")
|
|
}
|
|
|
|
func writeAttachmentPart(buf *bytes.Buffer, attachment queue.EmailAttachment) {
|
|
filename := attachment.Filename
|
|
if filename == "" {
|
|
filename = "attachment"
|
|
}
|
|
writeMIMEHeader(buf, "Content-Type", fmt.Sprintf(`%s; name="%s"`, attachment.ContentType, escapeMIMEHeaderValue(filename)))
|
|
writeMIMEHeader(buf, "Content-Transfer-Encoding", "base64")
|
|
disposition := "attachment"
|
|
if attachment.Inline {
|
|
disposition = "inline"
|
|
}
|
|
writeMIMEHeader(buf, "Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, escapeMIMEHeaderValue(filename)))
|
|
if attachment.Inline && strings.TrimSpace(attachment.ContentID) != "" {
|
|
writeMIMEHeader(buf, "Content-ID", "<"+strings.TrimSpace(attachment.ContentID)+">")
|
|
}
|
|
buf.WriteString("\r\n")
|
|
writeBase64Lines(buf, attachment.Content)
|
|
}
|
|
|
|
func writeBase64Lines(buf *bytes.Buffer, payload []byte) {
|
|
encoded := base64.StdEncoding.EncodeToString(payload)
|
|
for len(encoded) > 76 {
|
|
buf.WriteString(encoded[:76])
|
|
buf.WriteString("\r\n")
|
|
encoded = encoded[76:]
|
|
}
|
|
if encoded != "" {
|
|
buf.WriteString(encoded)
|
|
buf.WriteString("\r\n")
|
|
}
|
|
}
|
|
|
|
func writeMIMEHeader(buf *bytes.Buffer, key, value string) {
|
|
buf.WriteString(key)
|
|
buf.WriteString(": ")
|
|
buf.WriteString(value)
|
|
buf.WriteString("\r\n")
|
|
}
|
|
|
|
func writeBoundary(buf *bytes.Buffer, boundary string) {
|
|
buf.WriteString("--")
|
|
buf.WriteString(boundary)
|
|
buf.WriteString("\r\n")
|
|
}
|
|
|
|
func writeClosingBoundary(buf *bytes.Buffer, boundary string) {
|
|
buf.WriteString("--")
|
|
buf.WriteString(boundary)
|
|
buf.WriteString("--\r\n")
|
|
}
|
|
|
|
func randomMIMEBoundary(prefix string) string {
|
|
id, err := uuidv7.String(uuidv7.MustNew())
|
|
if err != nil {
|
|
return prefix + "_fallback"
|
|
}
|
|
return prefix + "_" + strings.ReplaceAll(id, "-", "")
|
|
}
|
|
|
|
func escapeMIMEHeaderValue(value string) string {
|
|
return strings.ReplaceAll(strings.TrimSpace(value), `"`, `'`)
|
|
}
|