{
  $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.126    4/28/2005 BTaylor
  Changed .Size to use Int64

  Rev 1.125    4/15/2005 9:10:10 AM  JPMugaas
  Changed the default timeout in TIdFTP to one minute and made a comment about
  this.

  Some firewalls don't handle control connections properly during long data
  transfers.  They will timeout the control connection because it's idle and
  making it worse is that they will chop off a connection instead of closing it
  causing TIdFTP to wait forever for nothing.

  Rev 1.124    3/20/2005 10:42:44 PM  JPMugaas
  Marked TIdFTP.Quit as deprecated.  We need to keep it only for compatibility.

  Rev 1.123    3/20/2005 2:44:08 PM  JPMugaas
  Should now send quit.  Verified here.

  Rev 1.122    3/12/2005 6:57:12 PM  JPMugaas
  Attempt to add ACCT support for firewalls.  I also used some logic from some
  WS-FTP Pro about ACCT to be more consistant with those Firescripts.

  Rev 1.121    3/10/2005 2:41:12 PM  JPMugaas
  Removed the UseTelnetAbort property.  It turns out that sending the sequence
  is causing problems on a few servers.  I have made a comment about this in
  the source-code so someone later on will know  why I decided not to send
  those.

  Rev 1.120    3/9/2005 10:05:54 PM  JPMugaas
  Minor changes for Indy conventions.

  Rev 1.119    3/9/2005 9:15:46 PM  JPMugaas
  Changes submitted by Craig Peterson, Scooter Software  He noted this:

  "We had a user who's FTP server prompted for account info after a
  regular login, so I had to add an explicit Account string property and
  an OnNeedAccount event that we could use for a prompt."  This does break any
  code using TIdFTP.Account.

  TODO:  See about integrating Account Info into the proxy login sequences.

  Rev 1.118    3/9/2005 10:40:16 AM  JPMugaas
  Made comment explaining why I had made a workaround in a procedure.

  Rev 1.117    3/9/2005 10:28:32 AM  JPMugaas
  Fix for Abort problem when uploading.  A workaround I made for WS-FTP Pro
  Server was not done correctly.

  Rev 1.116    3/9/2005 1:21:38 AM  JPMugaas
  Made refinement to Abort and the data transfers to follow what Kudzu had
  originally done in Indy 8.  I also fixed a problem with ABOR at
  ftp.ipswitch.com and I fixed a regression at ftp.marist.edu that occured when
  getting a directory.

  Rev 1.115    3/8/2005 12:14:50 PM  JPMugaas
  Renamed UseOOBAbort to UseTelnetAbort because that's more accurate.  We still
  don't support Out of Band Data (hopefully, we'll never have to do that).

  Rev 1.114    3/7/2005 10:40:10 PM  JPMugaas
  Improvements:

  1) Removed some duplicate code.
  2) ABOR should now be properly handled outside of a data operation.
  3) I added a UseOOBAbort read-write public property for controlling how the
  ABOR command is sent.  If true, the Telnet sequences are sent or if false,
  the ABOR without sequences is sent.  This is set to false by default because
  one FTP client (SmartFTP recently removed the Telnet sequences from their
  program).

  This code is expiriemental.

  Rev 1.113    3/7/2005 5:46:34 PM  JPMugaas
  Reworked FTP Abort code to make it more threadsafe and make abort work.  This
  is PRELIMINARY.

  Rev 1.112    3/5/2005 3:33:56 PM  JPMugaas
  Fix for some compiler warnings having to do with TStream.Read being platform
  specific.  This was fixed by changing the Compressor API to use TIdStreamVCL
  instead of TStream.  I also made appropriate adjustments to other units for
  this.

  Rev 1.111    2/24/2005 6:46:36 AM  JPMugaas
  Clarrified remarks I made and added a few more comments about syntax in
  particular cases in the set modified file date procedures.

  That's really been a ball....NOT!!!!

  Rev 1.110    2/24/2005 6:25:08 AM  JPMugaas
  Attempt to fix problem setting Date with Titan FTP Server.  I had made an
  incorrect assumption about MDTM on that system.  It uses Syntax 3 (see my
  earlier note above the File Date Set problem.

  Rev 1.109    2/23/2005 6:32:54 PM  JPMugaas
  Made note about MDTM syntax inconsistancy.  There's a discussion about it.

  Rev 1.108    2/12/2005 8:08:04 AM  JPMugaas
  Attempt to fix MDTM bug where msec was being sent.

  Rev 1.107    1/12/2005 11:26:44 AM  JPMugaas
  Memory Leak fix when processing MLSD output and some minor tweeks Remy had
  E-Mailed me last night.

  Rev 1.106    11/18/2004 2:39:32 PM  JPMugaas
  Support for another FTP Proxy type.

  Rev 1.105    11/18/2004 12:18:50 AM  JPMugaas
  Fixed compile error.

  Rev 1.104    11/17/2004 3:59:22 PM  JPMugaas
  Fixed a TODO item about FTP Proxy support with a "Transparent" proxy.  I
  think you connect to the regular host and the firewall will intercept its
  login information.

  Rev 1.103    11/16/2004 7:31:52 AM  JPMugaas
  Made a comment noting that UserSite is the same as USER after login for later
  reference.

  Rev 1.102    11/5/2004 1:54:42 AM  JPMugaas
  Minor adjustment - should not detect TitanFTPD better (tested at:
  ftp.southrivertech.com).

  If MLSD is being used, SITE ZONE will not be issued.  It's not needed because
  the MLSD spec indicates the time is based on GMT.

  Rev 1.101    10/27/2004 12:58:08 AM  JPMugaas
  Improvement from Tobias Giesen http://www.superflexible.com
  His notation is below:

  "here's a fix for TIdFTP.IndexOfFeatLine. It does not work the
  way it is used in TIdFTP.SetModTime, because it only
  compares the first word of the FeatLine."

  Rev 1.100    10/26/2004 9:19:10 PM  JPMugaas
  Fixed references.

  Rev 1.99    9/16/2004 3:24:04 AM  JPMugaas
  TIdFTP now compresses to the IOHandler and decompresses from the IOHandler.

  Noted some that the ZLib code is based was taken from ZLibEx.

  Rev 1.98    9/13/2004 12:15:42 AM  JPMugaas
  Now should be able to handle some values better as suggested by Michael J.
  Leave.

  Rev 1.97    9/11/2004 10:58:06 AM  JPMugaas
  FTP now decompresses output directly to the IOHandler.

  Rev 1.96    9/10/2004 7:37:42 PM  JPMugaas
  Fixed a bug.  We needed to set Passthrough instead of calling StartSSL.  This
  was causing a SSL problem with upload.

  Rev 1.95    8/2/04 5:56:16 PM  RLebeau
  Tweaks to TIdFTP.InitDataChannel()

    Rev 1.94    7/30/2004 1:55:04 AM  DSiders
  Corrected DoOnRetrievedDir naming.

    Rev 1.93    7/30/2004 12:36:32 AM  DSiders
  Corrected spelling in OnRetrievedDir, DoOnRetrievedDir declarations.

  Rev 1.92    7/29/2004 2:15:28 AM  JPMugaas
  New property for controlling what AUTH command is sent.  Fixed some minor
  issues with FTP properties.  Some were not set to defaults causing
  unpredictable results -- OOPS!!!

  Rev 1.91    7/29/2004 12:04:40 AM  JPMugaas
  New events for Get and Put as suggested by Don Sides and to complement an
  event done by APR.

  Rev 1.90    7/28/2004 10:16:14 AM  JPMugaas
  New events for determining when a listing is finished and when the dir
  parsing begins and ends.  Dir parsing is done sometimes when DirectoryListing
  is referenced.

  Rev 1.89    7/27/2004 2:03:54 AM  JPMugaas
  New property:

  ExternalIP - used to specify an IP address for the PORT and EPRT commands.
  This should be blank unless you are behind a NAT and you need to use PORT
  transfers with SSL.  You would set ExternalIP to the NAT's IP address on the
  Internet.

  The idea is this:

  1) You set up your NAT to forward a range ports ports to your computer behind
  the NAT.
  2) You specify that a port range with the DataPortMin and DataPortMin
  properties.
  3) You set ExternalIP to the NAT's Internet IP address.

  I have verified this with Indy and WS FTP Pro behind a NAT router.

  Rev 1.88    7/23/04 7:09:50 PM  RLebeau
  Bug fix for TFileStream access rights in Get()

    Rev 1.87    7/18/2004 3:00:12 PM  DSiders
  Added localization comments.

  Rev 1.86    7/16/2004 4:28:40 AM  JPMugaas
  CCC Support in TIdFTP to complement that capability in TIdFTPServer.

  Rev 1.85    7/13/04 6:48:14 PM  RLebeau
  Added support for new DataPort and DataPortMin/Max properties

    Rev 1.84    7/6/2004 4:51:46 PM  DSiders
  Corrected spelling of Challenge in properties, methods, types.

  Rev 1.83    7/3/2004 3:15:50 AM  JPMugaas
  Checked in so everyone else can work on stuff while I'm away.

  Rev 1.82    6/27/2004 1:45:38 AM  JPMugaas
  Can now optionally support LastAccessTime like Smartftp's FTP Server could.
  I also made the MLST listing object and parser support this as well.

  Rev 1.81    6/20/2004 8:31:58 PM  JPMugaas
  New events for reporting greeting and after login banners during the login
  sequence.

  Rev 1.80    6/20/2004 6:56:42 PM  JPMugaas
  Start oin attempt to support FXP with Deflate compression.  More work will
  need to be done.

  Rev 1.79    6/17/2004 3:42:32 PM  JPMugaas
  Adjusted code for removal of dmBlock and dmCompressed.  Made TransferMode a
  property.  Note that the Set method is odd because I am trying to keep
  compatibility with older Indy versions.

  Rev 1.78    6/14/2004 6:19:02 PM  JPMugaas
  This now refers to TIdStreamVCL when downloading isntead of directly to a
  memory stream when compressing data.

  Rev 1.77    6/14/2004 8:34:52 AM  JPMugaas
  Fix for AV on Put with Passive := True.

    Rev 1.76    6/11/2004 9:34:12 AM  DSiders
  Added "Do not Localize" comments.

  Rev 1.75    2004.05.20 11:37:16 AM  czhower
  IdStreamVCL

  Rev 1.74    5/6/2004 6:54:26 PM  JPMugaas
  FTP Port transfers with TransparentProxies is enabled.  This only works if
  the TransparentProxy supports a "bind" request.

  Rev 1.73    5/4/2004 11:16:28 AM  JPMugaas
  TransferTimeout property added and enabled (Bug 96).

  Rev 1.72    5/4/2004 11:07:12 AM  JPMugaas
  Timeouts should now be reenabled in TIdFTP.

  Rev 1.71    4/19/2004 5:05:02 PM  JPMugaas
  Class rework Kudzu wanted.

  Rev 1.70    2004.04.16 9:31:42 PM  czhower
  Remove unnecessary duplicate string parsing and replaced with .assign.

  Rev 1.69    2004.04.15 7:09:04 PM  czhower
  .NET overloads

  Rev 1.68    4/15/2004 9:46:48 AM  JPMugaas
  List  no longer requires a TStrings.  It turns out that it was an optional
  parameter.

  Rev 1.67    2004.04.15 2:03:28 PM  czhower
  Removed login param from connect and made it a prop like POP3.

  Rev 1.66    3/3/2004 5:57:40 AM  JPMugaas
  Some IFDEF excluses were removed because the functionality is now in DotNET.

  Rev 1.65    2004.03.03 11:54:26 AM  czhower
  IdStream change

  Rev 1.64    2/20/2004 1:01:06 PM  JPMugaas
  Preliminary FTP PRET command support for using PASV with a distributed FTP
  server (Distributed PASV -
  http://drftpd.org/wiki/wiki.phtml?title=Distributed_PASV).

  Rev 1.63    2/17/2004 12:25:52 PM  JPMugaas
  The client now supports MODE Z (deflate) uploads and downloads as specified
  by http://www.ietf.org/internet-drafts/draft-preston-ftpext-deflate-00.txt

  Rev 1.62    2004.02.03 5:45:10 PM  czhower
  Name changes

  Rev 1.61    2004.02.03 2:12:06 PM  czhower
  $I path change

  Rev 1.60    1/27/2004 10:17:10 PM  JPMugaas
  Fix from Steve Loft for a server that sends something like this:
  "227 Passive mode OK (195,92,195,164,4,99 )"

  Rev 1.59    1/27/2004 3:59:28 PM  SPerry
  StringStream ->IdStringStream

  Rev 1.58    24/01/2004 19:13:58  CCostelloe
  Cleaned up warnings

  Rev 1.57    1/21/2004 2:27:50 PM  JPMugaas
  Bullete Proof FTPD and Titan FTP support SITE ZONE.  Saw this in a command
  database in StaffFTP.
  InitComponent.

  Rev 1.56    1/19/2004 9:05:38 PM  JPMugaas
  Fixes to FTP Set Date functionality.
  Introduced properties for Time Zone information from the server.  The way it
  works is this, if TIdFTP detects you are using "Serv-U" or SITE ZONE is
  listed in the FEAT reply, Indy obtains the time zone information with the
  SITE ZONE command and makes the appropriate calculation.  Indy then uses this
  information to calculate a timestamp to send to the server with the MDTM
  command.  You can also use the Time Zone information yourself to convert the
  FTP directory listing item timestamps into GMT and than convert that to your
  local time.
  FTP Voyager uses SITE ZONE as I've described.

  Rev 1.55    1/19/2004 4:39:08 AM  JPMugaas
  You can now set the time for a file on the server.  Note that these methods
  try to treat the time as relative to GMT.

  Rev 1.54    1/17/2004 9:09:30 PM  JPMugaas
  Should now compile.

  Rev 1.53    1/17/2004 7:48:02 PM  JPMugaas
  FXP site to site transfer code was redone for improvements with FXP with TLS.
  It actually works and I verified with RaidenFTPD
  (http://www.raidenftpd.com/) and the Indy FTP server components.   I also
  lowered the requirements for TLS FXP transfers.  The requirements now are:
  1) Only server (either the recipient or the sendor) has to support SSCN

  or

  2) The server receiving a PASV must support CPSV and the transfer is done
  with IPv4.

  Rev 1.52    1/9/2004 2:51:26 PM  JPMugaas
  Started IPv6 support.

  Rev 1.51    11/27/2003 4:55:28 AM  JPMugaas
  Made STOU functionality separate from PUT functionality.  Put now requires a
  destination filename except where a source-file name is given.  In that case,
  the default is the filename from the source string.

  Rev 1.50    10/26/2003 04:28:50 PM  JPMugaas
  Reworked Status.

  The old one was problematic because it assumed that STAT was a request to
  send a directory listing through the control channel.  This assumption is not
  correct.  It provides a way to get a freeform status report from a server.
  With a Path parameter, it should work like a LIST command  except that the
  control connection is used.  We don't support that feature and you should use
  our LIst method to get the directory listing anyway, IMAO.

  Rev 1.49    10/26/2003 9:17:46 PM  BGooijen
  Compiles in DotNet, and partially works there

  Rev 1.48    10/24/2003 12:43:48 PM  JPMugaas
  Should work again.

  Rev 1.47    2003.10.24 10:43:04 AM  czhower
  TIdSTream to dos

  Rev 1.46    10/20/2003 03:06:10 PM  JPMugaas
  SHould now work.

  Rev 1.45    10/20/2003 01:00:38 PM  JPMugaas
  EIdException no longer raised.  Some things were being gutted needlessly.

    Rev 1.44    10/19/2003 12:58:20 PM  DSiders
  Added localization comments.

  Rev 1.43    2003.10.14 9:56:50 PM  czhower
  Compile todos

  Rev 1.42    2003.10.12 3:50:40 PM  czhower
  Compile todos

  Rev 1.41    10/10/2003 11:32:26 PM  SPerry
  -

  Rev 1.40    10/9/2003 10:17:02 AM  JPMugaas
  Added overload for GetLoginPassword for providing a challanage string which
  doesn't have to the last command reply.
  Added CLNT support.

  Rev 1.39    10/7/2003 05:46:20 AM  JPMugaas
  SSCN Support added.

  Rev 1.38    10/6/2003 08:56:44 PM  JPMugaas
  Reworked the FTP list parsing framework so that the user can obtain the list
  of capabilities from a parser class with TIdFTP.  This should permit the user
  to present a directory listing differently for each parser (some FTP list
  parsers do have different capabilities).

  Rev 1.37    10/1/2003 12:51:18 AM  JPMugaas
  SSL with active (PORT) transfers now should work again.

  Rev 1.36    9/30/2003 09:50:38 PM  JPMugaas
  FTP with TLS should work better.  It turned out that we were negotiating it
  several times causing a hang.  I also made sure that we send PBSZ 0 and PROT
  P for both implicit and explicit TLS.  Data ports should work in PASV again.

  Rev 1.35    9/28/2003 11:41:06 PM  JPMugaas
  Reworked Eldos's proposed FTP fix as suggested by Henrick Hellström by moving
  all of the IOHandler creation code to InitDataChannel.  This should reduce
  the likelihood of error.

  Rev 1.33    9/18/2003 11:22:40 AM  JPMugaas
  Removed a temporary workaround for an OnWork bug that was in the Indy Core.
  That bug was fixed so there's no sense in keeping a workaround here.

  Rev 1.32    9/12/2003 08:05:30 PM  JPMugaas
  A temporary fix for OnWork events not firing.  The bug is that OnWork events
  aren't used in IOHandler where ReadStream really is located.

  Rev 1.31    9/8/2003 02:33:00 AM  JPMugaas
  OnCustomFTPProxy added to allow Indy to support custom FTP proxies.  When
  using this event, you are responsible for programming the FTP Proxy and FTP
  Server login sequence.
  GetLoginPassword method function for returning the password used when logging
  into a FTP server which handles OTP calculation.  This way, custom firewall
  support can handle One-Time-Password system transparently.  You do have to
  send the User ID before calling this function because the OTP challenge is
  part of the reply.

  Rev 1.30    6/10/2003 11:10:00 PM  JPMugaas
  Made comments about our loop that tries several AUTH command variations.
  Some servers may only accept AUTH SSL while other servers only accept AUTH
  TLS.

  Rev 1.29    5/26/2003 12:21:54 PM  JPMugaas

  Rev 1.28    5/25/2003 03:54:20 AM  JPMugaas

  Rev 1.27    5/19/2003 08:11:32 PM  JPMugaas
  Now should compile properly with new code in Core.

  Rev 1.26    5/8/2003 11:27:42 AM  JPMugaas
  Moved feature negoation properties down to the ExplicitTLSClient level as
  feature negotiation goes hand in hand with explicit TLS support.

  Rev 1.25    4/5/2003 02:06:34 PM  JPMugaas
  TLS handshake itself can now be handled.

    Rev 1.24    4/4/2003 8:01:32 PM  BGooijen
  now creates iohandler for dataconnection

  Rev 1.23    3/31/2003 08:40:18 AM  JPMugaas
  Fixed problem with QUIT command.

    Rev 1.22    3/27/2003 3:41:28 PM  BGooijen
  Changed because some properties are moved to IOHandler

  Rev 1.21    3/27/2003 05:46:24 AM  JPMugaas
  Updated framework with an event if the TLS negotiation command fails.
  Cleaned up some duplicate code in the clients.

  Rev 1.20    3/26/2003 04:19:20 PM  JPMugaas
  Cleaned-up some code and illiminated some duplicate things.

  Rev 1.19    3/24/2003 04:56:10 AM  JPMugaas
  A typecast was incorrect and could cause a potential source of instability if
  a TIdIOHandlerStack was not used.

  Rev 1.18    3/16/2003 06:09:58 PM  JPMugaas
  Fixed port setting bug.

  Rev 1.17    3/16/2003 02:40:16 PM  JPMugaas
  FTP client with new design.

    Rev 1.16    3/16/2003 1:02:44 AM  BGooijen
  Added 2 events to give the user more control to the dataconnection, moved
  SendTransferType, enabled ssl

  Rev 1.15    3/13/2003 09:48:58 AM  JPMugaas
  Now uses an abstract SSL base class instead of OpenSSL so 3rd-party vendors
  can plug-in their products.

  Rev 1.14    3/7/2003 11:51:52 AM  JPMugaas
  Fixed a writeln bug and an IOError issue.

  Rev 1.13    3/3/2003 07:06:26 PM  JPMugaas
  FFreeIOHandlerOnDisconnect to FreeIOHandlerOnDisconnect at Bas's instruction

  Rev 1.12    2/21/2003 06:54:36 PM  JPMugaas
  The FTP list processing has been restructured so that Directory output is not
  done by IdFTPList.  This now also uses the IdFTPListParserBase for parsing so
  that the code is more scalable.

  Rev 1.11    2/17/2003 04:45:36 PM  JPMugaas
  Now temporarily change the transfer mode to ASCII when requesting a DIR.
  TOPS20 does not like transfering dirs in binary mode and it might be a good
  idea to do it anyway.

  Rev 1.10    2/16/2003 03:22:20 PM  JPMugaas
  Removed the Data Connection assurance stuff.  We figure things out from the
  draft specificaiton, the only servers we found would not send any data after
  the new commands were sent, and there were only 2 server types that supported
  it anyway.

  Rev 1.9    2/16/2003 10:51:08 AM  JPMugaas
  Attempt to implement:

  http://www.ietf.org/internet-drafts/draft-ietf-ftpext-data-connection-assuranc
  e-00.txt

  Currently commented out because it does not work.

  Rev 1.8    2/14/2003 11:40:16 AM  JPMugaas
  Fixed compile error.

  Rev 1.7    2/14/2003 10:38:32 AM  JPMugaas
  Removed a problematic override for GetInternelResponse.  It was messing up
  processing of the FEAT.

  Rev 1.6    12-16-2002 20:48:10  BGooijen
  now uses TIdIOHandler.ConstructIOHandler to construct iohandlers
  IPv6 works again
  Independant of TIdIOHandlerStack again

  Rev 1.5    12-15-2002 23:27:26  BGooijen
  now compiles on Indy 10, but some things like IPVersion still need to be
  changed

  Rev 1.4    12/15/2002 04:07:02 PM  JPMugaas
  Started port to Indy 10.  Still can not complete it though.

  Rev 1.3    12/6/2002 05:29:38 PM  JPMugaas
  Now decend from TIdTCPClientCustom instead of TIdTCPClient.

  Rev 1.2    12/1/2002 04:18:02 PM  JPMugaas
  Moved all dir parsing code to one place.  Reworked to use more than one line
  for determining dir format type along with flfNextLine dir format type.

  Rev 1.1    11/14/2002 04:02:58 PM  JPMugaas
  Removed cludgy code that was a workaround for the RFC Reply limitation.  That
  is no longer limited.

  Rev 1.0    11/14/2002 02:20:00 PM  JPMugaas

