工作中有个需求需要发送邮件,因为使用的delphi6,所以自然就选择了indy组件,想想这事挺简单的。实现的过程倒是简单,看着Indy的demo很快就完了,毕竟也不是很复杂的功能。

功能要求:

1、压缩日志文件并作为邮件的附件

2、邮件正文带上一些客户端信息

组件介绍

TIdSmtp:与服务器的连接及数据发送,基于smtp协议

TIdMessage:自然就是报文的信息了,包含收件人、发件人、主题、正文,以及附件。

代码展示:

  1. function TfrmMailSend.SendMail: Boolean;
  2. var
  3. objMailBody: TStrings;
  4. begin
  5. Result := False;
  6. IdSMTP1.Username := FMailSetting.Username;
  7. IdSMTP1.Password := FMailSetting.Password;
  8. IdSMTP1.Host := FMailSetting.Host;
  9. IdSMTP1.Port := FMailSetting.Port;
  10. IdSMTP1.AuthenticationType := atLogin;
  11.  
  12. IdMessage1.Priority := mpNormal;
  13. IdMessage1.From.Text := FMailSetting.FromAddress;
  14. IdMessage1.Recipients.EMailAddresses := edtToAddress.Text;
  15.  
  16. IdMessage1.Subject := '日志:'+FormatDateTime('YYYY-MM-DD', dtpLogDate.Date) + '['+hsGetOperatorNo+']';
  17. objMailBody := TStringList.Create;
  18. try
  19. objMailBody.Add('<table>');
  20. objMailBody.Add('<tr><td><b>ClientID:</b></td><td>'+ hsGetClientID + '</td></tr>');
  21. objMailBody.Add('<tr><td><b>CompanyName:</b></td><td>'+ AnsiToUtf8('中文革') + '</td></tr>');
  22. objMailBody.Add('<tr><td><b>Server DateTime:</b></td><td>'+ DateTimeToStr(GetServerDateTime) + '</td></tr>');
  23. objMailBody.Add('</table>');
  24.  
  25. //可能是Indy的bug,需要创建两次TIdText才能成功发送内容
  26. with TIdText.Create(IdMessage1.MessageParts, objMailBody) do
  27. begin
  28. ContentType:='text/html;charset=utf-8';
  29. ContentTransfer := 'quoted-printable'; //不能用base64,indy控件没实现
  30. end;
  31. with TIdText.Create(IdMessage1.MessageParts, objMailBody) do
  32. begin
  33. ContentType := 'text/html;charset=utf-8';
  34. ContentTransfer := 'quoted-printable'; //不能用base64,indy控件没实现
  35. end;
  36. finally
  37. FreeAndNil(objMailBody);
  38. end;
  39.  
  40. try
  41. IdSMTP1.Connect;
  42. try
  43. IdSMTP1.Send(IdMessage1);
  44. Result := True;
  45. finally
  46. if IdSMTP1.Connected then
  47. IdSMTP1.Disconnect;
  48. end;
  49. except
  50. on e: Exception do
  51. Console('[发送异常]'+e.Message);
  52. end;
  53. end;

代码流程主要是先准备好smtp主机信息,用户名与密码。然后组织好邮件内容,然后连接并发送。

关于附件

附件添加比较简单,Indy封装了一个专门的消息类TIdAttachment,只要将文件用TIdAttachment附加即可:

  1. TIdAttachment.Create(IdMessage1.MessageParts, AFileName);

这样就可以将附件添加到邮件人内容中了。

解决中文乱码问题

写这个小程序最头痛的就是中文乱码问题,由于对这个组件不熟悉,找了半天也没找到办法解决。因为delphi早期版本一直都是基于ansi字符集,所以对于中文需要支持时就得专门处理。对email的协议也不太熟悉,只知道是编码问题,但找了老半天也没找到相着的解决方法。设置了IdMessage的CharSet也没有效果。

于是没办法就只要查看foxmail,QQ邮箱之类的邮件原文来看看差别。发现主要是三个点:

Content-Type: text/html;
charset="GB2312"
Content-Transfer-Encoding: quoted-printable

对于前两个好理解,和html协议类似。但Content-Transfer-Encoding没怎么接触过。

Content-Transfer-Encoding主要值:

7bit:用于不编码的数据。数据为 7 位 US-ASCII 字符,总行长不超过 1000 个字符。

base64:不用解释了。这个通常用于字节流,比较附件就用这个格式。

