[返回]
计算机世界2000年第5期

用WinSock实现POP3 客户程序

西安交通大学计算机系 刘明华

  一、POP3 协议简介

  POP3 服务中包含三个阶段,它们是“身份验证”(Authorization)、“事务处理”(Transaction)和“更新”(Update)。首先,客户和服务器建立一个TCP 连接(RFC 1939 规定POP3 服务器应该在110 端口监听),服务器会发回一条欢迎信息。之后,POP3 服务就进入了“身份验证”阶段。RFC 1939 定义了两种身份验证的方法:USER 和PASS 命令,以及APOP 命令;目前大多数POP3 服务器使用USER 和PASS 命令来检验用户身份的真伪性;APOP 方式则是对口令进行了加密,故而安全性有所增强。如果客户方通过了服务器的身份验证,POP3 服务便进入了Transaction 阶段。POP3 服务器将锁住相应的邮箱以便对其进行互斥访问,这样可以确保在此Transaction 中,邮箱中的邮件不会被修改和增删。在Transaction 阶段,客户可以向POP3 服务器以任意的顺序发送以下的命令:STAT(获取邮箱信息,可用来获取邮件的数目),LIST(获取邮件的信息),RETR(取某个邮件),DELE(给某个邮件加上删除标志,实际的删除操作在Update 阶段进行),NOOP(空命令,不做任何事)和RSET(将所有邮件的删除标志清除掉)。如果客户程序在Transaction 阶段向服务器发送QUIT 命令,那么就会进入Update 阶段,此时服务器将删除所有在Transaction 阶段用DELE 命令加上删除标志的邮件。不管删除操作成功与否,服务器都会关闭TCP 连接,并将邮箱解锁。

  二、登录POP3 服务器

  为了从POP3 服务器上收取Email,程序的第一步便是登录到POP3 服务器。为此,我们首先来实现一个名为POP3Login 的函数,该函数的原型为:
function POP3Login(Host, User, Password:String;
     Port:Integer=110):Integer;
  其中,Host 为POP3 服务器的主机名或者它的IP 地址,User 为用户名,Password 为该用户的密码,Port 为端口号(缺省值为110)。函数POP3Login 将与POP3 服务器在Port 端口建立TCP 连接,之后便向服务器发送USER 和PASS 命令。如果登录成功(即PASS 命令通过),则返回与POP3 服务器之间的Socket 描述符;否则,关闭TCP 连接,并返回INVALID_SOCKET,以表示登录失败。下面是POP3Login 函数的实现。

function POP3Login(Host, User, Password:String;
     Port:Integer=110):Integer;
var
 Sockfd:Integer;
