program PostCommitMailQu; // Angus 27 July 2022 // run by post-commit.exe with the same arguments passed to it. {$APPTYPE CONSOLE} uses SysUtils, Windows, Messages, Classes, Contnrs, IniFiles, Iphelper, OverbyteIcsWndControl, OverbyteIcsWSocket, OverbyteIcsSmtpProt, OverbyteIcsUtils, OverbyteIcsLogger, OverbyteIcsLIBEAY, OverbyteIcsSSLEAY, OverbyteIcsMailQueue, OverbyteIcsBlacklist, OverbyteIcsSslHttpOAuth; var CurRepoPath : String = ''; CurRev : String = ''; type TCurState = (csGetMx, csSmtp); TMailer = class(TIcsWndControl) public MagMailQueue: TIcsMailQueue; IcsRestEmail: TIcsRestEmail; RcptList : TStringList; CurRepoPath : String; CurRev : String; Subject : String; FromEmail : String; LocalAddr : String; HdrReturnPath : String; CurCount : Integer; CurRcpt : Integer; Timer : TIcsTimer; LastError : String; SendFlag : Boolean; CurState : TCurState; Changelog : TStringList; PathToSvnBin: String; HeloMsg : String; CurThreadID : Cardinal; LogTitle : String; // Angus OldRefrToken : String; constructor Create(AOwner: TComponent); override; destructor Destroy; override; // procedure OnTimer(Sender: TObject); // procedure PumpMessages; function ReadIni: Boolean; // procedure SendNextMail; function GetChangeLog: Boolean; // procedure RelayRequestDone(Sender : TObject; RqType: TSmtpRequest; ErrCode : Word); // procedure SmtpSessionClosed(Sender : TObject; ErrCode : Word); procedure MagMailQueueLogEvent(LogLevel: TMailLogLevel; const Info: string); procedure IcsRestEmailEmailNewToken(Sender: TObject); procedure IcsRestEmailEmailProg(Sender: TObject; LogOption: TLogOption; const Msg: string); procedure MagMailQueueOATokenEvent(ServNr: Integer; var Token, TokAccount: string; var TokExpireDT: TDateTime); procedure Run; end; //////////////////////////////////////////////////////////////////////////////// function ExtractDomain(const Email: String): String; var At : Integer; begin At := Pos('@', Email); if At = -1 then Exit; Result := Copy(EMail, At + 1, MaxInt); if (Result = '') or (Pos('.', Result) = -1) then begin Result := ''; Exit; end; end; // must turn off range checking or encyption stuff dies !!!!! {$R-} {$Q-} // more encryption stuff, based on Borland but a little more random // encrypted string is binary values const D2 = 52845; function strRDecrypt(const S: AnsiString; Key: Word): AnsiString; var I, len: Integer; D1: word ; outstr: string [255] ; begin len := Length(S) ; if len > 2 then begin Move (S [1], D1, 2) ; // keep multiply value Key := Key + D1 ; outstr [0] := AnsiChar (len - 2) ; for I := 1 to Len do begin outstr[I] := AnsiChar (Ord (S [I + 2]) xor (Key shr 8)); Key := (Ord (S [I + 2]) + Key) * D1 + D2; end; end ; result := copy (outstr, 1, len - 2) ; end; {$R+} {$Q+} function strBXDecryptEx(const S: String; Key: Word): String; var keylen, strlen: integer; temp: AnsiString ; begin result := '' ; if S = '' then exit ; try temp := Base64decode (S) ; strlen := length (temp) ; if (strlen = 3) and (temp = '000') then exit ; // blank encoded string if (strlen > 3) then begin keylen := atoi (Copy (temp, 1, 3)) ; // get length from start // 7 Aug 2010 if (keylen + 3) = strlen then begin temp := Copy (temp, 4, 999) ; result := String (strRDecrypt (temp, Key)) ; end; end; if result = '' then result := S ; // blank except ; result := '' ; end ; end; //////////////////////////////////////////////////////////////////////////////// var LogStream : TFileStream = nil; procedure LogLine(const Msg: String); var FileName : String; DateStr : String; begin try if not Assigned(LogStream) then begin FileName := ChangeFileExt(ParamStr(0), '.log'); if not FileExists(FileName) then LogStream := TFileStream.Create(FileName, fmCreate or fmShareDenyWrite) else begin LogStream := TFileStream.Create(FileName, fmOpenReadWrite or fmShareDenyWrite); LogStream.Seek(0, sofromEnd); end; end; DateTimeToString(DateStr, 'yyyy:mm:dd hh:nn:ss ', Now); LogStream.Write(Pointer(DateStr)^, Length(DateStr)); LogStream.Write(Pointer(Msg)^, Length(Msg)); LogStream.Write(PChar(#13#10)^, 2); except end; end; //////////////////////////////////////////////////////////////////////////////// function TMailer.ReadIni : Boolean; const Section = 'SETTINGS'; var Ini : TIniFile; S : String; I, J, Len : Integer; begin Result := FALSE; Ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini')); try // Host := Ini.ReadString(Section, 'Host', 'mail.magsys.co.uk'); // Port := Ini.ReadString(Section, 'Port', '587'); // User := Ini.ReadString(Section, 'UserName', 'foo'); // Password := Ini.ReadString(Section, 'Password', 'bar'); // HeloMsg := Ini.ReadString(Section, 'HeloMsg', '79.99.69.242'); HdrReturnPath := Ini.ReadString(Section, 'HdrReturnPath', 'admin@localhost'); // LocalAddr := Ini.ReadString(Section, 'LocalAddr', '0.0.0.0'); // if LocalAddr = '' then LocalAddr := '0.0.0.0'; PathToSvnBin := Ini.ReadString(Section, 'PathToSvnBin', 'c:\program files\subversion\bin'); if PathToSvnBin = '' then PathToSvnBin := 'c:\program files\subversion\bin'; FromEmail := Ini.ReadString(Section, 'FromEmail', 'admin@localhost'); if FromEmail = '' then FromEmail := 'admin@localhost'; Subject := Ini.ReadString(Section, 'Subject', '[ICS SVN] Updated to revision #%s'); S := Ini.ReadString(Section, 'RcptList', 'admin@localhost'); I := 1; J := 1; Len := Length(S); while I <= Len do begin if I = Len then RcptList.Add(Copy(S, J, MaxInt)) else if (S[I] in [',', ';']) then begin RcptList.Add(Copy(S, J, I - J)); J := I + 1; end; Inc(I); end; Result := TRUE; IcsLoadMailQuFromIni(Ini, MagMailQueue, 'MailQueue'); IcsLoadRestEmailFromIni(Ini, IcsRestEmail, 'RestEmail'); FreeAndNil(Ini); except FreeAndNil(Ini); end; end; //////////////////////////////////////////////////////////////////////////////// procedure RunProc(const ConsoleApp: String; AStrings: TStrings); var SAttr : TSecurityAttributes; SInfo : TStartUpInfo; PInfo : TProcessInformation; ReadPipe,WritePipe : THandle; BytesRead : DWord; WaitRes : DWord; ReadBuffer : array [0..255] of Char; LineBuf : array [0..1024] of Char; LineBufPtr: Integer; I : Integer; NewLine : Boolean; begin with SAttr do begin nlength := SizeOf(TSecurityAttributes); binherithandle := True; lpsecuritydescriptor := nil; end; if not CreatePipe(ReadPipe, WritePipe, @SAttr, 0) then RaiseLastOSError; try FillChar(SInfo, Sizeof(SInfo), #0); SInfo.cb := SizeOf(SInfo); SInfo.hStdOutput := WritePipe; SInfo.hStdError := WritePipe; SInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE); SInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW; SInfo.wShowWindow := SW_HIDE; if CreateProcess(nil, PChar(ConsoleApp), @SAttr, @SAttr, True, NORMAL_PRIORITY_CLASS, nil, nil, SInfo, PInfo) then begin try if WaitForInputIdle(PInfo.hProcess, 100) <> $FFFFFFFF then raise Exception.Create('Not a console app.'); LineBufPtr := 0; Newline := True; CloseHandle(WritePipe); WritePipe := 0; while ReadFile(ReadPipe, ReadBuffer[0], SizeOf(ReadBuffer), BytesRead, nil) do begin for I := 0 to BytesRead - 1 do begin if (ReadBuffer[I] = #10) then Newline := True else if (ReadBuffer[I] = #13) then begin LineBuf[LineBufPtr]:= #0; OemToChar(LineBuf, LineBuf); if Assigned(AStrings) then begin if Newline then AStrings.Add(String(LineBuf)) else AStrings[AStrings.Count -1] := LineBuf; end; Newline := False; LineBufPtr := 0; end else begin LineBuf[LineBufPtr] := ReadBuffer[I]; Inc(LineBufPtr); if LineBufPtr >= (SizeOf(LineBuf) - 1) then begin Newline := True; LineBuf[LineBufPtr]:= #0; OemToChar(LineBuf, LineBuf); if Assigned(AStrings) then begin if Newline then AStrings.Add(LineBuf) else AStrings[AStrings.Count -1] := LineBuf; end; Newline := False; LineBufPtr := 0; end; end; end; end; WaitRes := WaitForSingleObject(PInfo.hProcess, 1000); if WaitRes <> 0 then TerminateProcess(PInfo.hProcess, BytesRead); finally CloseHandle(PInfo.hProcess); CloseHandle(PInfo.hThread); end; end else RaiseLastOSError; finally CloseHandle(ReadPipe); if WritePipe <> 0 then CloseHandle(WritePipe); end; end; //////////////////////////////////////////////////////////////////////////////// constructor TMailer.Create(AOwner: TComponent); begin inherited Create(AOwner); GetHandle; CurThreadID := GetCurrentThreadID; RcptList := TStringList.Create; MagMailQueue := TIcsMailQueue.Create(self); IcsRestEmail := TIcsRestEmail.Create(self); MagMailQueue.MultiThreaded := True; MagMailQueue.LogEvent := MagMailQueueLogEvent; MagMailQueue.OATokenEvent := MagMailQueueOATokenEvent; IcsRestEmail.OnEmailNewToken := IcsRestEmailEmailNewToken; IcsRestEmail.OnEmailProg := IcsRestEmailEmailProg; // DnsServers := TStringList.Create; end; //////////////////////////////////////////////////////////////////////////////// destructor TMailer.Destroy; begin FreeAndNil(MagMailQueue); FreeAndNil(IcsRestEmail); FreeAndNil(Timer); FreeAndNil(ChangeLog); FreeAndNil(RcptList); // FreeAndNil(DnsServers); inherited; end; //////////////////////////////////////////////////////////////////////////////// (* procedure TMailer.DirectRequestDone(Sender: TObject; RqType: TSmtpRequest; ErrCode: Word); var Cli : TSmtpCli; begin CurCount := 0; Cli := TSmtpCli(Sender); try if (ErrCode <> 0) or ((RqType <> smtpQuit) and (not Cli.Connected)) then begin if ErrCode > 10000 then LogLine(Cli.Host + ' ' + Cli.HdrTo + ' Rq=' + IntToStr(Ord(Cli.RequestType)) + ' ' + GetWinsockErr(ErrCode)) else LogLine(Cli.Host + ' ' + Cli.HdrTo + ' Rq=' + IntToStr(Ord(Cli.RequestType)) + ' ' + Cli.LastResponse); if Cli.Connected then Cli.Quit else Timer.Enabled := False; Exit; end; case RqType of smtpConnect : Cli.Ehlo; smtpEhlo : Cli.MailFrom; smtpMailFrom : Cli.RcptTo; smtpRcptTo : Cli.Data; smtpData : Cli.Quit; smtpQuit : Timer.Enabled := False; else Timer.Enabled := False; LastError := 'Unknown requesttype'; Cli.Quit; end; except on E: Exception do begin Timer.Enabled := False; LogLine(E.ClassName + ' ' + E.Message); Cli.Abort; PostThreadMessage(CurThreadID, WM_RESOLVE_NEXT, 0, 0); end; end; end; *) //////////////////////////////////////////////////////////////////////////////// (* procedure TMailer.RelayRequestDone(Sender: TObject; RqType: TSmtpRequest; ErrCode: Word); var Cli : TSmtpCli; begin try CurCount := 0; Cli := TSmtpCli(Sender); if (ErrCode <> 0) and Cli.Connected then begin if RqType = smtpRcptTo then begin if CurRcpt > 0 then begin Smtp.RcptName.Text := RcptList[CurRcpt - 1]; Dec(CurRcpt); Cli.RcptTo; end else if SendFlag then Cli.Data else Cli.Quit; Exit; end; CurRcpt := 0; LastError := Cli.HdrTo + ' Rq=' + IntToStr(Ord(Cli.RequestType)) + ' ' + Cli.LastResponse; Cli.Quit; Exit; end else if ((RqType <> smtpQuit) and (not Cli.Connected)) then begin if ErrCode > 10000 then LastError := Cli.HdrTo + ' Rq=' + IntToStr(Ord(Cli.RequestType)) + ' ' + GetWinsockErr(ErrCode) else LastError := Cli.HdrTo + ' Rq=' + IntToStr(Ord(Cli.RequestType)) + ' ' + Cli.LastResponse; CurRcpt := 0; Timer.Enabled := False; PostThreadMessage(CurThreadID, WM_QUIT, 0, 0); Exit; end; case RqType of smtpConnect : Cli.Ehlo; smtpEhlo : Cli.Auth; smtpAuth : Cli.MailFrom; smtpMailFrom : if CurRcpt > 0 then begin Cli.RcptName.Text := RcptList[CurRcpt - 1]; Cli.RcptTo; end; smtpRcptTo : begin if Cli.HdrTo <> '' then Cli.HdrTo := Cli.HdrTo + ';' + RcptList[CurRcpt - 1] else Cli.HdrTo := RcptList[CurRcpt - 1]; Dec(CurRcpt); SendFlag := TRUE; if CurRcpt > 0 then begin Cli.RcptName.Text := RcptList[CurRcpt - 1]; Cli.RcptTo; end else Cli.Data; end; smtpData : begin LogLine('Sent Email OK to: ' + Smtp.HdrTo); Cli.Quit; end; smtpQuit : Timer.Enabled := False; else Timer.Enabled := False; CurRcpt := 0; LastError := 'Unknown request type'; Cli.Quit; end; except on E: Exception do begin LogLine(E.ClassName + ' ' + E.Message); PostThreadMessage(CurThreadID, WM_QUIT, 0, 0); end; end; end; //////////////////////////////////////////////////////////////////////////////// procedure TMailer.SmtpSessionClosed(Sender: TObject; ErrCode: Word); begin Timer.Enabled := False; PostThreadMessage(CurThreadID, WM_QUIT, 0, 0); end; //////////////////////////////////////////////////////////////////////////////// procedure TMailer.SendNextMail; begin CurState := csSmtp; if not Assigned(Smtp) then Smtp := TSmtpCli.Create(Self); if Host = '' then raise Exception.Create('Host name empty'); Smtp.Host := Host; Smtp.LocalAddr := LocalAddr; Smtp.FromName := FromEmail; Smtp.HdrFrom := '<' + Smtp.FromName + '>'; Smtp.HdrSubject := Format(Subject, [CurRev, LogTitle]); // ANGUS added title Smtp.HdrReturnPath := '<' + HdrReturnPath + '>'; Smtp.SignOn := HeloMsg; Smtp.Allow8bitChars := FALSE; Smtp.WrapMessageText := TRUE; Smtp.FoldHeaders := TRUE; Smtp.MailMessage.Assign(ChangeLog); Smtp.AuthType := smtpAuthAutoSelect; Smtp.Username := User; Smtp.Password := Password; Smtp.HdrTo := RcptList[CurRcpt - 1]; Smtp.RcptName.Text := RcptList[CurRcpt -1]; Dec(CurRcpt); Smtp.OnSessionClosed := SmtpSessionClosed; Smtp.OnRequestDone := RelayRequestDone; SendFlag := FALSE; if not Assigned(Timer) then Timer := TIcsTimer.Create(Self); Timer.OnTimer := OnTimer; Timer.Interval := 2000; CurCount := 0; LogLine('Sending Email to: ' + Smtp.HdrTo); Smtp.Connect; Timer.Enabled := TRUE; end; //////////////////////////////////////////////////////////////////////////////// procedure TMailer.OnTimer(Sender: TObject); begin Inc(CurCount); if CurState = csSmtp then begin if CurCount * Integer(Timer.Interval) >= 30 * 1000 then begin Timer.Enabled := False; CurCount := 0; LogLine('Smtp timeout'); PostThreadMessage(CurThreadID, WM_QUIT, 0, 0); end; end; end; *) //////////////////////////////////////////////////////////////////////////////// function TMailer.GetChangeLog: Boolean; var S1, S2 : String; begin Result := FALSE; LogTitle := 'Unknown'; if not Assigned(ChangeLog) then ChangeLog := TStringList.Create; S1 := '"' + IncludeTrailingPathDelimiter(PathToSvnBin) + 'svnlook.exe" '; S2 := S1 + 'author -r ' + CurRev + ' "' + CurRepoPath + '"'; RunProc(S2, ChangeLog); if ChangeLog.Count > 0 then ChangeLog[0] := 'Author: ' + ChangeLog[0] else Exit; S2 := S1 + 'log "' + CurRepoPath + '" -r ' + CurRev; RunProc(S2, ChangeLog); if ChangeLog.Count > 1 then begin LogTitle := ChangeLog[1]; ChangeLog[1] := 'Log: ' + LogTitle; end; ChangeLog.Add(#13#10 + 'Files:'); S2 := S1 + 'changed "' + CurRepoPath + '" -r ' + CurRev; RunProc(S2, ChangeLog); Result := ChangeLog.Count > 2; end; //////////////////////////////////////////////////////////////////////////////// (* procedure TMailer.PumpMessages; var Msg : TMsg; begin while GetMessage(Msg, 0, 0, 0) do begin if Msg.hwnd = 0 then begin // case Msg.message of // WM_NEXT_MAIL : SendNextMail; // else TranslateMessage(Msg); DispatchMessage(Msg); // end; end else begin TranslateMessage(Msg); DispatchMessage(Msg); end; end; end; *) procedure TMailer.MagMailQueueLogEvent(LogLevel: TMailLogLevel; const Info: string); begin LogLine (Info) ; end; procedure TMailer.MagMailQueueOATokenEvent(ServNr: Integer; var Token, TokAccount: string; var TokExpireDT: TDateTime); begin LogLine('Starting to get OAuth2 Bearer Token'); if NOT IcsRestEmail.GetNewToken(False) then // allow interaction, waits for browser window to be completed LogLine('Failed to get OAuth2 Bearer Token') else begin Token := IcsRestEmail.AccToken; TokExpireDT := IcsRestEmail.AccExpireDT; TokAccount := IcsRestEmail.NewAccEmail; // note, AccToken has a short life, few hours, no need to save it in INI file, it can be refreshed end; end; procedure TMailer.IcsRestEmailEmailNewToken(Sender: TObject); var info: String; begin // we should save refresh token since only get it after a login window if (IcsRestEmail.RefrToken <> '') and (IcsRestEmail.RefrToken <> OldRefrToken) then begin info := 'New OAuth2 Refresh Token for Account: ' + IcsRestEmail.NewAccEmail + ', Should be Saved in Config File' + IcsCRLF + 'RefrToken=' + IcsRestEmail.RefrToken; LogLine(info); end; end; procedure TMailer.IcsRestEmailEmailProg(Sender: TObject; LogOption: TLogOption; const Msg: string); begin LogLine(Msg); end; //////////////////////////////////////////////////////////////////////////////// procedure TMailer.Run; var MyOpenSslVersion, S: string ; id, I: integer ; Trg: longword ; begin if not ReadIni then begin LastError := 'Could not read INI'; Exit; end; if RcptList.Count = 0 then begin LastError := 'No recipients'; Exit; end; if not GetChangeLog then begin LastError := 'Could not get change log'; Exit; end; CurRcpt := RcptList.Count; // load OpenSSL try // start email queue, reads INI, logs OpenSSL version // warning - SSL fails if one mail server does not use SSL if (MagMailQueue.MailQuDir = '') or (NOT ForceDirectories (MagMailQueue.MailQuDir)) then begin LogLine ('!! Failed to Start Mail Queue, No Directory') ; exit ; end; OldRefrToken := IcsRestEmail.RefrToken; // June 2022 if MagMailQueue.MailServers.Count = 0 then begin LogLine ('!! Failed to Start Mail Queue, No Mail Servers') ; exit ; end; S := ''; for I := 0 to MagMailQueue.MailServers.Count - 1 do begin MagMailQueue.MailServers[I].Password := strBXDecryptEx ( MagMailQueue.MailServers[I].Password, 61234) ; S := S + MagMailQueue.MailServers[I].Host + ', '; end; MagMailQueue.Active := true ; if MagMailQueue.Active then begin LogLine ('Started Mail Queue OK, Servers: ' + S) ; end; if GSSLStaticLinked then // Mar 2021 MyOpenSslVersion := 'Static ' + OpenSslVersion else MyOpenSslVersion := 'DLLs ' + OpenSslVersion; except LogLine ('!! Failed to Start Mail Queue - ' + IcsGetExceptMess (ExceptObject)) ; if NOT GSSLStaticLinked then LogLine ('SSL Filename: ' + GLIBEAY_DLL_FileName) ; end ; MagMailQueue.ProcessMessages; if NOT MagMailQueue.Active then begin LogLine ('Error Starting Mail Queue') ; Exit; end ; try with MagMailQueue.QuHtmlSmtp do begin EmailFiles.Clear ; RcptName.clear; Allow8bitChars := true ; ContentType := smtpPlainText ; WrapMessageText := false ; WrapMsgMaxLineLen := 76 ; PlainText.Clear ; PlainText.Assign(ChangeLog); FromName := FromEmail ; HdrCc := '' ; HdrSubject := Format(Subject, [CurRev, LogTitle]); // ANGUS added title HdrFrom := '<' + FromEmail + '>'; HdrReplyTo := FromEmail ; XMailer := '' ; for I := 0 to RcptList.Count - 1 do begin HdrTo := RcptList[I]; RcptName.Clear ; RcptName.Add (HdrTo) ; LogLine ('Queuing Email Form to ' + HdrTo) ; id := MagMailQueue.QueueMail ; MagMailQueue.ProcessMessages; if id > 0 then begin // done OK LogLine ('Email Queued OK') ; end else begin LogLine ('Failed to Queue Email: ' + ErrorMessage) ; end ; end; end ; except LogLine ('Failed to Queue Email: ' + IcsGetExceptMess (ExceptObject)) ; end; MagMailQueue.ProcessMessages; if MagMailQueue.Active and MagMailQueue.QuThreadRunning and (MagMailQueue.MailImmItems > 0) then begin LogLine ('Waiting up to 10 minutes to send Queued Mail Items') ; Trg := IcsGetTrgSecs (600) ; while true do begin if (MagMailQueue.MailImmItems = 0) then break ; // wait until all immediate items sent MagMailQueue.ProcessMessages; if MagMailQueue.Terminated then break ; if NOT MagMailQueue.QuThreadRunning then break ; // or thread stops if NOT MagMailQueue.Active then break ; // or someone else cancels it if IcsTestTrgTick (Trg) then break ; // or we get bored waiting end; end; (* Trg := IcsGetTrgSecs (20) ; while true do begin // PumpMessages; MagMailQueue.ProcessMessages; if MagMailQueue.Terminated then break ; // if NOT MagMailQueue.QuThreadRunning then break ; // or thread stops // if NOT MagMailQueue.Active then break ; // or someone else cancels it if IcsTestTrgTick (Trg) then break ; // or we get bored waiting // Sleep(100); end; *) end; //////////////////////////////////////////////////////////////////////////////// var Mailer : TMailer; begin Mailer := TMailer.Create(nil); try LogLine('Started ' + ParamStr(0)); LogLine('Command Line: ' + GetCommandLineA); if ParamCount > 1 then begin Mailer.CurRepoPath := ParamStr(1); Mailer.CurRev := ParamStr(2); try LogLine('Starting to Send Emails for SVN: ' + ParamStr(1) + ' revison ' + ParamStr(2)) ; Mailer.Run; except on E : Exception do begin LogLine(E.ClassName + ' ' + E.Message); Exit; end; end; if Mailer.LastError <> '' then LogLine(Mailer.LastError); end else begin LogLine('Failed to Send Email, No SVN Parameters Found'); end; LogLine('Stopped ' + ParamStr(0)); finally FreeAndNil(LogStream); FreeAndNil(Mailer); end; end.