|            
作者:江德华    无论Outlook XP还是最新的Foxmail 4.0,都只能在HTML格式的电子邮件中嵌入JPG、GIF格式图片,而音色俱佳的Flash动画却只能作为电子邮件附件发送给好友,你可曾感到遗憾?
    经过一番实验探索,我使用Delphi终于实现了将Flash动画(.SWF格式)嵌入到电子邮件正文中发送的功能,Foxmail、Outlook均可以按照HTML格式的邮件查看,而且263、163等免费电子邮件还支持Web界面直接浏览。可惜的是,Sina、Sohu免费电子邮件的Web界面则不支持,只能将Flash动画作为附件提供。
    一、程序原理
    下面对本程序相关的重要理论知识做些解释,帮助大家理解本程序的原理。更多的内容,限于文章篇幅,请参见相关的参考书、帮助文件。
    1.TNMSMTP控件
    电子邮件发送过程都必须遵循SMTP协议。客户端和服务器通过该协议进行通信,由客户端发送出电子邮件,SMTP服务器通过网络上其他SMTP服务器,最终将邮件传递到收信人的邮件服务器。本程序作为电子邮件客户端程序,最基本的功能是要实现电子邮件发送,为了简化编程,使用了Delphi 5自带的TNMSMTP控件,该控件封装了对SMTP编程。由于该控件继承自TPowersock,因而必要时可以直接调用底层的Socket过程,又不失灵活性。
    例如,虽然TNMSMTP不直接支持“SMTP服务器认证功能”(请参考《电脑爱好者》2001年16期《制作支持“服务器认证”的VB电子邮件程序》,包括下文“BASE64编码”),但是,可以利用TNMSMTP的Connect事件和继承自TPowersock的Transaction方法,实现SMTP服务器认证。其原理在于,Connect事件发生在TNMSMTP与服务器连接成功之时,此时,使用Transaction方法发出SMTP服务器认证所规定的“AUTH LOGIN”命令,并输入经过BASE64编码后的用户名和密码,可以实现认证。因此,本程序实现了SMTP服务器认证。
    另外,TNMSMTP的SendStart事件在邮件发送开始时触发,在该事件中,可以通过修改TNMSMTP的FinalHeader属性值,实现修改电子邮件的邮件头信息。在本程序中需要修改邮件头的Content-Type属性,使接收嵌入Flash动画电子邮件的客户端软件(如Foxmail)能正确处理邮件内容。
    2.MIME协议
    目前,几乎所有的邮件服务器和客户端软件都支持“多用途Internet邮件扩展”协议Multipurpose Internet Mail Extensions (MIME)。该协议的最新版由RFC2045(Internet消息体格式)、RFC2046(媒体类型)、RFC2047(无ASCII文本消息头扩展)、RFC2048(注册过程)、RFC2049(一致性标准和例子)等共同规定。该协议遵循了RFC822中规定的电子邮件内容只能包括简单的ASCII文本字符(如字母、数字、标点符号等),定义了在电子邮件中包括经过编码的二进制文件的方法,编码后的内容将只包括文本字符。目前,实现对二进制文件编码的规范有很多,BASE64编码规范也是其中之一。本程序使用TMemoryStream对象,实现了通用的BASE64编码函数EncodeBASE64,能够对SWF文件进行编码。
    通过MIME协议,电子邮件中不但可以添加附件,还可以添加HTML格式内容,包括图片、声音、动画等。Outlook、Foxmail 4.0等软件都可以编辑HTML格式的邮件,但是并不支持直接嵌入Flash动画。事实上,正确利用MIME协议,可以在HTML格式的邮件中嵌入Flash动画,并在邮件正文中显示出来,其原理和在HTML格式邮件中显示图片、播放声音是一样的。在HTML格式邮件中嵌入Flash动画与向网页中添加动画所需要的代码是一样的,同样使用< OBJECT >、< EMBED >标记,感兴趣的朋友可以查看一个包含Flash动画的网页源代码,对比本程序。
    在本程序中,设置邮件MIME消息头的Content-Type属性为multipart/related; boundary="---SwfEmail by JDH",表示该邮件包括多个不同数据类型的部分,各个部分之间用"---SwfEmail by JDH"(不包括引号)区分。在邮件内容的这些部分中,根据每部分的内容不同,再为其添加相应的MIME消息头,具体可参见程序源代码。 
      二、编程实战
    启动Delphi 5,参考图1在Form1上创建各种控件。图中黑色方框内标明了控件名称,其中txt前缀表示TEdit控件,mem前缀表示TMemo控件,chk前缀表示TCheckbox控件,btn前缀表示TButton控件,NMSMTP1是TNMSMTP控件,OpenDialog1是TOpenDialog控件。图2列出了一部分控件的某些关键属性。
  现在添加代码如下:
  {******Unit1.pas源代码内容如下******}
  unit Unit1;
  interface
  uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Psock, NMsmtp;
  type
  TForm1 = class(TForm)
  Label1: TLabel;
  txtTo: TEdit;
  Label2: TLabel;
  txtFrom: TEdit;
  Label3: TLabel;
  txtSubject: TEdit;
  Label4: TLabel;
  memContents: TMemo;
  Label5: TLabel;
  txtUserName: TEdit;
  Label6: TLabel;
  txtPassword: TEdit;
  chkSmtpVerify: TCheckBox;
  btnSend: TButton;
  btnOpen: TButton;
  txtSwfFile: TEdit;
  Label7: TLabel;
  OpenDialog1: TOpenDialog;
  Label8: TLabel;
  txtSmtpServer: TEdit;
  NMSMTP1: TNMSMTP;
  Label9: TLabel;
  txtPort: TEdit;
  procedure btnOpenClick(Sender: TObject);
  procedure btnSendClick(Sender: TObject);
  procedure NMSMTP1SendStart(Sender: TObject);
  procedure NMSMTP1Connect(Sender: TObject);
  procedure chkSmtpVerifyClick(Sender: TObject);
  private
  { Private declarations }
  public
  { Public declarations }
  end;
  var
  Form1: TForm1;
  function EncodeString(Decoded:string):String;
  function EncodeBASE64(Encoded: TMemoryStream {TMailText}; Decoded: TMemoryStream):  Integer; //编码函数
  implementation
  {$R *.dfm}
  {对参数TMemoryStrema中的字节流进行Base64编码,编码后的结果
  保存在Encoded中,函数返回编码长度}
  function EncodeBASE64(Encoded: TMemoryStream ; Decoded: TMemoryStream): Integer;
  const
  _Code64: String[64] =
  ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/');
  var
  I: LongInt;
  B: array[0..2279] of Byte;
  J, K, L, M, Quads: Integer;
  Stream: string[76];
  EncLine: String;
  begin
  Encoded.Clear;
  Stream := '';
  Quads := 0;
  {为提高效率,每2280字节流为一组进行编码}
  J := Decoded.Size div 2280;
  Decoded.Position := 0;
  {对前J*2280个字节流进行编码}
  for I := 1 to J do
  begin
  Decoded.Read(B, 2280);
  for M := 0 to 39 do
  begin
  for K := 0 to 18 do
  begin
  L:= 57*M + 3*K;
  Stream[Quads+1] := _Code64[(B[L] div 4)+1];
  Stream[Quads+2] := _Code64[(B[L] mod 4)*16 + (B[L+1] div 16)+1];
  Stream[Quads+3] := _Code64[(B[L+1] mod 16)*4 + (B[L+2] div 64)+1];
  Stream[Quads+4] := _Code64[B[L+2] mod 64+1];
  Inc(Quads, 4);
  if Quads = 76 then
  begin
  Stream[0] := #76;
  EncLine := Stream+#13#10;
  Encoded.Write(EncLine[1], Length(EncLine));
  Quads := 0;
  end;
  end;
  end;
  end;
 
 
  {对以2280为模的余数字节流进行编码}
  J := (Decoded.Size mod 2280) div 3;
  for I := 1 to J do
  begin
  Decoded.Read(B, 3);
  Stream[Quads+1] := _Code64[(B[0] div 4)+1];
  Stream[Quads+2] := _Code64[(B[0] mod 4)*16 + (B[1] div 16)+1];
  Stream[Quads+3] := _Code64[(B[1] mod 16)*4 + (B[2] div 64)+1];
  Stream[Quads+4] := _Code64[B[2] mod 64+1];
  Inc(Quads, 4);
  {每行76个字符}
  if Quads = 76 then
  begin
  Stream[0] := #76;
  EncLine := Stream+#13#10;
  Encoded.Write(EncLine[1], Length(EncLine));
  Quads := 0;
  end;
  end;
  {“=”补位}
  if (Decoded.Size mod 3) = 2 then
  begin
  Decoded.Read(B, 2);
  Stream[Quads+1] := _Code64[(B[0] div 4)+1];
  Stream[Quads+2] := _Code64[(B[0] mod 4)*16 + (B[1] div 16)+1];
  Stream[Quads+3] := _Code64[(B[1] mod 16)*4 + 1];
  Stream[Quads+4] := '=';
  Inc(Quads, 4);
  end;
 
 
  if (Decoded.Size mod 3) = 1 then
  begin
  Decoded.Read(B, 1);
  Stream[Quads+1] := _Code64[(B[0] div 4)+1];
  Stream[Quads+2] := _Code64[(B[0] mod 4)*16 + 1];
  Stream[Quads+3] := '=';
  Stream[Quads+4] := '=';
  Inc(Quads, 4);
  end;
 
 
  Stream[0] := Chr(Quads);
  if Quads > 0 then
  begin
  EncLine := Stream+#13#10;
  Encoded.Write(EncLine[1], Length(EncLine));
  end;
 
 
  Result := Encoded.Size;
  end;
  {对参数Decoded字符串进行Base64编码,返回编码后的字符串}
  function EncodeString(Decoded:string):String;
  var
  mmTemp,mmDecoded:TMemoryStream;
  strTemp:TStrings;
  begin
  mmTemp := TMemoryStream.Create;
  mmDecoded:=TMemoryStream.Create;
  strTemp:=TStringList.Create;
  strTemp.Add(Decoded);
  strTemp.SaveToStream(mmTemp);
  mmTemp.Position := 0;
  {剔除mmTemp从strTemp中带来的字符#13#10}
  mmDecoded.CopyFrom(mmTemp,mmTemp.Size-2);
  {对mmDecoded进行Base64编码,由mmTemp返回编码后的结果}
  EncodeBASE64(mmTemp,mmDecoded);
  {获得Base64编码后的字符串}
  mmTemp.Position:=0;
  strTemp.LoadFromStream(mmTemp);
  {返回结果必须从strTemp[0]中获得,如果使用strTemp.Text会
  带来不必要的字符#13#10}
  Result:=strTemp[0];
  end;
 
 
  procedure TForm1.btnOpenClick(Sender: TObject);
  begin
  {打开对话框,选择SWF文件}
  if OpenDialog1.Execute then
  begin
 
 
  end;
  end; 
  procedure TForm1.btnSendClick(Sender: TObject);
  var
  mmSwfFile,mmEncoded:TMemoryStream;
  iResult:Integer;
  strsTemp:TStrings;
  strContents:TStringList;
  i:Integer;
  begin
  {验证用户输入信息}
  if txtTo.Text='' then
  begin
  ShowMessage('请输入收信人!');
  Exit;
  end;
  if txtFrom.Text='' then
  begin
  ShowMessage('请输入发信人!');
  Exit;
  end;
  if txtSmtpServer.Text='' then
  begin
  ShowMessage('请输入SMTP服务器!');
  Exit;
  end;
  if txtPort.Text='' then
  begin
  ShowMessage('请输入端口号!');
  Exit;
  end;
  if txtSwfFile.Text='' then
  begin
  ShowMessage('请选择SWF文件!');
  Exit;
  end;
 
 
  {检验服务器认证的用户名和密码}
  if chkSmtpVerify.Checked = True then
  if (txtUserName.Text='') or (txtPassword.Text='') then
  begin
  ShowMessage('您已选择SMTP服务器需要认证'+#13#10+'请输入用户名和密码!');
  Exit;
  end;
 
 
  {设置SMTP服务器地址、端口}
  NMSMTP1.Host:=txtSmtpServer.Text;
  NMSMTP1.Port:=StrToInt(txtPort.Text);
  {断开原来的连接,保证TForm1.NMSMTP1Connect中服务器认证的执行}
  if NMSMTP1.Connected then
  begin
  NMSMTP1.Disconnect;
  end;
  {连接服务器}
  NMSMTP1.Connect;
  {创建流}
  mmSwfFile:=TMemoryStream.Create;
  mmEncoded:=TMemoryStream.Create;
  {加载文件至流mmSwfFile}
  mmSwfFile.LoadFromFile(txtSwfFile.Text);
  {对mmSwfFile进行Base64编码,mmEncoded为编码后内容}
  iResult:=EncodeBASE64(mmEncoded,mmSwfFile);
  strsTemp:=TStringList.Create;
  mmEncoded.Position:=0;
  strsTemp.LoadFromStream(mmEncoded);
  {----生成邮件内容----}
  strContents:=TStringList.Create;
  strContents.Add('--------------SwfEmail by JDH');
  strContents.Add('Content-Type: text/html; charset=gb2312');
  strContents.Add('Content-Transfer-Encoding: 8bit');
  {注意:空行是邮件格式所必需的!}
  strContents.Add('');
  strContents.Add('< HTML>< HEAD>< TITLE>SWFEMAIL< /TITLE>< /HEAD>');
  strContents.Add('< BODY>');
  {添加邮件正文内容}
  for i:=0 to memContents.Lines.Count-1 do
  begin
  strContents.Add(memContents.Lines[i] + '< br>');
  end;
  {添加SWF文件相关内容}
  strContents.Add('< object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"  codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/ swflash.cab#version=5,0,0,0">');
  strContents.Add('< param name=movie value="cid:jdh_swfemail@001">');
  strContents.Add('< param name=quality value=high>');
  strContents.Add('< embed src="cid:jdh_swfemail@001" quality=high  pluginspage="http://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash"  type="application/x-shockwave-flash" >');
  strContents.Add('< /embed>< /object>< /BODY>< /HTML>');
  strContents.Add('');
  strContents.Add('--------------SwfEmail by JDH');
  strContents.Add('Content-Type: image/swf');
  strContents.Add('Content-ID: < jdh_swfemail@001>');
  strContents.Add('Content-Transfer-Encoding: base64');
  strContents.Add('Content-Disposition: inline; filename="'+ExtractFileName(txtSwfFile.Text)+'"'  );
  strContents.Add('');
  strContents.Add(strsTemp.Text);
  strContents.Add('');
  {----生成邮件内容结束----}
  {设置邮件发送信息}
  NMSMTP1.PostMessage.FromAddress := txtFrom.Text;
  NMSMTP1.PostMessage.FromName := txtFrom.Text;
  NMSMTP1.PostMessage.ToAddress.Text := txtTo.Text;
  NMSMTP1.PostMessage.Body.Text := strContents.Text;
  NMSMTP1.PostMessage.Subject := txtSubject.Text;
  {发送电子邮件}
  NMSMTP1.SendMail;
  ShowMessage('邮件发送成功!');
  end;
 
 
  procedure TForm1.NMSMTP1Connect(Sender: TObject);
  var
  strUserName,strPassword:string;
  begin
  {如果SMTP服务器需要认证,则进行认证}
  if chkSmtpVerify.Checked = True then
  begin
  {对用户名和密码进行Base64编码}
  strUserName:=EncodeString(txtUserName.Text);
  strPassword:=EncodeString(txtPassword.Text);
  {进行认证,输入编码后的用户名、密码}
  nmsmtp1.Transaction('auth login');
  nmsmtp1.Transaction(strUserName);
  nmsmtp1.Transaction(strPassword);
  end;
  end;
 
 
  procedure TForm1.NMSMTP1SendStart(Sender: TObject);
  begin
  {在邮件发送开始时修改邮件的消息头,标明邮件为多部分组成}
  NMSMTP1.FinalHeader.Values['Content-Type'] := 'multipart/related; boundary="------------SwfEmail  by JDH"';
  end;
 
 
  procedure TForm1.chkSmtpVerifyClick(Sender: TObject);
  begin
  {根据是否需要SMTP服务器认证,改变用户名、密码状态}
  if chkSmtpVerify.Checked = True then
  begin
  txtUserName.Enabled := True;
  txtUserName.Color:= clWindow;
  txtPassword.Enabled := True;
  txtPassword.Color:= clWindow; 
  end
  else
  begin
  txtUserName.Enabled := False;
  txtUserName.Color:= clSilver;
  txtPassword.Enabled := False;
  txtPassword.Color:= clSilver;
  end;
  end;
  end.
 
 
    三、程序运行
    首先在Delphi 5中编译运行,接着在对话框中输入收信人、发信人、主题、正文内容以及SMTP服务器,比如填写“smtp.263.net”,端口号使用默认的25,选中“SMTP服务器需要认证”,并根据发信人输入用户名、密码。单击“打开文件”按钮,选择要发送的Flash动画文件,确认无误后,单击“发送”按钮即可。发送成功后,会出现发信成功。  
 |