net-imap 0.3.7 → 0.4.5

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.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +5 -12
  4. data/.gitignore +2 -0
  5. data/Gemfile +3 -0
  6. data/README.md +15 -4
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +0 -12
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +13 -6
  11. data/lib/net/imap/data_encoding.rb +14 -2
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +20 -0
  14. data/lib/net/imap/fetch_data.rb +518 -0
  15. data/lib/net/imap/response_data.rb +116 -252
  16. data/lib/net/imap/response_parser/parser_utils.rb +240 -0
  17. data/lib/net/imap/response_parser.rb +1535 -1003
  18. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  19. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  20. data/lib/net/imap/sasl/authenticators.rb +118 -0
  21. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  22. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
  23. data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
  24. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  25. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  26. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
  27. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  28. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  29. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  30. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  31. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  32. data/lib/net/imap/sasl/stringprep.rb +6 -66
  33. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  34. data/lib/net/imap/sasl.rb +144 -43
  35. data/lib/net/imap/sasl_adapter.rb +21 -0
  36. data/lib/net/imap/sequence_set.rb +67 -0
  37. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  38. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  39. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  40. data/lib/net/imap/stringprep/tables.rb +146 -0
  41. data/lib/net/imap/stringprep/trace.rb +85 -0
  42. data/lib/net/imap/stringprep.rb +159 -0
  43. data/lib/net/imap.rb +1055 -612
  44. data/net-imap.gemspec +4 -3
  45. data/rakelib/benchmarks.rake +91 -0
  46. data/rakelib/saslprep.rake +4 -4
  47. data/rakelib/string_prep_tables_generator.rb +82 -60
  48. metadata +31 -13
  49. data/benchmarks/stringprep.yml +0 -65
  50. data/benchmarks/table-regexps.yml +0 -39
  51. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  52. data/lib/net/imap/authenticators/plain.rb +0 -41
  53. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  54. data/lib/net/imap/sasl/saslprep.rb +0 -55
  55. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  56. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+
