package smtp import ( "context" "encoding/json" "fmt" "io" "github.com/ultisuite/ulti-backend/internal/mail/storage" ) // SendAttachment is a MIME body part ready for embedding in an outbound message. type SendAttachment struct { Filename string ContentType string ContentID string IsInline bool Data []byte } // AttachmentLoader fetches attachment bytes referenced by outbox JSON. type AttachmentLoader interface { Load(ctx context.Context, s3Key string) ([]byte, error) } // StorageAttachmentLoader loads attachment bytes from object storage. type StorageAttachmentLoader struct { Client *storage.Client } func (l *StorageAttachmentLoader) Load(ctx context.Context, s3Key string) ([]byte, error) { if l == nil || l.Client == nil { return nil, fmt.Errorf("object storage not configured") } obj, err := l.Client.Get(ctx, s3Key) if err != nil { return nil, err } defer obj.Close() return io.ReadAll(obj) } type outboxAttachmentRef struct { ID string `json:"id"` Filename string `json:"filename"` ContentType string `json:"content_type"` Size int64 `json:"size"` S3Bucket string `json:"s3_bucket"` S3Key string `json:"s3_key"` ContentID string `json:"content_id,omitempty"` IsInline bool `json:"is_inline"` } func parseOutboxAttachmentsJSON(data []byte) ([]outboxAttachmentRef, error) { if len(data) == 0 || string(data) == "[]" { return nil, nil } var refs []outboxAttachmentRef if err := json.Unmarshal(data, &refs); err != nil { return nil, fmt.Errorf("parse outbox attachments: %w", err) } return refs, nil } func resolveOutboxAttachments(ctx context.Context, loader AttachmentLoader, data []byte) ([]SendAttachment, error) { refs, err := parseOutboxAttachmentsJSON(data) if err != nil { return nil, err } if len(refs) == 0 { return nil, nil } if loader == nil { return nil, fmt.Errorf("attachment loader not configured") } attachments := make([]SendAttachment, 0, len(refs)) for _, ref := range refs { payload, err := loader.Load(ctx, ref.S3Key) if err != nil { return nil, fmt.Errorf("load attachment %q: %w", ref.Filename, err) } attachments = append(attachments, SendAttachment{ Filename: ref.Filename, ContentType: ref.ContentType, ContentID: ref.ContentID, IsInline: ref.IsInline, Data: payload, }) } return attachments, nil }