1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU Lesser General Public License 4 * (version 2.1 or later). See the COPYING file in this distribution. 5 */ 6 7/** 8 * A representations of IMAP's INTERNALDATE field. 9 * 10 * INTERNALDATE's format is 11 * 12 * dd-Mon-yyyy hh:mm:ss +hhmm 13 * 14 * Note that Mon is the standard ''English'' three-letter abbreviation. 15 * 16 * See [[http://tools.ietf.org/html/rfc3501#section-2.3.3]] 17 */ 18 19public class Geary.Imap.InternalDate : Geary.MessageData.AbstractMessageData, Geary.Imap.MessageData, 20 Gee.Hashable<InternalDate>, Gee.Comparable<InternalDate> { 21 // see get_en_us_mon() for explanation 22 private const string[] EN_US_MON = { 23 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 24 }; 25 26 private const string[] EN_US_MON_DOWN = { 27 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" 28 }; 29 30 public DateTime value { get; private set; } 31 public string? original { get; private set; default = null; } 32 33 private InternalDate(string original, DateTime datetime) { 34 this.original = original; 35 value = datetime; 36 } 37 38 public InternalDate.from_date_time(DateTime datetime) throws ImapError { 39 value = datetime; 40 } 41 42 public static InternalDate decode(string internaldate) throws ImapError { 43 if (String.is_empty(internaldate)) 44 throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE: empty string"); 45 46 if (internaldate.length > 64) 47 throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE: too long (%d)", internaldate.length); 48 49 // Alas, GMime.utils_header_decode_date() is too forgiving for our needs, so do it manually 50 int day, year, hour, min, sec; 51 char mon[4] = { 0 }; 52 char tz[6] = { 0 }; 53 int count = internaldate.scanf("%d-%3s-%d %d:%d:%d %5s", out day, mon, out year, out hour, 54 out min, out sec, tz); 55 if (count != 6 && count != 7) 56 throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": too few fields (%d)", internaldate, count); 57 58 // check numerical ranges; this does not verify this is an actual date, DateTime will do 59 // that (and round upward, which has to be accepted) 60 if (!Numeric.int_in_range_inclusive(day, 1, 31) 61 || !Numeric.int_in_range_inclusive(hour, 0, 23) 62 || !Numeric.int_in_range_inclusive(min, 0, 59) 63 || !Numeric.int_in_range_inclusive(sec, 0, 59) 64 || year < 1970) { 65 throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad numerical range", internaldate); 66 } 67 68 // check month (this catches localization problems) 69 int month = -1; 70 string mon_down = Ascii.strdown(((string) mon)); 71 for (int ctr = 0; ctr < EN_US_MON_DOWN.length; ctr++) { 72 if (mon_down == EN_US_MON_DOWN[ctr]) { 73 month = ctr; 74 75 break; 76 } 77 } 78 79 if (month < 0) 80 throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad month", internaldate); 81 82 // TODO: verify timezone 83 84 // if no timezone listed, ISO 8601 says to use local time 85 TimeZone timezone = (tz[0] != '\0') ? new TimeZone((string) tz) : new TimeZone.local(); 86 87 // assemble into DateTime, which validates the time as well (this is why we want to keep 88 // original around, for other reasons) ... month is 1-based in DateTime 89 DateTime datetime = new DateTime(timezone, year, month + 1, day, hour, min, sec); 90 91 return new InternalDate(internaldate, datetime); 92 } 93 94 /** 95 * Returns the {@link InternalDate} as a {@link Parameter}. 96 */ 97 public Parameter to_parameter() { 98 return Parameter.get_for_string(serialize()); 99 } 100 101 /** 102 * Returns the {@link InternalDate} as a {@link Parameter} for a {@link SearchCriterion}. 103 * 104 * @see serialize_for_search 105 */ 106 public Parameter to_search_parameter() { 107 return Parameter.get_for_string(serialize_for_search()); 108 } 109 110 /** 111 * Returns the {@link InternalDate}'s string representation. 112 * 113 * @see serialize_for_search 114 */ 115 public string serialize() { 116 return original ?? value.format("%d-%%s-%Y %H:%M:%S %z").printf(get_en_us_mon()); 117 } 118 119 /** 120 * Returns the {@link InternalDate}'s string representation for a SEARCH command. 121 * 122 * SEARCH does not respect time or timezone, so drop when sending it. See 123 * [[http://tools.ietf.org/html/rfc3501#section-6.4.4]] 124 * 125 * @see serialize 126 * @see SearchCommand 127 */ 128 public string serialize_for_search() { 129 return value.format("%d-%%s-%Y").printf(get_en_us_mon()); 130 } 131 132 /** 133 * Because IMAP's INTERNALDATE strings are ''never'' localized (as best as I can gather), so 134 * need to use en_US appreviated month names, as that's the only value in INTERNALDATE that is 135 * in a language and not a numeric value. 136 */ 137 private string get_en_us_mon() { 138 // month is 1-based inside of DateTime 139 int mon = (value.get_month() - 1).clamp(0, EN_US_MON.length - 1); 140 141 return EN_US_MON[mon]; 142 } 143 144 public uint hash() { 145 return value.hash(); 146 } 147 148 public bool equal_to(InternalDate other) { 149 return value.equal(other.value); 150 } 151 152 public int compare_to(InternalDate other) { 153 return value.compare(other.value); 154 } 155 156 public override string to_string() { 157 return serialize(); 158 } 159} 160 161