My Avatar

Shadow

I love bleak day, like something will happen

Mail Pipe

2016年11月03日 星期四, 发表于 北京

如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)

  简聊有一个功能是,对每一个讨论组,都有一个专门的邮箱,只要你往这个邮箱发邮件,邮件就能被转发到这个讨论组,amazing有没有!! 如下图:

  而我们也想要在我们的IM系统里也实现这样一套机制,就小小地研究了下如何实现。


简聊的实现方式

  一开始我们想,这种方式肯定是自己搭了个邮件服务器,然后为每个讨论组建立一个邮箱账号,接着,将发到这个账号的邮件都转发到自己的服务中去,但是具体细节还是不清楚,后来为了具体地了解简聊是如何做的,正好某天闲逛某乎时,发现有个答主是简聊的开发小哥,我就主动提问了:小哥,你们的简聊是如何实现转发邮件到讨论组的呀(求知脸 :))然后就发生了如下对话:

  相信机智的你已经看懂了一切,好吧,我把mailgun的官网贴出来,我只能帮你到这了:mailgun

  但是!!!mailgun要收费啊,按邮件量收费啊,天下果然没有免费的午餐啊,而且企业级邮件从外面服务器经过也不太好吧,那好吧,我们还是老老实实按照最开始想的那套方案来自己实现吧。

我们的实现方式

  首先,选了个邮件服务器,iRedmail,http://www.iredmail.com/,这家伙就是把一堆开源框架整合在一起,为你省去了大量复杂的配置过程,让你一键简单搭建自己的邮箱服务,其中最重要的组件是postfix(smtp协议 发邮件的)和dovecot(pop3和imap 收邮件的)这两个邮箱服务器。

  选了iRedmail后,就去下载然后搭建起来吧~搭建过程就不讲了,下载安装包一键安装吧,因为也不是我搭建的,so,just explore it

  搭好之后,我们会发现接下来会出现一堆问题需要你来解决哦,下面我们就来一一解决这些问题:

  如何正确的收发邮件

  搭建完邮箱服务器之后不代表你就能收发邮件了,首先,你得去域名DNS管理处,配置域名解析,首先加一个A类型记录(A代表地址,也就是将域名解析到ip地址),ip地址设为邮箱服务器ip,然后加一个MX类型记录(MX代表邮箱服务器,就是人家发邮件到xxx@domain.com时,会根据domain.com域名下的MX记录来找邮箱服务器),可以参考如下图:

  然而,事情总不会就这么顺利,因为,我们的邮箱服务器是内网机器,外网根本访问不到,只能是通过外网访问我们的nginx服务器,然后让nginx转发到内网机器,所以我们需要了解nginx是否能支持邮件服务的转发,如果可以支持,那么将上面的DNS配置,配置为nginx服务器的ip即可。

  那么强大的nginx,怎么可能不支持mail服务的转发呢,手动滑稽 呵呵呵呵。那么如何配置nginx呢?

  首先,linux版本的nginx,下载下来的是source code,需要手动编译,在configure时,如果不加参数,默认是没有添加mail module的,所以我们下载了nginx的源码包后,进入目录内,应该这样:

1
2
./configure --with-mail
make && make install

  当然如果你是在windows上,默认是所有模块都添加了的,你就不用管这些了。

  ok,安装好nginx,我们要在配置文件里加上如下这段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mail {
   server_name mail.yonyouyc.com;
   auth_http  xx.xx.xx.xx:8080/quercus-4.0.39/auth.php;
   pop3_capabilities  "TOP"  "USER";
   imap_capabilities  "IMAP4rev1"  "UIDPLUS";

   server {
        listen     111;
        protocol   pop3;
        proxy      on;
   }
   server {
        listen     144;
        protocol   imap;
        proxy      on;
   }
   server {
        listen    2525;
        protocol    smtp;
        proxy    on;
        smtp_auth login plain;
        xclient    off;
   }
}

  你需要注意的是其中auth_http这个参数,这个参数代表访问邮件服务前的权限认证,虽然pop3和imap协议本身在经过nginx后自己会做权限验证,但是smtp协议并不会再做权限验证,因此这里必须要配置一个权限验证的服务地址,其实就是一个http server,能接受一个post请求,并验证权限,我这里用了一个php小程序来验证(网上找的),然后通过quercus这个东西能部署在tomcat上,方便快速实现一个权限验证小程序,当然你完全可以自己实现,下面我就把这个php代码放上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
