Rebol [
    System: "REBOL [R3] Language Interpreter and Run-time Environment"
    title: "Synchronous POP3 protocol"
    file: %prot-pop3.reb
    author: ["Graham"]
    name: pop3
    type: module
    exports: [UIDL RSET DELETE-POP TOP STAT LIST]
    version: 0.0.4
    Date: [29-Mar-2017 14-Apr-2017 4-May-2017]
    Purpose: "receive POP3 email"
    Description: {
        mbox: open pop3://user:pass@pop.server.com:110

        or

        mbox: open pop3://user@gmail.com:pass@pop.gmail.com:995

        then;

        email: pick mbox 1
        length mbox
        close mbox
    }
    History: {
        14-Apr-2017 working version that uses either TLS or TCP depending on port number
        16-Apr-2017 now able to download multimegabyte email
         4-May-2017 net-log now working
    }
]


UIDL: func [port [port!]][
    port/actor/UIDL port
]
DELETE-POP: func [port [port!] n][
    port/actor/DELETE port n
]
RSET: func [port [port!]][
    port/actor/RSET port
]
TOP: func [port [port!] n len][
    port/actor/TOP port n len
]
STAT: func [port [port!]][
    port/actor/STAT port
]
LIST: func [port [port!]][
    port/actor/LIST port
]

crlfbin: to binary! crlf
digit: charset [#"0" - #"9"]
digits: [some digit]

make-pop3-error: func [
    message
][
    FAIL ["POP3 protocol error: " message]
]

write-nlb: func [port [port!] data][
    write port join-of to binary! data crlfbin
]

; tcp-port/data is used to hold the read/write buffer by the C code
; tcp-port/spec/data is used by the actor and handlers to store the incoming data

read-awake-handler: func [event /local tcp-port] [
    net-log ["=== RH Client event:" event/type]
    tcp-port: event/port
    switch/default event/type [
        error [
            net-log "error event received"
            tcp-port/spec/port-state: 'error
            true
        ]
        lookup [
            open tcp-port
            false
        ]
        connect [
            tcp-port/spec/port-state: 'ready
            net-log "reading from port"
            read tcp-port
            false
        ]
        read [
            net-log ["^\Read Handler read:" length tcp-port/data]
            tcp-port/spec/data: copy tcp-port/data
            clear tcp-port/data
            true
        ]
        wrote [
            read tcp-port
            false
        ]
        close [
            net-log "closed on us!"
            tcp-port/spec/port-state: _
            close tcp-port
            true
        ]
    ] [true]
]    

write-awake-handler: func [event /local tcp-port] [
    net-log ["=== WH Client event:" event/type]
    tcp-port: event/port
    switch/default event/type [
        error [
            net-log "error event received"
            tcp-port/spec/port-state: 'error
            true
        ]
        lookup [
            open tcp-port
            net-log "tcp-port opened in lookup"
            false
        ]
        connect [
            net-log "connected to tcp-port in write handler"
            tcp-port/spec/port-state: 'ready
            net-log "Writing TCP port locals: "
            net-log to string! tcp-port/locals
            write tcp-port tcp-port/locals
            ; do we ever get here since the wrote event takes us elsewhere
            false
        ]
        read [
            net-log ["^\Write Handler read:" length tcp-port/data]
            append tcp-port/spec/data copy tcp-port/data
            ; print ["Read: " probe join-of copy/part to string! tcp-port/data 100 "..."]
            clear tcp-port/data
            ; now decide if we need to exit the write-awake-handler
            case [
                tcp-port/spec/cmd = 'RETR 
                [
                    either findeofpop tcp-port/spec/data [
                        tcp-port/spec/cmd: _
                        true
                    ][
                        read tcp-port
                        false
                    ]
                ]
                true [true]
            ]
        ]
        wrote [
            read tcp-port
            false
        ]
        close [
            net-log "closed on us!"
            tcp-port/spec/port-state: _
            close tcp-port
            true
        ]
    ] [true]
]    
 
sync-write: procedure [port [port!] data
        /local tcp-port
] [
    unless open? port [
            open port
    ]
    tcp-port: port/state/tcp-port
    tcp-port/awake: :write-awake-handler
    either tcp-port/spec/port-state = 'ready [
            write tcp-port data
    ] [
            tcp-port/locals: copy data
    ]
    unless port? wait [tcp-port port/spec/timeout] [
            FAIL "timeout on tcp-port"
    ]
]

sync-read: procedure [port [port!] 
        /local tcp-port
] [
    unless open? port [
            open port
    ]
    tcp-port: port/state/tcp-port
    tcp-port/awake: :read-awake-handler
    either tcp-port/spec/port-state = 'ready [
        read tcp-port
    ] [
        ; FAIL "unable to open POP3 port"
    ]
    unless port? wait [tcp-port port/spec/timeout] [
        make-pop3-error "timeout on tcp-port"
    ]
]

check+: procedure [s [string!]][
    net-log/s s
    if not find/part s "+OK" 3 [
        FAIL "Error when checking for +OK"
    ]
]

