MS Exchangeメールデータベースのビューアの作成(パート1)


ある大きな、大きな国では、小さくてかわいい人が住んでいました。 そして、この大きな、またはかなり大きな国の真ん中に深く深い穴が現れるまで、すべてがうまくいきました。 さて、私は言わなければならない、彼女は一人ではなく、そしてもちろん、すぐには現れなかったが、誰もこれをもう覚えていないだろうし、これはもはや誰にとっても重要ではない。

主なものはピットであり、それは大きく、非常に大きく、むしろ無限であり、より正確には誰も知らなかった。 しかし、大きな国の政府は、ピットで何かをしなければならないことを知っていて、勉強して眠りにつくことに決めました。 この美しい状態にあった人たちの中で最も威圧的で器用で勇気があり、賢くて最も賢い人は何と呼ばれていましたか。

そして、最も予言的なセット。 彼らは勉強し、眠りに落ち、眠りに落ち、勉強し、勉強し、眠りに落ち、眠りに落ち、勉強しました。そして、これは何度も繰り返されませんでした。

私たちのほとんどのほとんどのヒーローが去ったか、むしろ彼らは去らなければならなかったので、ピットが残っていました。 しかし、新しいヒーローは、同じくらい小さく、同じで、まったく同じで、同じことを始めました。

一般に、これは1つが残っている限り続きました。 そして、ある晴れた日、ピット自体は姿を消し、まったく別の場所のどこかに現れました。そこで彼らは再び勉強し、埋め始めました...

すべての偶然はランダムであり、すべてが架空のものであり、そのようなものはかつてなかったため、現実にはありえませんでした! 真実、真実、まあ、またはほとんど真実。

そのようなピットの1つについては、後で説明します。 しかし、以来 プライバシーポリシーでは、その一部のみを対象とします。これには、 オープンソースと文書化された APIのみを使用してアクセスできます。

材料はほとんど排他的です 穀物のトピックに関する情報。

MS Exchange Serverがすべてのメールを保存する場所を考えたことがありますか、それとも最下位レベルでどのように動作しますか? これについて少し説明しますので、ここに書きます。

警告:このトピックに深く入ろうとしないでください。あなたの人生は十分ではありません。 警告した。

はじめに


MS Exchange Server(以下、単にExchange)は、Microsoft製品ラインの主要製品の1つです。 主な機能については、 wikiまたは公式Webサイトで読むことができます。 要するに、これはメール、カレンダー、その他のユーザーデータを操作するための一種の「ハーベスター」であり、さまざまなMS製品(SharePoint、TFSなど)と十分に統合されています。

ただし、この記事のフレームワークでは、エンドユーザーに提供するものではなく、このデータをどこから取得し、どのAPIを使用するかに関心があります。 Exchange 2010( メールボックスサーバー )のメールボックスの役割でメールボックスデータベースを個別に読み取ろうとします。

Exchangeには、ユーザーがデータにアクセスできるいくつかのエントリポイント( CASサーバー )と、これに使用できるいくつかのプロトコル(OWA、RPC(Outlook)、POP3 / IMAP4など)があります。

方法に関係なく、Exchangeへのアクセスを取得すると、すべての要求がメールボックスの役割 (Exchange 2007より前はこれが唯一の役割でした)に転送されます。 物理的に、これらのデータベースは* .edbファイル内のハードディスクにあります。 これらは、ExchangeがインストールされたディレクトリのMailbox \ <データベース名>フォルダーにあります。 また、データベースのライフサイクルに関連するトランザクションログやその他のファイルがそこに配置されますが、それらは必要ありません。最も基本的なものは* .edbです。

少し掘り下げると、ExchangeはExtensible Storage Engine(ESE)を使用してデータベースのコンテンツにアクセスしていることがわかります。 そして、掘り下げると、ESE関数の実装がライブラリese.dll(またはesent.dll)にあることが明らかになります。 これは、 すべての Exchange 操作の中核です 。 ESEは、データベースを操作するための広範なツールセットを提供します。 関数、定数、構造、および必要なすべての説明は、 ここにあります 。 残念ながら、このドキュメントは長い間更新されていないため、Exchange 2010に登場した機能は多くありませんが、このトピックでは必要ありません。 ese.dllは、Exchangeメインディレクトリ内のBinフォルダーにあります。

