email_address 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +18 -0
- data/Gemfile +1 -1
- data/README.md +255 -123
- data/Rakefile +2 -3
- data/email_address.gemspec +27 -22
- data/lib/email_address/active_record_validator.rb +10 -11
- data/lib/email_address/address.rb +126 -80
- data/lib/email_address/canonical_email_address_type.rb +16 -12
- data/lib/email_address/config.rb +102 -51
- data/lib/email_address/email_address_type.rb +17 -13
- data/lib/email_address/exchanger.rb +44 -33
- data/lib/email_address/host.rb +217 -105
- data/lib/email_address/local.rb +127 -87
- data/lib/email_address/messages.yaml +21 -0
- data/lib/email_address/rewriter.rb +144 -0
- data/lib/email_address/version.rb +1 -1
- data/lib/email_address.rb +48 -53
- data/test/activerecord/test_ar.rb +17 -13
- data/test/activerecord/user.rb +31 -30
- data/test/email_address/test_address.rb +84 -21
- data/test/email_address/test_config.rb +10 -10
- data/test/email_address/test_exchanger.rb +6 -7
- data/test/email_address/test_host.rb +59 -21
- data/test/email_address/test_local.rb +49 -36
- data/test/email_address/test_rewriter.rb +11 -0
- data/test/test_aliasing.rb +53 -0
- data/test/test_email_address.rb +15 -19
- data/test/test_helper.rb +9 -8
- metadata +43 -21
- data/.travis.yml +0 -10
data/lib/email_address/local.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EmailAddress
|
2
4
|
##############################################################################
|
3
5
|
# EmailAddress Local part consists of
|
@@ -65,42 +67,51 @@ module EmailAddress
|
|
65
67
|
# [CFWS]
|
66
68
|
############################################################################
|
67
69
|
class Local
|
68
|
-
attr_reader
|
70
|
+
attr_reader :local
|
69
71
|
attr_accessor :mailbox, :comment, :tag, :config, :original
|
70
|
-
attr_accessor :syntax
|
72
|
+
attr_accessor :syntax, :locale
|
71
73
|
|
72
74
|
# RFC-2142: MAILBOX NAMES FOR COMMON SERVICES, ROLES AND FUNCTIONS
|
73
|
-
BUSINESS_MAILBOXES = %w
|
74
|
-
NETWORK_MAILBOXES
|
75
|
-
SERVICE_MAILBOXES
|
76
|
-
SYSTEM_MAILBOXES
|
77
|
-
ROLE_MAILBOXES
|
78
|
-
SPECIAL_MAILBOXES
|
79
|
-
|
80
|
-
STANDARD_MAX_SIZE
|
75
|
+
BUSINESS_MAILBOXES = %w[info marketing sales support]
|
76
|
+
NETWORK_MAILBOXES = %w[abuse noc security]
|
77
|
+
SERVICE_MAILBOXES = %w[postmaster hostmaster usenet news webmaster www uucp ftp]
|
78
|
+
SYSTEM_MAILBOXES = %w[help mailer-daemon root] # Not from RFC-2142
|
79
|
+
ROLE_MAILBOXES = %w[staff office orders billing careers jobs] # Not from RFC-2142
|
80
|
+
SPECIAL_MAILBOXES = BUSINESS_MAILBOXES + NETWORK_MAILBOXES + SERVICE_MAILBOXES +
|
81
|
+
SYSTEM_MAILBOXES + ROLE_MAILBOXES
|
82
|
+
STANDARD_MAX_SIZE = 64
|
81
83
|
|
82
84
|
# Conventional : word([.-+'_]word)*
|
83
|
-
CONVENTIONAL_MAILBOX_REGEX
|
84
|
-
CONVENTIONAL_MAILBOX_WITHIN = /[\p{L}\p{N}]+ (?: [
|
85
|
+
CONVENTIONAL_MAILBOX_REGEX = /\A [\p{L}\p{N}_]+ (?: [.\-+'_] [\p{L}\p{N}_]+ )* \z/x
|
86
|
+
CONVENTIONAL_MAILBOX_WITHIN = /[\p{L}\p{N}_]+ (?: [.\-+'_] [\p{L}\p{N}_]+ )*/x
|
85
87
|
|
86
88
|
# Relaxed: same characters, relaxed order
|
87
|
-
RELAXED_MAILBOX_WITHIN = /[\p{L}\p{N}]+ (?: [
|
88
|
-
RELAXED_MAILBOX_REGEX = /\A [\p{L}\p{N}]+ (?: [
|
89
|
+
RELAXED_MAILBOX_WITHIN = /[\p{L}\p{N}_]+ (?: [.\-+'_]+ [\p{L}\p{N}_]+ )*/x
|
90
|
+
RELAXED_MAILBOX_REGEX = /\A [\p{L}\p{N}_]+ (?: [.\-+'_]+ [\p{L}\p{N}_]+ )* \z/x
|
89
91
|
|
90
92
|
# RFC5322 Token: token."token".token (dot-separated tokens)
|
91
93
|
# Quoted Token can also have: SPACE \" \\ ( ) , : ; < > @ [ \ ] .
|
92
94
|
STANDARD_LOCAL_WITHIN = /
|
93
|
-
(?: [\p{L}\p{N}
|
94
|
-
|
|
95
|
-
(?: \. (?: [\p{L}\p{N}
|
96
|
-
|
|
95
|
+
(?: [\p{L}\p{N}!\#$%&'*+\-\/=?\^_`{|}~()]+
|
96
|
+
| " (?: \\[" \\] | [\x20-\x21\x23-\x2F\x3A-\x40\x5B\x5D-\x60\x7B-\x7E\p{L}\p{N}] )+ " )
|
97
|
+
(?: \. (?: [\p{L}\p{N}!\#$%&'*+\-\/=?\^_`{|}~()]+
|
98
|
+
| " (?: \\[" \\] | [\x20-\x21\x23-\x2F\x3A-\x40\x5B\x5D-\x60\x7B-\x7E\p{L}\p{N}] )+ " ) )* /x
|
99
|
+
|
97
100
|
STANDARD_LOCAL_REGEX = /\A #{STANDARD_LOCAL_WITHIN} \z/x
|
98
101
|
|
99
102
|
REDACTED_REGEX = /\A \{ [0-9a-f]{40} \} \z/x # {sha1}
|
100
103
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
+
CONVENTIONAL_TAG_REGEX = # AZaz09_!'+-/=
|
105
|
+
%r{^([\w!'+\-/=.]+)$}i
|
106
|
+
RELAXED_TAG_REGEX = # AZaz09_!#$%&'*+-/=?^`{|}~
|
107
|
+
%r/^([\w.!\#$%&'*+\-\/=?\^`{|}~]+)$/i
|
108
|
+
|
109
|
+
def initialize(local, config = {}, host = nil, locale = "en")
|
110
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
111
|
+
self.local = local
|
112
|
+
@host = host
|
113
|
+
@locale = locale
|
114
|
+
@error = @error_message = nil
|
104
115
|
end
|
105
116
|
|
106
117
|
def local=(raw)
|
@@ -111,23 +122,23 @@ module EmailAddress
|
|
111
122
|
if @config[:local_parse].is_a?(Proc)
|
112
123
|
self.mailbox, self.tag, self.comment = @config[:local_parse].call(raw)
|
113
124
|
else
|
114
|
-
self.mailbox, self.tag, self.comment =
|
125
|
+
self.mailbox, self.tag, self.comment = parse(raw)
|
115
126
|
end
|
116
127
|
|
117
128
|
self.format
|
118
129
|
end
|
119
130
|
|
120
131
|
def parse(raw)
|
121
|
-
if raw =~ /\A
|
132
|
+
if raw =~ /\A"(.*)"\z/ # Quoted
|
122
133
|
raw = $1
|
123
|
-
raw.gsub
|
124
|
-
elsif @config[:local_fix]
|
125
|
-
raw.
|
126
|
-
raw.
|
127
|
-
raw.gsub!(/([^\p{L}\p{N}]{2,10})/) {|s| s[0] } # Stutter punctuation typo
|
134
|
+
raw = raw.gsub(/\\(.)/, '\1') # Unescape
|
135
|
+
elsif @config[:local_fix] && @config[:local_format] != :standard
|
136
|
+
raw = raw.delete(" ")
|
137
|
+
raw = raw.tr(",", ".")
|
138
|
+
# raw.gsub!(/([^\p{L}\p{N}]{2,10})/) {|s| s[0] } # Stutter punctuation typo
|
128
139
|
end
|
129
|
-
raw, comment =
|
130
|
-
mailbox, tag =
|
140
|
+
raw, comment = parse_comment(raw)
|
141
|
+
mailbox, tag = parse_tag(raw)
|
131
142
|
mailbox ||= ""
|
132
143
|
[mailbox, tag, comment]
|
133
144
|
end
|
@@ -146,28 +157,28 @@ module EmailAddress
|
|
146
157
|
end
|
147
158
|
|
148
159
|
def parse_tag(raw)
|
149
|
-
separator = @config[:tag_separator] ||=
|
160
|
+
separator = @config[:tag_separator] ||= "+"
|
150
161
|
raw.split(separator, 2)
|
151
162
|
end
|
152
163
|
|
153
164
|
# True if the the value contains only Latin characters (7-bit ASCII)
|
154
165
|
def ascii?
|
155
|
-
!
|
166
|
+
!unicode?
|
156
167
|
end
|
157
168
|
|
158
169
|
# True if the the value contains non-Latin Unicde characters
|
159
170
|
def unicode?
|
160
|
-
|
171
|
+
/[^\p{InBasicLatin}]/.match?(local)
|
161
172
|
end
|
162
173
|
|
163
174
|
# Returns true if the value matches the Redacted format
|
164
175
|
def redacted?
|
165
|
-
|
176
|
+
REDACTED_REGEX.match?(local)
|
166
177
|
end
|
167
178
|
|
168
179
|
# Returns true if the value matches the Redacted format
|
169
180
|
def self.redacted?(local)
|
170
|
-
|
181
|
+
REDACTED_REGEX.match?(local)
|
171
182
|
end
|
172
183
|
|
173
184
|
# Is the address for a common system or business role account?
|
@@ -180,81 +191,80 @@ module EmailAddress
|
|
180
191
|
end
|
181
192
|
|
182
193
|
# Builds the local string according to configurations
|
183
|
-
def format(form
|
194
|
+
def format(form = @config[:local_format] || :conventional)
|
184
195
|
if @config[:local_format].is_a?(Proc)
|
185
196
|
@config[:local_format].call(self)
|
186
197
|
elsif form == :conventional
|
187
|
-
|
198
|
+
conventional
|
188
199
|
elsif form == :canonical
|
189
|
-
|
190
|
-
elsif form == :
|
191
|
-
|
200
|
+
canonical
|
201
|
+
elsif form == :relaxed
|
202
|
+
relax
|
192
203
|
elsif form == :standard
|
193
|
-
|
204
|
+
standard
|
194
205
|
end
|
195
206
|
end
|
196
207
|
|
197
208
|
# Returns a conventional form of the address
|
198
209
|
def conventional
|
199
|
-
if
|
200
|
-
[
|
210
|
+
if tag
|
211
|
+
[mailbox, tag].join(@config[:tag_separator])
|
201
212
|
else
|
202
|
-
|
213
|
+
mailbox
|
203
214
|
end
|
204
215
|
end
|
205
216
|
|
206
217
|
# Returns a canonical form of the address
|
207
218
|
def canonical
|
208
219
|
if @config[:mailbox_canonical]
|
209
|
-
@config[:mailbox_canonical].call(
|
220
|
+
@config[:mailbox_canonical].call(mailbox)
|
210
221
|
else
|
211
|
-
|
222
|
+
mailbox.downcase
|
212
223
|
end
|
213
224
|
end
|
214
225
|
|
215
226
|
# Relaxed format: mailbox and tag, no comment, no extended character set
|
216
227
|
def relax
|
217
|
-
form =
|
218
|
-
form += @config[:tag_separator] +
|
219
|
-
form.gsub
|
220
|
-
form
|
228
|
+
form = mailbox
|
229
|
+
form += @config[:tag_separator] + tag if tag
|
230
|
+
form.gsub(/[ "(),:<>@\[\]\\]/, "")
|
221
231
|
end
|
222
232
|
|
223
233
|
# Returns a normalized version of the standard address parts.
|
224
234
|
def standard
|
225
|
-
form =
|
226
|
-
form += @config[:tag_separator] +
|
227
|
-
form += "(" +
|
228
|
-
form.gsub
|
229
|
-
if
|
230
|
-
form = %
|
235
|
+
form = mailbox
|
236
|
+
form += @config[:tag_separator] + tag if tag
|
237
|
+
form += "(" + comment + ")" if comment
|
238
|
+
form = form.gsub(/([\\"])/, '\\\1') # Escape \ and "
|
239
|
+
if /[ "(),:<>@\[\\\]]/.match?(form) # Space and "(),:;<>@[\]
|
240
|
+
form = %("#{form}")
|
231
241
|
end
|
232
242
|
form
|
233
243
|
end
|
234
244
|
|
235
245
|
# Sets the part to be the conventional form
|
236
246
|
def conventional!
|
237
|
-
self.local =
|
247
|
+
self.local = conventional
|
238
248
|
end
|
239
249
|
|
240
250
|
# Sets the part to be the canonical form
|
241
251
|
def canonical!
|
242
|
-
self.local =
|
252
|
+
self.local = canonical
|
243
253
|
end
|
244
254
|
|
245
255
|
# Dropps unusual parts of Standard form to form a relaxed version.
|
246
256
|
def relax!
|
247
|
-
self.local =
|
257
|
+
self.local = relax
|
248
258
|
end
|
249
259
|
|
250
260
|
# Returns the munged form of the address, like "ma*****"
|
251
261
|
def munge
|
252
|
-
|
262
|
+
to_s.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
|
253
263
|
end
|
254
264
|
|
255
265
|
# Mailbox with trailing numbers removed
|
256
266
|
def root_name
|
257
|
-
|
267
|
+
mailbox =~ /\A(.+?)\d+\z/ ? $1 : mailbox
|
258
268
|
end
|
259
269
|
|
260
270
|
############################################################################
|
@@ -262,19 +272,19 @@ module EmailAddress
|
|
262
272
|
############################################################################
|
263
273
|
|
264
274
|
# True if the part is valid according to the configurations
|
265
|
-
def valid?(format
|
275
|
+
def valid?(format = @config[:local_format] || :conventional)
|
266
276
|
if @config[:mailbox_validator].is_a?(Proc)
|
267
|
-
@config[:mailbox_validator].call(
|
277
|
+
@config[:mailbox_validator].call(mailbox, tag)
|
268
278
|
elsif format.is_a?(Proc)
|
269
279
|
format.call(self)
|
270
280
|
elsif format == :conventional
|
271
|
-
|
281
|
+
conventional?
|
272
282
|
elsif format == :relaxed
|
273
|
-
|
283
|
+
relaxed?
|
274
284
|
elsif format == :redacted
|
275
|
-
|
285
|
+
redacted?
|
276
286
|
elsif format == :standard
|
277
|
-
|
287
|
+
standard?
|
278
288
|
elsif format == :none
|
279
289
|
true
|
280
290
|
else
|
@@ -285,13 +295,13 @@ module EmailAddress
|
|
285
295
|
# Returns the format of the address
|
286
296
|
def format?
|
287
297
|
# if :custom
|
288
|
-
if
|
298
|
+
if conventional?
|
289
299
|
:conventional
|
290
|
-
elsif
|
300
|
+
elsif relaxed?
|
291
301
|
:relax
|
292
|
-
elsif
|
302
|
+
elsif redacted?
|
293
303
|
:redacted
|
294
|
-
elsif
|
304
|
+
elsif standard?
|
295
305
|
:standard
|
296
306
|
else
|
297
307
|
:invalid
|
@@ -299,24 +309,38 @@ module EmailAddress
|
|
299
309
|
end
|
300
310
|
|
301
311
|
def valid_size?
|
302
|
-
return
|
303
|
-
|
304
|
-
|
312
|
+
return set_error(:local_size_long) if local.size > STANDARD_MAX_SIZE
|
313
|
+
if @host&.hosted_service?
|
314
|
+
return false if @config[:local_private_size] && !valid_size_checks(@config[:local_private_size])
|
315
|
+
elsif @config[:local_size] && !valid_size_checks(@config[:local_size])
|
316
|
+
return false
|
317
|
+
end
|
318
|
+
return false if @config[:mailbox_size] && !valid_size_checks(@config[:mailbox_size])
|
305
319
|
true
|
306
320
|
end
|
307
321
|
|
308
|
-
def
|
309
|
-
return
|
310
|
-
return
|
322
|
+
def valid_size_checks(range)
|
323
|
+
return set_error(:local_size_short) if mailbox.size < range.first
|
324
|
+
return set_error(:local_size_long) if mailbox.size > range.last
|
325
|
+
true
|
326
|
+
end
|
327
|
+
|
328
|
+
def valid_encoding?(enc = @config[:local_encoding] || :ascii)
|
329
|
+
return false if enc == :ascii && unicode?
|
311
330
|
true
|
312
331
|
end
|
313
332
|
|
314
333
|
# True if the part matches the conventional format
|
315
334
|
def conventional?
|
316
335
|
self.syntax = :invalid
|
317
|
-
|
318
|
-
|
319
|
-
|
336
|
+
if tag
|
337
|
+
return false unless mailbox =~ CONVENTIONAL_MAILBOX_REGEX &&
|
338
|
+
tag =~ CONVENTIONAL_TAG_REGEX
|
339
|
+
else
|
340
|
+
return false unless CONVENTIONAL_MAILBOX_REGEX.match?(local)
|
341
|
+
end
|
342
|
+
valid_size? or return false
|
343
|
+
valid_encoding? or return false
|
320
344
|
self.syntax = :conventional
|
321
345
|
true
|
322
346
|
end
|
@@ -324,9 +348,12 @@ module EmailAddress
|
|
324
348
|
# Relaxed conventional is not so strict about character order.
|
325
349
|
def relaxed?
|
326
350
|
self.syntax = :invalid
|
327
|
-
|
328
|
-
|
329
|
-
if
|
351
|
+
valid_size? or return false
|
352
|
+
valid_encoding? or return false
|
353
|
+
if tag
|
354
|
+
return false unless mailbox =~ RELAXED_MAILBOX_REGEX &&
|
355
|
+
tag =~ RELAXED_TAG_REGEX
|
356
|
+
elsif RELAXED_MAILBOX_REGEX.match?(local)
|
330
357
|
self.syntax = :relaxed
|
331
358
|
true
|
332
359
|
else
|
@@ -337,9 +364,9 @@ module EmailAddress
|
|
337
364
|
# True if the part matches the RFC standard format
|
338
365
|
def standard?
|
339
366
|
self.syntax = :invalid
|
340
|
-
|
341
|
-
|
342
|
-
if
|
367
|
+
valid_size? or return false
|
368
|
+
valid_encoding? or return false
|
369
|
+
if STANDARD_LOCAL_REGEX.match?(local)
|
343
370
|
self.syntax = :standard
|
344
371
|
true
|
345
372
|
else
|
@@ -352,10 +379,23 @@ module EmailAddress
|
|
352
379
|
def matches?(*rules)
|
353
380
|
rules.flatten.each do |r|
|
354
381
|
if r =~ /(.+)@\z/
|
355
|
-
return r if File.fnmatch?($1,
|
382
|
+
return r if File.fnmatch?($1, local)
|
356
383
|
end
|
357
384
|
end
|
358
385
|
false
|
359
386
|
end
|
387
|
+
|
388
|
+
def set_error(err, reason = nil)
|
389
|
+
@error = err
|
390
|
+
@reason = reason
|
391
|
+
@error_message = Config.error_message(err, locale)
|
392
|
+
false
|
393
|
+
end
|
394
|
+
|
395
|
+
attr_reader :error_message
|
396
|
+
|
397
|
+
def error
|
398
|
+
valid? ? nil : (@error || :local_invalid)
|
399
|
+
end
|
360
400
|
end
|
361
401
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
en:
|
2
|
+
email_address:
|
3
|
+
address_unknown: "Unknown Email Address"
|
4
|
+
domain_does_not_accept_email: "This domain is not configured to accept email"
|
5
|
+
domain_invalid: "Invalid Domain Name"
|
6
|
+
domain_no_localhost: "localhost is not allowed for your domain name"
|
7
|
+
domain_unknown: "Domain name not registered"
|
8
|
+
exceeds_size: "Address too long"
|
9
|
+
incomplete_domain: "Domain name is incomplete"
|
10
|
+
invalid_address: "Invalid Email Address"
|
11
|
+
invalid_host: "Invalid Host/Domain Name"
|
12
|
+
invalid_mailbox: "Invalid Mailbox"
|
13
|
+
ip_address_forbidden: "IP Addresses are not allowed"
|
14
|
+
ip_address_no_localhost: "Localhost IP addresses are not allowed"
|
15
|
+
ipv4_address_invalid: "This is not a valid IPv4 address"
|
16
|
+
ipv6_address_invalid: "This is not a valid IPv6 address"
|
17
|
+
local_size_long: "Mailbox name too long"
|
18
|
+
local_size_short: "Mailbox name too short"
|
19
|
+
local_invalid: "Recipient is not valid"
|
20
|
+
not_allowed: "Address is not allowed"
|
21
|
+
server_not_available: "The remote email server is not available"
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module EmailAddress::Rewriter
|
6
|
+
SRS_FORMAT_REGEX = /\ASRS0=(....)=(\w\w)=(.+?)=(.+?)@(.+)\z/
|
7
|
+
|
8
|
+
def parse_rewritten(e)
|
9
|
+
@rewrite_scheme = nil
|
10
|
+
@rewrite_error = nil
|
11
|
+
parse_srs(e)
|
12
|
+
# e = parse_batv(e)
|
13
|
+
end
|
14
|
+
|
15
|
+
#---------------------------------------------------------------------------
|
16
|
+
# SRS (Sender Rewriting Scheme) allows an address to be forwarded from the
|
17
|
+
# original owner and encoded to be used with the domain name of the MTA (Mail
|
18
|
+
# Transport Agent). It encodes the original address within the local part of the
|
19
|
+
# sending email address and respects VERP. If example.com needs to forward a
|
20
|
+
# message from "sender@gmail.com", the SMTP envelope sender is used at this
|
21
|
+
# address. These methods respect DMARC and prevent spoofing email send using
|
22
|
+
# a different domain.
|
23
|
+
# Format: SRS0=HHH=TT=domain=local@sending-domain.com
|
24
|
+
#---------------------------------------------------------------------------
|
25
|
+
def srs(sending_domain, options = {}, &block)
|
26
|
+
tt = srs_tt
|
27
|
+
a = [tt, hostname, local.to_s].join("=") + "@" + sending_domain
|
28
|
+
hhh = srs_hash(a, options, &block)
|
29
|
+
|
30
|
+
["SRS0", hhh, a].join("=")
|
31
|
+
end
|
32
|
+
|
33
|
+
def srs?(email)
|
34
|
+
email.match(SRS_FORMAT_REGEX) ? true : false
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_srs(email, options = {}, &block)
|
38
|
+
if email&.match(SRS_FORMAT_REGEX)
|
39
|
+
@rewrite_scheme = :srs
|
40
|
+
hhh, tt, domain, local, sending_domain = [$1, $2, $3, $4, $5]
|
41
|
+
# hhh = tt = sending_domain if false && hhh # Hide warnings for now :-)
|
42
|
+
a = [tt, domain, local].join("=") + "@" + sending_domain
|
43
|
+
unless srs_hash(a, options, &block) === hhh
|
44
|
+
@rewrite_error = "Invalid SRS Email Address: Possibly altered"
|
45
|
+
end
|
46
|
+
unless tt == srs_tt
|
47
|
+
@rewrite_error = "Invalid SRS Email Address: Too old"
|
48
|
+
end
|
49
|
+
[local, domain].join("@")
|
50
|
+
else
|
51
|
+
email
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# SRS Timeout Token
|
56
|
+
# Returns a 2-character code for the day. After a few days the code will roll.
|
57
|
+
# TT has a one-day resolution in order to make the address invalid after a few days.
|
58
|
+
# The cycle period is 3.5 years. Used to control late bounces and harvesting.
|
59
|
+
def srs_tt(t = Time.now.utc)
|
60
|
+
Base64.encode64((t.to_i / (60 * 60 * 24) % 210).to_s)[0, 2]
|
61
|
+
end
|
62
|
+
|
63
|
+
def srs_hash(email, options = {}, &block)
|
64
|
+
key = options[:key] || @config[:key] || email.reverse
|
65
|
+
if block
|
66
|
+
block.call(email)[0, 4]
|
67
|
+
else
|
68
|
+
Base64.encode64(Digest::SHA1.digest(email + key))[0, 4]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#---------------------------------------------------------------------------
|
73
|
+
# Returns a BATV form email address with "Private Signature" (prvs).
|
74
|
+
# Options: key: 0-9 key digit to use
|
75
|
+
# key_0..key_9: secret key used to sign/verify
|
76
|
+
# prvs_days: number of days before address "expires"
|
77
|
+
#
|
78
|
+
# BATV - Bounce Address Tag Validation
|
79
|
+
# PRVS - Simple Private Signature
|
80
|
+
# Ex: prvs=KDDDSSSS=user@example.com
|
81
|
+
# * K: Digit for Key rotation
|
82
|
+
# * DDD: Expiry date, since 1970, low 3 digits
|
83
|
+
# * SSSSSS: sha1( KDDD + orig-mailfrom + key)[0,6]
|
84
|
+
# See: https://tools.ietf.org/html/draft-levine-smtp-batv-01
|
85
|
+
#---------------------------------------------------------------------------
|
86
|
+
def batv_prvs(options = {})
|
87
|
+
k = options[:prvs_key_id] || "0"
|
88
|
+
prvs_days = options[:prvs_days] || @config[:prvs_days] || 30
|
89
|
+
ddd = prvs_day(prvs_days)
|
90
|
+
ssssss = prvs_sign(k, ddd, to_s, options)
|
91
|
+
["prvs=", k, ddd, ssssss, "=", to_s].join("")
|
92
|
+
end
|
93
|
+
|
94
|
+
PRVS_REGEX = /\Aprvs=(\d)(\d{3})(\w{6})=(.+)\z/
|
95
|
+
|
96
|
+
def parse_prvs(email, options = {})
|
97
|
+
if email.match(PRVS_REGEX)
|
98
|
+
@rewrite_scheme = :prvs
|
99
|
+
k, ddd, ssssss, email = [$1, $2, $3, $4]
|
100
|
+
|
101
|
+
unless ssssss == prvs_sign(k, ddd, email, options)
|
102
|
+
@rewrite_error = "Invalid BATV Address: Signature unverified"
|
103
|
+
end
|
104
|
+
exp = ddd.to_i
|
105
|
+
roll = 1000 - exp # rolling 1000 day window
|
106
|
+
today = prvs_day(0)
|
107
|
+
# I'm sure this is wrong
|
108
|
+
if exp > today && exp < roll
|
109
|
+
@rewrite_error = "Invalid SRS Email Address: Address expired"
|
110
|
+
elsif exp < today && (today - exp) > 0
|
111
|
+
@rewrite_error = "Invalid SRS Email Address: Address expired"
|
112
|
+
end
|
113
|
+
[local, domain].join("@")
|
114
|
+
else
|
115
|
+
email
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def prvs_day(days)
|
120
|
+
((Time.now.to_i + (days * 24 * 60 * 60)) / (24 * 60 * 60)).to_s[-3, 3]
|
121
|
+
end
|
122
|
+
|
123
|
+
def prvs_sign(k, ddd, email, options = {})
|
124
|
+
str = [ddd, ssssss, "=", to_s].join("")
|
125
|
+
key = options["key_#{k}".to_i] || @config["key_#{k}".to_i] || str.reverse
|
126
|
+
Digest::SHA1.hexdigest([k, ddd, email, key].join(""))[0, 6]
|
127
|
+
end
|
128
|
+
|
129
|
+
#---------------------------------------------------------------------------
|
130
|
+
# VERP Embeds a recipient email address into the bounce address
|
131
|
+
# Bounce Address: message-id@example.net
|
132
|
+
# Recipient Email: recipient@example.org
|
133
|
+
# VERP : message-id+recipient=example.org@example.net
|
134
|
+
# To handle incoming verp, the "tag" is the recipient email address,
|
135
|
+
# remember to convert the last '=' into a '@' to reconstruct it.
|
136
|
+
#---------------------------------------------------------------------------
|
137
|
+
def verp(recipient, split_char = "+")
|
138
|
+
local.to_s +
|
139
|
+
split_char + recipient.tr("@", "=") +
|
140
|
+
"@" + hostname
|
141
|
+
end
|
142
|
+
|
143
|
+
# NEXT: DMARC, SPF Validation
|
144
|
+
end
|