用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。