| 
				 一、引言 
电子邮件如今已成为人们工作、生活中非常重要的通信工具,Outlook. Express作为微软公司在Windows操作系统中自带的电子邮件软件也因此得到广泛的使用。OE将全部邮件都存放在dbx文件中,这样做精巧简便,却不利于邮件的管理利用。例如用户有时需要将电子邮件存入数据库归档;有时需要根据时间、地址、内容对邮件进行分类检索,在OE中要实现这些操作就很繁琐。下面我们一起来研究一下dbx文件中邮件的存储结构,从而实现将邮件从中提取出来,以单个的.eml文件形式存放,为邮件数据的应用增加一种思路。 
  
二、设计编程 
1.读取dbx文件头 
表1 dbx文件头存储结构 
| 
 文件位置  | 
 数据类型  | 
 描述  |  
| 
 0x0000  | 
    | 
 dbx文件标识  |  
| 
 0x0004  | 
    | 
 dbx文件类型标识  |  
| 
 ......  | 
    | 
    |  
| 
 0x00c4  | 
 Int4  | 
 邮件数量  |  
| 
 0x00e4  | 
 Int4  | 
 邮件信息索引起始位置  |    
  
(1)       如表1所示,文件的起始0x0000位置记录了dbx文件的标识,必须是cf ad 12 fe。 
(2)       在文件的0x0004位置是dbx文件的格式标识。如果是c5 fd 74 6f,则表示该文件是邮件存储文件;如果是c6 fd 74 6f,则表示是OE的文件夹结构文件。本文只讨论邮件存储文件的格式。 
(3)       在文件的0x00c4位置记录了本文件中存储的邮件数量。 
(4)       在文件的0x00e4位置记录了邮件信息索引的起始位置。从该位置处可以读取全部邮件的信息索引。 
读取dbx文件头的代码如下: 
  AssignFile(fr,Edit1.text); ReSet(fr,1); 
  //查看.dbx文件标志 
  BlockRead(fr,l,4); 
  if l<>$fe12adcf then begin 
    ShowMessage('非OE的邮件文件!'); 
    exit; 
  end; 
  //查看.dbx文件中的邮件文件,不包括文件夹设置文件 
  BlockRead(fr,l,4); 
  if l<>$6f74fdc5 then begin 
    ShowMessage('可能是文件夹文件,不是邮件存放文件!'); 
    exit; 
  end; 
  //邮件数 
  Seek(fr,$c4); BlockRead(fr,MsgCount,4); 
  if MsgCount=0 then begin 
    ShowMessage('没有邮件!'); 
    exit; 
  end; 
  //邮件信息起始存放位置 
  Seek(fr,$e4); BlockRead(fr,MsgInfoPtr,4); 
  if MsgInfoPtr=0 then begin 
    ShowMessage('没有可提取的邮件!'); 
    exit; 
  end; 
2.读取邮件信息索引 
根据文件头0x00e4处所记录的地址找到邮件信息索引,这些索引的存储结构如表2所示。 
表2 邮件信息索引的存储结构 
| 
 序号  | 
 数据类型  | 
 描述  |  
| 
 1  | 
 Int4  | 
    |  
| 
 2  | 
 Int4  | 
    |  
| 
 3  | 
 Int4  | 
 下一段信息索引区的位置  |  
| 
 4  | 
 Int4  | 
 上一段信息索引区的位置  |  
| 
 5  | 
 Int1  | 
    |  
| 
 6  | 
 Int1  | 
 本区域所含邮件信息索引数量  |  
| 
 7  | 
 Int2  | 
    |  
| 
 8  | 
 Int4  | 
    |  
| 
 9  | 
    | 
 记录每封邮件信息的位置  |  
| 
    | 
 ......  | 
    |    
  
