メールデータの構造

メールデータの構造について調べたのでまとめてみます。

メールは全てただのテキストデータ

メールデータを理解する上で、最も重要なのは以下の1文です。以下で書かれている「テキスト」とは、ASCIIコード*1のことです。

インターネットメールは、もともとテキストベースのメールシステムであった。つまり、テキストしか送れないシステムである。

MIME(Multipurpose Internet Mail Extensions)〜前編:インターネット・プロトコル詳説(3) - @IT


でも写真やExcelファイルも添付できますよね?どういうことでしょうか?それは、以下の通りです。

あらゆるデータをテキスト(正確には、英語圏の人間にとってのテキスト)に変換して送受信しようとするのが、インターネットメールの根本的な考え方である。

MIME(Multipurpose Internet Mail Extensions)〜前編:インターネット・プロトコル詳説(3) - @IT


つまり、一通のメールは、件名も、本文も、添付ファイルも、一枚のテキストファイルとして扱っており、具体的には以下のようにまとめることができます。

  • メールで送信可能なのは、(7ビットの)ASCIIコードのみ
  • 以下のような7ビットで表現できないデータは、7ビットに(無理やり)変換して送信
    • 写真
    • Word,Excelなど
    • 英語圏以外の言語(日本語など)

参考:マルチパートメールが作られるようす

メールは「ヘッダ」と「ボディ」で構成されている

メールは以下のように、送信情報(宛先など)として「ヘッダー」があり、区切りとして「空行(CRLF)」の後に、実際のメッセージ「ボディ」というレイアウトになっています。

  1. ヘッダー
  2. 空行(CRLF)
  3. ボディ
ヘッダー

ヘッダーは、標準仕様/非標準仕様*2を含めると、かなり多くの種類があり複雑な仕様ですが、主なものを押さえておけば大抵は問題ないので、以下の項目は覚えておくと良いと思います。

  • To :宛先
  • From :差出人
  • Subject :メール件名
ボディー

ボディについては、まとめれば以下の2つしかありません。もちろん添付可能なファイルの種類はたくさんありますが。

  • 本文
  • 添付ファイル

メールデータの中身を見てみる

添付ファイルがある場合と無い場合で、メールの実際の中身を確認してみます。ここでは、特にMIMEや、Content-Typeの部分に着目して記述しています。


ケース1.テキストだけ(添付ファイル無し)のメール


下記のようなメールは、シングルパートメールと呼ばれます。Content-Typeに「text/plain;」とある通り、本文がテキスト情報だけ(添付ファイルが無い)ことが分かります。
さらにボディ部を見てみると、ヘッダーにも「charset="iso-2022-jp"」とある通り、本文の(日本語)テキストがiso-2022-jp*3エンコードされています。これは日本語を利用する場合の標準的なエンコード方法です。

メールヘッダー
From: test@example.ne.jp
To: sample@example.ne.jp
Subject: =?iso-2022-jp?B?GyRCJUYlOSVIGyhC?=
Mime-Version: 1.0
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit

空行(CRLF)

メールボディ
[(エスケープ文字)]$B%7%s%0%k%Q!<%H%a!<%k$N%\%G%#It$G$9!#[(エスケープ文字)](B

ケース2.画像が添付されたメール


下記のようなメールは、マルチパートメールと呼ばれ、先ほどと異なり、Content-Typeが「multipart/mixed;」となっています。
これは、「添付ファイルがあるので、ボディ部が複数のパートに分かれていますよ」という意味で、その後に記載されている「boundary="-----=_NextPart_11369_60974_45043"」が各パートの境界を示しています。


そして、マルチパートメールの場合、各パート毎にヘッダーが付与されており、以下のような情報が記載されています。

メールヘッダー
From: test@example.ne.jp
To: sample@example.ne.jp
Subject: =?iso-2022-jp?B?GyRCSiM/dCRORTpJVSVVJSElJCVrGyhC?=
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="-----=_NextPart_11369_60974_45043"

空行(CRLF)

メールボディ(1パート目)
 -------=_NextPart_11369_60974_45043
Content-Type: text/plain; charset="iso-2022-jp" ←本文テキストを表しています。
Content-Transfer-Encoding: 7bit

空行(CRLF)

メールボディ(2パート目)
 -------=_NextPart_11369_60974_45043
Content-Type: image/jpeg; name="=?iso-2022-jp?B?GyRCJVUlISUkJWsbKEIuanBn?=" JPEG画像を表しています。
Content-Transfer-Encoding: base64 BASE64形式でエンコードされています。
Content-Disposition: attachment; filename="=?iso-2022-jp?B?GyRCJVUlISUkJWsbKEIuanBn?=" ←「attachment」は添付ファイルということです。

