diff --git a/lib/net/imap/command_data.rb b/lib/net/imap/command_data.rb index b2ef5cba..f163a6b1 100644 --- a/lib/net/imap/command_data.rb +++ b/lib/net/imap/command_data.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "date" + require_relative "errors" module Net @@ -21,7 +23,7 @@ def validate_data(data) validate_data(i) end end - when Time + when Time, Date, DateTime when Symbol else data.validate @@ -38,7 +40,9 @@ def send_data(data, tag = nil) send_number_data(data) when Array send_list_data(data, tag) - when Time + when Date + send_date_data(data) + when Time, DateTime send_time_data(data) when Symbol send_symbol_data(data) @@ -101,15 +105,8 @@ def send_list_data(list, tag = nil) put_string(")") end - DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) - - def send_time_data(time) - t = time.dup.gmtime - s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', - t.day, DATE_MONTH[t.month - 1], t.year, - t.hour, t.min, t.sec) - put_string(s) - end + 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) diff --git a/lib/net/imap/data_encoding.rb b/lib/net/imap/data_encoding.rb index fd4f777e..0f6879a2 100644 --- a/lib/net/imap/data_encoding.rb +++ b/lib/net/imap/data_encoding.rb @@ -1,10 +1,50 @@ # frozen_string_literal: true +require "date" + 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 "%d" 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 @@ -35,14 +75,70 @@ def self.encode_utf7(s) }.force_encoding("ASCII-8BIT") end - # Formats +time+ as an IMAP-style date. - def self.format_date(time) - return time.strftime('%d-%b-%Y') + # 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 - # Formats +time+ as an IMAP-style date-time. + # :call-seq: decode_datetime(string) -> DateTime + # + # Decodes +string+ as an IMAP4 formatted "date-time". + # + # Note that double quotes are not optional. See STRFTIME. + def self.decode_datetime(string) + 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) + decode_datetime(string).to_time + 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) - return time.strftime('%d-%b-%Y %H:%M %z') + 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 diff --git a/net-imap.gemspec b/net-imap.gemspec index 4981bd0a..6b791c86 100644 --- a/net-imap.gemspec +++ b/net-imap.gemspec @@ -32,6 +32,8 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "net-protocol" + spec.add_dependency "date" + spec.add_development_dependency "digest" spec.add_development_dependency "strscan" end diff --git a/test/net/imap/test_imap_data_encoding.rb b/test/net/imap/test_imap_data_encoding.rb index 2ca1c182..e7f65a8b 100644 --- a/test/net/imap/test_imap_data_encoding.rb +++ b/test/net/imap/test_imap_data_encoding.rb @@ -31,16 +31,40 @@ def test_decode_utf7 assert_equal(utf8, s) end - def test_format_date - time = Time.mktime(2009, 7, 24) - s = Net::IMAP.format_date(time) - assert_equal("24-Jul-2009", s) + def test_encode_date + assert_equal("24-Jul-2009", Net::IMAP.encode_date(Time.mktime(2009, 7, 24))) + assert_equal("24-Jul-2009", Net::IMAP.format_date(Time.mktime(2009, 7, 24))) + assert_equal("06-Oct-2022", Net::IMAP.encode_date(Date.new(2022, 10, 6))) end - def test_format_datetime - time = Time.mktime(2009, 7, 24, 1, 23, 45) - s = Net::IMAP.format_datetime(time) - assert_match(/\A24-Jul-2009 01:23 [+\-]\d{4}\z/, s) + def test_decode_date + assert_equal Date.new(2022, 10, 6), Net::IMAP.decode_date("06-Oct-2022") + assert_equal Date.new(2022, 10, 6), Net::IMAP.decode_date('"06-Oct-2022"') + assert_equal Date.new(2022, 10, 6), Net::IMAP.parse_date("06-Oct-2022") + end + + def test_encode_datetime + time = Time.new(2009, 7, 24, 1, 3, 5, "+05:00") + assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.encode_datetime(time)) + # assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.format_datetime(time)) + assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.format_time(time)) + assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.encode_time(time)) + end + + def test_decode_datetime + expected = DateTime.new(2022, 10, 6, 1, 2, 3, "-04:00") + actual = Net::IMAP.decode_datetime('"06-Oct-2022 01:02:03 -0400"') + assert_equal expected, actual + actual = Net::IMAP.parse_datetime '" 6-Oct-2022 01:02:03 -0400"' + assert_equal expected, actual + end + + def test_decode_time + expected = DateTime.new(2020, 11, 7, 1, 2, 3, "-04:00").to_time + actual = Net::IMAP.parse_time '"07-Nov-2020 01:02:03 -0400"' + assert_equal expected, actual + actual = Net::IMAP.decode_time '" 7-Nov-2020 01:02:03 -0400"' + assert_equal expected, actual end end