Solved

Indy: POP3Server

Posted on 2004-09-14
7
643 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
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
  • 3
  • 3
7 Comments
 
LVL 10

Expert Comment

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

Author Comment

by:graga
ID: 12061705
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
ID: 12063255
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
Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
LVL 10

Assisted Solution

by:Jacco
Jacco earned 100 total points
ID: 12070375
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
ID: 12070384
Messages should be with "txt" extension (its coded that way...)
0
 

Author Comment

by:graga
ID: 12070999
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
ID: 12071053
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

Industry Leaders: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Introduction The parallel port is a very commonly known port, it was widely used to connect a printer to the PC, if you look at the back of your computer, for those who don't have newer computers, there will be a port with 25 pins and a small print…
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…
Come and listen to Percona CEO Peter Zaitsev discuss what’s new in Percona open source software, including Percona Server for MySQL (https://www.percona.com/software/mysql-database/percona-server) and MongoDB (https://www.percona.com/software/mongo-…
There are cases when e.g. an IT administrator wants to have full access and view into selected mailboxes on Exchange server, directly from his own email account in Outlook or Outlook Web Access. This proves useful when for example administrator want…

705 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