DEFINITIONS
This source file includes following functions.
1 =begin
2
3 = net/imap.rb
4
5 Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org>
6
7 This library is distributed under the terms of the Ruby license.
8 You can freely distribute/modify this library.
9
10 == Net::IMAP
11
12 Net::IMAP implements Internet Message Access Protocol (IMAP) clients.
13 (The protocol is described in ((<[IMAP]>)).)
14
15 Net::IMAP supports multiple commands. For example,
16
17 imap = Net::IMAP.new("imap.foo.net", "imap2")
18 imap.authenticate("cram-md5", "bar", "password")
19 imap.select("inbox")
20 fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
21 search_result = imap.search(["BODY", "hello"])
22 fetch_result = fetch_thread.value
23 imap.disconnect
24
25 This script invokes the FETCH command and the SEARCH command concurrently.
26
27 === Super Class
28
29 Object
30
31 === Class Methods
32
33 : new(host, port = 143, usessl = false, certs = nil, verify = false)
34 Creates a new Net::IMAP object and connects it to the specified
35 port on the named host. If usessl is true, then an attempt will
36 be made to use SSL (now TLS) to connect to the server. For this
37 to work OpenSSL((<[OSSL]>)) and the Ruby OpenSSL((<[RSSL]>))
38 extension need to be installed. The certs parameter indicates
39 the path or file containing the CA cert of the server, and the
40 verify parameter is for the OpenSSL verification callback.
41
42 : debug
43 Returns the debug mode.
44
45 : debug = val
46 Sets the debug mode.
47
48 : add_authenticator(auth_type, authenticator)
49 Adds an authenticator for Net::IMAP#authenticate.
50
51 === Methods
52
53 : greeting
54 Returns an initial greeting response from the server.
55
56 : responses
57 Returns recorded untagged responses.
58
59 ex).
60 imap.select("inbox")
61 p imap.responses["EXISTS"][-1]
62 #=> 2
63 p imap.responses["UIDVALIDITY"][-1]
64 #=> 968263756
65
66 : disconnect
67 Disconnects from the server.
68
69 : capability
70 Sends a CAPABILITY command, and returns a listing of
71 capabilities that the server supports.
72
73 : noop
74 Sends a NOOP command to the server. It does nothing.
75
76 : logout
77 Sends a LOGOUT command to inform the server that the client is
78 done with the connection.
79
80 : authenticate(auth_type, arg...)
81 Sends an AUTEHNTICATE command to authenticate the client.
82 The auth_type parameter is a string that represents
83 the authentication mechanism to be used. Currently Net::IMAP
84 supports "LOGIN" and "CRAM-MD5" for the auth_type.
85
86 ex).
87 imap.authenticate('LOGIN', user, password)
88
89 : login(user, password)
90 Sends a LOGIN command to identify the client and carries
91 the plaintext password authenticating this user.
92
93 : select(mailbox)
94 Sends a SELECT command to select a mailbox so that messages
95 in the mailbox can be accessed.
96
97 : examine(mailbox)
98 Sends a EXAMINE command to select a mailbox so that messages
99 in the mailbox can be accessed. However, the selected mailbox
100 is identified as read-only.
101
102 : create(mailbox)
103 Sends a CREATE command to create a new mailbox.
104
105 : delete(mailbox)
106 Sends a DELETE command to remove the mailbox.
107
108 : rename(mailbox, newname)
109 Sends a RENAME command to change the name of the mailbox to
110 the newname.
111
112 : subscribe(mailbox)
113 Sends a SUBSCRIBE command to add the specified mailbox name to
114 the server's set of "active" or "subscribed" mailboxes.
115
116 : unsubscribe(mailbox)
117 Sends a UNSUBSCRIBE command to remove the specified mailbox name
118 from the server's set of "active" or "subscribed" mailboxes.
119
120 : list(refname, mailbox)
121 Sends a LIST command, and returns a subset of names from
122 the complete set of all names available to the client.
123 The return value is an array of ((<Net::IMAP::MailboxList>)).
124
125 ex).
126 imap.create("foo/bar")
127 imap.create("foo/baz")
128 p imap.list("", "foo/%")
129 #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
130
131 : lsub(refname, mailbox)
132 Sends a LSUB command, and returns a subset of names from the set
133 of names that the user has declared as being "active" or
134 "subscribed".
135 The return value is an array of ((<Net::IMAP::MailboxList>)).
136
137 : status(mailbox, attr)
138 Sends a STATUS command, and returns the status of the indicated
139 mailbox.
140 The return value is a hash of attributes.
141
142 ex).
143 p imap.status("inbox", ["MESSAGES", "RECENT"])
144 #=> {"RECENT"=>0, "MESSAGES"=>44}
145
146 : append(mailbox, message, flags = nil, date_time = nil)
147 Sends a APPEND command to append the message to the end of
148 the mailbox.
149
150 ex).
151 imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
152 Subject: hello
153 From: shugo@ruby-lang.org
154 To: shugo@ruby-lang.org
155
156 hello world
157 EOF
158
159 : check
160 Sends a CHECK command to request a checkpoint of the currently
161 selected mailbox.
162
163 : close
164 Sends a CLOSE command to close the currently selected mailbox.
165 The CLOSE command permanently removes from the mailbox all
166 messages that have the \Deleted flag set.
167
168 : expunge
169 Sends a EXPUNGE command to permanently remove from the currently
170 selected mailbox all messages that have the \Deleted flag set.
171
172 : search(keys, charset = nil)
173 : uid_search(keys, charset = nil)
174 Sends a SEARCH command to search the mailbox for messages that
175 match the given searching criteria, and returns message sequence
176 numbers (search) or unique identifiers (uid_search).
177
178 ex).
179 p imap.search(["SUBJECT", "hello"])
180 #=> [1, 6, 7, 8]
181 p imap.search('SUBJECT "hello"')
182 #=> [1, 6, 7, 8]
183
184 : fetch(set, attr)
185 : uid_fetch(set, attr)
186 Sends a FETCH command to retrieve data associated with a message
187 in the mailbox. the set parameter is a number or an array of
188 numbers or a Range object. the number is a message sequence
189 number (fetch) or a unique identifier (uid_fetch).
190 The return value is an array of ((<Net::IMAP::FetchData>)).
191
192 ex).
193 p imap.fetch(6..8, "UID")
194 #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
195 p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
196 #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
197 data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
198 p data.seqno
199 #=> 6
200 p data.attr["RFC822.SIZE"]
201 #=> 611
202 p data.attr["INTERNALDATE"]
203 #=> "12-Oct-2000 22:40:59 +0900"
204 p data.attr["UID"]
205 #=> 98
206
207 : store(set, attr, flags)
208 : uid_store(set, attr, flags)
209 Sends a STORE command to alter data associated with a message
210 in the mailbox. the set parameter is a number or an array of
211 numbers or a Range object. the number is a message sequence
212 number (store) or a unique identifier (uid_store).
213 The return value is an array of ((<Net::IMAP::FetchData>)).
214
215 ex).
216 p imap.store(6..8, "+FLAGS", [:Deleted])
217 #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
218
219 : copy(set, mailbox)
220 : uid_copy(set, mailbox)
221 Sends a COPY command to copy the specified message(s) to the end
222 of the specified destination mailbox. the set parameter is
223 a number or an array of numbers or a Range object. the number is
224 a message sequence number (copy) or a unique identifier (uid_copy).
225
226 : sort(sort_keys, search_keys, charset)
227 : uid_sort(sort_keys, search_keys, charset)
228 Sends a SORT command to sort messages in the mailbox.
229
230 ex).
231 p imap.sort(["FROM"], ["ALL"], "US-ASCII")
232 #=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
233 p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
234 #=> [6, 7, 8, 1]
235
236 : setquota(mailbox, quota)
237 Sends a SETQUOTA command along with the specified mailbox and
238 quota. If quota is nil, then quota will be unset for that
239 mailbox. Typically one needs to be logged in as server admin
240 for this to work. The IMAP quota commands are described in
241 ((<[RFC-2087]>)).
242
243 : getquota(mailbox)
244 Sends the GETQUOTA command along with specified mailbox.
245 If this mailbox exists, then an array containing a
246 ((<Net::IMAP::MailboxQuota>)) object is returned. This
247 command generally is only available to server admin.
248
249 : getquotaroot(mailbox)
250 Sends the GETQUOTAROOT command along with specified mailbox.
251 This command is generally available to both admin and user.
252 If mailbox exists, returns an array containing objects of
253 ((<Net::IMAP::MailboxQuotaRoot>)) and ((<Net::IMAP::MailboxQuota>)).
254
255 : setacl(mailbox, user, rights)
256 Sends the SETACL command along with mailbox, user and the
257 rights that user is to have on that mailbox. If rights is nil,
258 then that user will be stripped of any rights to that mailbox.
259 The IMAP ACL commands are described in ((<[RFC-2086]>)).
260
261 : getacl(mailbox)
262 Send the GETACL command along with specified mailbox.
263 If this mailbox exists, an array containing objects of
264 ((<Net::IMAP::MailboxACLItem>)) will be returned.
265
266 : add_response_handler(handler = Proc.new)
267 Adds a response handler.
268
269 ex).
270 imap.add_response_handler do |resp|
271 p resp
272 end
273
274 : remove_response_handler(handler)
275 Removes the response handler.
276
277 : response_handlers
278 Returns all response handlers.
279
280 == Net::IMAP::ContinuationRequest
281
282 Net::IMAP::ContinuationRequest represents command continuation requests.
283
284 The command continuation request response is indicated by a "+" token
285 instead of a tag. This form of response indicates that the server is
286 ready to accept the continuation of a command from the client. The
287 remainder of this response is a line of text.
288
289 continue_req ::= "+" SPACE (resp_text / base64)
290
291 === Super Class
292
293 Struct
294
295 === Methods
296
297 : data
298 Returns the data (Net::IMAP::ResponseText).
299
300 : raw_data
301 Returns the raw data string.
302
303 == Net::IMAP::UntaggedResponse
304
305 Net::IMAP::UntaggedResponse represents untagged responses.
306
307 Data transmitted by the server to the client and status responses
308 that do not indicate command completion are prefixed with the token
309 "*", and are called untagged responses.
310
311 response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
312 mailbox_data / message_data / capability_data)
313
314 === Super Class
315
316 Struct
317
318 === Methods
319
320 : name
321 Returns the name such as "FLAGS", "LIST", "FETCH"....
322
323 : data
324 Returns the data such as an array of flag symbols,
325 a ((<Net::IMAP::MailboxList>)) object....
326
327 : raw_data
328 Returns the raw data string.
329
330 == Net::IMAP::TaggedResponse
331
332 Net::IMAP::TaggedResponse represents tagged responses.
333
334 The server completion result response indicates the success or
335 failure of the operation. It is tagged with the same tag as the
336 client command which began the operation.
337
338 response_tagged ::= tag SPACE resp_cond_state CRLF
339
340 tag ::= 1*<any ATOM_CHAR except "+">
341
342 resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
343
344 === Super Class
345
346 Struct
347
348 === Methods
349
350 : tag
351 Returns the tag.
352
353 : name
354 Returns the name. the name is one of "OK", "NO", "BAD".
355
356 : data
357 Returns the data. See ((<Net::IMAP::ResponseText>)).
358
359 : raw_data
360 Returns the raw data string.
361
362 == Net::IMAP::ResponseText
363
364 Net::IMAP::ResponseText represents texts of responses.
365 The text may be prefixed by the response code.
366
367 resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
368 ;; text SHOULD NOT begin with "[" or "="
369
370 === Super Class
371
372 Struct
373
374 === Methods
375
376 : code
377 Returns the response code. See ((<Net::IMAP::ResponseCode>)).
378
379 : text
380 Returns the text.
381
382 == Net::IMAP::ResponseCode
383
384 Net::IMAP::ResponseCode represents response codes.
385
386 resp_text_code ::= "ALERT" / "PARSE" /
387 "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
388 "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
389 "UIDVALIDITY" SPACE nz_number /
390 "UNSEEN" SPACE nz_number /
391 atom [SPACE 1*<any TEXT_CHAR except "]">]
392
393 === SuperClass
394
395 Struct
396
397 === Methods
398
399 : name
400 Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
401
402 : data
403 Returns the data if it exists.
404
405 == Net::IMAP::MailboxList
406
407 Net::IMAP::MailboxList represents contents of the LIST response.
408
409 mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
410 "\Noselect" / "\Unmarked" / flag_extension) ")"
411 SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
412
413 === Super Class
414
415 Struct
416
417 === Methods
418
419 : attr
420 Returns the name attributes. Each name attribute is a symbol
421 capitalized by String#capitalize, such as :Noselect (not :NoSelect).
422
423 : delim
424 Returns the hierarchy delimiter
425
426 : name
427 Returns the mailbox name.
428
429 == Net::IMAP::MailboxQuota
430
431 Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
432 This object can also be a response to GETQUOTAROOT. In the syntax
433 specification below, the delimiter used with the "#" construct is a
434 single space (SPACE).
435
436 quota_list ::= "(" #quota_resource ")"
437
438 quota_resource ::= atom SPACE number SPACE number
439
440 quota_response ::= "QUOTA" SPACE astring SPACE quota_list
441
442 === Super Class
443
444 Struct
445
446 === Methods
447
448 : mailbox
449 The mailbox with the associated quota.
450
451 : usage
452 Current storage usage of mailbox.
453
454 : quota
455 Quota limit imposed on mailbox.
456
457 == Net::IMAP::MailboxQuotaRoot
458
459 Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
460 response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
461
462 quotaroot_response
463 ::= "QUOTAROOT" SPACE astring *(SPACE astring)
464
465 === Super Class
466
467 Struct
468
469 === Methods
470
471 : mailbox
472 The mailbox with the associated quota.
473
474 : quotaroots
475 Zero or more quotaroots that effect the quota on the
476 specified mailbox.
477
478 == Net::IMAP::MailboxACLItem
479
480 Net::IMAP::MailboxACLItem represents response from GETACL.
481
482 acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE
483 rights)
484
485 identifier ::= astring
486
487 rights ::= astring
488
489 === Super Class
490
491 Struct
492
493 === Methods
494
495 : user
496 Login name that has certain rights to the mailbox
497 that was specified with the getacl command.
498
499 : rights
500 The access rights the indicated user has to the
501 mailbox.
502
503 == Net::IMAP::StatusData
504
505 Net::IMAP::StatusData represents contents of the STATUS response.
506
507 === Super Class
508
509 Object
510
511 === Methods
512
513 : mailbox
514 Returns the mailbox name.
515
516 : attr
517 Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
518 "UIDVALIDITY", "UNSEEN". Each value is a number.
519
520 == Net::IMAP::FetchData
521
522 Net::IMAP::FetchData represents contents of the FETCH response.
523
524 === Super Class
525
526 Object
527
528 === Methods
529
530 : seqno
531 Returns the message sequence number.
532 (Note: not the unique identifier, even for the UID command response.)
533
534 : attr
535 Returns a hash. Each key is a data item name, and each value is
536 its value.
537
538 The current data items are:
539
540 : BODY
541 A form of BODYSTRUCTURE without extension data.
542 : BODY[<section>]<<origin_octet>>
543 A string expressing the body contents of the specified section.
544 : BODYSTRUCTURE
545 An object that describes the ((<[MIME-IMB]>)) body structure of a message.
546 See ((<Net::IMAP::BodyTypeBasic>)), ((<Net::IMAP::BodyTypeText>)),
547 ((<Net::IMAP::BodyTypeMessage>)), ((<Net::IMAP::BodyTypeMultipart>)).
548 : ENVELOPE
549 A ((<Net::IMAP::Envelope>)) object that describes the envelope
550 structure of a message.
551 : FLAGS
552 A array of flag symbols that are set for this message. flag symbols
553 are capitalized by String#capitalize.
554 : INTERNALDATE
555 A string representing the internal date of the message.
556 : RFC822
557 Equivalent to BODY[].
558 : RFC822.HEADER
559 Equivalent to BODY.PEEK[HEADER].
560 : RFC822.SIZE
561 A number expressing the ((<[RFC-822]>)) size of the message.
562 : RFC822.TEXT
563 Equivalent to BODY[TEXT].
564 : UID
565 A number expressing the unique identifier of the message.
566
567 == Net::IMAP::Envelope
568
569 Net::IMAP::Envelope represents envelope structures of messages.
570
571 === Super Class
572
573 Struct
574
575 === Methods
576
577 : date
578 Retunns a string that represents the date.
579
580 : subject
581 Retunns a string that represents the subject.
582
583 : from
584 Retunns an array of ((<Net::IMAP::Address>)) that represents the from.
585
586 : sender
587 Retunns an array of ((<Net::IMAP::Address>)) that represents the sender.
588
589 : reply_to
590 Retunns an array of ((<Net::IMAP::Address>)) that represents the reply-to.
591
592 : to
593 Retunns an array of ((<Net::IMAP::Address>)) that represents the to.
594
595 : cc
596 Retunns an array of ((<Net::IMAP::Address>)) that represents the cc.
597
598 : bcc
599 Retunns an array of ((<Net::IMAP::Address>)) that represents the bcc.
600
601 : in_reply_to
602 Retunns a string that represents the in-reply-to.
603
604 : message_id
605 Retunns a string that represents the message-id.
606
607 == Net::IMAP::Address
608
609 ((<Net::IMAP::Address>)) represents electronic mail addresses.
610
611 === Super Class
612
613 Struct
614
615 === Methods
616
617 : name
618 Returns the phrase from ((<[RFC-822]>)) mailbox.
619
620 : route
621 Returns the route from ((<[RFC-822]>)) route-addr.
622
623 : mailbox
624 nil indicates end of ((<[RFC-822]>)) group.
625 If non-nil and host is nil, returns ((<[RFC-822]>)) group name.
626 Otherwise, returns ((<[RFC-822]>)) local-part
627
628 : host
629 nil indicates ((<[RFC-822]>)) group syntax.
630 Otherwise, returns ((<[RFC-822]>)) domain name.
631
632 == Net::IMAP::ContentDisposition
633
634 Net::IMAP::ContentDisposition represents Content-Disposition fields.
635
636 === Super Class
637
638 Struct
639
640 === Methods
641
642 : dsp_type
643 Returns the disposition type.
644
645 : param
646 Returns a hash that represents parameters of the Content-Disposition
647 field.
648
649 == Net::IMAP::BodyTypeBasic
650
651 Net::IMAP::BodyTypeBasic represents basic body structures of messages.
652
653 === Super Class
654
655 Struct
656
657 === Methods
658
659 : media_type
660 Returns the content media type name as defined in ((<[MIME-IMB]>)).
661
662 : subtype
663 Returns the content subtype name as defined in ((<[MIME-IMB]>)).
664
665 : param
666 Returns a hash that represents parameters as defined in
667 ((<[MIME-IMB]>)).
668
669 : content_id
670 Returns a string giving the content id as defined in ((<[MIME-IMB]>)).
671
672 : description
673 Returns a string giving the content description as defined in
674 ((<[MIME-IMB]>)).
675
676 : encoding
677 Returns a string giving the content transfer encoding as defined in
678 ((<[MIME-IMB]>)).
679
680 : size
681 Returns a number giving the size of the body in octets.
682
683 : md5
684 Returns a string giving the body MD5 value as defined in ((<[MD5]>)).
685
686 : disposition
687 Returns a ((<Net::IMAP::ContentDisposition>)) object giving
688 the content disposition.
689
690 : language
691 Returns a string or an array of strings giving the body
692 language value as defined in [LANGUAGE-TAGS].
693
694 : extension
695 Returns extension data.
696
697 : multipart?
698 Returns false.
699
700 == Net::IMAP::BodyTypeText
701
702 Net::IMAP::BodyTypeText represents TEXT body structures of messages.
703
704 === Super Class
705
706 Struct
707
708 === Methods
709
710 : lines
711 Returns the size of the body in text lines.
712
713 And Net::IMAP::BodyTypeText has all methods of ((<Net::IMAP::BodyTypeBasic>)).
714
715 == Net::IMAP::BodyTypeMessage
716
717 Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
718
719 === Super Class
720
721 Struct
722
723 === Methods
724
725 : envelope
726 Returns a ((<Net::IMAP::Envelope>)) giving the envelope structure.
727
728 : body
729 Returns an object giving the body structure.
730
731 And Net::IMAP::BodyTypeMessage has all methods of ((<Net::IMAP::BodyTypeText>)).
732
733 == Net::IMAP::BodyTypeText
734
735 === Super Class
736
737 Struct
738
739 === Methods
740
741 : media_type
742 Returns the content media type name as defined in ((<[MIME-IMB]>)).
743
744 : subtype
745 Returns the content subtype name as defined in ((<[MIME-IMB]>)).
746
747 : parts
748 Returns multiple parts.
749
750 : param
751 Returns a hash that represents parameters as defined in
752 ((<[MIME-IMB]>)).
753
754 : disposition
755 Returns a ((<Net::IMAP::ContentDisposition>)) object giving
756 the content disposition.
757
758 : language
759 Returns a string or an array of strings giving the body
760 language value as defined in [LANGUAGE-TAGS].
761
762 : extension
763 Returns extension data.
764
765 : multipart?
766 Returns true.
767
768 == References
769
770 : [IMAP]
771 M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
772 RFC 2060, December 1996.
773
774 : [LANGUAGE-TAGS]
775 Alvestrand, H., "Tags for the Identification of
776 Languages", RFC 1766, March 1995.
777
778 : [MD5]
779 Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
780 1864, October 1995.
781
782 : [MIME-IMB]
783 Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
784 Mail Extensions) Part One: Format of Internet Message Bodies", RFC
785 2045, November 1996.
786
787 : [RFC-822]
788 Crocker, D., "Standard for the Format of ARPA Internet Text
789 Messages", STD 11, RFC 822, University of Delaware, August 1982.
790
791 : [RFC-2087]
792 Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
793
794 : [RFC-2086]
795 Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
796
797 : [OSSL]
798 http:
799
800 : [RSSL]
801 http:
802
803 =end
804
805 require "socket"
806 require "monitor"
807 require "digest/md5"
808 begin
809 require "openssl"
810 rescue LoadError
811 end
812
813 module Net
814 class IMAP
815 include MonitorMixin
816 if defined?(OpenSSL)
817 include OpenSSL
818 include SSL
819 end
820
821 attr_reader :greeting, :responses, :response_handlers
822
823 SEEN = :Seen
824 ANSWERED = :Answered
825 FLAGGED = :Flagged
826 DELETED = :Deleted
827 DRAFT = :Draft
828 RECENT = :Recent
829
830 NOINFERIORS = :Noinferiors
831 NOSELECT = :Noselect
832 MARKED = :Marked
833 UNMARKED = :Unmarked
834
835 def self.debug
836 return @@debug
837 end
838
839 def self.debug=(val)
840 return @@debug = val
841 end
842
843 def self.add_authenticator(auth_type, authenticator)
844 @@authenticators[auth_type] = authenticator
845 end
846
847 def disconnect
848 @sock.shutdown unless @usessl
849 @receiver_thread.join
850 @sock.close
851 end
852
853 def capability
854 synchronize do
855 send_command("CAPABILITY")
856 return @responses.delete("CAPABILITY")[-1]
857 end
858 end
859
860 def noop
861 send_command("NOOP")
862 end
863
864 def logout
865 send_command("LOGOUT")
866 end
867
868 def authenticate(auth_type, *args)
869 auth_type = auth_type.upcase
870 unless @@authenticators.has_key?(auth_type)
871 raise ArgumentError,
872 format('unknown auth type - "%s"', auth_type)
873 end
874 authenticator = @@authenticators[auth_type].new(*args)
875 send_command("AUTHENTICATE", auth_type) do |resp|
876 if resp.instance_of?(ContinuationRequest)
877 data = authenticator.process(resp.data.text.unpack("m")[0])
878 send_data([data].pack("m").chomp)
879 end
880 end
881 end
882
883 def login(user, password)
884 send_command("LOGIN", user, password)
885 end
886
887 def select(mailbox)
888 synchronize do
889 @responses.clear
890 send_command("SELECT", mailbox)
891 end
892 end
893
894 def examine(mailbox)
895 synchronize do
896 @responses.clear
897 send_command("EXAMINE", mailbox)
898 end
899 end
900
901 def create(mailbox)
902 send_command("CREATE", mailbox)
903 end
904
905 def delete(mailbox)
906 send_command("DELETE", mailbox)
907 end
908
909 def rename(mailbox, newname)
910 send_command("RENAME", mailbox, newname)
911 end
912
913 def subscribe(mailbox)
914 send_command("SUBSCRIBE", mailbox)
915 end
916
917 def unsubscribe(mailbox)
918 send_command("UNSUBSCRIBE", mailbox)
919 end
920
921 def list(refname, mailbox)
922 synchronize do
923 send_command("LIST", refname, mailbox)
924 return @responses.delete("LIST")
925 end
926 end
927
928 def getquotaroot(mailbox)
929 synchronize do
930 send_command("GETQUOTAROOT", mailbox)
931 result = []
932 result.concat(@responses.delete("QUOTAROOT"))
933 result.concat(@responses.delete("QUOTA"))
934 return result
935 end
936 end
937
938 def getquota(mailbox)
939 synchronize do
940 send_command("GETQUOTA", mailbox)
941 return @responses.delete("QUOTA")
942 end
943 end
944
945 # setquota(mailbox, nil) will unset quota.
946 def setquota(mailbox, quota)
947 if quota.nil?
948 data = '()'
949 else
950 data = '(STORAGE ' + quota.to_s + ')'
951 end
952 send_command("SETQUOTA", mailbox, RawData.new(data))
953 end
954
955 # setacl(mailbox, user, nil) will remove rights.
956 def setacl(mailbox, user, rights)
957 if rights.nil?
958 send_command("SETACL", mailbox, user, "")
959 else
960 send_command("SETACL", mailbox, user, rights)
961 end
962 end
963
964 def getacl(mailbox)
965 synchronize do
966 send_command("GETACL", mailbox)
967 return @responses.delete("ACL")[-1]
968 end
969 end
970
971 def lsub(refname, mailbox)
972 synchronize do
973 send_command("LSUB", refname, mailbox)
974 return @responses.delete("LSUB")
975 end
976 end
977
978 def status(mailbox, attr)
979 synchronize do
980 send_command("STATUS", mailbox, attr)
981 return @responses.delete("STATUS")[-1].attr
982 end
983 end
984
985 def append(mailbox, message, flags = nil, date_time = nil)
986 args = []
987 if flags
988 args.push(flags)
989 end
990 args.push(date_time) if date_time
991 args.push(Literal.new(message))
992 send_command("APPEND", mailbox, *args)
993 end
994
995 def check
996 send_command("CHECK")
997 end
998
999 def close
1000 send_command("CLOSE")
1001 end
1002
1003 def expunge
1004 synchronize do
1005 send_command("EXPUNGE")
1006 return @responses.delete("EXPUNGE")
1007 end
1008 end
1009
1010 def search(keys, charset = nil)
1011 return search_internal("SEARCH", keys, charset)
1012 end
1013
1014 def uid_search(keys, charset = nil)
1015 return search_internal("UID SEARCH", keys, charset)
1016 end
1017
1018 def fetch(set, attr)
1019 return fetch_internal("FETCH", set, attr)
1020 end
1021
1022 def uid_fetch(set, attr)
1023 return fetch_internal("UID FETCH", set, attr)
1024 end
1025
1026 def store(set, attr, flags)
1027 return store_internal("STORE", set, attr, flags)
1028 end
1029
1030 def uid_store(set, attr, flags)
1031 return store_internal("UID STORE", set, attr, flags)
1032 end
1033
1034 def copy(set, mailbox)
1035 copy_internal("COPY", set, mailbox)
1036 end
1037
1038 def uid_copy(set, mailbox)
1039 copy_internal("UID COPY", set, mailbox)
1040 end
1041
1042 def sort(sort_keys, search_keys, charset)
1043 return sort_internal("SORT", sort_keys, search_keys, charset)
1044 end
1045
1046 def uid_sort(sort_keys, search_keys, charset)
1047 return sort_internal("UID SORT", sort_keys, search_keys, charset)
1048 end
1049
1050 def add_response_handler(handler = Proc.new)
1051 @response_handlers.push(handler)
1052 end
1053
1054 def remove_response_handler(handler)
1055 @response_handlers.delete(handler)
1056 end
1057
1058 private
1059
1060 CRLF = "\r\n"
1061 PORT = 143
1062
1063 @@debug = false
1064 @@authenticators = {}
1065
1066 def initialize(host, port = PORT, usessl = false, certs = nil, verify = false)
1067 super()
1068 @host = host
1069 @port = port
1070 @tag_prefix = "RUBY"
1071 @tagno = 0
1072 @parser = ResponseParser.new
1073 @sock = TCPSocket.open(host, port)
1074 if usessl
1075 unless defined?(OpenSSL)
1076 raise "SSL extension not installed"
1077 end
1078 @usessl = true
1079 @sock = SSLSocket.new(@sock)
1080
1081 # verify the server.
1082 @sock.ca_file = certs if certs && FileTest::file?(certs)
1083 @sock.ca_path = certs if certs && FileTest::directory?(certs)
1084 @sock.verify_mode = VERIFY_PEER if verify
1085 @sock.verify_callback = VerifyCallbackProc if defined?(VerifyCallbackProc)
1086
1087 @sock.connect # start ssl session.
1088 else
1089 @usessl = false
1090 end
1091 @responses = Hash.new([].freeze)
1092 @tagged_responses = {}
1093 @response_handlers = []
1094 @tag_arrival = new_cond
1095
1096 @greeting = get_response
1097 if /\ABYE\z/ni =~ @greeting.name
1098 @sock.close
1099 raise ByeResponseError, resp[0]
1100 end
1101
1102 @receiver_thread = Thread.start {
1103 receive_responses
1104 }
1105 end
1106
1107 def receive_responses
1108 while resp = get_response
1109 synchronize do
1110 case resp
1111 when TaggedResponse
1112 @tagged_responses[resp.tag] = resp
1113 @tag_arrival.broadcast
1114 when UntaggedResponse
1115 record_response(resp.name, resp.data)
1116 if resp.data.instance_of?(ResponseText) &&
1117 (code = resp.data.code)
1118 record_response(code.name, code.data)
1119 end
1120 end
1121 @response_handlers.each do |handler|
1122 handler.call(resp)
1123 end
1124 end
1125 end
1126 end
1127
1128 def get_tagged_response(tag, cmd)
1129 until @tagged_responses.key?(tag)
1130 @tag_arrival.wait
1131 end
1132 resp = @tagged_responses.delete(tag)
1133 case resp.name
1134 when /\A(?:NO)\z/ni
1135 raise NoResponseError, resp.data.text
1136 when /\A(?:BAD)\z/ni
1137 raise BadResponseError, resp.data.text
1138 else
1139 return resp
1140 end
1141 end
1142
1143 def get_response
1144 buff = ""
1145 while true
1146 s = @sock.gets(CRLF)
1147 break unless s
1148 buff.concat(s)
1149 if /\{(\d+)\}\r\n/n =~ s
1150 s = @sock.read($1.to_i)
1151 buff.concat(s)
1152 else
1153 break
1154 end
1155 end
1156 return nil if buff.length == 0
1157 if @@debug
1158 $stderr.print(buff.gsub(/^/n, "S: "))
1159 end
1160 return @parser.parse(buff)
1161 end
1162
1163 def record_response(name, data)
1164 unless @responses.has_key?(name)
1165 @responses[name] = []
1166 end
1167 @responses[name].push(data)
1168 end
1169
1170 def send_command(cmd, *args, &block)
1171 synchronize do
1172 tag = generate_tag
1173 data = args.collect {|i| format_data(i)}.join(" ")
1174 if data.length > 0
1175 put_line(tag + " " + cmd + " " + data)
1176 else
1177 put_line(tag + " " + cmd)
1178 end
1179 if block
1180 add_response_handler(block)
1181 end
1182 begin
1183 return get_tagged_response(tag, cmd)
1184 ensure
1185 if block
1186 remove_response_handler(block)
1187 end
1188 end
1189 end
1190 end
1191
1192 def generate_tag
1193 @tagno += 1
1194 return format("%s%04d", @tag_prefix, @tagno)
1195 end
1196
1197 def send_data(*args)
1198 data = args.collect {|i| format_data(i)}.join(" ")
1199 put_line(data)
1200 end
1201
1202 def put_line(line)
1203 line = line + CRLF
1204 @sock.print(line)
1205 if @@debug
1206 $stderr.print(line.gsub(/^/n, "C: "))
1207 end
1208 end
1209
1210 def format_data(data)
1211 case data
1212 when nil
1213 return "NIL"
1214 when String
1215 return format_string(data)
1216 when Integer
1217 return format_number(data)
1218 when Array
1219 return format_list(data)
1220 when Time
1221 return format_time(data)
1222 when Symbol
1223 return format_symbol(data)
1224 else
1225 return data.format_data
1226 end
1227 end
1228
1229 def format_string(str)
1230 case str
1231 when ""
1232 return '""'
1233 when /[\x80-\xff\r\n]/n
1234 # literal
1235 return "{" + str.length.to_s + "}" + CRLF + str
1236 when /[(){ \x00-\x1f\x7f%*"\\]/n
1237 # quoted string
1238 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
1239 else
1240 # atom
1241 return str
1242 end
1243 end
1244
1245 def format_number(num)
1246 if num < 0 || num >= 4294967296
1247 raise DataFormatError, num.to_s
1248 end
1249 return num.to_s
1250 end
1251
1252 def format_list(list)
1253 contents = list.collect {|i| format_data(i)}.join(" ")
1254 return "(" + contents + ")"
1255 end
1256
1257 DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
1258
1259 def format_time(time)
1260 t = time.dup.gmtime
1261 return format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
1262 t.day, DATE_MONTH[t.month - 1], t.year,
1263 t.hour, t.min, t.sec)
1264 end
1265
1266 def format_symbol(symbol)
1267 return "\\" + symbol.to_s
1268 end
1269
1270 def search_internal(cmd, keys, charset)
1271 if keys.instance_of?(String)
1272 keys = [RawData.new(keys)]
1273 else
1274 normalize_searching_criteria(keys)
1275 end
1276 synchronize do
1277 if charset
1278 send_command(cmd, "CHARSET", charset, *keys)
1279 else
1280 send_command(cmd, *keys)
1281 end
1282 return @responses.delete("SEARCH")[-1]
1283 end
1284 end
1285
1286 def fetch_internal(cmd, set, attr)
1287 if attr.instance_of?(String)
1288 attr = RawData.new(attr)
1289 end
1290 synchronize do
1291 @responses.delete("FETCH")
1292 send_command(cmd, MessageSet.new(set), attr)
1293 return @responses.delete("FETCH")
1294 end
1295 end
1296
1297 def store_internal(cmd, set, attr, flags)
1298 if attr.instance_of?(String)
1299 attr = RawData.new(attr)
1300 end
1301 synchronize do
1302 @responses.delete("FETCH")
1303 send_command(cmd, MessageSet.new(set), attr, flags)
1304 return @responses.delete("FETCH")
1305 end
1306 end
1307
1308 def copy_internal(cmd, set, mailbox)
1309 send_command(cmd, MessageSet.new(set), mailbox)
1310 end
1311
1312 def sort_internal(cmd, sort_keys, search_keys, charset)
1313 if search_keys.instance_of?(String)
1314 search_keys = [RawData.new(search_keys)]
1315 else
1316 normalize_searching_criteria(search_keys)
1317 end
1318 normalize_searching_criteria(search_keys)
1319 synchronize do
1320 send_command(cmd, sort_keys, charset, *search_keys)
1321 return @responses.delete("SORT")[-1]
1322 end
1323 end
1324
1325 def normalize_searching_criteria(keys)
1326 keys.collect! do |i|
1327 case i
1328 when -1, Range, Array
1329 MessageSet.new(i)
1330 else
1331 i
1332 end
1333 end
1334 end
1335
1336 class RawData
1337 def format_data
1338 return @data
1339 end
1340
1341 private
1342
1343 def initialize(data)
1344 @data = data
1345 end
1346 end
1347
1348 class Atom
1349 def format_data
1350 return @data
1351 end
1352
1353 private
1354
1355 def initialize(data)
1356 @data = data
1357 end
1358 end
1359
1360 class QuotedString
1361 def format_data
1362 return '"' + @data.gsub(/["\\]/n, "\\\\\\&") + '"'
1363 end
1364
1365 private
1366
1367 def initialize(data)
1368 @data = data
1369 end
1370 end
1371
1372 class Literal
1373 def format_data
1374 return "{" + @data.length.to_s + "}" + CRLF + @data
1375 end
1376
1377 private
1378
1379 def initialize(data)
1380 @data = data
1381 end
1382 end
1383
1384 class MessageSet
1385 def format_data
1386 return format_internal(@data)
1387 end
1388
1389 private
1390
1391 def initialize(data)
1392 @data = data
1393 end
1394
1395 def format_internal(data)
1396 case data
1397 when "*"
1398 return data
1399 when Integer
1400 ensure_nz_number(data)
1401 if data == -1
1402 return "*"
1403 else
1404 return data.to_s
1405 end
1406 when Range
1407 return format_internal(data.first) +
1408 ":" + format_internal(data.last)
1409 when Array
1410 return data.collect {|i| format_internal(i)}.join(",")
1411 else
1412 raise DataFormatError, data.inspect
1413 end
1414 end
1415
1416 def ensure_nz_number(num)
1417 if num < -1 || num == 0 || num >= 4294967296
1418 raise DataFormatError, num.inspect
1419 end
1420 end
1421 end
1422
1423 ContinuationRequest = Struct.new(:data, :raw_data)
1424 UntaggedResponse = Struct.new(:name, :data, :raw_data)
1425 TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1426 ResponseText = Struct.new(:code, :text)
1427 ResponseCode = Struct.new(:name, :data)
1428 MailboxList = Struct.new(:attr, :delim, :name)
1429 MailboxQuota = Struct.new(:mailbox, :usage, :quota)
1430 MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1431 MailboxACLItem = Struct.new(:user, :rights)
1432 StatusData = Struct.new(:mailbox, :attr)
1433 FetchData = Struct.new(:seqno, :attr)
1434 Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1435 :to, :cc, :bcc, :in_reply_to, :message_id)
1436 Address = Struct.new(:name, :route, :mailbox, :host)
1437 ContentDisposition = Struct.new(:dsp_type, :param)
1438
1439 class BodyTypeBasic < Struct.new(:media_type, :subtype,
1440 :param, :content_id,
1441 :description, :encoding, :size,
1442 :md5, :disposition, :language,
1443 :extension)
1444 def multipart?
1445 return false
1446 end
1447
1448 def media_subtype
1449 $stderr.printf("warning: media_subtype is obsolete.\n")
1450 $stderr.printf(" use subtype instead.\n")
1451 return subtype
1452 end
1453 end
1454
1455 class BodyTypeText < Struct.new(:media_type, :subtype,
1456 :param, :content_id,
1457 :description, :encoding, :size,
1458 :lines,
1459 :md5, :disposition, :language,
1460 :extension)
1461 def multipart?
1462 return false
1463 end
1464
1465 def media_subtype
1466 $stderr.printf("warning: media_subtype is obsolete.\n")
1467 $stderr.printf(" use subtype instead.\n")
1468 return subtype
1469 end
1470 end
1471
1472 class BodyTypeMessage < Struct.new(:media_type, :subtype,
1473 :param, :content_id,
1474 :description, :encoding, :size,
1475 :envelope, :body, :lines,
1476 :md5, :disposition, :language,
1477 :extension)
1478 def multipart?
1479 return false
1480 end
1481
1482 def media_subtype
1483 $stderr.printf("warning: media_subtype is obsolete.\n")
1484 $stderr.printf(" use subtype instead.\n")
1485 return subtype
1486 end
1487 end
1488
1489 class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1490 :parts,
1491 :param, :disposition, :language,
1492 :extension)
1493 def multipart?
1494 return true
1495 end
1496
1497 def media_subtype
1498 $stderr.printf("warning: media_subtype is obsolete.\n")
1499 $stderr.printf(" use subtype instead.\n")
1500 return subtype
1501 end
1502 end
1503
1504 class ResponseParser
1505 def parse(str)
1506 @str = str
1507 @pos = 0
1508 @lex_state = EXPR_BEG
1509 @token = nil
1510 return response
1511 end
1512
1513 private
1514
1515 EXPR_BEG = :EXPR_BEG
1516 EXPR_DATA = :EXPR_DATA
1517 EXPR_TEXT = :EXPR_TEXT
1518 EXPR_RTEXT = :EXPR_RTEXT
1519 EXPR_CTEXT = :EXPR_CTEXT
1520
1521 T_SPACE = :SPACE
1522 T_NIL = :NIL
1523 T_NUMBER = :NUMBER
1524 T_ATOM = :ATOM
1525 T_QUOTED = :QUOTED
1526 T_LPAR = :LPAR
1527 T_RPAR = :RPAR
1528 T_BSLASH = :BSLASH
1529 T_STAR = :STAR
1530 T_LBRA = :LBRA
1531 T_RBRA = :RBRA
1532 T_LITERAL = :LITERAL
1533 T_PLUS = :PLUS
1534 T_PERCENT = :PERCENT
1535 T_CRLF = :CRLF
1536 T_EOF = :EOF
1537 T_TEXT = :TEXT
1538
1539 BEG_REGEXP = /\G(?:\
1540 (?# 1: SPACE )( )|\
1541 (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1542 (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1543 (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
1544 (?# 5: QUOTED )"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)"|\
1545 (?# 6: LPAR )(\()|\
1546 (?# 7: RPAR )(\))|\
1547 (?# 8: BSLASH )(\\)|\
1548 (?# 9: STAR )(\*)|\
1549 (?# 10: LBRA )(\[)|\
1550 (?# 11: RBRA )(\])|\
1551 (?# 12: LITERAL )\{(\d+)\}\r\n|\
1552 (?# 13: PLUS )(\+)|\
1553 (?# 14: PERCENT )(%)|\
1554 (?# 15: CRLF )(\r\n)|\
1555 (?# 16: EOF )(\z))/ni
1556
1557 DATA_REGEXP = /\G(?:\
1558 (?# 1: SPACE )( )|\
1559 (?# 2: NIL )(NIL)|\
1560 (?# 3: NUMBER )(\d+)|\
1561 (?# 4: QUOTED )"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)"|\
1562 (?# 5: LITERAL )\{(\d+)\}\r\n|\
1563 (?# 6: LPAR )(\()|\
1564 (?# 7: RPAR )(\)))/ni
1565
1566 TEXT_REGEXP = /\G(?:\
1567 (?# 1: TEXT )([^\x00\x80-\xff\r\n]*))/ni
1568
1569 RTEXT_REGEXP = /\G(?:\
1570 (?# 1: LBRA )(\[)|\
1571 (?# 2: TEXT )([^\x00\x80-\xff\r\n]*))/ni
1572
1573 CTEXT_REGEXP = /\G(?:\
1574 (?# 1: TEXT )([^\x00\x80-\xff\r\n\]]*))/ni
1575
1576 Token = Struct.new(:symbol, :value)
1577
1578 def response
1579 token = lookahead
1580 case token.symbol
1581 when T_PLUS
1582 result = continue_req
1583 when T_STAR
1584 result = response_untagged
1585 else
1586 result = response_tagged
1587 end
1588 match(T_CRLF)
1589 match(T_EOF)
1590 return result
1591 end
1592
1593 def continue_req
1594 match(T_PLUS)
1595 match(T_SPACE)
1596 return ContinuationRequest.new(resp_text, @str)
1597 end
1598
1599 def response_untagged
1600 match(T_STAR)
1601 match(T_SPACE)
1602 token = lookahead
1603 if token.symbol == T_NUMBER
1604 return numeric_response
1605 elsif token.symbol == T_ATOM
1606 case token.value
1607 when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
1608 return response_cond
1609 when /\A(?:FLAGS)\z/ni
1610 return flags_response
1611 when /\A(?:LIST|LSUB)\z/ni
1612 return list_response
1613 when /\A(?:QUOTA)\z/ni
1614 return getquota_response
1615 when /\A(?:QUOTAROOT)\z/ni
1616 return getquotaroot_response
1617 when /\A(?:ACL)\z/ni
1618 return getacl_response
1619 when /\A(?:SEARCH|SORT)\z/ni
1620 return search_response
1621 when /\A(?:STATUS)\z/ni
1622 return status_response
1623 when /\A(?:CAPABILITY)\z/ni
1624 return capability_response
1625 else
1626 return text_response
1627 end
1628 else
1629 parse_error("unexpected token %s", token.symbol)
1630 end
1631 end
1632
1633 def response_tagged
1634 tag = atom
1635 match(T_SPACE)
1636 token = match(T_ATOM)
1637 name = token.value.upcase
1638 match(T_SPACE)
1639 return TaggedResponse.new(tag, name, resp_text, @str)
1640 end
1641
1642 def response_cond
1643 token = match(T_ATOM)
1644 name = token.value.upcase
1645 match(T_SPACE)
1646 return UntaggedResponse.new(name, resp_text, @str)
1647 end
1648
1649 def numeric_response
1650 n = number
1651 match(T_SPACE)
1652 token = match(T_ATOM)
1653 name = token.value.upcase
1654 case name
1655 when "EXISTS", "RECENT", "EXPUNGE"
1656 return UntaggedResponse.new(name, n, @str)
1657 when "FETCH"
1658 shift_token
1659 match(T_SPACE)
1660 data = FetchData.new(n, msg_att)
1661 return UntaggedResponse.new(name, data, @str)
1662 end
1663 end
1664
1665 def msg_att
1666 match(T_LPAR)
1667 attr = {}
1668 while true
1669 token = lookahead
1670 case token.symbol
1671 when T_RPAR
1672 shift_token
1673 break
1674 when T_SPACE
1675 shift_token
1676 token = lookahead
1677 end
1678 case token.value
1679 when /\A(?:ENVELOPE)\z/ni
1680 name, val = envelope_data
1681 when /\A(?:FLAGS)\z/ni
1682 name, val = flags_data
1683 when /\A(?:INTERNALDATE)\z/ni
1684 name, val = internaldate_data
1685 when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
1686 name, val = rfc822_text
1687 when /\A(?:RFC822\.SIZE)\z/ni
1688 name, val = rfc822_size
1689 when /\A(?:BODY(?:STRUCTURE)?)\z/ni
1690 name, val = body_data
1691 when /\A(?:UID)\z/ni
1692 name, val = uid_data
1693 else
1694 parse_error("unknown attribute `%s'", token.value)
1695 end
1696 attr[name] = val
1697 end
1698 return attr
1699 end
1700
1701 def envelope_data
1702 token = match(T_ATOM)
1703 name = token.value.upcase
1704 match(T_SPACE)
1705 return name, envelope
1706 end
1707
1708 def envelope
1709 @lex_state = EXPR_DATA
1710 match(T_LPAR)
1711 date = nstring
1712 match(T_SPACE)
1713 subject = nstring
1714 match(T_SPACE)
1715 from = address_list
1716 match(T_SPACE)
1717 sender = address_list
1718 match(T_SPACE)
1719 reply_to = address_list
1720 match(T_SPACE)
1721 to = address_list
1722 match(T_SPACE)
1723 cc = address_list
1724 match(T_SPACE)
1725 bcc = address_list
1726 match(T_SPACE)
1727 in_reply_to = nstring
1728 match(T_SPACE)
1729 message_id = nstring
1730 match(T_RPAR)
1731 @lex_state = EXPR_BEG
1732 return Envelope.new(date, subject, from, sender, reply_to,
1733 to, cc, bcc, in_reply_to, message_id)
1734 end
1735
1736 def flags_data
1737 token = match(T_ATOM)
1738 name = token.value.upcase
1739 match(T_SPACE)
1740 return name, flag_list
1741 end
1742
1743 def internaldate_data
1744 token = match(T_ATOM)
1745 name = token.value.upcase
1746 match(T_SPACE)
1747 token = match(T_QUOTED)
1748 return name, token.value
1749 end
1750
1751 def rfc822_text
1752 token = match(T_ATOM)
1753 name = token.value.upcase
1754 match(T_SPACE)
1755 return name, nstring
1756 end
1757
1758 def rfc822_size
1759 token = match(T_ATOM)
1760 name = token.value.upcase
1761 match(T_SPACE)
1762 return name, number
1763 end
1764
1765 def body_data
1766 token = match(T_ATOM)
1767 name = token.value.upcase
1768 token = lookahead
1769 if token.symbol == T_SPACE
1770 shift_token
1771 return name, body
1772 end
1773 name.concat(section)
1774 token = lookahead
1775 if token.symbol == T_ATOM
1776 name.concat(token.value)
1777 shift_token
1778 end
1779 match(T_SPACE)
1780 data = nstring
1781 return name, data
1782 end
1783
1784 def body
1785 @lex_state = EXPR_DATA
1786 match(T_LPAR)
1787 token = lookahead
1788 if token.symbol == T_LPAR
1789 result = body_type_mpart
1790 else
1791 result = body_type_1part
1792 end
1793 match(T_RPAR)
1794 @lex_state = EXPR_BEG
1795 return result
1796 end
1797
1798 def body_type_1part
1799 token = lookahead
1800 case token.value
1801 when /\A(?:TEXT)\z/ni
1802 return body_type_text
1803 when /\A(?:MESSAGE)\z/ni
1804 return body_type_msg
1805 else
1806 return body_type_basic
1807 end
1808 end
1809
1810 def body_type_basic
1811 mtype, msubtype = media_type
1812 match(T_SPACE)
1813 param, content_id, desc, enc, size = body_fields
1814 md5, disposition, language, extension = body_ext_1part
1815 return BodyTypeBasic.new(mtype, msubtype,
1816 param, content_id,
1817 desc, enc, size,
1818 md5, disposition, language, extension)
1819 end
1820
1821 def body_type_text
1822 mtype, msubtype = media_type
1823 match(T_SPACE)
1824 param, content_id, desc, enc, size = body_fields
1825 match(T_SPACE)
1826 lines = number
1827 md5, disposition, language, extension = body_ext_1part
1828 return BodyTypeText.new(mtype, msubtype,
1829 param, content_id,
1830 desc, enc, size,
1831 lines,
1832 md5, disposition, language, extension)
1833 end
1834
1835 def body_type_msg
1836 mtype, msubtype = media_type
1837 match(T_SPACE)
1838 param, content_id, desc, enc, size = body_fields
1839 match(T_SPACE)
1840 env = envelope
1841 match(T_SPACE)
1842 b = body
1843 match(T_SPACE)
1844 lines = number
1845 md5, disposition, language, extension = body_ext_1part
1846 return BodyTypeMessage.new(mtype, msubtype,
1847 param, content_id,
1848 desc, enc, size,
1849 env, b, lines,
1850 md5, disposition, language, extension)
1851 end
1852
1853 def body_type_mpart
1854 parts = []
1855 while true
1856 token = lookahead
1857 if token.symbol == T_SPACE
1858 shift_token
1859 break
1860 end
1861 parts.push(body)
1862 end
1863 mtype = "MULTIPART"
1864 msubtype = string.upcase
1865 param, disposition, language, extension = body_ext_mpart
1866 return BodyTypeMultipart.new(mtype, msubtype, parts,
1867 param, disposition, language,
1868 extension)
1869 end
1870
1871 def media_type
1872 mtype = string.upcase
1873 match(T_SPACE)
1874 msubtype = string.upcase
1875 return mtype, msubtype
1876 end
1877
1878 def body_fields
1879 param = body_fld_param
1880 match(T_SPACE)
1881 content_id = nstring
1882 match(T_SPACE)
1883 desc = nstring
1884 match(T_SPACE)
1885 enc = string.upcase
1886 match(T_SPACE)
1887 size = number
1888 return param, content_id, desc, enc, size
1889 end
1890
1891 def body_fld_param
1892 token = lookahead
1893 if token.symbol == T_NIL
1894 shift_token
1895 return nil
1896 end
1897 match(T_LPAR)
1898 param = {}
1899 while true
1900 token = lookahead
1901 case token.symbol
1902 when T_RPAR
1903 shift_token
1904 break
1905 when T_SPACE
1906 shift_token
1907 end
1908 name = string.upcase
1909 match(T_SPACE)
1910 val = string
1911 param[name] = val
1912 end
1913 return param
1914 end
1915
1916 def body_ext_1part
1917 token = lookahead
1918 if token.symbol == T_SPACE
1919 shift_token
1920 else
1921 return nil
1922 end
1923 md5 = nstring
1924
1925 token = lookahead
1926 if token.symbol == T_SPACE
1927 shift_token
1928 else
1929 return md5
1930 end
1931 disposition = body_fld_dsp
1932
1933 token = lookahead
1934 if token.symbol == T_SPACE
1935 shift_token
1936 else
1937 return md5, disposition
1938 end
1939 language = body_fld_lang
1940
1941 token = lookahead
1942 if token.symbol == T_SPACE
1943 shift_token
1944 else
1945 return md5, disposition, language
1946 end
1947
1948 extension = body_extensions
1949 return md5, disposition, language, extension
1950 end
1951
1952 def body_ext_mpart
1953 token = lookahead
1954 if token.symbol == T_SPACE
1955 shift_token
1956 else
1957 return nil
1958 end
1959 param = body_fld_param
1960
1961 token = lookahead
1962 if token.symbol == T_SPACE
1963 shift_token
1964 else
1965 return param
1966 end
1967 disposition = body_fld_dsp
1968 match(T_SPACE)
1969 language = body_fld_lang
1970
1971 token = lookahead
1972 if token.symbol == T_SPACE
1973 shift_token
1974 else
1975 return param, disposition, language
1976 end
1977
1978 extension = body_extensions
1979 return param, disposition, language, extension
1980 end
1981
1982 def body_fld_dsp
1983 token = lookahead
1984 if token.symbol == T_NIL
1985 shift_token
1986 return nil
1987 end
1988 match(T_LPAR)
1989 dsp_type = string.upcase
1990 match(T_SPACE)
1991 param = body_fld_param
1992 match(T_RPAR)
1993 return ContentDisposition.new(dsp_type, param)
1994 end
1995
1996 def body_fld_lang
1997 token = lookahead
1998 if token.symbol == T_LPAR
1999 shift_token
2000 result = []
2001 while true
2002 token = lookahead
2003 case token.symbol
2004 when T_RPAR
2005 shift_token
2006 return result
2007 when T_SPACE
2008 shift_token
2009 end
2010 result.push(string.upcase)
2011 end
2012 else
2013 lang = nstring
2014 if lang
2015 return lang.upcase
2016 else
2017 return lang
2018 end
2019 end
2020 end
2021
2022 def body_extensions
2023 result = []
2024 while true
2025 token = lookahead
2026 case token.symbol
2027 when T_RPAR
2028 return result
2029 when T_SPACE
2030 shift_token
2031 end
2032 result.push(body_extension)
2033 end
2034 end
2035
2036 def body_extension
2037 token = lookahead
2038 case token.symbol
2039 when T_LPAR
2040 shift_token
2041 result = body_extensions
2042 match(T_RPAR)
2043 return result
2044 when T_NUMBER
2045 return number
2046 else
2047 return nstring
2048 end
2049 end
2050
2051 def section
2052 str = ""
2053 token = match(T_LBRA)
2054 str.concat(token.value)
2055 token = match(T_ATOM, T_NUMBER, T_RBRA)
2056 if token.symbol == T_RBRA
2057 str.concat(token.value)
2058 return str
2059 end
2060 str.concat(token.value)
2061 token = lookahead
2062 if token.symbol == T_SPACE
2063 shift_token
2064 str.concat(token.value)
2065 token = match(T_LPAR)
2066 str.concat(token.value)
2067 while true
2068 token = lookahead
2069 case token.symbol
2070 when T_RPAR
2071 str.concat(token.value)
2072 shift_token
2073 break
2074 when T_SPACE
2075 shift_token
2076 str.concat(token.value)
2077 end
2078 str.concat(format_string(astring))
2079 end
2080 end
2081 token = match(T_RBRA)
2082 str.concat(token.value)
2083 return str
2084 end
2085
2086 def format_string(str)
2087 case str
2088 when ""
2089 return '""'
2090 when /[\x80-\xff\r\n]/n
2091 # literal
2092 return "{" + str.length.to_s + "}" + CRLF + str
2093 when /[(){ \x00-\x1f\x7f%*"\\]/n
2094 # quoted string
2095 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
2096 else
2097 # atom
2098 return str
2099 end
2100 end
2101
2102 def uid_data
2103 token = match(T_ATOM)
2104 name = token.value.upcase
2105 match(T_SPACE)
2106 return name, number
2107 end
2108
2109 def text_response
2110 token = match(T_ATOM)
2111 name = token.value.upcase
2112 match(T_SPACE)
2113 @lex_state = EXPR_TEXT
2114 token = match(T_TEXT)
2115 @lex_state = EXPR_BEG
2116 return UntaggedResponse.new(name, token.value)
2117 end
2118
2119 def flags_response
2120 token = match(T_ATOM)
2121 name = token.value.upcase
2122 match(T_SPACE)
2123 return UntaggedResponse.new(name, flag_list, @str)
2124 end
2125
2126 def list_response
2127 token = match(T_ATOM)
2128 name = token.value.upcase
2129 match(T_SPACE)
2130 return UntaggedResponse.new(name, mailbox_list, @str)
2131 end
2132
2133 def mailbox_list
2134 attr = flag_list
2135 match(T_SPACE)
2136 token = match(T_QUOTED, T_NIL)
2137 if token.symbol == T_NIL
2138 delim = nil
2139 else
2140 delim = token.value
2141 end
2142 match(T_SPACE)
2143 name = astring
2144 return MailboxList.new(attr, delim, name)
2145 end
2146
2147 def getquota_response
2148 # If quota never established, get back
2149 # `NO Quota root does not exist'.
2150 # If quota removed, get `()' after the
2151 # folder spec with no mention of `STORAGE'.
2152 token = match(T_ATOM)
2153 name = token.value.upcase
2154 match(T_SPACE)
2155 mailbox = astring
2156 match(T_SPACE)
2157 match(T_LPAR)
2158 token = lookahead
2159 case token.symbol
2160 when T_RPAR
2161 shift_token
2162 data = MailboxQuota.new(mailbox, nil, nil)
2163 return UntaggedResponse.new(name, data, @str)
2164 when T_ATOM
2165 shift_token
2166 match(T_SPACE)
2167 token = match(T_NUMBER)
2168 usage = token.value
2169 match(T_SPACE)
2170 token = match(T_NUMBER)
2171 quota = token.value
2172 match(T_RPAR)
2173 data = MailboxQuota.new(mailbox, usage, quota)
2174 return UntaggedResponse.new(name, data, @str)
2175 else
2176 parse_error("unexpected token %s", token.symbol)
2177 end
2178 end
2179
2180 def getquotaroot_response
2181 # Similar to getquota, but only admin can use getquota.
2182 token = match(T_ATOM)
2183 name = token.value.upcase
2184 match(T_SPACE)
2185 mailbox = astring
2186 quotaroots = []
2187 while true
2188 token = lookahead
2189 break unless token.symbol == T_SPACE
2190 shift_token
2191 quotaroots.push(astring)
2192 end
2193 data = MailboxQuotaRoot.new(mailbox, quotaroots)
2194 return UntaggedResponse.new(name, data, @str)
2195 end
2196
2197 def getacl_response
2198 token = match(T_ATOM)
2199 name = token.value.upcase
2200 match(T_SPACE)
2201 mailbox = astring
2202 data = []
2203 token = lookahead
2204 if token.symbol == T_SPACE
2205 shift_token
2206 while true
2207 token = lookahead
2208 case token.symbol
2209 when T_CRLF
2210 break
2211 when T_SPACE
2212 shift_token
2213 end
2214 user = astring
2215 match(T_SPACE)
2216 rights = astring
2217 ##XXX data.push([user, rights])
2218 data.push(MailboxACLItem.new(user, rights))
2219 end
2220 end
2221 return UntaggedResponse.new(name, data, @str)
2222 end
2223
2224 def search_response
2225 token = match(T_ATOM)
2226 name = token.value.upcase
2227 token = lookahead
2228 if token.symbol == T_SPACE
2229 shift_token
2230 data = []
2231 while true
2232 token = lookahead
2233 case token.symbol
2234 when T_CRLF
2235 break
2236 when T_SPACE
2237 shift_token
2238 end
2239 data.push(number)
2240 end
2241 else
2242 data = []
2243 end
2244 return UntaggedResponse.new(name, data, @str)
2245 end
2246
2247 def status_response
2248 token = match(T_ATOM)
2249 name = token.value.upcase
2250 match(T_SPACE)
2251 mailbox = astring
2252 match(T_SPACE)
2253 match(T_LPAR)
2254 attr = {}
2255 while true
2256 token = lookahead
2257 case token.symbol
2258 when T_RPAR
2259 shift_token
2260 break
2261 when T_SPACE
2262 shift_token
2263 end
2264 token = match(T_ATOM)
2265 key = token.value.upcase
2266 match(T_SPACE)
2267 val = number
2268 attr[key] = val
2269 end
2270 data = StatusData.new(mailbox, attr)
2271 return UntaggedResponse.new(name, data, @str)
2272 end
2273
2274 def capability_response
2275 token = match(T_ATOM)
2276 name = token.value.upcase
2277 match(T_SPACE)
2278 data = []
2279 while true
2280 token = lookahead
2281 case token.symbol
2282 when T_CRLF
2283 break
2284 when T_SPACE
2285 shift_token
2286 end
2287 data.push(atom.upcase)
2288 end
2289 return UntaggedResponse.new(name, data, @str)
2290 end
2291
2292 def resp_text
2293 @lex_state = EXPR_RTEXT
2294 token = lookahead
2295 if token.symbol == T_LBRA
2296 code = resp_text_code
2297 else
2298 code = nil
2299 end
2300 token = match(T_TEXT)
2301 @lex_state = EXPR_BEG
2302 return ResponseText.new(code, token.value)
2303 end
2304
2305 def resp_text_code
2306 @lex_state = EXPR_BEG
2307 match(T_LBRA)
2308 token = match(T_ATOM)
2309 name = token.value.upcase
2310 case name
2311 when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE)\z/n
2312 result = ResponseCode.new(name, nil)
2313 when /\A(?:PERMANENTFLAGS)\z/n
2314 match(T_SPACE)
2315 result = ResponseCode.new(name, flag_list)
2316 when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
2317 match(T_SPACE)
2318 result = ResponseCode.new(name, number)
2319 else
2320 match(T_SPACE)
2321 @lex_state = EXPR_CTEXT
2322 token = match(T_TEXT)
2323 @lex_state = EXPR_BEG
2324 result = ResponseCode.new(name, token.value)
2325 end
2326 match(T_RBRA)
2327 @lex_state = EXPR_RTEXT
2328 return result
2329 end
2330
2331 def address_list
2332 token = lookahead
2333 if token.symbol == T_NIL
2334 shift_token
2335 return nil
2336 else
2337 result = []
2338 match(T_LPAR)
2339 while true
2340 token = lookahead
2341 case token.symbol
2342 when T_RPAR
2343 shift_token
2344 break
2345 when T_SPACE
2346 shift_token
2347 end
2348 result.push(address)
2349 end
2350 return result
2351 end
2352 end
2353
2354 ADDRESS_REGEXP = /\G\
2355 (?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2356 (?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2357 (?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2358 (?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
2359 \)/ni
2360
2361 def address
2362 match(T_LPAR)
2363 if @str.index(ADDRESS_REGEXP, @pos)
2364 # address does not include literal.
2365 @pos = $~.end(0)
2366 name = $1
2367 route = $2
2368 mailbox = $3
2369 host = $4
2370 for s in [name, route, mailbox, host]
2371 if s
2372 s.gsub!(/\\(["\\])/n, "\\1")
2373 end
2374 end
2375 else
2376 name = nstring
2377 match(T_SPACE)
2378 route = nstring
2379 match(T_SPACE)
2380 mailbox = nstring
2381 match(T_SPACE)
2382 host = nstring
2383 match(T_RPAR)
2384 end
2385 return Address.new(name, route, mailbox, host)
2386 end
2387
2388 # def flag_list
2389 # result = []
2390 # match(T_LPAR)
2391 # while true
2392 # token = lookahead
2393 # case token.symbol
2394 # when T_RPAR
2395 # shift_token
2396 # break
2397 # when T_SPACE
2398 # shift_token
2399 # end
2400 # result.push(flag)
2401 # end
2402 # return result
2403 # end
2404
2405 # def flag
2406 # token = lookahead
2407 # if token.symbol == T_BSLASH
2408 # shift_token
2409 # token = lookahead
2410 # if token.symbol == T_STAR
2411 # shift_token
2412 # return token.value.intern
2413 # else
2414 # return atom.intern
2415 # end
2416 # else
2417 # return atom
2418 # end
2419 # end
2420
2421 FLAG_REGEXP = /\
2422 (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
2423 (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
2424
2425 def flag_list
2426 if @str.index(/\(([^)]*)\)/ni, @pos)
2427 @pos = $~.end(0)
2428 return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2429 atom || flag.capitalize.intern
2430 }
2431 else
2432 parse_error("invalid flag list")
2433 end
2434 end
2435
2436 def nstring
2437 token = lookahead
2438 if token.symbol == T_NIL
2439 shift_token
2440 return nil
2441 else
2442 return string
2443 end
2444 end
2445
2446 def astring
2447 token = lookahead
2448 if string_token?(token)
2449 return string
2450 else
2451 return atom
2452 end
2453 end
2454
2455 def string
2456 token = match(T_QUOTED, T_LITERAL)
2457 return token.value
2458 end
2459
2460 STRING_TOKENS = [T_QUOTED, T_LITERAL]
2461
2462 def string_token?(token)
2463 return STRING_TOKENS.include?(token.symbol)
2464 end
2465
2466 def atom
2467 result = ""
2468 while true
2469 token = lookahead
2470 if atom_token?(token)
2471 result.concat(token.value)
2472 shift_token
2473 else
2474 if result.empty?
2475 parse_error("unexpected token %s", token.symbol)
2476 else
2477 return result
2478 end
2479 end
2480 end
2481 end
2482
2483 ATOM_TOKENS = [
2484 T_ATOM,
2485 T_NUMBER,
2486 T_NIL,
2487 T_LBRA,
2488 T_RBRA,
2489 T_PLUS
2490 ]
2491
2492 def atom_token?(token)
2493 return ATOM_TOKENS.include?(token.symbol)
2494 end
2495
2496 def number
2497 token = match(T_NUMBER)
2498 return token.value.to_i
2499 end
2500
2501 def nil_atom
2502 match(T_NIL)
2503 return nil
2504 end
2505
2506 def match(*args)
2507 token = lookahead
2508 unless args.include?(token.symbol)
2509 parse_error('unexpected token %s (expected %s)',
2510 token.symbol.id2name,
2511 args.collect {|i| i.id2name}.join(" or "))
2512 end
2513 shift_token
2514 return token
2515 end
2516
2517 def lookahead
2518 unless @token
2519 @token = next_token
2520 end
2521 return @token
2522 end
2523
2524 def shift_token
2525 @token = nil
2526 end
2527
2528 def next_token
2529 case @lex_state
2530 when EXPR_BEG
2531 if @str.index(BEG_REGEXP, @pos)
2532 @pos = $~.end(0)
2533 if $1
2534 return Token.new(T_SPACE, $+)
2535 elsif $2
2536 return Token.new(T_NIL, $+)
2537 elsif $3
2538 return Token.new(T_NUMBER, $+)
2539 elsif $4
2540 return Token.new(T_ATOM, $+)
2541 elsif $5
2542 return Token.new(T_QUOTED,
2543 $+.gsub(/\\(["\\])/n, "\\1"))
2544 elsif $6
2545 return Token.new(T_LPAR, $+)
2546 elsif $7
2547 return Token.new(T_RPAR, $+)
2548 elsif $8
2549 return Token.new(T_BSLASH, $+)
2550 elsif $9
2551 return Token.new(T_STAR, $+)
2552 elsif $10
2553 return Token.new(T_LBRA, $+)
2554 elsif $11
2555 return Token.new(T_RBRA, $+)
2556 elsif $12
2557 len = $+.to_i
2558 val = @str[@pos, len]
2559 @pos += len
2560 return Token.new(T_LITERAL, val)
2561 elsif $13
2562 return Token.new(T_PLUS, $+)
2563 elsif $14
2564 return Token.new(T_PERCENT, $+)
2565 elsif $15
2566 return Token.new(T_CRLF, $+)
2567 elsif $16
2568 return Token.new(T_EOF, $+)
2569 else
2570 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
2571 end
2572 else
2573 @str.index(/\S*/n, @pos)
2574 parse_error("unknown token - %s", $&.dump)
2575 end
2576 when EXPR_DATA
2577 if @str.index(DATA_REGEXP, @pos)
2578 @pos = $~.end(0)
2579 if $1
2580 return Token.new(T_SPACE, $+)
2581 elsif $2
2582 return Token.new(T_NIL, $+)
2583 elsif $3
2584 return Token.new(T_NUMBER, $+)
2585 elsif $4
2586 return Token.new(T_QUOTED,
2587 $+.gsub(/\\(["\\])/n, "\\1"))
2588 elsif $5
2589 len = $+.to_i
2590 val = @str[@pos, len]
2591 @pos += len
2592 return Token.new(T_LITERAL, val)
2593 elsif $6
2594 return Token.new(T_LPAR, $+)
2595 elsif $7
2596 return Token.new(T_RPAR, $+)
2597 else
2598 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
2599 end
2600 else
2601 @str.index(/\S*/n, @pos)
2602 parse_error("unknown token - %s", $&.dump)
2603 end
2604 when EXPR_TEXT
2605 if @str.index(TEXT_REGEXP, @pos)
2606 @pos = $~.end(0)
2607 if $1
2608 return Token.new(T_TEXT, $+)
2609 else
2610 parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
2611 end
2612 else
2613 @str.index(/\S*/n, @pos)
2614 parse_error("unknown token - %s", $&.dump)
2615 end
2616 when EXPR_RTEXT
2617 if @str.index(RTEXT_REGEXP, @pos)
2618 @pos = $~.end(0)
2619 if $1
2620 return Token.new(T_LBRA, $+)
2621 elsif $2
2622 return Token.new(T_TEXT, $+)
2623 else
2624 parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
2625 end
2626 else
2627 @str.index(/\S*/n, @pos)
2628 parse_error("unknown token - %s", $&.dump)
2629 end
2630 when EXPR_CTEXT
2631 if @str.index(CTEXT_REGEXP, @pos)
2632 @pos = $~.end(0)
2633 if $1
2634 return Token.new(T_TEXT, $+)
2635 else
2636 parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
2637 end
2638 else
2639 @str.index(/\S*/n, @pos) #/
2640 parse_error("unknown token - %s", $&.dump)
2641 end
2642 else
2643 parse_error("illegal @lex_state - %s", @lex_state.inspect)
2644 end
2645 end
2646
2647 def parse_error(fmt, *args)
2648 if IMAP.debug
2649 $stderr.printf("@str: %s\n", @str.dump)
2650 $stderr.printf("@pos: %d\n", @pos)
2651 $stderr.printf("@lex_state: %s\n", @lex_state)
2652 if @token.symbol
2653 $stderr.printf("@token.symbol: %s\n", @token.symbol)
2654 $stderr.printf("@token.value: %s\n", @token.value.inspect)
2655 end
2656 end
2657 raise ResponseParseError, format(fmt, *args)
2658 end
2659 end
2660
2661 class LoginAuthenticator
2662 def process(data)
2663 case @state
2664 when STATE_USER
2665 @state = STATE_PASSWORD
2666 return @user
2667 when STATE_PASSWORD
2668 return @password
2669 end
2670 end
2671
2672 private
2673
2674 STATE_USER = :USER
2675 STATE_PASSWORD = :PASSWORD
2676
2677 def initialize(user, password)
2678 @user = user
2679 @password = password
2680 @state = STATE_USER
2681 end
2682 end
2683 add_authenticator "LOGIN", LoginAuthenticator
2684
2685 class CramMD5Authenticator
2686 def process(challenge)
2687 digest = hmac_md5(challenge, @password)
2688 return @user + " " + digest
2689 end
2690
2691 private
2692
2693 def initialize(user, password)
2694 @user = user
2695 @password = password
2696 end
2697
2698 def hmac_md5(text, key)
2699 if key.length > 64
2700 key = Digest::MD5.digest(key)
2701 end
2702
2703 k_ipad = key + "\0" * (64 - key.length)
2704 k_opad = key + "\0" * (64 - key.length)
2705 for i in 0..63
2706 k_ipad[i] ^= 0x36
2707 k_opad[i] ^= 0x5c
2708 end
2709
2710 digest = Digest::MD5.digest(k_ipad + text)
2711
2712 return Digest::MD5.hexdigest(k_opad + digest)
2713 end
2714 end
2715 add_authenticator "CRAM-MD5", CramMD5Authenticator
2716
2717 class Error < StandardError
2718 end
2719
2720 class DataFormatError < Error
2721 end
2722
2723 class ResponseParseError < Error
2724 end
2725
2726 class ResponseError < Error
2727 end
2728
2729 class NoResponseError < ResponseError
2730 end
2731
2732 class BadResponseError < ResponseError
2733 end
2734
2735 class ByeResponseError < ResponseError
2736 end
2737 end
2738 end
2739
2740 if __FILE__ == $0
2741 require "getoptlong"
2742
2743 $stdout.sync = true
2744 $port = "imap2"
2745 $user = ENV["USER"] || ENV["LOGNAME"]
2746 $auth = "cram-md5"
2747
2748 def usage
2749 $stderr.print <<EOF
2750 usage: #{$0} [options] <host>
2751
2752 --help print this message
2753 --port=PORT specifies port
2754 --user=USER specifies user
2755 --auth=AUTH specifies auth type
2756 EOF
2757 end
2758
2759 def get_password
2760 print "password: "
2761 system("stty", "-echo")
2762 begin
2763 return gets.chop
2764 ensure
2765 system("stty", "echo")
2766 print "\n"
2767 end
2768 end
2769
2770 def get_command
2771 printf("%s@%s> ", $user, $host)
2772 if line = gets
2773 return line.strip.split(/\s+/)
2774 else
2775 return nil
2776 end
2777 end
2778
2779 parser = GetoptLong.new
2780 parser.set_options(['--help', GetoptLong::NO_ARGUMENT],
2781 ['--port', GetoptLong::REQUIRED_ARGUMENT],
2782 ['--user', GetoptLong::REQUIRED_ARGUMENT],
2783 ['--auth', GetoptLong::REQUIRED_ARGUMENT])
2784 begin
2785 parser.each_option do |name, arg|
2786 case name
2787 when "--port"
2788 $port = arg
2789 when "--user"
2790 $user = arg
2791 when "--auth"
2792 $auth = arg
2793 when "--help"
2794 usage
2795 exit(1)
2796 end
2797 end
2798 rescue
2799 usage
2800 exit(1)
2801 end
2802
2803 $host = ARGV.shift
2804 unless $host
2805 usage
2806 exit(1)
2807 end
2808
2809 imap = Net::IMAP.new($host, $port)
2810 begin
2811 password = get_password
2812 imap.authenticate($auth, $user, password)
2813 while true
2814 cmd, *args = get_command
2815 break unless cmd
2816 begin
2817 case cmd
2818 when "list"
2819 for mbox in imap.list("", args[0] || "*")
2820 if mbox.attr.include?(Net::IMAP::NOSELECT)
2821 prefix = "!"
2822 elsif mbox.attr.include?(Net::IMAP::MARKED)
2823 prefix = "*"
2824 else
2825 prefix = " "
2826 end
2827 print prefix, mbox.name, "\n"
2828 end
2829 when "select"
2830 imap.select(args[0] || "inbox")
2831 print "ok\n"
2832 when "close"
2833 imap.close
2834 print "ok\n"
2835 when "summary"
2836 unless messages = imap.responses["EXISTS"][-1]
2837 puts "not selected"
2838 next
2839 end
2840 if messages > 0
2841 for data in imap.fetch(1..-1, ["ENVELOPE"])
2842 print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
2843 end
2844 else
2845 puts "no message"
2846 end
2847 when "fetch"
2848 if args[0]
2849 data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
2850 puts data.attr["RFC822.HEADER"]
2851 puts data.attr["RFC822.TEXT"]
2852 else
2853 puts "missing argument"
2854 end
2855 when "logout", "exit", "quit"
2856 break
2857 when "help", "?"
2858 print <<EOF
2859 list [pattern] list mailboxes
2860 select [mailbox] select mailbox
2861 close close mailbox
2862 summary display summary
2863 fetch [msgno] display message
2864 logout logout
2865 help, ? display help message
2866 EOF
2867 else
2868 print "unknown command: ", cmd, "\n"
2869 end
2870 rescue Net::IMAP::Error
2871 puts $!
2872 end
2873 end
2874 ensure
2875 imap.logout
2876 imap.disconnect
2877 end
2878 end