一. 前言

互联网发展到现在,邮件服务已经成为互联网企业中必备功能之一,应用场景非常广泛,比较常见的有:用户注册、忘记密码、监控提醒、企业营销等。

大多数互联网企业都会将邮件发送抽取为一个独立的微服务,对外提供REST接口来支持各种类型的邮件发送。

中国的第一封电子邮件

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

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

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

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

二. 邮件协议

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

· 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 扩展,它使二进制文件的传输变得几乎和纯文本一样简单。

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

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

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

  • 发信人在用户代理上编辑邮件,并写清楚收件人的邮箱地址;

  • 用户代理根据发信人编辑的信息,生成一封符合邮件格式的邮件;

  • 用户代理把邮件发送到发信人的邮件服务器上,邮件服务器上面有一个缓冲队列,发送到邮件服务器上面的邮件都会加入到缓冲队列中,等待邮件服务器上的 SMTP 客户端进行发送;

  • 发信人的邮件服务器使用 SMTP 协议把这封邮件发送到收件人的邮件服务器上

  • 收件人的邮件服务器收到邮件后,把这封邮件放到收件人在这个服务器上的信箱中;

  • 收件人使用用户代理来收取邮件。首先用户代理使用 POP3 协议来连接收件人所在的邮件服务器,身份验证成功后,用户代理就可以把邮件服务器上面的收件人邮箱里面的邮件读取出来,并展示给收件人。

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

三. 项目实战

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

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

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

下面给大家介绍如何使用 Spring Boot 发送邮件。

创建Spring Boot2项目,版本:2.3.0,项目名称:spring-boot2-mail

3.1 pom.xml配置

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>    

3.2 application.properties配置文件

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

server.port=8010
server.servlet.context-path=/mail

#启用优雅关机
server.shutdown=graceful
#缓冲10秒
spring.lifecycle.timeout-per-shutdown-phase=10s

#spring boot 健康检查
#启用邮件运行状况检查。
management.health.mail.enabled=false
# JavaMailSender 配置
spring.mail.host=smtp.jieyuechina.com
spring.mail.username=xxxx_service@jieyuechina.com
spring.mail.password=xxxxx
spring.mail.test-connection=false
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

# ssl 配置
spring.mail.port=465
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.imap.ssl.socketFactory.fallback=false
spring.mail.properties.mail.smtp.ssl.socketFactory.class=com.modules.common.utils.MailSSLSocketFactory

# 设置时间
spring.jackson.time-zone=GMT+8
# 全局格式化日期
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

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

3.3 代码实现

3.3.1 controller接口

package com.modules.project.controller;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.modules.common.enums.PublicEnum;
import com.modules.common.web.BaseController;
import com.modules.common.web.Result;
import com.modules.project.entity.BaseMessage;
import com.modules.project.service.MailManageService;
import com.modules.project.service.MailSendService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 * @Description: 邮件api
 * @author lc
 */
@Slf4j
@RestController
@RequestMapping("/api/mail")
@Api(tags="邮件api请求入口")
public class MailController extends BaseController {
	
	@Autowired
	private MailManageService manageService;
	@Autowired
	private MailSendService sendService;

	 /**
	  * @Description: 通用发送邮件接口
	  * @author lc
	  */
	 @ApiOperation(value= "发送邮件", notes= "发送邮件接口")
	 @RequestMapping(value = "/general/send", method = RequestMethod.POST, produces = "application/json")
	 @ResponseBody
	 public Result generalSend(HttpServletRequest request, @RequestBody BaseMessage baseMessage, BindingResult result){
		 log.info("[通用请求]开始请求发送邮件, 业务原始请求参数为: {}", JSONObject.toJSONString(baseMessage));
		 Assert.notNull(baseMessage.getTo(), "邮件缺少接收者信息");
		 Assert.notNull(baseMessage.getSubject(), "邮件缺少主题信息");
		 Assert.notNull(baseMessage.getAttachFlag(), "邮件缺少标识信息");
		 if(PublicEnum.Y.getCode().equals(baseMessage.getAttachFlag())){
			 Assert.notNull(baseMessage.getAttachType(), "邮件缺少附件类型信息");
			 Assert.notNull(baseMessage.getAttachName(), "邮件缺少附件名称信息");
			 Assert.notNull(baseMessage.getAttachContent(), "邮件缺少附件内容信息");
		 }
		 return success(manageService.preposition(baseMessage));
	 }
	 
