背景
とある事情でEnigMail(ThunderBird)のユーザーだった。
が、先日、PGP暗号化メールのdecrypted copyを一括で作ることになった。
単一復号化は、メールを右クリックして「Decrypt to folder」で行えるが、一括復号化は、メールフィルタの「If OpenPGP encrypted, create decrypted copy」で行わねばならない。しかし、私の環境ではなぜかフィルタがうごかなかった。
ところで、右クリックでの復号に微細な不具合が見受けられたので、修正を試みた。それが https://gitlab.com/enigmail/enigmail/-/merge_requests/47 である。その過程で https://gitlab.com/enigmail/enigmail/blob/enigmail-2.1-branch/package/persistentCrypto.jsm を調査していたのだが、実は復号処理は簡単なのではないかと思ったので実装を試みた。
方法
言語
最近最も触っている言語であり、またメールパーサーが標準ライブラリに存在することもあり、Pythonを選択1。
IMAP
EnigMailはThunderBirdの機能拡張だが、本ツールはスタンドアローンで動作する都合上、IMAPアクセスが必須となる。これは https://docs.python.org/ja/3/library/imaplib.html#imap4-example をほぼ丸写しで良い。
メール
復号エンジン
デファクトスタンダードとして、GnuPGとした。GnuPGは外部プログラムなので、subprocessモジュールを必要とする。ところが、subprocess.Popenにはコンテキストマネージャがない。そこで、(コード)ブロック中に例外発生時にはプロセスを停止させるようなPopenラッパーを作成した。
通常の暗号化
本文をgpg --decrypt --skip-verify
に流せば良い(検証結果に関わらず復号はされてほしいので--skip-verifyは必須である)。マルチパートの場合は1パートごとに。ただし、Content-Transfer-Encodingが指定されている場合は事前に処理する必要がある。base64.b64decodeやquopri.decodestringを用いる。
(中身が始めからエンコードされている場合あるいは)中身にマルチバイト文字が含まれる場合はエンコードする必要がある。このエンコードはEnigMailにならいBase64を72文字ごとに改行する実装としたが、末端のみ72文字未満でも改行する必要がある(A)。
添付ファイルのファイル名取り出しだが、PGPには、ファイル名を暗号化ファイルに埋め込む機能があるらしい(?)。GnuPGは--enable-special-filenames
なるオプションで、-&N
なるファイル名が埋め込まれることがあるので、この形式のファイル名が来た場合は無視する必要がある(B)。ただし今回作成したツールでは埋め込みファイル名ではなく常にContent-Disposition filenameを使うのでこの影響はない。
なお、この(A)および(B)がEnigMailの「微細な不具合」の内容である。
PGP/MIME
MIME partの1番目がバージョン情報(常に1)、2番目が実体メール(ただしヘッダは含まない)となっている。よって、「実体メール」にヘッダをコピーして返すようにすれば良い。
件名暗号化
PGP/MIMEは実体メールにヘッダは含まないと書いたが、EnigMailの件名暗号化は実体メールにsubjectヘッダを入力することで実装されている。よって、実体メールにヘッダをコピーする際、すでに存在しているヘッダは単にコピーしないようにすれば良い。
結果
https://github.com/cielavenir/imap_mass_decrypter/blob/master/imap_decrypter.py により、無事に一括復号化することができた(pypiは利用していない)。しかもメールフィルタはフィルタを掛けるフォルダを1つずつ指定する必要があるが、こちらであれば全フォルダを一度に対象にできる。
-
Rubyだとmail gemとかを使うことになると思いますが、標準ライブラリではない ↩