All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.uchicom.smtp.SmtpProcess Maven / Gradle / Ivy

// (C) 2014 uchicom
package com.uchicom.smtp;

import com.uchicom.server.ServerProcess;
import com.uchicom.smtp.type.AuthenticationStatus;
import com.uchicom.util.Parameter;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Writer;
import java.net.InetAddress;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;

/**
 * SMTP処理クラス. 送信処理認証後は、他サーバに直接接続してプロキシ―のような処理をする。
 *
 * @author uchicom: Shigeki Uchiyama
 */
public class SmtpProcess implements ServerProcess {

  private static final Logger logger = Logger.getLogger(SmtpProcess.class.getCanonicalName());

  private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss.SSS");
  private Parameter parameter;
  private Socket socket;

  private boolean bHelo;
  private boolean bMailFrom;
  private boolean bRcptTo;
  private boolean bData;
  private boolean bAuth;
  /** 外のメールサーバに転送する */
  private boolean bTransfer;

  private AuthenticationStatus authStatus;
  private boolean bSpam;

  private String senderAddress;
  private String helo;
  private String mailFrom;
  private Mail mail;
  private String authName;

  private List boxList = new ArrayList<>();

  /** 転送アドレス一覧 */
  private List transferList = new ArrayList<>();

  private long startTime = System.currentTimeMillis();

  /**
   * コンストラクタ.
   *
   * @param parameter パラメータ情報
   * @param socket ソケット
   */
  public SmtpProcess(Parameter parameter, Socket socket) {
    this.parameter = parameter;
    this.socket = socket;
  }

