FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

Javaで.mimeファイルから添付ファイルを抜き出す

こんにちは。
フリュー株式会社 ピクトリンク事業部新卒の森兼と申します。
メールのソースファイル (.mime) から添付ファイルを抽出するJavaについて書きたいと思います。

はじめに

 新卒研修としてメールを分析するシステムを作ることになりました。
そこで、メールの.mimeファイルから添付ファイルを抽出するJavaのコードが必要になりました。 添付ファイル付きのメールをJavaで送信する記事はたくさんありますが、 受信したメールから添付ファイルを抜き出すJavaは意外に少なかったので、記事にしようと思いました。

MIME形式のメールについて

 今回対象とするメールはMIME形式のメールです。 MIMEとは「Multipurpose Internet Mail Extensions」の略で、Multipurposeとあるように、添付ファイルなど単純なテキスト以外なものも送ることができるメール形式です。

 そして、MIMEメールにはマルチパートという概念があります。 詳しく解説されている記事は既にたくさんありますので、すごく簡単にいうと一つのメールの中にマルチパートという箱があり、その中に本文や添付ファイルの領域が別々に存在します。 また、マルチパートの中にさらにマルチパートということもありえます。

 マルチパートの場合、本文や添付ファイルは基本的にマルチパートの奥底に眠っているので、掘り起こしていく処理が必要です。 一方で、本文だけ or 添付ファイルだけのメールなどはマルチパートではありません。掘り起こす処理は不要です。

 つまり、メールから添付ファイルを抜き出そうとした場合に必要な処理の流れは以下の通りです。

  • 1. マルチパート構造なのかを確認
  • 2-1. マルチパートだった場合
    添付ファイルまで掘り進み、見つかったらStreamで抜き出す
  • 2-2. マルチパートじゃない場合
    添付ファイルだけが含まれているメールだったらStreamで抜き出す

以上の手順をJavaで作ったので共有します。

サンプルコード

Javaバージョン

サンプルコードは Java 21 で動作確認しています。

import lombok.extern.slf4j.Slf4j;
import javax.mail.BodyPart;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import java.io.*;
import java.util.Properties;

@Slf4j
public class GetAttachFile {
    public static void main(String[] args) {
        byte[] mailBytes;
        try (FileInputStream fileInputStream = new FileInputStream("添付ファイルを抜き出したいMIMEファイルのパス")){
            mailBytes = fileInputStream.readAllBytes();
        }catch (Exception e){
            throw new RuntimeException(e);
        }
        byte[] attachFileBytes = get(mailBytes);
        // テスト用に適当にStringで出力
        log.info(new String((attachFileBytes)));
    }
    static byte[] get(byte[] mailBytes){
        MimeMessage mimeMessage;
        byte[] contentsBytes;

        // byte配列をMimeMessageオブジェクトとして受け取る
        try(ByteArrayInputStream downloadFileStream = new ByteArrayInputStream(mailBytes)) {
            Session session  = Session.getInstance(new Properties(), null);
            mimeMessage = new MimeMessage(session, downloadFileStream);
        }catch (Exception e){
            log.error("mime形式に変換できません");
            throw new RuntimeException(e);
        }
        try {
            // 1. マルチパート構造を持つのか確認する
            if(mimeMessage.getContentType().startsWith("multipart")){
                
                // 2-1 マルチパートだった場合
                Multipart multipart = (Multipart)mimeMessage.getContent();
                BodyPart bodyPart = walkMultipart(multipart);
                String fileName = bodyPart.getFileName();
                if(fileName == null){
                    log.error("添付ファイルが存在しません");
                    return new byte[0];
                }
                try(InputStream inputStream = bodyPart.getInputStream()){
                    contentsBytes = inputStream.readAllBytes();
                    return contentsBytes;
                }catch (IOException e){
                    throw new RuntimeException(e);
                }
                
            }else{
                
                // 2-2 マルチパートじゃないときの処理
                String fileName = mimeMessage.getFileName();
                if(fileName == null){
                    log.error("添付ファイルが存在しません");
                    return new byte[0];
                }
                try(InputStream inputStream = mimeMessage.getInputStream()){
                    contentsBytes = inputStream.readAllBytes();
                    return contentsBytes;
                }catch (IOException e){
                    throw new RuntimeException(e);
                }
                
            }
        } catch (Exception e) {
            log.error("Mimeメッセージの取得に失敗");
            throw new RuntimeException(e);
        }
    }
    // multipartのメッセージの中身を再帰的に展開していき、添付ファイルを見つける関数。fileName != nullなら添付ファイルに辿り着いたことになる。
    static BodyPart walkMultipart(Multipart multipart) {
        BodyPart bodyPart = new MimeBodyPart();
        try {
            for(int i=0;i<multipart.getCount();i++){
                bodyPart = multipart.getBodyPart(i);
                if(bodyPart.getContentType().startsWith("multipart")){
                    Multipart nextPart = (Multipart)bodyPart.getContent();
                    bodyPart = walkMultipart(nextPart);
                }
                String fileName = bodyPart.getFileName();
                if(fileName != null){
                    break;
                }
            }
        } catch (Exception e) {
            log.error("mime展開に失敗");
            throw new RuntimeException(e);
        }
        return bodyPart;
    }
}

1. マルチパートなのかを確認

 まずMIME形式のメールをJavaで扱うためのライブラリとして、JavaMailライブラリを使用します。 解析したいメールをFileInputStreamなりを使ってInputStreamにしましょう。 それをMimeMessageクラスのコンストラクタに与えることで、MIME形式としてJavaで取り扱うことができます。

