diff --git a/app/models/channel/driver/imap.rb b/app/models/channel/driver/imap.rb index 474afb1a1..090dd9ee5 100644 --- a/app/models/channel/driver/imap.rb +++ b/app/models/channel/driver/imap.rb @@ -160,11 +160,11 @@ example message_meta = nil timeout(1.minute) do - message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0].attr + message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0] end # check how many content messages we have, for notice used - headers = parse_headers(message_meta['RFC822.HEADER']) + headers = self.class.extract_rfc822_headers(message_meta) next if messages_is_verify_message?(headers) next if messages_is_ignore_message?(headers) @@ -191,11 +191,12 @@ example message_meta = nil timeout(FETCH_METADATA_TIMEOUT) do - message_meta = @imap.fetch(message_id, ['ENVELOPE'])[0].attr + message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0] end # check if verify message exists - subject = message_meta['ENVELOPE'].subject + headers = self.class.extract_rfc822_headers(message_meta) + subject = headers['Subject'] next if !subject next if !subject.match?(/#{verify_string}/) @@ -237,7 +238,7 @@ example message_meta = nil timeout(FETCH_METADATA_TIMEOUT) do - message_meta = @imap.fetch(message_id, ['RFC822.SIZE', 'ENVELOPE', 'FLAGS', 'INTERNALDATE', 'RFC822.HEADER'])[0] + message_meta = @imap.fetch(message_id, ['RFC822.SIZE', 'FLAGS', 'INTERNALDATE', 'RFC822.HEADER'])[0] rescue Net::IMAP::ResponseParseError => e raise if !e.message.include?('unknown token') @@ -357,10 +358,35 @@ returns false end + # Parses RFC822 header + # @param [String] RFC822 header text blob + # @return [HashString>] + def self.parse_rfc822_headers(string) + array = string + .gsub("\r\n\t", ' ') # Some servers (e.g. office365) may put attribute value on a separate line and tab it + .lines(chomp: true) + .map { |line| line.split(/:\s*/, 2).map(&:strip) } + + array.each { |elem| elem.append(nil) if elem.one? } + + Hash[*array.flatten] + end + + # Parses RFC822 header + # @param [Net::IMAP::FetchData] fetched message + # @return [HashString>] + def self.extract_rfc822_headers(message_meta) + blob = message_meta&.attr&.dig 'RFC822.HEADER' + + return unless blob + + parse_rfc822_headers blob + end + private def messages_is_too_old_verify?(message_meta, count, count_all) - headers = parse_headers(message_meta.attr['RFC822.HEADER']) + headers = self.class.extract_rfc822_headers(message_meta) return true if !messages_is_verify_message?(headers) return true if headers['X-Zammad-Verify-Time'].blank? @@ -389,20 +415,6 @@ returns false end - def parse_headers(string) - return {} if string.blank? - - headers = {} - headers_pairs = string.split("\r\n") - headers_pairs.each do |pair| - key_value = pair.split(': ') - next if key_value[0].blank? - - headers[key_value[0]] = key_value[1] - end - headers - end - =begin check if email is already impoted @@ -419,10 +431,11 @@ returns def already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel) # rubocop:enable Metrics/ParameterLists return false if !keep_on_server - return false if !message_meta.attr - return false if !message_meta.attr['ENVELOPE'] - local_message_id = message_meta.attr['ENVELOPE'].message_id + headers = self.class.extract_rfc822_headers(message_meta) + retrurn false if !headers + + local_message_id = headers['Message-ID'] return false if local_message_id.blank? local_message_id_md5 = Digest::MD5.hexdigest(local_message_id) diff --git a/spec/models/channel/driver/imap_spec.rb b/spec/models/channel/driver/imap_spec.rb index 8d8b20791..b080ae542 100644 --- a/spec/models/channel/driver/imap_spec.rb +++ b/spec/models/channel/driver/imap_spec.rb @@ -23,4 +23,51 @@ RSpec.describe Channel::Driver::Imap do expect(result.dig(:result)).to eq 'ok' end end + + describe '.parse_rfc822_headers' do + it 'parses simple header' do + expect(described_class.parse_rfc822_headers('Key: Value')).to have_key('Key').and(have_value('Value')) + end + + it 'parses header with no white space' do + expect(described_class.parse_rfc822_headers('Key:Value')).to have_key('Key').and(have_value('Value')) + end + + it 'parses multiline header' do + expect(described_class.parse_rfc822_headers("Key: Value\r\n2nd-key: 2nd-value")) + .to have_key('Key').and(have_value('Value')).and(have_key('2nd-key')).and(have_value('2nd-value')) + end + + it 'parses value with semicolons' do + expect(described_class.parse_rfc822_headers('Key: Val:ue')).to have_key('Key').and(have_value('Val:ue')) + end + + it 'parses key-only lines' do + expect(described_class.parse_rfc822_headers('Key')).to have_key('Key') + end + + it 'handles empty line' do + expect { described_class.parse_rfc822_headers("Key: Value\r\n") }.not_to raise_error + end + + it 'handles tabbed value' do + expect(described_class.parse_rfc822_headers("Key: \r\n\tValue")).to have_key('Key').and(have_value('Value')) + end + end + + describe '.extract_rfc822_headers' do + it 'extracts header' do + object = Net::IMAP::FetchData.new :id, { 'RFC822.HEADER' => 'Key: Value' } + expect(described_class.extract_rfc822_headers(object)).to have_key('Key').and(have_value('Value')) + end + + it 'returns nil when header attribute is missing' do + object = Net::IMAP::FetchData.new :id, { 'Another' => 'Key: Value' } + expect(described_class.extract_rfc822_headers(object)).to be_nil + end + + it 'does not raise error when given nil' do + expect { described_class.extract_rfc822_headers(nil) }.not_to raise_error + end + end end