空行(CRLF)

メールボディ(3パート目)
 -------=_NextPart_11369_60974_45043
Content-Type: image/gif; name="=?iso-2022-jp?B?GyRCJCokZCQ5JF8bKEIuZ2lm?="
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="=?iso-2022-jp?B?GyRCJCokZCQ5JF8bKEIuZ2lm?="

 -------=_NextPart_11369_60974_45043-- ←「--」で終わる区切り文字は、マルチパートの全体の終了を意味します。


改めて、ちゃんと見てみると、メールって結構複雑なデータ構造ですね。でも一度分かってしまえば、特に難しく感じなくなると思います。

*1:8ビット中の下位7ビットまでしか使用していないため、「7ビットコード」とも呼ばれる

*2:参考:RFC2076

*3:iso-2022-jpは7ビットJISとも呼ばれ、日本語を7ビットで表現する文字コードです。

ホストcookieとドメインcookie

たいした話じゃないのですが、ちょっと調べてしまったのでメモ。


下記のようなHTTPレスポンスヘッダーが返却された場合、

Set-Cookie: aaa=111; expires=Tue, 31-Dec-2030 23:59:59 GMT; path=/;
Set-Cookie: bbb=222; expires=Tue, 31-Dec-2030 23:59:59 GMT; path=/; domain=.example.jp;


理由は、ドメインcookieになるのは、「domain=DAMAIN_NAME」としてちゃんと指定した場合であり、以下引用の通り、指定していない場合(つまりデフォルト値)は、ホストcookieとみなされます。

domainのデフォルト値は、クッキー応答を生成したサーバのホスト名です。

Cookie仕様 日本語訳 - futomi's CGI Cafe

syslogで長いメッセージは出力できない

「syslog出力したメッセージが、途中で切れてしまった!」という話があったので、ここにメモ。
その原因は単純で「メッセージが長すぎた」ことです。以下の通り、syslogプロトコルでは、メッセージは1024バイトまでなのです。

パケット全体の長さは、1024バイトまたはそれ以下でなければならない(MUST)

RFC 3164: The BSD syslog Protocol (日本語)


これを考慮して、下記くらいは、ログメッセージの設計で注意したいですね。

  • あまり長いメッセージにしない
  • 重要なエラー情報は前にもってくる

JavaでOSのコマンドを実行したい時は

Javaでバッチ開発をしていると、OSのコマンドを使いたくなるシーンがあります。例えば、以下のような場合などです。

  • ls コマンドで対象ディレクトリのファイル一覧を取得したい
  • ping コマンドで、通信先サーバの状況確認をしたい


そのような時に用いられるのが「java.lang.Runtime」というクラスで、Javaとその実行環境との橋渡しをする役目を持っています。基本的な使い方としては、以下のようになります。

  1. Runtimeインスタンスは「Runtime.getRuntime」メソッドで取得
  2. 取得したRuntimeインスタンスの「exec」メソッドを実行
public class Command {
    public static void main(String[] args) {
        try {
            Runtime runtime = Runtime.getRuntime();
            runtime.exec("ls");
        } catch (IOException ex) {
        }
    }
}


さらに、実行した結果が欲しい場合は、「java.lang.Process」というクラスを利用します。というより、Runtimeインスタンスの「exec」メソッドの戻り値は、この「Process」クラスになっています。

  1. execメソッドの戻り値の「Process」クラスを取得
  2. 「Process」クラスのメソッドを使って内容などを取得


内容を取得する場合は、「getInputStream」メソッドを使用します。

public static void main(String[] args) {
    try {
        Runtime runtime = Runtime.getRuntime();
        Process p = runtime.exec("ls");
        InputStream is = p.getInputStream();
        // InputStreamさえ得られれば、後は必要に応じて内容を取得すればよい。
        // 以下、省略
    } catch (IOException ex) {
    }
}


終了コードのみで問題ない場合は、「waitFor」メソッドを使用します。

public static void main(String[] args) {
    try {
        Runtime runtime = Runtime.getRuntime();
        Process p = runtime.exec("ping 192.168.0.1");
        return p.waitFor();
    } catch (IOException ex) {
    }
}

JavaScriptの setTimeout関数の実行タイミングに関する誤解

JavaScriptの「setTimeout」は、(ミリ秒で)指定した時間後に、指定した関数を実行してくれるのですが、実は実行タイミングがややこしい!のです。私も誤解していました。


