Class: SPF::MacroString

Inherits:
Object
  • Object
show all
Defined in:
lib/spf/macro_string.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ MacroString

Returns a new instance of MacroString.



22
23
24
25
26
27
28
29
30
# File 'lib/spf/macro_string.rb', line 22

def initialize(options = {})
  super()
  @text     = options[:text] \
    or raise ArgumentError, "Missing required 'text' option"
  @server   = options[:server]
  @request  = options[:request]
  @is_explanation = options[:is_explanation]
  @expanded = nil
end

Instance Attribute Details

#requestObject (readonly)

Returns the value of attribute request.



32
33
34
# File 'lib/spf/macro_string.rb', line 32

def request
  @request
end

#serverObject (readonly)

Returns the value of attribute server.



32
33
34
# File 'lib/spf/macro_string.rb', line 32

def server
  @server
end

#textObject (readonly)

Returns the value of attribute text.



32
33
34
# File 'lib/spf/macro_string.rb', line 32

def text
  @text
end

Class Method Details

.default_join_delimiterObject



14
15
16
# File 'lib/spf/macro_string.rb', line 14

def self.default_join_delimiter
  '.'
end

.default_split_delimitersObject



10
11
12
# File 'lib/spf/macro_string.rb', line 10

def self.default_split_delimiters
  '.'
end

.uri_unreserved_charsObject



18
19
20
# File 'lib/spf/macro_string.rb', line 18

def self.uri_unreserved_chars
  'A-Za-z0-9\-._~'
end

Instance Method Details

#context(server, request) ⇒ Object



34
35
36
37
38
39
40
# File 'lib/spf/macro_string.rb', line 34

def context(server, request)
  valid_context(true, server, request)
  @server   = server
  @request  = request
  @expanded = nil
  return
end

#expand(context = nil) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/spf/macro_string.rb', line 42

def expand(context = nil)
  return @expanded if @expanded

  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 = ''

  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

#to_sObject



169
170
171
172
173
174
175
# File 'lib/spf/macro_string.rb', line 169

def to_s
  if valid_context(false)
    return expand
  else
    return @text
  end
end

#valid_context(required, server = self.server, request = self.request) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
# File 'lib/spf/macro_string.rb', line 177

def valid_context(required, server = self.server, request = self.request)
  if not SPF::Server === server
    raise SPF::MacroExpansionCtxRequiredError.new('SPF server object required') if required
    return false
  end
  if not SPF::Request === request
    raise SPF::MacroExpansionCtxRequiredError.new('SPF request object required') if required
    return false
  end
  return true
end