Fixes #3146 - IMAP channel improvements to handle ProtonMail (bridge) and Office 365.

This commit is contained in:
Mantas 2020-08-01 21:19:42 +03:00 committed by Thorsten Eckel
parent 92152743d7
commit 577bb5f723
2 changed files with 83 additions and 23 deletions

View file

@ -160,11 +160,11 @@ example
message_meta = nil message_meta = nil
timeout(1.minute) do timeout(1.minute) do
message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0].attr message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0]
end end
# check how many content messages we have, for notice used # 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_verify_message?(headers)
next if messages_is_ignore_message?(headers) next if messages_is_ignore_message?(headers)
@ -191,11 +191,12 @@ example
message_meta = nil message_meta = nil
timeout(FETCH_METADATA_TIMEOUT) do timeout(FETCH_METADATA_TIMEOUT) do
message_meta = @imap.fetch(message_id, ['ENVELOPE'])[0].attr message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0]
end end
# check if verify message exists # 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
next if !subject.match?(/#{verify_string}/) next if !subject.match?(/#{verify_string}/)
@ -237,7 +238,7 @@ example
message_meta = nil message_meta = nil
timeout(FETCH_METADATA_TIMEOUT) do 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 rescue Net::IMAP::ResponseParseError => e
raise if !e.message.include?('unknown token') raise if !e.message.include?('unknown token')
@ -357,10 +358,35 @@ returns
false false
end end
# Parses RFC822 header
# @param [String] RFC822 header text blob
# @return [Hash<String=>String>]
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 [Hash<String=>String>]
def self.extract_rfc822_headers(message_meta)
blob = message_meta&.attr&.dig 'RFC822.HEADER'
return unless blob
parse_rfc822_headers blob
end
private private
def messages_is_too_old_verify?(message_meta, count, count_all) 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 !messages_is_verify_message?(headers)
return true if headers['X-Zammad-Verify-Time'].blank? return true if headers['X-Zammad-Verify-Time'].blank?
@ -389,20 +415,6 @@ returns
false false
end 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 =begin
check if email is already impoted 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) def already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel)
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
return false if !keep_on_server 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? return false if local_message_id.blank?
local_message_id_md5 = Digest::MD5.hexdigest(local_message_id) local_message_id_md5 = Digest::MD5.hexdigest(local_message_id)

View file

@ -23,4 +23,51 @@ RSpec.describe Channel::Driver::Imap do
expect(result.dig(:result)).to eq 'ok' expect(result.dig(:result)).to eq 'ok'
end end
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 end