Indy: POP3Server

I need a complete code example for a simple POP3 server with following requirements:
- server is installed on a server computer serving 50 workstations
- POP3 server validates users (user name and password are in local database)
- the mail messages are generated by the POP3 server (just a simple 'Hi ' + UserName will do)
thank you
gragaAsked:
Who is Participating?
 
NickRyanConnect With a Mentor Commented:
There is a Data property in the Thread object that is passed to all of the command events in the pop3 server. This is there so you can store relevant session data. You should create a suitable session object class which stores all of the information that you need, such as the user number, the database connection and anything else you might need. You should create this session object in the OnConnect event and free it in the OnDisconnect event. For example:

type
  TSessionObject = class(TObject)
    User_Number:integer;
  end; {class}

procedure TForm1.IdPOP3Server1Connect( AThread:TIdPeerThread );
begin
  AThread.Data := TSessionObject.Create;
end;

procedure TForm1.IdPOP3Server1Disconnect( AThread:TIdPeerThread );
begin
  AThread.Data.Free;
  AThread.Data := NIL;
end;

You always have access to this Thread object, however it's not passed directly to the command handlers, instead you need to access the thread object using code like this:

procedure TForm1.IdPOP3Server1LIST( ASender:TIdCommand; AMessageNum:Integer );
var
  SessionObject:TSessionObject;
begin
  SessionObject := TSessionObject(ASender.Thread);
  ...
end;

You next problem will be concurrent access to whatever database you're using to store the data in. The solution to this is that you must either serialise access to a shared database access object (or access code) or create a new connection to the database for each POP3 session. In this case, extend the session object to manage the individual database connections - for example:

TSessionObject = class(TObject)
  Database:TDatabase;
  User_Number:integer;
  constructor Create;
  destructor Destroy; override;
end; {class}

constructor TSessionObject.Create;
begin
  Database := TDatabase.Create( NIL );
  [connect to database]
end;

destructor TSessionObject.Destroy;
begin
  FreeAndNIL( Database );
end;

Now when you need to access the backend database, eg. in the command handler code, you can create the local query object, link it to the session's database object and do whatever you need to do. For example:

procedure TForm1.IdPOP3Server1LIST( ASender:TIdCommand; AMessageNum:Integer );
var
  SessionObject:TSessionObject;
  Query:TQuery;
begin
  SessionObject := TSessionObject(ASender.Thread);
  Query := TQuery.Create( NIL );
  try
    Query.Database := SessionObject.Database;
    Query.SQL.Text := 'select * from sometable where user_number = ' + IntToStr(Session.User_Number);
    [execute query and do your processing]
  finally
    Query.Free;
  end; {try}
end;

There are ways to optimise the code using pooled database connections and a certain amount of serialised access but the value of these optimisations will depend heavily on the number of simultaneous database connections your database can handle, the connection time and the number of users who are going to connect at exactly the same time.
0
 
JaccoCommented:
What backend do you want to use? Where/how do you want to store the user credentials and messages?
0
 
gragaAuthor Commented:
Jacco,

I will be using MySQL but for this purpose anything will do, MS Access probably will be the simplest for the prototype.
I have put together a prototype, but, being a newbie with Indy, I'm not sure how the threads work and how to make it work under multiuser load.

So, my idea of the prototype whould be something along these lines:

- User checks email from their email client with UserName and Password
  - this is done by setting POP3 server to 127.0.0.1 in each user's email client

- The POP3 server checks that UserName and Password exist in the user table
  - this is done in CheckUser event evaluating LThread - looking up the table returns some sort of key, let's say User_Number (integer)

- The POP server generates an email to that user but in the Body of the message embeded is UserName - this will ensure that the right message goes to the right recipient.
  - this is what I don't know how to do. How do I know in STAT, RETR and other command events what the User_Number is? What happens when all 50 users try to check email at the same time?

graga
0
Cloud Class® Course: Microsoft Windows 7 Basic

This introductory course to Windows 7 environment will teach you about working with the Windows operating system. You will learn about basic functions including start menu; the desktop; managing files, folders, and libraries.

 
JaccoConnect With a Mentor Commented:
Hi there,

I created a sample using a custom server side client thread. I implemented this simply using the file system (which is by default thread-safe). In the directory of the project there must be a file "Users.txt" with the following layout:

user1=pass1
user2=pass2

In subdirectories for each user (with the users name) there can be any number of message files.

!! Make sure Pop3 is not active in the IDE (otherwise the user defined thread class will not be used) !!

I tested this server sample with Outlook and it worked fine :)

If you want to use a DataBase backend you need to make a connection inside the thread. (If you use the BDE this means creating TSession/TDatabase/TQuery components). For MySQL I don't know what you need exactly).

Have fun!! Let me know if this is what you need.

Regards Jacco

P.S. You could optimize not reading the user file on every login. But reading it in only once would not handle alterations of the file.

*** start of code ***
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, IdBaseComponent, IdComponent, IdTCPServer, IdPOP3Server, SyncObjs;

