Eclipse Angus Mail(JavaMailの代替)
以前、java.mailの後継を探した際には、apceh commons-netを使ってメール処理を実装しましたが、最近になって後継にあたるAPIの開発が継続されているのを知ったので、再度そちらに乗り換えようと思います。
java.mail プロジェクトは、Jakarta Mailに移譲された後、2021年になってEclipse Angusに移されたようです。
このライブラリは、Eclipse Public License 2.0 (EPL-2.0)で利用できます。
今回筆者はpostfixとopenDKIMでDKIM署名を実装するのでここでは触れませんが、Javaのコード内でDKIM署名を完結させたいなら、Simple Java Mailというライブラリを使うのが簡単そうです。今のところEclipse Angus Mail単独でのDKIMの実装はなさそうです。
必要なライブラリ群
Mavenを利用するなら、Angus Mail Provider という名前で管理されています。
筆者はMavenを使っていませんので、自身で依存を解決します。ドキュメントによると、最もシンプルな構成は「 angus-mail.jar と jakarta.mail-api.jar、angus-activation.jar 」ということのようなのでそれに従います。
ちなみに Mavenで依存性を解決すると、前述の3つのライブラリの他に jakarta.activation-api.jar が含まれていました。
後継ということもあり多くは、jakarta.mail-api.jarに依存しています。使い方などのドキュメントも、リリースから3年たった今でもjakarta mailのものとなっています。
jakarta で pop3
POP3で受信する際のJavaのコード例は、次のようになります。
RecvMail.java
//メール受信
public boolean receiveMail(String strHost, int intPort, String strUser, String strPassword, int intSslMode, int intTimeout, int intConnectionTimeout) {
try {
Properties props = new Properties();
String strProtocol;
String strPort = "";
int intDefaultPort = 110;
switch(intSslMode) {
case 0://startls
strProtocol = "pop3";
props.put("mail."+strProtocol+".starttls.enable", "true");
intDefaultPort = 110;
break;
case 1: //pop3s
strProtocol = "pop3s";
intDefaultPort = 995;
break;
case 2://plain
strProtocol = "pop3";
intDefaultPort = 25;
break;
default:
return false;
}
//ホストのセット
props.put("mail."+strProtocol+".host", strHost);
if (0 <= intPort && 65535 < intPort) {
strPort = String.valueOf(intDefaultPort);
} else {
strPort = String.valueOf(intPort);
}
props.put("mail."+strProtocol+".port", strPort);
//タイムアウトの設定
if (0 < intTimeout) {
props.put("mail."+strProtocol+".timeout", String.valueOf(intTimeout));
}
if (0 < intConnectionTimeout) {
props.put("mail."+strProtocol+".connectiontimeout", String.valueOf(intConnectionTimeout));
}
Session emailSession = Session.getDefaultInstance(props);
Store store = emailSession.getStore(strProtocol);
store.connect(strHost, strUser, strPassword);
Folder emailFolder = store.getFolder("INBOX");
emailFolder.open(Folder.READ_WRITE);
//読み出し専用
//emailFolder.open(Folder.READ_ONLY);
Message[] messages = emailFolder.getMessages();
//System.out.println("メールの数: " + messages.length);
for (int i = 0, n = messages.length; i < n; i++) {
//メッセージを取得
//ここで取得したmessageは変更できない
//もしカスタムヘッダをつけたいなら、ファイル出力時に付ける
Message message = messages[i];
// メッセージをEML形式でファイルに保存
if (saveMessageAsEml(message,"recv-mail-"+String.valueOf(i+1)+".eml")==true) {
//受信したメッセージに削除フラグを立てる
message.setFlag(Flags.Flag.DELETED, true);
}
}
//引き数にtrueを渡すとフラグを立てたメールを削除
emailFolder.close(true);
store.close();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//受信データをemlファイルとして保存
private static boolean saveMessageAsEml(Message message,String strSavePath) throws IOException, MessagingException {
File emlFile = new File(strSavePath);
try (FileOutputStream out = new FileOutputStream(emlFile);
BufferedOutputStream buf = new BufferedOutputStream(out)) {
message.writeTo(buf);
return true;
} catch(Exception e) {
e.printStackTrace();
return false;
}
}
Message インスタンスの wirteToメソッドでそのまま eml形式のファイルとして保存することができます。
Jakarta Mailでは emlファイルのパースも簡単にできるようになっていますので、そちらも後述します。
jakarta で smtp
SMTPで送信する際のJavaのコード例は、次のようになります。
SendMail.java
//メール送信
public static boolean sendEmail(String strServer, String strUser, String strPassword, int intPort,
String strFrom, String strTo, String strCc, String strBcc,
String strTitle,String strBody, String[] filePaths,
int intSslMode, boolean blnAuth) {
Properties props = new Properties();
StringBuffer sbMessage = new StringBuffer();
//プロトコル(SSL)
String strProtocol = null;
try {
switch(intSslMode) {
case 2://smtps
strProtocol = "smtps";
//証明書の検証を無効にしたい場合
//props.put("mail."+strProtocol+".ssl.checkserveridentity", "false");
break;
case 1://startls
strProtocol = "smtp";
props.put("mail."+strProtocol+".starttls.enable", "true");
//証明書の検証を無効にしたい場合
//props.put("mail."+strProtocol+".ssl.checkserveridentity", "false");
break;
case 0://plain
default:
strProtocol = "smtp";
}
//サーバーの設定
props.put("mail."+strProtocol, strServer);
//認証の有無
if (blnAuth) {
props.put("mail."+strProtocol+".auth", "true");
}
//ポート
String strPort;
if (intPort <=0 || 65535 < intPort) {
strPort = "25";
} else {
strPort = String.valueOf(intPort);
}
props.put("mail."+strProtocol+".port", strPort);
//セッション生成
Session session = Session.getInstance(props, null);
//デバッグ時に通信状態をコンソールに出力します
//session.setDebug(true);
//メッセージ
Message msg = new MimeMessage(session);
//fromのセット
if (strFrom != null && strFrom.equals("")==false) {
msg.setFrom(new InternetAddress(strFrom));
} else {
msg.setFrom();
}
//toのセット
if (strTo!=null && strTo.equals("")==false) {
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(strTo));
}
//cc
if (strCc!=null && strCc.equals("")==false) {
msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(strCc));
}
//bcc
if (strBcc!=null && strBcc.equals("")==false) {
msg.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(strBcc));
}
//タイトル
if (strTitle==null) {
msg.setSubject("タイトルなし");
} else {
msg.setSubject(strTitle);
}
if (filePaths != null && 0 < filePaths.length) {
MimeMultipart mp = new MimeMultipart();
MimeBodyPart mbpMain = new MimeBodyPart();
mbpMain.setText(strBody);
mp.addBodyPart(mbpMain);
for(int i = 0; i<filePaths.length; i++) {
File f = new File(filePaths[i]);
if (f.isFile()) {
//System.out.println("AttatchFile:" + f.getName());
MimeBodyPart mbpFile = new MimeBodyPart();
mbpFile.attachFile(f);
mp.addBodyPart(mbpFile);
} else {
sbMessage.append("AttatchFile:" + f.getName()+" is not found\r\n");
}
}
msg.setContent(mp);
} else {
msg.setText(strBody);
}
//メーラーをセットしたければ
//msg.setHeader("X-Mailer", "jakarta-mail-api");
//日付
msg.setSentDate(new Date());
//送信
SMTPTransport t =(SMTPTransport)session.getTransport(strProtocol);
if (blnAuth) {
t.connect(strServer, strUser, strPassword);
} else {
t.connect();
}
//失敗時は例外が投げられる
t.sendMessage(msg, msg.getAllRecipients());
sbMessage.append(t.getLastServerResponse()+"\r\n");
//メッセージを取得
System.out.println(sbMessage.toString());
return true;
} catch(Exception e) {
//エラー
e.printStackTrace();
return false;
}
}
session.setDebug(true); を設定することで、デバッグ用に通信内容をコンソールに出力することができます。
ログ中で、SSL通信がされているかどうかを見るには、isSSLの項目を確認します。
STARTLSを設定した場合は、最初は非SSLで接続しますので、この値はfalseになり後に、STARTLSのログが出力されます。
DEBUG SMTP: trying to connect to host "mail.sv.jp", port 587, isSSL false
...
STARTTLS
220 2.0.0 Ready to start TLS
サーバー証明書の検証ができない場合は、SMTPSでは「 Could not connect to SMTP host: mail.sv.jp, port: 465 」、STARTLSでは「 Could not convert socket to TLS 」が出力されます。
このエラーが設定不備なのか、証明書の検証に失敗によるものなのかチェックしたい場合は、一旦検証を無効にしてみます(証明書の検証はデフォルトで有効になっているようです)。これには「 props.put("mail."+strProtocol+".ssl.checkserveridentity", "false"); 」をコード内で実行する必要があります。
ちなみに検証が有効な状態で検証失敗状態を作りたかったら、検証マシン上のホストファイルに mailserver.test.jp などと適当なホスト名と正当なIPアドレスを結び付け、適当なホスト名で名前解決した際に拒絶さるかチェックするという方法があります。
jakarta mailでemlファイルをパース
POP3で取得したデータや、Thunderbirdなどから取得した .emlファイルを読みだすには次のようなコードで実現できます。
ParseEmlFile.java
//.emlファイルをパース
public static boolean parseEmlFile(String strFilePath) {
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
try (FileInputStream is = new FileInputStream(strFilePath)) {
Message message = new MimeMessage(session, is);
//タイトルがエンコードされていることがあるのでデコード
System.out.println("subject:"+MimeUtility.decodeText(message.getSubject()));
//from
if (0 < message.getFrom().length) {
System.out.println("from:"+MimeUtility.decodeText(message.getFrom()[0].toString()));//fromはひとつにしておく
}
//to
if (0 < message.getAllRecipients().length) {
Address[] adds = message.getRecipients(Message.RecipientType.TO);
StringBuffer sbWk = new StringBuffer();
for (int i = 0, max = adds.length; i < max; i++) {
if (i!=0) {
sbWk.append(",");
}
sbWk.append(getPureAddress(adds[i]));
}
System.out.println("to-address:"+sbWk.toString());
}
// 本文を取得
Object content = message.getContent();
if (content instanceof String) {
//メッセージ単体の場合
System.out.println("message:"+content);
} else if (content instanceof Multipart) {
//マルチパートの場合
Multipart multipart = (Multipart) content;
for (int i = 0; i < multipart.getCount(); i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
//添付ファイルの場合
InputStream isAttachment = bodyPart.getInputStream();
//メールに指定してあるファイル名でカレントフォルダに保存
File f = new File(bodyPart.getFileName());
try (FileOutputStream fos = new FileOutputStream(f)) {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = isAttachment.read(buf)) != -1) {
fos.write(buf, 0, bytesRead);
}
}
} else {
//マルチパートメッセージの場合
System.out.println("message:"+ bodyPart.getContent());
}
}
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}