@@ -279,7 +279,11 @@ class ByeResponseError < ResponseError
279279 #
280280 # This is different from UnknownResponseError: the response has been
281281 # rejected. Although it may be parsable, the server is forbidden from
282- # sending it in the current context. The client should automatically
282+ # sending it in the current context.
283+ #
284+ # This could be caused by a bug in the server or in Net::IMAP. Or it
285+ # might indicate a malicious server, a man-in-the-middle attack, or
286+ # client-side command injection. So the client should automatically
283287 # disconnect, abruptly (without logout).
284288 #
285289 # Note that InvalidResponseError does not inherit from ResponseError: it
@@ -288,6 +292,67 @@ class ByeResponseError < ResponseError
288292 class InvalidResponseError < Error
289293 end
290294
295+ # Error raised when the server sends a tagged #response that is invalid for
296+ # the #command #state.
297+ #
298+ # This could be caused by a bug in the server or in Net::IMAP. Or it
299+ # might indicate a malicious server, a man-in-the-middle attack, or
300+ # client-side command injection, so the client should disconnect
301+ # automatically and abruptly (without logout).
302+ class InvalidTaggedResponseError < InvalidResponseError
303+ # A symbol representing the state of the matching tagged command.
304+ #
305+ # +:unknown+::
306+ # #response does not match any known command (#command will be +nil+).
307+ # +:unstarted+::
308+ # Any tagged #response is invalid before #command starts sending.
309+ # +:incomplete+::
310+ # A tagged +OK+ #response is invalid before #command is fully sent.
311+ # +:completed+::
312+ # Multiple tagged responses were received for the same command.
313+ #
314+ # NOTE: Command state is neither anticipated nor remembered indefinitely.
315+ # +:unknown+ is used before the matching command is called and after the
316+ # it is forgotten.
317+ #
318+ # NOTE: This version of Net::IMAP does not detect or raise an exception
319+ # for all of these states.
320+ attr_reader :state
321+
322+ # Metadata about the matching IMAP command.
323+ #
324+ # Returns +nil+ when #state is +:unknown+.
325+ #
326+ # NOTE: The non-nil return type is an unstable API, for debug only. It
327+ # may be changed by any release, without warning or deprecation.
328+ attr_reader :command
329+
330+ # The TaggedResponse which triggered this error
331+ attr_reader :response
332+
333+ def initialize ( state , response :, command : nil )
334+ response => TaggedResponse [ tag :, name : status ]
335+ case [ state , status , command ]
336+ in :unknown , _ , nil
337+ in :incomplete , "OK" , { tag : ^tag , name :}
338+ in :unstarted | :completed , _ , { tag : ^tag , name :}
339+ end
340+ @state , @command , @response = state , command , response
341+ cmd_desc = name ? "#{ state } #{ name } " : state
342+ super "Received tagged #{ status } to #{ cmd_desc } command (tag=#{ tag } )"
343+ rescue NoMatchingPatternError => err
344+ raise ArgumentError , err . message
345+ end
346+
347+ def detailed_message ( **)
348+ "#{ message } .\n " \
349+ "Disconnecting: This could indicate a malicious server, a " \
350+ "man-in-the-middle attack, a client-side command injection, or " \
351+ "a bug in net-imap.\n " \
352+ "response.data=#{ response . data . inspect } "
353+ end
354+ end
355+
291356 # Error raised upon an unknown response from the server.
292357 #
293358 # This is different from InvalidResponseError: the response may be a
0 commit comments