2002-10-25 - J. Peter Mugaas
  - added XCRC support - specified by "GlobalSCAPE Secure FTP Server User’s Guide"
    which is available at http://www.globalscape.com
    and also explained at http://www.southrivertech.com/support/titanftp/webhelp/titanftp.htm
  - added COMB support - specified by "GlobalSCAPE Secure FTP Server User’s Guide"
    which is available at http://www.globalscape.com
    and also explained at http://www.southrivertech.com/support/titanftp/webhelp/titanftp.htm

2002-10-24 - J. Peter Mugaas
  - now supports RFC 2640 - FTP Internalization

2002-09-18
  _ added AFromBeginning parameter to InternalPut to correctly honor the AAppend parameter of Put

2002-09-05 - J. Peter Mugaas
  - now complies with RFC 2389 - Feature negotiation mechanism for the File Transfer Protocol
  - now complies with RFC 2428 - FTP Extensions for IPv6 and NATs

2002-08-27 - Andrew P.Rybin
  - proxy support fix (non-standard ftp port's)

2002-01-xx - Andrew P.Rybin
  - Proxy support, OnAfterGet (ex:decrypt, set srv timestamp)
  - J.Peter Mugaas: not readonly ProxySettings

  A Neillans - 10/17/2001
    Merged changes submitted by Andrew P.Rybin
    Correct command case problems - some servers expect commands in Uppercase only.

  SP - 06/08/2001
    Added a few more functions

  Doychin - 02/18/2001
    OnAfterLogin event handler and Login method
    OnAfterLogin is executed after successfull login  but before setting up the
      connection properties. This event can be used to provide FTP proxy support
      from the user application. Look at the FTP demo program for more information
      on how to provide such support.

  Doychin - 02/17/2001
    New onFTPStatus event
    New Quote method for executing commands not implemented by the compoent

-CleanDir contributed by Amedeo Lanza
}

unit IdFTP;

{
  TODO: Change the FTP demo to demonstrate the use of the new events and add proxy support
}

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdAssignedNumbers, IdGlobal, IdCustomTransparentProxy, IdExceptionCore,
  IdExplicitTLSClientServerBase, IdFTPCommon, IdFTPList, IdFTPListParseBase,
  IdException, IdIOHandler, IdIOHandlerSocket, IdReplyFTP, IdBaseComponent,
  IdReplyRFC, IdReply, IdSocketHandle, IdTCPConnection, IdTCPClient,
  IdThreadSafe, IdZLibCompressorBase;

type
  //APR 011216:
  TIdFtpProxyType = (
    fpcmNone,//Connect method:
    fpcmUserSite, //Send command USER user@hostname - USER after login (see: http://isservices.tcd.ie/internet/command_config.php)
    fpcmSite, //Send command SITE (with logon)
    fpcmOpen, //Send command OPEN
    fpcmUserPass,//USER user@firewalluser@hostname / PASS pass@firewallpass
    fpcmTransparent, //First use the USER and PASS command with the firewall username and password, and then with the target host username and password.
    fpcmUserHostFireWallID,  //USER hostuserId@hostname firewallUsername
    fpcmNovellBorder, //Novell BorderManager Proxy
    fpcmHttpProxyWithFtp, //HTTP Proxy with FTP support. Will be supported in Indy 10
    fpcmCustomProxy // use OnCustomFTPProxy to customize the proxy login
  ); //TIdFtpProxyType

  //This has to be in the same order as TLS_AUTH_NAMES
  TAuthCmd = (tAuto, tAuthTLS, tAuthSSL, tAuthTLSC, tAuthTLSP);

const
  Id_TIdFTP_TransferType = {ftBinary} ftASCII; // RLebeau 1/22/08: per RFC 959
  Id_TIdFTP_Passive = False;
  Id_TIdFTP_UseNATFastTrack = False;
  Id_TIdFTP_HostPortDelimiter = ':';
  Id_TIdFTP_DataConAssurance = False;
  Id_TIdFTP_DataPortProtection = ftpdpsClear;
  //
  DEF_Id_TIdFTP_Implicit = False;
  DEF_Id_FTP_UseExtendedDataPort = False;
  DEF_Id_TIdFTP_UseExtendedData = False;
  DEF_Id_TIdFTP_UseMIS = True;
  DEF_Id_FTP_UseCCC = False;
  DEF_Id_FTP_AUTH_CMD = tAuto;
  DEF_Id_FTP_ListenTimeout = 10000; // ten seconds
  {
Soem firewalls don't handle control connections properly during long data transfers.
They will timeout the control connection because it's idle and making it worse is that they
will chop off a connection instead of closing it causing TIdFTP to wait forever for nothing.

  }
  DEF_Id_FTP_READTIMEOUT = 60000; //one minute
  DEF_Id_FTP_UseHOST = True;
  DEF_Id_FTP_PassiveUseControlHost = False;
  DEF_Id_FTP_AutoIssueFEAT = True;
  DEF_Id_FTP_AutoLogin = True;

type
  //Added by SP
  TIdCreateFTPList = procedure(ASender: TObject; var VFTPList: TIdFTPListItems) of object;
  //TIdCheckListFormat = procedure(ASender: TObject; const ALine: String; var VListFormat: TIdFTPListFormat) of object;
  TOnAfterClientLogin = TNotifyEvent;
  TIdFtpAfterGet = procedure(ASender: TObject; AStream: TStream) of object; //APR
  TIdOnDataChannelCreate = procedure(ASender: TObject; ADataChannel: TIdTCPConnection) of object;
  TIdOnDataChannelDestroy = procedure(ASender: TObject; ADataChannel: TIdTCPConnection) of object;
  TIdNeedAccountEvent = procedure(ASender: TObject; var VAcct: string) of object;

  TIdFTPBannerEvent = procedure (ASender: TObject; const AMsg : String) of object;

  TIdFTPClientIdentifier = class (TPersistent)
  protected
    FClientName : String;
    FClientVersion : String;
    FPlatformDescription : String;
    procedure SetClientName(const AValue: String);
    procedure SetClientVersion(const AValue: String);
    procedure SetPlatformDescription(const AValue: String);
    function GetClntOutput: String;
  public
    procedure Assign(Source: TPersistent); override;
    property ClntOutput : String read GetClntOutput;
  published
    property ClientName : String read FClientName write SetClientName;
    property ClientVersion : String read FClientVersion write SetClientVersion;
    property PlatformDescription : String read FPlatformDescription write SetPlatformDescription;
  end;

  TIdFtpProxySettings = class (TPersistent)
  protected
    FHost, FUserName, FPassword: String;
    FProxyType: TIdFtpProxyType;
    FPort: TIdPort;
  public
    procedure Assign(Source: TPersistent); override;
  published
    property  ProxyType: TIdFtpProxyType read FProxyType write FProxyType;
    property  Host: String read FHost write FHost;
    property  UserName: String read FUserName write FUserName;
    property  Password: String read FPassword write FPassword;
    property  Port: TIdPort read FPort write FPort;
  end;

  TIdFTPTZInfo = class(TPersistent)
  protected
    FGMTOffset : TDateTime;
    FGMTOffsetAvailable : Boolean;
  public
    procedure Assign(Source: TPersistent); override;
  published
    property GMTOffset : TDateTime read FGMTOffset write FGMTOffset;
    property GMTOffsetAvailable : Boolean read FGMTOffsetAvailable write FGMTOffsetAvailable;
  end;

  TIdFTPKeepAlive = class(TPersistent)
  protected
    FUseKeepAlive: Boolean;
    FIdleTimeMS: Integer;
    FIntervalMS: Integer;
  public
    procedure Assign(Source: TPersistent); override;
  published
    property UseKeepAlive: Boolean read FUseKeepAlive write FUseKeepAlive;
    property IdleTimeMS: Integer read FIdleTimeMS write FIdleTimeMS;
    property IntervalMS: Integer read FIntervalMS write FIntervalMS;
  end;

  TIdFTP = class(TIdExplicitTLSClient)
  protected
    FAutoLogin: Boolean;
    FAutoIssueFEAT : Boolean;
    FCurrentTransferMode : TIdFTPTransferMode;
    FClientInfo : TIdFTPClientIdentifier;

    FDataSettingsSent: Boolean; // only send SSL data settings once per connection
    FUsingSFTP : Boolean; //enable SFTP internel flag
    FUsingCCC : Boolean; //are we using FTP with SSL on a clear control channel?
    FUseHOST: Boolean;
    FServerHOST: String;
    FCanUseMLS : Boolean; //can we use MLISx instead of LIST
    FUsingExtDataPort : Boolean; //are NAT Extensions (RFC 2428 available) flag
    FUsingNATFastTrack : Boolean;//are we using NAT fastrack feature
    FCanResume: Boolean;
    FListResult: TStrings;
    FLoginMsg: TIdReplyFTP;

    FPassive: Boolean;
    FPassiveUseControlHost: Boolean;

    FDataPortProtection : TIdFTPDataPortSecurity;
    FAUTHCmd : TAuthCmd;
    FDataPort: TIdPort;
    FDataPortMin: TIdPort;
    FDataPortMax: TIdPort;
    FDefStringEncoding: IIdTextEncoding;
    FExternalIP : String;
    FResumeTested: Boolean;
    FServerDesc: string;
    FSystemDesc: string;
    FTransferType: TIdFTPTransferType;
    FTransferTimeout : Integer;
    FListenTimeout : Integer;
    FDataChannel: TIdTCPConnection;
    FDirectoryListing: TIdFTPListItems;
    FDirFormat : String;
    FListParserClass : TIdFTPListParseClass;
    FOnAfterClientLogin: TNotifyEvent;
    FOnCreateFTPList: TIdCreateFTPList;
    FOnBeforeGet: TNotifyEvent;
    FOnBeforePut: TIdFtpAfterGet;
    //in case someone needs to do something special with the data being uploaded
    FOnAfterGet: TIdFtpAfterGet; //APR
    FOnAfterPut: TNotifyEvent; //JPM at Don Sider's suggestion
    FOnNeedAccount: TIdNeedAccountEvent;
    FOnCustomFTPProxy : TNotifyEvent;
    FOnDataChannelCreate: TIdOnDataChannelCreate;
    FOnDataChannelDestroy: TIdOnDataChannelDestroy;
    FProxySettings: TIdFtpProxySettings;

    FUseExtensionDataPort : Boolean;
    FTryNATFastTrack : Boolean;
    FUseMLIS : Boolean;
    FLangsSupported : TStrings;
    FUseCCC: Boolean;
    //is the SSCN Client method on for this connection?
    FSSCNOn : Boolean;
    FIsCompressionSupported : Boolean;

    FOnBannerBeforeLogin : TIdFTPBannerEvent;
    FOnBannerAfterLogin : TIdFTPBannerEvent;
    FOnBannerWarning : TIdFTPBannerEvent;

    FTZInfo : TIdFTPTZInfo;

    {$IFDEF USE_OBJECT_ARC}[Weak]{$ENDIF} FCompressor : TIdZLibCompressorBase;
    //ZLib settings
    FZLibCompressionLevel : Integer; //7
    FZLibWindowBits : Integer; //-15
    FZLibMemLevel : Integer; //8
    FZLibStratagy : Integer; //0 - default

    //dir events for some GUI programs.
    //The directory was Retrieved from the FTP server.
    FOnRetrievedDir : TNotifyEvent;
    //parsing is done only when DirectoryListing is referenced
    FOnDirParseStart : TNotifyEvent;
    FOnDirParseEnd : TNotifyEvent;

    //we probably need an Abort flag so we know when an abort is sent.
    //It turns out that one server will send a 550 or 451 error followed by an
    //ABOR successfull 
    FAbortFlag : TIdThreadSafeBoolean;

    FAccount: string;
    FNATKeepAlive: TIdFTPKeepAlive;
    //
    procedure DoOnDataChannelCreate;
    procedure DoOnDataChannelDestroy;

    procedure DoOnRetrievedDir;
    procedure DoOnDirParseStart;
    procedure DoOnDirParseEnd;

    procedure FinalizeDataOperation;
    procedure SetTZInfo(const Value: TIdFTPTZInfo);
    function IsSiteZONESupported : Boolean;
    function IndexOfFeatLine(const AFeatLine : String):Integer;
    procedure ClearSSCN;
    function SetSSCNToOn : Boolean;
    procedure SendInternalPassive(const ACmd : String; var VIP: string; var VPort: TIdPort);
    procedure SendCPassive(var VIP: string; var VPort: TIdPort);
    function FindAuthCmd : String;
    //
    function GetReplyClass: TIdReplyClass; override;
    //
    procedure ParseFTPList;
    procedure SetPassive(const AValue : Boolean);
    procedure SetTryNATFastTrack(const AValue: Boolean);
    procedure DoTryNATFastTrack;
    procedure SetUseExtensionDataPort(const AValue: Boolean);

    procedure SetIPVersion(const AValue: TIdIPVersion); override;
    procedure SetIOHandler(AValue: TIdIOHandler); override;
    function GetSupportsTLS: Boolean; override;

    procedure ConstructDirListing;
    procedure DoAfterLogin;
    procedure DoFTPList;
    procedure DoCustomFTPProxy;
    procedure DoOnBannerAfterLogin(AText : TStrings);
    procedure DoOnBannerBeforeLogin(AText : TStrings);
    procedure DoOnBannerWarning(AText : TStrings);
    procedure SendPBSZ; //protection buffer size
    procedure SendPROT; //data port protection
    procedure SendDataSettings; //this is for the extensions only;
//    procedure DoCheckListFormat(const ALine: String);
    function GetDirectoryListing: TIdFTPListItems;
//    function GetOnParseCustomListFormat: TIdOnParseCustomListFormat;
    procedure InitDataChannel;
    //PRET is to help distributed FTP systems by letting them know what you will do
    //before issuing a PASV.  See: http://drftpd.mog.se/wiki/wiki.phtml?title=Distributed_PASV#PRE_Transfer_Command_for_Distributed_PASV_Transfers
    //for a discussion.
    procedure SendPret(const ACommand : String);
    procedure InternalGet(const ACommand: string; ADest: TStream; AResume: Boolean = false);
    procedure InternalPut(const ACommand: string; ASource: TStream; AFromBeginning: Boolean = True; AResume: Boolean = False);
