基于james实现在物理隔离环境下邮件的传输
背景
为了保护高安全级别的网络安全,国家保密局于1999年12月27日发布了涉密网络的物理隔离要求,并于2000年1月1日颁布实施的《计算机信息系统国际联网保密管理规定》,该规定中第二章保密制度第六条规定;“涉及国家秘密的计算机信息系统,不得直接或间接地与国际互联网或其他公共信息网络相连接,必须实行物理隔离。”
物理隔离通常是通过部署网闸来切断内网和外网的物理连接和逻辑连接,网闸只摆渡原始数据,而不容许任何连接或者协议经过网闸。在这种环境下,内外网邮件的传输成了难题。本文以探究的方式尝试提供一套思路和实现解决该场景下的邮件传输,本文包含的代码都是demo级别代码,秉着对新领域的探究,但不确定该方法是不是合适该场景的解决方案。
环境与配置
本文采用的环境如下:
Apache James-2.3.2.1
Apache RocketMQ-4.2.0
pom文件如下:
4.0.0
org.example
MailInPIE
1.0-SNAPSHOT
javax.mail
mail
1.4.7
org.slf4j
slf4j-api
1.7.30
mysql
mysql-connector-java
5.1.8
org.slf4j
slf4j-nop
1.7.2
org.apache.rocketmq
rocketmq-client
4.2.0
架构思路
大致的架构如下图:
- mailet程序片段主要包含DivestitureAgreementMatcher和DivestitureAgreementMailet两个程序,DivestitureAgreementMatcher主要用于匹配内外网用户,如果是内网用户则放行,如果是外网用户,则将该邮件交给DivestitureAgreementMailet进行处理;DivestitureAgreementMailet将邮件的原始内容抽出,必要时可对原始内容进行切割,发送到消息队列。
- 消息队列主要存储将要摆渡的消息,将内部的消息经由消息API取出,交给网闸进行摆渡
- 网闸负责消息的摆渡,本质是对一块共享内存的分时读写实现内外网在物理隔离和逻辑隔离环境下的数据传输
- 网闸另一端的消息API将网闸摆渡过来的内网消息发送给消息队列
- 邮件转发程序从消息队列中取出待转发的消息,必要时重组消息,并进行转发。
由于重重困难,所以这个demo将跳过网闸部分,消息发送到消息队列之后,消费者直接将消息进行转发,虽然没有实际在网闸和内外网的环境下测试,但是!我觉得是可行的!
代码实现
mailet程序片段
mailet程序片段的实现思路如下:
DivestitureAgreementMatcher参考代码:
package com.xxxxx.pie.mail.matcher;
import com.jlszkxa.pie.mail.db.DbOperation;
import org.apache.mailet.GenericRecipientMatcher;
import org.apache.mailet.MailAddress;
import java.sql.SQLException;
/**
* @ClassName DivestitureAgreementMatcher
* @Description 判断接收人是否是内网用户,是则匹配给Mailet进行原始内容抽取,否则放行
* @Author chenwj
* @Date 2020/2/20 14:53
* @Version 1.0
**/
public class DivestitureAgreementMatcher extends GenericRecipientMatcher {
@Override
public boolean matchRecipient(MailAddress mailAddress) {
DbOperation dbOperation = new DbOperation();
try {
dbOperation.connectDatabase();
String userName = mailAddress.getUser();
String host = mailAddress.getHost();
System.out.printf("截取到到发送给%s@%s的邮件\r\n", userName, host);
return !dbOperation.isInnerUser(userName + "@" + host);
} catch (Exception e) {
System.out.printf("发生异常 异常信息: %s\r\n", e.getMessage());
e.printStackTrace();
return true;
} finally {
try {
dbOperation.closeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
dbOperation参考代码:
package com.xxxxxx.pie.mail.db;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
/**
* @ClassName DbOperation
* @Description 数据库操作
* @Author chenwj
* @Date 2020/2/20 17:34
* @Version 1.0
**/
public class DbOperation {
private static final Logger logger = LoggerFactory.getLogger(DbOperation.class);
private Connection connection;
/*
连接数据库
*/
public void connectDatabase() {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/mail?characterEncoding=UTF-8";
String userName = "root";
String password = "123456";
logger.info("开始连接数据库");
try {
Class.forName(driver);
connection = DriverManager.getConnection(url, userName, password);
logger.info("数据库连接成功");
} catch (Exception e) {
logger.warn("数据库连接出现异常 异常信息: {}", e.getMessage());
}
}
/**
* 判断该用户是否为内网用户
*
* @param userName
* @return
* @throws Exception
*/
public boolean isInnerUser(String userName) throws Exception {
String sql = "select USER_NAME from james_user where USER_NAME = \'" + userName + "\';";
PreparedStatement pstmt = connection.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();
try {
if (rs.next()) {
return true;
}
return false;
} finally {
rs.close();
pstmt.close();
}
}
/*
关闭连接
*/
public void closeConnection() throws SQLException {
if (null != connection) {
connection.close();
}
}
public static void main(String[] args) throws Exception {
DbOperation test = new DbOperation();
test.connectDatabase();
boolean innerUser = test.isInnerUser("[email protected]");
System.out.printf("结果为: %s\r\n", "true");
test.closeConnection();
}
}
本例中搭建的james邮件服务器将用户信息存储在数据库中,因此可以直接通过查询数据库的手段判断是否是内网用户。此外,也可以直接截取域名进行判断。
DivestitureAgreementMailet参考代码:
package com.xxxxxx.pie.mail.mailet;
import com.alibaba.fastjson.JSONObject;
import com.jlszkxa.pie.mail.entity.ForwardMail;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import javax.mail.MessagingException;
import java.io.IOException;
/**
* @author chenwj
* @version 1.0
* @className DivestitureAgreementMailet
* @description 截取邮件,剥离协议,并放入队列等待网闸摆渡
* @date 2020/2/20 14:53
**/
public class DivestitureAgreementMailet extends GenericMailet {
@Override
public void service(Mail mail) throws MessagingException {
String sender = mail.getSender().toString();
String name = mail.getName();
String subject = mail.getMessage().getSubject();
String content = null;
try {
content = (String) mail.getMessage().getContent();
} catch (IOException e) {
e.printStackTrace();
}
System.out.printf("截取到%s的邮件 name:%s subject:%s content:%s\r\n", sender, name, subject, content);
System.out.println("将截取邮件发送到消息队列中...");
DefaultMQProducer producer = new DefaultMQProducer("DivestitureAgreementGroup");
producer.setNamesrvAddr("localhost:9876");
producer.setInstanceName("rmq-instance");
try {
producer.start();
System.out.println("开启消息队列");
} catch (MQClientException e) {
e.printStackTrace();
}
try {
ForwardMail forwardMail = ForwardMail.newBuilder()
.content(mail.getMessage().getContent())
.from(mail.getSender().getUser() + "@" + mail.getSender().getHost())
.hostName(mail.getRemoteHost())
.recipients(mail.getRecipients().iterator().next())
.subject(mail.getMessage().getSubject())
.build();
Message message = new Message("demo-topic", "demo-tag", JSONObject.toJSONString(forwardMail).getBytes("UTF-8"));
producer.send(message);
System.out.println("消息成功转发到消息队列中");
System.out.printf("转发内容如下: %s\r\n", JSONObject.toJSONString(forwardMail));
} catch (MQClientException e) {
e.printStackTrace();
} catch (RemotingException e) {
e.printStackTrace();
} catch (MQBrokerException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally{
producer.shutdown();
System.out.println("关闭消息队列");
}
}
}
ForwardMail对象如下:
package com.xxxxxx.pie.mail.entity;
/**
* @ClassName Mail
* @Description TODO
* @Author chenwj
* @Date 2020/2/21 13:42
* @Version 1.0
**/
public class ForwardMail {
private String hostName;
private String from;
private String subject;
private Object recipients;
private Object content;
public ForwardMail() {
}
private ForwardMail(Builder builder) {
setHostName(builder.hostName);
setFrom(builder.from);
setSubject(builder.subject);
setRecipients(builder.recipients);
setContent(builder.content);
}
public static Builder newBuilder() {
return new Builder();
}
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public Object getRecipients() {
return recipients;
}
public void setRecipients(Object recipients) {
this.recipients = recipients;
}
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
public static final class Builder {
private String hostName;
private String from;
private String subject;
private Object recipients;
private Object content;
private Builder() {
}
public Builder hostName(String val) {
hostName = val;
return this;
}
public Builder from(String val) {
from = val;
return this;
}
public Builder subject(String val) {
subject = val;
return this;
}
public Builder recipients(Object val) {
recipients = val;
return this;
}
public Builder content(Object val) {
content = val;
return this;
}
public ForwardMail build() {
return new ForwardMail(this);
}
}
}
将上述的mailet程序片段打成jar包,粘贴复制到james服务器下..\james-2.3.2.1\apps\james\SAR-INF\lib
,如果SAR-INF目录下没有lib目录,则手动新增该目录,在..\james-2.3.2.1\apps\james\SAR-INF\config.xml
中增加如下配置:
重启james服务器,mailet程序片段就可以生效了。下面是消费端消息的转发。
邮件转发程序
邮件的转发其实是我考虑比较久的,内网与外网无法建立连接的情况下,要将消息原封不动地进行转发,且消息发送人依旧标识是内网的用户,这是我期望实现的,但是我没有找到好的解决办法。我尝试将邮件的from设置为内网用户,或者将displayname设置为内网用户,在QQ邮箱中似乎都没有得到比较好的结果。因此最后我取巧实现,在外网统一由一个外网用户进行转发,转发时在subject中标识该邮件转发自内网的某个用户。
Consumer参考代码:
package com.xxxxxx.pie.mail.mq;
import com.alibaba.fastjson.JSONObject;
import com.jlszkxa.pie.mail.entity.ForwardMail;
import com.jlszkxa.pie.mail.mail.MailSender;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import javax.mail.MessagingException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-group");
consumer.setNamesrvAddr("localhost:9876");
consumer.setInstanceName("rmq-instance");
consumer.subscribe("demo-topic", "demo-tag");
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(
List msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
ForwardMail forwardMail = JSONObject.parseObject(new String(msg.getBody()), ForwardMail.class);
JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(forwardMail.getRecipients()));
String recipient = jsonObject.getString("user") + "@" + jsonObject.getString("host");
try {
System.out.printf("成功代理转发%s的邮件\r\n", recipient);
MailSender.sendHtml(forwardMail.getFrom(), "[email protected]", "xxxxx", "smtp.qq.com", recipient, "转发自代理服务器由" + forwardMail.getFrom().split("@")[0] + "发出的邮件:" + forwardMail.getSubject(), forwardMail.getContent());
} catch (MessagingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started.");
}
}
MailSender参考代码:
package com.xxxxxx.pie.mail.mail;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import java.io.UnsupportedEncodingException;
/**
* @author
*
*/
public class MailSender {
/**
* 服务邮箱
*/
private static MailServer mailServer = null;
//
private static String userName;
private static String password;
private static String stmp;
/**
* @param userName the userName to set
*/
public void setUserName(String userName) {
if(MailSender.userName==null)
MailSender.userName = userName;
}
/**
* @param password the password to set
*/
public void setPassword(String password) {
if(MailSender.password==null)
MailSender.password = password;
}
/**
* @param stmp the stmp to set
*/
public void setStmp(String stmp) {
if(MailSender.stmp==null)
MailSender.stmp = stmp;
}
/**
* 使用默认的用户名和密码发送邮件
* @param recipient
* @param subject
* @param content
* @throws MessagingException
* @throws AddressException
*/
public static void sendHtml(String recipient, String subject, Object content, String fromname) throws AddressException, MessagingException, UnsupportedEncodingException {
if (mailServer == null)
mailServer = new MailServer(stmp,userName,password);
mailServer.send(recipient, subject, content, fromname);
}
/**
* 使用指定的用户名和密码发送邮件
* @param server
* @param password
* @param recipient
* @param subject
* @param content
* @throws MessagingException
* @throws AddressException
*/
public static void sendHtml(String fromname, String server,String password,String stmpIp, String recipient, String subject, Object content) throws AddressException, MessagingException, UnsupportedEncodingException {
new MailServer(stmpIp,server,password).send(recipient, subject, content, fromname);
}
public static void main(String[] args) {
try {
String s = "这是一封来自公司内网的测试邮件,收到请勿回复!";
sendHtml(null, "[email protected]","test","localhost", "[email protected]", "测试邮件", s);
} catch (AddressException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
MailServer参考代码:
package com.xxxxxx.pie.mail.mail;
import org.apache.commons.lang3.StringUtils;
import javax.mail.*;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Properties;
/**
* 简单邮件发送器,可单发,群发。
*
* @author humingfeng
*
*/
public class MailServer {
/**
* 发送邮件的props文件
*/
private final transient Properties props = System.getProperties();
/**
* 邮件服务器登录验证
*/
private transient MailAuthenticator authenticator;
/**
* 邮箱session
*/
private transient Session session;
/**
* 初始化邮件发送器
*
* @param smtpHostName
* SMTP邮件服务器地址
* @param username
* 发送邮件的用户名(地址)
* @param password
* 发送邮件的密码
*/
public MailServer(final String smtpHostName, final String username,
final String password) {
init(username, password, smtpHostName);
}
/**
* 初始化邮件发送器
*
* @param username
* 发送邮件的用户名(地址),并以此解析SMTP服务器地址
* @param password
* 发送邮件的密码
*/
public MailServer(final String username, final String password) {
// 通过邮箱地址解析出smtp服务器,对大多数邮箱都管用
final String smtpHostName = "smtp." + username.split("@")[1];
init(username, password, smtpHostName);
}
/**
* 初始化
*
* @param username
* 发送邮件的用户名(地址)
* @param password
* 密码
* @param smtpHostName
* SMTP主机地址
*/
private void init(String username, String password, String smtpHostName) {
// 初始化props
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.host", smtpHostName);
if(smtpHostName==null)props.put("mail.smtp.host", smtpHostName);
// 验证
authenticator = new MailAuthenticator(username, password);
// 创建session
session = Session.getInstance(props, authenticator);
}
/**
* 发送邮件
*
* @param recipient
* 收件人邮箱地址
* @param subject
* 邮件主题
* @param content
* 邮件内容
* @throws AddressException
* @throws MessagingException
*/
public void send(String recipient, String subject, Object content, String fromname)
throws AddressException, MessagingException, UnsupportedEncodingException {
// 创建mime类型邮件
final MimeMessage message = new MimeMessage(session);
// 设置发信人
if(StringUtils.isBlank(fromname)) {
message.setFrom(new InternetAddress(authenticator.username, fromname));
} else {
message.setFrom(new InternetAddress(authenticator.username));
}
// 设置收件人
if(recipient!=null&&recipient.indexOf(";")!=-1){
//多收件人
String[] rec = recipient.split(";");
int len = rec.length;
InternetAddress[] iad = new InternetAddress[len];
for(int i=0; i recipients, String subject, Object content, String fromname)
throws AddressException, MessagingException, UnsupportedEncodingException {
// 创建mime类型邮件
final MimeMessage message = new MimeMessage(session);
// 设置发信人
if(StringUtils.isBlank(fromname)) {
message.setFrom(new InternetAddress(authenticator.username, fromname));
} else {
message.setFrom(new InternetAddress(authenticator.username));
}
// 设置收件人们
final int num = recipients.size();
InternetAddress[] addresses = new InternetAddress[num];
for (int i = 0; i < num; i++) {
addresses[i] = new InternetAddress(recipients.get(i));
}
message.setRecipients(MimeMessage.RecipientType.TO, addresses);
// 设置主题
message.setSubject(subject);
// 设置邮件内容
message.setContent(content.toString(), "text/html;charset=utf-8");
// 发送
Transport.send(message);
}
/**
* 服务器邮箱登录验证
*
* @author MZULE
*
*/
public class MailAuthenticator extends Authenticator {
/**
* 用户名(登录邮箱)
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 初始化邮箱和密码
*
* @param username
* 邮箱
* @param password
* 密码
*/
public MailAuthenticator(String username, String password) {
this.username = username;
this.password = password;
}
String getPassword() {
return password;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
String getUsername() {
return username;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
}
}
最后启动james服务和Comsumer服务,测试邮件转发结果如下:
通过某一对外用户转发的方式转发到外网的邮件不免存在一个问题,抵赖。某个用户明明发送了邮件,却抵赖自己未曾发过。关于这点我考虑可以通过数字签名的方式解决,某个用户在内网创建时生成公私钥对,通过对邮件内容签名的方式,保证邮件的不可篡改性和不可抵赖性。
最后,上面所有代码均已上传至github仓库。
上面的思路和实现仅仅是我在这个新领域的摸索,可能与实际落地的方案相差甚远,但也算是我对这个新领域的一次微弱的攻击。以后希望能在数据交换领域拓宽我的视野,结识更多在这个领域中兢兢业业的大佬们,为我的许多迷茫指引方向。
参考
物理隔离环境中的电子邮件安全交换