例えば、下記を実行すると、どうなるでしょうか?

document.write("hoge\n");
setTimeout(function(){ document.write("fuga\n") }, 1000);
document.write("piyo\n");


実行結果は、下記の引用の通り、setTimeout関数で指定した「fuga」という文字列が最後に表示されます。

普通に JavaScript を使いこなしてる人なら、hoge → piyo と表示して、 1 秒後に fuga が表示されるな。って思うはずなんです。
でも、 JavaScript を始めたばっかりの人の中には、 hoge と表示したあと 1 秒後に fuga → piyo と表示するな。って思ってる人が非常に多い。(経験的に)

JavaScript を学ぶ際に一番重要なのに、誤解されがちな setTimeout 系の概念 - IT戦記

タイマーのカウントダウンは「一連の処理が終わってから」

つまり、setTimeoutで指定した時間がカウントダウンされるのは、「一連の処理が終わってから」であり、「setTimeout の行を通過する時ではない」ということです。この「一連の処理」について確認するために、以下のコードを実行してみます。

function timer(){
    alert("aaa");
	setTimeout( function() {alert("bbb");}, 5000);
	alert("ccc");
}
function main(){
    alert("ddd");
	timer();
	alert("eee");
}
main();


実行結果は下記のようになり、特に「eee が表示されてから、5秒後に bbb が表示」されることがポイントです。ここでは、「一連の処理」=「mainメソッド⇒timerメソッド」となり、その一連の処理が完了してから指定時間(ここでは5秒)をカウントしていることが分かります。

ddd
aaa
ccc
eee
〜〜〜 ここで5秒間の待ちが発生 〜〜〜
bbb


この(実行タイミングの)特徴により、以下のことが保証されることになります。
setTimeoutで指定した関数が実行される時には、他の処理は完了している

補足1: ノンブロッキング

setTimeoutはノンブロッキング*1であるため、setTimeoutが実行された後、JavaScriptは結果を待たずに次の処理に取りかかることができます。

補足2: 極端に短い時間は指定できない

以下の引用にもあるとおり、あまりに短い時間(1msなど)で実行できてしまうと、クライアントPCに多大な負荷がかかってしまうため、ブラウザ側で最低ラインが設けられているようです。

マルチタスク・マルチウィンドウ・マルチタブ前提のパソコン用のブラウザーの場合、もしJavascriptを全力で実行してしまうと、このベンチマークのように最小の遅延でスクリプトを実行するページを開いたとたんにブラウザー自身のレスポンスが極端に悪くなってしまう。
 そのために、たとえプログラマーJavascript側でSetTimeout()の遅延パラメータとして1msを指定したとしても、最低でも10msとか15.6msの遅延後にしかタイマー関数を呼ばない、という設計になっているのが普通だ

Life is beautiful: Javascript雑学:SetTimeoutについて知っておくべき事

Javaの「可変長引数」

J2SE 5.0の新機能として追加された、「可変長引数」は普段あまり利用しませんが、共通メソッドに、任意の数のオプションを引数として渡せるようにしたい場合などに便利だと思います。


まず、「可変長引数」とは何かという話ですが、文字通り、あるメソッドの引数に任意の数の引数を渡すことができるというものです。可変長引数に対応したメソッドを呼び出す場合は、以下の例のように記述することができます。

method(); 
method("a"); 
method("a","b"); 
method("a","b","c");


可変長引数にするには、引数の型指定のあとに "..." と 3 つピリオドを続けて記述します。そして、受け側のメソッドは、結局、配列として扱うことになります。

void method(String... args) {
    // 受けたメソッドは、結局、配列として扱う
    for (String s : args){
        System.out.println(s);
    }
}


つまり、以前であれば、事前にパラメータ用の配列を生成していた処理を書かずに済むというメリットがあります。これは以下の引用にもある通り、開発者が書きやすくなるという程度ですので、利用するかは開発者の好みだと思います。

つまり、可変長引数を実現する機能は、配列によるエミュレーションの単なるシンタクス・シュガー(開発者が利用しやすいように書き換えた等価の構文)にすぎない。

itarchitect.jp


「可変長引数」を利用する場合の注意点としては、

  • メソッドが複数の引数をとる場合、最後の引数にしか利用できません。
  • 1つのメソッドで1つしか可変長引数を定義できません。
  • メソッドがオーバーロードされた場合、かつ、引数が固定のメソッドと可変個のメソッドがマッチした場合は、前者が優先されます。