    /**
     * @Description: 发送邮件是否连通
     * @author lc
     * @throws Exception 
     */
	@ApiOperation(value= "检查邮件服务器连通性", notes= "发送邮件检查是否连通邮件服务器")
	@RequestMapping(value = "/sendEmailReq", method = RequestMethod.POST, produces = "application/json")
	@ResponseBody
	public Result sendEmailReq(@RequestBody BaseMessage email) {
		log.info("发送开始-邮件信息:{}", JSONArray.toJSON(email));
		return success(sendService.sendMail(email));
	}
}

3.3.2 service实现

package com.modules.project.service;

import com.modules.common.web.Result;
import com.modules.project.entity.BaseMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import javax.activation.DataSource;
import javax.mail.internet.MimeMessage;
import javax.mail.util.ByteArrayDataSource;


/**
 * @Description: 邮件发送service
 * @author lc
 */
@Slf4j
@Service
public class MailSendService {

	@Autowired
	private JavaMailSender mailSender;
   
   
   /**
    * @Description: 发送普通邮件
    * @author lc
    */
   public Result sendMail(BaseMessage email){
	   MimeMessage message = null;
	    try {
	        message = mailSender.createMimeMessage();
	        MimeMessageHelper helper = new MimeMessageHelper(message, true);
	        helper.setFrom(email.getFrom());
	        helper.setTo(email.getTo());
	        helper.setSubject(email.getSubject());
	        // 发送htmltext值需要给个true,不然不生效
	        helper.setText(email.getText(), true);
			if(email.getCc() != null){
				helper.setCc(email.getCc());
			}
			mailSender.send(message);
			return Result.success("发送成功");
	    } catch (Exception e) {
	    	log.error("发送普通邮件异常!{}",e);
			return Result.error("发送普通邮件异常", e.getMessage());
	    }

   }
   
   /**
    * @Description: 发送带附件的邮件
    * @author lc
    */
   public Result sendAttachmentsMail(BaseMessage email){
	   MimeMessage message = null;
       try {
           message = mailSender.createMimeMessage();
           MimeMessageHelper helper = new MimeMessageHelper(message, true);
           helper.setFrom(email.getFrom());
           helper.setTo(email.getTo());
           helper.setSubject(email.getSubject());
           helper.setText(email.getText());
           if(email.getCc() != null){
        	   helper.setCc(email.getCc());
           }
           // 用流的形式发送附件邮件
           DataSource source = new ByteArrayDataSource(email.getIs(), email.getAttachType());
           helper.addAttachment(email.getAttachName(), source);
           mailSender.send(message);
           return Result.success("发送成功");
       } catch (Exception e){
           log.error("发送附件邮件异常!{}",e);
           return Result.error("发送附件邮件异常", e.getMessage());
       }
   }
}
package com.modules.project.service;

import com.modules.common.enums.PublicEnum;
import com.modules.common.web.Result;
import com.modules.project.entity.BaseMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;


/**
 * @Description:  邮件管理service
 * @author lc
 */
@Slf4j
@Service
public class MailManageService {
	
	//读取配置文件中的参数
    @Value("${spring.mail.username}")
    private String from; 
	
	@Autowired
	private MailSendService sendService;

	/**
	 * @Description: 邮件前置操作
	 * @author lc
	 */
	public Result preposition(BaseMessage message){
		message.setFrom(from);
		if (PublicEnum.N.getCode().equals(message.getAttachFlag())) {
			return sendService.sendMail(message);
		} else if (PublicEnum.Y.getCode().equals(message.getAttachFlag())) {
			message.setIs(changeInputStream(message.getAttachContent()));
			return sendService.sendAttachmentsMail(message);
		}else {
			return Result.error("发送成功");
		}
	}
		
	/**
	 * @Description: 字符串转InputStream
	 * @author lc
	 */
	public InputStream changeInputStream(String str){
		InputStream is = null;
		char[] hex = null;
		byte[] data = null;
		try{
			hex = str.toCharArray();
			data= Hex.decodeHex(hex);
			is = new ByteArrayInputStream(data);  
			is.close();
		}catch(Exception e){
			e.printStackTrace();
			log.error("字符串转InputStream异常!{}", e.getMessage());
		}
		return is;
	}
	
	/**
	 * @Description: InputStream转16进制字符串
	 * @author lc
	 */
	public String changeStr(InputStream is) throws IOException{
		byte[] data=null;
		char[] hex=null;
		try {
			data=new byte[is.available()];
			is.read(data);
			is.close();
			hex=Hex.encodeHex(data);
			System.out.println("byte length:"+data.length);
			System.out.println("--------------------------");
			System.out.println("char hex length:"+hex.length);
			System.out.println("--------------------------");
		} catch (Exception e) {
			e.printStackTrace();
		}
		String str = new String(hex);
		System.out.println("String length:"+ str.length());
		return str;  
	}
}

