ultisuite-backend/internal/mail/smtp/attachments.go
R3D347HR4Y 65fc9e517a Implement outbox management features with scheduling and attachment support
- Added new API endpoints for sending, rescheduling, and canceling scheduled outbox messages.
- Implemented outbox processing logic to handle attachments and manage message statuses.
- Introduced a dead-letter strategy for failed outbox messages, enhancing reliability.
- Updated database schema to support new outbox statuses and dead-letter entries.
- Enhanced unit tests for outbox functionalities, ensuring robust error handling and validation.
- Improved attachment handling in the outbox processor to support inline and regular attachments.
2026-05-22 17:46:30 +02:00

93 lines
2.3 KiB
Go

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
}