//    procedure SetOnParseCustomListFormat(const AValue: TIdOnParseCustomListFormat);
    procedure SendPassive(var VIP: string; var VPort: TIdPort);
    procedure SendPort(AHandle: TIdSocketHandle); overload;
    procedure SendPort(const AIP : String; const APort : TIdPort); overload;
    procedure ParseEPSV(const AReply : String; var VIP : String; var VPort : TIdPort);
    //These two are for RFC 2428.txt
    procedure SendEPort(AHandle: TIdSocketHandle); overload;
    procedure SendEPort(const AIP : String; const APort : TIdPort; const AIPVersion : TIdIPVersion); overload;
    procedure SendEPassive(var VIP: string; var VPort: TIdPort);
    function SendHost: Int16;
    procedure SetProxySettings(const Value: TIdFtpProxySettings);
    procedure SetClientInfo(const AValue: TIdFTPClientIdentifier);
    procedure SetCompressor(AValue: TIdZLibCompressorBase);
    procedure SendTransferType(AValue: TIdFTPTransferType);
    procedure SetTransferType(AValue: TIdFTPTransferType);
    procedure DoBeforeGet; virtual;
    procedure DoBeforePut(AStream: TStream); virtual;
    procedure DoAfterGet(AStream: TStream); virtual; //APR
    procedure DoAfterPut; virtual;
    class procedure FXPSetTransferPorts(AFromSite, AToSite: TIdFTP; const ATargetUsesPasv : Boolean);
    class procedure FXPSendFile(AFromSite, AToSite: TIdFTP; const ASourceFile, ADestFile: String);
    class function InternalEncryptedTLSFXP(AFromSite, AToSite: TIdFTP; const ASourceFile, ADestFile: String; const ATargetUsesPasv : Boolean) : Boolean;
    class function InternalUnencryptedFXP(AFromSite, AToSite: TIdFTP; const ASourceFile, ADestFile: String; const ATargetUsesPasv : Boolean): Boolean;
    class function ValidateInternalIsTLSFXP(AFromSite, AToSite: TIdFTP; const ATargetUsesPasv : Boolean): Boolean;
    procedure InitComponent; override;
    procedure SetUseTLS(AValue : TIdUseTLS); override;
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    procedure SetDataPortProtection(AValue : TIdFTPDataPortSecurity);
    procedure SetAUTHCmd(const AValue : TAuthCmd);
    procedure SetDefStringEncoding(AValue: IIdTextEncoding);
    procedure SetUseCCC(const AValue: Boolean);
    procedure SetNATKeepAlive(AValue: TIdFTPKeepAlive);
    procedure IssueFEAT;
    //specific server detection
    function IsOldServU: Boolean;
    function IsBPFTP : Boolean;
    function IsTitan : Boolean;
    function IsWSFTP : Boolean;
    function IsIIS: Boolean;
    function CheckAccount: Boolean;
    function IsAccountNeeded : Boolean;
    function GetSupportsVerification : Boolean;
  public
    procedure GetInternalResponse(AEncoding: IIdTextEncoding = nil); override;
    function CheckResponse(const AResponse: Int16; const AAllowedResponses: array of Int16): Int16; override;

    function IsExtSupported(const ACmd : String):Boolean;
    procedure ExtractFeatFacts(const ACmd : String; AResults : TStrings);
    //this function transparantly handles OTP based on the Last command response
    //so it needs to be called only after the USER command or equivilent.

    function GetLoginPassword : String; overload;
    function GetLoginPassword(const APrompt : String) : String; overload;
    procedure Abort; virtual;

    procedure Allocate(AAllocateBytes: Integer);
    procedure ChangeDir(const ADirName: string);
    procedure ChangeDirUp;
    procedure Connect; override;
    destructor Destroy; override;
    procedure Delete(const AFilename: string);
    procedure FileStructure(AStructure: TIdFTPDataStructure);
    procedure Get(const ASourceFile: string; ADest: TStream; AResume: Boolean = false); overload;
    procedure Get(const ASourceFile, ADestFile: string; const ACanOverwrite: boolean = false; AResume: Boolean = false); overload;
    procedure Help(AHelpContents: TStrings; ACommand: String = '');
    procedure KillDataChannel; virtual;
    //.NET Overload
    procedure List; overload;
    //.NET Overload
    procedure List(const ASpecifier: string; ADetails: Boolean = True); overload;
    procedure List(ADest: TStrings; const ASpecifier: string = ''; ADetails: Boolean = True); overload;
    procedure ExtListDir(ADest: TStrings = nil; const ADirectory: string = '');
    procedure ExtListItem(ADest: TStrings; AFList : TIdFTPListItems; const AItem: string='');  overload;
    procedure ExtListItem(ADest: TStrings; const AItem: string = ''); overload;
    procedure ExtListItem(AFList : TIdFTPListItems; const AItem : String= ''); overload;
    function  FileDate(const AFileName : String; const AsGMT : Boolean = False): TDateTime;

    procedure Login;
    procedure MakeDir(const ADirName: string);
    procedure Noop;
    procedure SetCmdOpt(const ACMD, AOptions : String);
    procedure Put(const ASource: TStream; const ADestFile: string;
      const AAppend: Boolean = False; const AStartPos: TIdStreamSize = -1); overload;
    procedure Put(const ASourceFile: string; const ADestFile: string = '';
      const AAppend: Boolean = False; const AStartPos: TIdStreamSize = -1); overload;

    procedure StoreUnique(const ASource: TStream; const AStartPos: TIdStreamSize = -1); overload;
    procedure StoreUnique(const ASourceFile: string; const AStartPos: TIdStreamSize = -1); overload;

    procedure SiteToSiteUpload(const AToSite : TIdFTP; const ASourceFile : String; const ADestFile : String = '');
    procedure SiteToSiteDownload(const AFromSite: TIdFTP; const ASourceFile : String; const ADestFile : String = '');
    procedure DisconnectNotifyPeer; override;
    procedure Quit; {$IFDEF HAS_DEPRECATED}deprecated{$IFDEF HAS_DEPECATED_MSG} 'Use Disconnect() instead'{$ENDIF};{$ENDIF}
    function  Quote(const ACommand: String): Int16;
    procedure RemoveDir(const ADirName: string);
    procedure Rename(const ASourceFile, ADestFile: string);
    function  ResumeSupported: Boolean;
    function  RetrieveCurrentDir: string;
    procedure Site(const ACommand: string);
    function  Size(const AFileName: String): Int64;
    procedure Status(AStatusList: TStrings);
    procedure StructureMount(APath: String);
    procedure TransferMode(ATransferMode: TIdFTPTransferMode);
    procedure ReInitialize(ADelay: UInt32 = 10);
    procedure SetLang(const ALangTag : String);
    function CRC(const AFIleName : String; const AStartPoint : Int64 = 0; const AEndPoint : Int64=0) : Int64;
    //verify file was uploaded, this is more comprehensive than the above
    function VerifyFile(ALocalFile : TStream; const ARemoteFile : String;
      const AStartPoint : TIdStreamSize = 0; const AByteCount : TIdStreamSize = 0) : Boolean; overload;
    function VerifyFile(const ALocalFile, ARemoteFile : String;
      const AStartPoint : TIdStreamSize = 0; const AByteCount : TIdStreamSize = 0) : Boolean; overload;
    //file parts must be in order in TStrings parameter
    //GlobalScape FTP Pro uses this for multipart simultanious file uploading
    procedure CombineFiles(const ATargetFile : String; AFileParts : TStrings);
    //Set modified file time.
    procedure SetModTime(const AFileName: String; const ALocalTime: TDateTime);
    procedure SetModTimeGMT(const AFileName : String; const AGMTTime: TDateTime);
    // servers that support MDTM yyyymmddhhmmss[+-xxx] and also support LIST -T
    //This is true for servers that are known to support these even if they aren't
    //listed in the FEAT reply.
    function IsServerMDTZAndListTForm : Boolean;
    property IsCompressionSupported : Boolean read FIsCompressionSupported;
    //
    property SupportsVerification : Boolean read GetSupportsVerification;
    property CanResume: Boolean read ResumeSupported;
    property CanUseMLS : Boolean read FCanUseMLS;
    property DirectoryListing: TIdFTPListItems read GetDirectoryListing;
    property DirFormat : String read FDirFormat;
    property LangsSupported : TStrings read FLangsSupported;
    property ListParserClass : TIdFTPListParseClass read FListParserClass write FListParserClass;
    property LoginMsg: TIdReplyFTP read FLoginMsg;
    property ListResult: TStrings read FListResult;
    property SystemDesc: string read FSystemDesc;
    property TZInfo : TIdFTPTZInfo read FTZInfo write SetTZInfo;
    property UsingExtDataPort : Boolean read FUsingExtDataPort;
    property UsingNATFastTrack : Boolean read FUsingNATFastTrack;
    property UsingSFTP : Boolean read FUsingSFTP;
    property CurrentTransferMode : TIdFTPTransferMode read FCurrentTransferMode write TransferMode;

  published
    {$IFDEF DOTNET}
      {$IFDEF DOTNET_2_OR_ABOVE}
    property IPVersion;
      {$ENDIF}
    {$ELSE}
    property IPVersion;
    {$ENDIF}
    property AutoIssueFEAT : Boolean read FAutoIssueFEAT write FAutoIssueFEAT default DEF_Id_FTP_AutoIssueFEAT;
    property AutoLogin: Boolean read FAutoLogin write FAutoLogin default DEF_Id_FTP_AutoLogin;
    // This is an object that can compress and decompress FTP Deflate encoding
    property Compressor : TIdZLibCompressorBase read FCompressor write SetCompressor;
    property Host;
    property UseCCC : Boolean read FUseCCC write SetUseCCC default DEF_Id_FTP_UseCCC;
    property Passive: boolean read FPassive write SetPassive default Id_TIdFTP_Passive;
    property PassiveUseControlHost: Boolean read FPassiveUseControlHost write FPassiveUseControlHost default DEF_Id_FTP_PassiveUseControlHost;
    property DataPortProtection : TIdFTPDataPortSecurity read FDataPortProtection write SetDataPortProtection default Id_TIdFTP_DataPortProtection;
    property AUTHCmd : TAuthCmd read FAUTHCmd write SetAUTHCmd default DEF_Id_FTP_AUTH_CMD;
    property ConnectTimeout;
    property DataPort: TIdPort read FDataPort write FDataPort default 0;
    property DataPortMin: TIdPort read FDataPortMin write FDataPortMin default 0;
    property DataPortMax: TIdPort read FDataPortMax write FDataPortMax default 0;
    property DefStringEncoding : IIdTextEncoding read FDefStringEncoding write SetDefStringEncoding;
    property ExternalIP : String read FExternalIP write FExternalIP;
    property Password;
    property TransferType: TIdFTPTransferType read FTransferType write SetTransferType default Id_TIdFTP_TransferType;
    property TransferTimeout: Integer read FTransferTimeout write FTransferTimeout default IdDefTimeout;
    property ListenTimeout : Integer read FListenTimeout write FListenTimeout default DEF_Id_FTP_ListenTimeout;
    property Username;
    property Port default IDPORT_FTP;
    property UseExtensionDataPort : Boolean read FUseExtensionDataPort write SetUseExtensionDataPort default DEF_Id_TIdFTP_UseExtendedData;
    property UseMLIS : Boolean read FUseMLIS write FUseMLIS default DEF_Id_TIdFTP_UseMIS;
    property TryNATFastTrack : Boolean read FTryNATFastTrack write SetTryNATFastTrack default Id_TIdFTP_UseNATFastTrack;
    property NATKeepAlive: TIdFTPKeepAlive read FNATKeepAlive write SetNATKeepAlive;
    property ProxySettings: TIdFtpProxySettings read FProxySettings write SetProxySettings;
    property Account: string read FAccount write FAccount;
    property ClientInfo : TIdFTPClientIdentifier read FClientInfo write SetClientInfo;
    property UseHOST: Boolean read FUseHOST write FUseHOST default DEF_Id_FTP_UseHOST;
    property ServerHOST: String read FServerHOST write FServerHOST;
    property UseTLS;
    property OnTLSNotAvailable;

    property OnBannerBeforeLogin : TIdFTPBannerEvent read FOnBannerBeforeLogin write FOnBannerBeforeLogin;
    property OnBannerAfterLogin : TIdFTPBannerEvent read FOnBannerAfterLogin write FOnBannerAfterLogin;
    property OnBannerWarning : TIdFTPBannerEvent read FOnBannerWarning write FOnBannerWarning;

    property OnAfterClientLogin: TOnAfterClientLogin read FOnAfterClientLogin write FOnAfterClientLogin;
    property OnCreateFTPList: TIdCreateFTPList read FOnCreateFTPList write FOnCreateFTPList;
    property OnAfterGet: TIdFtpAfterGet read FOnAfterGet write FOnAfterGet; //APR
    property OnAfterPut: TNotifyEvent read FOnAfterPut write FOnAfterPut;
    property OnNeedAccount: TIdNeedAccountEvent read FOnNeedAccount write FOnNeedAccount;
    property OnCustomFTPProxy : TNotifyEvent read FOnCustomFTPProxy write FOnCustomFTPProxy;
    property OnDataChannelCreate: TIdOnDataChannelCreate read FOnDataChannelCreate write FOnDataChannelCreate;
    property OnDataChannelDestroy: TIdOnDataChannelDestroy read FOnDataChannelDestroy write FOnDataChannelDestroy;
    //The directory was Retrieved from the FTP server.
    property OnRetrievedDir : TNotifyEvent read FOnRetrievedDir write FOnRetrievedDir;
    //parsing is done only when DirectoryLiusting is referenced
    property OnDirParseStart : TNotifyEvent read FOnDirParseStart write FOnDirParseStart;
    property OnDirParseEnd : TNotifyEvent read FOnDirParseEnd write FOnDirParseEnd;
    property ReadTimeout default DEF_Id_FTP_READTIMEOUT;
  end;

  EIdFTPException = class(EIdException);
  EIdFTPFileAlreadyExists = class(EIdFTPException);
  EIdFTPMustUseExtWithIPv6 = class(EIdFTPException);
  EIdFTPMustUseExtWithNATFastTrack = class(EIdFTPException);
  EIdFTPPassiveMustBeTrueWithNATFT = class(EIdFTPException);
  EIdFTPServerSentInvalidPort = class(EIdFTPException);
  EIdFTPSiteToSiteTransfer = class(EIdFTPException);
  EIdFTPSToSNATFastTrack = class(EIdFTPSiteToSiteTransfer);
  EIdFTPSToSNoDataProtection = class(EIdFTPSiteToSiteTransfer);
  EIdFTPSToSIPProtoMustBeSame = class(EIdFTPSiteToSiteTransfer);
  EIdFTPSToSBothMostSupportSSCN = class(EIdFTPSiteToSiteTransfer);
  EIdFTPSToSTransModesMustBeSame = class(EIdFTPSiteToSiteTransfer);
  EIdFTPOnCustomFTPProxyRequired = class(EIdFTPException);
  EIdFTPConnAssuranceFailure = class(EIdFTPException);
  EIdFTPWrongIOHandler = class(EIdFTPException);
  EIdFTPUploadFileNameCanNotBeEmpty = class(EIdFTPException);

  EIdFTPDataPortProtection = class(EIdFTPException);
  EIdFTPNoDataPortProtectionAfterCCC = class(EIdFTPDataPortProtection);
  EIdFTPNoDataPortProtectionWOEncryption = class(EIdFTPDataPortProtection);
  EIdFTPNoCCCWOEncryption = class(EIdFTPException);
  EIdFTPAUTHException = class(EIdFTPException);
  EIdFTPNoAUTHWOSSL = class(EIdFTPAUTHException);
  EIdFTPCanNotSetAUTHCon = class(EIdFTPAUTHException);

  EIdFTPMissingCompressor = class(EIdFTPException);
  EIdFTPCompressorNotReady = class(EIdFTPException);
  EIdFTPUnsupportedTransferMode = class(EIdFTPException);
  EIdFTPUnsupportedTransferType = class(EIdFTPException);

implementation

uses
  //facilitate inlining only.
  {$IFDEF KYLIXCOMPAT}
  Libc,
  {$ENDIF}
  {$IFDEF USE_VCL_POSIX}
  Posix.SysSelect,
  Posix.SysTime,
  Posix.Unistd,
  {$ENDIF}
  {$IFDEF WINDOWS}
  //facilitate inlining only.
  Windows,
  {$ENDIF}
  {$IFDEF DOTNET}
    {$IFDEF USE_INLINE}
  System.IO,
  System.Threading,
    {$ENDIF}
  {$ENDIF}
  IdComponent,
  IdFIPS,
  IdResourceStringsCore, IdIOHandlerStack, IdResourceStringsProtocols,
  IdSSL, IdGlobalProtocols, IdHash, IdHashCRC, IdHashSHA, IdHashMessageDigest,
  IdStack, IdStackConsts, IdSimpleServer, IdOTPCalculator, SysUtils;

const
  cIPVersions: array[TIdIPVersion] of String = ('1', '2'); {do not localize}

type
  TIdFTPListResult = class(TStringList)
  private
    FDetails: Boolean; //Did the developer use the NLST command for the last list command
    FUsedMLS : Boolean; //Did the developer use MLSx commands for the last list command
  public
    property Details: Boolean read FDetails;
    property UsedMLS: Boolean read FUsedMLS;
  end;

procedure TIdFTP.InitComponent;
begin
  inherited InitComponent;
  //
  FAutoLogin := DEF_Id_FTP_AutoLogin;
  FRegularProtPort := IdPORT_FTP;
  FImplicitTLSProtPort := IdPORT_ftps;
  //
  Port := IDPORT_FTP;
  Passive := Id_TIdFTP_Passive;
  FPassiveUseControlHost := DEF_Id_FTP_PassiveUseControlHost;

  FDataPortProtection := Id_TIdFTP_DataPortProtection;
  FUseCCC := DEF_Id_FTP_UseCCC;
  FAUTHCmd := DEF_Id_FTP_AUTH_CMD;
  FUseHOST := DEF_Id_FTP_UseHOST;

  FDataPort := 0;
  FDataPortMin := 0;
  FDataPortMax := 0;
  FDefStringEncoding := IndyTextEncoding_8Bit;
  FUseExtensionDataPort := DEF_Id_TIdFTP_UseExtendedData;
  FTryNATFastTrack := Id_TIdFTP_UseNATFastTrack;
  FTransferType := Id_TIdFTP_TransferType;
  FTransferTimeout := IdDefTimeout;
  FListenTimeout := DEF_Id_FTP_ListenTimeout;
  FLoginMsg := TIdReplyFTP.Create(nil);
  FListResult := TIdFTPListResult.Create;
  FLangsSupported := TStringList.Create;
  FCanResume := False;
  FResumeTested := False;
  FProxySettings:= TIdFtpProxySettings.Create; //APR
  FClientInfo := TIdFTPClientIdentifier.Create;
  FTZInfo := TIdFTPTZInfo.Create;
  FTZInfo.FGMTOffsetAvailable := False;
  FUseMLIS := DEF_Id_TIdFTP_UseMIS;
  FCanUseMLS := False; //initialize MLIS flags
  //Settings specified by
  // http://www.ietf.org/internet-drafts/draft-preston-ftpext-deflate-00.txt
  FZLibCompressionLevel :=  DEF_ZLIB_COMP_LEVEL;
  FZLibWindowBits := DEF_ZLIB_WINDOW_BITS; //-15 - no extra headers
  FZLibMemLevel := DEF_ZLIB_MEM_LEVEL;
  FZLibStratagy := DEF_ZLIB_STRATAGY; // - default
  //
  FAbortFlag := TIdThreadSafeBoolean.Create;
  FAbortFlag.Value := False;

  {
  Some firewalls don't handle control connections properly during long
  data transfers. They will timeout the control connection because it
  is idle and making it worse is that they will chop off a connection
  instead of closing it, causing TIdFTP to wait forever for nothing.
  }
  FNATKeepAlive := TIdFTPKeepAlive.Create;
  ReadTimeout := DEF_Id_FTP_READTIMEOUT;

  FAutoIssueFEAT := DEF_Id_FTP_AutoIssueFEAT;
end;

{$IFNDEF HAS_TryEncodeTime}
// TODO: move this to IdGlobal or IdGlobalProtocols...
function TryEncodeTime(Hour, Min, Sec, MSec: Word; out VTime: TDateTime): Boolean;
begin
  try
    VTime := EncodeTime(Hour, Min, Sec, MSec);
    Result := True;
  except
    Result := False;
  end;
end;
{$ENDIF}

{$IFNDEF HAS_TryStrToInt}
// TODO: use the implementation already in IdGlobalProtocols...
function TryStrToInt(const S: string; out Value: Integer): Boolean;
{$IFDEF USE_INLINE}inline;{$ENDIF}
var
  E: Integer;
begin
  Val(S, Value, E);
  Result := E = 0;
end;
{$ENDIF}

procedure TIdFTP.Connect;
var
  LHost: String;
  LPort: TIdPort;
  LBuf : String;
  LSendQuitOnError: Boolean;
  LOffs: Integer;
  LRetryWithoutHOST: Boolean;
begin
  LSendQuitOnError := False;

  FCurrentTransferMode := dmStream;
  FTZInfo.FGMTOffsetAvailable := False;
   //FSSCNOn should be set to false to prevent problems.
  FSSCNOn := False;
  FUsingSFTP := False;
  FUsingCCC := False;
  FDataSettingsSent := False;
  if FUseExtensionDataPort then begin
    FUsingExtDataPort := True;
  end;
  FUsingNATFastTrack := False;
  FCapabilities.Clear;

  try
    //APR 011216: proxy support
    LHost := FHost;
    LPort := FPort;
    try
      //I think  fpcmTransparent means to connect to the regular host and the firewalll
      //intercepts the login information.
      if (ProxySettings.ProxyType <> fpcmNone) and (ProxySettings.ProxyType <> fpcmTransparent) and
        (Length(ProxySettings.Host) > 0) then begin
        FHost := ProxySettings.Host;
        FPort := ProxySettings.Port;
      end;
      if FUseTLS = utUseImplicitTLS then begin
        //at this point, we treat implicit FTP as if it were explicit FTP with TLS
        FUsingSFTP := True;
      end;
      inherited Connect;
    finally
      FHost := LHost;
      FPort := LPort;
    end;

    // RLebeau: must not send/receive UTF-8 before negotiating for it...
    IOHandler.DefStringEncoding := FDefStringEncoding;
    {$IFDEF STRING_IS_ANSI}
    IOHandler.DefAnsiEncoding := IndyTextEncoding_OSDefault;
    {$ENDIF}

    // RLebeau: RFC 959 says that the greeting can be preceeded by a 1xx
    // reply and that the client should wait for the 220 reply when this 
    // happens.  Also, the RFC says that 120 should be used, but some
    // servers use other 1xx codes, such as 130, so handle 1xx generically

    // calling GetInternalResponse() directly to avoid duplicate calls
    // to CheckResponse() for the initial response if it is not 1xx
    GetInternalResponse;
    if (LastCmdResult.NumericCode div 100) = 1 then begin
      DoOnBannerWarning(LastCmdResult.FormattedReply);
      GetResponse(220);
    end else begin
      CheckResponse(LastCmdResult.NumericCode, [220]);
    end;

    LSendQuitOnError := True;

    FGreeting.Assign(LastCmdResult);
    // Save initial greeting for server identification in case FGreeting changes
    // in response to the HOST command
    if FGreeting.Text.Count > 0 then begin
      FServerDesc := FGreeting.Text[0];
    end else begin
      FServerDesc := '';
    end;
    // Implement HOST command as specified by
    // http://tools.ietf.org/html/draft-hethmon-mcmurray-ftp-hosts-01
    // Do not check the response for failures.  The draft suggests allowing
    // 220 (success) and 500/502 (unsupported), but vsftpd returns 530, and
    // whatever ftp.microsoft.com is running returns 504.
    if UseHOST then begin
      // RLebeau: WS_FTP Server 5.x disconnects if the command fails,
      // whereas WS_FTP Server 6+ does not.  If the server disconnected
      // here, let's mimic FTP Voyager by reconnecting without using
      // the HOST command again...
      //
      // RLebeau 11/18/2013: some other servers also disconnect on a failed
      // HOST command, so no longer retrying connect for WSFTP exclusively...
      //
      // RLebeau 11/22/2014: encountered one case where the server disconnects
      // before the reply is received.  So checking for that as well...
      //
      LRetryWithoutHOST := False;
      try
        if SendHost() <> 220 then begin
          IOHandler.CheckForDisconnect(True, True);
        end;
      except
        on E: EIdConnClosedGracefully do begin
          LRetryWithoutHOST := True;
        end;
        on E: EIdSocketError do begin
          if (E.LastError = Id_WSAECONNABORTED) or (E.LastError = Id_WSAECONNRESET) then begin
            LRetryWithoutHOST := True;
          end else begin
            raise;
          end;
        end;
      end;
      if LRetryWithoutHOST then
      begin
        Disconnect(False);
        if Assigned(IOHandler) then begin
          IOHandler.InputBuffer.Clear;
        end;
        UseHOST := False;
        try
          Connect;
        finally
          UseHOST := True;
        end;
        Exit;
      end;
    end else begin
      FGreeting.Assign(LastCmdResult);
    end;
    DoOnBannerBeforeLogin (FGreeting.FormattedReply);

    // RLebeau: having an AutoIssueFeat property doesn't make sense to
    // me.  There are commands below that require FEAT's response, but
    // if the user sets AutoIssueFeat to False, these commands will not
    // be allowed to execute!

    if AutoLogin then begin
      Login;
      DoAfterLogin;

      //Fast track is set only one time per connection and no more, even
      //with REINIT
      if TryNATFastTrack then begin
        DoTryNATFastTrack;
      end;

      if FUseTLS = utUseImplicitTLS then begin
        //at this point, we treat implicit FTP as if it were explicit FTP with TLS
        FUsingSFTP := True;
      end;

      // OpenVMS 7.1 replies with 200 instead of 215 - What does the RFC say about this?
      // if SendCmd('SYST', [200, 215, 500]) = 500 then begin  {do not localize}
      //Do not fault if SYST was not understood by the server.  Novel Netware FTP
      //may not understand SYST.
      if SendCmd('SYST') = 500 then begin  {do not localize}
        FSystemDesc := RSFTPUnknownHost;
      end else begin
        FSystemDesc := LastCmdResult.Text[0];
      end;

      if IsSiteZONESupported then begin
        if SendCmd('SITE ZONE') = 210 then begin {do not localize}
          if LastCmdResult.Text.Count > 0 then begin
            LBuf := LastCmdResult.Text[0];
            // some servers (Serv-U, etc) use a 'UTC' offset string, ie
            // "UTC-300", specifying the number of minutes from UTC.  Other
            // servers (Apache) use a GMT offset string instead, ie "-0300".
            if TextStartsWith(LBuf, 'UTC-') then begin {do not localize}
              // Titan FTP 6.26.634 incorrectly returns UTC-2147483647 when it's
              // first installed.
              FTZInfo.FGMTOffsetAvailable :=
                TryStrToInt(Copy(LBuf, 4, MaxInt), LOffs) and
                TryEncodeTime(Abs(LOffs) div 60, Abs(LOffs) mod 60, 0, 0, FTZInfo.FGMTOffset);
              if FTZInfo.FGMTOffsetAvailable and (LOffs < 0) then
                FTZInfo.FGMTOffset := -FTZInfo.FGMTOffset
            end else begin
              FTZInfo.FGMTOffsetAvailable := True;
              FTZInfo.GMTOffset := GmtOffsetStrToDateTime(LBuf);
            end;
          end;
        end;
      end;

      SendTransferType(FTransferType);
      DoStatus(ftpReady, [RSFTPStatusReady]);
    end else begin
      // OpenVMS 7.1 replies with 200 instead of 215 - What does the RFC say about this?
      // if SendCmd('SYST', [200, 215, 500]) = 500 then begin  {do not localize}
      //Do not fault if SYST was not understood by the server.  Novel Netware FTP
      //may not understand SYST.
      if SendCmd('SYST') = 500 then begin  {do not localize}
        FSystemDesc := RSFTPUnknownHost;
      end else begin
        FSystemDesc := LastCmdResult.Text[0];
      end;
      if FAutoIssueFEAT then begin
        IssueFEAT;
      end;
    end;
  except
    Disconnect(LSendQuitOnError); // RLebeau: do not send the QUIT command if the greeting was not received
    raise;
  end;
