Fixes #3146 - IMAP channel improvements to handle ProtonMail (bridge) and Office 365.
This commit is contained in:
parent
92152743d7
commit
577bb5f723
2 changed files with 83 additions and 23 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue