あるメールマガジンが Content-
multipart/releted メール構造
multipart/related のメール構造の例は以下のようなものである。HTML メールを表示出来ないソフトのための代替テキスト,quoted-
... Mime-Version: 1.0 Content-Type: multipart/related; boundary="L8jCUsh5" Subject: =?ISO-2022-JP?B?Gy... Message-ID: <833032597.31891656.1443556497891.Mail.root@xxx.yyy.jp> User-Agent: Mail/5.1.1 From: ZZZZ <mailmagazine@yyy.jp> To: isao@yasuda.homeip.net Date: Fri, 2 Oct 2015 18:01:37 +0900 (JST) --L8jCUsh5 Content-Type: multipart/alternative; boundary="----=_NextPart_000_000D_01C575C8.BEFD7DC0" ------=_NextPart_000_000D_01C575C8.BEFD7DC0 Content-Type: text/plain; charset=ISO-2022-JP Content-Transfer-Encoding: 7bit いつもお世話に... [ HTMLメールが表示できないメールソフトのための代替テキスト ] ------=_NextPart_000_000D_01C575C8.BEFD7DC0 Content-Type: text/html; charset=ISO-2022-JP Content-Transfer-Encoding: quoted-printable <HTML><HEAD><META http-equiv=3D'Content-Type' content=3D'text/html; charset= ... [ quoted-printable encode の HTML 本体 ] ------=_NextPart_000_000D_01C575C8.BEFD7DC0-- --L8jCUsh5 Content-Type: image/jpeg; name="14437727600.jpeg" Content-Transfer-Encoding: base64 Content-ID: <1f1443575835@DECOIMG> /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg ... [ jpeg base64 encode の画像データ ] --L8jCUsh5 Content-Type: image/jpeg; name="14437727601.jpeg" Content-Transfer-Encoding: base64 Content-ID: <1f1443772082@DECOIMG> /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg ... [ jpeg base64 encode の画像データ ] --L8jCUsh5--
プログラム設計
MIME::Parser Perl モジュールを利用すると簡単にデコードが出来る。
ただし,このモジュールはパースしたマルチパートすべてをファイル出力するデフォルト仕様になっており,必要なものを意図をもって書き出す以外は自動出力を抑止したい,というときは,
また,MIME::Parser モジュールは,ヘッダの Subject が ISO-2022-JP MIME エンコードの場合,デコードできないようである。本プログラムでは Encode モジュールの機能によりデコードすることとした。
プログラムの処理仕様は以下のとおり。
- 生成する HTML ファイル名は現在日付+メールメッセージファイル名(数字)4桁で整形したものとする。
- HTML エンコードを ISO-2022-JP から UTF-8 に変換する。
- <title> を設定する。この HTML パートのテキストには <title> タグがない。メールヘッダの Subject: のデコード文字列を <title> に設定することとした。
- 画像 src 属性の Content-ID 指定を画像ファイル名で置き換える。このメールマガジンでは,
image/ jpeg パートのヘッダで画像に Content- ID を振っており,HTML の <img> タグ src 属性がこの Content- ID で指定されている。たとえば,上記メール例では,一つ目の画像ファイル 14437727600. jpeg を用いる <img> タグの記述は,<img src= 'cid:1f1443575835 @DECOIMG'> とマークアップされている。よって,実際に閲覧可能な HTML を生成するには,この cid:1f1443575835 @DECOIMG の Content-ID 指定をそれに対応する画像ファイル名で置き換えなくてはならない。画像は複数挿入されている場合があり, Content- ID と画像ファイル名の対を配列に格納して,すべての画像を取り出したあとで,HTML の <img src= 'cid: xxx'../> の cid: xxx 部分を画像ファイル名で書き換える。
mailparser.pl Perl コード
Perl コードを以下に示す。mailparser 6 のように引数にメールメッセージファイル(番号)を指定して起動する。カレントディレクトリに HTML とそこから参照している jpeg ファイルが生成される。
#!/usr/bin/perl -w # -*- coding: utf-8; mode: cperl; -*- # mailparser.pl # usage: mailparser メールメッセージファイル # - メールメッセージからHTMLファイル,画像ファイルを生成する # - HTMLファイル名は実行日付yyyymmdd + メールメッセージ番号 xxxx + .html # - 画像ファイル名とContent-IDをマルチパートヘッダから取得する # - HTML 文字コードを iso 2022-jp から UTF-8 に変換する # - HTML画像srcのContent-ID指定を画像ファイル名指定に書き換える # $Id: mailparser.pl 20 2015-10-04 15:23:48Z isao $ use strict; use utf8; use MIME::Parser; use Encode; binmode(STDOUT, ":utf8"); binmode(STDERR, ":utf8"); ($#ARGV < 0) && die "usage $0 mailmessagefile\n"; # MIME::Parser オブジェクト my $parser = MIME::Parser->new; $parser->output_to_core(1); # ファイル自動出力を抑止 my $entity = $parser->parse_open($ARGV[0]); # Subject の取得: MIME::Parser では ISO-2022-JP 扱えず,Encode モジュールでデコード my $sub = decode('MIME-Header', $entity->head->get('subject')); $sub =~ s/\s+$//g; # 末尾の空白・改行を除去 # HTMLコンテンツ my $html = ""; my %imgpair; # 「Contet-ID:imageファイル名」の配列 # メールメッセージと現在日付からHTMLファイル名を生成 my ($day, $mon, $year) = (localtime)[3, 4, 5]; $year += 1900; $mon += 1; my $today = sprintf("%04d%02d%02d", $year, $mon, $day); my $num = $ARGV[0] + 0; my $htfn = $today . "-" . sprintf("%04d", $num) . ".html"; # parts_DFS メソッドで取り出したパート毎の処理 foreach my $part ($entity->parts_DFS) { my $ctype = $part->mime_type; # MIME Type my $body = $part->bodyhandle; # パートのボディ if ($body) { my $cont = $body->as_string; # 内容を取り出して変数に格納(デコード済) if ($ctype eq 'text/html') { # HTML の編集 # ISO 2022-JP を UTF-8 に変換 $html = decode("iso-2022-jp", $cont); # head に title を挿入 $html =~ s/<\/HEAD>/<title>$sub<\/title><\/HEAD>/i; } elsif ($ctype =~ /image\//) { # 画像ファイル名等の情報取得 # パートの header から画像ファイル名と画像IDを取得 my $head = $part->header; my $imgf; my $imgid; foreach my $i ($head) { for (my $j = 0; defined($i->[$j]); $j++) { if ($i->[$j] =~ /name="([^\.]*)\.([^"]*)"/i) { $imgf = $1 . "." . $2; # 画像ファイル名 } if ($i->[$j] =~ /Content-ID: <([^>]*)>/i) { $imgid = $1; # 画像 Content-ID } } } # ファイルに出力 open(FH, ">", $imgf) || die "can not open $imgf: $!\n"; binmode FH; print FH $cont; close(FH); # 画像 Content-ID とファイル名の対を登録 $imgpair{$imgid} = $imgf; } } } # html中のイメージsrcのIDをファイル名で書き換え # <img src="cid:content-id"> --> <img src="filename.jpeg"> foreach my $key (keys(%imgpair)) { $html =~ s/cid:$key/$imgpair{$key}/gi; } open(FH, ">:utf8", $htfn) || die "can not open $htfn: $!\n"; print FH "$html\n"; close(FH);