quoted-printable:将由 US-ASCII 字符集中可打印的字符组成的数据编码。

之所以是中文乱码,原因是添加邮件正文时的字符集与接收邮件客户端的字符集对上。比如Delphi默认发送的时候文本是Ansi的,结果Foxmail却是不支持。只有GB2312、UTF-8之类的。查看邮件正文:

Content-Type: text/html;
charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

这样一来肯定就显示乱码了,因为发的时候他中文并不是UTF-8的格式。解决这个问题办法也简单,那就把字符串转正特定的编码再发吧。

还好delphi里有个函数直接就用:

  1. AnsiToUtf8('中文革')

这样发过去的内容中文就可以显示了。

发送Html

直接在TIdMessage的body内容发送其实是text/plain,这种明格式的话就不太容易做样式,不太好看。所以就要支持Html格式。看了看网上的资料,就是使用另一个Indy类可以实现TIdText。

  1. with TIdText.Create(IdMessage1.MessageParts, objMailBody) do
  2. begin
  3. ContentType:='text/html;charset=utf-8';
  4. ContentTransfer := 'quoted-printable'; //不能用base64,indy控件没实现
  5. end;

和附件的使用方法类似,只是要设定一下格式。只不过让人失望了,发过去没有效果啊。。没有效果啊。。接收到的邮件正文是空白的,查看原文:

  1. --=_NextPart_2rfkindysadvnqw3nerasdf
  2. Content-Type: text/plain
  3. Content-Transfer-Encoding: 7bit
  4.  
  5. --=_NextPart_2rfkindysadvnqw3nerasdf
  6. Content-Type: application/octet-stream; name="Logs_2016-01-10[60001].7z"

这是QQ邮箱中收到的正文,发现在附件与正文之间的内容是空白,没有收到啊。再看看Foxmail(微软exchage)

------=_002_NextPart335103774317_=----
Content-Type: text/html;
charset="GB2312"
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; charset=3Dgb2312">
<META NAME=3D"Generator" CONTENT=3D"MS Exchange Server version 14.01.0421.=
002">
<TITLE>=C8=D5=D6=BE=A3=BA2016-01-10[60001]</TITLE>
</HEAD>
<BODY>
<!-- Converted from text/plain format -->

</BODY>
</HTML>
------=_002_NextPart335103774317_=------

------=_001_NextPart645231448141_=----
Content-Type: application/octet-stream;
name="Logs_2016-01-10[60001].7z"

这倒是有一段html内容,只是body部分显示的是空白。。后来在网上看到一篇文章才发现是indy的一个bug。链接

TIdSMTP是最终发送邮件的类,发送的代码主要是在它父类TIdMessageClient中实现。SendBody方法,看看代码片段:

  1. if AMsg.MessageParts.TextPartCount > then
  2. begin
  3. WriteLn('Content-Type: multipart/alternative; '); {do not localize}
  4. WriteLn(' boundary="' + IndyMultiPartAlternativeBoundary + '"'); {do not localize}
  5. WriteLn('');
  6. for i := to AMsg.MessageParts.Count - do
  7. begin
  8. if AMsg.MessageParts.Items[i] is TIdText then
  9. begin
  10. WriteLn('--' + IndyMultiPartAlternativeBoundary);
  11. DoStatus(hsStatusText, [RSMsgClientEncodingText]);
  12. WriteTextPart(AMsg.MessageParts.Items[i] as TIdText);
  13. WriteLn('');
  14. end;
  15. end;
  16. WriteLn('--' + IndyMultiPartAlternativeBoundary + '--');
  17. end
  18. else begin

TextPartCount > 1才会去写入TIdText的内容,我的天,这玩意有点坑人啊。。于是我也学着别人的方法再添加一次,本文最初那段代码已经知道用法了:

  1. //可能是Indy的bug,需要创建两次TIdText才能成功发送内容
  2. with TIdText.Create(IdMessage1.MessageParts, objMailBody) do
  3. begin
  4. ContentType:='text/html;charset=utf-8';
  5. ContentTransfer := 'quoted-printable'; //不能用base64,indy控件没实现
  6. end;
  7. with TIdText.Create(IdMessage1.MessageParts, objMailBody) do
  8. begin
  9. ContentType := 'text/html;charset=utf-8';
  10. ContentTransfer := 'quoted-printable'; //不能用base64,indy控件没实现
  11. end;

再说乱码问题

