1 """
2 Confirmation handling API that helps you get the whole confirm/pending/verify
3 process correct. It doesn't implement any handlers, but what it does do is
4 provide the logic for doing the following:
5
6 * Take an email, put it in a "pending" queue, and then send out a confirm
7 email with a strong random id.
8 * Store the pending message ID and the random secret someplace for later
9 verification.
10 * Verify an incoming email against the expected ID, and get back the
11 original.
12
13 You then just work this into your project's state flow, write your own
14 templates, and possibly write your own storage.
15 """
16
17 import uuid
18 from lamson import queue, view
19 from email.utils import parseaddr
20
22 """
23 This is the basic confirmation storage. For simple testing purposes
24 you can just use the default hash db parameter. If you do a deployment
25 you can probably get away with a shelf hash instead.
26
27 You can write your own version of this and use it. The confirmation engine
28 only cares that it gets something that supports all of these methods.
29 """
31 """
32 Change the db parameter to a shelf to get persistent storage.
33 """
34 self.confirmations = db
35
37 """
38 Used primarily in testing, this clears out all pending confirmations.
39 """
40 self.confirmations.clear()
41
42 - def key(self, target, from_address):
43 """
44 Used internally to construct a string key, if you write
45 your own you don't need this.
46
47 NOTE: To support proper equality and shelve storage, this encodes the
48 key into ASCII. Make a different subclass if you need unicode and your
49 storage supports it.
50 """
51 key = target + ':' + from_address
52
53 return key.encode('ascii')
54
55 - def get(self, target, from_address):
56 """
57 Given a target and a from address, this returns a tuple of (expected_secret, pending_message_id).
58 If it doesn't find that target+from_address, then it should return a (None, None) tuple.
59 """
60 return self.confirmations.get(self.key(target, from_address), (None, None))
61
62 - def delete(self, target, from_address):
63 """
64 Removes a target+from_address from the storage.
65 """
66 try:
67 del self.confirmations[self.key(target, from_address)]
68 except KeyError:
69 pass
70
71 - def store(self, target, from_address, expected_secret, pending_message_id):
72 """
73 Given a target, from_address it will store the expected_secret and pending_message_id
74 of later verification. The target should be a string indicating what is being
75 confirmed. Like "subscribe", "post", etc.
76
77 When implementing your own you should *never* allow more than one target+from_address
78 combination.
79 """
80 self.confirmations[self.key(target, from_address)] = (expected_secret,
81 pending_message_id)
82
84 """
85 The confirmation engine is what does the work of sending a confirmation,
86 and verifying that it was confirmed properly. In order to use it you
87 have to construct the ConfirmationEngine (usually in config/settings.py) and
88 you write your confirmation message templates for sending.
89
90 The primary methods you use are ConfirmationEngine.send and ConfirmationEngine.verify.
91 """
92 - def __init__(self, pending_queue, storage):
93 """
94 The pending_queue should be a string with the path to the lamson.queue.Queue
95 that will store pending messages. These messages are the originals the user
96 sent when they tried to confirm.
97
98 Storage should be something that is like ConfirmationStorage so that this
99 can store things for later verification.
100 """
101 self.pending = queue.Queue(pending_queue)
102 self.storage = storage
103
105 """
106 Returns the pending message for the given ID.
107 """
108 return self.pending.get(pending_id)
109
111 """
112 Puts a pending message into the pending queue.
113 """
114 return self.pending.push(message)
115
117 """
118 Removes the pending message from the pending queue.
119 """
120 self.pending.remove(pending_id)
121
122
123 - def cancel(self, target, from_address, expect_secret):
124 """
125 Used to cancel a pending confirmation.
126 """
127 name, addr = parseaddr(from_address)
128
129 secret, pending_id = self.storage.get(target, addr)
130
131 if secret == expect_secret:
132 self.storage.delete(target, addr)
133 self.delete_pending(pending_id)
134
136 """
137 Generates a random uuid as the secret, in hex form.
138 """
139 return uuid.uuid4().hex
140
142 """
143 Don't call this directly unless you know what you are doing.
144 It does the job of registering the original message and the
145 expected confirmation into the storage.
146 """
147 from_address = message.route_from
148
149 pending_id = self.push_pending(message)
150 secret = self.make_random_secret()
151 self.storage.store(target, from_address, secret, pending_id)
152
153 return "%s-confirm-%s" % (target, secret)
154
155 - def verify(self, target, from_address, expect_secret):
156 """
157 Given a target (i.e. "subscribe", "post", etc), a from_address
158 of someone trying to confirm, and the secret they should use, this
159 will try to verify their confirmation. If the verify works then
160 you'll get the original message back to do what you want with.
161
162 If the verification fails then you are given None.
163
164 The message is *not* deleted from the pending queue. You can do
165 that yourself with delete_pending.
166 """
167 assert expect_secret, "Must give an expected ID number."
168 name, addr = parseaddr(from_address)
169
170 secret, pending_id = self.storage.get(target, addr)
171
172 if secret == expect_secret:
173 self.storage.delete(target, addr)
174 return self.get_pending(pending_id)
175 else:
176 return None
177
178 - def send(self, relay, target, message, template, vars):
179 """
180 This is the method you should use to send out confirmation messages.
181 You give it the relay, a target (i.e. "subscribe"), the message they
182 sent requesting the confirm, your confirmation template, and any
183 vars that template needs.
184
185 The result of calling this is that the template message gets sent through
186 the relay, the original message is stored in the pending queue, and
187 data is put into the storage for later calls to verify.
188 """
189 confirm_address = self.register(target, message)
190 vars.update(locals())
191 msg = view.respond(vars, template, To=message['from'],
192 From="%(confirm_address)s@%(host)s",
193 Subject="Confirmation required")
194
195 msg['Reply-To'] = "%(confirm_address)s@%(host)s" % vars
196
197 relay.deliver(msg)
198
200 """
201 Used in testing to make sure there's nothing in the pending
202 queue or storage.
203 """
204 self.pending.clear()
205 self.storage.clear()
206