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 +4 -4
- data/.github/workflows/test.yml +6 -0
- data/lib/net/imap.rb +456 -68
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a804edc85533bfda64ba246d9c90163e9b3ae7de6dcd55cfb22e9788ffc0674b
|
4
|
+
data.tar.gz: 2c5481cc5def65f616575f484a56024805023cf223e8e184e39b0049ab16d15c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4292263bd4719686c854b85677e639b5ca6092ad3b44de29384cda759cfe754934d1a8f1ef04a5624b3f726ea9e875ed4c9991247a0d43eb80930c38f9978033
|
7
|
+
data.tar.gz: 2a8a211591e2da4b295b14564d7166664fdc695c3823bcff86b80ca3b45619a493482a5a5d1ee8bf621c4a7a89fc877d6a7717821412e8053ba613e6202dd2a6
|
data/.github/workflows/test.yml
CHANGED
@@ -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.
|
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
|
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
|
-
|
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 ::= ["["
|
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" /
|
1791
|
-
# "
|
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"
|
1794
|
-
# "UNSEEN"
|
1795
|
-
# atom [
|
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 =
|
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
|
-
|
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
|
3133
|
-
@lex_state = EXPR_RTEXT
|
3389
|
+
def namespaces
|
3134
3390
|
token = lookahead
|
3135
|
-
|
3136
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
3303
|
-
|
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
|
3329
|
-
|
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
|
-
|
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
|
-
|
3350
|
-
|
3351
|
-
|
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
|
-
|
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.
|
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:
|
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.
|
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
|