邮件和短信业务原理以及Spring Boot邮件系统示例

邮件和短信业务原理以及Spring Boot邮件系统示例

Scroll Down

一. 邮件实现与原理

1. 邮件概述(邮件在互联网中的位置)

img

上图就是互联网的TCP/IP架构。具体详情请见我的【计算机网络系列】。

应用层定义了应用程序使用互联网的规程。电子邮件的协议就建立在这一层。

为什么建立在TCP/IP的应用层??原因是??

  • 首先确立在现今社会,企业的正式工作都是通过邮件进行。邮件对于可靠性要求非常高,所以就要求一个可靠的传输协议。就把邮件协议建立在TCP/IP的应用层基础上了。

  • IP 协议的主要功能包括无连结数据报传送、数据报寻径以及差错处理三部分。

  • IP协议的特点是点到点的,IP对等实体间的通信不经过中间机器,对等实体所在的机器位于同一物理网络,
    对等机器之间有直接的物理连接。IP层的主要功能是屏蔽下面物理层的差别,向上一层提供一致的数据格式。
    所有要传输的数据,被按照一定的格式分组封装层IP数据报,数据报单元通过寻径等机制进行传输,
    在接收方数据报进行重组,得到最初要传送的数据。由于IP协议是不可靠的数据传输协议,
    由于网络的拥塞而发生的数据丢失等情况是不可避免的,因此Internet 还必须有一定的控制重传机制,
    这就是差错与控制报文协议(ICMP)。

  • 但IP协议还不能解决数据分组在传输过程中可能出现的问题。(透明传输等问题)。
    因此,还需要TCP协议来提供可靠的并且无差错的通信服务。
    TCP协议被称作一种端对端协议。
    这是因为它为两台计算机之间的连接起了重要作用:当一台计算机需要与另一台远程计算机连接时,
    TCP协议会让它们建立一个连接、发送和接收数据以及终止连接。传输控制协议TCP协议利用重发技术和拥塞控制机制,
    向应用程序提供可靠的通信连接,使它能够自动适应网上的各种变化。

  • IP协议只保证计算机能发送和接收分组数据,而TCP协议则可提供一个可靠的、可流控的、全双工的信息流传输服务。

  • 虽然IP和TCP这两个协议的功能不尽相同,也可以分开单独使用,但它们是在同一时期作为一个协议来设计的,
    并且在功能上也是互补的。只有两者的结合,才能保证 Internet 在复杂的环境下正常运行。凡是要连接到 Internet 的计算机,
    都必须同时安装和使用这两个协议,因此在实际中常把这两个协议统称作TCP/IP协议。
     TCP/IP 协议除了TCP协议和IP协议,
    还包含物理接口和IP层之间的ARP/RARP协议,应用层的FTP协议﹑SMTP协议和BOOTP协议等,
    所用的这些协议构成Intenet 的TCP/IP 协议族。

2. 邮件系统的组成分析

一个邮件系统组成必须包括邮件服务器,然后是用户代理和邮件传送协议。

1). 邮件服务器

是一个供在网上存储邮件的空间。(用户存储邮件的地方)

一般每个邮件服务器的提供商都有自己的邮件服务器,只要你申请了他的邮箱账号,你就会在他的邮件服务器上拥有自己邮箱。像Google,腾讯都是邮件服务的提供商,他们都有自己的邮件服务器,如果你申请了Gamil邮箱,那么在Google的邮件服务器上面,你就有自己的一块存储空间了。同样,如果你申请了qq邮箱,那么在qq邮件服务器上面也有你自己的空间了,也就是你的邮箱。当你要收取信件的时候,你就需要连接到不同的服务器上面。不同的邮件服务提供商,他们的邮件服务器的地址是不一样的。后面会介绍一些常用的邮件服务器的地址。

2). 用户代理

就是你用来从邮件服务器上读取或者发送邮件到邮件服务器上的一个软件。(用户读取邮件的地方)

比如常用的OutLook,qq邮箱(公司呈现出来的,邮箱服务器是看不到的另一区域)等等。我们知道,我们的邮件都是存储在邮件服务器上面的,我们发送邮件的时候,去往邮件服务器上面发,我们收取邮件的时候,也需要从服务器上面读。为了方便的完成这些工作,我们就需要用户代理。

3). 邮件传送协议

是指邮件在传送过程中必须遵守的约定,它规定了不同的服务器(或客户端)之间应如何交换信息。(邮件发送信息的约定)

比较常见的有邮件服务器之间的通信协议SMTP以及用户代理与邮件服务器之间的通信协议POP3。(注意:邮件服务器之间的协议是使用SMTP,用户发送邮件到邮件服务器使用的还是SMTP协议,用户从邮件服务器读取邮件用的才是POP3协议)。

3. 邮件系统–邮件通信过程

图取自教材的计算机网络–谢希任

电子邮件的最主要的组成构件

邮件通信过程(文字描述)

  1. 发信人调用自己的用户代理撰写、编辑邮件,并写清楚收件人的邮箱地址;
  2. 发信人的用户代理根据发信人编辑的信息,生成一封符合邮件格式的邮件;
  3. 发信人的用户代理把邮件发送到发信人的的邮件服务器上,邮件服务器上面有一个缓冲队列,发送到邮件服务器上面的邮件都会加入到缓冲队列中,等待邮件服务器上的SMTP客户端进行发送;
  4. 发信人的邮件服务器的 SMTP 客户端与接收方邮件服务器的 SMTP 服务器建立 TCP 连接,发信人的邮件服务器使用SMTP协议把这封邮件发送到收件人的邮件服务器上(它会自动根据收件人的邮箱来分析出收件人的邮箱服务器);
  5. 收件人的邮件服务器收到邮件后,把这封邮件放到收件人在这个服务器上的邮箱中,等待收件人进行读取
  6. 收件人使用他的用户代理来收取邮件。首先用户代理使用POP3协议来连接收件人所在的邮件服务器,身份验证成功后,用户代理就可以把邮件服务器上面的收件人邮箱里面的邮件读取出来,并展示给收件人。

