背景
一般来说,我们在系统中要验证用户邮箱有效性的常规做法,是向用户的邮箱发送一个验证码,当用户接收到该验证码并输入系统,就完成了有效性验证。此邮箱就可以作为账户邮箱接收系统的各类推送了。
同样的,手机验证码也是一样的道理。
但有一些场景,我们通过这种常规操作可能会造成操作的复杂性,比如,我们的用户是一些小学生,尽管有家长在帮忙操作,但也有部分家长在操作的时候是盲目的,他们需要一种引导性强,没有心智负担,便利的操作方式。在我遇到的场景里就是,不是所有用户都愿意去接收验证码还要输入到系统里,而作为服务方,邮箱暂时是必须输入且要尽可能保证有效,也就是邮箱虽然是必填项,但如果用户输入了无效的邮箱也是可以继续进行其他操作的,只是无法通过邮箱接收信息,但其他接受渠道是不受影响。
在这种前提下,可以试试反向操作这种做法。
也就是用户再自行输入完邮箱后,由用户主动在系统界面上向某收信邮箱发送验证邮件,当收信邮箱收到邮件后,即验证了该邮箱的有效性,再通过系统内部的消息机制完成后续的验证流程。
其实有些系统在验证手机有效性的时候也是类似的方法,通过向某个指定号码发送一个数字代码,发送完成后在页面上点击已发送,就完成手机号有效性的验证
邮箱协议
邮箱相关的协议主要有,STMP,POP3,IMAP3 种,其中 STMP 协议是发信协议,POP3 和 IMAP 协议都是收信协议,不同的是,IMAP 协议相对更加完善,更灵活,它在邮箱服务器和客户端之间进行双向通信,可以在不下载邮件的前提下在客户端预览邮件的摘要,发信人等基本信息。
其他主要区别如下图
👆图出处:https://zhuanlan.zhihu.com/p/425136361
我们在使用一些邮箱客户端的时候,会进行一些服务器配置,来完成一个终端收发多个邮箱的目的。
👇这是我在邮箱大师配置的 qq 邮箱和工作邮箱
这两个邮箱的配置区别有
了解了邮箱协议的一些内容,我们就可以考虑通过使用程序手段来监听收信邮箱,从而完成客户邮箱验证。
实现代码
这里我用到的时 Google 开源的 MailKit 组件,它是一个跨平台的邮件客户端库,在 dotnet 环境里,比 c#标准库里自带的 System.Net.Mail 类库(用自带的类库也是可以实现邮件首发的),要更加方便。
这里只介绍 POP3 和 IMAP 协议下邮件的收取,邮件发送相关的方法很多,不在赘述。
/// <summary>
/// 收信,imap(新邮箱基本都支持,接口更灵活,更丰富)
/// </summary>
/// <param name="subject"></param>
/// <returns></returns>
public async Task<bool> ReviceEmailAsync(string subject,string memberEmail)
{
try
{
using (var client = new ImapClient())
{
//设定60s超时
var TokenSource = new CancellationTokenSource(60000);
//根据需要觉得是否开启SSL/TSL
client.Connect("<支持IMAP协议的收信服务>", 993, true);
await client.AuthenticateAsync("<邮箱地址>, "<客户端授权码>");
//网易163/126邮箱闭坑(UnSafe login之类的错误)
//解决办法参见-->https://www.cnblogs.com/peterYong/p/15000793.html#_label2_0
var clientImp = new ImapImplementation
{
Name = "EXAMINE",
Version = "2.0"
};
client.Identify(clientImp);
var inbox = client.Inbox;
inbox.Open(MailKit.FolderAccess.ReadOnly, cancellationToken: TokenSource.Token);
//查找1天内包含该关键字的邮件
var query = SearchQuery.DeliveredAfter(DateTime.Now.AddDays(-1)).And(SearchQuery.SubjectContains(subject));
var result = await inbox.SearchAsync(query);
if(!result.Any())
{
return false;
}
//发信模板
string tmpTxt = System.IO.File.ReadAllText(System.IO.Path.Combine(Environment.CurrentDirectory, "wwwroot", "tamplate", "email.txt"));
string content = tmpTxt.Replace("?email?", memberEmail).Replace("?projectno?", subject);
//回一封结果邮件
await SendEmailKitAsync(memberEmail, "邮箱验证", content);
client.Disconnect(true);
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to revice {ex.Message}");
return false;
}
}
/// <summary>
/// 收信,pop3(部分邮箱服务器不支持imap,只能退而求其次选择pop)
/// </summary>
/// <param name="subject"></param>
/// <param name="memberEmail"></param>
/// <returns></returns>
public async Task<bool> ReviceEmailPopAsync(string subject, string memberEmail)
{
try
{
using (var client = new Pop3Client())
{
var TokenSource = new CancellationTokenSource(60000);
client.Connect("<支持POP协议的收信服务>", 110);
await client.AuthenticateAsync("<邮箱地址>", "<客户端授权码>");
//设定的筛选条件(按需修改)
if(!client.Any(x=>x.Subject==subject && x.Priority==MessagePriority.Urgent && x.MessageId== "<指定的id>" && x.Date > DateTime.Now.AddDays(-1)))
{
return false;
}
var result = client.Where(x => x.Subject.Contains(subject) && x.Date > DateTime.Now.AddDays(-1)).FirstOrDefault();
//发信模板
string tmpTxt = System.IO.File.ReadAllText(System.IO.Path.Combine(Environment.CurrentDirectory, "wwwroot", "tamplate", "email.txt"));
string content = tmpTxt.Replace("?email?", memberEmail).Replace("?projectno?", subject);
//回一封邮件
await SendEmailKitAsync(memberEmail, "邮箱验证", content);
client.Disconnect(true);
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to revice {ex.Message}");
return false;
}
}
复制代码
在实际的使用过程,还可以进一步优化,比如已经验证有效的邮箱地址,将其存入数据库或者缓存当中,作为白名单,再次需要验证时,先比对白名单,比对通过后则直接验证通过。
调用时的代码差不多可以这样写
public async Task<IActionResult> EmailTest([FromServices]IEmailKitHelper emailKitHelper, [FromServices] IRedisCachingProvider redisCachingProvider,string subject,string email)
{
if (await redisCachingProvider.HExistsAsync("AvailableEmails", email))
{
return Json(new { code = 0, msg = "邮箱已通过验证" });
}
if(await emailKitHelper.ReviceEmailViaImapAsync(subject,email))
{
await redisCachingProvider.HSetAsync("AvailableEmails", email, DateTime.Now.ToString("yyyyMMdd") + "," + subject);
}
return Json(new { code = 0, msg = "邮箱已通过验证并加入到可用邮箱列表" });
}
复制代码
收到的验证通过的效果
前台页面接收到接口返回的结果后,可以根据情况修改页面的渲染效果即可,相关代码不再赘述。
评论