【Java】JavaMail编程实现邮件客户端-OutBox & InBox

  • 在上一篇《JavaMail编程实现邮件客户端-总览》中我们已经说完了邮箱客户端的登录界面、主界面,在主界面上点击OutBox按钮就能够进入发件箱,点击InBox按钮就能进入收件箱。这篇文章中,会详细介绍OutBox和InBox的界面设计以及功能的实现。
    在这里插入图片描述

OutBox发送邮件.

UI.

  • 邮件的发送界面,我们都不陌生,一般在这个界面中我们需要填写三个部分:
  1. 收件人的邮箱地址;
  2. 邮件的主题;
  3. 邮件的正文以及附件(如果有的话).

下面是网易邮箱的发送界面,我们也是基于这种常见的邮箱发送界面进行的OutBox界面设计:
在这里插入图片描述
而本项目中,发送邮件的OutBox最终GUI效果如下所示:
在这里插入图片描述
界面中有三个待填写的文本输入框,分别对应于收件人邮箱地址、邮件主题和邮件正文。左手边的三个按钮,从上至下的功能依次为:发送编辑好的邮件、退出OutBox和添加附件。退出该界面的代码实现比较容易,只需要使用Java-Swing中提供的API即可:

private void Exit()
{
	int inquire = JOptionPane.showConfirmDialog(ClientSendPage.this,
		"Sure to leave OutBox?","Leave OutBox.",
		JOptionPane.YES_NO_OPTION);
	if(inquire==JOptionPane.YES_OPTION)
	{
		this.dispose();
	}
	else
	{
		this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
	}
}

发送无附件邮件.

  • 当我们编辑好一封邮件——填写了收件人地址、主题以及正文部分以后,例如下图中的状态:
    在这里插入图片描述
    接下来只要点击左侧第一个按钮,就开始进行邮件的发送工作了。那么,客户端程序实际上做了哪些事情呢?在本项目中,有附件的邮件和无附件的邮件是被区别对待的,当SendButton被触发时,绑定在其上的动作首先判断这封待发邮件有没有附件,再决定如何进行发送。
SendButton.addActionListener(new ActionListener()
{
	public void actionPerformed(ActionEvent e)
	{
		try
		{
			if(hasAttachment)
			{
				SendMailPro();
			}
			else
			{
				SendMail();
			}
		}
		catch(Exception ex)
		{
			ex.printStackTrace();
		}
	}
});

由于我们这里的测试邮件是不带有附件的,所以会调用SendMail()方法进行发送。SendMail()方法中,首先需要进行环境的配置,包括邮件发送协议、邮件服务器的地址以及实际发送使用的端口号,我们前面的设计思路中曾经说过这是属于Session类实例对象的内容。本次项目中我们使用的是网易的邮箱客户端进行开发,所以实际的环境配置代码如下:

Properties pro = new Properties();
pro.put("mail.transport.protocol","smtp");
pro.put("mail.smtp.class","com.sun.mail.smtp.SMTPTransport");
pro.put("mail.smtp.host",SMTPServer);
		
/**SMTP port.*/
pro.put("mail.smtp.port","25");
		
/**Verify account.*/
pro.put("mail.smtp.auth","true");
		
session = Session.getInstance(pro, new Authenticator() 
{
	public PasswordAuthentication getPasswordAuthentication() 
	{ 
		return new PasswordAuthentication(Account, Password); 
	}      
});
		
transport = session.getTransport();

上述代码中,第一部分是完成属性的配置,然后封装成一个Session的对象;第二部分从这个Session对象中创建Transport的实例对象。需要注意的是此时,用户已经完成了邮件的编辑,而客户端已经完成了环境的配置,接下来客户端可以对用户编辑好的邮件数据进行封装了。同意是在设计思路中提到过,信息的封装也需要Session提供支持,而收件人地址、邮件主题以及邮件正文的内容,则可以从界面上的文本编辑框中轻松获得,封装信息的代码如下:

//Create a MimeMessage object.
MimeMessage message = new MimeMessage(NewSession);

//Set sender.
message.setFrom(new InternetAddress(Account));
//Set receiver.
message.setRecipients(Message.RecipientType.TO,
	InternetAddress.parse(Receiver.getText()));

//Set Subject.
message.setSubject(Topic.getText());

//Set mail body.
message.setText(MailMessage.getText());

//SAVE CHANGES.
message.saveChanges();

其中一定不能忽视最后的saveChanges(),该方法用于保存并且生成最终的邮件内容。至此客户端已经完成了邮件的封装任务,下一步就是将其交付给已经获取到的Transport对象,进行传输了,代码如下:

transport.connect();
transport.sendMessage(message, message.getAllRecipients());

到这里,客户端已经完成了从配置环境,到封装邮件信息,再到最后的实际发送邮件的任务,接下来只需要在邮件发送成功后,给用户一个发送成功的信息即可。
在这里插入图片描述
稍后我们可以登录到实际的收件方邮箱中查看,是否本项目的第三方客户端真的发送了我们编辑的邮件。下图是[email protected]收件箱中实际收到的邮件内容,可以比较客户端上显示的发送时间和实际收到的邮件的发送时间,确认是同一份邮件。
在这里插入图片描述

添加附件.

  • 现代很多的邮件中都添加了附件进行传输,那么我们在进行邮件封装的时候,就需要考虑如何表示出附件的数据。同样是MimeMessage的实例对象,但与上面代码中
message.setText(MailMessage.getText());

不同的是,这一次我们不仅仅有Text,我们还有附件。我们使用JavaMail中的MimeMultipart来表示一份带有附件的复杂邮件的主体部分,我们依次向其中添加邮件的正文以及附件(如果有多个的话)。

MIME消息的头字段Content-Type有三种类型:multipart/mixedmultipart/relatedmultipart/alternative(一封MIME邮件中的MIME消息可以有这三种组合关系).

  • 其中multipart/mixed表示内容是混合组合类型,内容可以是文本、声音和附件等不同邮件内容的混合体
  • multipart/related表示消息体的内容是关联(依赖)组合类型,例如正文使用HTML代码引用内嵌图片资源等等
  • multipart/alternative表示消息体中的内容是选择组合类型,例如一封邮件的正文同时采用HTML格式和普通文本格式进行表达。这样做的好处在于如果邮件阅读程序不支持HTML格式时,可以采用其中的文本格式进行替换

前面的部分都与简单邮件的封装一致,需要重新编写代码的就是有关封装附件数据的部分,代码展示如下:

//Get mail body text.
MimeBodyPart ContentPart = CreateContent(MailMessage.getText());

//Create mixed MimeMultipart object.
MimeMultipart AllMultiPart  =new MimeMultipart("mixed");

//Add mail body text.
AllMultiPart.addBodyPart(ContentPart);

//Add attachments in FileList.
for(int i=0;i<FileList.size();++i)
{
	AllMultiPart.addBodyPart(FileList.get(i));
}

//setContent() & saveChanges().
message.setContent(AllMultiPart);
message.saveChanges();

关于用户如何选择附件的问题,我们需要用到Java中的JFileChooser,维护一个文件队列来进行多个被选中附件的记录。这部分的代码如下:

private void AppendAttachment() throws Exception
{
	JFileChooser FileChooser = new JFileChooser();
	if(FileChooser.showOpenDialog(ClientSendPage.this)==JFileChooser.APPROVE_OPTION)
	{
		String FileAddr = FileChooser.getSelectedFile().getCanonicalPath();
		if(FileAddr!=null&&FileAddr.length()!=0)
		{
			hasAttachment=true;
			FileName.add(FileAddr);
		}
	}
}

InBox.

UI.

  • 本项目中收件箱的界面设计和发件箱大同小异,在这个界面上,我们可以看到当前收件箱中一共有多少封邮件(由于POP3协议的关系,它无法区分邮件之间的状态,所以不存在已读和未读邮件)。通过一个下拉框,我们可以选择想要查看的邮件并且下载其中的附件(如果有的话)。展示邮件时,我们也是展示三个部分:发送方地址、邮件主题和邮件正文(正文末附加有关附件的信息)。InBox最终的GUI效果如下:
    在这里插入图片描述
    中间部分由于显示邮件的内容,左侧三个按钮分别用于刷新、查看选中邮件的内容以及下载选中邮件的附件,而右侧的一个按钮用于删除选中的邮件。

JComboBox.

  • 此处我们使用下拉框来展示当前收件箱中存在的邮件,JComboBox并没有很复杂的语法,实现的代码如下所示:
MailList = new JComboBox();	
MailList.setBounds(740, 30, 180, 50);
MailList.setMaximumRowCount(5);
		
for(int i=0;i<number;++i)
{
	MailList.addItem("Mail-No."+(i+1));
}
		
MailList.setSelectedIndex(0);

这里的两个方法setMaximunRowCount(int x)是指下列的视图中最多显示几个完整的item,我们从上面的下拉框效果图中也可以看出这一点,而setSelectedIndex(int x)则是设置默认选中的item的序列号(从0开始).
在这里插入图片描述

查看无附件邮件的内容.

  • 无附件邮件的发送和查看都是最简单的情况,当用户选中了一封邮件后,我们就可以通过ViewButton来查看其内容。在设计思路中我们说过可以从Session的实例对象中获取Store的实例对象,而Store的实例化对象表示了某种邮件接收协议(例如POP3协议)的接收对象。当客户端接收邮件时,只需要通过Store对象调用其接受方法,就能够从指定的邮件服务器(例如前面说到的pop.163.com)中获得邮件数据,后续再将其封装在Message对象中。在查看邮件内容时,我们首先进入Store对象中的【inobx】,也就是收件箱文件夹。而后根据用户选中的位置,读取相应的邮件数据封装成Message对象,最后只需要从Message对象中读取邮件的内容显示到用户界面上即可,包括发送方地址、邮件主题和邮件正文。另外需要注意的是,JTextField和JTextArea都是可以设置成不可编辑状态的,通过setEditable(false)即可。查看邮件内容的代码展示如下:
//Open floder with 'READ_WRITE' right.
Folder folder = store.getFolder("inbox");
folder.open(Folder.READ_WRITE);

//Create MimeMessage object.
int Mail_Index = MailList.getSelectedIndex();
MimeMessage ThisMessage = (MimeMessage)((folder.getMessages())[Mail_Index]);

//Set sender.
String sender = String.valueOf((ThisMessage.getFrom())[0]);
from.setText(sender);

//Set topic.
topic.setText(ThisMessage.getSubject());

//Set text.
String textBody = String.valueOf(ThisMessage.getContent());
body.setText(textBody);

//Clse floder.
folder.close(true);

下图是当前[email protected]邮箱中的第1封邮件的内容,后续我们通过InBox来查看这封邮件的内容作为对比:
在这里插入图片描述

在这里插入图片描述

下载邮件中的附件.

  • 当我们在查看内容时,如果客户端发现其中有附件,会从中提取出文本消息部分,并且在界面上Body显示部分的末尾附加上一行提示,实际的代码如下:
if(hasAttachment(ThisMessage))
{
	StringBuffer textbody = new StringBuffer();
	//Get text content.
	GetTextBody(ThisMessage,textbody);
	body.setText(textbody.toString()+"\n\nNOTE:This mail has ATTACHMENT.");
}
else
{
	String textBody = String.valueOf(ThisMessage.getContent());
	body.setText(textBody);
}
  • 代码中的GetTextBody()方法是为了获取邮件中Content中的的文本部分,它会对接收到的参数进行递归获取文本部分的操作。如果获取到了文本部分,就添加到StringBuffer的实例中,如果发现是复杂数据体,就一层一层深入获取文本部分。具体的代码如下:
private StringBuffer GetTextBody(Part part,StringBuffer textbody) throws Exception
{
	boolean hasTextAttach = part.getContentType().indexOf("name")>0;
	//text:Append directly.
	if(part.isMimeType("text/*")&&!hasTextAttach)
	{
		textbody.append(part.getContent().toString());
	}
	//message:getContent().
	else if(part.isMimeType("message/rfc822"))
	{
		GetTextBody((Part)part.getContent(),textbody);
	}
	//multipart:get every part.
	else if(part.isMimeType("multipart/*"))
	{
		Multipart multipart = (Multipart)part.getContent();
		int partCount = multipart.getCount();
		for(int i=0;i<partCount;++i)
		{
			BodyPart bodypart = multipart.getBodyPart(i);
			GetTextBody(bodypart, textbody);
		}
	}

	return textbody;	
}
  • 对于一个封装好的Message对象,我们需要判断其中是否含有附件,这是hasAttachment()方法的目的。hasAttachment()方法的完整代码如下所示:
private boolean hasAttachment(Part part)throws Exception
{
	boolean has = false;
	if(part.isMimeType("multipart/*"))
	{
		MimeMultipart multipart = (MimeMultipart)part.getContent();
		int partCount = multipart.getCount();
		for(int i=0;i<partCount;++i)
		{
			BodyPart bodyPart = multipart.getBodyPart(i);
			String disp = bodyPart.getDisposition();
			if(disp!=null&&
				(disp.equalsIgnoreCase(Part.ATTACHMENT)||
				disp.equalsIgnoreCase(Part.INLINE)))
			{
				has = true;
			}
			else if(bodyPart.isMimeType("multipart/*"))
			{
				has = hasAttachment(bodyPart);
			}
			else
			{
				String contentType = bodyPart.getContentType();
				if(contentType.indexOf("application")!=-1)
				{
					has = true;
				}
				if(contentType.indexOf("name")!=-1)
				{
					has = true;
				}
			}
			if(has)
			{
				break;
			}
		}
	}
	else if(part.isMimeType("message/rfc822"))
	{
		has = hasAttachment((Part)part.getContent());
	}
	return has;
}

