Skip to content

✨ Backport UIDPlusData, AppendUIDData, CopyUIDData to v0.4 #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 7, 2025
29 changes: 29 additions & 0 deletions lib/net/imap/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,32 @@ def self.[](config)
#
# Alias for responses_without_block

# Whether ResponseParser should use the deprecated UIDPlusData or
# CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
# AppendUIDData for +APPENDUID+ response codes.
#
# AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
# UIDPlusData. Most applications should be able to upgrade with little
# or no changes.
#
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
#
# <em>(Config option added in +v0.4.19+ and +v0.5.6+.)</em>
#
# <em>UIDPlusData will be removed in +v0.6+ and this config setting will
# be ignored.</em>
#
# ==== Valid options
#
# [+true+ <em>(original default)</em>]
# ResponseParser only uses UIDPlusData.
#
# [+false+ <em>(planned default for +v0.6+)</em>]
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
attr_accessor :parser_use_deprecated_uidplus_data, type: [
true, false
]

# Creates a new config object and initialize its attribute with +attrs+.
#
# If +parent+ is not given, the global config is used by default.
Expand Down Expand Up @@ -341,6 +367,7 @@ def defaults_hash
idle_response_timeout: 5,
sasl_ir: true,
responses_without_block: :silence_deprecation_warning,
parser_use_deprecated_uidplus_data: true,
).freeze

@global = default.new
Expand All @@ -349,6 +376,7 @@ def defaults_hash

version_defaults[0] = Config[0.4].dup.update(
sasl_ir: false,
parser_use_deprecated_uidplus_data: true,
).freeze
version_defaults[0.0] = Config[0]
version_defaults[0.1] = Config[0]
Expand All @@ -365,6 +393,7 @@ def defaults_hash

version_defaults[0.6] = Config[0.5].dup.update(
responses_without_block: :frozen_dup,
parser_use_deprecated_uidplus_data: false,
).freeze
version_defaults[:future] = Config[0.6]

Expand Down
57 changes: 3 additions & 54 deletions lib/net/imap/response_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class IMAP < Protocol
autoload :FetchData, "#{__dir__}/fetch_data"
autoload :SearchResult, "#{__dir__}/search_result"
autoload :SequenceSet, "#{__dir__}/sequence_set"
autoload :UIDPlusData, "#{__dir__}/uidplus_data"
autoload :AppendUIDData, "#{__dir__}/uidplus_data"
autoload :CopyUIDData, "#{__dir__}/uidplus_data"

# Net::IMAP::ContinuationRequest represents command continuation requests.
#
Expand Down Expand Up @@ -324,60 +327,6 @@ class ResponseCode < Struct.new(:name, :data)
# code data can take.
end

# Net::IMAP::UIDPlusData represents the ResponseCode#data that accompanies
# the +APPENDUID+ and +COPYUID+ response codes.
#
# See [[UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]].
#
# ==== Capability requirement
#
# The +UIDPLUS+ capability[rdoc-ref:Net::IMAP#capability] must be supported.
# A server that supports +UIDPLUS+ should send a UIDPlusData object inside
# every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append],
# copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid
# copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid
# move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination
# mailbox reports +UIDNOTSTICKY+.
#
#--
# TODO: support MULTIAPPEND
#++
#
class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
##
# method: uidvalidity
# :call-seq: uidvalidity -> nonzero uint32
#
# The UIDVALIDITY of the destination mailbox.

##
# method: source_uids
# :call-seq: source_uids -> nil or an array of nonzero uint32
#
# The UIDs of the copied or moved messages.
#
# Note:: Returns +nil+ for Net::IMAP#append.

##
# method: assigned_uids
# :call-seq: assigned_uids -> an array of nonzero uint32
#
# The newly assigned UIDs of the copied, moved, or appended messages.
#
# Note:: This always returns an array, even when it contains only one UID.

##
# :call-seq: uid_mapping -> nil or a hash
#
# Returns a hash mapping each source UID to the newly assigned destination
# UID.
#
# Note:: Returns +nil+ for Net::IMAP#append.
def uid_mapping
source_uids&.zip(assigned_uids)&.to_h
end
end

# Net::IMAP::MailboxList represents contents of the LIST response,
# representing a single mailbox path.
#
Expand Down
26 changes: 15 additions & 11 deletions lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1867,11 +1867,10 @@ def charset__list
#
# n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
# match uid_set even if that returns a single-member array.
#
def resp_code_apnd__data
validity = number; SP!
dst_uids = uid_set # uniqueid ⊂ uid-set
UIDPlusData.new(validity, nil, dst_uids)
AppendUID(validity, dst_uids)
end

# already matched: "COPYUID"
Expand All @@ -1881,6 +1880,17 @@ def resp_code_copy__data
validity = number; SP!
src_uids = uid_set; SP!
dst_uids = uid_set
CopyUID(validity, src_uids, dst_uids)
end

def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end

# TODO: remove this code in the v0.6.0 release
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
return unless config.parser_use_deprecated_uidplus_data
src_uids &&= src_uids.each_ordered_number.to_a
dst_uids = dst_uids.each_ordered_number.to_a
UIDPlusData.new(validity, src_uids, dst_uids)
end

Expand Down Expand Up @@ -2007,15 +2017,9 @@ def nparens__objectid; NIL? ? nil : parens__objectid end
# uniqueid = nz-number
# ; Strictly ascending
def uid_set
token = match(T_NUMBER, T_ATOM)
case token.symbol
when T_NUMBER then [Integer(token.value)]
when T_ATOM
token.value.split(",").flat_map {|range|
range = range.split(":").map {|uniqueid| Integer(uniqueid) }
range.size == 1 ? range : Range.new(range.min, range.max).to_a
}
end
set = sequence_set
parse_error("uid-set cannot contain '*'") if set.include_star?
set
end

def nil_atom
Expand Down
Loading