begin
 Result:=INVALID_SOCKET;
 Sockfd:=CreateClientSocket(Host, Port);
 if (Sockfd=INVALID_SOCKET)
   or not POP3Response(sockfd)
 then begin
    CloseSocket(Sockfd);
    MessageBox(0, 'Cannot connect to server', nil,MB_ICONERROR);
    Exit;
    end;
 // 发送USER 命令
 Write_Socket(sockfd,'USER ' +User +#13 #10);
 if not POP3Response(sockfd)
 then begin
    CloseSocket(sockfd);
    MessageBox(0, 'USER failed', nil,
MB_ICONERROR);
    Exit;
    end;
 // 发送PASS 命令
 Write_Socket(sockfd,'PASS ' +Password +#13 #10);
 if not POP3Response(sockfd)
 then begin
    CloseSocket(sockfd);
    MessageBox(0, 'PASS failed', nil,
MB_ICONERROR);
    Exit;
    end;
 Result:=Sockfd;
end;

  三、收取邮件

  接下来,我们来编写一个名叫POP3RetriveMail 的函数,调用此函数就可以从POP3 服务器下载邮件。POP3RetriveMail 函数的原型为:
  function POP3RetriveMail(Host, User,

  Password:String;DeleteMail:Bool; Port:Integer=110):Integer;

  功能描述:从POP3 服务器上收取电子邮件。收取的邮件将保存在当前目录的MailBox 子目录下,返回值等于信箱中邮件的数目。

  参数说明:Host 为POP3 主机名或者其IP 地址,User 和Password 分别为用户名和口令,而布尔参数DeleteMail 指示函数在收取邮件后是否将其从服务器上删除。由于POP3 服务通常在110 端口,我们将参数Port 定义成缺省值为110 的缺省参数(default parameter)。

function POP3RetriveMail(Host, User, Password:String;DeleteMail:Bool;
Port:Integer=110):Integer;
var
 sockfd,i:integer;
 hFile, c:THandle;
 S, T:String;
begin
 Result:=0;
 Sockfd:=POP3Login(Host, User, Password, Port);
 if Sockfd=INVALID_SOCKET then Exit;
 //Get the number of mails in the maildrop
 //POP3 服务器给STAT 命令的返回信息的格式为:
 // +OK 邮件数目所有邮件总的字节数
 Write_Socket(sockfd,'STAT' #13 #10);
 S:=Socket_readline(sockfd);
 if UpperCase(Copy(S,1,3))<>'+OK'
 then begin //error occurred
    CloseSocket(sockfd);
    Exit;
    end;
 S:=Copy(S,5,Length(S) -4);
 i:=Pos('',S);
 S:=Copy(S,1,i -1);
 Result:=StrToIntDef(S,0);
 for i:=1 to Result do
 begin
  // 收取第i 封邮件
  S:='RETR ' +IntToStr(i) +#13 #10;
  Write_Socket(sockfd,S);
  // 如果RETR 的返回信息的第一行是+OK 的话,
  // 接下来的便是邮件的内容,邮件内容都是一行行的ASCII
字符串。
  // 当遇到一个只包含一个“." 的行时,意味着邮件内容已经
结束。
  if not POP3Response(sockfd) then continue;
  S:=GetUniqueFileName; // 自定义函数
  // 创建一个文件,用于保存邮件
  hFile:=CreateFile(PChar(s), GENERIC_WRITE,
     FILE_SHARE_READ, nil, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,
0);
  SetFilePointer(hFile,0,nil,FILE_BEGIN);
  T:=Socket_ReadLine(sockfd);
  While T<>'.' do
   begin
   T:=T +#13 #10;
   WriteFile(hFile,PChar(T)^,Length(T),C,nil);
   T:=Socket_ReadLine(sockfd);
   end;//while
  CloseHandle(hFile);
  // 如果参数DeleteMail 为TRUE 则从服务器上删除此邮件
  if DeleteMail then Write_Socket(Sockfd,'DELE ' +IntToStr(i) +#13 #10);
 end; //for
 // 发送QUIT 命令,使已进行了的DELE 操作生效
 Write_Socket(sockfd,'QUIT' #13 #10);
 // 关闭Socket(释放Socket 描述符)
 Closesocket(sockfd);
end;

  四、其他函数的的实现

  在以上的POP3Login 函数和POP3RetriveMail 函数中我们调用了一些自定义函数,下面是它们的实现代码。
  *CreateClientSocket 函数

function CreateClientSocket(Host:string;Port:integer):Integer;
// 功能:与指定的主机Host 建立一个TCP 连接,使用Port 端口。
// 返回值:如果成功返回一个Socket 描述符;否则返回
INVALID_SOCKET。
var
 i:integer;p:^LongInt;
 phe:pHostEnt;
 sin:sockaddr_in;
begin
 Result:=INVALID_SOCKET;
 sin.sin_family:=AF_INET;
 sin.sin_port:=htons(Port);
 // 将主机名转换为32 位的IP
 phe:=gethostbyname(pchar(host));
 if phe<>nil
 then begin
    p:=Pointer(phe^.h_addr_list^);
    sin.sin_addr.s_addr:=p^;
    end
 else begin
    i:=inet_addr(PChar(Host));
    if i<> -1
    then sin.sin_addr.S_addr:=i
    else begin
      // 无法获取主机Host 的IP
      MessageBox(0, pChar('Cannot resolve ' +Host), nil, MB_ICONERROR);
      Exit;
      end;
    end;

 // 创建一个面向连接的字节流Socket
 Result:=socket(PF_INET,SOCK_STREAM,0);
 if (Result=INVALID_SOCKET)
 then begin
    MessageBox(0,'socket() failed', nil,
MB_ICONERROR);
    Exit;
    end;
 // 使用此Socket 描述符与远处的主机建立一个TCP 连接
 if Connect(Result,sin,sizeof(sin))=SOCKET_ERROR
 then begin
    MessageBox(0,'connect() failed', nil,
MB_ICONERROR);
    closesocket(Result);
    Result:=INVALID_SOCKET;
    end;
end;
  *POP3Response 函数

function POP3Response(Sockfd:Integer):Bool;
// 功能:检查POP3 服务器返回的状态信息。
// 返回值:如果是+OK,则返回TRUE;否则返回FALSE。
var
 S: string;
begin
 S:=socket_readline(sockfd);
 if copy(s,1,3)='+OK' then Result:=True
 else Result:=False;
end;
*Write_Socket 函数
function Write_Socket(sockfd:TSocket; const s:string):Integer;
// 功能:将字符串S 写入sockfd
begin
 Result:=Send(sockfd,pointer(s)^,Length(s),0)
end;
*Socket_Readline 函数
function Socket_Readline(sockfd:Integer):String;
// 功能:从sockfd 中读取一行(即,直至遇到换行符)。
// 返回值:返回从sockfd 中所读取的一行字符。
var
 S:String; buf:array[0..1]of Char;
 n:Cardinal;
begin
 buf[0]:= #0;buf[1]:= #0; S:='';
 n:=recv(sockfd,Buf,1,0);
 while n>0 do begin
      buf[1]:= #0;
      S:=S +buf;
      if (buf[0]= #10) then Break;
      n:=recv(sockfd, buf, 1, 0);
     end;
 Result:=Trim(S);
end;

  示例程序

  下面是一个简单的示例程序,它调用POP3RetriveMail 函数收
取263.net 上simon_liu 的邮件。

Program GetMail;
uses WinSock, Windows, SysUtils,
   POP3 in 'POP3.pas';
var
 w:TWSADATA; I:integer; S:String;
begin
// 初始化Windows Sockets 动态连接库
if WSAStartup(2,w)<>0
then begin
   MessageBox(0,'Initialization failed!','WinSock DLLError',MB_ICONERROR);
   Halt;
   end;

i:=POP3RetriveMail('263.net', 'simon_liu', 'mypass', TRUE, 110);
// 或者:i:=POP3RetriveMail('263.net', 'simon_liu', 'mypass', TRUE, 110);
if i=0 then MessageBox(0,'No Message!','GetMail',MB_ICONINFORMATION)
else begin
   S:=IntToStr(i) +‘messages!';
   MessageBox(0,PChar(S),'New Message(s)!',MB_ICONINFORMATION);
   end;
end.
  说明:以上代码均使用Delphi 5 编写;调试环境:Windows
98/Windows 2000。