restemplate/indy/Protocols/IdSMTPBase.pas

557 lines
17 KiB
Plaintext

{
$Project$
$Workfile$
$Revision$
$DateUTC$
$Id$
This file is part of the Indy (Internet Direct) project, and is offered
under the dual-licensing agreement described on the Indy website.
(http://www.indyproject.org/)
Copyright:
(c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
$Log$
}
{
Rev 1.19 10/01/2005 16:31:20 ANeillans
Minor bug fix for Exim compatibility.
Rev 1.18 11/27/04 3:03:22 AM RLebeau
Bug fix for 'STARTTLS' response handling
Rev 1.17 6/11/2004 9:38:44 AM DSiders
Added "Do not Localize" comments.
Rev 1.16 2004.02.03 5:45:46 PM czhower
Name changes
Rev 1.15 2004.02.03 2:12:18 PM czhower
$I path change
Rev 1.14 1/28/2004 8:08:10 PM JPMugaas
Fixed a bug in SendGreeting. It was sending an invalid EHLO and probably an
invalid HELO command. The problem is that it was getting the computer name.
There's an issue in NET with that which Kudzu commented on in IdGlobal.
Thus, "EHLO<space>" and probably "HELO<space>" were being sent causing a
failure. The fix is to to try to get the local computer's DNS name with
GStack.HostName. We only use the computer name if GStack.HostName fails.
Rev 1.13 1/25/2004 3:11:48 PM JPMugaas
SASL Interface reworked to make it easier for developers to use.
SSL and SASL reenabled components.
Rev 1.12 2004.01.22 10:29:58 PM czhower
Now supports default login mechanism with just username and pw.
Rev 1.11 1/21/2004 4:03:24 PM JPMugaas
InitComponent
Rev 1.10 22/12/2003 00:46:36 CCostelloe
.NET fixes
Rev 1.9 4/12/2003 10:24:08 PM GGrieve
Fix to Compile
Rev 1.8 25/11/2003 12:24:22 PM SGrobety
various IdStream fixes with ReadLn/D6
Rev 1.7 10/17/2003 1:02:56 AM DSiders
Added localization comments.
Rev 1.6 2003.10.14 1:31:16 PM czhower
DotNet
Rev 1.5 2003.10.12 6:36:42 PM czhower
Now compiles.
Rev 1.4 10/11/2003 7:14:36 PM BGooijen
Changed IdCompilerDefines.inc path
Rev 1.3 10/10/2003 10:45:12 PM BGooijen
DotNet
Rev 1.2 2003.10.02 9:27:54 PM czhower
DotNet Excludes
Rev 1.1 6/15/2003 03:28:24 PM JPMugaas
Minor class change.
Rev 1.0 6/15/2003 01:06:48 PM JPMugaas
TIdSMTP and TIdDirectSMTP now share common code in this base class.
}
unit IdSMTPBase;
interface
{$i IdCompilerDefines.inc}
uses
IdGlobal,
IdEMailAddress,
IdMessage,
IdMessageClient,
IdReply,
IdTCPClient;
//default property values
const
DEF_SMTP_PIPELINE = False;
IdDEF_UseEhlo = True; //APR: default behavior
IdDEF_UseVerp = False;
const
CAPAPIPELINE = 'PIPELINING'; {do not localize}
CAPAVERP = 'VERP'; {do not localize}
XMAILER_HEADER = 'X-Mailer'; {do not localize}
const
RCPTTO_ACCEPT : array [0..1] of Int16 = (250, 251);
MAILFROM_ACCEPT : Int16 = 250;
DATA_ACCEPT : Int16 = 354;
DATA_PERIOD_ACCEPT : Int16 = 250;
RSET_ACCEPT : Int16 = 250;
const
RSET_CMD = 'RSET'; {do not localize}
MAILFROM_CMD = 'MAIL FROM:'; {do not localize}
RCPTTO_CMD = 'RCPT TO:'; {do not localize}
DATA_CMD = 'DATA'; {do not localize}
type
TIdSMTPFailedRecipient = procedure(Sender: TObject; const AAddress, ACode, AText: String;
var VContinue: Boolean) of object;
TIdSMTPBase = class(TIdMessageClient)
protected
FMailAgent: string;
FHeloName : String;
FPipeline : Boolean;
FUseEhlo : Boolean;
FUseVerp : Boolean;
FVerpDelims: string;
FOnFailedRecipient: TIdSMTPFailedRecipient;
//
function GetSupportsTLS : Boolean; override;
function GetReplyClass: TIdReplyClass; override;
procedure InitComponent; override;
procedure SendGreeting;
procedure SetUseEhlo(const AValue: Boolean); virtual;
procedure SetPipeline(const AValue: Boolean);
procedure StartTLS;
function FailedRecipientCanContinue(const AAddress: string): Boolean;
//No pipeline send methods
function WriteRecipientNoPipelining(const AEmailAddress: TIdEmailAddressItem): Boolean;
procedure WriteRecipientsNoPipelining(AList: TIdEmailAddressList);
procedure SendNoPipelining(AMsg: TIdMessage; const AFrom: String; ARecipients: TIdEMailAddressList);
//pipeline send methods
procedure WriteRecipientPipeLine(const AEmailAddress: TIdEmailAddressItem);
procedure WriteRecipientsPipeLine(AList: TIdEmailAddressList);
procedure SendPipelining(AMsg: TIdMessage; const AFrom: String; ARecipients: TIdEMailAddressList);
//
procedure InternalSend(AMsg: TIdMessage; const AFrom: String; ARecipients: TIdEMailAddressList); virtual;
public
procedure Send(AMsg: TIdMessage); overload; virtual;
procedure Send(AMsg: TIdMessage; ARecipients: TIdEMailAddressList); overload; virtual;
procedure Send(AMsg: TIdMessage; const AFrom: string); overload; virtual;
procedure Send(AMsg: TIdMessage; ARecipients: TIdEMailAddressList; const AFrom: string); overload; virtual;
published
property MailAgent: string read FMailAgent write FMailAgent;
property HeloName : string read FHeloName write FHeloName;
property UseEhlo: Boolean read FUseEhlo write SetUseEhlo default IdDEF_UseEhlo;
property PipeLine : Boolean read FPipeLine write SetPipeline default DEF_SMTP_PIPELINE;
property UseVerp: Boolean read FUseVerp write FUseVerp default IdDEF_UseVerp;
property VerpDelims: string read FVerpDelims write FVerpDelims;
//
property OnFailedRecipient: TIdSMTPFailedRecipient read FOnFailedRecipient write FOnFailedRecipient;
end;
implementation
uses
{$IFDEF VCL_XE3_OR_ABOVE}
System.Classes,
{$ENDIF}
IdAssignedNumbers, IdException,
IdExplicitTLSClientServerBase,
IdGlobalProtocols, IdIOHandler, IdReplySMTP,
IdSSL,
IdStack, SysUtils; //required as we need to get the local computer DNS hostname.
{ TIdSMTPBase }
function TIdSMTPBase.GetReplyClass:TIdReplyClass;
begin
Result := TIdReplySMTP;
end;
procedure TIdSMTPBase.InitComponent;
begin
inherited InitComponent;
FImplicitTLSProtPort := IdPORT_ssmtp;
FRegularProtPort := IdPORT_SMTP;
FPipeLine := DEF_SMTP_PIPELINE;
FUseEhlo := IdDEF_UseEhlo;
FUseVerp := IdDEF_UseVerp;
FMailAgent := '';
FHeloName := '';
Port := IdPORT_SMTP;
end;
function TIdSMTPBase.GetSupportsTLS: Boolean;
begin
Result := ( FCapabilities.IndexOf('STARTTLS') > -1 ); //do not localize
end;
procedure TIdSMTPBase.SendGreeting;
var
LNameToSend : String;
begin
Capabilities.Clear;
if HeloName <> '' then begin
LNameToSend := HeloName;
end else begin
//Note: IndyComputerName gets the computer name.
//This is not always reliable in Indy because in Dot.NET,
//it is done with This is available through System.Windows.Forms.SystemInformation.ComputerName
//and that requires that we link to a problematic dependancy (Wystem.Windows.Forms).
//Besides, I think RFC 821 was refering to the computer's Internet
//DNS name. We use the Computer name only if we can't get the DNS name.
LNameToSend := GStack.HostName;
if LNameToSend = '' then
begin
LNameToSend := IndyComputerName;
end;
end;
if UseEhlo and (SendCmd('EHLO ' + LNameToSend ) = 250) then begin //APR: user can prevent EHLO {Do not Localize}
Capabilities.AddStrings(LastCmdResult.Text);
if Capabilities.Count > 0 then begin
//we drop the initial greeting. We only want the feature list
Capabilities.Delete(0);
end;
end else begin
SendCmd('HELO ' + LNameToSend, 250); {Do not Localize}
end;
end;
procedure TIdSMTPBase.SetPipeline(const AValue: Boolean);
begin
FPipeLine := AValue;
if AValue then begin
FUseEhlo := True;
end;
end;
procedure TIdSMTPBase.SetUseEhlo(const AValue: Boolean);
begin
FUseEhlo := AValue;
if not AValue then
begin
FPipeLine := False;
end;
end;
procedure TIdSMTPBase.SendNoPipelining(AMsg: TIdMessage; const AFrom: String; ARecipients: TIdEMailAddressList);
var
LCmd: string;
begin
LCmd := MAILFROM_CMD + '<' + AFrom + '>'; {Do not Localize}
if FUseVerp then begin
if Capabilities.IndexOf(CAPAVERP) > -1 then begin
LCmd := LCmd + ' VERP'; {Do not Localize}
end else begin
LCmd := LCmd + ' XVERP'; {Do not Localize}
end;
if FVerpDelims <> '' then begin
LCmd := LCmd + '=' + FVerpDelims; {Do not Localize}
end;
end;
// RLebeau 4/29/2013: DO NOT send a RSET command before the MAIL FROM command!
// Some servers are buggy and will reset the entire session, including any
// previously accepted authentication, when they are supposed to reset only
// their mail sending buffers and nothing else. Send a RSET only if the mail
// transaction fails and needs to be cleaned up...
// TODO: make this configurable?
//SendCmd(RSET_CMD);
SendCmd(LCmd, MAILFROM_ACCEPT);
try
WriteRecipientsNoPipelining(ARecipients);
SendCmd(DATA_CMD, DATA_ACCEPT);
SendMsg(AMsg);
SendCmd('.', DATA_PERIOD_ACCEPT); {Do not Localize}
except
on E: EIdSMTPReplyError do begin
SendCmd(RSET_CMD);
raise;
end;
end;
end;
procedure TIdSMTPBase.SendPipelining(AMsg: TIdMessage; const AFrom: String; ARecipients: TIdEMailAddressList);
var
LError : TIdReplySMTP;
I, LFailedRecips : Integer;
LCmd: string;
LBufferingStarted: Boolean;
function SetupErrorReply: TIdReplySMTP;
begin
Result := TIdReplySMTP.Create(nil);
Result.Assign(LastCmdResult);
end;
begin
LError := nil;
LCmd := MAILFROM_CMD + '<' + AFrom + '>'; {Do not Localize}
if FUseVerp then begin
if Capabilities.IndexOf(CAPAVERP) > -1 then begin
LCmd := LCmd + ' VERP'; {Do not Localize}
end else begin
LCmd := LCmd + ' XVERP'; {Do not Localize}
end;
if FVerpDelims <> '' then begin
LCmd := LCmd + '=' + FVerpDelims; {Do not Localize}
end;
end;
try
LBufferingStarted := not IOHandler.WriteBufferingActive;
if LBufferingStarted then begin
IOHandler.WriteBufferOpen;
end;
// RLebeau 4/29/2013: DO NOT send a RSET command before the MAIL FROM command!
// Some servers are buggy and will reset the entire session, including any
// previously accepted authentication, when they are supposed to reset only
// their mail sending buffers and nothing else. Send a RSET only if the mail
// transaction fails and needs to be cleaned up...
// TODO: make this configurable?
try
//IOHandler.WriteLn(RSET_CMD);
IOHandler.WriteLn(LCmd);
WriteRecipientsPipeLine(ARecipients);
IOHandler.WriteLn(DATA_CMD);
if LBufferingStarted then begin
IOHandler.WriteBufferClose;
end;
except
if LBufferingStarted then begin
IOHandler.WriteBufferCancel;
end;
raise;
end;
{
//RSET
if PosInSmallIntArray(GetResponse, RSET_ACCEPT) = -1 then begin
LError := SetupErrorReply;
end;
}
//MAIL FROM:
if PosInSmallIntArray(GetResponse, MAILFROM_ACCEPT) = -1 then begin
if not Assigned(LError) then begin
LError := SetupErrorReply;
end;
end;
//RCPT TO:
if ARecipients.Count > 0 then begin
LFailedRecips := 0;
for I := 0 to ARecipients.Count - 1 do begin
if PosInSmallIntArray(GetResponse, RCPTTO_ACCEPT) = -1 then begin
Inc(LFailedRecips);
if not FailedRecipientCanContinue(ARecipients[I].Address) then begin
if not Assigned(LError) then begin
LError := SetupErrorReply;
end;
end;
end;
end;
if not Assigned(LError) and (LFailedRecips = ARecipients.Count) then begin
LError := SetupErrorReply;
end;
end;
//DATA - last in the batch
if PosInSmallIntArray(GetResponse, DATA_ACCEPT) <> -1 then begin
SendMsg(AMsg);
if PosInSmallIntArray(SendCmd('.'), DATA_PERIOD_ACCEPT) = -1 then begin {Do not Localize}
if not Assigned(LError) then begin
LError := SetupErrorReply;
end;
end;
end else begin
if not Assigned(LError) then begin
LError := SetupErrorReply;
end;
end;
if Assigned(LError) then begin
SendCmd(RSET_CMD);
LError.RaiseReplyError;
end;
finally
FreeAndNil(LError);
end;
end;
procedure TIdSMTPBase.StartTLS;
var
LIO : TIdSSLIOHandlerSocketBase;
LSendQuitOnError: Boolean;
begin
LSendQuitOnError := True;
try
if (IOHandler is TIdSSLIOHandlerSocketBase) and (FUseTLS <> utNoTLSSupport) then
begin
LIO := TIdSSLIOHandlerSocketBase(IOHandler);
//we check passthrough because we can either be using TLS currently with
//implicit TLS support or because STARTLS was issued previously.
if LIO.PassThrough then
begin
if SupportsTLS then
begin
if SendCmd('STARTTLS') = 220 then begin {do not localize}
LSendQuitOnError := False;
TLSHandshake;
//send EHLO
SendGreeting;
end else begin
ProcessTLSNegCmdFailed;
end;
end else begin
ProcessTLSNotAvail;
end;
end;
end;
except
Disconnect(LSendQuitOnError); // RLebeau: do not send the QUIT command if the handshake was started
Raise;
end;
end;
function TIdSMTPBase.FailedRecipientCanContinue(const AAddress: string): Boolean;
begin
Result := Assigned(FOnFailedRecipient);
if Result then begin
FOnFailedRecipient(Self, AAddress, LastCmdResult.Code, LastCmdResult.Text.Text, Result);
end;
end;
function TIdSMTPBase.WriteRecipientNoPipelining(const AEmailAddress: TIdEmailAddressItem): Boolean;
var
LReply: Int16;
begin
LReply := SendCmd(RCPTTO_CMD + '<' + AEMailAddress.Address + '>'); {do not localize}
Result := PosInSmallIntArray(LReply, RCPTTO_ACCEPT) <> -1;
end;
procedure TIdSMTPBase.WriteRecipientPipeLine(const AEmailAddress: TIdEmailAddressItem);
begin
//we'll read the reply - LATER
IOHandler.WriteLn(RCPTTO_CMD + '<' + AEMailAddress.Address + '>');
end;
procedure TIdSMTPBase.WriteRecipientsNoPipelining(AList: TIdEmailAddressList);
var
I, LFailedRecips: Integer;
LContinue: Boolean;
begin
if AList.Count > 0 then begin
LFailedRecips := 0;
LContinue := True;
for I := 0 to AList.Count - 1 do begin
if not WriteRecipientNoPipelining(AList[I]) then begin
Inc(LFailedRecips);
if not FailedRecipientCanContinue(AList[I].Address) then begin
LContinue := False;
Break;
end;
end;
end;
if (not LContinue) or (LFailedRecips = AList.Count) then begin
LastCmdResult.RaiseReplyError;
end;
end;
end;
procedure TIdSMTPBase.WriteRecipientsPipeLine(AList: TIdEmailAddressList);
var
I: integer;
begin
for I := 0 to AList.Count - 1 do begin
WriteRecipientPipeLine(AList[I]);
end;
end;
procedure TIdSMTPBase.InternalSend(AMsg: TIdMessage; const AFrom: String; ARecipients: TIdEMailAddressList);
begin
if Pipeline and (Capabilities.IndexOf(CAPAPIPELINE) > -1) then begin
SendPipelining(AMsg, AFrom, ARecipients);
end else begin
SendNoPipelining(AMsg, AFrom, ARecipients);
end;
end;
// this version of Send() uses the TIdMessage to determine both the
// sender and the recipients...
procedure TIdSMTPBase.Send(AMsg: TIdMessage);
var
LRecipients: TIdEMailAddressList;
begin
LRecipients := TIdEMailAddressList.Create(Self);
try
LRecipients.AddItems(AMsg.Recipients);
LRecipients.AddItems(AMsg.CCList);
LRecipients.AddItems(AMsg.BccList);
Send(AMsg, LRecipients);
finally
FreeAndNil(LRecipients);
end;
end;
// this version of Send() uses the TIdMessage to determine the
// sender, but sends to the caller's specified recipients
procedure TIdSMTPBase.Send(AMsg: TIdMessage; ARecipients: TIdEMailAddressList);
var
LSender: string;
begin
LSender := Trim(AMsg.Sender.Address);
if LSender = '' then begin
LSender := Trim(AMsg.From.Address);
end;
InternalSend(AMsg, LSender, ARecipients);
end;
// this version of Send() uses the TIdMessage to determine the
// recipients, but sends using the caller's specified sender.
// The sender can be empty, which is useful for server-generated
// error messages...
procedure TIdSMTPBase.Send(AMsg: TIdMessage; const AFrom: string);
var
LRecipients: TIdEMailAddressList;
begin
LRecipients := TIdEMailAddressList.Create(Self);
try
LRecipients.AddItems(AMsg.Recipients);
LRecipients.AddItems(AMsg.CCList);
LRecipients.AddItems(AMsg.BccList);
Send(AMsg, LRecipients, AFrom);
finally
FreeAndNil(LRecipients);
end;
end;
// this version of Send() uses the caller's specified sender and
// recipients. The sender can be empty, which is useful for
// server-generated error messages...
procedure TIdSMTPBase.Send(AMsg: TIdMessage; ARecipients: TIdEMailAddressList;
const AFrom: string);
begin
InternalSend(AMsg, AFrom, ARecipients);
end;
end.