Skip to content

Commit

Permalink
Fixes date-time formatting, adds decode_datetime
Browse files Browse the repository at this point in the history
`Net::IMAP.format_datetime` was previously using an incorrect
`date-time` format (see the `Net::IMAP::STRFDATE` rdoc).  I don't know
if anyone actually *uses* `format_datetime`, but fixing the bug isn't
backwards compatible.  I implemented the correct behavior under a
different method name (and an alias).  The existing method keeps its
original behavior (for now), with a warning that it will be fixed in a
future version.

Also:
* adds "date" gem as a dependency
* adds `{format,parse}_date` methods.
* updates `send_data`
  * adds `Date` handling.  This can be used by `SEARCH`.
  * uses `encode_datetime` from `send_time_data` (reduces duplication)
* adds aliases
  * `format_*`    => `encode_*`
  * `parse_*`     => `decode_*`
  * `encode_time` => `encode_datetime`
  * but `decode_time` returns a Time object
  • Loading branch information
nevans committed Oct 31, 2022
1 parent c0c9ef3 commit bda56f5
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 24 deletions.
19 changes: 8 additions & 11 deletions lib/net/imap/command_data.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "date"

require_relative "errors"

module Net
Expand All @@ -21,7 +23,7 @@ def validate_data(data)
validate_data(i)
end
end
when Time
when Time, Date, DateTime
when Symbol
else
data.validate
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
106 changes: 101 additions & 5 deletions lib/net/imap/data_encoding.rb
Original file line number Diff line number Diff line change
@@ -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 <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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions net-imap.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 32 additions & 8 deletions test/net/imap/test_imap_data_encoding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit bda56f5

Please sign in to comment.