信息索引可以分为若干段,字段3、字段4分别记录了下一段、上一段的位置,形成数据链。字段6记录了本段所包含的邮件信息索引数量,字段9处则根据该数量,以3个Int4为单位记录每封邮件信息所在的位置。这3个Int4包含表3所示的信息: 
表3 邮件位置存储结构 
| 
 序号  | 
 数据类型  | 
 描述  |  
| 
 1  | 
 Int4  | 
 记录邮件信息存储的位置  |  
| 
 2  | 
 Int4  | 
 下一段信息索引区的位置  |  
| 
 3  | 
 Int4  | 
 下一段索引区所含邮件信息索引数量  |    
  
如果字段2的值不为0,表示继续指向下一段邮件信息索引区,此时应该采用递归的方法继续读取;字段2的值为0,表示结束。 
读取邮件信息索引的代码如下: 
procedure NodeTree(FirstPtr:Longword ); 
var 
  NodeCount : Byte; 
  i : Longword; 
  t : NodeBody; 
  Ptr : Longword; 
begin 
  Ptr:=FirstPtr; 
  while Ptr<>0 do begin 
    Seek(fr,Ptr+17); BlockRead(fr,NodeCount,1); 
    //将每封邮件的信息存放位置记入Msgs数组的InfoPtr 
    for i:=1 to NodeCount do begin 
      Seek(fr,Ptr+24+(i-1)*12); 
      BlockRead(fr,t,12); 
      //变长数组长度+1 
      SetLength(Msgs,Length(Msgs)+1); 
      Msgs[Length(Msgs)-1].InfoPtr:=t.Ptr; 
      //查看是否有子树,如果有,则进入递归读取 
      if t.NextPtr<>0 then NodeTree(t.NextPtr); 
    end; 
    //读取下一个Node,0表示结束 
    Seek(fr,Ptr+8); BlockRead(fr,Ptr,4); 
  end; 
end; 
3.读取邮件信息 
根据邮件信息索引中字段9所记录的位置可以找到每封邮件的信息,这些信息存储的结构如表4所示。 
表4  邮件信息的存储结构 
| 
 序号  | 
 数据类型  | 
 描述  |  
| 
 1  | 
 Int4  | 
    |  
| 
 2  | 
 Int4  | 
    |  
| 
 3  | 
 Int2  | 
    |  
| 
 4  | 
 Int1  | 
 所包含的索引项数量  |  
| 
 5  | 
 Int1  | 
    |  
| 
    | 
    | 
 索引项数量*4  |  
| 
 6  | 
 Int1 Bit 8  | 
 高位标志  |  
| 
 7  | 
    Bit 1-7  | 
 索引号  |  
| 
 8  | 
 Int3  | 
 数值  |  
| 
 ......  | 
    | 
    |  
| 
 9  | 
    | 
 数据区  |    
  
如果字段6的Bit 8位为0,表示该索引项的内容需要从下面的字段9数据区取得,起始位置就是Bit 1-7所记录的数据;如果字段6的Bit 8位为1,表示该索引项的数据就是Bit 1-7所记录的数据。 
根据索引数据就可以列举出全部邮件的信息,如表5所示。 
表5  全部邮件信息 
| 
 索引号  | 
 数据类型  | 
 描述  |  
| 
 0x00  | 
 Int4  | 
    |  
| 
 0x01  | 
 Int4  | 
    |  
| 
 0x02  | 
    | 
    |  
| 
 0x03  | 
 Int4  | 
    |  
| 
 0x04  | 
 Int4  | 
 存放邮件内容的起始块  |  
| 
 ......  | 
    | 
    |  
| 
 0x08  | 
 String  | 
 邮件标题  |  
| 
 ......  | 
    | 
    |    
  
