1 /// 2 module imap.searchquery; 3 4 import imap.defines; 5 import imap.socket; 6 import imap.session : Session; 7 import imap.sildoc : SILdoc; 8 9 import core.time : Duration; 10 import std.datetime : Date; 11 12 struct SearchQuery 13 { 14 @SILdoc("not flag if applied inverts the whole query") 15 @("NOT") 16 bool not; 17 18 ImapFlag[] flags; 19 20 @("FROM") 21 string fromContains; 22 23 @("CC") 24 string ccContains; 25 26 @("BCC") 27 string bccContains; 28 29 @("TO") 30 string toContains; 31 32 @("SUBJECT") 33 string subjectContains; 34 35 @("BODY") 36 string bodyContains; 37 38 @("TEXT") 39 string textContains; 40 41 @("BEFORE") 42 Date beforeDate; 43 44 @("HEADER") 45 string[string] headerFieldContains; 46 47 @("KEYWORD") 48 string[] hasKeyword; 49 50 @("SMALLER") 51 ulong smallerThanBytes; 52 53 @("LARGER") 54 ulong largerThanBytes; 55 56 @("NEW") 57 bool isNew; 58 59 @("OLD") 60 bool isOld; 61 62 @("ON") 63 Date onDate; 64 65 @("SENTBEFORE") 66 Date sentBefore; 67 68 @("SENTON") 69 Date sentOn; 70 71 @("SENTSINCE") 72 Date sentSince; 73 74 @("SINCE") 75 Date since; 76 77 @("UID") 78 ulong[] uniqueIdentifiers; 79 80 @("UID") 81 UidRange[] uniqueIdentifierRanges; 82 83 string applyNot(string s) 84 { 85 import std.string : join; 86 87 return not ? ("NOT " ~ s) : s; 88 } 89 90 template isSILdoc(alias T) 91 { 92 enum isSILdoc = is(typeof(T) == SILdoc); 93 } 94 95 void toString(scope void delegate(const(char)[]) sink) 96 { 97 import std.range : dropOne; 98 import std.string : toUpper; 99 import std.conv : to; 100 import std.meta : Filter, templateNot; 101 import std.traits : isFunction; 102 foreach(flag;flags) 103 { 104 sink(applyNot(flag.to!string.dropOne.toUpper)); 105 } 106 static foreach(M; Filter!(templateNot!isFunction,__traits(allMembers,typeof(this)))) 107 {{ 108 enum udas = Filter!(templateNot!isSILdoc,__traits(getAttributes, __traits(getMember,this,M))); 109 static if(udas.length > 0) 110 { 111 alias T = typeof( __traits(getMember,this,M)); 112 enum name = udas[0].to!string; 113 auto v = __traits(getMember,this,M); 114 static if (is(T==string)) 115 { 116 if (v.length > 0) 117 { 118 sink(applyNot(name)); 119 sink(" \""); 120 sink(v); 121 sink("\" "); 122 } 123 } 124 else static if (is(T==Date)) 125 { 126 if (v != Date.init) 127 { 128 sink(applyNot(name)); 129 sink(" "); 130 sink(__traits(getMember,this,M).rfcDate); 131 sink(" "); 132 } 133 } 134 else static if (is(T==bool) && (name != "NOT")) 135 { 136 if (v) 137 { 138 sink(applyNot(name)); 139 sink(" "); 140 } 141 } 142 else static if (is(T==string[string])) 143 { 144 foreach(entry;__traits(getMember,this,M).byKeyValue) 145 { 146 sink(applyNot(name)); 147 sink(" "); 148 sink(entry.key); 149 sink(" \""); 150 sink(entry.value); 151 sink("\" "); 152 } 153 } 154 else static if (is(T==string[])) 155 { 156 foreach(entry;__traits(getMember,this,M)) 157 { 158 sink(applyNot(name)); 159 sink(" \""); 160 sink(entry); 161 sink("\" "); 162 } 163 } 164 else static if (is(T==ulong[])) 165 { 166 if (v.length > 0) 167 { 168 sink(applyNot(name)); 169 sink(" "); 170 auto len = __traits(getMember,this,M).length; 171 foreach(i,entry;__traits(getMember,this,M)) 172 { 173 sink(entry.to!string); 174 if (i != len-1) 175 sink(","); 176 } 177 static if (name == "UID") 178 { 179 if (len > 0 && uniqueIdentifierRanges.length > 0) 180 sink(","); 181 len = uniqueIdentifierRanges.length; 182 foreach(i,entry;uniqueIdentifierRanges) 183 { 184 sink(entry.to!string); 185 if (i != len-1) 186 sink(","); 187 } 188 } 189 } 190 } 191 } 192 }} 193 } 194 } 195 196 @SILdoc(`Generate query string to serch the selected mailbox according to the supplied criteria. 197 This string may be passed to imap.search. 198 199 The searchQueries are ORed together. There is an implicit AND within a searchQuery 200 For NOT, set not within the query to be true - this applies to all the conditions within 201 the query. 202 `) 203 string createQuery(SearchQuery[] searchQueries) 204 { 205 import std.range : chain, repeat; 206 import std.algorithm : map; 207 import std.string : join, strip; 208 import std.conv : to; 209 210 if (searchQueries.length == 0) 211 return "ALL"; 212 213 return chain("OR".repeat(searchQueries.length - 1), 214 searchQueries.map!(q => q.to!string.strip)).join(" ").strip; 215 } 216 217 @SILdoc(`Search selected mailbox according to the supplied search criteria. 218 There is an implicit AND within a searchQuery. For NOT, set not within the query 219 to be true - this applies to all the conditions within the query. 220 `) 221 auto searchQuery(ref Session session, string mailbox, SearchQuery searchQuery, string charset = null) 222 { 223 import imap.namespace : Mailbox; 224 import imap.request; 225 select(session,Mailbox(mailbox)); 226 return search(session,createQuery([searchQuery]),charset); 227 } 228 229 @SILdoc(`Search selected mailbox according to the supplied search criteria. 230 The searchQueries are ORed together. There is an implicit AND within a searchQuery 231 For NOT, set not within the query to be true - this applies to all the conditions within 232 the query. 233 `) 234 auto searchQueries(ref Session session, string mailbox, SearchQuery[] searchQueries, string charset = null) 235 { 236 import imap.namespace : Mailbox; 237 import imap.request; 238 select(session,Mailbox(mailbox)); 239 return search(session,createQuery(searchQueries),charset); 240 } 241 242 @SILdoc("Convert a SIL date to an RFC-2822 / IMAP Date string") 243 string rfcDate(Date date) 244 { 245 import std.format : format; 246 import std.conv : to; 247 import std.string : capitalize; 248 return format!"%02d-%s-%04d"(date.day,date.month.to!string.capitalize,date.year); 249 } 250 251 252 253 struct UidRange 254 { 255 long start = -1; 256 long end = -1; 257 258 string toString() 259 { 260 import std.string: format; 261 import std.conv : to; 262 263 return format!"%s:%s"( 264 (start == -1 ) ? 0 : start, 265 (end == -1) ? "*" : end.to!string 266 ); 267 } 268 }