6
+ # Net::IMAP::FetchData represents the contents of a FETCH response.
7
+ # Net::IMAP#fetch and Net::IMAP#uid_fetch both return an array of
8
+ # FetchData objects.
9
+ #
10
+ # === Fetch attributes
11
+ #
12
+ # See {[IMAP4rev1 §7.4.2]}[https://www.rfc-editor.org/rfc/rfc3501.html#section-7.4.2]
13
+ # and {[IMAP4rev2 §7.5.2]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.5.2]
14
+ # for a full description of the standard fetch response data items, and
15
+ # Net::IMAP@Message+envelope+and+body+structure for other relevant RFCs.
16
+ #
17
+ # ==== Static fetch data items
18
+ #
19
+ # Most message attributes are static, and must never change for a given
20
+ # <tt>(server, account, mailbox, UIDVALIDITY, UID)</tt> tuple.
21
+ #
22
+ # The static fetch data items defined by both
23
+ # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] and
24
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] are:
25
+ #
26
+ # * <b><tt>"UID"</tt></b> --- See #uid.
27
+ # * <b><tt>"BODY"</tt></b> --- See #body.
28
+ # * <b><tt>"BODY[#{section_spec}]"</tt></b>,
29
+ # <b><tt>"BODY[#{section_spec}]<#{offset}>"</tt></b> --- See #message,
30
+ # #part, #header, #header_fields, #header_fields_not, #mime, and #text.
31
+ # * <b><tt>"BODYSTRUCTURE"</tt></b> --- See #bodystructure.
32
+ # * <b><tt>"ENVELOPE"</tt></b> --- See #envelope.
33
+ # * <b><tt>"INTERNALDATE"</tt></b> --- See #internaldate.
34
+ # * <b><tt>"RFC822.SIZE"</tt></b> --- See #rfc822_size.
35
+ #
36
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] adds the
37
+ # additional fetch items from the +BINARY+ extension
38
+ # {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516.html]:
39
+ #
40
+ # * <b><tt>"BINARY[#{part}]"</tt></b>,
41
+ # <b><tt>"BINARY[#{part}]<#{offset}>"</tt></b> -- See #binary.
42
+ # * <b><tt>"BINARY.SIZE[#{part}]"</tt></b> -- See #binary_size.
43
+ #
44
+ # Several static message attributes in
45
+ # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] are obsolete and
46
+ # been removed from
47
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]:
48
+ #
49
+ # * <b><tt>"RFC822"</tt></b> --- See #rfc822 or replace with
50
+ # <tt>"BODY[]"</tt> and #message.
51
+ # * <b><tt>"RFC822.HEADER"</tt></b> --- See #rfc822_header or replace with
52
+ # <tt>"BODY[HEADER]"</tt> and #header.
53
+ # * <b><tt>"RFC822.TEXT"</tt></b> --- See #rfc822_text or replace with
54
+ # <tt>"BODY[TEXT]"</tt> and #text.
55
+ #
56
+ # Net::IMAP supports static attributes defined by the following extensions:
57
+ # * +OBJECTID+ {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html]
58
+ # * <b><tt>"EMAILID"</tt></b> --- See #emailid.
59
+ # * <b><tt>"THREADID"</tt></b> --- See #threadid.
60
+ #
61
+ # * +X-GM-EXT-1+ {[non-standard Gmail
62
+ # extension]}[https://developers.google.com/gmail/imap/imap-extensions]
63
+ # * <b><tt>"X-GM-MSGID"</tt></b> --- unique message ID. Access via #attr.
64
+ # * <b><tt>"X-GM-THRID"</tt></b> --- Thread ID. Access via #attr.
65
+ #
66
+ # [Note:]
67
+ # >>>
68
+ # Additional static fields are defined in other \IMAP extensions, but
69
+ # Net::IMAP can't parse them yet.
70
+ #
71
+ # ==== Dynamic message attributes
72
+ #
73
+ # Some message attributes can be dynamically changed, for example using the
74
+ # {STORE command}[rdoc-ref:Net::IMAP#store].
75
+ #
76
+ # The only dynamic message attribute defined by
77
+ # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] and
78
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] is:
79
+ #
80
+ # * <b><tt>"FLAGS"</tt></b> --- See #flags.
81
+ #
82
+ # Net::IMAP supports dynamic attributes defined by the following extensions:
83
+ #
84
+ # * +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]:
85
+ # * <b><tt>"MODSEQ"</tt></b> --- See #modseq.
86
+ # * +X-GM-EXT-1+ {[non-standard Gmail
87
+ # extension]}[https://developers.google.com/gmail/imap/imap-extensions]
88
+ # * <b><tt>"X-GM-LABELS"</tt></b> --- Gmail labels. Access via #attr.
89
+ #
90
+ # [Note:]
91
+ # >>>
92
+ # Additional dynamic fields are defined in other \IMAP extensions, but
93
+ # Net::IMAP can't parse them yet.
94
+ #
95
+ # === Implicitly setting <tt>\Seen</tt> and using +PEEK+
96
+ #
97
+ # Unless the mailbox is has been opened as read-only, fetching
98
+ # <tt>BODY[#{section}]</tt> or <tt>BINARY[#{section}]</tt>
99
+ # will implicitly set the <tt>\Seen</tt> flag. To avoid this, fetch using
100
+ # <tt>BODY.PEEK[#{section}]</tt> or <tt>BINARY.PEEK[#{section}]</tt>
101
+ # instead.
102
+ #
103
+ # Note that the data will always be _returned_ without <tt>".PEEK"</tt>, in
104
+ # <tt>BODY[#{specifier}]</tt> or <tt>BINARY[#{section}]</tt>.
105
+ #
106
+ class FetchData < Struct.new(:seqno, :attr)
107
+ ##
108
+ # method: seqno
109
+ # :call-seq: seqno -> Integer
110
+ #
111
+ # The message sequence number.
112
+ #
113
+ # [Note]
114
+ # This is never the unique identifier (UID), not even for the
115
+ # Net::IMAP#uid_fetch result. The UID is available from #uid, if it was
116
+ # returned.
117
+
118
+ ##
119
+ # method: attr
120
+ # :call-seq: attr -> hash
121
+ #
122
+ # Each key specifies a message attribute, and the value is the
123
+ # corresponding data item. Standard data items have corresponding
124
+ # accessor methods. The definitions of each attribute type is documented
125
+ # on its accessor.
126
+ #
127
+ # >>>
128
+ # *Note:* #seqno is not a message attribute.
129
+
130
+ # :call-seq: attr_upcase -> hash
131
+ #
132
+ # A transformation of #attr, with all the keys converted to upper case.
133
+ #
134
+ # Header field names are case-preserved but not not case-sensitive, so
135
+ # this is used by #header_fields and #header_fields_not.
136
+ def attr_upcase; attr.transform_keys(&:upcase) end
137
+
138
+ # :call-seq:
139
+ # body -> body structure or nil
140
+ #
141
+ # Returns an alternate form of #bodystructure, without any extension data.
142
+ #
143
+ # This is the same as getting the value for <tt>"BODY"</tt> from #attr.
144
+ #
145
+ # [Note]
146
+ # Use #message, #part, #header, #header_fields, #header_fields_not,
147
+ # #text, or #mime to retrieve <tt>BODY[#{section_spec}]</tt> attributes.
148
+ def body; attr["BODY"] end
149
+
150
+ # :call-seq:
151
+ # message(offset: bytes) -> string or nil
152
+ #
153
+ # The RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html]
154
+ # expression of the entire message, as a string.
155
+ #
156
+ # See #part for a description of +offset+.
157
+ #
158
+ # <em>RFC5322 messages can be parsed using the "mail" gem.</em>
159
+ #
160
+ # This is the same as getting the value for <tt>"BODY[]"</tt> or
161
+ # <tt>"BODY[]<#{offset}>"</tt> from #attr.
162
+ #
163
+ # See also: #header, #text, and #mime.
164
+ def message(offset: nil) attr[body_section_attr(offset: offset)] end
165
+
166
+ # :call-seq:
167
+ # part(*part_nums, offset: bytes) -> string or nil
168
+ #
169
+ # The string representation of a particular MIME part.
170
+ #
171
+ # +part_nums+ forms a path of MIME part numbers, counting up from +1+,
172
+ # which may specify an arbitrarily nested part, similarly to Array#dig.
173
+ # Messages that don't use MIME, or MIME messages that are not multipart
174
+ # and don't hold an encapsulated message, only have part +1+.
175
+ #
176
+ # If a zero-based +offset+ is given, the returned string is a substring of
177
+ # the entire contents, starting at that origin octet. This means that
178
+ # <tt>BODY[]<0></tt> MAY be truncated, but <tt>BODY[]</tt> is never
179
+ # truncated.
180
+ #
181
+ # This is the same as getting the value of
182
+ # <tt>"BODY[#{part_nums.join(".")}]"</tt> or
183
+ # <tt>"BODY[#{part_nums.join(".")}]<#{offset}>"</tt> from #attr.
184
+ #
185
+ # See also: #message, #header, #text, and #mime.
186
+ def part(index, *subparts, offset: nil)
187
+ attr[body_section_attr([index, *subparts], offset: offset)]
188
+ end
189
+
190
+ # :call-seq:
191
+ # header(*part_nums, offset: nil) -> string or nil
192
+ # header(*part_nums, fields: names, offset: nil) -> string or nil
193
+ # header(*part_nums, except: names, offset: nil) -> string or nil
194
+ #
195
+ # The {[RFC5322]}[https://www.rfc-editor.org/rfc/rfc5322.html] header of a
196
+ # message or of an encapsulated
197
+ # {[MIME-IMT]}[https://www.rfc-editor.org/rfc/rfc2046.html]
198
+ # MESSAGE/RFC822 or MESSAGE/GLOBAL message.
199
+ #
200
+ # <em>Headers can be parsed using the "mail" gem.</em>
201
+ #
202
+ # See #part for a description of +part_nums+ and +offset+.
203
+ #
204
+ # ==== Without +fields+ or +except+
205
+ # This is the same as getting the value from #attr for one of:
206
+ # * <tt>BODY[HEADER]</tt>
207
+ # * <tt>BODY[HEADER]<#{offset}></tt>
208
+ # * <tt>BODY[#{part_nums.join "."}.HEADER]"</tt>
209
+ # * <tt>BODY[#{part_nums.join "."}.HEADER]<#{offset}>"</tt>
210
+ #
211
+ # ==== With +fields+
212
+ # When +fields+ is sent, returns a subset of the header which contains
213
+ # only the header fields that match one of the names in the list.
214
+ #
215
+ # This is the same as getting the value from #attr_upcase for one of:
216
+ # * <tt>BODY[HEADER.FIELDS (#{names.join " "})]</tt>
217
+ # * <tt>BODY[HEADER.FIELDS (#{names.join " "})]<#{offset}></tt>
218
+ # * <tt>BODY[#{part_nums.join "."}.HEADER.FIELDS (#{names.join " "})]</tt>
219
+ # * <tt>BODY[#{part_nums.join "."}.HEADER.FIELDS (#{names.join " "})]<#{offset}></tt>
220
+ #
221
+ # See also: #header_fields
222
+ #
223
+ # ==== With +except+
224
+ # When +except+ is sent, returns a subset of the header which contains
225
+ # only the header fields that do _not_ match one of the names in the list.
226
+ #
227
+ # This is the same as getting the value from #attr_upcase for one of:
228
+ # * <tt>BODY[HEADER.FIELDS.NOT (#{names.join " "})]</tt>
229
+ # * <tt>BODY[HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}></tt>
230
+ # * <tt>BODY[#{part_nums.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]</tt>
231
+ # * <tt>BODY[#{part_nums.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}></tt>
232
+ #
233
+ # See also: #header_fields_not
234
+ def header(*part_nums, fields: nil, except: nil, offset: nil)
235
+ fields && except and
236
+ raise ArgumentError, "conflicting 'fields' and 'except' arguments"
237
+ if fields
238
+ text = "HEADER.FIELDS (%s)" % [fields.join(" ").upcase]
239
+ attr_upcase[body_section_attr(part_nums, text, offset: offset)]
240
+ elsif except
241
+ text = "HEADER.FIELDS.NOT (%s)" % [except.join(" ").upcase]
242
+ attr_upcase[body_section_attr(part_nums, text, offset: offset)]
243
+ else
244
+ attr[body_section_attr(part_nums, "HEADER", offset: offset)]
245
+ end
246
+ end
247
+
248
+ # :call-seq:
249
+ # header_fields(*names, part: [], offset: nil) -> string or nil
250
+ #
251
+ # The result from #header when called with <tt>fields: names</tt>.
252
+ def header_fields(first, *rest, part: [], offset: nil)
253
+ header(*part, fields: [first, *rest], offset: offset)
254
+ end
255
+
256
+ # :call-seq:
257
+ # header_fields_not(*names, part: [], offset: nil) -> string or nil
258
+ #
259
+ # The result from #header when called with <tt>except: names</tt>.
260
+ def header_fields_not(first, *rest, part: [], offset: nil)
261
+ header(*part, except: [first, *rest], offset: offset)
262
+ end
263
+
264
+ # :call-seq:
265
+ # mime(*part_nums) -> string or nil
266
+ # mime(*part_nums, offset: bytes) -> string or nil
267
+ #
268
+ # The {[MIME-IMB]}[https://www.rfc-editor.org/rfc/rfc2045.html] header for
269
+ # a message part, if it was fetched.
270
+ #
271
+ # See #part for a description of +part_nums+ and +offset+.
272
+ #
273
+ # This is the same as getting the value for
274
+ # <tt>"BODY[#{part_nums}.MIME]"</tt> or
275
+ # <tt>"BODY[#{part_nums}.MIME]<#{offset}>"</tt> from #attr.
276
+ #
277
+ # See also: #message, #header, and #text.
278
+ def mime(part, *subparts, offset: nil)
279
+ attr[body_section_attr([part, *subparts], "MIME", offset: offset)]
280
+ end
281
+
282
+ # :call-seq:
283
+ # text(*part_nums) -> string or nil
284
+ # text(*part_nums, offset: bytes) -> string or nil
285
+ #
286
+ # The text body of a message or a message part, if it was fetched,
287
+ # omitting the {[RFC5322]}[https://www.rfc-editor.org/rfc/rfc5322.html]
288
+ # header.
289
+ #
290
+ # See #part for a description of +part_nums+ and +offset+.
291
+ #
292
+ # This is the same as getting the value from #attr for one of:
293
+ # * <tt>"BODY[TEXT]"</tt>,
294
+ # * <tt>"BODY[TEXT]<#{offset}>"</tt>,
295
+ # * <tt>"BODY[#{section}.TEXT]"</tt>, or
296
+ # * <tt>"BODY[#{section}.TEXT]<#{offset}>"</tt>.
297
+ #
298
+ # See also: #message, #header, and #mime.
299
+ def text(*part, offset: nil)
300
+ attr[body_section_attr(part, "TEXT", offset: offset)]
301
+ end
302
+
303
+ # :call-seq:
304
+ # bodystructure -> BodyStructure struct or nil
305
+ #
306
+ # A BodyStructure object that describes the message, if it was fetched.
307
+ #
308
+ # This is the same as getting the value for <tt>"BODYSTRUCTURE"</tt> from
309
+ # #attr.
310
+ def bodystructure; attr["BODYSTRUCTURE"] end
311
+ alias body_structure bodystructure
312
+
313
+ # :call-seq: envelope -> Envelope or nil
314
+ #
315
+ # An Envelope object that describes the envelope structure of a message.
316
+ # See the documentation for Envelope for a description of the envelope
317
+ # structure attributes.
318
+ #
319
+ # This is the same as getting the value for <tt>"ENVELOPE"</tt> from
320
+ # #attr.
321
+ def envelope; attr["ENVELOPE"] end
322
+
323
+ # :call-seq: flags -> array of Symbols and Strings
324
+ #
325
+ # A array of flags that are set for this message. System flags are
326
+ # symbols that have been capitalized by String#capitalize. Keyword flags
327
+ # are strings and their case is not changed.
328
+ #
329
+ # This is the same as getting the value for <tt>"FLAGS"</tt> from #attr.
330
+ #
331
+ # [Note]
332
+ # The +FLAGS+ field is dynamic, and can change for a uniquely identified
333
+ # message.
334
+ def flags; attr["FLAGS"] end
335
+
336
+ # :call-seq: internaldate -> Time or nil
337
+ #
338
+ # The internal date and time of the message on the server. This is not
339
+ # the date and time in the [RFC5322[https://tools.ietf.org/html/rfc5322]]
340
+ # header, but rather a date and time which reflects when the message was
341
+ # received.
342
+ #
343
+ # This is similar to getting the value for <tt>"INTERNALDATE"</tt> from
344
+ # #attr.
345
+ #
346
+ # [Note]
347
+ # <tt>attr["INTERNALDATE"]</tt> returns a string, and this method
348
+ # returns a Time object.
349
+ def internaldate
350
+ attr["INTERNALDATE"]&.then { IMAP.decode_time _1 }
351
+ end
352
+ alias internal_date internaldate
353
+
354
+ # :call-seq: rfc822 -> String
355
+ #
356
+ # Semantically equivalent to #message with no arguments.
357
+ #
358
+ # This is the same as getting the value for <tt>"RFC822"</tt> from #attr.
359
+ #
360
+ # [Note]
361
+ # +IMAP4rev2+ deprecates <tt>RFC822</tt>.
362
+ def rfc822; attr["RFC822"] end
363
+
364
+ # :call-seq: rfc822_size -> Integer
365
+ #
366
+ # A number expressing the [RFC5322[https://tools.ietf.org/html/rfc5322]]
367
+ # size of the message.
368
+ #
369
+ # This is the same as getting the value for <tt>"RFC822.SIZE"</tt> from
370
+ # #attr.
371
+ #
372
+ # [Note]
373
+ # \IMAP was originally developed for the older
374
+ # RFC822[https://www.rfc-editor.org/rfc/rfc822.html] standard, and as a
375
+ # consequence several fetch items in \IMAP incorporate "RFC822" in their
376
+ # name. With the exception of +RFC822.SIZE+, there are more modern
377
+ # replacements; for example, the modern version of +RFC822.HEADER+ is
378
+ # <tt>BODY.PEEK[HEADER]</tt>. In all cases, "RFC822" should be
379
+ # interpreted as a reference to the updated
380
+ # RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html] standard.
381
+ def rfc822_size; attr["RFC822.SIZE"] end
382
+ alias size rfc822_size
383
+
384
+ # :call-seq: rfc822_header -> String
385
+ #
386
+ # Semantically equivalent to #header, with no arguments.
387
+ #
388
+ # This is the same as getting the value for <tt>"RFC822.HEADER"</tt> from #attr.
389
+ #
390
+ # [Note]
391
+ # +IMAP4rev2+ deprecates <tt>RFC822.HEADER</tt>.
392
+ def rfc822_header; attr["RFC822.HEADER"] end
393
+
394
+ # :call-seq: rfc822_text -> String
395
+ #
396
+ # Semantically equivalent to #text, with no arguments.
397
+ #
398
+ # This is the same as getting the value for <tt>"RFC822.TEXT"</tt> from
399
+ # #attr.
400
+ #
401
+ # [Note]
402
+ # +IMAP4rev2+ deprecates <tt>RFC822.TEXT</tt>.
403
+ def rfc822_text; attr["RFC822.TEXT"] end
404
+
405
+ # :call-seq: uid -> Integer
406
+ #
407
+ # A number expressing the unique identifier of the message.
408
+ #
409
+ # This is the same as getting the value for <tt>"UID"</tt> from #attr.
410
+ def uid; attr["UID"] end
411
+
412
+ # :call-seq:
413
+ # binary(*part_nums, offset: nil) -> string or nil
414
+ #
415
+ # Returns the binary representation of a particular MIME part, which has
416
+ # already been decoded according to its Content-Transfer-Encoding.
417
+ #
418
+ # See #part for a description of +part_nums+ and +offset+.
419
+ #
420
+ # This is the same as getting the value of
421
+ # <tt>"BINARY[#{part_nums.join(".")}]"</tt> or
422
+ # <tt>"BINARY[#{part_nums.join(".")}]<#{offset}>"</tt> from #attr.
423
+ #
424
+ # The server must support either
425
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]
426
+ # or the +BINARY+ extension
427
+ # {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516.html].
428
+ #
429
+ # See also: #binary_size, #mime
430
+ def binary(*part_nums, offset: nil)
431
+ attr[section_attr("BINARY", part_nums, offset: offset)]
432
+ end
433
+
434
+ # :call-seq:
435
+ # binary_size(*part_nums) -> integer or nil
436
+ #
437
+ # Returns the decoded size of a particular MIME part (the size to expect
438
+ # in response to a <tt>BINARY</tt> fetch request).
439
+ #
440
+ # See #part for a description of +part_nums+.
441
+ #
442
+ # This is the same as getting the value of
443
+ # <tt>"BINARY.SIZE[#{part_nums.join(".")}]"</tt> from #attr.
444
+ #
445
+ # The server must support either
446
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]
447
+ # or the +BINARY+ extension
448
+ # {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516.html].
449
+ #
450
+ # See also: #binary, #mime
451
+ def binary_size(*part_nums)
452
+ attr[section_attr("BINARY.SIZE", part_nums)]
453
+ end
454
+
455
+ # :call-seq: modseq -> Integer
456
+ #
457
+ # The modification sequence number associated with this IMAP message.
458
+ #
459
+ # This is the same as getting the value for <tt>"MODSEQ"</tt> from #attr.
460
+ #
461
+ # The server must support the +CONDSTORE+ extension
462
+ # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
463
+ #
464
+ # [Note]
465
+ # The +MODSEQ+ field is dynamic, and can change for a uniquely
466
+ # identified message.
467
+ def modseq; attr["MODSEQ"] end
468
+
469
+ # :call-seq: emailid -> string or nil
470
+ #
471
+ # An ObjectID that uniquely identifies the immutable content of a single
472
+ # message.
473
+ #
474
+ # The server must return the same +EMAILID+ for both the source and
475
+ # destination messages after a COPY or MOVE command. However, it is
476
+ # possible for different messages with the same EMAILID to have different
477
+ # mutable attributes, such as flags.
478
+ #
479
+ # This is the same as getting the value for <tt>"EMAILID"</tt> from
480
+ # #attr.
481
+ #
482
+ # The server must support the +OBJECTID+ extension
483
+ # {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
484
+ def emailid; attr["EMAILID"] end
485
+
486
+ # :call-seq: threadid -> string or nil
487
+ #
488
+ # An ObjectID that uniquely identifies a set of messages that the server
489
+ # believes should be grouped together.
490
+ #
491
+ # It is generally based on some combination of References, In-Reply-To,
492
+ # and Subject, but the exact implementation is left up to the server
493
+ # implementation. The server should return the same thread identifier for
494
+ # related messages, even if they are in different mailboxes.
495
+ #
496
+ # This is the same as getting the value for <tt>"THREADID"</tt> from
497
+ # #attr.
498
+ #
499
+ # The server must support the +OBJECTID+ extension
500
+ # {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
501
+ def threadid; attr["THREADID"] end
502
+
503
+ private
504
+
505
+ def body_section_attr(...) section_attr("BODY", ...) end
506
+
507
+ def section_attr(attr, part = [], text = nil, offset: nil)
508
+ spec = Array(part).flatten.map { Integer(_1) }
509
+ spec << text if text
510
+ spec = spec.join(".")
511
+ if offset then "%s[%s]<%d>" % [attr, spec, Integer(offset)]
512
+ else "%s[%s]" % [attr, spec]
513
+ end
514
+ end
515
+
516
+ end
517
+ end
518
+ end