0%

简单验证邮件发送应用

avatar

背景

如今这样一个互联网迅速发展的时代,我们有时总是会需要注册不同的帐号以满足我们日常工作生活的需要。但是在注册时,不同的邮件验证码就成了注册中最为麻烦的一个步骤。从日常使用的角度来看,这样的设计简直就是”中秋夜里打灯笼——多此一举”。当然,切换角度从开发者的角度去度量,这些限制措施则是极有必要的。如发送验证码邮件,就是验证邮箱地址是否可用的一个有有效方法。每次获取验证码后,必须要间隔多少秒后才可以重新获取验证码。这是防止旧的验证码没有被使用就去请求新的验证码内容,从而请求过多导致服务器崩溃。那么这些限制又是依赖什么去实现的,随着应用的设计与完善,我们就可以解答这些问题了。

发送流程

为了实现这些限制,我们首先需要厘清发送邮件的流程。

终端发送

首先在终端使用telnet连接smtp.qq.com:25,来完成发送邮件的模拟:

  • 发送邮件
    avatar

  • 邮件接收
    avatar

分析流程

从以上的发送中,我们可以很轻松的提取出这个清晰的过程:

  1. 使用telnet连接到smtp.qq:25这个服务器上,即连接服务邮件服务器: telnet smtp.qq.com 25
  2. 发送EHLO命令,通知服务器: EHLO hello
  3. 由于qq邮箱需要密码验证,因此使用auth login命令: auth login
  4. 输入对应的邮件地址与密码
  5. 告知邮箱服务器发送地址: mail from: 1508498108@qq.com
  6. 告知邮箱服务器接收方地址 rcpt to: 15189121885@163.com
  7. 发送数据命令: data
  8. 指定主题: subject
  9. 换行后输入正文内容
  10. 输入quit,结束发送

提取需求

分析完整个流程后,发送邮箱的基本需求也就明了了。

  • 首先我需要连接到smtp.qq.com:25这个服务器上
  • 发送命令到服务器上
  • 接收服务返回的数据

在服务器连接上,我们使用socket进行连接。

  1. 使用socket去connect对应的ip:port
  2. 发送使用send()
  3. 接收使用recv()

基础实现

我们需要对外两个接口:

  1. initsocket 初始化连接
  2. SendVerificationCode 发送验证码

初始化socket