前面在解决乱码问题时提到了Content-Transfer-Encoding,看别家邮件发送的内容可以是Base64,那么我想这应该是比较好的一种方法,于是就设置了一下,呵呵哒。跪了。然后只能继续查看组件的源代码,还是TIdMessageClient的SendBody方法,其中有个子函数:WriteTextPart。

  1. procedure WriteTextPart(ATextPart: TIdText);
  2. var
  3. Data: string;
  4. i: Integer;
  5. begin
  6. if Length(ATextPart.ContentType) = then
  7. ATextPart.ContentType := 'text/plain'; {do not localize}
  8. if Length(ATextPart.ContentTransfer) = then
  9. ATextPart.ContentTransfer := 'quoted-printable'; {do not localize}
  10. WriteLn('Content-Type: ' + ATextPart.ContentType); {do not localize}
  11. WriteLn('Content-Transfer-Encoding: ' + ATextPart.ContentTransfer); {do not localize}
  12. WriteStrings(ATextPart.ExtraHeaders);
  13. WriteLn('');
  14.  
  15. // TODO: Provide B64 encoding later
  16. // if AnsiSameText(ATextPart.ContentTransfer, 'base64') then begin
  17. // LEncoder := TIdEncoder3to4.Create(nil);
  18.  
  19. if AnsiSameText(ATextPart.ContentTransfer, 'quoted-printable') then
  20. begin
  21. for i := to ATextPart.Body.Count - do
  22. begin
  23. if Copy(ATextPart.Body[i], , ) = '.' then
  24. begin
  25. ATextPart.Body[i] := '.' + ATextPart.Body[i];
  26. end;
  27. Data := TIdEncoderQuotedPrintable.EncodeString(ATextPart.Body[i] + EOL);
  28. if TransferEncoding = iso2022jp then
  29. Write(Encode2022JP(Data))
  30. else
  31. Write(Data);
  32. end;
  33. end
  34.  
  35. else begin
  36. WriteStrings(ATextPart.Body);
  37. end;
  38. WriteLn('');
  39. end;

看到注释我已经跪了。。T_T,原来base64还是TODO的功能,不知道后续的Indy版本有没有实现。。

发送邮件进度

由于发送邮件包括了附件,内容比较大必须给用户显示个进度条。看着TIdSMTP有个OnWorkBegin和OnWork事件,而且OnWorkBegin有个AWorkCountMax参数,喜出望外,这样就知道发送的总大小了,弄个进度条这不是分分钟就OK了嘛。。结果一试发现然并卵。于是只能自己想办法了。

发现OnWork有AWorkCount参数,发现这个参数是有用的,它会在被调用时返回当前已经发送的大小。那么就想这个大小会是什么大小呢?

测试了发下发现和附件的总大小是一样的。这样就只要解决附件总大小就可以了,方法也简单,在添加附件的时候计算一下文件长度然后保存在一个变量中即可。在OnWorkBegin的时候设置为进度条最大值就好了。

  1. procedure TfrmMailSend.IdSMTP1Work(Sender: TObject; AWorkMode: TWorkMode;
  2. const AWorkCount: Integer);
  3. begin
  4. self.ProgressBar1.Position := AWorkCount;
  5. Console('正在发送日志......');
  6. Application.ProcessMessages;
  7. end;
  8.  
  9. procedure TfrmMailSend.IdSMTP1WorkBegin(Sender: TObject;
  10. AWorkMode: TWorkMode; const AWorkCountMax: Integer);
  11. begin
  12. ProgressBar1.Position := ;
  13. if FAttaSize >= then
  14. ProgressBar1.Max := FAttaSize
  15. else
  16. ProgressBar1.Max := ;
  17. Console('开始发送日志......');
  18. end;
  19.  
  20. procedure TfrmMailSend.IdSMTP1WorkEnd(Sender: TObject;
  21. AWorkMode: TWorkMode);
  22. begin
  23. Application.ProcessMessages;
  24. FAttaSize := ;
  25. Console('发送完成');
  26. end;

效果还是挺好。