这段代码中,我们首先针对那些是MimeMessage类型的邮件,这是第一个if条件表达式要求匹配到multipart/*的结果。进入if分支后说明这一邮件由多个BodyPart组成,我们依次考察其中的每一个BodyPart。后续我们对于每一个BodyPart中的Disposition字段进行判断,该字段的值可以是null、ATTACHMENT或者INLINE.后两个预定义值在Java官方文档中的解释如下:
在这里插入图片描述
而getDisposition()方法在文档中的描述如下,该方法返回的是这一个part所被呈现出来的方式,ATTACHMENT表示它应该被当作附件呈现出来,而INLINE表示它应该被当作某种文本直接显示出来,而null则代表不知道。
在这里插入图片描述
所以这部分代码我自己的想法(如果有错,欢迎指出)是,只要有一个BodyPart的Disposition字段指明了它想要的呈现方式,无论是ATTACHMENT还是INLINE,我们都认为这封邮件中含有了附件,所以让has的值为true;后续如果该BodyPart还是一个复杂的multipart,我们就对其递归地调用hasAttachment()方法;而如果该BodyPart既不是multipart,它的Disposition字段也没有指明,我们就获取它的ContentTyep字段(在Http协议消息头中,使用ContentType来表示具体请求中的媒体类型信息),Java文档中对于getContentType()方法的描述如下:
在这里插入图片描述
当中提到了MIME typing system,关于MIME类型的完整列举,可以参看MIME参考。在这部分代码中,我们使用getContentType()方法获得了表示BodyPart的MIME类型的字符串,而后我们在该字符串中查找"application"和"name"两个字串,如果存在,就认为该消息中含有附件,令has的值为true。回到最外层的if分支,如果消息的类型是"message/rfc822",我们就对该消息的内容调用hasAttackment()方法。

  • 实际下载邮件中的附件并保存到本地。当用户查看邮件内容时,发现邮件正文的末尾有一句客户端的提示"This mail has ATTACHMENT.",此时用户触发下载附件按钮后,就开始进行附件的下载。下载附件的逻辑过程很清晰,我们对于那些确实拥有附件的邮件,执行下载操作;而如果是一封本没有附件的邮件,我们就给出错误信息提示用户这封邮件并没有附件。实际的下载操作中,我们还是需要将复杂体邮件multipart一层一层地剥开,对它的每一个BodyPart进行下载操作,而如果BodyPart还是一个复杂体multipart,我们就对其递归调用下载函数。其中最核心的操作,是我们找到了被封装的附件,将其下载到本地的某个位置,也就是项目中的SaveFile()方法,其代码如下:
private void SaveFile(InputStream is,String savePosition,String fileName) throws Exception
{
	BufferedInputStream bis = new BufferedInputStream(is);
	BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(
		new File(savePosition+fileName)));
	int index=-1;
	while((index=bis.read())!=-1)
	{
		bos.write(index);
		bos.flush();
	}
	bos.close();
	bis.close();
}

SaveFile的策略是一头连接着输入流,也就是待下载的附件,另一头连接着本地的某个文件位置,向其中写入数据。下图是一封带有附件的邮件,我们给出第三方客户端和网易邮箱的实际收件箱中的页面,保证该附件是确实存在的。
在这里插入图片描述
在这里插入图片描述
点击下载附件的JButton,再查看预先指定的保存位置,就能看到被下载好的附件。
在这里插入图片描述
在这里插入图片描述

删除指定的邮件.

  • 当用户选定了一封邮件,并且触发左侧的删除JButton,就可以删除掉该邮件。删除操作实际上只是给对这封邮件的标识进行了处理,将其修改为"DELETED",这样从store对象中打开的收件箱Floder对象就能够对已经标记为DELETED的邮件进行清理。
private void DeleteMail(int index) throws Exception
{
	Folder folder = store.getFolder(folderName);
	if(folder==null)
	{
		throw new Exception(folderName+" does not exist.");
	}
	folder.open(Folder.READ_WRITE);
	int inquire = JOptionPane.showConfirmDialog(ClientCheckPage.this,
			"Sure to delete Mail-No."+(index+1)+" ?","Delete Mail.",
			JOptionPane.YES_NO_OPTION);
	if(inquire==JOptionPane.YES_OPTION)
	{
		Message DeleteMessage = (folder.getMessages())[index];
		DeleteMessage.setFlag(Flags.Flag.DELETED, true);
		JOptionPane.showMessageDialog(ClientCheckPage.this, "Mail-No."+(index+1)+
			" has been deleted.");
	}
	else
	{
		this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
	}
	folder.close(true);
}

需要注意的是,最后一定要执行folder.close(true),否则表示收件箱的folder对象无法使删除操作生效。现在我们删除掉Megatron邮箱中的第5份邮件,再点击刷新JButton:
在这里插入图片描述
在这里插入图片描述
此时登录到网易邮箱的页面查看,发现该邮件确实已经被删除:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44246009/article/details/107464872