首页 > 代码库 > 邮件服务器编程

邮件服务器编程

刚开始做邮件服务器开发,一切都是茫然的。在书上网上都很难找到一套完整的邮件服务器开发教程。在个人的摸索中碰到了很多蛋疼得问题。现终于完成了,将我的开发经验分享给大家。


 

开发环境:vs2012 mfc

 

注意事项:

1、 网络环境:

作为邮件服务器,要接收来自互联网的邮件,就得有能映射到外网的服务器。至少映射25(SMTP)端口(pop3都暂时不重要)。对于没有外网条件的的小伙伴,推荐以下几种方法调试:

A、如果你使用model上网,查看你的电脑的外网IP。看看是否和model的一致。一致则说明你有一个公网IP。如果没有,就致电网络运营商,叫他给你换回公网IP,语气一定要强硬。有了公网IP,你就能把smtp的端口映射出去。具体方法请搜索。

B、对于一些校园网或大型局域网的用户,那就有点悲剧了,基本无法直接将端口映射出去。但可以参考C方法。

C、先在的网上的网络穿透都做的比较好,甚至能媲美公网ip的直接映射。去网上下载一些穿透软件:花生壳之类的。都有提供内网穿透。给大家推荐个免费的网络穿透软件:nat123 。这软件很好有,up主不经意的发现有大量的小学生用它在开MC的服务器。

D、如果你暂不需要使用外部的邮件服务器来测试你的服务器,那就在内网用其他的邮件服务器发送至你的邮箱进行测试,要注意端口冲突。

2、 编写smtp邮件服务器:

对于拥有外网映射的,可以先编写smtp 的接收服务器。这样的好处是,能接收识别它域的不同类型格式的邮件,到自己发送邮件的时候,对邮件格式能有一个很好的组织。 而对于只能使用内网的用户,建议先编写发送端。成功进行对各个邮件服务器的发送后(这里比较蛋疼,后面会介绍到),就可以在编写邮件接收端的时候用来测试。

3、 邮件服务器域名(MX)的获取:

最开始用telnet测试163的smtp。网上搜索各大邮件服务器的stmp服务器,当初搜索出来的结果把我着实误导了好一阵子。举个例子把,就常用的企鹅邮箱,搜索出来的结果是:smtp.qq.com  25 。



技术分享

于是傻傻地telnet上去helo他,他告诉我他是ehlo服务器,要登陆验证遇到这种问题真是想哭,我一个发邮件的我给你登陆什么啊,其他smtp.jbjb.com邮箱也是这样?与是想了各种奇葩的情况与方法浪费了很多时间。最后自己去抓了一个邮件服务器的向企鹅邮箱发送的包,首先是几个dns的包。看到有几个dns包,自己心理顿时想到了smtp前缀的域名不是接收外域邮件的域名,看了下dns到的地址:mx1.qq.com mx2.qq.com... 原来这才是接收邮件的服务器域名。这时候才明白自己以前挂的域名时也配置有MX地址。 总之,我们发邮件一定要先获取邮件接收服务器的域名。

4、 防止反垃圾邮件:

把邮件发往其他邮件服务器,很容易被识别为垃圾文件。新浪邮箱最讨厌,直接不信任我的域名和IP地址。最初邮件格式不完善,也被企鹅断断续续封了几天。就163最包容,我的邮件都慷慨地接收了。

现在发邮件,除了基本的格式,不用MINE根本不行,尤其是当需要图片或者附件的邮件。

5、 加密:

一般可用base64对邮件标题和内容进行加密。

6、 邮件存储:

建议储存为eml格式的邮件文件,不仅利于转发或其他应用查看,也便于之后的pop3服务器使用。

7、 web端cms

超麻烦的东西,找个模板改改吧。



 

先粗略讲讲SMTP协议发送邮件的过程:

直接举个例子:

R: 220 mx.jb.com MX JB Mail Sever

S: helo wchrt.vicp.cc

R: 250 OK

S: mail from: <jb@wchrt.vicp.cc>

R: 250 OK

S: rcpt to: <414322153@qq.com>

R: 250 OK

S: data

R: 354 End data with<CR><LF>.<CR><LF>

S: mail from: jb@wchrt.vicp.cc

S: rcpt to: 414322153@qq.com

S: subject: 你好

S: 约不约?

S: <CR><LF>.<CR><LF>

R: 250 OK in queue

S: QUIT

R: 221 close

 

 

该次对话中首先我们链接服务器后服务器给我们返回了220 和服务器的域名信息,表示该邮件服务器正常工作,可以提供服务。

然后我们发送helo过去标示自己服务器的域名。

服务收到后250说明和helo成功

之后是mailfrom 说明发件人的email地址

250 ok后再rcptto 说明发送目标的Email地址

成功后,就可以发送data命令发送邮件内容了。

Data返回的值说明邮件以<CR><LF>.<CR><LF>为结束标记。<CR><LF>这个标示符用换行符\r\n即可。

最后返回250 标示邮件接收成功。QUIT退出。

 

以下是服务器返回的一些编号:

501 参数格式错误

502 命令不可实现

503错误的命令序列

504 命令参数不可实现

211系统状态或系统帮助响应

214帮助信息

220 <domain>服务就绪

221 <domain>服务关闭传输信道

421 <domain>服务未就绪,关闭传输信道(当必须关闭时,此应答可以作为对任何命令的响应)

250要求的邮件操作完成

251用户非本地,将转发向<forward-path> 

450要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)

550要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)

451放弃要求的操作;处理过程中出错

551用户非本地,请尝试<forward-path> 

452 系统存储不足,要求的操作未执行

552 过量的存储分配,要求的操作未执行

553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误)

354 开始邮件输入,以<CRLF>.<CRLF>结束

554 操作失败

 

具体的内容请自行搜索smtp协议

 

 

大致清楚协议后就可以开始邮件接收端的编程,协议的细节的在调试过成功会慢慢清楚的。

服务器接收端代码:

UINT mailsever::listenthread(LPVOID Param)
{
	int ret;
	SOCKET listensock;//接收邮件请求的套接字
	listensock=socket(AF_INET,SOCK_STREAM,0);
	TRACE("listensock: %d\n",listensock);

	sockaddr_in saddr;
	saddr.sin_family=AF_INET;
	saddr.sin_port=htons(25);//25 smtp端口
	saddr.sin_addr.S_un.S_addr=INADDR_ANY;

	ret=bind(listensock,(sockaddr *)&saddr,sizeof(sockaddr_in));

	ret=listen(listensock,20);//一个小服务器,暂时就只设置20的链接上限

	that->isseverstart=true;
	CString str;
	that->GetDlgItemTextA(IDC_LOG,str);
	str+="收信服务开启\r\n";;
	that->SetDlgItemTextA(IDC_LOG,str);

	sockaddr_in newaddr;
	int newlen;
	while(1)
	{
		SOCKET newsock=accept(listensock,(sockaddr *)&newaddr,&newlen);//获取到新邮件请求的套接字
		if(newsock!=SOCKET_ERROR)
		{
			AfxBeginThread(dealthread,&newsock);//开启新线程接收邮件
		}
	}
	that->isseverstart=false;
	return 0;
}

static bool strcom(const char *s1,const char *s2)
{
	int len=strlen(s2);
	if(strlen(s1)<len)
	{
		return false;
	}
	int py=0;
	for(int i=0;i<len;i++)
	{
		if(s1[i]>=‘A‘&&s1[i]<=‘Z‘)
		{
			py=32;
		}
		else
		{
			py=0;
		}
		if(s1[i]+py!=s2[i])
		{
			return false;
		}
	}

	return true;
}

static bool isdataend(const char *ss)
{
	int len=strlen(ss);
	for(int i=0;i<len-2;i++)
	{
		if(ss[i]==‘\n‘&&ss[i+1]==‘.‘&&ss[i+2]==‘\r‘)
		{
			return true;
		}
	}
	return false;
}