Slony-Iを停止させずにテーブルを追加する

Slony-Iを用いた非同期レプリケーションを構築している環境で、テーブル追加をする場合、やや面倒な手順を踏まないといけません。このあたりはMySQLレプリケーションに劣るところですね。

手順の概要

Slony-I環境にテーブルを追加するには、以下の手順を実施する必要があります。

  1. 対象テーブル(のDDL)をレプリケーション対象の全ノードに追加する。
  2. 追加したテーブルをレプリケーションに参加させる。
    1. 新しいテーブル情報を設定ファイルに追記する。
    2. 追記した設定をSlony-Iに反映する。


以降で、手順の詳細は順番に書いていきます。

対象テーブル(のDDL)をレプリケーション対象の全ノードに追加

Slony-Iはトリガーベースのレプリケーションのため、DDLはマスター/スレーブ間で伝播しません。そのため、作業者自身の手で、全ノードにDDLを実行して回る必要があります。


オリジンノード、及び、サブスクライバノードにログインし、以下のようなDDLを実行します。ここでは、複数のノードで実行することを考慮し、DDLスクリプトファイルとして実行しています。

/usr/local/pgsql8/bin/psql -p5432 -U demo demodb -f create_table.sql

追加したテーブルをレプリケーションに参加させる

テーブルを全ノードで追加し終わったら、オリジン*1ノード にログインし、新しいテーブルをレプリケーションに参加させる設定を行います。


まずは、slon_tools.conf に新しいテーブル情報を追加します。追加する必要がある情報*2は下記の通りです。

    "set2" => { ・・・1
        "set_id"       => 2, ・・・2
        "table_id"     => 2, ・・・3
        "origin"     => 1,   ・・・4
        "sequence_id"  => 2, ・・・5
        "pkeyedtables" => ['schema.table1', 'schema.table2', ], ・・・6
    },
  1. テーブル情報は、この「セット(set)*3」という単位で追加します。
  2. セット単位に付与されるID。稼働中のset_idと被らないようにします。
  3. この変数は、テーブルを一意に特定するための番号付けがどこから開始されるのかということを意味します。
  4. オリジンのノード番号。
  5. この変数は、一意である必要がありますが、何の情報に紐づいているか、イマイチ分かりません。。
  6. 主キーを持ったテーブル名をカンマ区切りで記述します。


少し、長くなりましたが、追加情報は上記の通りです。で、実際に追記してみると以下のようになります。「table_id」と「sequence_id」はオリジンノードのPostgreSQLに格納されているSlony-Iの管理テーブルより、最新情報を取得の上、設定します。

[postgres]$
[postgres]$ vi /usr/local/slony1/etc/slon_tools.conf
〜〜〜〜〜〜〜〜〜〜
$SLONY_SETS = {
    # 既存の設定
    "set1" => {
        "set_id" => 1,
        "table_id"    => 1,
        "origin"     => 1,
        "sequence_id" => 1,
        "pkeyedtables" => ["demo.table1, demo.table2, demo.table3"],
    },

    # 今回追加した設定
    "set2" => {
        "set_id" => 2,
        "table_id"    => 4, # select max(tab_id) from _<<クラスタ名>>.sl_table;の結果 +1
        "origin"     => 1,
        "sequence_id" => 4, # select max(seq_id) from _<<クラスタ名>>.sl_sequence;の結果 +1
        "pkeyedtables" => ["demo.table4, demo.table5"],
    },
};
〜〜〜〜〜〜〜〜〜〜


設定ファイルへの追記が終わったら、Slony-Iへ設定を反映していきます。


Slony-Iの制御は、Slony-I独自の命令を、slonikというコマンドに入力することで行います。しかし、この独自の命令は小さなプログラミング言語のため、操作するのがかなり面倒です。そこで、独自の命令(プログラム)を生成するコマンドが用意されており、以下の形式で使用します。


命令生成コマンド 引数 | slonik


要するに、命令を自動生成し、パイプ(|)でslonikに入力するということです。ここでは、以下の命令生成コマンドを利用します。

  • slonik_create_set (追加したセット番号)
    • 追加したセットをオリジンノードに反映する命令を生成
  • slonik_subscribe_set (追加したセット番号) (スレーブnode番号)
    • 追加したセットをサブスクライバノードに反映する命令を生成
  • slonik_merge_sets (マスタnode番号) (マージ先セット番号) (追加したセット番号)


では、実際にSlony-Iへ設定を反映してみましょう。今回は、追加したセット番号=2、マスタnode番号=1、スレーブnode番号=2,3とすると、以下のようになります。

