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)
+