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实现发邮件的方法了,希望对大家有所帮助,如有错误,望提醒!!!