UINT mailsever::dealthread(LPVOID Param)//邮件接收线程
{

	SOCKET sock=*(SOCKET *)Param;
	CString sdata;
	char rdata[2048];
	int rlen;
	
	sdata.Format("220 wchrt.vicp.cc smtp WC Mail Server\r\n");
	send(sock,LPCTSTR(sdata),sdata.GetLength(),0);//回答本服务器状态

	maildata rmail;//储存邮件的maildata类

	bool ishelo=false;
	bool ismail=false;
	bool isrcpt=false;
	bool isdata=http://www.mamicode.com/false;"helo")||strcom(rdata,"ehlo"))//处理helo请求
		{
			sdata.Format("250-wchrt.vicp.cc\r\n250 OK\r\n");
			ishelo=true;
		}
		else if(strcom(rdata,"mail from"))//处理邮件来源信息
		{
			int i=0;
			while(i<rlen&&rdata[i]!=‘:‘)
			{
				i++;
			}
			if(i<rlen)
			{
				rmail.from.Format("%s",rdata+i);
			}
			ismail=true;
			sdata.Format("250 OK\r\n");
		}
		else if(strcom(rdata,"rcpt to"))//处理邮件目的地信息(本地暂未按邮件用户区分,统一接收在一起)
		{
			int i=0;
			while(i<rlen&&rdata[i]!=‘:‘)
			{
				i++;
			}
			if(i<rlen)
			{
				rmail.to.Format("%s",rdata+i);
			}
			isrcpt=true;
			sdata.Format("250 OK\r\n");
		}
		else if(strcom(rdata,"data"))//处理data请求
		{
			if(!ismail||!isrcpt)
			{
				sdata.Format("503 is not mail or rcpt\r\n");
			}
			else
			{
				sdata.Format("354 end with <CRLF>.<CRLF>\r\n");
				isdata=http://www.mamicode.com/true;"quit"))//处理退出服务请求
		{
			isend=true;
			break;
		}
		else
		{
			if(isdata)//接收邮件内容
			{
				rmail.alldata+=rdata;
				if(isdataend(rdata))
				{
					rmail.alldata.Replace("\r\n.\r\n","\r\n");
					isdata=http://www.mamicode.com/false;"250 OK\r\n");
					isenddata=http://www.mamicode.com/true;"250 OK\r\n");
			}
		}
		send(sock,(LPCTSTR)sdata,sdata.GetLength(),0);//返回应答
	}

	// 开始处理并储存接收到的邮件,处理过程详见maildata.cpp
	//maildata::getmailinfo(rmail);
	CString mid;
	mid.Format("%d",that->mailid+1);
	if(maildata::saveeml("all",mid,rmail))
	{
		that->mailid++;
		that->mailnum++;
		CFile file;
		if(!file.Open("mail\\info",CFile::modeReadWrite))
		{
			if(!file.Open("mail\\info",CFile::modeCreate|CFile::modeReadWrite))
			{
				AfxMessageBox("makeinfo error");
				return false;
			}
		}
		CString str;
		str.Format("%d\r\n%d",that->mailid,that->mailnum);
		file.Write(str.GetString(),str.GetLength());
		file.Close();

		that->GetDlgItemTextA(IDC_LOG,str);
		str+="new mail\r\n";
		that->SetDlgItemTextA(IDC_LOG,str);
	}
	return 0;
}


邮件发送端:

 

举个例子,我们要往邮箱:414322153@qq.com 发送一个邮件。应遵循以下步骤:

1、提取出域名后缀:qq.com。

2、DNS查询该域名的MX记录。

3、根据查询到的MX域名或IP地址链接该邮件服务器

4、使用smtp协议发送邮件

 

因为要进行DNS查询,mfc没有提供关于dns查询的类。只好自己手动链接dns服务器根据dns协议查询mx记录。或者是调用windows自带的nslookup来查询dns。

为了防止大家一些接触太多东西,这里就给大家讲一下简单实用nslookup查询mx记录的方法。等有精力再去学习dns协议。

 

cmd输入:nslookup

将查询设置为mx:set q=mx

开始查询:qq.com

 

见截图:

技术分享



为方便起见,我们就不做服务器的连通测试,直接使用第一个MX地址发送邮件。

以下是发送邮件的代码:

static bool getres(SOCKET sock,const char *s,const char *s2=NULL)
{
	char *rdata=http://www.mamicode.com/new char [2048];"%s",rdata);
	that->GetDlgItemTextA(IDC_LOG,str);
	str+=rr+"\r\n";;
	that->SetDlgItemTextA(IDC_LOG,str);
	
	/*TRACE("%s\n",rdata);
	CString ss=rdata;
	AfxMessageBox(ss);*/

	if(!strcom(rdata,s))
	{
		if(s2!=NULL)
		{
			if(!strcom(rdata,s2))
			{
				return false;
			}
		}
		else
		{
			return false;
		}
	}
	return true;
}
UINT mailsever::sendthread(LPVOID Param)
{
	CString mpath;
	mpath.Format("%s",Param);
	//AfxMessageBox(mpath);
	CFile file;
	if(!file.Open(mpath,CFile::modeRead))
	{
		return -1;
	}

	maildata rmail;
	char s[20480];
	memset(s,‘\0‘,sizeof(s));
	file.Read(s,min(file.GetLength(),20470));
	rmail.alldata.Format(s);

	maildata::getmailinfo(rmail);

	rmail.gettoaddress();

	//AfxMessageBox(rmail.toaddress);
	
	
	//dns获取域名mx记录
	char *szDomainName= (char *)rmail.toaddress.GetString();
    std::vector<ULONG> veculIPList;
    std::vector<std::string> vecstrIPList;
    std::vector<std::string> vecMXList;
    ULONG ulTimeSpent = 0;
    CDNSLookup dnslookup;
	//使用114.114.114.144 dns服务
    BOOL bRet = dnslookup.DNSLookup(inet_addr("114.114.114.114"), szDomainName, &vecstrIPList, &vecMXList, 1000, &ulTimeSpent);
	if(!bRet)
	{
		return -1;
	}
	vecMXList[0].c_str();
	CString ss;
	ss.Format("%s",vecMXList[0].c_str());//获取第一条记录
	
	//AfxMessageBox(ss);

	SOCKET sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	sockaddr_in saddr;
	saddr.sin_family =AF_INET;
	saddr.sin_port=htons(25);
	saddr.sin_addr.S_un.S_addr=*(u_long *)gethostbyname(vecMXList[0].c_str())->h_addr_list[0];

	/*CString sd=inet_ntoa(saddr.sin_addr);

	AfxMessageBox(sd);*/

	if(SOCKET_ERROR==connect(sock,(sockaddr*)&saddr,sizeof(saddr)))
	{
		return -1;
	}

	

	CString sdata;
	

	if(!getres(sock,"220"))
	{
		return -1;
	}


	sdata.Format("HELO wchrt.vicp.cc\r\n");
	send(sock,sdata.GetString(),sdata.GetLength(),0);
	if(!getres(sock,"250"))
	{
		return -1;
	}


	sdata.Format("MAIL FROM: <%s>\r\n",rmail.from.GetString());
	send(sock,sdata.GetString(),sdata.GetLength(),0);
	if(!getres(sock,"250"))
	{
		return -1;
	}


	sdata.Format("RCPT TO: <%s>\r\n",rmail.to.GetString());
	send(sock,sdata.GetString(),sdata.GetLength(),0);
	if(!getres(sock,"250"))
	{
		return -1;
	}



	sdata.Format("DATA\r\n");
	send(sock,sdata.GetString(),sdata.GetLength(),0);
	if(!getres(sock,"2","3"))
	{
		return -1;
	}

	
	sdata=http://www.mamicode.com/rmail.alldata;"%d %d \n",i,sdata.GetLength());
		char bb[1025];
		int  j;
		for(j=0;j<min(strlen(sdata.GetString()+i),1024);j++)
		{
			bb[j]=(sdata.GetString()+i)[j];
		}bb[j]=‘\0‘;
		TRACE("%s",bb);
		CString ss=bb;
		AfxMessageBox(ss);*/

		send(sock,sdata.GetString()+i,min(strlen(sdata.GetString()+i),1024),0);
	}
	sdata.Format("\r\n\r\n.\r\n");
	send(sock,sdata.GetString(),sdata.GetLength(),0);
	if(!getres(sock,"2"))
	{
		return -1;
	}


	sdata.Format("QUIT\r\n");
	send(sock,sdata.GetString(),sdata.GetLength(),0);
	if(!getres(sock,"2"))
	{
		return -1;
	}
	AfxMessageBox("ok");
}


maildata类:

maildata.h

#pragma once
#define MAIL_TYPE_RECV           1
#define MAIL_TYPE_SEND           2
#define MAIL_TYPE_USEND          3

class maildata:public CObject
{
public:
	maildata(void);
	
	~maildata(void);

	USHORT type;
	CString alldata;

	CString date;
	CString from;
	CString to;
	CString subject;
	CString content;
	CString contenttype;

	CString toaddress;

	//void operator = (const maildata &);
	void Serialize(CArchive &);
	DECLARE_SERIAL(maildata);


	static void getmailbaseinfo(maildata &);
	static void getmailinfo(maildata &);
	static bool setmailforsend(maildata &);

	static bool openeml(const CString,const CString,maildata &);
	static bool saveeml(const CString,const CString,maildata &);
	bool gettoaddress();
};
maildata.cpp

#include "stdafx.h"
#include "maildata.h"

#include "include/atlrx.h"
#include "base64.h"

IMPLEMENT_SERIAL(maildata,CObject,VERSIONABLE_SCHEMA|2);
#ifdef _DEBUG
#define new DEBUG_NEW
#endif

maildata::maildata(void)
{
}


maildata::~maildata(void)
{
}


/*void maildata::operator = (const maildata &m)
{
}*/


void maildata::Serialize(CArchive &ar)
{
	if(ar.IsStoring())
	{
		ar<<alldata;
	}
	else if(ar.IsLoading())
	{
		ar>>alldata;
	}
}


static bool strcom(const char *s1,const char *s2)
{
	int len=strlen(s2);
	if(strlen(s1)<len)
	{
		return false;
	}
	int py=0;
	for(int i=0;i<len;i++)
	{
		if(s1[i]>=‘A‘&&s1[i]<=‘Z‘)
		{
			py=32;
		}
		else
		{
			py=0;
		}
		if(s1[i]+py!=s2[i])
		{
			return false;
		}
	}

	return true;
}
static CString* strmake(const char *st,const char *ed)
{
	if(ed<=st)
	{
		CString *str=new CString;
		*str="";
		return str;
	}
	char *data=http://www.mamicode.com/new char[ed-st+1];"%s",jb.c_str());
				
				rep[rlen++]=s[i++];
				rep[rlen++]=s[i++];
				rep[rlen]=‘\0‘;
				str.Replace(rep,jb.c_str());
			}
		}
		i++;
	}
}