end;

function TIdFTP.SendHost: Int16;
var
  LHost: String;
begin
  LHost := FServerHOST;
  if LHost = '' then begin
    LHost := FHost;
  end;
  if Socket <> nil then begin
    if LHost = Socket.Binding.PeerIP then begin
      LHost := '[' + LHost + ']'; {do not localize}
    end;
  end;
  Result := SendCmd('HOST ' + LHost); {do not localize}
end;

procedure TIdFTP.SetTransferType(AValue: TIdFTPTransferType);
begin
  if AValue <> FTransferType then begin
    if not Assigned(FDataChannel) then begin
      if Connected then begin
        SendTransferType(AValue);
      end;
      FTransferType := AValue;
    end;
  end;
end;

procedure TIdFTP.SendTransferType(AValue: TIdFTPTransferType);
var
  s: string;
begin
  s := '';
  case AValue of
    ftAscii: s := 'A';      {do not localize}
    ftBinary: s := 'I';     {do not localize}
    else
      raise EIdFTPUnsupportedTransferType.Create(RSFTPUnsupportedTransferType);
  end;
  SendCmd('TYPE ' + s, 200); {do not localize}
end;

function TIdFTP.ResumeSupported: Boolean;
begin
  if not FResumeTested then begin
    FResumeTested := True;
    FCanResume := Quote('REST 1') = 350;   {do not localize}
    Quote('REST 0');  {do not localize}
  end;
  Result := FCanResume;
end;

procedure TIdFTP.Get(const ASourceFile: string; ADest: TStream; AResume: Boolean = False);
begin
  //for SSL FXP, we have to do it here because InternalGet is used by the LIST command
  //where SSCN is ignored.
  ClearSSCN;
  AResume := AResume and CanResume;
  DoBeforeGet;
  // RLebeau 7/26/06: do not do this! It breaks the ability to resume files
  // ADest.Position := 0;
  InternalGet('RETR ' + ASourceFile, ADest, AResume);
  DoAfterGet(ADest);
end;

procedure TIdFTP.Get(const ASourceFile, ADestFile: string; const ACanOverwrite: Boolean = False;
  AResume: Boolean = False);
var
  LDestStream: TStream;
begin
  AResume := AResume and CanResume;
  if ACanOverwrite and (not AResume) then begin
    SysUtils.DeleteFile(ADestFile);
    LDestStream := TIdFileCreateStream.Create(ADestFile);
  end
  else if (not ACanOverwrite) and AResume then begin
    LDestStream := TIdAppendFileStream.Create(ADestFile);
  end
  else if not FileExists(ADestFile) then begin
    LDestStream := TIdFileCreateStream.Create(ADestFile);
  end
  else begin
    raise EIdFTPFileAlreadyExists.Create(RSDestinationFileAlreadyExists);
  end;
  try
    Get(ASourceFile, LDestStream, AResume);
  finally
    FreeAndNil(LDestStream);
  end;
end;

procedure TIdFTP.DoBeforeGet;
begin
  if Assigned(FOnBeforeGet) then begin
    FOnBeforeGet(Self);
  end;
end;

procedure TIdFTP.DoBeforePut(AStream: TStream);
begin
  if Assigned(FOnBeforePut) then begin
    FOnBeforePut(Self, AStream);
  end;
end;

procedure TIdFTP.DoAfterGet(AStream: TStream);//APR
begin
  if Assigned(FOnAfterGet) then begin
    FOnAfterGet(Self, AStream);
  end;
end;

procedure TIdFTP.DoAfterPut;
begin
  if Assigned(FOnAfterPut) then begin
    FOnAfterPut(Self);
  end;
end;

procedure TIdFTP.ConstructDirListing;
begin
  if not Assigned(FDirectoryListing) then begin
    if not IsDesignTime then begin
      DoFTPList;
    end;
    if not Assigned(FDirectoryListing) then begin
      FDirectoryListing := TIdFTPListItems.Create;
    end;
  end else begin
    FDirectoryListing.Clear;
  end;
end;

procedure TIdFTP.List(ADest: TStrings; const ASpecifier: string = ''; ADetails: Boolean = True);      {do not localize}
var
  LDest: TMemoryStream;
  LTrans : TIdFTPTransferType;
begin
  if ADetails and UseMLIS and FCanUseMLS then begin
    ExtListDir(ADest, ASpecifier);
    Exit;
  end;
  // Note that for LIST, it might be best to put the connection in ASCII mode
  // because some old servers such as TOPS20 might require this.  We restore
  // it if the original mode was not ASCII.  It's a good idea to do this
  // anyway because some clients still do this such as WS_FTP Pro and
  // Microsoft's FTP Client.
  LTrans := TransferType;
  if LTrans <> ftASCII then begin
    Self.TransferType := ftASCII;
  end;
  try
    LDest := TMemoryStream.Create;
    try
      InternalGet(Trim(iif(ADetails, 'LIST', 'NLST') + ' ' + ASpecifier), LDest); {do not localize}
      FreeAndNil(FDirectoryListing);
      FDirFormat := '';
      LDest.Position := 0;
      FListResult.Text := ReadStringFromStream(LDest, -1, IOHandler.DefStringEncoding{$IFDEF STRING_IS_ANSI}, IOHandler.DefAnsiEncoding{$ENDIF});
      TIdFTPListResult(FListResult).FDetails := ADetails;
      TIdFTPListResult(FListResult).FUsedMLS := False;
      // FDirFormat will be updated in ParseFTPList...
    finally
      FreeAndNil(LDest);
    end;
    if ADest <> nil then begin
      ADest.Assign(FListResult);
    end;
    DoOnRetrievedDir;
  finally
    if LTrans <> ftASCII then begin
      TransferType := LTrans;
    end;
  end;
end;

const
  AbortedReplies : array [0..5] of Int16 =
                   (226,426, 450,451,425,550);
  //226 was added because one server will return that twice if you aborted
  //during an upload.
  AcceptableAbortReplies : array [0..8] of Int16 =
    (225, 226, 250, 426, 450,451,425,550,552);
  //GlobalScape Secure FTP Server returns a 552 for an aborted file
  
procedure TIdFTP.FinalizeDataOperation;
var
  LResponse : Int16;
begin
  DoOnDataChannelDestroy;
  if FDataChannel <> nil then begin
    {$IFNDEF USE_OBJECT_ARC}
    FDataChannel.IOHandler.Free;
    {$ENDIF}
    FDataChannel.IOHandler := nil;
    FreeAndNil(FDataChannel);
  end;
  {
This is a bug fix for servers will do something like this:

[2] Mon 06Jun05 13:33:28 - (000007) PASV
[6] Mon 06Jun05 13:33:28 - (000007) 227 Entering Passive Mode (192,168,1,107,4,22)
[2] Mon 06Jun05 13:33:28 - (000007) RETR test.txt.txt
[6] Mon 06Jun05 13:33:28 - (000007) 550 /test.txt.txt: No such file or directory.
[2] Mon 06Jun05 13:34:28 - (000007) QUIT
[6] Mon 06Jun05 13:34:28 - (000007) 221 Goodbye!
[5] Mon 06Jun05 13:34:28 - (000007) Closing connection for user TEST (00:01:08 connected)
  }
  if (LastCmdResult.NumericCode div 100) > 2 then
  begin
    DoStatus(ftpAborted, [RSFTPStatusAbortTransfer]);
    Exit;
  end;
  DoStatus(ftpReady, [RSFTPStatusDoneTransfer]);
  // 226 = download successful, 225 = Abort successful}
  if FAbortFlag.Value then begin
    LResponse := GetResponse(AcceptableAbortReplies);
//Experimental -
    if PosInSmallIntArray(LResponse,AbortedReplies) > -1 then begin
      GetResponse([226, 225]);
    end;
//IMPORTANT!!!  KEEP THIS COMMENT!!!
//
//This is a workaround for a problem.  When uploading a file on
//one FTP server and aborting that upload, I got this:
//
//Sent 3/9/2005 10:34:58 AM: STOR --------
//Recv 3/9/2005 10:34:58 AM: 150 Opening BINARY mode data connection for [3513]Red_Glas.zip
//Sent 3/9/2005 10:34:59 AM: ABOR
//Recv 3/9/2005 10:35:00 AM: 226 Transfer complete.
//Recv 3/9/2005 10:35:00 AM: 226 Abort successful
//
//but at ftp.ipswitch.com (a WS_FTP Server 5.0.4 (2555009845) server ),
//I was getting this when aborting a download
//
//Sent 3/9/2005 12:43:41 AM: RETR imail6.pdf
//Recv 3/9/2005 12:43:41 AM: 150 Opening BINARY data connection for imail6.pdf (2150082 bytes)
//Sent 3/9/2005 12:43:42 AM: ABOR
//Recv 3/9/2005 12:43:42 AM: 226 abort successful
//Recv 3/9/2005 12:43:43 AM: 425 transfer canceled
//
    if LResponse = 226 then begin
      if IOHandler.Readable(10) then begin
        GetResponse(AbortedReplies);
      end;
    end;
    DoStatus(ftpAborted, [RSFTPStatusAbortTransfer]);
//end experimental section
  end else begin
    //ftp.marist.edu returns 250
    GetResponse([226, 225, 250]);
  end;
end;

procedure TIdFTP.InternalPut(const ACommand: string; ASource: TStream;
  AFromBeginning: Boolean = True; AResume: Boolean = False);
    {$IFNDEF MSWINDOWS}
    procedure WriteStreamFromBeginning;
    var
      LBuffer: TIdBytes;
      LBufSize: Integer;
    begin
      // Copy entire stream without relying on ASource.Size so Unix-to-DOS
      // conversion can be done on the fly.
      BeginWork(wmWrite, ASource.Size);
      try
        SetLength(LBuffer, FDataChannel.IOHandler.SendBufferSize);
        while True do begin
          LBufSize := ASource.Read(LBuffer[0], Length(LBuffer));
          if LBufSize > 0 then
            FDataChannel.IOHandler.Write(LBuffer, LBufSize)
          else
            Break;
        end;
      finally
        EndWork(wmWrite);
      end;
    end;
    {$ENDIF}
var
  LIP: string;
  LPort: TIdPort;
  LPasvCl : TIdTCPClient;
  LPortSv : TIdSimpleServer;
  // under ARC, convert a weak reference to a strong reference before working with it
  LCompressor : TIdZLibCompressorBase;
begin
  FAbortFlag.Value := False;
  LCompressor := nil;

  if FCurrentTransferMode = dmDeflate then begin
    LCompressor := FCompressor;
    if not Assigned(LCompressor) then begin
      raise EIdFTPMissingCompressor.Create(RSFTPMissingCompressor);
    end;
    if not LCompressor.IsReady then begin
      raise EIdFTPCompressorNotReady.Create(RSFTPCompressorNotReady);
    end;
  end;

  //for SSL FXP, we have to do it here because there is no command were a client
  //submits data through a data port where the SSCN setting is ignored.
  ClearSSCN;
  DoStatus(ftpTransfer, [RSFTPStatusStartTransfer]);
  // try
    if FPassive then begin
      SendPret(ACommand);
      if FUsingExtDataPort then begin
        SendEPassive(LIP, LPort);
      end else begin
        SendPassive(LIP, LPort);
      end;
      if AResume then begin
        Self.SendCmd('REST ' + IntToStr(ASource.Position), [350]);   {do not localize}
      end;
      IOHandler.WriteLn(ACommand);

      if Socket <> nil then begin
        FDataChannel := TIdTCPClient.Create(nil);
      end else begin
        FDataChannel := nil;
      end;

      LPasvCl := TIdTCPClient(FDataChannel);
      try
        InitDataChannel;

        if (Self.Socket <> nil) and PassiveUseControlHost then begin
          //Do not use an assignment from Self.Host
          //because a DNS name may not resolve to the same
          //IP address every time.  This is the case where
          //the workload is distributed around several servers.
          //Besides, we already know the Peer's IP address so
          //why waste time querying it.
          LIP := Self.Socket.Binding.PeerIP;
        end;

        if LPasvCl <> nil then begin
          LPasvCl.Host := LIP;
          LPasvCl.Port := LPort;

          DoOnDataChannelCreate;

          LPasvCl.Connect;
        end;
        try
          Self.GetResponse([110, 125, 150]);
          try
            if FDataChannel <> nil then begin
              if FUsingSFTP and (FDataPortProtection = ftpdpsPrivate) then begin
                TIdSSLIOHandlerSocketBase(FDataChannel.IOHandler).Passthrough := False;
              end;
              if Assigned(LCompressor) then begin
                LCompressor.CompressFTPToIO(ASource, FDataChannel.IOHandler,
                  FZLibCompressionLevel, FZLibWindowBits, FZLibMemLevel, FZLibStratagy);
              end else begin
                if AFromBeginning then begin
                  {$IFNDEF MSWINDOWS}
                  WriteStreamFromBeginning;
                  {$ELSE}
                  FDataChannel.IOHandler.Write(ASource, 0, False);  // from beginning
                  {$ENDIF}
                end else begin
                  FDataChannel.IOHandler.Write(ASource, -1, False); // from current position
                end;
              end;
            end;
          except
            on E: EIdSocketError do
            begin
              // If 10038 - abort was called. Server will return 225
              if E.LastError <> 10038 then begin
                raise;
              end;
            end;
          end;
        finally
          if LPasvCl <> nil then begin
            LPasvCl.Disconnect(False);
          end;
        end;
      finally
        FinalizeDataOperation;
      end;
    end else begin
      if Socket <> nil then begin
        FDataChannel := TIdSimpleServer.Create(nil);
      end else begin
        FDataChannel := nil;
      end;

      LPortSv := TIdSimpleServer(FDataChannel);
      try
        InitDataChannel;

        if LPortSv <> nil then begin
          LPortSv.BoundIP := Self.Socket.Binding.IP;
          LPortSv.BoundPort := FDataPort;
          LPortSv.BoundPortMin := FDataPortMin;
          LPortSv.BoundPortMax := FDataPortMax;

          DoOnDataChannelCreate;

          LPortSv.BeginListen;
          if FUsingExtDataPort then begin
            SendEPort(LPortSv.Binding);
          end else begin
            SendPort(LPortSv.Binding);
          end;
        end else begin
          // TODO:
          {
          if FUsingExtDataPort then begin
            SendEPort(?);
          end else begin
            SendPort(?);
          end;
          }
        end;

        if AResume then begin
          Self.SendCmd('REST ' + IntToStr(ASource.Position), [350]);   {do not localize}
        end;
        Self.SendCmd(ACommand, [125, 150]);

        if LPortSv <> nil then begin
          LPortSv.Listen(ListenTimeout);
          if FUsingSFTP and (FDataPortProtection = ftpdpsPrivate) then begin
            TIdSSLIOHandlerSocketBase(FDataChannel.IOHandler).PassThrough := False;
          end;
          if Assigned(LCompressor) then begin
            LCompressor.CompressFTPToIO(ASource, FDataChannel.IOHandler,
              FZLibCompressionLevel, FZLibWindowBits, FZLibMemLevel, FZLibStratagy);
          end else begin
            if AFromBeginning then begin
              {$IFNDEF MSWINDOWS}
              WriteStreamFromBeginning;
              {$ELSE}
              FDataChannel.IOHandler.Write(ASource, 0, False);  // from beginning
              {$ENDIF}
            end else begin
              FDataChannel.IOHandler.Write(ASource, -1, False); // from current position
            end;
          end;
        end;
      finally
        FinalizeDataOperation;
      end;
    end;
  { This will silently ignore the STOR request if the server has forcibly disconnected 
    (kicked or timed out) before the request starts
  except
    //Note that you are likely to get an exception you abort a transfer
    //hopefully, this will make things work better.
    on E: EIdConnClosedGracefully do begin
    end;
  end;}

{ commented out because we might need to revert back to this
  if new code fails.
  if (LResponse = 426) or (LResponse = 450) then
  begin
    // some servers respond with 226 on ABOR
    GetResponse([226, 225]);
    DoStatus(ftpAborted, [RSFTPStatusAbortTransfer]);
  end;
  }
end;


procedure TIdFTP.InternalGet(const ACommand: string; ADest: TStream; AResume: Boolean = false);
var
  LIP: string;
  LPort: TIdPort;
  LPasvCl : TIdTCPClient;
  LPortSv : TIdSimpleServer;
  // under ARC, convert a weak reference to a strong reference before working with it
  LCompressor: TIdZLibCompressorBase;
begin
  FAbortFlag.Value := False;
  LCompressor := nil;

  if FCurrentTransferMode = dmDeflate then begin
    LCompressor := FCompressor;
    if not Assigned(LCompressor) then begin
      raise EIdFTPMissingCompressor.Create(RSFTPMissingCompressor);
    end;
    if not LCompressor.IsReady then begin
      raise EIdFTPCompressorNotReady.Create(RSFTPCompressorNotReady);
    end;
  end;

  DoStatus(ftpTransfer, [RSFTPStatusStartTransfer]);
  if FPassive then begin
    SendPret(ACommand);
    //PASV or EPSV
    if FUsingExtDataPort then begin
      SendEPassive(LIP, LPort);
    end else begin
      SendPassive(LIP, LPort);
    end;

    if Socket <> nil then begin
      FDataChannel := TIdTCPClient.Create(nil);
    end else begin
      FDataChannel := nil;
    end;

    LPasvCl := TIdTCPClient(FDataChannel);
    try
      InitDataChannel;

      if (Self.Socket <> nil) and PassiveUseControlHost then begin
        //Do not use an assignment from Self.Host
        //because a DNS name may not resolve to the same
        //IP address every time.  This is the case where
        //the workload is distributed around several servers.
        //Besides, we already know the Peer's IP address so
        //why waste time querying it.
        LIP := Self.Socket.Binding.PeerIP;
      end;

      if LPasvCl <> nil then begin
        LPasvCl.Host := LIP;
        LPasvCl.Port := LPort;

        DoOnDataChannelCreate;

        LPasvCl.Connect;
      end;
      try
        if AResume then begin
          Self.SendCmd('REST ' + IntToStr(ADest.Position), [350]);   {do not localize}
        end;
        // APR: Ericsson Switch FTP
        //
        // RLebeau: some servers send 450 when no files are
        // present, so do not read the stream in that case
        if Self.SendCmd(ACommand, [125, 150, 154, 450]) <> 450 then
        begin
          if LPasvCl <> nil then begin
            if FUsingSFTP and (FDataPortProtection = ftpdpsPrivate) then begin
              TIdSSLIOHandlerSocketBase(FDataChannel.IOHandler).Passthrough := False;
            end;
            if Assigned(LCompressor) then begin
              LCompressor.DecompressFTPFromIO(LPasvCl.IOHandler, ADest, FZLibWindowBits);
            end else begin
              LPasvCl.IOHandler.ReadStream(ADest, -1, True);
            end;
          end;
        end;
      finally
        if LPasvCl <> nil then begin
          LPasvCl.Disconnect(False);
        end;
      end;
    finally
      FinalizeDataOperation;
    end;
  end else begin
    // PORT or EPRT
    if Socket <> nil then begin
      FDataChannel := TIdSimpleServer.Create(nil);
    end else begin
      FDataChannel := nil;
    end;

    LPortSv := TIdSimpleServer(FDataChannel);
    try
      InitDataChannel;

      if LPortSv <> nil then begin
        LPortSv.BoundIP := Self.Socket.Binding.IP;
        LPortSv.BoundPort := FDataPort;
        LPortSv.BoundPortMin := FDataPortMin;
        LPortSv.BoundPortMax := FDataPortMax;

        DoOnDataChannelCreate;

        LPortSv.BeginListen;
        if FUsingExtDataPort then begin
          SendEPort(LPortSv.Binding);
        end else begin
          SendPort(LPortSv.Binding);
        end;
      end else begin
        // TODO:
        {
        if FUsingExtDataPort then begin
          SendEPort(?);
        end else begin
          SendPort(?);
        end;
        }
      end;

      if AResume then begin
        SendCmd('REST ' + IntToStr(ADest.Position), [350]);  {do not localize}
      end;
      SendCmd(ACommand, [125, 150, 154]); //APR: Ericsson Switch FTP);

      if LPortSv <> nil then begin
        LPortSv.Listen(ListenTimeout);
        if FUsingSFTP and (FDataPortProtection = ftpdpsPrivate) then begin
          TIdSSLIOHandlerSocketBase(FDataChannel.IOHandler).PassThrough := False;
        end;
        if Assigned(LCompressor) then begin
          LCompressor.DecompressFTPFromIO(LPortSv.IOHandler, ADest, FZLibWindowBits);
        end else begin
          FDataChannel.IOHandler.ReadStream(ADest, -1, True);
        end;
      end;
    finally
      FinalizeDataOperation;
    end;
  end;

  // ToDo: Change that to properly handle response code (not just success or except)
  // 226 = download successful, 225 = Abort successful}
  //commented out in case we need to revert back to this.
{  LResponse := GetResponse([225, 226, 250, 426, 450]);
  if (LResponse = 426) or (LResponse = 450) then begin
    GetResponse([226, 225]);
    DoStatus(ftpAborted, [RSFTPStatusAbortTransfer]);
  end; }
end;

procedure TIdFTP.DoOnDataChannelCreate;
begin
  // While the Control Channel is idle, Enable/disable TCP/IP keepalives.
  // They're very small (40-byte) packages and will be sent every
  // NATKeepAlive.IntervalMS after the connection has been idle for
  // NATKeepAlive.IdleTimeMS.  Prior to Windows 2000, the idle and
  // timeout values are system wide and have to be set in the registry;
  // the default is idle = 2 hours, interval = 1 second.
  if (Socket <> nil) and NATKeepAlive.UseKeepAlive then begin
    Socket.Binding.SetKeepAliveValues(True, NATKeepAlive.IdleTimeMS, NATKeepAlive.IntervalMS);
  end;
  if Assigned(FOnDataChannelCreate) then begin
    OnDataChannelCreate(Self, FDataChannel);
  end;
end;

procedure TIdFTP.DoOnDataChannelDestroy;
begin
  if Assigned(FOnDataChannelDestroy) then begin
    OnDataChannelDestroy(Self, FDataChannel);
  end;
  if (Socket <> nil) and NATKeepAlive.UseKeepAlive then begin
    Socket.Binding.SetKeepAliveValues(False, 0, 0);
  end;
end;

procedure TIdFTP.SetNATKeepAlive(AValue: TIdFTPKeepAlive);
begin
  FNATKeepAlive.Assign(AValue);
end;

{ TIdFtpKeepAlive }

procedure TIdFtpKeepAlive.Assign(Source: TPersistent);
var
  LSource: TIdFTPKeepAlive;
begin
  if Source is TIdFTPKeepAlive then begin
    LSource := TIdFTPKeepAlive(Source);
    FUseKeepAlive := LSource.UseKeepAlive;
    FIdleTimeMS := LSource.IdleTimeMS;
    FIntervalMS := LSource.IntervalMS;
  end else begin
    inherited Assign(Source);
  end;
end;

procedure TIdFTP.DisconnectNotifyPeer;
begin
  if IOHandler.Connected then begin
    IOHandler.WriteLn('QUIT');      {do not localize}
    IOHandler.CheckForDataOnSource(100);
    if not IOHandler.InputBufferIsEmpty then begin
      GetInternalResponse;
    end;
  end;
end;

{$I IdDeprecatedImplBugOff.inc}
procedure TIdFTP.Quit;
{$I IdDeprecatedImplBugOn.inc}
begin
  Disconnect;
end;

procedure TIdFTP.KillDataChannel;
begin
  // Had kill the data channel ()
  if Assigned(FDataChannel) then begin
    FDataChannel.Disconnect(False); //FDataChannel.IOHandler.DisconnectSocket;  {//BGO}
  end;
end;

// IMPORTANT!!! THis is for later reference.
//
// Note that we do not send the Telnet IP and Sync as suggestedc by RFC 959.
// We do not do so because some servers will mistakenly assume that the sequences
// are part of the command and than give a syntax error.
// I noticed this with FTPSERVE IBM VM Level 510, Microsoft FTP Service (Version 5.0),
// GlobalSCAPE Secure FTP Server (v. 2.0), and Pure-FTPd [privsep] [TLS].
//
// Thus, I feel that sending sequences is just going to aggravate this situation.
// It is probably the reason why some FTP clients no longer are sending Telnet IP
// and Sync with the ABOR command.
procedure TIdFTP.Abort;
begin
  // only send the abort command. The Data channel is supposed to disconnect
  if Connected then begin
    IOHandler.WriteLn('ABOR');                   {do not localize}
  end;
  // Kill the data channel: usually, the server doesn't close it by itself
  KillDataChannel;
  if Assigned(FDataChannel) then begin
    FAbortFlag.Value := True;
  end else begin
    GetResponse([]);
  end;
end;

procedure TIdFTP.SendPort(AHandle: TIdSocketHandle);
begin
  if FExternalIP <> '' then begin
    SendPort(FExternalIP, AHandle.Port);
  end else begin
    SendPort(AHandle.IP, AHandle.Port);
  end;
end;

procedure TIdFTP.SendPort(const AIP: String; const APort: TIdPort);
begin
  SendDataSettings;
  SendCmd('PORT ' + ReplaceAll(AIP, '.', ',')   {do not localize}
    + ',' + IntToStr(APort div 256) + ',' + IntToStr(APort mod 256), [200]); {do not localize}
end;

procedure TIdFTP.InitDataChannel;
var
  LSSL : TIdSSLIOHandlerSocketBase;
begin
  if FDataChannel = nil then begin
    Exit;
  end;
  if FDataPortProtection = ftpdpsPrivate then begin
    LSSL := TIdSSLIOHandlerSocketBase(IOHandler);
    FDataChannel.IOHandler := LSSL.Clone;
    //we have to delay the actual negotiation until we get the reply and
    //and just before the readString
    TIdSSLIOHandlerSocketBase(FDataChannel.IOHandler).Passthrough := True;
  end else begin
    FDataChannel.IOHandler := TIdIOHandler.MakeDefaultIOHandler(Self);
  end;
  if FDataChannel is TIdTCPClient then
  begin
    TIdTCPClient(FDataChannel).IPVersion := IPVersion;
    TIdTCPClient(FDataChannel).ReadTimeout := FTransferTimeout;
    //Now SocksInfo are multi-thread safe
    FDataChannel.IOHandler.ConnectTimeout := IOHandler.ConnectTimeout;
  end
  else if FDataChannel is TIdSimpleServer then
  begin
    TIdSimpleServer(FDataChannel).IPVersion := IPVersion;
  end;
  if Assigned(FDataChannel.Socket) and Assigned(Socket) then
  begin
    FDataChannel.Socket.TransparentProxy := Socket.TransparentProxy;
  end;
  FDataChannel.IOHandler.ReadTimeout := FTransferTimeout;
  FDataChannel.IOHandler.SendBufferSize := IOHandler.SendBufferSize;
  FDataChannel.IOHandler.RecvBufferSize := IOHandler.RecvBufferSize;
  FDataChannel.IOHandler.LargeStream := True;
 // FDataChannel.IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
 // FDataChannel.IOHandler.DefAnsiEncoding := IndyTextEncoding_OSDefault;
  FDataChannel.WorkTarget := Self;
end;

procedure TIdFTP.Put(const ASource: TStream; const ADestFile: string;
  const AAppend: Boolean = False; const AStartPos: TIdStreamSize = -1);
begin
  if ADestFile = '' then begin
    raise EIdFTPUploadFileNameCanNotBeEmpty.Create(RSFTPFileNameCanNotBeEmpty);
  end;
  if AStartPos > -1 then begin
    ASource.Position := AStartPos;
  end;
  DoBeforePut(ASource); //APR);
  if AAppend then begin
    InternalPut('APPE ' + ADestFile, ASource, False, False);  {Do not localize}
  end else begin
    InternalPut('STOR ' + ADestFile, ASource, AStartPos = -1, AStartPos > -1);  {Do not localize}
  end;
  DoAfterPut;
end;

procedure TIdFTP.Put(const ASourceFile: string; const ADestFile: string = '';
  const AAppend: Boolean = False; const AStartPos: TIdStreamSize = -1);
var
  LSourceStream: TStream;
  LDestFileName : String;
begin
  LDestFileName := ADestFile;
  if LDestFileName = '' then begin
    LDestFileName := ExtractFileName(ASourceFile);
  end;
  LSourceStream := TIdReadFileNonExclusiveStream.Create(ASourceFile);
  try
    Put(LSourceStream, LDestFileName, AAppend, AStartPos);
  finally
    FreeAndNil(LSourceStream);
  end;
end;

procedure TIdFTP.StoreUnique(const ASource: TStream; const AStartPos: TIdStreamSize = -1);
begin
  if AStartPos > -1 then begin
    ASource.Position := AStartPos;
  end;
  DoBeforePut(ASource);
  InternalPut('STOU', ASource, AStartPos = -1, False);  {Do not localize}
  DoAfterPut;
end;

procedure TIdFTP.StoreUnique(const ASourceFile: string; const AStartPos: TIdStreamSize = -1);
var
  LSourceStream: TStream;
begin
  LSourceStream := TIdReadFileExclusiveStream.Create(ASourceFile);
  try
    StoreUnique(LSourceStream, AStartPos);
  finally
    FreeAndNil(LSourceStream);
  end;
end;

procedure TIdFTP.SendInternalPassive(const ACmd: String; var VIP: string;
  var VPort: TIdPort);

  function IsRoutableAddress(AIP: string): Boolean;
  begin
  Result := not TextStartsWith(AIP, '127') and              // Loopback   127.0.0.0-127.255.255.255
     not TextStartsWith(AIP, '10.') and                     // Private    10.0.0.0-10.255.255.255
     not TextStartsWith(AIP, '169.254') and                 // Link-local 169.254.0.0-169.254.255.255
     not TextStartsWith(AIP, '192.168') and                 // Private    192.168.0.0-192.168.255.255
     not (TextStartsWith(AIP, '172') and (AIP[7] = '.') and // Private    172.16.0.0-172.31.255.255
      (IndyStrToInt(Copy(AIP, 5, 2)) in [16..31]))
  end;

var
  i, bLeft, bRight: integer;
  s: string;
