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 }