4. 邮件协议详解

SMTP

SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,是一种提供可靠且有效电子邮件传输的协议。

SMTP是建立在FTP文件传输服务上的一种邮件服务,主要用于传输系统之间的邮件信息并提供与来信有关的通知。

SMTP主要负责底层的邮件系统如何将邮件从一台机器传至另外一台机器。

SMTP提供了一种邮件传输的机制,当收件方和发件方都在一个网络上时,可以把邮件直传给对方;当双方不在同一个网络上时,需要通过一个或几个中间服务器转发。

SMTP首先由发件方提出申请,要求与接收方SMTP建立双向的通信渠道,收件方可以是最终收件人也可以是中间转发的服务器。收件方服务器确认可以建立连接后,双发就可以开始通信。

POP3

是把邮件从电子邮箱服务器中传输到本地计算机客户端的协议。

POP3(Post Office Protocol 3)即邮局协议的第3个版本,它是规定个人计算机如何连接到互联网上的邮件服务器进行收发邮件的协议。它是因特网电子邮件的第一个离线协议标准,POP3协议允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时根据客户端的操作删除或保存在邮件服务器上的邮件,而POP3服务器则是遵循POP3协议的接收邮件服务器,用来接收电子邮件的。

IMAP

Internet Mail Access Protocol(交互式邮件存取协议)。它的主要作用是邮件客户端(例如MS Outlook Express)可以通过这种协议从邮件服务器上获取邮件的信息,下载邮件等。

IMAP协议运行在TCP/IP协议之上,使用的端口是143。它与POP3协议的主要区别是用户可以不用把所有的邮件全部下载,可以通过客户端直接对服务器上的邮件进行操作。

IMAP协议比较自由的功能是用户可以维护自己在服务器上的邮件目录;可以直接抓取邮件的特定部分(例如只有文本)。

IMAP的一个与POP3的区别是:IMAP它只下载邮件的主题,并不是把所有的邮件内容都下载下来,而是你邮箱当中还保留着邮件的副本,没有把你原邮箱中的邮件删除,你用邮件客户软件阅读邮件时才下载邮件的内容。

5. Java实现邮件发送

QQ邮箱为例,一会封装用网易邮箱(很多坑)

导入库

	<!-- poi -->
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi</artifactId>
      <version>3.15</version>
    </dependency>
    <!--zdk add 2017-5-16-->
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>3.15</version>
    </dependency>

    <!-- Java邮件操作类-->
    <dependency>
      <groupId>javax.mail</groupId>
      <artifactId>mail</artifactId>
      <version>1.4.5</version>
    </dependency>

一键复制即可实现

我们需要修改的东西:邮箱、邮箱服务器授权码

import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Date;
import java.util.Properties;
/**
 * Created by 符柱成 on 2017/6/6.
 */
public class JavaMailSendTest {
    // 发件人的 邮箱 和 密码(替换为自己的邮箱和密码)
    // PS: 某些邮箱服务器为了增加邮箱本身密码的安全性,给 SMTP 客户端设置了独立密码(有的邮箱称为“授权码”),
    // 对于开启了独立密码的邮箱, 这里的邮箱密码必需使用这个独立密码(授权码)。
    public static String myEmailAccount = "751197996@qq.com";//我们申请服务的邮箱
    public static String myEmailPassword = "";//这个就要填上我们刚刚拿到的授权码
    // 发件人邮箱的 SMTP 服务器地址, 必须准确, 不同邮件服务器地址不同, 一般(只是一般, 绝非绝对)格式为: smtp.xxx.com
    // 网易163邮箱的 SMTP 服务器地址为: smtp.163.com;qq邮箱的SMTP服务器地址:smtp.qq.com
    public static String myEmailSMTPHost = "smtp.qq.com";

    // 收件人邮箱(替换为自己知道的有效邮箱)
    public static String receiveMailAccount = "1433317518@qq.com";

    public static void main(String[] args) throws Exception {
        // 1. 创建参数配置, 用于连接邮件服务器的参数配置
        Properties props = new Properties();                    // 参数配置
        props.setProperty("mail.transport.protocol", "smtp");   // 使用的协议(JavaMail规范要求)
        props.setProperty("mail.smtp.host", myEmailSMTPHost);   // 发件人的邮箱的 SMTP 服务器地址
        props.setProperty("mail.smtp.auth", "true");            // 需要请求认证


        // PS: 某些邮箱服务器要求 SMTP 连接需要使用 SSL 安全认证 (为了提高安全性, 邮箱支持SSL连接, 也可以自己开启),
        // 如果无法连接邮件服务器, 仔细查看控制台打印的 log, 如果有有类似 “连接失败, 要求 SSL 安全连接” 等错误,
        // 打开下面 /* ... */ 之间的注释代码, 开启 SSL 安全连接。
        
      /*
        // SMTP 服务器的端口 (非 SSL 连接的端口一般默认为 25, 可以不添加, 如果开启了 SSL 连接,
        //                  需要改为对应邮箱的 SMTP 服务器的端口, 具体可查看对应邮箱服务的帮助,
        //                  QQ邮箱的SMTP(SLL)端口为465或587, 其他邮箱自行去查看)
        final String smtpPort = "465";
        props.setProperty("mail.smtp.port", smtpPort);
        props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.setProperty("mail.smtp.socketFactory.fallback", "false");
        props.setProperty("mail.smtp.socketFactory.port", smtpPort);
        */
      
        final String smtpPort = "465";
        props.setProperty("mail.smtp.port", smtpPort);
        props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.setProperty("mail.smtp.socketFactory.fallback", "false");
        props.setProperty("mail.smtp.socketFactory.port", smtpPort);

        // 2. 根据配置创建会话对象, 用于和邮件服务器交互
        Session session = Session.getDefaultInstance(props);
        session.setDebug(true); // 设置为debug模式, 可以查看详细的发送 log

        // 3. 创建一封邮件
        MimeMessage message = createMimeMessage(session, myEmailAccount, receiveMailAccount);

        // 4. 根据 Session 获取邮件传输对象
        Transport transport = session.getTransport();

        // 5. 使用 邮箱账号 和 密码 连接邮件服务器, 这里认证的邮箱必须与 message 中的发件人邮箱一致, 否则报错
        //
        //    PS_01: 成败的判断关键在此一句, 如果连接服务器失败, 都会在控制台输出相应失败原因的 log,
        //           仔细查看失败原因, 有些邮箱服务器会返回错误码或查看错误类型的链接, 根据给出的错误
        //           类型到对应邮件服务器的帮助网站上查看具体失败原因。
        //
        //    PS_02: 连接失败的原因通常为以下几点, 仔细检查代码:
        //           (1) 邮箱没有开启 SMTP 服务;
        //           (2) 邮箱密码错误, 例如某些邮箱开启了独立密码;
        //           (3) 邮箱服务器要求必须要使用 SSL 安全连接;
        //           (4) 请求过于频繁或其他原因, 被邮件服务器拒绝服务;
        //           (5) 如果以上几点都确定无误, 到邮件服务器网站查找帮助。
        //
        //    PS_03: 仔细看log, 认真看log, 看懂log, 错误原因都在log已说明。
        transport.connect(myEmailAccount, myEmailPassword);

        // 6. 发送邮件, 发到所有的收件地址, message.getAllRecipients() 获取到的是在创建邮件对象时添加的所有收件人, 抄送人, 密送人
        transport.sendMessage(message, message.getAllRecipients());

        // 7. 关闭连接
        transport.close();
    }

