phisher_phinder 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +2 -1
  3. data/.gitignore +3 -0
  4. data/Gemfile +0 -11
  5. data/Gemfile.lock +45 -13
  6. data/README.md +108 -2
  7. data/exe/phisher_phinder +61 -0
  8. data/lib/phisher_phinder.rb +11 -2
  9. data/lib/phisher_phinder/command.rb +20 -0
  10. data/lib/phisher_phinder/display.rb +64 -0
  11. data/lib/phisher_phinder/extended_ip.rb +4 -0
  12. data/lib/phisher_phinder/extended_ip_factory.rb +4 -2
  13. data/lib/phisher_phinder/geoip_ip_data.rb +9 -2
  14. data/lib/phisher_phinder/mail.rb +10 -3
  15. data/lib/phisher_phinder/mail_parser.rb +43 -30
  16. data/lib/phisher_phinder/mail_parser/authentication_headers/auth_results_parser.rb +150 -0
  17. data/lib/phisher_phinder/mail_parser/authentication_headers/parser.rb +25 -0
  18. data/lib/phisher_phinder/mail_parser/authentication_headers/received_spf_parser.rb +222 -0
  19. data/lib/phisher_phinder/mail_parser/body/block_classifier.rb +106 -0
  20. data/lib/phisher_phinder/mail_parser/body/block_parser.rb +37 -0
  21. data/lib/phisher_phinder/mail_parser/body_parser.rb +26 -31
  22. data/lib/phisher_phinder/mail_parser/header_value_parser.rb +25 -10
  23. data/lib/phisher_phinder/mail_parser/received_headers/by_parser.rb +35 -5
  24. data/lib/phisher_phinder/mail_parser/received_headers/for_parser.rb +25 -5
  25. data/lib/phisher_phinder/mail_parser/received_headers/from_parser.rb +50 -6
  26. data/lib/phisher_phinder/mail_parser/received_headers/parser.rb +50 -29
  27. data/lib/phisher_phinder/mail_parser/received_headers/starttls_parser.rb +8 -1
  28. data/lib/phisher_phinder/null_lookup_client.rb +9 -0
  29. data/lib/phisher_phinder/null_response.rb +12 -0
  30. data/lib/phisher_phinder/sender_extractor.rb +74 -0
  31. data/lib/phisher_phinder/simple_ip.rb +4 -0
  32. data/lib/phisher_phinder/tracing_report.rb +47 -0
  33. data/lib/phisher_phinder/version.rb +1 -1
  34. data/phisher_phinder.gemspec +15 -1
  35. metadata +208 -13
@@ -12,5 +12,9 @@ module PhisherPhinder
12
12
  def ==(other)
13
13
  ip_address == other.ip_address && geoip_ip_data == other.geoip_ip_data
14
14
  end
15
+
16
+ def to_s
17
+ @ip_address.to_s
18
+ end
15
19
  end
16
20
  end
@@ -8,9 +8,11 @@ module PhisherPhinder
8
8
  end
9
9
 
10
10
  def build(ip_string)
11
+ return nil unless ip_string
12
+
11
13
  ip = IPAddr.new(ip_string)
12
14
 
13
- if non_public_ip?(ip)
15
+ if non_public_ip?(ip) || ip.ipv6?
14
16
  SimpleIp.new(ip_address: ip)
15
17
  else
16
18
  ExtendedIp.new(ip_address: ip, geoip_ip_data: geoip_data(ip_string))
@@ -45,7 +47,7 @@ module PhisherPhinder
45
47
 
46
48
  def geoip_data(ip_string)
47
49
  @geoip_client.lookup(ip_string)
48
- rescue MaxMind::GeoIP2::AddressNotFoundError
50
+ rescue MaxMind::GeoIP2::AddressNotFoundError, MaxMind::GeoIP2::AddressReservedError
49
51
  end
50
52
  end
51
53
  end
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module PhisherPhinder
4
- class GeoipIpData < Sequel::Model(:geoip_ip_data)
3
+ if ENV['DATABASE_URL']
4
+ require 'sequel'
5
+ Sequel::Model.plugin :timestamps, update_on_create: true
6
+
7
+ DB = Sequel.connect(ENV.fetch('DATABASE_URL'))
8
+
9
+ module PhisherPhinder
10
+ class GeoipIpData < Sequel::Model(:geoip_ip_data)
11
+ end
5
12
  end
6
13
  end
@@ -2,21 +2,28 @@
2
2
 