使用socket是为了连接QQ邮件服务器,这里作为我们仅是作为客户端,使用服务器来发送邮件的。但是对于socket而言,只接收ip与Port来进行初始化。因此,我们需要将smtp.qq.com这个域名进行有效的转化,才可以初始化socket。
我们可以定义一个private的函数: HostNameToIp(const std::string& hostname) 来进行域名与IP的转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
bool SendMail::initSocket(const std::string &Host, const int &Port) {
const std::string Ip = HostNameToIp(Host);
ser_sock = socket(AF_INET, SOCK_STREAM, 0);

if (ser_sock < 0) {
std::cout << "Server: create socket error!" << std::endl;
return false;
}

memset(&server_address, 0, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_port = htons(Port);
inet_pton(AF_INET, Ip.c_str(), &server_address.sin_addr);

int ret = 0;

ret = connect(ser_sock, (struct sockaddr*)&server_address, sizeof(server_address));
if (ret < 0) {
std::cout << "Server: connect failed! " << std::endl;
return false;
}

return true;
}

const std::string SendMail::HostNameToIp(const std::string& hostname) {
struct hostent* host;
std::string resIp = "";

// IP地址的正则匹配表达式
std::regex ip_match("^(?:[01]?\\d{1,2}|2(?:[0-4][0-9]|5[0-5]))(?:\\.(?:[01]?\\d{1,2}|2(?:[0-4][0-9]|5[0-5]))){3}$");
// 判断是否为IP,若是IP,则直接返回
if (std::regex_match(hostname, ip_match)) {
std::cout << "successfully";
return hostname;
}
host = gethostbyname(hostname.c_str());

if (host->h_addrtype != AF_INET) {
std::cout << "Resolved domain name is not IPV4!" << std::endl;
return resIp;
}

for (int i = 0; host->h_addr_list[i]; i++) {
resIp = inet_ntoa(*(struct in_addr*) host->h_addr_list[i]);
if (resIp != "") return resIp;
}
return resIp;
}

验证码发送

这是一个public接口,其内部封装了三个功能: 发送、接收、验证码生成。明确的属性分类,有效降低心智负担。

发送与接收

发送封装成一个private函数,通过返回值确认是否发送成功。
接收也是一样,不过需要留意的是: 由于邮件服务器返回的不是一个常规的字符串(包含转义字符的字符串),因此不可以使用sizeof来获取接收的长度,而应使用strlen来获取。
具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool SendMail::sendMsg(const char* msg) {
int ret = 0;
ret = send(ser_sock, msg, strlen(msg), 0);
if (ret == -1) {
std::cout << "Server: send message error !" << std::endl;
return false;
}
memset(message, 0x00, BUFSIZE);
return true;
}

bool SendMail::recvMsg() {
int ret = 0;
memset(message, 0x00, BUFSIZE);
ret = recv(ser_sock, message, BUFSIZE, 0);
if (ret == -1) {
std::cout << "Server: recv failed!" << std::endl;
return false;
}
return true;
}

验证码生成

验证码生成采用了六位随机数,来实现。验证码发送后,本地变量进行保存。

1
2
3
4
5
6
bool SendMail::GenerateVerificationCode(const std::string& mail) {
srand(time(0));
long num = (rand()%(MAXNUM - MINNUM + 1)) + MINNUM;
VerficationCode = std::to_string(num);
return true;
}

功能整合

将三个功能全部整合到public接口SendVerificationMail()中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
bool SendMail::SendVerificationMail(const std::string& mail) {

sendMsg("HELO qq.com\r\n");
recvMsg();
// 验证登陆
sendMsg("auth login\r\n");
recvMsg();
// 用户名(bash64编码)
sendMsg("MTUwODQ5ODEwOEBxcS5jb20=");
sendMsg("\r\n");
recvMsg();
// 密码(bash64编码)
sendMsg("xxxxxxxx");
sendMsg("\r\n");
recvMsg();

// 发送方邮箱配置
sendMsg("mail from: <");
sendMsg("1508498108@qq.com");
sendMsg(">");
sendMsg("\r\n");
recvMsg();

// 接收方邮箱配置
sendMsg("rcpt to: <");
sendMsg(mail.c_str());
sendMsg(">");
sendMsg("\r\n");
recvMsg();

bool ret = GenerateVerificationCode(mail);
if (!ret) {
return false;
}

// 开始发送邮件内容
sendMsg("data\r\n");
sendMsg("subject: Verfication Code\r\n\r");

// 发送的生成的六位验证码
sendMsg(VerficationCode.c_str());
sendMsg("\r\n.\r\n");
recvMsg();

return true;
}

添加限制

显然,对于单人使用而言,这些功能,已然可以全部覆盖需求。回归我们之前的问题: 为什么需要这些依赖?我们可以设想一个场景: 这是一个注册界面,十个用户都需要去使用你的这个SendVerificationCode来获取验证码,以及最后的验证需求。但是你仅仅使用了一个本地变量去存储它们,本地变量一直在改变,直到最后一个用户邮件发送完成。到比对验证码时,只有最后一名用户可以注册成功。如此来看,上面的接口,已然无法覆盖我们的需求。这里我们就可以使用unordered_map,用以完成我们的需求。以邮件地址为键,验证码为值。若是使用完成,则将其中map中删除。
应用设计到这里,似乎已经完成了。但是,我们仍需考虑一个问题: 邮件验证码,是否可以不断的请求发送。显示这是行不通,因为这对服务器资源是一种无意义的消耗。因此,我们需要引入定时器来为每个验证码设定一个过期时间,即在一定时间范围内验证码都是有效的。这样,也同样有效降低了服务的负担。

定时器

这里,我们使用时间堆定时器。这是一个基于优先队列完成的时间堆,可以存储多个定时器。具体的代码实现请看: TimeHeap
引入定时器,主要需要两个函数: AddTimer()与TicK()。通过它们来完成对于定时器的设定。

SIGALRM信号

在AddTimer()之后,我们就需要为其提供alarm函数,设定闹钟的时间。
在处理signal函数时,由于其所需要的回调函数是一个static函数。因此,我们就通过单例模式来调用其内部函数。

若是想要了解全部的代码: 项目地址

总结

使用socket完成数据的收发,并没有什么难度。主要设计关键在于设置验证码的限制措施:

  1. 确保不会在同一时间内向服务器发送大量获取验证码请求
  2. 确保验证码会在使用完成后被有效的删除,不会被重复的使用

这仅仅是笔者所能考虑到的范畴,若是应用到真正落实到应用,可能需要的限制更多。
笔者技术比较菜,若是文中在在什么错误,还请大家多多批评与指正。邮箱地址: 1508498108zty@gmail.com