Creating a WebSnap Email Application

by Corbin Dunn

We have all seen applications that allow you to check your email on a web page. This document gives step-by-step instructions on how to create a WebSnap application that allows you to check your email from a web page.

First, select File->New->Other... and from the WebSnap tab select WebSnap Application.

For ease of debugging, select a Web App Debugger executable, and give it a CoClass Name of WebMail. Press the Components... button and check the following:

For the End User Adapter, select the TEndUserSessionAdapter. Leave all the others with the defaults. Close the dialog with the OK button. Rename the Page Name to be MainModule and then close the New WebSnap Application dialog with its OK button.

Select File->New->Other... and from the WebSnap tab select WebSnap Page Module to create another page. Name it LoginModule and uncheck Published. Create another page, naming it ViewMailModule, but leave Published checked and also check Login Required. Finally, create one more page named EmailMessageModule, unchecking the Published option.

Select File->Save All and save the project into a new folder of your choice. Save Unit5.pas (EmailMessageModule) as EmailMessageMod.pas, Unit4.pas (ViewMailModule) as ViewMailMod.pas, Unit3.pas (LoginModule) as LoginMod.pas, Unit2.pas (MainModule) as MainMod.pas, Unit1.pas (Form1) as MainFrm.pas and Project1.dpr (Project1) as WebMailApp.dpr.

The first thing to do is to create the Login page. On the LoginModule, place a TLoginFormAdapter. We want to replace the PageProducer with a TAdapterPageProducer, so select PageProducer and delete it. Place a TAdapterPageProducer on the LoginModule. Click anywhere on the LoginModule make sure that the PageProducer property in the Object Inspector now says AdapterPageProducer1. Double-click on the LoginFormAdapter1 to bring up the Fields editor. Right click in the right-hand pane and select Add all fields.

Double-click on the AdapterPageProducer1 to bring up the web page editor, and then right-click on the AdapterPageProducer1 and select New Component.... Add an AdapterForm. Select the AdapterForm1, right-click on it, and select New Component... to add an AdapterFieldGroup. Select the AdapterFieldGroup1 and in the Object Inspector set the Adapter property to be LoginFormAdapter1. Now, right click on the AdapterForm1 and select New Component... to add an AdapterCommandGroup. Select the AdapterCommandGroup1 and set the DisplayComponent property to be AdapterFieldGroup1. Next, right click on the AdapterForm1 and from New Component... select AdapterErrorList. Then, select the AdapterErrorList1 and set the Adapter to be the LoginFormAdapter1. If you want errors to appear on top of everything else, drag the AdapterErrorList1 to the top of the list.

We now have to make the associated HTML page display the information on the LoginFormAdapter1. To do this, select the LoginMod.html file from the tabs at the bottom of the Code Editor. Before the closing BODY tag, add:
<#SERVERSCRIPT> If you take a look at the preview, you should see the edit boxes to login with.

Now, add a TIdPop3 component from the Indy Clients tab to the ViewMailModule and rename it to be popEmail. Set the Host property of the popEmail component to be your email server's POP3 hostname (such as, mail.your-host.com). Under the function ViewMailModule in the interface, add the following constants:

const
  cUserName = 'UserName';
  cPassword = 'Password';
Then, in the LoginModule's LoginFormAdapter1, double-click on the OnLogin event and add the code in the event handler:
procedure TLoginModule.LoginFormAdapter1Login(Sender: TObject;
  UserID: Variant);
begin
  // Try to log them in and verify the username/pass are correct
  with ViewMailModule do
  begin
    if popEmail.Connected then
    begin
      try
        popEmail.Disconnect;
      except
      end;
    end;
    if AdaptUserName.ActionValue <> nil then
      popEmail.UserId := AdaptUserName.ActionValue.Values[0]
    else
      raise Exception.Create('No user name passed from web page!');

    if AdaptPassword.ActionValue <> nil then
      popEmail.Password := AdaptPassword.ActionValue.Values[0]
    else
      raise Exception.Create('No password passed from web page!');

    // Try to connect, which will test the username and password
    // against the mail server
    try
      popEmail.Connect;                
    finally
      try
        popEmail.Disconnect;
      except
      end;
    end;
    // On a successful login, save the users information in a session.
    WebContext.Session.Values[cUserName] := popEmail.UserId;
    WebContext.Session.Values[cPassword] := popEmail.Password;
  end;
end;

You will have to add ViewMailMod to the uses list to make it compile.

Now that the login page is completed, we need to tell the application to use this login page. Select the EndUserSessionAdapter from the MainModule and in the Object Inspector set the LoginPage property to be LoginModule (you will have to type in the string).

Next, open back up the ViewMailModule. Add a TIdMessage (from the Indy Misc tab) to the ViewMailModule and name it msgEmail. We want to expose the email headers to the web page and use Javascript to iterate through all the messages. First, lets tackle the iteration part. Add the following to the private section of the TViewMailModule:

  private
    { Private declarations }
    FMessageId: Integer;
    FMessageCount: Integer;
    FMessageSubject: string;
    FMessageFromName: string;
    FMessageFromEmail: string;
    FMessageDate: string;
    FMessageSize: Integer;
These private data fields will allow us to keep track of what message we are on, and all the associated items in that message.

Next, place a TAdapter on the ViewMailModule and rename it to be EmailAdapter. Select the EmailAdapter and double-click on the OnIterateRecords event. Add the following code, allowing us to iterate through all the emails:
procedure TViewMailModule.EmailAdapterIterateRecords(Sender: TObject;
  Action: TIteratorMethod; var EOF: Boolean);

  procedure FillInMessageInfo;
  begin
    msgEmail.Clear;
    popEmail.RetrieveHeader(FMessageId, msgEmail);
    FMessageSubject := msgEmail.Subject;
    FMessageFromName := msgEmail.From.Name;
    FMessageFromEmail := msgEmail.From.Address;
    FMessageDate := DateTimeToStr(msgEmail.Date);
  end;

begin
  if Action = itStart then
  begin
    // Connect to the server, using the user name and password stored
    // in the session.
    if popEmail.Connected then
    begin
      try
        popEmail.Disconnect
      except
      end;
    end;
    popEmail.UserId := WebContext.Session.Values[cUserName];
    popEmail.Password := WebContext.Session.Values[cPassword];
    popEmail.Connect;
    // Find out the message count
    FMessageCount := popEmail.CheckMessages;
    FMessageId := 0;
    EOF := FMessageCount = 0;
  end
  else if Action = itNext then
  begin
    Inc(FMessageId);
    EOF := FMessageId >= FMessageCount;
  end
  else
    EOF := True;

  if not EOF then
    FillInMessageInfo;
end;
Notice how a different code is done depending on what Action the iterator is going through.

Double-click on the EmailAdapter, right-click on the right-hand pane and add the following AdapterField's named: MessageId, MessageSubject, MessageFromName, MessageFromEmail, MessageSize, MessageDate and MessageCount. Select MessageId from the list and double-click on the OnGetValue event in the Object Inspector. Add the following code:
procedure TViewMailModule.MessageIdGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageId;
end;

Repeat for the other AdapterFields, with using the respective private data member:

procedure TViewMailModule.MessageSubjectGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageSubject;
end;

procedure TViewMailModule.MessageFromNameGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageFromName;
end;

procedure TViewMailModule.MessageDateGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageDate;
end;

procedure TViewMailModule.MessageFromEmailGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageFromEmail;
end;

procedure TViewMailModule.MessageSizeGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageSize;
end;

procedure TViewMailModule.MessageCountGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageCount;
end;

The AdapterFields are what allows you to access your data with Javascript from an HTML page.

Now, we need the ability to allow the user to view a particular email message. We will create an Action to do this. Select the EmailAdapter and click on the Actions property in the Object Inspector to bring up the Actions list editor. Click the New Item button and add a new AdapterAction. Rename it to GetEmail. We need to add a parameter of what email message that the GetEmail action should be associated with. In the Object Inspector's Events for the GetEmail AdapterAction, double-click on the OnGetParams event and add the following code:

procedure TViewMailModule.GetEmailGetParams(Sender: TObject;
  Params: TStrings);
begin
  Params.Values['id'] := IntToStr(FMessageId);
end;
Now, we need to specify what happens when this action is invoked. Double-click on the OnExecute event and add the following code:
procedure TViewMailModule.GetEmailExecute(Sender: TObject;
  Params: TStrings);
begin
  if (Params.Values['id'] <> '') and (WebContext.EndUser.LoggedIn) then
    DispatchPageName(EmailMessageModule.Name, Response, []);
end;
The DispatchPageName will return a Response with the EmailMessageModule page. Since the EmailMessageModule is not Published, this will be the only way to access this page. To make this compile, you will have to add EmailMessageMod and WebDisp to your uses list for the ViewMailMod unit.

Now, we want to add Javascript that interacts with the EmailAdapter. Open the ViewMailMod.html page by clicking on the tab at the bottom of the Code Editor. We can write javascript accessing any TAdapter's Data or Actions with things such as: AdapterName.AdapterFieldName.Value or AdapterName.AdapterFieldName.DisplayText. Select the ViewMailMod.html tab at the bottom of the code editor and add the following HTML just above the closing BODY tag:

<%
  // Display server-side errors that may have occured.
  errors = new Enumerator(EmailAdapter.Errors)
  if (!errors.atEnd())
   Response.Write('<p>The following error(s) happened:');
  for (; !errors.atEnd(); errors.moveNext())
    Response.Write("<li>" + errors.item().Message)

  // The call to the EmailAdapter's records starts the iteration,
  // and fills in the MessageCount
  emails = new Enumerator(EmailAdapter.Records);
  if (EmailAdapter.MessageCount.Value > 0)
  {
    var i = 0;
%>
<p>You have <%=EmailAdapter.MessageCount.DisplayText%> messages.<br>

<center>
<table width="90%">
<tr>
  <td bgcolor="#DCDCDC"><font
    face="Arial, Helvetica" size="2"><b>Id</b></font></td>
  <td bgcolor="#DCDCDC"><font
    face="Arial, Helvetica" size="2"><b>Message</b></font></td>
  <td bgcolor="#DCDCDC"><font
    face="Arial, Helvetica" size="2"><b>Date</b></font></td>
  <td bgcolor="#DCDCDC"><font
    face="Arial, Helvetica" size="2"><b>Size</b></font></td>
</tr>

<%
    for (;!emails.atEnd(); emails.moveNext())
    {
      Response.Write('<tr>');
      var color; // Color every other row
      if (i % 2 == 0)
        color = 'bgcolor="#DCDCDC"';
      else
        color = '';

        
      Response.Write('<td ' + color + '>' +
        EmailAdapter.MessageId.DisplayText + '</td>\n');
      // For the message subject, create a link to the EmailMessageModule
      // to display that message.
      Response.Write('<td ' + color + '><b>Subject:</b> ' +
        '<a href="' + EmailAdapter.GetEmail.AsHREF + '">' +
        EmailAdapter.MessageSubject.DisplayText + '</a><br>');

      Response.Write(
        '<b>From:</b> ' + EmailAdapter.MessageFromName.DisplayText +
        ' &lt;<a href="mailto:' +
        EmailAdapter.MessageFromEmail.DisplayText +
        '">' + EmailAdapter.MessageFromEmail.DisplayText + '</a>&gt;' +
        '</td>\n');
      Response.Write('<td ' + color + '>' +
        EmailAdapter.MessageDate.DisplayText + '</td>\n');
      Response.Write('<td ' + color + '>' +
     EmailAdapter.MessageSize.DisplayText + '</td>\n');

      Response.Write('</tr>');
      i++;
    }

%>
</table>
</center>
<%
  }
  else
    Response.Write('<P>You have no messages waiting.<br>');
