基於james實如今物理隔離環境下郵件的傳輸

基於james實如今物理隔離環境下郵件的傳輸

背景

爲了保護高安全級別的網絡安全,國家保密局於1999年12月27日發佈了涉密網絡的物理隔離要求,並於2000年1月1日頒佈實施的《計算機信息系統國際聯網保密管理規定》,該規定中第二章保密制度第六條規定;「涉及國家祕密的計算機信息系統,不得直接或間接地與國際互聯網或其餘公共信息網絡相鏈接,必須實行物理隔離。」

物理隔離一般是經過部署網閘來切斷內網和外網的物理鏈接和邏輯鏈接,網閘只擺渡原始數據,而不允許任何鏈接或者協議通過網閘。在這種環境下,內外網郵件的傳輸成了難題。本文以探究的方式嘗試提供一套思路和實現解決該場景下的郵件傳輸,本文包含的代碼都是demo級別代碼,秉着對新領域的探究,但不肯定該方法是否是合適該場景的解決方案。html

環境與配置

本文采用的環境以下:java

Apache James-2.3.2.1mysql

Apache RocketMQ-4.2.0git

pom文件以下:github

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>MailInPIE</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>mail</artifactId>
            <version>1.4.7</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.8</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.2.0</version>
        </dependency>
    </dependencies>

</project>

架構思路

大體的架構以下圖:sql

物理隔離環境下郵件傳輸架構圖.jpg

  • mailet程序片斷主要包含DivestitureAgreementMatcher和DivestitureAgreementMailet兩個程序,DivestitureAgreementMatcher主要用於匹配內外網用戶,若是是內網用戶則放行,若是是外網用戶,則將該郵件交給DivestitureAgreementMailet進行處理;DivestitureAgreementMailet將郵件的原始內容抽出,必要時可對原始內容進行切割,發送到消息隊列。
  • 消息隊列主要存儲將要擺渡的消息,將內部的消息經由消息API取出,交給網閘進行擺渡
  • 網閘負責消息的擺渡,本質是對一塊共享內存的分時讀寫實現內外網在物理隔離和邏輯隔離環境下的數據傳輸
  • 網閘另外一端的消息API將網閘擺渡過來的內網消息發送給消息隊列
  • 郵件轉發程序從消息隊列中取出待轉發的消息,必要時重組消息,並進行轉發。

因爲重重困難,因此這個demo將跳過網閘部分,消息發送到消息隊列以後,消費者直接將消息進行轉發,雖然沒有實際在網閘和內外網的環境下測試,可是!我以爲是可行的!數據庫

代碼實現

mailet程序片斷

mailet程序片斷的實現思路以下:apache

mailet程序片斷實現思路.jpg

DivestitureAgreementMatcher參考代碼:json

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參考代碼:api

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("97983398@qq.com");
        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配置1.png

james配置2.png

重啓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<MessageExt> 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(), "979831398@qq.com", "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, "test@xxxxx.com","test","localhost", "979831398@qq.com", "測試郵件", 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<len; i++){
                iad[i] =  new InternetAddress(rec[i]);
            }
            message.setRecipients(MimeMessage.RecipientType.TO, iad);
        }else{
            //單收件人
            message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(recipient));
        }
        // 設置主題
        message.setSubject(subject);
        // 設置郵件內容
        message.setContent(content.toString(), "text/html;charset=utf-8");
//        message.setText(content.toString(), "GBK");
        // 發送
        Transport.send(message);
    }
 
    /**
     * 羣發郵件
     * 
     * @param recipients
     *            收件人們
     * @param subject
     *            主題
     * @param content
     *            內容
     * @throws AddressException
     * @throws MessagingException
     */
    public void send(List<String> 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服務,測試郵件轉發結果以下:

測試結果.png

經過某一對外用戶轉發的方式轉發到外網的郵件難免存在一個問題,抵賴。某個用戶明明發送了郵件,卻抵賴本身不曾發過。關於這點我考慮能夠經過數字簽名的方式解決,某個用戶在內網建立時生成公私鑰對,經過對郵件內容簽名的方式,保證郵件的不可篡改性和不可抵賴性。

最後,上面全部代碼均已上傳至github倉庫

上面的思路和實現僅僅是我在這個新領域的摸索,可能與實際落地的方案相差甚遠,但也算是我對這個新領域的一次微弱的攻擊。之後但願能在數據交換領域拓寬個人視野,結識更多在這個領域中兢兢業業的大佬們,爲個人許多迷茫指引方向。

參考

物理隔離環境中的電子郵件安全交換

相關文章
相關標籤/搜索