mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-26 16:02:23 +00:00
265 lines
6.6 KiB
Go
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
|
|
}
|