# frozen_string_literal: true

require "date"
require "time"

require_relative "errors"

module Net
  class IMAP < Protocol

    # strftime/strptime format for an IMAP4 +date+, excluding optional dquotes.
    # Use via the encode_date and decode_date methods.
    #
    #   date            = date-text / DQUOTE date-text DQUOTE
    #   date-text       = date-day "-" date-month "-" date-year
    #
    #   date-day        = 1*2DIGIT
    #                       ; Day of month
    #   date-month      = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
    #                     "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
    #   date-year       = 4DIGIT
    STRFDATE = "%d-%b-%Y"

    # strftime/strptime format for an IMAP4 +date-time+, including dquotes.
    # See the encode_datetime and decode_datetime methods.
    #
    #   date-time       = DQUOTE date-day-fixed "-" date-month "-" date-year
    #                     SP time SP zone DQUOTE
    #
    #   date-day-fixed  = (SP DIGIT) / 2DIGIT
    #                       ; Fixed-format version of date-day
    #   date-month      = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
    #                     "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
    #   date-year       = 4DIGIT
    #   time            = 2DIGIT ":" 2DIGIT ":" 2DIGIT
    #                       ; Hours minutes seconds
    #   zone            = ("+" / "-") 4DIGIT
    #                       ; Signed four-digit value of hhmm representing
    #                       ; hours and minutes east of Greenwich (that is,
    #                       ; the amount that the given time differs from
    #                       ; Universal Time).  Subtracting the timezone
    #                       ; from the given time will give the UT form.
    #                       ; The Universal Time zone is "+0000".
    #
    # Note that Time.strptime <tt>"%d"</tt> flexibly parses either space or zero
    # padding.  However, the DQUOTEs are *not* optional.
    STRFTIME = '"%d-%b-%Y %H:%M:%S %z"'

    # Decode a string from modified UTF-7 format to UTF-8.
    #
    # UTF-7 is a 7-bit encoding of Unicode [UTF7].  IMAP uses a
    # slightly modified version of this to encode mailbox names
    # containing non-ASCII characters; see [IMAP] section 5.1.3.
    #
    # Net::IMAP does _not_ automatically encode and decode
    # mailbox names to and from UTF-7.
    def self.decode_utf7(s)
      return s.gsub(/&([A-Za-z0-9+,]+)?-/n) {
        if base64 = $1
          (base64.tr(",", "/") + "===").unpack1("m").encode(Encoding::UTF_8, Encoding::UTF_16BE)
        else
          "&"
        end
      }
    end

    # Encode a string from UTF-8 format to modified UTF-7.
    def self.encode_utf7(s)
      return s.gsub(/(&)|[^\x20-\x7e]+/) {
        if $1
          "&-"
        else
          base64 = [$&.encode(Encoding::UTF_16BE)].pack("m0")
          "&" + base64.delete("=").tr("/", ",") + "-"
        end
      }.force_encoding("ASCII-8BIT")
    end

    # Formats +time+ as an IMAP4 date.
    def self.encode_date(date)
      date.to_date.strftime STRFDATE
    end

    # :call-seq: decode_date(string) -> Date
    #
    # Decodes +string+ as an IMAP formatted "date".
    #
    # Double quotes are optional.  Day of month may be padded with zero or
    # space.  See STRFDATE.
    def self.decode_date(string)
      string = string.delete_prefix('"').delete_suffix('"')
      Date.strptime(string, STRFDATE)
    end

    # :call-seq: encode_datetime(time) -> string
    #
    # Formats +time+ as an IMAP4 date-time.
    def self.encode_datetime(time)
      time.to_datetime.strftime STRFTIME
    end

    # :call-seq: decode_datetime(string) -> DateTime
    #
    # Decodes +string+ as an IMAP4 formatted "date-time".
    #
    # NOTE: Although double-quotes are not optional in the IMAP grammar,
    # Net::IMAP currently parses "date-time" values as "quoted" strings and this
    # removes the quotation marks.  To be useful for strings which have already
    # been parsed as a quoted string, this method makes double-quotes optional.
    #
    # See STRFTIME.
    def self.decode_datetime(string)
      unless string.start_with?(?") && string.end_with?(?")
        string = '"%s"' % [string]
      end
      DateTime.strptime(string, STRFTIME)
    end

    # :call-seq: decode_time(string) -> Time
    #
    # Decodes +string+ as an IMAP4 formatted "date-time".
    #
    # Same as +decode_datetime+, but returning a Time instead.
    def self.decode_time(string)
      unless string.start_with?(?") && string.end_with?(?")
        string = '"%s"' % [string]
      end
      Time.strptime(string, STRFTIME)
    end

    class << self
      alias encode_time     encode_datetime
      alias format_date     encode_date
      alias format_time     encode_time
      alias parse_date      decode_date
      alias parse_datetime  decode_datetime
      alias parse_time      decode_time

      # alias format_datetime encode_datetime  # n.b: this is overridden below...
    end

    # DEPRECATED:: The original version returned incorrectly formatted strings.
    #              Strings returned by encode_datetime or format_time use the
    #              correct IMAP4rev1 syntax for "date-time".
    #
    # This invalid format has been temporarily retained for backward
    # compatibility.  A future release will change this method to return the
    # correct format.
    def self.format_datetime(time)
      warn("#{self}.format_datetime incorrectly formats IMAP date-time. " \
           "Convert to #{self}.encode_datetime or #{self}.format_time instead.",
           uplevel: 1, category: :deprecated)
      time.strftime("%d-%b-%Y %H:%M %z")
    end

    # Common validators of number and nz_number types
    module NumValidator # :nodoc
      NUMBER_RE = /\A\d+\z/
      NZ_NUMBER_RE = /\A[1-9]\d*\z/
      module_function

      # Check if argument is a valid 'number' according to RFC 3501
      #     number          = 1*DIGIT
      #                        ; Unsigned 32-bit integer
      #                        ; (0 <= n < 4,294,967,296)
      def valid_number?(num)
        0 <= num && num <= 0xffff_ffff
      end

      # Check if argument is a valid 'nz-number' according to RFC 3501
      #     nz-number       = digit-nz *DIGIT
      #                        ; Non-zero unsigned 32-bit integer
      #                        ; (0 < n < 4,294,967,296)
      def valid_nz_number?(num)
        0 < num && num <= 0xffff_ffff
      end

      # Check if argument is a valid 'number64' according to RFC 9051
      #     number64        = 1*DIGIT
      #                        ; Unsigned 63-bit integer
      #                        ; (0 <= n <= 9,223,372,036,854,775,807)
      def valid_number64?(num)
        0 <= num && num <= 0x7fff_ffff_ffff_ffff
      end

      # Check if argument is a valid 'number64' according to RFC 9051
      #     nz-number64     = digit-nz *DIGIT
      #                        ; Unsigned 63-bit integer
      #                        ; (0 < n <= 9,223,372,036,854,775,807)
      def valid_nz_number64?(num)
        0 < num && num <= 0x7fff_ffff_ffff_ffff
      end

      # Check if argument is a valid 'mod-sequence-value' according to RFC 4551
      #     mod-sequence-value  = 1*DIGIT
      #                            ; Positive unsigned 64-bit integer
      #                            ; (mod-sequence)
      #                            ; (1 <= n < 18,446,744,073,709,551,615)
      def valid_mod_sequence_value?(num)
        1 <= num && num < 0xffff_ffff_ffff_ffff
      end

      # Check if argument is a valid 'mod-sequence-valzer' according to RFC 4551
      #     mod-sequence-valzer = "0" / mod-sequence-value
      def valid_mod_sequence_valzer?(num)
        0 <= num && num < 0xffff_ffff_ffff_ffff
      end

      # Ensure argument is 'number' or raise DataFormatError
      def ensure_number(num)
        return num if valid_number?(num)
        raise DataFormatError,
          "number must be unsigned 32-bit integer: #{num}"
      end

      # Ensure argument is 'nz-number' or raise DataFormatError
      def ensure_nz_number(num)
        return num if valid_nz_number?(num)
        raise DataFormatError,
          "nz-number must be non-zero unsigned 32-bit integer: #{num}"
      end

      # Ensure argument is 'number64' or raise DataFormatError
      def ensure_number64(num)
        return num if valid_number64?(num)
        raise DataFormatError,
          "number64 must be unsigned 63-bit integer: #{num}"
      end

      # Ensure argument is 'nz-number64' or raise DataFormatError
      def ensure_nz_number64(num)
        return num if valid_nz_number64?(num)
        raise DataFormatError,
          "nz-number64 must be non-zero unsigned 63-bit integer: #{num}"
      end

      # Ensure argument is 'mod-sequence-value' or raise DataFormatError
      def ensure_mod_sequence_value(num)
        return num if valid_mod_sequence_value?(num)
        raise DataFormatError,
          "mod-sequence-value must be non-zero unsigned 64-bit integer: #{num}"
      end

      # Ensure argument is 'mod-sequence-valzer' or raise DataFormatError
      def ensure_mod_sequence_valzer(num)
        return num if valid_mod_sequence_valzer?(num)
        raise DataFormatError,
          "mod-sequence-valzer must be unsigned 64-bit integer: #{num}"
      end

      # Like #ensure_number, but usable with numeric String input.
      def coerce_number(num)
        case num
        when Integer   then ensure_number num
        when NUMBER_RE then ensure_number num.to_i
        else
          raise DataFormatError, "%p is not a valid number" % [num]
        end
      end

      # Like #ensure_nz_number, but usable with numeric String input.
      def coerce_nz_number(num)
        case num
        when Integer      then ensure_nz_number num
        when NZ_NUMBER_RE then ensure_nz_number num.to_i
        else
          raise DataFormatError, "%p is not a valid nz-number" % [num]
        end
      end

      # Like #ensure_number64, but usable with numeric String input.
      def coerce_number64(num)
        case num
        when Integer   then ensure_number64 num
        when NUMBER_RE then ensure_number64 num.to_i
        else
          raise DataFormatError, "%p is not a valid number64" % [num]
        end
      end

      # Like #ensure_nz_number64, but usable with numeric String input.
      def coerce_nz_number64(num)
        case num
        when Integer      then ensure_nz_number64 num
        when NZ_NUMBER_RE then ensure_nz_number64 num.to_i
        else
          raise DataFormatError, "%p is not a valid nz-number64" % [num]
        end
      end

      # Like #ensure_mod_sequence_value, but usable with numeric String input.
      def coerce_mod_sequence_value(num)
        case num
        when Integer   then ensure_mod_sequence_value num
        when NUMBER_RE then ensure_mod_sequence_value num.to_i
        else
          raise DataFormatError, "%p is not a valid mod-sequence-value" % [num]
        end
      end

      # Like #ensure_mod_sequence_valzer, but usable with numeric String input.
      def coerce_mod_sequence_valzer(num)
        case num
        when Integer   then ensure_mod_sequence_valzer num
        when NUMBER_RE then ensure_mod_sequence_valzer num.to_i
        else
          raise DataFormatError, "%p is not a valid mod-sequence-valzer" % [num]
        end
      end

    end

  end
end
