概要

メールの受信用サーバーであるPOPサーバーにアクセスしてメールの一覧を取得するプログラムである。
認証にはUSER/PASS認証と違い、パスワード名を直接サーバーへ送信せず、サーバーから送信されるチャレンジ文字とパスワードからMD5ハッシュしして送信するのでパスワードの漏洩の可能性は減る。
しかし、MD5ハッシュは特定の字列に対して1つだけ値ではなく、他の文字列でも同じMD5ハッシュ値を作成できる。また、POPサーバーのふりをして、チャレンジ文字を送信して、クライアントからの送信を収集すれば、乗っ取りも可能となる。
メールの一覧は、mail_list.txtファイルに保存される。
ファイルの中身はTAB区切りであり、1件が1行で記述され1行は以下の順序で構成されている。
メール番号 件名 送信者アドレス 送信日時

テスト環境

コンパイラ

Visual C++ 2013 Express 32bit/64bit

実行環境

Windows 7 Enterprise Service Pack 1 64bit(Sandy Bridge-E)

認証でメールの一覧を取得する方法

サーバー名等の定義

説明に当たり、以下のような架空の名称を定義する。

POPサーバー名 testserver.com
POPサーバーログインユーザー名 user
POPサーバーパスワード名 pass
上記と対応するWindows Liveメールの設定例は以下のとおりである。


以下の文章で赤字がサーバーへの送信文字列青字がサーバーからの受信文字列である。
\0はバイト値が0を示し、\r\nはバイト値が0x0d 0x0aで改行を示す。

winsockへの接続

SMTPサーバーとの接続および送受信は、winsockを用いている。
winsockでPOPサーバーへ接続する。接続されると例えば以下のような文字列がサーバーから送信されるのでクライアントで受信する。
+OK Hello there. <12345.678910@testserver.com>\r\n
+OKが返されたので正常に接続されたと判断できる。

APOPコマンドの送信

サーバーへの接続時にサーバーから受信する文字列のうち<>で囲まれた場分がチャレンジ文字列である。
上記の場合、12345.678910@testserver.com である。
これにユーザー名を結合して、MD5ハッシュ値を求める。
チャレンジ文字列とユーザー名の結合
12345.678910@testserver.compass
上記のMD5ハッシュ値を求めると
ac2325546c56719a466686ef1ea0cb64 となる。
ハッシュ値の算定方法はMD5ハッシュを計算(Cryptography API)を参照されたい。
APOP ユーザー名 ハッシュ値を送信する。
APOP user ac2325546c56719a466686ef1ea0cb64\r\n
サーバーからの応答を受信する。
+OK logged in.\r\n

メール数の取得

STATコマンドによりメール数を取得する

STAT\r\n
+OK 50 388837\r\n
上記のサーバーからの応答例のうち50がメール数、388837がメールの全容量を示している。

ヘッダーのみ取得

TOPコマンドにより指定したメール番号(本例では29番)のヘッダーのみ取得する
TOP 29 0\r\n
サーバーからの応答をクライアントで受信する。
以下が、ヘッダーの主要な部分を抽出したものである。
以下の紫色の文字は説明のために付記したものである。

+OK headers follow.\r\n
From: =?utf-8?Q?6YCB5L+h6ICF?=\r\n    送信者 送信者という文字列ををBase64でエンコードすると6YCB5L+h6ICFになる
To: user\r\n    受信者のメールアドレス
Date: 2 Mar 2015 13:16:42 +0900\r\n    メール送信日時 2015年3月2日 13:16:42 時差9時間0分
Subject: =?utf-8?44Oh44O844Or44OG44K544OI?=\r\n    メールテスト
 =?utf-8?B?5YmN55Wl?=\r\n    前略
 =?utf-8?B?5Lit55Wl?=\r\n    中略
 =?utf-8?B?5b6M55Wl?=\r\n    後略
ヘッダのうち代表的なフィールドを以下に記す。
フィールド名 内容 上記ヘッダが示す意味
Form フィールドは送信者を示す 送信者
To フィールドは受信者を示す user
Date メールの送信日時を示す 2015年3月2日 13:16:42 時差9時間0分
Subject 件名を表す。複数行にまたがる場合もある メールテスト前略中略後略

プログラムソースの概要

main

グローバル変数にサーバー名等を設定します。
mail_list関数でメールの一覧を取得保存します。

mail_list

WSAStartup関数でwinsockを初期化します。
gethostbyname関数でホスト情報を取得します。
socket関数でwinscokへ接続します。 connect関数でサーバーソケットへ接続します。
recv関数でサーバーからの応答を受信します。
strchr関数を使用しサーバーから受信したデータから<>で囲まれたチャレンジ文字列を抽出します。
strcpy_sとstrcat_s関数を用いチャレンジ文字列とパスワードを結合します。
md5Hashed関数を用いて文字列からMD5ハッシュである128bit値を作成します。
HASH_UINT128クラスのメンバ関数hexを用い、128bit値を16進数文字列に変換します。
APOP ユーザー名 ハッシュ値をsend関数を用いてサーバーへ送信します。
recv関数でサーバーからの応答を受信します。
文字列STATをsend関数を用いてサーバーへ送信します。
recv関数でサーバーからの応答を受信します。
strtok_sを2回使用して2個目のトークンを取り出しこれをatoi関数で数値に変換しメール数を取得します。
printf関数で標準出力にメール数を出力します。
fopen_s関数でメールの一覧を保存するためのmail_list.txtファイルを新規書き込み用にオープンします。
ファイルのオープンに失敗した場合は、ソケットを閉じて本関数を終了します。
取得されたメール数全部に対して以下の処理を行います。
文字列TOPに取得したいメールの番号及び0を結合してsend関数を用いてサーバーへ送信します。
recv関数でサーバーからの応答を受信します。
受信された内容はヘッダー全部です。
parse_header関数によりヘッダーを解析します。
MIMEstrDecord関数により件名と送信者に含まれるMIMEエンコードをSJISまたはUNICODEに変換します。
mimeGetData関数により送信日時を日付シリアル値を示すtime_t型に変換します。
_tctime_s関数によりtime_t型を日付を表す文字列に変換します。
_ftprintf関数によりファイルへ件名・送信者・送信日時をTAB区切りで出力します。
メール全部に以上の処理を実行したらfclose関数によりファイルを閉じます。
ソケットを閉じて関数を終了します。

