java发送邮件的两种实现方式(包括如何伪造发件人及其原理)

论坛 期权论坛 脚本     
匿名技术用户   2020-12-27 10:58   20   0

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 sora.test.exampl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

public class SendEmailServiceImpl 
{
 private String userName;
 
 private String password;
 
 private String host;
 
 private Socket socket;
 
 private BufferedReader bufferedReader;
 
 private PrintWriter printWriter;
 
 public void sendEmailByJavaToUseSmtp(String sender, String reciver, String ccs)
 {
  try 
  {
   this.socket = new Socket(host, 25);
   this.getReader(socket);
   this.getWriter(socket);
   
   writeCommandStream(null);
   //按照命令行发送邮件的顺序与smtp服务器进行交流
   writeCommandStream("helo hello");//与smtp服务器进行对话
   writeCommandStream("auth login");//登录命令
            //用户名和密码都是用base64进行编码了的,不是普通的字符串
   writeCommandStream(userName);//登录用户用户名
   writeCommandStream(password);//密码
   
   //登录成功之后,设置发件人
   writeCommandStream("mail from:<" + "xxxxxxxx" + ">");//设置发件人,xxxxxx为真实的邮件发送源地址,如xxx@qq.com这种邮箱地址
   //设置收件人,可以设置多个,所以采用遍历方式进行设置
   //参数reciver里面装了所有收件人的邮箱地址,多个邮箱用","号分隔,所以我用逗号拆分
   for (String oneReciver : reciver.split(",")) 
   {
    writeCommandStream("rcpt to:" + oneReciver);
   }
   //开始输入邮件内容
   writeCommandStream("data");//邮件内容,在输入命令data之后开始
   
   //这个地方就是伪造邮件发件人的时候,from之后的字符串任意填,
   //填了之后,收到邮件的人,会看到以这个名字发送的邮件,但是他不能回复,因为这个是伪造的地址,无效的。
   printWriter.println("from:" + "伪造的邮件发件人");
   //收件人,格式和抄送者一样
   printWriter.println("to:" + reciver);
   //这是抄送者,同收件人一样,可以设置多个,中间用,号分隔
   //比如: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实现发邮件的方法了,希望对大家有所帮助,如有错误,望提醒!!!

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP