net-imap 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3963dc001573cfafeb31305d2ba569c89bf14ec1fd1b75afa5b27d3d8f94ee8
4
- data.tar.gz: 0feb88b9ed0be0c63684f291060321f493e43343e5b761ef0019977b1c7cc3b7
3
+ metadata.gz: a804edc85533bfda64ba246d9c90163e9b3ae7de6dcd55cfb22e9788ffc0674b
4
+ data.tar.gz: 2c5481cc5def65f616575f484a56024805023cf223e8e184e39b0049ab16d15c
5
5
  SHA512:
6
- metadata.gz: 2a0a7efa65c793e3022b7768669a69b821894bee3610f92c27a95626407ac6ce194ddb1e32f395dc9bad3e68b91f94d54f47a823fedffd13c7b5cea2cd4ba50d
7
- data.tar.gz: 1b1b1ae36f4be343cddf551e4c86cda1848406a04edf70238fd8efd3aa793fad7b180b782b9044b44235d202866b50de4efb26fdac2da01bd3d27f3ef3ae278f
6
+ metadata.gz: 4292263bd4719686c854b85677e639b5ca6092ad3b44de29384cda759cfe754934d1a8f1ef04a5624b3f726ea9e875ed4c9991247a0d43eb80930c38f9978033
7
+ data.tar.gz: 2a8a211591e2da4b295b14564d7166664fdc695c3823bcff86b80ca3b45619a493482a5a5d1ee8bf621c4a7a89fc877d6a7717821412e8053ba613e6202dd2a6
@@ -9,7 +9,13 @@ jobs:
9
9
  matrix:
10
10
  ruby: [ 2.7, 2.6, 2.5, head ]
11
11
  os: [ ubuntu-latest, macos-latest ]
12
+ experimental: [false]
13
+ include:
14
+ - ruby: 2.6
15
+ os: ubuntu-latest
16
+ experimental: true
12
17
  runs-on: ${{ matrix.os }}
18
+ continue-on-error: ${{ matrix.experimental }}
13
19
  steps:
14
20
  - uses: actions/checkout@master
15
21
  - name: Set up Ruby
data/lib/net/imap.rb CHANGED
@@ -201,7 +201,7 @@ module Net
201
201
  # Unicode", RFC 2152, May 1997.
202
202
  #
203
203
  class IMAP < Protocol
204
- VERSION = "0.1.1"
204
+ VERSION = "0.2.0"
205
205
 
206
206
  include MonitorMixin
207
207
  if defined?(OpenSSL::SSL)
@@ -304,6 +304,16 @@ module Net
304
304
  @@authenticators[auth_type] = authenticator
305
305
  end
306
306
 
307
+ # Builds an authenticator for Net::IMAP#authenticate.
308
+ def self.authenticator(auth_type, *args)
309
+ auth_type = auth_type.upcase
310
+ unless @@authenticators.has_key?(auth_type)
311
+ raise ArgumentError,
312
+ format('unknown auth type - "%s"', auth_type)
313
+ end
314
+ @@authenticators[auth_type].new(*args)
315
+ end
316
+
307
317
  # The default port for IMAP connections, port 143
308
318
  def self.default_port
309
319
  return PORT
@@ -365,6 +375,30 @@ module Net
365
375
  end
366
376
  end
367
377
 
378
+ # Sends an ID command, and returns a hash of the server's
379
+ # response, or nil if the server does not identify itself.
380
+ #
381
+ # Note that the user should first check if the server supports the ID
382
+ # capability. For example:
383
+ #
384
+ # capabilities = imap.capability
385
+ # if capabilities.include?("ID")
386
+ # id = imap.id(
387
+ # name: "my IMAP client (ruby)",
388
+ # version: MyIMAP::VERSION,
389
+ # "support-url": "mailto:bugs@example.com",
390
+ # os: RbConfig::CONFIG["host_os"],
391
+ # )
392
+ # end
393
+ #
394
+ # See RFC 2971, Section 3.3, for defined fields.
395
+ def id(client_id=nil)
396
+ synchronize do
397
+ send_command("ID", ClientID.new(client_id))
398
+ @responses.delete("ID")&.last
399
+ end
400
+ end
401
+
368
402
  # Sends a NOOP command to the server. It does nothing.
369
403
  def noop
370
404
  send_command("NOOP")
