{
  $Project$
  $Workfile$
  $Revision$
  $DateUTC$
  $Id$

  This file is part of the Indy (Internet Direct) project, and is offered
  under the dual-licensing agreement described on the Indy website.
  (http://www.indyproject.org/)

  Copyright:
   (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
  $Log$
}
{
  Rev 1.8    7/23/04 1:32:08 PM  RLebeau
  Bug fix for TIdSyslogFacility where sfUUCP and sfClockDeamonOne were in the
  wrong order

  Rev 1.7    7/8/04 11:43:08 PM  RLebeau
  Updated ReadFromBytes(c) to use new BytesToString() parameters

  Rev 1.6    2004.02.03 5:44:28 PM  czhower
  Name changes

  Rev 1.5    1/31/2004 1:23:24 PM  JPMugaas
  Eliminated Todo item.

  Rev 1.4    2004.01.22 3:23:36 PM  czhower
  IsCharInSet

  Rev 1.3    1/21/2004 4:03:58 PM  JPMugaas
  InitComponent

  Rev 1.2    10/24/2003 01:58:30 PM  JPMugaas
  Attempt to port Syslog over to new code.

  Rev 1.1    2003.10.12 6:36:44 PM  czhower
  Now compiles.

  Rev 1.0    11/13/2002 08:02:12 AM  JPMugaas
}

unit IdSysLogMessage;

{
  Copyright the Indy pit crew
  Original Author: Stephane Grobety (grobety@fulgan.com)
  Release history:
  25/2/02; - Stephane Grobety
    - Moved Facility and Severity translation functions out of the class
    - Restored the "SendToHost" method
    - Changed the ASCII check tzo include only the PRI and HEADER part.
    - Now allow nul chars in message result (Special handeling should be required, though)
  09/20/01;  - J. Peter Mugaas
    Added more properties dealing with Msg parts of the SysLog Message
  09/19/01; - J. Peter Mugaas
    restructured syslog classes
  08/09/01: Dev started
}

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdGlobal, IdGlobalProtocols, IdBaseComponent;

type
//  TIdSyslogSeverity = ID_SYSLOG_SEVERITY_EMERGENCY..ID_SYSLOG_SEVERITY_DEBUG;
//  TIdSyslogFacility = ID_SYSLOG_FACILITY_KERNEL..ID_SYSLOG_FACILITY_LOCAL7;
  TIdSyslogPRI = 0..191;
  TIdSyslogFacility = (sfKernel, { ID_SYSLOG_FACILITY_KERNEL}
                      sfUserLevel, { ID_SYSLOG_FACILITY_USER }
                      sfMailSystem, { ID_SYSLOG_FACILITY_MAIL }
                      sfSystemDaemon, { ID_SYSLOG_FACILITY_SYS_DAEMON }
                      sfSecurityOne, { ID_SYSLOG_FACILITY_SECURITY1 }
                      sfSysLogInternal, { ID_SYSLOG_FACILITY_INTERNAL }
                      sfLPR, {ID_SYSLOG_FACILITY_LPR}
                      sfNNTP, { ID_SYSLOG_FACILITY_NNTP }
                      sfUUCP, { ID_SYSLOG_FACILITY_UUCP }
                      sfClockDaemonOne, { CILITY_CLOCK1 }
                      sfSecurityTwo, { ID_SYSLOG_FACILITY_SECURITY2 }
                      sfFTPDaemon, { ID_SYSLOG_FACILITY_FTP }
                      sfNTP, { ID_SYSLOG_FACILITY_NTP }
                      sfLogAudit, { ID_SYSLOG_FACILITY_AUDIT  }
                      sfLogAlert, { ID_SYSLOG_FACILITY_ALERT }
                      sfClockDaemonTwo, { ID_SYSLOG_FACILITY_CLOCK2 }
                      sfLocalUseZero, { ID_SYSLOG_FACILITY_LOCAL0 }
                      sfLocalUseOne, { ID_SYSLOG_FACILITY_LOCAL1 }
                      sfLocalUseTwo, { ID_SYSLOG_FACILITY_LOCAL2 }
                      sfLocalUseThree, { ID_SYSLOG_FACILITY_LOCAL3 }
                      sfLocalUseFour, { ID_SYSLOG_FACILITY_LOCAL4 }
                      sfLocalUseFive, { ID_SYSLOG_FACILITY_LOCAL5 }
                      sfLocalUseSix, { ID_SYSLOG_FACILITY_LOCAL6 }
                      sfLocalUseSeven); { ID_SYSLOG_FACILITY_LOCAL7  }

  TIdSyslogSeverity = (slEmergency, {0 - emergency - system unusable}
              slAlert, {1 - action must be taken immediately }
              slCritical, { 2 - critical conditions }
              slError, {3 - error conditions }
              slWarning, {4 - warning conditions }
              slNotice, {5 - normal but signification condition }
              slInformational, {6 - informational }
              slDebug); {7 - debug-level messages }

  TIdSysLogMsgPart = class(TPersistent)
  protected
    FPIDAvailable: Boolean;
    FProcess: String;
    FPID: Integer;
    FContent: String;
    procedure SetPID(AValue: Integer);
    procedure SetProcess(const AValue: String);
    function GetText: String;
    procedure SetText(const AValue: String);
  public
    procedure Assign(Source: TPersistent); override;
  published
    property Text: String read GetText write SetText;
    property PIDAvailable : Boolean read FPIDAvailable write FPIDAvailable stored false;
    property Process : String read FProcess write SetProcess stored false;
    property PID : Integer read FPID write SetPID stored false;
    property Content : String read FContent write FContent stored false;
  end;

  TIdSysLogMessage = class(TIdBaseComponent)
  protected
    FMsg : TIdSysLogMsgPart;
    FFacility: TidSyslogFacility;
    FSeverity: TIdSyslogSeverity;
    FHostname: string;
    FMessage: String;
    FTimeStamp: TDateTime;
    FRawMessage: String;
    FPeer: String;
    FPri: TIdSyslogPRI;
    FUDPCliComp: TIdBaseComponent;
    procedure SetFacility(const AValue: TidSyslogFacility);
    procedure SetSeverity(const AValue: TIdSyslogSeverity);
    procedure SetHostname(const AValue: string);
    procedure SetRawMessage(const Value: string);
    procedure SetTimeStamp(const AValue: TDateTime);
    procedure SetMsg(const AValue : TIdSysLogMsgPart);
    procedure SetPri(const Value: TIdSyslogPRI);
    function GetHeader: String;
    procedure CheckASCIIRange(var Data: String); virtual;
    procedure ReadPRI(var StartPos: Integer); virtual;
    procedure ReadHeader(var StartPos: Integer); virtual;
    procedure ReadMSG(var StartPos: Integer); virtual;
    procedure Parse; virtual;
    procedure UpdatePRI; virtual;
    function DecodeTimeStamp(TimeStampString: String): TDateTime; virtual;
    procedure InitComponent; override;
  public
    procedure Assign(Source: TPersistent); override;
    destructor Destroy; override;
    function EncodeMessage: String; virtual;
    procedure ReadFromBytes(const ASrc: TIdBytes; const APeer : String); virtual;
    //
    property RawMessage: string read FRawMessage write SetRawMessage;
    procedure SendToHost(const Dest: String);
    property Peer: string read FPeer write FPeer;
    property TimeStamp: TDateTime read FTimeStamp write SetTimeStamp;
  published
    property Pri: TIdSyslogPRI read FPri write SetPri default 13;
    property Facility: TidSyslogFacility read FFacility write SetFacility stored false;
    property Severity: TIdSyslogSeverity read FSeverity write SetSeverity stored false;
    property Hostname: string read FHostname write SetHostname stored false;
    property Msg : TIdSysLogMsgPart read FMsg write SetMsg;
  end; // class

function FacilityToString(AFac: TIdSyslogFacility): string;
function SeverityToString(ASec: TIdsyslogSeverity): string;
function NoToSeverity(ASev :  Word) : TIdSyslogSeverity;
function logSeverityToNo(ASev :  TIdSyslogSeverity) : Word;
function NoToFacility(AFac : Word) : TIdSyslogFacility;
function logFacilityToNo(AFac : TIdSyslogFacility) : Word;

implementation

uses
  IdAssignedNumbers, IdException, IdExceptionCore, IdResourceStringsProtocols, IdStack, IdStackConsts, IdUDPClient, SysUtils;

const
  // facility
  ID_SYSLOG_FACILITY_KERNEL     = 0;  // kernel messages
  ID_SYSLOG_FACILITY_USER       = 1;  // user-level messages
  ID_SYSLOG_FACILITY_MAIL       = 2;  // mail system
  ID_SYSLOG_FACILITY_SYS_DAEMON = 3;  // system daemons
  ID_SYSLOG_FACILITY_SECURITY1  = 4;  // security/authorization messages (1)
  ID_SYSLOG_FACILITY_INTERNAL   = 5;  // messages generated internally by syslogd
  ID_SYSLOG_FACILITY_LPR        = 6;  // line printer subsystem
  ID_SYSLOG_FACILITY_NNTP       = 7;  // network news subsystem
  ID_SYSLOG_FACILITY_UUCP       = 8;  // UUCP subsystem
  ID_SYSLOG_FACILITY_CLOCK1     = 9;  // clock daemon (1)
  ID_SYSLOG_FACILITY_SECURITY2  = 10; // security/authorization messages (2)
  ID_SYSLOG_FACILITY_FTP        = 11; // FTP daemon
  ID_SYSLOG_FACILITY_NTP        = 12; // NTP subsystem
  ID_SYSLOG_FACILITY_AUDIT      = 13; // log audit
  ID_SYSLOG_FACILITY_ALERT      = 14; // log alert
  ID_SYSLOG_FACILITY_CLOCK2     = 15; // clock daemon (2)
  ID_SYSLOG_FACILITY_LOCAL0     = 16; // local use 0  (local0)
  ID_SYSLOG_FACILITY_LOCAL1     = 17; // local use 1  (local1)
  ID_SYSLOG_FACILITY_LOCAL2     = 18; // local use 2  (local2)
  ID_SYSLOG_FACILITY_LOCAL3     = 19; // local use 3  (local3)
  ID_SYSLOG_FACILITY_LOCAL4     = 20; // local use 4  (local4)
  ID_SYSLOG_FACILITY_LOCAL5     = 21; // local use 5  (local5)
  ID_SYSLOG_FACILITY_LOCAL6     = 22; // local use 6  (local6)
  ID_SYSLOG_FACILITY_LOCAL7     = 23; // local use 7  (local7)

  // Severity
  ID_SYSLOG_SEVERITY_EMERGENCY     = 0; // Emergency: system is unusable
  ID_SYSLOG_SEVERITY_ALERT         = 1; // Alert: action must be taken immediately
  ID_SYSLOG_SEVERITY_CRITICAL      = 2; // Critical: critical conditions
  ID_SYSLOG_SEVERITY_ERROR         = 3; // Error: error conditions
  ID_SYSLOG_SEVERITY_WARNING       = 4; // Warning: warning conditions
  ID_SYSLOG_SEVERITY_NOTICE        = 5; // Notice: normal but significant condition
  ID_SYSLOG_SEVERITY_INFORMATIONAL = 6; // Informational: informational messages
  ID_SYSLOG_SEVERITY_DEBUG         = 7; // Debug: debug-level messages

function logFacilityToNo(AFac : TIdSyslogFacility) : Word;
begin
  case AFac of
   sfKernel : Result := ID_SYSLOG_FACILITY_KERNEL;
   sfUserLevel : Result := ID_SYSLOG_FACILITY_USER;
   sfMailSystem : Result := ID_SYSLOG_FACILITY_MAIL;
   sfSystemDaemon : Result := ID_SYSLOG_FACILITY_SYS_DAEMON;
   sfSecurityOne : Result := ID_SYSLOG_FACILITY_SECURITY1;
   sfSysLogInternal : Result := ID_SYSLOG_FACILITY_INTERNAL;
   sfLPR : Result := ID_SYSLOG_FACILITY_LPR;
   sfNNTP : Result := ID_SYSLOG_FACILITY_NNTP;
   sfClockDaemonOne : Result := ID_SYSLOG_FACILITY_CLOCK1;
   sfUUCP : Result := ID_SYSLOG_FACILITY_UUCP;
   sfSecurityTwo : Result := ID_SYSLOG_FACILITY_SECURITY2;
   sfFTPDaemon : Result := ID_SYSLOG_FACILITY_FTP;
   sfNTP : Result := ID_SYSLOG_FACILITY_NTP;
   sfLogAudit : Result := ID_SYSLOG_FACILITY_AUDIT;
   sfLogAlert : Result := ID_SYSLOG_FACILITY_ALERT;
   sfClockDaemonTwo : Result := ID_SYSLOG_FACILITY_CLOCK2;
   sfLocalUseZero : Result := ID_SYSLOG_FACILITY_LOCAL0;
   sfLocalUseOne : Result := ID_SYSLOG_FACILITY_LOCAL1;
   sfLocalUseTwo : Result := ID_SYSLOG_FACILITY_LOCAL2;
   sfLocalUseThree : Result := ID_SYSLOG_FACILITY_LOCAL3;
   sfLocalUseFour : Result := ID_SYSLOG_FACILITY_LOCAL4;
   sfLocalUseFive : Result := ID_SYSLOG_FACILITY_LOCAL5;
   sfLocalUseSix : Result := ID_SYSLOG_FACILITY_LOCAL6;
   sfLocalUseSeven : Result := ID_SYSLOG_FACILITY_LOCAL7;
  else
    Result := ID_SYSLOG_FACILITY_LOCAL7;
  end;
end;

function NoToFacility(AFac : Word) : TIdSyslogFacility;
begin
  case AFac of
    ID_SYSLOG_FACILITY_KERNEL : Result := sfKernel;
    ID_SYSLOG_FACILITY_USER : Result := sfUserLevel;
    ID_SYSLOG_FACILITY_MAIL : Result := sfMailSystem;
    ID_SYSLOG_FACILITY_SYS_DAEMON : Result := sfSystemDaemon;
    ID_SYSLOG_FACILITY_SECURITY1 : Result := sfSecurityOne;
    ID_SYSLOG_FACILITY_INTERNAL : Result := sfSysLogInternal;
    ID_SYSLOG_FACILITY_LPR : Result := sfLPR;
    ID_SYSLOG_FACILITY_NNTP : Result := sfNNTP;
    ID_SYSLOG_FACILITY_CLOCK1 : Result := sfClockDaemonOne;
    ID_SYSLOG_FACILITY_UUCP : Result := sfUUCP;
    ID_SYSLOG_FACILITY_SECURITY2 : Result := sfSecurityTwo;
    ID_SYSLOG_FACILITY_FTP : Result := sfFTPDaemon;
    ID_SYSLOG_FACILITY_NTP : Result := sfNTP;
    ID_SYSLOG_FACILITY_AUDIT : Result := sfLogAudit;
    ID_SYSLOG_FACILITY_ALERT : Result := sfLogAlert;
    ID_SYSLOG_FACILITY_CLOCK2 : Result := sfClockDaemonTwo;
    ID_SYSLOG_FACILITY_LOCAL0 : Result := sfLocalUseZero;
    ID_SYSLOG_FACILITY_LOCAL1 : Result := sfLocalUseOne;
    ID_SYSLOG_FACILITY_LOCAL2 : Result := sfLocalUseTwo;
    ID_SYSLOG_FACILITY_LOCAL3 : Result := sfLocalUseThree;
    ID_SYSLOG_FACILITY_LOCAL4 : Result := sfLocalUseFour;
    ID_SYSLOG_FACILITY_LOCAL5 : Result := sfLocalUseFive;
    ID_SYSLOG_FACILITY_LOCAL6 : Result := sfLocalUseSix;
    ID_SYSLOG_FACILITY_LOCAL7 : Result := sfLocalUseSeven;
    else
      Result := sfLocalUseSeven;
  end;
end;

function logSeverityToNo(ASev :  TIdSyslogSeverity) : Word;
begin
  case ASev of
    slEmergency : Result := ID_SYSLOG_SEVERITY_EMERGENCY;
    slAlert :  Result := ID_SYSLOG_SEVERITY_ALERT;
    slCritical : Result := ID_SYSLOG_SEVERITY_CRITICAL;
    slError : Result := ID_SYSLOG_SEVERITY_ERROR;
    slWarning : Result := ID_SYSLOG_SEVERITY_WARNING;
    slNotice : Result := ID_SYSLOG_SEVERITY_NOTICE;
    slInformational : Result := ID_SYSLOG_SEVERITY_INFORMATIONAL;
    slDebug : Result := ID_SYSLOG_SEVERITY_DEBUG;
  else
    Result := ID_SYSLOG_SEVERITY_DEBUG;
  end;
end;

function NoToSeverity(ASev :  Word) : TIdSyslogSeverity;
begin
  case ASev of
    ID_SYSLOG_SEVERITY_EMERGENCY : Result := slEmergency;
    ID_SYSLOG_SEVERITY_ALERT : Result := slAlert;
    ID_SYSLOG_SEVERITY_CRITICAL : Result := slCritical;
    ID_SYSLOG_SEVERITY_ERROR : Result := slError;
    ID_SYSLOG_SEVERITY_WARNING : Result := slWarning;
    ID_SYSLOG_SEVERITY_NOTICE : Result := slNotice;
    ID_SYSLOG_SEVERITY_INFORMATIONAL : Result := slInformational;
    ID_SYSLOG_SEVERITY_DEBUG : Result := slDebug;
  else
    Result := slDebug;
  end;
end;

function SeverityToString(ASec: TIdsyslogSeverity): string;
begin
  case ASec of
    slEmergency: Result := STR_SYSLOG_SEVERITY_EMERGENCY;
    slAlert: Result := STR_SYSLOG_SEVERITY_ALERT;
    slCritical: Result := STR_SYSLOG_SEVERITY_CRITICAL;
    slError: Result := STR_SYSLOG_SEVERITY_ERROR;
    slWarning: Result := STR_SYSLOG_SEVERITY_WARNING;
    slNotice: Result := STR_SYSLOG_SEVERITY_NOTICE;
    slInformational: Result := STR_SYSLOG_SEVERITY_INFORMATIONAL;
    slDebug: Result := STR_SYSLOG_SEVERITY_DEBUG;
  else
    Result := STR_SYSLOG_SEVERITY_UNKNOWN;
  end;
end;

function FacilityToString(AFac: TIdSyslogFacility): string;
begin
  case AFac of
    sfKernel: Result := STR_SYSLOG_FACILITY_KERNEL;
    sfUserLevel: Result := STR_SYSLOG_FACILITY_USER;
    sfMailSystem: Result := STR_SYSLOG_FACILITY_MAIL;
    sfSystemDaemon: Result := STR_SYSLOG_FACILITY_SYS_DAEMON;
    sfSecurityOne: Result := STR_SYSLOG_FACILITY_SECURITY1;
    sfSysLogInternal: Result := STR_SYSLOG_FACILITY_INTERNAL;
    sfLPR: Result := STR_SYSLOG_FACILITY_LPR;
    sfNNTP: Result := STR_SYSLOG_FACILITY_NNTP;
    sfClockDaemonOne: Result := STR_SYSLOG_FACILITY_CLOCK1;
    sfUUCP: Result := STR_SYSLOG_FACILITY_UUCP;
    sfSecurityTwo: Result := STR_SYSLOG_FACILITY_SECURITY2;
    sfFTPDaemon: Result := STR_SYSLOG_FACILITY_FTP;
    sfNTP: Result := STR_SYSLOG_FACILITY_NTP;
    sfLogAudit: Result := STR_SYSLOG_FACILITY_AUDIT;
    sfLogAlert: Result := STR_SYSLOG_FACILITY_ALERT;
    sfClockDaemonTwo: Result := STR_SYSLOG_FACILITY_CLOCK2;
    sfLocalUseZero: Result := STR_SYSLOG_FACILITY_LOCAL0;
    sfLocalUseOne: Result := STR_SYSLOG_FACILITY_LOCAL1;
    sfLocalUseTwo: Result := STR_SYSLOG_FACILITY_LOCAL2;
    sfLocalUseThree: Result := STR_SYSLOG_FACILITY_LOCAL3;
    sfLocalUseFour: Result := STR_SYSLOG_FACILITY_LOCAL4;
    sfLocalUseFive: Result := STR_SYSLOG_FACILITY_LOCAL5;
    sfLocalUseSix: Result := STR_SYSLOG_FACILITY_LOCAL6;
    sfLocalUseSeven: Result := STR_SYSLOG_FACILITY_LOCAL7;
  else
    Result := STR_SYSLOG_FACILITY_UNKNOWN;
  end;
end;

function ExtractAlphaNumericStr(var VString : String) : String;
var
  i, len : Integer;
begin
  len := 0;  
  for i := 1 to IndyMin(Length(VString), 32) do begin
    //numbers or alphabet only
    if IsAlphaNumeric(VString[i]) then begin
      Inc(len);
    end else begin
      Break;
    end;
  end;
  Result := Copy(VString, 1, len);
  VString := Copy(VString, len+1, MaxInt);
end;

{ TIdSysLogMessage }

procedure TIdSysLogMessage.Assign(Source: TPersistent);
var
  ms : TIdSysLogMessage;
begin
  if Source is TIdSysLogMessage then begin
    ms := Source as TIdSysLogMessage;
    {Priority and facility properties are set with this so those assignments
    are not needed}
    Pri := Ms.Pri;
    HostName := ms.Hostname;
    FMsg.Assign(ms.Msg);
    TimeStamp := ms.TimeStamp;
  end else begin
    inherited Assign(Source);
  end;
end;

function TIdSysLogMessage.DecodeTimeStamp(TimeStampString: String): TDateTime;
var
  AYear, AMonth, ADay, AHour, AMin, ASec: Word;
  LDate : TDateTime;
begin
  // SG 25/2/02: Check the ASCII range
  CheckASCIIRange(TimeStampString);
  // Get the current date to get the current year
  LDate := Now;
  DecodeDate(LDate, AYear, AMonth, ADay);
  if Length(TimeStampString) <> 16 then begin
    raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  // Month
  AMonth := StrToMonth(Copy(TimeStampString, 1, 3));
  if not AMonth in [1..12] then begin
    raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  // day
  ADay := IndyStrToInt(Copy(TimeStampString, 5, 2), 0);
  if not (ADay in [1..31]) then begin
    raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  // Time
  AHour := IndyStrToInt(Copy(TimeStampString, 8, 2), 0);
  if not AHour in [0..23] then begin
    raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  AMin := IndyStrToInt(Copy(TimeStampString, 11, 2), 0);
  if not AMin in [0..59] then begin
    raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  ASec := IndyStrToInt(Copy(TimeStampString, 14, 2), 0);
  if not ASec in [0..59] then begin
    raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  if TimeStampString[16] <> ' ' then begin   {Do not Localize}
    Raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogTimeStamp, [TimeStampString]);
  end;
  Result := EncodeDate(AYear, AMonth, ADay) + EncodeTime(AHour, AMin, ASec, 0);
end;

procedure TIdSysLogMessage.ReadFromBytes(const ASrc: TIdBytes; const APeer : String);
const
  MSGLEN = 1024;
begin
  FPeer := APeer;
  RawMessage := BytesToString(ASrc, 0, MSGLEN);
end;

procedure TIdSysLogMessage.Parse;
var
  APos: Integer;
begin
  APos := 1;
  ReadPRI(APos);
  ReadHeader(APos);
  ReadMSG(APos);
end;

procedure TIdSysLogMessage.ReadHeader(var StartPos: Integer);
var
  AHostNameEnd: Integer;
begin
  // DateTimeToInternetStr and StrInternetToDateTime
  // Time stamp string is 15 char long
  try
    FTimeStamp := DecodeTimeStamp(Copy(FRawMessage, StartPos, 16));
    Inc(StartPos, 16);
    // HostName
    AHostNameEnd := StartPos;
    while (AHostNameEnd < Length(FRawMessage)) and (FRawMessage[AHostNameEnd] <> ' ') do begin    {Do not Localize}
      Inc(AHostNameEnd);
    end;    // while

    FHostname := Copy(FRawMessage, StartPos, AHostNameEnd - StartPos);

    if Pos(':', FHostname) <> 0 then begin // check if the hostname doesn't contain a semicolon (so it's not a process)
      FHostname := Peer;
    end else begin
      StartPos := AHostNameEnd + 1;
    end;

    // SG 25/2/02: Check the ASCII range of host name
    CheckASCIIRange(FHostname);
  except
    on e: Exception do
    begin
      FTimeStamp := Now;
      FHostname := FPeer;
    end;
  end;
end;

procedure TIdSysLogMessage.ReadMSG(var StartPos: Integer);
begin
  FMessage := Copy(FRawMessage, StartPos, Length(FRawMessage));
  Msg.Text := FMessage;
end;

procedure TIdSysLogMessage.ReadPRI(var StartPos: Integer);
var
  StartPosSave: Integer;
  Buffer: string;
begin
  StartPosSave := StartPos;
  try
    // Read the PRI string
    // PRI must start with "less than" sign
    Buffer := '';    {Do not Localize}
    if not CharEquals(FRawMessage, StartPos, '<') then begin   {Do not Localize}
      raise EInvalidSyslogMessage.Create(RSInvalidSyslogPRI);
    end;
    repeat
      Inc(StartPos);
      if CharEquals(FRawMessage, StartPos, '>') then begin   {Do not Localize}
        Break;
      end;
      if not IsNumeric(FRawMessage, 1, StartPos) then begin   {Do not Localize}
        raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogPRINumber, [Buffer]);
      end;
      Buffer := Buffer + FRawMessage[StartPos];
    until StartPos = StartPosSave + 5;

    // PRI must end with "greater than" sign
    if not CharEquals(FRawMessage, StartPos, '>') then begin   {Do not Localize}
      raise EInvalidSyslogMessage.Create(RSInvalidSyslogPRI);
    end;
    // Convert PRI to numerical value
    Inc(StartPos);
    CheckASCIIRange(Buffer);
    PRI := IndyStrToInt(Buffer, -1);
  except
    // as per RFC, on invalid/missing PRI, use value 13
    on e: Exception do
    begin
      Pri := 13;
      // Reset the position to saved value
      StartPos := StartPosSave;
    end;
  end;
