123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779
/**
 * Author:          Lester L. Martin II
 *                  UWB, bobef
 * Copyright:       (c) Lester L. Martin II
 *                  UWB, bobef
 * Based upon prior FtpClient.d 
 * License:         BSD style: $(LICENSE)
 * Initial release:  August 8, 2008  
 */

module tango.net.ftp.FtpClient;

private 
{
    import tango.net.ftp.Telnet;
    import tango.net.device.Berkeley;
    import tango.text.Util;
    import tango.time.Clock;
    import tango.text.Regex: Regex;
    import tango.time.chrono.Gregorian;
    import tango.core.Array;
    import tango.net.device.Socket;
    import tango.io.device.Conduit;
    import tango.io.device.Array;
    import tango.io.device.File;

    import Text = tango.text.Util;
    import Ascii = tango.text.Ascii;
    import Integer = tango.text.convert.Integer;
    import Timestamp = tango.text.convert.TimeStamp;
}

/******************************************************************************
 An FTP progress delegate.
 
 You may need to add the restart position to this, and use SIZE to determine
 percentage completion.  This only represents the number of bytes
 transferred.
 
 Params:
 pos =                 the current offset into the stream
 ******************************************************************************/
alias void delegate(in size_t pos) FtpProgress;

/******************************************************************************
 The format of data transfer.
 ******************************************************************************/
enum FtpFormat 
{
    /**********************************************************************
     Indicates ASCII NON PRINT format (line ending conversion to CRLF.)
     **********************************************************************/
    ascii,
    /**********************************************************************
     Indicates IMAGE format (8 bit binary octets.)
     **********************************************************************/
    image,
}

/******************************************************************************
 A FtpAddress structure that contains all 
 that is needed to access a FTPConnection; Contributed by Bobef
 
 Since: 0.99.8
 ******************************************************************************/
struct FtpAddress 
{
    static FtpAddress* opCall(const(char)[] str) {
        if(str.length == 0)
            return null;
        try {
            auto ret = new FtpAddress;
            //remove ftp://
            auto i = locatePattern(str, "ftp://");
            if(i == 0)
                str = str[6 .. $];

            //check for username and/or password user[:pass]@
            i = locatePrior(str, '@');
            if(i != str.length) {
                const(char)[] up = str[0 .. i];
                str = str[i + 1 .. $];
                i = locate(up, ':');
                if(i != up.length) {
                    ret.user = up[0 .. i];
                    ret.pass = up[i + 1 .. $];
                } else
                    ret.user = up;
            }

            //check for port
            i = locatePrior(str, ':');
            if(i != str.length) {
                ret.port = cast(uint) Integer.toLong(str[i + 1 .. $]);
                str = str[0 .. i];
            }

            //check any directories after the adress
            i = locate(str, '/');
            if(i != str.length)
                ret.directory = str[i + 1 .. $];

            //the rest should be the address
            ret.address = str[0 .. i];
            if(ret.address.length == 0)
                return null;

            return ret;

        } catch(Throwable o) {
            return null;
        }
    }

    const(char)[] address;
    const(char)[] directory;
    const(char)[] user = "anonymous";
    const(char)[] pass = "anonymous@anonymous";
    uint port = 21;
}

/******************************************************************************
 A server response, consisting of a code and a potentially multi-line 
 message.
 ******************************************************************************/
struct FtpResponse 
{
    /**********************************************************************
     The response code.
     
     The digits in the response code can be used to determine status
     programatically.
     
     First Digit (status):
     1xx =             a positive, but preliminary, reply
     2xx =             a positive reply indicating completion
     3xx =             a positive reply indicating incomplete status
     4xx =             a temporary negative reply
     5xx =             a permanent negative reply
     
     Second Digit (subject):
     x0x =             condition based on syntax
     x1x =             informational
     x2x =             connection
     x3x =             authentication/process
     x5x =             file system
     **********************************************************************/
    char[3] code = "000";

    /*********************************************************************
     The message from the server.
     
     With some responses, the message may contain parseable information.
     For example, this is true of the 257 response.
     **********************************************************************/
    const(char)[] message = null;
}

/******************************************************************************
 Active or passive connection mode.
 ******************************************************************************/
enum FtpConnectionType 
{
    /**********************************************************************
     Active - server connects to client on open port.
     **********************************************************************/
    active,
    /**********************************************************************
     Passive - server listens for a connection from the client.
     **********************************************************************/
    passive,
}

/******************************************************************************
 Detail about the data connection.
 
 This is used to properly send PORT and PASV commands.
 ******************************************************************************/
struct FtpConnectionDetail 
{
    /**********************************************************************
     The type to be used.
     **********************************************************************/
    FtpConnectionType type = FtpConnectionType.passive;

    /**********************************************************************
     The address to give the server.
     **********************************************************************/
    Address address = null;

    /**********************************************************************
     The address to actually listen on.
     **********************************************************************/
    Address listen = null;
}

/******************************************************************************
 A supported feature of an FTP server.
 ******************************************************************************/
struct FtpFeature 
{
    /**********************************************************************
     The command which is supported, e.g. SIZE.
     **********************************************************************/
    const(char)[] command = null;
    /**********************************************************************
     Parameters for this command; e.g. facts for MLST.
     **********************************************************************/
    const(char)[] params = null;
}

/******************************************************************************
 The type of a file in an FTP listing.
 ******************************************************************************/
enum FtpFileType 
{
    /**********************************************************************
     An unknown file or type (no type fact.)
     **********************************************************************/
    unknown,
    /**********************************************************************
     A regular file, or similar.
     **********************************************************************/
    file,
    /**********************************************************************
     The current directory (e.g. ., but not necessarily.)
     **********************************************************************/
    cdir,
    /**********************************************************************
     A parent directory (usually "..".)
     **********************************************************************/
    pdir,
    /**********************************************************************
     Any other type of directory.
     **********************************************************************/
    dir,
    /**********************************************************************
     Another type of file.  Consult the "type" fact.
     **********************************************************************/
    other,
}