技术笔记:Indy控件发送邮件的更多相关文章

  1. Delphi中,indy控件实现收发邮件的几点学习记录( 可以考虑加入多线程,用多个邮箱做一个邮箱群发器) 转

    关于用Delphi中的Indy控件实现收发邮件的几点学习记录             这几天心里颇不宁静,不是因为项目延期,而是因为自己几个月前做的邮件发送程序至今无任何进展,虽然一向谦虚的人在网上发 ...

  2. iOS学习笔记——基础控件(上)

    本篇简单罗列一下一些常用的UI控件以及它们特有的属性,事件等等.由于是笔记,相比起来不会太详细 UIView 所有UI控件都继承于这个UIView,它所拥有的属性必是所有控件都拥有,这些属性都是控件最 ...

  3. 分页技术之GridView控件

    GridView控件实现分页技术 第一步:设置GridView控件的属性,跟分页相关的属性设置如下: AllowPaging="true":允许分页, PageSize=" ...

  4. Windows窗体技术及基础控件

    创建winform程序 Visual studio是一套完整的开发工具集 RAD 工具(rapid application development) 创建用户界面时,把控件从工具箱拖放到窗体上,把它们 ...

  5. ios 学习笔记之控件属性

    1.文本框 设置密码属性:Secure Text Entry 勾选; 设置文本框带清除属性: Clear Button =Is always visible;  默认是不带清除属性:Never app ...

  6. jQuery学习笔记(控件位置定位、尺寸大小的获取等)

    想做一个幽灵按钮出来,效果大概如下图: 当点击按钮的时候,会有四根线条从四个方向飞入,经历从“无-有-无”的闪入过程. 那么我的设计想法是,先在HTML中定义一个按钮,然后在jQuery中设计按钮点击 ...

  7. [开发笔记]-DataGridView控件中自定义控件的使用

    最近工作之余在做一个百度歌曲搜索播放的小程序,需要显示歌曲列表的功能.在winform中采用DataGirdView来实现. 很久不写winform程序了,有些控件的用法也有些显得生疏了,特记录一下. ...

  8. 学习笔记-menusript控件中条目权限设置使用

    在做一个小程序的时候,偶然发现了使用menusript控件做权限设置的方法,仅此标记,以供参考. 首先创建一个实例:testuseright.sln, 在项目文件里创建两个窗体:Form1.cs和us ...

  9. IOS 学习笔记(7) 控件 分隔栏控件(UISegmentControl)的使用方法

    分隔栏控件的系统默认式样一共有3种,分别是“普通式样”,"边框式样","条状式样" 分隔栏控件中有一个momentary属性,默认时NO.当开发者配置成YES时 ...

随机推荐

  1. 踩石行动:ViewPager无限轮播的坑

    2016-6-19 前言 View轮播效果在app中很常见,一想到左右滑动的效果就很容易想到使用ViewPager来实现.对于像我们常说的banner这样的效果,具备无限滑动的功能是可以用ViewPa ...

  2. TODO:即将开发的第一个小程序

    TODO:即将开发的第一个小程序 微信小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验.个人理解小程序是寄宿在微信平台上的一个前端框架,具有跨平台功能, ...

  3. 23种设计模式--建造者模式-Builder Pattern

    一.建造模式的介绍       建造者模式就是将零件组装成一个整体,用官方一点的话来讲就是将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示.生活中比如说组装电脑,汽车等等这些都是建 ...

  4. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  5. 【原创分享·微信支付】C# MVC 微信支付之微信模板消息推送

    微信支付之微信模板消息推送                    今天我要跟大家分享的是“模板消息”的推送,这玩意呢,你说用途嘛,那还是真真的牛逼呐.原因在哪?就是因为它是依赖微信生存的呀,所以他能不 ...

  6. Linux主机上使用交叉编译移植u-boot到树莓派

    0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...

  7. Node.js入门(一)

    一.Node.js本质上是js的运行环境. 二.可以解析js代码(没有浏览器安全级的限制): 提供系统级的API:1.文件的读写 2.进程的管理 3.网络通信 三.可以关注的四个网站: 1.https ...

  8. Javascript学习笔记

    Javascript 2016年12月19日整理 JS基础 Chapter1 JS是一门运行在浏览器客户端的脚本编程语言,前台语言 组成部分 1. ECMAscript JS标准 2. DOM 通过J ...

  9. [OC] NSURLSession

    有的程序员老了,还没听过NSURLSession 有的程序员还嫩,没用过NSURLConnection 有的程序员很单纯,他只知道AFN. NSURLConnection在iOS9被宣布弃用,NSUR ...

  10. 【教程】SQLite数据库修复

    SQLite 大家都知道,就不多说了. 有时候数据量大了,或者存储过程中出现异常,数据库就可能会出问题. 这是以前公司产品出现过的问题,导致软件都打不开了,我花了不少时间才解决的,趁现在有空贡献出来. ...