Class: EmailAddress::Local

Inherits:
Object
  • Object
show all
Defined in:
lib/email_address/local.rb

Overview

EmailAddress Local part consists of

  • comments

  • mailbox

  • tag


Parsing id provider-dependent, but RFC allows: Chars: A-Z a-z 0-9 . ! # $ % ‘ * + - / = ? ^G _ { | } ~ Quoted: space ( ) , : ; < > @ [ ] Quoted-Backslash-Escaped: \ “ Quote local part or dot-separated sub-parts x.”y“.z RFC-5321 warns ”a host that expects to receive mail SHOULD avoid defining mailboxes

where the Local-part requires (or uses) the Quoted-string form".

(comment)mailbox | mailbox(comment) . can not appear at beginning or end, or appear consecutively 8-bit/UTF-8: allowed but mail-system defined RFC 5321 also warns that “a host that expects to receive mail SHOULD avoid

defining mailboxes where the Local-part requires (or uses) the Quoted-string form".

Postmaster: must always be case-insensitive Case: sensitive, but usually treated as equivalent Local Parts: comment, mailbox tag Length: up to 64 characters Note: gmail does allow “..” against RFC because they are ignored. This will

be fixed by collapsing consecutive punctuation in conventional formats,
and consider them typos.

RFC5322 Rules (Oct 2008):


addr-spec = local-part “@” domain local-part = dot-atom / quoted-string / obs-local-part domain = dot-atom / domain-literal / obs-domain domain-literal = [CFWS] “[” *([FWS] dtext) [FWS] “]” [CFWS] dtext = %d33-90 / ; Printable US-ASCII

%d94-126 /         ;  characters not including
obs-dtext          ;  "[", "]", or "\"

atext = ALPHA / DIGIT / ; Printable US-ASCII

"!" / "#" /        ;  characters not including
"$" / "%" /        ;  specials.  Used for atoms.
"&" / "'" /
"*" / "+" /
"-" / "/" /
"=" / "?" /
"^" / "_" /
"`" / "{" /
"|" / "}" /
"~"

atom = [CFWS] 1*atext [CFWS] dot-atom-text = 1*atext *(“.” 1*atext) dot-atom = [CFWS] dot-atom-text [CFWS] specials = “(” / “)” / ; Special characters that do

"<" / ">" /        ;  not appear in atext
"[" / "]" /
":" / ";" /
"@" / "\" /
"," / "." /
DQUOTE

qtext = %d33 / ; Printable US-ASCII

%d35-91 /          ;  characters not including
%d93-126 /         ;  "\" or the quote character
obs-qtext

qcontent = qtext / quoted-pair quoted-string = [CFWS]

DQUOTE *([FWS] qcontent) [FWS] DQUOTE
[CFWS]

Constant Summary collapse

BUSINESS_MAILBOXES =

RFC-2142: MAILBOX NAMES FOR COMMON SERVICES, ROLES AND FUNCTIONS

%w[info marketing sales support]
NETWORK_MAILBOXES =
%w[abuse noc security]
SERVICE_MAILBOXES =
%w[postmaster hostmaster usenet news webmaster www uucp ftp]
SYSTEM_MAILBOXES =

Not from RFC-2142

%w[help mailer-daemon root]
ROLE_MAILBOXES =

Not from RFC-2142

%w[staff office orders billing careers jobs]
SPECIAL_MAILBOXES =
BUSINESS_MAILBOXES + NETWORK_MAILBOXES + SERVICE_MAILBOXES +
SYSTEM_MAILBOXES + ROLE_MAILBOXES
STANDARD_MAX_SIZE =
64
CONVENTIONAL_MAILBOX_REGEX =

Conventional : word(word)*