type
  TForm1 = class(TForm)
    Pop3: TIdPOP3Server;
    procedure FormCreate(Sender: TObject);
    procedure Pop3CheckUser(AThread: TIdPeerThread; LThread: TIdPOP3ServerThread);
    procedure Pop3STAT(ASender: TIdCommand);
    procedure Pop3LIST(ASender: TIdCommand; AMessageNum: Integer);
    procedure Pop3RETR(ASender: TIdCommand; AMessageNum: Integer);
    procedure Pop3DELE(ASender: TIdCommand; AMessageNum: Integer);
  private
    fUserVerificationSection: TCriticalSection;
  public
    function VerifyUserLogin(const aUserName, aPassword: string): Boolean;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type
  TPop3Thread = class(TIdPOP3ServerThread)
  private
    fMessages: TStringList;
    fTotal: Integer;
  public
    constructor Create(ACreateSuspended: Boolean = True); override;
    destructor Destroy; override;
    procedure GetMessages;
  end;

{ TPop3Thread }

constructor TPop3Thread.Create(ACreateSuspended: Boolean);
begin
  inherited Create(ACreateSuspended);
  fMessages := TStringList.Create;
end;

destructor TPop3Thread.Destroy;
begin
  fMessages.Free;
  inherited Destroy;
end;

procedure TPop3Thread.GetMessages;
var
  lSR: TSearchRec;
begin
  fTotal := 0;
  if FindFirst(Username + '\*.txt', faAnyFile, lSR) = 0 then
  begin
    repeat
      fMessages.AddObject(Username + '\' + lSR.Name, Pointer(lSR.Size));
      Inc(fTotal, lSR.Size);
    until FindNext(lSR) <> 0;
    FindClose(lSR);
  end;
end;

{ TForm1 }

procedure TForm1.Pop3CheckUser(AThread: TIdPeerThread; LThread: TIdPOP3ServerThread);
begin
  if VerifyUserLogin(LThread.Username, LThread.Password) then
  begin
    LThread.State := Trans;
    TPop3Thread(AThread).GetMessages;
  end;
end;

function TForm1.VerifyUserLogin(const aUserName, aPassword: string): Boolean;
var
  lUsers: TStringList;
begin
  Result := False;
  lUsers := TStringList.Create;
  try
    lUsers.LoadFromFile('Users.txt');
    Result := lUsers.Values[aUserName] = aPassword;
  finally
    lUsers.Free;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Pop3.ThreadClass := TPop3Thread;
  Pop3.Active := True;
end;

procedure TForm1.Pop3STAT(ASender: TIdCommand);
begin
  with TPop3Thread(ASender.Thread) do
    Connection.WriteLn('+OK ' + IntToStr(fMessages.Count) + ' ' + IntToStr(fTotal));
end;

procedure TForm1.Pop3LIST(ASender: TIdCommand; AMessageNum: Integer);
var
  liMessage: Integer;
begin
  with TPop3Thread(ASender.Thread) do
  begin
    Connection.WriteLn('+OK ' + IntToStr(fMessages.Count) + ' messages (' + IntToStr(fTotal) + ' octets)');
    for liMessage := 0 to fMessages.Count-1 do
      Connection.WriteLn(IntToStr(Succ(liMessage)) + ' ' + IntToStr(Integer(fMessages.Objects[liMessage])));
    Connection.WriteLn('.');
  end;
end;

procedure TForm1.Pop3RETR(ASender: TIdCommand; AMessageNum: Integer);
var
  lMessage: TStringList;
  liLine: Integer;
begin
  with TPop3Thread(ASender.Thread) do
  begin
    if AMessageNum <= fMessages.Count then
    begin
      Connection.WriteLn('+OK ' + IntToStr(Integer(fMessages.Objects[AMessageNum-1])) + ' octets');
      lMessage := TStringList.Create;
      try
        lMessage.LoadFromFile(fMessages[AMessageNum-1]);
        for liLine := 0 to lMessage.Count-1 do
          if lMessage[liLine] = '.' then
            Connection.WriteLn('..')
          else
            Connection.WriteLn(lMessage[liLine]);
          Connection.WriteLn('.');
      finally
        lMessage.Free;
      end;
    end else
      Connection.WriteLn('-Message ' + IntToStr(AMessageNum) + ' not found!');
  end;
end;

procedure TForm1.Pop3DELE(ASender: TIdCommand; AMessageNum: Integer);
begin
  with TPop3Thread(ASender.Thread) do
  begin
    if AMessageNum <= fMessages.Count then
    begin
      DeleteFile(fMessages[AMessageNum-1]);
    end;
  end;
end;

end.
*** end of code ***
0
 
JaccoCommented:
Messages should be with "txt" extension (its coded that way...)
0
 
gragaAuthor Commented:
Thank you. Now it's all clear.
I have accepted Nicks answer, but Jacko has also put a lot of work in and I will use his code for other purpose. You guys are champions!
0
 
gragaAuthor Commented:
I have also posted another Indy question a couple of days ago which is worth 500 points. You may want to have a look.
Thanks
0
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

All Courses

From novice to tech pro — start learning today.