Visual Basic Online
WRITING YOUR OWN SMTP GATEWAY
by M. John Rodriguez (john-r@vb-online.com)
October, 1995

In the last article, I discussed the various preparation work that needed to be done in order to create the SMTP Gateway and what types of research needed to be accomplished. Hopefully, by now, you have read all the literature you need in order to understand what SMTP means and how to design the right prototype to fit your needs. In this article, I will go over construction of the SMTP server portion that receives messages from remote hosts.

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.

Considerations in Development

The SMTP server development will depend on a number of factors. These factors range from the types of tools that are being used in the development to how closely the program will follow RFC-821 and the modifications laid out in RFC-1123. They will also include modifications, if needed, for extensions as defined in RFC-1651 and other standards. The other considerations were referred to in the last article. Here are the considerations for the development of the SMTP server:
  1. SMTP Server will follow the implementation set forth in RFC-821 with the following exceptions:
  2. 2. ESMTP extensions and any MIME processing will not be supported in initial iteration. Future iterations will provide support for ESMTP and the application will need to be developed to allow for that.
  3. 3. We are using DevSoft's IPDAEMON.VBX for the daemon functionality. Because of the way the VBX is designed, we will have 32 simultaneous connections available for which we need to depend heavily on event driven coding.
  4. 4. SMTP Gateway will need to work on a minimum platform of a 386DX-40 with 8 MB RAM on a high traffic LAN. Flow control of incoming data will be necessary.
  5. 5. In our design, all data that corresponds to the server connections be placed into one module so as to occupy its own data segment. This means we will use strictly modular-scope variables for our procedures. This allows us to maintain some modularity for maintenance and upgrade.
These are the five basic considerations for our SMTP implementation. Now that we (should) have read the RFC materials, it is now time to begin our development of the SMTP server itself.

Writing the code

At this time, we need to create some components in our project. Because the IPDaemon control supports multiple inbound connections, we also might need to consider if we need to display anything about the connections. To this end, the project will have an MDI (multiple document interface) form as the main form. The name of this MDI form will be frmMain. We will also need another form called frmLog, in which a new instance is created for each connection that shows the communications between both hosts. This has two benefits. First, we can visually display the connection and see the protocol in action. This will be more visual to the user of what connections are coming in and simply by opening the form, the user can see the communication of both hosts. Second, we don't have to worry about keeping a continuous log. The idea here, is to show the communication between hosts in one form only, so as to monitor each connection separately and use it only as we need it. Next we add the I PDaemon control to the MDI form and set its name to tcpDaemon. No particular reason to name it as such, only that when we reference the control, we should make it something that is independent if you are using a control from a different vendor. The only other property we need to set is the Port property - it needs to be set to port 25 for the SMTP port. There are no other properties to set except for the Listening property, which should be set to true in your program's initial startup once any other initialization processes are completed.

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 Type
Each 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 buffer
The 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.

tcpDaemon Code

In the case of the IPDaemon control, there are 5 events that are available to us. Of those events, we are only going to use three. The first event we write code for is the Connected event shown below.
Sub tcpDaemon_Connected (ConnectionID As Integer, StatusCode As Integer, Description As String)

InitInBoundConnection ConnectionID, tcpDaemon.RemoteHost(ConnectionID)

End Sub
This 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 Sub
In 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 Sub
The 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.

SMTP_IN.BAS Code

In this module, you should have already included the declarations of the variables and the user-defined types previously listed. In this module, we will write the code that does all of the processing of the data.

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 Sub
The 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 Sub
As 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 Sub
In 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 Sub
In 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 Sub
This 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         False
Then, add this code to the InTimer_Timer event:
Sub InTimer_Timer()

UpdateInConnections

End Sub
Then add the following code to the InitInBoundConnection() procedure:
frmMain.InTimer.Enabled = True
Now 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 Sub
However, 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.

Additional Code

The following is the additional code used in the SMTP_IN.BAS file that is called by the InBoundRemoteCmd procedure:

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 Sub
Procedure 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 HELP "
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
Procedure for the MAIL command:
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 Sub
Procedure 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 Sub
Procedure 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 Sub
Procedure 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 Sub
I 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.

Conclusion

In this article, I discussed the major components and layout of code necessary to implement the SMTP Server side of the gateway. You should be able to follow the code closely and get a very good idea of how it works. While I have made decisions to forego some of the functionality in some areas, I have written this in such a way for you to be able to add those features back in quickly and easily. You should also be able to add additional support for SMTP Extensions (ESMTP) and other functionality as well.

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.


Click here to go back to the October '95 Article Index