net-imap 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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