2 Copyright (C) 2010-2018 MegaBrutal
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.
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.
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/>.
20 It holds some common definitions for MgSMTP, and some helper functions.
29 uses Windows
, SysUtils
, DateUtils
, Classes
, INIFiles
, RFCSMTP
;
33 TStringArray
= array of string;
34 TUnixTimeStamp
= longint;
38 Option
, Value
: string;
42 TArgumentParser
= class
43 constructor Create(RawArguments
: array of string; AllowedPrefixes
: array of string);
44 destructor Destroy
; override;
46 Arguments
: array of TArgument
;
47 procedure ParseArgument(Arg
: string; const AllowedPrefixes
: array of string);
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;
57 constructor Create(const Name
: string; Config
: TINIFile
; const Section
: string);
62 property Name
: string read FName
;
63 property Aliases
: TStrings read FAliases
;
64 function IsItYourName(const Name
: string): boolean; virtual;
68 TMainServerConfig
= class(TNamedObject
)
69 constructor Create(Config
: TINIFile
);
71 FPolicies
, FMailbox
, FRelay
, FLog
: boolean;
73 {FTimeCorrection: integer;}
75 FTimeOffsetStr
: string;
76 FListenAddresses
, FListenAddresses6
: TStrings
;
77 FBindAddress
, FBindAddress6
: string;
79 function GetVersionStr
: string;
80 property ListenAddresses
: TStrings read FListenAddresses
;
81 property ListenAddresses6
: TStrings read FListenAddresses6
;
82 property BindAddress
: string read FBindAddress
;
83 property BindAddress6
: string read FBindAddress6
;
84 property Databytes
: longint read FDatabytes
;
85 {property TimeCorrection: integer read FTimeCorrection;}
86 property TimeOffset
: integer read FTimeOffset
;
87 property TimeOffsetStr
: string read FTimeOffsetStr
;
88 property Policies
: boolean read FPolicies
;
89 property Mailbox
: boolean read FMailbox
;
90 property Relay
: boolean read FRelay
;
91 property Log
: boolean read FLog
;
92 property VersionStr
: string read GetVersionStr
;
98 TEMailProperties
= class
104 procedure SetSize(Value
: longint);
105 procedure WriteFlags(Value
: TEMailFlags
);
106 procedure SetFlag(Flag
: TEMailFlags
);
107 function HasFlag(Flag
: TEMailFlags
): boolean;
108 property Size
: longint read FSize write SetSize
;
109 property Flags
: TEMailFlags read FFlags write WriteFlags
;
114 Address
, RMsg
: string;
120 constructor Create(const Name
, IP
: string);
124 property Name
: string read FName
;
125 property IP
: string read FIP
;
126 function Copy
: TIPNamePair
;
132 destructor Destroy
; override;
134 FReturnPath
, FRelayHost
: string;
135 FReturnPathSpecified
: boolean;
136 FRecipients
: array of TRecipient
;
138 function GetNumberOfRecipients
: integer;
139 function GetRecipient(Index
: integer): TRecipient
;
140 function IsComplete
: boolean;
141 procedure AddRecipient(Address
: string; Data
: integer = 0; RMsg
: string = ''); overload
;
142 procedure AddRecipient(Recipient
: TRecipient
); overload
;
143 procedure SetReturnPath(Address
: string);
144 procedure SetRecipientData(Index
, Data
: integer; RMsg
: string = '');
145 procedure SetAllRecipientData(Data
: integer; RMsg
: string = '');
146 procedure SetRelayHost(HostName
: string);
147 property ReturnPath
: string read FReturnPath write SetReturnPath
;
148 property RelayHost
: string read FRelayHost write SetRelayHost
;
152 TEnvelopeArray
= array of TEnvelope
;
155 function EMailUserName(EMail
: string): string;
156 function EMailHost(EMail
: string): string;
157 function CleanEMailAddress(EMail
: string): string;
158 function IsValidEMailAddress(EMail
: string): boolean;
159 function EMailTimeStamp(DateTime
: TDateTime
): string;
160 function EMailTimeStampCorrected(DateTime
: TDateTime
): string;
161 function StatusToStr(Status
: integer): string;
162 procedure AssignDeliveryStatusToSMTPCodes(Envelope
: TEnvelope
; TransactionComplete
: boolean);
164 function CleanEOLN(S
: string): string;
165 function GenerateRandomString(Length
: integer): string;
166 function GetAlphabetStr
: string;
167 function GetServiceCodeStr(Ctrl
: dword
): string;
168 function GetWinMajorVersion
: longword
;
169 function IsPrintableString(S
: string): boolean;
170 function UnixTimeStamp(DateTime
: TDateTime
): TUnixTimeStamp
;
171 function CmdlineToStringArray
: TStringArray
;
172 procedure ParseIPv6Address(S
: string; var Address
: string; var Port
: word);
173 procedure SplitParameters(S
: string; var FirstPrm
, Remainder
: string; Separator
: char = #32);
175 function ReadLineFromStream(Stream
: TStream
): string;
176 function WriteLineToStream(Stream
: TStream
; Line
: string): boolean;
182 VERSION_STR
= '0.9t';
193 { Delivery statuses: }
194 DS_DELIVERED
= 1 shl 10;
195 DS_DELAYED
= 1 shl 11;
196 DS_PERMANENT
= 1 shl 12;
197 DS_INTERNALFAIL
= 1 shl 13;
198 DS_CONNECTIONFAIL
= 1 shl 14;
199 DS_UNEXPECTEDFAIL
= 1 shl 15;
200 DS_SMTPFAIL
= 1 shl 16;
201 DS_SMTPREPLYMASK
= $000003FF;
202 DS_ALLFLAGS
= $FFFFFFFF;
204 { E-mail property flags: }
207 DayNames
: array[1..7] of shortstring
= ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
208 MonthNames
: array[1..12] of shortstring
= ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
209 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
211 { Support for PRESHUTDOWN is not yet present in the Free Pascal library,
212 therefore I define the necessary constants here. It's a temporary
213 solution, I hope I won't need it for the next release of FPC. }
214 SERVICE_ACCEPT_PRESHUTDOWN
= $00000100;
215 SERVICE_CONTROL_PRESHUTDOWN
= $0000000F;
220 MainServerConfig
: TMainServerConfig
;
226 { Unit-private functions/prodecures: }
228 function InStringArray(const S
: string; const SA
: array of string): boolean;
232 while (i
< Length(SA
)) and (SA
[i
] <> S
) do Inc(i
);
233 Result
:= i
< Length(SA
);
236 function MakeTimeOffsetStr(TimeOffset
: integer): string;
237 var CorrS
: string; CorrI
: integer;
240 CorrS
:= IntToStr(Abs(CorrI
));
241 while Length(CorrS
) < 4 do CorrS
:= '0' + CorrS
;
242 if CorrI
>= 0 then CorrS
:= '+' + CorrS
else CorrS
:= '-' + CorrS
;
247 { Unit-public functions/procedures: }
249 function EMailUserName(EMail
: string): string;
253 while (p
> 0) and (EMail
[p
] <> '@') do Dec(p
);
255 Result
:= Copy(EMail
, 1, p
- 1);
260 function EMailHost(EMail
: string): string;
264 while (p
> 0) and (EMail
[p
] <> '@') do Dec(p
);
265 if (p
<> 0) and (p
< Length(EMail
)) then begin
266 Result
:= Copy(EMail
, p
+ 1, Length(EMail
) - p
);
271 function CleanEMailAddress(EMail
: string): string;
272 var po
, pc
, p
: integer;
274 po
:= Pos('<', EMail
);
275 pc
:= Pos('>', EMail
);
276 if (po
<> 0) and (pc
<> 0) and (po
< pc
) then begin
277 Result
:= Copy(EMail
, po
+ 1, pc
- po
- 1);
278 p
:= Pos(':', Result
);
280 Result
:= Copy(Result
, p
+ 1, Length(Result
) - p
);
286 function IsValidEMailAddress(EMail
: string): boolean;
288 { !!! TODO: Implement more strict checking later !!! }
289 Result
:= Pos('@', EMail
) <> 0;
292 function EMailTimeStamp(DateTime
: TDateTime
): string;
293 var Year
, Month
, Day
: word;
295 DecodeDate(DateTime
, Year
, Month
, Day
);
296 Result
:= DayNames
[DayOfWeek(DateTime
)] + ' ' + MonthNames
[Month
] + ' '
297 + FormatDateTime('dd hh:nn:ss', DateTime
) + ' ' + IntToStr(Year
);
300 function EMailTimeStampCorrected(DateTime
: TDateTime
): string;
302 Result
:= EMailTimeStamp(DateTime
) + ' ' + MainServerConfig
.TimeOffsetStr
;
305 function StatusToStr(Status
: integer): string;
306 { Returns the delivery status code in human-readable format. }
308 Result
:= IntToStr(Status
and (DS_ALLFLAGS
- DS_SMTPREPLYMASK
- DS_SMTPFAIL
))
309 + '+' + IntToStr(Status
and DS_SMTPREPLYMASK
);
312 procedure AssignDeliveryStatusToSMTPCodes(Envelope
: TEnvelope
; TransactionComplete
: boolean);
313 var i
, code
, cond
, status
: integer; Recipient
: TRecipient
;
315 for i
:= 0 to Envelope
.GetNumberOfRecipients
- 1 do begin
316 Recipient
:= Envelope
.GetRecipient(i
);
317 code
:= Recipient
.Data
and DS_SMTPREPLYMASK
;
320 0: status
:= DS_DELAYED
or DS_UNEXPECTEDFAIL
;
321 2: if TransactionComplete
then status
:= DS_DELIVERED
322 else status
:= DS_DELAYED
or DS_UNEXPECTEDFAIL
;
323 4: status
:= DS_DELAYED
;
324 5: status
:= DS_PERMANENT
;
325 else status
:= DS_PERMANENT
or DS_UNEXPECTEDFAIL
;
327 if code
<> 0 then status
:= status
or DS_SMTPFAIL
;
328 Envelope
.SetRecipientData(i
, code
or status
, Recipient
.RMsg
);
333 function CleanEOLN(S
: string): string;
335 while (Length(S
) <> 0) and (S
[Length(S
)] in [#13, #10]) do Delete(S
, Length(S
), 1);
339 function GenerateRandomString(Length
: integer): string;
340 var What
, Chrn
, i
: integer; Value
: string;
343 for i
:= 1 to Length
do begin
346 0: begin Chrn
:= Random(10)+48; Value
:= Value
+ Chr(Chrn
); end;
347 1: begin Chrn
:= Random(26)+65; Value
:= Value
+ Chr(Chrn
); end;
348 2: begin Chrn
:= Random(26)+97; Value
:= Value
+ Chr(Chrn
); end;
354 function GetAlphabetStr
: string;
358 for i
:= Ord('0') to Ord('9') do Result
:= Result
+ Chr(i
);
359 for i
:= Ord('A') to Ord('Z') do Result
:= Result
+ Chr(i
);
362 function GetServiceCodeStr(Ctrl
: dword
): string;
365 SERVICE_CONTROL_STOP
: Result
:= 'STOP';
366 SERVICE_CONTROL_SHUTDOWN
: Result
:= 'SHUTDOWN';
367 SERVICE_CONTROL_PRESHUTDOWN
: Result
:= 'PRESHUTDOWN';
368 else Result
:= IntToStr(Ctrl
);
372 function GetWinMajorVersion
: longword
;
373 var OSVersionInfo
: TOSVersionInfo
;
375 { Get OS version info. }
376 OSVersionInfo
.dwOSVersionInfoSize
:= SizeOf(TOSVersionInfo
);
377 GetVersionEx(OSVersionInfo
);
378 Result
:= OSVersionInfo
.dwMajorVersion
;
381 function IsPrintableString(S
: string): boolean;
382 { Check if string contains only printable ASCII characters. }
387 while Result
and (i
<= Length(S
)) do begin
388 Result
:= (Ord(S
[i
]) > 31) and (Ord(S
[i
]) < 127);
393 procedure ParseIPv6Address(S
: string; var Address
: string; var Port
: word);
394 { IPv6 addresses can be supplied in the following formats:
395 [<IPv6 address>]:<port> e.g. [::1]:25
397 <hostname>:<port> e.g. mail.example.com:25 }
398 var SPort
: string; c
: integer;
400 if S
[1] = '[' then begin
401 { Guess format is "[<IPv6 address>]:<port>". }
404 Address
:= Copy(S
, 2, c
- 2);
405 if c
= Length(S
) then begin
406 { There is no port to extract. }
407 Port
:= STANDARD_SMTP_PORT
;
410 { The closing bracket should be followed by a colon. }
412 { Extract port number. }
413 Port
:= StrToIntDef(Copy(S
, c
+2, Length(S
) - (c
+1)), 0)
420 { Format is incorrect, return invalid data. }
426 { Guess format is "<hostname>:<port>". }
427 SplitParameters(S
, Address
, SPort
, ':');
428 if (pos(':', Address
) = 0) and (pos(':', SPort
) = 0) then begin
429 { Format seems correct. }
430 if SPort
= '' then Port
:= STANDARD_SMTP_PORT
431 else Port
:= StrToIntDef(SPort
, 0);
434 { Format is incorrect, return invalid data. }
441 procedure SplitParameters(S
: string; var FirstPrm
, Remainder
: string; Separator
: char = #32);
444 i
:= pos(Separator
, S
);
446 FirstPrm
:= Copy(S
, 1, i
- 1);
447 Remainder
:= Copy(S
, i
+ 1, Length(S
) - i
);
455 function CmdlineToStringArray
: TStringArray
;
458 SetLength(Result
, ParamCount
);
459 for i
:= 1 to ParamCount
do
460 Result
[i
-1]:= ParamStr(i
);
463 function UnixTimeStamp(DateTime
: TDateTime
): TUnixTimeStamp
;
465 {Result:= Trunc((DateTime - EncodeDate(1970, 1 ,1)) * 24 * 60 * 60);}
466 Result
:= DateTimeToUnix(DateTime
);
470 function ReadLineFromStream(Stream
: TStream
): string;
471 var S
: string; B
: char;
476 B
:= Char(Stream
.ReadByte
);
477 if not (B
in [#10, #13]) then S
:= S
+ B
;
484 function WriteLineToStream(Stream
: TStream
; Line
: string): boolean;
490 Stream
.WriteBuffer(PChar(Line
)^, Length(Line
));
497 { Object constructors/destructors: }
499 constructor TArgumentParser
.Create(RawArguments
: array of string; AllowedPrefixes
: array of string);
502 for i
:= 0 to Length(RawArguments
) - 1 do
503 ParseArgument(RawArguments
[i
], AllowedPrefixes
);
506 destructor TArgumentParser
.Destroy
;
508 SetLength(Arguments
, 0);
511 constructor TNamedObject
.Create(const Name
: string; Config
: TINIFile
; const Section
: string);
515 FAliases
:= TStringList
.Create
;
516 FAliases
.Delimiter
:= ',';
517 FAliases
.DelimitedText
:= FName
+ ',' + Config
.ReadString(Section
, 'Alias', '');
520 constructor TMainServerConfig
.Create(Config
: TINIFile
);
521 var i
: integer; rawaddresslist
: string; portlist
: TStringList
;
523 inherited Create(Config
.ReadString('Server', 'Name', ''), Config
, 'Server');
524 FListenAddresses
:= TStringList
.Create
;
525 FListenAddresses
.Delimiter
:= ',';
527 rawaddresslist
:= Config
.ReadString('Server', 'ListenAddress', '');
528 if rawaddresslist
<> '' then
529 FListenAddresses
.DelimitedText
:= rawaddresslist
531 portlist
:= TStringList
.Create
;
532 portlist
.Delimiter
:= ',';
533 portlist
.DelimitedText
:= Config
.ReadString('Server', 'ListenPort', '25');
534 for i
:= 0 to portlist
.Count
- 1 do
535 FListenAddresses
.Add('0.0.0.0:' + portlist
.Strings
[i
]);
539 FListenAddresses6
:= TStringList
.Create
;
540 FListenAddresses6
.Delimiter
:= ',';
541 FListenAddresses6
.DelimitedText
:= Config
.ReadString('Server', 'ListenAddress6', '');
543 FBindAddress
:= Config
.ReadString('Server', 'BindAddress', '0.0.0.0');
544 FBindAddress6
:= Config
.ReadString('Server', 'BindAddress6', '[::]');
546 FDatabytes
:= Config
.ReadInteger('Server', 'Databytes', 1024 * 1024 * 1024);
547 FTimeOffset
:= Config
.ReadInteger('Server', 'TimeOffset', Config
.ReadInteger('Server', 'TimeCorrection', 0) * 100);
548 FTimeOffsetStr
:= MakeTimeOffsetStr(FTimeOffset
);
550 FPolicies
:= Config
.ReadBool('Server', 'Policies', false);
551 FMailbox
:= Config
.ReadBool('Server', 'Mailbox', false);
552 FRelay
:= Config
.ReadBool('Server', 'Relay', false);
553 FLog
:= Config
.ReadBool('Server', 'Log', false);
556 constructor TEMailProperties
.Create
;
563 constructor TIPNamePair
.Create(const Name
, IP
: string);
569 constructor TEnvelope
.Create
;
573 FReturnPathSpecified
:= false;
575 SetLength(FRecipients
, 0);
578 destructor TEnvelope
.Destroy
;
580 SetLength(FRecipients
, 0);
587 procedure TArgumentParser
.ParseArgument(Arg
: string; const AllowedPrefixes
: array of string);
588 var i
, n
: integer; found
: boolean;
590 { Strip prefix if present. }
591 i
:= 0; found
:= false;
592 while ((i
< Length(AllowedPrefixes
)) and (not found
)) do begin
593 if pos(AllowedPrefixes
[i
], Arg
) = 1 then
595 Delete(Arg
, 1, Length(AllowedPrefixes
[i
]));
601 n
:= Length(Arguments
);
602 SetLength(Arguments
, n
+ 1);
603 SplitParameters(Arg
, Arguments
[n
].Option
, Arguments
[n
].Value
, '=');
604 { To be case-insensitive: }
605 Arguments
[n
].Option
:= UpCase(Arguments
[n
].Option
);
608 function TArgumentParser
.GetArgument(ID
: integer): TArgument
;
610 { No index checking... you'd better use it return value of ValidateArguments. }
611 Result
:= Arguments
[ID
];
614 function TArgumentParser
.IsPresent(ArgumentName
: string): boolean;
618 while (i
< Length(Arguments
)) and (Arguments
[i
].Option
<> UpCase(ArgumentName
)) do
620 Result
:= i
< Length(Arguments
);
623 function TArgumentParser
.GetValue(ArgumentName
: string; DefValue
: string = ''): string;
627 while (i
< Length(Arguments
)) and (Arguments
[i
].Option
<> UpCase(ArgumentName
)) do
630 if i
< Length(Arguments
) then begin
631 if Arguments
[i
].Value
<> '' then
632 Result
:= Arguments
[i
].Value
640 function TArgumentParser
.ValidateArguments(ValidArguments
: array of string): integer;
641 { Returns -1 if all arguments are valid. Otherwise, returns the ID of the first
646 while (i
< Length(Arguments
)) and InStringArray(Arguments
[i
].Option
, ValidArguments
) do
649 if i
>= Length(Arguments
) then
656 function TNamedObject
.IsItYourName(const Name
: string): boolean;
658 Result
:= FAliases
.IndexOf(Name
) <> -1;
662 function TMainServerConfig
.GetVersionStr
: string;
664 Result
:= VERSION_STR
;
668 function TIPNamePair
.Copy
: TIPNamePair
;
670 Result
:= TIPNamePair
.Create(Name
, IP
);
674 procedure TEMailProperties
.SetSize(Value
: longint);
679 procedure TEMailProperties
.WriteFlags(Value
: TEMailFlags
);
684 procedure TEMailProperties
.SetFlag(Flag
: TEMailFlags
);
686 FFlags
:= FFlags
or Flag
;
689 function TEMailProperties
.HasFlag(Flag
: TEMailFlags
): boolean;
691 Result
:= (FFlags
and Flag
) = Flag
;
695 function TEnvelope
.GetNumberOfRecipients
: integer;
697 Result
:= Length(FRecipients
);
700 function TEnvelope
.GetRecipient(Index
: integer): TRecipient
;
702 Result
:= FRecipients
[Index
];
705 function TEnvelope
.IsComplete
: boolean;
707 Result
:= FReturnPathSpecified
and (Length(FRecipients
) > 0);
710 procedure TEnvelope
.AddRecipient(Address
: string; Data
: integer = 0; RMsg
: string = '');
713 i
:= Length(FRecipients
);
714 SetLength(FRecipients
, i
+ 1);
715 FRecipients
[i
].Address
:= Address
;
716 FRecipients
[i
].RMsg
:= RMsg
;
717 FRecipients
[i
].Data
:= Data
;
720 procedure TEnvelope
.AddRecipient(Recipient
: TRecipient
);
723 i
:= Length(FRecipients
);
724 SetLength(FRecipients
, i
+ 1);
725 FRecipients
[i
]:= Recipient
;
728 procedure TEnvelope
.SetRecipientData(Index
, Data
: integer; RMsg
: string = '');
730 FRecipients
[Index
].RMsg
:= RMsg
;
731 FRecipients
[Index
].Data
:= Data
;
734 procedure TEnvelope
.SetAllRecipientData(Data
: integer; RMsg
: string = '');
737 for i
:= 0 to Length(FRecipients
) - 1 do
738 SetRecipientData(i
, Data
, RMsg
);
741 procedure TEnvelope
.SetReturnPath(Address
: string);
743 FReturnPath
:= Address
;
744 FReturnPathSpecified
:= true;
747 procedure TEnvelope
.SetRelayHost(HostName
: string);
749 FRelayHost
:= HostName
;