begin
  SendDataSettings;
  SendCmd(ACmd, 227);      {do not localize}
  s := Trim(LastCmdResult.Text[0]);
  // Case 1 (Normal)
  // 227 Entering passive mode(100,1,1,1,23,45)
  bLeft := IndyPos('(', s);   {do not localize}
  bRight := IndyPos(')', s);  {do not localize}
  // Microsoft FTP Service may include a leading ( but not a trailing ),
  // so handle any combination of "(..)", "(..", "..)", and ".."
  if bLeft = 0 then bLeft := RPos(#32, S);
  if bRight = 0 then bRight := Length(S) + 1;
  S := Copy(S, bLeft + 1, bRight - bLeft - 1);
  VIP := '';                 {do not localize}
  for i := 1 to 4 do begin
    VIP := VIP + '.' + Fetch(s, ','); {do not localize}
  end;
  IdDelete(VIP, 1, 1);
  // Server sent an unroutable address (private/reserved/etc).  Use the IP we
  // connected to instead
  if not IsRoutableAddress(VIP) and IsRoutableAddress(Socket.Binding.PeerIP) then begin
    VIP := Socket.Binding.PeerIP;
  end;
  // Determine port
  VPort := TIdPort(IndyStrToInt(Fetch(s, ',')) and $FF) shl 8;   {do not localize}
  //use trim as one server sends something like this:
  //"227 Passive mode OK (195,92,195,164,4,99 )"
  VPort := VPort or TIdPort(IndyStrToInt(Fetch(s, ',')) and $FF); {Do not translate}
end;

procedure TIdFTP.SendPassive(var VIP: string; var VPort: TIdPort);
begin
  SendInternalPassive('PASV', VIP, VPort); {do not localize}
end;

procedure TIdFTP.SendCPassive(var VIP: string; var VPort: TIdPort);
begin
  SendInternalPassive('CPSV', VIP, VPort); {do not localize}
end;

procedure TIdFTP.Noop;
begin
  SendCmd('NOOP', 200); {do not localize}
end;

procedure TIdFTP.MakeDir(const ADirName: string);
begin
  SendCmd('MKD ' + ADirName, 257); {do not localize}
end;

function TIdFTP.RetrieveCurrentDir: string;
begin
  SendCmd('PWD', 257); {do not localize}
  Result := LastCmdResult.Text[0];
  IdDelete(Result, 1, IndyPos('"', Result)); // Remove first doublequote                             {do not localize}
  Result := Copy(Result, 1, IndyPos('"', Result) - 1); // Remove anything from second doublequote  {do not localize}                               // to end of line
  // TODO: handle embedded quotation marks.  RFC 959 allows them to be present
end;

procedure TIdFTP.RemoveDir(const ADirName: string);
begin
  SendCmd('RMD ' + ADirName, 250); {do not localize}
end;

procedure TIdFTP.Delete(const AFilename: string);
begin
  // Linksys NSLU2 NAS returns 200, Ultimodule IDAL returns 257
  SendCmd('DELE ' + AFilename, [200, 250, 257]); {do not localize}
end;

(*
CHANGE WORKING DIRECTORY (CWD)

  This command allows the user to work with a different
  directory or dataset for file storage or retrieval without
  altering his login or accounting information.  Transfer
  parameters are similarly unchanged.  The argument is a
  pathname specifying a directory or other system dependent
  file group designator.

CWD
  250
  500, 501, 502, 421, 530, 550
*)
procedure TIdFTP.ChangeDir(const ADirName: string);
begin
  SendCmd('CWD ' + ADirName, [200, 250, 257]); //APR: Ericsson Switch FTP     {do not localize}
end;

(*
CHANGE TO PARENT DIRECTORY (CDUP)

  This command is a special case of CWD, and is included to
  simplify the implementation of programs for transferring
  directory trees between operating systems having different
  syntaxes for naming the parent directory.  The reply codes
  shall be identical to the reply codes of CWD.  See
  Appendix II for further details.

CDUP
  200
  500, 501, 502, 421, 530, 550
*)
procedure TIdFTP.ChangeDirUp;
begin
  // RFC lists 200 as the proper response, but in another section says that it can return the
  // same as CWD, which expects 250. That is it contradicts itself.
  // MS in their infinite wisdom chnaged IIS 5 FTP to return 250.
  SendCmd('CDUP', [200, 250]);   {do not localize}
end;

procedure TIdFTP.Site(const ACommand: string);
begin
  SendCmd('SITE ' + ACommand, 200);   {do not localize}
end;

procedure TIdFTP.Rename(const ASourceFile, ADestFile: string);
begin
  SendCmd('RNFR ' + ASourceFile, 350);  {do not localize}
  SendCmd('RNTO ' + ADestFile, 250);    {do not localize}
end;

function TIdFTP.Size(const AFileName: String): Int64;
var
  LTrans : TIdFTPTransferType;
  SizeStr: String;
begin
  Result := -1;
  // RLebeau 03/13/2009: some servers refuse to accept the SIZE command in
  // ASCII mode, returning a "550 SIZE not allowed in ASCII mode" reply.
  // We put the connection in BINARY mode, even though no data connection is
  // actually being used. We restore it if the original mode was not BINARY.
  // It's a good idea to do this anyway because some other clients do this
  // as well.
  LTrans := TransferType;
  if LTrans <> ftBinary then begin
    Self.TransferType := ftBinary;
  end;
  try
    if SendCmd('SIZE ' + AFileName) = 213 then begin  {do not localize}
      SizeStr := Trim(LastCmdResult.Text.Text);
      IdDelete(SizeStr, 1, IndyPos(' ', SizeStr)); // delete the response   {do not localize}
      Result := IndyStrToInt64(SizeStr, -1);
    end;
  finally
    if LTrans <> ftBinary then begin
      TransferType := LTrans;
    end;
  end;
end;

//Added by SP
procedure TIdFTP.ReInitialize(ADelay: UInt32 = 10);
begin
  IndySleep(ADelay); //Added
  if SendCmd('REIN', [120, 220, 500]) <> 500 then begin  {do not localize}
    FLoginMsg.Clear;
    FCanResume := False;
    if Assigned(FDirectoryListing) then begin
      FDirectoryListing.Clear;
    end;
    FUsername := '';                 {do not localize}
    FPassword := '';                 {do not localize}
    FPassive := Id_TIdFTP_Passive;
    FCanResume := False;
    FResumeTested := False;
    FSystemDesc := '';
    FTransferType := Id_TIdFTP_TransferType;
    IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
    {$IFDEF STRING_IS_ANSI}
    IOHandler.DefAnsiEncoding := IndyTextEncoding_OSDefault;
    {$ENDIF}
    if FUsingSFTP and (FUseTLS <> utUseImplicitTLS) then begin
      (IOHandler as TIdSSLIOHandlerSocketBase).PassThrough := True;
      FUsingSFTP := False;
      FUseCCC := False;
    end;
  end;
end;

procedure TIdFTP.Allocate(AAllocateBytes: Integer);
begin
  SendCmd('ALLO ' + IntToStr(AAllocateBytes), [200]); {do not localize}
end;

procedure TIdFTP.Status(AStatusList: TStrings);
begin
  if SendCmd('STAT', [211, 212, 213, 500]) <> 500 then begin  {do not localize}
    AStatusList.Text := LastCmdResult.Text.Text;
  end;
end;

procedure TIdFTP.Help(AHelpContents: TStrings; ACommand: String = ''); {do not localize}
begin
  if SendCmd(Trim('HELP ' + ACommand), [211, 214, 500]) <> 500 then begin      {do not localize}
    AHelpContents.Text := LastCmdResult.Text.Text;
  end;
end;

function TIdFTP.CheckAccount: Boolean;
begin
  if (FAccount = '') and Assigned(FOnNeedAccount) then begin
    FOnNeedAccount(Self, FAccount);
  end;
  Result := FAccount <> '';
end;

procedure TIdFTP.StructureMount(APath: String);
begin
  SendCmd('SMNT ' + APath, [202, 250, 500]);  {do not localize}
end;

procedure TIdFTP.FileStructure(AStructure: TIdFTPDataStructure);
const
  StructureTypes: array[TIdFTPDataStructure] of String = ('F', 'R', 'P'); {do not localize}
begin
  SendCmd('STRU ' + StructureTypes[AStructure], [200, 500]);  {do not localize}
  { TODO: Needs to be finished }
end;

procedure TIdFTP.TransferMode(ATransferMode: TIdFTPTransferMode);
var
  s: String;
begin
  if FCurrentTransferMode <> ATransferMode then begin
    s := '';
    case ATransferMode of
//    dmBlock: begin
//      s := 'B';                {do not localize}
//    end;
//    dmCompressed: begin
//      s := 'C';                {do not localize}
//    end;
      dmStream: begin
        s := 'S';                {do not localize}
      end;
      dmDeflate: begin
        if not Assigned(FCompressor) then begin
          raise EIdFTPMissingCompressor.Create(RSFTPMissingCompressor);
        end;
        if Self.IsCompressionSupported then begin
          s := 'Z';  {Do not localize}
        end;
      end;
    end;
    if s = '' then begin
      raise EIdFTPUnsupportedTransferMode.Create(RSFTPUnsupportedTransferMode);
    end;
    SendCmd('MODE ' + s, 200); {do not localize}
    FCurrentTransferMode := ATransferMode;
  end;
end;

destructor TIdFTP.Destroy;
begin
  FreeAndNil(FClientInfo);
  FreeAndNil(FListResult);
  FreeAndNil(FLoginMsg);
  FreeAndNil(FDirectoryListing);
  FreeAndNil(FLangsSupported);
  FreeAndNil(FProxySettings); //APR
  FreeAndNil(FTZInfo);
  FreeAndNil(FAbortFlag);
  FreeAndNil(FNATKeepAlive);
  inherited Destroy;
end;

function TIdFTP.Quote(const ACommand: String): Int16;
begin
  Result := SendCmd(ACommand);
end;

procedure TIdFTP.IssueFEAT;
var
  LClnt: String;
  LBuf : String;
  i : Integer;
begin
  //Feat data

  SendCmd('FEAT');  {do not localize}
  FCapabilities.Clear;

  //Ipswitch's FTP WS-FTP Server may issue 221 as success
  if LastCmdResult.NumericCode in [211,221] then begin
    FCapabilities.AddStrings(LastCmdResult.Text);

    //we remove the first and last lines because we only want the list
    if FCapabilities.Count > 0 then begin
      FCapabilities.Delete(0);
    end;
    if FCapabilities.Count > 0 then begin
      FCapabilities.Delete(FCapabilities.Count-1);
    end;
  end;

  if FUsingExtDataPort then begin
    FUsingExtDataPort := IsExtSupported('EPRT') and IsExtSupported('EPSV');  {do not localize}
  end;

  FCanUseMLS := IsExtSupported('MLSD') or IsExtSupported('MLST'); {do not localize}
  ExtractFeatFacts('LANG', FLangsSupported); {do not localize}

  //see if compression is supported.
  //we parse this way because IxExtensionSupported can only work
  //with one word.
  FIsCompressionSupported := False;
  for i := 0 to FCapabilities.Count-1 do begin
    LBuf := Trim(FCapabilities[i]);
    if LBuf = 'MODE Z' then begin {do not localize}
      FIsCompressionSupported := True;
      Break;
    end;
  end;

  // send the CLNT command before sending the OPTS UTF8 command.
  // some servers need this in order to work around a bug in
  // Microsoft Internet Explorer's UTF-8 handling
  if IsExtSupported('CLNT') then begin {do not localize}
    LClnt := FClientInfo.ClntOutput;
    if LClnt = '' then begin
      LClnt := gsIdProductName + ' ' + gsIdVersion;
    end;
    SendCmd('CLNT ' + LClnt);  {do not localize}
  end;

  if IsExtSupported('UTF8') then begin {do not localize}
    // RLebeau 10/1/13: per RFC 2640, OPTS commands are no longer used to
    // activate UTF-8. If the server reports the 'UTF8' capability, it is
    // required to detect and accept UTF-8 encoded paths/filenames...
    {
    // trying non-standard UTF-8 extension first, many servers use this...
    // Cerberus and RaidenFTP return 220, but TitanFTP and Gene6 return 200 instead...
    if not SendCmd('OPTS UTF8 ON') in [200, 220] then begin {do not localize
      // trying draft-ietf-ftpext-utf-8-option-00.txt next...
      if SendCmd('OPTS UTF-8 NLST') <> 200 then begin {do not localize
        Exit;
      end;
    end;
    }
    IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
  end;
end;

procedure TIdFTP.Login;
var
  i : Integer;
  LResp : Word;
  LCmd : String;

  function FtpHost: String;
  begin
    if FPort = IDPORT_FTP then begin
      Result := FHost;
    end else begin
      Result := FHost + Id_TIdFTP_HostPortDelimiter + IntToStr(FPort);
    end;
  end;

begin
//This has to be here because the Rein command clears encryption.
//RFC 4217
  //TLS part
  FUsingSFTP := False;
  if UseTLS in ExplicitTLSVals then begin
    if FAUTHCmd = tAuto then begin
      {Note that we can not call SupportsTLS at all.  That depends upon the FEAT response
      and unfortunately, some servers such as WS_FTP Server 4.0.0 (78162662)
      will not accept a FEAT command until you login.  In other words, you have to do
      this by trial and error.
      }

      //334 has to be accepted because of a broekn implementation
      //see: http://www.ford-hutchinson.com/~fh-1-pfh/ftps-ext.html#bad

      {Note that we have to try several commands because some servers use AUTH TLS while others use
      AUTH SSL.  GlobalScape's FTP Server only uses AUTH SSL while IpSwitch's uses AUTH TLS (the correct behavior).
      We try two other commands for historical reasons.
      }
      for i := 0 to 3 do begin
        LResp := SendCmd('AUTH ' + TLS_AUTH_NAMES[i]);  {do not localize}
        if (LResp = 234) or (LResp = 334) then begin
          //okay.  do the handshake
          TLSHandshake;
          FUsingSFTP := True;
          //we are done with the negotiation, let's close this.
          Break;
        end;
        //see if the error was not any type of syntax error code
        //if it wasn't, we fail the command.
        if (LResp div 500) <> 1 then begin
          ProcessTLSNegCmdFailed;
          Break;
        end;
      end;
    end else begin
      LResp := SendCmd('AUTH ' + TLS_AUTH_NAMES[Ord(FAUTHCmd)-1]);  {do not localize}
      if (LResp = 234) or (LResp = 334) then begin
        //okay.  do the handshake
        TLSHandshake;
        FUsingSFTP := True;
      end else begin
        ProcessTLSNegCmdFailed;
      end;
    end;
  end;
  // TODO: should this be moved inside the 'if UseTLS in ExplicitTLSVals' block?
  if not FUsingSFTP then begin
    ProcessTLSNotAvail;
  end;
  //login
  case ProxySettings.ProxyType of
  fpcmNone:
    begin
      LCmd := MakeXAUTCmd( Greeting.Text.Text , FUserName, GetLoginPassword);
      if (LCmd <> '') and (not GetFIPSMode ) then begin
        if SendCmd(LCmd, [230, 232, 331]) = 331 then begin {do not localize}
          if IsAccountNeeded then begin
            if CheckAccount then begin
              SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
            end else begin
              RaiseExceptionForLastCmdResult;
            end;
          end;
        end;
      end else begin
        if SendCmd('USER ' + FUserName, [230, 232, 331]) = 331 then begin {do not localize}
          SendCmd('PASS ' + GetLoginPassword, [230, 332]);  {do not localize}
          if IsAccountNeeded then begin
            if CheckAccount then begin
              SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
            end else begin
              RaiseExceptionForLastCmdResult;
            end;
         end;
        end;
      end;
    end;
  fpcmUserSite:
    begin
      //This also supports WinProxy
      if Length(ProxySettings.UserName) > 0 then begin
        if SendCmd('USER ' + ProxySettings.UserName, [230, 331]) = 331 then begin {do not localize}
          SendCmd('PASS ' + ProxySettings.Password, 230); {do not localize}
          if IsAccountNeeded then begin
            if CheckAccount then begin
              SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
            end else begin
              RaiseExceptionForLastCmdResult;
            end;
          end;
        end;
      end;
      if SendCmd('USER ' + FUserName + '@' + FtpHost, [230, 232, 331]) = 331 then begin {do not localize}
        SendCmd('PASS ' + GetLoginPassword, [230, 331]);  {do not localize}
        if IsAccountNeeded then
        begin
          if CheckAccount then begin
            SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
          end else begin
            RaiseExceptionForLastCmdResult;
          end;
        end;
      end;
    end;
  fpcmSite:
    begin
      if Length(ProxySettings.UserName) > 0 then begin
        if SendCmd('USER ' + ProxySettings.UserName, [230, 331]) = 331 then begin {do not localize}
          SendCmd('PASS ' + ProxySettings.Password, 230); {do not localize}
        end;
      end;
      SendCmd('SITE ' + FtpHost); // ? Server Reply? 220?   {do not localize}
      if SendCmd('USER ' + FUserName, [230, 232, 331]) = 331 then begin {do not localize}
        SendCmd('PASS ' + GetLoginPassword, [230, 332]); {do not localize}
        if IsAccountNeeded then begin
          if CheckAccount then begin
            SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
          end else begin
            RaiseExceptionForLastCmdResult;
          end;
        end;
      end;
    end;
  fpcmOpen:
    begin
      if Length(ProxySettings.UserName) > 0 then begin
        if SendCmd('USER ' + ProxySettings.UserName, [230, 331]) = 331 then begin   {do not localize}
          SendCmd('PASS ' + GetLoginPassword, [230, 332]); {do not localize}
          if IsAccountNeeded then begin
            if CheckAccount then begin
              SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
            end else begin
              RaiseExceptionForLastCmdResult;
            end;
          end;
        end;
      end;
      SendCmd('OPEN ' + FtpHost);//? Server Reply? 220?     {do not localize}
      if SendCmd('USER ' + FUserName, [230, 232, 331]) = 331 then begin  {do not localize}
        SendCmd('PASS ' + GetLoginPassword, [230, 332]); {do not localize}
        if IsAccountNeeded then begin
          if CheckAccount then begin
            SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
          end else begin
            RaiseExceptionForLastCmdResult;
          end;
        end;
      end;
    end;
  fpcmUserPass: //USER user@firewalluser@hostname / PASS pass@firewallpass
    begin
      if SendCmd(IndyFormat('USER %s@%s@%s',
        [FUserName, ProxySettings.UserName, FtpHost]), [230, 232, 331]) = 331 then begin    {do not localize}
        if Length(ProxySettings.Password) > 0 then begin
          SendCmd('PASS ' + GetLoginPassword + '@' + ProxySettings.Password, [230, 332]); {do not localize}
        end else begin
          //// needs otp ////
          SendCmd('PASS ' + GetLoginPassword, [230,332]);  {do not localize}
        end;
        if IsAccountNeeded then begin
          if CheckAccount then begin
            SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
          end else begin
            RaiseExceptionForLastCmdResult;
          end;
        end;
      end;
    end;
  fpcmTransparent:
    begin
      //I think  fpcmTransparent means to connect to the regular host and the firewalll
      //intercepts the login information.
      if Length(ProxySettings.UserName) > 0 then begin
        if SendCmd('USER ' + ProxySettings.UserName, [230, 331]) = 331 then begin    {do not localize}
          SendCmd('PASS ' + ProxySettings.Password, [230,332]);     {do not localize}
        end;
      end;
      if SendCmd('USER ' + FUserName, [230, 232, 331]) = 331 then begin   {do not localize}
        SendCmd('PASS ' + GetLoginPassword, [230,332]); {do not localize}
        if IsAccountNeeded then begin
          if CheckAccount then begin
            SendCmd('ACCT ' + FAccount, [202, 230, 500]);
          end else begin
            RaiseExceptionForLastCmdResult;
          end;
        end;
      end;
    end;
  fpcmUserHostFireWallID :  //USER hostuserId@hostname firewallUsername
    begin
       if SendCmd(Trim('USER ' + Username + '@' + FtpHost + ' ' + ProxySettings.UserName), [230, 331]) = 331 then begin   {do not localize}
         if SendCmd('PASS ' + GetLoginPassword, [230,232,202,332]) = 332 then begin
           SendCmd('ACCT ' + ProxySettings.Password, [230,232,332]);
           if IsAccountNeeded then begin
             if CheckAccount then begin
               SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
             end else begin
               RaiseExceptionForLastCmdResult;
             end;
           end;
         end;
       end;
    end;
  fpcmNovellBorder : //Novell Border PRoxy
    begin
{Done like this:

USER ProxyUserName$ DestFTPUserName$DestFTPHostName

PASS UsereDirectoryPassword$ DestFTPPassword

Novell BorderManager 3.8 Proxy and Firewall Overview and Planning Guide
Copyright © 1997-1998, 2001, 2002-2003, 2004 Novell, Inc. All rights reserved.
===
From a WS-FTP Pro firescript at:

http://support.ipswitch.com/kb/WS-20050315-DM01.htm

send ("USER %FwUserId$%HostUserId$%HostAddress") 

//send ("PASS %FwPassword$%HostPassword")

}
      if SendCmd(Trim('USER ' + ProxySettings.UserName + '$' + Username + '$' + FtpHost), [230, 331]) = 331 then begin   {do not localize}
        if SendCmd('PASS ' + ProxySettings.UserName + '$' + GetLoginPassword, [230,232,202,332]) = 332 then begin
          if IsAccountNeeded then begin
            if CheckAccount then begin
              SendCmd('ACCT ' + FAccount, [202, 230, 500]);  {do not localize}
            end else begin
              RaiseExceptionForLastCmdResult;
            end;
          end;
        end;
      end;
    end;
  fpcmHttpProxyWithFtp :
    begin
{GET ftp://XXX:YYY@indy.nevrona.com/ HTTP/1.0
Host: indy.nevrona.com
User-Agent: Mozilla/4.0 (compatible; Wincmd; Windows NT)
Proxy-Authorization: Basic B64EncodedUserPass==
Connection: close}
      raise EIdSocksServerCommandError.Create(RSSocksServerCommandError);
    end;//fpcmHttpProxyWithFtp
  fpcmCustomProxy :
    begin
      DoCustomFTPProxy;
    end;
  end;//case

  FLoginMsg.Assign(LastCmdResult);
  DoOnBannerAfterLogin(FLoginMsg.FormattedReply);
  //should be here because this can be issued more than once per connection.

  if FAutoIssueFEAT then begin
    IssueFEAT;
  end;

  SendTransferType(FTransferType);
end;

procedure TIdFTP.DoAfterLogin;
begin
  if Assigned(FOnAfterClientLogin) then begin
    OnAfterClientLogin(Self);
  end;
end;

procedure TIdFTP.DoFTPList;
begin
  if Assigned(FOnCreateFTPList) then begin
    FOnCreateFTPList(Self, FDirectoryListing);
  end;
end;

function TIdFTP.GetDirectoryListing: TIdFTPListItems;
begin
  if FDirectoryListing = nil then begin
    if Assigned(FOnDirParseStart) then begin
      FOnDirParseStart(Self);
    end;
    ConstructDirListing;
    ParseFTPList;
  end;
  Result := FDirectoryListing;
end;

procedure TIdFTP.SetProxySettings(const Value: TIdFtpProxySettings);
begin
  FProxySettings.Assign(Value);
end;

{ TIdFtpProxySettings }

procedure TIdFtpProxySettings.Assign(Source: TPersistent);
var
  LSource: TIdFtpProxySettings;
begin
  if Source is TIdFtpProxySettings then begin
    LSource := TIdFtpProxySettings(Source);
    FProxyType := LSource.ProxyType;
    FHost := LSource.Host;
    FUserName := LSource.UserName;
    FPassword := LSource.Password;
    FPort := LSource.Port;
  end else begin
    inherited Assign(Source);
  end;
end;

procedure TIdFTP.SendPBSZ;
begin
  {NOte that PBSZ - protection buffer size must always be zero for FTP TLS}
  if FUsingSFTP or (FUseTLS = utUseImplicitTLS) then begin
    //protection buffer size
    SendCmd('PBSZ 0');  {do not localize}
  end;
end;

procedure TIdFTP.SendPROT;
begin
  case FDataPortProtection of
    ftpdpsClear : SendCmd('PROT C', 200);  //'C' - Clear - neither Integrity nor Privacy   {do not localize}
    // NOT USED - 'S' - Safe - Integrity without Privacy
    // NOT USED - 'E' - Confidential - Privacy without Integrity
    // 'P' - Private - Integrity and Privacy
    ftpdpsPrivate : SendCmd('PROT P', 200); {do not localize}
  end;
end;

procedure TIdFTP.SendDataSettings;
begin
  if FUsingSFTP then begin
    if not FDataSettingsSent then begin
      FDataSettingsSent := True;
      SendPBSZ;
      SendPROT;
      if FUseCCC then begin
        FUsingCCC := (SendCmd('CCC') div 100) = 2; {do not localize}
        if FUsingCCC then begin
         (IOHandler as TIdSSLIOHandlerSocketBase).PassThrough := True;
        end;
      end;
    end;
  end;
end;

procedure TIdFTP.SetIOHandler(AValue: TIdIOHandler);
begin
  inherited SetIOHandler(AValue);
  // UseExtensionDataPort must be true for IPv6 connections.
  // PORT and PASV can not communicate IPv6 Addresses
  if Socket <> nil then begin
    if Socket.IPVersion = Id_IPv6 then begin
      FUseExtensionDataPort := True;
    end;
  end;
end;

procedure TIdFTP.SetUseExtensionDataPort(const AValue: Boolean);
begin
  if (not AValue) and (IPVersion = Id_IPv6) then begin
    raise EIdFTPMustUseExtWithIPv6.Create(RSFTPMustUseExtWithIPv6);
  end;
  if TryNATFastTrack then begin
    raise EIdFTPMustUseExtWithNATFastTrack.Create(RSFTPMustUseExtWithNATFastTrack);
  end;
  FUseExtensionDataPort := AValue;
end;

procedure TIdFTP.ParseEPSV(const AReply : String; var VIP : String; var VPort : TIdPort);
var
  bLeft, bRight, LPort: Integer;
  delim : Char;
  s : String;
begin
  s := Trim(AReply);
  // "229 Entering Extended Passive Mode (|||59028|)"
  bLeft := IndyPos('(', s);   {do not localize}
  bRight := IndyPos(')', s);  {do not localize}
  s := Copy(s, bLeft + 1, bRight - bLeft - 1);
  delim := s[1]; // normally is | but the RFC say it may be different
  Fetch(S, delim);
  Fetch(S, delim);
  VIP := Fetch(S, delim);
  if VIP = '' then begin
    VIP := Host;
  end;
  s := Trim(Fetch(S, delim));
  LPort := IndyStrToInt(s, 0);
  if (LPort < 1) or (LPort > 65535) then begin
    raise EIdFTPServerSentInvalidPort.CreateFmt(RSFTPServerSentInvalidPort, [s]);
  end;
  VPort := TIdPort(LPort and $FFFF);
end;

procedure TIdFTP.SendEPassive(var VIP: string; var VPort: TIdPort);
begin
  SendDataSettings;
  //Note that for FTP Proxies, it is not desirable for the server to choose
  //the EPSV data port IP connection type.  We try to if we can.
  if FProxySettings.ProxyType <> fpcmNone then begin
    if SendCMD('EPSV ' + cIPVersions[IPVersion]) <> 229 then begin  {do not localize}
      //Raidon and maybe a few others may honor EPSV but not with the proto numbers
      SendCMD('EPSV');  {do not localize}
    end;
  end else begin
    SendCMD('EPSV');  {do not localize}
  end;
  if LastCmdResult.NumericCode <> 229 then begin
    SendPassive(VIP, VPort);
    FUsingExtDataPort := False;
    Exit;
  end;
  try
    ParseEPSV(LastCmdResult.Text[0], VIP, VPort);
  except
    SendCmd('ABOR');  {do not localize}
    raise;
  end;
end;

procedure TIdFTP.SendEPort(AHandle: TIdSocketHandle);
begin
  SendDataSettings;
  if FExternalIP <> '' then begin
    SendEPort(FExternalIP, AHandle.Port, AHandle.IPVersion);
  end else begin
    SendEPort(AHandle.IP, AHandle.Port, AHandle.IPVersion);
  end;
end;

procedure TIdFTP.SendEPort(const AIP: String; const APort: TIdPort; const AIPVersion: TIdIPVersion);
begin
  if SendCmd('EPRT |' + cIPVersions[AIPVersion] + '|' + AIP + '|' + IntToStr(APort) + '|') <> 200 then begin  {do not localize}
    SendPort(AIP, APort);
    FUsingExtDataPort := False;
  end;
end;

procedure TIdFTP.SetPassive(const AValue: Boolean);
begin
  if (not AValue) and TryNATFastTrack then begin
    raise EIdFTPPassiveMustBeTrueWithNATFT.Create(RSFTPFTPPassiveMustBeTrueWithNATFT);
  end;
  FPassive := AValue;
end;

procedure TIdFTP.SetTryNATFastTrack(const AValue: Boolean);
begin
  FTryNATFastTrack := AValue;
  if FTryNATFastTrack then begin
    FPassive := True;
    FUseExtensionDataPort := True;
  end;
end;

procedure TIdFTP.DoTryNATFastTrack;
begin
  if IsExtSupported('EPSV') then begin {do not localize}
    if SendCmd('EPSV ALL') = 229 then begin  {do not localize}
      //Surge FTP treats EPSV ALL as if it were a standard EPSV
      //We send ABOR in that case so it can close the data connection it created
      SendCmd('ABOR');   {do not localize}
    end;
    FUsingNATFastTrack := True;
  end;
end;

procedure TIdFTP.SetCmdOpt(const ACmd, AOptions: String);
begin
  SendCmd('OPTS ' + ACmd + ' ' + AOptions, 200); {do not localize}
end;

procedure TIdFTP.ExtListDir(ADest: TStrings = nil; const ADirectory: string = '');
var
  LDest: TMemoryStream;
  LEncoding: IIdTextEncoding;
begin
  // RLebeau 6/4/2009: According to RFC 3659 Section 7.2:
  //
  // The data connection opened for a MLSD response shall be a connection
  // as if the "TYPE L 8", "MODE S", and "STRU F" commands had been given,
  // whatever FTP transfer type, mode and structure had actually been set,
  // and without causing those settings to be altered for future commands.
  // That is, this transfer type shall be set for the duration of the data
  // connection established for this command only.  While the content of
  // the data sent can be viewed as a series of lines, implementations
  // should note that there is no maximum line length defined.
  // Implementations should be prepared to deal with arbitrarily long
  // lines.

  LDest := TMemoryStream.Create;
  try
    InternalGet(Trim('MLSD ' + ADirectory), LDest);  {do not localize}
    FreeAndNil(FDirectoryListing);
    FDirFormat := '';
    DoOnRetrievedDir;
    LDest.Position := 0;
    // RLebeau: using IndyTextEncoding_8Bit here.  TIdFTPListParseBase will
    // decode UTF-8 sequences later on...
    LEncoding := IndyTextEncoding_8Bit;
    FListResult.Text := ReadStringFromStream(LDest, -1, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
    LEncoding := nil;
    TIdFTPListResult(FListResult).FDetails := True;
    TIdFTPListResult(FListResult).FUsedMLS := True;
    FDirFormat := MLST;
  finally
    FreeAndNil(LDest);
  end;
  if Assigned(ADest) then begin //APR: User can use ListResult and DirectoryListing
    ADest.Assign(FListResult);
  end;
end;

procedure TIdFTP.ExtListItem(ADest: TStrings; AFList : TIdFTPListItems; const AItem: string);
var
  i : Integer;
begin
  ADest.Clear;
  SendCmd(Trim('MLST ' + AItem), 250, IndyTextEncoding_8Bit);  {do not localize}
  for i := 0 to LastCmdResult.Text.Count -1 do begin
    if IndyPos(';', LastCmdResult.Text[i]) > 0 then begin
      ADest.Add(LastCmdResult.Text[i]);
    end;
  end;
  if Assigned(AFList) then begin
    IdFTPListParseBase.ParseListing(ADest, AFList, 'MLST'); {do not localize}
  end;
end;

procedure TIdFTP.ExtListItem(ADest: TStrings; const AItem: string);
begin
  ExtListItem(ADest, nil, AItem);
end;

procedure TIdFTP.ExtListItem(AFList: TIdFTPListItems; const AItem: String);
var
  LBuf : TStrings;
begin
  LBuf := TStringList.Create;
  try
    ExtListItem(LBuf, AFList, AItem);
  finally
    FreeAndNil(LBuf);
  end;
end;

function TIdFTP.IsExtSupported(const ACmd: String): Boolean;
var
  i : Integer;
  LBuf : String;
begin
  Result := False;
  for i := 0 to FCapabilities.Count -1 do begin
    LBuf := TrimLeft(FCapabilities[i]);
    if TextIsSame(Fetch(LBuf), ACmd) then begin
      Result := True;
      Exit;
    end;
  end;
end;

function TIdFTP.FileDate(const AFileName: String; const AsGMT: Boolean): TDateTime;
var
  LBuf : String;
begin
  //Do not use the FEAT list because some servers
  //may support it even if FEAT isn't supported
  if SendCmd('MDTM ' + AFileName) = 213 then begin {do not localize}
    LBuf := LastCmdResult.Text[0];
    LBuf := Trim(LBuf);
    if AsGMT then begin
      Result := FTPMLSToGMTDateTime(LBuf);
    end else begin
      Result := FTPMLSToLocalDateTime(LBuf);
    end;
  end else begin
    Result := 0;
  end;
end;

procedure TIdFTP.SiteToSiteUpload(const AToSite : TIdFTP; const ASourceFile : String;
  const ADestFile : String = '');
{
SiteToSiteUpload

  From:  PASV   To: PORT   - ATargetUsesPasv = False
  From:  RETR   To: STOR

SiteToSiteDownload

  From: PORT    To: PASV   - ATargetUsesPasv = True
  From: RETR    To: STOR
}
begin
  if ValidateInternalIsTLSFXP(Self, AToSite, True) then begin
    InternalEncryptedTLSFXP(Self, AToSite, ASourceFile, ADestFile, True);
  end else begin
    InternalUnencryptedFXP(Self, AToSite, ASourceFile, ADestFile, True);
  end;
end;

procedure TIdFTP.SiteToSiteDownload(const AFromSite: TIdFTP; const ASourceFile : String;
  const ADestFile : String = '');
{
 The only use of this function is to get the passive mode on the other connection.
 Because not all hosts allow it. This way you get a second chance.
 If uploading from host A doesn't work, try downloading from host B
}
begin
  if ValidateInternalIsTLSFXP(AFromSite, Self, True) then begin
    InternalEncryptedTLSFXP(AFromSite, Self, ASourceFile, ADestFile, False);
  end else begin
    InternalUnencryptedFXP(AFromSite, Self, ASourceFile, ADestFile, False);
  end;
end;

procedure TIdFTP.ExtractFeatFacts(const ACmd: String; AResults: TStrings);
var
  i : Integer;
  LBuf, LFact : String;
begin
  AResults.Clear;
  for i := 0 to FCapabilities.Count -1 do begin
    LBuf := FCapabilities[i];
    if TextIsSame(Fetch(LBuf), ACmd) then begin
      LBuf := Trim(LBuf);
      while LBuf <> '' do begin
        LFact := Trim(Fetch(LBuf, ';'));
        if LFact <> '' then begin
          AResults.Add(LFact);
        end;
      end;
      Exit;
    end;
  end;
end;

procedure TIdFTP.SetLang(const ALangTag: String);
begin
  if IsExtSupported('LANG') then begin {do not localize}
    SendCMD('LANG ' + ALangTag, 200);  {do not localize}
  end;
end;

function TIdFTP.CRC(const AFIleName : String; const AStartPoint : Int64 = 0;
  const AEndPoint : Int64 = 0) : Int64;
var
  LCmd : String;
  LCRC : String;
begin
  Result := -1;
  if IsExtSupported('XCRC') then begin {do not localize}
    LCmd := 'XCRC "' + AFileName + '"'; {do not localize}
    if AStartPoint <> 0 then begin
      LCmd := LCmd + ' ' + IntToStr(AStartPoint);
      if AEndPoint <> 0 then begin
        LCmd := LCmd + ' ' + IntToStr(AEndPoint);
      end;
    end;
    if SendCMD(LCMD) = 250 then begin
      LCRC := Trim(LastCmdResult.Text.Text);
      IdDelete(LCRC, 1, IndyPos(' ', LCRC)); // delete the response
      Result := IndyStrToInt64('$' + LCRC, -1);
    end;
  end;
end;

procedure TIdFTP.CombineFiles(const ATargetFile: String; AFileParts: TStrings);
var
  i : Integer;
  LCmd: String;
begin
  if IsExtSupported('COMB') and (AFileParts.Count > 0) then begin {do not localize}
    LCmd := 'COMB "' + ATargetFile + '"'; {do not localize}
    for i := 0 to AFileParts.Count -1 do begin
      LCmd := LCmd + ' ' + AFileParts[i];
    end;
    SendCmd(LCmd, 250);
  end;
end;

procedure TIdFTP.ParseFTPList;
begin
  DoOnDirParseStart;
  try
    // Parse directory listing
    if FListResult.Count > 0 then begin
      if TIdFTPListResult(FListResult).UsedMLS then begin
        FDirFormat := MLST;
        // TODO: set the FListParserClass as well..
        IdFTPListParseBase.ParseListing(FListResult, FDirectoryListing, MLST);
      end else begin
        CheckListParseCapa(FListResult, FDirectoryListing, FDirFormat,
          FListParserClass, SystemDesc, TIdFTPListResult(FListResult).Details);
      end;
    end else begin
      FDirFormat := '';
    end;
  finally
    DoOnDirParseEnd;
  end;
end;

function TIdFTP.GetSupportsTLS: Boolean;
begin
  Result := (FindAuthCmd <> '');
end;

function TIdFTP.FindAuthCmd: String;
var
  i : Integer;
  LBuf : String;
  LWord : String;
begin
  Result := '';
  for i := 0 to FCapabilities.Count -1 do begin
    LBuf := TrimLeft(FCapabilities[i]);
    if TextIsSame(Fetch(LBuf), 'AUTH') then begin {do not localize}
      repeat
        LWord := Trim(Fetch(LBuf, ';'));
        if PosInStrArray(LWord, TLS_AUTH_NAMES, False) > -1 then begin
          Result := 'AUTH ' + LWord;  {do not localize}
          Exit;
        end;
      until LBuf = '';
      Break;
    end;
  end;
end;

procedure TIdFTP.DoCustomFTPProxy;
begin
  if Assigned(FOnCustomFTPProxy) then begin
    FOnCustomFTPProxy(Self);
  end else begin
    raise EIdFTPOnCustomFTPProxyRequired.Create(RSFTPOnCustomFTPProxyReq);
  end;
end;

function TIdFTP.GetLoginPassword: String;
begin
  Result := GetLoginPassword(LastCmdResult.Text.Text);
end;

function TIdFTP.GetLoginPassword(const APrompt: String): String;
begin
  if TIdOTPCalculator.IsValidOTPString(APrompt) then begin
    TIdOTPCalculator.GenerateSixWordKey(APrompt, FPassword, Result);
  end else begin
    Result := FPassword;
  end;
end;

function TIdFTP.SetSSCNToOn : Boolean;
begin
  Result := FUsingSFTP;
  if not Result then begin
    Exit;
  end;
  Result := (DataPortProtection = ftpdpsPrivate);
  if not Result then begin
    Exit;
  end;
  Result := not IsExtSupported(SCCN_FEAT);
  if not Result then begin
    Exit;
  end;
  if not FSSCNOn then begin
    SendCmd(SSCN_ON, SSCN_OK_REPLY);
    FSSCNOn := True;
  end;
end;

procedure TIdFTP.ClearSSCN;
begin
  if FSSCNOn then begin
    SendCmd(SSCN_OFF, SSCN_OK_REPLY);
  end;
end;

procedure TIdFTP.SetClientInfo(const AValue: TIdFTPClientIdentifier);
begin
  FClientInfo.Assign(AValue);
end;

procedure TIdFTP.SetCompressor(AValue: TIdZLibCompressorBase);
var
  // under ARC, convert a weak reference to a strong reference before working with it
  LCompressor: TIdZLibCompressorBase;
begin
  LCompressor := FCompressor;

  if LCompressor <> AValue then begin
    // under ARC, all weak references to a freed object get nil'ed automatically

    {$IFNDEF USE_OBJECT_ARC}
    if Assigned(LCompressor) then begin
      LCompressor.RemoveFreeNotification(Self);
    end;
    {$ENDIF}

    FCompressor := AValue;

    if Assigned(AValue) then begin
      {$IFNDEF USE_OBJECT_ARC}
      AValue.FreeNotification(Self);
      {$ENDIF}
    end
    else if Connected then begin
      TransferMode(dmStream);
    end;
  end;
end;

procedure TIdFTP.GetInternalResponse(AEncoding: IIdTextEncoding = nil);
var
  LLine: string;
  LResponse: TStringList;
  LReplyCode: string;
begin
  CheckConnected;
  LResponse := TStringList.Create;
  try
    // Some servers with bugs send blank lines before reply. Dont remember
    // which ones, but I do remember we changed this for a reason
    //
    // RLebeau 9/14/06: this can happen in between lines of the reply as well

    // RLebeau 3/9/09: according to RFC 959, when reading a multi-line reply,
    // we are supposed to look at the first line's reply code and then keep
    // reading until that specific reply code is encountered again, and
    // everything in between is the text.  So, do not just look for arbitrary
    // 3-digit values on each line, but instead look for the specific reply
    // code...

    LLine := IOHandler.ReadLnWait(MaxInt, AEncoding);
    LResponse.Add(LLine);

    if CharEquals(LLine, 4, '-') then begin
      LReplyCode := Copy(LLine, 1, 3);
      repeat
        LLine := IOHandler.ReadLnWait(MaxInt, AEncoding);
        LResponse.Add(LLine);
      until TIdReplyFTP(FLastCmdResult).IsEndReply(LReplyCode, LLine);
    end;

    //Note that FormattedReply uses an assign in it's property set method.
    FLastCmdResult.FormattedReply := LResponse;
  finally
    FreeAndNil(LResponse);
  end;
end;

function TIdFTP.CheckResponse(const AResponse: Int16;
  const AAllowedResponses: array of Int16): Int16;
var
  i: Integer;
begin
  // any FTP command can return a 421 reply if the server is going to shut
  // down the command connection.  This way, we can close the connection
  // immediately instead of waiting for a future action that would raise
  // an EIdConnClosedGracefully exception instead...

  if AResponse = 421 then
  begin
    // check if the caller explicitally wants to handle 421 replies...
    if High(AAllowedResponses) > -1 then begin
      for i := Low(AAllowedResponses) to High(AAllowedResponses) do begin
        if AResponse = AAllowedResponses[i] then begin
          Result := AResponse;
          Exit;
        end;
      end;
    end;
    Disconnect(False);
    if IOHandler <> nil then begin
      IOHandler.InputBuffer.Clear;
    end;
    RaiseExceptionForLastCmdResult;
  end;

  Result := inherited CheckResponse(AResponse, AAllowedResponses);
end;

function TIdFTP.GetReplyClass: TIdReplyClass;
begin
  Result := TIdReplyFTP;
end;

procedure TIdFTP.SetIPVersion(const AValue: TIdIPVersion);
begin
  if AValue <> FIPVersion then begin
    inherited SetIPVersion(AValue);
    if IPVersion = Id_IPv6 then begin
      UseExtensionDataPort := True;
    end;
  end;
end;

class function TIdFTP.InternalEncryptedTLSFXP(AFromSite, AToSite: TIdFTP;
  const ASourceFile, ADestFile: String; const ATargetUsesPasv : Boolean): Boolean;
{

SiteToSiteUpload

  From:  PASV   To: PORT   - ATargetUsesPasv = False
  From:  RETR   To: STOR

SiteToSiteDownload

  From: PORT    To: PASV   - ATargetUsesPasv = True
  From: RETR    To: STOR


To do FXP transfers with TLS FTP, you have to have one computer do the
TLS handshake as a client (ssl_connect).  Thus, one of the following conditions must be meet.

1) SSCN must be supported on one of the FTP servers

or

2) If IPv4 is used, the computer receiving a "PASV" command must support
  CPSV.  CPSV will NOT work with IPv6.

IMAO, when doing FXP transfers, you should use SSCN whenever possible as
SSCN will support IPv6 and SSCN may be in wider use than CPSV.  CPSV should
only be used as a fallback if SSCN isn't supported by both servers and IPv4
is being used.
}
var
  LIP : String;
  LPort : TIdPort;
begin
  Result := True;
  if AFromSite.SetSSCNToOn then begin
    AToSite.ClearSSCN;
  end
  else if AToSite.SetSSCNToOn then begin
    AFromSite.ClearSSCN;
  end
  else if AToSite.IPVersion = Id_IPv4 then begin
    if ATargetUsesPasv then begin
      AToSite.SendCPassive(LIP, LPort);
      AFromSite.SendPort(LIP, LPort);
    end else begin
      AFromSite.SendCPassive(LIP, LPort);
      AToSite.SendPort(LIP, LPort);
    end;
  end;
  FXPSendFile(AFromSite, AToSite, ASourceFile, ADestFile);
end;

class function TIdFTP.InternalUnencryptedFXP(AFromSite, AToSite: TIdFTP;
  const ASourceFile, ADestFile: String; const ATargetUsesPasv : Boolean): Boolean;
{
SiteToSiteUpload

  From:  PASV   To: PORT   - ATargetUsesPasv = False
  From:  RETR   To: STOR

SiteToSiteDownload

  From: PORT    To: PASV   - ATargetUsesPasv = True
  From: RETR    To: STOR
}
begin
  FXPSetTransferPorts(AFromSite, AToSite, ATargetUsesPasv);
  FXPSendFile(AFromSite, AToSite, ASourceFile, ADestFile);
  Result := True;
end;

class function TIdFTP.ValidateInternalIsTLSFXP(AFromSite, AToSite: TIdFTP;
  const ATargetUsesPasv : Boolean): Boolean;
{
SiteToSiteUpload

  From:  PASV   To: PORT   - ATargetUsesPasv = False
  From:  RETR   To: STOR

SiteToSiteDownload

  From: PORT    To: PASV   - ATargetUsesPasv = True
  From: RETR    To: STOR

This will raise an exception if FXP can not be done.  Result = True for encrypted
or False for unencrypted.

Note:

The following is required:
   SiteToSiteUpload
     Source must do P
}
begin
  if ATargetUsesPasv then begin
    if AToSite.UsingNATFastTrack then begin
      raise EIdFTPSToSNATFastTrack.Create(RSFTPNoSToSWithNATFastTrack);
    end;
  end else begin
    if AFromSite.UsingNATFastTrack then begin
      raise EIdFTPSToSNATFastTrack.Create(RSFTPNoSToSWithNATFastTrack);
    end;
  end;

  if AFromSite.IPVersion <> AToSite.IPVersion then begin
    raise EIdFTPStoSIPProtoMustBeSame.Create(RSFTPSToSProtosMustBeSame);
  end;
  if AFromSite.CurrentTransferMode <> AToSite.CurrentTransferMode then begin
    raise EIdFTPSToSTransModesMustBeSame.Create(RSFTPSToSTransferModesMusbtSame);
  end;
  if AFromSite.FUsingSFTP <> AToSite.FUsingSFTP then begin
    raise EIdFTPSToSNoDataProtection.Create(RSFTPSToSNoDataProtection);
  end;

  Result := AFromSite.FUsingSFTP and AToSite.FUsingSFTP;
  if Result then begin
    if not (AFromSite.IsExtSupported('SSCN') or AToSite.IsExtSupported('SSCN')) then begin {do not localize}
      //Second chance fallback, is CPSV supported on the server where PASV would
      // be sent
      if AToSite.IPVersion = Id_IPv4 then begin
        if ATargetUsesPasv then begin
          if not AToSite.IsExtSupported('CPSV') then begin {do not localize}
            raise EIdFTPSToSNATFastTrack.Create(RSFTPSToSSSCNNotSupported);
          end;
        end else begin
          if not AFromSite.IsExtSupported('CPSV') then begin {do not localize}
            raise EIdFTPSToSNATFastTrack.Create(RSFTPSToSSSCNNotSupported);
          end;
        end;
      end;
    end;
  end;
end;

class procedure TIdFTP.FXPSendFile(AFromSite, AToSite: TIdFTP; const ASourceFile, ADestFile: String);
var
  LDestFile : String;
begin
  LDestFile := ADestFile;
  if LDestFile = '' then begin
    LDestFile := ASourceFile;
  end;
  AToSite.SendCmd('STOR ' + LDestFile, [110, 125, 150]); {do not localize}
  try
    AFromSite.SendCmd('RETR ' + ASourceFile, [110, 125, 150]); {do not localize}
  except
    AToSite.Abort;
    raise;
  end;
  AToSite.GetInternalResponse;
  AFromSite.GetInternalResponse;
  AToSite.CheckResponse(AToSite.LastCmdResult.NumericCode, [225, 226, 250]);
  AFromSite.CheckResponse(AFromSite.LastCmdResult.NumericCode, [225, 226, 250]);
end;

class procedure TIdFTP.FXPSetTransferPorts(AFromSite, AToSite: TIdFTP; const ATargetUsesPasv: Boolean);
var
  LIP : String;
  LPort : TIdPort;
{
{
SiteToSiteUpload

  From:  PASV   To: PORT   - ATargetUsesPasv = False
  From:  RETR   To: STOR

SiteToSiteDownload

  From: PORT    To: PASV   - ATargetUsesPasv = True
  From: RETR    To: STOR
}
begin
  if ATargetUsesPasv then begin
    if AToSite.UsingExtDataPort then begin
      AToSite.SendEPassive(LIP, LPort);
    end else begin
      AToSite.SendPassive(LIP, LPort);
    end;
    if AFromSite.UsingExtDataPort then begin
      AFromSite.SendEPort(LIP, LPort, AToSite.IPVersion);
    end else begin
      AFromSite.SendPort(LIP, LPort);
    end;
  end else begin
    if AFromSite.UsingExtDataPort then begin
      AFromSite.SendEPassive(LIP, LPort);
    end else begin
      AFromSite.SendPassive(LIP, LPort);
    end;
    if AToSite.UsingExtDataPort then begin
      AToSite.SendEPort(LIP, LPort, AFromSite.IPVersion);
    end else begin
      AToSite.SendPort(LIP, LPort);
    end;
  end;
end;

{ TIdFTPClientIdentifier }

procedure TIdFTPClientIdentifier.Assign(Source: TPersistent);
var
  LSource: TIdFTPClientIdentifier;
begin
  if Source is TIdFTPClientIdentifier then begin
    LSource := TIdFTPClientIdentifier(Source);
    ClientName  := LSource.ClientName;
    ClientVersion := LSource.ClientVersion;
    PlatformDescription := LSource.PlatformDescription;
  end else begin
    inherited Assign(Source);
  end;
end;

//assume syntax such as this:
//214 Syntax: CLNT <sp> <client-name> <sp> <client-version> [<sp> <optional platform info>] (Set client name)
function TIdFTPClientIdentifier.GetClntOutput: String;
begin
  if FClientName <> '' then begin
    Result := FClientName;
    if FClientVersion <> '' then begin
      Result := Result + ' ' + FClientVersion;
      if FPlatformDescription <> '' then begin
        Result := Result + ' ' + FPlatformDescription;
      end;
    end;
  end else begin
    Result := '';
  end;
end;

procedure TIdFTPClientIdentifier.SetClientName(const AValue: String);
begin
  FClientName := Trim(AValue);
  // Don't call Fetch;  it prevents multi-word client names
end;

procedure TIdFTPClientIdentifier.SetClientVersion(const AValue: String);
begin
  FClientVersion := Trim(AValue);
end;

procedure TIdFTPClientIdentifier.SetPlatformDescription(const AValue: String);
begin
  FPlatformDescription := AValue;
end;

{Note about SetTime procedures:

The first syntax is one used by current Serv-U versions and servers that report "MDTM YYYYMMDDHHMMSS[+-TZ];filename " in their FEAT replies is:

1) MDTM [Time in GMT format] Filename

some Bullete Proof FTPD versions, Indy's FTP Server component, and servers reporting "MDTM YYYYMMDDHHMMSS[+-TZ] filename" in their FEAT replies uses an older Syntax which is:

2) MDTM yyyymmddhhmmss[+-minutes from Universal Time] Filename

and then there is the classic

3) MDTM [local timestamp] Filename

So for example, if I was a file dated Jan 3, 5:00:00 pm from my computer in the Eastern Standard Time (-5 hours from Universal Time), the 3 syntaxes
Indy would use are:

Syntax 1:

1) MDTM 0103220000 MyFile.exe  (notice the 22 hour)