%>
Finally, we need to work on the EmailMessageModule to allow the user to display a particular message. Add a TAdapter to the EmailMessageModule and name it EmailMessage. Double-click on the EmailMessage Adapter and add the following new AdapterFields: StartMessage, MessageId, MessageFromName, MessageFromEmail, MessageSubject, MessageDate and MessageBody. Add the following to the private section of the EmailMessageModule:
    { Private declarations }
    FMessageId: Integer;
    FMessageSubject: string;
    FMessageFromName: string;
    FMessageFromEmail: string;
    FMessageDate: string;
    FMessageBody: string;
Select the StartMessage AdapterField and in the OnGetValue Event add the following code:

procedure TEmailMessageModule.StartMessageGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := False; // Failure

  if Request.QueryFields.Values['id'] <> '' then
  begin
    FMessageId := StrToInt(Request.QueryFields.Values['id']);

    with ViewMailModule do
    begin
      popEmail.UserId := WebContext.Session.Values[cUserName];
      popEmail.Password := WebContext.Session.Values[cPassword];
      popEmail.Connect;
      try
        msgEmail.Clear;
        msgEMail.NoDecode := True;
        popEmail.Retrieve(FMessageId, msgEmail);

        FMessageSubject := msgEmail.Subject;
        FMessageFromName := msgEmail.From.Name;
        FMessageFromEmail := msgEmail.From.Address;
        FMessageDate := DateTimeToStr(msgEmail.Date);
        FMessageBody := msgEmail.Body.Text;

        Value := True;
      finally
        try
          popEmail.Disconnect;
        except
        end
      end;
    end;
  end
  else
    raise Exception.Create('No valid message id passed to the EmailMessageModule');
end;
Be sure to add ViewMailMod to your lower uses list (in the implementation section) of the EmailMessageMod to make the code compile. Adding it to the upper uses list (the interface section) may give you a "circular reference" error.

With Javascript in the associated HTML page, we will access EmailMessage.StartMessage.Value to see if the user could sucessfully logon to the email server and retrive the requested email.

For the OnGetValue Events of the other TAdapterFields, add the following code:
procedure TEmailMessageModule.MessageIdGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageId;
end;

procedure TEmailMessageModule.MessageFromNameGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageFromName;
end;

procedure TEmailMessageModule.MessageFromEmailGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageFromEmail; 
end;

procedure TEmailMessageModule.MessageSubjectGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageSubject; 
end;

procedure TEmailMessageModule.MessageDateGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageDate;  
end;

procedure TEmailMessageModule.MessageBodyGetValue(Sender: TObject;
  var Value: Variant);
begin
  Value := FMessageBody;
end;

Finally, we have to add HTML to the EmailMessageMod.html page, above the closing BODY tag:
<P>
<%
  // First, see if we can view the message
  if (EmailMessage.StartMessage.Value)
  {
%>
<table width="100%">
<tr>
  <td bgcolor="#DCDCDC"><b>From:</b> <%=EmailMessage.MessageFromName.DisplayText%>
    <a href="mailto:<%=EmailMessage.MessageFromEmail.DisplayText%>"
      ><%=EmailMessage.MessageFromEmail.DisplayText%></a>
  </td>
  <td bgcolor="#DCDCDC"><b>Date:</b> <%=EmailMessage.MessageDate.DisplayText%></td>
</tr>
<tr>
  <td bgcolor="#DCDCDC" colspan="2"><b>Subject:</b> <%=EmailMessage.MessageSubject.DisplayText%></td>
</tr>
<tr>
  <td colspan="2">
<pre>
<%=EmailMessage.MessageBody.DisplayText%>
</pre>
  </td>
</tr>

</table>
<%
  }
  else
    Response.Write('Can not display the given email message id<br>');

  errors = new Enumerator(EmailMessage.Errors)
  if (!errors.atEnd())
   Response.Write('<p>The following error(s) happened:');
  for (; !errors.atEnd(); errors.moveNext())
    Response.Write("<li>" + errors.item().Message)    
%>

Run the application by pressing F9. Start the Web App Debugger and click on the default URL. Select WebMailApp.WebMail from the list. You should now be able to login to your email account and check your messages. Note that the WebMailApp.exe must stay running to keep the session active.