@@ -408,7 +442,7 @@ module Net
408
442
  # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
409
443
  #
410
444
  # Authentication is done using the appropriate authenticator object:
411
- # see @@authenticators for more information on plugging in your own
445
+ # see +add_authenticator+ for more information on plugging in your own
412
446
  # authenticator.
413
447
  #
414
448
  # For example:
@@ -417,12 +451,7 @@ module Net
417
451
  #
418
452
  # A Net::IMAP::NoResponseError is raised if authentication fails.
419
453
  def authenticate(auth_type, *args)
420
- auth_type = auth_type.upcase
421
- unless @@authenticators.has_key?(auth_type)
422
- raise ArgumentError,
423
- format('unknown auth type - "%s"', auth_type)
424
- end
425
- authenticator = @@authenticators[auth_type].new(*args)
454
+ authenticator = self.class.authenticator(auth_type, *args)
426
455
  send_command("AUTHENTICATE", auth_type) do |resp|
427
456
  if resp.instance_of?(ContinuationRequest)
428
457
  data = authenticator.process(resp.data.text.unpack("m")[0])
@@ -552,6 +581,60 @@ module Net
552
581
  end
553
582
  end
554
583
 
584
+ # Sends a NAMESPACE command [RFC2342] and returns the namespaces that are
585
+ # available. The NAMESPACE command allows a client to discover the prefixes
586
+ # of namespaces used by a server for personal mailboxes, other users'
587
+ # mailboxes, and shared mailboxes.
588
+ #
589
+ # This extension predates IMAP4rev1 (RFC3501), so most IMAP servers support
590
+ # it. Many popular IMAP servers are configured with the default personal
591
+ # namespaces as `("" "/")`: no prefix and "/" hierarchy delimiter. In that
592
+ # common case, the naive client may not have any trouble naming mailboxes.
593
+ #
594
+ # But many servers are configured with the default personal namespace as
595
+ # e.g. `("INBOX." ".")`, placing all personal folders under INBOX, with "."
596
+ # as the hierarchy delimiter. If the client does not check for this, but
597
+ # naively assumes it can use the same folder names for all servers, then
598
+ # folder creation (and listing, moving, etc) can lead to errors.
599
+ #
600
+ # From RFC2342:
601
+ #
602
+ # Although typically a server will support only a single Personal
603
+ # Namespace, and a single Other User's Namespace, circumstances exist
604
+ # where there MAY be multiples of these, and a client MUST be prepared
605
+ # for them. If a client is configured such that it is required to create
606
+ # a certain mailbox, there can be circumstances where it is unclear which
607
+ # Personal Namespaces it should create the mailbox in. In these
608
+ # situations a client SHOULD let the user select which namespaces to
609
+ # create the mailbox in.
610
+ #
611
+ # The user of this method should first check if the server supports the
612
+ # NAMESPACE capability. The return value is a +Net::IMAP::Namespaces+
613
+ # object which has +personal+, +other+, and +shared+ fields, each an array
614
+ # of +Net::IMAP::Namespace+ objects. These arrays will be empty when the
615
+ # server responds with nil.
616
+ #
617
+ # For example:
618
+ #
619
+ # capabilities = imap.capability
620
+ # if capabilities.include?("NAMESPACE")
621
+ # namespaces = imap.namespace
622
+ # if namespace = namespaces.personal.first
623
+ # prefix = namespace.prefix # e.g. "" or "INBOX."
624
+ # delim = namespace.delim # e.g. "/" or "."
625
+ # # personal folders should use the prefix and delimiter
626
+ # imap.create(prefix + "foo")
627
+ # imap.create(prefix + "bar")
628
+ # imap.create(prefix + %w[path to my folder].join(delim))
629
+ # end
630
+ # end
631
+ def namespace
632
+ synchronize do
633
+ send_command("NAMESPACE")
634
+ return @responses.delete("NAMESPACE")[-1]
635
+ end
636
+ end
637
+
555
638
  # Sends a XLIST command, and returns a subset of names from
556
639
  # the complete set of all names available to the client.
557
640
  # +refname+ provides a context (for instance, a base directory
@@ -1656,6 +1739,74 @@ module Net
1656
1739
  end
1657
1740
  end
1658
1741
 
