java发送邮件的两种通用方法

一、

本文讲解的是基于smtp协议,发送邮件的方法(一种是底层实现,一种是利用第三方jar包)。而关于smtp协议,不了解的可以在网上搜一下,有很多资料并且很容易懂;不过不了解也没关系,只需要知道,smtp协议存在一个安全漏洞,就是smtp协议允许你两次设置发件人和收件人信息。第一次发送命令行mail from:真正的发送邮件的源地址 ;第二次则是在发送data命令之后,开始写邮件内容。在写邮件内容时,还能再一次设置发件人、收件人、抄送者等信息(在data里面写的发件人、收件人、抄送人信息,只能显示,其实没有其他作用,比如你在设置收件人的命令里面没有写123456@qq.com这个邮件地址,但是你在data命令之后,抄送者里面输入了123456@qq.com这个地址,最后这封邮件并不会发给这个抄送人,只是在邮件的抄送者这一栏里面,有这么一个邮箱账号。所以要真的发送给这些人,只有在最开始设置发件源之后,设置收件源,可以多个)。

顺便说一下笔者最开始写邮件在网上遇到的大坑:笔者写邮件的背景是,利用公司邮箱公共账号(比如公共账号名字是public),将一封邮件发送抄送给一些人,但是要求发件人不能是公共账号,因为一些员工设置的邮件过滤,可能会导致用公共账号名字发送的邮件被直接扔垃圾箱,导致员工看不到邮件,但是利用公共账号发送的邮件,对方接收的时候显示的就是公共账号的名字,即public(PS:修改邮件发件人昵称,并不能修改接收方看到的发件人名字,昵称只提供在邮件正文里面,实际上邮箱显示的发件人还是公共账号的名字,比如你修改发件人昵称为test,其实对方收到的提醒还是public发送的邮件,并不是test发送的邮件,只有对方点开这封邮件,才会在邮件里面看到test这个昵称。),而且,可能是笔者自己的原因,网上那些利用javax.mail包,设置昵称的办法(就是这种:InternetAddress senderEmailAddress = new InternetAddress(nick + "<xxxxx@qq.com>")),笔者这里根本不管用,最后看了很多源码之后,终于把昵称设置好了(这种方法message.setHeader("Sender", "我是昵称")),结果却发现,设置的昵称根本不能伪造发件人,当时笔者心里是非常崩溃的(尼玛,搞了半天,好不容易搞定了昵称,居然发现没有起到想要的效果,最后笔者只有了解smtp协议,然后用Java进行底层实现),所以,笔者要告诉大家的是,使用java封装好的第三方jar包发送邮件,不能伪造发件人,不能伪造,不能伪造,重要的事说三遍,详细的情况在后面会贴一部分源码讲解。

二、基于smtp协议发送邮件(该方法能够伪造任意发件人)

package cn.su.core.util;

import java.io.*;
import java.net.Socket;
import java.util.Base64;

/**
 * @Author: su rui
 * @Date: 2021/6/15 10:36
 * @Description: 伪造邮件发件人工具类
 */
public class ForgeEmailSenderUtil {
    private static final String defaultHost = "smtp.exmail.qq.com";
    private static final int defaultPort = 25;
    private String host = "";
    private Integer port = null;
    private String userName = "test@qq.com";
    private String password = "xxxxx";
    private Socket socket;
    private BufferedReader bufferedReader;
    private PrintWriter printWriter;

    public ForgeEmailSenderUtil setSenderAccount(String userName, String passwordOrAuthCode) {
        this.userName = userName;
        this.password = passwordOrAuthCode;
        return this;
    }

    public ForgeEmailSenderUtil setEmailHostAndPort(String host, int port) {
        this.host = host;
        this.port = port;
        return this;
    }

    private Socket createSocket() {
        try {
            return null == host || host.trim().length() == 0 ? new Socket(defaultHost, defaultPort) : new Socket(host, port);
        } catch (Exception e) {
            throw new IllegalArgumentException("创建会话失败,请稍后重试");
        }
    }

    public void sendForgeSenderEmail(String sender, String recipient, String ccs) {
        sender = null == sender || sender.trim().length() == 0 ? userName : sender;
        try {
            String baseUserName = Base64.getEncoder().encodeToString(userName.getBytes("UTF-8"));
            String basePassword = Base64.getEncoder().encodeToString(password.getBytes("UTF-8"));
            this.socket = createSocket();
            this.bufferedReader = getReader(socket);
            this.printWriter = getWriter(socket);

            writeCommandStream(null);
            //按照命令行发送邮件的顺序与smtp服务器进行交流
            writeCommandStream("helo hello");//与smtp服务器进行对话
            writeCommandStream("auth login");//登录命令
            //用户名和密码都是用base64进行编码了的,不是普通的字符串
            writeCommandStream(baseUserName);//登录用户用户名
            writeCommandStream(basePassword);//密码

            //登录成功之后,设置发件人
            writeCommandStream("mail from:<" + userName + ">");//设置发件人,xxxxxx为真实的邮件发送源地址,如xxx@qq.com这种邮箱地址
            //设置收件人,可以设置多个,所以采用遍历方式进行设置
            //参数reciver里面装了所有收件人的邮箱地址,多个邮箱用","号分隔,所以我用逗号拆分
            for (String oneReciver : recipient.split(",")) {
                writeCommandStream("rcpt to:" + oneReciver);
            }
            //开始输入邮件内容
            writeCommandStream("data");//邮件内容,在输入命令data之后开始

            //这个地方就是伪造邮件发件人的时候,from之后的字符串任意填,
            //填了之后,收到邮件的人,会看到以这个名字发送的邮件,但是他不能回复,因为这个是伪造的地址,无效的。
            printWriter.println("from:" + sender);
            //收件人,格式和抄送者一样
            printWriter.println("to:" + recipient);
            //这是抄送者,同收件人一样,可以设置多个,中间用,号分隔
            //比如:xxx@qq.com,xxxxxx@qq.com,xxxx@qq.com
            printWriter.println("Cc:" + ccs);

            //设置邮件主题
            printWriter.println("subject:" + "这是邮件主题");
            //设置邮件正文
            //注意下面这个设置类型的,这一句代码是必须的,不然你发的邮件的正文内容是不会存在的
            //笔者最开始没有设置邮件正文类型,发了很多封,但是每一封邮件的正文内容都为空,后来才发现必须加上这个
            printWriter.println("Content-Type:text/html;");//这个是HTML格式的邮件正文,如果是纯文本,用text/plain
            //注意这个空行是必须的,设置好了类型,需要空一行再起一行输入正文内容
            printWriter.println();

            printWriter.println("<span>这是邮件的内容,该邮件是一封HTML格式的邮件,如果要切换邮件格式,"
                    + "设置conten-type的值就可以改变,当然还可以加上超链接<a href=\"xxxx\">这是超链接</a></span>");

            printWriter.println();
            //结束邮件发送"."命令
            writeCommandStream(".");
            //关闭
            writeCommandStream("quit");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                printWriter.close();
                bufferedReader.close();
                socket.close();
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
    }

    private PrintWriter getWriter(Socket socket) throws IOException {
        OutputStream socketOut = socket.getOutputStream();
        return new PrintWriter(socketOut, true); //注意设置为true
    }

    private BufferedReader getReader(Socket socket) throws IOException {
        InputStream socketIn = socket.getInputStream();
        return new BufferedReader(new InputStreamReader(socketIn));
    }

    private void writeCommandStream(String command) throws IOException {
        if (command != null) {
            printWriter.println(command);
            printWriter.flush();
            System.out.println("客户端命令行信息→" + command);
        }

        char[] serviceResponse = new char[1024];

        bufferedReader.read(serviceResponse);
        System.out.println("服务器响应→" + new String(serviceResponse));
    }
}

三、基于javax.mail包进行邮件发送

就笔者而言,利用该jar包进行邮件发送,没有真正实现伪造发件人,只能设置邮件发件人昵称,之前看网上很多伪造都是设置邮件服务器属性smtp.auth为false,意思就是不对邮件进行用户验证等操作。笔者在设置之后,发送邮件只会提示,作为该发送者没有权限,或者xxxxx权限验证失败等提示。

另外关于设置昵称,网上这种方法其实是不能设置昵称的(也可能是笔者太垃圾,这里只是代表我个人看法,说不定以后我自己也会发现是错,现在就讲讲当时我看源码的理解,因为资源原因,源码以后会陆续贴上)

public void sendEmailByJar(String sender, String recvier, String cc)
	{
		//设置邮件服务器参数
		Properties props = new Properties();
		
		props.put("mail.smtp.host", host);
		props.put("mail.smtp.auth", "true");
		props.put("mail.transport.protocol", "smtp");
		
		//设置邮件Session对象,同时配置验证方法
		//注意这里的Session是javax.mail.session包的Session,利用该Jar包,这个Session是必须的,
		//关于邮件的一切信息,都是通过这个session进行创建的
		Session session = Session.getInstance(props, new javax.mail.Authenticator()
				{
					protected PasswordAuthentication getPasswordAuthentication()
					{
						return new PasswordAuthentication(userName, password);
					}
				});
		
		//网上大多数设置昵称的方法,至少笔者使用该方法不管用
		String nick = null;
		
		try 
		{
			nick = javax.mail.internet.MimeUtility.encodeText("我是昵称");
		} 
		catch (Exception e) 
		{
			e.printStackTrace();
		}
		
		try 
		{
			//创建Message对象,并设置相关参数
			InternetAddress senderEmailAddress = new InternetAddress(nick + "<xxxx>");
			
			//设置抄送者,cc参数里面是多个邮箱,用,号分隔
			@SuppressWarnings("static-access")
			InternetAddress[] ccsAddress = new InternetAddress().parse(cc);
			
			@SuppressWarnings("static-access")
			InternetAddress[] reciverAddress = new InternetAddress().parse(recvier);
			
			Message message = new MimeMessage(session);
			
			//笔者亲测设置邮件发件人昵称的方法,至少笔者设置成功
			//顺便讲一下Message对象里面的header属性,笔者调试的时候,发现Message对象header属性保存了我们写的邮件的所有信息
			//里面有from,sender,to,cc,subject,content-type(包括resent-to,resent-from等,好像是重发邮件的属性)等属性,目测就是对应邮件的各个信息
			//所以,其实邮件的所有信息,我们都可以通过messaget.setHeader("键", "值")来设置
			//比如我们调用的设置邮件发件地址的方法setFrom(xxxxx),其实等同于setHeader("From", "xxxxx"),
			//如果你同时使用了俩个方法setFrom,setHeader("From", "xxx"),那么后一个会覆盖前一个的值
			//这里讲一下我理解的为什么网上设置昵称的方法不起作用的原因:网上设置的昵称都是在setFrom()方法里面设置的
			//而阅读源码,我们会发现,setFrom里面的值,会被拆分到俩个字段里面保存:personal字段和address字段
			//其中,你设置的nick昵称就会被保存在personnal字段,而邮箱地址会被保存在address字段
			//同时,你在源码里面也能找到smtp协议的命令行语句mail from这些命令
			//源码里面,我只看到了这些必要的命令行:发件人mail from ,接收者rcpt to,正文data,结束.  
			//其中,data源码是用一个流写入的,所以具体写的,怎么解析的我们设置的参数我也没看懂,但是实验证明就是不能伪造发件人
			//而mail from,设置的参数的值,是从address字段取的,并没有取你设置的昵称personnal,所以直接设置昵称在from这个header的值是无效的
			//rcpt to是从你的收件人里面取的值。
			//而笔者成功的昵称设置,是通过设置setHeader("Sender", "xxx")成功的,所以可以猜测,源码解析的时候,取昵称是从这个字段sender里面取的
			//那么其实最后jar包源码里面,设置smtp mail from还是设置的邮箱,并没有带上你设置的昵称
			//所以笔者认为这个就是使用网上方法设置昵称不管用的原因(笔者的观点,可能会有错,毕竟笔者源码也没有完全看懂)
			//另外,setFrom()设置的值,必须和登录验证用的用户名和密码的账号匹配,不然就会报权限验证错误,所以这也是笔者认为不能伪造的根本原因
			message.setHeader("Sender", "nick");
			
			message.setFrom(senderEmailAddress);//该方法等同于message.setHeader("From","xxx");
			message.setRecipients(Message.RecipientType.CC, ccsAddress);
			message.setRecipients(Message.RecipientType.TO, reciverAddress);
			message.setSubject("主题");
			message.setText("简单文本邮件");
			
			//不管是调用Transport静态方法send,还是通过session获取transport,在链接,在发送,其实都一样,源码已经帮我们处理好了
			//如果调用静态方法,源码会获取session对象并用session创建一个transport,如果获取到session对象为null,会创建一个默认的session对象
			Transport.send(message);
			
			
		} 
		catch (Exception e) 
		{
			e.printStackTrace();
		}
	}

这就是笔者总结的两种java实现发邮件的方法了,希望对大家有所帮助,如有错误,望提醒!!!