Syntax 2:

2) MDTM 0103170000-300 MyFile.exe (notice the 17 hour and the -300 offset)

Syntax 3;

3) MDTM 0103170000 MyFile.exe  (notice the 17 hour)

Note from:
http://www.ftpvoyager.com/releasenotes10x.asp
====
Added support for RFC change and the MDTM. MDTM requires sending the server
GMT (UTC) instead of a "fixed" date and time. FTP Voyager supports this with
Serv-U automatically by checking the Serv-U version number and by checking the
response to the FEAT command for MDTM. Servers returning "MDTM" or
"MDTM YYYYMMDDHHMMSS[+-TZ] filename" will use the old method. Servers
returning "MDTM YYYYMMDDHHMMSS" only will use the new method where the date a
and time is GMT (UTC).
===
}
procedure TIdFTP.SetModTime(const AFileName: String; const ALocalTime: TDateTime);
var
  LCmd: String;
begin
  //use MFMT instead of MDTM because that always takes the time as Universal
  //time (the most accurate).
  if IsExtSupported('MFMT') then begin {do not localize}
    LCmd := 'MFMT ' + FTPLocalDateTimeToMLS(ALocalTime, False) + ' ' + AFileName; {do not localize}
  end

  //Syntax 1 - MDTM [Time in GMT format] Filename
  else if (IndexOfFeatLine('MDTM YYYYMMDDHHMMSS[+-TZ];filename') > 0) or IsIIS then begin {do not localize}
    //we use the new method
    LCmd := 'MDTM ' + FTPLocalDateTimeToMLS(ALocalTime, False) + ' ' + AFileName; {do not localize}
  end

  //Syntax 2 -  MDTM yyyymmddhhmmss[+-minutes from Universal Time] Filename
  //use old method for old versions of Serv-U and BPFTP Server
  else if (IndexOfFeatLine('MDTM YYYYMMDDHHMMSS[+-TZ] filename') > 0) or IsOldServU or IsBPFTP then begin {do not localize}
    LCmd := 'MDTM '+ FTPDateTimeToMDTMD(ALocalTime, False, True) + ' ' + AFileName; {do not localize}
  end

  //syntax 3 - MDTM [local timestamp] Filename
  else if FTZInfo.FGMTOffsetAvailable then begin
    //send it relative to the server's time-zone
    LCmd := 'MDTM '+ FTPDateTimeToMDTMD(ALocalTime - OffSetFromUTC + FTZInfo.FGMTOffset, False, False) + ' ' + AFileName; {do not localize}
  end
  
  else begin
    LCmd := 'MDTM '+ FTPDateTimeToMDTMD(ALocalTime, False, False) + ' ' + AFileName; {do not localize}
  end;

  // When using MDTM, Titan FTP 5 returns 200 and vsFTPd returns 213
  SendCmd(LCmd, [200, 213, 253]);
