mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
In Chatwoot, we rely on the Content-ID for inline attachments to replace the link with the uploaded attachment URL. Our expectation was that only images would be inline, while other attachments would not. However, email clients like Apple Mail (sigh) allow users to send inline attachments that are not images, and these attachments often lack a Content-ID. This creates significant issues in rendering. I investigated how other email clients handle this scenario. When viewing the same email (sent from Apple Mail) in Gmail, only one image appears—and it’s treated as an attachment, not inline. This happens because both attachments are the same image, and Apple Mail only sends one copy. See the screenshot below. | Apple Mail | Gmail | | -- | -- | | <img width="646" alt="Screenshot 2025-02-27 at 8 20 17 PM" src="https://github.com/user-attachments/assets/e0d1cd2d-e47c-4081-a53b-7a67106341b3" /> | <img width="360" alt="Screenshot 2025-02-27 at 8 20 51 PM" src="https://github.com/user-attachments/assets/b206e56e-8f86-43e9-867b-d895c36aff78" /> | A good fix for this would be to check if the Content-ID is missing and then upload the file as a regular attachment. However, the Mail gem (for some reason) automatically adds a default Content-ID to inline parts. I need to dig into the source code to understand why this happens. For now, I’ve implemented a check to treat non-image attachments as regular attachments. Inline image attachments are already handled by appending an image tag at the end if the content-id is not found in the body. A sample conversation to test this behavior is [here](https://app.chatwoot.com/app/accounts/1/conversations/46732).
130 lines
4.6 KiB
Ruby
130 lines
4.6 KiB
Ruby
module MailboxHelper
|
|
private
|
|
|
|
def create_message
|
|
Rails.logger.info "[MailboxHelper] Creating message #{processed_mail.message_id}"
|
|
return if @conversation.messages.find_by(source_id: processed_mail.message_id).present?
|
|
|
|
@message = @conversation.messages.create!(
|
|
account_id: @conversation.account_id,
|
|
sender: @conversation.contact,
|
|
content: mail_content&.truncate(150_000),
|
|
inbox_id: @conversation.inbox_id,
|
|
message_type: 'incoming',
|
|
content_type: 'incoming_email',
|
|
source_id: processed_mail.message_id,
|
|
content_attributes: {
|
|
email: processed_mail.serialized_data,
|
|
cc_email: processed_mail.cc,
|
|
bcc_email: processed_mail.bcc
|
|
}
|
|
)
|
|
end
|
|
|
|
def add_attachments_to_message
|
|
return if @message.blank?
|
|
|
|
# ensure we don't add more than the permitted number of attachments
|
|
all_attachments = processed_mail.attachments.last(Message::NUMBER_OF_PERMITTED_ATTACHMENTS)
|
|
grouped_attachments = group_attachments(all_attachments)
|
|
|
|
process_inline_attachments(grouped_attachments[:inline]) if grouped_attachments[:inline].present?
|
|
process_regular_attachments(grouped_attachments[:regular]) if grouped_attachments[:regular].present?
|
|
|
|
@message.save!
|
|
end
|
|
|
|
def group_attachments(attachments)
|
|
# If the email lacks a text body or if inline attachments aren't images,
|
|
# treat them as standard attachments for processing.
|
|
inline_attachments = attachments.select do |attachment|
|
|
mail_content.present? && attachment[:original].inline? && attachment[:original].content_type.to_s.start_with?('image/')
|
|
end
|
|
|
|
regular_attachments = attachments - inline_attachments
|
|
{ inline: inline_attachments, regular: regular_attachments }
|
|
end
|
|
|
|
def process_regular_attachments(attachments)
|
|
Rails.logger.info "[MailboxHelper] Processing regular attachments for message with ID: #{processed_mail.message_id}"
|
|
attachments.each do |mail_attachment|
|
|
attachment = @message.attachments.new(
|
|
account_id: @conversation.account_id,
|
|
file_type: 'file'
|
|
)
|
|
attachment.file.attach(mail_attachment[:blob])
|
|
end
|
|
end
|
|
|
|
def process_inline_attachments(attachments)
|
|
Rails.logger.info "[MailboxHelper] Processing inline attachments for message with ID: #{processed_mail.message_id}"
|
|
|
|
# create an instance variable here, the `embed_inline_image_source`
|
|
# updates them directly. And then the value is eventaully used to update the message content
|
|
@html_content = processed_mail.serialized_data[:html_content][:full]
|
|
@text_content = processed_mail.serialized_data[:text_content][:reply]
|
|
|
|
attachments.each do |mail_attachment|
|
|
embed_inline_image_source(mail_attachment)
|
|
end
|
|
|
|
# update the message content with the updated html and text content
|
|
@message.content_attributes[:email][:html_content][:full] = @html_content
|
|
@message.content_attributes[:email][:text_content][:full] = @text_content
|
|
end
|
|
|
|
def embed_inline_image_source(mail_attachment)
|
|
if @html_content.present?
|
|
upload_inline_image(mail_attachment)
|
|
elsif @text_content.present?
|
|
embed_plain_text_email_with_inline_image(mail_attachment)
|
|
end
|
|
end
|
|
|
|
def upload_inline_image(mail_attachment)
|
|
content_id = mail_attachment[:original].cid
|
|
|
|
@html_content = @html_content.gsub("cid:#{content_id}", inline_image_url(mail_attachment[:blob]).to_s)
|
|
end
|
|
|
|
def embed_plain_text_email_with_inline_image(mail_attachment)
|
|
attachment_name = mail_attachment[:original].filename
|
|
img_tag = "<img src=\"#{inline_image_url(mail_attachment[:blob])}\" alt=\"#{attachment_name}\">"
|
|
|
|
tag_to_replace = "[image: #{attachment_name}]"
|
|
|
|
if @text_content.include?(tag_to_replace)
|
|
@text_content = @text_content.gsub(tag_to_replace, img_tag)
|
|
else
|
|
@text_content += "\n\n#{img_tag}"
|
|
end
|
|
end
|
|
|
|
def inline_image_url(blob)
|
|
Rails.application.routes.url_helpers.url_for(blob)
|
|
end
|
|
|
|
def create_contact
|
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
|
source_id: processed_mail.original_sender,
|
|
inbox: @inbox,
|
|
contact_attributes: {
|
|
name: identify_contact_name,
|
|
email: processed_mail.original_sender,
|
|
additional_attributes: { source_id: "email:#{processed_mail.message_id}" }
|
|
}
|
|
).perform
|
|
|
|
@contact = @contact_inbox.contact
|
|
Rails.logger.info "[MailboxHelper] Contact created with ID: #{@contact.id} for inbox with ID: #{@inbox.id}"
|
|
end
|
|
|
|
def mail_content
|
|
if processed_mail.text_content.present?
|
|
processed_mail.text_content[:reply]
|
|
elsif processed_mail.html_content.present?
|
|
processed_mail.html_content[:reply]
|
|
end
|
|
end
|
|
end
|