3
3
  module PhisherPhinder
4
4
  class Mail
5
- attr_reader :original_email, :original_headers, :original_body, :headers, :tracing_headers, :body
5
+ attr_reader :original_email,
6
+ :original_headers,
7
+ :original_body,
8
+ :headers,
9
+ :tracing_headers,
10
+ :body,
11
+ :authentication_headers
6
12
 
7
13
  def initialize(
8
- original_email:, original_headers:, original_body:, headers:, tracing_headers:, body:
14
+ original_email:, original_headers:, original_body:, headers:, tracing_headers:, body:, authentication_headers:
9
15
  )
10
16
  @original_email = original_email
11
17
  @original_headers = original_headers
12
18
  @original_body = original_body
13
19
  @headers = headers
14
20
  @tracing_headers = tracing_headers
21
+ @authentication_headers = authentication_headers
15
22
  @body = body
16
23
  end
17
24
 
18
25
  def reply_to_addresses
19
- @headers[:reply_to].map do |value_string|
26
+ (@headers[:reply_to] || []).map do |value_string|
20
27
  value_string.split(",")
21
28
  end.flatten.map do |email_address_string|
22
29
  extract_email_address(email_address_string)
@@ -5,8 +5,8 @@ require_relative('mail_parser/header_value_parser')
5
5
  module PhisherPhinder
6
6
  module MailParser
7
7
  class Parser
8
- def initialize(enriched_ip_factory, line_ending_type)
9
- @line_end = line_ending_type == 'dos' ? "\r\n" : "\n"
8
+ def initialize(enriched_ip_factory, line_ending)
9
+ @line_ending = line_ending
10
10
  @enriched_ip_factory = enriched_ip_factory
11
11
  end
12
12
 
@@ -19,34 +19,31 @@ module PhisherPhinder
19
19
  original_body: original_body,
20
20
  headers: headers,
21
21
  tracing_headers: generate_tracing_headers(headers),
22
- body: parse_body(original_body, headers)
22
+ body: parse_body(original_body, headers),
23
+ authentication_headers: generate_authentication_headers(headers)
23
24
  )
24
25
  end
25
26
 
26
27
  private
27
28
 
28
29
  def separate(contents)
29
- contents.split("#{@line_end}#{@line_end}", 2)
30
+ contents.split("#{@line_ending}#{@line_ending}", 2)
30
31
  end
31
32
 
32
33
  def extract_headers(headers)
33
- parse_headers(unfold_headers(headers).split(@line_end))
34
+ parse_headers(unfold_headers(headers).split(@line_ending))
34
35
  end
35
36
 
36
37
  def unfold_headers(headers)