1742
+ class ClientID # :nodoc:
1743
+
1744
+ def send_data(imap, tag)
1745
+ imap.__send__(:send_data, format_internal(@data), tag)
1746
+ end
1747
+
1748
+ def validate
1749
+ validate_internal(@data)
1750
+ end
1751
+
1752
+ private
1753
+
1754
+ def initialize(data)
1755
+ @data = data
1756
+ end
1757
+
1758
+ def validate_internal(client_id)
1759
+ client_id.to_h.each do |k,v|
1760
+ unless StringFormatter.valid_string?(k)
1761
+ raise DataFormatError, client_id.inspect
1762
+ end
1763
+ end
1764
+ rescue NoMethodError, TypeError # to_h failed
1765
+ raise DataFormatError, client_id.inspect
1766
+ end
1767
+
1768
+ def format_internal(client_id)
1769
+ return nil if client_id.nil?
1770
+ client_id.to_h.flat_map {|k,v|
1771
+ [StringFormatter.string(k), StringFormatter.nstring(v)]
1772
+ }
1773
+ end
1774
+
1775
+ end
1776
+
1777
+ module StringFormatter
1778
+
1779
+ LITERAL_REGEX = /[\x80-\xff\r\n]/n
1780
+
1781
+ module_function
1782
+
1783
+ # Allows symbols in addition to strings
1784
+ def valid_string?(str)
1785
+ str.is_a?(Symbol) || str.respond_to?(:to_str)
1786
+ end
1787
+
1788
+ # Allows nil, symbols, and strings
1789
+ def valid_nstring?(str)
1790
+ str.nil? || valid_string?(str)
1791
+ end
1792
+
1793
+ # coerces using +to_s+
1794
+ def string(str)
1795
+ str = str.to_s
1796
+ if str =~ LITERAL_REGEX
1797
+ Literal.new(str)
1798
+ else
1799
+ QuotedString.new(str)
1800
+ end
1801
+ end
1802
+
1803
+ # coerces non-nil using +to_s+
1804
+ def nstring(str)
1805
+ str.nil? ? nil : string(str)
1806
+ end
1807
+
1808
+ end
1809
+
1659
1810
  # Common validators of number and nz_number types
1660
1811
  module NumValidator # :nodoc
1661
1812
  class << self
@@ -1747,6 +1898,18 @@ module Net
1747
1898
  # raw_data:: Returns the raw data string.
1748
1899
  UntaggedResponse = Struct.new(:name, :data, :raw_data)
1749
1900
 
1901
+ # Net::IMAP::IgnoredResponse represents intentionaly ignored responses.
1902
+ #
1903
+ # This includes untagged response "NOOP" sent by eg. Zimbra to avoid some
1904
+ # clients to close the connection.
1905
+ #
1906
+ # It matches no IMAP standard.
1907
+ #
1908
+ # ==== Fields:
1909
+ #
1910
+ # raw_data:: Returns the raw data string.
1911
+ IgnoredResponse = Struct.new(:raw_data)
1912
+
1750
1913
  # Net::IMAP::TaggedResponse represents tagged responses.
1751
1914
  #
1752
1915
  # The server completion result response indicates the success or
@@ -1774,8 +1937,7 @@ module Net
1774
1937
  # Net::IMAP::ResponseText represents texts of responses.
1775
1938
  # The text may be prefixed by the response code.
1776
1939
  #
1777
- # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1778
- # ;; text SHOULD NOT begin with "[" or "="
1940
+ # resp_text ::= ["[" resp-text-code "]" SP] text
1779
1941
  #
1780
1942
  # ==== Fields:
1781
1943
  #
@@ -1787,12 +1949,15 @@ module Net
1787
1949
 
1788
1950
  # Net::IMAP::ResponseCode represents response codes.
1789
1951
  #
1790
- # resp_text_code ::= "ALERT" / "PARSE" /
1791
- # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
1952
+ # resp_text_code ::= "ALERT" /
1953
+ # "BADCHARSET" [SP "(" astring *(SP astring) ")" ] /
1954
+ # capability_data / "PARSE" /
1955
+ # "PERMANENTFLAGS" SP "("
1956
+ # [flag_perm *(SP flag_perm)] ")" /
1792
1957
  # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1793
- # "UIDVALIDITY" SPACE nz_number /
1794
- # "UNSEEN" SPACE nz_number /
1795
- # atom [SPACE 1*<any TEXT_CHAR except "]">]
1958
+ # "UIDNEXT" SP nz_number / "UIDVALIDITY" SP nz_number /
1959
+ # "UNSEEN" SP nz_number /
1960
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1796
1961
  #
