diff --git a/backend/controller/email.go b/backend/controller/email.go index 4d3fc4f..0fd295c 100644 --- a/backend/controller/email.go +++ b/backend/controller/email.go @@ -27,8 +27,16 @@ var EmailOrderByMap = map[string]string{ } // AddAttachmentsToEmailRequest is a request to add attachments to a message +// supports both old format (ids array) and new format (attachments array with isInline) type AddAttachmentsToEmailRequest struct { - Attachments []string `json:"ids"` // attachment IDs + IDs []string `json:"ids"` // deprecated: old format for backward compatibility + Attachments []AttachmentWithInline `json:"attachments"` // new format with isInline support +} + +// AttachmentWithInline represents an attachment ID with inline flag +type AttachmentWithInline struct { + ID string `json:"id"` + IsInline bool `json:"isInline"` } // RemoveAttachmentFromEmailRequest is a request to remove an attachment from a message @@ -66,13 +74,24 @@ func (m *Email) AddAttachments(g *gin.Context) { if !ok { return } + + // handle backward compatibility: if old format (ids) is used, convert to new format + if len(request.IDs) > 0 && len(request.Attachments) == 0 { + for _, idStr := range request.IDs { + request.Attachments = append(request.Attachments, AttachmentWithInline{ + ID: idStr, + IsInline: false, // default to false for backward compatibility + }) + } + } + if len(request.Attachments) == 0 { m.Response.BadRequestMessage(g, "No attachments provided") return } - attachmentIDs := []*uuid.UUID{} - for _, idParam := range request.Attachments { - id, err := uuid.Parse(idParam) + attachments := []service.AttachmentWithInline{} + for _, att := range request.Attachments { + id, err := uuid.Parse(att.ID) if err != nil { m.Logger.Debugw(errs.MsgFailedToParseUUID, "error", err, @@ -80,14 +99,17 @@ func (m *Email) AddAttachments(g *gin.Context) { m.Response.BadRequestMessage(g, "Invalid attachment ID") return } - attachmentIDs = append(attachmentIDs, &id) + attachments = append(attachments, service.AttachmentWithInline{ + ID: &id, + IsInline: att.IsInline, + }) } // add attachments to email err := m.EmailService.AddAttachments( g.Request.Context(), session, id, - attachmentIDs, + attachments, ) // handle responses if ok := m.handleErrors(g, err); !ok { diff --git a/backend/database/emailAttachment.go b/backend/database/emailAttachment.go index 69da4e9..41f10c0 100644 --- a/backend/database/emailAttachment.go +++ b/backend/database/emailAttachment.go @@ -9,6 +9,7 @@ import ( type EmailAttachment struct { EmailID *uuid.UUID `gorm:"primary_key;not null;index;type:uuid;unique_index:idx_message_attachment;"` AttachmentID *uuid.UUID `gorm:"primary_key;not null;index;type:uuid;unique_index:idx_message_attachment;"` + IsInline bool `gorm:"not null;default:false;"` } func (EmailAttachment) TableName() string { diff --git a/backend/model/email.go b/backend/model/email.go index a5c285e..906d0da 100644 --- a/backend/model/email.go +++ b/backend/model/email.go @@ -22,8 +22,8 @@ type Email struct { AddTrackingPixel nullable.Nullable[bool] `json:"addTrackingPixel"` CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"` - Attachments []*Attachment `json:"attachments"` - Company *Company `json:"company"` + Attachments []*EmailAttachment `json:"attachments"` + Company *Company `json:"company"` } // Validate checks if the mail has a valid state @@ -132,3 +132,10 @@ type EmailOverview struct { Company *Company `json:"company"` } + +// EmailAttachment represents an attachment associated with an email +// with additional metadata about how it should be displayed +type EmailAttachment struct { + *Attachment + IsInline bool `json:"isInline"` // if true, use Content-Disposition: inline and set Content-ID for cid: references +} diff --git a/backend/repository/email.go b/backend/repository/email.go index c2733de..6b51ab6 100644 --- a/backend/repository/email.go +++ b/backend/repository/email.go @@ -54,11 +54,13 @@ func (m *Email) AddAttachment( ctx context.Context, emailID *uuid.UUID, attachmentID *uuid.UUID, + isInline bool, ) error { result := m.DB.Create( &database.EmailAttachment{ EmailID: emailID, AttachmentID: attachmentID, + IsInline: isInline, }, ) if result.Error != nil { @@ -103,6 +105,19 @@ func (m *Email) GetAttachmentIDsByEmailID( return attachmentIDs, nil } +// GetEmailAttachments gets all email-attachment relationships for an email including isInline status +func (m *Email) GetEmailAttachments( + ctx context.Context, + emailID uuid.UUID, +) ([]database.EmailAttachment, error) { + var emailAttachments []database.EmailAttachment + result := m.DB.Where("email_id = ?", emailID).Find(&emailAttachments) + if result.Error != nil { + return nil, result.Error + } + return emailAttachments, nil +} + // RemoveAttachment removes an attachments from a email by attachment ID func (m *Email) RemoveAttachmentsByAttachmentID( ctx context.Context, @@ -342,10 +357,8 @@ func ToEmail(row *database.Email) *model.Email { content := nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(row.Content)) addTrackingPixel := nullable.NewNullableWithValue(row.AddTrackingPixel) - attachments := []*model.Attachment{} - for _, attachment := range row.Attachments { - attachments = append(attachments, ToAttachment(attachment)) - } + // attachments are loaded separately via loadEmailAttachmentsWithContext + // which properly loads EmailAttachment with isInline status from junction table return &model.Email{ ID: id, CreatedAt: row.CreatedAt, @@ -357,7 +370,7 @@ func ToEmail(row *database.Email) *model.Email { Content: content, AddTrackingPixel: addTrackingPixel, CompanyID: companyID, - Attachments: attachments, + Attachments: []*model.EmailAttachment{}, } } diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index 008da78..f434e2d 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -2,6 +2,7 @@ package seed import ( "crypto/rand" + "strings" "github.com/go-errors/errors" "github.com/google/uuid" @@ -350,20 +351,35 @@ func SeedSettings( return nil } -// Migration approach +// Migration migrates db func migrate(db *gorm.DB) error { - // First add column as nullable + // migration for attachments.embedded_content + // first add column as nullable if err := db.Exec(`ALTER TABLE attachments ADD COLUMN embedded_content BOOLEAN`).Error; err != nil { + // column might already exist, ignore error + errMsg := strings.ToLower(err.Error()) + if !strings.Contains(errMsg, "duplicate") && !strings.Contains(errMsg, "already exists") { + return errs.Wrap(err) + } + } + + // update existing rows + if err := db.Exec(`UPDATE attachments SET embedded_content = false WHERE embedded_content IS NULL`).Error; err != nil { return errs.Wrap(err) } - // Update existing rows - if err := db.Exec(`UPDATE attachments SET embedded_content = false`).Error; err != nil { - return errs.Wrap(err) + // migration for email_attachments.is_inline + // first add column as nullable + if err := db.Exec(`ALTER TABLE email_attachments ADD COLUMN is_inline BOOLEAN`).Error; err != nil { + // column might already exist, ignore error + errMsg := strings.ToLower(err.Error()) + if !strings.Contains(errMsg, "duplicate") && !strings.Contains(errMsg, "already exists") { + return errs.Wrap(err) + } } - // Then make it not nullable - if err := db.Exec(`ALTER TABLE attachments MODIFY COLUMN embedded_content BOOLEAN NOT NULL DEFAULT false`).Error; err != nil { + // update existing rows - default to false (regular attachments) + if err := db.Exec(`UPDATE email_attachments SET is_inline = false WHERE is_inline IS NULL`).Error; err != nil { return errs.Wrap(err) } diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 1a2dfac..de8eec7 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -2129,19 +2129,82 @@ func (c *Campaign) sendCampaignMessages( m.SetBodyString("text/html", bodyBuffer.String()) // attachments attachments := email.Attachments - for _, attachment := range attachments { + for _, emailAttachment := range attachments { + attachment := emailAttachment.Attachment p, err := c.MailService.AttachmentService.GetPath(attachment) if err != nil { return fmt.Errorf("failed to get attachment path: %s", err) } - if !attachment.EmbeddedContent.MustGet() { + + // check if attachment should be inline (for cid: references) + if emailAttachment.IsInline { + // inline attachment - embedded in email body, can be referenced via cid:filename + if !attachment.EmbeddedContent.MustGet() { + // simple inline image - no template processing needed + m.EmbedFile(p.String()) + } else { + // inline image with template processing + attachmentContent, err := os.ReadFile(p.String()) + if err != nil { + return errs.Wrap(err) + } + // setup attachment for executing as email template + attachmentAsEmail := model.Email{ + ID: email.ID, + CreatedAt: email.CreatedAt, + UpdatedAt: email.UpdatedAt, + Name: email.Name, + MailEnvelopeFrom: email.MailEnvelopeFrom, + MailHeaderFrom: email.MailHeaderFrom, + MailHeaderSubject: email.MailHeaderSubject, + Content: email.Content, + AddTrackingPixel: email.AddTrackingPixel, + CompanyID: email.CompanyID, + Attachments: email.Attachments, + Company: email.Company, + } + attachmentAsEmail.Content = nullable.NewNullableWithValue( + *vo.NewUnsafeOptionalString1MB(string(attachmentContent)), + ) + // generate custom campaign URL for attachment + recipientID := campaignRecipient.ID.MustGet() + customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID) + if err != nil { + c.Logger.Errorw("failed to get campaign url for attachment", "error", err) + return errs.Wrap(err) + } + + attachmentStr, err := c.TemplateService.CreateMailBodyWithCustomURL( + ctx, + urlIdentifier.Name.MustGet(), + urlPath, + domain, + campaignRecipient, + &attachmentAsEmail, + nil, + customCampaignURL, + campaignCompanyID, + ) + if err != nil { + return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err)) + } + // use EmbedReader for inline images - sets Content-Disposition: inline and Content-ID header + // the filename becomes the Content-ID, so use in HTML + m.EmbedReader( + filepath.Base(p.String()), + strings.NewReader(attachmentStr), + ) + } + } else if !attachment.EmbeddedContent.MustGet() { + // regular attachment - shows in attachment list m.AttachFile(p.String()) } else { + // regular attachment with template processing attachmentContent, err := os.ReadFile(p.String()) if err != nil { return errs.Wrap(err) } - // hacky setup of attachment for executing as email template + // setup attachment for executing as email template attachmentAsEmail := model.Email{ ID: email.ID, CreatedAt: email.CreatedAt, @@ -2156,7 +2219,6 @@ func (c *Campaign) sendCampaignMessages( Attachments: email.Attachments, Company: email.Company, } - // really hacky / unsafe attachmentAsEmail.Content = nullable.NewNullableWithValue( *vo.NewUnsafeOptionalString1MB(string(attachmentContent)), ) @@ -3716,14 +3778,77 @@ func (c *Campaign) sendSingleEmailSMTP( // handle attachments attachments := email.Attachments - for _, attachment := range attachments { + for _, emailAttachment := range attachments { + attachment := emailAttachment.Attachment p, err := c.MailService.AttachmentService.GetPath(attachment) if err != nil { return fmt.Errorf("failed to get attachment path: %s", err) } - if !attachment.EmbeddedContent.MustGet() { + + // check if attachment should be inline (for cid: references) + if emailAttachment.IsInline { + // inline attachment - embedded in email body, can be referenced via cid:filename + if !attachment.EmbeddedContent.MustGet() { + // simple inline image - no template processing needed + m.EmbedFile(p.String()) + } else { + // inline image with template processing + attachmentContent, err := os.ReadFile(p.String()) + if err != nil { + return errs.Wrap(err) + } + // setup attachment for executing as email template + attachmentAsEmail := model.Email{ + ID: email.ID, + CreatedAt: email.CreatedAt, + UpdatedAt: email.UpdatedAt, + Name: email.Name, + MailEnvelopeFrom: email.MailEnvelopeFrom, + MailHeaderFrom: email.MailHeaderFrom, + MailHeaderSubject: email.MailHeaderSubject, + Content: email.Content, + AddTrackingPixel: email.AddTrackingPixel, + CompanyID: email.CompanyID, + Attachments: email.Attachments, + Company: email.Company, + } + attachmentAsEmail.Content = nullable.NewNullableWithValue( + *vo.NewUnsafeOptionalString1MB(string(attachmentContent)), + ) + // generate custom campaign URL for attachment + recipientID := campaignRecipient.ID.MustGet() + customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID) + if err != nil { + c.Logger.Errorw("failed to get campaign url for attachment", "error", err) + return errs.Wrap(err) + } + + attachmentStr, err := c.TemplateService.CreateMailBodyWithCustomURL( + ctx, + urlIdentifier.Name.MustGet(), + urlPath, + domain, + campaignRecipient, + &attachmentAsEmail, + nil, + customCampaignURL, + campaignCompanyID, + ) + if err != nil { + return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err)) + } + // use EmbedReader for inline images - sets Content-Disposition: inline and Content-ID header + // the filename becomes the Content-ID, so use in HTML + m.EmbedReader( + filepath.Base(p.String()), + strings.NewReader(attachmentStr), + ) + } + } else if !attachment.EmbeddedContent.MustGet() { + // regular attachment - shows in attachment list m.AttachFile(p.String()) } else { + // regular attachment with template processing attachmentContent, err := os.ReadFile(p.String()) if err != nil { return errs.Wrap(err) diff --git a/backend/service/email.go b/backend/service/email.go index aebd944..ef1e055 100644 --- a/backend/service/email.go +++ b/backend/service/email.go @@ -37,12 +37,18 @@ type Email struct { AttachmentPath string } +// AttachmentWithInline represents an attachment with inline flag +type AttachmentWithInline struct { + ID *uuid.UUID + IsInline bool +} + // AddAttachments adds an attachments to a message func (m *Email) AddAttachments( ctx context.Context, session *model.Session, messageID *uuid.UUID, - attachmentIDs []*uuid.UUID, + attachments []AttachmentWithInline, ) error { ae := NewAuditEvent("Email.AddAttachments", session) ae.Details["messageId"] = messageID.String() @@ -69,13 +75,13 @@ func (m *Email) AddAttachments( } // add attachment to message attachmentIdsStr := []string{} - for _, attachmentID := range attachmentIDs { - attachmentIdsStr = append(attachmentIdsStr, attachmentID.String()) + for _, attachment := range attachments { + attachmentIdsStr = append(attachmentIdsStr, attachment.ID.String()) // get the message to ensure it exists and the user is privliged _, err = m.AttachmentService.GetByID( ctx, session, - attachmentID, + attachment.ID, ) if err != nil { m.Logger.Errorw("failed to add attachment to email", "error", err) @@ -84,7 +90,8 @@ func (m *Email) AddAttachments( err = m.EmailRepository.AddAttachment( ctx, messageID, - attachmentID, + attachment.ID, + attachment.IsInline, ) if err != nil { m.Logger.Errorw("failed to add attachment to email", "error", err) @@ -640,19 +647,73 @@ func (m *Email) SendTestEmail( msg.SetBodyString("text/html", bodyBuffer.String()) // attachments attachments := email.Attachments - for _, attachment := range attachments { + for _, emailAttachment := range attachments { + attachment := emailAttachment.Attachment p, err := m.AttachmentService.GetPath(attachment) if err != nil { return fmt.Errorf("failed to get attachment path: %s", err) } - if !attachment.EmbeddedContent.MustGet() { + + // check if attachment should be inline (for cid: references) + if emailAttachment.IsInline { + // inline attachment - embedded in email body, can be referenced via cid:filename + if !attachment.EmbeddedContent.MustGet() { + // simple inline image - no template processing needed + msg.EmbedFile(p.String()) + } else { + // inline image with template processing + attachmentContent, err := os.ReadFile(p.String()) + if err != nil { + return errs.Wrap(err) + } + // setup attachment for executing as email template + attachmentAsEmail := model.Email{ + ID: email.ID, + CreatedAt: email.CreatedAt, + UpdatedAt: email.UpdatedAt, + Name: email.Name, + MailEnvelopeFrom: email.MailEnvelopeFrom, + MailHeaderFrom: email.MailHeaderFrom, + MailHeaderSubject: email.MailHeaderSubject, + Content: email.Content, + AddTrackingPixel: email.AddTrackingPixel, + CompanyID: email.CompanyID, + Attachments: email.Attachments, + Company: email.Company, + } + attachmentAsEmail.Content = nullable.NewNullableWithValue( + *vo.NewUnsafeOptionalString1MB(string(attachmentContent)), + ) + attachmentStr, err := m.TemplateService.CreateMailBody( + ctx, + "id", + "/", + testDomain, + campaignRecipient, + &attachmentAsEmail, + nil, + companyID, + ) + if err != nil { + return fmt.Errorf("failed to setup attachment with embedded content: %s", err) + } + // use EmbedReader for inline images - sets Content-Disposition: inline and Content-ID header + // the filename becomes the Content-ID, so use in HTML + msg.EmbedReader( + filepath.Base(p.String()), + strings.NewReader(attachmentStr), + ) + } + } else if !attachment.EmbeddedContent.MustGet() { + // regular attachment - shows in attachment list msg.AttachFile(p.String()) } else { + // inline attachment - embedded in email body, can be referenced via cid:filename attachmentContent, err := os.ReadFile(p.String()) if err != nil { return errs.Wrap(err) } - // hacky setup of attachment for executing as email template + // setup attachment for executing as email template attachmentAsEmail := model.Email{ ID: email.ID, CreatedAt: email.CreatedAt, @@ -667,7 +728,6 @@ func (m *Email) SendTestEmail( Attachments: email.Attachments, Company: email.Company, } - // really hacky / unsafe attachmentAsEmail.Content = nullable.NewNullableWithValue( *vo.NewUnsafeOptionalString1MB(string(attachmentContent)), ) @@ -684,7 +744,9 @@ func (m *Email) SendTestEmail( if err != nil { return fmt.Errorf("failed to setup attachment with embedded content: %s", err) } - msg.AttachReadSeeker( + // use EmbedReader for inline images - sets Content-Disposition: inline and Content-ID header + // the filename becomes the Content-ID, so use in HTML + msg.EmbedReader( filepath.Base(p.String()), strings.NewReader(attachmentStr), ) @@ -907,25 +969,25 @@ func (m *Email) loadEmailAttachmentsWithContext( email *model.Email, companyID *uuid.UUID, ) error { - // get all attachment IDs associated with this email - attachmentIDs, err := m.EmailRepository.GetAttachmentIDsByEmailID(ctx, email.ID.MustGet()) + // get all email-attachment relationships with isInline status + emailAttachments, err := m.EmailRepository.GetEmailAttachments(ctx, email.ID.MustGet()) if err != nil { return errs.Wrap(err) } // if no attachments, nothing to do - if len(attachmentIDs) == 0 { - email.Attachments = []*model.Attachment{} + if len(emailAttachments) == 0 { + email.Attachments = []*model.EmailAttachment{} return nil } // get attachments with proper context filtering - contextFilteredAttachments := []*model.Attachment{} - for _, attachmentID := range attachmentIDs { - attachment, err := m.AttachmentService.AttachmentRepository.GetByID(ctx, attachmentID) + contextFilteredAttachments := []*model.EmailAttachment{} + for _, ea := range emailAttachments { + attachment, err := m.AttachmentService.AttachmentRepository.GetByID(ctx, ea.AttachmentID) if err != nil { // if attachment doesn't exist, log and continue - m.Logger.Debugw("attachment not found", "attachmentID", attachmentID, "error", err) + m.Logger.Debugw("attachment not found", "attachmentID", ea.AttachmentID, "error", err) continue } @@ -947,7 +1009,10 @@ func (m *Email) loadEmailAttachmentsWithContext( } if isAttachmentAccessible { - contextFilteredAttachments = append(contextFilteredAttachments, attachment) + contextFilteredAttachments = append(contextFilteredAttachments, &model.EmailAttachment{ + Attachment: attachment, + IsInline: ea.IsInline, + }) } } diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 0ec90ac..d7ccbeb 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -1528,12 +1528,12 @@ export class API { * Add attachments to a email. * * @param {string} emailID - * @param {string[]} attachmentIDs + * @param {Array<{id: string, isInline: boolean}>} attachments - Array of attachment objects with id and isInline flag * @returns {Promise} */ - addAttachments: async (emailID, attachmentIDs) => { + addAttachments: async (emailID, attachments) => { return await postJSON(this.getPath(`/email/${emailID}/attachment`), { - ids: attachmentIDs + attachments: attachments }); }, diff --git a/frontend/src/lib/components/editor/Editor.svelte b/frontend/src/lib/components/editor/Editor.svelte index b14b511..2119746 100644 --- a/frontend/src/lib/components/editor/Editor.svelte +++ b/frontend/src/lib/components/editor/Editor.svelte @@ -19,6 +19,7 @@ export let domainMap = new BiMap({}); export let selectedDomain = ''; export let externalVimMode = null; // allow external control of vim mode + export let attachments = []; // array of email attachments for inline image preview let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled; let editor = null; let previewFrame = null; @@ -287,6 +288,54 @@ }; const replaceTemplateVariables = async (text) => { + // replace inline image cid: references with data URLs for preview in shadow DOM + if (attachments && attachments.length > 0) { + // find all cid: references in the text + const cidRegex = /src=["']cid:([^"']+)["']/gi; + const matches = []; + let match; + while ((match = cidRegex.exec(text)) !== null) { + matches.push(match[1]); + } + + // fetch and replace each cid: reference with data URL + for (const filename of matches) { + // find the attachment with this filename that is inline + const attachment = attachments.find((att) => att.isInline && att.fileName === filename); + if (attachment && attachment.id) { + try { + // fetch attachment content from API + const response = await fetch(`/api/v1/attachment/${attachment.id}/content`); + const json = await response.json(); + + if (json.success && json.data && json.data.file) { + // determine mime type from filename extension + const ext = filename.split('.').pop().toLowerCase(); + const mimeTypes = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + bmp: 'image/bmp' + }; + const mimeType = mimeTypes[ext] || 'image/png'; + + // create data URL from base64 data + const dataUrl = `data:${mimeType};base64,${json.data.file}`; + + // replace cid: with data URL (escape filename for regex) + const escapedFilename = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + text = text.replace(new RegExp(`cid:${escapedFilename}`, 'g'), dataUrl); + } + } catch (error) { + console.error('Failed to fetch attachment for preview:', error); + } + } + } + } + let param = '?id=905f286e-486b-434b-8ecc-d82456a07f7b'; let _baseURL = `https://${baseURL}`; let _url = `https://${baseURL}${param}`; diff --git a/frontend/src/routes/email/+page.svelte b/frontend/src/routes/email/+page.svelte index 8e7fbe8..38274b7 100644 --- a/frontend/src/routes/email/+page.svelte +++ b/frontend/src/routes/email/+page.svelte @@ -54,7 +54,8 @@ mailEnvelopeFrom: null, mailHeaderFrom: null, mailHeaderSubject: null, - addTrackingPixel: false + addTrackingPixel: false, + attachments: [] }; let isDeleteAlertVisible = false; let deleteValues = { @@ -259,7 +260,8 @@ mailEnvelopeFrom: null, mailHeaderFrom: null, mailHeaderSubject: null, - addTrackingPixel: false + addTrackingPixel: false, + attachments: [] }; form.reset(); isModalVisible = false; @@ -310,6 +312,7 @@ formValues.mailHeaderFrom = email.mailHeaderFrom; formValues.mailHeaderSubject = email.mailHeaderSubject; formValues.addTrackingPixel = email.addTrackingPixel; + formValues.attachments = email.attachments || []; }; const openSendTestModal = async (id) => { @@ -437,7 +440,12 @@ - +
{ try { isSubmitting = true; - const idsToAdd = formValues.attachmentIDs.map((id) => { - return availableAttachmentMap.byValue(id); + const attachmentsToAdd = formValues.attachmentIDs.map((id) => { + return { + id: availableAttachmentMap.byValue(id), + isInline: formValues.isInline + }; }); - const res = await api.email.addAttachments($page.params.id, idsToAdd); + const res = await api.email.addAttachments($page.params.id, attachmentsToAdd); if (!res.success) { addError = res.error; return; } addError = ''; - const msg = idsToAdd.length > 1 ? 'attachments' : 'attachment'; + const msg = attachmentsToAdd.length > 1 ? 'attachments' : 'attachment'; addToast(`Added ${msg}`, 'Success'); refreshData(); showAddAttachmentModal = false; @@ -186,12 +192,14 @@ const openModal = () => { showAddAttachmentModal = true; formValues.attachmentIDs = []; + formValues.isInline = false; addError = ''; }; const closeModal = () => { showAddAttachmentModal = false; formValues.attachmentIDs = []; + formValues.isInline = false; addError = ''; }; @@ -201,8 +209,8 @@ Attachments: {emailName} Add attachement + @@ -243,6 +252,13 @@ >Attachment + + Inline (for images) +