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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Local.



104
105
106
107
108
109
# File 'lib/email_address/local.rb', line 104

def initialize(local, config={}, host=nil)
  self.config   = config.empty? ? EmailAddress::Config.all_settings : config
  self.local    = local
  @host         = host
  @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

#localObject

Returns the value of attribute local.



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

def local
  @local
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)


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

def self.redacted?(local)
  local =~ REDACTED_REGEX ? true : false
end

Instance Method Details

#ascii?Boolean

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

Returns:

  • (Boolean)


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

def ascii?
  ! self.unicode?
end

#canonicalObject

Returns a canonical form of the address



212
213
214
215
216
217
218
# File 'lib/email_address/local.rb', line 212

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

#canonical!Object

Sets the part to be the canonical form



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

def canonical!
  self.local = self.canonical
end

#conventionalObject

Returns a conventional form of the address



203
204
205
206
207
208
209
# File 'lib/email_address/local.rb', line 203

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

#conventional!Object

Sets the part to be the conventional form



241
242
243
# File 'lib/email_address/local.rb', line 241

def conventional!
  self.local = self.conventional
end

#conventional?Boolean

True if the part matches the conventional format

Returns:

  • (Boolean)


329
330
331
332
333
334
335
336
# File 'lib/email_address/local.rb', line 329

def conventional?
  self.syntax = :invalid
  self.local =~ CONVENTIONAL_MAILBOX_REGEX or return false
  self.valid_size? or return false
  self.valid_encoding? or return false
  self.syntax = :conventional
  true
end

#errorObject



386
387
388
# File 'lib/email_address/local.rb', line 386

def error
  self.valid? ? nil : @error
end

#error_messageObject



382
383
384
# File 'lib/email_address/local.rb', line 382

def error_message
  @error_message
end

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

Builds the local string according to configurations



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/email_address/local.rb', line 188

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

#format?Boolean

Returns the format of the address

Returns:

  • (Boolean)


291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/email_address/local.rb', line 291

def format?
  # if :custom
  if self.conventional?
    :conventional
  elsif self.relaxed?
    :relax
  elsif self.redacted?
    :redacted
  elsif self.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)


366
367
368
369
370
371
372
373
# File 'lib/email_address/local.rb', line 366

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

#mungeObject

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



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

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

#parse(raw) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/email_address/local.rb', line 125

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



142
143
144
145
146
147
148
149
150
151
# File 'lib/email_address/local.rb', line 142

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



153
154
155
156
# File 'lib/email_address/local.rb', line 153

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)


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

def redacted?
  self.local =~ REDACTED_REGEX ? true : false
end

#relaxObject

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



221
222
223
224
225
226
# File 'lib/email_address/local.rb', line 221

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

#relax!Object

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



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

def relax!
  self.local = self.relax
end

#relaxed?Boolean

Relaxed conventional is not so strict about character order.

Returns:

  • (Boolean)


339
340
341
342
343
344
345
346
347
348
349
# File 'lib/email_address/local.rb', line 339

def relaxed?
  self.syntax = :invalid
  self.valid_size? or return false
  self.valid_encoding? or return false
  if self.local =~ RELAXED_MAILBOX_REGEX
    self.syntax = :relaxed
    true
  else
    false
  end
end

#root_nameObject

Mailbox with trailing numbers removed



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

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

#set_error(err, reason = nil) ⇒ Object



375
376
377
378
379
380
# File 'lib/email_address/local.rb', line 375

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

#special?Boolean

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

Returns:

  • (Boolean)


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

def special?
  SPECIAL_MAILBOXES.include?(mailbox)
end

#standardObject

Returns a normalized version of the standard address parts.



229
230
231
232
233
234
235
236
237
238
# File 'lib/email_address/local.rb', line 229

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

#standard?Boolean

True if the part matches the RFC standard format

Returns:

  • (Boolean)


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

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

#to_sObject



183
184
185
# File 'lib/email_address/local.rb', line 183

def to_s
  self.format
end

#unicode?Boolean

True if the the value contains non-Latin Unicde characters

Returns:

  • (Boolean)


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

def unicode?
  self.local =~ /[^\p{InBasicLatin}]/ ? true : false
end

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

True if the part is valid according to the configurations

Returns:

  • (Boolean)


270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/email_address/local.rb', line 270

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

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

Returns:

  • (Boolean)


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

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

#valid_size?Boolean

Returns:

  • (Boolean)


306
307
308
309
310
311
312
313
314
315
# File 'lib/email_address/local.rb', line 306

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

#valid_size_checks(range) ⇒ Object



317
318
319
320
321
# File 'lib/email_address/local.rb', line 317

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