One small change from the previous article: I mentioned that in this article I would discuss DNR (Domain Name Resolution) services. In order to avoid confusion and to better understand the flow of the implementation, I decided to cover that in the next article.
Now we need to decide what type of information we need for handling an incoming connection. The IPDaemon control handles the connection information for us already by passing the connection ID in each event. This will help us when processing data for the appropriate connection. Below is a user-defined type variable of the elements we are going to need to track for this implementation. These variables are placed in the [general][declarations] section of the module SMTP_IN.BAS.
Type InMsgData sRemoteIP As String * 15 'Remote Host IP Address sRemoteHost As String * 64 'Remote Host Name iRcptFh As Integer 'Handle to the Recipient File iMsgFh As Integer 'Handle to the Message File sMsgID As String * 8 'Message ID for incoming message bMailFrom As Integer 'Flag if we got the MAIL FROM command iRcptCnt As Integer 'Recipient count bRecMsg As Integer 'In Message receive Flag iTimeOut As Long 'TimeOut counter bActive As Integer 'active connection flag End TypeEach element in this variable has a specific function related to the processing of the incoming data into the server for each connection. The following variables are declared also in this module:
Dim maInBound(32) As InMsgData 'Inbound connection data Dim maInBuffer(32) As String 'Internal input bufferThe maInBound(32) array handles the connection information and controls some processing that takes place. Each connection ID is passed to the processing procedure so the appropriate element in the array is accessed. The maInBuffer(32) array stores any data that comes in that needs to be processed at a later time, usually after data that is received is incomplete. You may receive a partial packet or partial information that needs to be processed but can't be unless a certain set of circumstances occurs. RFC-821 explains how a command is to be sent.
Sub tcpDaemon_Connected (ConnectionID As Integer, StatusCode As Integer, Description As String) InitInBoundConnection ConnectionID, tcpDaemon.RemoteHost(ConnectionID) End SubThis event is fired every time we get a connection from an inbound host. This is the signal we use to set up the array element for the connection ID that the control passes to us. The procedure invokes the InitInBoundConnection procedure which begins the process of initializing the array element we are going to reference for this connection. The event procedure also passes the IP address of the remote host that we use to display on the caption of the form that logs the communication between the hosts.
The next event is the DataIn event, which is used to process incoming data from the connection. Here is the code for the procedure.
Sub tcpDaemon_DataIn (ConnectionID As Integer, Text As String, EOL As Integer) tcpDaemon.AcceptData(ConnectionID) = False InBoundRemoteCmd ConnectionID, Text tcpDaemon.AcceptData(ConnectionID) = True End SubIn this procedure, we establish flow control on the connection by suspending the reception of data (blocking) until we have processed our data. When the InBoundRemoteCmd returns, we unblock the connection and resume. This allows us to control the flow over a high-volume network where you may have several computers which send large amounts of data over the connection at high speed and also protects you from problems resulting from the re-entrance of the procedure during the actual message transmission. Of course, if you are on a network that has a single data line and the line runs at say less than 128KB, then you should be able to remove the two lines that set the AcceptData property. I only included them because on one of my LANs, I have four AT&T 3B2GR600 mini's that are registered hosts and have SMTP capability for remote users not physically located on the LAN that also exist with this SMTP gateway. They have a tendency to send messages of 1MB or more and that causes some problems on the 386 computer used for the gateway.
The last event is the Disconnected event. This event is only fired if the remote computer closes the connection. If the remote computer does not close the connection, then this event is not fired. It is more of a safety precaution in case you lose connectivity and need to terminate a connection.
Sub tcpDaemon_Disconnected (ConnectionID As Integer, StatusCode As Integer, Description As String) CloseDaemonConnection ConnectionID, False End SubThe only thing we don't process is the StatusCode and the Description because that is used in the case of an error that occurred when the connection was broken. For our implementation, this particular error is meaningless and so we ignore the secondary parameters.
Below is the InitInBoundConnection code and how we initialize the connection data.
Sub InitInBoundConnection (piConnectID%, ByVal psIPAddr$) Dim NewID As New frmLog, sReturnString$ 'Initialize the connection information... maInBound(piConnectID%).iRcptFh = 0 maInBound(piConnectID%).iMsgFh = 0 maInBound(piConnectID%).bMailFrom = False maInBound(piConnectID%).iRcptCnt = 0 maInBound(piConnectID%).sRemoteIP = psIPAddr$ maInBound(piConnectID%).sRemoteHost = " " maInBound(piConnectID%).bRecMsg = 0 maInBound(piConnectID%).iTimeOut = 0 maInBound(piConnectID%).bActive = True 'Load the log form Load NewID 'Set the properties of the form for the user.. NewID.Tag = "R" + LTrim$(Str$(piConnectID%)) NewID.cLogText = "Receiving Connection from: " + psIPAddr$ + gsCRLF NewID.Caption = "Receiving: " + psIPAddr$ 'Display the form NewID.Show 'Post a message to the status bar PostStatusMessage "Connected to: " + psIPAddr$, PANEL_STATUSIN 'Once ready, send the appropriate response... sReturnString$ = "220 SMART SMTP Server Version 2.00 Simple Mail Transfer Ready." + gsCRLF SendCmdResponse piConnectID%, sReturnString$ End SubThe function of this procedure basically initializes the element in the maInBound() array and assigns information to the array elements UDT . It also loads a new log form and assigns properties to the form while adding text to the log form's text box for the user. Finally, the 220 service ready message is sent to the remote host indicating the system is ready.
At this point, the system is now ready to process commands from the remote host. The following procedure is called in the tcpDaemon_DataIn event.
Sub InBoundRemoteCmd (piConnectID%, psInCmd$) Dim sReturnCmd$, iStopFlag% Dim sTempBuffer$ On Local Error Resume Next 'Place the data passed to us in the buffer. maInBuffer(piConnectID%) = maInBuffer(piConnectID%) + psInCmd$ 'Reset the timeout to zero so we don't get aborted. maInBound(piConnectID%).iTimeOut = 0 'If we are receiving the message data... If maInBound(piConnectID%).bRecMsg Then 'Find out if we have received the end of message flag iStopFlag% = InStr(maInBuffer(piConnectID%), gsXMITend) 'If we haven't If iStopFlag% = 0 Then 'Copy the last five characters of the buffer into a temporary buffer. sTempBuffer$ = Right$(maInBuffer(piConnectID%), 5) 'Save the remaining data into the message file... Print #maInBound(piConnectID%).iMsgFh, Left$(maInBuffer(piConnectID%), Len(maInBuffer(piConnectID%)) - 5); 'Copy the temporary buffer back into the main buffer maInBuffer(piConnectID%) = sTempBuffer$ 'Exit the procedure Exit Sub Else 'We have received the end of message flag, now save everything 'before the flag itself Print #maInBound(piConnectID%).iMsgFh, Left$(maInBuffer(piConnectID%), iStopFlag% - 1); 'reset our in message flag maInBound(piConnectID%).bRecMsg = False 'Close the message file Close maInBound(piConnectID%).iRcptFh, maInBound(piConnectID%).iMsgFh 'Now we need to move the files into the processing directory If RenameInBound(maInBound(piConnectID%).sMsgID) Then 'If our move was successful, set up the OK response sReturnCmd$ = "250 Message ID: " + maInBound(piConnectID%).sMsgID + ". Mail Being Delivered." 'Update the message received stats gStats.lInMsgCnt = gStats.lInMsgCnt + 1 Else 'Out move wasn't successful.. set up the error response sReturnCmd$ = "554 Processing Error - Cannot deliver mail." 'Now Kill the files off... Kill Trim$(gSettings.SMARTApps) + "\SMTP\IN\" + Trim$(maInBound(piConnectID%).sMsgID) + ".IN?" End If 'Reset the message data since we are finished. maInBuffer(piConnectID%) = "" maInBound(piConnectID%).bMailFrom = False maInBound(piConnectID%).iRcptFh% = 0 maInBound(piConnectID%).iMsgFh% = 0 maInBound(piConnectID%).iRcptCnt% = 0 maInBound(piConnectID%).sMsgID = "" 'Now send the response message to the user If Right$(sReturnCmd$, 2) <> gsCRLF$ Then sReturnCmd$ = sReturnCmd$ + gsCRLF$ SendCmdResponse piConnectID%, sReturnCmd$ End If Else 'We aren't receiving the message data so process the command. 'If the command has a CR/LF at the end per RFC-821 If Right$(maInBuffer(piConnectID%), 2) = gsCRLF$ Then 'Post the response to the log windows PostToMessageWindow piConnectID%, maInBuffer(piConnectID%) 'Strip off the CRLF from the inbuffer maInBuffer(piConnectID%) = Left$(maInBuffer(piConnectID%), Len(maInBuffer(piConnectID%)) - 2) 'Check the command Select Case UCase$(Left$(maInBuffer(piConnectID%), 4)) Case "HELO" cmd_HELO piConnectID%, maInBuffer(piConnectID%), sReturnCmd$ Case "MAIL" cmd_MAIL piConnectID%, maInBuffer(piConnectID%), sReturnCmd$ Case "RCPT" cmd_RCPT piConnectID%, maInBuffer(piConnectID%), sReturnCmd$ Case "VRFY" cmd_VRFY piConnectID%, maInBuffer(piConnectID%), sReturnCmd$ Case "DATA" If maInBound(piConnect%).bMailFrom Then 'In this command, we check to see if we have any recipients If maInBound(piConnectID%).iRcptCnt > 0 Then 'If so, set the flag to true maInBound(piConnectID%).bRecMsg = True 'Add the Received: field Print #maInBound(piConnectID%).iMsgFh, "Received: From " + Trim$(maInBound(piConnectID%).sRemoteHost) + " by " + Trim$(gSettings.SMTPName) + " " + Format$(Now, "ddd, d mmm, yyyy") + " at " + Format$(Now, "h:nn:ss") 'Set the return command so we can receive mail sReturnCmd$ = "354 Send mail; end with a line containing only a period" Else 'We don't have any recipients, set the error message sReturnCmd$ = "503 No Recipients specified" End If Else 'We don't have the mail from data, set the error message sReturnCmd$ = "503 No Mail From sender specified." End If Case "NOOP" 'This command returns an OK response sReturnCmd$ = "250 OK" Case "RSET" cmd_RSET piConnectID%, maInBuffer(piConnectID%), sReturnCmd$ Case "HELP" cmd_HELP piConnectID%, maInBuffer(piConnectID%), sReturnCmd$ Case "QUIT" 'Remote host wants to quit. set the remote host return message sReturnCmd$ = "221 " + RTrim$(gSettings.SMTPName) + " says goodbye to " sReturnCmd$ = sReturnCmd$ + RTrim$(maInBound(piConnectID%).sRemoteHost) sReturnCmd$ = sReturnCmd$ + " [" + RTrim$(maInBound(piConnectID%).sRemoteIP) + "]" sReturnCmd$ = sReturnCmd$ + " at " + Format$(Now, "ddd dd mmm yyyy hh:nn:ss") Case Else 'Command is not recognized, return the error sReturnCmd$ = "500 Syntax Error, command unrecognized" End Select 'Clear the input buffer... maInBuffer(piConnectID%) = "" 'Fix up the response command... If Right$(sReturnCmd$, 2) <> gsCRLF$ Then sReturnCmd$ = sReturnCmd$ + gsCRLF$ 'Send it... SendCmdResponse piConnectID%, sReturnCmd$ 'If this is a 221 message, close the connection. If Left$(sReturnCmd$, 3) = "221" Then CloseDaemonConnection piConnectID%, False End If End If End SubAs you can see, this is where we do most of the processing. After the text is processed, then if a response is necessary, the procedure then sends the response to the remote system. Below is the code for sending the remote response.
Sub SendCmdResponse (piConnectID%, psReturnCmd$) Dim sWinID$ 'Sends the string to the remote host... On Error Resume Next sWinID$ = "R" + Right$("00" + Trim$(Str$(piConnectID%)), 2) PostToMessageWindow sWinID$, psReturnCmd$ frmMain.tcpDaemon.DataToSend(piConnectID%) = psReturnCmd$ End SubIn this procedure, the response is first posted to the log window assigned to the connection. Then the response command is sent using the DataToSend property.
Once the QUIT command is received, the server terminates the connection and resets the array elements. This is done in the following procedure.
Sub CloseDaemonConnection (ByVal piConnectID%, ByVal pbAbort%) On Local Error Resume Next Dim fLog As Form Dim sFrmID As String Dim iCnt% sFrmID = "R" + LTrim$(Str$(piConnectID%)) For iCnt% = 0 To Forms.Count - 1 If TypeOf Forms(iCnt%) Is frmLog Then If Forms(iCnt%).Tag = sFrmID Then Set fLog = Forms(iCnt%) Unload fLog Set fLog = Nothing End If End If Next 'Close the files that are opened just in case Close maInBound(piConnectID%).iRcptFh, maInBound(piConnectID%).iMsgFh 'Kill the files as well.. Kill Trim$(gSettings.SMARTApps) + "\SMTP\IN\" + Trim$(maInBound(piConnectID%).sMsgID) + ".IN?" 'Show this connection as inactive maInBound(piConnectID%).bActive = False 'If we did an abort then we need to reset the Linger Property to force a hard close If pbAbort% Then frmMain.tcpDaemon.Linger = False 'Close the connection... frm_main.tcpDaemon.Connected(piConnectID%) = False 'If this was an abort, reset the linger property. If pbAbort% Then DoEvents frmMain.tcpDaemon.Linger = True End If End SubIn this procedure, we pass the connection ID and whether this connection is aborted. If the connection is aborted, the tcpDaemon.Linger property is set to false which forces a hard close on the connection port. Since the IPDaemon control uses only asynchronous blocking calls, the connection may never be terminated properly on a connection where trouble exists. In this case, we need to force the connection closed to guarantee that the WINSOCK stack does not keep the connection open, thus using up valuable system resources. If this is a normal disconnection, then the procedure just closes the connection setting the tcpDaemon.Connected property to False.
The last procedure is, of course, those connections which may have problems. Usually, this is indicated by a connection from a remote host that stops responding, yet leaves the connection active. RFC-821 discusses and RFC-1123 sets the guidelines for terminating connections from non-responsive hosts. This procedure is called from a Timer event which is set to 1 second intervals to update the iTimeOut element. As the threshold for each connection is reached, the procedure checks to see if the value has passed 300 ticks, which amounts to approximately 5 minutes. If so, the connection is aborted. In the InBoundRemoteCmd procedure, each time the procedure is called, the iTimeOut element is reset to zero. Here is the code that tracks the connections.
Sub UpdateInConnections () Dim iCnt% For iCnt% = 1 To 32 If maInBound(iCnt%).bActive Then maInBound(iCnt%).iTimeOut = maInBound(iCnt%).iTimeOut + 1 If maInBound(iCnt%).iTimeOut > 300 Then CloseDaemonConnection iCnt%, True End If Next End SubThis procedure is relatively flexible in that you can have a separate timer for your inbound connections. In this case, you could have it set up in such a way that shuts off the timer when there are no connections. However, you must turn it back on the instant one is established. To do this, you could create a timer control with the following properties set
Interval 1000 Name InTimer Enabled FalseThen, add this code to the InTimer_Timer event:
Sub InTimer_Timer() UpdateInConnections End SubThen add the following code to the InitInBoundConnection() procedure:
frmMain.InTimer.Enabled = TrueNow change the UpdateInConnections() procedure as follows...
Sub UpdateInConnections () Dim iCnt%, bActiveConnection% For iCnt% = 1 To 32 If maInBound(iCnt%).bActive Then bGotConnection% = True maInBound(iCnt%).iTimeOut = maInBound(iCnt%).iTimeOut + 1 If maInBound(iCnt%).iTimeOut > 300 Then CloseDaemonConnection iCnt%, True End If Next If Not bActiveConnection% then frmMain.InTimer.Enabled = False End SubHowever, I like to maximize my timers as best as possible and so I don't use but two timers in the whole project. One of them is for my Status Bar (the code for which I have developed separately and included in the sample - it is also available in Visual Basic Tips and Tricks '95 June issue) and I use that timer for handling the connections data.
Procedure for the HELO command:
Sub cmd_HELO (ByVal piConnectID%, ByVal psDataIn$, psResponse$) 'Get the name of the Remote host maInBound(piConnectID%).sRemoteHost = Trim$(Mid$(psDataIn$, 5)) 'Update the stats info gStats.sLastHostReceive = maInBound(piConnectID%).sRemoteHost 'Send the OK response.... psResponse$ = "250 " + RTrim$(gSettings.SMTPName) End SubProcedure for the HELP command:
Sub cmd_HELP (ByVal piConnectID%, ByVal psDataIn$, psResponse$) 'system help message.... Dim sTemp$ 'Get the argument passed sTemp$ = Mid$(psDataIn$, 5) 'If no argument passed... If RTrim$(sTemp$) = "" Then 'send the response psResponse$ = "211-Supported commands are HELO, MAIL, RCPT, VRFY, HELP, NOOP, RSET, QUIT" psResponse$ = psResponse$ + gsCRLF + "211 To get help on a specified command, use HELPProcedure for the MAIL command:" Else 'Otherwise, send the response for the correct topic Select Case Trim$(UCase(sTemp$)) Case "HELO" psResponse$ = "214-Usage: HELO domain" psResponse$ = psResponse$ + gsCRLF + "214 Example: HELO domain" Case "HELP" psResponse$ = "214-Usage: HELP or HELP " psResponse$ = psResponse$ + gsCRLF + "214 Example: HELP HELP" Case "MAIL" psResponse$ = "214-Usage: MAIL FROM: " psResponse$ = psResponse$ + gsCRLF + "214 Example: MAIL FROM: " Case "NOOP" psResponse$ = "214 Usage: NOOP" Case "QUIT" psResponse$ = "214 Usage: QUIT" Case "RCPT" psResponse$ = "214-Usage: RCPT TO: " psResponse$ = psResponse$ + gsCRLF + "214 Example: RCPT TO: " Case "RSET" psResponse$ = "214-Usage: RSET" Case "VRFY" psResponse$ = "214-Usage: VRFY user" psResponse$ = psResponse$ + gsCRLF + "214 Example: VRFY smith" Case Else psResponse$ = "501 Syntax Error" End Select End If End Sub
Sub cmd_MAIL (ByVal piConnectID%, ByVal psDataIn$, psResponse$) Dim sTemp$, iFh%, iErr%, sMsgID$, iLBracket%, iRBracket%, sFrom$ 'Want to check for the Correct ID Connect If RTrim$(maInBound(piConnectID%).sRemoteHost) = "" Then psResponse$ = "503 No HELO command specified - you are out of sequence" Exit Sub End If If UCase$(Left$(psDataIn$, 10)) = "MAIL FROM:" Then 'Snap off the from part iLBracket% = InStr(11, psDataIn$, "<") iRBracket% = InStr(psDataIn$, ">") If (iLBracket% > 10) And (iRBracket% > iLBracket% + 1) Then sFrom$ = Mid$(psDataIn$, iLBracket% + 1, iRBracket% - iLBracket% - 1) 'This process handles the mail from line.. it is generally accepted... maInBound(piConnectID%).bMailFrom = True 'Get a unique message id If GetUniqueID(sMsgID$) Then maInBound(piConnectID%).sMsgID = sMsgID$ 'Set up our file for the recipients.. sTemp$ = RTrim$(gSettings.SMARTApps) + "\SMTP\IN\" + sMsgID$ + ".IN1" 'Now get the file handle iFh% = GetNewFileHandle(sTemp$, FILEIO_OUTPUT, 0, iErr%) 'If we didn't get a file handle, return an error response If iFh% = 0 Then psResponse$ = "451 System Failure - Cannot Open Buffer for Recipients" Exit Sub End If 'Save the file handle to the connection array maInBound(piConnectID%).iRcptFh = iFh% 'Print the mail from address as the first line Print #maInBound(piConnectID%).iRcptFh%, sFrom$ 'Now set up the message file buffer.. sTemp$ = RTrim$(gSettings.SMARTApps) + "\SMTP\IN\" + sMsgID$ + ".IN2" 'Reset the file handle iFh% = 0 'Get the file handle iFh% = GetNewFileHandle(sTemp$, FILEIO_OUTPUT, 0, iErr%) 'If we couldn't get the file handle send an error message If iFh% = 0 Then psResponse$ = "451 System Failure - Cannot Open Message Buffer" Close maInBound(piConnectID%).iRcptFh% Exit Sub End If 'Save the file handle to the connection record maInBound(piConnectID%).iMsgFh = iFh% 'Send the OK response psResponse$ = "250 OK" Else psResponse$ = "451 System Failure - Cannot get buffers" End If Else psResponse$ = "501 Syntax Error" End If Else psResponse$ = "501 Syntax Error" End If End SubProcedure for the RCPT command:
Sub cmd_RCPT (ByVal iConnectID, ByVal sDataIn, sReturnString) On Local Error Resume Next 'This procedure processed the RCPT TO: Property... Dim sTemp$, iDMarker%, sDomain$ 'Check to see if the command is out of order.. If Not maInBound(iConnectID).bMailFrom Then sReturnString = "503 No Mail From sender specified." Exit Sub End If 'Check to see if this is an invalid command.. If UCase$(Left$(sDataIn, 8)) <> "RCPT TO:" Then sReturnString = "501 Syntax Error" Exit Sub End If 'Separate the argument sTemp$ = Trim$(Mid$(sDataIn, 9)) 'Make sure it is valid If Left$(sTemp$, 1) = "<" And Right$(sTemp$, 1) = ">" Then 'Ok check the mailbox... 'We will process the recipient address and let the distribution function handle 'the delivery... sTemp$ = Mid$(Left$(sTemp$, Len(sTemp$) - 1), 2) 'If the recipient is not null... If sTemp$ <> "" Then 'Save the recipient data Err = 0 Print #maInBound(iConnectID).iRcptFh%, sTemp$ 'If no error then send the ok response If Err = 0 Then maInBound(iConnectID).iRcptCnt = maInBound(iConnectID).iRcptCnt + 1 sReturnString = "250 Recipient OK" Else 'Otherwise send an error response sReturnString = "552 Error Processing Recipient - Cannot deliver to recipient at this time." End If Else sReturnString = "501 Syntax Error" End If Else sReturnString = "501 Syntax Error" End If End SubProcedure for the RSET command:
Sub cmd_RSET (ByVal piConnectID%, ByVal psDataIn$, psResponse$) 'Close the Files and reset them back to 0 Close maInBound(piConnectID%).iRcptFh, maInBound(piConnectID%).iMsgFh 'Reset the connection information... maInBound(piConnectID%).iRcptFh = 0 maInBound(piConnectID%).iMsgFh = 0 maInBound(piConnectID%).bMailFrom = False maInBound(piConnectID%).iRcptCnt = 0 Kill Trim$(gSettings.SMARTApps) + "\SMTP\IN\" + Trim$(maInBound(piConnectID%).sMsgID) + ".IN?" maInBound(piConnectID%).sMsgID = "" psResponse$ = "250 OK" End SubProcedure for the VRFY command:
Sub cmd_VRFY (ByVal piConnectID%, ByVal psDataIn$, psReturnString$) On Local Error Resume Next psReturnString$ = "551 VRFY command is currently not available." End SubI do not support the VRFY command in this implementation. However, you could by simply using this procedure to access whatever mailbox information exists on your local post office. However, you may not be able to do so if you have multiple post offices that the gateway supports. In any regard, however, the implementation is up to you.
In the next article, I will go over the SMTP Client functions and wrap it all up by going over the final product. At the same time, the code for the final project will be available for downloading and you can use that as the model. If you have any questions, you can send e-mail to me at john-r@vb-online.com.