end;

{
Note from:
http://www.ftpvoyager.com/releasenotes10x.asp
====
Added support for RFC change and the MDTM. MDTM requires sending the server
GMT (UTC) instead of a "fixed" date and time. FTP Voyager supports this with
Serv-U automatically by checking the Serv-U version number and by checking the
response to the FEAT command for MDTM. Servers returning "MDTM" or
"MDTM YYYYMMDDHHMMSS[+-TZ] filename" will use the old method. Servers
returning "MDTM YYYYMMDDHHMMSS" only will use the new method where the date a
and time is GMT (UTC).
===
}
procedure TIdFTP.SetModTimeGMT(const AFileName : String; const AGMTTime: TDateTime);
var
  LCmd: String;
begin
  //use MFMT instead of MDTM because that always takes the time as Universal
  //time (the most accurate).
  if IsExtSupported('MFMT') then begin {do not localize}
    LCmd := 'MFMT ' + FTPGMTDateTimeToMLS(AGMTTime) + ' ' + AFileName; {do not localize}
  end

  //Syntax 1 - MDTM [Time in GMT format] Filename
  else if (IndexOfFeatLine('MDTM YYYYMMDDHHMMSS[+-TZ];filename') > 0) or IsIIS then begin {do not localize}
    //we use the new method
    LCmd := 'MDTM ' + FTPGMTDateTimeToMLS(AGMTTime, False) + ' ' + AFileName; {do not localize}
  end
  
  //Syntax 2 -  MDTM yyyymmddhhmmss[+-minutes from Universal Time] Filename
  //use old method for old versions of Serv-U and BPFTP Server
  else if (IndexOfFeatLine('MDTM YYYYMMDDHHMMSS[+-TZ] filename') > 0) or IsOldServU or IsBPFTP then begin {do not localize}
    LCmd := 'MDTM '+ FTPDateTimeToMDTMD(AGMTTime + OffSetFromUTC, False, True) + ' ' + AFileName; {do not localize}
  end
  
  //syntax 3 - MDTM [local timestamp] Filename
  else if FTZInfo.FGMTOffsetAvailable then begin
    //send it relative to the server's time-zone
    LCmd := 'MDTM '+ FTPDateTimeToMDTMD(AGMTTime + FTZInfo.FGMTOffset, False, False) + ' ' + AFileName; {do not localize}
  end

  else begin
    LCmd := 'MDTM '+ FTPDateTimeToMDTMD(AGMTTime + OffSetFromUTC, False, False) + ' ' + AFileName; {do not localize}
  end;

  // When using MDTM, Titan FTP 5 returns 200 and vsFTPd returns 213
  SendCmd(LCmd, [200, 213, 253]);
end;

{Improvement from Tobias Giesen http://www.superflexible.com
His notation is below:

"here's a fix for TIdFTP.IndexOfFeatLine. It does not work the
way it is used in TIdFTP.SetModTime, because it only
compares the first word of the FeatLine." }
function TIdFTP.IndexOfFeatLine(const AFeatLine: String): Integer;
var
  LBuf : String;
  LNoSpaces: Boolean;
begin
  LNoSpaces := IndyPos(' ', AFeatLine) = 0;
  for Result := 0 to FCapabilities.Count -1 do begin
    LBuf := TrimLeft(FCapabilities[Result]);
    // RLebeau: why Fetch() if no spaces are present?
    if LNoSpaces then begin
      LBuf := Fetch(LBuf);
    end;
    if TextIsSame(AFeatLine, LBuf) then begin
      Exit;
    end;
  end;
  Result := -1;
end;

{ TIdFTPTZInfo }

procedure TIdFTPTZInfo.Assign(Source: TPersistent);
var
  LSource: TIdFTPTZInfo;
begin
  if Source is TIdFTPTZInfo then begin
    LSource := TIdFTPTZInfo(Source);
    FGMTOffset := LSource.GMTOffset;
    FGMTOffsetAvailable := LSource.GMTOffsetAvailable;
  end else begin
    inherited Assign(Source);
  end;
end;

function TIdFTP.IsSiteZONESupported: Boolean;
var
  LFacts : TStrings;
  i : Integer;
begin
  Result := False;
  if IsServerMDTZAndListTForm then begin
    Result := True;
    Exit;
  end;
  LFacts := TStringList.Create;
  try
    ExtractFeatFacts('SITE', LFacts);
    for i := 0 to LFacts.Count-1 do begin
      if TextIsSame(LFacts[i], 'ZONE') then begin {do not localize}
        Result := True;
        Exit;
      end;
    end;
  finally
    FreeAndNil(LFacts);
  end;
end;

procedure TIdFTP.SetTZInfo(const Value: TIdFTPTZInfo);
begin
  FTZInfo.Assign(Value);
end;

function TIdFTP.IsOldServU: Boolean;
begin
  Result := TextStartsWith(FServerDesc, 'Serv-U ');  {do not localize}
end;

function TIdFTP.IsBPFTP : Boolean;
begin
  Result := TextStartsWith(FServerDesc, 'BPFTP Server ');  {do not localize}
end;

function TIdFTP.IsTitan : Boolean;
begin
  Result := TextStartsWith(FServerDesc, 'TitanFTP server ') or {do not localize}
            TextStartsWith(FServerDesc, 'Titan FTP Server '); {do not localize}
end;

function TIdFTP.IsWSFTP : Boolean;
begin
  Result := IndyPos('WS_FTP Server', FServerDesc) > 0; {do not localize}
end;

function TIdFTP.IsIIS: Boolean;
begin
  Result := TextStartsWith(FServerDesc, 'Microsoft FTP Service'); {do not localize}
end;
function TIdFTP.IsServerMDTZAndListTForm: Boolean;
begin
  Result := IsOldServU or IsBPFTP or IsTitan;
end;

procedure TIdFTP.Notification(AComponent: TComponent; Operation: TOperation);
begin
  if (Operation = opRemove) and (AComponent = FCompressor) then begin
    SetCompressor(nil);
  end;
  inherited Notification(AComponent, Operation);
end;

procedure TIdFTP.SendPret(const ACommand: String);
begin
  if IsExtSupported('PRET') then begin {do not localize}
    //note that we don't check for success or failure here
    //as some servers might fail and then succede with the transfer.
    //Pret might not work for some commands.
    SendCmd('PRET ' + ACommand); {do not localize}
  end;
end;

procedure TIdFTP.List;
begin
  List(nil);
end;

procedure TIdFTP.List(const ASpecifier: string; ADetails: Boolean);
begin
  List(nil, ASpecifier, ADetails);
end;

procedure TIdFTP.DoOnBannerAfterLogin(AText: TStrings);
begin
  if Assigned(OnBannerAfterLogin) then begin
    OnBannerAfterLogin(Self, AText.Text);
  end;
end;

procedure TIdFTP.DoOnBannerBeforeLogin(AText: TStrings);
begin
  if Assigned(OnBannerBeforeLogin) then begin
    OnBannerBeforeLogin(Self, AText.Text);
  end;
end;

procedure TIdFTP.DoOnBannerWarning(AText: TStrings);
begin
  if Assigned(OnBannerWarning) then begin
    OnBannerWarning(Self, AText.Text);
  end;
end;

procedure TIdFTP.SetDataPortProtection(AValue: TIdFTPDataPortSecurity);
begin
  if IsLoading then begin
    FDataPortProtection := AValue;
    Exit;
  end;
  if FDataPortProtection <> AValue then begin
    if FUseTLS = utNoTLSSupport then begin
      raise EIdFTPNoDataPortProtectionWOEncryption.Create(RSFTPNoDataPortProtectionWOEncryption);
    end;
    if FUsingCCC then begin
      raise EIdFTPNoDataPortProtectionAfterCCC.Create(RSFTPNoDataPortProtectionAfterCCC);
    end;
    FDataPortProtection := AValue;
  end;
end;

procedure TIdFTP.SetAUTHCmd(const AValue : TAuthCmd);
begin
  if IsLoading then begin
    FAUTHCmd := AValue;
    Exit;
  end;
  if FAUTHCmd <> AValue then begin
    if FUseTLS = utNoTLSSupport then begin
      raise EIdFTPNoAUTHWOSSL.Create(RSFTPNoAUTHWOSSL);
    end;
    if FUsingSFTP then begin
      raise EIdFTPCanNotSetAUTHCon.Create(RSFTPNoAUTHCon);
    end;
    FAUTHCmd := AValue;
  end;
end;

procedure TIdFTP.SetDefStringEncoding(AValue: IIdTextEncoding);
begin
  FDefStringEncoding := AValue;
  if IOHandler <> nil then begin
    IOHandler.DefStringEncoding := FDefStringEncoding;
  end;
end;

procedure TIdFTP.SetUseTLS(AValue: TIdUseTLS);
begin
  inherited SetUseTLS(AValue);
  if IsLoading then begin
    Exit;
  end;
  if AValue = utNoTLSSupport then begin
    FDataPortProtection := Id_TIdFTP_DataPortProtection;
    FUseCCC := DEF_Id_FTP_UseCCC;
    FAUTHCmd := DEF_Id_FTP_AUTH_CMD;
  end;
end;

procedure TIdFTP.SetUseCCC(const AValue: Boolean);
begin
  if (not IsLoading) and (FUseTLS = utNoTLSSupport) then begin
    raise EIdFTPNoCCCWOEncryption.Create(RSFTPNoCCCWOEncryption);
  end;
  FUseCCC := AValue;
end;

procedure TIdFTP.DoOnRetrievedDir;
begin
  if Assigned(OnRetrievedDir) then begin
    OnRetrievedDir(Self);
  end;
end;

procedure TIdFTP.DoOnDirParseEnd;
begin
  if Assigned(FOnDirParseEnd) then begin
    FOnDirParseEnd(Self);
  end;
end;

procedure TIdFTP.DoOnDirParseStart;
begin
  if Assigned(FOnDirParseStart) then begin
    FOnDirParseStart(Self);
  end;
end;

//we do this to match some WS-FTP Pro firescripts I saw
function TIdFTP.IsAccountNeeded: Boolean;
begin
  Result := LastCmdResult.NumericCode = 332;
  if not Result then begin
    if IndyPos('ACCOUNT', LastCmdResult.Text.Text) > 0 then begin  {do not localize}
      Result := FAccount <> '';
    end;
  end;
end;

//we can use one of three commands for verifying a file or stream
function TIdFTP.GetSupportsVerification: Boolean;
begin
  Result := Connected;
  if Result then begin
    Result := TIdHashSHA512.IsAvailable and IsExtSupported('XSHA512');
    if not Result then begin
      Result := TIdHashSHA256.IsAvailable and IsExtSupported('XSHA256');
    end;
    if not Result then begin
      Result := IsExtSupported('XSHA1') or
        (IsExtSupported('XMD5') and (not GetFIPSMode)) or
        IsExtSupported('XCRC');
    end;
  end;
end;

function TIdFTP.VerifyFile(const ALocalFile, ARemoteFile: String; const AStartPoint, AByteCount: TIdStreamSize): Boolean;
var
  LLocalStream: TStream;
  LRemoteFileName : String;
begin
  LRemoteFileName := ARemoteFile;
  if LRemoteFileName = '' then begin
    LRemoteFileName := ExtractFileName(ALocalFile);
  end;
  LLocalStream := TIdReadFileExclusiveStream.Create(ALocalFile);
  try
    Result := VerifyFile(LLocalStream, LRemoteFileName, AStartPoint, AByteCount);
  finally
    FreeAndNil(LLocalStream);
  end;
end;

{
This procedure can use three possible commands to verify file integriety and the
syntax does very amoung these.  The commands are:

XSHA1 - get SHA1 checksum for a file or file part
XMD5 - get MD5 checksum for a file or file part
XCRC - get CRC32 checksum

The command preference is from first to last (going from longest length to shortest).
}
function TIdFTP.VerifyFile(ALocalFile: TStream; const ARemoteFile: String;
  const AStartPoint, AByteCount: TIdStreamSize): Boolean;
var
  LRemoteCRC : String;
  LLocalCRC : String;
  LCmd : String;
  LRemoteFile: String;
  LStartPoint : TIdStreamSize;
  LByteCount : TIdStreamSize;  //used instead of AByteCount so we don't exceed the file size
  LHashClass: TIdHashClass;
  LHash: TIdHash;
begin
  LLocalCRC := '';
  LRemoteCRC := '';

  if AStartPoint > -1 then begin
    ALocalFile.Position := AStartPoint;
  end;

  LStartPoint := ALocalFile.Position;
  LByteCount := ALocalFile.Size - LStartPoint;

  if (LByteCount > AByteCount) and (AByteCount > 0) then begin
    LByteCount := AByteCount;
  end;

  //just in case the server doesn't support file names in quotes.
  if IndyPos(' ', ARemoteFile) > 0 then begin
    LRemoteFile := '"' + ARemoteFile + '"';
  end else begin
    LRemoteFile := ARemoteFile;
  end;

  if TIdHashSHA512.IsAvailable and IsExtSupported('XSHA512') then begin
    //XSHA256 <sp> pathname [<sp> startposition <sp> endposition]
    LCmd := 'XSHA512 ' + LRemoteFile;
    if AByteCount > 0 then begin
      LCmd := LCmd + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LByteCount);
    end
    else if AStartPoint > 0 then begin
      LCmd := LCmd + ' ' + IntToStr(LStartPoint);
    end;
    LHashClass := TIdHashSHA512;
  end
  else if TIdHashSHA256.IsAvailable and IsExtSupported('XSHA256') then begin
    //XSHA256 <sp> pathname [<sp> startposition <sp> endposition]
    LCmd := 'XSHA256 ' + LRemoteFile;
    if AByteCount > 0 then begin
      LCmd := LCmd + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LByteCount);
    end
    else if AStartPoint > 0 then begin
      LCmd := LCmd + ' ' + IntToStr(LStartPoint);
    end;
    LHashClass := TIdHashSHA256;
  end
  else if IsExtSupported('XSHA1') then begin
    //XMD5 "filename" startpos endpos
    //I think there's two syntaxes to this:
    //
    //Raiden Syntax if FEAT line contains " XMD5 filename;start;end"
    //
    //or what's used by some other servers if "FEAT line contains XMD5"
    //
    //XCRC "filename" [startpos] [number of bytes to calc]

    if IndexOfFeatLine('XSHA1 filename;start;end') > -1 then begin
      LCmd := 'XSHA1 ' + LRemoteFile + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LStartPoint + LByteCount-1);
    end else
    begin
      //BlackMoon FTP Server uses this one.
      LCmd := 'XSHA1 ' + LRemoteFile;
      if AByteCount > 0 then begin
        LCmd := LCmd + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LByteCount);
      end
      else if AStartPoint > 0 then begin
        LCmd := LCmd + ' ' + IntToStr(LStartPoint);
      end;
    end;
    LHashClass := TIdHashSHA1;
  end
  else if IsExtSupported('XMD5') and (not GetFIPSMode) then begin
    //XMD5 "filename" startpos endpos
    //I think there's two syntaxes to this:
    //
    //Raiden Syntax if FEAT line contains " XMD5 filename;start;end"
    //
    //or what's used by some other servers if "FEAT line contains XMD5"
    //
    //XCRC "filename" [startpos] [number of bytes to calc]

    if IndexOfFeatLine('XMD5 filename;start;end') > -1 then begin
      LCmd := 'XMD5 ' + LRemoteFile + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LStartPoint + LByteCount-1);
    end else
    begin
      //BlackMoon FTP Server uses this one.
      LCmd := 'XMD5 ' + LRemoteFile;
      if AByteCount > 0 then begin
        LCmd := LCmd + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LByteCount);
      end
      else if AStartPoint > 0 then begin
        LCmd := LCmd + ' ' + IntToStr(LStartPoint);
      end;
    end;
    LHashClass := TIdHashMessageDigest5;
  end else
  begin
    LCmd := 'XCRC ' + LRemoteFile;
    if AByteCount > 0 then begin
      LCmd := LCmd + ' ' + IntToStr(LStartPoint) + ' ' + IntToStr(LByteCount);
    end
    else if AStartPoint > 0 then begin
      LCmd := LCmd + ' ' + IntToStr(LStartPoint);
    end;
    LHashClass := TIdHashCRC32;
  end;

  LHash := LHashClass.Create;
  try
    LLocalCRC := LHash.HashStreamAsHex(ALocalFile, LStartPoint, LByteCount);
  finally
    LHash.Free;
  end;

  if SendCmd(LCmd) = 250 then begin
    LRemoteCRC := Trim(LastCmdResult.Text.Text);
    IdDelete(LRemoteCRC, 1, IndyPos(' ', LRemoteCRC)); // delete the response
    Result := TextIsSame(LLocalCRC, LRemoteCRC);
  end else begin
    Result := False;
  end;
end;

end.


