|||||||||||||||||||||

なんぶ電子

- 更新: 

javax.mailの代替

Commons-net

以前、PHPMailerを使ってPHPからメールを送信する記事を書きましたが、今回はJavaからメールを送ります。

Javaには古くからjavax.mailというメールAPIがありましたが、そのライブラリが標準のJDKに同梱されなくなってしばらくたちます。com.sunという懐かしいパッケージ名がついていることもありメンテナンスも期待できないので代替を探して実装しました。

追記

後になって、javax.mailの後継として jakarta.mail が存在する事を知りました。それも移譲され、現在はEclipse Angus mailとしてEclipse Foundationによって管理が行われているようです。Eclipse Angus mail の使い方は別記事にて紹介しています。

commons-net

javax.mailの代替を探したところ、Apacheのcommons-netに、smtpやpop3用のライブラリがありそれを使うことにしました。ちなみに、同じくApacheが公開しているcommons.mailというライブラリもありました。こちらの方が簡単そうだったのですが、内部でjavax.mailを使っているとの文献を見つけたのに加え、しばらくメンテナンスもされていないようでしたので、採用をやめました。

commons-netはひとつのjarファイルになっているので、先のページよりダウンロードして展開したライブラリをクラスパスに入れることで利用可能になります。

POP3

まず、POP3での接続して指定したフォルダにメールをダウンロードする方法を紹介します。後に紹介するJavaのコードを次のパラメータを指定して実行してください。

