- 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.
93 lines
2.3 KiB
Go
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
|
|
}
|