java發送郵件的兩種實現方式(包括如何僞造發件人及其原理)

                         java發送郵件的兩種通用方法

一、

本文講解的是基於smtp協議,發送郵件的方法(一種是底層實現,一種是利用第三方jar包)。而關於smtp協議,不瞭解的可以在網上搜一下,有很多資料並且很容易懂;不過不了解也沒關係,只需要知道,smtp協議存在一個安全漏洞,就是smtp協議允許你兩次設置發件人和收件人信息。第一次發送命令行mail from:真正的發送郵件的源地址 ;第二次則是在發送data命令之後,開始寫郵件內容。在寫郵件內容時,還能再一次設置發件人、收件人、抄送者等信息(在data裏面寫的發件人、收件人、抄送人信息,只能顯示,其實沒有其他作用,比如你在設置收件人的命令裏面沒有寫[email protected]這個郵件地址,但是你在data命令之後,抄送者裏面輸入了[email protected]這個地址,最後這封郵件並不會發給這個抄送人,只是在郵件的抄送者這一欄裏面,有這麼一個郵箱賬號。所以要真的發送給這些人,只有在最開始設置發件源之後,設置收件源,可以多個)。

順便說一下筆者最開始寫郵件在網上遇到的大坑:筆者寫郵件的背景是,利用公司郵箱公共賬號(比如公共賬號名字是public),將一封郵件發送抄送給一些人,但是要求發件人不能是公共賬號,因爲一些員工設置的郵件過濾,可能會導致用公共賬號名字發送的郵件被直接扔垃圾箱,導致員工看不到郵件,但是利用公共賬號發送的郵件,對方接收的時候顯示的就是公共賬號的名字,即public(PS:修改郵件發件人暱稱,並不能修改接收方看到的發件人名字,暱稱只提供在郵件正文裏面,實際上郵箱顯示的發件人還是公共賬號的名字,比如你修改發件人暱稱爲test,其實對方收到的提醒還是public發送的郵件,並不是test發送的郵件,只有對方點開這封郵件,纔會在郵件裏面看到test這個暱稱。),而且,可能是筆者自己的原因,網上那些利用javaemail包,設置暱稱的辦法(就是這種:InternetAddress senderEmailAddress = new InternetAddress(nick + "<[email protected]>")),筆者這裏根本不管用,最後看了很多源碼之後,終於把暱稱設置好了(這種方法message.setHeader("Sender", "我是暱稱")),結果卻發現,設置的暱稱根本不能僞造發件人,當時筆者心裏的非常崩潰的(尼瑪,搞了半天,好不容易搞定了暱稱,居然發現沒有起到想要的效果,最後筆者只有瞭解smtp協議,然後用Java進行底層實現),所以,筆者要告訴大家的是,使用java封裝好的第三方jar包java發送郵件,不能僞造發件人,不能僞造,不能僞造,重要的事說三遍,詳細的情況在後面會貼一部分源碼講解。

二、基於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");//登錄命令
			writeCommandStream(userName);//登錄用戶用戶名
			writeCommandStream(password);//密碼
			
			//登錄成功之後,設置發件人
			writeCommandStream("mail from:<" + "xxxxxxxx" + ">");//設置發件人,xxxxxx爲真實的郵件發送源地址,如[email protected]這種郵箱地址
			//設置收件人,可以設置多個,所以採用遍歷方式進行設置
			//參數reciver裏面裝了所有收件人的郵箱地址,多個郵箱用","號分隔,所以我用逗號拆分
			for (String oneReciver : reciver.split(",")) 
			{
				writeCommandStream("rcpt to:" + oneReciver);
			}
			//開始輸入郵件內容
			writeCommandStream("data");//郵件內容,在輸入命令data之後開始
			
			//這個地方就是僞造郵件發件人的時候,from之後的字符串任意填,
			//填了之後,收到郵件的人,會看到以這個名字發送的郵件,但是他不能回覆,因爲這個是僞造的地址,無效的。
			printWriter.println("from:" + "僞造的郵件發件人");
			//收件人,格式和抄送者一樣
			printWriter.println("to:" + reciver);
			//這是抄送者,同收件人一樣,可以設置多個,中間用,號分隔
			//比如:[email protected],[email protected],[email protected]
			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實現發郵件的方法了,希望對大家有所幫助,如有錯誤,望提醒!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章