pjmorse
pjmorse

Reputation: 9294

Unzip binary email attachment

I have a background job in a Rails app which generates a big CSV file and sends it as an attachment to an email message. To manage the size of the email, I'm zipping the CSV before it's attached. This works fine; we have Letter Opener in development and I can open the zipped attachment and read the data. I'm using the "rubyzip" gem to do the zipping.

The trick seems to be testing it. Using rspec, I can verify that the message has an attachment and that the type of the attachment is application/x-zip-encoded, which is all good. What I'd like to do is unzip the attachment and validate the data within the test, though, and that's proving more difficult. The attachment is an object of type Mail::Part. If I call the attachment part, I can get data from it using part.body.raw_source, part.body.encoded or part.decode_body. (The first and third are identical.) part.body.encoding tells me the body is binary. When I try to feed the data to a Zip::InputStream object (using Zip::InputStream.open(part.body.raw_source) or any of the other data methods) I get something like ArgumentError: string contains null byte.

How can I open this attachment and read its data within rspec? I have a hunch there's a decoding step I need to do in here somewhere.

The errors look a lot like this question, but there's no answer there either.

Upvotes: 2

Views: 782

Answers (1)

pjmorse
pjmorse

Reputation: 9294

The solution here was a little less than ideal, but it works. I had to use the Mail library to write the attachment to a file, then use Zip::File to extract the original data and verify it.

it 'has a ZIP attachment' do
  additional_fields = ["student_identifiers"]

  expect(message.attachments.size).to eq(1)
  message.attachments.first.tap do |zip|
    expect(zip.content_type).to eq('application/x-zip-compressed')
    tmpfile_name = "tmp/#{zip.filename}"
    File.open(tmpfile_name, "w+b", 0644) { |f| f.write zip.decoded }
    csv = Zip::File.open(tmpfile_name) do |zip_file|
      entry = zip_file.glob('*.csv').first
      entry.get_input_stream.read
    end
    csv.split("\n").tap do |rows|
      expect(rows.size).to be(2)
      rows.each do |row|
        expect(CSV.parse_line(row).size).to eq(dimensions_count)
      end
    end
    File.delete(tmpfile_name)
  end
end

Upvotes: 2

Related Questions