-
Notifications
You must be signed in to change notification settings - Fork 0
/
Sx.DDD.Infra.Email.pas
458 lines (415 loc) · 17.1 KB
/
Sx.DDD.Infra.Email.pas
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
unit Sx.DDD.Infra.Email;
{$mode delphi}
interface
uses
SysUtils,
mormot.core.base,
mormot.core.data,
mormot.core.json,
mormot.core.search,
mormot.rest.core,
mormot.rest.server,
mormot.orm.base,
mormot.orm.core,
Sx.DDD.Core,
Sx.DDD.Email.Interfaces,
Sx.DDD.User.Types;
{ ****************** Email Verification Service }
type
/// exception raised during any email process of this DDD's infrastructure
// implementation
EDDDEmail = class(EDDDInfraException);
/// parameters used for the validation link of an email address
// - may be stored as daemon/service level settings, using e.g. dddInfraSettings
TDDDEmailRedirection = class(TSynPersistent)
private
fSuccessRedirectURI: RawUTF8;
fRestServerPublicRootURI: RawUTF8;
fValidationMethodName: RawUTF8;
published
/// the public URI which would be accessible from the Internet
// - may be e.g 'http://publicserver/restroot'
property RestServerPublicRootURI: RawUTF8
read fRestServerPublicRootURI write fRestServerPublicRootURI;
/// the validation method name for the URI
// - if not set, TDDDEmailValidationService will use 'EmailValidate'
// - clickable URI would be RestServerPublicRootURI+'/'+ValidationMethodName
property ValidationMethodName: RawUTF8
read fValidationMethodName write fValidationMethodName;
/// the URI on which the browser will be redirected on validation success
// - you can specify some '%' parameter markers, ordered as logon, email,
// and validation IP
// - may be e.g. 'http://publicwebsite/success&logon=%'
property SuccessRedirectURI: RawUTF8
read fSuccessRedirectURI write fSuccessRedirectURI;
end;
/// parameters used for the validation/verification process of an email address
// - may be stored as daemon/service level settings, using e.g. dddInfraSettings
TDDDEmailValidation = class(TSynAutoCreateFields)
private
fTemplate: TDomUserEmailTemplate;
fTemplateFolder: TFileName;
fRedirection: TDDDEmailRedirection;
public
/// will fill some default values in the properties, if none is set
procedure SetDefaultValuesIfVoid(const aSenderEmail,aApplication,
aRedirectionURIPublicRoot,aRedirectionURISuccess: RawUTF8);
published
/// how the email should be created from a given template
property Template: TDomUserEmailTemplate read fTemplate;
/// where the template files are to be found
property TemplateFolder: TFileName
read fTemplateFolder write fTemplateFolder;
/// parameters defining the validation link of an email address
property Redirection: TDDDEmailRedirection read fRedirection;
end;
TSQLRecordEmailAbstract = class;
TSQLRecordEmailValidation = class;
TSQLRecordEmailValidationClass = class of TSQLRecordEmailValidation;
/// abstract parent of any email-related service
// - will define some common methods to validate an email address
TDDDEmailServiceAbstract = class(TCQRSQueryObjectRest,IDomUserEmailCheck)
protected
fEmailValidate: TSynValidate;
function CheckEmailCorrect(aEmail: TSQLRecordEmailAbstract;
var aResult: TCQRSResult): boolean; virtual;
procedure SetEmailValidate(const Value: TSynValidate); virtual;
public
constructor Create(aRest: TRest); override;
destructor Destroy; override;
function CheckRecipient(const aEmail: RawUTF8): TCQRSResult; virtual;
function CheckRecipients(const aEmails: TRawUTF8DynArray): TCQRSResult;
published
/// direct access to the email validation instance
// - you can customize the default TSynValidateEmail to meet your own
// expectations - once set, it will be owned by this class instance
property EmailValidate: TSynValidate read fEmailValidate write SetEmailValidate;
end;
/// service used to validate an email address via an URL link to be clicked
TDDDEmailValidationService = class(TDDDEmailServiceAbstract,
IDomUserEmailValidation)
protected
fRestClass: TSQLRecordEmailValidationClass;
fEMailer: IDomUserEmailer;
fTemplate: IDomUserTemplate;
fValidationSalt: integer;
fValidationServerRoot: RawUTF8;
fValidationMethodName: RawUTF8;
fSuccessRedirectURI: RawUTF8;
function GetEmailValidation(const aLogonName: RawUTF8): TSQLRecordEmailValidation;
function GetWithSalt(const aLogonName,aEmail: RawUTF8; aSalt: integer): RawUTF8;
procedure EmailValidate(Ctxt: TRestServerURIContext);
public
/// initialize the validation service for a given ORM persistence
// - would recognize the TSQLRecordEmailValidation class from aRest.Model
// - will use aRest.Services for IoC, e.g. EMailer/Template properties
constructor Create(aRest: TRest); override;
/// register the callback URI service
procedure SetURIForServer(aRestServerPublic: TRestServer;
aParams: TDDDEmailRedirection); overload;
/// register the callback URI service
// - same as the overloaded function, but taking parameters one by one
procedure SetURIForServer(aRestServerPublic: TRestServer;
const aRestServerPublicRootURI,aSuccessRedirectURI,aValidationMethodName: RawUTF8); overload;
/// compute the target URI corresponding to SetURIForServer() parameters
function ComputeURIForReply(const aLogonName,aEmail: RawUTF8): RawUTF8;
/// check the supplied parameters, and send an email for validation
function StartEmailValidation(const aTemplate: TDomUserEmailTemplate;
const aLogonName,aEmail: RawUTF8): TCQRSResult; virtual;
/// check if an email has been validated for a given logon
function IsEmailValidated(const aLogonName,aEmail: RawUTF8): boolean; virtual;
published
/// will be injected (and freed) with the emailer service
property EMailer: IDomUserEmailer read fEmailer;
/// will be injected (and freed) with the email template service
property Template: IDomUserTemplate read fTemplate;
published
/// the associated ORM class used to store the email validation process
// - any class inheriting from TSQLRecordEmailValidation in the aRest.Model
// will be recognized by Create(aRest) to store its information
// - this temporary storage should not be the main user persistence domain
property RestClass: TSQLRecordEmailValidationClass read fRestClass;
/// the validation method name for the URI
// - if not set, TDDDEmailValidationService will use 'EmailValidate'
// - clickable URI would be ValidationServerRoot+'/'+ValidationMethodName
property ValidationURI: RawUTF8 read fValidationMethodName;
/// the public URI which would be accessible from the Internet
// - may be e.g 'http://publicserver/restroot'
property ValidationServerRoot: RawUTF8 read fValidationServerRoot;
end;
/// base ORM class, which will have creation and modification timestamp fields
TOrmTimed = class(TOrm)
protected
fCreated: TCreateTime;
fModified: TModTime;
published
/// will be filled by the ORM when this item will be created in the database
property Created: TCreateTime
read fCreated write fCreated;
/// will be filled by the ORM each time this item will be written in the database
property Modified: TModTime
read fModified write fModified;
end;
/// ORM class storing an email in addition to creation/modification timestamps
// - declared as its own class, since may be reused
TSQLRecordEmailAbstract = class(TOrmTimed)
private
fEmail: RawUTF8;
published
/// the stored email address
property Email: RawUTF8 read fEmail write fEmail;
end;
/// ORM class for email validation process
// - we do not create a whole domain here, just an ORM persistence layer
// - any class inheriting from TSQLRecordEmailValidation in the Rest.Model
// will be recognized by TDDDEmailValidationService to store its information
TSQLRecordEmailValidation = class(TSQLRecordEmailAbstract)
protected
fLogon: RawUTF8;
fRequestTime: TTimeLog;
fValidationSalt: Integer;
fValidationTime: TTimeLog;
fValidationIP: RawUTF8;
public
function IsValidated(const aEmail: RawUTF8): Boolean;
published
property Logon: RawUTF8 read fLogon write fLogon stored AS_UNIQUE;
property RequestTime: TTimeLog read fRequestTime write fRequestTime;
property ValidationSalt: Integer read fValidationSalt write fValidationSalt;
property ValidationTime: TTimeLog read fValidationTime write fValidationTime;
property ValidationIP: RawUTF8 read fValidationIP write fValidationIP;
end;
implementation
uses
mormot.core.buffers,
mormot.core.datetime,
mormot.core.os,
mormot.core.text,
mormot.core.unicode,
mormot.core.variants,
mormot.crypt.core;
{ TDDDEmailServiceAbstract }
constructor TDDDEmailServiceAbstract.Create(aRest: TRest);
begin
inherited Create(aRest);
fEmailValidate := TSynValidateEmail.Create;
end;
destructor TDDDEmailServiceAbstract.Destroy;
begin
fEmailValidate.Free;
inherited;
end;
function TDDDEmailServiceAbstract.CheckEmailCorrect(
aEmail: TSQLRecordEmailAbstract; var aResult: TCQRSResult): boolean;
var msg: string;
begin
if (aEmail<>nil) and fEmailValidate.Process(0,aEmail.Email,msg) and
aEmail.FilterAndValidate(Rest.Orm,msg) then
result := true else begin
CqrsSetResultString(cqrsDDDValidationFailed,msg,aResult);
result := false;
end;
end;
function TDDDEmailServiceAbstract.CheckRecipient(
const aEmail: RawUTF8): TCQRSResult;
var msg: string;
begin
CqrsBeginMethod(qaNone,result);
if fEmailValidate.Process(0,aEmail,msg) then
CqrsSetResult(cqrsSuccess,result) else
CqrsSetResultString(cqrsDDDValidationFailed,msg,result);
end;
function TDDDEmailServiceAbstract.CheckRecipients(
const aEmails: TRawUTF8DynArray): TCQRSResult;
var msg: string;
i: integer;
begin
CqrsBeginMethod(qaNone,result);
for i := 0 to high(aEMails) do
if not fEmailValidate.Process(0,aEmails[i],msg) then begin
CqrsSetResultString(cqrsDDDValidationFailed,msg,result);
exit;
end;
CqrsSetResult(cqrsSuccess,result);
end;
procedure TDDDEmailServiceAbstract.SetEmailValidate(
const Value: TSynValidate);
begin
fEmailValidate.Free;
fEmailValidate := Value;
end;
{ TDDDEmailValidationService }
constructor TDDDEmailValidationService.Create(aRest: TRest);
var rnd: Int64;
begin
inherited Create(aRest); // will inject aRest.Services for IoC
fRestClass := fRest.Model.AddTableInherited(TSQLRecordEmailValidation);
fRestClass.AddFilterNotVoidText(['Email','Logon']);
rnd := GetTickCount64*PtrInt(self)*Random(MaxInt);
fValidationSalt := crc32c(PtrInt(self),@rnd,sizeof(rnd));
end;
function TDDDEmailValidationService.GetWithSalt(const aLogonName,
aEmail: RawUTF8; aSalt: integer): RawUTF8;
begin
result := SHA256(FormatUTF8('%'#1'%'#2'%'#3,[aLogonName,aEmail,aSalt]));
end;
function TDDDEmailValidationService.ComputeURIForReply(
const aLogonName, aEmail: RawUTF8): RawUTF8;
begin
result := aLogonName+#1+aEmail;
result := fValidationServerRoot+fValidationMethodName+'/'+
GetWithSalt(aLogonName,aEmail,fValidationSalt)+
BinToBase64URI(pointer(result),length(result));
end;
procedure TDDDEmailValidationService.EmailValidate(
Ctxt: TRestServerURIContext);
var code: RawUTF8;
logon,email,signature: RawUTF8;
EmailValidation: TSQLRecordEmailValidation;
begin
signature := Copy(Ctxt.URIBlobFieldName,1,SHA256DIGESTSTRLEN);
if length(signature)<>SHA256DIGESTSTRLEN then
exit;
code := Base64uriToBin(Copy(Ctxt.URIBlobFieldName,SHA256DIGESTSTRLEN+1,200));
Split(code,#1,logon,email);
if (logon='') or (email='') then
exit;
EmailValidation := GetEmailValidation(logon);
if EmailValidation<>nil then
try
if signature=GetWithSalt(logon,email,EmailValidation.ValidationSalt) then begin
EmailValidation.ValidationTime := TimeLogNowUTC;
EmailValidation.ValidationIP := Ctxt.InHeader['remoteip'];
if Rest.Orm.Update(EmailValidation) then
Ctxt.Redirect(FormatUTF8(fSuccessRedirectURI,
[UrlEncode(logon),UrlEncode(email),UrlEncode(EmailValidation.ValidationIP)]));
end;
finally
EmailValidation.Free;
end;
end;
procedure TDDDEmailValidationService.SetURIForServer(
aRestServerPublic: TRestServer; aParams: TDDDEmailRedirection);
begin
if aParams=nil then
raise EDDDEmail.CreateUTF8('Invalid %.SetURIForServer(%,nil)',
[self,aRestServerPublic]);
SetURIForServer(aRestServerPublic,aParams.RestServerPublicRootURI,
aParams.SuccessRedirectURI,aParams.ValidationMethodName);
end;
procedure TDDDEmailValidationService.SetURIForServer(
aRestServerPublic: TRestServer; const aRestServerPublicRootURI,
aSuccessRedirectURI, aValidationMethodName: RawUTF8);
begin
fSuccessRedirectURI := Trim(aSuccessRedirectURI);
fValidationServerRoot := IncludeTrailingURIDelimiter(Trim(aRestServerPublicRootURI));
if (aRestServerPublic=nil) or (fSuccessRedirectURI='') or (fValidationServerRoot='') then
raise EDDDEmail.CreateUTF8('Invalid %.SetURIForServer(%,"%","%")',
[self,aRestServerPublic,fValidationServerRoot,fSuccessRedirectURI]);
if not IdemPChar(pointer(fValidationServerRoot),'HTTP') then
fValidationServerRoot := 'http://'+fValidationServerRoot;
fValidationMethodName := Trim(aValidationMethodName);
if fValidationMethodName='' then
fValidationMethodName := 'EmailValidate'; // match method name by default
aRestServerPublic.ServiceMethodRegister(fValidationMethodName,EmailValidate,true);
end;
function TDDDEmailValidationService.GetEmailValidation(const aLogonName: RawUTF8): TSQLRecordEmailValidation;
begin
result := RestClass.Create(Rest.Orm,'Logon=?',[aLogonName]);
if result.fID=0 then
FreeAndNil(result);
end;
function TDDDEmailValidationService.IsEmailValidated(const aLogonName,
aEmail: RawUTF8): boolean;
var EmailValidation: TSQLRecordEmailValidation;
begin
EmailValidation := GetEmailValidation(aLogonName);
try
result := EmailValidation.IsValidated(trim(aEmail));
finally
EmailValidation.Free;
end;
end;
procedure _ObjAddProps(Value: TObject; var Obj: variant); overload;
var v: variant;
begin
ObjectToVariant(Value,v,[woDontStoreDefault]);
_ObjAddProps(v,Obj);
end;
function TDDDEmailValidationService.StartEmailValidation(
const aTemplate: TDomUserEmailTemplate; const aLogonName, aEmail: RawUTF8): TCQRSResult;
var EmailValidation: TSQLRecordEmailValidation;
email,msg: RawUTF8;
context: variant;
begin
email := Trim(aEmail);
result := CheckRecipient(email);
if result<>cqrsSuccess then
exit; // supplied email address is invalid
CqrsBeginMethod(qaNone,result);
EmailValidation := GetEmailValidation(aLogonName);
try
if EmailValidation.IsValidated(email) then begin
CqrsSetResultMsg(cqrsSuccess,'Already validated',result);
exit;
end;
if EmailValidation=nil then begin
EmailValidation := RestClass.Create;
EmailValidation.Email := aEmail;
EmailValidation.Logon := aLogonName;
if not CheckEmailCorrect(EmailValidation,result) then
exit;
end else
if EmailValidation.Email<>email then
EmailValidation.Email := email; // allow validation for a new email
EmailValidation.RequestTime := TimeLogNowUTC;
EmailValidation.ValidationSalt := fValidationSalt;
context := EmailValidation.GetSimpleFieldsAsDocVariant(true);
_ObjAddProps(aTemplate,context);
_ObjAddProps(['ValidationUri',
ComputeURIForReply(EmailValidation.Logon,EmailValidation.Email)],context);
msg := Template.ComputeMessage(context,aTemplate.FileName);
if msg='' then
CqrsSetResultMsg(cqrsInvalidContent,
'Impossible to render template [%]',[aTemplate.FileName],result) else
if EMailer.SendEmail(TRawUTF8DynArrayFrom([aEmail]),
aTemplate.SenderEmail,aTemplate.Subject,'',msg)=cqrsSuccess then
if Rest.Orm.AddOrUpdate(EmailValidation)=0 then
CqrsSetResultError(cqrsDataLayerError) else
CqrsSetResultMsg(cqrsSuccess,'Validation email sent',result);
finally
EmailValidation.Free;
end;
end;
{ TSQLRecordEmailValidation }
function TSQLRecordEmailValidation.IsValidated(const aEmail: RawUTF8): Boolean;
begin
result := (self<>nil) and (ValidationTime<>0) and (Email=aEmail);
end;
{ TDDDEmailValidation }
procedure TDDDEmailValidation.SetDefaultValuesIfVoid(
const aSenderEmail,aApplication,
aRedirectionURIPublicRoot,aRedirectionURISuccess: RawUTF8);
begin
if Template.SenderEmail='' then
Template.SenderEmail := aSenderEmail;
if Template.Application='' then
Template.Application := aApplication;
if Template.FileName='' then
Template.FileName := 'EmailValidate.txt';
if (TemplateFolder='') and
not FileExists(string(Template.FileName)) then
FileFromString('Welcome to {{Application}}!'#13#10#13#10+
'You have registered as "{{Logon}}", using {{EMail}} as contact address.'#13#10#13#10+
'Please click on the following link to validate your email:'#13#10+
'{{ValidationUri}}'#13#10#13#10'Best regards from the clouds'#13#10#13#10+
'(please do not respond to this email)',
UTF8ToString(Template.FileName));
if Template.Subject='' then
Template.Subject := 'Please Validate Your Email';
if Redirection.RestServerPublicRootURI='' then
Redirection.RestServerPublicRootURI := aRedirectionURIPublicRoot;
if Redirection.SuccessRedirectURI='' then
Redirection.SuccessRedirectURI := aRedirectionURISuccess;
end;
end.