35
36:- module(swish_email,
37 [ smtp_send_mail/3, 38 smtp_send_html/3, 39
40 dear//1, 41 signature//0,
42 profile_name//1, 43 email_action_link//4, 44
45 email_style//0, 46
47 email_cleanup_db/0,
48
49 public_url/4 50 ]). 51:- use_module(library(smtp)). 52:- use_module(library(option)). 53:- use_module(library(settings)). 54:- use_module(library(base64)). 55:- use_module(library(http/http_dispatch)). 56:- use_module(library(http/http_host)). 57:- use_module(library(http/html_write)). 58:- use_module(library(apply)). 59:- use_module(library(random)). 60:- use_module(library(persistency)). 61:- use_module(library(broadcast)). 62:- use_module(library(user_profile)). 63
69
70:- html_meta
71 smtp_send_html(+, html, +),
72 email_action_link(html, 1, 0, +, ?, ?). 73
74:- setting(timeout, integer, 24*3600*7,
75 "Timeout for handling email reply"). 76:- setting(database, callable, data('confirm.db'),
77 "File specification for E-mail confirmations"). 78:- setting(subject_prefix, atom, '[SWISH] ',
79 "Prefix for the subject of emails sent"). 80
81:- http_handler(swish('mail/action/'), on_mail_link,
82 [prefix, id(on_mail_link)]). 83
84
85 88
89:- persistent
90 request(key:string,
91 deadline:integer,
92 action:callable,
93 reply:callable). 94
95email_open_db :-
96 db_attached(_),
97 !.
98email_open_db :-
99 setting(database, Spec),
100 absolute_file_name(Spec, Path, [access(write)]),
101 db_attach(Path, [sync(close)]).
102
106
107email_cleanup_db :-
108 with_mutex(swish_email, email_cleanup_db_sync).
109
110email_cleanup_db_sync :-
111 get_time(Now),
112 forall(( request(Key, Deadline, _, _),
113 Now > Deadline
114 ),
115 retract_request(Key, Deadline, _, _)),
116 db_sync(gc).
117
118
119
120 123
129
130smtp_send_html(To, Content, Options) :-
131 select_option(subject(Subject), Options, Options1, "<no subject>"),
132 setting(subject_prefix, Prefix),
133 string_concat(Prefix, Subject, Subject1),
134 merge_options(Options1,
135 [ header('MIME-Version'('1.0')),
136 content_type(text/html)
137 ], Options2),
138 smtp_send_mail(To, html_body(Content),
139 [ subject(Subject1)
140 | Options2
141 ]).
142
143html_body(Content, Out) :-
144 phrase(html(html([ head([]),
145 body(Content)
146 ])), Tokens),
147 print_html(Out, Tokens).
148
152
153generate_key(Key) :-
154 length(Codes, 16),
155 maplist(random_between(0,255), Codes),
156 phrase(base64url(Codes), Encoded),
157 string_codes(Key, Encoded).
158
159
160 163
164email_style -->
165 html({|html||
166<style>
167address { width: 80%; text-align: right;
168 margin-left: 18%; margin-top: 2em; border-top: 1px solid #888;}
169</style>
170 |}).
171
172
173
174 177
181
182dear(Profile) -->
183 html(p(['Dear ', \profile_name(Profile), ','])).
184
188
189signature -->
190 { host_url(HostURL, []) },
191 !,
192 html(address(['SWISH at ', a(href(HostURL), HostURL)])).
193signature -->
194 html(address(['SWISH'])).
195
199
200profile_name(User) -->
201 { user_field(Field),
202 Term =.. [Field, Name],
203 profile_property(User, Term)
204 },
205 html(Name).
206
207user_field(name).
208user_field(given_name).
209user_field(nick_name).
210user_field(family_name).
211
215
216mailto(Address) -->
217 html(a(href('mailto:'+Address), Address)).
218
219
220 223
228
229email_action_link(Label, Reply, Action, Options) -->
230 { email_open_db,
231 generate_key(Key),
232 public_url(on_mail_link, path_postfix(Key), HREF, Options),
233 setting(timeout, TMODef),
234 option(timeout(TMO), Options, TMODef),
235 get_time(Now),
236 Deadline is round(Now+TMO),
237 with_mutex(swish_email,
238 assert_request(Key, Deadline, Action, Reply))
239 },
240 html(a(href(HREF), Label)).
241
245
246on_mail_link(Request) :-
247 email_open_db,
248 option(path_info(Path), Request),
249 atom_string(Path, Key),
250 with_mutex(swish_email,
251 retract_request(Key, Deadline, Action, Reply)),
252 !,
253 ( get_time(Now),
254 Now =< Deadline
255 -> call(Action),
256 call(Reply, Request)
257 ; reply_expired(Request)
258 ).
259on_mail_link(Request) :-
260 email_open_db,
261 option(path_info(Path), Request),
262 atom_string(Path, Key),
263 reply_html_page(
264 email_confirmation,
265 title('Unknown request'),
266 [ \email_style,
267 p([ 'Cannot find request ~w.'-[Key], ' This typically means the \c
268 request has already been executed, is expired or the link \c
269 is invalid.'
270 ]),
271 \signature
272 ]).
273on_mail_link(_Request) :-
274 throw(http_reply(bad_request(missing_key))).
275
276reply_expired(_Request) :-
277 reply_html_page(
278 email_confirmation,
279 title('Request expired'),
280 [ \email_style,
281 p([ 'Your request has expired.'
282 ]),
283 \signature
284 ]).
285
286
290
291public_url(To, Query, URL, Options) :-
292 http_link_to_id(To, Query, RequestURI),
293 host_url(HostURL, Options),
294 atom_concat(HostURL, RequestURI, URL).
295
296host_url(HostURL, Options) :-
297 option(host_url(HostURL), Options),
298 !.
299host_url(HostURL, _Options) :-
300 http_public_host_url(_Request, HostURL).
301
302
303 306
307:- listen(user_profile(modified(User, email, Old, New)),
308 email_verify(User, Old, New)). 309
310email_verify(_User, _Old, "") :-
311 !.
312email_verify(User, Old, Email) :-
313 smtp_send_html(Email, \email_verify(User, Old, Email),
314 [ subject("Please verify email")
315 ]).
316
317
318email_verify(User, "", New) -->
319 html([ \email_style,
320 \dear(User),
321 p(['We have received a request to set the email account \c
322 for SWISH to ', \mailto(New), '.' ]),
323 ul([ li(\confirm_link(User, New))
324 ]),
325 \signature
326 ]).
327email_verify(User, Old, New) -->
328 html([ \email_style,
329 \dear(User),
330 p(['We have received a request to change the email account \c
331 for SWISH from ', \mailto(Old), ' to ', \mailto(New), '.' ]),
332 ul([ li(\confirm_link(User, New))
333 ]),
334 \signature
335 ]).
336
337confirm_link(User, New) -->
338 email_action_link(["Verify email as ", New], verified_email(User, New),
339 verify_email(User), []).
340
341verify_email(User) :-
342 set_profile(User, email_verified(true)).
343
344verified_email(User, NewEmail, _Request) :-
345 reply_html_page(
346 email_confirmation,
347 title('SWISH -- Email verified'),
348 [ \email_style,
349 \dear(User),
350 p(['Your email address ', \mailto(NewEmail), ' has been verified.']),
351 \signature
352 ])