mimeGetDate

prase_header関数で抽出された日時を示す文字列をtime_t型に変換します。
文字列は以下の2つの形式に対応しています。

3 Mar 2015 12:48:50 +0900
Thu, 02 Aug 2001 10:45:23 +0900
strtoke_s関数により文字列を区切りatoi関数で数値に変換します。
曜日や月については3文字の略した英語で表記されているので、getWeek関数、getMonth関数で文字列を数値に変換します。
得られた数値は、tm型の構造体に代入しmktime関数でtime_t型に変換します。

parse_header

ヘッダーから件名・送信者・送信日時を抽出して各グローバル変数に保存します。
保存結果はMIMEデコードされていません。
引数で示されるヘッダーを1文字ずつ解析し、改行が検出されたら1行の終わりとみなします。top変数に次の行の先頭位置を保存します。
行の先頭の文字列を比較し、From: Subject: Data:に該当するか確認します。

From:

From:に該当する場合、送信者ですので、そのあとに続く文字列をグローバル変数fromに保存します。 type変数に解析中のフィールドタイプを示すFROMを代入します。

Subject:

Subject:に該当する場合、件名ですので、そのあとに続く文字列をグローバル変数subjectに保存します。
type変数に解析中のフィールドタイプを示すSUBJECTを代入します。
行の最初が空白でない場合は、前のフィールドの続きです。
解析中のフィールドがSUBJECTの場合、件名の続きですので、そのあとに続く文字列をグローバル変数subjectに結合します。

MIMEstrDecord

MIMEでエンコードされた文字列をSJISまたはUNICODEに変換します。
対応するフォーマットは以下の4つです。

=?utf-8?Q?変換対象文字列?=
=?utf-8?B?変換対象文字列?=
=?iso-2022-jp?Q?変換対象文字列?=
=?iso-2022-jp?B?変換対象文字列?=
上記の赤文字の部分が文字コードの種類を示し青文字の部分がエンコード形式を示します。
utf-8は文字コードがUTF-8であること、iso-2022-jpは文字コードがISO-2022であること。 Qは文字コードのエンコードがQuoted-Printable、Bの場合Base64を示します。
件名の場合、複数行にまたがることがあります。この場合parse_header関数は例えば以下のように変換します。

=?utf-8?B?変換対象文字列?==?utf-8?B?変換対象文字列?=
本関数は上記の場合にも対応しています。
注意しなければならないのは、複数行に分割される場合、例えば2バイトで表現される文字がバイトの途中で分断される場合があることです。これを単純に処理すると文字化けの原因となります。
分断に対応するために、1パス目ではMIMEのエンコードされている文字の位置をマークしておき、2パス目で変換しています。
具体的には2パス目で行が分割されていてもエンコード、文字コードの種類が同じ場合は、MIMEデコード結果を結合してから文字コードの変換をするようにしています。

 1パス目

文字列=?
=?を検出するとMIMEエンコードの始まりと解釈します。
=?の次の文字から次の?までをcharset変数に格納します。
さらに次の?までを1文字をmethod変数に格納します。
これらをm配列にマークします。
文字列?=
?=を検出するとMIMEの終了と解釈します。
文字列の最後に0を代入します。
m配列に1個進めます。

 2パス目

1文字ずつ処理を行い、m配列にマークされた文字位置の場合、前回と同じ文字コードの種類でエンコードが同一の場合、配列mime_binにデコード対象の文字列をデコードしその結果を結合します。
前回と異なる場合は、デコード・文字コードの変換を行い、配列dtcに結合します。
デコード後m配列を1個進めます。 MIMEのデコードはBase64がbase64Decode関数、Quoted-PrintableがqpDecode関数で文字列をバイナリ値に変換します。
文字コードの変換は、文字種類を示す文字列からgetCodePage関数により文字コードの種類を示す数値に変換し、MultiByteToWideChar関数によりUNICODE(UTF16)文字列に変換します。
SJISが必要な場合は、更にWideCharToMultiByte関数によりUNICODEからSJISに変換します。

getCodePage

引数で与えられる文字列、utf-8またはiso-2022-jpをMultiByteToWideChar関数の第1引数で必要な値に変換します。

qpDecode

Quoted-Printableでエンコードされた結果である文字列をデコードします。
文字が=で始まる場合は、次の2つの文字を1byteを表す16進数とみなし、hexc2deg関数を用い1文字ずつバイナリ値に変換します。
=で始まらない場合は、エンコードされていない文字列とみなします。

base64Decode

Base64を16進数で表した文字列をデコードします。
詳しくはBase64エンコード・デコードを参照してください。

base64_to_6bit

base64の1文字を6bitの値に変換します。
詳しくはBase64エンコード・デコードを参照してください。

HASH_UINT128共用体

ハッシュ128bit値に対して各型式の配列でアクセスできるようにしています。
メンバ関数に128bit値を16進数文字列に変換するためのhex関数があります。

md5Hashed

文字列からハッシュ値を計算する関数です。 詳細はMD5ハッシュを計算(Cryptography API)を参照されたい。

ソースコード


ソースファイルのダウンロード(送信先等をマクロで定義しなければ使用できません)