diff --git a/app/mailers/conversation_reply_mailer.rb b/app/mailers/conversation_reply_mailer.rb index f9d1faf3d..cba95cb69 100644 --- a/app/mailers/conversation_reply_mailer.rb +++ b/app/mailers/conversation_reply_mailer.rb @@ -1,4 +1,8 @@ class ConversationReplyMailer < ApplicationMailer + # We needs to expose large attachments to the view as links + # Small attachments are linked as mail attachments directly + attr_reader :large_attachments + include ConversationReplyMailerHelper default from: ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot ') layout :choose_layout diff --git a/app/mailers/conversation_reply_mailer_helper.rb b/app/mailers/conversation_reply_mailer_helper.rb index 198e9f2a1..4f12bdb51 100644 --- a/app/mailers/conversation_reply_mailer_helper.rb +++ b/app/mailers/conversation_reply_mailer_helper.rb @@ -17,11 +17,44 @@ module ConversationReplyMailerHelper google_smtp_settings set_delivery_method - Rails.logger.info("Email sent from #{email_from} to #{to_emails} with subject #{mail_subject}") - + # Email type detection logic: + # - email_reply: Sets @message with a single message + # - Other actions: Set @messages with a collection of messages + # + # So this check implicitly determines we're handling an email_reply + # and not one of the other email types (summary, transcript, etc.) + process_attachments_as_files_for_email_reply if @message&.attachments.present? mail(@options) end + def process_attachments_as_files_for_email_reply + # Attachment processing for direct email replies (when replying to a single message) + # + # How attachments are handled: + # 1. Total file size (<20MB): Added directly to the email as proper attachments + # 2. Total file size (>20MB): Added to @large_attachments to be displayed as links in the email + + @options[:attachments] = [] + @large_attachments = [] + current_total_size = 0 + + @message.attachments.each do |attachment| + raw_data = attachment.file.download + attachment_name = attachment.file.filename.to_s + file_size = raw_data.bytesize + + # Attach files directly until we hit 20MB total + # After reaching 20MB, send remaining files as links + if current_total_size + file_size <= 20.megabytes + mail.attachments[attachment_name] = raw_data + @options[:attachments] << { name: attachment_name } + current_total_size += file_size + else + @large_attachments << attachment + end + end + end + private def google_smtp_settings diff --git a/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb b/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb index 0ea0f301b..29ee49f67 100644 --- a/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb +++ b/app/views/mailers/conversation_reply_mailer/conversation_transcript.html.erb @@ -32,9 +32,10 @@ <% if message.content %> <%= ChatwootMarkdownRenderer.new(message.content).render_message %> <% end %> - <% if message.attachments %> + <% if message.attachments.present? %> +

Attachments:

<% message.attachments.each do |attachment| %> - Attachment [Click here to view] +

<%= attachment.file.filename.to_s %>

<% end %> <% end %>

diff --git a/app/views/mailers/conversation_reply_mailer/email_reply.html.erb b/app/views/mailers/conversation_reply_mailer/email_reply.html.erb index 1d943c27e..319ba4b4e 100644 --- a/app/views/mailers/conversation_reply_mailer/email_reply.html.erb +++ b/app/views/mailers/conversation_reply_mailer/email_reply.html.erb @@ -1,8 +1,9 @@ <% if @message.content %> <%= ChatwootMarkdownRenderer.new(@message.content).render_message %> <% end %> -<% if @message.attachments %> - <% @message.attachments.each do |attachment| %> - attachment [click here to view] +<% if @large_attachments.present? %> +

Attachments:

+ <% @large_attachments.each do |attachment| %> +

<%= attachment.file.filename.to_s %>

<% end %> <% end %> diff --git a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb index 6c498d793..fd0bf068f 100644 --- a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb +++ b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb @@ -20,9 +20,10 @@ <% if message.content.present? %>
<% end %> - This message contains <%= message.attachments.count > 1 ? 'attachments' : 'an attachment' %>. - <% message.attachments.each do |attachment| %> -
- View the attachment here. + Attachments: + <% message.attachments.each_with_index do |attachment, index| %> + <% if index > 0 %>
<% end %> + <%= attachment.file.filename.to_s %> <% end %>