end;

procedure TIdSysLogMessage.UpdatePRI;
begin
  PRI := logFacilityToNo(Facility) * 8 + logSeverityToNo(Severity);
end;

procedure TIdSysLogMessage.SetFacility(const AValue: TidSyslogFacility);
begin
  if FFacility <> AValue then begin
    FFacility := AValue;
    UpdatePRI;
  end;
end;

procedure TIdSysLogMessage.SetHostname(const AValue: string);
begin
  if FHostname <> AValue then begin
    if Pos(' ', AValue) <> 0 then begin   {Do not Localize}
      raise EInvalidSyslogMessage.CreateFmt(RSInvalidHostName, [AValue]);
    end;
    FHostname := AValue;
  end;
end;

procedure TIdSysLogMessage.SetSeverity(const AValue: TIdSyslogSeverity);
begin
  if FSeverity <> AValue then begin
    FSeverity := AValue;
    UpdatePRI;
  end;
end;

procedure TIdSysLogMessage.SetTimeStamp(const AValue: TDateTime);
begin
  FTimeStamp := AValue;
end;

function TIdSysLogMessage.GetHeader: String;
var
  AYear, AMonth, ADay, AHour, AMin, ASec, AMSec: Word;

  function YearOf(ADate : TDateTime) : Word;
  var
    mm, dd : Word;
  begin
    DecodeDate(ADate, Result, mm, dd);
  end;

  Function DayToStr(day: Word): String;
  begin
    if Day < 10 then begin
       Result :=  ' ' + IntToStr(day);    {Do not Localize}
    end else begin
      Result := IntToStr(day);
    end;
  end;