Pop3.recvMail(...

  • strServer

    メールサーバーを指定してください。ドメインかIPアドレス。

  • strPort

    ポートを指定してください。nullにするとデフォルトのポートを利用します。

  • strUser

    サーバー認証時のユーザー名です。

  • strPass

    サーバー認証時のパスワードです。

  • strProtocol

    プロトコルです。暗号化通信の際は"TLS"そうでない場合はnullを指定します。

  • blnImplicit

    暗黙モードの設定です。

    Implicitは暗黙という意味で、セッションの最初から暗号化通信を開始します。利用しているメールサーバーによって違うかもしれませんが、POP3においては多くがこのモードで接続すると思います。後で紹介するSMTPの認証の場合はImplicit(明示)モードが利用されることがあります。STARTTLSがそれにあたります。これは平文通信で開始して後から暗号化通信に切り替える方法です。この時は暗黙モードをfalseにします。

  • strSaveDir

    メールは.emlファイルとしてここで指定したフォルダに保存されます。受信時のid+.emlというファイル名で保存されますが、ファイル名の衝突や上書きの確認等は実装していませんので、注意してください。

公式サイトのサンプルコード(Apache License 2.0)利用させていただき、次のようなコードを組みました。

Pop3.java

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;

import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.pop3.POP3Client;
import org.apache.commons.net.pop3.POP3MessageInfo;
import org.apache.commons.net.pop3.POP3SClient;

public class Pop3 {
  
  public static String FS = System.getProperty("file.separator");;
  
  public static void main(String[] args) {
    Pop3.recvMail("server", "995", "user", "password", "TLS", true,"d:¥¥savedir");
  }
  
  public static void recvMail(
      String strServer, 
      String strPort,
      String strUser,
      String strPass,
      String strProtocol,
      boolean blnImplicit,
      String strSaveDir
      ){
    
    POP3Client pop3;

    File saveDir = new File(strSaveDir);
    
    if (saveDir.exists()==false) {
       System.err.println("保存フォルダが存在しません");
       return;
    }
    
    if (strProtocol != null) {
      pop3 = new POP3SClient(strProtocol, blnImplicit);
    } else {
      //暗号化なし
      pop3 = new POP3Client();
    }

    int intPort =pop3.getDefaultPort(); 
    if (strPort != null) {
      try {
        intPort = Integer.parseInt(strPort);
      } catch(NumberFormatException e) {
        intPort = pop3.getDefaultPort();
      }
    }
    
    // ミリ秒でタイムアウトを設定
    pop3.setDefaultTimeout(60* 1000);

    // サーバーとのやり取りを出力
    pop3.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
    
    try{
      pop3.connect(strServer,intPort);
    }catch (IOException e){
      System.err.println("サーバーに接続にできませんでした.");
      e.printStackTrace();
      return;
    }

    try {
      if (!pop3.login(strUser, strPass)){
        System.err.println("ログインに失敗しました");
        pop3.disconnect();
        return;
      }

      POP3MessageInfo status = pop3.status();
      if (status == null) {
        System.err.println("ステータスを取得できませんでした");
        pop3.logout();
        pop3.disconnect();
        return;
      }
  
      System.out.println("Status: " + status);
  
      POP3MessageInfo[] messages = pop3.listMessages();
  
      if (messages == null) {
        System.err.println("メッセージリストの取得に失敗しました");
        pop3.logout();
        pop3.disconnect();
        return;
      } else if (messages.length == 0) {
        System.out.println("メッセージはありません");
        pop3.logout();
        pop3.disconnect();
        return;
      }

      System.out.println("メッセージ数: " + messages.length);

      for (POP3MessageInfo msginfo : messages) {
        
        // ヘッダ読み出し
        BufferedReader reader = (BufferedReader) pop3.retrieveMessageTop(msginfo.number, 0);

        if (reader == null) {
          System.err.println("メッセージヘッダを取得できませんでした");
          pop3.logout();
          pop3.disconnect();
          return;
        }
        
        printMessageInfo(reader, msginfo.number);
        
        // 保存
        BufferedReader bodyReader = (BufferedReader)pop3.retrieveMessage(msginfo.number);
        saveMail(bodyReader, msginfo.number,strSaveDir);
        
        
        // 削除(指示)
        pop3.deleteMessage(msginfo.number);
        
      }

      pop3.logout();
      pop3.disconnect();
    } catch (IOException e){
      e.printStackTrace();
      return;
    }
  }
  
  public static void saveMail(BufferedReader reader, int id, String strSaveDir) throws IOException {
    File file = new File(strSaveDir + FS +String.valueOf(id)+".eml");
    FileOutputStream outSt = new FileOutputStream(file,true);
    
    //reader.readLineとするとtext/plain時に本文の日本語が文字化けすることがあるので
    //readlineで文字列にはしていません
    int intLen = 0;
    char[] cbuf = new char[1024];
	try {
      while (true) {
        intLen = reader.read(cbuf);
        if (intLen <0) {
          break;
        }
        byte[] b = new byte[intLen];
        for (int i = 0; i < intLen;i++) {
          b[i]=(byte)cbuf[i];
        }       
        outSt.write(b);
      }
    } finally { 
      reader.close();
      outSt.close();
    }
  }
  
  public static void printMessageInfo(BufferedReader reader, int id) throws IOException {
    
    String from = "";
    String subject = "";
    String line;
    while ((line = reader.readLine()) != null) {
      //メール内の項目を小文字化して判定
      String lower = line.toLowerCase(Locale.ENGLISH);
      if (lower.startsWith("from: ")) {
        from = line.substring(6).trim();
      } else if (lower.startsWith("subject: ")) {
        subject = line.substring(9).trim();
      }
    }

    System.out.println(Integer.toString(id) + " From: " + from + " Subject: " + subject);
  }
}

SMTP

SMTPの場合、コードは少し複雑になるので、平文通信の可能性は考慮せずプロトコルを"TLS"固定にしています。

先ほど同様公式サイトのサンプル(Apache License 2.0)の他、stack exchangejavatips.netを参考にさせていただきました。

Smtp.sendMail(...

  • strServer

    メールサーバーを指定してください。ドメインかIPアドレス。

  • strPort

    ポートを指定してください。nullにするとデフォルトのポートを利用します。

  • strUser

    サーバー認証時のユーザー名です。

  • strPass

    サーバー認証時のパスワードです。

  • strSender

    送信者のアドレスです。

  • strReceiver

    受信者のアドレスです。

  • strSubject

    メールのタイトルです。

  • strBody

    メール本文です。

  • lstCc

    ArrayList<String>で、カーボンコピーのアドレスを設定します。空のリストかnullで省略可能です。

  • lstFile

    ArrayList<File>で、添付ファイルを設定します。こちらも空のリストかnullで省略可能です。

  • blnImplicit

    暗黙モードの設定です。筆者の環境ではSTARTLS接続なので、falseを指定しています。

Smtp.java

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.UUID;

import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.smtp.AuthenticatingSMTPClient;
import org.apache.commons.net.smtp.AuthenticatingSMTPClient.AUTH_METHOD;
import org.apache.commons.net.smtp.SMTPReply;
import org.apache.commons.net.smtp.SimpleSMTPHeader;
import org.apache.commons.net.util.Base64;

public class Smtp {

  public static void main(String[] args) {
    Smtp.sendMail("server", "587", "username", "password", "from@mail.address.jp", "to@mail.address.jp", "タイトル", "本文", lstCc, lstFile,false);
  }
  
  public static void sendMail(String strServer, String strPort, String strUser, String strPass, String strSender, String strReceiver, String strSubject, String strBody, ArrayList<String> lstCc, ArrayList<File> lstFile,boolean blnImplicit){
    final Writer writer;
    final SimpleSMTPHeader header;
    final AuthenticatingSMTPClient client;

    try {
      
      header = new SimpleSMTPHeader(strSender, strReceiver, "=?UTF-8?B?"+Base64.encodeBase64String(strSubject.getBytes("UTF-8"),false)+"?=");
      
      if(lstCc != null) {
        for (int i =0; i < lstCc.size(); i++) {
          header.addCC(lstCc.get(i));
        }
      }
      
      client = new AuthenticatingSMTPClient("TLS",blnImplicit);
      
      client.setCharset(Charset.forName("UTF-8"));
      
      int intPort =client.getDefaultPort(); 
      if (strPort != null) {
         try {
           intPort = Integer.parseInt(strPort);
         } catch(NumberFormatException e) {
           intPort = client.getDefaultPort();
         }
      }
       
      client.setDefaultTimeout(60 * 1000);//タイムアウト設定
      
      //ログ出力用
      client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
      
      client.connect(strServer, intPort);

      if (!SMTPReply.isPositiveCompletion(client.getReplyCode())) {
        client.disconnect();
        System.err.println("サーバー接続に失敗しました");
        System.exit(1);
      }

      try {
        
        if (client.auth(AUTH_METHOD.CRAM_MD5, strUser, strPass)==false) {
          System.err.println("認証に失敗しました");
          System.exit(1);
        }
      } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException e) {
        e.printStackTrace();
        System.err.println("認証に失敗しました");
        System.exit(1);
      }
      
      client.login();

      client.setSender(strSender);
      client.addRecipient(strReceiver);
      
      if (lstCc != null) {
        for (int i =0; i < lstCc.size(); i++) {
          client.addRecipient(lstCc.get(i));
        }
      }

      writer = client.sendMessageData();
      if (writer == null) {
        System.err.println("メッセージ送信に失敗しました");
        System.exit(1);
      }
      
      if(lstFile == null) {
        header.addHeaderField("Content-Type", "text/plain; charset=¥"UTF-8¥"");
        header.addHeaderField("Content-Transfer-Encoding", "base64");
        
        writer.write(header.toString());
        writer.write(Base64.encodeBase64String(strBody.getBytes("UTF-8")));

      } else {
        
        // メールのパートを分けるためのランダムな文字列 boundary
        String boundary = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 28);
        
        header.addHeaderField("Content-Type", "multipart/mixed; boundary=" + boundary);
        
        writer.write(header.toString());
        
        //本文
        writer.write("--" + boundary + "¥r¥n");
        writer.write("Content-Type: text/plain; charset=¥"UTF-8¥"¥r¥n");
        writer.write("Content-Transfer-Encoding: base64¥r¥n");
        writer.write("¥r¥n");
        writer.write(Base64.encodeBase64String(strBody.getBytes("UTF-8")));
        writer.write("¥r¥n");
        
        //添付ファイル
        for (int i =0; i < lstFile.size();i++) {
          if (lstFile.get(i).isFile()) {
            appendFile(writer,boundary,lstFile.get(i));
          }
        }
        
        //最後
        writer.write("--" + boundary + "--¥r¥n¥r¥n");
      }
      
      writer.close();
      client.completePendingCommand();
      client.logout();
      client.disconnect();
     } catch (final IOException e) {
        e.printStackTrace();
        System.exit(1);
     }
   }
  
  private static void appendFile(Writer writer, String boundary, File f) throws IOException {
    writer.write("¥r¥n");
    writer.write("--" + boundary + "¥r¥n");
    writer.write("Content-Type: application/octet-stream¥r¥n");
    writer.write("Content-Transfer-Encoding: base64¥r¥n");
    writer.write("Content-Disposition: attachment; filename=¥""+"=?UTF-8?B?"+Base64.encodeBase64String(f.getName().getBytes("UTF-8"),false)+"?="+"¥"¥r¥n");
    writer.write("¥r¥n");
    writer.write(convFile(f));
    writer.write("¥r¥n");
  }
  
  private static String convFile(File file) throws FileNotFoundException, IOException {
    
    FileInputStream fileInputStream = new FileInputStream(file);
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    int bytesRead;
    byte[] data = new byte[1024];

    while ((bytesRead= fileInputStream.read(data, 0, data.length)) != -1) {
      buffer.write(data, 0, bytesRead);
    }

    buffer.flush();
    fileInputStream.close();

    return Base64.encodeBase64String(buffer.toByteArray());
  }

}

認証の際「AUTH_METHOD.CRAM_MD5」として、認証プロトコルを指定しています。AuthenticatingSMTPClientのAUTH_METHODに列挙されているプトロコルが選べます。他の値には「PLAIN」や「LOGIN」などがあります。

また、メール送信時は76字での改行が要求されていますが、タイトルと添付ファイルのファイル名に関してそれは考慮していません。普段筆者が受信するメールでもタイトルが76字を超えているものがあるので長い文字数で送っても問題ないと思いますが気になるようなら修正してください。本文に関しては、APIの実装で76字で自動的に改行が入るようです。


参考にさせていただきましたサイトやコードの作者の皆様、ありがとうございました。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告