/******************************************************************************
 Information about a file in an FTP listing.
 ******************************************************************************/
struct FtpFileInfo 
{
    /**********************************************************************
     The filename.
     **********************************************************************/
    const(char)[] name = null;
    /**********************************************************************
     Its type.
     **********************************************************************/
    FtpFileType type = FtpFileType.unknown;
    /**********************************************************************
     Size in bytes (8 bit octets), or ulong.max if not available.
     Since: 0.99.8
     **********************************************************************/
    ulong size = ulong.max;
    /**********************************************************************
     Modification time, if available.
     **********************************************************************/
    Time modify = Time.max;
    /**********************************************************************
     Creation time, if available (not often.)
     **********************************************************************/
    Time create = Time.max;
    /**********************************************************************
     The file's mime type, if known.
     **********************************************************************/
    const(char)[] mime = null;
    /***********************************************************************
     An associative array of all facts returned by the server, lowercased.
     ***********************************************************************/
    const(char)[][const(char)[]] facts;
}

/*******************************************************************************
 Changed location Since: 0.99.8
 Documentation Pending
 *******************************************************************************/
class FtpException: Exception 
{
    char[3] responseCode_ = "000";

    /***********************************************************************
     Construct an FtpException based on a message and code.
     
     Params:
     message =         the exception message
     code =            the code (5xx for fatal errors)
     ***********************************************************************/
    this(string message, char[3] code = "420") {
        this.responseCode_[] = code;
        super(message);
    }

    /***********************************************************************
     Construct an FtpException based on a response.
     
     Params:
     r =               the server response
     ***********************************************************************/
    this(FtpResponse r) {
        this.responseCode_[] = r.code;
        super(r.message.idup);
    }

    /***********************************************************************
     A string representation of the error.
     ***********************************************************************/
    override string toString() {
        char[] buffer = new char[this.msg.length + 4];

        buffer[0 .. 3] = this.responseCode_;
        buffer[3] = ' ';
        buffer[4 .. buffer.length] = this.msg;

        return buffer.idup;
    }
}

/*******************************************************************************
 Seriously changed Since: 0.99.8
 Documentation pending
 *******************************************************************************/
class FTPConnection: Telnet 
{