/\A [\p{L}\p{N}_]+ (?: [.\-+'_] [\p{L}\p{N}_]+ )* \z/x
CONVENTIONAL_MAILBOX_WITHIN =
/[\p{L}\p{N}_]+ (?: [.\-+'_] [\p{L}\p{N}_]+ )*/x
RELAXED_MAILBOX_WITHIN =

Relaxed: same characters, relaxed order

/[\p{L}\p{N}_]+ (?: [.\-+'_]+ [\p{L}\p{N}_]+ )*/x
RELAXED_MAILBOX_REGEX =
/\A [\p{L}\p{N}_]+ (?: [.\-+'_]+ [\p{L}\p{N}_]+ )* \z/x
STANDARD_LOCAL_WITHIN =

RFC5322 Token: token.“token”.token (dot-separated tokens)

Quoted Token can also have: SPACE \" \\ ( ) , : ; < > @ [ \ ] .
/
      (?: [\p{L}\p{N}!\#$%&'*+\-\/=?\^_`{|}~()]+
        | " (?: \\[" \\] | [\x20-\x21\x23-\x2F\x3A-\x40\x5B\x5D-\x60\x7B-\x7E\p{L}\p{N}] )+ " )
      (?: \.  (?: [\p{L}\p{N}!\#$%&'*+\-\/=?\^_`{|}~()]+
| " (?: \\[" \\] | [\x20-\x21\x23-\x2F\x3A-\x40\x5B\x5D-\x60\x7B-\x7E\p{L}\p{N}] )+ " ) )* /x
STANDARD_LOCAL_REGEX =
/\A #{STANDARD_LOCAL_WITHIN} \z/x
REDACTED_REGEX =

sha1

/\A \{ [0-9a-f]{40} \} \z/x
CONVENTIONAL_TAG_REGEX =

AZaz09_!‘+-/=

%r{^([\w!'+\-/=.]+)$}i
RELAXED_TAG_REGEX =

AZaz09_!#$%&‘*+-/=?^`{|}~

%r/^([\w.!\#$%&'*+\-\/=?\^`{|}~]+)$/i

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(local, config = {}, host = nil, locale = "en") ⇒ Local

Returns a new instance of Local.



109
110
111
112
113
114
115
# File 'lib/email_address/local.rb', line 109

def initialize(local, config = {}, host = nil, locale = "en")
  @config = config.is_a?(Hash) ? Config.new(config) : config
  self.local = local
  @host = host
  @locale = locale
  @error = @error_message = nil
end

Instance Attribute Details

#commentObject

Returns the value of attribute comment.



71
72
73
# File 'lib/email_address/local.rb', line 71

def comment
  @comment
end

#configObject

Returns the value of attribute config.



71
72
73
# File 'lib/email_address/local.rb', line 71

def config
  @config
end

#error_messageObject (readonly)

Returns the value of attribute error_message.



397
398
399
# File 'lib/email_address/local.rb', line 397

def error_message
  @error_message
end

#localObject

Returns the value of attribute local.



70
71
72
# File 'lib/email_address/local.rb', line 70

def local
  @local
end

#localeObject

Returns the value of attribute locale.



72
73
74
# File 'lib/email_address/local.rb', line 72

def locale
  @locale
end

#mailboxObject

Returns the value of attribute mailbox.



71
72
73
# File 'lib/email_address/local.rb', line 71

def mailbox
  @mailbox
end

#originalObject

Returns the value of attribute original.



71
72
73
# File 'lib/email_address/local.rb', line 71

def original
  @original
end

#syntaxObject

Returns the value of attribute syntax.



72
73
74
# File 'lib/email_address/local.rb', line 72

def syntax
  @syntax
end

#tagObject

Returns the value of attribute tag.



71
72
73
# File 'lib/email_address/local.rb', line 71

def tag
  @tag
end

Class Method Details

.redacted?(local) ⇒ Boolean

Returns true if the value matches the Redacted format

Returns:

  • (Boolean)


180
181
182
# File 'lib/email_address/local.rb', line 180

def self.redacted?(local)
  REDACTED_REGEX.match?(local)
end

Instance Method Details

#ascii?Boolean

True if the the value contains only Latin characters (7-bit ASCII)

Returns:

  • (Boolean)


165
166
167
# File 'lib/email_address/local.rb', line 165

def ascii?
  !unicode?
end

#canonicalObject

Returns a canonical form of the address



218
219
220
221
222
223
224
# File 'lib/email_address/local.rb', line 218

def canonical
  if @config[:mailbox_canonical]
    @config[:mailbox_canonical].call(mailbox)
  else
    mailbox.downcase
  end
end

#canonical!Object

Sets the part to be the canonical form



251
252
253
# File 'lib/email_address/local.rb', line 251

def canonical!
  self.local = canonical
end

#conventionalObject

Returns a conventional form of the address



209
210
211
212
213
214
215
# File 'lib/email_address/local.rb', line 209

def conventional
  if tag
    [mailbox, tag].join(@config[:tag_separator])
  else
    mailbox
  end
end

#conventional!Object

Sets the part to be the conventional form



246
247
248
# File 'lib/email_address/local.rb', line 246

def conventional!
  self.local = conventional
end

#conventional?Boolean

True if the part matches the conventional format

Returns:

  • (Boolean)


334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/email_address/local.rb', line 334

def conventional?
  self.syntax = :invalid
  if tag
    return false unless mailbox =~ CONVENTIONAL_MAILBOX_REGEX &&
      tag =~ CONVENTIONAL_TAG_REGEX
  else
    return false unless CONVENTIONAL_MAILBOX_REGEX.match?(local)
  end
  valid_size? or return false
  valid_encoding? or return false
  self.syntax = :conventional
  true
end

#errorObject



399
400
401
# File 'lib/email_address/local.rb', line 399

def error
  valid? ? nil : (@error || :local_invalid)
end

#format(form = @config[:local_format] || :conventional) ⇒ Object

Builds the local string according to configurations



194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/email_address/local.rb', line 194

def format(form = @config[:local_format] || :conventional)
  if @config[:local_format].is_a?(Proc)
    @config[:local_format].call(self)
  elsif form == :conventional
    conventional
  elsif form == :canonical
    canonical
  elsif form == :relaxed
    relax
  elsif form == :standard
    standard
  end
end

#format?Boolean

Returns the format of the address

Returns:

  • (Boolean)


296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/email_address/local.rb', line 296

def format?
  # if :custom
  if conventional?
    :conventional
  elsif relaxed?
    :relax
  elsif redacted?
    :redacted
  elsif standard?
    :standard
  else
    :invalid
  end
end

#matches?(*rules) ⇒ Boolean

Matches configured formated form against File glob strings given. Rules must end in @ to distinguish themselves from other email part matches.

Returns:

  • (Boolean)


381
382
383
384
385
386
387
388
# File 'lib/email_address/local.rb', line 381

def matches?(*rules)
  rules.flatten.each do |r|
    if r =~ /(.+)@\z/
      return r if File.fnmatch?($1, local)
    end
  end
  false
end

#mungeObject

Returns the munged form of the address, like “ma*****”



261
262
263
# File 'lib/email_address/local.rb', line 261

def munge
  to_s.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
end

#parse(raw) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/email_address/local.rb', line 131

def parse(raw)
  if raw =~ /\A"(.*)"\z/ # Quoted
    raw = $1
    raw = raw.gsub(/\\(.)/, '\1') # Unescape
  elsif @config[:local_fix] && @config[:local_format] != :standard
    raw = raw.delete(" ")
    raw = raw.tr(",", ".")
    # raw.gsub!(/([^\p{L}\p{N}]{2,10})/) {|s| s[0] } # Stutter punctuation typo
  end
  raw, comment = parse_comment(raw)
  mailbox, tag = parse_tag(raw)
  mailbox ||= ""
  [mailbox, tag, comment]
end

#parse_comment(raw) ⇒ Object

“(comment)mailbox” or “mailbox(comment)”, only one comment RFC Doesn’t say what to do if 2 comments occur, so last wins



148
149
150
151
152
153
154
155
156
157
# File 'lib/email_address/local.rb', line 148

def parse_comment(raw)
  c = nil
  if raw =~ /\A\((.+?)\)(.+)\z/
    c, raw = [$2, $1]
  end
  if raw =~ /\A(.+)\((.+?)\)\z/
    raw, c = [$1, $2]
  end
  [raw, c]
end

#parse_tag(raw) ⇒ Object



159
160
161
162
# File 'lib/email_address/local.rb', line 159

def parse_tag(raw)
  separator = @config[:tag_separator] ||= "+"
  raw.split(separator, 2)
end

#redacted?Boolean

Returns true if the value matches the Redacted format

Returns:

  • (Boolean)


175
176
177
# File 'lib/email_address/local.rb', line 175

def redacted?
  REDACTED_REGEX.match?(local)
end

#relaxObject

Relaxed format: mailbox and tag, no comment, no extended character set



227
228
229
230
231
# File 'lib/email_address/local.rb', line 227

def relax
  form = mailbox
  form += @config[:tag_separator] + tag if tag
  form.gsub(/[ "(),:<>@\[\]\\]/, "")
end

#relax!Object

Dropps unusual parts of Standard form to form a relaxed version.



256
257
258
# File 'lib/email_address/local.rb', line 256

def relax!
  self.local = relax
end

#relaxed?Boolean

Relaxed conventional is not so strict about character order.

Returns:

  • (Boolean)


349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/email_address/local.rb', line 349

def relaxed?
  self.syntax = :invalid
  valid_size? or return false
  valid_encoding? or return false
  if tag
    return false unless RELAXED_MAILBOX_REGEX.match?(mailbox) &&
      RELAXED_TAG_REGEX.match?(tag)
    self.syntax = :relaxed
    true
  elsif RELAXED_MAILBOX_REGEX.match?(local)
    self.syntax = :relaxed
    true
  else
    false
  end
end

#root_nameObject

Mailbox with trailing numbers removed



266
267
268
# File 'lib/email_address/local.rb', line 266

def root_name
  mailbox =~ /\A(.+?)\d+\z/ ? $1 : mailbox
end

#set_error(err, reason = nil) ⇒ Object



390
391
392
393
394
395
# File 'lib/email_address/local.rb', line 390

def set_error(err, reason = nil)
  @error = err
  @reason = reason
  @error_message = Config.error_message(err, locale)
  false
end

#special?Boolean

Is the address for a common system or business role account?

Returns:

  • (Boolean)


185
186
187
# File 'lib/email_address/local.rb', line 185

def special?
  SPECIAL_MAILBOXES.include?(mailbox)
end

#standardObject

Returns a normalized version of the standard address parts.



234
235
236
237
238
239
240
241
242
243
# File 'lib/email_address/local.rb', line 234

def standard
  form = mailbox
  form += @config[:tag_separator] + tag if tag
  form += "(" + comment + ")" if comment
  form = form.gsub(/([\\"])/, '\\\1') # Escape \ and "
  if /[ "(),:<>@\[\\\]]/.match?(form) # Space and "(),:;<>@[\]
    form = %("#{form}")
  end
  form
end

#standard?Boolean

True if the part matches the RFC standard format

Returns:

  • (Boolean)


367
368
369
370
371
372
373
374
375
376
377
# File 'lib/email_address/local.rb', line 367

def standard?
  self.syntax = :invalid
  valid_size? or return false
  valid_encoding? or return false
  if STANDARD_LOCAL_REGEX.match?(local)
    self.syntax = :standard
    true
  else
    false
  end
end

#to_sObject



189
190
191
# File 'lib/email_address/local.rb', line 189

def to_s
  self.format
end

#unicode?Boolean

True if the the value contains non-Latin Unicde characters

Returns:

  • (Boolean)


170
171
172
# File 'lib/email_address/local.rb', line 170

def unicode?
  /[^\p{InBasicLatin}]/.match?(local)
end

#valid?(format = @config[:local_format] || :conventional) ⇒ Boolean

True if the part is valid according to the configurations

Returns:

  • (Boolean)


275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/email_address/local.rb', line 275

def valid?(format = @config[:local_format] || :conventional)
  if @config[:mailbox_validator].is_a?(Proc)
    @config[:mailbox_validator].call(mailbox, tag)
  elsif format.is_a?(Proc)
    format.call(self)
  elsif format == :conventional
    conventional?
  elsif format == :relaxed
    relaxed?
  elsif format == :redacted
    redacted?
  elsif format == :standard
    standard?
  elsif format == :none
    true
  else
    raise "Unknown format #{format}"
  end
end

#valid_encoding?(enc = @config[:local_encoding] || :ascii) ⇒ Boolean

Returns:

  • (Boolean)


328
329
330
331
# File 'lib/email_address/local.rb', line 328

def valid_encoding?(enc = @config[:local_encoding] || :ascii)
  return false if enc == :ascii && unicode?
  true
end

#valid_size?Boolean

Returns:

  • (Boolean)


311
312
313
314
315
316
317
318
319
320
# File 'lib/email_address/local.rb', line 311

def valid_size?
  return set_error(:local_size_long) if local.size > STANDARD_MAX_SIZE
  if @host&.hosted_service?
    return false if @config[:local_private_size] && !valid_size_checks(@config[:local_private_size])
  elsif @config[:local_size] && !valid_size_checks(@config[:local_size])
    return false
  end
  return false if @config[:mailbox_size] && !valid_size_checks(@config[:mailbox_size])
  true
end

#valid_size_checks(range) ⇒ Object



322
323
324
325
326
# File 'lib/email_address/local.rb', line 322

def valid_size_checks(range)
  return set_error(:local_size_short) if mailbox.size < range.first
  return set_error(:local_size_long) if mailbox.size > range.last
  true
end