diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b41a82..c5e77f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### Unreleased +* Fixed sending attachments less than 3MB + ### 6.1.0 / 2024-06-25 * Added query support for messages * Added support for clean messages diff --git a/lib/nylas/resources/drafts.rb b/lib/nylas/resources/drafts.rb index 4be5a0a8..6dbf8fb2 100644 --- a/lib/nylas/resources/drafts.rb +++ b/lib/nylas/resources/drafts.rb @@ -40,19 +40,11 @@ def find(identifier:, draft_id:) # @param identifier [String] Grant ID or email account in which to create the draft. # @param request_body [Hash] The values to create the message with. # If you're attaching files, you must pass an array of [File] objects, or - # you can use {FileUtils::attach_file_request_builder} to build each object attach. + # you can pass in base64 encoded strings if the total attachment size is less than 3mb. + # You can also use {FileUtils::attach_file_request_builder} to build each object attach. # @return [Array(Hash, String)] The created draft and API Request ID. def create(identifier:, request_body:) - payload = request_body - opened_files = [] - - # Use form data only if the attachment size is greater than 3mb - attachments = request_body[:attachments] || request_body["attachments"] || [] - attachment_size = attachments&.sum { |attachment| attachment[:size] || 0 } || 0 - - if attachment_size >= FileUtils::FORM_DATA_ATTACHMENT_SIZE - payload, opened_files = FileUtils.build_form_request(request_body) - end + payload, opened_files = FileUtils.handle_message_payload(request_body) response = post( path: "#{api_uri}/v3/grants/#{identifier}/drafts", @@ -70,19 +62,11 @@ def create(identifier:, request_body:) # @param draft_id [String] The id of the draft to update. # @param request_body [Hash] The values to create the message with. # If you're attaching files, you must pass an array of [File] objects, or - # you can use {FileUtils::attach_file_request_builder} to build each object attach. + # you can pass in base64 encoded strings if the total attachment size is less than 3mb. + # You can also use {FileUtils::attach_file_request_builder} to build each object attach. # @return [Array(Hash, String)] The updated draft and API Request ID. def update(identifier:, draft_id:, request_body:) - payload = request_body - opened_files = [] - - # Use form data only if the attachment size is greater than 3mb - attachments = request_body[:attachments] || request_body["attachments"] || [] - attachment_size = attachments&.sum { |attachment| attachment[:size] || 0 } || 0 - - if attachment_size >= FileUtils::FORM_DATA_ATTACHMENT_SIZE - payload, opened_files = FileUtils.build_form_request(request_body) - end + payload, opened_files = FileUtils.handle_message_payload(request_body) response = put( path: "#{api_uri}/v3/grants/#{identifier}/drafts/#{draft_id}", diff --git a/lib/nylas/resources/messages.rb b/lib/nylas/resources/messages.rb index bc6b4e10..544da573 100644 --- a/lib/nylas/resources/messages.rb +++ b/lib/nylas/resources/messages.rb @@ -92,19 +92,11 @@ def clean_messages(identifier:, request_body:) # @param identifier [String] Grant ID or email account from which to delete an object. # @param request_body [Hash] The values to create the message with. # If you're attaching files, you must pass an array of [File] objects, or - # you can use {FileUtils::attach_file_request_builder} to build each object attach. + # you can pass in base64 encoded strings if the total attachment size is less than 3mb. + # You can also use {FileUtils::attach_file_request_builder} to build each object attach. # @return [Array(Hash, String)] The sent message and the API Request ID. def send(identifier:, request_body:) - payload = request_body - opened_files = [] - - # Use form data only if the attachment size is greater than 3mb - attachments = request_body[:attachments] || request_body["attachments"] || [] - attachment_size = attachments&.sum { |attachment| attachment[:size] || 0 } || 0 - - if attachment_size >= FileUtils::FORM_DATA_ATTACHMENT_SIZE - payload, opened_files = FileUtils.build_form_request(request_body) - end + payload, opened_files = FileUtils.handle_message_payload(request_body) response = post( path: "#{api_uri}/v3/grants/#{identifier}/messages/send", diff --git a/lib/nylas/utils/file_utils.rb b/lib/nylas/utils/file_utils.rb index 0c0f96f8..fff71f04 100644 --- a/lib/nylas/utils/file_utils.rb +++ b/lib/nylas/utils/file_utils.rb @@ -36,6 +36,50 @@ def self.build_form_request(request_body) [form_data, opened_files] end + # Build a json attachment request for the API. + # @param attachments The attachments to send with the message. Can be a file object or a base64 string. + # @return The properly-formatted json data to send to the API and the opened files. + # @!visibility private + def self.build_json_request(attachments) + opened_files = [] + + attachments.each_with_index do |attachment, _index| + current_attachment = attachment[:content] + next unless current_attachment + + if current_attachment.respond_to?(:read) + attachment[:content] = Base64.strict_encode64(current_attachment.read) + opened_files << current_attachment + else + attachment[:content] = current_attachment + end + end + + [attachments, opened_files] + end + + # Handle encoding the message payload. + # @param request_body The values to create the message with. + # @return The encoded message payload and any opened files. + # @!visibility private + def self.handle_message_payload(request_body) + payload = request_body.transform_keys(&:to_sym) + opened_files = [] + + # Use form data only if the attachment size is greater than 3mb + attachments = payload[:attachments] + attachment_size = attachments&.sum { |attachment| attachment[:size] || 0 } || 0 + + # Handle the attachment encoding depending on the size + if attachment_size >= FORM_DATA_ATTACHMENT_SIZE + payload, opened_files = build_form_request(request_body) + else + payload[:attachments], opened_files = build_json_request(attachments) unless attachments.nil? + end + + [payload, opened_files] + end + # Build the request to attach a file to a message/draft object. # @param file_path [String] The path to the file to attach. # @return [Hash] The request that will attach the file to the message/draft diff --git a/spec/nylas/resources/drafts_spec.rb b/spec/nylas/resources/drafts_spec.rb index 9981caf4..bc01b57f 100644 --- a/spec/nylas/resources/drafts_spec.rb +++ b/spec/nylas/resources/drafts_spec.rb @@ -100,6 +100,8 @@ it "calls the post method with the correct parameters for small attachments" do identifier = "abc-123-grant-id" mock_file = instance_double("file") + allow(mock_file).to receive(:read).and_return("file content") + allow(mock_file).to receive(:close).and_return(true) request_body = { subject: "Hello from Nylas!", to: [{ name: "Jon Snow", email: "jsnow@gmail.com" }], @@ -183,6 +185,8 @@ identifier = "abc-123-grant-id" draft_id = "5d3qmne77v32r8l4phyuksl2x" mock_file = instance_double("file") + allow(mock_file).to receive(:read).and_return("file content") + allow(mock_file).to receive(:close).and_return(true) request_body = { subject: "Hello from Nylas!", to: [{ name: "Jon Snow", email: "jsnow@gmail.com" }], diff --git a/spec/nylas/resources/messages_spec.rb b/spec/nylas/resources/messages_spec.rb index c1ec75b4..a87abe89 100644 --- a/spec/nylas/resources/messages_spec.rb +++ b/spec/nylas/resources/messages_spec.rb @@ -140,6 +140,8 @@ it "calls the post method with the correct parameters and attachments" do identifier = "abc-123-grant-id" mock_file = instance_double("file") + allow(mock_file).to receive(:read).and_return("file content") + allow(mock_file).to receive(:close).and_return(true) request_body = { subject: "Hello from Nylas!", to: [{ name: "Jon Snow", email: "jsnow@gmail.com" }], diff --git a/spec/nylas/utils/file_utils_spec.rb b/spec/nylas/utils/file_utils_spec.rb index e250e7fc..ac896192 100644 --- a/spec/nylas/utils/file_utils_spec.rb +++ b/spec/nylas/utils/file_utils_spec.rb @@ -78,4 +78,120 @@ expect(form_request).to eq([request_body, []]) end end + + describe "#build_json_request" do + let(:mock_file) { instance_double("file") } + + it "encodes the content of each attachment" do + allow(mock_file).to receive(:read).and_return("file content") + attachments = [{ content: mock_file }] + + result, opened_files = described_class.build_json_request(attachments) + + expect(result.first[:content]).to eq(Base64.strict_encode64("file content")) + expect(opened_files).to include(mock_file) + end + + it "skips attachments with no content" do + attachments = [{ content: nil }] + + result, opened_files = described_class.build_json_request(attachments) + + expect(result.first[:content]).to be_nil + expect(opened_files).to be_empty + end + + it "returns empty arrays when attachments are empty" do + attachments = [] + + result, opened_files = described_class.build_json_request(attachments) + + expect(result).to eq([]) + expect(opened_files).to eq([]) + end + + it "handles multiple attachments" do + mock_file1 = instance_double("file1") + mock_file2 = instance_double("file2") + allow(mock_file1).to receive(:read).and_return("file content 1") + allow(mock_file2).to receive(:read).and_return("file content 2") + attachments = [{ content: mock_file1 }, { content: mock_file2 }] + + result, opened_files = described_class.build_json_request(attachments) + + expect(result[0][:content]).to eq(Base64.strict_encode64("file content 1")) + expect(result[1][:content]).to eq(Base64.strict_encode64("file content 2")) + expect(opened_files).to include(mock_file1, mock_file2) + end + + it "sends a b64 string without further encoding" do + attachments = [{ content: "SGVsbG8gd29ybGQ=" }] + + result, opened_files = described_class.build_json_request(attachments) + + expect(result.first[:content]).to match(Base64.strict_encode64("Hello world")) + expect(opened_files).to be_empty + end + end + + describe "#handle_message_payload" do + let(:mock_file) { instance_double("file") } + + it "returns form data when attachment size is greater than 3MB" do + large_attachment = { size: 4 * 1024 * 1024, content: mock_file } + request_body = { attachments: [large_attachment] } + + allow(mock_file).to receive(:read).and_return("file content") + allow(File).to receive(:size).and_return(large_attachment[:size]) + + payload, opened_files = described_class.handle_message_payload(request_body) + + expect(payload).to include("multipart" => true) + expect(opened_files).to include(mock_file) + end + + it "returns json data when attachment size is less than 3MB" do + small_attachment = { size: 2 * 1024 * 1024, content: mock_file } + request_body = { attachments: [small_attachment] } + + allow(mock_file).to receive(:read).and_return("file content") + + payload, opened_files = described_class.handle_message_payload(request_body) + + expect(payload[:attachments].first[:content]).to eq(Base64.strict_encode64("file content")) + expect(opened_files).to include(mock_file) + end + + it "returns json data when there are no attachments" do + request_body = { attachments: [] } + + payload, opened_files = described_class.handle_message_payload(request_body) + + expect(payload[:attachments]).to eq([]) + expect(opened_files).to eq([]) + end + + it "returns json data when attachments is nil" do + request_body = { attachments: nil } + + payload, opened_files = described_class.handle_message_payload(request_body) + + expect(payload[:attachments]).to be_nil + expect(opened_files).to eq([]) + end + + it "handles multiple attachments with mixed sizes" do + small_attachment = { size: 2 * 1024 * 1024, content: mock_file } + large_attachment = { size: 4 * 1024 * 1024, content: mock_file } + request_body = { attachments: [small_attachment, large_attachment] } + + allow(mock_file).to receive(:read).and_return("file content") + allow(File).to receive(:size).and_return(small_attachment[:size], large_attachment[:size]) + + payload, opened_files = described_class.handle_message_payload(request_body) + + expect(payload).to include("multipart" => true) + expect(opened_files).to include(mock_file) + end + end end