begin
  // if the year of the message is not the current year, the timestamp is
  // invalid -> Create a new timestamp with the current date/time
  if YearOf(Now) <> YearOf(TimeStamp) then
  begin
    TimeStamp := Now;
  end;
  DecodeDate(TimeStamp, AYear, AMonth, ADay);
  DecodeTime(TimeStamp, AHour, AMin, ASec, AMSec);

  Result := IndyFormat('%s %s %.2d:%.2d:%.2d %s', [monthnames[AMonth], DayToStr(ADay), AHour, AMin, ASec, Hostname]);    {Do not Localize}

end;

function TIdSysLogMessage.EncodeMessage: String;
begin
  // Create a syslog message string
  // PRI
  Result := IndyFormat('<%d>%s %s', [PRI, GetHeader, FMsg.Text]);    {Do not Localize}
  // If the message is too long, tuncate it
  if Length(result) > 1024  then
  begin
    result := Copy(result, 1, 1024);
  end;
end;

procedure TIdSysLogMessage.SetPri(const Value: TIdSyslogPRI);
begin
  if FPri <> Value then begin
    if not (Value in [0..191]) then begin
      raise EInvalidSyslogMessage.CreateFmt(RSInvalidSyslogPRINumber, [IntToStr(value)]);
    end;
    FPri := Value;
    FFacility := NoToFacility(Value div 8);
    FSeverity := NoToSeverity(Value mod 8);
  end;
