net-imap 0.4.17 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of net-imap might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +7 -1
- data/README.md +7 -3
- data/docs/styles.css +70 -14
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +32 -51
- data/lib/net/imap/config.rb +36 -10
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/data_lite.rb +225 -0
- data/lib/net/imap/deprecated_client_options.rb +6 -3
- data/lib/net/imap/errors.rb +6 -0
- data/lib/net/imap/esearch_result.rb +140 -0
- data/lib/net/imap/response_data.rb +7 -93
- data/lib/net/imap/response_parser/parser_utils.rb +5 -0
- data/lib/net/imap/response_parser.rb +77 -18
- data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
- data/lib/net/imap/sasl/authenticators.rb +8 -4
- data/lib/net/imap/sasl/client_adapter.rb +77 -26
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +1 -1
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +213 -51
- data/lib/net/imap/sasl/login_authenticator.rb +2 -1
- data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl.rb +6 -3
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/sequence_set.rb +18 -23
- data/lib/net/imap.rb +538 -150
- data/net-imap.gemspec +1 -1
- data/rakelib/string_prep_tables_generator.rb +2 -0
- metadata +6 -4
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP
|
5
|
+
# An "extended search" response (+ESEARCH+). ESearchResult should be
|
6
|
+
# returned (instead of SearchResult) by IMAP#search, IMAP#uid_search,
|
7
|
+
# IMAP#sort, and IMAP#uid_sort under any of the following conditions:
|
8
|
+
#
|
9
|
+
# * Return options were specified for IMAP#search or IMAP#uid_search.
|
10
|
+
# The server must support a search extension which allows
|
11
|
+
# RFC4466[https://www.rfc-editor.org/rfc/rfc4466.html] +return+ options,
|
12
|
+
# such as +ESEARCH+, +PARTIAL+, or +IMAP4rev2+.
|
13
|
+
# * Return options were specified for IMAP#sort or IMAP#uid_sort.
|
14
|
+
# The server must support the +ESORT+ extension
|
15
|
+
# {[RFC5267]}[https://www.rfc-editor.org/rfc/rfc5267.html#section-3].
|
16
|
+
#
|
17
|
+
# *NOTE:* IMAP#search and IMAP#uid_search do not support +ESORT+ yet.
|
18
|
+
# * The server supports +IMAP4rev2+ but _not_ +IMAP4rev1+, or +IMAP4rev2+
|
19
|
+
# has been enabled. +IMAP4rev2+ requires +ESEARCH+ results.
|
20
|
+
#
|
21
|
+
# Note that some servers may claim to support a search extension which
|
22
|
+
# requires an +ESEARCH+ result, such as +PARTIAL+, but still only return a
|
23
|
+
# +SEARCH+ result when +return+ options are specified.
|
24
|
+
#
|
25
|
+
# Some search extensions may result in the server sending ESearchResult
|
26
|
+
# responses after the initiating command has completed. Use
|
27
|
+
# IMAP#add_response_handler to handle these responses.
|
28
|
+
class ESearchResult < Data.define(:tag, :uid, :data)
|
29
|
+
def initialize(tag: nil, uid: nil, data: nil)
|
30
|
+
tag => String | nil; tag = -tag if tag
|
31
|
+
uid => true | false | nil; uid = !!uid
|
32
|
+
data => Array | nil; data ||= []; data.freeze
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
# :call-seq: to_a -> Array of integers
|
37
|
+
#
|
38
|
+
# When #all contains a SequenceSet of message sequence
|
39
|
+
# numbers or UIDs, +to_a+ returns that set as an array of integers.
|
40
|
+
#
|
41
|
+
# When #all is +nil+, either because the server
|
42
|
+
# returned no results or because +ALL+ was not included in
|
43
|
+
# the IMAP#search +RETURN+ options, #to_a returns an empty array.
|
44
|
+
#
|
45
|
+
# Note that SearchResult also implements +to_a+, so it can be used without
|
46
|
+
# checking if the server returned +SEARCH+ or +ESEARCH+ data.
|
47
|
+
def to_a; all&.numbers || [] end
|
48
|
+
|
49
|
+
##
|
50
|
+
# attr_reader: tag
|
51
|
+
#
|
52
|
+
# The tag string for the command that caused this response to be returned.
|
53
|
+
#
|
54
|
+
# When +nil+, this response was not caused by a particular command.
|
55
|
+
|
56
|
+
##
|
57
|
+
# attr_reader: uid
|
58
|
+
#
|
59
|
+
# Indicates whether #data in this response refers to UIDs (when +true+) or
|
60
|
+
# to message sequence numbers (when +false+).
|
61
|
+
|
62
|
+
##
|
63
|
+
alias uid? uid
|
64
|
+
|
65
|
+
##
|
66
|
+
# attr_reader: data
|
67
|
+
#
|
68
|
+
# Search return data, as an array of <tt>[name, value]</tt> pairs. Most
|
69
|
+
# return data corresponds to a search +return+ option with the same name.
|
70
|
+
#
|
71
|
+
# Note that some return data names may be used more than once per result.
|
72
|
+
#
|
73
|
+
# This data can be more simply retrieved by #min, #max, #all, #count,
|
74
|
+
# #modseq, and other methods.
|
75
|
+
|
76
|
+
# :call-seq: min -> integer or nil
|
77
|
+
#
|
78
|
+
# The lowest message number/UID that satisfies the SEARCH criteria.
|
79
|
+
#
|
80
|
+
# Returns +nil+ when the associated search command has no results, or when
|
81
|
+
# the +MIN+ return option wasn't specified.
|
82
|
+
#
|
83
|
+
# Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
|
84
|
+
# +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
|
85
|
+
def min; data.assoc("MIN")&.last end
|
86
|
+
|
87
|
+
# :call-seq: max -> integer or nil
|
88
|
+
#
|
89
|
+
# The highest message number/UID that satisfies the SEARCH criteria.
|
90
|
+
#
|
91
|
+
# Returns +nil+ when the associated search command has no results, or when
|
92
|
+
# the +MAX+ return option wasn't specified.
|
93
|
+
#
|
94
|
+
# Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
|
95
|
+
# +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
|
96
|
+
def max; data.assoc("MAX")&.last end
|
97
|
+
|
98
|
+
# :call-seq: all -> sequence set or nil
|
99
|
+
#
|
100
|
+
# A SequenceSet containing all message sequence numbers or UIDs that
|
101
|
+
# satisfy the SEARCH criteria.
|
102
|
+
#
|
103
|
+
# Returns +nil+ when the associated search command has no results, or when
|
104
|
+
# the +ALL+ return option was not specified but other return options were.
|
105
|
+
#
|
106
|
+
# Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
|
107
|
+
# +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
|
108
|
+
#
|
109
|
+
# See also: #to_a
|
110
|
+
def all; data.assoc("ALL")&.last end
|
111
|
+
|
112
|
+
# :call-seq: count -> integer or nil
|
113
|
+
#
|
114
|
+
# Returns the number of messages that satisfy the SEARCH criteria.
|
115
|
+
#
|
116
|
+
# Returns +nil+ when the associated search command has no results, or when
|
117
|
+
# the +COUNT+ return option wasn't specified.
|
118
|
+
#
|
119
|
+
# Requires +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.1] or
|
120
|
+
# +IMAP4rev2+ {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.3.4].
|
121
|
+
def count; data.assoc("COUNT")&.last end
|
122
|
+
|
123
|
+
# :call-seq: modseq -> integer or nil
|
124
|
+
#
|
125
|
+
# The highest +mod-sequence+ of all messages being returned.
|
126
|
+
#
|
127
|
+
# Returns +nil+ when the associated search command has no results, or when
|
128
|
+
# the +MODSEQ+ search criterion wasn't specified.
|
129
|
+
#
|
130
|
+
# Note that there is no search +return+ option for +MODSEQ+. It will be
|
131
|
+
# returned whenever the +CONDSTORE+ extension has been enabled. Using the
|
132
|
+
# +MODSEQ+ search criteria will implicitly enable +CONDSTORE+.
|
133
|
+
#
|
134
|
+
# Requires +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]
|
135
|
+
# and +ESEARCH+ {[RFC4731]}[https://www.rfc-editor.org/rfc/rfc4731.html#section-3.2].
|
136
|
+
def modseq; data.assoc("MODSEQ")&.last end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Net
|
4
4
|
class IMAP < Protocol
|
5
|
+
autoload :ESearchResult, "#{__dir__}/esearch_result"
|
5
6
|
autoload :FetchData, "#{__dir__}/fetch_data"
|
6
7
|
autoload :SearchResult, "#{__dir__}/search_result"
|
7
8
|
autoload :SequenceSet, "#{__dir__}/sequence_set"
|
@@ -939,7 +940,8 @@ module Net
|
|
939
940
|
# for something else?
|
940
941
|
#++
|
941
942
|
def media_subtype
|
942
|
-
warn("media_subtype is obsolete, use subtype instead.\n",
|
943
|
+
warn("media_subtype is obsolete, use subtype instead.\n",
|
944
|
+
uplevel: 1, category: :deprecated)
|
943
945
|
return subtype
|
944
946
|
end
|
945
947
|
end
|
@@ -984,7 +986,8 @@ module Net
|
|
984
986
|
# generate a warning message to +stderr+, then return
|
985
987
|
# the value of +subtype+.
|
986
988
|
def media_subtype
|
987
|
-
warn("media_subtype is obsolete, use subtype instead.\n",
|
989
|
+
warn("media_subtype is obsolete, use subtype instead.\n",
|
990
|
+
uplevel: 1, category: :deprecated)
|
988
991
|
return subtype
|
989
992
|
end
|
990
993
|
end
|
@@ -1040,77 +1043,6 @@ module Net
|
|
1040
1043
|
end
|
1041
1044
|
end
|
1042
1045
|
|
1043
|
-
# BodyTypeAttachment is not used and will be removed in an upcoming release.
|
1044
|
-
#
|
1045
|
-
# === Bug Analysis
|
1046
|
-
#
|
1047
|
-
# \IMAP body structures are parenthesized lists and assign their fields
|
1048
|
-
# positionally, so missing fields change the interpretation of all
|
1049
|
-
# following fields. Additionally, different body types have a different
|
1050
|
-
# number of required fields, followed by optional "extension" fields.
|
1051
|
-
#
|
1052
|
-
# BodyTypeAttachment was previously returned when a "message/rfc822" part,
|
1053
|
-
# which should be sent as <tt>body-type-msg</tt> with ten required fields,
|
1054
|
-
# was actually sent as a <tt>body-type-basic</tt> with _seven_ required
|
1055
|
-
# fields.
|
1056
|
-
#
|
1057
|
-
# basic => type, subtype, param, id, desc, enc, octets, md5=nil, dsp=nil, lang=nil, loc=nil, *ext
|
1058
|
-
# msg => type, subtype, param, id, desc, enc, octets, envelope, body, lines, md5=nil, ...
|
1059
|
-
#
|
1060
|
-
# Normally, +envelope+ and +md5+ are incompatible, but Net::IMAP leniently
|
1061
|
-
# allowed buggy servers to send +NIL+ for +envelope+. As a result, when a
|
1062
|
-
# server sent a <tt>message/rfc822</tt> part with +NIL+ for +md5+ and a
|
1063
|
-
# non-<tt>NIL</tt> +dsp+, Net::IMAP misinterpreted the
|
1064
|
-
# <tt>Content-Disposition</tt> as if it were a strange body type. In all
|
1065
|
-
# reported cases, the <tt>Content-Disposition</tt> was "attachment", so
|
1066
|
-
# BodyTypeAttachment was created as the workaround.
|
1067
|
-
#
|
1068
|
-
# === Current behavior
|
1069
|
-
#
|
1070
|
-
# When interpreted strictly, +envelope+ and +md5+ are incompatible. So the
|
1071
|
-
# current parsing algorithm peeks ahead after it has received the seventh
|
1072
|
-
# body field. If the next token is not the start of an +envelope+, we assume
|
1073
|
-
# the server has incorrectly sent us a <tt>body-type-basic</tt> and return
|
1074
|
-
# BodyTypeBasic. As a result, what was previously BodyTypeMessage#body =>
|
1075
|
-
# BodyTypeAttachment is now BodyTypeBasic#disposition => ContentDisposition.
|
1076
|
-
#
|
1077
|
-
class BodyTypeAttachment < Struct.new(:dsp_type, :_unused_, :param)
|
1078
|
-
# *invalid for BodyTypeAttachment*
|
1079
|
-
def media_type
|
1080
|
-
warn(<<~WARN, uplevel: 1)
|
1081
|
-
BodyTypeAttachment#media_type is obsolete. Use dsp_type instead.
|
1082
|
-
WARN
|
1083
|
-
dsp_type
|
1084
|
-
end
|
1085
|
-
|
1086
|
-
# *invalid for BodyTypeAttachment*
|
1087
|
-
def subtype
|
1088
|
-
warn("BodyTypeAttachment#subtype is obsolete.\n", uplevel: 1)
|
1089
|
-
nil
|
1090
|
-
end
|
1091
|
-
|
1092
|
-
##
|
1093
|
-
# method: dsp_type
|
1094
|
-
# :call-seq: dsp_type -> string
|
1095
|
-
#
|
1096
|
-
# Returns the content disposition type, as defined by
|
1097
|
-
# [DISPOSITION[https://tools.ietf.org/html/rfc2183]].
|
1098
|
-
|
1099
|
-
##
|
1100
|
-
# method: param
|
1101
|
-
# :call-seq: param -> hash
|
1102
|
-
#
|
1103
|
-
# Returns a hash representing parameters of the Content-Disposition
|
1104
|
-
# field, as defined by [DISPOSITION[https://tools.ietf.org/html/rfc2183]].
|
1105
|
-
|
1106
|
-
##
|
1107
|
-
def multipart?
|
1108
|
-
return false
|
1109
|
-
end
|
1110
|
-
end
|
1111
|
-
|
1112
|
-
deprecate_constant :BodyTypeAttachment
|
1113
|
-
|
1114
1046
|
# Net::IMAP::BodyTypeMultipart represents body structures of messages and
|
1115
1047
|
# message parts, when <tt>Content-Type</tt> is <tt>multipart/*</tt>.
|
1116
1048
|
class BodyTypeMultipart < Struct.new(:media_type, :subtype,
|
@@ -1182,29 +1114,11 @@ module Net
|
|
1182
1114
|
# generate a warning message to +stderr+, then return
|
1183
1115
|
# the value of +subtype+.
|
1184
1116
|
def media_subtype
|
1185
|
-
warn("media_subtype is obsolete, use subtype instead.\n",
|
1117
|
+
warn("media_subtype is obsolete, use subtype instead.\n",
|
1118
|
+
uplevel: 1, category: :deprecated)
|
1186
1119
|
return subtype
|
1187
1120
|
end
|
1188
1121
|
end
|
1189
1122
|
|
1190
|
-
# === Obsolete
|
1191
|
-
# BodyTypeExtension is not used and will be removed in an upcoming release.
|
1192
|
-
#
|
1193
|
-
# >>>
|
1194
|
-
# BodyTypeExtension was (incorrectly) used for <tt>message/*</tt> parts
|
1195
|
-
# (besides <tt>message/rfc822</tt>, which correctly uses BodyTypeMessage).
|
1196
|
-
#
|
1197
|
-
# Net::IMAP now (correctly) parses all message types (other than
|
1198
|
-
# <tt>message/rfc822</tt> or <tt>message/global</tt>) as BodyTypeBasic.
|
1199
|
-
class BodyTypeExtension < Struct.new(:media_type, :subtype,
|
1200
|
-
:params, :content_id,
|
1201
|
-
:description, :encoding, :size)
|
1202
|
-
def multipart?
|
1203
|
-
return false
|
1204
|
-
end
|
1205
|
-
end
|
1206
|
-
|
1207
|
-
deprecate_constant :BodyTypeExtension
|
1208
|
-
|
1209
1123
|
end
|
1210
1124
|
end
|
@@ -769,7 +769,6 @@ module Net
|
|
769
769
|
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
|
770
770
|
alias response_data__noop response_data__ignored
|
771
771
|
|
772
|
-
alias esearch_response response_data__unhandled
|
773
772
|
alias expunged_resp response_data__unhandled
|
774
773
|
alias uidfetch_resp response_data__unhandled
|
775
774
|
alias listrights_data response_data__unhandled
|
@@ -1317,31 +1316,19 @@ module Net
|
|
1317
1316
|
# header-fld-name = astring
|
1318
1317
|
#
|
1319
1318
|
# NOTE: Previously, Net::IMAP recreated the raw original source string.
|
1320
|
-
# Now, it
|
1321
|
-
#
|
1322
|
-
#
|
1323
|
-
# standard header field names are valid atoms:
|
1319
|
+
# Now, it returns the decoded astring value. Although this is technically
|
1320
|
+
# incompatible, it should almost never make a difference: all standard
|
1321
|
+
# header field names are valid atoms:
|
1324
1322
|
#
|
1325
1323
|
# https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
1326
1324
|
#
|
1327
|
-
#
|
1328
|
-
# or more of the printable US-ASCII characters, except SP and colon. So
|
1329
|
-
# empty string isn't valid, and literals aren't needed and should not be
|
1330
|
-
# used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
|
1331
|
-
#
|
1332
|
-
# RFC5233:
|
1325
|
+
# See also RFC5233:
|
1333
1326
|
# optional-field = field-name ":" unstructured CRLF
|
1334
1327
|
# field-name = 1*ftext
|
1335
1328
|
# ftext = %d33-57 / ; Printable US-ASCII
|
1336
1329
|
# %d59-126 ; characters not including
|
1337
1330
|
# ; ":".
|
1338
|
-
|
1339
|
-
assert_no_lookahead
|
1340
|
-
start = @pos
|
1341
|
-
astring
|
1342
|
-
end_pos = @token ? @pos - 1 : @pos
|
1343
|
-
@str[start...end_pos]
|
1344
|
-
end
|
1331
|
+
alias header_fld_name astring
|
1345
1332
|
|
1346
1333
|
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
|
1347
1334
|
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
|
@@ -1480,6 +1467,78 @@ module Net
|
|
1480
1467
|
end
|
1481
1468
|
alias sort_data mailbox_data__search
|
1482
1469
|
|
1470
|
+
# esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
|
1471
|
+
# *(SP search-return-data)
|
1472
|
+
# ;; Note that SEARCH and ESEARCH responses
|
1473
|
+
# ;; SHOULD be mutually exclusive,
|
1474
|
+
# ;; i.e., only one of the response types
|
1475
|
+
# ;; should be
|
1476
|
+
# ;; returned as a result of a command.
|
1477
|
+
# esearch-response = "ESEARCH" [search-correlator] [SP "UID"]
|
1478
|
+
# *(SP search-return-data)
|
1479
|
+
# ; ESEARCH response replaces SEARCH response
|
1480
|
+
# ; from IMAP4rev1.
|
1481
|
+
# search-correlator = SP "(" "TAG" SP tag-string ")"
|
1482
|
+
def esearch_response
|
1483
|
+
name = label("ESEARCH")
|
1484
|
+
tag = search_correlator if peek_str?(" (")
|
1485
|
+
uid = peek_re?(/\G UID\b/i) && (SP!; label("UID"); true)
|
1486
|
+
data = []
|
1487
|
+
data << search_return_data while SP?
|
1488
|
+
esearch = ESearchResult.new(tag, uid, data)
|
1489
|
+
UntaggedResponse.new(name, esearch, @str)
|
1490
|
+
end
|
1491
|
+
|
1492
|
+
# From RFC4731 (ESEARCH):
|
1493
|
+
# search-return-data = "MIN" SP nz-number /
|
1494
|
+
# "MAX" SP nz-number /
|
1495
|
+
# "ALL" SP sequence-set /
|
1496
|
+
# "COUNT" SP number /
|
1497
|
+
# search-ret-data-ext
|
1498
|
+
# ; All return data items conform to
|
1499
|
+
# ; search-ret-data-ext syntax.
|
1500
|
+
# search-ret-data-ext = search-modifier-name SP search-return-value
|
1501
|
+
# search-modifier-name = tagged-ext-label
|
1502
|
+
# search-return-value = tagged-ext-val
|
1503
|
+
#
|
1504
|
+
# From RFC4731 (ESEARCH):
|
1505
|
+
# search-return-data =/ "MODSEQ" SP mod-sequence-value
|
1506
|
+
#
|
1507
|
+
def search_return_data
|
1508
|
+
label = search_modifier_name; SP!
|
1509
|
+
value =
|
1510
|
+
case label
|
1511
|
+
when "MIN" then nz_number
|
1512
|
+
when "MAX" then nz_number
|
1513
|
+
when "ALL" then sequence_set
|
1514
|
+
when "COUNT" then number
|
1515
|
+
when "MODSEQ" then mod_sequence_value # RFC7162: CONDSTORE
|
1516
|
+
else search_return_value
|
1517
|
+
end
|
1518
|
+
[label, value]
|
1519
|
+
end
|
1520
|
+
|
1521
|
+
# search-modifier-name = tagged-ext-label
|
1522
|
+
alias search_modifier_name tagged_ext_label
|
1523
|
+
|
1524
|
+
# search-return-value = tagged-ext-val
|
1525
|
+
# ; Data for the returned search option.
|
1526
|
+
# ; A single "nz-number"/"number"/"number64" value
|
1527
|
+
# ; can be returned as an atom (i.e., without
|
1528
|
+
# ; quoting). A sequence-set can be returned
|
1529
|
+
# ; as an atom as well.
|
1530
|
+
def search_return_value; ExtensionData.new(tagged_ext_val) end
|
1531
|
+
|
1532
|
+
# search-correlator = SP "(" "TAG" SP tag-string ")"
|
1533
|
+
def search_correlator
|
1534
|
+
SP!; lpar; label("TAG"); SP!; tag = tag_string; rpar
|
1535
|
+
tag
|
1536
|
+
end
|
1537
|
+
|
1538
|
+
# tag-string = astring
|
1539
|
+
# ; <tag> represented as <astring>
|
1540
|
+
alias tag_string astring
|
1541
|
+
|
1483
1542
|
# RFC5256: THREAD
|
1484
1543
|
# thread-data = "THREAD" [SP 1*thread-list]
|
1485
1544
|
def thread_data
|
@@ -4,44 +4,76 @@ module Net
|
|
4
4
|
class IMAP
|
5
5
|
module SASL
|
6
6
|
|
7
|
-
#
|
7
|
+
# AuthenticationExchange is used internally by Net::IMAP#authenticate.
|
8
|
+
# But the API is still *experimental*, and may change.
|
8
9
|
#
|
9
10
|
# TODO: catch exceptions in #process and send #cancel_response.
|
10
11
|
# TODO: raise an error if the command succeeds after being canceled.
|
11
12
|
# TODO: use with more clients, to verify the API can accommodate them.
|
13
|
+
# TODO: pass ClientAdapter#service to SASL.authenticator
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# ).authenticate
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# private
|
15
|
+
# An AuthenticationExchange represents a single attempt to authenticate
|
16
|
+
# a SASL client to a SASL server. It is created from a client adapter, a
|
17
|
+
# mechanism name, and a mechanism authenticator. When #authenticate is
|
18
|
+
# called, it will send the appropriate authenticate command to the server,
|
19
|
+
# returning the client response on success and raising an exception on
|
20
|
+
# failure.
|
23
21
|
#
|
24
|
-
#
|
22
|
+
# In most cases, the client will not need to use
|
23
|
+
# SASL::AuthenticationExchange directly at all. Instead, use
|
24
|
+
# SASL::ClientAdapter#authenticate. If customizations are needed, the
|
25
|
+
# custom client adapter is probably the best place for that code.
|
25
26
|
#
|
26
|
-
# Or delegate creation of the authenticator to ::build:
|
27
27
|
# def authenticate(...)
|
28
|
-
#
|
29
|
-
# .authenticate
|
28
|
+
# MyClient::SASLAdapter.new(self).authenticate(...)
|
30
29
|
# end
|
31
30
|
#
|
32
|
-
#
|
31
|
+
# SASL::ClientAdapter#authenticate delegates to ::authenticate, like so:
|
32
|
+
#
|
33
33
|
# def authenticate(...)
|
34
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
34
35
|
# SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
|
35
36
|
# end
|
36
37
|
#
|
37
|
-
#
|
38
|
-
#
|
38
|
+
# ::authenticate simply delegates to ::build and #authenticate, like so:
|
39
|
+
#
|
40
|
+
# def authenticate(...)
|
41
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
42
|
+
# SASL::AuthenticationExchange
|
43
|
+
# .build(sasl_adapter, ...)
|
44
|
+
# .authenticate
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# And ::build delegates to SASL.authenticator and ::new, like so:
|
48
|
+
#
|
49
|
+
# def authenticate(mechanism, ...)
|
50
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
51
|
+
# authenticator = SASL.authenticator(mechanism, ...)
|
52
|
+
# SASL::AuthenticationExchange
|
53
|
+
# .new(sasl_adapter, mechanism, authenticator)
|
54
|
+
# .authenticate
|
55
|
+
# end
|
39
56
|
#
|
40
57
|
class AuthenticationExchange
|
41
58
|
# Convenience method for <tt>build(...).authenticate</tt>
|
59
|
+
#
|
60
|
+
# See also: SASL::ClientAdapter#authenticate
|
42
61
|
def self.authenticate(...) build(...).authenticate end
|
43
62
|
|
44
|
-
#
|
63
|
+
# Convenience method to combine the creation of a new authenticator and
|
64
|
+
# a new Authentication exchange.
|
65
|
+
#
|
66
|
+
# +client+ must be an instance of SASL::ClientAdapter.
|
67
|
+
#
|
68
|
+
# +mechanism+ must be a SASL mechanism name, as a string or symbol.
|
69
|
+
#
|
70
|
+
# +sasl_ir+ allows or disallows sending an "initial response", depending
|
71
|
+
# also on whether the server capabilities, mechanism authenticator, and
|
72
|
+
# client adapter all support it. Defaults to +true+.
|
73
|
+
#
|
74
|
+
# +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to
|
75
|
+
# SASL.authenticator. Use the +registry+ kwarg to override the global
|
76
|
+
# SASL::Authenticators registry.
|
45
77
|
def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
|
46
78
|
authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
|
47
79
|
new(client, mechanism, authenticator, sasl_ir: sasl_ir)
|
@@ -51,7 +83,7 @@ module Net
|
|
51
83
|
|
52
84
|
def initialize(client, mechanism, authenticator, sasl_ir: true)
|
53
85
|
@client = client
|
54
|
-
@mechanism =
|
86
|
+
@mechanism = Authenticators.normalize_name(mechanism)
|
55
87
|
@authenticator = authenticator
|
56
88
|
@sasl_ir = sasl_ir
|
57
89
|
@processed = false
|
@@ -21,6 +21,10 @@ module Net::IMAP::SASL
|
|
21
21
|
# ScramSHA1Authenticator for examples.
|
22
22
|
class Authenticators
|
23
23
|
|
24
|
+
# Normalize the mechanism name as an uppercase string, with underscores
|
25
|
+
# converted to dashes.
|
26
|
+
def self.normalize_name(mechanism) -(mechanism.to_s.upcase.tr(?_, ?-)) end
|
27
|
+
|
24
28
|
# Create a new Authenticators registry.
|
25
29
|
#
|
26
30
|
# This class is usually not instantiated directly. Use SASL.authenticators
|
@@ -65,7 +69,6 @@ module Net::IMAP::SASL
|
|
65
69
|
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
|
66
70
|
# preserved and non-alphanumeric characters are removed..
|
67
71
|
def add_authenticator(name, authenticator = nil)
|
68
|
-
key = -name.to_s.upcase.tr(?_, ?-)
|
69
72
|
authenticator ||= begin
|
70
73
|
class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
|
71
74
|
auth_class = nil
|
@@ -74,17 +77,18 @@ module Net::IMAP::SASL
|
|
74
77
|
auth_class.new(*creds, **props, &block)
|
75
78
|
}
|
76
79
|
end
|
80
|
+
key = Authenticators.normalize_name(name)
|
77
81
|
@authenticators[key] = authenticator
|
78
82
|
end
|
79
83
|
|
80
84
|
# Removes the authenticator registered for +name+
|
81
85
|
def remove_authenticator(name)
|
82
|
-
key =
|
86
|
+
key = Authenticators.normalize_name(name)
|
83
87
|
@authenticators.delete(key)
|
84
88
|
end
|
85
89
|
|
86
90
|
def mechanism?(name)
|
87
|
-
key =
|
91
|
+
key = Authenticators.normalize_name(name)
|
88
92
|
@authenticators.key?(key)
|
89
93
|
end
|
90
94
|
|
@@ -105,7 +109,7 @@ module Net::IMAP::SASL
|
|
105
109
|
# only. Protocol client users should see refer to their client's
|
106
110
|
# documentation, e.g. Net::IMAP#authenticate.
|
107
111
|
def authenticator(mechanism, ...)
|
108
|
-
key =
|
112
|
+
key = Authenticators.normalize_name(mechanism)
|
109
113
|
auth = @authenticators.fetch(key) do
|
110
114
|
raise ArgumentError, 'unknown auth type - "%s"' % key
|
111
115
|
end
|