(Relay) Handle unexpected replies and disconnections adequately
[mgsmtp.git] / Common.pas
1 {
2 Copyright (C) 2010-2015 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 FListenPorts: TStrings;
77 public
78 function GetVersionStr: string;
79 property ListenPorts: TStrings read FListenPorts;
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 begin
469 inherited Create(Config.ReadString('Server', 'Name', ''), Config, 'Server');
470 FListenPorts:= TStringList.Create;
471 FListenPorts.Delimiter:= ',';
472 FListenPorts.DelimitedText:= Config.ReadString('Server', 'ListenPort', '25');
473
474 FDatabytes:= Config.ReadInteger('Server', 'Databytes', 1024 * 1024 * 1024);
475 {FTimeCorrection:= Config.ReadInteger('Server', 'TimeCorrection', 0);}
476 FTimeOffset:= Config.ReadInteger('Server', 'TimeOffset', Config.ReadInteger('Server', 'TimeCorrection', 0) * 100);
477 FTimeOffsetStr:= MakeTimeOffsetStr(FTimeOffset);
478
479 FPolicies:= Config.ReadBool('Server', 'Policies', false);
480 FMailbox:= Config.ReadBool('Server', 'Mailbox', false);
481 FRelay:= Config.ReadBool('Server', 'Relay', false);
482 FLog:= Config.ReadBool('Server', 'Log', false);
483 end;
484
485 constructor TEMailProperties.Create;
486 begin
487 inherited Create;
488 SetSize(0);
489 WriteFlags(0);
490 end;
491
492 constructor TIPNamePair.Create(const Name, IP: string);
493 begin
494 FName:= Name;
495 FIP:= IP;
496 end;
497
498 constructor TEnvelope.Create;
499 begin
500 inherited Create;
501 FReturnPath:= '';
502 FReturnPathSpecified:= false;
503 FRelayHost:= '';
504 SetLength(FRecipients, 0);
505 end;
506
507 destructor TEnvelope.Destroy;
508 begin
509 SetLength(FRecipients, 0);
510 inherited Destroy;
511 end;
512
513
514 { Object methods: }
515
516 procedure TArgumentParser.ParseArgument(Arg: string; const AllowedPrefixes: array of string);
517 var i, n: integer; found: boolean;
518 begin
519 { Strip prefix if present. }
520 i:= 0; found:= false;
521 while ((i < Length(AllowedPrefixes)) and (not found)) do begin
522 if pos(AllowedPrefixes[i], Arg) = 1 then
523 begin
524 Delete(Arg, 1, Length(AllowedPrefixes[i]));
525 found:= true;
526 end;
527 Inc(i);
528 end;
529
530 n:= Length(Arguments);
531 SetLength(Arguments, n + 1);
532 SplitParameters(Arg, Arguments[n].Option, Arguments[n].Value, '=');
533 { To be case-insensitive: }
534 Arguments[n].Option:= UpCase(Arguments[n].Option);
535 end;
536
537 function TArgumentParser.GetArgument(ID: integer): TArgument;
538 begin
539 { No index checking... you'd better use it return value of ValidateArguments. }
540 Result:= Arguments[ID];
541 end;
542
543 function TArgumentParser.IsPresent(ArgumentName: string): boolean;
544 var i: integer;
545 begin
546 i:= 0;
547 while (i < Length(Arguments)) and (Arguments[i].Option <> UpCase(ArgumentName)) do
548 Inc(i);
549 Result:= i < Length(Arguments);
550 end;
551
552 function TArgumentParser.GetValue(ArgumentName: string; DefValue: string = ''): string;
553 var i: integer;
554 begin
555 i:= 0;
556 while (i < Length(Arguments)) and (Arguments[i].Option <> UpCase(ArgumentName)) do
557 Inc(i);
558
559 if i < Length(Arguments) then begin
560 if Arguments[i].Value <> '' then
561 Result:= Arguments[i].Value
562 else
563 Result:= DefValue;
564 end
565 else
566 Result:= DefValue;
567 end;
568
569 function TArgumentParser.ValidateArguments(ValidArguments: array of string): integer;
570 { Returns -1 if all arguments are valid. Otherwise, returns the ID of the first
571 invalid parameter. }
572 var i: integer;
573 begin
574 i:= 0;
575 while (i < Length(Arguments)) and InStringArray(Arguments[i].Option, ValidArguments) do
576 Inc(i);
577
578 if i >= Length(Arguments) then
579 Result:= -1
580 else
581 Result:= i;
582 end;
583
584
585 function TNamedObject.IsItYourName(const Name: string): boolean;
586 begin
587 Result:= FAliases.IndexOf(Name) <> -1;
588 end;
589
590
591 function TMainServerConfig.GetVersionStr: string;
592 begin
593 Result:= VERSION_STR;
594 end;
595
596
597 function TIPNamePair.Copy: TIPNamePair;
598 begin
599 Result:= TIPNamePair.Create(Name, IP);
600 end;
601
602
603 procedure TEMailProperties.SetSize(Value: longint);
604 begin
605 FSize:= Value;
606 end;
607
608 procedure TEMailProperties.WriteFlags(Value: TEMailFlags);
609 begin
610 FFlags:= Value;
611 end;
612
613 procedure TEMailProperties.SetFlag(Flag: TEMailFlags);
614 begin
615 FFlags:= FFlags or Flag;
616 end;
617
618 function TEMailProperties.HasFlag(Flag: TEMailFlags): boolean;
619 begin
620 Result:= (FFlags and Flag) = Flag;
621 end;
622
623
624 function TEnvelope.GetNumberOfRecipients: integer;
625 begin
626 Result:= Length(FRecipients);
627 end;
628
629 function TEnvelope.GetRecipient(Index: integer): TRecipient;
630 begin
631 Result:= FRecipients[Index];
632 end;
633
634 function TEnvelope.IsComplete: boolean;
635 begin
636 Result:= FReturnPathSpecified and (Length(FRecipients) > 0);
637 end;
638
639 procedure TEnvelope.AddRecipient(Address: string; Data: integer = 0; RMsg: string = '');
640 var i: integer;
641 begin
642 i:= Length(FRecipients);
643 SetLength(FRecipients, i + 1);
644 FRecipients[i].Address:= Address;
645 FRecipients[i].RMsg:= RMsg;
646 FRecipients[i].Data:= Data;
647 end;
648
649 procedure TEnvelope.AddRecipient(Recipient: TRecipient);
650 var i: integer;
651 begin
652 i:= Length(FRecipients);
653 SetLength(FRecipients, i + 1);
654 FRecipients[i]:= Recipient;
655 end;
656
657 procedure TEnvelope.SetRecipientData(Index, Data: integer; RMsg: string = '');
658 begin
659 FRecipients[Index].RMsg:= RMsg;
660 FRecipients[Index].Data:= Data;
661 end;
662
663 procedure TEnvelope.SetAllRecipientData(Data: integer; RMsg: string = '');
664 var i: integer;
665 begin
666 for i:= 0 to Length(FRecipients) - 1 do
667 SetRecipientData(i, Data, RMsg);
668 end;
669
670 procedure TEnvelope.SetReturnPath(Address: string);
671 begin
672 FReturnPath:= Address;
673 FReturnPathSpecified:= true;
674 end;
675
676 procedure TEnvelope.SetRelayHost(HostName: string);
677 begin
678 FRelayHost:= HostName;
679 end;
680
681
682 end.