ESEの説明を読んだ後、データが形式で保存されていることが明らかになります。 列と行を持つテーブルのセット。 テーブルセルは、さまざまな種類のほか、ベースのすべての機能(インデックス、検索など)を使用できます。

合計で、Exchangeはデータベースを.edb拡張子を持つファイルの形式でメールボックスの役割に格納し、ESE(ese.dll)のおかげでそれらにアクセスすることを知っています。 これで十分であり、コーディングを開始できます。

データベース内のテーブル、およびすべての列と列をリストします。 もちろん、それらが何を意味するのか決してわかりません。 このことを知っているのはMSだけです。 これらの列には、ユーザーのフォルダー名から文字で終わるメールボックスに関連するほぼすべての情報が含まれているはずですが、リバースエンジニアリングのタスクが既にどこにあるかを理解することはここでは考慮されません。

プログラミング


準備する

最初に必要なもの:
Exchangeで、ベースを作成し、それを読み取ろうとします。 これを行うには、 Exchange管理コンソールEMC )を使用できます。 手順については説明しません。 このトピックに関する多くの情報がインターネット上にあります。 このデータベースに1人のユーザーを(同じEMCを介して)作成して、コンテンツが含まれ、このユーザーがこのメールボックスにログインして、たとえばOWAを介してすべてが正しく行われていることを確認します。 その後、メールボックスディレクトリに移動し、データベースの名前のフォルダーとその中のEDBファイルを探します。 ベースをコピーする前に、EMCを使用してアンマウントします。 すべて、実験の基礎があります。 たとえば、将来のプロジェクトのディレクトリにコピーします。

Binフォルダーからese.dllをコピーします。これにより、データベースを操作できます。

Visual Studioでは、コンソールC ++プロジェクトを作成します。 ここに重要なニュアンスがあります Exchange 2010(以前のすべてのバージョンとは異なり)は64ビットバージョンのみであるため、x64をサポートするプロジェクトを作成する必要があります。 そうしないと、単にアドレススペースにese.dllをロードできません。 したがって、アプリケーションをテストするには、64ビットバージョンのOSが必要であり、Exchange自体で確実にテストできますが、この目的のためにWindows 7でワークステーションを使用します。また、UnicodeバージョンのAPIを使用します。

したがって、新しく作成されたプロジェクトでは、x64およびUnicode(一般-Unicode文字セットを使用)がサポートされていることを確認します。 次に、ESEのメインヘッダーファイルを接続します。
#include <esent.h>
このファイルには、VS 2008以降のスタジオと共にSDKが付属しています。
stdafx.hで、JETバージョン(ESE)で2つの定義追加し、ユニコードバージョンのAPIを使用することを示します。
#define JET_UNICODE
#define JET_VERSION 0x0600

さて、データベースから何を取得したいかを決定する必要があります。 ESEは、テーブル、列、および行を持つデータベースであり、これから抽出しようとするものがまさにテーブル、列、および行です。 これを行うには、次の構造を準備します。
typedef struct tagDBColumnsInfo
{
std :: wstring sColumnName ;
std :: vector < std :: wstring > sColumnValues ;
} SDBColumnInfo ;

typedef struct tagDBTableInfo
{
std :: wstring sTableName ;
std :: vector < SDBColumnInfo > sColumnInfo ;
} SDBTableInfo ;

typedef struct tagDBTablesInfo
{
std :: wstring sDBName ;
std :: vector < SDBTableInfo > sTablesInfo ;
} SDBTablesInfo ;

最初に行うことは、DLL自体をロードすることです。これは、通常どおり:: LoadLibrary(...)を使用して行います。
ese.dllから関数を動的にロードし、次の関数が必要になります。

ベース開口部

