Files
warmbly/internal/pkg/emsg/emsg.go
T

265 lines
6.6 KiB
Go

package emsg
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
)
const Magic = "EMSG"
const Version = 1
// Bitmask flags for sections
const (
FlagPlainText uint32 = 1 << 0
FlagHTMLBody uint32 = 1 << 1
FlagAttachments uint32 = 1 << 2
FlagFromName uint32 = 1 << 3
FlagFromEmail uint32 = 1 << 4
FlagReplyTo uint32 = 1 << 5
FlagReferences uint32 = 1 << 6
FlagDispatchNonce uint32 = 1 << 7
)
// Attachment is a single attachment reference carried inside the S3 body blob.
// Only the metadata travels here; the worker fetches the bytes from object
// storage by S3Key. Keeping these refs in the blob (not the Avro Kafka event)
// preserves the published worker event contract.
type Attachment struct {
S3Key string
Filename string
MimeType string
}
// EmailBlob represents a binary-encoded email body and metadata.
type EmailBlob struct {
PlainText []byte
HTMLBody []byte
Attachments []Attachment
// FromName is the sender display name at publish time. It rides in the
// blob so a renamed mailbox sends under its new name without the worker's
// cached ADD_EMAIL identity having to be refreshed. Empty means "use the
// worker's cached name".
FromName string
// FromEmail is the verified provider alias the mailbox was told to send
// as, resolved at publish time. Empty, which is almost every send, means
// the mailbox's own address.
FromEmail string
// ReplyTo is the address the send's Reply-To header names. Empty means
// no header, so replies go to the From address.
ReplyTo string
References string
DispatchNonce string
}
// EncodeBinary serializes the blob into binary format. Layout:
//
// "EMSG" | version(1) | flags(4) | [plain] | [html] | [attachments] | [from] | [from-email] | [reply-to]
//
// where each body section is uint32-length-prefixed and only present when its
// flag bit is set. The attachments section, when present, is a uint32 count
// followed by that many (s3key, filename, mimetype) triples of length-prefixed
// strings. Attachment metadata travels here (inside the S3 body blob), not in
// the Avro Kafka event, so the published worker event contract is unchanged.
// The identity sections sit last, in flag order, so a worker that predates
// any of them decodes the rest of the blob untouched and simply never reads the
// trailing bytes.
func (b *EmailBlob) EncodeBinary() ([]byte, error) {
var flags uint32
parts := make([][]byte, 0, 2)
if len(b.PlainText) > 0 {
flags |= FlagPlainText
parts = append(parts, b.PlainText)
}
if len(b.HTMLBody) > 0 {
flags |= FlagHTMLBody
parts = append(parts, b.HTMLBody)
}
if len(b.Attachments) > 0 {
flags |= FlagAttachments
}
if b.FromName != "" {
flags |= FlagFromName
}
if b.FromEmail != "" {
flags |= FlagFromEmail
}
if b.ReplyTo != "" {
flags |= FlagReplyTo
}
if b.References != "" {
flags |= FlagReferences
}
if b.DispatchNonce != "" {
flags |= FlagDispatchNonce
}
buf := new(bytes.Buffer)
// Write header
buf.WriteString(Magic) // 4 bytes
buf.WriteByte(Version) // 1 byte
binary.Write(buf, binary.BigEndian, flags) // 4 bytes
// Write each variable-length body section [len][data] in flag order.
for _, p := range parts {
binary.Write(buf, binary.BigEndian, uint32(len(p)))
buf.Write(p)
}
// Attachments section: [count] then [len][str] x3 per attachment.
if flags&FlagAttachments != 0 {
binary.Write(buf, binary.BigEndian, uint32(len(b.Attachments)))
writeStr := func(s string) {
binary.Write(buf, binary.BigEndian, uint32(len(s)))
buf.WriteString(s)
}
for _, a := range b.Attachments {
writeStr(a.S3Key)
writeStr(a.Filename)
writeStr(a.MimeType)
}
}
if flags&FlagFromName != 0 {
binary.Write(buf, binary.BigEndian, uint32(len(b.FromName)))
buf.WriteString(b.FromName)
}
if flags&FlagFromEmail != 0 {
binary.Write(buf, binary.BigEndian, uint32(len(b.FromEmail)))
buf.WriteString(b.FromEmail)
}
if flags&FlagReplyTo != 0 {
binary.Write(buf, binary.BigEndian, uint32(len(b.ReplyTo)))
buf.WriteString(b.ReplyTo)
}
if flags&FlagReferences != 0 {
binary.Write(buf, binary.BigEndian, uint32(len(b.References)))
buf.WriteString(b.References)
}
if flags&FlagDispatchNonce != 0 {
binary.Write(buf, binary.BigEndian, uint32(len(b.DispatchNonce)))
buf.WriteString(b.DispatchNonce)
}
return buf.Bytes(), nil
}
// DecodeBinary parses a binary blob into an EmailBlob struct.
func DecodeBinary(r io.Reader) (*EmailBlob, error) {
header := make([]byte, 9)
if _, err := io.ReadFull(r, header); err != nil {
return nil, err
}
if string(header[:4]) != Magic {
return nil, errors.New("invalid magic header")
}
version := header[4]
if version != Version {
return nil, fmt.Errorf("unsupported version: %d", version)
}
flags := binary.BigEndian.Uint32(header[5:9])
readSection := func() ([]byte, error) {
var length uint32
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return nil, err
}
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
return data, nil
}
b := &EmailBlob{}
if flags&FlagPlainText != 0 {
b.PlainText, _ = readSection()
}
if flags&FlagHTMLBody != 0 {
b.HTMLBody, _ = readSection()
}
if flags&FlagAttachments != 0 {
var count uint32
if err := binary.Read(r, binary.BigEndian, &count); err != nil {
return nil, err
}
readStr := func() (string, error) {
data, err := readSection()
if err != nil {
return "", err
}
return string(data), nil
}
b.Attachments = make([]Attachment, 0, count)
for i := uint32(0); i < count; i++ {
s3Key, err := readStr()
if err != nil {
return nil, err
}
filename, err := readStr()
if err != nil {
return nil, err
}
mimeType, err := readStr()
if err != nil {
return nil, err
}
b.Attachments = append(b.Attachments, Attachment{
S3Key: s3Key,
Filename: filename,
MimeType: mimeType,
})
}
}
if flags&FlagFromName != 0 {
name, err := readSection()
if err != nil {
return nil, err
}
b.FromName = string(name)
}
if flags&FlagFromEmail != 0 {
addr, err := readSection()
if err != nil {
return nil, err
}
b.FromEmail = string(addr)
}
if flags&FlagReplyTo != 0 {
addr, err := readSection()
if err != nil {
return nil, err
}
b.ReplyTo = string(addr)
}
if flags&FlagReferences != 0 {
refs, err := readSection()
if err != nil {
return nil, err
}
b.References = string(refs)
}
if flags&FlagDispatchNonce != 0 {
nonce, err := readSection()
if err != nil {
return nil, err
}
b.DispatchNonce = string(nonce)
}
return b, nil
}