1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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
|
require_relative '../hostname/validator'
module KDL
module Types
class Email < Value
class Parser
def initialize(string, idn: false)
@string = string
@idn = idn
@tokenizer = Tokenizer.new(string, idn: idn)
end
def parse
local = ''
unicode_domain = nil
domain = nil
context = :start
loop do
type, value = @tokenizer.next_token
case type
when :part
case context
when :start, :after_dot
local += value
context = :after_part
else
raise ArgumentError, "invalid email #{@string} (unexpected part #{value} at #{context})"
end
when :dot
case context
when :after_part
local += value
context = :after_dot
else
raise ArgumentError, "invalid email #{@string} (unexpected dot at #{context})"
end
when :at
case context
when :after_part
context = :after_at
end
when :domain
case context
when :after_at
validator = (@idn ? IDNHostname : Hostname)::Validator.new(value)
raise ArgumentError, "invalid hostname #{value}" unless validator.valid?
unicode_domain = validator.unicode
domain = validator.ascii
context = :after_domain
else
raise ArgumentError, "invalid email #{@string} (unexpected domain at #{context})"
end
when :end
case context
when :after_domain
if local.size > 64
raise ArgumentError, "invalid email #{@string} (local part length #{local.size} exceeds maximaum of 64)"
end
return [local, domain, unicode_domain]
else
raise ArgumentError, "invalid email #{@string} (unexpected end at #{context})"
end
end
end
end
end
class Tokenizer
LOCAL_PART_ASCII = %r{[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]}.freeze
LOCAL_PART_IDN = /[^\x00-\x1f\s".@]/.freeze
def initialize(string, idn: false)
@string = string
@idn = idn
@index = 0
@after_at = false
end
def next_token
if @after_at
if @index < @string.size
domain_start = @index
@index = @string.size
return [:domain, @string[domain_start..-1]]
else
return [:end, nil]
end
end
@context = nil
@buffer = ''
loop do
c = @string[@index]
return [:end, nil] if c.nil?
case @context
when nil
case c
when '.'
@index += 1
return [:dot, '.']
when '@'
@after_at = true
@index += 1
return [:at, '@']
when '"'
@context = :quote
@index += 1
when local_part_chars
@context = :part
@buffer += c
@index += 1
else
raise ArgumentError, "invalid email #{@string} (unexpected #{c})"
end
when :part
case c
when local_part_chars
@buffer += c
@index += 1
when '.', '@'
return [:part, @buffer]
else
raise ArgumentError, "invalid email #{@string} (unexpected #{c})"
end
when :quote
case c
when '"'
n = @string[@index + 1]
raise ArgumentError, "invalid email #{@string} (unexpected #{c})" unless n == '.' || n == '@'
@index += 1
return [:part, @buffer]
else
@buffer += c
@index += 1
end
end
end
end
def local_part_chars
@idn ? LOCAL_PART_IDN : LOCAL_PART_ASCII
end
end
end
end
end
|