/**
* @see xiabaibai.net
*/
if(!isset($_SERVER ["HTTP_AUTH_USER"] ) || ! isset($_SERVER ["HTTP_AUTH_PASS"] )) {
	fail(0);
}
$username = $_SERVER ["HTTP_AUTH_USER"];
$userpass = $_SERVER ["HTTP_AUTH_PASS"];
$protocol = $_SERVER ["HTTP_AUTH_PROTOCOL"];
$backend_port = 110;
if($protocol == "imap") {
	$backend_port = 143;
} elseif ($protocol == "smtp") {
	$backend_port = 25;
}
list($uid, $domain) = explode("@", $username);
$auth = authuser($username, $userpass);
if(!$auth) fail (-2);
pass($_SERVER["SERVER_ADDR"], $backend_port);
//自己实现权限验证逻辑,我这里就省去了权限验证
function authuser($user, $pass) {
	return true;
}
function fail($code) {
	switch($code){
    	case 0: header("Auth-Status: Parameter lost"); break;
    	case -1: header("Auth-Status: No Back-end Server"); break;
    	case -2: header("Auth-Status: Invalid login or password" ); break;
	}
exit();
}
function pass($server, $port) {
	header("Auth-Status: OK" );
	header("Auth-Server: 172.20.4.28" );
	header("Auth-Port: $port" );
	exit();
}

?>

  需要注意的是,这个权限验证服务,需要返回给nginx的数据,都在header中,基本的包括Auth-Status、Auth-Server(这个就是邮箱服务器的ip地址,不能是域名)、Auth-Port(这个就是邮箱服务器本身协议所使用的端口号,一般默认的pop3为110,imap为143,smtp为25)

  其他的nginx参数就没什么好说了,自己查去,需要注意的各个协议的端口号,这是你外界client要连接服务器时需要指定的端口,如果端口冲突,nginx会报错的,放心。

  这样配置好后,你就可以通过一些邮箱客户端来连接邮箱服务器了:例如我用foxmail来连接:

  注意pop服务器和smtp服务器,都写的mail.yonyouyc.com,因为我们的DNS配置已经写好了,所以它会定向到我们的nginx服务器,所以这里,如果你直接写nginx服务器的外网ip也是可以的。

  这样我们就可以正常滴收发邮件啦~~

  如何为讨论组自动建立邮箱账户

  一开始为这个问题还挺苦恼,后来发现,iRedmail已经提供了自动化脚本来建立邮箱账户,当你安装好iRedmail后,进到iRedmail的安装目录,你会发现下面有个tools目录,进去后有一堆脚本,其中有一个:create_mail_user_SQL.sh,按如下方式使用:

1
./create_mail_user_SQL.sh yonyouyc.com liangjfc

  这样就会在当前目录下,生成一个output.sql文件,我们只要进到mysql(iRedmail默认使用的数据库),执行这些语句就可以创建相应的域为yonyouyc.com下的用户为liangjfc的账号了,如果想通过脚本执行,可以按照如下语句:

1
2
3
1. ./create_mail_user_SQL.sh yonyouyc.com liangjfc
2. vi output.sql  增加USE vmail; 用户表在这个库里
3. mysql -uroot -pXXXXXXXXXXX < output.sql       这样就可以了。

  但是,既然我们知道,要新建用户就是在数据库里插入一条数据,那我们完全可以在自己的IM系统里,直接生成sql语句(参考它生成的output.sql可以知道要生成什么样的SQL语句),然后连接到邮箱服务器上的mysql数据库,并且执行相应的sql语句即可。

  但是,我们发现,有一个关键点需要解决的就是数据库里的用户密码是加密了,而这个脚本用的加密算法是用python实现的,所以我们需要用java实现一套相应的加密算法来为密码属性进行加密,以SHA512为例,我们的java加密实现如下:

