Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sending attachments less than 3MB #483

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 6 additions & 22 deletions lib/nylas/resources/drafts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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}",
Expand Down
14 changes: 3 additions & 11 deletions lib/nylas/resources/messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 44 additions & 0 deletions lib/nylas/utils/file_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions spec/nylas/resources/drafts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]" }],
Expand Down Expand Up @@ -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: "[email protected]" }],
Expand Down
2 changes: 2 additions & 0 deletions spec/nylas/resources/messages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]" }],
Expand Down
116 changes: 116 additions & 0 deletions spec/nylas/utils/file_utils_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading