Class: Sisimai::Address

Inherits:
Object
  • Object
show all
Defined in:
lib/sisimai/address.rb

Overview

Sisimai::Address provide methods for dealing email address.

Constant Summary collapse

Indicators =
{
  :'email-address' => (1 << 0),    # <neko@example.org>
  :'quoted-string' => (1 << 1),    # "Neko, Nyaan"
  :'comment-block' => (1 << 2),    # (neko)
}.freeze
Delimiters =
{'<' => 1, '>' => 1, '(' => 1, ')' => 1, '"' => 1, ',' => 1}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argvs) ⇒ Sisimai::Address

Constructor of Sisimai::Address

Examples:

new(address: ‘neko@example.org’, name: ‘Neko’, comment: ‘(nyaan)’) # => Sisimai::Address object

Parameters:

  • argvs (Hash)

    Email address, name, and other elements



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/sisimai/address.rb', line 372

def initialize(argvs)
  return nil if argvs.is_a?(Hash) == false || argvs[:address].nil? || argvs[:address].empty?

  heads = ['<']
  tails = ['>', ',', '.', ';']
  point = argvs[:address].rindex('@')
  if cv = argvs[:address].match(/\A([^\s]+)[@]([^@]+)\z/) ||
          argvs[:address].match(/\A(["].+?["])[@]([^@]+)\z/)
    # Get the local part and the domain part from the email address
    lpart = argvs[:address][0, point]
    dpart = argvs[:address][point + 1, argvs[:address].size]
    email = Sisimai::Address.expand_verp(argvs[:address])
    aname = nil

    if email.empty?
      # Is not VERP address, try to expand the address as an alias
      email = Sisimai::Address.expand_alias(argvs[:address])
      aname = true if email.empty? == false
    end

    if email.include?('@')
      # The address is a VERP or an alias
      if aname
        # The address is an alias: neko+nyaan@example.jp
        @alias = argvs[:address]
      else
        # The address is a VERP: b+neko=example.jp@example.org
        @verp  = argvs[:address]
      end
    end

    heads.each do |e|
      while lpart[0, 1] == e
        lpart[0, 1] = ''
      end
    end

    tails.each do |e|
      while dpart[-1, 1] == e
        dpart[-1, 1] = ''
      end
    end

    @user    = lpart
    @host    = dpart
    @address = lpart + '@' + dpart
  else
    # The argument does not include "@"
    return nil if Sisimai::Address.is_mailerdaemon(argvs[:address]) == false || argvs[:address].include?(' ')

    # The argument does not include " "
    @user    = argvs[:address]
    @host  ||= ''
    @address = argvs[:address]
  end

  @alias ||= ''
  @verp  ||= ''
  @name    = argvs[:name]    || ''
  @comment = argvs[:comment] || ''
end

Instance Attribute Details

#addressObject (readonly)

:address, # [String] Email address :user, # [String] local part of the email address :host, # [String] domain part of the email address :verp, # [String] VERP :alias, # [String] alias of the email address :name, # [String] Display name :comment, # [String] Comment



365
366
367
# File 'lib/sisimai/address.rb', line 365

def address
  @address
end

#aliasObject (readonly)

:address, # [String] Email address :user, # [String] local part of the email address :host, # [String] domain part of the email address :verp, # [String] VERP :alias, # [String] alias of the email address :name, # [String] Display name :comment, # [String] Comment



365
366
367
# File 'lib/sisimai/address.rb', line 365

def alias
  @alias
end

#commentObject

Returns the value of attribute comment.



366
367
368
# File 'lib/sisimai/address.rb', line 366

def comment
  @comment
end

#hostObject (readonly)

:address, # [String] Email address :user, # [String] local part of the email address :host, # [String] domain part of the email address :verp, # [String] VERP :alias, # [String] alias of the email address :name, # [String] Display name :comment, # [String] Comment



365
366
367
# File 'lib/sisimai/address.rb', line 365

def host
  @host
end

#nameObject

Returns the value of attribute name.



366
367
368
# File 'lib/sisimai/address.rb', line 366

def name
  @name
end

#userObject (readonly)

:address, # [String] Email address :user, # [String] local part of the email address :host, # [String] domain part of the email address :verp, # [String] VERP :alias, # [String] alias of the email address :name, # [String] Display name :comment, # [String] Comment



365
366
367
# File 'lib/sisimai/address.rb', line 365

def user
  @user
end

#verpObject (readonly)

:address, # [String] Email address :user, # [String] local part of the email address :host, # [String] domain part of the email address :verp, # [String] VERP :alias, # [String] alias of the email address :name, # [String] Display name :comment, # [String] Comment



365
366
367
# File 'lib/sisimai/address.rb', line 365

def verp
  @verp
end

Class Method Details

.expand_alias(email) ⇒ String

Expand alias: remove from ‘+’ to ‘@’

Examples:

Expand alias

expand_alias('neko+straycat@example.org') #=> 'neko@example.org'

Parameters:

  • email (String)

    Email alias string

Returns:

  • (String)

    Expanded email address



350
351
352
353
354
355
356
# File 'lib/sisimai/address.rb', line 350

def self.expand_alias(email)
  return "" if Sisimai::Address.is_emailaddress(email) == false

  local = email.split('@')
  return "" unless cv = local[0].match(/\A([-\w]+?)[+].+\z/)
  return cv[1] + '@' + local[1]
end

.expand_verp(email) ⇒ String

Expand VERP: Get the original recipient address from VERP

Examples:

Expand VERP address

expand_verp('bounce+neko=example.org@example.org') #=> 'neko@example.org'

Parameters:

  • email (String)

    VERP Address

Returns:



338
339
340
341
342
343
# File 'lib/sisimai/address.rb', line 338

def self.expand_verp(email)
  return "" unless email.is_a? Object::String
  return "" unless cv = email.split('@', 2).first.match(/\A[-\w]+?[+](\w[-.\w]+\w)[=](\w[-.\w]+\w)\z/)
  verp0 = cv[1] + '@' + cv[2]
  return verp0 if Sisimai::Address.is_emailaddress(verp0)
end

.find(argv1 = nil, addrs = false) ⇒ Object



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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/sisimai/address.rb', line 123