# オリジンノードに設定を反映
[postgres]$ slonik_create_set 2 | slonik

# サブスクライバノードに設定を反映
[postgres]$ slonik_subscribe_set 2 2 | slonik
[postgres]$ slonik_subscribe_set 2 3 | slonik

# 設定追加のために一時的に作成したセットを既存セットにマージ
[postgres]$ slonik_merge_sets 1 1 2 | slonik


Slony-I上では、set1(元のセット)と、set2(追加用の一時的なセット)をマージしましたので、設定ファイル(slon_tools.conf)も合わせておきます。

[postgres]$ vi /usr/local/slony1/etc/slon_tools.conf
〜〜〜〜〜〜〜〜〜〜
$SLONY_SETS = {
    # マージして一つのセットに
    "set1" => {
        "set_id" => 1,
        "table_id"    => 1,
        "origin"     => 1,
        "sequence_id" => 1,
        "pkeyedtables" => ["demo.table1, demo.table2, demo.table3,demo.table4, demo.table5"],
    },

    # set2の記述は削除

};
〜〜〜〜〜〜〜〜〜〜


作業は以上です。いや〜、結構大変ですね。

補足1

  • この手順実施後に、PostgreSQL、及び、Slony-Iの再起動は不要です。
  • 手順として、「set2など追記せず、いきなり既存設定(set1)にテーブル名を追記すればいいのでは?」と思われるかもしれませんが、いきなり既存設定を修正すると、レプリケーション設定がおかしくなってしまいます。
  • 原則的に、slon_tools.conf のメンテナンス作業は、オリジンノードのみでOKですが、フェイルオーバーなどを考慮すれば、全ノードに同じslon_tools.conf を配っておくのがよいとは思います。

補足2(slon_tools.confの解説)

slon_tools.conf は、Slony-Iの基本となる設定ファイルで、レプリケーション対象のノード情報や、対象テーブルの情報を設定します。ここで少し内容を確認しておきます。

  • $CLUSTER_NAME : クラスタ名を設定。ここでは"slony_demo"。
  • $MASTERNODE : マスターノードには、マスターサーバの番号を設定。ここでは"1"を設定。
  • add_node : ノード情報を記述。
  • $SLONY_SETS : レプリケーションセット情報を記述。
    • pkeyedtables : PKのあるテーブルはここに追記。
    • keyedtables : PKはないがNOT NULL制約とユニーク制約が定義された、事実上のPKを持つテーブルはここに追記。
    • serialtables : PKとなりうるカラムのないテーブルはここに追記。*4
[postgres]$ cat /usr/local/slony1/etc/slon_tools.conf

if ($ENV{"SLONYNODES"}) {
    require $ENV{"SLONYNODES"};
} else {
    $CLUSTER_NAME = 'slony_demo';
    $LOGDIR = '/var/log/slony1';
    $MASTERNODE = 1;

    add_node(node     => 1,
             host     => '192.168.0.1',
             dbname   => 'demodb',
             port     => 5432,
             user     => 'repl',
             password => 'repl');

    add_node(node     => 2,
             host     => '192.168.0.2',
             dbname   => 'demodb',
             port     => 5432,
             user     => 'repl',
             password => 'repl');
}

$SLONY_SETS = {
    "set1" => {
        "set_id" => 1,
        "table_id"    => 1,
        "origin"     => 1,
        "sequence_id" => 1,
        "pkeyedtables" => ["t_pkey_table"],
        "keyedtables" => {"t_ukey_table" => "t_ukey_table_key",},
        "serialtables" => ["t_nokey_table"],
        "sequences" => ["t_pkey_table_key"],
    },
};

if ($ENV{"SLONYSET"}) {
    require $ENV{"SLONYSET"};
}

1;

*1:いわゆるマスターサーバ、サブスクライバとはスレーブサーバのこと。

*2:ここではPKが存在するテーブルを追加するケースを前提にしています。PKが存在しないテーブルの場合には、もう少し別の項目も併せて追記する必要があります。

*3:セットとは、Slony-Iでのレプリケーションするテーブルの組のことです。例えばセットset1はテーブルtbl_1をノード1からノード2にレプリケート、 セット2はテーブルtbl_2をノード1からノード3にレプリケートするなど、細かな設定が可能です。

*4:Slony-Iレプリケーションを行う全てのテーブルにユニークなキーが必要なため、それらがない場合は、Slony-Iが自動的にシリアルナンバーを割り振る動作をします。