    /**
     * 创建一封只包含文本的简单邮件
     *
     * @param session 和服务器交互的会话
     * @param sendMail 发件人邮箱
     * @param receiveMail 收件人邮箱
     * @return
     * @throws Exception
     */
    public static MimeMessage createMimeMessage(Session session, String sendMail, String receiveMail) throws Exception {
        // 1. 创建一封邮件
        MimeMessage message = new MimeMessage(session);
        // 2. From: 发件人
        message.setFrom(new InternetAddress(sendMail, "符柱成主页", "UTF-8"));
        // 3. To: 收件人(可以增加多个收件人、抄送、密送)
        message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(receiveMail, "XX用户", "UTF-8"));
        // 4. Subject: 邮件主题
        message.setSubject("重要通知啊,老哥", "UTF-8");
        // 5. Content: 邮件正文(可以使用html标签)
        message.setContent("辅助:打声招呼而已", "text/html;charset=UTF-8");
        // 6. 设置发件时间
        message.setSentDate(new Date());
        // 7. 保存设置
        message.saveChanges();
        return message;
    }
}

收件示例

Java邮件封装和异步实现(网易邮箱为例)

为什么要异步??邮件和短信都要经过复杂的网络通信,这就意味着很可能极其耗时,使用同步方式,很容易导致主线程卡死,导致极差的体验。
所以,我们应该以异步方式去执行此业务,然后直接告诉用户已经发送(发送情况失败毕竟很少见)。

定义接口以及实现类

public interface EmailService {
	/*
	 * hisEmail收件人email
	 * subject主题(标题)
	 * content内容(文本)
	 */
	void sendEmail(String hisEmail, String subject, String content);
	
}

记得配置好config.properties。分别是发件人邮箱(开通SMTP服务的),授权码,邮件名字

SENDER_MAILBOX=???
MAIL_PASSWPRD=???
MAIL_NAME=BSS系统邮箱

可以看到跟使用qq邮箱有很大区别,但是必须如此配置使用,必须使用Authenticator 去验证

public class EmailServiceImpl implements EmailService {
    private static final Log log = LogFactory.getLog(EmailServiceImpl.class);

    // 发件人的 邮箱 和 密码(替换为自己的邮箱和密码)
    // 对于开启了独立密码的邮箱, 这里的邮箱密码必需使用这个独立密码(授权码)。
    public static String myEmailAccount = "";
    public static String myEmailPassword = "";
    public static String myEmailName = "";
    // qq邮箱的SMTP服务器地址:smtp.qq.com
    public static String myEmailSMTPHost = "smtp.126.com";

    static {
        //博主自己封装了一个获取本地文件的配置参数方式,大家可以参考使用。针对config.properties的,想改别的文件请大家自行修改。
        try {
            myEmailAccount = Config.getConfigValue("SENDER_MAILBOX");
            myEmailPassword = Config.getConfigValue("MAIL_PASSWPRD");
            myEmailName = Config.getConfigValue("MAIL_NAME");
            System.out.println(myEmailAccount);
            System.out.println(myEmailPassword);
            System.out.println(myEmailName);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void sendEmail(String hisEmail, String subject, String content) {
        System.out.println("myEmailAccount  :" + myEmailAccount);
        System.out.println("myEmailPassword :" + myEmailPassword);
        try {
            // 1. 创建参数配置, 用于连接邮件服务器的参数配置
            final Properties props = new Properties(); // 参数配置
            props.put("mail.smtp.auth", "true");
            props.put("mail.smtp.host", "smtp.126.com");

            // 发件人的账号
            props.put("mail.user", myEmailAccount);
            // 发件人的密码
            props.put("mail.password", myEmailPassword);
            //网易邮箱必须这样,使用Authenticator,进行一系列的验证。不然就是给你504,验证失败或者辣鸡邮件发不出去
            Authenticator authenticator = new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    String userName = props.getProperty("mail.user");
                    String password = props.getProperty("mail.password");
                    return new PasswordAuthentication(userName, password);
                }
            };

            // 使用环境属性和授权信息,创建邮件会话
            Session mailSession = Session.getInstance(props, authenticator);
            // 创建邮件消息
            MimeMessage message = new MimeMessage(mailSession);
            // 设置发件人
            String username = props.getProperty("mail.user");
            InternetAddress form = new InternetAddress(username);
            message.setFrom(form);

            // 设置收件人
            InternetAddress to = new InternetAddress(hisEmail);
            message.setRecipient(RecipientType.TO, to);

            // 设置邮件标题
            message.setSubject(subject);

            // 设置邮件的内容体
            message.setContent(content, "text/html;charset=UTF-8");
            // 发送邮件
            Transport.send(message);
        } catch (AddressException e) {
            e.printStackTrace();
        } catch (MessagingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

采用工厂模式

public class EmailServiceFactory {

	public static EmailService getEmailService(){
		return new EmailServiceImpl();
	}
}

使用线程异步一键调用

public class EmailTest {
    //创建一个线程池,可装载大概3个线程任务的
    private static ExecutorService executorService = Executors.newFixedThreadPool(3);

    // 收件人邮箱(替换为自己知道的有效邮箱)1433317518
    public static String receiveMailAccount = "751197996@qq.com";

    public static void main(String[] args)  {
        //这种写法详情请去参考lambda表达式
        executorService.submit(() -> EmailServiceFactory.getEmailService().sendEmail(receiveMailAccount, "BSS系统工单", "<body><p>工单工号GGGGDADA</p></body>"));
        //调用 shutdown() 方法,ExecutorService 并不会马上关闭,而是不再接收新的任务,一但所有的线程结束执行当前任务,ExecutorServie 才会真的关闭。所有在调用 shutdown() 方法之前提交到 ExecutorService 的任务都会执行。
        //调用时机由你自己去决定
        //你希望立即关闭 ExecutorService,你可以调用 shutdownNow() 方法。这個方法会尝试马上关闭所有正在执行的任务,并且跳过所有已经提交但是还没有运行的任务。但是对于正在执行的任务,是否能够成功关闭它是无法保证的,有可能他们真的被关闭掉了,也有可能它会一直执行到任务结束。这是一個最好的尝试。
        executorService.shutdown();
    }
}

二、 短信实现以及原理

短信的概述

短信在互联网通信架构中的位置:

首先短信与邮件同属于TCP/IP这一层。

SMS表示短信服务。简单来讲,它是在手机之间发送文字信息或从个人计算机或手持设备向手机发送信息的一种方式。

短信的“短”指的是文本信息的最大发送量:160个字符(字母、数字或拉丁字母中的符号)。至于其他字母,例如中文,一条短信的最大发送量为70个字符。

短信原理

在理解其原理前,需要先懂得手机的通信原理。

手机通信原理

尽管您没有使用手机打电话,您的手机也在不停地发送和接收着信息。

它通过被称为控制通道的通路与手机发射塔进行通信。这种通讯的目的是让手机系统了解自己所在的信号区域,以便在您移动时,手机可以切换到其他信号区域。

每隔一段时间,手机和发射塔将交换数据包以确定一切工作正常。手机也使用控制通道来建立呼叫。

当有人打电话给您时,手机发射塔将通过控制通道向手机发送信号,然后手机就会振铃。同时,手机发射塔为手机提供两个语音信道频率用来进行通话。

手机通信为什么这样做

在美国的标准模拟手机系统中,手机运营商获准在整个城市使用约800个频率。运营商将城市细分成小区,每个小区面积通常约26平方公里。通常把小区看作是一个大六边形网格上的一个个六边形。

由于手机和基站使用低功率发射器,因此相同频率可以在非邻小区中重复使用。

每个小区有一个基站,由一个塔和一个安装有无线电设备的小机房组成。在模拟系统中,一个小区使用七分之一可用的双工语音信道。换句话说,在由七个小区构成的六边形网格中,每个小区使用七分之一的可用信道,因此每个小区都有唯一的一组频率,彼此间不会发生冲突:手机运营商通常可以在一个城市中使用832个无线电频率。每部手机在每次通话期间使用两个频率,即一个双工信道,因此每个运营商通常有395个语音信道。其他42个频率用于控制信道。

因此,每个小区大约有56个语音信道可用。也就是在任何小区中,可以有56个人同时用手机通话。以上是第一代的蜂窝方案设计。往后的方案基于此去扩展。

基站与小区内手机之间的传输不会超出该小区太远。蜂窝方案要求无论城市大小,都需要有大量的基站。一般的大城市可能有数百个发射塔,不过由于很多人使用手机,因此按用户平均下来,成本仍能保持较低。每个运营商在各个城市还会设置一个中心局,也称为移动电话交换局(MTSO)。该局处理与普通陆地电话系统的所有电话连接,控制所辖区域的所有基站。

短信通信原理

基于手机通信原理,控制通道也为SMS短信提供通路。当朋友给您发送SMS短信时,该条短信将以控制通道上小型数据包的形式先通过SMSC(短信业务中心),然后通过手机发射塔,再由发射塔将短信发送到手机。同理,当您发送短信时,手机将通过控制通道将短信发送到发射塔,再由发射塔传送到SMSC,最后从这个位置到达接收目标。

短信的实际数据格式包括短信的长度、时戳、目标电话号码以及格式等等。

为什么是160个字符?就是为什么叫短信??

手机短信可以提供像数字页那样的短量数据。为了避免使用多于标准的转发和回复操作而使系统过载,短信技术的开发人员一致同意使用一次160个字符的最大发送量。但是160个字符的限制并不是绝对的。字符长度限制可能会因网络、手机型号以及无线运营商的差异而不同。许多手机在达到160个字符的限制时将不允许继续键入。这样您只能在发送后才可以继续键入。但是,许多服务可以自动将你所发送的短信拆分成若干个小于等于160个字符的信息块。这样,您就可以键入并发送一长条短信,只不过它会以几条短信的方式传输。

相对于电话的优势

短信交流要比电话交流更加私密,而且省时。

手机短信是一种存储和转发服务,这意味着,如果您向朋友发送一条短信,短信不会直接进入到您朋友的手机上。这种方法的优势在于,您朋友的手机不必开机或处于服务区内,您也可以发送短信。您发出的短信将被存储在短消息业务中心(可以根据需要存储数日),当您的朋友打开手机或进入服务区时,就会立即收到这条信息。如果不将它删除,这条短信将始终存储在您朋友的SIM卡上。

除了一人对一人的短信交流,SMS也可以用于同时将一条短信发送给很多人,包括联系人列表或是特定区域的所有用户。这种服务叫做群发,企业用它来联系各组员工或通过在线服务向订阅用户发布新闻或其他信息。

短信系统–短信通信过程

短信通信过程

当朋友给您发送SMS短信时,该条短信将通过基站以控制通道上小型数据包的形式先通过SMSC(短信业务中心),然后通过手机发射塔,再由发射塔(基站)将短信发送到手机。同理,当您发送短信时,手机将通过控制通道将短信发送到发射塔,再由发射塔传送到SMSC,最后从这个位置到达接收目标。

短信的协议

标准的短信协议是SMPP。

SMPP(ShortMessage Peer to Peer)协议是一个开放的消息转换协议;它定义了一系列操作的协议数据单元(PDUS)和当SMPP运行时ESMS应用系统与SMSC之间交换的数据格式。从而完成SMSC与ESMES(外部短消息实体)的信息交换。SMPP是基于SMSC与ESME之间的请求和响应协议数据单元的交换,每一个SMPP操作都由一个请求PDU和相应的一个响应PDU组成,这种交换一般是基于IP网络。

SMPP协议是一个应用层协议,不提供传输功能。因此,底层网络连接将提供点对点的可靠数据传输。这些传输包括加密包,窗口,流量控制和错误处理等。

但是,广大运营商门都是自己定义自己的协议。如中国电信:SGMP;中国移动:CMPP; 中国联通:SGIP。

一键实现短信发送

public class MessageTest {

		//短信服务提供商。这个就百度吧,很多,真的很多。我随便找的一家提供商。
    private static String Url = "http://106.ihuyi.cn/webservice/sms.php?method=Submit";

    public static void main(String[] args) {
	    	/*
	    		方式二是一键完成短信功能的展示而已
        */
        HttpClient client = new HttpClient();
        PostMethod method = new PostMethod(Url);

        client.getParams().setContentCharset("GBK");// 在头文件中设置转码 
        method.setRequestHeader("ContentType","application/x-www-form-urlencoded;charset=GBK");
      
        int mobile_code = (int)((Math.random()*9+1)*100000);

        String content = new String("您的验证码是:" + mobile_code + "。请不要把验证码泄露给其他人。");
      
        NameValuePair[] data = {//提交短信
                new NameValuePair("account", "xxxxxx"),// 注册的用户名  
                new NameValuePair("password", "xxxxxxx"), //查看密码请登录用户中心->验证码、通知短信->帐户及签名设置->APIKEY
                //new NameValuePair("password", util.StringUtil.MD5Encode("密码")),
                new NameValuePair("mobile", "xxxxx"),//要发的手机
                new NameValuePair("content", content),//要发的内容
        };
        method.setRequestBody(data);

        try {
            client.executeMethod(method);//发送短信

        		Header[] headers = method.getResponseHeaders();//短信返回信息
        		int statusCode = method.getStatusCode();//状态码
       			System.out.println("statusCode:" + statusCode);
        		for (Header h : headers) {//响应头的打印
            		System.out.println(h.toString());
        		}
        		String result = null;

            result = new String(method.getResponseBodyAsString().getBytes("gbk"));//打印响应体
        		System.out.println(result);
        		method.releaseConnection();
        } catch (IOException e) {
            e.printStackTrace();
        }

    	}
    }
}

实现的效果如下

在这里插入图片描述

Java短信封装和异步实现

同样用工厂模式

public class MessageServiceFactory {
	
	public static MessageService getMobileMessageService(){
		return new MessageServiceSupport(){
			public String getType() {
				return MessageServiceSupport.PHONO_MESSAGE_TYPE;
			}
		};
	}
}

接口

public interface MessageService {
	
	/**
	 * 
	 * @param acceptorName 消息接收者名称
	 * @param acceptor 消息接收者,若是短信为手机号码,若是邮件则是邮箱地址
	 * @param context 消息内容
	 * @return 成功返回true, 内部采用异步机制,这里返回成功只是代表初步校验成功,比如手机号码或邮箱格式校验
	 */
	public boolean send(String acceptorName, String acceptor, String context);
	
	/**
	 * 
	 * @param acceptorName 消息接收者名称
	 * @param acceptor 消息接收者,若是短信为手机号码,若是邮件则是邮箱地址
	 * @param context 消息内容
	 * @return 成功返回true, 内部采用异步机制,这里返回成功只是代表初步校验成功,比如手机号码或邮箱格式校验
	 */
	public boolean[] send(String acceptorName, String[] acceptor, String context);
	

	public boolean send(String acceptorName, String acceptor, String context, String title);
	
	/**
	 * 
	 * @param acceptorUserId 接收者的用户ID, cf_user.userid
	 * @param context
	 * @return
	 */
	public boolean send(Long acceptorUserId, String context);
	
	/**
	 * 
	 * @param acceptorLoginId 接收者的登陆ID, cf_user.loginid
	 * @param context
	 * @return
	 */
	public boolean send(String acceptorLoginId, String context);

}

短信接口的实现类:
可以基于我暴露出来的状态码设计,进行一些短信的分门别类判断。

public abstract class MessageServiceSupport implements MessageService {

	private static String Url = "http://106.ihuyi.cn/webservice/sms.php?method=Submit";

	public abstract String getType();
	
	public static String PHONO_MESSAGE_TYPE = "1";
	


	protected boolean checkAcceptor(String acceptor){
		if(acceptor == null || "".equals(acceptor)){
			return false;
		}


		 if(PHONO_MESSAGE_TYPE.equals(getType())){
			// TODO 验证手机号码
		}

		return true;
	}
	


	public boolean send(String acceptorName, String acceptor, String context){
		return send(acceptorName, acceptor, context, "无标题");
	}
	
	public boolean send(String acceptorName, String acceptor, String context, String title){
		if(checkAcceptor(acceptor)){
			try {
				//如果有业务需求要保存,就用此bean保存。
				CfMessage message = new CfMessage();
				message.setType(getType());
				message.setAcceptor(acceptor);
				message.setAcceptorname(acceptorName);
				message.setTitle(title);
				message.setContext(context);
				message.setCreatetime(new Date());
				message.setRecordstatus(1);
				//前面应该加短信内容状态判断
				//TODO  短信内容判断,因为一般第三方的API会根据内容去发送
				//调用发送短信
				sendMessage(acceptor,context);
				return true;
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return false;
	}
	public void sendMessage(String acceptor,String content){
		HttpClient client = new HttpClient();
		PostMethod method = new PostMethod(Url);
		client.getParams().setContentCharset("GBK");
		method.setRequestHeader("ContentType","application/x-www-form-urlencoded;charset=GBK");
		NameValuePair[] data = {//提交短信
				new NameValuePair("account", "C65868831"),
				new NameValuePair("password", "fc422f9380ae002985db316ecce0ab27"), //查看密码请登录用户中心->验证码、通知短信->帐户及签名设置->APIKEY
				//new NameValuePair("password", util.StringUtil.MD5Encode("密码")),
				new NameValuePair("mobile", acceptor),
				new NameValuePair("content", content),
		};
		method.setRequestBody(data);

		try {
			client.executeMethod(method);//发送短信

			Header[] headers = method.getResponseHeaders();//短信返回信息
			int statusCode = method.getStatusCode();
			System.out.println("statusCode:" + statusCode);
			for (Header h : headers) {
				System.out.println(h.toString());
			}
			String result = null;

			result = new String(method.getResponseBodyAsString().getBytes(
					"gbk"));

			System.out.println(result);
			method.releaseConnection();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}
	

	public boolean[] send(String acceptorName, String[] acceptors, String context) {
		if(acceptors == null || acceptors.length == 0){
			return null;
		}
		boolean[] result = new boolean[acceptors.length];
		int i = 0;
		for(String acceptor : acceptors){
			result[i++] = send(acceptorName, acceptor, context);
		}
		return result;
	}
	
	public boolean send(Long acceptorUserId, String context){
		if(acceptorUserId == null ){
			return false;
		}
		try {
			List<User> userList=null ;
			if(userList != null && userList.size() == 1){

			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}
	
	public boolean send(String acceptorLoginId, String context){
		if(acceptorLoginId == null || "".equals(acceptorLoginId)){
			return false;
		}
		try {
			List<User> userList=null ;
			if(userList != null && userList.size() == 1){
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}
}

至于如何异步实现,请参考上面邮件的发送。


三、如何使用 Spring Boot 开发邮件系统?

邮件系统脑图

1. 中国的第一封电子邮件

1987 年 9 月 14 日中国第一封电子邮件是由“德国互联网之父”维纳·措恩与王运丰在当时的兵器工业部下属单位—计算机应用技术研究所(简称 ICA)发往德国卡尔斯鲁厄大学的,其内容为德文和英文双语,第一段大意如下:

原文:“ Across the Great Wall we can reach every corner in the world. ”

中文大意:“ 越过长城,我们可以到达世界的每一个角落。 ”

这是中国通过北京与德国卡尔斯鲁厄大学之间的网络连接,发出的第一封电子邮件。现在看这封邮件内容,颇具深意!

2. 邮件协议

发送邮件的本质是将一个人的信息传输给另外一个人,那么如何传输就需要商量好标准,这些标准就是协议。最初只有两个协议:

· SMTP 协议

SMTP 的全称是 “Simple Mail Transfer Protocol”,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。它的一个重要特点是它能够在传送中接力传送邮件,即邮件可以通过不同网络上的主机接力式传送。

SMTP 认证,简单地说就是要求必须在提供了账户名和密码之后才可以登录 SMTP 服务器,这就使得那些垃圾邮件的散播者无可乘之机。增加 SMTP 认证的目的是为了使用户避免受到垃圾邮件的侵扰。SMTP主要负责底层的邮件系统如何将邮件从一台机器传至另外一台机器。

· POP3 协议

POP3 是 Post Office Protocol 3 的简称,即邮局协议的第3个版本,它规定怎样将个人计算机连接到 Internet 的邮件服务器和下载电子邮件的电子协议。

它是因特网电子邮件的第一个离线协议标准,POP3 允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件。

POP 协议支持“离线”邮件处理。其具体过程是:邮件发送到服务器上,电子邮件客户端调用邮件客户机程序以连接服务器,并下载所有未阅读的电子邮件。

这种离线访问模式是一种存储转发服务,将邮件从邮件服务器端送到个人终端机器上,一般是 PC 机或 MAC。

一旦邮件发送到 PC 机或 MAC上,邮件服务器上的邮件将会被删除。但目前的 POP3 邮件服务器大都可以“只下载邮件,服务器端并不删除”,也就是改进的 POP3 协议。

SMTP 和 POP3 是最初的两个协议,随着邮件的不断发展后来又增加了两个协议:

· IMAP 协议

全称 Internet Mail Access Protocol(交互式邮件存取协议),IMAP 是斯坦福大学在 1986 年开发的研发的一种邮件获取协议,即交互式邮件存取协议,它是跟 POP3 类似邮件访问标准协议之一。

不同的是,开启了 IMAP 后,在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。

所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的。

IMAP 的一个与 POP3 的区别是:IMAP 它只下载邮件的主题,并不是把所有的邮件内容都下载下来,而是你邮箱当中还保留着邮件的副本,没有把你原邮箱中的邮件删除,你用邮件客户软件阅读邮件时才下载邮件的内容。

较好支持这两种协议的邮件客户端有:Foxmail、Outlook 等。

· Mime 协议

由于 SMTP 这个协议开始是基于纯 ASCⅡ文本的,在二进制文件上处理得并不好。后来开发了用来编码二进制文件的标准,如 MIME,以使其通过 SMTP 来传输。

今天,大多数 SMTP 服务器都支持 8 位 MIME 扩展,它使二进制文件的传输变得几乎和纯文本一样简单。

用一张图来看发送邮件过程中的协议使用:

640?wx_fmt=png

实线代表 neo@126.com 发送邮件给 itclub@aa.com;

虚线代表 itclub@aa.com 发送邮件给 neo@126.com

3. 邮件发送流程

640?wx_fmt=png

  • 发信人在用户代理上编辑邮件,并写清楚收件人的邮箱地址;
  • 用户代理根据发信人编辑的信息,生成一封符合邮件格式的邮件;
  • 用户代理把邮件发送到发信人的邮件服务器上,邮件服务器上面有一个缓冲队列,发送到邮件服务器上面的邮件都会加入到缓冲队列中,等待邮件服务器上的 SMTP 客户端进行发送;
  • 发信人的邮件服务器使用 SMTP 协议把这封邮件发送到收件人的邮件服务器上
  • 收件人的邮件服务器收到邮件后,把这封邮件放到收件人在这个服务器上的信箱中;
  • 收件人使用用户代理来收取邮件。首先用户代理使用 POP3 协议来连接收件人所在的邮件服务器,身份验证成功后,用户代理就可以把邮件服务器上面的收件人邮箱里面的邮件读取出来,并展示给收件人。

这就是邮件发送的一个完整流程。

##4. 简单使用

最早期的时候使用 JavaMail 相关 API 来开发,需要自己去封装消息体,代码量比较庞大;

后来 Spring 推出了 JavaMailSender 简化了邮件发送过程,JavaMailSender 提供了强大的邮件发送功能,可支持各种类型的邮件发送。

现在 Spring Boot 在 JavaMailSender 的基础上又进行了封装,就有了现在的 spring-boot-starter-mail,让邮件发送流程更加简洁和完善。

如何使用 Spring Boot 发送邮件

pom 包配置

引入加 spring-boot-starter-mail 依赖包:

640?wx_fmt=png

配置文件

在 application.properties 中添加邮箱配置,不同的邮箱参数稍有不同,下面列举几个常用邮箱配置:

163 邮箱配置:

img

126 邮箱配置

640?wx_fmt=png

qq 邮箱配置如下:

640?wx_fmt=png

**注意:**测试时需要将 spring.mail.username 和 spring.mail.password 改成自己邮箱对应的登录名和密码,这里的密码不是邮箱的登录密码,是开启 POP3 之后设置的客户端授权密码。

这里以 126 为邮件举例,有两个地方需要邮箱中设置:

开启 POP3/SMTP 服务、IMAP/SMTP 服务

640?wx_fmt=png

图片下方会有 smtp 等相关信息的配置提示。

开通设置客户端授权密码

640?wx_fmt=png

设置客户端授权密码一般需要手机验证码验证。

文本邮件发送

Spring 已经帮我们内置了 JavaMailSender,直接在项目中引用即可。我们封装一个 MailService 类来实现普通的邮件发送方法。

640?wx_fmt=png

文本邮件抄送使用:message.copyTo(copyTo) 来实现。

  • from,即为邮件发送者,一般设置在配置文件中
  • to,邮件接收者,此参数可以为数组,同时发送多人
  • subject,邮件主题
  • content,邮件的主体

邮件发送者 from 一般采用固定的形式写到配置文件中。

编写 test 类进行测试

img

稍微等待几秒,就可以在邮箱中找到此邮件内容了。至此一个简单的文本邮件发送就完成了。

640?wx_fmt=png

##5. 富文本邮件

在日常使用的过程中,我们通常在邮件中加入图片或者附件来丰富邮件的内容,下面讲介绍如何使用 Spring Boot 来发送富文本邮件。

发送 HTML 格式邮件

邮件发送支持以 HTML 语法去构建自定义的邮件格式,Spring Boot 支持使用 HTML 发送邮件。

我们在 MailService 中添加支持 HTML 邮件发送的方法.

640?wx_fmt=png

富文本邮件抄送使用:helper.addCc(cc) 来实现。

和文本邮件发送代码对比,富文本邮件发送使用 MimeMessageHelper 类。

MimeMessageHelper 支持发送复杂邮件模板,支持文本、附件、HTML、图片等,接下来会一一使用到。

在测试类中构建 HTML 内容,测试发送

img

邮件内容大写了一段话,下面为接收到的效果:

640?wx_fmt=png

由此我们发现发送 HTML 邮件,就是需要拼接一段 HTML 的 String 字符串交给 MimeMessageHelper 来处理,最后由邮件客户端负责渲染显示内容。

发送带附件的邮件

在 MailService 添加 sendAttachmentsMail 方法,发送带附件的邮件主要是使用 FileSystemResource 对文件进行封装,在添加到 MimeMessageHelper 中。

640?wx_fmt=png

添加多个附件可以使用多条 helper.addAttachment(fileName, file)

在测试类中添加测试方法

img

附件可以是图片、压缩包、Word 等任何文件,但是邮件厂商一般都会对附件大小有限制,太大的附件建议使用网盘上传后,在邮件中给出链接。

效果图如下:

640?wx_fmt=png

发送带静态资源的邮件

邮件中的静态资源一般指图片,在 MailService 添加 sendInlineResourceMail 方法。

640?wx_fmt=png

在测试类中添加测试方法

640?wx_fmt=png

添加多个图片可以使用多条 和helper.addInline(rscId, res) 来实现

效果图如下:

640?wx_fmt=png

以上是邮件发送的基础服务,已演示支持各种类型邮件。

##6. 邮件系统

如果只是想在系统中做一个邮件工具类的话,以上的内容基本就可以满足要求了。要做成一个邮件系统的话还需要考虑以下几方面:

  • 对外提供发送邮件的服务接口
  • 固定格式邮件是否考虑使用模板
  • 发送邮件时出现网络错误,是否考虑适当的重试机制
  • 邮件系统是否考虑异步化,提升服务响应时间
  • 是否开发邮件后台管理系统,开发出对应的管理软件,通过页面发送邮件,统计发送邮件成功率等数据。
  • 常见异常处理措施

对外提供接口

作为一个独立的邮件系统,需要对外提供接口调用,我们以简单文本邮件为例做个演示:

首先需要定义个实例返回对象:

640?wx_fmt=png

默认成功的返回码为:00,返回消息为:发送成功。

创建一个 MailController 类对外提供 HTTP 请求接口。

640?wx_fmt=png

外部请求过来时首先进行参数校验,如果参数有误返回请求;发送邮件出现异常时返回错误,正常情况下返回 00;

注意在 Service 层如果对异常信息进行了捕获的话,需要将异常信息抛到上层。

640?wx_fmt=png

类似上述代码。

按照这个思路也可以提供发送带图片、带附件的邮件,同时也可以封装发送多人邮件,群发邮件等复杂情况。

邮件模板

通常我们使用邮件发送服务的时候,都会有一些固定的场景,比如重置密码、注册确认等,给每个用户发送的内容可能只有小部分是变化的。

所以,很多时候我们会使用模板引擎来为各类邮件设置成模板,这样我们只需要在发送时去替换变化部分的参数即可。

我们会经常收到这样的邮件:

尊敬的 neo 用户:

恭喜您注册成为 xxx 网的用户,同时感谢您对 xxx 的关注与支持并欢迎您使用 xx 的产品与服务。

邮件正文只有 neo 这个用户名在变化,邮件其它内容均不变,如果每次发送邮件都需拼接 HTML 代码,程序不够优雅,并且每次邮件正文有变化都需修改代码非常不方便。

因此对于这类邮件,都建议做成邮件模板来处理,模板的本质很简单,就是在模板中替换变化的参数,转换为 HTML 字符串即可,这里以 Thymeleaf 为例来演示。

Thymeleaf 是 Spring 官方推荐的前端模板引擎,类似 Velocity、FreeMarker 等模板引擎,相较与其他的模板引擎,Thymeleaf 开箱即用的特性。

它提供标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式效果,避免每天套模板、该 Jstl、改标签的困扰。

Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。

下面我们来演示使用 Thymeleaf 制作邮件模板:

  1. 添加依赖包

640?wx_fmt=png

  1. 在 resorces/templates 下创建 emailTemplate.html

emailTemplate.html 文件内容即为邮件的正文内容模板。

640?wx_fmt=png

我们发现上述的模板中只有 id 是一个动态的值,发送过程中会根据传入的 id 值来替换链接中的

  1. 解析模板并发送

640?wx_fmt=png

我们发现最后调用的还是 sendHtmlMail 的方法,邮件模板的作用只是处理 HTML 生成部分,通过 Thymeleaf 模板引擎解析固定的模板,再更具参数来动态替换其中的变量,最后通过前面的 HTML 发送的方法发送邮件。

效果图如下:

640?wx_fmt=png

点击“激活账号”跳转的链接为:http://www.ityouknow.com/register/006

发送失败

因为各种原因,总会有邮件发送失败的情况,比如:邮件发送过于频繁、网络异常等。在出现这种情况的时候,我们一般会考虑重新重试发送邮件,会分为以下几个步骤来实现:

  1. 接收到发送邮件请求,首先记录请求并且入库。
  2. 调用邮件发送接口发送邮件,并且将发送结果记录入库。
  3. 启动定时系统扫描时间段内,未发送成功并且重试次数小于3次的邮件,进行再次发送.
  4. 重新发送邮件的时间,建议以 2 的次方间隔时间,比如:2、4、8、16 ...

常见的错误返回码:

  • 421 HL:ICC 该IP同时并发连接数过大,超过了网易的限制,被临时禁止连接。
  • 451 Requested mail action not taken: too much fail authentication 登录失败次数过多,被临时禁止登录。请检查密码与帐号验证设置
  • 553 authentication is required,密码配置不正确
  • 554 DT:SPM 发送的邮件内容包含了未被许可的信息,或被系统识别为垃圾邮件。请检查是否有用户发送病毒或者垃圾邮件;
  • 550 Invalid User 请求的用户不存在
  • 554 MI:STC 发件人当天内累计邮件数量超过限制,当天不再接受该发件人的投信。

如果使用一个邮箱频繁发送相同内容邮件,也会被认定为垃圾邮件,报 554 DT:SPM 错误

如果使用网易邮箱可以查看这里的提示:企业退信的常见问题?

异步发送

很多时候邮件发送并不是主业务必须关注的结果,比如通知类、提醒类的业务可以允许延时或者失败。这个时候可以采用异步的方式来发送邮件,加快主交易执行速度。

在实际项目中可以采用消息中间件 MQ 发送邮件,具体做法是创建一个邮件发送的消息队列,在业务中有需要用到邮件发送功能时,给对应消息队列按照规定参数发送一条消息,邮件系统监听此队列,当有消息过来时,处理邮件发送的逻辑。

管理后台

考虑做一个完善的邮件系统,可以设计一个独立的邮件管理后台,不但可以让系统之间调用时使用,也可以提供图形化界面让公司的运营、市场部的同事来发送邮件,查询邮件的发送进度,统计邮件发送成功率。

也可以设置一些代码钩子,统计用户点击固定链接次数,方便公司营销人员监控邮件营销转化率。

一个非常完善的邮件系统需要考虑的因素非常多,比如是否设置白名单、黑名单来做邮件接收人的过滤机制,是否给用户提供邮件退订的接口等。

因此在初期邮件发送的基本功能完成之后,再结合公司业务,快速迭代的逐步完善邮件系统,是一个推荐的做法。

总结

使用 Spring Boot 集成发送邮件的功能非常简单,只需要简单编码就可以实现发送普通文本邮件、带附件邮件、HTML 格式邮件、带图片邮件等。

如果需要做成一个邮件系统还需要考虑很多因素,比如:邮箱发送失败重试机制、防止邮件被识别为垃圾邮件,固定时间内发送邮件的限制等。

在微服务架构中,常常将一些基础功能下沉下来,作为独立的服务来使用,邮件系统作为平台的基础功能,特别适合做为独立的微服务来支持整个系统。