    FtpFeature[] supportedFeatures_ = null;
    FtpConnectionDetail inf_;
    size_t restartPos_ = 0;
    const(char)[] currFile_ = "";
    Socket dataSocket_;
    TimeSpan timeout_ = TimeSpan.fromMillis(5000);

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    @property public TimeSpan timeout() {
        return timeout_;
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    @property public void timeout(TimeSpan t) {
        timeout_ = t;
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    public TimeSpan shutdownTime() {
        return timeout_ + timeout_;
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    public FtpFeature[] supportedFeatures() {
        if(supportedFeatures_ !is null) {
            return supportedFeatures_;
        }
        getFeatures();
        return supportedFeatures_;
    }

    /***********************************************************************
     Changed Since: 0.99.8
     ***********************************************************************/
    override void exception(string message) {
        throw new FtpException(message);
    }

    /***********************************************************************
     Changed Since: 0.99.8
     ***********************************************************************/
    void exception(FtpResponse fr) {
        exception(fr.message.idup);
    }

    public this() {

    }

    public this(const(char)[] hostname, const(char)[] username = "anonymous",
            const(char)[] password = "anonymous@anonymous", uint port = 21) {
        this.connect(hostname, username, password, port);
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    public this(FtpAddress fad) {
        connect(fad);
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    public void connect(FtpAddress fad) {
        this.connect(fad.address, fad.user, fad.pass, fad.port);
    }

    /************************************************************************
     Changed Since: 0.99.8
     ************************************************************************/
    public void connect(const(char)[] hostname, const(char)[] username = "anonymous",
            const(char)[] password = "anonymous@anonymous", uint port = 21)
    in {
        // We definitely need a hostname and port.
        assert(hostname.length > 0);
        assert(port > 0);
    }
    body {

        if(socket_ !is null) {
            socket_.close();
        }

        this.findAvailableServer(hostname, port);

        scope(failure) {
            close();
        }

        readResponse("220");

        if(username.length == 0) {
            return;
        }

        sendCommand("USER", username);
        FtpResponse response = readResponse();

        if(response.code == "331") {
            sendCommand("PASS", password);
            response = readResponse();
        }

        if(response.code != "230" && response.code != "202") {
            exception(response);
        }
    }

    public void close() {
        //make sure no open data connection and if open data connection then kill
        if(dataSocket_ !is null)
            this.finishDataCommand(dataSocket_);
        if(socket_ !is null) {
            try {
                sendCommand("QUIT");
                readResponse("221");
            } catch(FtpException) {

            }

            socket_.close();

            delete supportedFeatures_;
            delete socket_;
        }
    }

    public void setPassive() {
        inf_.type = FtpConnectionType.passive;

        delete inf_.address;
        delete inf_.listen;
    }

    public void setActive(const(char)[] ip, ushort port, const(char)[] listen_ip = null,
            ushort listen_port = 0)
    in {
        assert(ip.length > 0);
        assert(port > 0);
    }
    body {
        inf_.type = FtpConnectionType.active;
        inf_.address = new IPv4Address(ip, port);

        // A local-side port?
        if(listen_port == 0)
            listen_port = port;

        // Any specific IP to listen on?
        if(listen_ip == null)
            inf_.listen = new IPv4Address(IPv4Address.ADDR_ANY, listen_port);
        else
            inf_.listen = new IPv4Address(listen_ip, listen_port);
    }

    public void cd(const(char)[] dir)
    in {
        assert(dir.length > 0);
    }
    body {
        sendCommand("CWD", dir);
        readResponse("250");
    }

    public void cdup() {
        sendCommand("CDUP");
        FtpResponse fr = readResponse();
        if(fr.code == "200" || fr.code == "250")
            return;
        else
            exception(fr);
    }

    public const(char)[] cwd() {
        sendCommand("PWD");
        auto response = readResponse("257");

        return parse257(response);
    }

    public void chmod(const(char)[] path, int mode)
    in {
        assert(path.length > 0);
        assert(mode >= 0 && (mode >> 16) == 0);
    }
    body {
        char[3] tmp = "000";
        // Convert our octal parameter to a string.
        Integer.format(tmp, cast(long) mode, "o");
        sendCommand("SITE CHMOD", tmp, path);
        readResponse("200");
    }

    public void del(const(char)[] path)
    in {
        assert(path.length > 0);
    }
    body {
        sendCommand("DELE", path);
        auto response = readResponse("250");

        //Try it as a directory, then...?
        if(response.code != "250")
            rm(path);
    }

    public void rm(const(char)[] path)
    in {
        assert(path.length > 0);
    }
    body {
        sendCommand("RMD", path);
        readResponse("250");
    }

    public void rename(const(char)[] old_path, const(char)[] new_path)
    in {
        assert(old_path.length > 0);
        assert(new_path.length > 0);
    }
    body {
        // Rename from... rename to.  Pretty simple.
        sendCommand("RNFR", old_path);
        readResponse("350");

        sendCommand("RNTO", new_path);
        readResponse("250");
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    int exist(const(char)[] file) {
        try {
            auto fi = getFileInfo(file);
            if(fi.type == FtpFileType.file) {
                return 1;
            } else if(fi.type == FtpFileType.dir || fi.type == FtpFileType.cdir || fi.type == FtpFileType.pdir) {
                return 2;
            }
        } catch(FtpException o) {
            if(o.responseCode_ != "501") {
                return 0;
            }
        }
        return 0;
    }

    public size_t size(const(char)[] path, FtpFormat format = FtpFormat.image)
    in {
        assert(path.length > 0);
    }
    body {
        type(format);

        sendCommand("SIZE", path);
        auto response = this.readResponse("213");

        // Only try to parse the numeric bytes of the response.
        size_t end_pos = 0;
        while(end_pos < response.message.length) {
            if(response.message[end_pos] < '0' || response.message[end_pos] > '9')
                break;
            end_pos++;
        }

        return cast(int) Integer.parse((response.message[0 .. end_pos]));
    }

    public void type(FtpFormat format) {
        if(format == FtpFormat.ascii)
            sendCommand("TYPE", "A");
        else
            sendCommand("TYPE", "I");

        readResponse("200");
    }

    /***********************************************************************
     Added Since: 0.99.8
     ***********************************************************************/
    Time modified(const(char)[] file)
    in {
        assert(file.length > 0);
    }
    body {
        this.sendCommand("MDTM", file);
        auto response = this.readResponse("213");

        // The whole response should be a timeval.
        return this.parseTimeval(response.message);
    }

    protected Time parseTimeval(const(char)[] timeval) {
        if(timeval.length < 14)
            throw new FtpException("CLIENT: Unable to parse timeval", "501");

        return Gregorian.generic.toTime(
                Integer.atoi(timeval[0 .. 4]),
                Integer.atoi(timeval[4 .. 6]), 
                Integer.atoi(timeval[6 .. 8]),
                Integer.atoi(timeval[8 .. 10]),
                Integer.atoi(timeval[10 .. 12]),
                Integer.atoi(timeval[12 .. 14]));
    }

    public void noop() {
        this.sendCommand("NOOP");
        this.readResponse("200");
    }

    public const(char)[] mkdir(const(char)[] path)
    in {
        assert(path.length > 0);
    }
    body {
        this.sendCommand("MKD", path);
        auto response = this.readResponse("257");

        return this.parse257(response);
    }

    public void getFeatures() {
        this.sendCommand("FEAT");
        auto response = this.readResponse();

        // 221 means FEAT is supported, and a list follows.  Otherwise we don't know...
        if(response.code != "211")
            delete supportedFeatures_;
        else {
            const(char)[][] lines = Text.splitLines(response.message);

            // There are two more lines than features, but we also have FEAT.
            supportedFeatures_ = new FtpFeature[lines.length - 1];
            supportedFeatures_[0].command = "FEAT";

            for(size_t i = 1; i < lines.length - 1; i++) {
                size_t pos = Text.locate(lines[i], ' ');

                supportedFeatures_[i].command = lines[i][0 .. pos];
                if(pos < lines[i].length - 1)
                    supportedFeatures_[i].params = lines[i][pos + 1 .. lines[i].length];
            }

            delete lines;
        }
    }

    public void sendCommand(const(char)[] command, const(char)[][] parameters...) {

        const(char)[] socketCommand = command;

        // Send the command, parameters, and then a CRLF.

        foreach(const(char)[] param; parameters) {
            socketCommand ~= " " ~ param;

        }

        socketCommand ~= "\r\n";

        debug(FtpDebug) {
            Stdout.formatln("[sendCommand] Sending command '{0}'",
                    socketCommand);
        }
        sendData(socketCommand);
    }

    public FtpResponse readResponse(const(char)[] expected_code) {
        debug(FtpDebug) {
            Stdout.formatln("[readResponse] Expected Response {0}",
                    expected_code)();
        }
        auto response = readResponse();
        debug(FtpDebug) {
            Stdout.formatln("[readResponse] Actual Response {0}", response.code)();
        }

        if(response.code != expected_code)
            exception(response);

        return response;
    }

    public FtpResponse readResponse() {
        assert(this.socket_ !is null);

        // Pick a time at which we stop reading.  It can't take too long, but it could take a bit for the whole response.
        Time end_time = Clock.now + TimeSpan.fromMillis(2500) * 10;

        FtpResponse response;
        const(char)[] single_line = null;

        // Danger, Will Robinson, don't fall into an endless loop from a malicious server.
        while(Clock.now < end_time) {
            single_line = this.readLine();

            // This is the first line.
            if(response.message.length == 0) {
                // The first line must have a code and then a space or hyphen.
                // #1
                // Response might be exactly 4 chars e.g. '230-'
                // (see ftp-stud.fht-esslingen.de or ftp.sunfreeware.com)
                if(single_line.length < 4) {
                    response.code[] = "500";
                    break;
                }

                // The code is the first three characters.
                response.code[] = single_line[0 .. 3];
                response.message = single_line[4 .. single_line.length];
            }
            // This is either an extra line, or the last line.
            else {
                response.message ~= "\n";

                // If the line starts like "123-", that is not part of the response message.
                if(single_line.length > 4 && single_line[0 .. 3] == response.code)
                    response.message ~= single_line[4 .. single_line.length];
                // If it starts with a space, that isn't either.
                else if(single_line.length > 2 && single_line[0] == ' ')
                    response.message ~= single_line[1 .. single_line.length];
                else
                    response.message ~= single_line;
            }

            // We're done if the line starts like "123 ".  Otherwise we're not.
            // #1
            // Response might be exactly 4 chars e.g. '220 '
            // (see ftp.knoppix.nl)
            if(single_line.length >= 4 && single_line[0 .. 3] == response.code && single_line[3] == ' ')
                break;
        }

        return response;
    }

    protected const(char)[] parse257(FtpResponse response) {
        char[] path = new char[response.message.length];
        size_t pos = 1, len = 0;

        // Since it should be quoted, it has to be at least 3 characters in length.
        if(response.message.length <= 2)
            exception(response);

        //assert (response.message[0] == '"');

        // Trapse through the response...
        while(pos < response.message.length) {
            if(response.message[pos] == '"') {
                // #2
                // Is it the last character?
                if(pos + 1 == response.message.length)
                    // then we are done
                    break;

                // An escaped quote, keep going.  False alarm.
                if(response.message[++pos] == '"')
                    path[len++] = response.message[pos];
                else
                    break;
            } else
                path[len++] = response.message[pos];

            pos++;
        }

        // Okay, done!  That wasn't too hard.
        path.length = len;
        return path;
    }

    /*******************************************************************************
     Get a data socket from the server.
     
     This sends PASV/PORT as necessary.
     
     Returns:             the data socket or a listener
     Changed Since: 0.99.8
     *******************************************************************************/
    protected Socket getDataSocket() {
        //make sure no open data connection and if open data connection then kill
        if(dataSocket_ !is null)
            this.finishDataCommand(dataSocket_);

        // What type are we using?
        switch(this.inf_.type) {
            default:
                exception("unknown connection type"); assert(0);

            // Passive is complicated.  Handle it in another member.
            case FtpConnectionType.passive:
                return this.connectPassive();

            // Active is simpler, but not as fool-proof.
            case FtpConnectionType.active:
                IPv4Address data_addr = cast(IPv4Address) this.inf_.address;

                // Start listening.
                Socket listener = new Socket;
                listener.bind(this.inf_.listen);
                listener.socket.listen(32);

                // Use EPRT if we know it's supported.
                if(this.is_supported("EPRT")) {
                    char[64] tmp = void;

                    this.sendCommand("EPRT", Text.layout(tmp, "|1|%0|%1|",
                            data_addr.toAddrString, data_addr.toPortString));
                    // this.sendCommand("EPRT", format("|1|%s|%s|", data_addr.toAddrString(), data_addr.toPortString()));
                    this.readResponse("200");
                } else {
                    int h1, h2, h3, h4, p1, p2;
                    h1 = (data_addr.addr() >> 24) % 256;
                    h2 = (data_addr.addr() >> 16) % 256;
                    h3 = (data_addr.addr() >> 8_) % 256;
                    h4 = (data_addr.addr() >> 0_) % 256;
                    p1 = (data_addr.port() >> 8_) % 256;
                    p2 = (data_addr.port() >> 0_) % 256;

                    // low overhead method to format a numerical string
                    char[64] tmp = void;
                    char[20] foo = void;
                    auto str = Text.layout(tmp, "%0,%1,%2,%3,%4,%5",
                                    Integer.format(foo[0 .. 3], h1), 
                                    Integer.format(foo[3 .. 6], h2), 
                                    Integer.format(foo[6 .. 9], h3), 
                                    Integer.format(foo[9 .. 12], h4), 
                                    Integer.format(foo[12 .. 15], p1), 
                                    Integer.format(foo[15 .. 18], p2));

                    // This formatting is weird.
                    // this.sendCommand("PORT", format("%d,%d,%d,%d,%d,%d", h1, h2, h3, h4, p1, p2));

                    this.sendCommand("PORT", str);
                    this.readResponse("200");
                }

                return listener;
        }
    }

    /*******************************************************************************
     Send a PASV and initiate a connection.
     
     Returns:             a connected socket
     Changed Since: 0.99.8
     *******************************************************************************/
version(TangoDoc)
{
    public Socket connectPassive();
}
else
{
    public Socket connectPassive() {
        Address connect_to = null;

        // SPSV, which is just a port number.
        if(this.is_supported("SPSV")) {
            this.sendCommand("SPSV");
            auto response = this.readResponse("227");

            // Connecting to the same host.
            IPv4Address
                    remote = cast(IPv4Address) this.socket_.socket.remoteAddress();
            assert(remote !is null);

            uint address = remote.addr();
            uint port = cast(int) Integer.parse(((response.message)));

            connect_to = new IPv4Address(address, cast(ushort) port);
        }
        // Extended passive mode (IP v6, etc.)
        else if(this.is_supported("EPSV")) {
            this.sendCommand("EPSV");
            auto response = this.readResponse("229");

            // Try to pull out the (possibly not parenthesized) address.
            auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`);
            if(!r.test(response.message[0 .. find(response.message, '\n')]))
                throw new FtpException("CLIENT: Unable to parse address", "501");

            IPv4Address
                    remote = cast(IPv4Address) this.socket_.socket.remoteAddress();
            assert(remote !is null);

            uint address = remote.addr();
            uint port = cast(int) Integer.parse(((r.match(1))));

            connect_to = new IPv4Address(address, cast(ushort) port);
        } else {
            this.sendCommand("PASV");
            auto response = this.readResponse("227");

            // Try to pull out the (possibly not parenthesized) address.
            auto r = Regex(`(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`);
            if(!r.test(response.message[0 .. find(response.message, '\n')]))
                throw new FtpException("CLIENT: Unable to parse address", "501");

            // Now put it into something std.socket will understand.
            const(char)[] address = r.match(1) ~ "." ~ r.match(2) ~ "." ~ r.match(3) ~ "." ~ r.match(4);
            uint port = (((cast(int) Integer.parse(r.match(5))) << 8) + (r.match(7).
                           length > 0 ? cast(int) Integer.parse(r.match(7)) : 0));

            // Okay, we've got it!
            connect_to = new IPv4Address(address, cast(ushort)port);
        }

        scope(exit)
            delete connect_to;

        // This will throw an exception if it cannot connect.
        auto sock = new Socket;
        sock.connect(connect_to);
        return sock;
    }
}

    /*
     Socket sock = new Socket();
     sock.connect(connect_to);
     return sock;
     */

    public bool isSupported(const(char)[] command)
    in {
        assert(command.length > 0);
    }
    body {
        if(this.supportedFeatures_.length == 0)
            return true;

        // Search through the list for the feature.
        foreach(FtpFeature feat; this.supportedFeatures_) {
            if(Ascii.icompare(feat.command, command) == 0)
                return true;
        }

        return false;
    }

    public bool is_supported(const(char)[] command) {
        if(this.supportedFeatures_.length == 0)
            return false;

        return this.isSupported(command);
    }

    /*******************************************************************************
     Prepare a data socket for use.
     
     This modifies the socket in some cases.
     
     Params:
     data =            the data listener socket
     Changed Since: 0.99.8
     ********************************************************************************/
    protected void prepareDataSocket(ref Socket data) {
        switch(this.inf_.type) {
            default:
                exception("unknown connection type"); assert(0);

            case FtpConnectionType.active:
                Berkeley new_data;

                scope set = new SocketSet;

                // At end_time, we bail.
                Time end_time = Clock.now + this.timeout;

                while(Clock.now < end_time) {
                    set.reset();
                    set.add(data.socket);

                    // Can we accept yet?
                    int code = set.select(set, null, null, timeout.micros);
                    if(code == -1 || code == 0)
                        break;

                    data.socket.accept(new_data);
                    break;
                }

            if(new_data.sock is new_data.sock.init)
               throw new FtpException("CLIENT: No connection from server", "420");

            // We don't need the listener anymore.
            data.shutdown().detach();

            // This is the actual socket.
            data.socket.sock = new_data.sock;
            break;

            case FtpConnectionType.passive:
            break;
        }
    }

    /*****************************************************************************
     Changed Since: 0.99.8
     *****************************************************************************/
    public void finishDataCommand(Socket data) {
        // Close the socket.  This tells the server we're done (EOF.)
        data.close();
        data.detach();

        // We shouldn't get a 250 in STREAM mode.
        FtpResponse r = readResponse();
        if(!(r.code == "226" || r.code == "420"))
            exception("Bad finish");

    }

    /*****************************************************************************
     Changed Since: 0.99.8
     *****************************************************************************/
    public Socket processDataCommand(const(char)[] command, const(char)[][] parameters...) {
        // Create a connection.
        Socket data = this.getDataSocket();
        scope(failure) {
            // Close the socket, whether we were listening or not.
            data.close();
        }

        // Tell the server about it.
        this.sendCommand(command, parameters);

        // We should always get a 150/125 response.
        auto response = this.readResponse();
        if(response.code != "150" && response.code != "125")
            exception(response);

        // We might need to do this for active connections.
        this.prepareDataSocket(data);

        return data;
    }

    public FtpFileInfo[] ls(const(char)[] path = "")
    // default to current dir
    in {
        assert(path.length == 0 || path[path.length - 1] != '/');
    }
    body {
        FtpFileInfo[] dir;

        // We'll try MLSD (which is so much better) first... but it may fail.
        bool mlsd_success = false;
        Socket data = null;

        // Try it if it could/might/maybe is supported.
        if(this.isSupported("MLST")) {
            mlsd_success = true;

            // Since this is a data command, processDataCommand handles
            // checking the response... just catch its Exception.
            try {
                if(path.length > 0)
                    data = this.processDataCommand("MLSD", path);
                else
                    data = this.processDataCommand("MLSD");
            } catch(FtpException)
                mlsd_success = false;
        }

        // If it passed, parse away!
        if(mlsd_success) {
            auto listing = new Array(256, 65536);
            this.readStream(data, listing);
            this.finishDataCommand(data);

            // Each line is something in that directory.
            const(char)[][] lines = Text.splitLines(cast(const(char)[]) listing.slice());
            scope(exit)
                delete lines;

            foreach(const(char)[] line; lines) {
                if(line.length == 0)
                    continue;
                // Parse each line exactly like MLST does.
                try {
                    FtpFileInfo info = this.parseMlstLine(line);
                    if(info.name.length > 0)
                        dir ~= info;
                } catch(FtpException) {
                    return this.sendListCommand(path);
                }
            }

            return dir;
        }
        // Fall back to LIST.
        else
            return this.sendListCommand(path);
    }

    /*****************************************************************************
     Changed Since: 0.99.8
     *****************************************************************************/
    protected void readStream(Socket data, OutputStream stream,
            FtpProgress progress = null)
    in {
        assert(data !is null);
        assert(stream !is null);
    }
    body {
        // Set up a SocketSet so we can use select() - it's pretty efficient.
        scope set = new SocketSet;

        // At end_time, we bail.
        Time end_time = Clock.now + this.timeout;

        // This is the buffer the stream data is stored in.
        ubyte[8 * 1024] buf;
        int buf_size = 0;

        bool completed = false;
        size_t pos;
        while(Clock.now < end_time) {
            set.reset();
            set.add(data.socket);

            // Can we read yet, can we read yet?
            int code = set.select(set, null, null, timeout.micros);
            if(code == -1 || code == 0)
                break;

            buf_size = data.socket.receive(buf);
            if(buf_size == data.socket.ERROR)
                break;

            if(buf_size == 0) {
                completed = true;
                break;
            }

            stream.write(buf[0 .. buf_size]);

            pos += buf_size;
            if(progress !is null)
                progress(pos);

            // Give it more time as long as data is going through.
            end_time = Clock.now + this.timeout;
        }

        // Did all the data get received?
        if(!completed)
            throw new FtpException("CLIENT: Timeout when reading data", "420");
    }

    /*****************************************************************************
     Changed Since: 0.99.8
     *****************************************************************************/
    protected void sendStream(Socket data, InputStream stream,
            FtpProgress progress = null)
    in {
        assert(data !is null);
        assert(stream !is null);
    }
    body {
        // Set up a SocketSet so we can use select() - it's pretty efficient.
        scope set = new SocketSet;

        // At end_time, we bail.
        Time end_time = Clock.now + this.timeout;

        // This is the buffer the stream data is stored in.
        ubyte[8 * 1024] buf;
        size_t buf_size = 0, buf_pos = 0;
        int delta = 0;

        size_t pos = 0;
        bool completed = false;
        while(!completed && Clock.now < end_time) {
            set.reset();
            set.add(data.socket);

            // Can we write yet, can we write yet?
            int code = set.select(null, set, null, timeout.micros);
            if(code == -1 || code == 0)
                break;

            if(buf_size - buf_pos <= 0) {
                if((buf_size = stream.read(buf)) is stream.Eof)
                    buf_size = 0 , completed = true;
                buf_pos = 0;
            }

            // Send the chunk (or as much of it as possible!)
            delta = data.socket.send(buf[buf_pos .. buf_size]);
            if(delta == data.socket.ERROR)
                break;

            buf_pos += delta;

            pos += delta;
            if(progress !is null)
                progress(pos);

            // Give it more time as long as data is going through.
            if(delta != 0)
                end_time = Clock.now + this.timeout;
        }

        // Did all the data get sent?
        if(!completed)
            throw new FtpException("CLIENT: Timeout when sending data", "420");
    }

    protected FtpFileInfo[] sendListCommand(const(char)[] path) {
        FtpFileInfo[] dir;
        Socket data = null;

        if(path.length > 0)
            data = this.processDataCommand("LIST", path);
        else
            data = this.processDataCommand("LIST");

        // Read in the stupid non-standardized response.
        auto listing = new Array(256, 65536);
        this.readStream(data, listing);
        this.finishDataCommand(data);

        // Split out the lines.  Most of the time, it's one-to-one.
        const(char)[][] lines = Text.splitLines(cast(const(char)[]) listing.slice());
        scope(exit)
            delete lines;

        foreach(const(char)[] line; lines) {
            if(line.length == 0)
                continue;
            // If there are no spaces, or if there's only one... skip the line.
            // This is probably like a "total 8" line.
            if(Text.locate(line, ' ') == Text.locatePrior(line, cast(const(char))' '))
                continue;

            // Now parse the line, or try to.
            FtpFileInfo info = this.parseListLine(line);
            if(info.name.length > 0)
                dir ~= info;
        }

        return dir;
    }

    protected FtpFileInfo parseListLine(const(char)[] line) {
        FtpFileInfo info;
        size_t pos = 0;

        // Convenience function to parse a word from the line.
        const(char)[] parse_word() {
            size_t start = 0, end = 0;

            // Skip whitespace before.
            while(pos < line.length && line[pos] == ' ')
                pos++;

            start = pos;
            while(pos < line.length && line[pos] != ' ')
                pos++;
            end = pos;

            // Skip whitespace after.
            while(pos < line.length && line[pos] == ' ')
                pos++;

            return line[start .. end];
        }

        // We have to sniff this... :/.
        switch(!Text.contains("0123456789", line[0])) {
            // Not a number; this is UNIX format.
            case true:
                // The line must be at least 20 characters long.
                if(line.length < 20)
                    return info;

                // The first character tells us what it is.
                if(line[0] == 'd')
                    info.type = FtpFileType.dir;
                // #3
                // Might be a link entry - additional test down below
                else if(line[0] == 'l')
                    info.type = FtpFileType.other;
                else if(line[0] == '-')
                    info.type = FtpFileType.file;
                else
                    info.type = FtpFileType.unknown;

                // Parse out the mode... rwxrwxrwx = 777.
                char[] unix_mode = "0000".dup;
                void read_mode(int digit) {
                    for(pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) {
                        if(line[pos] == 'r')
                            unix_mode[digit + 1] |= 4;
                        else if(line[pos] == 'w')
                            unix_mode[digit + 1] |= 2;
                        else if(line[pos] == 'x')
                            unix_mode[digit + 1] |= 1;
                    }
                }

                // This makes it easier, huh?
                read_mode(0);
                read_mode(1);
                read_mode(2);

                info.facts["UNIX.mode"] = unix_mode;

                // #4
                // Not only parse lines like
                //    drwxrwxr-x    2 10490    100          4096 May 20  2005 Acrobat
                //    lrwxrwxrwx    1 root     other           7 Sep 21  2007 Broker.link -> Acrobat
                //    -rwxrwxr-x    1 filelib  100           468 Nov  1  1999 Web_Users_Click_Here.html
                // but also parse lines like 
                //    d--x--x--x   2 staff        512 Sep 24  2000 dev
                // (see ftp.sunfreeware.com)

                // Links, owner.  These are hard to translate to MLST facts.
                parse_word();
                parse_word();

                // Group or size in bytes
                const(char)[] group_or_size = parse_word();
                size_t oldpos = pos;

                // Size in bytes or month
                const(char)[] size_or_month = parse_word();

                if(!Text.contains("0123456789", size_or_month[0])) {
                    // Oops, no size here - go back to previous column
                    pos = oldpos;
                    info.size = cast(ulong) Integer.parse(group_or_size);
                } else
                    info.size = cast(ulong) Integer.parse(size_or_month);

                // Make sure we still have enough space.
                if(pos + 13 >= line.length)
                    return info;

                // Not parsing date for now.  It's too weird (last 12 months, etc.)
                pos += 13;

                info.name = line[pos .. line.length];
                // #3
                // Might be a link entry - additional test here
                if(info.type == FtpFileType.other) {
                    // Is name like 'name -> /some/other/path'?
                    size_t pos2 = Text.locatePattern(info.name, cast(const(char)[])" -> ");
                    if(pos2 != info.name.length) {
                        // It is a link - split into target and name
                        info.facts["target"] = info.name[pos2 + 4 .. info.name.length];
                        info.name = info.name[0 .. pos2];
                        info.facts["type"] = "link";
                    }
                }
            break;

            // A number; this is DOS format.
            case false:
                // We need some data here, to parse.
                if(line.length < 18)
                    return info;

                // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P
                auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`);
                // #5
                // wrong test
                if(!r.test(line))
                    return info;

                if(Timestamp.dostime(r.match(0), info.modify) is 0)
                    info.modify = Time.max;

                pos = r.match(0).length;
                delete r;

                // This will either be <DIR>, or a number.
                const(char)[] dir_or_size = parse_word();

                if(dir_or_size.length < 0)
                    return info;
                else if(dir_or_size[0] == '<')
                    info.type = FtpFileType.dir;
                else {
                    // #5
                    // It is a file
                    info.size = cast(ulong) Integer.parse((dir_or_size));
                    info.type = FtpFileType.file;
                }

                info.name = line[pos .. line.length];
            break;

            // Something else, not supported.
            default:
                throw new FtpException("CLIENT: Unsupported LIST format", "501");
        }

        // Try to fix the type?
        if(info.name == ".")
            info.type = FtpFileType.cdir;
        else if(info.name == "..")
            info.type = FtpFileType.pdir;

        return info;
    }

    protected FtpFileInfo parseMlstLine(const(char)[] line) {
        FtpFileInfo info;

        // After this loop, filename_pos will be location of space + 1.
        size_t filename_pos = 0;
        while(filename_pos < line.length && line[filename_pos++] != ' ')
            continue;

        if(filename_pos == line.length)
            throw new FtpException("CLIENT: Bad syntax in MLSx response", "501");
        /*{
         info.name = "";
         return info;
         }*/

        info.name = line[filename_pos .. line.length];

        // Everything else is frosting on top.
        if(filename_pos > 1) {
            const(char)[][]
                    temp_facts = Text.delimit(line[0 .. filename_pos - 1], cast(const(char)[])";");

            // Go through each fact and parse them into the array.
            foreach(const(char)[] fact; temp_facts) {
                size_t pos = Text.locate(fact, '=');
                if(pos == fact.length)
                    continue;

                info.facts[cast(immutable(char)[])Ascii.toLower(fact[0 .. pos].dup)] = fact[pos + 1 .. fact.length];
            }

            // Do we have a type?
            if("type" in info.facts) {
                // Some reflection might be nice here.
                switch(Ascii.toLower(info.facts["type"].dup)) {
                    case "file":
                        info.type = FtpFileType.file;
                    break;

                    case "cdir":
                        info.type = FtpFileType.cdir;
                    break;

                    case "pdir":
                        info.type = FtpFileType.pdir;
                    break;

                    case "dir":
                        info.type = FtpFileType.dir;
                    break;

                    default:
                        info.type = FtpFileType.other;
                }
            }

            // Size, mime, etc...
            if("size" in info.facts)
                info.size = cast(ulong) Integer.parse((info.facts["size"]));
            if("media-type" in info.facts)
                info.mime = info.facts["media-type"];

            // And the two dates.
            if("modify" in info.facts)
                info.modify = this.parseTimeval(info.facts["modify"]);
            if("create" in info.facts)
                info.create = this.parseTimeval(info.facts["create"]);
        }

        return info;
    }

    public FtpFileInfo getFileInfo(const(char)[] path)
    in {
        assert(path.length > 0);
    }
    body {
        // Start assuming the MLST didn't work.
        bool mlst_success = false;
        FtpResponse response;
        auto inf = ls(path);
        if(inf.length == 1)
            return inf[0];
        else {
            debug(FtpUnitTest) {
                Stdout("In getFileInfo.").newline.flush;
            }
            {
                // Send a list command.  This may list the contents of a directory, even.
                FtpFileInfo[] temp = this.sendListCommand(path);

                // If there wasn't at least one line, the file didn't exist?
                // We should have already handled that.
                if(temp.length < 1)
                    throw new FtpException(
                            "CLIENT: Bad LIST response from server", "501");

                // If there are multiple lines, try to return the correct one.
                if(temp.length != 1)
                    foreach(FtpFileInfo info; temp) {
                        if(info.type == FtpFileType.cdir)
                            return info;
                    }

                // Okay then, the first line.  Best we can do?
                return temp[0];
            }
        }
    }

    public void put(const(char)[] path, const(char)[] local_file,
            FtpProgress progress = null, FtpFormat format = FtpFormat.image)
    in {
        assert(path.length > 0);
        assert(local_file.length > 0);
    }
    body {
        // Open the file for reading...
        auto file = new File(local_file);
        scope(exit) {
            file.detach();
            delete file;
        }

        // Seek to the correct place, if specified.
        if(this.restartPos_ > 0) {
            file.seek(this.restartPos_);
            this.restartPos_ = 0;
        } else {
            // Allocate space for the file, if we need to.
            //this.allocate(file.length);
        }

        // Now that it's open, we do what we always do.
        this.put(path, file, progress, format);
    }

    /********************************************************************************
     Store data from a stream on the server.
     
     Calling this function will change the current data transfer format.
     
     Params:
     path =            the path to the remote file
     stream =          data to store, or null for a blank file
     progress =        a delegate to call with progress information
     format =          what format to send the data in
     ********************************************************************************/
    public void put(const(char)[] path, InputStream stream = null,
            FtpProgress progress = null, FtpFormat format = FtpFormat.image)
    in {
        assert(path.length > 0);
    }
    body {
        // Change to the specified format.
        this.type(format);

        // Okay server, we want to store something...
        Socket data = this.processDataCommand("STOR", path);

        // Send the stream over the socket!
        if(stream !is null)
            this.sendStream(data, stream, progress);

        this.finishDataCommand(data);
    }

    /********************************************************************************
     Append data to a file on the server.
     
     Calling this function will change the current data transfer format.
     
     Params:
     path =            the path to the remote file
     stream =          data to append to the file
     progress =        a delegate to call with progress information
     format =          what format to send the data in
     ********************************************************************************/
    public void append(const(char)[] path, InputStream stream,
            FtpProgress progress = null, FtpFormat format = FtpFormat.image)
    in {
        assert(path.length > 0);
        assert(stream !is null);
    }
    body {
        // Change to the specified format.
        this.type(format);

        // Okay server, we want to store something...
        Socket data = this.processDataCommand("APPE", path);

        // Send the stream over the socket!
        this.sendStream(data, stream, progress);

        this.finishDataCommand(data);
    }

    /*********************************************************************************
     Seek to a byte offset for the next transfer.
     
     Params:
     offset =          the number of bytes to seek forward
     **********************************************************************************/
    public void restartSeek(size_t offset) {
        char[16] tmp;
        this.sendCommand("REST", Integer.format(tmp, cast(long) offset));
        this.readResponse("350");

        // Set this for later use.
        this.restartPos_ = offset;
    }

    /**********************************************************************************
     Allocate space for a file.
     
     After calling this, append() or put() should be the next command.
     
     Params:
     bytes =           the number of bytes to allocate
     ***********************************************************************************/
    public void allocate(long bytes)
    in {
        assert(bytes > 0);
    }
    body {
        char[16] tmp;
        this.sendCommand("ALLO", Integer.format(tmp, bytes));
        auto response = this.readResponse();

        // For our purposes 200 and 202 are both fine.
        if(response.code != "200" && response.code != "202")
            exception(response);
    }

    /**********************************************************************************
     Retrieve a remote file's contents into a local file.
     
     Calling this function will change the current data transfer format.
     
     Params:
     path =            the path to the remote file
     local_file =      the path to the local file
     progress =        a delegate to call with progress information
     format =          what format to read the data in
     **********************************************************************************/
    public void get(const(char)[] path, const(char)[] local_file,
            FtpProgress progress = null, FtpFormat format = FtpFormat.image)
    in {
        assert(path.length > 0);
        assert(local_file.length > 0);
    }
    body {
        File file = null;

        // We may either create a new file...
        if(this.restartPos_ == 0)
            file = new File(local_file, File.ReadWriteCreate);
        // Or open an existing file, and seek to the specified position (read: not end, necessarily.)
        else {
            file = new File(local_file, File.ReadWriteExisting);
            file.seek(this.restartPos_);

            this.restartPos_ = 0;
        }

        scope(exit) {
            file.detach();
            delete file;
        }

        // Now that it's open, we do what we always do.
        this.get(path, file, progress, format);
    }

    /*********************************************************************************
     Enable UTF8 on servers that don't use this as default. Might need some work
     *********************************************************************************/
    public void enableUTF8() {
        sendCommand("OPTS UTF8 ON");
        readResponse("200");
    }

    /**********************************************************************************
     Retrieve a remote file's contents into a local file.
     
     Calling this function will change the current data transfer format.
     
     Params:
     path =            the path to the remote file
     stream =          stream to write the data to
     progress =        a delegate to call with progress information
     format =          what format to read the data in
     ***********************************************************************************/
    public void get(const(char)[] path, OutputStream stream,
            FtpProgress progress = null, FtpFormat format = FtpFormat.image)
    in {
        assert(path.length > 0);
        assert(stream !is null);
    }
    body {
        // Change to the specified format.
        this.type(format);

        // Okay server, we want to get this file...
        Socket data = this.processDataCommand("RETR", path);

        // Read the stream in from the socket!
        this.readStream(data, stream, progress);

        this.finishDataCommand(data);
    }

    /*****************************************************************************
     Added Since: 0.99.8
     *****************************************************************************/
    public InputStream input(const(char)[] path) {
        type(FtpFormat.image);
        dataSocket_ = this.processDataCommand("RETR", path);
        return dataSocket_;
    }

    /*****************************************************************************
     Added Since: 0.99.8
     *****************************************************************************/
    public OutputStream output(const(char)[] path) {
        type(FtpFormat.image); 
        dataSocket_ = this.processDataCommand("STOR", path);
        return dataSocket_;
    }
}