使用场景是系统中要导入一部分简历,而这部分简历是保存在邮件中的,需要每一封都查询下,并解析出来保存到数据库中。
1、读取邮件列表,并筛查是否是需要的简历
2、对简历进行解析,并将解析字段保存到数据库中
pom引入(含后面的上传阿里云和json解析)
org.springframework.boot
spring-boot-starter-mail
com.aliyun
aliyun-java-sdk-core
4.4.0
com.aliyun.oss
aliyun-sdk-oss
3.10.2
com.alibaba
fastjson
1.2.60
org.jsoup
jsoup
1.12.1
org.apache.httpcomponents
httpmime
org.apache.commons
commons-lang3
有几个辅助方法。
很多时候读取出来的邮件标题等字段,需要解码后才能正常显示,该方法用来将部分字段以汉字形式展示。
private static String decodeText(String encodeText) throws UnsupportedEncodingException {
if (encodeText == null || "".equals(encodeText)) {
return "";
} else {
return MimeUtility.decodeText(encodeText);
}
}
读取附件,将附件名称赋值给传入的fileName,并返回附件的InputStream。
下面代码的问题是只会返回第一个附件,如果邮件有多个附件需要返回,可自行进行改造。因为本人项目只会有一个附件,所以就这样用了。
private static InputStream getAttachment(Part part, StringBuffer fileName)
throws UnsupportedEncodingException, MessagingException, FileNotFoundException, IOException {
if (part.isMimeType("multipart/*")) {
Multipart multipart = (Multipart) part.getContent();
int partCount = multipart.getCount();
for (int i = 1; i < partCount; i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
if(bodyPart == null) {
continue;
}
String disp = bodyPart.getDisposition();
if (disp != null && (disp.equalsIgnoreCase(Part.ATTACHMENT) || disp.equalsIgnoreCase(Part.INLINE))) {
InputStream is = bodyPart.getInputStream();
fileName = fileName.append(MimeUtility.decodeText(bodyPart.getFileName()));
return is;
} else if (bodyPart.isMimeType("multipart/*")) {
return getAttachment(bodyPart, fileName);
} else {
String contentType = bodyPart.getContentType();
if (contentType.indexOf("name") != -1 || contentType.indexOf("application") != -1) {
InputStream is = bodyPart.getInputStream();
fileName = fileName.append(MimeUtility.decodeText(bodyPart.getFileName()));
return is;
}
}
}
} else if (part.isMimeType("message/rfc822")) {
return getAttachment((Part) part.getContent(), fileName);
}
return null;
}
如果有场景需要将InputStream转为文件存储,可以使用下面的方式
private void saveFile(InputStream is, String destDir, String fileName)
throws FileNotFoundException, IOException {
BufferedInputStream bis = new BufferedInputStream(is);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(destDir + fileName)));
int len = -1;
while ((len = bis.read()) != -1) {
bos.write(len);
bos.flush();
}
bos.close();
bis.close();
}
正文信息会赋值给传入的content
private static void getMailTextContent(Part part, StringBuffer content) throws MessagingException, IOException {
boolean isContainTextAttach = part.getContentType().indexOf("name") > 0;
if (part.isMimeType("text/*") && !isContainTextAttach) {
content.append(part.getContent().toString());
} else if (part.isMimeType("message/rfc822")) {
getMailTextContent((Part) part.getContent(), content);
} 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);
getMailTextContent(bodyPart, content);
}
}
}
1(High):紧急 3:普通(Normal) 5:低(Low)
private String getPriority(MimeMessage msg) throws MessagingException {
String priority = "普通";
String[] headers = msg.getHeader("X-Priority");
if (headers != null) {
String headerPriority = headers[0];
if (headerPriority.indexOf("1") != -1 || headerPriority.indexOf("High") != -1)
priority = "紧急";
else if (headerPriority.indexOf("5") != -1 || headerPriority.indexOf("Low") != -1)
priority = "低";
else
priority = "普通";
}
return priority;
}
private boolean isContainAttachment(Part part) throws MessagingException, IOException {
boolean flag = 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))) {
flag = true;
} else if (bodyPart.isMimeType("multipart/*")) {
flag = isContainAttachment(bodyPart);
} else {
String contentType = bodyPart.getContentType();
if (contentType.indexOf("application") != -1) {
flag = true;
}
if (contentType.indexOf("name") != -1) {
flag = true;
}
}
if (flag)
break;
}
} else if (part.isMimeType("message/rfc822")) {
flag = isContainAttachment((Part) part.getContent());
}
return flag;
}
收件人可能是常规收件人,也可能是密送或者抄送的,所以方法需要传递要哪种的收件人。当然也可以传递null。
Message.RecipientType.TO 收件人
Message.RecipientType.CC 抄送
Message.RecipientType.BCC 密送
public String getReceiveAddress(MimeMessage msg, Message.RecipientType type) throws MessagingException {
StringBuffer receiveAddress = new StringBuffer();
Address[] addresss = null;
if (type == null) {
addresss = msg.getAllRecipients();
} else {
addresss = msg.getRecipients(type);
}
if (addresss == null || addresss.length < 1)
throw new MessagingException("没有收件人!");
for (Address address : addresss) {
InternetAddress internetAddress = (InternetAddress) address;
receiveAddress.append(internetAddress.toUnicodeString()).append(",");
}
receiveAddress.deleteCharAt(receiveAddress.length() - 1);
return receiveAddress.toString();
}
这个有一定意义,比如只获取指定发件人的邮件(像简历这种,只会是部分发件人,简单筛选即可)。当然也可以设置哪些发件人的过滤点不看。
有时候发件人的姓名需要解码才能正常显示,但大部分情况又不需要解码,代码中加了注释。
private String[] getFrom(MimeMessage msg) throws MessagingException, UnsupportedEncodingException {
String[] from = new String[2];
Address[] froms = msg.getFrom();
if (froms.length < 1)
throw new MessagingException("没有发件人!");
InternetAddress address = (InternetAddress) froms[0];
from[0] = address.getPersonal(); // 需要解码的话:MimeUtility.decodeText(address.getPersonal())
from[1] = address.getAddress();
return from;
}
慎重使用,非必要不删除。
private void deleteMessage(Message... messages) throws MessagingException, IOException {
if (messages == null || messages.length < 1)
throw new MessagingException("未找到要删除的邮件!");
for (int i = 0, count = messages.length; i < count; i++) {
Message message = messages[i];
String subject = message.getSubject();
message.setFlag(Flags.Flag.DELETED, true);
System.out.println("删除邮件: " + subject);
}
}
需要注意的地方
1、获取MimeMessage的时候,常规是直接MimeMessage msg = (MimeMessage) messages[i];即可,但是这种对于发送退回的邮件、部分平台的通知邮件等,都会报错javax.mail.MessagingException: Unable to load BODYSTRUCTURE,所以需要将获取到的数据重新new一下使用。
2、代码中理论上是按照收到邮件时间倒序排列的,但是如果邮件数量非常大,比如有三四百,这时候顺序就不准了,最后面十封左右是中间位置的。要实现完全的时间倒序,只能全部读取出来,然后用代码再排序一次。
3、得到的参数是inputstream,如果需要传递参数,必须转化为byte[]或者存储文件后使用路径。存储文件的方法上面已经提供过,转化为byte[]以及从byte[]转化会inputstream方法会提供。
4、代码中没有解析邮件的唯一值,如果需要,可以使用String messageId = msg.getMessageID();拿到
private void parseMessage(Message... messages) throws MessagingException, IOException {
if (messages == null || messages.length < 1)
throw new MessagingException("未找到要解析的邮件!");
// 解析所有邮件
int count = messages.length;
for (int i = count - 1; i >= 0; i--) {
MimeMessage msg2 = (MimeMessage) messages[i];
MimeMessage msg = new MimeMessage(msg2);
System.out.println("------------------解析第" + msg.getMessageNumber() + "封邮件-------------------- ");
// 如果有编码,可以先解码: MimeUtility.decodeText(msg.getSubject());
System.out.println("主题: " + msg.getSubject());
System.out.println("发件人名称: " + getFrom(msg)[0]);
System.out.println("发件人邮箱: " + getFrom(msg)[1]);
System.out.println("收件人:" + getReceiveAddress(msg, null));
System.out.println("发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(msg.getSentDate()));
System.out.println("是否已读:" + msg.getFlags().contains(Flags.Flag.SEEN));
System.out.println("邮件优先级:" + getPriority(msg));
System.out.println("是否需要回执:" + (msg.getHeader("Disposition-Notification-To") != null ? true : false));
System.out.println("邮件大小:" + msg.getSize() * 1024 + "kb");
StringBuffer content = new StringBuffer(100);
getMailTextContent(msg, content);
System.out.println("邮件正文:" + content);
boolean isContainerAttachment = isContainAttachment(msg);
System.out.println("是否包含附件:" + isContainerAttachment);
if (isContainerAttachment) {
InputStream is = getAttachment(msg, fileName);
// 这里可以开始使用is,比如保存附件或者上传到其他地方
// 如果是传递参数,不能直接使用inputstream,可以先转换为byte[]或者存储为文件然后传递文件地址
}
System.out.println("------------------第" + msg.getMessageNumber() + "封邮件解析结束-------------------- ");
System.out.println();
}
}
inputstream转byte[]
byte[] byteBuffer = null;
ByteArrayOutputStream outByte = new ByteArrayOutputStream();
byte[] tmpByte = new byte[2048];
int len = 0;
while ((len = is.read(tmpByte)) != -1) {
outByte.write(tmpByte, 0, len);
}
byteBuffer = outByte.toByteArray();
byte[]转inputstream
InputStream is = new ByteArrayInputStream(b);
1、代码中是以腾讯企业邮箱为例,其他邮箱需更换host、port等。
2、代码中的email指的是邮箱地址,password指的是邮箱密码(部分邮箱需要使用授权码,而不是登录密码)
3、如果附件超过1M,请务必加上mail.imap.partialfetch和mail.imaps.partialfetch,不然读取或非常非常慢,大概率超时
4、代码中读取的是收件箱INBOX,可以替换这个收取不同位置邮件,如垃圾箱Junk、已删除Deleted Messages、草稿Drafts、已发送Sent Messages
public void resceive() throws Exception {
Properties props = System.getProperties();
props.setProperty("mail.imap.host", host);
props.setProperty("mail.imap.port", "143");
props.setProperty("mail.store.protocol", "imap");
props.setProperty("mail.imap.partialfetch", "false");
props.setProperty("mail.imaps.partialfetch", "false");
Session session = Session.getInstance(props);
IMAPStore store = (IMAPStore) session.getStore("imap");
store.connect(email, password);
/**
* 获得收件箱INBOX
* 除了收件箱,其他的可以使用以下方法查看到
* Folder defaultFolder = store.getDefaultFolder();
* Folder[] allFolder = defaultFolder.list();
* for(int i = 0; i < allFolder.length; i++) {
* System.out.println(allFolder[i].getName());
* }
* 比如垃圾箱:Junk 已删除:Deleted Messages 草稿:Drafts 已发送:Sent Messages
*/
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_WRITE); //Folder.READ_ONLY:只读权限 Folder.READ_WRITE:可读可写(可以修改邮件的状态)
System.out.println("未读邮件数: " + folder.getUnreadMessageCount());
System.out.println("邮件总数: " + folder.getMessageCount());
// 得到收件箱中的所有邮件,并解析
Message[] messages = folder.getMessages();
parseMessage(messages);
// 释放资源
folder.close(true);
store.close();
}
上面代码已经获取到了附件的inputsream,可以使用这个直接上传到阿里云oss,做个文件备份,也方便后续使用。
其中uploadOssFile方法第一个参数可以带上路径,oss会自动创建文件夹保存。
public class AliyunUtil {
private static final String ACCESS_KEY_ID = "查看阿里云后台";
private static final String SECRET = "查看阿里云后台";
/**
* 文件地址获取
*/
public static String getOssUrl(String filePathName) {
OSS ossClient = new OSSClientBuilder().build("http://oss-cn-beijing.aliyuncs.com", ACCESS_KEY_ID, SECRET);
Date expiration = new Date(new Date().getTime() + 3 * 3600 * 1000);
URL url = ossClient.generatePresignedUrl("xingchenlie", filePathName, expiration);
ossClient.shutdown();
return url.toString();
}
/**
* OSS文件上传
*/
public static boolean uploadOssFile(String fileName, InputStream inputStream) {
OSS ossClient = new OSSClientBuilder().build("http://oss-cn-beijing.aliyuncs.com", ACCESS_KEY_ID, SECRET);
ossClient.putObject("查看OSS后台的bucketName", fileName, inputStream);
ossClient.shutdown();
return true;
}
}
没有自定义方法,而是从网上找了几个,也包括阿里云市场上的,费用都不高,平均下来不到一毛钱。
但是but,程序猿想要免费,所有又找了一个免费的解析网站,大概解析十几次就挂了,封了我的ip。于是乎挂上了ip代理,最终愉快的跑完了两千多份的简历解析。不足的是不支持pdf中的图片简历,只能是文字类型。索性我的简历只有一封是图片的。
如果需要简历解析网站,可以联系微信fymod1988。如果想要代理设置代码,可以看其他文章,接下来的一篇就是介绍代理的使用,包括访问网站设置代理以及上传文件设置代理。