3.3.3 实体对象

package com.modules.project.entity;

import java.io.InputStream;
import java.io.Serializable;

/**
 * @Description: 邮件模型
 * @author lc
 */
public class BaseMessage implements Serializable{

	/**
	 * 序列化
	 */
	private static final long serialVersionUID = -8840079326802564269L;
	
	/**
	 * 发送者
	 */
	private String from;
	
	/**
	 * 接受者
	 */
	private String[] to;
	
	/**
	 * 抄送着
	 */
	private String[] cc;
	
	/**
	 * 邮件主题
	 */
	private String subject;
	
	/**
	 * 邮件主题内容
	 */
	private String text;
	
	/**
	 * 标识
	 */
	private String attachFlag;
	
	/**
	 * 附件类型
	 */
	private String attachType;
	
	/**
	 * 附件名称
	 */
	private String attachName;
	
	/**
	 * 附件内容
	 */
	private String attachContent;
	
	/**
	 * 附件流
	 */
	private InputStream is;
	
	public BaseMessage(){
	}

	public String getFrom() {
		return from;
	}

	public void setFrom(String from) {
		this.from = from;
	}

	public String[] getTo() {
		return to;
	}


	public void setTo(String[] to) {
		this.to = to;
	}

	public String[] getCc() {
		return cc;
	}

	public void setCc(String[] cc) {
		this.cc = cc;
	}

	public String getSubject() {
		return subject;
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}

	public String getText() {
		return text;
	}


	public void setText(String text) {
		this.text = text;
	}


	public String getAttachFlag() {
		return attachFlag;
	}


	public void setAttachFlag(String attachFlag) {
		this.attachFlag = attachFlag;
	}


	public String getAttachType() {
		return attachType;
	}


	public void setAttachType(String attachType) {
		this.attachType = attachType;
	}


	public String getAttachName() {
		return attachName;
	}


	public void setAttachName(String attachName) {
		this.attachName = attachName;
	}


	public String getAttachContent() {
		return attachContent;
	}


	public void setAttachContent(String attachContent) {
		this.attachContent = attachContent;
	}

	public InputStream getIs() {
		return is;
	}

	public void setIs(InputStream is) {
		this.is = is;
	}
}

3.3.4 SSL工具类

package com.modules.common.utils;

import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

/**
 * @Description: 邮箱信证管理
 * @author lc
 */
public class MailTrustManager implements X509TrustManager {

    public void checkClientTrusted(X509Certificate[] cert, String authType) {
        // everything is trusted
    }

    public void checkServerTrusted(X509Certificate[] cert, String authType) {
        // everything is trusted
    }

    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}
package com.modules.common.utils;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;

/**
 * @Description: 邮件ssl
 * @author lc
 */
public class MailSSLSocketFactory extends SSLSocketFactory {
    private SSLSocketFactory factory;

    public MailSSLSocketFactory() {
        try {
            SSLContext sslcontext = SSLContext.getInstance("TLS");
            sslcontext.init(null, new TrustManager[] { new MailTrustManager() }, null);
            factory = sslcontext.getSocketFactory();
        } catch (Exception ex) {
            // ignore
        }
    }

    public static SocketFactory getDefault() {
        return new MailSSLSocketFactory();
    }

    @Override
    public Socket createSocket() throws IOException {
        return factory.createSocket();
    }

    @Override
    public Socket createSocket(Socket socket, String s, int i, boolean flag) throws IOException {
        return factory.createSocket(socket, s, i, flag);
    }

    @Override
    public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr1, int j) throws IOException {
        return factory.createSocket(inaddr, i, inaddr1, j);
    }

    @Override
    public Socket createSocket(InetAddress inaddr, int i) throws IOException {
        return factory.createSocket(inaddr, i);
    }

    @Override
    public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
        return factory.createSocket(s, i, inaddr, j);
    }

    @Override
    public Socket createSocket(String s, int i) throws IOException {
        return factory.createSocket(s, i);
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return factory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return factory.getSupportedCipherSuites();
    }
}

3.4 测试

注意说明

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

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

640?wx_fmt=png

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

开通设置客户端授权密码

640?wx_fmt=png

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

一般都是公司邮件进行发送的,这个设置可以不关注。

源码地址:https://gitee.com/lichao12/spring-boot2-mail

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