Block more HTTP request methods
[mgsmtp.git] / Common.pas
1 {
2 Copyright (C) 2010-2018 MegaBrutal
3
4 This unit is free software: you can redistribute it and/or modify
5 it under the terms of the GNU Lesser General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This unit is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU Lesser General Public License for more details.
13
14 You should have received a copy of the GNU Lesser General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 }
17
18 {
19 Unit: Common
20 It holds some common definitions for MgSMTP, and some helper functions.
21 }
22
23
24 {$MODE DELPHI}
25
26 unit Common;
27
28 interface
29 uses Windows, SysUtils, DateUtils, Classes, INIFiles;
30
31 type
32
33 TStringArray = array of string;
34 TUnixTimeStamp = longint;
35
36
37 TArgument = record
38 Option, Value: string;
39 end;
40
41
42 TArgumentParser = class
43 constructor Create(RawArguments: array of string; AllowedPrefixes: array of string);
44 destructor Destroy; override;
45 private
46 Arguments: array of TArgument;
47 procedure ParseArgument(Arg: string; const AllowedPrefixes: array of string);
48 public
49 function GetArgument(ID: integer): TArgument;
50 function IsPresent(ArgumentName: string): boolean;
51 function GetValue(ArgumentName: string; DefValue: string = ''): string;
52 function ValidateArguments(ValidArguments: array of string): integer;
53 end;
54
55
56 TNamedObject = class
57 constructor Create(const Name: string; Config: TINIFile; const Section: string);
58 private
59 FName: string;
60 FAliases: TStrings;
61 public
62 property Name: string read FName;
63 property Aliases: TStrings read FAliases;
64 function IsItYourName(const Name: string): boolean; virtual;
65 end;
66
67
68 TMainServerConfig = class(TNamedObject)
69 constructor Create(Config: TINIFile);
70 private
71 FPolicies, FMailbox, FRelay, FLog: boolean;
72 FDatabytes: longint;
73 {FTimeCorrection: integer;}
74 FTimeOffset: integer;
75 FTimeOffsetStr: string;
76 FListenAddresses: TStrings;
77 FBindAddress: string;
78 public
79 function GetVersionStr: string;
80 property ListenAddresses: TStrings read FListenAddresses;
81 property BindAddress: string read FBindAddress;
82 property Databytes: longint read FDatabytes;
83 {property TimeCorrection: integer read FTimeCorrection;}
84 property TimeOffset: integer read FTimeOffset;
85 property TimeOffsetStr: string read FTimeOffsetStr;
86 property Policies: boolean read FPolicies;
87 property Mailbox: boolean read FMailbox;
88 property Relay: boolean read FRelay;
89 property Log: boolean read FLog;
90 property VersionStr: string read GetVersionStr;
91 end;
92
93
94 TEMailFlags = word;
95
96 TEMailProperties = class
97 constructor Create;
98 protected
99 FSize: longint;
100 FFlags: TEMailFlags;
101 public
102 procedure SetSize(Value: longint);
103 procedure WriteFlags(Value: TEMailFlags);
104 procedure SetFlag(Flag: TEMailFlags);
105 function HasFlag(Flag: TEMailFlags): boolean;
106 property Size: longint read FSize write SetSize;
107 property Flags: TEMailFlags read FFlags write WriteFlags;
108 end;
109
110
111 TRecipient = record
112 Address, RMsg: string;
113 Data: integer;
114 end;
115
116
117 TIPNamePair = class
118 constructor Create(const Name, IP: string);
119 protected
120 FName, FIP: string;
121 public
122 property Name: string read FName;
123 property IP: string read FIP;
124 function Copy: TIPNamePair;
125 end;
126
127
128 TEnvelope = class
129 constructor Create;
130 destructor Destroy; override;
131 private
132 FReturnPath, FRelayHost: string;
133 FReturnPathSpecified: boolean;
134 FRecipients: array of TRecipient;
135 public
136 function GetNumberOfRecipients: integer;
137 function GetRecipient(Index: integer): TRecipient;
138 function IsComplete: boolean;
139 procedure AddRecipient(Address: string; Data: integer = 0; RMsg: string = ''); overload;
140 procedure AddRecipient(Recipient: TRecipient); overload;
141 procedure SetReturnPath(Address: string);
142 procedure SetRecipientData(Index, Data: integer; RMsg: string = '');
143 procedure SetAllRecipientData(Data: integer; RMsg: string = '');
144 procedure SetRelayHost(HostName: string);
145 property ReturnPath: string read FReturnPath write SetReturnPath;
146 property RelayHost: string read FRelayHost write SetRelayHost;
147 end;
148
149
150 TEnvelopeArray = array of TEnvelope;
151
152
153 function EMailUserName(EMail: string): string;
154 function EMailHost(EMail: string): string;
155 function CleanEMailAddress(EMail: string): string;
156 function IsValidEMailAddress(EMail: string): boolean;
157 function EMailTimeStamp(DateTime: TDateTime): string;
158 function EMailTimeStampCorrected(DateTime: TDateTime): string;
159 function StatusToStr(Status: integer): string;
160 procedure AssignDeliveryStatusToSMTPCodes(Envelope: TEnvelope; TransactionComplete: boolean);
161
162 function CleanEOLN(S: string): string;
163 function GenerateRandomString(Length: integer): string;
164 function GetAlphabetStr: string;
165 function GetServiceCodeStr(Ctrl: dword): string;
166 function GetWinMajorVersion: longword;
167 function IsPrintableString(S: string): boolean;
168 function UnixTimeStamp(DateTime: TDateTime): TUnixTimeStamp;
169 function CmdlineToStringArray: TStringArray;
170 procedure SplitParameters(S: string; var FirstPrm, Remainder: string; Separator: char = #32);
171
172 function ReadLineFromStream(Stream: TStream): string;
173 function WriteLineToStream(Stream: TStream; Line: string): boolean;
174
175
176 const
177
178 { MgSMTP version: }
179 VERSION_STR = '0.9t';
180
181 { Architecture: }
182 {$IFDEF CPU64}
183 PLATFORM_BITS = 64;
184 {$ELSE}
185 {$IFDEF CPU32}
186 PLATFORM_BITS = 32;
187 {$ENDIF}
188 {$ENDIF}
189
190 { Delivery statuses: }
191 DS_DELIVERED = 1 shl 10;
192 DS_DELAYED = 1 shl 11;
193 DS_PERMANENT = 1 shl 12;
194 DS_INTERNALFAIL = 1 shl 13;
195 DS_CONNECTIONFAIL = 1 shl 14;
196 DS_UNEXPECTEDFAIL = 1 shl 15;
197 DS_SMTPFAIL = 1 shl 16;
198 DS_SMTPREPLYMASK = $000003FF;
199 DS_ALLFLAGS = $FFFFFFFF;
200
201 { E-mail property flags: }
202 EF_8BITMIME = 1;
203
204 DayNames: array[1..7] of shortstring = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
205 MonthNames: array[1..12] of shortstring = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
206 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
207
208 { Support for PRESHUTDOWN is not yet present in the Free Pascal library,
209 therefore I define the necessary constants here. It's a temporary
210 solution, I hope I won't need it for the next release of FPC. }
211 SERVICE_ACCEPT_PRESHUTDOWN = $00000100;
212 SERVICE_CONTROL_PRESHUTDOWN = $0000000F;
213
214
215 var
216
217 MainServerConfig: TMainServerConfig;
218
219
220 implementation
221
222
223 { Unit-private functions/prodecures: }
224
225 function InStringArray(const S: string; const SA: array of string): boolean;
226 var i: integer;
227 begin
228 i:= 0;
229 while (i < Length(SA)) and (SA[i] <> S) do Inc(i);
230 Result:= i < Length(SA);
231 end;
232
233 function MakeTimeOffsetStr(TimeOffset: integer): string;
234 var CorrS: string; CorrI: integer;
235 begin
236 CorrI:= TimeOffset;
237 CorrS:= IntToStr(Abs(CorrI));
238 while Length(CorrS) < 4 do CorrS:= '0' + CorrS;
239 if CorrI >= 0 then CorrS:= '+' + CorrS else CorrS:= '-' + CorrS;
240 Result:= CorrS;
241 end;
242
243
244 { Unit-public functions/procedures: }
245
246 function EMailUserName(EMail: string): string;
247 var p: integer;
248 begin
249 p:= Length(EMail);
250 while (p > 0) and (EMail[p] <> '@') do Dec(p);
251 if p <> 0 then begin
252 Result:= Copy(EMail, 1, p - 1);
253 end
254 else Result:= EMail;
255 end;
256
257 function EMailHost(EMail: string): string;
258 var p: integer;
259 begin
260 p:= Length(EMail);
261 while (p > 0) and (EMail[p] <> '@') do Dec(p);
262 if (p <> 0) and (p < Length(EMail)) then begin
263 Result:= Copy(EMail, p + 1, Length(EMail) - p);
264 end
265 else Result:= '';
266 end;
267
268 function CleanEMailAddress(EMail: string): string;
269 var po, pc, p: integer;
270 begin
271 po:= Pos('<', EMail);
272 pc:= Pos('>', EMail);
273 if (po <> 0) and (pc <> 0) and (po < pc) then begin
274 Result:= Copy(EMail, po + 1, pc - po - 1);
275 p:= Pos(':', Result);
276 if p <> 0 then
277 Result:= Copy(Result, p + 1, Length(Result) - p);
278 end
279 else
280 Result:= EMail;
281 end;
282
283 function IsValidEMailAddress(EMail: string): boolean;
284 begin
285 { !!! TODO: Implement more strict checking later !!! }
286 Result:= Pos('@', EMail) <> 0;
287 end;
288
289 function EMailTimeStamp(DateTime: TDateTime): string;
290 var Year, Month, Day: word;
291 begin
292 DecodeDate(DateTime, Year, Month, Day);
293 Result:= DayNames[DayOfWeek(DateTime)] + ' ' + MonthNames[Month] + ' '
294 + FormatDateTime('dd hh:nn:ss', DateTime) + ' ' + IntToStr(Year);
295 end;
296
297 function EMailTimeStampCorrected(DateTime: TDateTime): string;
298 begin
299 Result:= EMailTimeStamp(DateTime) + ' ' + MainServerConfig.TimeOffsetStr;
300 end;
301
302 function StatusToStr(Status: integer): string;
303 { Returns the delivery status code in human-readable format. }
304 begin
305 Result:= IntToStr(Status and (DS_ALLFLAGS - DS_SMTPREPLYMASK - DS_SMTPFAIL))
306 + '+' + IntToStr(Status and DS_SMTPREPLYMASK);
307 end;
308
309 procedure AssignDeliveryStatusToSMTPCodes(Envelope: TEnvelope; TransactionComplete: boolean);
310 var i, code, cond, status: integer; Recipient: TRecipient;
311 begin
312 for i:= 0 to Envelope.GetNumberOfRecipients - 1 do begin
313 Recipient:= Envelope.GetRecipient(i);
314 code:= Recipient.Data and DS_SMTPREPLYMASK;
315 cond:= code div 100;
316 case cond of
317 0: status:= DS_DELAYED or DS_UNEXPECTEDFAIL;
318 2: if TransactionComplete then status:= DS_DELIVERED
319 else status:= DS_DELAYED or DS_UNEXPECTEDFAIL;
320 4: status:= DS_DELAYED;
321 5: status:= DS_PERMANENT;
322 else status:= DS_PERMANENT or DS_UNEXPECTEDFAIL;
323 end;
324 if code <> 0 then status:= status or DS_SMTPFAIL;
325 Envelope.SetRecipientData(i, code or status, Recipient.RMsg);
326 end;
327 end;
328
329
330 function CleanEOLN(S: string): string;
331 begin
332 while (Length(S) <> 0) and (S[Length(S)] in [#13, #10]) do Delete(S, Length(S), 1);
333 Result:= S;
334 end;
335
336 function GenerateRandomString(Length: integer): string;
337 var What, Chrn, i: integer; Value: string;
338 begin
339 Value:= '';
340 for i:= 1 to Length do begin
341 What:= Random(3);
342 case What of
343 0: begin Chrn:= Random(10)+48; Value:= Value + Chr(Chrn); end;
344 1: begin Chrn:= Random(26)+65; Value:= Value + Chr(Chrn); end;
345 2: begin Chrn:= Random(26)+97; Value:= Value + Chr(Chrn); end;
346 end;
347 end;
348 Result:= Value;
349 end;
350
351 function GetAlphabetStr: string;
352 var i: byte;
353 begin
354 Result:= '';
355 for i:= Ord('0') to Ord('9') do Result:= Result + Chr(i);
356 for i:= Ord('A') to Ord('Z') do Result:= Result + Chr(i);
357 end;
358
359 function GetServiceCodeStr(Ctrl: dword): string;
360 begin
361 case Ctrl of
362 SERVICE_CONTROL_STOP: Result:= 'STOP';
363 SERVICE_CONTROL_SHUTDOWN: Result:= 'SHUTDOWN';
364 SERVICE_CONTROL_PRESHUTDOWN: Result:= 'PRESHUTDOWN';
365 else Result:= IntToStr(Ctrl);
366 end;
367 end;
368
369 function GetWinMajorVersion: longword;
370 var OSVersionInfo: TOSVersionInfo;
371 begin
372 { Get OS version info. }
373 OSVersionInfo.dwOSVersionInfoSize:= SizeOf(TOSVersionInfo);
374 GetVersionEx(OSVersionInfo);
375 Result:= OSVersionInfo.dwMajorVersion;
376 end;
377
378 function IsPrintableString(S: string): boolean;
379 { Check if string contains only printable ASCII characters. }
380 var i: integer;
381 begin
382 i:= 1;
383 Result:= true;
384 while Result and (i <= Length(S)) do begin
385 Result:= (Ord(S[i]) > 31) and (Ord(S[i]) < 127);
386 Inc(i);
387 end;
388 end;
389
390 procedure SplitParameters(S: string; var FirstPrm, Remainder: string; Separator: char = #32);
391 var i: integer;
392 begin
393 i:= pos(Separator, S);
394 if i > 0 then begin
395 FirstPrm:= Copy(S, 1, i - 1);
396 Remainder:= Copy(S, i + 1, Length(S) - i);
397 end
398 else begin
399 FirstPrm:= S;
400 Remainder:= '';
401 end;
402 end;
403
404 function CmdlineToStringArray: TStringArray;
405 var i: integer;
406 begin
407 SetLength(Result, ParamCount);
408 for i:= 1 to ParamCount do
409 Result[i-1]:= ParamStr(i);
410 end;
411
412 function UnixTimeStamp(DateTime: TDateTime): TUnixTimeStamp;
413 begin
414 {Result:= Trunc((DateTime - EncodeDate(1970, 1 ,1)) * 24 * 60 * 60);}
415 Result:= DateTimeToUnix(DateTime);
416 end;
417
418
419 function ReadLineFromStream(Stream: TStream): string;
420 var S: string; B: char;
421 begin
422 S:= '';
423 try
424 repeat
425 B:= Char(Stream.ReadByte);
426 if not (B in [#10, #13]) then S:= S + B;
427 until (B = #10);
428 finally
429 Result:= S;
430 end;
431 end;
432
433 function WriteLineToStream(Stream: TStream; Line: string): boolean;
434 const EOLN = #13#10;
435 begin
436 Result:= true;
437 Line:= Line + EOLN;
438 try
439 Stream.WriteBuffer(PChar(Line)^, Length(Line));
440 except
441 Result:= false;
442 end;
443 end;
444
445
446 { Object constructors/destructors: }
447
448 constructor TArgumentParser.Create(RawArguments: array of string; AllowedPrefixes: array of string);
449 var i: integer;
450 begin
451 for i:= 0 to Length(RawArguments) - 1 do
452 ParseArgument(RawArguments[i], AllowedPrefixes);
453 end;
454
455 destructor TArgumentParser.Destroy;
456 begin
457 SetLength(Arguments, 0);
458 end;
459
460 constructor TNamedObject.Create(const Name: string; Config: TINIFile; const Section: string);
461 begin
462 inherited Create;
463 FName:= Name;
464 FAliases:= TStringList.Create;
465 FAliases.Delimiter:= ',';
466 FAliases.DelimitedText:= FName + ',' + Config.ReadString(Section, 'Alias', '');
467 end;
468
469 constructor TMainServerConfig.Create(Config: TINIFile);
470 var i: integer; rawaddresslist: string; portlist: TStringList;
471 begin
472 inherited Create(Config.ReadString('Server', 'Name', ''), Config, 'Server');
473 FListenAddresses:= TStringList.Create;
474 FListenAddresses.Delimiter:= ',';
475
476 rawaddresslist:= Config.ReadString('Server', 'ListenAddress', '');
477 if rawaddresslist <> '' then
478 FListenAddresses.DelimitedText:= rawaddresslist
479 else begin
480 portlist:= TStringList.Create;
481 portlist.Delimiter:= ',';
482 portlist.DelimitedText:= Config.ReadString('Server', 'ListenPort', '25');
483 for i:= 0 to portlist.Count - 1 do
484 FListenAddresses.Add('0.0.0.0:' + portlist.Strings[i]);
485 portlist.Free;
486 end;
487
488 FBindAddress:= Config.ReadString('Server', 'BindAddress', '0.0.0.0');
489
490 FDatabytes:= Config.ReadInteger('Server', 'Databytes', 1024 * 1024 * 1024);
491 FTimeOffset:= Config.ReadInteger('Server', 'TimeOffset', Config.ReadInteger('Server', 'TimeCorrection', 0) * 100);
492 FTimeOffsetStr:= MakeTimeOffsetStr(FTimeOffset);
493
494 FPolicies:= Config.ReadBool('Server', 'Policies', false);
495 FMailbox:= Config.ReadBool('Server', 'Mailbox', false);
496 FRelay:= Config.ReadBool('Server', 'Relay', false);
497 FLog:= Config.ReadBool('Server', 'Log', false);
498 end;
499
500 constructor TEMailProperties.Create;
501 begin
502 inherited Create;
503 SetSize(0);
504 WriteFlags(0);
505 end;
506
507 constructor TIPNamePair.Create(const Name, IP: string);
508 begin
509 FName:= Name;
510 FIP:= IP;
511 end;
512
513 constructor TEnvelope.Create;
514 begin
515 inherited Create;
516 FReturnPath:= '';
517 FReturnPathSpecified:= false;
518 FRelayHost:= '';
519 SetLength(FRecipients, 0);
520 end;
521
522 destructor TEnvelope.Destroy;
523 begin
524 SetLength(FRecipients, 0);
525 inherited Destroy;
526 end;
527
528
529 { Object methods: }
530
531 procedure TArgumentParser.ParseArgument(Arg: string; const AllowedPrefixes: array of string);
532 var i, n: integer; found: boolean;
533 begin
534 { Strip prefix if present. }
535 i:= 0; found:= false;
536 while ((i < Length(AllowedPrefixes)) and (not found)) do begin
537 if pos(AllowedPrefixes[i], Arg) = 1 then
538 begin
539 Delete(Arg, 1, Length(AllowedPrefixes[i]));
540 found:= true;
541 end;
542 Inc(i);
543 end;
544
545 n:= Length(Arguments);
546 SetLength(Arguments, n + 1);
547 SplitParameters(Arg, Arguments[n].Option, Arguments[n].Value, '=');
548 { To be case-insensitive: }
549 Arguments[n].Option:= UpCase(Arguments[n].Option);
550 end;
551
552 function TArgumentParser.GetArgument(ID: integer): TArgument;
553 begin
554 { No index checking... you'd better use it return value of ValidateArguments. }
555 Result:= Arguments[ID];
556 end;
557
558 function TArgumentParser.IsPresent(ArgumentName: string): boolean;
559 var i: integer;
560 begin
561 i:= 0;
562 while (i < Length(Arguments)) and (Arguments[i].Option <> UpCase(ArgumentName)) do
563 Inc(i);
564 Result:= i < Length(Arguments);
565 end;
566
567 function TArgumentParser.GetValue(ArgumentName: string; DefValue: string = ''): string;
568 var i: integer;
569 begin
570 i:= 0;
571 while (i < Length(Arguments)) and (Arguments[i].Option <> UpCase(ArgumentName)) do
572 Inc(i);
573
574 if i < Length(Arguments) then begin
575 if Arguments[i].Value <> '' then
576 Result:= Arguments[i].Value
577 else
578 Result:= DefValue;
579 end
580 else
581 Result:= DefValue;
582 end;
583
584 function TArgumentParser.ValidateArguments(ValidArguments: array of string): integer;
585 { Returns -1 if all arguments are valid. Otherwise, returns the ID of the first
586 invalid parameter. }
587 var i: integer;
588 begin
589 i:= 0;
590 while (i < Length(Arguments)) and InStringArray(Arguments[i].Option, ValidArguments) do
591 Inc(i);
592
593 if i >= Length(Arguments) then
594 Result:= -1
595 else
596 Result:= i;
597 end;
598
599
600 function TNamedObject.IsItYourName(const Name: string): boolean;
601 begin
602 Result:= FAliases.IndexOf(Name) <> -1;
603 end;
604
605
606 function TMainServerConfig.GetVersionStr: string;
607 begin
608 Result:= VERSION_STR;
609 end;
610
611
612 function TIPNamePair.Copy: TIPNamePair;
613 begin
614 Result:= TIPNamePair.Create(Name, IP);
615 end;
616
617
618 procedure TEMailProperties.SetSize(Value: longint);
619 begin
620 FSize:= Value;
621 end;
622
623 procedure TEMailProperties.WriteFlags(Value: TEMailFlags);
624 begin
625 FFlags:= Value;
626 end;
627
628 procedure TEMailProperties.SetFlag(Flag: TEMailFlags);
629 begin
630 FFlags:= FFlags or Flag;
631 end;
632
633 function TEMailProperties.HasFlag(Flag: TEMailFlags): boolean;
634 begin
635 Result:= (FFlags and Flag) = Flag;
636 end;
637
638
639 function TEnvelope.GetNumberOfRecipients: integer;
640 begin
641 Result:= Length(FRecipients);
642 end;
643
644 function TEnvelope.GetRecipient(Index: integer): TRecipient;
645 begin
646 Result:= FRecipients[Index];
647 end;
648
649 function TEnvelope.IsComplete: boolean;
650 begin
651 Result:= FReturnPathSpecified and (Length(FRecipients) > 0);
652 end;
653
654 procedure TEnvelope.AddRecipient(Address: string; Data: integer = 0; RMsg: string = '');
655 var i: integer;
656 begin
657 i:= Length(FRecipients);
658 SetLength(FRecipients, i + 1);
659 FRecipients[i].Address:= Address;
660 FRecipients[i].RMsg:= RMsg;
661 FRecipients[i].Data:= Data;
662 end;
663
664 procedure TEnvelope.AddRecipient(Recipient: TRecipient);
665 var i: integer;
666 begin
667 i:= Length(FRecipients);
668 SetLength(FRecipients, i + 1);
669 FRecipients[i]:= Recipient;
670 end;
671
672 procedure TEnvelope.SetRecipientData(Index, Data: integer; RMsg: string = '');
673 begin
674 FRecipients[Index].RMsg:= RMsg;
675 FRecipients[Index].Data:= Data;
676 end;
677
678 procedure TEnvelope.SetAllRecipientData(Data: integer; RMsg: string = '');
679 var i: integer;
680 begin
681 for i:= 0 to Length(FRecipients) - 1 do
682 SetRecipientData(i, Data, RMsg);
683 end;
684
685 procedure TEnvelope.SetReturnPath(Address: string);
686 begin
687 FReturnPath:= Address;
688 FReturnPathSpecified:= true;
689 end;
690
691 procedure TEnvelope.SetRelayHost(HostName: string);
692 begin
693 FRelayHost:= HostName;
694 end;
695
696
697 end.