check+space: procedure [s [string!]][
    net-log/s s
    if not find/part s "+ " 2 [
        FAIL "Error when checking for +n&bsp;"
    ]
]

findeofpop: function [data [binary!]][
    equal? marker: to binary! ajoin [crlf "." crlf]
    find skip data -5 + length data marker
]

sys/make-scheme [
    name: 'pop3
    title: "Sync POP3 Protocol"
    spec: make system/standard/port-spec-net [port-id: 995 timeout: 30]

    actor: [
        open: func [
            port [port!]
            /local tcp-port w authstring method sasl-methods
        ] [
            if port/state [return port]
            if blank? port/spec/host [make-pop3-error "Missing host address"]
            port/state: context [
                tcp-port: _
            ]
            either find [465 587 993 995] port/spec/port-id [
                port/state/tcp-port: tcp-port: make port! [
                    scheme: 'tls
                    host: port/spec/host
                    port-id: port/spec/port-id
                    timeout: port/spec/timeout
                    ref: rejoin [tls:// host ":" port-id]
                    port-state: _
                    data: _
                    cmd: _
                    authentication: copy []
                ]
            ][
                port/state/tcp-port: tcp-port: make port! [
                    scheme: 'tcp
                    host: port/spec/host
                    port-id: port/spec/port-id
                    timeout: port/spec/timeout
                    ref: rejoin [tcp:// host ":" port-id]
                    port-state: _
                    data: _
                    cmd: _
                    authentication: copy []
                ]
            ]
            if any [
                not something? in port/spec 'user
                not something? in port/spec 'pass
            ][
                make-pop3-error "credentials were not supplied when opening the pop3 port"
            ]
            open tcp-port
            ; now authenticate using username and password.  we need to first parse out the authentication methods
            check+ w: to string! read port
            check+ w: to string! write-nlb port "CAPA"
            if parse w [thru "USER" to end][
                append port/state/tcp-port/spec/authentication "USER"
            ]
            parse w [thru "SASL" space copy sasl-methods: to newline (
                append port/state/tcp-port/spec/authentication split sasl-methods space
            )]
            ; by default we use the strongest method we have to authenticate
            case [
                find port/state/tcp-port/spec/authentication "PLAIN" [
                    check+space w: to string! write-nlb port "AUTH PLAIN"
                    authstring: enbase ajoin ["^@" port/spec/user "^@" port/spec/pass]
                    check+ w: to string! write-nlb port authstring
                ]
                find port/state/tcp-port/spec/authentication "USER" [
                    check+ w: to string! write-nlb port join-of "USER " port/spec/user 
                    check+ w: to string! write-nlb port join-of "PASS " port/spec/pass 
                ]
                true [FAIL "no suppported authentication methods found"]
            ]
            check+ w: to string! write-nlb port {STAT}
            port
        ]
        open?: func [port [port!]] [
            port/state/tcp-port/spec/port-state
        ]
        pick*: func [port [port!] n [integer!]][
            ; RETR message n
            ; signal to the READ actor that will need to keep reading and not exit the event handler
            port/state/tcp-port/spec/cmd: 'RETR
            write-nlb port join-of "RETR " n
        ]
        length: func [port [port!]
        ][
            port/state/tcp-port/spec/cmd: 'length
            write-nlb port "STAT"
        ]
        remove: func [port [port!]][
            return "Remove Not implemented Yet"
        ]
        write: func [port [port!] data
            <local> len
        ][
            if not open? port [
                net-log "Port not open, attempting to reopen"
                open port
            ]
            port/state/tcp-port/awake: default [:write-awake-handler]
            clear port/state/tcp-port/spec/data

            sync-write port data
            switch/default port/state/tcp-port/spec/cmd [
                'length [
                    either parse to string! port/state/tcp-port/spec/data ["+OK " copy len: digits to end][
                        port/state/tcp-port/spec/cmd: _
                        to integer! len
                    ][copy port/state/tcp-port/spec/data]
                ]
            ][
                copy port/state/tcp-port/spec/data
            ]
        ]
        read: func [port [port!]] [
            unless open? port [
                net-log "Port not open, attempting to reopen"
                open port
            ]
            port/state/tcp-port/awake: default [:read-awake-handler]
            sync-read port
            copy port/state/tcp-port/spec/data
        ]
        close: func [port [port!]] [
            write-nlb port "QUIT"
            close port/state/tcp-port
            port/state/tcp-port/spec/port-state: _
        ] 
        query: func [
            port [port!]
            /local error state
        ][
            query port/state/tcp-port
        ]
        LIST: func [port [port!]][
            to string! write-nlb port "LIST"
        ]
        UIDL: func [port [port!]][
            to string! write-nlb port "UIDL"
        ]
        DELETE: func [port [port!] n][
            to string! write-nlb port join-of "DELE " n
            ; net-log ["You provided a parameter of " n]
        ]
        RSET: func [port [port!]][
            to string! write-nlb port "RSET"
        ]
        TOP: func [port [port!] n len][
            to string! write-nlb port reform ["TOP" n len]
        ]
        STAT: func [port [port!]][
            to string! write-nlb port {STAT}
        ]
    ]
]