必要な関数を正常にロードしたら、データベースの直接読み取りを開始します。 MSDNによると、 JET_paramDatabasePageSize (esent.h)パラメーターを設定して、 データベースのページサイズを指定する必要があります。 これが問題の出番です。 EDBファイルのみを持つこの値を見つけることはできませんが、データベースを開かない場合は正確に指定する必要があります。 これはeseutils(Exchangeに含まれています)を使用して実行できますが、少し異なるパスを使用して、この値は同じバージョンのExchangeで一定であり、常に4096の倍数であることがわかりました。Exchange2010では32768であることが実験的にわかりました

まず、ページサイズを設定します。
JET_ERR jRes = _JetSetSystemParameter NULLNULL 、JET_paramDatabasePageSize、 32768NULL ;


JET_ERRは、エラーコードを含む単なるlongです。 JetGetSystemParameter関数(ala :: FormatMessage(...))を使用して、このコードをテキスト記述に変換できます。
JetGetSystemParameter m_instance、m_sesid、JET_paramErrorToString、
reinterpret_cast < JET_API_PTR * > jeterror 、cBuff、MAX_BUFFER_SIZE ;

エラーコードの解析の便宜上、次のマクロを使用します(m_cLogは私の内部ログクラスです)。
#define WRITE_TO_LOG_AND_RETURN_IF_ERROR(jeterror)\
if(jeterror){\
char cBuff [MAX_BUFFER_SIZE] = {0}; \
if(m_instance)_JetGetSystemParameter(m_instance、m_sesid、\
JET_paramErrorToString、reinterpret_cast <JET_API_PTR *>(&jeterror)、cBuff、MAX_BUFFER_SIZE); \
m_cLog.write(m_sEDBPath、cBuff、jeterror、__ FILE __、__ LINE__); \
return jeterror; }


次に、Exchange固有のコールバックを無効にする必要があります。 それらについては何も知りません:
jRes = _JetSetSystemParameter NULLNULL 、JET_paramDisableCallbacks、 trueNULL ;


次に、データベースを操作するための新しいインスタンス (JET_INSTANCE m_instance)を作成します。
jRes = _JetCreateInstance m_instance、 NULL ;


作成されたインスタンスを初期化して、データベースの操作を開始します。
jRes = _JetInit m_instance ;


新しいセッションの開始(JET_SESID m_sesid):
jRes = _JetBeginSession m_instance、 m_sesid、 NULLNULL ;


EDBファイルを接続します。
jRes = _JetAttachDatabase m_sesid、L "demo.edb" 、JET_bitDbReadOnly ;


そしてそれを開きます:
jRes = _JetOpenDatabase m_sesid、L "demo.edb"NULL m_dbid、JET_bitDbReadOnly ;


合計。すべての関数がJET_errSuccessを返した場合、データベースは開いています。つまり、コンテンツの読み取りを開始できます。

次はコードです。 持ってくるから この件については、午後に火がついている彼を見つけることはできません。

リスト表

列挙するには、次の関数を作成します。
JET_ERR CJetDBReaderCore :: EnumRootTables SDBTablesInfo および sDBTablesInfo
{
sDBTablesInfo。 sDBName = m_sEDBPath ;
JET_ERR jRes = OpenTable ROOT_TABLE ;
if jRes == JET_errSuccess
{
JET_COLUMNBASE sNameInfo、
sTypeInfo ;
if ReadFromTable ROOT_TABLE、NAME_COLUMN、sNameInfo &&
ReadFromTable ROOT_TABLE、TYPE_COLUMN、sTypeInfo
{
JET_RETRIEVECOLUMN sJetRC [ 2 ] ;
sJetRC [ 0 ] columnid = sNameInfo。 columnid ;
sJetRC [ 0 ] cbData = sNameInfo。 cbMax ;
sJetRC [ 0 ] itagSequence = 1 ;
sJetRC [ 0 ] grbit = 0 ;
CHAR szName [ MAX_BUFFER_SIZE ] ;
sJetRC [ 0 ] pvData = szName ;

sJetRC [ 1 ]columnid = sTypeInfo。 columnid ;
sJetRC [ 1 ]cbData = sTypeInfo。 cbMax ;
sJetRC [ 1 ]itagSequence = 1 ;
sJetRC [ 1 ]grbit = 0 ;
WORD wType ;
sJetRC [ 1 ]pvData = wType ;

する
{
jRes = GetColumns ROOT_TABLE、sJetRC、 2 ;
if jRes = JET_errSuccess return jRes ;
if wType == 1
{
szName [ sJetRC [ 0 ]cbActual ] = 0 ;

SDBTableInfo sTableInfo ;
std :: string tmp szName ;
sTableInfo。 sTableName assign tmp。begin 、tmp.end ;

sDBTablesInfo。 sTablesInfopush_back sTableInfo ;
}

} while TableEnd ROOT_TABLE )) ;
}

