javax.mailの代替
以前、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 exchangeやjavatips.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字で自動的に改行が入るようです。
参考にさせていただきましたサイトやコードの作者の皆様、ありがとうございました。