phisher_phinder 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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