jRes = CloseTable ROOT_TABLE ;
}

return jRes ;
}

どこで:

コードでわかるように、最初にルートテーブルを開きます。これは、 JetOpenTable関数を使用して行います。
JET_ERR CJetDBReaderCore :: OpenTable std :: wstring sTableName
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 sTableName ;
if iter == m_tables。end
{
JET_TABLEID tableid 0 ;
JET_ERR jRes = _JetOpenTable m_sesid、m_dbid、 sTableName。C_str NULL
0 、JET_bitTableReadOnly、 tableid ;
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 jRes

m_tables [ sTableName ] = tableid ;
}

return JET_errSuccess ;
}

次に、ReadFromTable内の列に関する情報を取得します。 コンテンツを取得するには彼女のIDが必要です。
JET_ERR CJetDBReaderCore :: ReadFromTable
std :: wstring sTableName、
std :: wstring sColumnName、
JET_COLUMNBASE sColumnBase
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 sTableName ;
if iter = m_tables。end
{
JET_ERR jRes = _JetGetColumnInfo m_sesid、m_dbid、 sTableName。C_str
sColumnName。 c_str sColumnBase、 sizeof JET_COLUMNBASE 、JET_ColInfoBase ;
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 jRes
}

return JET_errSuccess ;
}

IDをJET_RETRIEVECOLUMN構造体に入力し、GetColumns内でJetRetrieveColumnsを実行してテーブル名を取得します。
JET_ERR CJetDBReaderCore :: GetColumns
std :: wstring sTableName、
JET_RETRIEVECOLUMN * sJetRC、
INT nCount
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 sTableName ;
if iter = m_tables。end
{
JET_ERR jRes = _JetRetrieveColumns m_sesid、iter- > second、sJetRC、nCount ;
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 jRes
}

return JET_errSuccess ;
}

これでテーブルのリストが取得できました。次に列のコンテンツの取得に進みましょう。 各テーブルについて、その中の列のリストを受け取り、受け取ったときに、この情報を構造に保存します。

列をリストします

次の関数を作成します。
JET_ERR CJetDBReaderCore :: EnumColumns
SDBTableInfo sTableInfo、
std :: list < SColumnInfo > sColumnsInfo
{
if OpenTable sTableInfo。sTableName
{
JET_COLUMNLIST sColumnInfo ;
GetTableColumnInfo sTableInfo。STableName、 sColumnInfo ;
MoveToFirst sTableInfo。STableName ;

char szNameBuff [ MAX_BUFFER_SIZE ] ;
する
{
SColumnInfo ci ;
JET_RETRIEVECOLUMN sJetRC [ 4 ] ;

sJetRC [ 0 ] columnid = sColumnInfo。 columnidcolumnname ;
sJetRC [ 0 ] cbData = sizeof szNameBuff ;
sJetRC [ 0 ] itagSequence = 1 ;
sJetRC [ 0 ] grbit = 0 ;
sJetRC [ 0 ] pvData = szNameBuff ;

sJetRC [ 1 ]columnid = sColumnInfo。 columnidcolumnid ;
sJetRC [ 1 ]cbData = sizeof DWORD ;
sJetRC [ 1 ]itagSequence = 1 ;
sJetRC [ 1 ]grbit = 0 ;
sJetRC [ 1 ]pvData = ci。 dwId ;

sJetRC [ 2 ]columnid = sColumnInfo。 columnidcoltyp ;
sJetRC [ 2 ]cbData = sizeof DWORD ;
sJetRC [ 2 ]itagSequence = 1 ;
sJetRC [ 2 ]grbit = 0 ;
sJetRC [ 2 ]pvData = ci。 dwType ;

sJetRC [ 3 ]columnid = sColumnInfo。 columnidcbMax ;
sJetRC [ 3 ]cbData = sizeof DWORD ;
sJetRC [ 3 ]itagSequence = 1 ;
sJetRC [ 3 ]grbit = 0 ;
sJetRC [ 3 ]pvData = ci。 dwMaxSize ;

GetColumns sTableInfo。STableName、sJetRC、 4 ;

ci。 sNameassign reinterpret_cast < wchar_t * > sJetRC [ 0 ] .pvData )) 、sJetRC [ 0 ] .cbActual / 2 ;

SDBColumnInfo sDBColumnInfo ;
sDBColumnInfo。 sColumnName = ci。 sName ;

sColumnsInfo。 push_back ci ;
sTableInfo。 sColumnInfopush_back sDBColumnInfo ;
}
while TableEnd sTableInfo.sTableName )) ;