<% end %> diff --git a/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb b/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb index 3c9b8034a..4c043a11e 100644 --- a/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb +++ b/app/views/mailers/conversation_reply_mailer/reply_without_summary.html.erb @@ -3,9 +3,10 @@ <% if message.content %> <%= ChatwootMarkdownRenderer.new(message.content).render_message %> <% end %> - <% if message.attachments %> + <% if message.attachments.present? %> +

Attachments:

<% message.attachments.each do |attachment| %> - attachment [click here to view] +

<%= attachment.file.filename.to_s %>

<% end %> <% end %>

diff --git a/spec/assets/large_file.pdf b/spec/assets/large_file.pdf new file mode 100644 index 000000000..8693adbce Binary files /dev/null and b/spec/assets/large_file.pdf differ diff --git a/spec/mailers/conversation_reply_mailer_spec.rb b/spec/mailers/conversation_reply_mailer_spec.rb index b8984e2c2..28bc55385 100644 --- a/spec/mailers/conversation_reply_mailer_spec.rb +++ b/spec/mailers/conversation_reply_mailer_spec.rb @@ -153,6 +153,74 @@ RSpec.describe ConversationReplyMailer do it 'updates the source_id' do expect(mail.message_id).to eq message.source_id end + + context 'with email attachments' do + it 'includes small attachments as email attachments' do + message_with_attachment = create(:message, conversation: conversation, account: account, message_type: 'outgoing', + content: 'Message with small attachment') + attachment = message_with_attachment.attachments.new(account_id: account.id, file_type: :file) + attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + attachment.save! + + mail = described_class.email_reply(message_with_attachment).deliver_now + + # Should be attached to the email + expect(mail.attachments.map(&:filename).map(&:to_s)).to include('avatar.png') + # Should not be in large_attachments + expect(mail.body.encoded).not_to include('Attachments:') + end + + it 'renders large attachments as links in the email body' do + message_with_large_attachment = create(:message, conversation: conversation, account: account, message_type: 'outgoing', + content: 'Message with large attachment') + attachment = message_with_large_attachment.attachments.new(account_id: account.id, file_type: :file) + attachment.file.attach(io: Rails.root.join('spec/assets/large_file.pdf').open, filename: 'large_file.pdf', content_type: 'application/pdf') + attachment.save! + + mail = described_class.email_reply(message_with_large_attachment).deliver_now + + # Should NOT be attached to the email + expect(mail.attachments.map(&:filename).map(&:to_s)).not_to include('large_file.pdf') + # Should be rendered as a link in the body + expect(mail.body.encoded).to include('Attachments:') + expect(mail.body.encoded).to include('large_file.pdf') + # Should render a link with large_file.pdf as the link text + expect(mail.body.encoded).to match(%r{]*>large_file\.pdf}) + # Small file should not be rendered as a link in the body + expect(mail.body.encoded).not_to match(%r{]*>avatar\.png}) + end + + it 'handles both small and large attachments correctly' do + message_with_mixed_attachments = create(:message, conversation: conversation, account: account, message_type: 'outgoing', + content: 'Message with mixed attachments') + + # Small attachment + small_attachment = message_with_mixed_attachments.attachments.new(account_id: account.id, file_type: :file) + small_attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + small_attachment.save! + + # Large attachment + large_attachment = message_with_mixed_attachments.attachments.new(account_id: account.id, file_type: :file) + large_attachment.file.attach(io: Rails.root.join('spec/assets/large_file.pdf').open, filename: 'large_file.pdf', + content_type: 'application/pdf') + large_attachment.save! + + mail = described_class.email_reply(message_with_mixed_attachments).deliver_now + + # Small file should be attached + expect(mail.attachments.map(&:filename).map(&:to_s)).to include('avatar.png') + # Large file should NOT be attached + expect(mail.attachments.map(&:filename).map(&:to_s)).not_to include('large_file.pdf') + + # Large file should be rendered as a link in the body + expect(mail.body.encoded).to include('Attachments:') + expect(mail.body.encoded).to include('large_file.pdf') + # Should render a link with large_file.pdf as the link text + expect(mail.body.encoded).to match(%r{]*>large_file\.pdf}) + # Small file should not be rendered as a link in the body + expect(mail.body.encoded).not_to match(%r{]*>avatar\.png}) + end + end end context 'when smtp enabled for email channel' do