读取邮件信息的代码如下: 
  //读取每封邮件的标题和内容的存放位置 
  for i:=0 to Length(Msgs)-1 do begin 
    ProgressBar1.Position:=i*100 div Length(Msgs); Application.ProcessMessages; 
    Seek(fr,Msgs[i].InfoPtr+10); BlockRead(fr,NodeCount,1); 
    for j:=1 to NodeCount do begin 
      Seek(fr,Msgs[i].InfoPtr+12+(j-1)*4); 
      BlockRead(fr,b1,1); BlockRead(fr,b2,1); BlockRead(fr,b3,1); BlockRead(fr,b4,1); 
      m.Hi:=b1 shr 7; 
      m.Pos:=b1 and $7F; 
      m.Value:=(b4 shl 16)+(b3 shl 8)+b2; 
      if m.Hi=0 then m.Value:=Msgs[i].InfoPtr+12+NodeCount*4+m.Value; 
      case m.Pos of 
        4 : begin 
          //邮件内容存放的起始位置 
          if m.Hi=1 then Msgs[i].EMLPtr:=m.Value else begin 
            Seek(fr,m.Value); BlockRead(fr,l,4); 
            Msgs[i].EMLPtr:=l; 
          end; 
        end; 
        8 : begin 
          //邮件标题 
          s:=''; 
          Seek(fr,m.Value); 
          repeat 
            BlockRead(fr,c,1); 
            if c in BadFilenameChar then c:='_'; 
            if c<>#0 then s:=s+c; 
          until c=#0; 
          if s='' then s:='[无标题]'; 
          if Length(s)>200 then s:=Copy(s,1,200); 
          Msgs[i].Subject:=s; 
        end; 
      end; 
    end; 
  end; 
在dbx文件中邮件的内容数据是分块存储的,起始块的位置就记录在索引号为0x04的数据中。邮件内容数据块的数据结构如表6所示: 
表6 邮件内容数据块的数据结构 
| 
 序号  | 
 数据类型  | 
 描述  |  
| 
 1  | 
 Int4  | 
    |  
| 
 2  | 
 Int4  | 
 本块数据区长度  |  
| 
 3  | 
 Int4  | 
 数据区内实际数据长度  |  
| 
 4  | 
 Int4  | 
 下一个数据块的位置  |  
| 
 5  | 
    | 
 数据区  |    
  
如果字段3的数值小于字段2,即数据区内实际的数据长度小于本块数据区长度,则后面的是无用数据。字段4记录了下一块内容数据的位置,0x0000表示结束。 
读取邮件内容,并以邮件标题为文件名生成eml文件的代码如下: 
  //读取每封邮件的内容并存盘 
  for i:=0 to Length(Msgs)-1 do begin 
    if Msgs[i].EMLPtr=0 then continue; 
    //先检查是否有同名文件,有的话在文件名后面添加计数 
    s1:=Edit2.Text+Msgs[i].Subject; s:=s1; 
    j:=1; 
    while FileExists(s+'.eml') do begin 
      s:=s1+' ['+InttoStr(j)+']'; 
      Inc(j); 
    end; 
    s:=s+'.eml'; 
    AssignFile(fw,s); ReWrite(fw,1); 
    Seek(fr,Msgs[i].EMLPtr); 
    repeat 
      BlockRead(fr,mh,16); 
      SetLength(Buf,mh.BlockLen); 
      //按照BlockLen读取 
      BlockRead(fr,Buf[0],mh.BlockLen); 
      //按照TextLen实际内容长度存盘 
      BlockWrite(fw,Buf[0],mh.TextLen); 
      //读取下一段所在位置 
      Seek(fr,mh.NextPtr); 
    until mh.NextPtr=0; 
    CloseFile(fw); 
  end; 
  
三、程序运行界面 
提取dbx文件中邮件程序的运行界面如图1所示。 
 
  
 
图1 程序运行界面 
  
四、结语 
以上简单介绍了dbx文件的存储结构,虽然OE本身也提供了通过拖拽方式生成.eml文件的功能,但是在我们了解dbx文件的存储结构后,就可以实现邮件数据在不同操作系统、不同应用软件之间交换,从而方便用户灵活使用。 
  			
				 |