end;

procedure TIdSysLogMessage.InitComponent;
begin
  inherited;
  PRI := 13; //default
  {This stuff is necessary to prevent an AV in the IDE if GStack does not exist}
  TIdStack.IncUsage;
  try
    Hostname := GStack.HostName;
  finally
    TIdStack.DecUsage;
  end;
  FMsg := TIdSysLogMsgPart.Create;
end;

procedure TIdSysLogMessage.CheckASCIIRange(var Data: String);
var
  i: Integer;
  ValidChars : String;
  {$IFDEF STRING_IS_IMMUTABLE}
  LSB: TIdStringBuilder;
  {$ENDIF}
begin
  ValidChars := CharRange(#0, #127);
  {$IFDEF STRING_IS_IMMUTABLE}
  LSB := TIdStringBuilder.Create(Data);
  for i := 0 to LSB.Length-1 do    // Iterate
  begin
    if not CharIsInSet(LSB, i, ValidChars) then begin
      LSB[i] := '?';    {Do not Localize}
    end;
  end;    // for
  Data := LSB.ToString;
  {$ELSE}
  for i := 1 to Length(Data) do    // Iterate
  begin
    if not CharIsInSet(Data, i, ValidChars) then begin
      Data[i] := '?';    {Do not Localize}
    end;
  end;    // for
  {$ENDIF}
end;

destructor TIdSysLogMessage.Destroy;
begin
  FreeAndNil(FMsg);
  inherited Destroy;
end;

procedure TIdSysLogMessage.SetMsg(const AValue: TIdSysLogMsgPart);
begin
  FMsg.Assign(AValue);
end;

procedure TIdSysLogMessage.SetRawMessage(const Value: string);
begin
  FRawMessage := Value;
  // check that message contains only valid ASCII chars.
  // Replace Invalid entries by "?"
  // SG 25/2/02: Moved to header decoding
  Parse;
end;

procedure TIdSysLogMessage.SendToHost(const Dest: String);
var
  LEncoding: IIdTextEncoding;
begin
  if not Assigned(FUDPCliComp) then begin
    FUDPCliComp := TIdUDPClient.Create(Self);
  end;
  LEncoding := IndyTextEncoding_8Bit;
  (FUDPCliComp as TIdUDPClient).Send(Dest, IdPORT_syslog, EncodeMessage, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

{ TIdSysLogMsgPart }

procedure TIdSysLogMsgPart.Assign(Source: TPersistent);
begin
  if Source is TIdSysLogMsgPart then begin
    {This sets about everything here}
    Text := (Source as TIdSysLogMsgPart).Text;
  end else begin
    inherited Assign(Source);
  end;
end;

procedure TIdSysLogMsgPart.SetPID(AValue: Integer);
begin
  FPID := AValue;
  FPIDAvailable := FPID <> -1;
end;

procedure TIdSysLogMsgPart.SetProcess(const AValue: String);
var
  LTmp: String;
begin
  //we have to ensure that the TAG field will never be greater than 32 characters
  //and the program name must contain alphanumeric characters
  LTmp := AValue;
  FProcess := ExtractAlphaNumericStr(LTmp);
end;

function TIdSysLogMsgPart.GetText: String;
begin
  Result := Process;
  if FPIDAvailable then begin
    Result := Result + IndyFormat('[%d]', [FPID]); {Do not Localize}
  end;
  Result := Result + ': ' + Content; {Do not Localize}
  if Result = ': ' then begin {Do not Localize}
    Result := '';
  end;
end;

procedure TIdSysLogMsgPart.SetText(const AValue: String);
var
  SBuf: String;
begin
  FProcess := '';  {Do not Localize}
  FPID := -1;
  FPIDAvailable := False;
  FContent := '';  {Do not Localize}

  SBuf := AValue;
  FProcess := ExtractAlphaNumericStr(SBuf);

  if TextStartsWith(SBuf, '[') then begin  {Do not Localize}
    SBuf := Copy(SBuf, 2, MaxInt);
    FPID := IndyStrToInt(Fetch(SBuf, ']'), -1);  {Do not Localize}
    FPIDAvailable := FPID <> -1;
  end;
  if TextStartsWith(SBuf, ': ') then begin  {Do not Localize}
    SBuf := Copy(SBuf, 3, MaxInt);
  end
  else if TextStartsWith(SBuf, ':') or TextStartsWith(SBuf, ' ') then begin  {Do not Localize}
    SBuf := Copy(SBuf, 2, MaxInt);
  end;

  FContent := SBuf;
end;

end.
