canonical-emails 0.1.0 → 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 +7 -0
- data/CHANGELOG.md +17 -1
- data/README.md +238 -54
- data/UPGRADING.md +92 -0
- data/lib/canonical-emails/address.rb +32 -0
- data/lib/canonical-emails/behaviors.rb +44 -0
- data/lib/canonical-emails/downcase.rb +7 -18
- data/lib/canonical-emails/extensions.rb +29 -7
- data/lib/canonical-emails/gmail.rb +6 -20
- data/lib/canonical-emails/tagged_gmail.rb +14 -0
- data/lib/canonical-emails/version.rb +3 -1
- data/lib/canonical-emails.rb +44 -1
- data/lib/canonical_emails.rb +2 -0
- metadata +24 -47
- data/.gitignore +0 -2
- data/.rspec +0 -2
- data/.travis.yml +0 -6
- data/Gemfile +0 -6
- data/Rakefile +0 -13
- data/canonical-emails.gemspec +0 -18
- data/spec/canonical-emails/downcase_spec.rb +0 -5
- data/spec/canonical-emails/extensions_spec.rb +0 -5
- data/spec/canonical-emails/fixtures/downcase.yml +0 -30
- data/spec/canonical-emails/fixtures/extensions.yml +0 -16
- data/spec/canonical-emails/fixtures/gmail.yml +0 -29
- data/spec/canonical-emails/gmail_spec.rb +0 -5
- data/spec/canonical-emails/version_spec.rb +0 -7
- data/spec/shared/extension_examples.rb +0 -14
- data/spec/shared/transform_examples.rb +0 -11
- data/spec/spec_helper.rb +0 -12
- data/spec/support/reverse.rb +0 -18
- data/spec/support/yml_examples.rb +0 -10
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e8ad63767c830088daf91f1b1b442a4c2bddc209a4ed720a1b6e493c0b038f55
|
|
4
|
+
data.tar.gz: ac6f3c6327e206c0f7313c87fcb78a9d74176d42c7042e933ad837ed2abd4a82
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 68b08b165db5f62e1aa2e0c227013279047991cb08cfc607219e3029614423e862e2ae4ffd10c541e9f92df37feb8d4b05ee194d6501f143b6d72eb584b00710
|
|
7
|
+
data.tar.gz: f1c8199c48581cfbfe6b8b79da5d1784ec6cf8afd7918b9e736f07c74c2520fa8c4c72553ec69a294e5e559cd8fd995ee09ce33022a80fc6fdebff36e1ae2c4a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
### 0.
|
|
1
|
+
### 0.2.0 (2026-10-20)
|
|
2
|
+
|
|
3
|
+
* [#15](https://github.com/dblock/canonical-emails/pull/15): Cache canonical model accessors by raw field value and remove `address=` from all canonical address types - [@dblock](https://github.com/dblock).
|
|
4
|
+
* [#14](https://github.com/dblock/canonical-emails/pull/14): Add `as:` for custom canonical email accessor names and optional original-reader overrides - [@dblock](https://github.com/dblock).
|
|
5
|
+
* [#13](https://github.com/dblock/canonical-emails/pull/13): Document and test independent canonical accessors for multiple email fields - [@dblock](https://github.com/dblock).
|
|
6
|
+
* [#12](https://github.com/dblock/canonical-emails/pull/12): Add explicit, case-insensitive provider registration, registry clearing, and an unregistered `TaggedGMail` type that retains plus-tags - [@dblock](https://github.com/dblock).
|
|
7
|
+
* [#11](https://github.com/dblock/canonical-emails/pull/11): Introduce breaking provider-aware `.parse` and `type:` APIs, replace nested provider address classes and transform pipelines, and enforce provider restrictions on address reassignment; see UPGRADING.md - [@dblock](https://github.com/dblock).
|
|
8
|
+
* [#10](https://github.com/dblock/canonical-emails/pull/10): Refactor provider transformations into a shared address subclass with opt-in behavior mixins and document migration in UPGRADING.md - [@dblock](https://github.com/dblock).
|
|
9
|
+
* [#6](https://github.com/dblock/canonical-emails/pull/6): Add release and agent guides and prepare version 0.2.0 - [@dblock](https://github.com/dblock).
|
|
10
|
+
* [#8](https://github.com/dblock/canonical-emails/pull/8): Add Danger checks with danger-changelog - [@dblock](https://github.com/dblock).
|
|
11
|
+
* [#7](https://github.com/dblock/canonical-emails/pull/7): Add line and branch coverage reporting with SimpleCov and Coveralls - [@dblock](https://github.com/dblock).
|
|
12
|
+
* [#5](https://github.com/dblock/canonical-emails/pull/5): Canonicalize Gmail plus-address tags and expose the original `username` and `tag` through `CanonicalEmails::GMail::Address`, adapted from [#3](https://github.com/dblock/canonical-emails/pull/3) - [@jdpopkin](https://github.com/jdpopkin).
|
|
13
|
+
* [#4](https://github.com/dblock/canonical-emails/pull/4): Replace Travis CI with GitHub Actions for Ruby 3.1–4.0 and ActiveSupport 6.1–8.1, require Ruby 3.1, and modernize development dependencies and lint configuration - [@dblock](https://github.com/dblock).
|
|
14
|
+
* Implemented RuboCop, Ruby style linter - [@dblock](https://github.com/dblock).
|
|
15
|
+
* Upgraded RSpec to 3.x syntax - [@dblock](https://github.com/dblock).
|
|
16
|
+
|
|
17
|
+
### 0.1.0 (2013-06-22)
|
|
2
18
|
|
|
3
19
|
* Initial public release, written on a plane from NYC to SF - [@dblock](https://github.com/dblock).
|
data/README.md
CHANGED
|
@@ -1,100 +1,284 @@
|
|
|
1
1
|
CanonicalEmails
|
|
2
2
|
==============
|
|
3
3
|
|
|
4
|
-
[](http://badge.fury.io/rb/canonical-emails)
|
|
5
|
+
[](https://github.com/dblock/canonical-emails/actions/workflows/ci.yml)
|
|
6
|
+
[](https://coveralls.io/github/dblock/canonical-emails?branch=master)
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
Parse email addresses into provider-aware canonical forms. Gmail addresses become `CanonicalEmails::GMail` instances; unsupported domains become unchanged `CanonicalEmails::Address` instances. Both inherit from `Mail::Address`.
|
|
7
9
|
|
|
8
10
|
### Install
|
|
9
11
|
|
|
10
|
-
Add `canonical-emails` to your Gemfile.
|
|
12
|
+
Add `canonical-emails` to your Gemfile. Requires Ruby 3.1 or newer and compatible Mail and ActiveSupport versions.
|
|
11
13
|
|
|
12
|
-
```
|
|
14
|
+
```ruby
|
|
13
15
|
gem 'canonical-emails'
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
See [UPGRADING.md](UPGRADING.md) for breaking API changes, Gmail plus-tag behavior, and migration considerations for stored canonical addresses.
|
|
19
|
+
|
|
20
|
+
### Basic Usage
|
|
21
|
+
|
|
22
|
+
Add a canonical email accessor to your model:
|
|
17
23
|
|
|
18
|
-
```
|
|
24
|
+
```ruby
|
|
19
25
|
class User
|
|
20
26
|
include CanonicalEmails::Extensions
|
|
21
27
|
|
|
22
28
|
attr_accessor :email
|
|
23
|
-
canonical_email :email
|
|
29
|
+
canonical_email :email
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
user = User.new
|
|
33
|
+
user.email = "Alex Smith <Alex.Smith+Newsletter@gmail.com>"
|
|
34
|
+
user.canonical_email.class # CanonicalEmails::GMail
|
|
35
|
+
user.canonical_email.address # "alexsmith@gmail.com"
|
|
36
|
+
user.canonical_email.tag # "Newsletter"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Multiple Email Fields
|
|
40
|
+
|
|
41
|
+
Declare `canonical_email` once for each field. Each declaration creates a separate `canonical_<field>` accessor and can select its own `type:`:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class User
|
|
45
|
+
include CanonicalEmails::Extensions
|
|
46
|
+
|
|
47
|
+
attr_accessor :email, :backup_email
|
|
48
|
+
canonical_email :email
|
|
49
|
+
canonical_email :backup_email, type: CanonicalEmails::TaggedGMail
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
user = User.new
|
|
53
|
+
user.email = "Alex.Smith+Newsletter@gmail.com"
|
|
54
|
+
user.backup_email = "Alex.Smith+Backup@gmail.com"
|
|
55
|
+
user.canonical_email.address # "alexsmith@gmail.com"
|
|
56
|
+
user.canonical_backup_email.address # "alexsmith+backup@gmail.com"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each accessor reads its own field without changing the original value. A nil field returns nil.
|
|
60
|
+
|
|
61
|
+
### Custom Accessor Names
|
|
62
|
+
|
|
63
|
+
Use `as:` to choose the generated accessor's name instead of `canonical_<field>`. String and symbol names are supported, and each field can have its own name and type:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class User
|
|
67
|
+
include CanonicalEmails::Extensions
|
|
68
|
+
|
|
69
|
+
attr_accessor :email, :backup_email
|
|
70
|
+
canonical_email :email, as: :normalized_email
|
|
71
|
+
canonical_email :backup_email, as: :normalized_backup, type: CanonicalEmails::TaggedGMail
|
|
24
72
|
end
|
|
25
73
|
|
|
26
74
|
user = User.new
|
|
27
|
-
user.email = "
|
|
28
|
-
user.
|
|
29
|
-
user.
|
|
30
|
-
user.
|
|
31
|
-
```
|
|
75
|
+
user.email = "Alex.Smith+Newsletter@gmail.com"
|
|
76
|
+
user.backup_email = "Alex.Smith+Backup@gmail.com"
|
|
77
|
+
user.normalized_email.address # "alexsmith@gmail.com"
|
|
78
|
+
user.normalized_backup.address # "alexsmith+backup@gmail.com"
|
|
79
|
+
```
|
|
32
80
|
|
|
33
|
-
|
|
81
|
+
Custom names replace the default generated names. When the name differs from the source field, the original reader and writer remain unchanged.
|
|
34
82
|
|
|
35
|
-
####
|
|
83
|
+
#### Override the Original Reader
|
|
36
84
|
|
|
37
|
-
|
|
85
|
+
Use the source field's name with `as:` to return a parsed address directly from that reader:
|
|
38
86
|
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
```ruby
|
|
88
|
+
class User
|
|
89
|
+
include CanonicalEmails::Extensions
|
|
90
|
+
|
|
91
|
+
attr_accessor :email
|
|
92
|
+
canonical_email :email, as: :email
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
user = User.new
|
|
96
|
+
user.email = "Alex.Smith+Newsletter@gmail.com"
|
|
97
|
+
user.email.class # CanonicalEmails::GMail
|
|
98
|
+
user.email.address # "alexsmith@gmail.com"
|
|
99
|
+
user.email.tag # "Newsletter"
|
|
43
100
|
```
|
|
44
101
|
|
|
45
|
-
|
|
102
|
+
The reader now returns an address object (or `nil`) instead of a string. The original writer still stores the submitted value unchanged. The source reader must exist when accessed; inherited and generated readers are supported. `type:` also works with reader overrides.
|
|
46
103
|
|
|
47
|
-
|
|
104
|
+
Raw-value access depends on your model: for the `attr_accessor` example, `user.instance_variable_get(:@email)` returns the original string; in ActiveRecord, use `user[:email]`. No additional raw-value accessor is generated.
|
|
48
105
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
106
|
+
### Parse
|
|
107
|
+
|
|
108
|
+
To parse an address directly without a model, use `CanonicalEmails.parse`:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
email = CanonicalEmails.parse("Alex Smith <Alex.Smith+Newsletter+Offers@GMAIL.COM>")
|
|
112
|
+
email.class # CanonicalEmails::GMail
|
|
113
|
+
email.to_s # "Alex Smith <alexsmith@gmail.com>"
|
|
114
|
+
email.address # "alexsmith@gmail.com"
|
|
115
|
+
email.local # "alexsmith"
|
|
116
|
+
email.domain # "gmail.com"
|
|
117
|
+
email.username # "Alex.Smith+Newsletter+Offers"
|
|
118
|
+
email.tag # "Newsletter+Offers"
|
|
119
|
+
|
|
120
|
+
email = CanonicalEmails.parse("Alex.Smith+Newsletter@EXAMPLE.COM")
|
|
121
|
+
email.class # CanonicalEmails::Address
|
|
122
|
+
email.address # "Alex.Smith+Newsletter@EXAMPLE.COM"
|
|
54
123
|
```
|
|
55
124
|
|
|
56
|
-
|
|
125
|
+
Provider detection matches the complete domain case-insensitively. Unregistered domains return generic addresses. See [Available Providers](#available-providers) for built-in behavior and [Register Providers](#register-providers) for adding your own types.
|
|
126
|
+
|
|
127
|
+
`parse(nil)` returns `nil`. Empty or whitespace-only strings return empty generic addresses. Domainless input remains a generic address. Non-string input raises `TypeError`; malformed input may raise Mail parsing errors.
|
|
128
|
+
|
|
129
|
+
The read-only `username` property preserves the original local part, including case, periods, and plus-tags. `tag` preserves the removed suffix, including case and additional `+` characters, when tag stripping is enabled. A missing tag returns `nil`; an explicitly empty tag returns `""`. Generic addresses have `tag == nil`.
|
|
57
130
|
|
|
58
|
-
|
|
131
|
+
### Available Providers
|
|
59
132
|
|
|
133
|
+
#### Gmail
|
|
134
|
+
|
|
135
|
+
`CanonicalEmails::GMail` is the default Gmail provider type. It registers `gmail.com` through the same registration API available to applications, matching the complete domain case-insensitively. Outlook, `googlemail.com`, and Google Workspace custom domains remain generic unless your application registers them.
|
|
136
|
+
|
|
137
|
+
Gmail normalization removes periods and the first `+` and everything after it from the local part, and lowercases the local part and domain. Display names are preserved. [Gmail period equivalence](https://support.google.com/mail/answer/7436150?hl=en) applies to `gmail.com`, not custom domains.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
email = CanonicalEmails::GMail.parse("Alex Smith <Alex.Smith+Newsletter+Offers@GMAIL.COM>")
|
|
141
|
+
email.class # CanonicalEmails::GMail
|
|
142
|
+
email.to_s # "Alex Smith <alexsmith@gmail.com>"
|
|
143
|
+
email.address # "alexsmith@gmail.com"
|
|
144
|
+
email.username # "Alex.Smith+Newsletter+Offers"
|
|
145
|
+
email.tag # "Newsletter+Offers"
|
|
60
146
|
```
|
|
61
|
-
|
|
147
|
+
|
|
148
|
+
#### Tagged Gmail
|
|
149
|
+
|
|
150
|
+
`CanonicalEmails::TaggedGMail` normalizes Gmail periods and case while retaining the complete plus suffix in the address, lowercased along with the rest of the local part. It is built in but not registered for automatic dispatch; select it explicitly:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
email = CanonicalEmails::TaggedGMail.parse("Alex Smith <Alex.Smith+Newsletter+Offers@GMAIL.COM>")
|
|
154
|
+
email.class # CanonicalEmails::TaggedGMail
|
|
155
|
+
email.to_s # "Alex Smith <alexsmith+newsletter+offers@gmail.com>"
|
|
156
|
+
email.address # "alexsmith+newsletter+offers@gmail.com"
|
|
157
|
+
email.username # "Alex.Smith+Newsletter+Offers"
|
|
158
|
+
email.tag # nil
|
|
62
159
|
```
|
|
63
160
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
161
|
+
It enforces the same Gmail domain restrictions as `GMail`. `tag` remains `nil` because the suffix is not stripped.
|
|
162
|
+
|
|
163
|
+
### Register Providers
|
|
164
|
+
|
|
165
|
+
Register a custom class during application initialization to enable automatic selection in both `CanonicalEmails.parse` and model accessors. For example, an application can define an Outlook type that lowercases the domain while preserving the local part, periods, and plus-tags:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
class Outlook < CanonicalEmails::Address
|
|
169
|
+
include CanonicalEmails::Behaviors::DowncaseDomain
|
|
170
|
+
|
|
171
|
+
def self.accepts?(domain)
|
|
172
|
+
domain&.casecmp?("outlook.com")
|
|
80
173
|
end
|
|
81
174
|
end
|
|
82
175
|
|
|
83
|
-
|
|
84
|
-
email
|
|
176
|
+
CanonicalEmails.register("outlook.com", type: Outlook)
|
|
177
|
+
email = CanonicalEmails.parse("Alex.Smith+Newsletter@OUTLOOK.COM")
|
|
178
|
+
email.class # Outlook
|
|
179
|
+
email.address # "Alex.Smith+Newsletter@outlook.com"
|
|
180
|
+
email.username # "Alex.Smith+Newsletter"
|
|
181
|
+
email.tag # nil
|
|
85
182
|
```
|
|
86
183
|
|
|
87
|
-
|
|
184
|
+
See [Custom Address Types](#custom-address-types) for additional normalization behaviors.
|
|
185
|
+
|
|
186
|
+
Registrations match complete domains case-insensitively, not subdomains or suffixes. The type must subclass `Address` and accept the registered domain. Repeating the same mapping is harmless; a conflicting mapping raises `ArgumentError` without replacing the existing type. Explicit `type:` and class-level `.parse` bypass the registry.
|
|
187
|
+
|
|
188
|
+
Call `CanonicalEmails.clear` during application initialization to remove all registrations, including the default Gmail mapping, before configuring your own provider set. For example, to use `TaggedGMail` for automatic Gmail detection:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
CanonicalEmails.clear
|
|
192
|
+
CanonicalEmails.register("gmail.com", type: CanonicalEmails::TaggedGMail)
|
|
193
|
+
|
|
194
|
+
email = CanonicalEmails.parse("Alex.Smith+Newsletter@gmail.com")
|
|
195
|
+
email.class # CanonicalEmails::TaggedGMail
|
|
196
|
+
email.address # "alexsmith+newsletter@gmail.com"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Defaults are not automatically restored: domains remain generic until registered again. Explicit `type:` and class-level `.parse` continue to work. Configure the full provider set before serving requests.
|
|
200
|
+
|
|
201
|
+
### Explicit Types
|
|
202
|
+
|
|
203
|
+
Use `canonical_email :email, type: CanonicalEmails::Address` to disable provider detection in a model, or select another class with `type:`.
|
|
204
|
+
|
|
205
|
+
Specify `type:` to request an exact address class instead of provider detection:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
email = CanonicalEmails.parse("Alex.Smith+Newsletter@GMAIL.COM", type: CanonicalEmails::Address)
|
|
209
|
+
email.address # "Alex.Smith+Newsletter@GMAIL.COM" (no normalization)
|
|
210
|
+
|
|
211
|
+
email = CanonicalEmails.parse("Alex.Smith+Newsletter@GMAIL.COM", type: CanonicalEmails::Downcase)
|
|
212
|
+
email.address # "alex.smith+newsletter@gmail.com" (lowercase both parts only)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`type:` must be `CanonicalEmails::Address` or one of its subclasses. Classes also expose `.parse`, equivalent to specifying `type:`:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
CanonicalEmails::GMail.parse("Alex.Smith+Newsletter@GMAIL.COM")
|
|
219
|
+
CanonicalEmails::Address.parse("Alex.Smith+Newsletter@GMAIL.COM")
|
|
220
|
+
CanonicalEmails::Downcase.parse("Alex.Smith+Newsletter@EXAMPLE.COM")
|
|
221
|
+
```
|
|
88
222
|
|
|
89
|
-
|
|
223
|
+
An explicit provider class rejects mismatched domains with `ArgumentError`, including empty and domainless input. All `.parse` entry points return `nil` for nil input.
|
|
224
|
+
|
|
225
|
+
Generic addresses preserve local-part and domain casing. Use `type: CanonicalEmails::Downcase` to lowercase both parts regardless of provider.
|
|
226
|
+
|
|
227
|
+
### Assignment
|
|
228
|
+
|
|
229
|
+
#### Model Fields
|
|
230
|
+
|
|
231
|
+
Both separately named and overridden model readers cache their results per accessor and model instance. Each read compares the current raw input against a saved string copy, detecting writer assignments, changes that bypass the writer, and in-place string edits. Unchanged input returns the same address object (or cached nil); changed input is parsed again. Parsing errors propagate rather than returning a stale result. Configure provider registrations during initialization; registry changes alone do not invalidate existing model caches.
|
|
232
|
+
|
|
233
|
+
Canonical address objects do not support `address=`. Assign through the model's original writer instead:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
user.email = "Alex.Smith+Newsletter@gmail.com"
|
|
237
|
+
user.canonical_email.equal?(user.canonical_email) # true
|
|
238
|
+
user.canonical_email.address = "Alex@example.com" # raises NoMethodError
|
|
239
|
+
user.email = "Alex@example.com"
|
|
240
|
+
user.canonical_email.address # "Alex@example.com"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For `as: :email`, use `user.email = ...` to update the stored value. Removing `address=` does not make other inherited Mail APIs read-only.
|
|
244
|
+
|
|
245
|
+
#### Directly Parsed Addresses
|
|
246
|
+
|
|
247
|
+
Parse a new value instead of reassigning a directly parsed address:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
email = CanonicalEmails.parse("Alex.Smith@gmail.com")
|
|
251
|
+
email = CanonicalEmails.parse("Alex Smith <Alex.Smith+Newsletter@GMAIL.COM>")
|
|
252
|
+
email.to_s # "Alex Smith <alexsmith@gmail.com>"
|
|
253
|
+
email.username # "Alex.Smith+Newsletter"
|
|
254
|
+
email.tag # "Newsletter"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Custom Address Types
|
|
258
|
+
|
|
259
|
+
`CanonicalEmails::Address` performs no normalization by default. Include any combination of these mixins to opt in:
|
|
260
|
+
|
|
261
|
+
| Mixin under `CanonicalEmails::Behaviors` | Behavior |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `DowncaseLocal` | Lowercase the local part. |
|
|
264
|
+
| `DowncaseDomain` | Lowercase the domain. |
|
|
265
|
+
| `RemovePeriods` | Remove periods from the local part. |
|
|
266
|
+
| `StripTags` | Remove the first `+` and its suffix, storing the original suffix in `tag`. |
|
|
267
|
+
|
|
268
|
+
`Downcase` includes the two downcasing mixins for all domains. `TaggedGMail` composes both downcasing mixins and `RemovePeriods`; `GMail` adds `StripTags`.
|
|
269
|
+
|
|
270
|
+
Custom types are not automatically registered for provider detection. Override `.accepts?(domain)` to restrict allowed domains; it receives the original parsed domain. Mixins otherwise apply to all domains. Only remove periods or tags where your application has established equivalence. A custom type works directly in a model: `canonical_email :email, type: CustomAddress`.
|
|
271
|
+
|
|
272
|
+
### Validation
|
|
273
|
+
|
|
274
|
+
CanonicalEmails normalizes email addresses; it does not validate them. For Active Model email validations, consider [valid_email](https://github.com/hallelujah/valid_email), configured separately.
|
|
275
|
+
|
|
276
|
+
### Contribute
|
|
90
277
|
|
|
91
|
-
|
|
92
|
-
* Make changes, write tests.
|
|
93
|
-
* Updated CHANGELOG.
|
|
94
|
-
* Make a pull request, bonus points for topic branches.
|
|
278
|
+
See [CONTRIBUTING](CONTRIBUTING.md).
|
|
95
279
|
|
|
96
280
|
### Copyright and License
|
|
97
281
|
|
|
98
|
-
Copyright Daniel Doubrovkine and Contributors, Artsy
|
|
282
|
+
Copyright Daniel Doubrovkine and Contributors, [Artsy](https://artsy.net), 2013-2014
|
|
99
283
|
|
|
100
284
|
[MIT License](LICENSE.md)
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Upgrading CanonicalEmails
|
|
2
|
+
|
|
3
|
+
## From 0.1.x to 0.2.0
|
|
4
|
+
|
|
5
|
+
Requires Ruby 3.1 or newer. Both `require 'canonical-emails'` and `require 'canonical_emails'` remain supported.
|
|
6
|
+
|
|
7
|
+
### Replace transform with parse
|
|
8
|
+
|
|
9
|
+
Names below are relative to `CanonicalEmails`.
|
|
10
|
+
|
|
11
|
+
| Before | After |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `GMail.transform(value)` | `CanonicalEmails.parse(value)` |
|
|
14
|
+
| `Downcase.transform(value)` | `Downcase.parse(value)` |
|
|
15
|
+
| `canonical_email :email, GMail` | `canonical_email :email` |
|
|
16
|
+
| `canonical_email :email, Downcase` | `canonical_email :email, type: Downcase` |
|
|
17
|
+
| `email.class == Mail::Address` | `email.is_a?(Mail::Address)` |
|
|
18
|
+
|
|
19
|
+
`GMail` and `Downcase` are now subclasses of `CanonicalEmails::Address < Mail::Address`. Automatic parsing selects `GMail` only for `gmail.com`; other domains return an unchanged `Address`. Explicit `GMail.parse` rejects other domains with `ArgumentError`.
|
|
20
|
+
|
|
21
|
+
### Gmail plus-tags are removed
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Before
|
|
25
|
+
CanonicalEmails::GMail.transform("Alex.Smith+Newsletter@gmail.com").address
|
|
26
|
+
# => "alexsmith+newsletter@gmail.com"
|
|
27
|
+
|
|
28
|
+
# After
|
|
29
|
+
email = CanonicalEmails.parse("Alex.Smith+Newsletter@gmail.com")
|
|
30
|
+
email.address # => "alexsmith@gmail.com"
|
|
31
|
+
email.username # => "Alex.Smith+Newsletter"
|
|
32
|
+
email.tag # => "Newsletter"
|
|
33
|
+
|
|
34
|
+
# To retain plus-tags
|
|
35
|
+
CanonicalEmails::TaggedGMail.parse("Alex.Smith+Newsletter@gmail.com").address
|
|
36
|
+
# => "alexsmith+newsletter@gmail.com"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Check stored canonical addresses and unique indexes for collisions before backfilling.** Previously distinct Gmail values may now canonicalize to the same address.
|
|
40
|
+
|
|
41
|
+
### Handle nil and reject non-string input
|
|
42
|
+
|
|
43
|
+
| Before | After |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `GMail.transform(nil)` returns an empty address | `CanonicalEmails.parse(nil)` returns `nil` |
|
|
46
|
+
| Model accessors stringify input before transformation | Input must be a String or nil; other types raise `TypeError` |
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Before
|
|
50
|
+
user.canonical_email.address
|
|
51
|
+
|
|
52
|
+
# After: the source field may be nil
|
|
53
|
+
user.canonical_email&.address
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Empty and domainless strings remain generic addresses with default parsing. Mail parsing errors propagate.
|
|
57
|
+
|
|
58
|
+
### Replace address assignment
|
|
59
|
+
|
|
60
|
+
`address=` has been removed and raises `NoMethodError`.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Before
|
|
64
|
+
email.address = "Alex@example.com"
|
|
65
|
+
|
|
66
|
+
# After: standalone address
|
|
67
|
+
email = CanonicalEmails.parse("Alex@example.com")
|
|
68
|
+
|
|
69
|
+
# After: model field
|
|
70
|
+
user.email = "Alex@example.com"
|
|
71
|
+
user.canonical_email.address # => "Alex@example.com"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Model readers now return the same parsed object while the raw field value is unchanged, including nil results. Assignments and in-place string edits refresh the cache; provider registry changes alone do not.
|
|
75
|
+
|
|
76
|
+
### Replace transformer pipelines with one address type
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# Before: custom transformers applied in sequence
|
|
80
|
+
canonical_email :email, RemovePeriods, CanonicalEmails::Downcase
|
|
81
|
+
|
|
82
|
+
# After: select one class with the required behaviors
|
|
83
|
+
class CustomAddress < CanonicalEmails::Address
|
|
84
|
+
include CanonicalEmails::Behaviors::RemovePeriods
|
|
85
|
+
include CanonicalEmails::Behaviors::DowncaseLocal
|
|
86
|
+
include CanonicalEmails::Behaviors::DowncaseDomain
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
canonical_email :email, type: CustomAddress
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Move custom `.transform` logic into address subclasses or behavior mixins. Restrict provider-specific rules with `.accepts?(domain)` and register types during initialization for automatic dispatch. See [Custom Address Types](README.md#custom-address-types) and [Register Providers](README.md#register-providers).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalEmails
|
|
4
|
+
# An unchanged Mail address with original local-part metadata.
|
|
5
|
+
class Address < Mail::Address
|
|
6
|
+
attr_reader :username, :tag
|
|
7
|
+
|
|
8
|
+
undef_method :address=
|
|
9
|
+
|
|
10
|
+
def self.parse(value)
|
|
11
|
+
CanonicalEmails.parse(value, type: self)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.accepts?(_domain)
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(value = nil)
|
|
19
|
+
super
|
|
20
|
+
raise ArgumentError, "#{self.class} does not support domain #{get_domain.inspect}" unless self.class.accepts?(get_domain)
|
|
21
|
+
|
|
22
|
+
update_properties
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def update_properties
|
|
28
|
+
@username = get_local
|
|
29
|
+
@tag = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalEmails
|
|
4
|
+
# Opt-in transformations for Address subclasses.
|
|
5
|
+
module Behaviors
|
|
6
|
+
# Lowercases the local part.
|
|
7
|
+
module DowncaseLocal
|
|
8
|
+
def local(*)
|
|
9
|
+
super&.downcase
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Lowercases the domain.
|
|
14
|
+
module DowncaseDomain
|
|
15
|
+
def domain(*)
|
|
16
|
+
super&.downcase
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Removes periods from the local part.
|
|
21
|
+
module RemovePeriods
|
|
22
|
+
def local(*)
|
|
23
|
+
super&.delete('.')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Removes the plus suffix while retaining it in the tag property.
|
|
28
|
+
module StripTags
|
|
29
|
+
def local(*)
|
|
30
|
+
value = super
|
|
31
|
+
return unless value
|
|
32
|
+
|
|
33
|
+
value.split('+', 2).first
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def update_properties
|
|
39
|
+
super
|
|
40
|
+
@tag = username.split('+', 2).at(1) if username
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
module Downcase
|
|
3
|
-
|
|
4
|
-
def self.transform(value)
|
|
5
|
-
Mail::Address.new(value).tap do |email|
|
|
6
|
-
email.instance_eval do
|
|
7
|
-
def get_local
|
|
8
|
-
value = super
|
|
9
|
-
value.downcase if value
|
|
10
|
-
end
|
|
11
|
-
def domain
|
|
12
|
-
value = super
|
|
13
|
-
value.downcase if value
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end if value
|
|
17
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
18
2
|
|
|
3
|
+
module CanonicalEmails
|
|
4
|
+
# Normalizes the local part and domain without changing the display name.
|
|
5
|
+
class Downcase < Address
|
|
6
|
+
include Behaviors::DowncaseLocal
|
|
7
|
+
include Behaviors::DowncaseDomain
|
|
19
8
|
end
|
|
20
|
-
end
|
|
9
|
+
end
|
|
@@ -1,17 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CanonicalEmails
|
|
4
|
+
# Adds lazily evaluated canonical address accessors to application models.
|
|
2
5
|
module Extensions
|
|
3
6
|
extend ActiveSupport::Concern
|
|
4
7
|
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def cached_canonical_email(key, raw, type)
|
|
11
|
+
@canonical_email_cache ||= {}
|
|
12
|
+
cached = @canonical_email_cache[key]
|
|
13
|
+
return cached.last if cached && cached.first == raw
|
|
14
|
+
|
|
15
|
+
address = CanonicalEmails.parse(raw, type: type)
|
|
16
|
+
@canonical_email_cache[key] = [raw&.dup, address]
|
|
17
|
+
address
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Defines provider-aware canonical address accessors.
|
|
5
21
|
module ClassMethods
|
|
6
|
-
def canonical_email(field,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
def canonical_email(field, type: nil, as: :"canonical_#{field}")
|
|
23
|
+
raise ArgumentError, 'as must be a nonempty String or Symbol' unless (as.is_a?(String) || as.is_a?(Symbol)) && !as.to_s.empty?
|
|
24
|
+
|
|
25
|
+
key = Object.new
|
|
26
|
+
if as.to_s == field.to_s
|
|
27
|
+
@canonical_email_readers ||= Module.new.tap { |readers| prepend readers }
|
|
28
|
+
@canonical_email_readers.define_method(as) do
|
|
29
|
+
cached_canonical_email(key, super(), type)
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
define_method as do
|
|
33
|
+
cached_canonical_email(key, send(field), type)
|
|
11
34
|
end
|
|
12
|
-
value
|
|
13
35
|
end
|
|
14
36
|
end
|
|
15
37
|
end
|
|
16
38
|
end
|
|
17
|
-
end
|
|
39
|
+
end
|
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
module GMail
|
|
3
|
-
|
|
4
|
-
def self.transform(value)
|
|
5
|
-
Mail::Address.new(value).tap do |email|
|
|
6
|
-
if email.domain && [ "gmail.com" ].include?(email.domain.downcase)
|
|
7
|
-
email.instance_eval do
|
|
8
|
-
def get_local
|
|
9
|
-
value = super
|
|
10
|
-
value.gsub(".", "").downcase if value
|
|
11
|
-
end
|
|
12
|
-
def domain
|
|
13
|
-
value = super
|
|
14
|
-
value.downcase if value
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end if value
|
|
18
|
-
end
|
|
19
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
20
2
|
|
|
3
|
+
module CanonicalEmails
|
|
4
|
+
# Removes periods and plus tags and normalizes case for gmail.com addresses only.
|
|
5
|
+
class GMail < TaggedGMail
|
|
6
|
+
include Behaviors::StripTags
|
|
21
7
|
end
|
|
22
|
-
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalEmails
|
|
4
|
+
# Normalizes Gmail periods and case while retaining plus-tags.
|
|
5
|
+
class TaggedGMail < Address
|
|
6
|
+
include Behaviors::DowncaseLocal
|
|
7
|
+
include Behaviors::DowncaseDomain
|
|
8
|
+
include Behaviors::RemovePeriods
|
|
9
|
+
|
|
10
|
+
def self.accepts?(domain)
|
|
11
|
+
domain&.casecmp?('gmail.com')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/canonical-emails.rb
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'mail'
|
|
2
|
-
require 'active_support'
|
|
4
|
+
require 'active_support/concern'
|
|
3
5
|
|
|
4
6
|
require 'canonical-emails/version'
|
|
7
|
+
require 'canonical-emails/address'
|
|
8
|
+
require 'canonical-emails/behaviors'
|
|
5
9
|
require 'canonical-emails/downcase'
|
|
10
|
+
require 'canonical-emails/tagged_gmail'
|
|
6
11
|
require 'canonical-emails/gmail'
|
|
7
12
|
require 'canonical-emails/extensions'
|
|
13
|
+
|
|
14
|
+
# Parses addresses into provider-aware canonical forms.
|
|
15
|
+
module CanonicalEmails
|
|
16
|
+
@types = {}
|
|
17
|
+
@types_mutex = Mutex.new
|
|
18
|
+
|
|
19
|
+
def self.clear
|
|
20
|
+
@types_mutex.synchronize { @types.clear }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.register(domain, type:)
|
|
24
|
+
raise ArgumentError, 'domain must be a nonempty domain String without whitespace or @' unless domain.is_a?(String) && !domain.empty? && !domain.match?(/[\s@]/)
|
|
25
|
+
raise ArgumentError, 'type must be a CanonicalEmails::Address subclass' unless type.is_a?(Class) && type <= Address
|
|
26
|
+
|
|
27
|
+
domain = domain.downcase
|
|
28
|
+
raise ArgumentError, "#{type} does not accept #{domain}" unless type.accepts?(domain)
|
|
29
|
+
|
|
30
|
+
@types_mutex.synchronize do
|
|
31
|
+
existing = @types[domain]
|
|
32
|
+
raise ArgumentError, "#{domain} is already registered to #{existing}" if existing && existing != type
|
|
33
|
+
|
|
34
|
+
@types[domain] = type
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.parse(value, type: nil)
|
|
39
|
+
raise ArgumentError, 'type must be a CanonicalEmails::Address subclass' unless type.nil? || (type.is_a?(Class) && type <= Address)
|
|
40
|
+
return if value.nil?
|
|
41
|
+
|
|
42
|
+
raise TypeError, 'address must be a String or nil' unless value.is_a?(String)
|
|
43
|
+
|
|
44
|
+
data = Mail::Parsers::AddressListsParser.parse(value).addresses.first unless value.strip.empty?
|
|
45
|
+
type ||= @types_mutex.synchronize { @types.fetch(data&.domain&.downcase, Address) }
|
|
46
|
+
type.new(data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
register('gmail.com', type: GMail)
|
|
50
|
+
end
|
data/lib/canonical_emails.rb
CHANGED
metadata
CHANGED
|
@@ -1,107 +1,84 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canonical-emails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
5
|
-
prerelease:
|
|
4
|
+
version: 0.2.0
|
|
6
5
|
platform: ruby
|
|
7
6
|
authors:
|
|
8
7
|
- Daniel Doubrovkine
|
|
9
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
|
-
name:
|
|
14
|
+
name: activesupport
|
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
|
17
|
-
none: false
|
|
18
16
|
requirements:
|
|
19
|
-
- -
|
|
17
|
+
- - ">="
|
|
20
18
|
- !ruby/object:Gem::Version
|
|
21
19
|
version: '0'
|
|
22
20
|
type: :runtime
|
|
23
21
|
prerelease: false
|
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
-
none: false
|
|
26
23
|
requirements:
|
|
27
|
-
- -
|
|
24
|
+
- - ">="
|
|
28
25
|
- !ruby/object:Gem::Version
|
|
29
26
|
version: '0'
|
|
30
27
|
- !ruby/object:Gem::Dependency
|
|
31
|
-
name:
|
|
28
|
+
name: mail
|
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
|
33
|
-
none: false
|
|
34
30
|
requirements:
|
|
35
|
-
- -
|
|
31
|
+
- - ">="
|
|
36
32
|
- !ruby/object:Gem::Version
|
|
37
33
|
version: '0'
|
|
38
34
|
type: :runtime
|
|
39
35
|
prerelease: false
|
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
-
none: false
|
|
42
37
|
requirements:
|
|
43
|
-
- -
|
|
38
|
+
- - ">="
|
|
44
39
|
- !ruby/object:Gem::Version
|
|
45
40
|
version: '0'
|
|
46
|
-
description:
|
|
41
|
+
description:
|
|
47
42
|
email: dblock@dblock.org
|
|
48
43
|
executables: []
|
|
49
44
|
extensions: []
|
|
50
45
|
extra_rdoc_files: []
|
|
51
46
|
files:
|
|
52
|
-
- .gitignore
|
|
53
|
-
- .rspec
|
|
54
|
-
- .travis.yml
|
|
55
47
|
- CHANGELOG.md
|
|
56
|
-
- Gemfile
|
|
57
48
|
- LICENSE.md
|
|
58
49
|
- README.md
|
|
59
|
-
-
|
|
60
|
-
- canonical-emails.gemspec
|
|
50
|
+
- UPGRADING.md
|
|
61
51
|
- lib/canonical-emails.rb
|
|
52
|
+
- lib/canonical-emails/address.rb
|
|
53
|
+
- lib/canonical-emails/behaviors.rb
|
|
62
54
|
- lib/canonical-emails/downcase.rb
|
|
63
55
|
- lib/canonical-emails/extensions.rb
|
|
64
56
|
- lib/canonical-emails/gmail.rb
|
|
57
|
+
- lib/canonical-emails/tagged_gmail.rb
|
|
65
58
|
- lib/canonical-emails/version.rb
|
|
66
59
|
- lib/canonical_emails.rb
|
|
67
|
-
|
|
68
|
-
- spec/canonical-emails/extensions_spec.rb
|
|
69
|
-
- spec/canonical-emails/fixtures/downcase.yml
|
|
70
|
-
- spec/canonical-emails/fixtures/extensions.yml
|
|
71
|
-
- spec/canonical-emails/fixtures/gmail.yml
|
|
72
|
-
- spec/canonical-emails/gmail_spec.rb
|
|
73
|
-
- spec/canonical-emails/version_spec.rb
|
|
74
|
-
- spec/shared/extension_examples.rb
|
|
75
|
-
- spec/shared/transform_examples.rb
|
|
76
|
-
- spec/spec_helper.rb
|
|
77
|
-
- spec/support/reverse.rb
|
|
78
|
-
- spec/support/yml_examples.rb
|
|
79
|
-
homepage: http://github.com/dblock/canonical-emails
|
|
60
|
+
homepage: https://github.com/dblock/canonical-emails
|
|
80
61
|
licenses:
|
|
81
62
|
- MIT
|
|
82
|
-
|
|
63
|
+
metadata:
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
post_install_message:
|
|
83
66
|
rdoc_options: []
|
|
84
67
|
require_paths:
|
|
85
68
|
- lib
|
|
86
69
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
-
none: false
|
|
88
70
|
requirements:
|
|
89
|
-
- -
|
|
71
|
+
- - ">="
|
|
90
72
|
- !ruby/object:Gem::Version
|
|
91
|
-
version: '
|
|
92
|
-
segments:
|
|
93
|
-
- 0
|
|
94
|
-
hash: 247467439
|
|
73
|
+
version: '3.1'
|
|
95
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
-
none: false
|
|
97
75
|
requirements:
|
|
98
|
-
- -
|
|
76
|
+
- - ">="
|
|
99
77
|
- !ruby/object:Gem::Version
|
|
100
78
|
version: 1.3.6
|
|
101
79
|
requirements: []
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
specification_version: 3
|
|
80
|
+
rubygems_version: 3.5.16
|
|
81
|
+
signing_key:
|
|
82
|
+
specification_version: 4
|
|
106
83
|
summary: Combine email validation and transformations to produce canonical email addresses.
|
|
107
84
|
test_files: []
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
require 'rubygems'
|
|
2
|
-
require 'bundler/gem_tasks'
|
|
3
|
-
|
|
4
|
-
Bundler.setup :default, :development
|
|
5
|
-
|
|
6
|
-
require 'rspec/core'
|
|
7
|
-
require 'rspec/core/rake_task'
|
|
8
|
-
|
|
9
|
-
RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
10
|
-
spec.pattern = FileList["spec/**/*_spec.rb"]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
task :default => :spec
|
data/canonical-emails.gemspec
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
$:.push File.expand_path("../lib", __FILE__)
|
|
2
|
-
require 'canonical-emails/version'
|
|
3
|
-
|
|
4
|
-
Gem::Specification.new do |s|
|
|
5
|
-
s.name = "canonical-emails"
|
|
6
|
-
s.version = CanonicalEmails::VERSION
|
|
7
|
-
s.authors = [ "Daniel Doubrovkine" ]
|
|
8
|
-
s.email = "dblock@dblock.org"
|
|
9
|
-
s.platform = Gem::Platform::RUBY
|
|
10
|
-
s.required_rubygems_version = '>= 1.3.6'
|
|
11
|
-
s.files = `git ls-files`.split("\n")
|
|
12
|
-
s.require_paths = [ "lib" ]
|
|
13
|
-
s.homepage = "http://github.com/dblock/canonical-emails"
|
|
14
|
-
s.licenses = [ "MIT" ]
|
|
15
|
-
s.summary = "Combine email validation and transformations to produce canonical email addresses."
|
|
16
|
-
s.add_dependency "mail"
|
|
17
|
-
s.add_dependency "activesupport"
|
|
18
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
-
|
|
2
|
-
source: Donald <donald@example.org>
|
|
3
|
-
name: Donald
|
|
4
|
-
local: donald
|
|
5
|
-
domain: example.org
|
|
6
|
-
-
|
|
7
|
-
source: Donald < donald@example.org >
|
|
8
|
-
name: Donald
|
|
9
|
-
local: donald
|
|
10
|
-
domain: example.org
|
|
11
|
-
-
|
|
12
|
-
source: donald@example.org
|
|
13
|
-
local: donald
|
|
14
|
-
domain: example.org
|
|
15
|
-
-
|
|
16
|
-
source: ! ' donald@example.org'
|
|
17
|
-
local: donald
|
|
18
|
-
domain: example.org
|
|
19
|
-
-
|
|
20
|
-
source: donald@eXaMPLe.org
|
|
21
|
-
local: donald
|
|
22
|
-
domain: example.org
|
|
23
|
-
-
|
|
24
|
-
source: DONALD@eXaMPLe.org
|
|
25
|
-
local: donald
|
|
26
|
-
domain: example.org
|
|
27
|
-
-
|
|
28
|
-
source: DoNaLD@example.org
|
|
29
|
-
local: donald
|
|
30
|
-
domain: example.org
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
-
|
|
2
|
-
email: Donald Duck <DoNaLD.DuCK@GMaiL.CoM>
|
|
3
|
-
transforms:
|
|
4
|
-
- CanonicalEmails::Downcase
|
|
5
|
-
canonical_email: Donald Duck <donald.duck@gmail.com>
|
|
6
|
-
-
|
|
7
|
-
email: Donald Duck <DoNaLD.DuCK@GMaiL.CoM>
|
|
8
|
-
transforms:
|
|
9
|
-
- CanonicalEmails::GMail
|
|
10
|
-
canonical_email: Donald Duck <donaldduck@gmail.com>
|
|
11
|
-
-
|
|
12
|
-
email: Donald Duck <DoNaLD.DuCK@GMaiL.CoM>
|
|
13
|
-
transforms:
|
|
14
|
-
- CanonicalEmails::GMail
|
|
15
|
-
- CanonicalEmails::Reverse
|
|
16
|
-
canonical_email: Donald Duck <kcuddlanod@moc.liamg>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
-
|
|
2
|
-
source: Donald Duck <example@example.org>
|
|
3
|
-
name: Donald Duck
|
|
4
|
-
local: example
|
|
5
|
-
domain: example.org
|
|
6
|
-
-
|
|
7
|
-
source: donald@example.org
|
|
8
|
-
local: donald
|
|
9
|
-
domain: example.org
|
|
10
|
-
-
|
|
11
|
-
source: donald@eXaMPLe.org
|
|
12
|
-
local: donald
|
|
13
|
-
domain: eXaMPLe.org
|
|
14
|
-
-
|
|
15
|
-
source: donald.duck@gmail.com
|
|
16
|
-
local: donaldduck
|
|
17
|
-
domain: gmail.com
|
|
18
|
-
-
|
|
19
|
-
source: donaldduck@gmail.com
|
|
20
|
-
local: donaldduck
|
|
21
|
-
domain: gmail.com
|
|
22
|
-
-
|
|
23
|
-
source: donald.duck@gMaiL.com
|
|
24
|
-
local: donaldduck
|
|
25
|
-
domain: gmail.com
|
|
26
|
-
-
|
|
27
|
-
source: donald.duck@google.com
|
|
28
|
-
local: donald.duck
|
|
29
|
-
domain: google.com
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
shared_examples 'extension fixture_examples' do
|
|
2
|
-
yml_examples.each do |example|
|
|
3
|
-
it "#{example['transforms'].join(', ')}" do
|
|
4
|
-
klass = Class.new do
|
|
5
|
-
include CanonicalEmails::Extensions
|
|
6
|
-
attr_accessor :email
|
|
7
|
-
canonical_email :email, * example['transforms'].map(&:constantize)
|
|
8
|
-
end
|
|
9
|
-
user = klass.new
|
|
10
|
-
user.email = example['email']
|
|
11
|
-
user.canonical_email.to_s.should == example['canonical_email']
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
shared_examples 'transform fixture_examples' do
|
|
2
|
-
yml_examples.each do |example|
|
|
3
|
-
it "transforms '#{example['source']}'" do
|
|
4
|
-
subject.transform(example['source']).tap do |email|
|
|
5
|
-
email.local.should == example['local']
|
|
6
|
-
email.domain.should == example['domain']
|
|
7
|
-
email.name.should == example['name']
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
data/spec/spec_helper.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
2
|
-
|
|
3
|
-
require 'rubygems'
|
|
4
|
-
require 'rspec'
|
|
5
|
-
require 'canonical-emails'
|
|
6
|
-
require 'yaml'
|
|
7
|
-
|
|
8
|
-
[ "support", "shared" ].each do |path|
|
|
9
|
-
Dir["#{File.dirname(__FILE__)}/#{path}/**/*.rb"].each do |f|
|
|
10
|
-
require f
|
|
11
|
-
end
|
|
12
|
-
end
|
data/spec/support/reverse.rb
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module CanonicalEmails
|
|
2
|
-
module Reverse
|
|
3
|
-
|
|
4
|
-
def self.transform(value)
|
|
5
|
-
Mail::Address.new(value).tap do |email|
|
|
6
|
-
email.instance_eval do
|
|
7
|
-
def get_local
|
|
8
|
-
super.reverse
|
|
9
|
-
end
|
|
10
|
-
def domain
|
|
11
|
-
super.reverse
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
def yml_examples
|
|
2
|
-
caller.each do |c|
|
|
3
|
-
c =~ /(.*):(\d+)/
|
|
4
|
-
dir_name = File.dirname($1)
|
|
5
|
-
file_name = File.basename($1)
|
|
6
|
-
next unless file_name.end_with?("_spec.rb")
|
|
7
|
-
fixture_file = "#{file_name.split('_spec').first}.yml"
|
|
8
|
-
return YAML::load(File.open([ dir_name, 'fixtures', fixture_file ].join('/')))
|
|
9
|
-
end
|
|
10
|
-
end
|