USING: accessors arrays assocs calendar calendar.english calendar.format calendar.parser formatting grouping io io.crlf io.encodings.ascii io.encodings.binary io.encodings.string io.encodings.utf7 io.encodings.utf8 io.sockets io.sockets.secure io.streams.duplex io.streams.string kernel math math.parser multiline sequences sequences.extras splitting strings ; QUALIFIED: pcre IN: imap ERROR: imap4-error ind data ; CONSTANT: IMAP4_PORT 143 CONSTANT: IMAP4_SSL_PORT 993 ! Converts a timestamp to the format imap4 expects. : timestamp>internal-date ( timestamp -- str ) [ { DD "-" MONTH "-" YYYY " " hh ":" mm ":" ss " " [ gmt-offset>> write-gmt-offset ] } formatted ] with-string-writer ; : internal-date>timestamp ( str -- timestamp ) [ ! Date, month, year. "-" read-token checked-number "-" read-token month-abbreviations index 1 + read-sp checked-number spin ! Hour, minute second and gmt offset. read-hms " " expect readln parse-rfc822-gmt-offset ] with-string-reader ; utf7imap4 ( str -- str' ) utf7imap4 encode >string ; : comma-list ( numbers -- str ) [ number>string ] map "," join ; : check-status ( ind data -- ) over "OK" = not [ imap4-error ] [ 2drop ] if ; : read-response-chunk ( stop-expr -- item ? ) read-?crlf ascii decode swap dupd pcre:findall [ dup [[ ^.*{(\d+)}$]] pcre:findall [ dup [[ ^\* (\d+) [A-Z-]+ (.*)$]] pcre:findall [ ] [ nip first third second ] if-empty ] [ ! Literal item to read, such as message body. nip first second second string>number read ascii decode read-?crlf drop ] if-empty t ] [ nip first rest values f ] if-empty ; : read-response ( tag -- lines ) "^%s (BAD|NO|OK) (.*)$" sprintf '[ _ read-response-chunk ] loop>array* unclip-last first2 [ check-status ] keep suffix ; : write-command ( command literal tag -- ) -rot [ [ "%s %s\r\n" sprintf ] [ length "%s %s {%d}\r\n" sprintf ] if-empty ascii encode write flush ] keep [ read-?crlf drop "\r\n" append write flush ] unless-empty ; : command-response ( command literal -- obj ) "ABCD" [ write-command ] [ read-response ] bi ; ! Special parsing : parse-items ( seq -- items ) first split-words 2 tail ; : parse-list-folders ( str -- folder ) [[ \* LIST \(([^\)]+)\) "([^"]+)" "?([^"]+)"?]] pcre:findall first rest values [ utf7imap4 decode ] map ; : parse-select-folder ( seq -- count ) [ [[ \* (\d+) EXISTS]] pcre:findall ] map harvest [ f ] [ first first last last string>number ] if-empty ; ! Returns uid if the server supports the UIDPLUS extension. : parse-append-mail ( seq -- uid/f ) [ [=[ \[APPENDUID (\d+) \d+\]]=] pcre:findall ] map harvest [ f ] [ first first last last string>number ] if-empty ; : parse-status ( seq -- assoc ) first [[ \* STATUS "[^"]+" \(([^\)]+)\)]] pcre:findall first last last split-words 2 group [ string>number ] assoc-map ; : parse-store-mail-line ( str -- pair/f ) [[ \(FLAGS \(([^\)]+)\) UID (\d+)\)]] pcre:findall [ f ] [ first rest values first2 [ split-words ] dip string>number swap 2array ] if-empty ; : parse-store-mail ( seq -- assoc ) but-last [ parse-store-mail-line ] map sift ; PRIVATE> ! Constructor : ( host -- imap4 ) IMAP4_SSL_PORT f binary drop ! Read the useless welcome message. dup [ "\\*" read-response drop ] with-stream* ; ! IMAP commands : capabilities ( -- caps ) "CAPABILITY" "" command-response parse-items ; : login ( username password -- caps ) "LOGIN %s \"%s\"" sprintf "" command-response parse-items ; ! Folder management : list-folders ( directory -- folders ) "LIST \"%s\" *" sprintf "" command-response but-last [ parse-list-folders ] map ; : list-all-folders ( -- folders ) "" list-folders ; : select-folder ( mailbox -- count ) >utf7imap4 "SELECT \"%s\"" sprintf "" command-response parse-select-folder ; : create-folder ( mailbox -- ) >utf7imap4 "CREATE \"%s\"" sprintf "" command-response drop ; : delete-folder ( mailbox -- ) >utf7imap4 "DELETE \"%s\"" sprintf "" command-response drop ; : rename-folder ( old-name new-name -- ) [ >utf7imap4 ] bi@ "RENAME \"%s\" \"%s\"" sprintf "" command-response drop ; : status-folder ( mailbox keys -- assoc ) [ >utf7imap4 ] dip join-words "STATUS \"%s\" (%s)" sprintf "" command-response parse-status ; : close-folder ( -- ) "CLOSE" "" command-response drop ; ! Mail management : search-mails ( data-spec str -- uids ) [ "UID SEARCH CHARSET UTF-8 %s" sprintf ] dip utf8 encode command-response parse-items [ string>number ] map ; : fetch-mails ( uids data-spec -- texts ) [ comma-list ] dip "UID FETCH %s %s" sprintf "" command-response but-last ; : copy-mails ( uids mailbox -- ) [ comma-list ] dip >utf7imap4 "UID COPY %s \"%s\"" sprintf "" command-response drop ; : append-mail ( mailbox flags date-time mail -- uid/f ) [ [ >utf7imap4 ] [ [ "" ] [ " " append ] if-empty ] [ timestamp>internal-date ] tri* "APPEND \"%s\" %s\"%s\"" sprintf ] dip utf8 encode command-response parse-append-mail ; : store-mail ( uids command flags -- mail-flags ) [ comma-list ] 2dip "UID STORE %s %s %s" sprintf "" command-response parse-store-mail ; ! High level API : with-imap ( host email password quot -- ) [ ] 3dip '[ _ _ login drop @ ] with-stream ; inline TUPLE: imap-settings host email password ; : ( host email password -- obj ) imap-settings new swap >>password swap >>email swap >>host ; inline : with-imap-settings ( imap-settings quot -- ) [ [ host>> ] [ email>> ] [ password>> ] tri ] dip with-imap ; inline