Solved

Indy: POP3Server

Posted on 2004-09-14
7
630 Views
Last Modified: 2010-04-05
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
0
Comment
Question by:graga
  • 3
  • 3
7 Comments
 
LVL 10

Expert Comment

by:Jacco
Comment Utility
What backend do you want to use? Where/how do you want to store the user credentials and messages?
0
 

Author Comment

by:graga
Comment Utility
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
 
LVL 1

Accepted Solution

by:
NickRyan earned 150 total points
Comment Utility
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
What Should I Do With This Threat Intelligence?

Are you wondering if you actually need threat intelligence? The answer is yes. We explain the basics for creating useful threat intelligence.

 
LVL 10

Assisted Solution

by:Jacco
Jacco earned 100 total points
Comment Utility
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
 
LVL 10

Expert Comment

by:Jacco
Comment Utility
Messages should be with "txt" extension (its coded that way...)
0
 

Author Comment

by:graga
Comment Utility
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
 

Author Comment

by:graga
Comment Utility
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

Featured Post

Highfive Gives IT Their Time Back

Highfive is so simple that setting up every meeting room takes just minutes and every employee will be able to start or join a call from any room with ease. Never be called into a meeting just to get it started again. This is how video conferencing should work!

Join & Write a Comment

Creating an auto free TStringList The TStringList is a basic and frequently used object in Delphi. On many occasions, you may want to create a temporary list, process some items in the list and be done with the list. In such cases, you have to…
In my programming career I have only very rarely run into situations where operator overloading would be of any use in my work.  Normally those situations involved math with either overly large numbers (hundreds of thousands of digits or accuracy re…
Illustrator's Shape Builder tool will let you combine shapes visually and interactively. This video shows the Mac version, but the tool works the same way in Windows. To follow along with this video, you can draw your own shapes or download the file…
Get a first impression of how PRTG looks and learn how it works.   This video is a short introduction to PRTG, as an initial overview or as a quick start for new PRTG users.

744 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

12 Experts available now in Live!

Get 1:1 Help Now