1
2
3
4
5
6
7
8
byte[] salt = new byte[8];
Random random = new Random();
random.nextBytes(salt);
byte[] digest = CryptoUtils.SHADigest(passwd, salt,"SHA512");
byte[] result = new byte[digest.length + salt.length];
System.arraycopy(digest, 0, result, 0, digest.length);
System.arraycopy(salt, 0, result, digest.length, salt.length);
return "{SSHA512}" + Base64.getEncoder().encodeToString(result);

  其中CryptoUtils.SHA512Digest方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static byte[] SHADigest(final String strText, final byte[] salt, final String strType) {
	// 是否是有效字符串
	if (strText != null && strText.length() > 0) {
  		try {
    		// SHA 加密开始
    		// 创建加密对象 并傳入加密類型
    		MessageDigest messageDigest = MessageDigest.getInstance(strType);
    		// 传入要加密的字符串
    		messageDigest.update(strText.getBytes());
    		messageDigest.update(salt);
    		// 得到 byte 類型结果
    		byte byteBuffer[] = messageDigest.digest();

    		return byteBuffer;
  		} catch (NoSuchAlgorithmException e) {
    		e.printStackTrace();
  		}
	}
	return null;
}

  OK,这样,我们就能生成sql语句来创建邮箱账号了

  如何将邮件转发到IM系统中

  其实一开始我就觉得这块才是最难搞定的。一开始,我想的是邮箱服务器接到邮件之后,邮件是以MIME message的格式以本地文件存在磁盘上的,那么我们用logstash去监控收件目录,如果发现有新的文件,将文件内容发送到IM系统中去,但是这样,我们没法知道什么时候邮件算结束了,后来想到用多行处理那个可以根据邮件开头的标识,将这个标识之下的所有信息作为一行来处理,但是这样也会有很多问题,例如,如果下一封邮件没到之时,这一封邮件的信息是不会发出去的(因为要等遇到下一封邮件的开头标识,前一封邮件才会作为一行信息发送出去),还有并发的问题等等。所以这个方案还是舍弃了,那么只能看看iRedmail官网是不是有相关信息了,然后在他们的documentation页,还真被我找到了这样一个页面:http://www.iredmail.com/docs/pipe.incoming.email.for.certain.user.to.external.script.html

  这个邮件pipe不正是我们想要的功能吗!!!

  苍天有眼啊,而且看上去还挺简单,ok,干

  首先,修改,postfix的配置,我们知道,其实iRedmail使用postfix去收邮件,然后转给dovecot的,在/etc/postfix/master.cf的最底下加上:

1
2
external-pipe   unix    -   n   n   -   -   pipe
	flags=DRhu user=vmail:vmail argv=/path/to/your/external/script.sh -f ${sender} -d ${user}@${domain} -m ${extension}

  然后,执行postfix reload,来重启postfix

  接着你需要哪个邮箱账号的邮件被pipe到脚本处理,你就修改这个账号的数据库信息:

1
2
sql> USE vmail;
sql> UPDATE mailbox SET transport='external-pipe' WHERE username='user@domain.ltd';

  简单来说就是将你开始插数据的那个表里面的transport属性,置为external-pipe即可,这样我们其实在开始创建邮箱账号的时候,就可以完成这件事。

  接下来重点在于这个脚本怎么写,看了postfix关于pipe的文档:http://www.postfix.org/pipe.8.html

  死活都没找到,怎么获得邮件内容。。。上面的什么${sender} ${user} ${domain}都可以获得,并且通过参数传递给脚本,但是邮件的具体内容呢???怎么获得??这个地方我是研究了半天,百度就算了,关于这种东西,百度基本上搜不到啥,后来用google查了下,发现,妈蛋,原来是标准输入即stdin,会将邮件内容传递给脚本!!

  知道之后,经过不断地实验,我终于完成了最终版本的脚本文件如下(其中之纠结只有我寄几知道 可怜脸 :( ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cd /tmp
mkdir report-$$
cd report-$$
cat > input

num=$(cat input | wc -l)

mail=$(sed -n 1,${num}p input)

curl -H "Content-Type: application/json" -X POST  --data "$mail" xxxx/im/message/send

cd /tmp
rm -rf report-$$

exit 0

  简单来说,就是通过cat > input将输入先放到一个临时文件input里面,然后你想怎么处理这个文件,就看你自己了。我这里是将文件内容原封不动地发送到我们的IM服务里,再做转发。

  神坑!!      奇怪!为什么每篇都有神坑。。

  对于不太熟shell脚本的我,真是一脸辛酸泪,一开始我用read去读标准输入,老是丢一行开头的空白符,然后echo也是各种丢,后来查了才知道,这两个方法就是会丢开头的空白。。。。。。还是用sed靠谱,其他各种神坑,我就不一一详述了,你们自己去体会,shell脚本真难写,吐槽脸的我 - -  

  如何处理MIME格式的邮件内容

  通过上面的脚本发送过来的字符串,是一个MIME格式的邮件内容,那我们要如何解析呢?其实用javamail就可以了:

1
2
3
4
5
Session s = Session.getDefaultInstance(new Properties());
InputStream is = new ByteArrayInputStream(mail.getBytes());
MimeMessage message = new MimeMessage(s, is);

ReciveOneMail mail = new ReciveOneMail(message);

  其中ReciveOneMail是一位小哥自己对mimemessage的封装,可以方便获取邮件的各项属性,如发送方、接收方、主题、内容、附件、日期等等,附上链接:http://blog.csdn.net/coder_giser/article/details/48783809,算了,给你们贴出来吧 = =

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
	package com.yonyou.nccpub.webim.server.msgcenter.mail;

	import java.io.*;
	import java.text.*;
	import java.util.*;

	import javax.mail.*;
	import javax.mail.internet.InternetAddress;
	import javax.mail.internet.MimeMessage;
	import javax.mail.internet.MimeUtility;

	/**
	* 有一封邮件就需要建立一个ReciveMail对象
	*/
	public class ReciveOneMail {
		private MimeMessage mimeMessage = null;
		private String saveAttachPath = ""; // 附件下载后的存放目录
		private StringBuffer bodytext = new StringBuffer();// 存放邮件内容
		private String dateformat = "yy-MM-dd HH:mm"; // 默认的日前显示格式

		public ReciveOneMail(MimeMessage mimeMessage) {
    		this.mimeMessage = mimeMessage;
		}

		public void setMimeMessage(MimeMessage mimeMessage) {
    		this.mimeMessage = mimeMessage;
		}

		/**
		* 获得发件人的地址和姓名
		*/
		public String getFrom() throws Exception {
    		InternetAddress address[] = (InternetAddress[]) mimeMessage.getFrom();
    		String from = address[0].getAddress();
    		if (from == null)
      			from = "";
    		String personal = address[0].getPersonal();
    		if (personal == null)
      			personal = "";
    		String fromaddr = personal + "<" + from + ">";
    		return fromaddr;
		}

		/**
		* 获得邮件的收件人,抄送,和密送的地址和姓名,根据所传递的参数的不同 "to"----收件人 "cc"---抄送人地址 "bcc"---密送人地址
		*/
		public String getMailAddress(String type) throws Exception {
    		String mailaddr = "";
    		String addtype = type.toUpperCase();
    		InternetAddress[] address = null;
    		if (addtype.equals("TO") || addtype.equals("CC") || addtype.equals("BCC")) {
      			if (addtype.equals("TO")) {
        			address = (InternetAddress[]) mimeMessage.getRecipients(Message.RecipientType.TO);
      			} else if (addtype.equals("CC")) {
        			address = (InternetAddress[]) mimeMessage.getRecipients(Message.RecipientType.CC);
      			} else {
        			address = (InternetAddress[]) mimeMessage.getRecipients(Message.RecipientType.BCC);
      			}
      			if (address != null) {
        			for (int i = 0; i < address.length; i++) {
          				String email = address[i].getAddress();
          				if (email == null)
            				email = "";
          				else {
            				email = MimeUtility.decodeText(email);
          				}
          				String personal = address[i].getPersonal();
          				if (personal == null)
            				personal = "";
          				else {
            				personal = MimeUtility.decodeText(personal);
          				}
          				String compositeto = personal + "<" + email + ">";
          				mailaddr += "," + compositeto;
        			}
        			mailaddr = mailaddr.substring(1);
      			}
    		} else {
      			throw new Exception("Error emailaddr type!");
    		}
    		return mailaddr;
		}

		/**
		* 获得邮件主题
		*/
		public String getSubject() throws MessagingException {
    		String subject = "";
    		try {
      			subject = MimeUtility.decodeText(mimeMessage.getSubject());
      			if (subject == null)
        			subject = "";
    		} catch (Exception exce) {
    		}
    		return subject;
		}

		/**
		* 获得邮件发送日期
		*/
		public String getSentDate() throws Exception {
    		Date sentdate = mimeMessage.getSentDate();
    		SimpleDateFormat format = new SimpleDateFormat(dateformat);
    		return format.format(sentdate);
		}

		/**
		* 获得邮件正文内容
		*/
		public String getBodyText() {
    		return bodytext.toString();
		}

		/**
		* 解析邮件,把得到的邮件内容保存到一个StringBuffer对象中,解析邮件 主要是根据MimeType类型的不同执行不同的操作,一步一步的解析
		*/
		public void getMailContent(Part part) throws Exception {
    		String contenttype = part.getContentType();
    		int nameindex = contenttype.indexOf("name");
    		boolean conname = false;
    		if (nameindex != -1)
      			conname = true;
    		System.out.println("CONTENTTYPE: " + contenttype);
    		if (part.isMimeType("text/plain") && !conname) {
      			bodytext.append((String) part.getContent());
    		} else if (part.isMimeType("text/html") && !conname) {
      			bodytext.append((String) part.getContent());
    		} else if (part.isMimeType("multipart/*")) {
      			Multipart multipart = (Multipart) part.getContent();
      			int counts = multipart.getCount();
      			for (int i = 0; i < counts; i++) {
        			getMailContent(multipart.getBodyPart(i));
      			}
    		} else if (part.isMimeType("message/rfc822")) {
     			 getMailContent((Part) part.getContent());
    		} else {
    		}
		}

		/**
		* 判断此邮件是否需要回执,如果需要回执返回"true",否则返回"false"
		*/
		public boolean getReplySign() throws MessagingException {
    		boolean replysign = false;
    		String needreply[] = mimeMessage.getHeader("Disposition-Notification-To");
    		if (needreply != null) {
      			replysign = true;
    		}
    		return replysign;
		}

		/**
		* 获得此邮件的Message-ID
		*/
		public String getMessageId() throws MessagingException {
    		return mimeMessage.getMessageID();
		}

		/**
		* 【判断此邮件是否已读,如果未读返回返回false,反之返回true】
		*/
		public boolean isNew() throws MessagingException {
			boolean isnew = false;
    		Flags flags = ((Message) mimeMessage).getFlags();
    		Flags.Flag[] flag = flags.getSystemFlags();
    		System.out.println("flags's length: " + flag.length);
    		for (int i = 0; i < flag.length; i++) {
      			if (flag[i] == Flags.Flag.SEEN) {
        			isnew = true;
        			System.out.println("seen Message.......");
        			break;
      			}
    		}
    		return isnew;
		}

		/**
		* 判断此邮件是否包含附件
		*/
		public boolean isContainAttach(Part part) throws Exception {
			boolean attachflag = false;
    		@SuppressWarnings("unused")
    		String contentType = part.getContentType();
    		if (part.isMimeType("multipart/*")) {
      			Multipart mp = (Multipart) part.getContent();
      			for (int i = 0; i < mp.getCount(); i++) {
        			BodyPart mpart = mp.getBodyPart(i);
        			String disposition = mpart.getDisposition();
        			if ((disposition != null) && ((disposition.equals(Part.ATTACHMENT)) || (disposition.equals(Part.INLINE))))
          				attachflag = true;
        			else if (mpart.isMimeType("multipart/*")) {
          				attachflag = isContainAttach((Part) mpart);
        			} else {
          				String contype = mpart.getContentType();
          				if (contype.toLowerCase().indexOf("application") != -1)
            				attachflag = true;
          				if (contype.toLowerCase().indexOf("name") != -1)
            				attachflag = true;
        			}
      			}
    		} else if (part.isMimeType("message/rfc822")) {
      			attachflag = isContainAttach((Part) part.getContent());
    		}
    		return attachflag;
		}

		/**
		* 【保存附件】
		*/
		public void saveAttachMent(Part part) throws Exception {
    		String fileName = "";
    		if (part.isMimeType("multipart/*")) {
      			Multipart mp = (Multipart) part.getContent();
      			for (int i = 0; i < mp.getCount(); i++) {
        			BodyPart mpart = mp.getBodyPart(i);
        			String disposition = mpart.getDisposition();
        			if ((disposition != null) && ((disposition.equals(Part.ATTACHMENT)) || (disposition.equals(Part.INLINE)))) {
          				fileName = mpart.getFileName();
          				if (fileName.toLowerCase().indexOf("gb2312") != -1) {
            				fileName = MimeUtility.decodeText(fileName);
          				}
          				saveFile(fileName, mpart.getInputStream());
        			} else if (mpart.isMimeType("multipart/*")) {
          				saveAttachMent(mpart);
        			} else {
          				fileName = mpart.getFileName();
          				if ((fileName != null) && (fileName.toLowerCase().indexOf("GB2312") != -1)) {
            				fileName = MimeUtility.decodeText(fileName);
            				saveFile(fileName, mpart.getInputStream());
          				}
        			}
      			}
    		} else if (part.isMimeType("message/rfc822")) {
      			saveAttachMent((Part) part.getContent());
    		}
		}

		/**
		* 【设置附件存放路径】
		*/
		public void setAttachPath(String attachpath) {
    		this.saveAttachPath = attachpath;
		}

		/**
		* 【设置日期显示格式】
		*/
		public void setDateFormat(String format) throws Exception {
			this.dateformat = format;
		}

		/**
		* 【获得附件存放路径】
		*/
		public String getAttachPath() {
    		return saveAttachPath;
		}

		/**
		* 【真正的保存附件到指定目录里】
		*/
		private void saveFile(String fileName, InputStream in) throws Exception {
    		String osName = System.getProperty("os.name");
    		String storedir = getAttachPath();
    		String separator = "";
    		if (osName == null)
      			osName = "";
    		if (osName.toLowerCase().indexOf("win") != -1) {
      			separator = "\\";
      			if (storedir == null || storedir.equals(""))
        			storedir = "c:\\tmp";
    		} else {
      			separator = "/";
      			storedir = "/tmp";
    		}
    		File storefile = new File(storedir + separator + fileName);
    		System.out.println("storefile's path: " + storefile.toString());
    		BufferedOutputStream bos = null;
    		BufferedInputStream bis = null;
    		try {
      			bos = new BufferedOutputStream(new FileOutputStream(storefile));
      			bis = new BufferedInputStream(in);
      			int c;
      			while ((c = bis.read()) != -1) {
        			bos.write(c);
        			bos.flush();
      			}
    		} catch (Exception exception) {
      			exception.printStackTrace();
      			throw new Exception("文件保存失败!");
    		} finally {
      			bos.close();
      			bis.close();
    		}
		}

		/**
		* PraseMimeMessage类测试
		*/
		public static void main(String args[]) throws Exception {
		}

	}

  这样总算是一套完整解决方案了吧 哼 傲娇脸 ( ˘ω˘ )