1797
1962
  # ==== Fields:
1798
1963
  #
@@ -1872,6 +2037,39 @@ module Net
1872
2037
  #
1873
2038
  MailboxACLItem = Struct.new(:user, :rights, :mailbox)
1874
2039
 
2040
+ # Net::IMAP::Namespace represents a single [RFC-2342] namespace.
2041
+ #
2042
+ # Namespace = nil / "(" 1*( "(" string SP (<"> QUOTED_CHAR <"> /
2043
+ # nil) *(Namespace_Response_Extension) ")" ) ")"
2044
+ #
2045
+ # Namespace_Response_Extension = SP string SP "(" string *(SP string)
2046
+ # ")"
2047
+ #
2048
+ # ==== Fields:
2049
+ #
2050
+ # prefix:: Returns the namespace prefix string.
2051
+ # delim:: Returns nil or the hierarchy delimiter character.
2052
+ # extensions:: Returns a hash of extension names to extension flag arrays.
2053
+ #
2054
+ Namespace = Struct.new(:prefix, :delim, :extensions)
2055
+
2056
+ # Net::IMAP::Namespaces represents the response from [RFC-2342] NAMESPACE.
2057
+ #
2058
+ # Namespace_Response = "*" SP "NAMESPACE" SP Namespace SP Namespace SP
2059
+ # Namespace
2060
+ #
2061
+ # ; The first Namespace is the Personal Namespace(s)
2062
+ # ; The second Namespace is the Other Users' Namespace(s)
2063
+ # ; The third Namespace is the Shared Namespace(s)
2064
+ #
2065
+ # ==== Fields:
2066
+ #
2067
+ # personal:: Returns an array of Personal Net::IMAP::Namespace objects.
2068
+ # other:: Returns an array of Other Users' Net::IMAP::Namespace objects.
2069
+ # shared:: Returns an array of Shared Net::IMAP::Namespace objects.
2070
+ #
2071
+ Namespaces = Struct.new(:personal, :other, :shared)
2072
+
1875
2073
  # Net::IMAP::StatusData represents the contents of the STATUS response.
1876
2074
  #
1877
2075
  # ==== Fields:
@@ -2291,8 +2489,12 @@ module Net
2291
2489
  return response_cond
2292
2490
  when /\A(?:FLAGS)\z/ni
2293
2491
  return flags_response
2492
+ when /\A(?:ID)\z/ni
2493
+ return id_response
2294
2494
  when /\A(?:LIST|LSUB|XLIST)\z/ni
2295
2495
  return list_response
2496
+ when /\A(?:NAMESPACE)\z/ni
2497
+ return namespace_response
2296
2498
  when /\A(?:QUOTA)\z/ni
2297
2499
  return getquota_response
2298
2500
  when /\A(?:QUOTAROOT)\z/ni
@@ -2307,6 +2509,8 @@ module Net
2307
2509
  return status_response
2308
2510
  when /\A(?:CAPABILITY)\z/ni
2309
2511
  return capability_response
2512
+ when /\A(?:NOOP)\z/ni
2513
+ return ignored_response
2310
2514
  else
2311
2515
  return text_response
2312
2516
  end
@@ -2316,7 +2520,7 @@ module Net
2316
2520
  end
2317
2521
 
2318
2522
  def response_tagged
2319
- tag = atom
2523
+ tag = astring_chars
2320
2524
  match(T_SPACE)
2321
2525
  token = match(T_ATOM)
2322
2526
  name = token.value.upcase
@@ -2876,14 +3080,18 @@ module Net
2876
3080
  return name, modseq
2877
3081
  end
2878
3082
 
3083
+ def ignored_response
3084
+ while lookahead.symbol != T_CRLF
3085
+ shift_token
3086
+ end
3087
+ return IgnoredResponse.new(@str)
3088
+ end
3089
+
2879
3090
  def text_response
2880
3091
  token = match(T_ATOM)
2881
3092
  name = token.value.upcase
2882
3093
  match(T_SPACE)
2883
- @lex_state = EXPR_TEXT
2884
- token = match(T_TEXT)
2885
- @lex_state = EXPR_BEG
2886
- return UntaggedResponse.new(name, token.value)
3094
+ return UntaggedResponse.new(name, text)
2887
3095
  end
2888
3096
 
2889
3097
  def flags_response
@@ -3114,11 +3322,15 @@ module Net
3114
3322
  token = match(T_ATOM)
3115
3323
  name = token.value.upcase
3116
3324
  match(T_SPACE)
3325
+ UntaggedResponse.new(name, capability_data, @str)
3326
+ end
3327
+
3328
+ def capability_data
3117
3329
  data = []
3118
3330
  while true
3119
3331
  token = lookahead
3120
3332
  case token.symbol
3121
- when T_CRLF
3333
+ when T_CRLF, T_RBRA
3122
3334
  break
3123
3335
  when T_SPACE
3124
3336
  shift_token
@@ -3126,30 +3338,142 @@ module Net
3126
3338
  end
3127
3339
  data.push(atom.upcase)
3128
3340
  end
3341
+ data
3342
+ end
3343
+
3344
+ def id_response
3345
+ token = match(T_ATOM)
3346
+ name = token.value.upcase
3347
+ match(T_SPACE)
3348
+ token = match(T_LPAR, T_NIL)
3349
+ if token.symbol == T_NIL
3350
+ return UntaggedResponse.new(name, nil, @str)
3351
+ else
3352
+ data = {}
3353
+ while true
3354
+ token = lookahead
3355
+ case token.symbol
3356
+ when T_RPAR
3357
+ shift_token
3358
+ break
3359
+ when T_SPACE
3360
+ shift_token
3361
+ next
3362
+ else
3363
+ key = string
3364
+ match(T_SPACE)
3365
+ val = nstring
3366
+ data[key] = val
3367
+ end
3368
+ end
3369
+ return UntaggedResponse.new(name, data, @str)
3370
+ end
3371
+ end
3372
+
3373
+ def namespace_response
3374
+ @lex_state = EXPR_DATA
3375
+ token = lookahead
3376
+ token = match(T_ATOM)
3377
+ name = token.value.upcase
3378
+ match(T_SPACE)
3379
+ personal = namespaces
3380
+ match(T_SPACE)
3381
+ other = namespaces
3382
+ match(T_SPACE)
3383
+ shared = namespaces
3384
+ @lex_state = EXPR_BEG
3385
+ data = Namespaces.new(personal, other, shared)
3129
3386
  return UntaggedResponse.new(name, data, @str)
3130
3387
  end
3131
3388
 
3132
- def resp_text
3133
- @lex_state = EXPR_RTEXT
3389
+ def namespaces
3134
3390
  token = lookahead
3135
- if token.symbol == T_LBRA
3136
- code = resp_text_code
3391
+ # empty () is not allowed, so nil is functionally identical to empty.
3392
+ data = []
3393
+ if token.symbol == T_NIL
3394
+ shift_token
3137
3395
  else
3138
- code = nil
3396
+ match(T_LPAR)
3397
+ loop do
3398
+ data << namespace
3399
+ break unless lookahead.symbol == T_SPACE
3400
+ shift_token
3401
+ end
3402
+ match(T_RPAR)
3403
+ end
3404
+ data
3405
+ end
3406
+
3407
+ def namespace
3408
+ match(T_LPAR)
3409
+ prefix = match(T_QUOTED, T_LITERAL).value
3410
+ match(T_SPACE)
3411
+ delimiter = string
3412
+ extensions = namespace_response_extensions
3413
+ match(T_RPAR)
3414
+ Namespace.new(prefix, delimiter, extensions)
3415
+ end
3416
+
3417
+ def namespace_response_extensions
3418
+ data = {}
3419
+ token = lookahead
3420
+ if token.symbol == T_SPACE
3421
+ shift_token
3422
+ name = match(T_QUOTED, T_LITERAL).value
3423
+ data[name] ||= []
3424
+ match(T_SPACE)
3425
+ match(T_LPAR)
3426
+ loop do
3427
+ data[name].push match(T_QUOTED, T_LITERAL).value
3428
+ break unless lookahead.symbol == T_SPACE
3429
+ shift_token
3430
+ end
3431
+ match(T_RPAR)
3432
+ end
3433
+ data
3434
+ end
3435
+
3436
+ # text = 1*TEXT-CHAR
3437
+ # TEXT-CHAR = <any CHAR except CR and LF>
3438
+ def text
3439
+ match(T_TEXT, lex_state: EXPR_TEXT).value
3440
+ end
3441
+
3442
+ # resp-text = ["[" resp-text-code "]" SP] text
3443
+ def resp_text
3444
+ token = match(T_LBRA, T_TEXT, lex_state: EXPR_RTEXT)
3445
+ case token.symbol
3446
+ when T_LBRA
3447
+ code = resp_text_code
3448
+ match(T_RBRA)
3449
+ accept_space # violating RFC
3450
+ ResponseText.new(code, text)
3451
+ when T_TEXT
3452
+ ResponseText.new(nil, token.value)
3139
3453
  end
3140
- token = match(T_TEXT)
3141
- @lex_state = EXPR_BEG
3142
- return ResponseText.new(code, token.value)
3143
3454
  end
3144
3455
 
3456
+ # See https://www.rfc-editor.org/errata/rfc3501
3457
+ #
3458
+ # resp-text-code = "ALERT" /
3459
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
3460
+ # capability-data / "PARSE" /
3461
+ # "PERMANENTFLAGS" SP "("
3462
+ # [flag-perm *(SP flag-perm)] ")" /
3463
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
3464
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
3465
+ # "UNSEEN" SP nz-number /
3466
+ # atom [SP 1*<any TEXT-CHAR except "]">]
3145
3467
  def resp_text_code
3146
- @lex_state = EXPR_BEG
3147
- match(T_LBRA)
3148
3468
  token = match(T_ATOM)
3149
3469
  name = token.value.upcase
3150
3470
  case name
3151
3471
  when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
3152
3472
  result = ResponseCode.new(name, nil)
3473
+ when /\A(?:BADCHARSET)\z/n
3474
+ result = ResponseCode.new(name, charset_list)
3475
+ when /\A(?:CAPABILITY)\z/ni
3476
+ result = ResponseCode.new(name, capability_data)
3153
3477
  when /\A(?:PERMANENTFLAGS)\z/n
3154
3478
  match(T_SPACE)
3155
3479
  result = ResponseCode.new(name, flag_list)
@@ -3160,19 +3484,28 @@ module Net
3160
3484
  token = lookahead
3161
3485
  if token.symbol == T_SPACE
3162
3486
  shift_token
3163
- @lex_state = EXPR_CTEXT
3164
- token = match(T_TEXT)
3165
- @lex_state = EXPR_BEG
3487
+ token = match(T_TEXT, lex_state: EXPR_CTEXT)
3166
3488
  result = ResponseCode.new(name, token.value)
3167
3489
  else
3168
3490
  result = ResponseCode.new(name, nil)
3169
3491
  end
3170
3492
  end
3171
- match(T_RBRA)
3172
- @lex_state = EXPR_RTEXT
3173
3493
  return result
3174
3494
  end
3175
3495
 
3496
+ def charset_list
3497
+ result = []
3498
+ if accept(T_SPACE)
3499
+ match(T_LPAR)
3500
+ result << charset
3501
+ while accept(T_SPACE)
3502
+ result << charset
3503
+ end
3504
+ match(T_RPAR)
3505
+ end
3506
+ result
3507
+ end
3508
+
3176
3509
  def address_list
3177
3510
  token = lookahead
3178
3511
  if token.symbol == T_NIL
@@ -3269,7 +3602,7 @@ module Net
3269
3602
  if string_token?(token)
3270
3603
  return string
3271
3604
  else
3272
- return atom
3605
+ return astring_chars
3273
3606
  end
3274
3607
  end
3275
3608
 
@@ -3299,34 +3632,49 @@ module Net
3299
3632
  return token.value.upcase
3300
3633
  end
3301
3634
 
3302
- def atom
3303
- result = String.new
3304
- while true
3305
- token = lookahead
3306
- if atom_token?(token)
3307
- result.concat(token.value)
3308
- shift_token
3309
- else
3310
- if result.empty?
3311
- parse_error("unexpected token %s", token.symbol)
3312
- else
3313
- return result
3314
- end
3315
- end
3316
- end
3317
- end
3318
-
3635
+ # atom = 1*ATOM-CHAR
3636
+ # ATOM-CHAR = <any CHAR except atom-specials>
3319
3637
  ATOM_TOKENS = [
3320
3638
  T_ATOM,
3321
3639
  T_NUMBER,
3322
3640
  T_NIL,
3323
3641
  T_LBRA,
3324
- T_RBRA,
3325
3642
  T_PLUS
3326
3643
  ]
