1# 2# = net/imap.rb 3# 4# Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org> 5# 6# This library is distributed under the terms of the Ruby license. 7# You can freely distribute/modify this library. 8# 9# Documentation: Shugo Maeda, with RDoc conversion and overview by William 10# Webber. 11# 12# See Net::IMAP for documentation. 13# 14 15 16require "socket" 17require "monitor" 18require "digest/md5" 19require "strscan" 20begin 21 require "openssl" 22rescue LoadError 23end 24 25module Net 26 27 # 28 # Net::IMAP implements Internet Message Access Protocol (IMAP) client 29 # functionality. The protocol is described in [IMAP]. 30 # 31 # == IMAP Overview 32 # 33 # An IMAP client connects to a server, and then authenticates 34 # itself using either #authenticate() or #login(). Having 35 # authenticated itself, there is a range of commands 36 # available to it. Most work with mailboxes, which may be 37 # arranged in an hierarchical namespace, and each of which 38 # contains zero or more messages. How this is implemented on 39 # the server is implementation-dependent; on a UNIX server, it 40 # will frequently be implemented as a files in mailbox format 41 # within a hierarchy of directories. 42 # 43 # To work on the messages within a mailbox, the client must 44 # first select that mailbox, using either #select() or (for 45 # read-only access) #examine(). Once the client has successfully 46 # selected a mailbox, they enter _selected_ state, and that 47 # mailbox becomes the _current_ mailbox, on which mail-item 48 # related commands implicitly operate. 49 # 50 # Messages have two sorts of identifiers: message sequence 51 # numbers, and UIDs. 52 # 53 # Message sequence numbers number messages within a mail box 54 # from 1 up to the number of items in the mail box. If new 55 # message arrives during a session, it receives a sequence 56 # number equal to the new size of the mail box. If messages 57 # are expunged from the mailbox, remaining messages have their 58 # sequence numbers "shuffled down" to fill the gaps. 59 # 60 # UIDs, on the other hand, are permanently guaranteed not to 61 # identify another message within the same mailbox, even if 62 # the existing message is deleted. UIDs are required to 63 # be assigned in ascending (but not necessarily sequential) 64 # order within a mailbox; this means that if a non-IMAP client 65 # rearranges the order of mailitems within a mailbox, the 66 # UIDs have to be reassigned. An IMAP client cannot thus 67 # rearrange message orders. 68 # 69 # == Examples of Usage 70 # 71 # === List sender and subject of all recent messages in the default mailbox 72 # 73 # imap = Net::IMAP.new('mail.example.com') 74 # imap.authenticate('LOGIN', 'joe_user', 'joes_password') 75 # imap.examine('INBOX') 76 # imap.search(["RECENT"]).each do |message_id| 77 # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] 78 # puts "#{envelope.from[0].name}: \t#{envelope.subject}" 79 # end 80 # 81 # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" 82 # 83 # imap = Net::IMAP.new('mail.example.com') 84 # imap.authenticate('LOGIN', 'joe_user', 'joes_password') 85 # imap.select('Mail/sent-mail') 86 # if not imap.list('Mail/', 'sent-apr03') 87 # imap.create('Mail/sent-apr03') 88 # end 89 # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| 90 # imap.copy(message_id, "Mail/sent-apr03") 91 # imap.store(message_id, "+FLAGS", [:Deleted]) 92 # end 93 # imap.expunge 94 # 95 # == Thread Safety 96 # 97 # Net::IMAP supports concurrent threads. For example, 98 # 99 # imap = Net::IMAP.new("imap.foo.net", "imap2") 100 # imap.authenticate("cram-md5", "bar", "password") 101 # imap.select("inbox") 102 # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") } 103 # search_result = imap.search(["BODY", "hello"]) 104 # fetch_result = fetch_thread.value 105 # imap.disconnect 106 # 107 # This script invokes the FETCH command and the SEARCH command concurrently. 108 # 109 # == Errors 110 # 111 # An IMAP server can send three different types of responses to indicate 112 # failure: 113 # 114 # NO:: the attempted command could not be successfully completed. For 115 # instance, the username/password used for logging in are incorrect; 116 # the selected mailbox does not exists; etc. 117 # 118 # BAD:: the request from the client does not follow the server's 119 # understanding of the IMAP protocol. This includes attempting 120 # commands from the wrong client state; for instance, attempting 121 # to perform a SEARCH command without having SELECTed a current 122 # mailbox. It can also signal an internal server 123 # failure (such as a disk crash) has occurred. 124 # 125 # BYE:: the server is saying goodbye. This can be part of a normal 126 # logout sequence, and can be used as part of a login sequence 127 # to indicate that the server is (for some reason) unwilling 128 # to accept our connection. As a response to any other command, 129 # it indicates either that the server is shutting down, or that 130 # the server is timing out the client connection due to inactivity. 131 # 132 # These three error response are represented by the errors 133 # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and 134 # Net::IMAP::ByeResponseError, all of which are subclasses of 135 # Net::IMAP::ResponseError. Essentially, all methods that involve 136 # sending a request to the server can generate one of these errors. 137 # Only the most pertinent instances have been documented below. 138 # 139 # Because the IMAP class uses Sockets for communication, its methods 140 # are also susceptible to the various errors that can occur when 141 # working with sockets. These are generally represented as 142 # Errno errors. For instance, any method that involves sending a 143 # request to the server and/or receiving a response from it could 144 # raise an Errno::EPIPE error if the network connection unexpectedly 145 # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2), 146 # and associated man pages. 147 # 148 # Finally, a Net::IMAP::DataFormatError is thrown if low-level data 149 # is found to be in an incorrect format (for instance, when converting 150 # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is 151 # thrown if a server response is non-parseable. 152 # 153 # 154 # == References 155 # 156 # [[IMAP]] 157 # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1", 158 # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501) 159 # 160 # [[LANGUAGE-TAGS]] 161 # Alvestrand, H., "Tags for the Identification of 162 # Languages", RFC 1766, March 1995. 163 # 164 # [[MD5]] 165 # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC 166 # 1864, October 1995. 167 # 168 # [[MIME-IMB]] 169 # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet 170 # Mail Extensions) Part One: Format of Internet Message Bodies", RFC 171 # 2045, November 1996. 172 # 173 # [[RFC-822]] 174 # Crocker, D., "Standard for the Format of ARPA Internet Text 175 # Messages", STD 11, RFC 822, University of Delaware, August 1982. 176 # 177 # [[RFC-2087]] 178 # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997. 179 # 180 # [[RFC-2086]] 181 # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997. 182 # 183 # [[RFC-2195]] 184 # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension 185 # for Simple Challenge/Response", RFC 2195, September 1997. 186 # 187 # [[SORT-THREAD-EXT]] 188 # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD 189 # Extensions", draft-ietf-imapext-sort, May 2003. 190 # 191 # [[OSSL]] 192 # http://www.openssl.org 193 # 194 # [[RSSL]] 195 # http://savannah.gnu.org/projects/rubypki 196 # 197 # [[UTF7]] 198 # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of 199 # Unicode", RFC 2152, May 1997. 200 # 201 class IMAP 202 include MonitorMixin 203 if defined?(OpenSSL::SSL) 204 include OpenSSL 205 include SSL 206 end 207 208 # Returns an initial greeting response from the server. 209 attr_reader :greeting 210 211 # Returns recorded untagged responses. For example: 212 # 213 # imap.select("inbox") 214 # p imap.responses["EXISTS"][-1] 215 # #=> 2 216 # p imap.responses["UIDVALIDITY"][-1] 217 # #=> 968263756 218 attr_reader :responses 219 220 # Returns all response handlers. 221 attr_reader :response_handlers 222 223 # The thread to receive exceptions. 224 attr_accessor :client_thread 225 226 # Flag indicating a message has been seen 227 SEEN = :Seen 228 229 # Flag indicating a message has been answered 230 ANSWERED = :Answered 231 232 # Flag indicating a message has been flagged for special or urgent 233 # attention 234 FLAGGED = :Flagged 235 236 # Flag indicating a message has been marked for deletion. This 237 # will occur when the mailbox is closed or expunged. 238 DELETED = :Deleted 239 240 # Flag indicating a message is only a draft or work-in-progress version. 241 DRAFT = :Draft 242 243 # Flag indicating that the message is "recent", meaning that this 244 # session is the first session in which the client has been notified 245 # of this message. 246 RECENT = :Recent 247 248 # Flag indicating that a mailbox context name cannot contain 249 # children. 250 NOINFERIORS = :Noinferiors 251 252 # Flag indicating that a mailbox is not selected. 253 NOSELECT = :Noselect 254 255 # Flag indicating that a mailbox has been marked "interesting" by 256 # the server; this commonly indicates that the mailbox contains 257 # new messages. 258 MARKED = :Marked 259 260 # Flag indicating that the mailbox does not contains new messages. 261 UNMARKED = :Unmarked 262 263 # Returns the debug mode. 264 def self.debug 265 return @@debug 266 end 267 268 # Sets the debug mode. 269 def self.debug=(val) 270 return @@debug = val 271 end 272 273 # Returns the max number of flags interned to symbols. 274 def self.max_flag_count 275 return @@max_flag_count 276 end 277 278 # Sets the max number of flags interned to symbols. 279 def self.max_flag_count=(count) 280 @@max_flag_count = count 281 end 282 283 # Adds an authenticator for Net::IMAP#authenticate. +auth_type+ 284 # is the type of authentication this authenticator supports 285 # (for instance, "LOGIN"). The +authenticator+ is an object 286 # which defines a process() method to handle authentication with 287 # the server. See Net::IMAP::LoginAuthenticator, 288 # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator 289 # for examples. 290 # 291 # 292 # If +auth_type+ refers to an existing authenticator, it will be 293 # replaced by the new one. 294 def self.add_authenticator(auth_type, authenticator) 295 @@authenticators[auth_type] = authenticator 296 end 297 298 # The default port for IMAP connections, port 143 299 def self.default_port 300 return PORT 301 end 302 303 # The default port for IMAPS connections, port 993 304 def self.default_tls_port 305 return SSL_PORT 306 end 307 308 class << self 309 alias default_imap_port default_port 310 alias default_imaps_port default_tls_port 311 alias default_ssl_port default_tls_port 312 end 313 314 # Disconnects from the server. 315 def disconnect 316 begin 317 begin 318 # try to call SSL::SSLSocket#io. 319 @sock.io.shutdown 320 rescue NoMethodError 321 # @sock is not an SSL::SSLSocket. 322 @sock.shutdown 323 end 324 rescue Errno::ENOTCONN 325 # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms. 326 rescue Exception => e 327 @receiver_thread.raise(e) 328 end 329 @receiver_thread.join 330 synchronize do 331 unless @sock.closed? 332 @sock.close 333 end 334 end 335 raise e if e 336 end 337 338 # Returns true if disconnected from the server. 339 def disconnected? 340 return @sock.closed? 341 end 342 343 # Sends a CAPABILITY command, and returns an array of 344 # capabilities that the server supports. Each capability 345 # is a string. See [IMAP] for a list of possible 346 # capabilities. 347 # 348 # Note that the Net::IMAP class does not modify its 349 # behaviour according to the capabilities of the server; 350 # it is up to the user of the class to ensure that 351 # a certain capability is supported by a server before 352 # using it. 353 def capability 354 synchronize do 355 send_command("CAPABILITY") 356 return @responses.delete("CAPABILITY")[-1] 357 end 358 end 359 360 # Sends a NOOP command to the server. It does nothing. 361 def noop 362 send_command("NOOP") 363 end 364 365 # Sends a LOGOUT command to inform the server that the client is 366 # done with the connection. 367 def logout 368 send_command("LOGOUT") 369 end 370 371 # Sends a STARTTLS command to start TLS session. 372 def starttls(options = {}, verify = true) 373 send_command("STARTTLS") do |resp| 374 if resp.kind_of?(TaggedResponse) && resp.name == "OK" 375 begin 376 # for backward compatibility 377 certs = options.to_str 378 options = create_ssl_params(certs, verify) 379 rescue NoMethodError 380 end 381 start_tls_session(options) 382 end 383 end 384 end 385 386 # Sends an AUTHENTICATE command to authenticate the client. 387 # The +auth_type+ parameter is a string that represents 388 # the authentication mechanism to be used. Currently Net::IMAP 389 # supports authentication mechanisms: 390 # 391 # LOGIN:: login using cleartext user and password. 392 # CRAM-MD5:: login with cleartext user and encrypted password 393 # (see [RFC-2195] for a full description). This 394 # mechanism requires that the server have the user's 395 # password stored in clear-text password. 396 # 397 # For both these mechanisms, there should be two +args+: username 398 # and (cleartext) password. A server may not support one or other 399 # of these mechanisms; check #capability() for a capability of 400 # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5". 401 # 402 # Authentication is done using the appropriate authenticator object: 403 # see @@authenticators for more information on plugging in your own 404 # authenticator. 405 # 406 # For example: 407 # 408 # imap.authenticate('LOGIN', user, password) 409 # 410 # A Net::IMAP::NoResponseError is raised if authentication fails. 411 def authenticate(auth_type, *args) 412 auth_type = auth_type.upcase 413 unless @@authenticators.has_key?(auth_type) 414 raise ArgumentError, 415 format('unknown auth type - "%s"', auth_type) 416 end 417 authenticator = @@authenticators[auth_type].new(*args) 418 send_command("AUTHENTICATE", auth_type) do |resp| 419 if resp.instance_of?(ContinuationRequest) 420 data = authenticator.process(resp.data.text.unpack("m")[0]) 421 s = [data].pack("m").gsub(/\n/, "") 422 send_string_data(s) 423 put_string(CRLF) 424 end 425 end 426 end 427 428 # Sends a LOGIN command to identify the client and carries 429 # the plaintext +password+ authenticating this +user+. Note 430 # that, unlike calling #authenticate() with an +auth_type+ 431 # of "LOGIN", #login() does *not* use the login authenticator. 432 # 433 # A Net::IMAP::NoResponseError is raised if authentication fails. 434 def login(user, password) 435 send_command("LOGIN", user, password) 436 end 437 438 # Sends a SELECT command to select a +mailbox+ so that messages 439 # in the +mailbox+ can be accessed. 440 # 441 # After you have selected a mailbox, you may retrieve the 442 # number of items in that mailbox from @responses["EXISTS"][-1], 443 # and the number of recent messages from @responses["RECENT"][-1]. 444 # Note that these values can change if new messages arrive 445 # during a session; see #add_response_handler() for a way of 446 # detecting this event. 447 # 448 # A Net::IMAP::NoResponseError is raised if the mailbox does not 449 # exist or is for some reason non-selectable. 450 def select(mailbox) 451 synchronize do 452 @responses.clear 453 send_command("SELECT", mailbox) 454 end 455 end 456 457 # Sends a EXAMINE command to select a +mailbox+ so that messages 458 # in the +mailbox+ can be accessed. Behaves the same as #select(), 459 # except that the selected +mailbox+ is identified as read-only. 460 # 461 # A Net::IMAP::NoResponseError is raised if the mailbox does not 462 # exist or is for some reason non-examinable. 463 def examine(mailbox) 464 synchronize do 465 @responses.clear 466 send_command("EXAMINE", mailbox) 467 end 468 end 469 470 # Sends a CREATE command to create a new +mailbox+. 471 # 472 # A Net::IMAP::NoResponseError is raised if a mailbox with that name 473 # cannot be created. 474 def create(mailbox) 475 send_command("CREATE", mailbox) 476 end 477 478 # Sends a DELETE command to remove the +mailbox+. 479 # 480 # A Net::IMAP::NoResponseError is raised if a mailbox with that name 481 # cannot be deleted, either because it does not exist or because the 482 # client does not have permission to delete it. 483 def delete(mailbox) 484 send_command("DELETE", mailbox) 485 end 486 487 # Sends a RENAME command to change the name of the +mailbox+ to 488 # +newname+. 489 # 490 # A Net::IMAP::NoResponseError is raised if a mailbox with the 491 # name +mailbox+ cannot be renamed to +newname+ for whatever 492 # reason; for instance, because +mailbox+ does not exist, or 493 # because there is already a mailbox with the name +newname+. 494 def rename(mailbox, newname) 495 send_command("RENAME", mailbox, newname) 496 end 497 498 # Sends a SUBSCRIBE command to add the specified +mailbox+ name to 499 # the server's set of "active" or "subscribed" mailboxes as returned 500 # by #lsub(). 501 # 502 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be 503 # subscribed to, for instance because it does not exist. 504 def subscribe(mailbox) 505 send_command("SUBSCRIBE", mailbox) 506 end 507 508 # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name 509 # from the server's set of "active" or "subscribed" mailboxes. 510 # 511 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be 512 # unsubscribed from, for instance because the client is not currently 513 # subscribed to it. 514 def unsubscribe(mailbox) 515 send_command("UNSUBSCRIBE", mailbox) 516 end 517 518 # Sends a LIST command, and returns a subset of names from 519 # the complete set of all names available to the client. 520 # +refname+ provides a context (for instance, a base directory 521 # in a directory-based mailbox hierarchy). +mailbox+ specifies 522 # a mailbox or (via wildcards) mailboxes under that context. 523 # Two wildcards may be used in +mailbox+: '*', which matches 524 # all characters *including* the hierarchy delimiter (for instance, 525 # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', 526 # which matches all characters *except* the hierarchy delimiter. 527 # 528 # If +refname+ is empty, +mailbox+ is used directly to determine 529 # which mailboxes to match. If +mailbox+ is empty, the root 530 # name of +refname+ and the hierarchy delimiter are returned. 531 # 532 # The return value is an array of +Net::IMAP::MailboxList+. For example: 533 # 534 # imap.create("foo/bar") 535 # imap.create("foo/baz") 536 # p imap.list("", "foo/%") 537 # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\ 538 # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\ 539 # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">] 540 def list(refname, mailbox) 541 synchronize do 542 send_command("LIST", refname, mailbox) 543 return @responses.delete("LIST") 544 end 545 end 546 547 # Sends a XLIST command, and returns a subset of names from 548 # the complete set of all names available to the client. 549 # +refname+ provides a context (for instance, a base directory 550 # in a directory-based mailbox hierarchy). +mailbox+ specifies 551 # a mailbox or (via wildcards) mailboxes under that context. 552 # Two wildcards may be used in +mailbox+: '*', which matches 553 # all characters *including* the hierarchy delimiter (for instance, 554 # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', 555 # which matches all characters *except* the hierarchy delimiter. 556 # 557 # If +refname+ is empty, +mailbox+ is used directly to determine 558 # which mailboxes to match. If +mailbox+ is empty, the root 559 # name of +refname+ and the hierarchy delimiter are returned. 560 # 561 # The XLIST command is like the LIST command except that the flags 562 # returned refer to the function of the folder/mailbox, e.g. :Sent 563 # 564 # The return value is an array of +Net::IMAP::MailboxList+. For example: 565 # 566 # imap.create("foo/bar") 567 # imap.create("foo/baz") 568 # p imap.xlist("", "foo/%") 569 # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\ 570 # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\ 571 # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">] 572 def xlist(refname, mailbox) 573 synchronize do 574 send_command("XLIST", refname, mailbox) 575 return @responses.delete("XLIST") 576 end 577 end 578 579 # Sends the GETQUOTAROOT command along with specified +mailbox+. 580 # This command is generally available to both admin and user. 581 # If mailbox exists, returns an array containing objects of 582 # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota. 583 def getquotaroot(mailbox) 584 synchronize do 585 send_command("GETQUOTAROOT", mailbox) 586 result = [] 587 result.concat(@responses.delete("QUOTAROOT")) 588 result.concat(@responses.delete("QUOTA")) 589 return result 590 end 591 end 592 593 # Sends the GETQUOTA command along with specified +mailbox+. 594 # If this mailbox exists, then an array containing a 595 # Net::IMAP::MailboxQuota object is returned. This 596 # command generally is only available to server admin. 597 def getquota(mailbox) 598 synchronize do 599 send_command("GETQUOTA", mailbox) 600 return @responses.delete("QUOTA") 601 end 602 end 603 604 # Sends a SETQUOTA command along with the specified +mailbox+ and 605 # +quota+. If +quota+ is nil, then quota will be unset for that 606 # mailbox. Typically one needs to be logged in as server admin 607 # for this to work. The IMAP quota commands are described in 608 # [RFC-2087]. 609 def setquota(mailbox, quota) 610 if quota.nil? 611 data = '()' 612 else 613 data = '(STORAGE ' + quota.to_s + ')' 614 end 615 send_command("SETQUOTA", mailbox, RawData.new(data)) 616 end 617 618 # Sends the SETACL command along with +mailbox+, +user+ and the 619 # +rights+ that user is to have on that mailbox. If +rights+ is nil, 620 # then that user will be stripped of any rights to that mailbox. 621 # The IMAP ACL commands are described in [RFC-2086]. 622 def setacl(mailbox, user, rights) 623 if rights.nil? 624 send_command("SETACL", mailbox, user, "") 625 else 626 send_command("SETACL", mailbox, user, rights) 627 end 628 end 629 630 # Send the GETACL command along with specified +mailbox+. 631 # If this mailbox exists, an array containing objects of 632 # Net::IMAP::MailboxACLItem will be returned. 633 def getacl(mailbox) 634 synchronize do 635 send_command("GETACL", mailbox) 636 return @responses.delete("ACL")[-1] 637 end 638 end 639 640 # Sends a LSUB command, and returns a subset of names from the set 641 # of names that the user has declared as being "active" or 642 # "subscribed". +refname+ and +mailbox+ are interpreted as 643 # for #list(). 644 # The return value is an array of +Net::IMAP::MailboxList+. 645 def lsub(refname, mailbox) 646 synchronize do 647 send_command("LSUB", refname, mailbox) 648 return @responses.delete("LSUB") 649 end 650 end 651 652 # Sends a STATUS command, and returns the status of the indicated 653 # +mailbox+. +attr+ is a list of one or more attributes that 654 # we are request the status of. Supported attributes include: 655 # 656 # MESSAGES:: the number of messages in the mailbox. 657 # RECENT:: the number of recent messages in the mailbox. 658 # UNSEEN:: the number of unseen messages in the mailbox. 659 # 660 # The return value is a hash of attributes. For example: 661 # 662 # p imap.status("inbox", ["MESSAGES", "RECENT"]) 663 # #=> {"RECENT"=>0, "MESSAGES"=>44} 664 # 665 # A Net::IMAP::NoResponseError is raised if status values 666 # for +mailbox+ cannot be returned, for instance because it 667 # does not exist. 668 def status(mailbox, attr) 669 synchronize do 670 send_command("STATUS", mailbox, attr) 671 return @responses.delete("STATUS")[-1].attr 672 end 673 end 674 675 # Sends a APPEND command to append the +message+ to the end of 676 # the +mailbox+. The optional +flags+ argument is an array of 677 # flags to initially passing to the new message. The optional 678 # +date_time+ argument specifies the creation time to assign to the 679 # new message; it defaults to the current time. 680 # For example: 681 # 682 # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now) 683 # Subject: hello 684 # From: shugo@ruby-lang.org 685 # To: shugo@ruby-lang.org 686 # 687 # hello world 688 # EOF 689 # 690 # A Net::IMAP::NoResponseError is raised if the mailbox does 691 # not exist (it is not created automatically), or if the flags, 692 # date_time, or message arguments contain errors. 693 def append(mailbox, message, flags = nil, date_time = nil) 694 args = [] 695 if flags 696 args.push(flags) 697 end 698 args.push(date_time) if date_time 699 args.push(Literal.new(message)) 700 send_command("APPEND", mailbox, *args) 701 end 702 703 # Sends a CHECK command to request a checkpoint of the currently 704 # selected mailbox. This performs implementation-specific 705 # housekeeping, for instance, reconciling the mailbox's 706 # in-memory and on-disk state. 707 def check 708 send_command("CHECK") 709 end 710 711 # Sends a CLOSE command to close the currently selected mailbox. 712 # The CLOSE command permanently removes from the mailbox all 713 # messages that have the \Deleted flag set. 714 def close 715 send_command("CLOSE") 716 end 717 718 # Sends a EXPUNGE command to permanently remove from the currently 719 # selected mailbox all messages that have the \Deleted flag set. 720 def expunge 721 synchronize do 722 send_command("EXPUNGE") 723 return @responses.delete("EXPUNGE") 724 end 725 end 726 727 # Sends a SEARCH command to search the mailbox for messages that 728 # match the given searching criteria, and returns message sequence 729 # numbers. +keys+ can either be a string holding the entire 730 # search string, or a single-dimension array of search keywords and 731 # arguments. The following are some common search criteria; 732 # see [IMAP] section 6.4.4 for a full list. 733 # 734 # <message set>:: a set of message sequence numbers. ',' indicates 735 # an interval, ':' indicates a range. For instance, 736 # '2,10:12,15' means "2,10,11,12,15". 737 # 738 # BEFORE <date>:: messages with an internal date strictly before 739 # <date>. The date argument has a format similar 740 # to 8-Aug-2002. 741 # 742 # BODY <string>:: messages that contain <string> within their body. 743 # 744 # CC <string>:: messages containing <string> in their CC field. 745 # 746 # FROM <string>:: messages that contain <string> in their FROM field. 747 # 748 # NEW:: messages with the \Recent, but not the \Seen, flag set. 749 # 750 # NOT <search-key>:: negate the following search key. 751 # 752 # OR <search-key> <search-key>:: "or" two search keys together. 753 # 754 # ON <date>:: messages with an internal date exactly equal to <date>, 755 # which has a format similar to 8-Aug-2002. 756 # 757 # SINCE <date>:: messages with an internal date on or after <date>. 758 # 759 # SUBJECT <string>:: messages with <string> in their subject. 760 # 761 # TO <string>:: messages with <string> in their TO field. 762 # 763 # For example: 764 # 765 # p imap.search(["SUBJECT", "hello", "NOT", "NEW"]) 766 # #=> [1, 6, 7, 8] 767 def search(keys, charset = nil) 768 return search_internal("SEARCH", keys, charset) 769 end 770 771 # As for #search(), but returns unique identifiers. 772 def uid_search(keys, charset = nil) 773 return search_internal("UID SEARCH", keys, charset) 774 end 775 776 # Sends a FETCH command to retrieve data associated with a message 777 # in the mailbox. The +set+ parameter is a number or an array of 778 # numbers or a Range object. The number is a message sequence 779 # number. +attr+ is a list of attributes to fetch; see the 780 # documentation for Net::IMAP::FetchData for a list of valid 781 # attributes. 782 # The return value is an array of Net::IMAP::FetchData. For example: 783 # 784 # p imap.fetch(6..8, "UID") 785 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\ 786 # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\ 787 # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>] 788 # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]") 789 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>] 790 # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0] 791 # p data.seqno 792 # #=> 6 793 # p data.attr["RFC822.SIZE"] 794 # #=> 611 795 # p data.attr["INTERNALDATE"] 796 # #=> "12-Oct-2000 22:40:59 +0900" 797 # p data.attr["UID"] 798 # #=> 98 799 def fetch(set, attr) 800 return fetch_internal("FETCH", set, attr) 801 end 802 803 # As for #fetch(), but +set+ contains unique identifiers. 804 def uid_fetch(set, attr) 805 return fetch_internal("UID FETCH", set, attr) 806 end 807 808 # Sends a STORE command to alter data associated with messages 809 # in the mailbox, in particular their flags. The +set+ parameter 810 # is a number or an array of numbers or a Range object. Each number 811 # is a message sequence number. +attr+ is the name of a data item 812 # to store: 'FLAGS' means to replace the message's flag list 813 # with the provided one; '+FLAGS' means to add the provided flags; 814 # and '-FLAGS' means to remove them. +flags+ is a list of flags. 815 # 816 # The return value is an array of Net::IMAP::FetchData. For example: 817 # 818 # p imap.store(6..8, "+FLAGS", [:Deleted]) 819 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ 820 # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ 821 # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>] 822 def store(set, attr, flags) 823 return store_internal("STORE", set, attr, flags) 824 end 825 826 # As for #store(), but +set+ contains unique identifiers. 827 def uid_store(set, attr, flags) 828 return store_internal("UID STORE", set, attr, flags) 829 end 830 831 # Sends a COPY command to copy the specified message(s) to the end 832 # of the specified destination +mailbox+. The +set+ parameter is 833 # a number or an array of numbers or a Range object. The number is 834 # a message sequence number. 835 def copy(set, mailbox) 836 copy_internal("COPY", set, mailbox) 837 end 838 839 # As for #copy(), but +set+ contains unique identifiers. 840 def uid_copy(set, mailbox) 841 copy_internal("UID COPY", set, mailbox) 842 end 843 844 # Sends a SORT command to sort messages in the mailbox. 845 # Returns an array of message sequence numbers. For example: 846 # 847 # p imap.sort(["FROM"], ["ALL"], "US-ASCII") 848 # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9] 849 # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII") 850 # #=> [6, 7, 8, 1] 851 # 852 # See [SORT-THREAD-EXT] for more details. 853 def sort(sort_keys, search_keys, charset) 854 return sort_internal("SORT", sort_keys, search_keys, charset) 855 end 856 857 # As for #sort(), but returns an array of unique identifiers. 858 def uid_sort(sort_keys, search_keys, charset) 859 return sort_internal("UID SORT", sort_keys, search_keys, charset) 860 end 861 862 # Adds a response handler. For example, to detect when 863 # the server sends us a new EXISTS response (which normally 864 # indicates new messages being added to the mail box), 865 # you could add the following handler after selecting the 866 # mailbox. 867 # 868 # imap.add_response_handler { |resp| 869 # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS" 870 # puts "Mailbox now has #{resp.data} messages" 871 # end 872 # } 873 # 874 def add_response_handler(handler = Proc.new) 875 @response_handlers.push(handler) 876 end 877 878 # Removes the response handler. 879 def remove_response_handler(handler) 880 @response_handlers.delete(handler) 881 end 882 883 # As for #search(), but returns message sequence numbers in threaded 884 # format, as a Net::IMAP::ThreadMember tree. The supported algorithms 885 # are: 886 # 887 # ORDEREDSUBJECT:: split into single-level threads according to subject, 888 # ordered by date. 889 # REFERENCES:: split into threads by parent/child relationships determined 890 # by which message is a reply to which. 891 # 892 # Unlike #search(), +charset+ is a required argument. US-ASCII 893 # and UTF-8 are sample values. 894 # 895 # See [SORT-THREAD-EXT] for more details. 896 def thread(algorithm, search_keys, charset) 897 return thread_internal("THREAD", algorithm, search_keys, charset) 898 end 899 900 # As for #thread(), but returns unique identifiers instead of 901 # message sequence numbers. 902 def uid_thread(algorithm, search_keys, charset) 903 return thread_internal("UID THREAD", algorithm, search_keys, charset) 904 end 905 906 # Sends an IDLE command that waits for notifications of new or expunged 907 # messages. Yields responses from the server during the IDLE. 908 # 909 # Use #idle_done() to leave IDLE. 910 def idle(&response_handler) 911 raise LocalJumpError, "no block given" unless response_handler 912 913 response = nil 914 915 synchronize do 916 tag = Thread.current[:net_imap_tag] = generate_tag 917 put_string("#{tag} IDLE#{CRLF}") 918 919 begin 920 add_response_handler(response_handler) 921 @idle_done_cond = new_cond 922 @idle_done_cond.wait 923 @idle_done_cond = nil 924 if @receiver_thread_terminating 925 raise Net::IMAP::Error, "connection closed" 926 end 927 ensure 928 unless @receiver_thread_terminating 929 remove_response_handler(response_handler) 930 put_string("DONE#{CRLF}") 931 response = get_tagged_response(tag, "IDLE") 932 end 933 end 934 end 935 936 return response 937 end 938 939 # Leaves IDLE. 940 def idle_done 941 synchronize do 942 if @idle_done_cond.nil? 943 raise Net::IMAP::Error, "not during IDLE" 944 end 945 @idle_done_cond.signal 946 end 947 end 948 949 # Decode a string from modified UTF-7 format to UTF-8. 950 # 951 # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a 952 # slightly modified version of this to encode mailbox names 953 # containing non-ASCII characters; see [IMAP] section 5.1.3. 954 # 955 # Net::IMAP does _not_ automatically encode and decode 956 # mailbox names to and from utf7. 957 def self.decode_utf7(s) 958 return s.gsub(/&([^-]+)?-/n) { 959 if $1 960 ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE) 961 else 962 "&" 963 end 964 } 965 end 966 967 # Encode a string from UTF-8 format to modified UTF-7. 968 def self.encode_utf7(s) 969 return s.gsub(/(&)|[^\x20-\x7e]+/) { 970 if $1 971 "&-" 972 else 973 base64 = [$&.encode(Encoding::UTF_16BE)].pack("m") 974 "&" + base64.delete("=\n").tr("/", ",") + "-" 975 end 976 }.force_encoding("ASCII-8BIT") 977 end 978 979 # Formats +time+ as an IMAP-style date. 980 def self.format_date(time) 981 return time.strftime('%d-%b-%Y') 982 end 983 984 # Formats +time+ as an IMAP-style date-time. 985 def self.format_datetime(time) 986 return time.strftime('%d-%b-%Y %H:%M %z') 987 end 988 989 private 990 991 CRLF = "\r\n" # :nodoc: 992 PORT = 143 # :nodoc: 993 SSL_PORT = 993 # :nodoc: 994 995 @@debug = false 996 @@authenticators = {} 997 @@max_flag_count = 10000 998 999 # :call-seq: 1000 # Net::IMAP.new(host, options = {}) 1001 # 1002 # Creates a new Net::IMAP object and connects it to the specified 1003 # +host+. 1004 # 1005 # +options+ is an option hash, each key of which is a symbol. 1006 # 1007 # The available options are: 1008 # 1009 # port:: port number (default value is 143 for imap, or 993 for imaps) 1010 # ssl:: if options[:ssl] is true, then an attempt will be made 1011 # to use SSL (now TLS) to connect to the server. For this to work 1012 # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to 1013 # be installed. 1014 # if options[:ssl] is a hash, it's passed to 1015 # OpenSSL::SSL::SSLContext#set_params as parameters. 1016 # 1017 # The most common errors are: 1018 # 1019 # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening 1020 # firewall. 1021 # Errno::ETIMEDOUT:: connection timed out (possibly due to packets 1022 # being dropped by an intervening firewall). 1023 # Errno::ENETUNREACH:: there is no route to that network. 1024 # SocketError:: hostname not known or other socket error. 1025 # Net::IMAP::ByeResponseError:: we connected to the host, but they 1026 # immediately said goodbye to us. 1027 def initialize(host, port_or_options = {}, 1028 usessl = false, certs = nil, verify = true) 1029 super() 1030 @host = host 1031 begin 1032 options = port_or_options.to_hash 1033 rescue NoMethodError 1034 # for backward compatibility 1035 options = {} 1036 options[:port] = port_or_options 1037 if usessl 1038 options[:ssl] = create_ssl_params(certs, verify) 1039 end 1040 end 1041 @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) 1042 @tag_prefix = "RUBY" 1043 @tagno = 0 1044 @parser = ResponseParser.new 1045 @sock = TCPSocket.open(@host, @port) 1046 if options[:ssl] 1047 start_tls_session(options[:ssl]) 1048 @usessl = true 1049 else 1050 @usessl = false 1051 end 1052 @responses = Hash.new([].freeze) 1053 @tagged_responses = {} 1054 @response_handlers = [] 1055 @tagged_response_arrival = new_cond 1056 @continuation_request_arrival = new_cond 1057 @idle_done_cond = nil 1058 @logout_command_tag = nil 1059 @debug_output_bol = true 1060 @exception = nil 1061 1062 @greeting = get_response 1063 if @greeting.nil? 1064 @sock.close 1065 raise Error, "connection closed" 1066 end 1067 if @greeting.name == "BYE" 1068 @sock.close 1069 raise ByeResponseError, @greeting 1070 end 1071 1072 @client_thread = Thread.current 1073 @receiver_thread = Thread.start { 1074 begin 1075 receive_responses 1076 rescue Exception 1077 end 1078 } 1079 @receiver_thread_terminating = false 1080 end 1081 1082 def receive_responses 1083 connection_closed = false 1084 until connection_closed 1085 synchronize do 1086 @exception = nil 1087 end 1088 begin 1089 resp = get_response 1090 rescue Exception => e 1091 synchronize do 1092 @sock.close 1093 @exception = e 1094 end 1095 break 1096 end 1097 unless resp 1098 synchronize do 1099 @exception = EOFError.new("end of file reached") 1100 end 1101 break 1102 end 1103 begin 1104 synchronize do 1105 case resp 1106 when TaggedResponse 1107 @tagged_responses[resp.tag] = resp 1108 @tagged_response_arrival.broadcast 1109 if resp.tag == @logout_command_tag 1110 return 1111 end 1112 when UntaggedResponse 1113 record_response(resp.name, resp.data) 1114 if resp.data.instance_of?(ResponseText) && 1115 (code = resp.data.code) 1116 record_response(code.name, code.data) 1117 end 1118 if resp.name == "BYE" && @logout_command_tag.nil? 1119 @sock.close 1120 @exception = ByeResponseError.new(resp) 1121 connection_closed = true 1122 end 1123 when ContinuationRequest 1124 @continuation_request_arrival.signal 1125 end 1126 @response_handlers.each do |handler| 1127 handler.call(resp) 1128 end 1129 end 1130 rescue Exception => e 1131 @exception = e 1132 synchronize do 1133 @tagged_response_arrival.broadcast 1134 @continuation_request_arrival.broadcast 1135 end 1136 end 1137 end 1138 synchronize do 1139 @receiver_thread_terminating = true 1140 @tagged_response_arrival.broadcast 1141 @continuation_request_arrival.broadcast 1142 if @idle_done_cond 1143 @idle_done_cond.signal 1144 end 1145 end 1146 end 1147 1148 def get_tagged_response(tag, cmd) 1149 until @tagged_responses.key?(tag) 1150 raise @exception if @exception 1151 @tagged_response_arrival.wait 1152 end 1153 resp = @tagged_responses.delete(tag) 1154 case resp.name 1155 when /\A(?:NO)\z/ni 1156 raise NoResponseError, resp 1157 when /\A(?:BAD)\z/ni 1158 raise BadResponseError, resp 1159 else 1160 return resp 1161 end 1162 end 1163 1164 def get_response 1165 buff = "" 1166 while true 1167 s = @sock.gets(CRLF) 1168 break unless s 1169 buff.concat(s) 1170 if /\{(\d+)\}\r\n/n =~ s 1171 s = @sock.read($1.to_i) 1172 buff.concat(s) 1173 else 1174 break 1175 end 1176 end 1177 return nil if buff.length == 0 1178 if @@debug 1179 $stderr.print(buff.gsub(/^/n, "S: ")) 1180 end 1181 return @parser.parse(buff) 1182 end 1183 1184 def record_response(name, data) 1185 unless @responses.has_key?(name) 1186 @responses[name] = [] 1187 end 1188 @responses[name].push(data) 1189 end 1190 1191 def send_command(cmd, *args, &block) 1192 synchronize do 1193 args.each do |i| 1194 validate_data(i) 1195 end 1196 tag = generate_tag 1197 put_string(tag + " " + cmd) 1198 args.each do |i| 1199 put_string(" ") 1200 send_data(i) 1201 end 1202 put_string(CRLF) 1203 if cmd == "LOGOUT" 1204 @logout_command_tag = tag 1205 end 1206 if block 1207 add_response_handler(block) 1208 end 1209 begin 1210 return get_tagged_response(tag, cmd) 1211 ensure 1212 if block 1213 remove_response_handler(block) 1214 end 1215 end 1216 end 1217 end 1218 1219 def generate_tag 1220 @tagno += 1 1221 return format("%s%04d", @tag_prefix, @tagno) 1222 end 1223 1224 def put_string(str) 1225 @sock.print(str) 1226 if @@debug 1227 if @debug_output_bol 1228 $stderr.print("C: ") 1229 end 1230 $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: ")) 1231 if /\r\n\z/n.match(str) 1232 @debug_output_bol = true 1233 else 1234 @debug_output_bol = false 1235 end 1236 end 1237 end 1238 1239 def validate_data(data) 1240 case data 1241 when nil 1242 when String 1243 when Integer 1244 if data < 0 || data >= 4294967296 1245 raise DataFormatError, num.to_s 1246 end 1247 when Array 1248 data.each do |i| 1249 validate_data(i) 1250 end 1251 when Time 1252 when Symbol 1253 else 1254 data.validate 1255 end 1256 end 1257 1258 def send_data(data) 1259 case data 1260 when nil 1261 put_string("NIL") 1262 when String 1263 send_string_data(data) 1264 when Integer 1265 send_number_data(data) 1266 when Array 1267 send_list_data(data) 1268 when Time 1269 send_time_data(data) 1270 when Symbol 1271 send_symbol_data(data) 1272 else 1273 data.send_data(self) 1274 end 1275 end 1276 1277 def send_string_data(str) 1278 case str 1279 when "" 1280 put_string('""') 1281 when /[\x80-\xff\r\n]/n 1282 # literal 1283 send_literal(str) 1284 when /[(){ \x00-\x1f\x7f%*"\\]/n 1285 # quoted string 1286 send_quoted_string(str) 1287 else 1288 put_string(str) 1289 end 1290 end 1291 1292 def send_quoted_string(str) 1293 put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"') 1294 end 1295 1296 def send_literal(str) 1297 put_string("{" + str.bytesize.to_s + "}" + CRLF) 1298 @continuation_request_arrival.wait 1299 raise @exception if @exception 1300 put_string(str) 1301 end 1302 1303 def send_number_data(num) 1304 put_string(num.to_s) 1305 end 1306 1307 def send_list_data(list) 1308 put_string("(") 1309 first = true 1310 list.each do |i| 1311 if first 1312 first = false 1313 else 1314 put_string(" ") 1315 end 1316 send_data(i) 1317 end 1318 put_string(")") 1319 end 1320 1321 DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) 1322 1323 def send_time_data(time) 1324 t = time.dup.gmtime 1325 s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', 1326 t.day, DATE_MONTH[t.month - 1], t.year, 1327 t.hour, t.min, t.sec) 1328 put_string(s) 1329 end 1330 1331 def send_symbol_data(symbol) 1332 put_string("\\" + symbol.to_s) 1333 end 1334 1335 def search_internal(cmd, keys, charset) 1336 if keys.instance_of?(String) 1337 keys = [RawData.new(keys)] 1338 else 1339 normalize_searching_criteria(keys) 1340 end 1341 synchronize do 1342 if charset 1343 send_command(cmd, "CHARSET", charset, *keys) 1344 else 1345 send_command(cmd, *keys) 1346 end 1347 return @responses.delete("SEARCH")[-1] 1348 end 1349 end 1350 1351 def fetch_internal(cmd, set, attr) 1352 case attr 1353 when String then 1354 attr = RawData.new(attr) 1355 when Array then 1356 attr = attr.map { |arg| 1357 arg.is_a?(String) ? RawData.new(arg) : arg 1358 } 1359 end 1360 1361 synchronize do 1362 @responses.delete("FETCH") 1363 send_command(cmd, MessageSet.new(set), attr) 1364 return @responses.delete("FETCH") 1365 end 1366 end 1367 1368 def store_internal(cmd, set, attr, flags) 1369 if attr.instance_of?(String) 1370 attr = RawData.new(attr) 1371 end 1372 synchronize do 1373 @responses.delete("FETCH") 1374 send_command(cmd, MessageSet.new(set), attr, flags) 1375 return @responses.delete("FETCH") 1376 end 1377 end 1378 1379 def copy_internal(cmd, set, mailbox) 1380 send_command(cmd, MessageSet.new(set), mailbox) 1381 end 1382 1383 def sort_internal(cmd, sort_keys, search_keys, charset) 1384 if search_keys.instance_of?(String) 1385 search_keys = [RawData.new(search_keys)] 1386 else 1387 normalize_searching_criteria(search_keys) 1388 end 1389 normalize_searching_criteria(search_keys) 1390 synchronize do 1391 send_command(cmd, sort_keys, charset, *search_keys) 1392 return @responses.delete("SORT")[-1] 1393 end 1394 end 1395 1396 def thread_internal(cmd, algorithm, search_keys, charset) 1397 if search_keys.instance_of?(String) 1398 search_keys = [RawData.new(search_keys)] 1399 else 1400 normalize_searching_criteria(search_keys) 1401 end 1402 normalize_searching_criteria(search_keys) 1403 send_command(cmd, algorithm, charset, *search_keys) 1404 return @responses.delete("THREAD")[-1] 1405 end 1406 1407 def normalize_searching_criteria(keys) 1408 keys.collect! do |i| 1409 case i 1410 when -1, Range, Array 1411 MessageSet.new(i) 1412 else 1413 i 1414 end 1415 end 1416 end 1417 1418 def create_ssl_params(certs = nil, verify = true) 1419 params = {} 1420 if certs 1421 if File.file?(certs) 1422 params[:ca_file] = certs 1423 elsif File.directory?(certs) 1424 params[:ca_path] = certs 1425 end 1426 end 1427 if verify 1428 params[:verify_mode] = VERIFY_PEER 1429 else 1430 params[:verify_mode] = VERIFY_NONE 1431 end 1432 return params 1433 end 1434 1435 def start_tls_session(params = {}) 1436 unless defined?(OpenSSL::SSL) 1437 raise "SSL extension not installed" 1438 end 1439 if @sock.kind_of?(OpenSSL::SSL::SSLSocket) 1440 raise RuntimeError, "already using SSL" 1441 end 1442 begin 1443 params = params.to_hash 1444 rescue NoMethodError 1445 params = {} 1446 end 1447 context = SSLContext.new 1448 context.set_params(params) 1449 if defined?(VerifyCallbackProc) 1450 context.verify_callback = VerifyCallbackProc 1451 end 1452 @sock = SSLSocket.new(@sock, context) 1453 @sock.sync_close = true 1454 @sock.connect 1455 if context.verify_mode != VERIFY_NONE 1456 @sock.post_connection_check(@host) 1457 end 1458 end 1459 1460 class RawData # :nodoc: 1461 def send_data(imap) 1462 imap.send(:put_string, @data) 1463 end 1464 1465 def validate 1466 end 1467 1468 private 1469 1470 def initialize(data) 1471 @data = data 1472 end 1473 end 1474 1475 class Atom # :nodoc: 1476 def send_data(imap) 1477 imap.send(:put_string, @data) 1478 end 1479 1480 def validate 1481 end 1482 1483 private 1484 1485 def initialize(data) 1486 @data = data 1487 end 1488 end 1489 1490 class QuotedString # :nodoc: 1491 def send_data(imap) 1492 imap.send(:send_quoted_string, @data) 1493 end 1494 1495 def validate 1496 end 1497 1498 private 1499 1500 def initialize(data) 1501 @data = data 1502 end 1503 end 1504 1505 class Literal # :nodoc: 1506 def send_data(imap) 1507 imap.send(:send_literal, @data) 1508 end 1509 1510 def validate 1511 end 1512 1513 private 1514 1515 def initialize(data) 1516 @data = data 1517 end 1518 end 1519 1520 class MessageSet # :nodoc: 1521 def send_data(imap) 1522 imap.send(:put_string, format_internal(@data)) 1523 end 1524 1525 def validate 1526 validate_internal(@data) 1527 end 1528 1529 private 1530 1531 def initialize(data) 1532 @data = data 1533 end 1534 1535 def format_internal(data) 1536 case data 1537 when "*" 1538 return data 1539 when Integer 1540 if data == -1 1541 return "*" 1542 else 1543 return data.to_s 1544 end 1545 when Range 1546 return format_internal(data.first) + 1547 ":" + format_internal(data.last) 1548 when Array 1549 return data.collect {|i| format_internal(i)}.join(",") 1550 when ThreadMember 1551 return data.seqno.to_s + 1552 ":" + data.children.collect {|i| format_internal(i).join(",")} 1553 end 1554 end 1555 1556 def validate_internal(data) 1557 case data 1558 when "*" 1559 when Integer 1560 ensure_nz_number(data) 1561 when Range 1562 when Array 1563 data.each do |i| 1564 validate_internal(i) 1565 end 1566 when ThreadMember 1567 data.children.each do |i| 1568 validate_internal(i) 1569 end 1570 else 1571 raise DataFormatError, data.inspect 1572 end 1573 end 1574 1575 def ensure_nz_number(num) 1576 if num < -1 || num == 0 || num >= 4294967296 1577 msg = "nz_number must be non-zero unsigned 32-bit integer: " + 1578 num.inspect 1579 raise DataFormatError, msg 1580 end 1581 end 1582 end 1583 1584 # Net::IMAP::ContinuationRequest represents command continuation requests. 1585 # 1586 # The command continuation request response is indicated by a "+" token 1587 # instead of a tag. This form of response indicates that the server is 1588 # ready to accept the continuation of a command from the client. The 1589 # remainder of this response is a line of text. 1590 # 1591 # continue_req ::= "+" SPACE (resp_text / base64) 1592 # 1593 # ==== Fields: 1594 # 1595 # data:: Returns the data (Net::IMAP::ResponseText). 1596 # 1597 # raw_data:: Returns the raw data string. 1598 ContinuationRequest = Struct.new(:data, :raw_data) 1599 1600 # Net::IMAP::UntaggedResponse represents untagged responses. 1601 # 1602 # Data transmitted by the server to the client and status responses 1603 # that do not indicate command completion are prefixed with the token 1604 # "*", and are called untagged responses. 1605 # 1606 # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye / 1607 # mailbox_data / message_data / capability_data) 1608 # 1609 # ==== Fields: 1610 # 1611 # name:: Returns the name such as "FLAGS", "LIST", "FETCH".... 1612 # 1613 # data:: Returns the data such as an array of flag symbols, 1614 # a ((<Net::IMAP::MailboxList>)) object.... 1615 # 1616 # raw_data:: Returns the raw data string. 1617 UntaggedResponse = Struct.new(:name, :data, :raw_data) 1618 1619 # Net::IMAP::TaggedResponse represents tagged responses. 1620 # 1621 # The server completion result response indicates the success or 1622 # failure of the operation. It is tagged with the same tag as the 1623 # client command which began the operation. 1624 # 1625 # response_tagged ::= tag SPACE resp_cond_state CRLF 1626 # 1627 # tag ::= 1*<any ATOM_CHAR except "+"> 1628 # 1629 # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text 1630 # 1631 # ==== Fields: 1632 # 1633 # tag:: Returns the tag. 1634 # 1635 # name:: Returns the name. the name is one of "OK", "NO", "BAD". 1636 # 1637 # data:: Returns the data. See ((<Net::IMAP::ResponseText>)). 1638 # 1639 # raw_data:: Returns the raw data string. 1640 # 1641 TaggedResponse = Struct.new(:tag, :name, :data, :raw_data) 1642 1643 # Net::IMAP::ResponseText represents texts of responses. 1644 # The text may be prefixed by the response code. 1645 # 1646 # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) 1647 # ;; text SHOULD NOT begin with "[" or "=" 1648 # 1649 # ==== Fields: 1650 # 1651 # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)). 1652 # 1653 # text:: Returns the text. 1654 # 1655 ResponseText = Struct.new(:code, :text) 1656 1657 # 1658 # Net::IMAP::ResponseCode represents response codes. 1659 # 1660 # resp_text_code ::= "ALERT" / "PARSE" / 1661 # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / 1662 # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / 1663 # "UIDVALIDITY" SPACE nz_number / 1664 # "UNSEEN" SPACE nz_number / 1665 # atom [SPACE 1*<any TEXT_CHAR except "]">] 1666 # 1667 # ==== Fields: 1668 # 1669 # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY".... 1670 # 1671 # data:: Returns the data if it exists. 1672 # 1673 ResponseCode = Struct.new(:name, :data) 1674 1675 # Net::IMAP::MailboxList represents contents of the LIST response. 1676 # 1677 # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" / 1678 # "\Noselect" / "\Unmarked" / flag_extension) ")" 1679 # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox 1680 # 1681 # ==== Fields: 1682 # 1683 # attr:: Returns the name attributes. Each name attribute is a symbol 1684 # capitalized by String#capitalize, such as :Noselect (not :NoSelect). 1685 # 1686 # delim:: Returns the hierarchy delimiter 1687 # 1688 # name:: Returns the mailbox name. 1689 # 1690 MailboxList = Struct.new(:attr, :delim, :name) 1691 1692 # Net::IMAP::MailboxQuota represents contents of GETQUOTA response. 1693 # This object can also be a response to GETQUOTAROOT. In the syntax 1694 # specification below, the delimiter used with the "#" construct is a 1695 # single space (SPACE). 1696 # 1697 # quota_list ::= "(" #quota_resource ")" 1698 # 1699 # quota_resource ::= atom SPACE number SPACE number 1700 # 1701 # quota_response ::= "QUOTA" SPACE astring SPACE quota_list 1702 # 1703 # ==== Fields: 1704 # 1705 # mailbox:: The mailbox with the associated quota. 1706 # 1707 # usage:: Current storage usage of mailbox. 1708 # 1709 # quota:: Quota limit imposed on mailbox. 1710 # 1711 MailboxQuota = Struct.new(:mailbox, :usage, :quota) 1712 1713 # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT 1714 # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.) 1715 # 1716 # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring) 1717 # 1718 # ==== Fields: 1719 # 1720 # mailbox:: The mailbox with the associated quota. 1721 # 1722 # quotaroots:: Zero or more quotaroots that effect the quota on the 1723 # specified mailbox. 1724 # 1725 MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots) 1726 1727 # Net::IMAP::MailboxACLItem represents response from GETACL. 1728 # 1729 # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights) 1730 # 1731 # identifier ::= astring 1732 # 1733 # rights ::= astring 1734 # 1735 # ==== Fields: 1736 # 1737 # user:: Login name that has certain rights to the mailbox 1738 # that was specified with the getacl command. 1739 # 1740 # rights:: The access rights the indicated user has to the 1741 # mailbox. 1742 # 1743 MailboxACLItem = Struct.new(:user, :rights, :mailbox) 1744 1745 # Net::IMAP::StatusData represents contents of the STATUS response. 1746 # 1747 # ==== Fields: 1748 # 1749 # mailbox:: Returns the mailbox name. 1750 # 1751 # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT", 1752 # "UIDVALIDITY", "UNSEEN". Each value is a number. 1753 # 1754 StatusData = Struct.new(:mailbox, :attr) 1755 1756 # Net::IMAP::FetchData represents contents of the FETCH response. 1757 # 1758 # ==== Fields: 1759 # 1760 # seqno:: Returns the message sequence number. 1761 # (Note: not the unique identifier, even for the UID command response.) 1762 # 1763 # attr:: Returns a hash. Each key is a data item name, and each value is 1764 # its value. 1765 # 1766 # The current data items are: 1767 # 1768 # [BODY] 1769 # A form of BODYSTRUCTURE without extension data. 1770 # [BODY[<section>]<<origin_octet>>] 1771 # A string expressing the body contents of the specified section. 1772 # [BODYSTRUCTURE] 1773 # An object that describes the [MIME-IMB] body structure of a message. 1774 # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText, 1775 # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart. 1776 # [ENVELOPE] 1777 # A Net::IMAP::Envelope object that describes the envelope 1778 # structure of a message. 1779 # [FLAGS] 1780 # A array of flag symbols that are set for this message. flag symbols 1781 # are capitalized by String#capitalize. 1782 # [INTERNALDATE] 1783 # A string representing the internal date of the message. 1784 # [RFC822] 1785 # Equivalent to BODY[]. 1786 # [RFC822.HEADER] 1787 # Equivalent to BODY.PEEK[HEADER]. 1788 # [RFC822.SIZE] 1789 # A number expressing the [RFC-822] size of the message. 1790 # [RFC822.TEXT] 1791 # Equivalent to BODY[TEXT]. 1792 # [UID] 1793 # A number expressing the unique identifier of the message. 1794 # 1795 FetchData = Struct.new(:seqno, :attr) 1796 1797 # Net::IMAP::Envelope represents envelope structures of messages. 1798 # 1799 # ==== Fields: 1800 # 1801 # date:: Returns a string that represents the date. 1802 # 1803 # subject:: Returns a string that represents the subject. 1804 # 1805 # from:: Returns an array of Net::IMAP::Address that represents the from. 1806 # 1807 # sender:: Returns an array of Net::IMAP::Address that represents the sender. 1808 # 1809 # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to. 1810 # 1811 # to:: Returns an array of Net::IMAP::Address that represents the to. 1812 # 1813 # cc:: Returns an array of Net::IMAP::Address that represents the cc. 1814 # 1815 # bcc:: Returns an array of Net::IMAP::Address that represents the bcc. 1816 # 1817 # in_reply_to:: Returns a string that represents the in-reply-to. 1818 # 1819 # message_id:: Returns a string that represents the message-id. 1820 # 1821 Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to, 1822 :to, :cc, :bcc, :in_reply_to, :message_id) 1823 1824 # 1825 # Net::IMAP::Address represents electronic mail addresses. 1826 # 1827 # ==== Fields: 1828 # 1829 # name:: Returns the phrase from [RFC-822] mailbox. 1830 # 1831 # route:: Returns the route from [RFC-822] route-addr. 1832 # 1833 # mailbox:: nil indicates end of [RFC-822] group. 1834 # If non-nil and host is nil, returns [RFC-822] group name. 1835 # Otherwise, returns [RFC-822] local-part 1836 # 1837 # host:: nil indicates [RFC-822] group syntax. 1838 # Otherwise, returns [RFC-822] domain name. 1839 # 1840 Address = Struct.new(:name, :route, :mailbox, :host) 1841 1842 # 1843 # Net::IMAP::ContentDisposition represents Content-Disposition fields. 1844 # 1845 # ==== Fields: 1846 # 1847 # dsp_type:: Returns the disposition type. 1848 # 1849 # param:: Returns a hash that represents parameters of the Content-Disposition 1850 # field. 1851 # 1852 ContentDisposition = Struct.new(:dsp_type, :param) 1853 1854 # Net::IMAP::ThreadMember represents a thread-node returned 1855 # by Net::IMAP#thread 1856 # 1857 # ==== Fields: 1858 # 1859 # seqno:: The sequence number of this message. 1860 # 1861 # children:: an array of Net::IMAP::ThreadMember objects for mail 1862 # items that are children of this in the thread. 1863 # 1864 ThreadMember = Struct.new(:seqno, :children) 1865 1866 # Net::IMAP::BodyTypeBasic represents basic body structures of messages. 1867 # 1868 # ==== Fields: 1869 # 1870 # media_type:: Returns the content media type name as defined in [MIME-IMB]. 1871 # 1872 # subtype:: Returns the content subtype name as defined in [MIME-IMB]. 1873 # 1874 # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. 1875 # 1876 # content_id:: Returns a string giving the content id as defined in [MIME-IMB]. 1877 # 1878 # description:: Returns a string giving the content description as defined in 1879 # [MIME-IMB]. 1880 # 1881 # encoding:: Returns a string giving the content transfer encoding as defined in 1882 # [MIME-IMB]. 1883 # 1884 # size:: Returns a number giving the size of the body in octets. 1885 # 1886 # md5:: Returns a string giving the body MD5 value as defined in [MD5]. 1887 # 1888 # disposition:: Returns a Net::IMAP::ContentDisposition object giving 1889 # the content disposition. 1890 # 1891 # language:: Returns a string or an array of strings giving the body 1892 # language value as defined in [LANGUAGE-TAGS]. 1893 # 1894 # extension:: Returns extension data. 1895 # 1896 # multipart?:: Returns false. 1897 # 1898 class BodyTypeBasic < Struct.new(:media_type, :subtype, 1899 :param, :content_id, 1900 :description, :encoding, :size, 1901 :md5, :disposition, :language, 1902 :extension) 1903 def multipart? 1904 return false 1905 end 1906 1907 # Obsolete: use +subtype+ instead. Calling this will 1908 # generate a warning message to +stderr+, then return 1909 # the value of +subtype+. 1910 def media_subtype 1911 $stderr.printf("warning: media_subtype is obsolete.\n") 1912 $stderr.printf(" use subtype instead.\n") 1913 return subtype 1914 end 1915 end 1916 1917 # Net::IMAP::BodyTypeText represents TEXT body structures of messages. 1918 # 1919 # ==== Fields: 1920 # 1921 # lines:: Returns the size of the body in text lines. 1922 # 1923 # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic. 1924 # 1925 class BodyTypeText < Struct.new(:media_type, :subtype, 1926 :param, :content_id, 1927 :description, :encoding, :size, 1928 :lines, 1929 :md5, :disposition, :language, 1930 :extension) 1931 def multipart? 1932 return false 1933 end 1934 1935 # Obsolete: use +subtype+ instead. Calling this will 1936 # generate a warning message to +stderr+, then return 1937 # the value of +subtype+. 1938 def media_subtype 1939 $stderr.printf("warning: media_subtype is obsolete.\n") 1940 $stderr.printf(" use subtype instead.\n") 1941 return subtype 1942 end 1943 end 1944 1945 # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages. 1946 # 1947 # ==== Fields: 1948 # 1949 # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure. 1950 # 1951 # body:: Returns an object giving the body structure. 1952 # 1953 # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText. 1954 # 1955 class BodyTypeMessage < Struct.new(:media_type, :subtype, 1956 :param, :content_id, 1957 :description, :encoding, :size, 1958 :envelope, :body, :lines, 1959 :md5, :disposition, :language, 1960 :extension) 1961 def multipart? 1962 return false 1963 end 1964 1965 # Obsolete: use +subtype+ instead. Calling this will 1966 # generate a warning message to +stderr+, then return 1967 # the value of +subtype+. 1968 def media_subtype 1969 $stderr.printf("warning: media_subtype is obsolete.\n") 1970 $stderr.printf(" use subtype instead.\n") 1971 return subtype 1972 end 1973 end 1974 1975 # Net::IMAP::BodyTypeAttachment represents attachment body structures 1976 # of messages. 1977 # 1978 # ==== Fields: 1979 # 1980 # media_type:: Returns the content media type name. 1981 # 1982 # subtype:: Returns +nil+. 1983 # 1984 # param:: Returns a hash that represents parameters. 1985 # 1986 # multipart?:: Returns false. 1987 # 1988 class BodyTypeAttachment < Struct.new(:media_type, :subtype, 1989 :param) 1990 def multipart? 1991 return false 1992 end 1993 end 1994 1995 # Net::IMAP::BodyTypeMultipart represents multipart body structures 1996 # of messages. 1997 # 1998 # ==== Fields: 1999 # 2000 # media_type:: Returns the content media type name as defined in [MIME-IMB]. 2001 # 2002 # subtype:: Returns the content subtype name as defined in [MIME-IMB]. 2003 # 2004 # parts:: Returns multiple parts. 2005 # 2006 # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. 2007 # 2008 # disposition:: Returns a Net::IMAP::ContentDisposition object giving 2009 # the content disposition. 2010 # 2011 # language:: Returns a string or an array of strings giving the body 2012 # language value as defined in [LANGUAGE-TAGS]. 2013 # 2014 # extension:: Returns extension data. 2015 # 2016 # multipart?:: Returns true. 2017 # 2018 class BodyTypeMultipart < Struct.new(:media_type, :subtype, 2019 :parts, 2020 :param, :disposition, :language, 2021 :extension) 2022 def multipart? 2023 return true 2024 end 2025 2026 # Obsolete: use +subtype+ instead. Calling this will 2027 # generate a warning message to +stderr+, then return 2028 # the value of +subtype+. 2029 def media_subtype 2030 $stderr.printf("warning: media_subtype is obsolete.\n") 2031 $stderr.printf(" use subtype instead.\n") 2032 return subtype 2033 end 2034 end 2035 2036 class BodyTypeExtension < Struct.new(:media_type, :subtype, 2037 :params, :content_id, 2038 :description, :encoding, :size) 2039 def multipart? 2040 return false 2041 end 2042 end 2043 2044 class ResponseParser # :nodoc: 2045 def initialize 2046 @str = nil 2047 @pos = nil 2048 @lex_state = nil 2049 @token = nil 2050 @flag_symbols = {} 2051 end 2052 2053 def parse(str) 2054 @str = str 2055 @pos = 0 2056 @lex_state = EXPR_BEG 2057 @token = nil 2058 return response 2059 end 2060 2061 private 2062 2063 EXPR_BEG = :EXPR_BEG 2064 EXPR_DATA = :EXPR_DATA 2065 EXPR_TEXT = :EXPR_TEXT 2066 EXPR_RTEXT = :EXPR_RTEXT 2067 EXPR_CTEXT = :EXPR_CTEXT 2068 2069 T_SPACE = :SPACE 2070 T_NIL = :NIL 2071 T_NUMBER = :NUMBER 2072 T_ATOM = :ATOM 2073 T_QUOTED = :QUOTED 2074 T_LPAR = :LPAR 2075 T_RPAR = :RPAR 2076 T_BSLASH = :BSLASH 2077 T_STAR = :STAR 2078 T_LBRA = :LBRA 2079 T_RBRA = :RBRA 2080 T_LITERAL = :LITERAL 2081 T_PLUS = :PLUS 2082 T_PERCENT = :PERCENT 2083 T_CRLF = :CRLF 2084 T_EOF = :EOF 2085 T_TEXT = :TEXT 2086 2087 BEG_REGEXP = /\G(?:\ 2088(?# 1: SPACE )( +)|\ 2089(?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*#{'"'}\\\[\]+])|\ 2090(?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*#{'"'}\\\[\]+])|\ 2091(?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*#{'"'}\\\[\]+]+)|\ 2092(?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ 2093(?# 6: LPAR )(\()|\ 2094(?# 7: RPAR )(\))|\ 2095(?# 8: BSLASH )(\\)|\ 2096(?# 9: STAR )(\*)|\ 2097(?# 10: LBRA )(\[)|\ 2098(?# 11: RBRA )(\])|\ 2099(?# 12: LITERAL )\{(\d+)\}\r\n|\ 2100(?# 13: PLUS )(\+)|\ 2101(?# 14: PERCENT )(%)|\ 2102(?# 15: CRLF )(\r\n)|\ 2103(?# 16: EOF )(\z))/ni 2104 2105 DATA_REGEXP = /\G(?:\ 2106(?# 1: SPACE )( )|\ 2107(?# 2: NIL )(NIL)|\ 2108(?# 3: NUMBER )(\d+)|\ 2109(?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ 2110(?# 5: LITERAL )\{(\d+)\}\r\n|\ 2111(?# 6: LPAR )(\()|\ 2112(?# 7: RPAR )(\)))/ni 2113 2114 TEXT_REGEXP = /\G(?:\ 2115(?# 1: TEXT )([^\x00\r\n]*))/ni 2116 2117 RTEXT_REGEXP = /\G(?:\ 2118(?# 1: LBRA )(\[)|\ 2119(?# 2: TEXT )([^\x00\r\n]*))/ni 2120 2121 CTEXT_REGEXP = /\G(?:\ 2122(?# 1: TEXT )([^\x00\r\n\]]*))/ni 2123 2124 Token = Struct.new(:symbol, :value) 2125 2126 def response 2127 token = lookahead 2128 case token.symbol 2129 when T_PLUS 2130 result = continue_req 2131 when T_STAR 2132 result = response_untagged 2133 else 2134 result = response_tagged 2135 end 2136 match(T_CRLF) 2137 match(T_EOF) 2138 return result 2139 end 2140 2141 def continue_req 2142 match(T_PLUS) 2143 match(T_SPACE) 2144 return ContinuationRequest.new(resp_text, @str) 2145 end 2146 2147 def response_untagged 2148 match(T_STAR) 2149 match(T_SPACE) 2150 token = lookahead 2151 if token.symbol == T_NUMBER 2152 return numeric_response 2153 elsif token.symbol == T_ATOM 2154 case token.value 2155 when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni 2156 return response_cond 2157 when /\A(?:FLAGS)\z/ni 2158 return flags_response 2159 when /\A(?:LIST|LSUB|XLIST)\z/ni 2160 return list_response 2161 when /\A(?:QUOTA)\z/ni 2162 return getquota_response 2163 when /\A(?:QUOTAROOT)\z/ni 2164 return getquotaroot_response 2165 when /\A(?:ACL)\z/ni 2166 return getacl_response 2167 when /\A(?:SEARCH|SORT)\z/ni 2168 return search_response 2169 when /\A(?:THREAD)\z/ni 2170 return thread_response 2171 when /\A(?:STATUS)\z/ni 2172 return status_response 2173 when /\A(?:CAPABILITY)\z/ni 2174 return capability_response 2175 else 2176 return text_response 2177 end 2178 else 2179 parse_error("unexpected token %s", token.symbol) 2180 end 2181 end 2182 2183 def response_tagged 2184 tag = atom 2185 match(T_SPACE) 2186 token = match(T_ATOM) 2187 name = token.value.upcase 2188 match(T_SPACE) 2189 return TaggedResponse.new(tag, name, resp_text, @str) 2190 end 2191 2192 def response_cond 2193 token = match(T_ATOM) 2194 name = token.value.upcase 2195 match(T_SPACE) 2196 return UntaggedResponse.new(name, resp_text, @str) 2197 end 2198 2199 def numeric_response 2200 n = number 2201 match(T_SPACE) 2202 token = match(T_ATOM) 2203 name = token.value.upcase 2204 case name 2205 when "EXISTS", "RECENT", "EXPUNGE" 2206 return UntaggedResponse.new(name, n, @str) 2207 when "FETCH" 2208 shift_token 2209 match(T_SPACE) 2210 data = FetchData.new(n, msg_att(n)) 2211 return UntaggedResponse.new(name, data, @str) 2212 end 2213 end 2214 2215 def msg_att(n) 2216 match(T_LPAR) 2217 attr = {} 2218 while true 2219 token = lookahead 2220 case token.symbol 2221 when T_RPAR 2222 shift_token 2223 break 2224 when T_SPACE 2225 shift_token 2226 next 2227 end 2228 case token.value 2229 when /\A(?:ENVELOPE)\z/ni 2230 name, val = envelope_data 2231 when /\A(?:FLAGS)\z/ni 2232 name, val = flags_data 2233 when /\A(?:INTERNALDATE)\z/ni 2234 name, val = internaldate_data 2235 when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni 2236 name, val = rfc822_text 2237 when /\A(?:RFC822\.SIZE)\z/ni 2238 name, val = rfc822_size 2239 when /\A(?:BODY(?:STRUCTURE)?)\z/ni 2240 name, val = body_data 2241 when /\A(?:UID)\z/ni 2242 name, val = uid_data 2243 else 2244 parse_error("unknown attribute `%s' for {%d}", token.value, n) 2245 end 2246 attr[name] = val 2247 end 2248 return attr 2249 end 2250 2251 def envelope_data 2252 token = match(T_ATOM) 2253 name = token.value.upcase 2254 match(T_SPACE) 2255 return name, envelope 2256 end 2257 2258 def envelope 2259 @lex_state = EXPR_DATA 2260 token = lookahead 2261 if token.symbol == T_NIL 2262 shift_token 2263 result = nil 2264 else 2265 match(T_LPAR) 2266 date = nstring 2267 match(T_SPACE) 2268 subject = nstring 2269 match(T_SPACE) 2270 from = address_list 2271 match(T_SPACE) 2272 sender = address_list 2273 match(T_SPACE) 2274 reply_to = address_list 2275 match(T_SPACE) 2276 to = address_list 2277 match(T_SPACE) 2278 cc = address_list 2279 match(T_SPACE) 2280 bcc = address_list 2281 match(T_SPACE) 2282 in_reply_to = nstring 2283 match(T_SPACE) 2284 message_id = nstring 2285 match(T_RPAR) 2286 result = Envelope.new(date, subject, from, sender, reply_to, 2287 to, cc, bcc, in_reply_to, message_id) 2288 end 2289 @lex_state = EXPR_BEG 2290 return result 2291 end 2292 2293 def flags_data 2294 token = match(T_ATOM) 2295 name = token.value.upcase 2296 match(T_SPACE) 2297 return name, flag_list 2298 end 2299 2300 def internaldate_data 2301 token = match(T_ATOM) 2302 name = token.value.upcase 2303 match(T_SPACE) 2304 token = match(T_QUOTED) 2305 return name, token.value 2306 end 2307 2308 def rfc822_text 2309 token = match(T_ATOM) 2310 name = token.value.upcase 2311 token = lookahead 2312 if token.symbol == T_LBRA 2313 shift_token 2314 match(T_RBRA) 2315 end 2316 match(T_SPACE) 2317 return name, nstring 2318 end 2319 2320 def rfc822_size 2321 token = match(T_ATOM) 2322 name = token.value.upcase 2323 match(T_SPACE) 2324 return name, number 2325 end 2326 2327 def body_data 2328 token = match(T_ATOM) 2329 name = token.value.upcase 2330 token = lookahead 2331 if token.symbol == T_SPACE 2332 shift_token 2333 return name, body 2334 end 2335 name.concat(section) 2336 token = lookahead 2337 if token.symbol == T_ATOM 2338 name.concat(token.value) 2339 shift_token 2340 end 2341 match(T_SPACE) 2342 data = nstring 2343 return name, data 2344 end 2345 2346 def body 2347 @lex_state = EXPR_DATA 2348 token = lookahead 2349 if token.symbol == T_NIL 2350 shift_token 2351 result = nil 2352 else 2353 match(T_LPAR) 2354 token = lookahead 2355 if token.symbol == T_LPAR 2356 result = body_type_mpart 2357 else 2358 result = body_type_1part 2359 end 2360 match(T_RPAR) 2361 end 2362 @lex_state = EXPR_BEG 2363 return result 2364 end 2365 2366 def body_type_1part 2367 token = lookahead 2368 case token.value 2369 when /\A(?:TEXT)\z/ni 2370 return body_type_text 2371 when /\A(?:MESSAGE)\z/ni 2372 return body_type_msg 2373 when /\A(?:ATTACHMENT)\z/ni 2374 return body_type_attachment 2375 else 2376 return body_type_basic 2377 end 2378 end 2379 2380 def body_type_basic 2381 mtype, msubtype = media_type 2382 token = lookahead 2383 if token.symbol == T_RPAR 2384 return BodyTypeBasic.new(mtype, msubtype) 2385 end 2386 match(T_SPACE) 2387 param, content_id, desc, enc, size = body_fields 2388 md5, disposition, language, extension = body_ext_1part 2389 return BodyTypeBasic.new(mtype, msubtype, 2390 param, content_id, 2391 desc, enc, size, 2392 md5, disposition, language, extension) 2393 end 2394 2395 def body_type_text 2396 mtype, msubtype = media_type 2397 match(T_SPACE) 2398 param, content_id, desc, enc, size = body_fields 2399 match(T_SPACE) 2400 lines = number 2401 md5, disposition, language, extension = body_ext_1part 2402 return BodyTypeText.new(mtype, msubtype, 2403 param, content_id, 2404 desc, enc, size, 2405 lines, 2406 md5, disposition, language, extension) 2407 end 2408 2409 def body_type_msg 2410 mtype, msubtype = media_type 2411 match(T_SPACE) 2412 param, content_id, desc, enc, size = body_fields 2413 2414 # If this is not message/rfc822, we shouldn't apply the RFC822 spec 2415 # to it. 2416 # We should handle anything other than message/rfc822 using 2417 # multipart extension data [rfc3501] (i.e. the data itself won't be 2418 # returned, we would have to retrieve it with BODYSTRUCTURE instead 2419 # of with BODY 2420 if "#{mtype}/#{msubtype}" != 'MESSAGE/RFC822' then 2421 return BodyTypeExtension.new(mtype, msubtype, 2422 param, content_id, 2423 desc, enc, size) 2424 end 2425 2426 # Also, sometimes a message/rfc822 is included as a large 2427 # attachment instead of having all of the other details 2428 # (e.g. attaching a .eml file to an email) 2429 2430 token = lookahead 2431 if token.symbol == T_RPAR then 2432 return BodyTypeMessage.new(mtype, msubtype, param, content_id, 2433 desc, enc, size, nil, nil, nil, nil, 2434 nil, nil, nil) 2435 end 2436 2437 match(T_SPACE) 2438 env = envelope 2439 match(T_SPACE) 2440 b = body 2441 match(T_SPACE) 2442 lines = number 2443 md5, disposition, language, extension = body_ext_1part 2444 return BodyTypeMessage.new(mtype, msubtype, 2445 param, content_id, 2446 desc, enc, size, 2447 env, b, lines, 2448 md5, disposition, language, extension) 2449 end 2450 2451 def body_type_attachment 2452 mtype = case_insensitive_string 2453 match(T_SPACE) 2454 param = body_fld_param 2455 return BodyTypeAttachment.new(mtype, nil, param) 2456 end 2457 2458 def body_type_mpart 2459 parts = [] 2460 while true 2461 token = lookahead 2462 if token.symbol == T_SPACE 2463 shift_token 2464 break 2465 end 2466 parts.push(body) 2467 end 2468 mtype = "MULTIPART" 2469 msubtype = case_insensitive_string 2470 param, disposition, language, extension = body_ext_mpart 2471 return BodyTypeMultipart.new(mtype, msubtype, parts, 2472 param, disposition, language, 2473 extension) 2474 end 2475 2476 def media_type 2477 mtype = case_insensitive_string 2478 token = lookahead 2479 if token.symbol != T_SPACE 2480 return mtype, nil 2481 end 2482 match(T_SPACE) 2483 msubtype = case_insensitive_string 2484 return mtype, msubtype 2485 end 2486 2487 def body_fields 2488 param = body_fld_param 2489 match(T_SPACE) 2490 content_id = nstring 2491 match(T_SPACE) 2492 desc = nstring 2493 match(T_SPACE) 2494 enc = case_insensitive_string 2495 match(T_SPACE) 2496 size = number 2497 return param, content_id, desc, enc, size 2498 end 2499 2500 def body_fld_param 2501 token = lookahead 2502 if token.symbol == T_NIL 2503 shift_token 2504 return nil 2505 end 2506 match(T_LPAR) 2507 param = {} 2508 while true 2509 token = lookahead 2510 case token.symbol 2511 when T_RPAR 2512 shift_token 2513 break 2514 when T_SPACE 2515 shift_token 2516 end 2517 name = case_insensitive_string 2518 match(T_SPACE) 2519 val = string 2520 param[name] = val 2521 end 2522 return param 2523 end 2524 2525 def body_ext_1part 2526 token = lookahead 2527 if token.symbol == T_SPACE 2528 shift_token 2529 else 2530 return nil 2531 end 2532 md5 = nstring 2533 2534 token = lookahead 2535 if token.symbol == T_SPACE 2536 shift_token 2537 else 2538 return md5 2539 end 2540 disposition = body_fld_dsp 2541 2542 token = lookahead 2543 if token.symbol == T_SPACE 2544 shift_token 2545 else 2546 return md5, disposition 2547 end 2548 language = body_fld_lang 2549 2550 token = lookahead 2551 if token.symbol == T_SPACE 2552 shift_token 2553 else 2554 return md5, disposition, language 2555 end 2556 2557 extension = body_extensions 2558 return md5, disposition, language, extension 2559 end 2560 2561 def body_ext_mpart 2562 token = lookahead 2563 if token.symbol == T_SPACE 2564 shift_token 2565 else 2566 return nil 2567 end 2568 param = body_fld_param 2569 2570 token = lookahead 2571 if token.symbol == T_SPACE 2572 shift_token 2573 else 2574 return param 2575 end 2576 disposition = body_fld_dsp 2577 match(T_SPACE) 2578 language = body_fld_lang 2579 2580 token = lookahead 2581 if token.symbol == T_SPACE 2582 shift_token 2583 else 2584 return param, disposition, language 2585 end 2586 2587 extension = body_extensions 2588 return param, disposition, language, extension 2589 end 2590 2591 def body_fld_dsp 2592 token = lookahead 2593 if token.symbol == T_NIL 2594 shift_token 2595 return nil 2596 end 2597 match(T_LPAR) 2598 dsp_type = case_insensitive_string 2599 match(T_SPACE) 2600 param = body_fld_param 2601 match(T_RPAR) 2602 return ContentDisposition.new(dsp_type, param) 2603 end 2604 2605 def body_fld_lang 2606 token = lookahead 2607 if token.symbol == T_LPAR 2608 shift_token 2609 result = [] 2610 while true 2611 token = lookahead 2612 case token.symbol 2613 when T_RPAR 2614 shift_token 2615 return result 2616 when T_SPACE 2617 shift_token 2618 end 2619 result.push(case_insensitive_string) 2620 end 2621 else 2622 lang = nstring 2623 if lang 2624 return lang.upcase 2625 else 2626 return lang 2627 end 2628 end 2629 end 2630 2631 def body_extensions 2632 result = [] 2633 while true 2634 token = lookahead 2635 case token.symbol 2636 when T_RPAR 2637 return result 2638 when T_SPACE 2639 shift_token 2640 end 2641 result.push(body_extension) 2642 end 2643 end 2644 2645 def body_extension 2646 token = lookahead 2647 case token.symbol 2648 when T_LPAR 2649 shift_token 2650 result = body_extensions 2651 match(T_RPAR) 2652 return result 2653 when T_NUMBER 2654 return number 2655 else 2656 return nstring 2657 end 2658 end 2659 2660 def section 2661 str = "" 2662 token = match(T_LBRA) 2663 str.concat(token.value) 2664 token = match(T_ATOM, T_NUMBER, T_RBRA) 2665 if token.symbol == T_RBRA 2666 str.concat(token.value) 2667 return str 2668 end 2669 str.concat(token.value) 2670 token = lookahead 2671 if token.symbol == T_SPACE 2672 shift_token 2673 str.concat(token.value) 2674 token = match(T_LPAR) 2675 str.concat(token.value) 2676 while true 2677 token = lookahead 2678 case token.symbol 2679 when T_RPAR 2680 str.concat(token.value) 2681 shift_token 2682 break 2683 when T_SPACE 2684 shift_token 2685 str.concat(token.value) 2686 end 2687 str.concat(format_string(astring)) 2688 end 2689 end 2690 token = match(T_RBRA) 2691 str.concat(token.value) 2692 return str 2693 end 2694 2695 def format_string(str) 2696 case str 2697 when "" 2698 return '""' 2699 when /[\x80-\xff\r\n]/n 2700 # literal 2701 return "{" + str.bytesize.to_s + "}" + CRLF + str 2702 when /[(){ \x00-\x1f\x7f%*"\\]/n 2703 # quoted string 2704 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"' 2705 else 2706 # atom 2707 return str 2708 end 2709 end 2710 2711 def uid_data 2712 token = match(T_ATOM) 2713 name = token.value.upcase 2714 match(T_SPACE) 2715 return name, number 2716 end 2717 2718 def text_response 2719 token = match(T_ATOM) 2720 name = token.value.upcase 2721 match(T_SPACE) 2722 @lex_state = EXPR_TEXT 2723 token = match(T_TEXT) 2724 @lex_state = EXPR_BEG 2725 return UntaggedResponse.new(name, token.value) 2726 end 2727 2728 def flags_response 2729 token = match(T_ATOM) 2730 name = token.value.upcase 2731 match(T_SPACE) 2732 return UntaggedResponse.new(name, flag_list, @str) 2733 end 2734 2735 def list_response 2736 token = match(T_ATOM) 2737 name = token.value.upcase 2738 match(T_SPACE) 2739 return UntaggedResponse.new(name, mailbox_list, @str) 2740 end 2741 2742 def mailbox_list 2743 attr = flag_list 2744 match(T_SPACE) 2745 token = match(T_QUOTED, T_NIL) 2746 if token.symbol == T_NIL 2747 delim = nil 2748 else 2749 delim = token.value 2750 end 2751 match(T_SPACE) 2752 name = astring 2753 return MailboxList.new(attr, delim, name) 2754 end 2755 2756 def getquota_response 2757 # If quota never established, get back 2758 # `NO Quota root does not exist'. 2759 # If quota removed, get `()' after the 2760 # folder spec with no mention of `STORAGE'. 2761 token = match(T_ATOM) 2762 name = token.value.upcase 2763 match(T_SPACE) 2764 mailbox = astring 2765 match(T_SPACE) 2766 match(T_LPAR) 2767 token = lookahead 2768 case token.symbol 2769 when T_RPAR 2770 shift_token 2771 data = MailboxQuota.new(mailbox, nil, nil) 2772 return UntaggedResponse.new(name, data, @str) 2773 when T_ATOM 2774 shift_token 2775 match(T_SPACE) 2776 token = match(T_NUMBER) 2777 usage = token.value 2778 match(T_SPACE) 2779 token = match(T_NUMBER) 2780 quota = token.value 2781 match(T_RPAR) 2782 data = MailboxQuota.new(mailbox, usage, quota) 2783 return UntaggedResponse.new(name, data, @str) 2784 else 2785 parse_error("unexpected token %s", token.symbol) 2786 end 2787 end 2788 2789 def getquotaroot_response 2790 # Similar to getquota, but only admin can use getquota. 2791 token = match(T_ATOM) 2792 name = token.value.upcase 2793 match(T_SPACE) 2794 mailbox = astring 2795 quotaroots = [] 2796 while true 2797 token = lookahead 2798 break unless token.symbol == T_SPACE 2799 shift_token 2800 quotaroots.push(astring) 2801 end 2802 data = MailboxQuotaRoot.new(mailbox, quotaroots) 2803 return UntaggedResponse.new(name, data, @str) 2804 end 2805 2806 def getacl_response 2807 token = match(T_ATOM) 2808 name = token.value.upcase 2809 match(T_SPACE) 2810 mailbox = astring 2811 data = [] 2812 token = lookahead 2813 if token.symbol == T_SPACE 2814 shift_token 2815 while true 2816 token = lookahead 2817 case token.symbol 2818 when T_CRLF 2819 break 2820 when T_SPACE 2821 shift_token 2822 end 2823 user = astring 2824 match(T_SPACE) 2825 rights = astring 2826 data.push(MailboxACLItem.new(user, rights, mailbox)) 2827 end 2828 end 2829 return UntaggedResponse.new(name, data, @str) 2830 end 2831 2832 def search_response 2833 token = match(T_ATOM) 2834 name = token.value.upcase 2835 token = lookahead 2836 if token.symbol == T_SPACE 2837 shift_token 2838 data = [] 2839 while true 2840 token = lookahead 2841 case token.symbol 2842 when T_CRLF 2843 break 2844 when T_SPACE 2845 shift_token 2846 else 2847 data.push(number) 2848 end 2849 end 2850 else 2851 data = [] 2852 end 2853 return UntaggedResponse.new(name, data, @str) 2854 end 2855 2856 def thread_response 2857 token = match(T_ATOM) 2858 name = token.value.upcase 2859 token = lookahead 2860 2861 if token.symbol == T_SPACE 2862 threads = [] 2863 2864 while true 2865 shift_token 2866 token = lookahead 2867 2868 case token.symbol 2869 when T_LPAR 2870 threads << thread_branch(token) 2871 when T_CRLF 2872 break 2873 end 2874 end 2875 else 2876 # no member 2877 threads = [] 2878 end 2879 2880 return UntaggedResponse.new(name, threads, @str) 2881 end 2882 2883 def thread_branch(token) 2884 rootmember = nil 2885 lastmember = nil 2886 2887 while true 2888 shift_token # ignore first T_LPAR 2889 token = lookahead 2890 2891 case token.symbol 2892 when T_NUMBER 2893 # new member 2894 newmember = ThreadMember.new(number, []) 2895 if rootmember.nil? 2896 rootmember = newmember 2897 else 2898 lastmember.children << newmember 2899 end 2900 lastmember = newmember 2901 when T_SPACE 2902 # do nothing 2903 when T_LPAR 2904 if rootmember.nil? 2905 # dummy member 2906 lastmember = rootmember = ThreadMember.new(nil, []) 2907 end 2908 2909 lastmember.children << thread_branch(token) 2910 when T_RPAR 2911 break 2912 end 2913 end 2914 2915 return rootmember 2916 end 2917 2918 def status_response 2919 token = match(T_ATOM) 2920 name = token.value.upcase 2921 match(T_SPACE) 2922 mailbox = astring 2923 match(T_SPACE) 2924 match(T_LPAR) 2925 attr = {} 2926 while true 2927 token = lookahead 2928 case token.symbol 2929 when T_RPAR 2930 shift_token 2931 break 2932 when T_SPACE 2933 shift_token 2934 end 2935 token = match(T_ATOM) 2936 key = token.value.upcase 2937 match(T_SPACE) 2938 val = number 2939 attr[key] = val 2940 end 2941 data = StatusData.new(mailbox, attr) 2942 return UntaggedResponse.new(name, data, @str) 2943 end 2944 2945 def capability_response 2946 token = match(T_ATOM) 2947 name = token.value.upcase 2948 match(T_SPACE) 2949 data = [] 2950 while true 2951 token = lookahead 2952 case token.symbol 2953 when T_CRLF 2954 break 2955 when T_SPACE 2956 shift_token 2957 next 2958 end 2959 data.push(atom.upcase) 2960 end 2961 return UntaggedResponse.new(name, data, @str) 2962 end 2963 2964 def resp_text 2965 @lex_state = EXPR_RTEXT 2966 token = lookahead 2967 if token.symbol == T_LBRA 2968 code = resp_text_code 2969 else 2970 code = nil 2971 end 2972 token = match(T_TEXT) 2973 @lex_state = EXPR_BEG 2974 return ResponseText.new(code, token.value) 2975 end 2976 2977 def resp_text_code 2978 @lex_state = EXPR_BEG 2979 match(T_LBRA) 2980 token = match(T_ATOM) 2981 name = token.value.upcase 2982 case name 2983 when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n 2984 result = ResponseCode.new(name, nil) 2985 when /\A(?:PERMANENTFLAGS)\z/n 2986 match(T_SPACE) 2987 result = ResponseCode.new(name, flag_list) 2988 when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n 2989 match(T_SPACE) 2990 result = ResponseCode.new(name, number) 2991 else 2992 token = lookahead 2993 if token.symbol == T_SPACE 2994 shift_token 2995 @lex_state = EXPR_CTEXT 2996 token = match(T_TEXT) 2997 @lex_state = EXPR_BEG 2998 result = ResponseCode.new(name, token.value) 2999 else 3000 result = ResponseCode.new(name, nil) 3001 end 3002 end 3003 match(T_RBRA) 3004 @lex_state = EXPR_RTEXT 3005 return result 3006 end 3007 3008 def address_list 3009 token = lookahead 3010 if token.symbol == T_NIL 3011 shift_token 3012 return nil 3013 else 3014 result = [] 3015 match(T_LPAR) 3016 while true 3017 token = lookahead 3018 case token.symbol 3019 when T_RPAR 3020 shift_token 3021 break 3022 when T_SPACE 3023 shift_token 3024 end 3025 result.push(address) 3026 end 3027 return result 3028 end 3029 end 3030 3031 ADDRESS_REGEXP = /\G\ 3032(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ 3033(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ 3034(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ 3035(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\ 3036\)/ni 3037 3038 def address 3039 match(T_LPAR) 3040 if @str.index(ADDRESS_REGEXP, @pos) 3041 # address does not include literal. 3042 @pos = $~.end(0) 3043 name = $1 3044 route = $2 3045 mailbox = $3 3046 host = $4 3047 for s in [name, route, mailbox, host] 3048 if s 3049 s.gsub!(/\\(["\\])/n, "\\1") 3050 end 3051 end 3052 else 3053 name = nstring 3054 match(T_SPACE) 3055 route = nstring 3056 match(T_SPACE) 3057 mailbox = nstring 3058 match(T_SPACE) 3059 host = nstring 3060 match(T_RPAR) 3061 end 3062 return Address.new(name, route, mailbox, host) 3063 end 3064 3065# def flag_list 3066# result = [] 3067# match(T_LPAR) 3068# while true 3069# token = lookahead 3070# case token.symbol 3071# when T_RPAR 3072# shift_token 3073# break 3074# when T_SPACE 3075# shift_token 3076# end 3077# result.push(flag) 3078# end 3079# return result 3080# end 3081 3082# def flag 3083# token = lookahead 3084# if token.symbol == T_BSLASH 3085# shift_token 3086# token = lookahead 3087# if token.symbol == T_STAR 3088# shift_token 3089# return token.value.intern 3090# else 3091# return atom.intern 3092# end 3093# else 3094# return atom 3095# end 3096# end 3097 3098 FLAG_REGEXP = /\ 3099(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\ 3100(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n 3101 3102 def flag_list 3103 if @str.index(/\(([^)]*)\)/ni, @pos) 3104 @pos = $~.end(0) 3105 return $1.scan(FLAG_REGEXP).collect { |flag, atom| 3106 if atom 3107 atom 3108 else 3109 symbol = flag.capitalize.untaint.intern 3110 @flag_symbols[symbol] = true 3111 if @flag_symbols.length > IMAP.max_flag_count 3112 raise FlagCountError, "number of flag symbols exceeded" 3113 end 3114 symbol 3115 end 3116 } 3117 else 3118 parse_error("invalid flag list") 3119 end 3120 end 3121 3122 def nstring 3123 token = lookahead 3124 if token.symbol == T_NIL 3125 shift_token 3126 return nil 3127 else 3128 return string 3129 end 3130 end 3131 3132 def astring 3133 token = lookahead 3134 if string_token?(token) 3135 return string 3136 else 3137 return atom 3138 end 3139 end 3140 3141 def string 3142 token = lookahead 3143 if token.symbol == T_NIL 3144 shift_token 3145 return nil 3146 end 3147 token = match(T_QUOTED, T_LITERAL) 3148 return token.value 3149 end 3150 3151 STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL] 3152 3153 def string_token?(token) 3154 return STRING_TOKENS.include?(token.symbol) 3155 end 3156 3157 def case_insensitive_string 3158 token = lookahead 3159 if token.symbol == T_NIL 3160 shift_token 3161 return nil 3162 end 3163 token = match(T_QUOTED, T_LITERAL) 3164 return token.value.upcase 3165 end 3166 3167 def atom 3168 result = "" 3169 while true 3170 token = lookahead 3171 if atom_token?(token) 3172 result.concat(token.value) 3173 shift_token 3174 else 3175 if result.empty? 3176 parse_error("unexpected token %s", token.symbol) 3177 else 3178 return result 3179 end 3180 end 3181 end 3182 end 3183 3184 ATOM_TOKENS = [ 3185 T_ATOM, 3186 T_NUMBER, 3187 T_NIL, 3188 T_LBRA, 3189 T_RBRA, 3190 T_PLUS 3191 ] 3192 3193 def atom_token?(token) 3194 return ATOM_TOKENS.include?(token.symbol) 3195 end 3196 3197 def number 3198 token = lookahead 3199 if token.symbol == T_NIL 3200 shift_token 3201 return nil 3202 end 3203 token = match(T_NUMBER) 3204 return token.value.to_i 3205 end 3206 3207 def nil_atom 3208 match(T_NIL) 3209 return nil 3210 end 3211 3212 def match(*args) 3213 token = lookahead 3214 unless args.include?(token.symbol) 3215 parse_error('unexpected token %s (expected %s)', 3216 token.symbol.id2name, 3217 args.collect {|i| i.id2name}.join(" or ")) 3218 end 3219 shift_token 3220 return token 3221 end 3222 3223 def lookahead 3224 unless @token 3225 @token = next_token 3226 end 3227 return @token 3228 end 3229 3230 def shift_token 3231 @token = nil 3232 end 3233 3234 def next_token 3235 case @lex_state 3236 when EXPR_BEG 3237 if @str.index(BEG_REGEXP, @pos) 3238 @pos = $~.end(0) 3239 if $1 3240 return Token.new(T_SPACE, $+) 3241 elsif $2 3242 return Token.new(T_NIL, $+) 3243 elsif $3 3244 return Token.new(T_NUMBER, $+) 3245 elsif $4 3246 return Token.new(T_ATOM, $+) 3247 elsif $5 3248 return Token.new(T_QUOTED, 3249 $+.gsub(/\\(["\\])/n, "\\1")) 3250 elsif $6 3251 return Token.new(T_LPAR, $+) 3252 elsif $7 3253 return Token.new(T_RPAR, $+) 3254 elsif $8 3255 return Token.new(T_BSLASH, $+) 3256 elsif $9 3257 return Token.new(T_STAR, $+) 3258 elsif $10 3259 return Token.new(T_LBRA, $+) 3260 elsif $11 3261 return Token.new(T_RBRA, $+) 3262 elsif $12 3263 len = $+.to_i 3264 val = @str[@pos, len] 3265 @pos += len 3266 return Token.new(T_LITERAL, val) 3267 elsif $13 3268 return Token.new(T_PLUS, $+) 3269 elsif $14 3270 return Token.new(T_PERCENT, $+) 3271 elsif $15 3272 return Token.new(T_CRLF, $+) 3273 elsif $16 3274 return Token.new(T_EOF, $+) 3275 else 3276 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid") 3277 end 3278 else 3279 @str.index(/\S*/n, @pos) 3280 parse_error("unknown token - %s", $&.dump) 3281 end 3282 when EXPR_DATA 3283 if @str.index(DATA_REGEXP, @pos) 3284 @pos = $~.end(0) 3285 if $1 3286 return Token.new(T_SPACE, $+) 3287 elsif $2 3288 return Token.new(T_NIL, $+) 3289 elsif $3 3290 return Token.new(T_NUMBER, $+) 3291 elsif $4 3292 return Token.new(T_QUOTED, 3293 $+.gsub(/\\(["\\])/n, "\\1")) 3294 elsif $5 3295 len = $+.to_i 3296 val = @str[@pos, len] 3297 @pos += len 3298 return Token.new(T_LITERAL, val) 3299 elsif $6 3300 return Token.new(T_LPAR, $+) 3301 elsif $7 3302 return Token.new(T_RPAR, $+) 3303 else 3304 parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid") 3305 end 3306 else 3307 @str.index(/\S*/n, @pos) 3308 parse_error("unknown token - %s", $&.dump) 3309 end 3310 when EXPR_TEXT 3311 if @str.index(TEXT_REGEXP, @pos) 3312 @pos = $~.end(0) 3313 if $1 3314 return Token.new(T_TEXT, $+) 3315 else 3316 parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid") 3317 end 3318 else 3319 @str.index(/\S*/n, @pos) 3320 parse_error("unknown token - %s", $&.dump) 3321 end 3322 when EXPR_RTEXT 3323 if @str.index(RTEXT_REGEXP, @pos) 3324 @pos = $~.end(0) 3325 if $1 3326 return Token.new(T_LBRA, $+) 3327 elsif $2 3328 return Token.new(T_TEXT, $+) 3329 else 3330 parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid") 3331 end 3332 else 3333 @str.index(/\S*/n, @pos) 3334 parse_error("unknown token - %s", $&.dump) 3335 end 3336 when EXPR_CTEXT 3337 if @str.index(CTEXT_REGEXP, @pos) 3338 @pos = $~.end(0) 3339 if $1 3340 return Token.new(T_TEXT, $+) 3341 else 3342 parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid") 3343 end 3344 else 3345 @str.index(/\S*/n, @pos) #/ 3346 parse_error("unknown token - %s", $&.dump) 3347 end 3348 else 3349 parse_error("invalid @lex_state - %s", @lex_state.inspect) 3350 end 3351 end 3352 3353 def parse_error(fmt, *args) 3354 if IMAP.debug 3355 $stderr.printf("@str: %s\n", @str.dump) 3356 $stderr.printf("@pos: %d\n", @pos) 3357 $stderr.printf("@lex_state: %s\n", @lex_state) 3358 if @token 3359 $stderr.printf("@token.symbol: %s\n", @token.symbol) 3360 $stderr.printf("@token.value: %s\n", @token.value.inspect) 3361 end 3362 end 3363 raise ResponseParseError, format(fmt, *args) 3364 end 3365 end 3366 3367 # Authenticator for the "LOGIN" authentication type. See 3368 # #authenticate(). 3369 class LoginAuthenticator 3370 def process(data) 3371 case @state 3372 when STATE_USER 3373 @state = STATE_PASSWORD 3374 return @user 3375 when STATE_PASSWORD 3376 return @password 3377 end 3378 end 3379 3380 private 3381 3382 STATE_USER = :USER 3383 STATE_PASSWORD = :PASSWORD 3384 3385 def initialize(user, password) 3386 @user = user 3387 @password = password 3388 @state = STATE_USER 3389 end 3390 end 3391 add_authenticator "LOGIN", LoginAuthenticator 3392 3393 # Authenticator for the "PLAIN" authentication type. See 3394 # #authenticate(). 3395 class PlainAuthenticator 3396 def process(data) 3397 return "\0#{@user}\0#{@password}" 3398 end 3399 3400 private 3401 3402 def initialize(user, password) 3403 @user = user 3404 @password = password 3405 end 3406 end 3407 add_authenticator "PLAIN", PlainAuthenticator 3408 3409 # Authenticator for the "CRAM-MD5" authentication type. See 3410 # #authenticate(). 3411 class CramMD5Authenticator 3412 def process(challenge) 3413 digest = hmac_md5(challenge, @password) 3414 return @user + " " + digest 3415 end 3416 3417 private 3418 3419 def initialize(user, password) 3420 @user = user 3421 @password = password 3422 end 3423 3424 def hmac_md5(text, key) 3425 if key.length > 64 3426 key = Digest::MD5.digest(key) 3427 end 3428 3429 k_ipad = key + "\0" * (64 - key.length) 3430 k_opad = key + "\0" * (64 - key.length) 3431 for i in 0..63 3432 k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr 3433 k_opad[i] = (k_opad[i].ord ^ 0x5c).chr 3434 end 3435 3436 digest = Digest::MD5.digest(k_ipad + text) 3437 3438 return Digest::MD5.hexdigest(k_opad + digest) 3439 end 3440 end 3441 add_authenticator "CRAM-MD5", CramMD5Authenticator 3442 3443 # Authenticator for the "DIGEST-MD5" authentication type. See 3444 # #authenticate(). 3445 class DigestMD5Authenticator 3446 def process(challenge) 3447 case @stage 3448 when STAGE_ONE 3449 @stage = STAGE_TWO 3450 sparams = {} 3451 c = StringScanner.new(challenge) 3452 while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/) 3453 k, v = c[1], c[2] 3454 if v =~ /^"(.*)"$/ 3455 v = $1 3456 if v =~ /,/ 3457 v = v.split(',') 3458 end 3459 end 3460 sparams[k] = v 3461 end 3462 3463 raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0 3464 raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth") 3465 3466 response = { 3467 :nonce => sparams['nonce'], 3468 :username => @user, 3469 :realm => sparams['realm'], 3470 :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]), 3471 :'digest-uri' => 'imap/' + sparams['realm'], 3472 :qop => 'auth', 3473 :maxbuf => 65535, 3474 :nc => "%08d" % nc(sparams['nonce']), 3475 :charset => sparams['charset'], 3476 } 3477 3478 response[:authzid] = @authname unless @authname.nil? 3479 3480 # now, the real thing 3481 a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') ) 3482 3483 a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':') 3484 a1 << ':' + response[:authzid] unless response[:authzid].nil? 3485 3486 a2 = "AUTHENTICATE:" + response[:'digest-uri'] 3487 a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/ 3488 3489 response[:response] = Digest::MD5.hexdigest( 3490 [ 3491 Digest::MD5.hexdigest(a1), 3492 response.values_at(:nonce, :nc, :cnonce, :qop), 3493 Digest::MD5.hexdigest(a2) 3494 ].join(':') 3495 ) 3496 3497 return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',') 3498 when STAGE_TWO 3499 @stage = nil 3500 # if at the second stage, return an empty string 3501 if challenge =~ /rspauth=/ 3502 return '' 3503 else 3504 raise ResponseParseError, challenge 3505 end 3506 else 3507 raise ResponseParseError, challenge 3508 end 3509 end 3510 3511 def initialize(user, password, authname = nil) 3512 @user, @password, @authname = user, password, authname 3513 @nc, @stage = {}, STAGE_ONE 3514 end 3515 3516 private 3517 3518 STAGE_ONE = :stage_one 3519 STAGE_TWO = :stage_two 3520 3521 def nc(nonce) 3522 if @nc.has_key? nonce 3523 @nc[nonce] = @nc[nonce] + 1 3524 else 3525 @nc[nonce] = 1 3526 end 3527 return @nc[nonce] 3528 end 3529 3530 # some responses need quoting 3531 def qdval(k, v) 3532 return if k.nil? or v.nil? 3533 if %w"username authzid realm nonce cnonce digest-uri qop".include? k 3534 v.gsub!(/([\\"])/, "\\\1") 3535 return '%s="%s"' % [k, v] 3536 else 3537 return '%s=%s' % [k, v] 3538 end 3539 end 3540 end 3541 add_authenticator "DIGEST-MD5", DigestMD5Authenticator 3542 3543 # Superclass of IMAP errors. 3544 class Error < StandardError 3545 end 3546 3547 # Error raised when data is in the incorrect format. 3548 class DataFormatError < Error 3549 end 3550 3551 # Error raised when a response from the server is non-parseable. 3552 class ResponseParseError < Error 3553 end 3554 3555 # Superclass of all errors used to encapsulate "fail" responses 3556 # from the server. 3557 class ResponseError < Error 3558 3559 # The response that caused this error 3560 attr_accessor :response 3561 3562 def initialize(response) 3563 @response = response 3564 3565 super @response.data.text 3566 end 3567 3568 end 3569 3570 # Error raised upon a "NO" response from the server, indicating 3571 # that the client command could not be completed successfully. 3572 class NoResponseError < ResponseError 3573 end 3574 3575 # Error raised upon a "BAD" response from the server, indicating 3576 # that the client command violated the IMAP protocol, or an internal 3577 # server failure has occurred. 3578 class BadResponseError < ResponseError 3579 end 3580 3581 # Error raised upon a "BYE" response from the server, indicating 3582 # that the client is not being allowed to login, or has been timed 3583 # out due to inactivity. 3584 class ByeResponseError < ResponseError 3585 end 3586 3587 # Error raised when too many flags are interned to symbols. 3588 class FlagCountError < Error 3589 end 3590 end 3591end 3592 3593if __FILE__ == $0 3594 # :enddoc: 3595 require "getoptlong" 3596 3597 $stdout.sync = true 3598 $port = nil 3599 $user = ENV["USER"] || ENV["LOGNAME"] 3600 $auth = "login" 3601 $ssl = false 3602 $starttls = false 3603 3604 def usage 3605 <<EOF 3606usage: #{$0} [options] <host> 3607 3608 --help print this message 3609 --port=PORT specifies port 3610 --user=USER specifies user 3611 --auth=AUTH specifies auth type 3612 --starttls use starttls 3613 --ssl use ssl 3614EOF 3615 end 3616 3617 begin 3618 require 'io/console' 3619 rescue LoadError 3620 def _noecho(&block) 3621 system("stty", "-echo") 3622 begin 3623 yield STDIN 3624 ensure 3625 system("stty", "echo") 3626 end 3627 end 3628 else 3629 def _noecho(&block) 3630 STDIN.noecho(&block) 3631 end 3632 end 3633 3634 def get_password 3635 print "password: " 3636 begin 3637 return _noecho(&:gets).chomp 3638 ensure 3639 puts 3640 end 3641 end 3642 3643 def get_command 3644 printf("%s@%s> ", $user, $host) 3645 if line = gets 3646 return line.strip.split(/\s+/) 3647 else 3648 return nil 3649 end 3650 end 3651 3652 parser = GetoptLong.new 3653 parser.set_options(['--debug', GetoptLong::NO_ARGUMENT], 3654 ['--help', GetoptLong::NO_ARGUMENT], 3655 ['--port', GetoptLong::REQUIRED_ARGUMENT], 3656 ['--user', GetoptLong::REQUIRED_ARGUMENT], 3657 ['--auth', GetoptLong::REQUIRED_ARGUMENT], 3658 ['--starttls', GetoptLong::NO_ARGUMENT], 3659 ['--ssl', GetoptLong::NO_ARGUMENT]) 3660 begin 3661 parser.each_option do |name, arg| 3662 case name 3663 when "--port" 3664 $port = arg 3665 when "--user" 3666 $user = arg 3667 when "--auth" 3668 $auth = arg 3669 when "--ssl" 3670 $ssl = true 3671 when "--starttls" 3672 $starttls = true 3673 when "--debug" 3674 Net::IMAP.debug = true 3675 when "--help" 3676 usage 3677 exit 3678 end 3679 end 3680 rescue 3681 abort usage 3682 end 3683 3684 $host = ARGV.shift 3685 unless $host 3686 abort usage 3687 end 3688 3689 imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl) 3690 begin 3691 imap.starttls if $starttls 3692 class << password = method(:get_password) 3693 alias to_str call 3694 end 3695 imap.authenticate($auth, $user, password) 3696 while true 3697 cmd, *args = get_command 3698 break unless cmd 3699 begin 3700 case cmd 3701 when "list" 3702 for mbox in imap.list("", args[0] || "*") 3703 if mbox.attr.include?(Net::IMAP::NOSELECT) 3704 prefix = "!" 3705 elsif mbox.attr.include?(Net::IMAP::MARKED) 3706 prefix = "*" 3707 else 3708 prefix = " " 3709 end 3710 print prefix, mbox.name, "\n" 3711 end 3712 when "select" 3713 imap.select(args[0] || "inbox") 3714 print "ok\n" 3715 when "close" 3716 imap.close 3717 print "ok\n" 3718 when "summary" 3719 unless messages = imap.responses["EXISTS"][-1] 3720 puts "not selected" 3721 next 3722 end 3723 if messages > 0 3724 for data in imap.fetch(1..-1, ["ENVELOPE"]) 3725 print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n" 3726 end 3727 else 3728 puts "no message" 3729 end 3730 when "fetch" 3731 if args[0] 3732 data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0] 3733 puts data.attr["RFC822.HEADER"] 3734 puts data.attr["RFC822.TEXT"] 3735 else 3736 puts "missing argument" 3737 end 3738 when "logout", "exit", "quit" 3739 break 3740 when "help", "?" 3741 print <<EOF 3742list [pattern] list mailboxes 3743select [mailbox] select mailbox 3744close close mailbox 3745summary display summary 3746fetch [msgno] display message 3747logout logout 3748help, ? display help message 3749EOF 3750 else 3751 print "unknown command: ", cmd, "\n" 3752 end 3753 rescue Net::IMAP::Error 3754 puts $! 3755 end 3756 end 3757 ensure 3758 imap.logout 3759 imap.disconnect 3760 end 3761end 3762 3763