void maildata::getmailbaseinfo(maildata &rmail)
{
	getcontent("Date: {.*?}\r\n",rmail.alldata,rmail.date);
	getcontent("From: {.*?}\r\n",rmail.alldata,rmail.from);
	getcontent("To: {.*?}\r\n",rmail.alldata,rmail.to);
	getcontent("Subject: {.*?}\r\n",rmail.alldata,rmail.subject);
	dealsbstr(rmail.from);
	dealsbstr(rmail.to);
	dealsbstr(rmail.subject);
}
void maildata::getmailinfo(maildata &rmail)
{
	getmailbaseinfo(rmail);
	getcontent("Content-Type: text/plain;.*?Content-Transfer-Encoding: {.*?}\r\n",rmail.alldata,rmail.contenttype);
	getcontent("Content-Type: text/plain;.*?\r\n\r\n{.*?}--",rmail.alldata,rmail.content);
	//AfxMessageBox(rmail.titletype+"\r\n"+rmail.title);
	rmail.content.Replace("\r\n","");
	if(strcom(rmail.contenttype.GetString(),"base64"))
	{
		std::string ss=rmail.content.GetString();
		ss=base64_decode(ss);
		rmail.content=ss.c_str();
		
	}
}

bool maildata::setmailforsend(maildata &rmail)//生成可用于发送的邮件内容
{
	if(rmail.from.GetLength()<1||rmail.to.GetLength()<1||rmail.subject.GetLength()<1)
	{
		return false;
	}

	rmail.type=MAIL_TYPE_USEND;
	rmail.alldata.Format("");
	
	CString str;

	/*str.Format("Date: Tue, 9 Dec 2014 11:20:55 +0800\r\n",
		rmail.from
		);
	rmail.alldata+=str;*/

	//格式化基本信息

	str.Format("From: %s\r\n",
		rmail.from
		);
	rmail.alldata+=str;

	str.Format("To: %s\r\n",
		rmail.to
		);
	rmail.alldata+=str;


	str.Format("Subject: =?GBK?B?%s?=\r\n",
		base64_encode((unsigned char *)rmail.subject.GetString(),rmail.subject.GetLength()).c_str()
		);
	rmail.alldata+=str;

	/*str.Format("X-Priority: 3\r\n");
	rmail.alldata+=str;
	str.Format("X-Mailer: wchrt‘s pro mail sever 1.0.0\r\n");
	rmail.alldata+=str;
	str.Format("X-Client-IP: 118.112.48.107\r\n");
	rmail.alldata+=str;*/

	//加入MIME格式的邮件内容

	str.Format("Content-Type: multipart/alternative;\r\n boundary=\"--=_Part=\"\r\n");
	rmail.alldata+=str;
	str.Format("MIME-Version: 1.0\r\n");
	rmail.alldata+=str;
	str.Format("\r\nThis is a multi-part message in MIME format.\r\n\r\n----=_Part=\r\n");
	rmail.alldata+=str;

	if(rmail.content.GetLength()>0)
	{
		str.Format("Content-Type: text/plain; charset=GBK\r\nContent-Transfer-Encoding: base64\r\n\r\n%s\r\n----=_Part=--\r\n",
			base64_encode((unsigned char *)rmail.content.GetString(),rmail.content.GetLength()).c_str()
			);
		rmail.alldata+=str;
	}

	//需要发送附件的话将文件读入后添加MIME部分即可

	AfxMessageBox(rmail.alldata);
}


bool maildata::openeml(const CString uname,const CString mid,maildata &rmail)//打开邮件
{
	CString mpath="mail";
	mpath+="\\";
	mpath+=uname;
	mpath+="\\";
	mpath+=mid+".eml";
	CFile file;
	if(!file.Open(mpath,CFile::modeRead))
	{
		return false;
	}
	char temp[40960];
	UINT len=file.Read(temp,40900);
	temp[len]=‘\0‘;
	rmail.alldata.Format(temp);
	//AfxMessageBox(rmail.alldata);
	return true;
}

bool maildata::saveeml(const CString uname,const CString mid,maildata &rmail)//邮件储存
{
	CString mpath="mail";
	if(!PathIsDirectory(mpath))
	{
		if(!CreateDirectory(mpath,NULL))
		{
			return false;
		}
	}
	mpath+="\\";
	mpath+=uname;
	if(!PathIsDirectory(mpath))
	{
		if(!CreateDirectory(mpath,NULL))
		{
			return false;
		}
	}
	mpath+="\\";
	mpath+=mid+".eml";

	CFile file;
	if(!file.Open(mpath,CFile::modeWrite))
	{
		if(!file.Open(mpath,CFile::modeCreate|CFile::modeWrite))
		{
			return false;
		}
	}
	file.Write(rmail.alldata.GetString(),rmail.alldata.GetLength());
	file.Close();
	return true;
}

bool maildata::gettoaddress()//获取后缀地址
{
	getcontent("@{.*}",to,toaddress);
	if(toaddress.GetLength()<1)
	{
		return false;
	}
	toaddress.Replace(" ","");
	return true;
}


基本的smtp邮件服务器就告一段落了。剩下的就是用户管理以及在smtp服务器基础上制作pop3服务器以及web等。


本文出自 “wchrt” 博客,请务必保留此出处http://wchrt.blog.51cto.com/8472636/1598077

邮件服务器编程