Skip to content
Merged
116 changes: 88 additions & 28 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,9 @@ module Net
# +LITERAL-+, and +SPECIAL-USE+.</em>
#
# ==== RFC2087: +QUOTA+
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
# - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
# although the commands are backward compatible.
# - #getquota: returns the resource usage and limits for a quota root
# - #getquotaroot: returns the list of quota roots for a mailbox, as well as
# their resource usage and limits.
Expand Down Expand Up @@ -572,6 +575,16 @@ module Net
# See FetchData#emailid and FetchData#emailid.
# - Updates #status with support for the +MAILBOXID+ status attribute.
#
# ==== RFC9208: <tt>QUOTA=RES-*</tt>
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
# - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
# extension and provides strict semantics for different resource types.
# - #getquota: returns the resource usage and limits for a quota root
# - #getquotaroot: returns the list of quota roots for a mailbox, as well as
# their resource usage and limits.
# - #setquota: sets the resource limits for a given quota root.
# - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
#
# == References
#
# [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]::
Expand Down Expand Up @@ -681,14 +694,13 @@ module Net
#
# === \IMAP Extensions
#
# [QUOTA[https://tools.ietf.org/html/rfc9208]]::
# Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
# March 2022, <https://www.rfc-editor.org/info/rfc9208>.
# [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
# Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
# January 1997, <https://www.rfc-editor.org/info/rfc2087>.
#
# <em>Note: obsoletes</em>
# RFC-2087[https://tools.ietf.org/html/rfc2087]<em> (January 1997)</em>.
# <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
# [IDLE[https://tools.ietf.org/html/rfc2177]]::
# *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
# (March 2022).
# [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
# Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
# June 1997, <https://www.rfc-editor.org/info/rfc2177>.
# [NAMESPACE[https://tools.ietf.org/html/rfc2342]]::
Expand Down Expand Up @@ -739,9 +751,15 @@ module Net
# Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
# RFC 8474, DOI 10.17487/RFC8474, September 2018,
# <https://www.rfc-editor.org/info/rfc8474>.
# [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
# Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
# March 2022, <https://www.rfc-editor.org/info/rfc9208>.
#
# Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
#
# === IANA registries
# * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
# * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
# * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml]
# * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml]
# * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml]
Expand Down Expand Up @@ -1742,12 +1760,18 @@ def xlist(refname, mailbox)
# to both admin and user. If this mailbox exists, it returns an array
# containing objects of type MailboxQuotaRoot and MailboxQuota.
#
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
# resource type. This is usually +STORAGE+, but you may need to verify this
# with UntaggedResponse#raw_data.
#
# Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
#
# ===== Capabilities
#
# The server's capabilities must include +QUOTA+
# [RFC2087[https://tools.ietf.org/html/rfc2087]].
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
# resource type.
def getquotaroot(mailbox)
synchronize do
send_command("GETQUOTAROOT", mailbox)
Expand All @@ -1759,41 +1783,59 @@ def getquotaroot(mailbox)
end

# Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
# along with specified +mailbox+. If this mailbox exists, then an array
# containing a MailboxQuota object is returned. This command is generally
# only available to server admin.
# for the +quota_root+. If this quota root exists, then an array
# containing a MailboxQuota object is returned.
#
# The names of quota roots that are applicable to a particular mailbox can
# be discovered with #getquotaroot.
#
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
# resource type. This is usually +STORAGE+, but you may need to verify this
# with UntaggedResponse#raw_data.
#
# Related: #getquotaroot, #setquota, MailboxQuota
#
# ===== Capabilities
#
# The server's capabilities must include +QUOTA+
# [RFC2087[https://tools.ietf.org/html/rfc2087]].
def getquota(mailbox)
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
# resource type.
def getquota(quota_root)
synchronize do
send_command("GETQUOTA", mailbox)
send_command("GETQUOTA", quota_root)
clear_responses("QUOTA")
end
end

# Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
# along with the specified +mailbox+ and +quota+. If +quota+ is nil, then
# +quota+ will be unset for that mailbox. Typically one needs to be logged
# in as a server admin for this to work.
# along with the specified +quota_root+ and +storage_limit+. If
# +storage_limit+ is +nil+, resource limits are unset for that quota root.
# If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
#
# imap.setquota "#user/alice", 100
# imap.getquota "#user/alice"
# # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
#
# Typically one needs to be logged in as a server admin for this to work.
#
# *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
#
# Related: #getquota, #getquotaroot
#
# ===== Capabilities
#
# The server's capabilities must include +QUOTA+
# [RFC2087[https://tools.ietf.org/html/rfc2087]].
def setquota(mailbox, quota)
if quota.nil?
data = '()'
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
# capability, or both +QUOTASET+ and a capability prefixed with
# <tt>QUOTA=RES-*</tt> {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208]
# for each supported resource type.
def setquota(quota_root, storage_limit)
if storage_limit.nil?
list = []
else
data = '(STORAGE ' + quota.to_s + ')'
list = ["STORAGE", Integer(storage_limit)]
end
send_command("SETQUOTA", mailbox, RawData.new(data))
send_command("SETQUOTA", quota_root, list)
end

# Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
Expand Down Expand Up @@ -1900,7 +1942,10 @@ def lsub(refname, mailbox)
# <tt>STATUS=SIZE</tt>
# {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
#
# +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
# +DELETED+ must be supported when the server's capabilities includes
# +IMAP4rev2+.
# or <tt>QUOTA=RES-MESSAGES</tt>
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
#
# +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
# {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
Expand Down Expand Up @@ -2049,6 +2094,14 @@ def uid_expunge(uid_set)
#
# ===== Search criteria
#
# >>>
# When +criteria+ is an Array, elements in the array will be validated and
# formatted. When +criteria+ is a String, it will be sent <em>with
# minimal validation and no encoding or formatting</em>.
#
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
# types of attribute injection attack if unvetted user input is used.</em>
#
# For a full list of search criteria,
# see [{IMAP4rev1 §6.4.4}[https://www.rfc-editor.org/rfc/rfc3501.html#section-6.4.4]],
# or [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]],
Expand Down Expand Up @@ -2136,6 +2189,13 @@ def uid_search(keys, charset = nil)
#
# +attr+ is a list of attributes to fetch; see the documentation
# for FetchData for a list of valid attributes.
# >>>
# When +attr+ is a String, it will be sent <em>with minimal validation and
# no encoding or formatting</em>. When +attr+ is an Array, each String in
# +attr+ will be sent this way.
#
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
# types of attribute injection attack if unvetted user input is used.</em>
#
# +changedsince+ is an optional integer mod-sequence. It limits results to
# messages with a mod-sequence greater than +changedsince+.
Expand Down Expand Up @@ -3071,7 +3131,7 @@ def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil)
end

def store_internal(cmd, set, attr, flags, unchangedsince: nil)
attr = RawData.new(attr) if attr.instance_of?(String)
attr = Atom.new(attr) if attr.instance_of?(String)
args = [MessageSet.new(set)]
args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
args << attr << flags
Expand Down
117 changes: 109 additions & 8 deletions lib/net/imap/command_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def validate_data(data)
end
when Time, Date, DateTime
when Symbol
Flag.validate(data)
else
data.validate
end
Expand All @@ -45,7 +46,7 @@ def send_data(data, tag = nil)
when Date
send_date_data(data)
when Symbol
send_symbol_data(data)
Flag[data].send_data(self, tag)
else
data.send_data(self, tag)
end
Expand Down Expand Up @@ -129,10 +130,6 @@ def send_list_data(list, tag = nil)
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
def send_time_data(time) put_string Net::IMAP.encode_time(time) end

def send_symbol_data(symbol)
put_string("\\" + symbol.to_s)
end

# simplistic emulation of CommandData = Data.define(:data)
class CommandData # :nodoc:
class << self
Expand All @@ -154,6 +151,12 @@ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
# following class definition goes beyond the basic Data.define(:data)
##

def self.validate(...)
data = new(...)
data.validate
data
end

def send_data(imap, tag)
raise NoMethodError, "#{self.class} must implement #{__method__}"
end
Expand All @@ -162,15 +165,113 @@ def validate
end
end

# Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
# except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
# multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
# been enabled, or when the server supports only +IMAP4rev2+ and not earlier
# IMAP revisions, or when the server advertises +UTF8=ONLY+.
#
# NOTE: The current implementation does not validate whether the connection
# currently supports UTF-8. Future versions may change.
#
# The string's bytes must be valid ASCII or valid UTF-8. The string's
# reported encoding is ignored, but the string is _not_ transcoded.
class RawText < CommandData # :nodoc:
def initialize(data:)
data = String(data.to_str)
data = if [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
-data
elsif data.ascii_only?
-(data.dup.force_encoding("ASCII"))
else
-(data.dup.force_encoding("UTF-8"))
end
super
validate
end

def validate
if data.include?("\0")
raise DataFormatError, "NULL byte must be binary literal encoded"
elsif !data.valid_encoding?
raise DataFormatError, "invalid UTF-8 must be literal encoded"
elsif /[\r\n]/.match?(data)
raise DataFormatError, "CR and LF bytes must be literal encoded"
end
end

def ascii_only?; data.ascii_only? end

def send_data(imap, tag) imap.__send__(:put_string, data) end
end

class RawData < CommandData # :nodoc:
def send_data(imap, tag)
imap.__send__(:put_string, @data)
def initialize(data:)
data = split_parts(data)
super
validate
end

def send_data(imap, tag) data.each do _1.send_data(imap, tag) end end

def validate
return unless RawText === data.last
text = data.last.data
if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
raise DataFormatError, "RawData cannot end with literal continuation"
end
end

private

def split_parts(data)
data = data.b # dups and ensures BINARY encoding
parts = []
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
bytesize = Integer bytesize, 10
parts << RawText[text] unless text.empty?
parts << extract_literal(data,
binary: binary,
bytesize: bytesize,
non_sync: non_sync)
data[0, bytesize] = ""
end
parts << RawText[data] unless data.empty?
parts
end

def extract_literal(data, binary:, bytesize:, non_sync:)
if data.bytesize < bytesize
raise DataFormatError, "Too few bytes in string for literal, " \
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
end
literal = data.byteslice(0, bytesize)
(binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
end
end

class Atom < CommandData # :nodoc:
def initialize(**)
super
validate
end

def validate
data.to_s.ascii_only? \
or raise DataFormatError, "#{self.class} must be ASCII only"
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
and raise DataFormatError, "#{self.class} must not contain atom-specials"
end

def send_data(imap, tag)
imap.__send__(:put_string, data.to_s)
end
end

class Flag < Atom # :nodoc:
def send_data(imap, tag)
imap.__send__(:put_string, @data)
imap.__send__(:put_string, "\\#{data}")
end
end

Expand Down
Loading
Loading