Module: EmailAddress::Rewriter

Included in:
Address
Defined in:
lib/email_address/rewriter.rb

Constant Summary collapse

SRS_FORMAT_REGEX =
/\ASRS0=(....)=(\w\w)=(.+?)=(.+?)@(.+)\z/
PRVS_REGEX =
/\Aprvs=(\d)(\d{3})(\w{6})=(.+)\z/

Instance Method Summary collapse

Instance Method Details

#batv_prvs(options = {}) ⇒ Object


Returns a BATV form email address with “Private Signature” (prvs). Options: key: 0-9 key digit to use

key_0..key_9: secret key used to sign/verify
prvs_days: number of days before address "expires"

BATV - Bounce Address Tag Validation PRVS - Simple Private Signature Ex: [email protected]

* K: Digit for Key rotation
* DDD: Expiry date, since 1970, low 3 digits
* SSSSSS: sha1( KDDD + orig-mailfrom + key)[0,6]

See: tools.ietf.org/html/draft-levine-smtp-batv-01




86
87
88
89
90
91
92
# File 'lib/email_address/rewriter.rb', line 86

def batv_prvs(options = {})
  k = options[:prvs_key_id] || "0"
  prvs_days = options[:prvs_days] || @config[:prvs_days] || 30
  ddd = prvs_day(prvs_days)
  ssssss = prvs_sign(k, ddd, to_s, options)
  ["prvs=", k, ddd, ssssss, "=", to_s].join("")
end

#parse_prvs(email, options = {}) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/email_address/rewriter.rb', line 96

def parse_prvs(email, options = {})
  if email.match(PRVS_REGEX)
    @rewrite_scheme = :prvs
    k, ddd, ssssss, email = [$1, $2, $3, $4]

    unless ssssss == prvs_sign(k, ddd, email, options)
      @rewrite_error = "Invalid BATV Address: Signature unverified"
    end
    exp = ddd.to_i
    roll = 1000 - exp # rolling 1000 day window
    today = prvs_day(0)
    # I'm sure this is wrong
    if exp > today && exp < roll
      @rewrite_error = "Invalid SRS Email Address: Address expired"
    elsif exp < today && (today - exp) > 0
      @rewrite_error = "Invalid SRS Email Address: Address expired"
    end
    [local, domain].join("@")
  else
    email
  end
end

#parse_rewritten(e) ⇒ Object



8
9
10
11
12
13
# File 'lib/email_address/rewriter.rb', line 8

def parse_rewritten(e)
  @rewrite_scheme = nil
  @rewrite_error = nil
  parse_srs(e)
  # e = parse_batv(e)
end

#parse_srs(email, options = {}, &block) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/email_address/rewriter.rb', line 37

def parse_srs(email, options = {}, &block)
  if email&.match(SRS_FORMAT_REGEX)
    @rewrite_scheme = :srs
    hhh, tt, domain, local, sending_domain = [$1, $2, $3, $4, $5]
    # hhh = tt = sending_domain if false && hhh # Hide warnings for now :-)
    a = [tt, domain, local].join("=") + "@" + sending_domain
    unless srs_hash(a, options, &block) === hhh
      @rewrite_error = "Invalid SRS Email Address: Possibly altered"
    end
    unless tt == srs_tt
      @rewrite_error = "Invalid SRS Email Address: Too old"
    end
    [local, domain].join("@")
  else
    email
  end
end

#prvs_day(days) ⇒ Object



119
120
121
# File 'lib/email_address/rewriter.rb', line 119

def prvs_day(days)
  ((Time.now.to_i + (days * 24 * 60 * 60)) / (24 * 60 * 60)).to_s[-3, 3]
end

#prvs_sign(k, ddd, email, options = {}) ⇒ Object



123
124
125
126
127
# File 'lib/email_address/rewriter.rb', line 123

def prvs_sign(k, ddd, email, options = {})
  str = [ddd, ssssss, "=", to_s].join("")
  key = options["key_#{k}".to_i] || @config["key_#{k}".to_i] || str.reverse
  Digest::SHA1.hexdigest([k, ddd, email, key].join(""))[0, 6]
end

#srs(sending_domain, options = {}, &block) ⇒ Object


SRS (Sender Rewriting Scheme) allows an address to be forwarded from the original owner and encoded to be used with the domain name of the MTA (Mail Transport Agent). It encodes the original address within the local part of the sending email address and respects VERP. If example.com needs to forward a message from “[email protected]”, the SMTP envelope sender is used at this address. These methods respect DMARC and prevent spoofing email send using a different domain. Format: [email protected]




25
26
27
28
29
30
31
# File 'lib/email_address/rewriter.rb', line 25

def srs(sending_domain, options = {}, &block)
  tt = srs_tt
  a = [tt, hostname, local.to_s].join("=") + "@" + sending_domain
  hhh = srs_hash(a, options, &block)

  ["SRS0", hhh, a].join("=")
end

#srs?(email) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/email_address/rewriter.rb', line 33

def srs?(email)
  email.match(SRS_FORMAT_REGEX) ? true : false
end

#srs_hash(email, options = {}, &block) ⇒ Object



63
64
65
66
67
68
69
70
# File 'lib/email_address/rewriter.rb', line 63

def srs_hash(email, options = {}, &block)
  key = options[:key] || @config[:key] || email.reverse
  if block
    block.call(email)[0, 4]
  else
    Base64.encode64(Digest::SHA1.digest(email + key))[0, 4]
  end
end

#srs_tt(t = Time.now.utc) ⇒ Object

SRS Timeout Token Returns a 2-character code for the day. After a few days the code will roll. TT has a one-day resolution in order to make the address invalid after a few days. The cycle period is 3.5 years. Used to control late bounces and harvesting.



59
60
61
# File 'lib/email_address/rewriter.rb', line 59

def srs_tt(t = Time.now.utc)
  Base64.encode64((t.to_i / (60 * 60 * 24) % 210).to_s)[0, 2]
end

#verp(recipient, split_char = "+") ⇒ Object


VERP Embeds a recipient email address into the bounce address

Bounce Address:  [email protected]
Recipient Email: [email protected]
VERP :           [email protected]

To handle incoming verp, the “tag” is the recipient email address, remember to convert the last ‘=’ into a ‘@’ to reconstruct it.




137
138
139
140
141
# File 'lib/email_address/rewriter.rb', line 137

def verp(recipient, split_char = "+")
  local.to_s +
    split_char + recipient.tr("@", "=") +
    "@" + hostname
end