lib/net/imap.rb


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://www.openssl.org
 799  
 800  : [RSSL]
 801      http://savannah.gnu.org/projects/rubypki
 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