fix: incoming email attachments being ignored for uploads

- make disposition media column null in code. use null type.
This commit is contained in:
Abhinav Raut
2025-01-30 01:45:54 +05:30
parent 60bb6e48bf
commit 52178238a7
8 changed files with 86 additions and 53 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/abhinavxd/libredesk/internal/stringutil"
"github.com/google/uuid"
"github.com/valyala/fasthttp"
"github.com/volatiletech/null/v9"
"github.com/zerodha/fastglue"
)
@@ -50,10 +51,10 @@ func handleMediaUpload(r *fastglue.Request) error {
defer file.Close()
// Inline?
var disposition = attachment.DispositionAttachment
var disposition = null.StringFrom(attachment.DispositionAttachment)
inline, ok := form.Value["inline"]
if ok && len(inline) > 0 && inline[0] == "true" {
disposition = attachment.DispositionInline
disposition = null.StringFrom(attachment.DispositionInline)
}
// Linked model?
@@ -94,11 +95,11 @@ func handleMediaUpload(r *fastglue.Request) error {
}
}()
// Generate and upload thumbnail and save it's dimensions if it's an image.
// Generate and upload thumbnail and store image dimensions in the media meta.
var meta = []byte("{}")
if slices.Contains(image.Exts, srcExt) {
file.Seek(0, 0)
thumbFile, err := image.CreateThumb(thumbnailSize, file)
thumbFile, err := image.CreateThumb(image.DefThumbSize, file)
if err != nil {
app.lo.Error("error creating thumb image", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error creating image thumbnail", nil, envelope.GeneralError)
@@ -121,7 +122,6 @@ func handleMediaUpload(r *fastglue.Request) error {
"width": width,
"height": height,
})
}
file.Seek(0, 0)
@@ -133,7 +133,7 @@ func handleMediaUpload(r *fastglue.Request) error {
}
// Insert in DB.
media, err := app.media.Insert(srcFileName, srcContentType, "" /**content_id**/, linkedModel, disposition, uuid.String(), 0, int(srcFileSize), meta)
media, err := app.media.Insert(disposition, srcFileName, srcContentType, "" /**content_id**/, linkedModel, uuid.String(), 0, int(srcFileSize), meta)
if err != nil {
cleanUp = true
app.lo.Error("error inserting metadata into database", "error", err)

View File

@@ -17,6 +17,7 @@ import (
tmpl "github.com/abhinavxd/libredesk/internal/template"
"github.com/abhinavxd/libredesk/internal/user/models"
"github.com/valyala/fasthttp"
"github.com/volatiletech/null/v9"
"github.com/zerodha/fastglue"
)
@@ -141,7 +142,7 @@ func handleUpdateCurrentUser(r *fastglue.Request) error {
// Reset ptr.
file.Seek(0, 0)
media, err := app.media.UploadAndInsert(srcFileName, srcContentType, "", mmodels.ModelUser, user.ID, file, int(srcFileSize), "", []byte("{}"))
media, err := app.media.UploadAndInsert(srcFileName, srcContentType, "" /**content_id**/, mmodels.ModelUser, user.ID, file, int(srcFileSize), null.NewString("", false) /**disposition**/, []byte("{}") /**meta**/)
if err != nil {
app.lo.Error("error uploading file", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error uploading file", nil, envelope.GeneralError)
@@ -188,7 +189,7 @@ func handleCreateUser(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Empty `first_name`", nil, envelope.InputError)
}
// Right now, only agents can be created, can be named better.
// Right now, only agents can be created.
if err := app.user.CreateAgent(&user); err != nil {
return sendErrorEnvelope(r, err)
}

View File

@@ -32,6 +32,7 @@ import (
"github.com/jmoiron/sqlx"
"github.com/knadh/go-i18n"
"github.com/lib/pq"
"github.com/volatiletech/null/v9"
"github.com/zerodha/logf"
)
@@ -100,7 +101,8 @@ type mediaStore interface {
Attach(id int, model string, modelID int) error
GetByModel(id int, model string) ([]mmodels.Media, error)
ContentIDExists(contentID string) (bool, error)
UploadAndInsert(fileName, contentType, contentID, modelType string, modelID int, content io.ReadSeeker, fileSize int, disposition string, meta []byte) (mmodels.Media, error)
Upload(fileName, contentType string, content io.ReadSeeker) (string, error)
UploadAndInsert(fileName, contentType, contentID, modelType string, modelID int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (mmodels.Media, error)
}
type inboxStore interface {

View File

@@ -7,17 +7,22 @@ import (
"encoding/json"
"errors"
"fmt"
"path/filepath"
"slices"
"strings"
"time"
"github.com/abhinavxd/libredesk/internal/attachment"
amodels "github.com/abhinavxd/libredesk/internal/automation/models"
"github.com/abhinavxd/libredesk/internal/conversation/models"
"github.com/abhinavxd/libredesk/internal/envelope"
"github.com/abhinavxd/libredesk/internal/image"
"github.com/abhinavxd/libredesk/internal/inbox"
mmodels "github.com/abhinavxd/libredesk/internal/media/models"
"github.com/abhinavxd/libredesk/internal/stringutil"
umodels "github.com/abhinavxd/libredesk/internal/user/models"
"github.com/lib/pq"
"github.com/volatiletech/null/v9"
)
const (
@@ -460,16 +465,14 @@ func (m *Manager) getMessageActivityContent(activityType, newValue, actorName st
// conversations, and creates a new conversation if necessary. It also
// inserts the message, uploads any attachments, and queues the conversation evaluation of automation rules.
func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
var err error
// Find or create contact and set sender ID.
if err = m.userStore.CreateContact(&in.Contact); err != nil {
// Find or create contact and set sender ID in message.
if err := m.userStore.CreateContact(&in.Contact); err != nil {
m.lo.Error("error upserting contact", "error", err)
return err
}
in.Message.SenderID = in.Contact.ID
// This message already exists?
// Conversations exists for this message?
conversationID, err := m.findConversationID([]string{in.Message.SourceID.String})
if err != nil && err != ErrConversationNotFound {
return err
@@ -489,9 +492,10 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
return err
}
// Upload attachments.
// Upload message attachments.
if err := m.uploadMessageAttachments(&in.Message); err != nil {
return err
// Log error but continue processing.
m.lo.Error("error uploading message attachments", "message_source_id", in.Message.SourceID, "error", err)
}
// Evaluate automation rules for this conversation.
@@ -499,7 +503,6 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
m.automation.EvaluateNewConversationRules(in.Message.ConversationUUID)
} else {
m.automation.EvaluateConversationUpdateRules(in.Message.ConversationUUID, amodels.EventConversationMessageIncoming)
// Reopen conversation if it's closed, snoozed, or resolved.
systemUser, err := m.userStore.GetSystemUser()
if err != nil {
@@ -581,29 +584,32 @@ func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, p
}
// uploadMessageAttachments uploads attachments for a message.
func (m *Manager) uploadMessageAttachments(message *models.Message) error {
func (m *Manager) uploadMessageAttachments(message *models.Message) []error {
if len(message.Attachments) == 0 {
return nil
}
var uploadErr error
var uploadErr []error
for _, attachment := range message.Attachments {
// Check if this attachment already exists by content ID.
exists, err := m.mediaStore.ContentIDExists(attachment.ContentID)
if err != nil {
m.lo.Error("error checking media existence", "error", err)
continue
// Check if this attachment already exists by the content ID.
if attachment.ContentID != "" {
exists, err := m.mediaStore.ContentIDExists(attachment.ContentID)
if err != nil {
m.lo.Error("error checking media existence by content ID", "content_id", attachment.ContentID, "error", err)
continue
}
if exists {
m.lo.Debug("attachment with content ID already exists", "content_id", attachment.ContentID)
continue
}
}
if exists {
m.lo.Debug("attachment already exists", "content_id", attachment.ContentID)
continue
}
m.lo.Debug("uploading message attachment", "name", attachment.Name, "content_id", attachment.ContentID, "size", attachment.Size)
m.lo.Debug("uploading message attachment", "name", attachment.Name)
// Sanitize filename and upload.
attachment.Name = stringutil.SanitizeFilename(attachment.Name)
reader := bytes.NewReader(attachment.Content)
_, err = m.mediaStore.UploadAndInsert(
media, err := m.mediaStore.UploadAndInsert(
attachment.Name,
attachment.ContentType,
attachment.ContentID,
@@ -611,14 +617,23 @@ func (m *Manager) uploadMessageAttachments(message *models.Message) error {
message.ID,
reader,
attachment.Size,
attachment.Disposition,
null.StringFrom(attachment.Disposition),
[]byte("{}"),
)
if err != nil {
uploadErr = err
uploadErr = append(uploadErr, err)
m.lo.Error("failed to upload attachment", "name", attachment.Name, "error", err)
}
// If the attachment is an image, generate and upload thumbnail.
attachmentExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(attachment.Name)), ".")
if slices.Contains(image.Exts, attachmentExt) {
if err := m.uploadThumbnailForMedia(media, attachment.Content); err != nil {
uploadErr = append(uploadErr, err)
m.lo.Error("error uploading thumbnail", "error", err)
}
}
}
return uploadErr
}
@@ -704,7 +719,7 @@ func (m *Manager) attachAttachmentsToMessage(message *models.Message) error {
attachment := attachment.Attachment{
Name: media.Filename,
Content: blob,
Header: attachment.MakeHeader(media.ContentType, media.UUID, media.Filename, "base64", media.Disposition),
Header: attachment.MakeHeader(media.ContentType, media.UUID, media.Filename, "base64", media.Disposition.String),
}
attachments = append(attachments, attachment)
}
@@ -726,3 +741,28 @@ func (m *Manager) getOutgoingProcessingMessageIDs() []int {
})
return out
}
// uploadThumbnailForMedia prepares and uploads a thumbnail for an image attachment.
func (m *Manager) uploadThumbnailForMedia(media mmodels.Media, content []byte) error {
// Create a reader from the content
file := bytes.NewReader(content)
// Seek to the beginning of the file
file.Seek(0, 0)
// Create the thumbnail
thumbFile, err := image.CreateThumb(image.DefThumbSize, file)
if err != nil {
return fmt.Errorf("error creating thumbnail: %w", err)
}
// Generate thumbnail name
thumbName := fmt.Sprintf("thumb_%s", media.UUID)
// Upload the thumbnail
if _, err := m.mediaStore.Upload(thumbName, media.ContentType, thumbFile); err != nil {
m.lo.Error("error uploading thumbnail", "error", err)
return fmt.Errorf("error uploading thumbnail: %w", err)
}
return nil
}

View File

@@ -10,7 +10,8 @@ import (
)
var (
Exts = []string{"gif", "png", "jpg", "jpeg"}
Exts = []string{"gif", "png", "jpg", "jpeg"}
DefThumbSize = 150
)
// GetDimensions returns the width and height of the image in the provided file.

View File

@@ -232,6 +232,7 @@ func (e *Email) processFullMessage(item imapclient.FetchItemDataBodySection, inc
Name: att.FileName,
Content: att.Content,
ContentType: att.ContentType,
ContentID: att.ContentID,
Size: len(att.Content),
Disposition: attachment.DispositionAttachment,
})

View File

@@ -15,6 +15,7 @@ import (
"github.com/abhinavxd/libredesk/internal/media/models"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/volatiletech/null/v9"
"github.com/zerodha/logf"
)
@@ -72,14 +73,14 @@ type queries struct {
}
// UploadAndInsert uploads file on storage and inserts an entry in db.
func (m *Manager) UploadAndInsert(srcFilename, contentType, contentID, modelType string, modelID int, content io.ReadSeeker, fileSize int, disposition string, meta []byte) (models.Media, error) {
func (m *Manager) UploadAndInsert(srcFilename, contentType, contentID, modelType string, modelID int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (models.Media, error) {
var uuid = uuid.New()
_, err := m.Upload(uuid.String(), contentType, content)
if err != nil {
return models.Media{}, err
}
media, err := m.Insert(srcFilename, contentType, contentID, modelType, disposition, uuid.String(), modelID, fileSize, meta)
media, err := m.Insert(disposition, srcFilename, contentType, contentID, modelType, uuid.String(), modelID, fileSize, meta)
if err != nil {
m.store.Delete(uuid.String())
return models.Media{}, err
@@ -98,7 +99,7 @@ func (m *Manager) Upload(fileName, contentType string, content io.ReadSeeker) (s
}
// Insert inserts media details into the database and returns the inserted media record.
func (m *Manager) Insert(fileName, contentType, contentID, modelType, disposition, uuid string, modelID int, fileSize int, meta []byte) (models.Media, error) {
func (m *Manager) Insert(disposition null.String, fileName, contentType, contentID, modelType, uuid string, modelID int, fileSize int, meta []byte) (models.Media, error) {
var id int
if err := m.queries.Insert.QueryRow(m.store.Name(), fileName, contentType, fileSize, meta, modelID, modelType, disposition, contentID, uuid).Scan(&id); err != nil {
m.lo.Error("error inserting media", "error", err)
@@ -184,19 +185,6 @@ func (m *Manager) Delete(name string) error {
return nil
}
// Delete deletes a media file from both the storage backend and the database.
func (m *Manager) DeleteByUUID(uuid string) error {
if err := m.store.Delete(uuid); err != nil {
m.lo.Error("error deleting media from store", "error", err)
return envelope.NewError(envelope.GeneralError, "Error deleting media from store", nil)
}
if _, err := m.queries.Delete.Exec(uuid); err != nil {
m.lo.Error("error deleting media from db", "error", err)
return envelope.NewError(envelope.GeneralError, "Error deleting media from DB", nil)
}
return nil
}
// DeleteUnlinkedMedia is a blocking function that periodically deletes media files that are not linked to any conversation message.
func (m *Manager) DeleteUnlinkedMedia(ctx context.Context) {
m.deleteUnlinkedMessageMedia()
@@ -221,7 +209,7 @@ func (m *Manager) deleteUnlinkedMessageMedia() error {
return err
}
for _, mm := range media {
if err := m.DeleteByUUID(mm.UUID); err != nil {
if err := m.Delete(mm.UUID); err != nil {
return err
}
}

View File

@@ -24,7 +24,7 @@ type Media struct {
ModelID null.Int `db:"model_id" json:"-"`
Size int `db:"size" json:"size"`
Store string `db:"store" json:"store"`
Disposition string `db:"disposition" json:"disposition"`
Disposition null.String `db:"disposition" json:"disposition"`
URL string `json:"url"`
ContentID string `json:"-"`
Content []byte `json:"-"`