37
- headers.gsub(/#{@line_end}[\s\t]+/, ' ')
38
+ headers.gsub(/#{@line_ending}[\s\t]+/, ' ')
38
39
  end
39
40
 
40
41
  def parse_headers(headers_array)
41
42
  headers_array.each_with_index.inject({}) do |memo, (header_string, index)|
42
43
  header, value = header_string.split(":", 2)
43
44
  sequence = headers_array.length - index - 1
44
- memo.merge(convert_header_name(header) => enrich_header_value(value, sequence)) do |_, existing, new|
45
- if existing.is_a? Array
46
- existing << new
47
- else
48
- [existing, new]
49
- end
45
+ memo.merge(convert_header_name(header) => [enrich_header_value(value, sequence)]) do |_, existing, new|
46
+ existing + new
50
47
  end
51
48
  end
52
49
  end
@@ -61,16 +58,8 @@ module PhisherPhinder
61
58
 
62
59
  def generate_tracing_headers(headers)
63
60
  received_header_values = headers.inject([]) do |memo, (header_name, header_value)|
64
- if [:received, :x_received].include? header_name
65
- if header_value.is_a? Array
66
- memo += header_value
67
- else
68
- memo << header_value
69
- end
70
- end
71
-
72
- memo
73
- end.flatten
61
+ [:received, :x_received].include?(header_name) ? memo + header_value : memo
62
+ end
74
63
 
75
64
  {
76
65
  received: restore_sequence(received_header_values).map { |v| parse_received_header(v[:data]) }
@@ -78,11 +67,15 @@ module PhisherPhinder
78
67
  end
79
68
 
80
69
  def parse_received_header(value)
70
+ starttls_parser = MailParser::ReceivedHeaders::StarttlsParser.new
81
71
  parser = MailParser::ReceivedHeaders::Parser.new(
82
- by_parser: MailParser::ReceivedHeaders::ByParser.new(@enriched_ip_factory),
83
- for_parser: MailParser::ReceivedHeaders::ForParser.new,
84
- from_parser: MailParser::ReceivedHeaders::FromParser.new(@enriched_ip_factory),
85
- starttls_parser: MailParser::ReceivedHeaders::StarttlsParser.new,
72
+ by_parser: MailParser::ReceivedHeaders::ByParser.new(
73
+ ip_factory: @enriched_ip_factory, starttls_parser: starttls_parser
74
+ ),
75
+ for_parser: MailParser::ReceivedHeaders::ForParser.new(starttls_parser: starttls_parser),
76
+ from_parser: MailParser::ReceivedHeaders::FromParser.new(
77
+ ip_factory: @enriched_ip_factory, starttls_parser: starttls_parser
78
+ ),
86
79
  timestamp_parser: MailParser::ReceivedHeaders::TimestampParser.new,
87
80
  classifier: MailParser::ReceivedHeaders::Classifier.new
88
81
  )
@@ -94,18 +87,38 @@ module PhisherPhinder
94
87
  end
95
88
 
96
89
  def parse_body(original_body, headers)
97
- MailParser::BodyParser.new(@line_end).parse(
90
+ MailParser::BodyParser.new(@line_ending).parse(
98
91
  body_contents: original_body,
99
- content_type: headers.dig(:content_type, :data),
100
- content_transfer_encoding: headers.dig(:content_transfer_encoding, :data),
92
+ content_type: content_type_data(headers),
93
+ content_transfer_encoding: content_transfer_encoding_data(headers),
101
94
  )
102
95
  end
103
96
 
104
97
  def valid_base64_decoded(text)
105
- if Base64.strict_encode64(Base64.decode64(text)) == text.gsub(/#{@line_end}/, '')
98
+ if Base64.strict_encode64(Base64.decode64(text)) == text.gsub(/#{@line_ending}/, '')
106
99
  Base64.decode64(text)
107
100
  end
108
101
  end
102
+
103
+ def content_type_data(headers)
104
+ (headers[:content_type] && headers[:content_type].first[:data]) || nil
105
+ end
106
+
107
+ def content_transfer_encoding_data(headers)
108
+ (headers[:content_transfer_encoding] && headers[:content_transfer_encoding].first[:data]) || nil
109
+ end
110
+
111
+ def generate_authentication_headers(headers)
112
+ auth_parser = MailParser::AuthenticationHeaders::Parser.new(
113
+ authentication_results_parser: MailParser::AuthenticationHeaders::AuthResultsParser.new(
114
+ ip_factory: @enriched_ip_factory
115
+ ),
116
+ received_spf_parser: MailParser::AuthenticationHeaders::ReceivedSpfParser.new(
117
+ ip_factory: @enriched_ip_factory
118
+ ),
119
+ )
120
+ auth_parser.parse(headers)
121
+ end
109
122
  end
110
123
  end
111
124
  end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module AuthenticationHeaders
6
+ class AuthResultsParser
7
+ def initialize(ip_factory:)
8
+ @ip_factory = ip_factory
9
+ end
10
+
11
+ def parse(value)
12
+ output_template = {authserv_id: nil, auth: [], dkim: [], dmarc: [], iprev: [], spf: []}
13
+
14
+ components(value).inject(output_template) do |output, component|
15
+ component_type, parsed_component = parse_component(component)
16
+ if output[component_type].respond_to?(:<<)
17
+ output.merge(component_type => (output[component_type] << parsed_component))
18
+ else
19
+ output.merge(component_type => parsed_component)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def components(value)
27
+ value.split(';').map { |component| component.strip }
28
+ end
29
+
30
+ def parse_component(component)
31
+ if component =~ /\Aspf=/
32
+ [:spf, spf_data(component)]
33
+ elsif component =~ /\Adkim=/
34
+ [:dkim, dkim_data(component)]
35
+ elsif component =~ /\Aiprev=/
36
+ [:iprev, iprev_data(component)]
37
+ elsif component =~ /\Aauth=/
38
+ [:auth, auth_data(component)]
39
+ elsif component =~ /\Admarc=/
40
+ [:dmarc, {}]
41
+ else
42
+ [:authserv_id, component]
43
+ end
44
+ end
45
+
46
+ def spf_data(value)
47
+ patterns = [
48
+ %r{
49
+ spf=(?<result>[\S]+)\s
50
+ \([^:]+:\s(?<ip>[\S]+)\sis\sneither\spermitted[^\)]+\)\s
51
+ smtp.mailfrom=(?<from>[^\s;]+)
52
+ }x,
53
+ %r{
54
+ spf=(?<result>[\S]+)\s
55
+ \(.+\s(?<ip>[\S]+)\sas\spermitted.+\)\s
56
+ smtp.mailfrom=(?<from>[^\s;]+)
57
+ }x,
58
+ %r{
59
+ spf=(?<result>[\S]+)\ssmtp.mailfrom=(?<from>[^\s;]+)
60
+ }x,
61
+ ]
62
+
63
+ matches = patterns.inject(nil) do |memo, pattern|
64
+ memo || value.match(pattern)
65
+ end
66
+
67
+ if matches
68
+ {
69
+ result: matches[:result].to_sym,
70
+ ip: matches.names.include?('ip') ? @ip_factory.build(matches[:ip]) : nil,
71
+ from: matches[:from]
72
+ }
73
+ else
74
+ {}
75
+ end
76
+ end
77
+
78
+ def dkim_data(value)
79
+ patterns = [
80
+ %r{
81
+ dkim=(?<result>[\S]+)\s.*?
82
+ header.i=(?<identity>[\S]+)\s
83
+ header.s=(?<selector>[\S]+)\s
84
+ header.b=(?<hash_snippet>.{8})
85
+ }x,
86
+ %r{
87
+ dkim=(?<result>[\S]+)\s.*?
88
+ header.i=(?<identity>[\S]+)\s
89
+ header.s=(?<selector>[\S]+)
90
+ }x,
91
+ ]
92
+
93
+ matches = patterns.inject(nil) do |memo, pattern|
94
+ memo || value.match(pattern)
95
+ end
96
+
97
+ if matches
98
+ {
99
+ result: matches[:result].to_sym,
100
+ identity: matches[:identity],
101
+ selector: matches[:selector],
102
+ hash_snippet: extract(matches, :hash_snippet)
103
+ }
104
+ else
105
+ {}
106
+ end
107
+ end
108
+
109
+ def iprev_data(value)
110
+ matches = value.match(
111
+ /
112
+ iprev=(?<result>[\S]+)\s
113
+ \((?<remote_host_name>[^\)]+)\)\s
114
+ smtp.remote-ip=(?<remote_ip>[^\s;]+)
115
+ /x
116
+ )
117
+
118
+ if matches
119
+ {
120
+ result: matches[:result].to_sym,
121
+ remote_host_name: matches[:remote_host_name],
122
+ remote_ip: @ip_factory.build(matches[:remote_ip]),
123
+ }
124
+ else
125
+ {}
126
+ end
127
+ end
128
+
129
+ def auth_data(value)
130
+ matches = value.match(/auth=(?<result>[\S]+)\s.+smtp.auth=(?<domain>[^\s;]+)/)
131
+
132
+ if matches
133
+ {
134
+ result: matches[:result].to_sym,
135
+ domain: matches[:domain],
136
+ }
137
+ else
138
+ {}
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def extract(matches, key)
145
+ matches.names.include?(key.to_s) ? matches[key] : nil
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module AuthenticationHeaders
6
+ class Parser
7
+ def initialize(authentication_results_parser:, received_spf_parser:)
8
+ @authentication_results_parser = authentication_results_parser
9
+ @received_spf_parser = received_spf_parser
10
+ end
11
+
12
+ def parse(headers)
13
+ {
14
+ authentication_results: (headers[:authentication_results] || []).map do |header|
15
+ @authentication_results_parser.parse(header[:data])
16
+ end,
17
+ received_spf: (headers[:received_spf] || []).map do |header|
18
+ @received_spf_parser.parse(header[:data])
19
+ end
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module AuthenticationHeaders
6
+ class ReceivedSpfParser
7
+ def initialize(ip_factory:)
8
+ @ip_factory = ip_factory
9
+ end
10
+
11
+ def parse(value)
12
+ matches = value.match(
13
+ %r{
14
+ \A(?<result>\S+)\s
15
+ \((?<additional_data>[^\)]+)\)
16
+ (?<attributes>.*)
17
+ }x
18
+ )
19
+
20
+ {
21
+ result: matches[:result].downcase.to_sym,
22
+ }.merge(parse_additional_data(matches[:additional_data])).merge(parse_attributes(matches[:attributes]))
23
+
24
+ # {
25
+ # result: matches[:result].downcase.to_sym,
26
+ # authserv_id: extract(matches, :authserv_id),
27
+ # mailfrom: extract(matches, :mailfrom),
28
+ # ip: @ip_factory.build(extract(matches, :ip)),
29
+ # client_ip: expand_ip(extract(matches, :client_ip)),
30
+ # receiver: extract(matches, :receiver),
31
+ # helo: expand_ip(extract(matches, :helo)),
32
+ # envelope_from: extract(matches, :envelope_from)
33
+ # }
34
+ end
35
+
36
+ # def parse(value)
37
+ # patterns = [
38
+ # /\A(?<result>\S+)\s\(domain\sof\s(?<mailfrom>\S+)\sdesignates\s(?<ip>\S+)\sas\spermitted\ssender\)/,
39
+ # /\A(?<result>\S+)\s\(domain\sof\s(?<mailfrom>\S+)\sdoes\snot\sdesignate\spermitted\ssender\shosts\)/,
40
+ # %r{
41
+ # \A(?<result>\S+)\s\(mailfrom\)\s
42
+ # identity=(?<identity>[^;]+);\s
43
+ # client-ip=(?<client_ip>[^;]+);\s
44
+ # helo=(?<helo>[^;]+);\s
45
+ # envelope-from=(?<envelope_from>[^;]+);\s
46
+ # receiver=(?<receiver>[^;]+)
47
+ # }x,
48
+ # %r{
49
+ # \A(?<result>\S+)\s
50
+ # \(
51
+ # (?<authserv_id>[^:]+):\s
52
+ # transitioning\sdomain\sof\s(?<mailfrom>\S+)\s
53
+ # .+?
54
+ # \s(?<ip>\S+)\sas\spermitted\ssender
55
+ # \)\s
56
+ # client-ip=(?<client_ip>[^;]+);\s
57
+ # envelope-from=(?<envelope_from>[^;]+);\s
58
+ # helo=\[?(?<helo>[^;]+?)\]?;
59
+ # }x,
60
+ # %r{
61
+ # \A(?<result>\S+)\s
62
+ # \(
63
+ # (?<authserv_id>[^:]+):\s
64
+ # best\sguess\srecord\sfor\sdomain\sof\s(?<mailfrom>\S+)\s
65
+ # .+?
66
+ # \s(?<ip>\S+)\sas\spermitted\ssender
67
+ # \)\s
68
+ # client-ip=(?<client_ip>[^;]+);
69
+ # }x,
70
+ # %r{
71
+ # \A(?<result>\S+)\s
72
+ # \(
73
+ # (?<authserv_id>[^:]+):\s
74
+ # domain\sof\stransitioning\s(?<mailfrom>\S+)\s
75
+ # .+?
76
+ # \s(?<ip>\S+)\sas\spermitted\ssender
77
+ # \)\s
78
+ # client-ip=(?<client_ip>[^;]+);
79
+ # }x,
80
+ # %r{
81
+ # \A(?<result>\S+)\s
82
+ # \(
83
+ # (?<authserv_id>[^:]+):\s
84
+ # domain\sof\s(?<mailfrom>\S+)\s
85
+ # .+?
86
+ # \s(?<ip>\S+)\sas\spermitted\ssender
87
+ # \)\s
88
+ # receiver=(?<receiver>[^;]+);\s
89
+ # client-ip=(?<client_ip>[^;]+);\s
90
+ # helo=\[?(?<helo>[^;]+?)\]?;
91
+ # }x,
92
+ # %r{
93
+ # \A(?<result>\S+)\s
94
+ # \(
95
+ # (?<authserv_id>[^:]+):\s
96
+ # domain\sof\s(?<mailfrom>\S+)\s
97
+ # .+?
98
+ # \s(?<ip>\S+)\sas\spermitted\ssender
99
+ # \)\s
100
+ # client-ip=(?<client_ip>[^;]+);
101
+ # }x,
102
+ # %r{
103
+ # \A(?<result>\S+)\s
104
+ # \(
105
+ # (?<authserv_id>[^:]+):\s
106
+ # (?<ip>\S+)\sis\sneither\s
107
+ # .+?
108
+ # domain\sof\s(?<mailfrom>\S+)
109
+ # \)\s
110
+ # client-ip=(?<client_ip>[^;]+);
111
+ # }x
112
+ # ]
113
+ #
114
+ # matches = patterns.inject(nil) do |memo, pattern|
115
+ # memo || value.match(pattern)
116
+ # end
117
+ #
118
+ # if matches
119
+ # {
120
+ # result: matches[:result].downcase.to_sym,
121
+ # authserv_id: extract(matches, :authserv_id),
122
+ # mailfrom: extract(matches, :mailfrom),
123
+ # ip: @ip_factory.build(extract(matches, :ip)),
124
+ # client_ip: expand_ip(extract(matches, :client_ip)),
125
+ # receiver: extract(matches, :receiver),
126
+ # helo: expand_ip(extract(matches, :helo)),
127
+ # envelope_from: extract(matches, :envelope_from)
128
+ # }
129
+ # end
130
+ # end
131
+
132
+ private
133
+
134
+ def parse_additional_data(data)
135
+ patterns = [
136
+ %r{
137
+ (?<authserv_id>[^:]+):\s
138
+ best\sguess\srecord\sfor\sdomain\sof\s(?<mailfrom>\S+)\s
139
+ .+?
140
+ \s(?<ip>\S+)\sas\spermitted\ssender
141
+ }x,
142
+ /domain\sof\s(?<mailfrom>\S+)\sdesignates\s(?<ip>\S+)\sas\spermitted\ssender/,
143
+ /domain\sof\s(?<mailfrom>\S+)\sdoes\snot\sdesignate\spermitted\ssender\shosts/,
144
+ %r{
145
+ (?<authserv_id>[^:]+):\s
146
+ transitioning\sdomain\sof\s(?<mailfrom>\S+)\s
147
+ .+?
148
+ \s(?<ip>\S+)\sas\spermitted\ssender
149
+ }x,
150
+ %r{
151
+ (?<authserv_id>[^:]+):\s
152
+ domain\sof\stransitioning\s(?<mailfrom>\S+)\s
153
+ .+?
154
+ \s(?<ip>\S+)\sas\spermitted\ssender
155
+ }x,
156
+ %r{
157
+ (?<authserv_id>[^:]+):\s
158
+ domain\sof\s(?<mailfrom>\S+)\s
159
+ .+?
160
+ \s(?<ip>\S+)\sas\spermitted\ssender
161
+ }x,
162
+ %r{
163
+ (?<authserv_id>[^:]+):\s
164
+ (?<ip>\S+)\sis\sneither\s
165
+ .+?
166
+ domain\sof\s(?<mailfrom>\S+)
167
+ }x
168
+ ]
169
+
170
+ matches = patterns.inject(nil) do |memo, pattern|
171
+ memo || data.match(pattern)
172
+ end
173
+
174
+ if matches
175
+ {
176
+ authserv_id: extract(matches, :authserv_id),
177
+ mailfrom: extract(matches, :mailfrom),
178
+ ip: @ip_factory.build(extract(matches, :ip)),
179
+ # client_ip: expand_ip(extract(matches, :client_ip)),
180
+ # receiver: extract(matches, :receiver),
181
+ # helo: expand_ip(extract(matches, :helo)),
182
+ # envelope_from: extract(matches, :envelope_from)
183
+ }
184
+ else
185
+ {
186
+ authserv_id: nil,
187
+ mailfrom: nil,
188
+ ip: nil,
189
+ }
190
+ end
191
+ end
192
+
193
+ def parse_attributes(attribute_data)
194
+ output_template = {
195
+ client_ip: nil, receiver: nil, helo: nil, envelope_from: nil, identity: nil
196
+ }
197
+ attribute_data.scan(/[^=]+=\S+/).inject(output_template) do |memo, attr_string|
198
+ attribute, value = parse_attr_value(attr_string)
199
+ if ['client_ip', 'helo'].include?(attribute)
200
+ memo.merge(attribute.to_sym => expand_ip(value))
201
+ else
202
+ memo.merge(attribute.to_sym => value)
203
+ end
204
+ end
205
+ end
206
+
207
+ def parse_attr_value(attr_value_string)
208
+ attribute, value = attr_value_string.strip.gsub(/[;\[\]]/, '').split('=')
209
+ [attribute.gsub(/-/, '_'), value]
210
+ end
211
+
212
+ def extract(matches, key)
213
+ matches.names.include?(key.to_s) ? matches[key] : nil
214
+ end
215
+
216
+ def expand_ip(ip)
217
+ ip ? (@ip_factory.build(ip) || ip) : nil
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end