3327
3644
 
3328
- def atom_token?(token)
3329
- return ATOM_TOKENS.include?(token.symbol)
3645
+ def atom
3646
+ -combine_adjacent(*ATOM_TOKENS)
3647
+ end
3648
+
3649
+ # ASTRING-CHAR = ATOM-CHAR / resp-specials
3650
+ # resp-specials = "]"
3651
+ ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA]
3652
+
3653
+ def astring_chars
3654
+ combine_adjacent(*ASTRING_CHARS_TOKENS)
3655
+ end
3656
+
3657
+ def combine_adjacent(*tokens)
3658
+ result = "".b
3659
+ while token = accept(*tokens)
3660
+ result << token.value
3661
+ end
3662
+ if result.empty?
3663
+ parse_error('unexpected token %s (expected %s)',
3664
+ lookahead.symbol, args.join(" or "))
3665
+ end
3666
+ result
3667
+ end
3668
+
3669
+ # See https://www.rfc-editor.org/errata/rfc3501
3670
+ #
3671
+ # charset = atom / quoted
3672
+ def charset
3673
+ if token = accept(T_QUOTED)
3674
+ token.value
3675
+ else
3676
+ atom
3677
+ end
3330
3678
  end
3331
3679
 
3332
3680
  def number
@@ -3344,22 +3692,62 @@ module Net
3344
3692
  return nil
3345
3693
  end
3346
3694
 
3347
- def match(*args)
3695
+ SPACES_REGEXP = /\G */n
3696
+
3697
+ # This advances @pos directly so it's safe before changing @lex_state.
3698
+ def accept_space
3699
+ if @token
3700
+ shift_token if @token.symbol == T_SPACE
3701
+ elsif @str[@pos] == " "
3702
+ @pos += 1
3703
+ end
3704
+ end
3705
+
3706
+ # The RFC is very strict about this and usually we should be too.
3707
+ # But skipping spaces is usually a safe workaround for buggy servers.
3708
+ #
3709
+ # This advances @pos directly so it's safe before changing @lex_state.
3710
+ def accept_spaces
3711
+ shift_token if @token&.symbol == T_SPACE
3712
+ if @str.index(SPACES_REGEXP, @pos)
3713
+ @pos = $~.end(0)
3714
+ end
3715
+ end
3716
+
3717
+ def match(*args, lex_state: @lex_state)
3718
+ if @token && lex_state != @lex_state
3719
+ parse_error("invalid lex_state change to %s with unconsumed token",
3720
+ lex_state)
3721
+ end
3722
+ begin
3723
+ @lex_state, original_lex_state = lex_state, @lex_state
3724
+ token = lookahead
3725
+ unless args.include?(token.symbol)
3726
+ parse_error('unexpected token %s (expected %s)',
3727
+ token.symbol.id2name,
3728
+ args.collect {|i| i.id2name}.join(" or "))
3729
+ end
3730
+ shift_token
3731
+ return token
3732
+ ensure
3733
+ @lex_state = original_lex_state
3734
+ end
3735
+ end
3736
+
3737
+ # like match, but does not raise error on failure.
3738
+ #
3739
+ # returns and shifts token on successful match
3740
+ # returns nil and leaves @token unshifted on no match
3741
+ def accept(*args)
3348
3742
  token = lookahead
3349
- unless args.include?(token.symbol)
3350
- parse_error('unexpected token %s (expected %s)',
3351
- token.symbol.id2name,
3352
- args.collect {|i| i.id2name}.join(" or "))
3743
+ if args.include?(token.symbol)
3744
+ shift_token
3745
+ token
3353
3746
  end
3354
- shift_token
3355
- return token
3356
3747
  end
3357
3748
 
3358
3749
  def lookahead
3359
- unless @token
3360
- @token = next_token
3361
- end
3362
- return @token
3750
+ @token ||= next_token
3363
3751
  end
3364
3752
 
3365
3753
  def shift_token
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-22 00:00:00.000000000 Z
11
+ date: 2021-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-protocol
@@ -92,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
94
  requirements: []
95
- rubygems_version: 3.2.2
95
+ rubygems_version: 3.3.0.dev
96
96
  signing_key:
97
97
  specification_version: 4
98
98
  summary: Ruby client api for Internet Message Access Protocol