www.pudn.com > EmailServer.zip > SmtpServer.pas
unit SmtpServer;
(******************************************************************************)
(* *)
(* SMTP Server Objects *)
(* Part of Hermes SMTP/POP3 Server. *)
(* Copyright(C) 2000 by Alexander J. Fanti, All Rights Reserver Worldwide. *)
(* *)
(* Contains: TSmtpServer, TSmtpConnection *)
(* *)
(* Created January 12, 2000 by Alexander J. Fanti. See License.txt *)
(* *)
(* Depends on: MailRouting (ListServer, MailDeliver) *)
(* DataU1 *)
(* UtilU1 *)
(* *)
(* Also Uses: WSocket (Francois Piette Internet Component Suite) *)
(* *)
(* Used by: Main *)
(* *)
(* Description: *)
(* TSmtpServer - This server object manages the Smtp connections, controls *)
(* listening for connections, and accepts them. *)
(* TSmtpConnection - The Connection object is the real server. It handles *)
(* the individual Smtp connection, and any requests by the *)
(* connected user. *)
(* *)
(* Revisions: 1/14/2000 AJF Commented *)
(* 1/21/2000 AJF Commented *)
(* 2/12/2000 AJF Added Access control to Smtp Server to deter *)
(* spam *)
(* 2/13/2000 AJF Re-worked anti-spam to be more user friendly *)
(* *)
(******************************************************************************)
interface
uses Classes, SysUtils, FileCtrl, ExtCtrls, INIFiles, Messages, Windows,
WSocket,
MailRouting, {TMessageInformation, TListServer, TDeliverMail}
DataU1;
const
CRLF = #13 + #10;
WM_FREECONNECTION = WM_USER + 100;
type
TSmtpConnection = class; // Forward declaration of later object
TSmtpServer_ErrorCode = (sec_CantListen, sec_Bad_Mailbox_Path,
sec_SocketError);
TSmtpServer_StateChange = procedure(Sender : TObject; Active : Boolean;
OpenConnections : Longint) of Object;
TSmtpServer_StatusUpdate = procedure(Sender : TObject; Status : String;
Level : Integer) of Object;
TSmtpServer_Error = procedure(Sender : TObject;
ErrorCode : TSmtpServer_ErrorCode) of Object;
TSmtpServer = class(TWHComponent)
private
FSocket : TWSocket; // Socket for Listening for Smtp requests
ConnectionList : TList; // List of TSmtpConnection objects currently open
// Bind address, Port, server name, mailboxpath, queuepath and other
// Smtp Server settings come straight from global INI object (DataU1)
FActive : Boolean; // True when server is listening. This flag is used
// because there are more socket states than listen
// and not. we want to reduce them to 2, a binary.
FOnStateChange : TSmtpServer_StateChange; // Event Ptr for OnChangeState
FOnStatusUpdate : TSmtpServer_StatusUpdate; // Event Ptr for StatusUpdate
FOnError : TSmtpServer_Error; // Event Ptr for Server Error
// We use this to let the connection object tell us when it's finished
// so we can free it!
procedure WindowsMessage(Sender : TObject; Msg: TMessage);
function GetActive : Boolean; // Read if server's active
function GetConnectionCount : Longint; // Read SmtpConnection Count
// Socket procedures for Listening socket
procedure SocketSessionAvailable(Sender: TObject; Error: Word);
procedure SocketChangeState(Sender: TObject;
OldState, NewState: TSocketState);
procedure SocketSessionClosed(Sender: TObject; Error: Word);
procedure SocketError(Sender: TObject);
procedure SocketBgException(Sender: TObject; E: Exception;
var CanClose: Boolean);
// Triggered when TSmtpConnection fires a StatusUpdate event
procedure SmtpConnectionStatusUpdate(Sender : TObject; Status : String;
Level : Integer);
// Triggered when TSmtpConnection fires a ConnectionClosed event
procedure SmtpConnectionClosed(Sender : TObject);
procedure CloseConnection(AConnection : TSmtpConnection);
procedure CloseAllConnections;
public
constructor Create(AOwner : TComponent); Override;
destructor Destroy; Override;
// Methods
procedure Listen; // Listen for Smtp Connections
procedure Stop; // Stop listening for Smtp Connections
procedure Shutdown; // Stop listening and close all open Smtp Connections
procedure ConnectionInactivityTimeout(Minutes : Integer);
// Properties
property Active : Boolean read GetActive;
function PublicServerName : String;
property Count : Longint read GetConnectionCount;
// Events
property OnStateChange : TSmtpServer_StateChange // Fired on Server State
read FOnStateChange write FOnStateChange; // Change (Active)
property OnStatusUpdate : TSmtpServer_StatusUpdate // Fired on Status
read FOnStatusUpdate write FOnStatusUpdate;// Update
property OnError : TSmtpServer_Error // Fired on Server Error
read FOnError write FOnError;
end;
TSmtpConnection_State = (scs_IDENTIFICATION, scs_WAITCOMMAND,
scs_RECEIVINGMAIL, scs_RECEIVINGMAILTO,
scs_RECEIVINGMAILDATA);
TSmtpConnection_StatusUpdate = procedure(Sender : TObject; Status : String;
Level : Integer) of Object;
TSmtpConnection = class(TComponent)
private
FServer : TSmtpServer;
FSocket : TWSocket; // Socket for Talking to Smtp User
FBufferStr : String; // To buffer commands from Socket
FState : TSmtpConnection_State; // State of Smtp Connection
FLastActivity : TDateTime; // Time of last activity (for timeout)
AccCtrl_FromAcceptedAddress : Boolean; // MAIL FROM is from an Accepted
// IP Address
AccCtrl_FromBannedAddress : Boolean; // MAIL FROM or connection is from
// a banned IP Address
AccCtrl_FromIPAddress : String; // IP Address of connected sender
FUserID : String; // The Machine Address given when they connect
FMessageInfo : TSmtpMessageInformation; // Smtp Message information user
// sends us through the connection
FOnStatusUpdate : TSmtpConnection_StatusUpdate; // Event Ptr for StatusUpd.
procedure StatusUpdate(Status : String; Level : Integer);
// Used internally to Trigger Status Update
procedure ProcessRequest(UserRequest : String); // Process Smtp Request
// Socket Procedures for Connected Socket
procedure SocketDataAvailable(Sender: TObject; Error: Word);
procedure SocketDataSent(Sender: TObject; Error: Word);
procedure SocketSessionClosed(Sender: TObject; Error: Word);
procedure SocketError(Sender: TObject);
procedure SocketBgException(Sender: TObject; E: Exception;
var CanClose: Boolean);
procedure Close; // Close connection and terminate
public
constructor Create(AOwner : TSmtpServer);
destructor Destroy; Override;
procedure Accept(SocketHandle : Integer); // Smtp Connection
property LastActivity : TDateTime read FLastActivity;
property OnStatusUpdate : TSmtpConnection_StatusUpdate // Fired on Status
read FOnStatusUpdate write FOnStatusUpdate; // Update
end;
implementation
uses UtilU1; {For Domain Name formatting}
(******************************************************************************)
(* *)
(* START SMTP Server Object *)
(* *)
(* This Object listens for connections, accepts them and tracks them. It *)
(* also reports on them, and can drop them. *)
(* *)
(******************************************************************************)
constructor TSmtpServer.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
OnWindowsMessage := WindowsMessage;
// Initialize variables
ConnectionList := TList.Create;
FActive := False;
// Listening Socket Create and Setup
FSocket := TWSocket.Create(Self);
FSocket.OnSessionAvailable := SocketSessionAvailable;
FSocket.OnChangeState := SocketChangeState;
FSocket.OnSessionClosed := SocketSessionClosed;
FSocket.OnError := SocketError;
FSocket.OnBgException := SocketBgException;
end;
procedure TSmtpServer.WindowsMessage(Sender : TObject; Msg: TMessage);
begin
if Msg.Msg = WM_FREECONNECTION then
CloseConnection(TSmtpConnection(Msg.WParam));
end;
destructor TSmtpServer.Destroy;
begin
if Assigned(FSocket) then begin
FSocket.Destroy;
FSocket := nil;
end;
CloseAllConnections;
ConnectionList.Free;
inherited Destroy;
end;
procedure TSmtpServer.CloseConnection(AConnection : TSmtpConnection);
var
x : Longint;
Connection : TSmtpConnection;
begin
for x := ConnectionList.Count -1 downto 0 do begin
Connection := TSmtpConnection(ConnectionList[x]);
if Connection = AConnection then begin
ConnectionList.Delete(x);
AConnection.Free;
if Assigned(FOnStateChange) then
OnStateChange(Self, FSocket.State = wsListening, ConnectionList.Count);
end;
end;
end;
procedure TSmtpServer.CloseAllConnections;
var
x : Longint;
Connection : TSmtpConnection;
begin
for x := ConnectionList.Count -1 downto 0 do begin
Connection := TSmtpConnection(ConnectionList[x]);
CloseConnection(Connection);
end;
end;
procedure TSmtpServer.ConnectionInactivityTimeout(Minutes : Integer);
const
HOUR = 0.04167;
MINUTE = 0.00069;
var
x : Longint;
Connection : TSmtpConnection;
DT : TDateTime;
begin
// See if there's a connection that hasn't been active for a while
if Minutes > 0 then begin
for x := ConnectionList.Count -1 downto 0 do begin
Connection := TSmtpConnection(ConnectionList[x]);
DT := Now - Connection.LastActivity;
if DT > (Minutes * MINUTE) then begin
if Assigned(FOnStatusUpdate) then
OnStatusUpdate(Self, ' ' +
'Closing due to inactivity (' + IntToStr(Minutes) + ' minutes).',
STAT_SERVERERROR);
CloseConnection(Connection);
end;
end;
end;
end;
function TSmtpServer.GetActive : Boolean;
begin
FActive := FSocket.State = wsListening;
Result := FActive;
end;
function TSmtpServer.PublicServerName : String;
begin
Result := FormatedDomain(INI.ServerName);
end;
function TSmtpServer.GetConnectionCount : Longint;
begin
Result := ConnectionList.Count;
end;
procedure TSmtpServer.Listen;
begin
FSocket.Close;
FSocket.Addr := INI.Smtp_BindAddress;
FSocket.Port := IntToStr(INI.Smtp_Port);
FSocket.Proto := 'TCP';
FSocket.Listen;
end;
procedure TSmtpServer.Stop;
begin
FSocket.Close;
end;
procedure TSmtpServer.Shutdown;
begin
Stop; // Stop listening for new connections
CloseAllConnections;
end;
procedure TSmtpServer.SocketSessionAvailable(Sender: TObject; Error: Word);
var
Connection : TSmtpConnection;
begin
// Smtp Session request
// Create a new Smtp Connection and accept the request to it...
Connection := TSmtpConnection.Create(Self);
Connection.OnStatusUpdate := SmtpConnectionStatusUpdate;
ConnectionList.Add(Connection); // Keep track of the connection (add to list)
if Assigned(FOnStateChange) then
OnStateChange(Self, FSocket.State = wsListening, ConnectionList.Count);
Connection.Accept(FSocket.Accept);
end;
procedure TSmtpServer.SocketChangeState(Sender: TObject;
OldState, NewState: TSocketState);
var
OldActive : Boolean;
begin
OldActive := FActive;
FActive := FSocket.State = wsListening;
if (OldActive <> FActive) and Assigned(FOnStateChange) then
OnStateChange(Self, FSocket.State = wsListening, ConnectionList.Count);
end;
procedure TSmtpServer.SocketSessionClosed(Sender: TObject; Error: Word);
begin
if Assigned(FOnStateChange) then
OnStateChange(Self, FSocket.State = wsListening, ConnectionList.Count);
end;
procedure TSmtpServer.SocketError(Sender: TObject);
var
Error : Integer;
begin
// Socket error. This could be because the listening socket can't bind to
// the port (10048) or something else. In fact, bind failure is so common,
// I'll treat it seperately.
Error := FSocket.LastError;
if Error = 10048 then begin // unable to bind to port
if Assigned(FOnStatusUpdate) then
OnStatusUpdate(Self, 'Can''t Bind to ' + INI.Smtp_BindAddress +':' +
IntToStr(INI.Smtp_Port), STAT_SERVERERROR);
if Assigned(FOnError) then OnError(Self, sec_CantListen);
end else begin // other error
if Assigned(FOnStatusUpdate) then
OnStatusUpdate(Self, 'Error #' + IntToStr(FSocket.LastError),
STAT_SERVERERROR);
if Assigned(FOnError) then OnError(Self, sec_SocketError);
end;
end;
procedure TSmtpServer.SocketBgException(Sender: TObject; E: Exception;
var CanClose: Boolean);
begin
// Critical Socket Error...
// This is because something caused an Exception during the socket's
// processing while it was in an event handler.
// If the program is good, this will never happen... but...
CanClose := False;
if Assigned(FOnStatusUpdate) then
OnStatusUpdate(Self, 'Unknown BG Exception', STAT_CRITICALERROR);
if Assigned(FOnError) then OnError(Self, sec_SocketError);
end;
procedure TSmtpServer.SmtpConnectionStatusUpdate(Sender : TObject;
Status : String;
Level : Integer);
begin
// the SmtpConnection has something to report... I'll pass it on,
// but add where I got it from...
if Assigned(FOnStatusUpdate) then
OnStatusUpdate(Self, ' ' +
Status, Level);
end;
procedure TSmtpServer.SmtpConnectionClosed(Sender : TObject);
var
x : Longint;
Connection : TSmtpConnection;
begin
// SmtpConnection is letting us know it's destroying itself.
// we need to remove it from the connection list
if Sender is TSmtpConnection then
for x := ConnectionList.Count -1 downto 0 do begin
Connection := TSmtpConnection(ConnectionList[x]);
if Connection = Sender then ConnectionList.Delete(x);
end;
if Assigned(FOnStateChange) then
OnStateChange(Self, FSocket.State = wsListening, ConnectionList.Count);
end;
(******************************************************************************)
(* *)
(* STOP SMTP Server Object *)
(* *)
(******************************************************************************)
(******************************************************************************)
(* *)
(* START SMTP Connection Object *)
(* *)
(* The actual Smtp server protocols are implemented here. This is the real *)
(* Smtp server code. We try to handle all Smtp requests here and act on *)
(* whatever we get. *)
(* *)
(* Note, while all commands are implemented, not all replies are possible. *)
(* this is because we won't do certain things. For instance, if we can't *)
(* accept a RCPT TO address, we'll never suggest an alternate route, cause *)
(* we don't store routing information. We do DNS every time. *)
(* *)
(******************************************************************************)
constructor TSmtpConnection.Create(AOwner : TSmtpServer);
begin
inherited Create(AOwner);
FServer := TSmtpServer(AOwner);
// Certain things cannot be done from event handlers. This is because an
// event handler is basically a function call. That means you can't free
// the sender in the handler, for example. If event handlers were activated
// by Windows messages, you could, because the code would be
// object-independant, but then you'd have messages flying everywhere...
// not pretty.
//
// Unfortunately, because of the nature of socket communications, there
// are some things you really want to do on socket events. For example,
// free an object that contains a socket when the socket closes...
// after all, we don't need it anymore.
//
// Well, we need to do it with a Windows message to divorce the object
// from the event. I was using TTimers, but this means every connection
// needs one. Not too efficient. So I made a Component that just has a
// Windows Handle so I could send messages to it, and derived the servers
// from them. Now the connections send messages to their servers to close.
// Initialize variables
FState := scs_IDENTIFICATION;
FUserID := '';
FMessageInfo := TSmtpMessageInformation.Create;
// Connecting Socket Create and Setup
FSocket := TWSocket.Create(Self);
FSocket.OnDataAvailable := SocketDataAvailable;
FSocket.OnDataSent := SocketDataSent;
FSocket.OnSessionClosed := SocketSessionClosed;
FSocket.OnError := SocketError;
FSocket.OnBgException := SocketBgException;
FBufferStr := '';
end;
destructor TSmtpConnection.Destroy;
begin
FMessageInfo.Free;
if Assigned(FSocket) then begin
FSocket.Destroy;
FSocket := nil;
end;
inherited Destroy;
end;
procedure TSmtpConnection.StatusUpdate(Status : String; Level : Integer);
begin
if Assigned(FOnStatusUpdate) then OnStatusUpdate(Self, Status, Level);
end;
procedure TSmtpConnection.Accept(SocketHandle : Integer);
var
Accept : Boolean;
begin
FSocket.Dup(SocketHandle);
StatusUpdate('Accepted', STAT_CONNECTIONEVENT);
// Initialize Connection State
FState := scs_IDENTIFICATION;
FMessageInfo.Initialize;
AccCtrl_FromAcceptedAddress := False;
AccCtrl_FromBannedAddress := False;
AccCtrl_FromIPAddress := '';
if FSocket.State = wsConnected then begin
StatusUpdate('Connected', STAT_CONNECTIONEVENT);
Accept := True;
AccCtrl_FromIPAddress := FSocket.GetPeerAddr;
AccCtrl_FromBannedAddress
:= INI.Smtp_Access_IsThisAddressBanned(AccCtrl_FromIPAddress);
AccCtrl_FromAcceptedAddress
:= INI.Smtp_Access_IsThisDomainAccepted(AccCtrl_FromIPAddress);
if INI.Smtp_Access_BanAddresses and AccCtrl_FromBannedAddress then
Accept := False;
if Accept then begin
// Send Greeting
case INI.Banner_Level of
bannerlevel_NameVersionService : FSocket.SendStr('220 ' + FServer.PublicServerName + ' Hermes ' + AppVersion + ' SMTP Ready.' + CRLF);
bannerlevel_NameService : FSocket.SendStr('220 ' + FServer.PublicServerName + ' Hermes SMTP Ready.' + CRLF);
bannerlevel_Service : FSocket.SendStr('220 ' + FServer.PublicServerName + ' SMTP Ready.' + CRLF);
else FSocket.SendStr('220 ' + FServer.PublicServerName + ' Hermes SMTP Ready.' + CRLF);
end;
end else begin
FSocket.SendStr('221 ' + FServer.PublicServerName +
' Closing Channel, Address banned.' + CRLF);
FSocket.Close;
StatusUpdate('Address ' + FUserID + ' BANNED', STAT_COMMANDERROR);
end;
// The SMTP specification allows other responses... 221 or 421, for example.
// These are to let the connected user know that while they connected, they
// can't talk to us because we're closing, or something. We won't do that.
// We simply don't accept a connection if we don't want to talk to a user.
// If they got this far... we're willing and able to talk.
end else begin
//
// DEBUG
//
// Is there ever a time we could accept the connection and then not be
// connected? If this happened, we assume the SessionClosed event would
// fire, thereby closing our connection and object.
//
// But we should check this out.
// This is also seen in the TPop3Connection Object
end;
end;
procedure TSmtpConnection.Close;
begin
// We want to close.
// If the socket is open, close it... if not,
// send the message that will free this connection object
if FSocket.State <> wsClosed then FSocket.Close
else PostMessage(FServer.Handle, WM_FREECONNECTION, Integer(Self), 0);
end;
// procedure SocketSessionConnected(Sender: TObject; Error: Word);
// Not called on accept
procedure TSmtpConnection.SocketDataAvailable(Sender: TObject; Error: Word);
var
x : Longint;
len : Integer; // Length of data accepted from the socket
Buffer : Array[0..1023] of Char; // buffer of data we'll accept from socket
// we add this data to the socket's command
// buffer (FBufferStr) and then parse it
// for CRLF to seperate out commands we
// need to act on.
UserRequest : String; // the command we got from the buffer
begin
// Data is available from the socket for processing.
// we'll receive the data, and buffer it until we get a CRLF,
// indicating the end of some sort of command from the client
len := FSocket.Receive(@Buffer[0], 1024);
FLastActivity := Now;
// add to Command Buffer (FBufferStr)
for x := 0 to len -1 do FBufferStr := FBufferStr + Buffer[x];
// Process buffer (look for CRLF) and process each command
while Pos(CRLF, FBufferStr) > 0 do begin
UserRequest := Copy(FBufferStr, 1, Pos(CRLF, FBufferStr) -1);
FBufferStr := Copy(FBufferStr, Pos(CRLF, FBufferStr) +2,
Length(FBufferStr));
// Process a command
ProcessRequest(UserRequest);
end;
end;
procedure TSmtpConnection.SocketDataSent(Sender: TObject; Error: Word);
begin
FLastActivity := Now;
end;
procedure TSmtpConnection.ProcessRequest(UserRequest : String);
var
Command, SubCommand : String; // User Command and sub-command
Parameter : String; // Possible Command Parameter
x : Longint;
Accept : Boolean; // I use this to decide to accept various
// commands from a client (after I've
// considered them, of course!)
RejectReason : String; // Reason a mail was rejected!
AliasID, AliasUser : String; // When considering who the mail is for,
// we'll use these to determine if it's a
// mail alias, which user it belongs to
UserInfo : TPop3UserInformation; // We use this to VeRiFY a user is here
// on the server
ListInfo : TMailListInformation; // we use these to EXPaNd a mail list
ListMember : PMailListMemberInfoRec; // to its membership if requested
SL : TStringList;
ToRoute : TMessageRouteInformation; // Used to analyze the RCPT TO command
// parameter to decide if we want to
// accept mail bound for the destination
FromRoute : TMessageRouteInformation; // Used to analyze the MAIL FROM command
// parameter to decide if we want to
// accept mail from the source
Route : TMessageRouteInformation;
Deliverer : TDeliverMail; // Object to handle actual delivery of mail
begin
// Are we receiving mail data (message data)? If we are, go to mail
// data processing, but if we're not, then we're accepting cammands
if FState <> scs_RECEIVINGMAILDATA then begin
// Accepting Smtp Commands
if UserRequest <> '' then begin
// Seperate out command from parameters to command
Command := UpperCase(Trim(UserRequest));
Parameter := '';
if Pos(' ', UserRequest) > 0 then begin
Command := UpperCase(Trim(Copy(UserRequest, 1,
Pos(' ', UserRequest))));
Parameter := Trim(Copy(UserRequest, Pos(' ', UserRequest),
Length(UserRequest)));
end;
StatusUpdate('Command: ' + Command + ' (' + Parameter + ')',
STAT_CONNECTIONEVENT);
// Process each command
if (Command = 'HELO') and // HELO machine-ID
(FState = scs_IDENTIFICATION) and
(FSocket.State = wsConnected) then begin
// User wants to identify self... Parameter is their ID
FUserID := Parameter;
// Here I could decide to accept or not...
Accept := True;
// Accept
if Accept then begin
// Accept connection request from the user.
FMessageInfo.Initialize; // Initialize message buffer
FState := scs_WAITCOMMAND;
FSocket.SendStr('250 ' + FServer.PublicServerName + CRLF);
StatusUpdate('User ' + FUserID + ' at ' + FSocket.GetPeerAddr + ' OK',
STAT_COMMANDEVENT);
end else begin
// Reject requesting user/machine
FSocket.SendStr('421 ' + FServer.PublicServerName +
' Closing Channel, Address banned.' + CRLF);
FSocket.Close;
StatusUpdate('User ' + FUserID + ' BAD', STAT_COMMANDERROR);
end;
end else
if (Command = 'MAIL') and // MAIL FROM:
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// Verify the command is well-formed (has a FROM:)
if Pos(':', Parameter) > 0 then begin
SubCommand := UpperCase(Trim(Copy(Parameter, 1,
Pos(':', Parameter) -1)));
Parameter := Trim(Copy(Parameter, Pos(':', Parameter) +1,
Length(Parameter)));
if SubCommand = 'FROM' then begin
// Analize From (Parameter)
FromRoute := TMessageRouteInformation.Create(mrte_From);
x := FromRoute.ParseRoute(Parameter);
if (x = 0) or (x = 1) then begin
// X = 1 allows me to accept empty return routes...
// These are usually failure notices... they're OK
FMessageInfo.AccCtrl_ToLocalUser := False;
FMessageInfo.AccCtrl_ToLocalCount := 0;
FMessageInfo.AccCtrl_FromLocalDomain
:= INI.Domain_IsThisOneOfMine(FromRoute.Domain);
FMessageInfo.AccCtrl_FromLocalUser :=
FMessageInfo.AccCtrl_FromLocalDomain and
INI.Mailbox_IsThisOneOfMine(FromRoute.Mailbox);
FMessageInfo.AccCtrl_FromAcceptedDomain
:= INI.Smtp_Access_IsThisDomainAccepted(FromRoute.Domain);
FMessageInfo.AccCtrl_FromBannedDomain
:= INI.Smtp_Access_IsThisDomainBanned(FromRoute.Domain);
FMessageInfo.AccCtrl_FromBannedMailbox
:= INI.Smtp_Access_IsThisMailboxBanned(FromRoute.Mailbox);
AccCtrl_FromBannedAddress
:= INI.Smtp_Access_IsThisAddressBanned(FromRoute.Domain);
FMessageInfo.AccCtrl_MessgaeSizeInBytes := 0;
Accept := True;
// Here I'll apply my rejection/Acceptance criteria!
if INI.Smtp_Access_BanDomains and
FMessageInfo.AccCtrl_FromBannedDomain then begin
Accept := False;
RejectReason := 'Domain (' + FromRoute.Domain + ') Banned';
end;
if INI.Smtp_Access_BanMailboxes and
FMessageInfo.AccCtrl_FromBannedMailbox then begin
Accept := False;
RejectReason := 'Mailbox (' + FromRoute.Mailbox + ') Banned';
end;
if INI.Smtp_Access_BanAddresses and
AccCtrl_FromBannedAddress then begin
Accept := False;
RejectReason := 'Address (' + FromRoute.Domain + ') Banned';
end;
// Here I could reject mail originating from any domain
// other than one of mine
// if not INI.Smtp_Forward then
// Accept := INI.Domain_IsThisOneOfMine(FromRoute.Domain);
// But I want to get mail from other people...
// so I'll accept any mail from anywhere...
if Accept then begin
// Store the Reverse-Path Route
FMessageInfo.ReverseRoute.ParseRoute(Parameter);
// Now I'm ready to accept RCPT TOs
FState := scs_RECEIVINGMAILTO;
FSocket.SendStr('250 Sender Accepted' + CRLF);
StatusUpdate('Mail From (' +
FMessageInfo.ReverseRoute.BuildRoute +
') accepted', STAT_COMMANDEVENT);
end else begin
// I didn't like the sender, so I'm gonna reject them
// They can still send other MAIL FROM commands...
FMessageInfo.Initialize; // Initialize message buffer
FState := scs_WAITCOMMAND;
FSocket.SendStr('550 Sender Not Accepted: ' +
RejectReason + CRLF);
StatusUpdate('Sender BAD (' + RejectReason + ')',
STAT_COMMANDERROR);
end;
end else begin
// I can't parse the reverse-path specified...
// I have to reject the MAIL FROM request
FSocket.SendStr('501 Reverse-Path not understood' + CRLF);
StatusUpdate('Bad Reverse Path', STAT_COMMANDERROR);
end;
FromRoute.Free;
end else begin
// there's no FROM in the Mail command... again it's not correct
FSocket.SendStr('501 Mail From?' + CRLF);
StatusUpdate('Missing FROM', STAT_COMMANDERROR);
end;
end else begin
// No : in command... it's not correctly formed
FSocket.SendStr('501 Mail From?' + CRLF);
StatusUpdate('Missing :', STAT_COMMANDERROR);
end;
end else
if (Command = 'RCPT') and // RCPT TO:
(FSocket.State = wsConnected) then begin
// Are we willing to accept RCPT TOs? Did we accept a MAIL FROM?
if (FState = scs_RECEIVINGMAILTO) then begin
FState := scs_RECEIVINGMAILTO;
// verify the command is well-formed (has a TO:)
if Pos(':', Parameter) > 0 then begin
SubCommand := UpperCase(Trim(Copy(Parameter, 1,
Pos(':', Parameter) -1)));
Parameter := Trim(Copy(Parameter, Pos(':', Parameter) +1,
Length(Parameter)));
if SubCommand = 'TO' then begin
// if accept (local or forwardable) 250(local)
// or 251 (forward)
// Analize To (Parameter)
ToRoute := TMessageRouteInformation.Create(mrte_To);
x := ToRoute.ParseRoute(Parameter);
if x = 0 then begin
// Here I can reject mail based on who it's going to...
// I'm going to accept mail for other domains unconditionally...
// So local users can send mail to non-locals
Accept := True;
// But mail for my domain myst have a valid user mailbox...
if INI.Domain_IsThisOneOfMine(ToRoute.Domain) then begin
Accept := False;
// then I check lists, users, and aliases
if INI.Alias_Exists(ToRoute.Mailbox) or
INI.Alias_Exists(ToRoute.Mailbox + '@' + ToRoute.Domain) or // The addressed user could be a fullt qualified mail alias!
INI.User_Exists(ToRoute.Mailbox) or
INI.List_Exists(ToRoute.Mailbox) then begin
StatusUpdate('Mail To is Local', STAT_PROCESSINGEVENT);
FMessageInfo.AccCtrl_ToLocalUser := True;
Accept := True;
end;
// I could go further here...
// UserInfo : TPop3UserInformation;
// I could open the list or user and find out weather the
// message was too big, or other stuff if I cared to...
end;
if Accept then begin
Inc(FMessageInfo.AccCtrl_ToLocalCount);
FState := scs_RECEIVINGMAILTO;
// Still willing to accept RCPT TOs
// Store this Receipient
FMessageInfo.AddForwardRoute(Parameter);
FSocket.SendStr('250 Destination Accepted' + CRLF);
StatusUpdate('Mail To accepted', STAT_COMMANDEVENT);
end else begin
FState := scs_RECEIVINGMAILTO;
// Still willing to accept RCPT TOs
// I couldn't understand the forward-path route
// but that's OK... I'll let then send more...
FSocket.SendStr('550 Destination Not Accepted' + CRLF);
StatusUpdate('Receiver BAD', STAT_COMMANDERROR);
end;
end else begin
FState := scs_RECEIVINGMAILTO;
// I couldn't understand the forward-path route
// but that's OK... I'll let then send more...
FSocket.SendStr('501 Forward-Path not understood' + CRLF);
StatusUpdate('Bad Forward Path', STAT_COMMANDERROR);
end;
ToRoute.Free;
end else begin
FState := scs_WAITCOMMAND;
// I'm resetting to wait for MAIL FROM again...
// there's no TO in the Rcpt command... again it's not correct
FSocket.SendStr('501 Mail To?' + CRLF);
StatusUpdate('Missing TO', STAT_COMMANDERROR);
end;
end else begin
FState := scs_WAITCOMMAND;
// I'm resetting to wait for MAIL FROM again...
// No : in command... it's not correctly formed
FSocket.SendStr('501 Mail To?' + CRLF);
StatusUpdate('Missing :', STAT_COMMANDERROR);
end;
end else begin
FState := scs_WAITCOMMAND;
// I'm resetting to wait for MAIL FROM again...
// I havn't accepted the MAIL FROM command yet. This is premature
FSocket.SendStr('503 Please Mail From before RCPT' + CRLF);
StatusUpdate('No From yet, how can we have Tos?', STAT_COMMANDERROR);
end;
end else
if (Command = 'DATA') and // DATA
(FSocket.State = wsConnected) then begin
// User wants to send me some mail data (actual message)
// THey had better already have given me one Rcpt To route
if (FState = scs_RECEIVINGMAILTO) and
(FMessageInfo.ForwardRouteCount > 0) then begin
Accept := True;
if INI.Smtp_Access_Restricted then begin
if ((not AccCtrl_FromAcceptedAddress) and
(not FMessageInfo.AccCtrl_FromAcceptedDomain) and
(not FMessageInfo.AccCtrl_FromLocalUser) and
(not FMessageInfo.AccCtrl_ToLocalUser)) then begin
Accept := False;
if FMessageInfo.AccCtrl_ToLocalUser then
RejectReason := 'NOT addressed to local user.'
else
RejectReason := 'Domain or user NOT Accepted';
end;
end;
if INI.Smtp_Access_OnlyForUnderXUsers and
(INI.Smtp_Access_OnlyForUsersCount > 0) and
(FMessageInfo.AccCtrl_ToLocalCount >
INI.Smtp_Access_OnlyForUsersCount) then begin
Accept := False;
RejectReason := 'Too Many Recipients (' +
IntToStr(FMessageInfo.AccCtrl_ToLocalCount) + ' > '
+ IntToStr(INI.Smtp_Access_OnlyForUsersCount) + ')';
end;
if Accept then begin
FState := scs_RECEIVINGMAILDATA;
FSocket.SendStr('354 Start Mail Input, end with .' + CRLF);
StatusUpdate('Ready to accept mail', STAT_COMMANDEVENT);
end else begin
FMessageInfo.Initialize; // Initialize message buffer
FState := scs_WAITCOMMAND;
FSocket.SendStr('451 Error in processing: ' + RejectReason + CRLF);
StatusUpdate('Mail Rejected: ' + RejectReason, STAT_COMMANDERROR);
end;
end else begin
// I've never gotten a rcpt to route...
FSocket.SendStr('503 Please RCPT TO before DATA' + CRLF);
StatusUpdate('No To yet, how can we have Data?', STAT_COMMANDERROR);
end;
end else
if (Command = 'RSET') and // RSET
// (FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// Reset connection and drop mail in progress
// OK.
FState := scs_WAITCOMMAND;
FMessageInfo.Initialize; // Initialize message buffer
FSocket.SendStr('250 Ready.' + CRLF);
StatusUpdate('Connection Reset', STAT_COMMANDEVENT);
end else
if (Command = 'SEND') and // SEND FROM:
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// Send to user terminal... no terminals, so not implemented
FState := scs_WAITCOMMAND;
FSocket.SendStr('502 SEND not implemented.' + CRLF);
StatusUpdate('Command not implemented', STAT_COMMANDERROR);
end else
if (Command = 'SOML') and // SOML FROM:
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// Send to user terminal... OR Mail...
// Not implemented. I could mail here... but
// I'm not gonna. Let them MAIL if they want to MAIL
FState := scs_WAITCOMMAND;
FSocket.SendStr('502 SOML not implemented.' + CRLF);
StatusUpdate('Command not implemented', STAT_COMMANDERROR);
end else
if (Command = 'SAML') and // SAML FROM:
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// Send to user terminal... AND Mail...
// Not implemented. I could mail here... but
// I'm not gonna. Let them MAIL if they want to MAIL
FState := scs_WAITCOMMAND;
FSocket.SendStr('502 SAML not implemented.' + CRLF);
StatusUpdate('Command not implemented', STAT_COMMANDERROR);
end else
if (Command = 'VRFY') and // VRFY
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// Verify a user is valid...
// see if it's an alias, and if so, recover real UserID
if INI.Alias_Exists(Parameter) then begin
Parameter := INI.Alias_Find(Parameter);
INI.Alias_Parse(Parameter, AliasID, AliasUser);
Parameter := AliasUser;
StatusUpdate('Alias Converted', STAT_COMMANDEVENT);
end;
// Parameter is UserID
if INI.User_Exists(Parameter) then begin
// The user is a valid user on this system...
UserInfo := TPop3UserInformation.Create;
UserInfo.LoadFromFile(Parameter);
// we can decide how to reply now
// Maybe user wants to hide himself?
Accept := not UserInfo.UB_DoNotReportUserExists_SMTP;
if Accept then begin
// DEBUG
// It needs to be a routeable response...
// I think this'll work but I'm not certain...
FSocket.SendStr('250 ' + UserInfo.RealName + ' <' +
FormatedAddress(Parameter, INI.ServerName) +
'>' + CRLF);
StatusUpdate('User Reported', STAT_COMMANDEVENT);
end else begin
// Couldn't report on user... they want to stay hidden
FSocket.SendStr('550 User Unknown' + CRLF);
StatusUpdate('User is Hidden', STAT_COMMANDERROR);
end;
UserInfo.Free;
end else begin
// Couldn't find user...
FSocket.SendStr('550 User Unknown' + CRLF);
StatusUpdate('User is Unknown', STAT_COMMANDERROR);
end;
end else
if (Command = 'EXPN') and // EXPN
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// User wants information on a Mail List...
// the list name is given in Parameter
if INI.List_Exists(Parameter) then begin
// The list exists here, not get list information
ListInfo := TMailListInformation.Create;
ListInfo.LoadFromFile(Parameter);
// we can decide how to reply now
// Maybe List wants to hide members?
Accept := not ListInfo.LB_DoNotReportListMembers_SMTP;
if Accept then begin
// Reply with list membership
// Build a StringList of the responses...
// We want to limit our responses to "Active" and non-"Hidden"
// list members with routeable addresses.
SL := TStringList.Create;
Route := TMessageRouteInformation.Create(mrte_Unknown);
for x := 0 to ListInfo.MemberCount -1 do begin
ListMember := ListInfo.Members[x];
if (ListMember.Active) and (not ListMember.Hidden) and
(Route.ParseRoute(ListMember.EMail) = 0) then
SL.Add(Route.BuildRoute);
end;
Route.Free;
if SL.Count = 0 then begin
// DEBUG
// List exists, but has no visible (active and not hidden)
// members... I'll call this an errer, but I'm not sure it
// really is...
FSocket.SendStr('550 List is Empty' + CRLF);
StatusUpdate('List is Empty', STAT_COMMANDERROR);
end else
if SL.Count = 1 then begin
// The list has one active and visible member... let's report
FSocket.SendStr('250 ' + SL[0] + CRLF);
StatusUpdate('List Entry Reported', STAT_COMMANDEVENT);
end else begin
// The list has several active and visible members, here they are
for x := 0 to SL.Count -2 do
FSocket.SendStr('250-' + SL[x] + CRLF);
FSocket.SendStr('250 ' + SL[SL.Count -1] + CRLF);
StatusUpdate('List Entries Reported', STAT_COMMANDEVENT);
end;
SL.Free;
end else begin
// we didn't accept the request for some reason
// (like the list is hidden or hiding its membership)
FSocket.SendStr('550 List Unknown' + CRLF);
StatusUpdate('List is Hidden', STAT_COMMANDERROR);
end;
ListInfo.Free;
end else begin
// The listname is unknown.
FSocket.SendStr('550 List Unknown' + CRLF);
StatusUpdate('List Unknown', STAT_COMMANDERROR);
end;
end else
if (Command = 'HELP') and // HELP , optional>
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// User wants some help... yeah right ;-)
// I've got no help to offer... maybe I'll add this later, but
// realistically, this is a holdover from the days when the user
// might have been a human... now they never are.
FState := scs_WAITCOMMAND;
FSocket.SendStr('502 No Help Available' + CRLF);
StatusUpdate('Command not implemented', STAT_COMMANDEVENT);
end else
if (Command = 'NOOP') and // NOOP
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// User wants to do a "Noop"...
// No problaemo...
FState := scs_WAITCOMMAND;
FSocket.SendStr('250 OK' + CRLF);
StatusUpdate('No Operation', STAT_COMMANDEVENT);
end else
if (Command = 'TURN') and // TURN
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// User wants to switch roles. no thanks.
// This implementation is not capeable of reversing
// course because it's implemented as two seperate
// parts (Server and Agent)
FState := scs_WAITCOMMAND;
FSocket.SendStr('502 TURN not permitted.' + CRLF);
StatusUpdate('Command not implemented', STAT_COMMANDERROR);
end else
if (Command = 'QUIT') and // QUIT
(FState = scs_WAITCOMMAND) and
(FSocket.State = wsConnected) then begin
// User wants to Quit
FState := scs_WAITCOMMAND;
// FSocket.SendStr('250 OK' + CRLF);
FSocket.SendStr('221 OK, Closed' + CRLF);
// Mail is processed on receipt of .
// If I never got it, then mail gets dropped
StatusUpdate('Closing connection', STAT_CONNECTIONEVENT);
Close;
end else
begin
// This is not a command I understand
if FState <> scs_IDENTIFICATION then FState := scs_WAITCOMMAND;
FSocket.SendStr('500 Command not recognized.' + CRLF);
StatusUpdate('Command not recognized', STAT_COMMANDERROR);
end;
end;
end else begin
// We're accepting Mail (message) data....
if UserRequest = '.' then begin // end of data
// if the user sent us just a period (.) then this is the sign that
// the mail data is finished, and we should try to process it.
StatusUpdate('Processing Incoming Mail...', STAT_COMMANDEVENT);
// I need to record the fact that I received this mail in the mail header
// before trying to deliver it to anybody...
StatusUpdate('Adding Received to Message Header', STAT_PROCESSINGEVENT);
FMessageInfo.InsertReceived;
// Here's the format:
// Received: from HOST by HOST ; DD MON YY HH:MM:SS ZONE
// Now I can try to deliver the mail... anyway, this step is successful
// If there's a delivery failure, then the Smtp Agent will take care of
// that when it's processing the mail for future delivery...
Deliverer := TDeliverMail.Create(FMessageInfo);
Deliverer.OnStatusUpdate := FOnStatusUpdate;
Deliverer.Deliver;
Deliverer.Free;
FMessageInfo.Initialize;
// Added 5-22-2000 to fix the multi-mail to one mail (compounding) bug!
// Now I'm ready to accept more commands...
FState := scs_WAITCOMMAND;
StatusUpdate('Incoming Mail Processed', STAT_COMMANDEVENT);
FSocket.SendStr('250 Mail Queued for Delivery' + CRLF);
end else begin
// the data the user sent us must be added to the mail message for
// delivery but if the line starts with a period (.) (but is longer
// than a period) then we know they padded it to send to us, and we
// need to remove the first period.
if Copy(UserRequest, 1, 1) = '.' then // Remove padded period (.)
UserRequest := Copy(UserRequest, 2, Length(UserRequest));
// Add data to message data already stored
FMessageInfo.Data_AppendLine(UserRequest);
if INI.LogSpyMessageContent then
StatusUpdate('Data: ' + UserRequest, STAT_PROCESSINGEVENT);
end;
end;
end;
procedure TSmtpConnection.SocketSessionClosed(Sender: TObject; Error: Word);
begin
StatusUpdate('Closed', STAT_CONNECTIONEVENT);
// Socket is closed, we must tell Server Object
// to free this connection. There's no point to
// carrying on without a connection, now is there?
PostMessage(FServer.Handle, WM_FREECONNECTION, Integer(Self), 0);
end;
procedure TSmtpConnection.SocketError(Sender: TObject);
begin
StatusUpdate('Socket Error', STAT_CONNECTIONERROR);
// We had a socket error. This isn't a protocol error like the user
// typed the wrong command, it's a dropped connection, or something else.
// we'll close on this too, because we don't know how to recover from it.
// the user can open a new connection if they really want.
Close; // Close on Error
end;
procedure TSmtpConnection.SocketBgException(Sender: TObject; E: Exception;
var CanClose: Boolean);
begin
CanClose := False;
StatusUpdate('Background Exception Error', STAT_CRITICALERROR);
// We had a background exception. This is like a socket error in that
// we don't know what happened, and we don't know how to recover, so
// we'd better just close this connection.
Close; // Close on Error
end;
(******************************************************************************)
(* *)
(* STOP SMTP Connection Object *)
(* *)
(******************************************************************************)
(* Bugs Fixed *)
{ // This is not a command I understand
Was: FState := scs_WAITCOMMAND;
Now: if FState <> scs_IDENTIFICATION then FState := scs_WAITCOMMAND;
// Changed because a bad HELO caused us to skip the Identification state.
// Thanks to "Vassilis Stathopoulos" on 2/1/00
}
end.