  /** SMTP処理を実行 */
  public void execute() {
    this.senderAddress = socket.getInetAddress().getHostAddress();
    logger.log(Level.INFO, String.valueOf(senderAddress));
    Writer writer = null;
    try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintStream ps = new PrintStream(socket.getOutputStream()); ) {
      SmtpUtil.recieveLine(ps, "220 ", parameter.get("host"), " SMTP");
      String line = br.readLine();
      // HELO
      // MAIL FROM:
      // RCPT TO:
      // DATA
      // MAIL FROM:
      // RCPT TO:
      // DATAの順で処理する
      // 認証状態、非認証状態で、判定できるコマンドを分ける、状態遷移もつける
      while (line != null) {
        logger.log(Level.INFO, "[" + line + "]");
        logger.log(Level.INFO, "status:" + authStatus);
        if (authStatus == AuthenticationStatus.USER
            || authStatus == AuthenticationStatus.PASSWORD) {
          switch (authStatus) {
            case USER:
              String name = new String(Base64.getDecoder().decode(line));
              File dir = new File(parameter.getFile("dir"), name);
              if (dir.exists() && dir.isDirectory()) {
                authStatus = AuthenticationStatus.PASSWORD;
                authName = name;

                SmtpUtil.recieveLine(ps, Constants.RECV_334, " UGFzc3dvcmQ6");
              }
              break;
            case PASSWORD:
              String pass = new String(Base64.getDecoder().decode(line));
              File passwordFile =
                  new File(
                      new File(parameter.getFile("dir"), authName), Constants.PASSWORD_FILE_NAME);
              if (passwordFile.exists() && passwordFile.isFile()) {
                try (BufferedReader passReader =
                    new BufferedReader(
                        new InputStreamReader(new FileInputStream(passwordFile))); ) {
                  String password = passReader.readLine();
                  while ("".equals(password)) {
                    password = passReader.readLine();
                  }
                  if (pass.equals(password)) {

                    SmtpUtil.recieveLine(ps, Constants.RECV_235);
                    bAuth = true;
                    authStatus = AuthenticationStatus.AUTH;
                  } else {
                    // パスワード不一致エラー
                    SmtpUtil.recieveLine(ps, Constants.RECV_535);
                    authStatus = AuthenticationStatus.NOAUTH;
                  }
                }
              }
              break;
            default:
          }
        } else if (bData) {
          if (".".equals(line)) {
            // メッセージ終了
            writer.close();
            if (bSpam) {
              // 迷惑メールフォルダに移動
              try {
                boxList.stream()
                    .forEach(
                        (mailBox) -> {
                          mailBox.setDir(new File(parameter.getFile("dir"), Constants.SPAM_DIR));
                        });
                mail.copy(
                    boxList,
                    socket.getLocalAddress().getHostName(),
                    socket.getInetAddress().getHostName());
              } catch (Exception e) {
                e.printStackTrace();
              }
            } else if (bTransfer) {
              logger.log(Level.INFO, "transferList:" + transferList);
              for (String address : transferList) {
                // 転送処理を実行する。
                logger.log(Level.INFO, "transfer:" + address);
                String[] addresses = address.split("@");
                String[] hosts = lookupMailHosts(addresses[1]);
                boolean send = false;
                for (String mxHost : hosts) {

                  MailSender.sendMail(
                      parameter.get("host"),
                      mxHost,
                      address,
                      mail.getData(),
                      parameter.getFile("dkim"));
                  send = true;
                  break;
                }
                // MXで送信できなかった場合は、ホスト名に送信する
                if (!send) {
                  MailSender.sendMail(
                      parameter.get("host"),
                      addresses[1],
                      address,
                      mail.getData(),
                      parameter.getFile("dkim"));
                }
              }
            } else {
              // メッセージコピー処理
              try {
                mail.copy(
                    boxList,
                    socket.getLocalAddress().getHostName(),
                    socket.getInetAddress().getHostName());
              } catch (Exception e) {
                e.printStackTrace();
              }
            }
            mail.delete();
            mail = null;
            transferList.clear();

            if (bSpam) {
              SmtpUtil.recieveLine(ps, "550");
            } else {
              SmtpUtil.recieveLine(ps, Constants.RECV_250_OK);
            }
            init();
          } else if (!line.isEmpty() && line.charAt(0) == '.') {
            writer.write(line.substring(1));
            writer.write("\r\n");
          } else {
            // メッセージ本文
            // 禁止文字が含まれる場合は、迷惑メールに追加
            Matcher matcher = Constants.pattern.matcher(line);
            if (matcher.find()) {
              bSpam = true;
            }
            writer.write(line);
            writer.write("\r\n");
          }
        } else if (!bHelo && (SmtpUtil.isEhlo(line) || SmtpUtil.isHelo(line))) {
          bHelo = true;
          String[] lines = line.split(" +");
          helo = lines[1];
          if (parameter.is("transfer")) {
            SmtpUtil.recieveLine(
                ps, Constants.RECV_250, "-", parameter.get("host"), " Hello ", senderAddress);
            SmtpUtil.recieveLine(ps, Constants.RECV_250, " AUTH LOGIN"); // TODO CRAM-MD5も追加したい
          } else {
            SmtpUtil.recieveLine(
                ps, Constants.RECV_250, " ", parameter.get("host"), " Hello ", senderAddress);
          }
          init();
        } else if (SmtpUtil.isAuthLogin(line) && parameter.is("transfer")) {
          authStatus = AuthenticationStatus.USER;
          SmtpUtil.recieveLine(ps, Constants.RECV_334, " VXNlcm5hbWU6");
        } else if (SmtpUtil.isRset(line)) {
          SmtpUtil.recieveLine(ps, Constants.RECV_250_OK);
          init();
        } else if (SmtpUtil.isMailFrom(line)) {
          if (bHelo) {
            mailFrom = line.substring(10).trim().replaceAll("[<>]", "");
            logger.log(Level.INFO, mailFrom);
            SmtpUtil.recieveLine(ps, Constants.RECV_250_OK);
            bMailFrom = true;
          } else {
            // エラー500
            SmtpUtil.recieveLine(ps, "451 ERROR");
          }
        } else if (SmtpUtil.isRcptTo(line)) {
          if (bMailFrom) {
            String[] heads = line.split(":");
            String address = heads[1].trim().replaceAll("[<>]", "");
            String[] addresses = address.split("@");
            logger.log(Level.INFO, addresses[0]);
            logger.log(Level.INFO, addresses[1]);
            if (addresses[1].equals(parameter.get("host"))) {
              // 宛先チェック
              if (parameter.is("memory")) {
                for (String user : Context.singleton().getUsers()) {
                  if (addresses[0].equals(user)) {
                    boxList.add(new MailBox(address, Context.singleton().getMailList(user)));
                    break;
                  }
                }

              } else {
                for (File box : parameter.getFile("dir").listFiles()) {
                  if (box.isDirectory()) {
                    if (addresses[0].equals(box.getName())) {
                      if (mailFromCheck(box)) {
                        boxList.add(new MailBox(address, box));
                      }
                      break;
                    }
                  }
                }
              }
              if (boxList.size() > 0) {
                SmtpUtil.recieveLine(ps, Constants.RECV_250_OK);
                bRcptTo = true;
              } else {
                // エラーユーザー存在しない
                SmtpUtil.recieveLine(ps, "550 Failure reply");
              }
            } else if (bAuth && parameter.is("transfer")) { // 念のためチェック
              // 認証済みなので転送OKする

              SmtpUtil.recieveLine(ps, Constants.RECV_250_OK);
              bTransfer = true;
              transferList.add(address);
              bRcptTo = true;
            } else {
              // エラーホストが違う
              SmtpUtil.recieveLine(ps, "554 <" + address + ">: Relay access denied");
            }
          } else {
            // エラー500
            SmtpUtil.recieveLine(ps, "500");
          }
        } else if (SmtpUtil.isData(line)) {
          if (bRcptTo) {
            if (bTransfer) {
              mail = new MemoryMail();
            } else if (parameter.is("memory")) {
              mail = new MemoryMail();
            } else {
              mail =
                  new FileMail(
                      new File(
                          new File(parameter.getFile("dir"), "@rcpt"),
                          helo.replaceAll(":", "_")
                              + "_"
                              + mailFrom
                              + "~"
                              + senderAddress.replaceAll(":", "_")
                              + "_"
                              + LocalDateTime.now().format(dateTimeFormatter) // 日付とuuid スレッド番号
                              + "_"
                              + Thread.currentThread().getId()
                              + ".eml"));
            }
            writer = mail.getWriter();
            SmtpUtil.recieveLine(ps, Constants.RECV_354);
            bData = true;
          } else {
            // エラー
            SmtpUtil.recieveLine(ps, "500");
          }
        } else if (SmtpUtil.isHelp(line)) {
          SmtpUtil.recieveLine(ps, "250");
        } else if (SmtpUtil.isQuit(line)) {
          SmtpUtil.recieveLine(ps, "221 ", parameter.get("host"));
          break;
        } else if (SmtpUtil.isNoop(line)) {
          SmtpUtil.recieveLine(ps, "250");
        } else if (line.length() == 0) {
          // 何もしない
        } else {
          // 対応コマンドなし
          SmtpUtil.recieveLine(ps, "500 Syntax error, command unrecognized");
        }
        startTime = System.currentTimeMillis();
        line = br.readLine();
      }

    } catch (IOException e) {
      e.printStackTrace();
    } catch (Throwable e) {
      e.printStackTrace();
    } finally {
      if (mail != null) {
        // メッセージコピー処理
        try {
          mail.copy(
              boxList,
              socket.getInetAddress().getHostName(),
              InetAddress.getLocalHost().getHostName());
          mail.delete();
        } catch (Exception e) {
          e.printStackTrace();
        }
        mail = null;
      }
      if (writer != null) {
        try {
          writer.close();
        } catch (IOException e) {
          // TODO 自動生成された catch ブロック
          e.printStackTrace();
        }
      }
      if (socket != null) {
        try {
          socket.close();
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          socket = null;
        }
      }
    }
  }

  /** 初期化. */
  private void init() {
    mail = null;
    bMailFrom = false;
    bRcptTo = false;
    bData = false;
    boxList.clear();
  }

  public long getStartTime() {
    return startTime;
  }

  public void forceClose() {
    System.out.println("forceClose!");
    if (socket != null && socket.isConnected()) {
      try {
        socket.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
      socket = null;
    }
  }

  public List getMailList(String user) {
    if (parameter.is("memory")) {
      return Context.singleton().getMailList(user);
    } else {
      List mailList = new ArrayList<>();
      File box = new File(parameter.getFile("dir"), user);
      if (box.exists()) {
        for (File file : box.listFiles()) {
          try {
            mailList.add(new FileMail(file));
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
      return mailList;
    }
  }

  @Override
  public long getLastTime() {
    return System.currentTimeMillis();
  }

  /**
   * DNS問合せでMXレコードを抽出します.
   *
   * @param domainName ドメイン名
   * @return MXホスト一覧
   * @throws NamingException エラーが初声死した場合
   */
  public static String[] lookupMailHosts(String domainName) throws NamingException {

    InitialDirContext idc = new InitialDirContext();
    Attributes attributes = idc.getAttributes("dns:/" + domainName, new String[] {"MX"});
    Attribute attributeMX = attributes.get("MX");

    if (attributeMX == null) {
      return (new String[] {domainName});
    }

    String[][] pvhn = new String[attributeMX.size()][2];
    for (int i = 0; i < attributeMX.size(); i++) {
      pvhn[i] = ("" + attributeMX.get(i)).split("\\s+");
    }

    Arrays.sort(
        pvhn,
        new Comparator() {
          public int compare(String[] o1, String[] o2) {
            return (Integer.parseInt(o1[0]) - Integer.parseInt(o2[0]));
          }
        });

    String[] sortedHostNames = new String[pvhn.length];
    for (int i = 0; i < pvhn.length; i++) {
      if (pvhn[i][1].endsWith(".")) {
        sortedHostNames[i] = pvhn[i][1].substring(0, pvhn[i][1].length() - 1);
      } else {
        sortedHostNames[i] = pvhn[i][1];
      }
    }
    return sortedHostNames;
  }

  /**
   * MAIL FROM で制御する
   *
   * @param box
   * @param logStream
   * @return
   */
  boolean mailFromCheck(File box) {
    boolean add = true;
    File mailFromFile = new File(box, Constants.IGNORE_FILE_NAME);
    if (mailFromFile.exists() && mailFromFile.isFile()) {
      Properties prop = new Properties();
      try (FileInputStream fis = new FileInputStream(mailFromFile)) {
        prop.load(fis);
        String all = prop.getProperty("*");
        if (all != null) {
          add = Boolean.parseBoolean(all);
        }
        String topLevelDomain =
            prop.getProperty("*" + mailFrom.substring(mailFrom.lastIndexOf(".")));
        if (topLevelDomain != null) {
          add = Boolean.parseBoolean(topLevelDomain);
        }
        String domain = prop.getProperty("*" + mailFrom.substring(mailFrom.indexOf('@')));
        if (domain != null) {
          add = Boolean.parseBoolean(domain);
        }
        String ignore = prop.getProperty(mailFrom);
        if (ignore != null) {
          add = Boolean.parseBoolean(ignore);
        }
      } catch (Exception e) {
        logger.log(Level.WARNING, e.getMessage(), e);
      }
      prop.clear();
      if (!add) {
        File mailFromResultFile = new File(box, Constants.IGNORE_RESULT_FILE_NAME);
        int cnt = 1;
        try {
          mailFromResultFile.createNewFile();
          try (FileInputStream fis = new FileInputStream(mailFromResultFile)) {
            prop.load(fis);
          }
          String ignore = prop.getProperty(mailFrom);
          if (ignore != null) {
            cnt = Integer.parseInt(ignore);
            cnt++;
          }
          prop.setProperty(mailFrom, String.valueOf(cnt));
          try (FileOutputStream fos = new FileOutputStream(mailFromResultFile)) {
            prop.store(fos, "");
          }
        } catch (IOException e) {
          logger.log(Level.WARNING, e.getMessage(), e);
        } catch (Exception e) {
          logger.log(Level.WARNING, e.getMessage(), e);
        }
      }
    }
    return add;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy