imap: vocab for handling imap
							parent
							
								
									669eb2482c
								
							
						
					
					
						commit
						e05c1214bd
					
				|  | @ -0,0 +1,185 @@ | ||||||
|  | USING: accessors arrays assocs calendar calendar.format combinators | ||||||
|  | continuations formatting fry grouping.extras imap io.streams.duplex kernel | ||||||
|  | math math.parser math.ranges math.statistics namespaces pcre random sequences | ||||||
|  | sets sorting strings tools.test ; | ||||||
|  | IN: imap.tests | ||||||
|  | 
 | ||||||
|  | ! Set these to your email account. | ||||||
|  | SYMBOLS: email host password ; | ||||||
|  | 
 | ||||||
|  | : random-ascii ( n -- str ) | ||||||
|  |     [ CHAR: a CHAR: z [a,b] random ] "" replicate-as ; | ||||||
|  | 
 | ||||||
|  | : make-mail ( from -- mail ) | ||||||
|  |     now timestamp>rfc822 swap 10000 random | ||||||
|  |     3array { | ||||||
|  |         "Date: %s" | ||||||
|  |         "From: %s" | ||||||
|  |         "Subject: afternoon meeting" | ||||||
|  |         "To: mooch@owatagu.siam.edu" | ||||||
|  |         "Message-Id: <%08d@Blurdybloop.COM>" | ||||||
|  |         "MIME-Version: 1.0" | ||||||
|  |         "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII" | ||||||
|  |         "" | ||||||
|  |         "Hello Joe, do you think we can meet at 3:30 tomorrow?" | ||||||
|  |     } "\r\n" join vsprintf ; | ||||||
|  | 
 | ||||||
|  | : sample-mail ( -- mail ) | ||||||
|  |     "Fred Foobar <foobar@Blurdybloop.COM>" make-mail ; | ||||||
|  | 
 | ||||||
|  | ! Fails unless you have set the settings. | ||||||
|  | : imap-login ( -- imap4 ) | ||||||
|  |     host get <imap4ssl> dup [ | ||||||
|  |         email get password get login drop | ||||||
|  |     ] with-stream* ; | ||||||
|  | 
 | ||||||
|  | : imap-test ( result quot -- ) | ||||||
|  |     '[ imap-login _ with-stream ] unit-test ; inline | ||||||
|  | 
 | ||||||
|  | [ t ] [ | ||||||
|  |     host get <imap4ssl> duplex-stream? | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | [ t ] [ | ||||||
|  |     host get <imap4ssl> [ capabilities ] with-stream | ||||||
|  |     { "IMAP4rev1" "UNSELECT" "IDLE" "NAMESPACE" "QUOTA" } swap subset? | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | [ "NO" ] [ | ||||||
|  |     [ host get <imap4ssl> [ "dont@exist.com" "foo" login ] with-stream ] | ||||||
|  |     [ ind>> ] recover | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | [ "BAD" ] [ | ||||||
|  |     [ host get <imap4ssl> [ f f login ] with-stream ] [ ind>> ] recover | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | [ f ] [ | ||||||
|  |     host get <imap4ssl> [ | ||||||
|  |         email get password get login | ||||||
|  |     ] with-stream empty? | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | ! Newly created and then selected folder is empty. | ||||||
|  | [ 0 { } ] [ | ||||||
|  |     10 random-ascii | ||||||
|  |     [ create-folder ] | ||||||
|  |     [ select-folder ] | ||||||
|  |     [ delete-folder ] tri | ||||||
|  |     "ALL" "" search-mails | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Create delete select again. | ||||||
|  | [ 0 ] [ | ||||||
|  |     "örjan" [ create-folder ] [ select-folder ] [ delete-folder ] tri | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Test list folders | ||||||
|  | [ t ] [ | ||||||
|  |     10 random-ascii | ||||||
|  |     [ create-folder "*" list-folders length 0 > ] [ delete-folder ] bi | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Generate some mails for searching | ||||||
|  | [ t t f f ] [ | ||||||
|  |     10 random-ascii { | ||||||
|  |         [ create-folder ] | ||||||
|  |         [ | ||||||
|  |             '[ _ "(\\Seen)" now sample-mail append-mail drop ] | ||||||
|  |             10 swap times | ||||||
|  |         ] | ||||||
|  |         [ | ||||||
|  |             select-folder drop | ||||||
|  |             "ALL" "" search-mails | ||||||
|  |             5 sample "(RFC822)" fetch-mails | ||||||
|  |             [ [ string? ] all? ] [ length 5 = ] bi | ||||||
|  |             "SUBJECT" "afternoon" search-mails empty? | ||||||
|  |             "(SINCE \"01-Jan-2014\")" "" search-mails empty? | ||||||
|  |         ] | ||||||
|  |         [ delete-folder ] | ||||||
|  |     } cleave | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Stat folder | ||||||
|  | [ t ] [ | ||||||
|  |     10 random-ascii { | ||||||
|  |         [ create-folder ] | ||||||
|  |         [ | ||||||
|  |             '[ _ "(\\Seen)" now sample-mail append-mail drop ] | ||||||
|  |             10 swap times | ||||||
|  |         ] | ||||||
|  |         [ | ||||||
|  |             { "MESSAGES" "UNSEEN" } status-folder | ||||||
|  |             [ "MESSAGES" of 0 > ] [ "UNSEEN" of 0 >= ] bi and | ||||||
|  |         ] | ||||||
|  |         [ delete-folder ] | ||||||
|  |     } cleave | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Rename folder | ||||||
|  | [ ] [ | ||||||
|  |     "日本語" [ create-folder ] [ | ||||||
|  |         "ascii-name" [ rename-folder ] [ delete-folder ] bi | ||||||
|  |     ] bi | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Create a folder hierarchy | ||||||
|  | [ t ] [ | ||||||
|  |     "*" list-folders length | ||||||
|  |     "foo/bar/baz/日本語" [ | ||||||
|  |         create-folder "*" list-folders length 4 - = | ||||||
|  |     ] [ delete-folder ] bi | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! A gmail compliant way of creating a folder hierarchy. | ||||||
|  | [ ] [ | ||||||
|  |     "foo/bar/baz/boo" "/" split { } [ suffix ] cum-map [ "/" join ] map | ||||||
|  |     [ [ create-folder ] each ] [ [ delete-folder ] each ] bi | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | [ ] [ | ||||||
|  |     "örjan" { | ||||||
|  |         [ create-folder ] | ||||||
|  |         [ select-folder drop ] | ||||||
|  |         ! Append mail with a seen flag | ||||||
|  |         [ "(\\Seen)" now sample-mail append-mail drop ] | ||||||
|  |         ! And one without | ||||||
|  |         [ "" now sample-mail append-mail drop ] | ||||||
|  |         [ delete-folder ] | ||||||
|  |     } cleave | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Exercise store-mail | ||||||
|  | [ 5 ] [ | ||||||
|  |     "INBOX" select-folder drop "ALL" "" search-mails | ||||||
|  |     5 sample "+FLAGS" "(\\Recent)" store-mail length | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Internal date parsing | ||||||
|  | [ "Mon, 19 Aug 2013 23:16:36 GMT" ] [ | ||||||
|  |     "19-Aug-2013 23:16:36 +0000" internal-date>timestamp timestamp>rfc822 | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | [ "19-Aug-2014 23:16:36 GMT" ] [ | ||||||
|  |     "Mon, 19 Aug 2014 23:16:36 GMT" rfc822>timestamp timestamp>internal-date | ||||||
|  | ] unit-test | ||||||
|  | 
 | ||||||
|  | ! Test parsing an INTERNALDATE from a real mail. | ||||||
|  | [ t ] [ | ||||||
|  |     "INBOX" select-folder drop | ||||||
|  |     "ALL" "" search-mails | ||||||
|  |     "(INTERNALDATE)" fetch-mails first | ||||||
|  |     "\"([^\"]+)\"" findall first second last | ||||||
|  |     internal-date>timestamp timestamp? | ||||||
|  | ] imap-test | ||||||
|  | 
 | ||||||
|  | ! Just an interesting verb to gmail thread mails. Wonder if you can | ||||||
|  | ! avoid the double fetch-mails? | ||||||
|  | : threaded-mailbox ( uids -- threads ) | ||||||
|  |     [ | ||||||
|  |         "(X-GM-THRID)" fetch-mails [ | ||||||
|  |             "\\d+" findall [ first last string>number | ||||||
|  |             ] map | ||||||
|  |         ] map | ||||||
|  |     ] [ "(BODY[HEADER.FIELDS (SUBJECT)])" fetch-mails ] bi zip | ||||||
|  |     [ first first ] [ sort-with ] [ group-by ] bi ; | ||||||
|  | @ -0,0 +1,166 @@ | ||||||
|  | USING: accessors arrays assocs calendar calendar.format calendar.format.macros | ||||||
|  | formatting fry 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 | ||||||
|  | sequences 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 -rot swap | ||||||
|  |         ! Hour, minute second and gmt offset. | ||||||
|  |         read-hms " " expect readln parse-rfc822-gmt-offset <timestamp> | ||||||
|  |     ] 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 1 tail values f ] if-empty ; | ||||||
|  | 
 | ||||||
|  | : read-response ( tag -- lines ) | ||||||
|  |     "^%s (BAD|NO|OK) (.*)$" sprintf | ||||||
|  |     '[ _ read-response-chunk [ suffix ] dip ] { } swap loop | ||||||
|  |     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 2 tail ; | ||||||
|  | 
 | ||||||
|  | : parse-list-folders ( str -- folder ) | ||||||
|  |     "\\* LIST \\(([^\\)]+)\\) \"([^\"]+)\" \"([^\"]+)\"" pcre:findall | ||||||
|  |     first 1 tail 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 2 group [ string>number ] assoc-map ; | ||||||
|  | 
 | ||||||
|  | : parse-store-mail ( seq -- assoc ) | ||||||
|  |     but-last [ | ||||||
|  |         "\\(FLAGS \\(([^\\)]+)\\) UID (\\d+)\\)" pcre:findall | ||||||
|  |         first 1 tail values first2 [ " " split ] dip string>number swap 2array | ||||||
|  |     ] map ; | ||||||
|  | 
 | ||||||
|  | ! Constructor | ||||||
|  | : <imap4ssl> ( host -- imap4 ) | ||||||
|  |     IMAP4_SSL_PORT <inet> <secure> binary <client> drop | ||||||
|  |     ! Read the useless welcome message. | ||||||
|  |     dup [ "\\*" read-response drop ] with-stream* ; | ||||||
|  | 
 | ||||||
|  | ! IMAP commands | ||||||
|  | : capabilities ( -- caps ) | ||||||
|  |     "CAPABILITY" "" command-response parse-items ; | ||||||
|  | 
 | ||||||
|  | : login ( user pass -- 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 ; | ||||||
|  | 
 | ||||||
|  | : 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 "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 ( message-set data-spec -- texts ) | ||||||
|  |     [ comma-list ] dip "UID FETCH %s %s" sprintf "" command-response but-last ; | ||||||
|  | 
 | ||||||
|  | : copy-mails ( message-set 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 ( message-set command flags -- mail-flags ) | ||||||
|  |     [ comma-list ] 2dip "UID STORE %s %s %s" sprintf "" command-response | ||||||
|  |     parse-store-mail ; | ||||||
		Loading…
	
		Reference in New Issue