1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
|
require 'login_format_validation'
require 'local_email'
#
# Identity states:
#
# DISABLED -- An identity is disabled if and only if its associated user
# is also disabled. In the disabled state, incoming email
# should bounce and outgoing email should not be relayed.
#
# ORPHANED -- An identity is orphaned if it has lost its association
# with a user account. This is in order to keep the name
# reserved to prevent anyone else from using it.
#
class Identity < CouchRest::Model::Base
include LoginFormatValidation
use_database :identities
belongs_to :user
property :address, LocalEmail
property :destination, Email
property :keys, HashWithIndifferentAccess
property :cert_fingerprints, Hash
property :disabled_cert_fingerprints, Hash
property :enabled, TrueClass, :default => true
validates :address, presence: true
validate :address_available
validates :destination, presence: true, if: :user_id
validates :destination, uniqueness: {scope: :address}
validate :address_local_email
validate :destination_email
design do
own_path = Pathname.new(File.dirname(__FILE__))
load_views(own_path.join('..', 'designs', 'identity'), nil)
view :by_user_id
view :by_address_and_destination
view :by_address
end
def self.address_starts_with(query)
self.by_address.startkey(query).endkey(query + "\ufff0")
end
def self.for(user, attributes = {})
find_for(user, attributes) || build_for(user, attributes)
end
def self.find_for(user, attributes = {})
attributes.reverse_merge! attributes_from_user(user)
id = find_by_address_and_destination attributes.values_at(:address, :destination)
return id if id && id.user == user
end
def self.build_for(user, attributes = {})
attributes.reverse_merge! attributes_from_user(user)
Identity.new(attributes)
end
def self.create_for(user, attributes = {})
identity = build_for(user, attributes)
identity.save
identity
end
# currently leap_mx ignores enabled property, so we
# also disable the fingerprints instead of just marking
# identity as disabled.
def disable!
self.disabled_cert_fingerprints = self.cert_fingerprints
self.cert_fingerprints = {}
self.write_attribute(:enabled, false)
self.save
end
def enable!
self.cert_fingerprints = self.disabled_cert_fingerprints
self.disabled_cert_fingerprints = nil
self.write_attribute(:enabled, true)
self.save
end
# removes the association between this identity and the user.
def orphan!
self.destination = nil
self.user_id = nil
self.disable!
end
def self.destroy_all_orphaned
Identity.orphaned.each do |identity|
identity.destroy
end
end
def self.attributes_from_user(user)
{ user_id: user.id,
address: user.email_address,
destination: user.email_address
}
end
def status
if !enabled? || orphaned?
return :blocked
else
case destination
when address
:main_email
when /@#{APP_CONFIG[:domain]}\Z/i,
:alias
else
:forward
end
end
end
def actions
if !orphaned?
[] # [:show, :edit]
else
[:destroy]
end
end
def keys
read_attribute('keys') || HashWithIndifferentAccess.new
end
def set_key(type, key)
return if keys[type] == key.to_s
write_attribute('keys', keys.merge(type => key.to_s))
end
def cert_fingerprints
read_attribute('cert_fingerprints') || Hash.new
end
def register_cert(cert)
expiry = cert.expiry.to_date.to_s
write_attribute 'cert_fingerprints',
cert_fingerprints.merge(cert.fingerprint => expiry)
end
# for LoginFormatValidation
def login
address.handle if address.present?
end
def orphaned?
self.user_id.nil?
end
def self.orphaned
# the "disabled" view is a misnomer. it returns
# identities that have been orphaned, not identities that
# have been disabled.
# TODO: fix the view name
Identity.disabled
end
protected
def address_available
blocking_identities = Identity.by_address.key(address).all
blocking_identities.delete self
if self.user
blocking_identities.reject! { |other| other.user == self.user }
end
if blocking_identities.any?
errors.add :address, :taken
end
end
def address_local_email
# caught by presence validation
return if address.blank?
return if address.valid?
address.errors.each do |attribute, error|
self.errors.add(:address, error)
end
end
def destination_email
# caught by presence validation or this identity is disabled
return if destination.blank?
return if destination.valid?
destination.errors.each do |attribute, error|
self.errors.add(:destination, error)
end
end
ActiveSupport.run_load_hooks(:identity, self)
end
|