CloseTable sTableInfo。STableName ;
}

return JET_errSuccess ;
}

ここで、テーブルを再び開きますが、ルートではなく、前の手順で見つけたテーブルを開きます。

次に、すべての列に関する情報を取得する必要があります。このため、最初のポインターを取得し、最後の列に1つずつ移動します。
JET_ERR CJetDBReaderCore :: MoveToFirst std :: wstring sTableName
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 sTableName ;
if iter = m_tables。end //既に開いている場合
{
JET_ERR jRes = _JetMove m_sesid、iter- > second、JET_MoveFirst、 0 ;
BOOL bIsEmpty = jRes == JET_errNoCurrentRecord ;
if bIsEmpty jResを返します //空の場合は無視します
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 jRes ;
}

NO_ERRORを返します
}

JET_ERR CJetDBReaderCore :: GetTableColumnInfo
std :: wstring sTableName、
JET_COLUMNLIST * pCl、
BOOL bReplaceOld
{
JET_ERR jRes = JET_errSuccess ;
std :: map < std :: wstring 、JET_TABLEID > :: イテレータ iter = m_tables。 検索 sTableName ;
if iter = m_tables。end
{
jRes = _JetGetTableColumnInfo m_sesid、iter- > second、 NULL 、pCl、
sizeof JET_COLUMNLIST 、JET_ColInfoList ;
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 jRes

if bReplaceOld //最後にテーブルを開く必要がない場合
{
jRes = CloseTable sTableName ;
m_tables [ sTableName ] = pCl- > tableid ;
}
他に
{
jRes = _JetCloseTable m_sesid、pCl- > tableid ;
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 jRes
}
}

return jRes ;
}

申し訳ありませんが、投稿の最初のバージョンは切断され、私は時間内に応答することができませんでした。明らかに、記事のサイズには何らかの種類の制限があるため、2つの部分に分割されます。
継続するは...

次のパートでは、ベースを読んで結論を導き、ベースから「引き裂かれた」可能性のあるデータの例を見ていきます。

PSこのコードは、投稿用に変更されたバージョンです。 したがって、コードにはいくつかの欠陥があり、機能が切り詰められている場所にはギャグがあります。 それらに注意を払ってはいけません、これは生産ではありませんが、 実際の例を示したかったのです。 コードは完全に機能しており、インターネット上に配置できると同時に、ページ上のすべてのスペースを「食べない」ように記述されています。 ご理解いただきありがとうございます。

PPS私は、詳細のために、この情報が広範囲の人々にとって有用であるとは思わないが、それが1人でも役立つならば、私はうれしいです、そして、このポストに費やされた時間は報われるでしょう。

関連リンク
  1. MSDNのExtensible Storage Engine
  2. ロシア語の記事 (ESE APIの使用に関するRuNetの記事はほぼ唯一)
  3. ESE機能の

Source: https://habr.com/ru/post/J116308/


All Articles