【更新日志】
2019.11.18
- 注意使用 QQ 邮箱发送邮件时,要传它的授权码。具体参考官方说明
2016.2.25
- 已知 Indy 在 android 6.0 上无法正确的初始化OpenSSL库,造成无法使用 SendBySSL 函数,通过加密通道发送邮件,但 Send 是OK的。解决办法:
(1)、用 Send 发送邮件,不使用SSL;
(2)、用 Android 5 的 libssl.so 放到程序的启动目录下;
(3)、等待 Idera 的官方更新,参考官方论坛。- 修正了在 Android 发送邮件时,中文标题出现乱码的问题
【正文】
这个是自己写的 SMTP 发送邮件的代码,渣浪昨天不行是渣浪的小霸王服务器出问题了,今天好了。现在渣浪和QQ 邮箱测试通过。我自己只在 Delphi 10 上测试通过,其它的环境就请大家自己测试了。本人不承诺程序没有 Bug,有问题,请及时告诉我,大家一起改进。
unit sendmail; interface {$I 'qdac.inc'} { 发送邮件实现单元,方便发送邮件,您可以免费使用本单元,但请保留版权信息 (C)2016,swish,chinawsb@sina.com 本单元实现参考了 BCCSafe 的实现,详情参考: http://www.bccsafe.com/delphi%E7%AC%94%E8%AE%B0/2015/05/12/IndySendMail/?utm_source=tuicool&utm_medium=referral } uses classes, sysutils; type PMailAttachment = ^TMailAttachment; TMailAttachment = record ContentType: UnicodeString; ContentId: UnicodeString; ContentFile: UnicodeString; ContentStream: TStream; end; IMailAttachments = interface procedure AddFile(const AFileName: UnicodeString; const AContentId: UnicodeString = ''); procedure AddStream(AData: TStream; const AContentType: UnicodeString; const AContentId: UnicodeString = ''); function GetCount: Integer; function GetItems(AIndex: Integer): PMailAttachment; property Count: Integer read GetCount; property Items[AIndex: Integer]: PMailAttachment read GetItems; end; TMailSender = record public SMTPServer: UnicodeString; // 服务器地址 SMTPPort: Integer; // 服务器端口 UserName: UnicodeString; // 用户名 Password: UnicodeString; // 密码 CCList: UnicodeString; // 抄送地址列表 BCCList: UnicodeString; // 暗抄送地址列表 Attachements: IMailAttachments; // 附件 SenderName: UnicodeString; // 发送者姓名 SenderMail: UnicodeString; // 发送者邮箱 RecipientMail: UnicodeString; // 收件人邮箱 Subject: UnicodeString; // 邮件主题 Body: UnicodeString; // 邮件内容 LastError: UnicodeString; UseSASL: Boolean; private public class function Create(AServer, AUserName, APassword: UnicodeString) : TMailSender; overload; static; class function Create: TMailSender; overload; static; function Send: Boolean; function SendBySSL: Boolean; end; TGraphicFormat = (gfUnknown, gfBitmap, gfJpeg, gfPng, gfGif, gfMetafile, gfTga, gfPcx, gfTiff, gfIcon, gfCursor, gfIff, gfAni); function DetectImageFormat(AStream: TStream): TGraphicFormat; overload; function DetectImageFormat(AFileName: String): TGraphicFormat; overload; function EncodeMailImage(AStream: TStream; AId: UnicodeString = ''; AWidth: UnicodeString = ''; AHeight: UnicodeString = '') : UnicodeString; overload; function EncodeMailImage(AFileName: UnicodeString; AId: UnicodeString = ''; AWidth: UnicodeString = ''; AHeight: UnicodeString = '') : UnicodeString; overload; var DefaultSMTPServer: String; DefaultSMTPUserName: String; DefaultSMTPPassword: String; implementation uses IdComponent, IdTCPConnection, IdTCPClient, IdExplicitTLSClientServerBase, IdMessage, IdMessageClient, IdMessageBuilder, IdSMTPBase, IdBaseComponent, IdIOHandler, IdSmtp, IdIOHandlerSocket, IdIOHandlerStack, IdSSL, IdSSLOpenSSL, IdSASLLogin, IdSASL_CRAM_SHA1, IdSASL, IdSASLUserPass, IdSASL_CRAMBase, IdSASL_CRAM_MD5, IdSASLSKey, IdSASLPlain, IdSASLOTP, IdSASLExternal, IdSASLDigest, IdSASLAnonymous, IdUserPassProvider, QString, EncdDecd{$IF RTLVersion>27}, NetEncoding{$IFEND}{$IFDEF UNICODE}, Generics.Collections{$ENDIF}; resourcestring SUnsupportImageFormat = '不支持的图片格式,HTML中图片只支持JPG/PNG/GIF/BMP'; type {$IF RTLVersion>=21} TAttachmentList = TList<PMailAttachment>; {$ELSE} TAttachmentList = TList; {$IFEND} TMailAttachments = class(TInterfacedObject, IMailAttachments) protected FItems: TAttachmentList; procedure AddFile(const AFileName: UnicodeString; const AContentId: UnicodeString = ''); procedure AddStream(AData: TStream; const AContentType: UnicodeString; const AContentId: UnicodeString = ''); function GetCount: Integer; function GetItems(AIndex: Integer): PMailAttachment; procedure DoInitializeISO(var VHeaderEncoding: Char; var VCharSet: string); public constructor Create; overload; destructor Destroy; override; end; { TMailSender } class function TMailSender.Create(AServer, AUserName, APassword: UnicodeString) : TMailSender; var AHost: UnicodeString; begin AHost := DecodeTokenW(AServer, ':', #0, true, true); Result.SMTPServer := AHost; if not TryStrToInt(AServer, Result.SMTPPort) then Result.SMTPPort := 25; Result.UserName := AUserName; Result.Password := APassword; Result.Attachements := TMailAttachments.Create; Result.UseSASL := true; end; procedure BuildHtmlMessage(const AData: TMailSender; AMsg: TIdMessage); var I: Integer; ABuilder: TIdMessageBuilderHtml; begin ABuilder := TIdMessageBuilderHtml.Create; try ABuilder.HtmlCharSet := 'UTF-8'; if StartWithW(PWideChar(AData.Body), '<', False) then ABuilder.Html.Text := AData.Body else ABuilder.PlainText.Text := AData.Body; for I := 0 to AData.Attachements.Count - 1 do begin with AData.Attachements.Items[I]^ do begin if Assigned(ContentStream) then ABuilder.Attachments.Add(ContentStream, ContentType, ContentId) else if Length(ContentFile) > 0 then ABuilder.Attachments.Add(ContentFile, ContentId); end; end; ABuilder.FillMessage(AMsg); finally FreeAndNil(ABuilder); end; AMsg.CharSet := 'UTF-8'; AMsg.Body.Text := AData.Body; AMsg.Sender.Text := AData.SenderMail; AMsg.From.Address := AData.SenderMail; AMsg.From.Name := AData.SenderName; AMsg.ReplyTo.EMailAddresses := AData.SenderMail; AMsg.Recipients.EMailAddresses := AData.RecipientMail; AMsg.Subject := AData.Subject; AMsg.CCList.EMailAddresses := AData.CCList; AMsg.ReceiptRecipient.Text := ''; AMsg.BCCList.EMailAddresses := AData.BCCList; end; procedure InitSASL(ASmtp: TIdSmtp; AUserName, APassword: String); var IdUserPassProvider: TIdUserPassProvider; IdSASLCRAMMD5: TIdSASLCRAMMD5; IdSASLCRAMSHA1: TIdSASLCRAMSHA1; IdSASLPlain: TIdSASLPlain; IdSASLLogin: TIdSASLLogin; IdSASLSKey: TIdSASLSKey; IdSASLOTP: TIdSASLOTP; IdSASLAnonymous: TIdSASLAnonymous; IdSASLExternal: TIdSASLExternal; begin IdUserPassProvider := TIdUserPassProvider.Create(ASmtp); IdUserPassProvider.UserName := AUserName; IdUserPassProvider.Password := APassword; IdSASLCRAMSHA1 := TIdSASLCRAMSHA1.Create(ASmtp); IdSASLCRAMSHA1.UserPassProvider := IdUserPassProvider; IdSASLCRAMMD5 := TIdSASLCRAMMD5.Create(ASmtp); IdSASLCRAMMD5.UserPassProvider := IdUserPassProvider; IdSASLSKey := TIdSASLSKey.Create(ASmtp); IdSASLSKey.UserPassProvider := IdUserPassProvider; IdSASLOTP := TIdSASLOTP.Create(ASmtp); IdSASLOTP.UserPassProvider := IdUserPassProvider; IdSASLAnonymous := TIdSASLAnonymous.Create(ASmtp); IdSASLExternal := TIdSASLExternal.Create(ASmtp); IdSASLLogin := TIdSASLLogin.Create(ASmtp); IdSASLLogin.UserPassProvider := IdUserPassProvider; IdSASLPlain := TIdSASLPlain.Create(ASmtp); IdSASLPlain.UserPassProvider := IdUserPassProvider; ASmtp.SASLMechanisms.Add.SASL := IdSASLCRAMSHA1; ASmtp.SASLMechanisms.Add.SASL := IdSASLCRAMMD5; ASmtp.SASLMechanisms.Add.SASL := IdSASLSKey; ASmtp.SASLMechanisms.Add.SASL := IdSASLOTP; ASmtp.SASLMechanisms.Add.SASL := IdSASLAnonymous; ASmtp.SASLMechanisms.Add.SASL := IdSASLExternal; ASmtp.SASLMechanisms.Add.SASL := IdSASLLogin; ASmtp.SASLMechanisms.Add.SASL := IdSASLPlain; end; procedure AddSSLSupport(ASmtp: TIdSmtp); var SSLHandler: TIdSSLIOHandlerSocketOpenSSL; begin SSLHandler := TIdSSLIOHandlerSocketOpenSSL.Create(ASmtp); // SSL/TLS handshake determines the highest available SSL/TLS version dynamically SSLHandler.SSLOptions.Method := sslvSSLv23; SSLHandler.SSLOptions.Mode := sslmClient; SSLHandler.SSLOptions.VerifyMode := []; SSLHandler.SSLOptions.VerifyDepth := 0; ASmtp.IOHandler := SSLHandler; end; procedure SendMailEx(const AData: TMailSender; AUseSSL, AUseSASL: Boolean); var AMsg: TIdMessage; ASmtp: TIdSmtp; begin AMsg := TIdMessage.Create; ASmtp := TIdSmtp.Create; try AMsg.OnInitializeISO := (AData.Attachements as TMailAttachments) .DoInitializeISO; BuildHtmlMessage(AData, AMsg); if AUseSSL then begin AddSSLSupport(ASmtp); if AData.SMTPPort = 587 then ASmtp.UseTLS := utUseExplicitTLS else ASmtp.UseTLS := utUseImplicitTLS; end; if (Length(AData.UserName) > 0) or (Length(AData.Password) > 0) then begin if AUseSASL then begin ASmtp.AuthType := satSASL; InitSASL(ASmtp, AData.UserName, AData.Password); end else begin ASmtp.UserName := AData.UserName; ASmtp.Password := AData.Password; end; end else begin ASmtp.AuthType := satNone; end; ASmtp.Host := AData.SMTPServer; ASmtp.Port := AData.SMTPPort; ASmtp.ConnectTimeout := 30000; ASmtp.UseEHLO := true; ASmtp.Connect; try ASmtp.Send(AMsg); finally ASmtp.Disconnect; end; finally FreeAndNil(ASmtp); FreeAndNil(AMsg); end; end; class function TMailSender.Create: TMailSender; begin Result := Create(DefaultSMTPServer, DefaultSMTPUserName, DefaultSMTPPassword); end; function TMailSender.Send: Boolean; begin try Result := true; SendMailEx(Self, False, UseSASL); except on E: Exception do begin LastError := E.Message; Result := False; end; end; end; function TMailSender.SendBySSL: Boolean; begin try Result := true; SendMailEx(Self, true, UseSASL); except on E: Exception do begin LastError := E.Message; Result := False; end; end; end; { TMailAttachments } procedure TMailAttachments.AddFile(const AFileName, AContentId: UnicodeString); var AItem: PMailAttachment; begin New(AItem); AItem.ContentFile := AFileName; AItem.ContentId := AContentId; FItems.Add(AItem); end; procedure TMailAttachments.AddStream(AData: TStream; const AContentType, AContentId: UnicodeString); var AItem: PMailAttachment; begin New(AItem); AItem.ContentStream := AData; AItem.ContentType := AContentType; AItem.ContentId := AContentId; FItems.Add(AItem); end; constructor TMailAttachments.Create; begin FItems := TAttachmentList.Create; end; destructor TMailAttachments.Destroy; var I: Integer; begin for I := 0 to FItems.Count - 1 do Dispose(PMailAttachment(FItems[I])); FreeAndNil(FItems); inherited; end; procedure TMailAttachments.DoInitializeISO(var VHeaderEncoding: Char; var VCharSet: string); begin VCharSet := 'UTF-8'; VHeaderEncoding := 'B'; end; function TMailAttachments.GetCount: Integer; begin Result := FItems.Count; end; function TMailAttachments.GetItems(AIndex: Integer): PMailAttachment; begin Result := FItems[AIndex]; end; /// <summary>检测图片格式</summary> /// <params> /// <param name="AStream">要检测的图片数据流</param> /// </params> /// <returns>返回可以识别的图片格式代码</returns> function DetectImageFormat(AStream: TStream): TGraphicFormat; overload; var ABuf: array [0 .. 7] of Byte; AReaded: Integer; begin FillChar(ABuf, 8, 0); AReaded := AStream.Read(ABuf[0], 8); AStream.Seek(-AReaded, soFromCurrent); // 回到原始位置 if (ABuf[0] = $FF) and (ABuf[1] = $D8) then // JPEG文件头标识 (2 bytes): $ff, $d8 (SOI) (JPEG 文件标识) Result := gfJpeg else if (ABuf[0] = $89) and (ABuf[1] = $50) and (ABuf[2] = $4E) and (ABuf[3] = $47) and (ABuf[4] = $0D) and (ABuf[5] = $0A) and (ABuf[6] = $1A) and (ABuf[7] = $0A) then Result := gfPng // 3.PNG文件头标识 (8 bytes) 89 50 4E 47 0D 0A 1A 0A else if (ABuf[0] = $42) and (ABuf[1] = $4D) then Result := gfBitmap else if (ABuf[0] = $47) and (ABuf[1] = $49) and (ABuf[2] = $46) and (ABuf[3] = $38) and (ABuf[4] in [$37, $39]) and (ABuf[5] = $61) then Result := gfGif // GIF- 文件头标识 (6 bytes) 47 49 46 38 39(37) 61 G I F 8 9 (7) a else if (ABuf[0] = $01) and (ABuf[1] = $00) and (ABuf[2] = $00) and (ABuf[3] = $00) then Result := gfMetafile // EMF 01 00 00 00 else if (ABuf[0] = $01) and (ABuf[1] = $00) and (ABuf[2] = $09) and (ABuf[3] = $00) and (ABuf[4] = $00) and (ABuf[5] = $03) then Result := gfMetafile // WMF 01 00 09 00 00 03 else if (ABuf[0] = $00) and (ABuf[1] = $00) and ((ABuf[2] = $02) or (ABuf[2] = $10)) and (ABuf[3] = $00) and (ABuf[4] = $00) then Result := gfTga // TGA- 未压缩的前5字节 00 00 02 00 00,RLE压缩的前5字节 00 00 10 00 00 else if (ABuf[0] = $0A) then Result := gfPcx // PCX - 文件头标识 (1 bytes) 0A else if ((ABuf[0] = $4D) and (ABuf[1] = $4D)) or ((ABuf[0] = $49) and (ABuf[1] = $49)) then Result := gfTiff // TIFF - 文件头标识 (2 bytes) 4D 4D 或 49 49 else if (ABuf[0] = $00) and (ABuf[1] = $00) and (ABuf[2] = $01) and (ABuf[3] = $00) and (ABuf[4] = $01) and (ABuf[5] = $00) and (ABuf[6] = $20) and (ABuf[7] = $20) then Result := gfIcon // ICO - 文件头标识 (8 bytes) 00 00 01 00 01 00 20 20 else if (ABuf[0] = $00) and (ABuf[1] = $00) and (ABuf[2] = $02) and (ABuf[3] = $00) and (ABuf[4] = $01) and (ABuf[5] = $00) and (ABuf[6] = $20) and (ABuf[7] = $20) then Result := gfCursor // CUR - 文件头标识 (8 bytes) 00 00 02 00 01 00 20 20 else if (ABuf[0] = $46) and (ABuf[1] = $4F) and (ABuf[2] = $52) and (ABuf[3] = $4D) then Result := gfIff // IFF - 文件头标识 (4 bytes) 46 4F 52 4D(FORM) else if (ABuf[0] = $52) and (ABuf[1] = $49) and (ABuf[2] = $46) and (ABuf[3] = $46) then Result := gfAni // 11.ANI- 文件头标识 (4 bytes) 52 49 46 46(RIFF) else Result := gfUnknown; end; /// <summary>检测图片格式</summary> /// <params> /// <param name="AFileName">要检测的图片文件名</param> /// </params> /// <returns>返回可以识别的图片格式代码</returns> function DetectImageFormat(AFileName: String): TGraphicFormat; overload; var AStream: TStream; begin AStream := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite); try Result := DetectImageFormat(AStream); finally FreeAndNil(AStream); end; end; function EncodeImageHeader(AStream: TStream; AId, AWidth, AHeight: String) : UnicodeString; begin if Length(AId) > 0 then Result := '<img id=' + QuotedStrW(AId, '"') else Result := '<img'; if Length(AWidth) > 0 then Result := Result + ' width=' + QuotedStrW(AWidth, '"'); if Length(AHeight) > 0 then Result := Result + ' height=' + QuotedStrW(AHeight, '"'); Result := Result + ' src="data:image/'; case DetectImageFormat(AStream) of gfBitmap: Result := Result + 'bmp;base64,'; gfJpeg: Result := Result + 'jpeg;base64,'; gfPng: Result := Result + 'png;base64,'; gfGif: Result := Result + 'gif;base64,'; else // 其它格式就不支持了,想支持的自己参考处理 raise Exception.Create(SUnsupportImageFormat); end; end; function EncodeMailImage(AStream: TStream; AId, AWidth, AHeight: UnicodeString) : UnicodeString; var ATemp: TCustomMemoryStream; begin if AStream is TCustomMemoryStream then begin ATemp := AStream as TCustomMemoryStream; Result := EncodeImageHeader(ATemp, '', '', '') + EncodeBase64(PByte(IntPtr(ATemp.Memory) + ATemp.Position), ATemp.Size - ATemp.Position) + '">'; end else begin ATemp := TMemoryStream.Create; try ATemp.CopyFrom(AStream, AStream.Size - AStream.Position); Result := EncodeImageHeader(ATemp, '', '', '') + EncodeBase64(ATemp.Memory, ATemp.Size) + '">'; finally FreeAndNil(ATemp); end; end; end; function EncodeMailImage(AFileName: UnicodeString; AId, AWidth, AHeight: UnicodeString): UnicodeString; var AStream: TMemoryStream; begin AStream := TMemoryStream.Create; try AStream.LoadFromFile(AFileName); AStream.Position := 0; Result := EncodeImageHeader(AStream, '', '', '') + EncodeBase64(AStream.Memory, AStream.Size) + '">'; finally FreeAndNil(AStream); end; end; end.
感谢 BccSafe 的原创,青春等群友的共同测试。
发送邮件的示例代码:
var ASender: TMailSender; begin ASender := TMailSender.Create('SMTP服务器地址', '账号','密码'); ASender.Body := '<html><body>Hello <B> QDAC </B></body></html>'; ASender.SenderName := '发送人姓名'; ASender.SenderMail := '发送者邮箱'; ASender.RecipientMail := '收件人邮箱'; ASender.Subject := '邮件主题'; //如果不启用SSL,就直接 ASender.Send 就可以了。 ASender.SendBySSL; end;
如果要添加附件,就使用 Attachments 成员的 AddFile 或 AddStream 就行了。
如果想在HTML邮件中插入图片,有两种方式:
- 添加附件,然后在附件中<img src=”cid:附件的ContentId”>,缺点是会显示为附件。
- 直接用 EncodeMailImage 插入基于Base64 编码的图片资源,这种不会显示为附件,示例如下:
procedure TForm1.Button1Click(Sender: TObject); var ASender: TMailSender; begin ASender := TMailSender.Create; // 使用全局的发送设置 ASender.Body := '<html><body>Hello <B> swish </B><br/>' + EncodeMailImage('D:\User\QDAC3\logo.jpg') + '</body></html>'; ASender.SenderName := '发送者姓名'; ASender.SenderMail := '发送者邮箱地址'; ASender.RecipientMail := '接收者邮件地址'; ASender.Subject := 'Indy SendMail 测试'; ASender.SendBySSL; end;
因为使用了全局的邮件SMTP服务器设置,所以请先设置好DefaultSMTPServer、DefaultSMTPUserName 、
DefaultSMTPPassword 三个值。
【注意】
在 Windows 下如果使用 SSL,需要32位或64位的 libeay32.dll、ssleay32.dll 两个动态链接库及相应的 VC 运行时库支持。