# frozen_string_literal: true
module Net
class IMAP < Protocol
# Net::IMAP::FetchData represents the contents of a FETCH response.
# Net::IMAP#fetch and Net::IMAP#uid_fetch both return an array of
# FetchData objects.
#
# === Fetch attributes
#
# See {[IMAP4rev1 §7.4.2]}[https://www.rfc-editor.org/rfc/rfc3501.html#section-7.4.2]
# and {[IMAP4rev2 §7.5.2]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.5.2]
# for a full description of the standard fetch response data items, and
# Net::IMAP@Message+envelope+and+body+structure for other relevant RFCs.
#
# ==== Static fetch data items
#
# Most message attributes are static, and must never change for a given
# (server, account, mailbox, UIDVALIDITY, UID) tuple.
#
# The static fetch data items defined by both
# IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] and
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] are:
#
# * "UID" --- See #uid.
# * "BODY" --- See #body.
# * "BODY[#{section_spec}]",
# "BODY[#{section_spec}]<#{offset}>" --- See #message,
# #part, #header, #header_fields, #header_fields_not, #mime, and #text.
# * "BODYSTRUCTURE" --- See #bodystructure.
# * "ENVELOPE" --- See #envelope.
# * "INTERNALDATE" --- See #internaldate.
# * "RFC822.SIZE" --- See #rfc822_size.
#
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] adds the
# additional fetch items from the +BINARY+ extension
# {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516.html]:
#
# * "BINARY[#{part}]",
# "BINARY[#{part}]<#{offset}>" -- See #binary.
# * "BINARY.SIZE[#{part}]" -- See #binary_size.
#
# Several static message attributes in
# IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] are obsolete and
# been removed from
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]:
#
# * "RFC822" --- See #rfc822 or replace with
# "BODY[]" and #message.
# * "RFC822.HEADER" --- See #rfc822_header or replace with
# "BODY[HEADER]" and #header.
# * "RFC822.TEXT" --- See #rfc822_text or replace with
# "BODY[TEXT]" and #text.
#
# Net::IMAP supports static attributes defined by the following extensions:
# * +OBJECTID+ {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html]
# * "EMAILID" --- See #emailid.
# * "THREADID" --- See #threadid.
#
# * +X-GM-EXT-1+ {[non-standard Gmail
# extension]}[https://developers.google.com/gmail/imap/imap-extensions]
# * "X-GM-MSGID" --- unique message ID. Access via #attr.
# * "X-GM-THRID" --- Thread ID. Access via #attr.
#
# [Note:]
# >>>
# Additional static fields are defined in other \IMAP extensions, but
# Net::IMAP can't parse them yet.
#
# ==== Dynamic message attributes
#
# Some message attributes can be dynamically changed, for example using the
# {STORE command}[rdoc-ref:Net::IMAP#store].
#
# The only dynamic message attribute defined by
# IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] and
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] is:
#
# * "FLAGS" --- See #flags.
#
# Net::IMAP supports dynamic attributes defined by the following extensions:
#
# * +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]:
# * "MODSEQ" --- See #modseq.
# * +X-GM-EXT-1+ {[non-standard Gmail
# extension]}[https://developers.google.com/gmail/imap/imap-extensions]
# * "X-GM-LABELS" --- Gmail labels. Access via #attr.
#
# [Note:]
# >>>
# Additional dynamic fields are defined in other \IMAP extensions, but
# Net::IMAP can't parse them yet.
#
# === Implicitly setting \Seen and using +PEEK+
#
# Unless the mailbox is has been opened as read-only, fetching
# BODY[#{section}] or BINARY[#{section}]
# will implicitly set the \Seen flag. To avoid this, fetch using
# BODY.PEEK[#{section}] or BINARY.PEEK[#{section}]
# instead.
#
# Note that the data will always be _returned_ without ".PEEK", in
# BODY[#{specifier}] or BINARY[#{section}].
#
class FetchData < Struct.new(:seqno, :attr)
##
# method: seqno
# :call-seq: seqno -> Integer
#
# The message sequence number.
#
# [Note]
# This is never the unique identifier (UID), not even for the
# Net::IMAP#uid_fetch result. The UID is available from #uid, if it was
# returned.
##
# method: attr
# :call-seq: attr -> hash
#
# Each key specifies a message attribute, and the value is the
# corresponding data item. Standard data items have corresponding
# accessor methods. The definitions of each attribute type is documented
# on its accessor.
#
# >>>
# *Note:* #seqno is not a message attribute.
# :call-seq: attr_upcase -> hash
#
# A transformation of #attr, with all the keys converted to upper case.
#
# Header field names are case-preserved but not case-sensitive, so this is
# used by #header_fields and #header_fields_not.
def attr_upcase; attr.transform_keys(&:upcase) end
# :call-seq:
# body -> body structure or nil
#
# Returns an alternate form of #bodystructure, without any extension data.
#
# This is the same as getting the value for "BODY" from #attr.
#
# [Note]
# Use #message, #part, #header, #header_fields, #header_fields_not,
# #text, or #mime to retrieve BODY[#{section_spec}] attributes.
def body; attr["BODY"] end
# :call-seq:
# message(offset: bytes) -> string or nil
#
# The RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html]
# expression of the entire message, as a string.
#
# See #part for a description of +offset+.
#
# RFC5322 messages can be parsed using the "mail" gem.
#
# This is the same as getting the value for "BODY[]" or
# "BODY[]<#{offset}>" from #attr.
#
# See also: #header, #text, and #mime.
def message(offset: nil) attr[body_section_attr(offset: offset)] end
# :call-seq:
# part(*part_nums, offset: bytes) -> string or nil
#
# The string representation of a particular MIME part.
#
# +part_nums+ forms a path of MIME part numbers, counting up from +1+,
# which may specify an arbitrarily nested part, similarly to Array#dig.
# Messages that don't use MIME, or MIME messages that are not multipart
# and don't hold an encapsulated message, only have part +1+.
#
# If a zero-based +offset+ is given, the returned string is a substring of
# the entire contents, starting at that origin octet. This means that
# BODY[]<0> MAY be truncated, but BODY[] is never
# truncated.
#
# This is the same as getting the value of
# "BODY[#{part_nums.join(".")}]" or
# "BODY[#{part_nums.join(".")}]<#{offset}>" from #attr.
#
# See also: #message, #header, #text, and #mime.
def part(index, *subparts, offset: nil)
attr[body_section_attr([index, *subparts], offset: offset)]
end
# :call-seq:
# header(*part_nums, offset: nil) -> string or nil
# header(*part_nums, fields: names, offset: nil) -> string or nil
# header(*part_nums, except: names, offset: nil) -> string or nil
#
# The {[RFC5322]}[https://www.rfc-editor.org/rfc/rfc5322.html] header of a
# message or of an encapsulated
# {[MIME-IMT]}[https://www.rfc-editor.org/rfc/rfc2046.html]
# MESSAGE/RFC822 or MESSAGE/GLOBAL message.
#
# Headers can be parsed using the "mail" gem.
#
# See #part for a description of +part_nums+ and +offset+.
#
# ==== Without +fields+ or +except+
# This is the same as getting the value from #attr for one of:
# * BODY[HEADER]
# * BODY[HEADER]<#{offset}>
# * BODY[#{part_nums.join "."}.HEADER]"
# * BODY[#{part_nums.join "."}.HEADER]<#{offset}>"
#
# ==== With +fields+
# When +fields+ is sent, returns a subset of the header which contains
# only the header fields that match one of the names in the list.
#
# This is the same as getting the value from #attr_upcase for one of:
# * BODY[HEADER.FIELDS (#{names.join " "})]
# * BODY[HEADER.FIELDS (#{names.join " "})]<#{offset}>
# * BODY[#{part_nums.join "."}.HEADER.FIELDS (#{names.join " "})]
# * BODY[#{part_nums.join "."}.HEADER.FIELDS (#{names.join " "})]<#{offset}>
#
# See also: #header_fields
#
# ==== With +except+
# When +except+ is sent, returns a subset of the header which contains
# only the header fields that do _not_ match one of the names in the list.
#
# This is the same as getting the value from #attr_upcase for one of:
# * BODY[HEADER.FIELDS.NOT (#{names.join " "})]
# * BODY[HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}>
# * BODY[#{part_nums.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]
# * BODY[#{part_nums.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}>
#
# See also: #header_fields_not
def header(*part_nums, fields: nil, except: nil, offset: nil)
fields && except and
raise ArgumentError, "conflicting 'fields' and 'except' arguments"
if fields
text = "HEADER.FIELDS (%s)" % [fields.join(" ").upcase]
attr_upcase[body_section_attr(part_nums, text, offset: offset)]
elsif except
text = "HEADER.FIELDS.NOT (%s)" % [except.join(" ").upcase]
attr_upcase[body_section_attr(part_nums, text, offset: offset)]
else
attr[body_section_attr(part_nums, "HEADER", offset: offset)]
end
end
# :call-seq:
# header_fields(*names, part: [], offset: nil) -> string or nil
#
# The result from #header when called with fields: names.
def header_fields(first, *rest, part: [], offset: nil)
header(*part, fields: [first, *rest], offset: offset)
end
# :call-seq:
# header_fields_not(*names, part: [], offset: nil) -> string or nil
#
# The result from #header when called with except: names.
def header_fields_not(first, *rest, part: [], offset: nil)
header(*part, except: [first, *rest], offset: offset)
end
# :call-seq:
# mime(*part_nums) -> string or nil
# mime(*part_nums, offset: bytes) -> string or nil
#
# The {[MIME-IMB]}[https://www.rfc-editor.org/rfc/rfc2045.html] header for
# a message part, if it was fetched.
#
# See #part for a description of +part_nums+ and +offset+.
#
# This is the same as getting the value for
# "BODY[#{part_nums}.MIME]" or
# "BODY[#{part_nums}.MIME]<#{offset}>" from #attr.
#
# See also: #message, #header, and #text.
def mime(part, *subparts, offset: nil)
attr[body_section_attr([part, *subparts], "MIME", offset: offset)]
end
# :call-seq:
# text(*part_nums) -> string or nil
# text(*part_nums, offset: bytes) -> string or nil
#
# The text body of a message or a message part, if it was fetched,
# omitting the {[RFC5322]}[https://www.rfc-editor.org/rfc/rfc5322.html]
# header.
#
# See #part for a description of +part_nums+ and +offset+.
#
# This is the same as getting the value from #attr for one of:
# * "BODY[TEXT]",
# * "BODY[TEXT]<#{offset}>",
# * "BODY[#{section}.TEXT]", or
# * "BODY[#{section}.TEXT]<#{offset}>".
#
# See also: #message, #header, and #mime.
def text(*part, offset: nil)
attr[body_section_attr(part, "TEXT", offset: offset)]
end
# :call-seq:
# bodystructure -> BodyStructure struct or nil
#
# A BodyStructure object that describes the message, if it was fetched.
#
# This is the same as getting the value for "BODYSTRUCTURE" from
# #attr.
def bodystructure; attr["BODYSTRUCTURE"] end
alias body_structure bodystructure
# :call-seq: envelope -> Envelope or nil
#
# An Envelope object that describes the envelope structure of a message.
# See the documentation for Envelope for a description of the envelope
# structure attributes.
#
# This is the same as getting the value for "ENVELOPE" from
# #attr.
def envelope; attr["ENVELOPE"] end
# :call-seq: flags -> array of Symbols and Strings
#
# A array of flags that are set for this message. System flags are
# symbols that have been capitalized by String#capitalize. Keyword flags
# are strings and their case is not changed.
#
# This is the same as getting the value for "FLAGS" from #attr.
#
# [Note]
# The +FLAGS+ field is dynamic, and can change for a uniquely identified
# message.
def flags; attr["FLAGS"] end
# :call-seq: internaldate -> Time or nil
#
# The internal date and time of the message on the server. This is not
# the date and time in the [RFC5322[https://tools.ietf.org/html/rfc5322]]
# header, but rather a date and time which reflects when the message was
# received.
#
# This is similar to getting the value for "INTERNALDATE" from
# #attr.
#
# [Note]
# attr["INTERNALDATE"] returns a string, and this method
# returns a Time object.
def internaldate
attr["INTERNALDATE"]&.then { IMAP.decode_time _1 }
end
alias internal_date internaldate
# :call-seq: rfc822 -> String
#
# Semantically equivalent to #message with no arguments.
#
# This is the same as getting the value for "RFC822" from #attr.
#
# [Note]
# +IMAP4rev2+ deprecates RFC822.
def rfc822; attr["RFC822"] end
# :call-seq: rfc822_size -> Integer
#
# A number expressing the [RFC5322[https://tools.ietf.org/html/rfc5322]]
# size of the message.
#
# This is the same as getting the value for "RFC822.SIZE" from
# #attr.
#
# [Note]
# \IMAP was originally developed for the older
# RFC822[https://www.rfc-editor.org/rfc/rfc822.html] standard, and as a
# consequence several fetch items in \IMAP incorporate "RFC822" in their
# name. With the exception of +RFC822.SIZE+, there are more modern
# replacements; for example, the modern version of +RFC822.HEADER+ is
# BODY.PEEK[HEADER]. In all cases, "RFC822" should be
# interpreted as a reference to the updated
# RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html] standard.
def rfc822_size; attr["RFC822.SIZE"] end
alias size rfc822_size
# :call-seq: rfc822_header -> String
#
# Semantically equivalent to #header, with no arguments.
#
# This is the same as getting the value for "RFC822.HEADER" from #attr.
#
# [Note]
# +IMAP4rev2+ deprecates RFC822.HEADER.
def rfc822_header; attr["RFC822.HEADER"] end
# :call-seq: rfc822_text -> String
#
# Semantically equivalent to #text, with no arguments.
#
# This is the same as getting the value for "RFC822.TEXT" from
# #attr.
#
# [Note]
# +IMAP4rev2+ deprecates RFC822.TEXT.
def rfc822_text; attr["RFC822.TEXT"] end
# :call-seq: uid -> Integer
#
# A number expressing the unique identifier of the message.
#
# This is the same as getting the value for "UID" from #attr.
def uid; attr["UID"] end
# :call-seq:
# binary(*part_nums, offset: nil) -> string or nil
#
# Returns the binary representation of a particular MIME part, which has
# already been decoded according to its Content-Transfer-Encoding.
#
# See #part for a description of +part_nums+ and +offset+.
#
# This is the same as getting the value of
# "BINARY[#{part_nums.join(".")}]" or
# "BINARY[#{part_nums.join(".")}]<#{offset}>" from #attr.
#
# The server must support either
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]
# or the +BINARY+ extension
# {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516.html].
#
# See also: #binary_size, #mime
def binary(*part_nums, offset: nil)
attr[section_attr("BINARY", part_nums, offset: offset)]
end
# :call-seq:
# binary_size(*part_nums) -> integer or nil
#
# Returns the decoded size of a particular MIME part (the size to expect
# in response to a BINARY fetch request).
#
# See #part for a description of +part_nums+.
#
# This is the same as getting the value of
# "BINARY.SIZE[#{part_nums.join(".")}]" from #attr.
#
# The server must support either
# IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]
# or the +BINARY+ extension
# {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516.html].
#
# See also: #binary, #mime
def binary_size(*part_nums)
attr[section_attr("BINARY.SIZE", part_nums)]
end
# :call-seq: modseq -> Integer
#
# The modification sequence number associated with this IMAP message.
#
# This is the same as getting the value for "MODSEQ" from #attr.
#
# The server must support the +CONDSTORE+ extension
# {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
#
# [Note]
# The +MODSEQ+ field is dynamic, and can change for a uniquely
# identified message.
def modseq; attr["MODSEQ"] end
# :call-seq: emailid -> string or nil
#
# An ObjectID that uniquely identifies the immutable content of a single
# message.
#
# The server must return the same +EMAILID+ for both the source and
# destination messages after a COPY or MOVE command. However, it is
# possible for different messages with the same EMAILID to have different
# mutable attributes, such as flags.
#
# This is the same as getting the value for "EMAILID" from
# #attr.
#
# The server must support the +OBJECTID+ extension
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
def emailid; attr["EMAILID"] end
# :call-seq: threadid -> string or nil
#
# An ObjectID that uniquely identifies a set of messages that the server
# believes should be grouped together.
#
# It is generally based on some combination of References, In-Reply-To,
# and Subject, but the exact implementation is left up to the server
# implementation. The server should return the same thread identifier for
# related messages, even if they are in different mailboxes.
#
# This is the same as getting the value for "THREADID" from
# #attr.
#
# The server must support the +OBJECTID+ extension
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
def threadid; attr["THREADID"] end
private
def body_section_attr(...) section_attr("BODY", ...) end
def section_attr(attr, part = [], text = nil, offset: nil)
spec = Array(part).flatten.map { Integer(_1) }
spec << text if text
spec = spec.join(".")
if offset then "%s[%s]<%d>" % [attr, spec, Integer(offset)]
else "%s[%s]" % [attr, spec]
end
end
end
end
end