MimeMessage mimeMessage;
byte[] contentsBytes;

// byte配列をMimeMessageオブジェクトとして受け取る
try(ByteArrayInputStream downloadFileStream = new ByteArrayInputStream(mailBytes)) {
    Session session  = Session.getInstance(new Properties(), null);
    mimeMessage = new MimeMessage(session, downloadFileStream);
}catch (Exception e){
    log.error("mime形式に変換できません");
    throw new RuntimeException(e);
}

 そうして得られたMimeMessageオブジェクトのgetContentType()メソッドによって、メールがマルチパート構造を持っているのかを確認します。

 マルチパートの場合は、Content-Typemultipart/alternativeだったり、multipart/mixedだったり、multipart/relatedなどと書いています。
そのため、Content-Typeに対してstartsWith("multipart")を使って確認します。 マルチパートだった場合は、さらに内部にマルチパートが埋まっていたりするので、再帰的に確認していく処理が必要になります。

 マルチパートでない場合は、本文だけのメールや、添付ファイルがポツンと置かれているメールです。 ファイル名が存在するかどうかでテキストなのか、添付ファイルなのかを見分けることができます。

if(mimeMessage.getContentType().startsWith("multipart")){
               // 2-1の処理
            }else{
               // 2-2の処理
            }
}

2-1.マルチパートだった場合の処理

 マルチパートだった場合は、その中身 (BodyPartといいます) をgetContent()で抜き出し、別途作成したwalkMultipart()で中身を再帰的に確認します。
walkMultipartは添付ファイルを見つけるか、マルチパートの中身を全探索すると、それをbodypartに詰めて戻します。

 最終的に得られたbodypartにファイル名が存在すれば、それは添付ファイルであり、 ファイル名が存在しなければ、添付ファイルが無かったことになります。

 添付ファイルを見つけることができた場合は、getInputStream()readAllBytes()を使って中身をbyte配列に書き出します。
添付ファイルがBase64*1エンコードされていた場合は、きちんとBase64DecorderStreamを返してくれるため、readAllBytes()でデコード済みのbyte配列を得ることができます。
添付ファイルがなかった場合は何もせずreturnです。

// 2-1 マルチパートだった場合
Multipart multipart = (Multipart)mimeMessage.getContent();
BodyPart bodyPart = walkMultipart(multipart);
String fileName = bodyPart.getFileName();
if(fileName == null){
    log.error("添付ファイルが存在しません");
    return new byte[0];
}
try(InputStream inputStream = bodyPart.getInputStream()){
    contentsBytes = inputStream.readAllBytes();
    return contentsBytes;
}catch (IOException e){
    throw new RuntimeException(e);
}

 マルチパートの再帰的処理 walkMultipart の中身はこちらです。
マルチパートの中には複数のセクションがあり、その中にまたマルチパートがあったりします。
そのため、マルチパートのセクションを一つずつ確認し、マルチパートだった場合はさらにその中のセクションを確認します。
イメージとしては木構造の深さ優先探索です。

 そうして、マルチパート以外のものが得られた場合はgetFileName()でファイル名が存在するかを確認します。 ファイル名がある場合は添付ファイルなので、ループ処理を停止し、再帰処理を中止します。 もし、ループ処理が最後まで行っても添付ファイルがない場合は、mutipart構造の最も奥にあるコンテンツが返されます。 当然このコンテンツにはファイル名が存在しないので、2-1の条件分岐で判定します。

// multipartのメッセージの中身を再帰的に展開していき、添付ファイルを見つける関数。fileName != nullなら添付ファイルに辿り着いたことになる。
static BodyPart walkMultipart(Multipart multipart) {
    BodyPart bodyPart = new MimeBodyPart();
        try {
            for(int i=0;i<multipart.getCount();i++){
                bodyPart = multipart.getBodyPart(i);
                if(bodyPart.getContentType().startsWith("multipart")){
                    Multipart nextPart = (Multipart)bodyPart.getContent();
                    bodyPart = walkMultipart(nextPart);
                }
                String fileName = bodyPart.getFileName();
                if(fileName != null){
                    break;
                }
            }
        } catch (Exception e) {
            log.error("mime展開に失敗");
            throw new RuntimeException(e);
        }
    return bodyPart;
}

2-1.マルチパートじゃなかった場合の処理

 マルチパートじゃなかった場合の処理は単純です。 何度も述べた通り、添付ファイルだった場合はファイル名が存在します。 ファイル名がなかった場合は何もせず、 あった場合はInputStreamを取得し、ファイルの中身をバイト配列で取得します。

// 2-2 マルチパートじゃないときの処理
String fileName = mimeMessage.getFileName();
if(fileName == null){
    log.error("添付ファイルが存在しません");
    return new byte[0];
}
try(InputStream inputStream = mimeMessage.getInputStream()){
    contentsBytes = inputStream.readAllBytes();
    return contentsBytes;
}catch (IOException e){
   throw new RuntimeException(e);
}

終わりに

 MIMEメールを扱う際には便利なライブラリのあるPythonを使う人が多いと思います。
でも今回のようにJavaでも十分扱えるので、ぜひ参考にしてもらいつつコードを書いていただければ嬉しいです。

*1:64種の英数字で表現するエンコード方式。電子メールで使われることが多い。