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