lib/spf/macro_string.rb in spf-0.0.50 vs lib/spf/macro_string.rb in spf-0.0.51
- old
+ new
@@ -1,8 +1,11 @@
# encoding: ASCII-8BIT
require 'spf/util'
+require 'spf/error'
+require 'uri'
+
module SPF
class MacroString
def self.default_split_delimiters
'.'
@@ -20,12 +23,12 @@
super()
@text = options[:text] \
or raise ArgumentError, "Missing required 'text' option"
@server = options[:server]
@request = options[:request]
+ @is_explanation = options[:is_explanation]
@expanded = nil
- self.expand
end
attr_reader :text, :server, :request
def context(server, request)
@@ -41,13 +44,128 @@
return nil unless @text
return (@expanded = @text) unless @text =~ /%/
# Short-circuit expansion if text has no '%' characters.
+ server, request = context ? context : [@server, @request]
+
+ valid_context(true, server, request)
+
expanded = ''
- # TODO
- return (@expanded = @text)
+
+ text = @text
+
+ while m = text.match(/ (.*?) %(.) /x) do
+ expanded += m[1]
+ key = m[2]
+
+ if (key == '{')
+ if m2 = m.post_match.match(/ (\w|_\p{Alpha}+) ([0-9]+)? (r)? ([.\-+,\/_=])? } /x)
+ char, rh_parts, reverse, delimiter = m2.captures
+
+ # Upper-case macro chars trigger URL-escaping AKA percent-encoding
+ # (RFC 4408, 8.1/26):
+ do_percent_encode = char =~ /\p{Upper}/
+ char.downcase!
+
+ if char == 's' # RFC 4408, 8.1/19
+ value = request.identity
+ elsif char == 'l' # RFC 4408, 8.1/19
+ value = request.localpart
+ elsif char == 'o' # RFC 4408, 8.1/19
+ value = request.domain
+ elsif char == 'd' # RFC 4408, 8.1/6/4
+ value = request.authority_domain
+ elsif char == 'i' # RFC 4408, 8.1/20, 8.1/21
+ ip_address = request.ip_address
+ ip_address = SPF::Util.ipv6_address_to_ipv4(ip_address) if SPF::Util.ipv6_address_is_ipv4_mapped(ip_address)
+ if IP::V4 === ip_address
+ value = ip_address.to_addr
+ elsif IP::V6 === ip_address
+ value = ip_address.to_hex.upcase.split('').join('.')
+ else
+ server.throw_result(:permerror, request, "Unexpected IP address version in request")
+ end
+ elsif char == 'p' # RFC 4408, 8.1/22
+ # According to RFC 7208 the "p" macro letter should not be used (or even published).
+ # Here it is left unexpanded and transformers and delimiters are not applied.
+ value = '%{' + m2.to_s
+ rh_parts = nil
+ reverse = nil
+ elsif char == 'v' # RFC 4408, 8.1/6/7
+ if IP::V4 === request.ip_address
+ value = 'in-addr'
+ elsif IP::V6 === request.ip_address
+ value = 'ip6'
+ else
+ # Unexpected IP address version.
+ server.throw_result(:permerror, request, "Unexpected IP address version in request")
+ end
+ elsif char == 'h' # RFC 4408, 8.1/6/8
+ value = request.helo_identity || 'unknown'
+ elsif char == 'c' # RFC 4408, 8.1/20, 8.1/21
+ raise SPF::InvalidMacroStringError.new("Illegal 'c' macro in non-explanation macro string '#{@text}'") unless @is_explanation
+ ip_address = request.ip_address
+ value = SPF::Util::ip_address_to_string(ip_address)
+ elsif char == 'r' # RFC 4408, 8.1/23
+ value = server.hostname || 'unknown'
+ elsif char == 't'
+ raise SPF::InvalidMacroStringError.new("Illegal 't' macro in non-explanation macro string '#{@text}'") unless @is_explanation
+ value = Time.now.to_i.to_s
+ elsif char == '_scope'
+ # Scope pseudo macro for internal use only!
+ value = request.scope.to_s
+ else
+ # Unknown macro character.
+ raise SPF::InvalidMacroStringError.new("Invalid macro character #{char} in macro string '#{@text}'")
+ end
+
+ if rh_parts || reverse
+ delimiter ||= self.class.default_split_delimiters
+ list = value.split(delimiter)
+ list.reverse! if reverse
+ # Extract desired parts:
+ if rh_parts && rh_parts.to_i > 0
+ list = list.last(rh_parts.to_i)
+ end
+ if rh_parts && rh_parts.to_i == 0
+ raise SPF::InvalidMacroStringError.new("Illegal selection of 0 (zero) right-hand parts in macro string '#{@text}'")
+ end
+ value = list.join(self.class.default_join_delimiter)
+ end
+
+ if do_percent_encode
+ unsafe = Regexp.new('^' + self.class.uri_unreserved_chars)
+ value = URI.escape(value, unsafe)
+ end
+
+ expanded += value
+
+ text = m2.post_match
+ else
+ # Invalid macro expression.
+ raise SPF::InvalidMacroStringError.new("Invalid macro expression in macro string '#{@text}'")
+ end
+ elsif key == '-'
+ expanded += '-'
+ text = m.post_match
+ elsif key == '_'
+ expanded += ' '
+ text = m.post_match
+ elsif key == '%'
+ expanded += '%'
+ text = m.post_match
+ else
+ # Invalid macro expression.
+ pos = m.offset(2).first
+ raise SPF::InvalidMacroStringError.new("Invalid macro expression at pos #{pos} in macro string '#{@text}'")
+ end
+ end
+
+ expanded += text # Append remaining unmatched characters.
+
+ context ? expanded : @expanded = expanded
end
def to_s
if valid_context(false)
return expand
@@ -56,19 +174,18 @@
end
end
def valid_context(required, server = self.server, request = self.request)
if not SPF::Server === server
- raise MacroExpansionCtxRequired, 'SPF server object required' if required
+ raise SPF::MacroExpansionCtxRequiredError.new('SPF server object required') if required
return false
end
if not SPF::Request === request
- raise MacroExpansionCtxRequired, 'SPF request object required' if required
+ raise SPF::MacroExpansionCtxRequiredError.new('SPF request object required') if required
return false
end
return true
end
-
end
end
# vim:sw=2 sts=2