Вложение winmail.dat повреждено с помощью ActionMailer в приложении Rails

Я использую ActionMailer в приложении Ruby on Rails для чтения электронных писем (ruby 1.9.3, rails 3.2.13). У меня есть электронное письмо, к которому прикреплен файл winmail.dat (ms-tnef), и я использую гем tnef для извлечения его содержимого.

Проблема в том, что когда я читаю вложение из почты, оно повреждается и tnef не может извлечь из него файлы.

$ tnef winmail.dat
ERROR: invalid checksum, input file may be corrupted

Распаковав вложение winmail.dat с помощью любого почтового приложения, извлеченный winmail.dat прекрасно работает с tnef, и я получил его содержимое.

Сравнивая два файла, я заметил, что: - исходный файл больше (76 КБ против 72 КБ) - они различаются по разрывам строк: оригинальный файл имеет формат окна (0D 0A), а файл, сохраненный с помощью rails, имеет формат linux (0A)

Я написал этот тест:

it 'should extract winmail.dat from email and extract its contents' do
    file_path = "#{::Rails.root}/spec/files/winmail-dat-001.eml"
    message = Mail::Message.new(File.read(file_path))
    anexo = message.attachments[0]
    files = []
    Tnef.unpack(anexo) do |file|
      files << File.basename(file)
    end
    puts files.inspect
    files.size.should == 2
end

Это не с этими сообщениями:

WARNING: invalid checksum, input file may be corrupted
Invalid RTF CRC, input file may be corrupted
WARNING: invalid checksum, input file may be corrupted

Assertion failed: ((attr->lvl_type == LVL_MESSAGE) || (attr->lvl_type == LVL_ATTACHMENT)), function attr_read, file attr.c, line 240.

Errno::EPIPE: Broken pipe


anexo = message.attachments[0]
 => #<Mail::Part:2159872060, Multipart: false, Headers: <Content-Type: application/ms-tnef; name="winmail.dat">, <Content-Transfer-Encoding: quoted-printable>, <Content-Disposition: attachment; filename="winmail.dat">>

Я пытался сохранить его на диск как bynary и перечитал снова, но получил тот же результат

it 'should extract winmail.dat from email and extract its contents' do
    file_path = "#{::Rails.root}/spec/files/winmail-dat-001.eml"
    message = Mail::Message.new(File.read(file_path))
    anexo = message.attachments[0]

    tmpfile_name = "#{::Rails.root}/tmp/#{anexo.filename}"
    File.open(tmpfile_name, 'w+b', 0644) { |f| f.write anexo.body.decoded }
    anexo = File.open(tmpfile_name)

    files = []
    Tnef.unpack(anexo) do |file|
      files << File.basename(file)
    end
    puts files.inspect
    files.size.should == 2
end

Как я должен прочитать вложение?

1 ответ

Решение

Метод anexo.body.decoded вызывает метод декодирования наиболее подходящего кодирования (Mail::Encodings) для вложения, в вашем случае quoted_printable.

Некоторые из этих кодировок (7- битные, 8-битные и quoted_printable) выполняют преобразование, меняя различные типы разрывов строк на разрывы, характерные для конкретной платформы. *quoted_printable"вызов.to_lf, который повреждает файл winmail.dat

  # Decode the string from Quoted-Printable. Cope with hard line breaks
  # that were incorrectly encoded as hex instead of literal CRLF.
  def self.decode(str)
    str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first.to_lf
  end

почта /core_extensions/string.rb:

def to_lf
  to_str.gsub(/\n|\r\n|\r/) { "\n" }
end

Чтобы решить эту проблему, вы должны выполнить ту же кодировку без последнего.to_lf. Для этого вы можете создать новую кодировку, которая не повредит ваш файл, и использовать ее для кодирования вашего вложения.

создайте файл: lib/encodings/tnef_encoding.rb

require 'mail/encodings/7bit'

module Mail
  module Encodings

    # Encoding to handle Microsoft TNEF format
    # It's pretty similar to quoted_printable, except for the 'to_lf' (decode) and 'to_crlf' (encode)
    class TnefEncoding < SevenBit
      NAME='tnef'

      PRIORITY = 2

      def self.can_encode?(str)
        EightBit.can_encode? str
      end

      def self.decode(str)
        # **difference here** removed '.to_lf'
        str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
      end

      def self.encode(str)
        # **difference here** removed '.to_crlf'
        [str.to_lf].pack("M")
      end

      def self.cost(str)
        # These bytes probably do not need encoding
        c = str.count("\x9\xA\xD\x20-\x3C\x3E-\x7E")
        # Everything else turns into =XX where XX is a
        # two digit hex number (taking 3 bytes)
        total = (str.bytesize - c)*3 + c
        total.to_f/str.bytesize
      end

      private

      Encodings.register(NAME, self)
    end
  end
end

Чтобы использовать вашу пользовательскую кодировку, вы должны сначала зарегистрировать ее:

Mail::Encodings.register('tnef', Mail::Encodings::TnefEncoding)

А затем установите его в качестве предпочитаемой кодировки для вложения:

anexo.body.encoding('tnef')

Тогда ваш тест станет:

it 'should extract winmail.dat from email and extract its contents' do
    file_path = "#{::Rails.root}/spec/files/winmail-dat-001.eml"
    message = Mail::Message.new(File.read(file_path))
    anexo = message.attachments[0]

    tmpfile_name = "#{::Rails.root}/tmp/#{anexo.filename}"
    Mail::Encodings.register('tnef', Mail::Encodings::TnefEncoding)
    anexo.body.encoding('tnef')
    File.open(tmpfile_name, 'w+b', 0644) { |f| f.write anexo.body.decoded }
    anexo = File.open(tmpfile_name)

    files = []
    Tnef.unpack(anexo) do |file|
        files << File.basename(file)
    end
    puts files.inspect
    files.size.should == 2
end

Надеюсь, поможет!

Другие вопросы по тегам