def self.find(argv1 = nil, addrs = false)
  # Email address parser with a name and a comment
  # @param    [String] argv1  String including email address
  # @param    [Boolean] addrs true:  Returns list including all the elements
  #                           false: Returns list including email addresses only
  # @return   [Array, Nil]    Email address list or nil when there is no email address in the argument
  # @example  Parse email address
  #   find('Neko <neko(nyaan)@example.org>')
  #   #=> [{ address: 'neko@example.org', name: 'Neko', comment: '(nyaan)'}]
  return nil if argv1.nil? || argv1.empty?

  emailtable = {address: '', name: '', comment: ''}
  addrtables = []
  readbuffer = []
  readcursor = 0

  v = emailtable  # temporary buffer
  p = ''          # current position

  argv1.delete!("\r") if argv1.include?("\r")
  argv1.delete!("\n") if argv1.include?("\n")
  characters = argv1.split('')

  while e = characters.shift do
    # Check each characters
    if Delimiters[e]
      # The character is a delimiter character
      case e
      when "," # Separator of email addresses or not
        if v[:address].start_with?('<') && v[:address].end_with?('>') && v[:address].include?('@')
          # An email address has already been picked
          if readcursor & Indicators[:'comment-block'] > 0
            # The cursor is in the comment block (Neko, Nyaan)
            v[:comment] += e
          elsif readcursor & Indicators[:'quoted-string'] > 0
            # "Neko, Nyaan"
            v[:name] += e
          else
            # The cursor is not in neither the quoted-string nor the comment block
            readcursor = 0  # reset cursor position
            readbuffer << v
            v = {address: '', name: '', comment: ''}
            p = ''
          end
        else
          # "Neko, Nyaan" <neko@nyaan.example.org> OR <"neko,nyaan"@example.org>
          p.empty? ? (v[:name] += e) : (v[p] += e)
        end
      when "<" # <: The beginning of an email address or not
        if v[:address].size > 0
          p.empty? ? (v[:name] += e) : (v[p] += e)
        else
          # <neko@nyaan.example.org>
          readcursor |= Indicators[:'email-address']
          v[:address] += e
          p = :address
        end
      when ">" # >: The end of an email address or not
        if readcursor & Indicators[:'email-address'] > 0
          # <neko@example.org>
          readcursor &= ~Indicators[:'email-address']
          v[:address] += e
          p = ''
        else
          # a comment block or a display name
          p.empty? ? (v[:name] == e) : (v[:comment] -= e)
        end
      when "(" # The beginning of a comment block or not
        if readcursor & Indicators[:'email-address'] > 0
          # <"neko(nyaan)"@example.org> or <neko(nyaan)@example.org>
          if v[:address].include?('"')
            # Quoted local part: <"neko(nyaan)"@example.org>
            v[:address] += e
          else
            # Comment: <neko(nyaan)@example.org>
            readcursor |= Indicators[:'comment-block']
            v[:comment] += ' ' if v[:comment].end_with?(')')
            v[:comment] += e
            p = :comment
          end
        elsif readcursor & Indicators[:'comment-block'] > 0
          # Comment at the outside of an email address (...(...)
          v[:comment] += ' ' if v[:comment].end_with?(')')
          v[:comment] += e

        elsif readcursor & Indicators[:'quoted-string'] > 0
          # "Neko, Nyaan(cat)", Deal as a display name
          v[:name] += e
        else
          # The beginning of a comment block
          readcursor |= Indicators[:'comment-block']
          v[:comment] += ' ' if v[:comment].end_with?(')')
          v[:comment] += e
          p = :comment
        end
      when ")" # The end of a comment block or not
        if readcursor & Indicators[:'email-address'] > 0
          # <"neko(nyaan)"@example.org> OR <neko(nyaan)@example.org>
          if v[:address].include?('"')
            # Quoted string in the local part: <"neko(nyaan)"@example.org>
            v[:address] += e
          else
            # Comment: <neko(nyaan)@example.org>
            readcursor &= ~Indicators[:'comment-block']
            v[:comment] += e
            p = :address
          end
        elsif readcursor & Indicators[:'comment-block'] > 0
          # Comment at the outside of an email address (...(...)
          readcursor &= ~Indicators[:'comment-block']
          v[:comment] += e
          p = ''
        else
          # Deal as a display name
          readcursor &= ~Indicators[:'comment-block']
          v[:name] += e
          p = ''
        end
      when '"' # The beginning or the end of a quoted-string
        if p.size > 0
          # email-address or comment-block
          v[p] += e
        else
          # Display name like "Neko, Nyaan"
          v[:name] += e
          next if readcursor & Indicators[:'quoted-string'] == 0
          next if v[:name].end_with?(%Q|\x5c"|) # "Neko, Nyaan \"...
          readcursor &= ~Indicators[:'quoted-string']
          p = ''
        end
      end # End of case-when
    else
      # The character is not a delimiter
      p.empty? ? (v[:name] += e) : (v[p] += e)
    end
  end

  if v[:address].size > 0
    # Push the latest values
    readbuffer << v
  else
    # No email address like <neko@example.org> in the argument
    if cv = v[:name].match(/(?>(?:([^\s]+|["].+?["]))[@](?:([^@\s]+|[0-9A-Za-z:\.]+)))/)
      # String like an email address will be set to the value of "address"
      v[:address] = cv[1] + '@' + cv[2]

    elsif Sisimai::Address.is_mailerdaemon(v[:name])
      # Allow if the argument is MAILER-DAEMON
      v[:address] = v[:name]
    end

    if v[:address].empty? == false
      # Remove the comment from the address
      if Sisimai::String.aligned(v[:address], ['(', ')'])
        # (nyaan)nekochan@example.org, nekochan(nyaan)cat@example.org or nekochan(nyaan)@example.org
        p1 = v[:address].index('(')
        p2 = v[:address].index(')')
        v[:address] = v[:address][0, p1] + v[:address][p2 + 1, v[:address].size]
        v[:comment] = v[:address][p1, p2 - p1 - 1]
      end
      readbuffer << v
    end
  end

  while e = readbuffer.shift do
    # The element must not include any character except from 0x20 to 0x7e.
    next if e[:address] =~ /[^\x20-\x7e]/
    if e[:address].include?('@') == false
      # Allow if the argument is MAILER-DAEMON
      next if Sisimai::Address.is_mailerdaemon(e[:address]) == false
    end

    # Remove angle brackets, other brackets, and quotations: []<>{}'` except a domain part is
    # an IP address like neko@[192.0.2.222]
    e[:address] = e[:address].gsub(/\A[\[<{('`]/, '').gsub(/[.,'`>});]\z/, '')
    e[:address] = e[:address].gsub(/[^A-Za-z]\z/, '') if e[:address].include?('@[') == false
    e[:address] = e[:address].delete_prefix('"').delete_suffix('"') if is_quotedaddress(e[:address]) == false

    if addrs
      # Almost compatible with parse() method, returns email address only
      e.delete(:name)
      e.delete(:comment)
    else
      # Remove double-quotations, trailing spaces.
      [:name, :comment].each { |f| e[f] = e[f].strip }
      e[:comment] = ''              unless e[:comment] =~ /\A[(].+[)]/
      e[:name].squeeze!(' ')        unless e[:name]    =~ /\A["].+["]\z/
      e[:name].delete_prefix!('"')  unless is_quotedaddress(e[:name])
      e[:name].delete_suffix!('"')
    end
    addrtables << e
  end

  return nil if addrtables.empty?
  return addrtables
end

.is_emailaddress(email) ⇒ Boolean

Check that the argument is an email address or not

Parameters:

  • email (String)

    Email address string

Returns:

  • (Boolean)

    true: is an email address, false: is not an email address



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
# File 'lib/sisimai/address.rb', line 22

def self.is_emailaddress(email)
  return false if email.is_a?(::String) == false || email.size < 5;

  email = email.strip
  width = email.size
  lasta = email.rindex('@') || -1

  return false if width > 254;            # The maximum length of an email address is 254
  return false if lasta < 1 || lasta > 64 # The maximum length of a local part is 64
  return false if width - lasta > 253     # The maximum length of a domain part is 252

  quote = is_quotedaddress(email); if quote == false
    # The email address is not a quoted address
    ap = email.index('@') || -1
    return false if ap != lasta             # There are 2 or more '@'.
    return false if email.index(' ') != nil # There is 1 or more ' '.
  end

  ipv46 = Sisimai::RFC1123.is_domainliteral(email)

  j = -1; email.split('').each do |e|
    # 31 < The ASCII code of each character < 127
    p = e.ord; j += 1

    if j < lasta
      # The email address has quoted local part like "neko@cat"@example.org
      return false if p < 32 || p > 126 # Before ' ' || After '~'
      next         if j == 0            # The character is the first character

      if quote
        # The email address has quoted local part like "neko@cat"@example.org
        jp = email[j - 1, 1]
        if jp.ord == 92 # 92 = '\'
          # When the previous character IS '\', only the followings are allowed: '\', '"'
          return false if p != 92 && p != 34
        else
          # When the previous character IS NOT '\'
          return false if p == 34 && j + 1 < lasta
        end
      else
        # The local part is not quoted
        # ".." is not allowed in a local part when the local part is not quoted by "" but
        # Non-RFC compliant email addresses still persist in the world.
        #
        # The following characters are not allowed in a local part without "..."@example.jp
        return false if e == ',' || e == '@' || e == ':' || e == ';' || e == '('
        return false if e == ')' || e == '<' || e == '>' || e == '[' || e == ']'
      end
    else
      # A domain part of the email address: string after the last "@"
      next         if p == 64   # '@'
      return false if p <   45  # Before '-'
      return false if p ==  47  # Equals '/'
      return false if p ==  92  # Equals '\'
      return false if p >  122  # After  'z'

      if ipv46 == false
        # Such as "example.jp", "neko.example.org"
        return false if p > 57 && p < 64  # ':' to '?'
        return false if p > 90 && p < 97  # '[' to '`'
      else
        # Such as "[IPv4:192.0.2.25]"
        return false if p > 59 && p < 64  # ';' to '?'
        return false if p > 93 && p < 97  # '^' to '`'
      end
    end
  end
  return true if ipv46

  # Check that the domain part is a valid internet host or not
  return Sisimai::RFC1123.is_internethost(email[lasta + 1, 255])
end

.is_mailerdaemon(argv0 = nil) ⇒ True, False

Check that the argument is mailer-daemon or not

Parameters:

  • argv0 (String) (defaults to: nil)

    Email address

Returns:

  • (True, False)

    true: mailer-daemon false: Not mailer-daemon



109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/sisimai/address.rb', line 109

def self.is_mailerdaemon(argv0 = nil)
  return false if argv0.to_s == "" || argv0.is_a?(::String) == false

  email = argv0.downcase
  postmaster = [
    'mailer-daemon@', '<mailer-daemon>', '(mailer-daemon)', ' mailer-daemon ',
    'postmaster@', '<postmaster>', '(postmaster)'
  ].freeze

  return true if postmaster.any? { |a| email.include?(a) }
  return true if email == 'mailer-daemon' || email == 'postmaster'
  return false
end

.is_quotedaddress(argv0) ⇒ True, False

Checks that the local part of the argument is quoted address or not.

Parameters:

  • argv0 (String)

    Email address

Returns:

  • (True, False)

    false: is not a quoted address true: is a quoted address



99
100
101
102
103
# File 'lib/sisimai/address.rb', line 99

def self.is_quotedaddress(argv0)
  return false if argv0.is_a?(::String) == false
  return true  if argv0.start_with?('"') && argv0.include?('"@')
  return false
end

.s3s4(input) ⇒ String

Runs like ruleset 3,4 of sendmail.cf

Examples:

s3s4(‘<neko@example.cat>’) #=> ‘neko@example.cat’

Parameters:

  • input (String)

    Text including an email address

Returns:

  • (String)

    Email address without comment, brackets



324
325
326
327
328
329
330
331
# File 'lib/sisimai/address.rb', line 324

def self.s3s4(input)
  return "" if input.to_s == ""
  return "" if input.is_a?(Object::String) == false

  addrs = Sisimai::Address.find(input, true) || []
  return input if addrs.empty?
  return addrs[0][:address]
end

.undisclosed(argv0 = false) ⇒ String?

Return pseudo recipient or sender address

Parameters:

  • argv0 (Symbol) (defaults to: false)

    Address type: true = recipient, false = sender

Returns:

  • (String, nil)

    Pseudo recipient address or sender address or nil when the argv1 is neither :r nor :s



15
16
17
# File 'lib/sisimai/address.rb', line 15

def self.undisclosed(argv0 = false)
  return sprintf('undisclosed-%s-in-headers@libsisimai.org.invalid', argv0 ? 'recipient' : 'sender')
end

Instance Method Details

#to_jsonString

Returns the value of address as String

Returns:



443
# File 'lib/sisimai/address.rb', line 443

def to_json(*); return self.address.to_s; end

#to_sString

Returns the value of address as String

Returns:



447
# File 'lib/sisimai/address.rb', line 447

def to_s; return self.address.to_s; end

#voidTrue, False

Check whether the object has valid content or not

Returns:

  • (True, False)

    returns true if the object is void



436
437
438
439
# File 'lib/sisimai/address.rb', line 436

def void
  return true unless @address
  return false
end