mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
fix: incoming email attachments being ignored for uploads
- make disposition media column null in code. use null type.
This commit is contained in:
12
cmd/media.go
12
cmd/media.go
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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,
|
||||
})
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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:"-"`
|
||||
|
Reference in New Issue
Block a user