Skip to content

Commit bda56f5

Browse files
committed
Fixes date-time formatting, adds decode_datetime
`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
1 parent c0c9ef3 commit bda56f5

File tree

4 files changed

+143
-24
lines changed

4 files changed

+143
-24
lines changed

lib/net/imap/command_data.rb

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require "date"
4+
35
require_relative "errors"
46

57
module Net
@@ -21,7 +23,7 @@ def validate_data(data)
2123
validate_data(i)
2224
end
2325
end
24-
when Time
26+
when Time, Date, DateTime
2527
when Symbol
2628
else
2729
data.validate
@@ -38,7 +40,9 @@ def send_data(data, tag = nil)
3840
send_number_data(data)
3941
when Array
4042
send_list_data(data, tag)
41-
when Time
43+
when Date
44+
send_date_data(data)
45+
when Time, DateTime
4246
send_time_data(data)
4347
when Symbol
4448
send_symbol_data(data)
@@ -101,15 +105,8 @@ def send_list_data(list, tag = nil)
101105
put_string(")")
102106
end
103107

104-
DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
105-
106-
def send_time_data(time)
107-
t = time.dup.gmtime
108-
s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
109-
t.day, DATE_MONTH[t.month - 1], t.year,
110-
t.hour, t.min, t.sec)
111-
put_string(s)
112-
end
108+
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
109+
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
113110

114111
def send_symbol_data(symbol)
115112
put_string("\\" + symbol.to_s)

lib/net/imap/data_encoding.rb

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
11
# frozen_string_literal: true
22

3+
require "date"
4+
35
require_relative "errors"
46

57
module Net
68
class IMAP < Protocol
79

10+
# strftime/strptime format for an IMAP4 +date+, excluding optional dquotes.
11+
# Use via the encode_date and decode_date methods.
12+
#
13+
# date = date-text / DQUOTE date-text DQUOTE
14+
# date-text = date-day "-" date-month "-" date-year
15+
#
16+
# date-day = 1*2DIGIT
17+
# ; Day of month
18+
# date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
19+
# "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
20+
# date-year = 4DIGIT
21+
STRFDATE = "%d-%b-%Y"
22+
23+
# strftime/strptime format for an IMAP4 +date-time+, including dquotes.
24+
# See the encode_datetime and decode_datetime methods.
25+
#
26+
# date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
27+
# SP time SP zone DQUOTE
28+
#
29+
# date-day-fixed = (SP DIGIT) / 2DIGIT
30+
# ; Fixed-format version of date-day
31+
# date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
32+
# "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
33+
# date-year = 4DIGIT
34+
# time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
35+
# ; Hours minutes seconds
36+
# zone = ("+" / "-") 4DIGIT
37+
# ; Signed four-digit value of hhmm representing
38+
# ; hours and minutes east of Greenwich (that is,
39+
# ; the amount that the given time differs from
40+
# ; Universal Time). Subtracting the timezone
41+
# ; from the given time will give the UT form.
42+
# ; The Universal Time zone is "+0000".
43+
#
44+
# Note that Time.strptime <tt>"%d"</tt> flexibly parses either space or zero
45+
# padding. However, the DQUOTEs are *not* optional.
46+
STRFTIME = '"%d-%b-%Y %H:%M:%S %z"'
47+
848
# Decode a string from modified UTF-7 format to UTF-8.
949
#
1050
# UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
@@ -35,14 +75,70 @@ def self.encode_utf7(s)
3575
}.force_encoding("ASCII-8BIT")
3676
end
3777

38-
# Formats +time+ as an IMAP-style date.
39-
def self.format_date(time)
40-
return time.strftime('%d-%b-%Y')
78+
# Formats +time+ as an IMAP4 date.
79+
def self.encode_date(date)
80+
date.to_date.strftime STRFDATE
81+
end
82+
83+
# :call-seq: decode_date(string) -> Date
84+
#
85+
# Decodes +string+ as an IMAP formatted "date".
86+
#
87+
# Double quotes are optional. Day of month may be padded with zero or
88+
# space. See STRFDATE.
89+
def self.decode_date(string)
90+
string = string.delete_prefix('"').delete_suffix('"')
91+
Date.strptime(string, STRFDATE)
92+
end
93+
94+
# :call-seq: encode_datetime(time) -> string
95+
#
96+
# Formats +time+ as an IMAP4 date-time.
97+
def self.encode_datetime(time)
98+
time.to_datetime.strftime STRFTIME
4199
end
42100

43-
# Formats +time+ as an IMAP-style date-time.
101+
# :call-seq: decode_datetime(string) -> DateTime
102+
#
103+
# Decodes +string+ as an IMAP4 formatted "date-time".
104+
#
105+
# Note that double quotes are not optional. See STRFTIME.
106+
def self.decode_datetime(string)
107+
DateTime.strptime(string, STRFTIME)
108+
end
109+
110+
# :call-seq: decode_time(string) -> Time
111+
#
112+
# Decodes +string+ as an IMAP4 formatted "date-time".
113+
#
114+
# Same as +decode_datetime+, but returning a Time instead.
115+
def self.decode_time(string)
116+
decode_datetime(string).to_time
117+
end
118+
119+
class << self
120+
alias encode_time encode_datetime
121+
alias format_date encode_date
122+
alias format_time encode_time
123+
alias parse_date decode_date
124+
alias parse_datetime decode_datetime
125+
alias parse_time decode_time
126+
127+
# alias format_datetime encode_datetime # n.b. this is overridden below...
128+
end
129+
130+
# DEPRECATED:: The original version returned incorrectly formatted strings.
131+
# Strings returned by encode_datetime or format_time use the
132+
# correct IMAP4rev1 syntax for "date-time".
133+
#
134+
# This invalid format has been temporarily retained for backward
135+
# compatibility. A future release will change this method to return the
136+
# correct format.
44137
def self.format_datetime(time)
45-
return time.strftime('%d-%b-%Y %H:%M %z')
138+
warn("#{self}.format_datetime incorrectly formats IMAP date-time. " \
139+
"Convert to #{self}.encode_datetime or #{self}.format_time instead.",
140+
uplevel: 1, category: :deprecated)
141+
time.strftime("%d-%b-%Y %H:%M %z")
46142
end
47143

48144
# Common validators of number and nz_number types

net-imap.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Gem::Specification.new do |spec|
3232
spec.require_paths = ["lib"]
3333

3434
spec.add_dependency "net-protocol"
35+
spec.add_dependency "date"
36+
3537
spec.add_development_dependency "digest"
3638
spec.add_development_dependency "strscan"
3739
end

test/net/imap/test_imap_data_encoding.rb

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,40 @@ def test_decode_utf7
3131
assert_equal(utf8, s)
3232
end
3333

34-
def test_format_date
35-
time = Time.mktime(2009, 7, 24)
36-
s = Net::IMAP.format_date(time)
37-
assert_equal("24-Jul-2009", s)
34+
def test_encode_date
35+
assert_equal("24-Jul-2009", Net::IMAP.encode_date(Time.mktime(2009, 7, 24)))
36+
assert_equal("24-Jul-2009", Net::IMAP.format_date(Time.mktime(2009, 7, 24)))
37+
assert_equal("06-Oct-2022", Net::IMAP.encode_date(Date.new(2022, 10, 6)))
3838
end
3939

40-
def test_format_datetime
41-
time = Time.mktime(2009, 7, 24, 1, 23, 45)
42-
s = Net::IMAP.format_datetime(time)
43-
assert_match(/\A24-Jul-2009 01:23 [+\-]\d{4}\z/, s)
40+
def test_decode_date
41+
assert_equal Date.new(2022, 10, 6), Net::IMAP.decode_date("06-Oct-2022")
42+
assert_equal Date.new(2022, 10, 6), Net::IMAP.decode_date('"06-Oct-2022"')
43+
assert_equal Date.new(2022, 10, 6), Net::IMAP.parse_date("06-Oct-2022")
44+
end
45+
46+
def test_encode_datetime
47+
time = Time.new(2009, 7, 24, 1, 3, 5, "+05:00")
48+
assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.encode_datetime(time))
49+
# assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.format_datetime(time))
50+
assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.format_time(time))
51+
assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.encode_time(time))
52+
end
53+
54+
def test_decode_datetime
55+
expected = DateTime.new(2022, 10, 6, 1, 2, 3, "-04:00")
56+
actual = Net::IMAP.decode_datetime('"06-Oct-2022 01:02:03 -0400"')
57+
assert_equal expected, actual
58+
actual = Net::IMAP.parse_datetime '" 6-Oct-2022 01:02:03 -0400"'
59+
assert_equal expected, actual
60+
end
61+
62+
def test_decode_time
63+
expected = DateTime.new(2020, 11, 7, 1, 2, 3, "-04:00").to_time
64+
actual = Net::IMAP.parse_time '"07-Nov-2020 01:02:03 -0400"'
65+
assert_equal expected, actual
66+
actual = Net::IMAP.decode_time '" 7-Nov-2020 01:02:03 -0400"'
67+
assert_equal expected, actual
4468
end
4569

4670
end

0 commit comments

Comments
 (0)