1 module jmap.types;
2 import std.datetime : SysTime;
3 import core.time : seconds;
4 import std.typecons : Nullable;
5 
6 version(SIL):
7 import kaleidic.sil.lang.types : Variable,Function,SILdoc;
8 import kaleidic.sil.lang.json : toVariable, toJsonString;
9 import std.datetime : DateTime;
10 import asdf;
11 
12 struct Credentials
13 {
14 	string user;
15 	string pass;
16 }
17 
18 alias Url = string;
19 alias Emailer = string;
20 alias Attachment = ubyte[];
21 alias ModSeq = ulong;
22 
23 alias Set = string[bool];
24 
25 @("urn:ietf:params:jmap:core")
26 struct SessionCoreCapabilities
27 {
28 	uint maxSizeUpload;
29 	uint maxConcurrentUpload;	
30 	uint maxSizeRequest;
31 	uint maxConcurrentRequests;
32 	uint maxCallsInRequest;
33 	uint maxObjectsInGet;
34 	uint maxObjectsInSet;
35 	string[] collationAlgorithms;
36 }
37 																															    
38 															 
39 @("urn:ietf:params:jmap:mail")
40 enum EmailQuerySortOption
41 {
42 	receivedAt,
43 	from,
44 	to,
45 	subject,
46 	size,
47 
48 	@serializationKeys("header.x-spam-score")
49 	headerXSpamScore,
50 }
51 
52 struct AccountParams
53 {
54 	//EmailQuerySortOption[] emailQuerySortOptions;
55 	string[] emailQuerySortOptions;
56 	Nullable!int maxMailboxDepth;
57 	Nullable!int maxMailboxesPerEmail;
58 	Nullable!int maxSizeAttachmentsPerEmail;
59 	Nullable!int maxSizeMailboxName;
60 	bool mayCreateTopLevelMailbox;
61 }
62 
63 struct SubmissionParams
64 {
65    int maxDelayedSend;
66    string[] submissionExtensions;
67 }
68 
69 struct AccountCapabilities
70 {
71 	@serializationKeys("urn:ietf:params:jmap:mail")
72 	AccountParams accountParams;
73 
74 	@serializationKeys("urn:ietf:params:jmap:submission")
75 	SubmissionParams submissionParams;
76 
77 	//@serializationIgnoreIn Asdf vacationResponseParams;
78 
79 	version(SIL)
80 	{
81 		@serializationIgnoreIn Variable[string] allAccountCapabilities;
82 
83 		void finalizeDeserialization(Asdf data)
84 		{
85 			import asdf : deserialize, Asdf;
86 
87 			foreach(el;data.byKeyValue)
88 				allAccountCapabilities[el.key] = el.value.get!Asdf(Asdf.init).toVariable;
89 		}
90 	}
91 }
92 
93 struct Account
94 {
95 	string name;
96 	bool isPersonal;
97 	bool isReadOnly;
98 
99 	bool isArchiveUser = false;
100 	AccountCapabilities accountCapabilities;
101 	string[string] primaryAccounts;
102 }
103 
104 struct Session
105 {
106 	SessionCoreCapabilities coreCapabilities;
107 
108 	Account[string] accounts;
109 	string[string] primaryAccounts;
110 	string username;
111 	Url apiUrl;
112 	Url downloadUrl;
113 	Url uploadUrl;
114 	Url eventSourceUrl;
115 	string state;
116 	@serializationIgnoreIn Asdf[string] capabilities;
117 	package Credentials credentials;
118 	private string activeAccountId_;
119 	private bool debugMode = false;
120 
121 	void setDebug(bool debugMode = true)
122 	{
123 		this.debugMode = debugMode;
124 	}
125 
126 	private string activeAccountId()
127 	{
128 		import std.format : format;
129 		import std.exception : enforce;
130 		import std.string : join;
131 		import std.range : front;
132 		import std.algorithm : canFind;
133 
134 		if (activeAccountId_.length == 0)
135 		{
136 			enforce(accounts.keys.length == 1,
137 					format!"multiple accounts - [%s] - and you must call setActiveAccount to pick one"
138 						(accounts.keys.join(",")));
139 			this.activeAccountId_ = accounts.keys.front;
140 		}
141 
142 		enforce(accounts.keys.canFind(activeAccountId_,
143 					format!"active account ID is set to %s but it is not found amongst account IDs: [%s]"
144 						(activeAccountId_, accounts.keys.join(","))));
145 		return activeAccountId_;
146 	}
147 
148 	string[] listCapabilities()
149 	{
150 		return capabilities.keys;
151 	}
152 
153 	string[] listAccounts()
154 	{
155 		import std.algorithm : map;
156 		import std.array : array;
157 		return accounts.keys.map!(key => accounts[key].name).array;
158 	}
159 
160 	Account getActiveAccountInfo()
161 	{
162 		import std.exception : enforce;
163 		auto p = activeAccountId() in accounts;
164 		enforce(p !is null, "no currently active account");
165 		return *p;
166 	}
167 
168 	@SILdoc("set active account - name is the account name, not the id")
169 	Session setActiveAccount(string name)
170 	{
171 		import std.format : format;
172 		import std.exception : enforce;
173 		import std.format : format;
174 
175 		foreach(kv; accounts.byKeyValue)
176 		{
177 			if (kv.value.name == name)
178 			{
179 				this.activeAccountId_ = kv.key;
180 				return this;
181 			}
182 		}
183 		throw new Exception(format!"account %s not found"(name));
184 	}
185 
186 
187 	void finalizeDeserialization(Asdf data)
188 	{
189 		import asdf : deserialize, Asdf;
190 
191 		foreach(el;data["capabilities"].byKeyValue)
192 			capabilities[el.key] = el.value.get!Asdf(Asdf.init);
193 		this.coreCapabilities = deserialize!SessionCoreCapabilities(capabilities["urn:ietf:params:jmap:core"]);
194 	}
195 
196 	private Asdf post(JmapRequest request)
197 	{
198 		import asdf;
199 	    import requests : Request, BasicAuthentication;
200 		import std.string : strip;
201 		import std.stdio : writefln, stderr;
202 		auto json = serializeToJsonPretty(request); // serializeToJsonPretty
203 		if (debugMode)
204 			stderr.writefln("post request to apiUrl (%s) with data: %s",apiUrl,json);
205 	    auto req = Request();
206 		req.timeout = 3 * 60.seconds;
207 	    req.authenticator = new BasicAuthentication(credentials.user,credentials.pass);
208 	    auto result = (cast(string) req.post(apiUrl, json,"application/json").responseBody.data.idup).strip;
209 		if (debugMode)
210 			stderr.writefln("response: %s",result);
211 	    return (result.length == 0) ? Asdf.init : parseJson(result);
212 	}
213 
214 	version(SIL)
215 	{
216 		Variable uploadBinary(string data, string type = "application/binary")
217 		{
218 			import std.string : replace;
219 			import asdf;
220 			import requests : Request, BasicAuthentication;
221 			auto uri = this.uploadUrl.replace("{accountId}",this.activeAccountId());
222 			auto req = Request();
223 			req.authenticator = new BasicAuthentication(credentials.user,credentials.pass);
224 			auto result = cast(string) req.post(uploadUrl, data,type).responseBody.data.idup;
225 			return parseJson(result).toVariable;
226 		}
227 	}
228 	else
229 	{
230 		Asdf uploadBinary(string data, string type = "application/binary")
231 		{
232 			import std.string : replace;
233 			import asdf;
234 			import requests : Request, BasicAuthentication;
235 			auto uri = this.uploadUrl.replace("{accountId}",this.activeAccountId());
236 			auto req = Request();
237 			req.authenticator = new BasicAuthentication(credentials.user,credentials.pass);
238 			auto result = cast(string) req.post(uploadUrl, data,type).responseBody.data.idup;
239 			return parseJson(result);
240 		}
241 	}
242 
243 
244 	string downloadBinary(string blobId, string type = "application/binary", string name = "default.bin", string downloadUrl=null)
245 	{
246 		import std.string : replace;
247 		import asdf;
248 	    import requests : Request, BasicAuthentication;
249 		import std.algorithm : canFind;
250 
251 		downloadUrl = (downloadUrl.length == 0) ? this.downloadUrl : downloadUrl;
252 		downloadUrl = downloadUrl
253 						.replace("{accountId}",this.activeAccountId().uriEncode)
254 						.replace("{blobId}",blobId.uriEncode)
255 						.replace("{type}",type.uriEncode)
256 						.replace("{name}",name.uriEncode);
257 
258 		downloadUrl = downloadUrl ~  "&accept=" ~ type.uriEncode;
259 	    auto req = Request();
260 	    req.authenticator = new BasicAuthentication(credentials.user,credentials.pass);
261 	    auto result = cast(string) req.get(downloadUrl).responseBody.data.idup;
262 		return result;
263 	}
264 	
265 
266 
267 	version(SIL)
268 	{
269 		Variable get(string type, string[] ids, Variable properties = Variable.init, Variable[string] additionalArguments = (Variable[string]).init)
270 		{
271 			return getRaw(type,ids,properties, additionalArguments).toVariable;
272 		}
273 
274 		Asdf getRaw(string type, string[] ids, Variable properties = Variable.init, Variable[string] additionalArguments = (Variable[string]).init)
275 		{
276 			import std.algorithm : map;
277 			import std.array : array;
278 			import std.stdio : stderr, writefln;
279 			auto invocationId = "12345678";
280 			if (debugMode)
281 				stderr.writefln("props: %s", toJsonString(properties));
282 			auto props =  parseJson(toJsonString(properties));
283 			auto invocation = Invocation.get(type,activeAccountId(), invocationId,ids, props,additionalArguments);
284 			auto request = JmapRequest(listCapabilities(),[invocation],null);
285 			return post(request);
286 		}
287 	}
288 
289 
290 	Mailbox[] getMailboxes()
291 	{
292 		import std.range : front, dropOne;
293 		auto asdf = getRaw("Mailbox",null);
294 		return deserialize!(Mailbox[])(asdf["methodResponses"].byElement.front.byElement.dropOne.front["list"]);
295 	}
296 
297 	Variable getContact(string[] ids, Variable properties = Variable([]), Variable[string] additionalArguments =(Variable[string]).init)
298 	{
299 		import std.range : front, dropOne;
300 		return Variable(
301 				this.get("Contact",ids,properties, additionalArguments)
302 				.get!(Variable[string])
303 				["methodResponses"]
304 				.get!(Variable[])
305 				.front
306 				.get!(Variable[])
307 				.front
308 				.get!(Variable[])
309 				.dropOne
310 				.front
311 		);
312 	}
313 
314 	Variable getEmails(string[] ids, Variable properties = Variable([ "id", "blobId", "threadId", "mailboxIds", "keywords", "size", "receivedAt", "messageId", "inReplyTo", "references", "sender", "from", "to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment", "preview", "bodyValues", "textBody", "htmlBody", "attachments" ]), Variable bodyProperties = Variable(["all"]),
315 			bool fetchTextBodyValues = true, bool fetchHTMLBodyValues = true, bool fetchAllBodyValues = true)
316 	{
317 		import std.range : front, dropOne;
318 		return Variable(
319 				this.get(
320 					"Email",ids,properties, [
321 						"bodyProperties":bodyProperties,
322 						"fetchTextBodyValues":fetchTextBodyValues.Variable,
323 						"fetchAllBodyValues":fetchAllBodyValues.Variable,
324 						"fetchHTMLBodyValues": fetchHTMLBodyValues.Variable,
325 					]
326 				)
327 				.get!(Variable[string])
328 				["methodResponses"] //,(Variable[]).init)
329 				.get!(Variable[])
330 				.front
331 				.get!(Variable[])
332 				.dropOne
333 				.front
334 				.get!(Variable[string])
335 				["list"]
336 		);
337 	}
338 
339 
340 	Asdf changesRaw(string type, string sinceState, Nullable!uint maxChanges = (Nullable!uint).init, Variable[string] additionalArguments = (Variable[string]).init)
341 	{
342 		import std.algorithm : map;
343 		import std.array : array;
344 		auto invocationId = "12345678";
345 		auto invocation = Invocation.changes(type,activeAccountId(), invocationId,sinceState,maxChanges,additionalArguments);
346 		auto request = JmapRequest(listCapabilities(),[invocation],null);
347 		return post(request);
348 	}
349 
350 	Variable changes(string type, string sinceState, Nullable!uint maxChanges = (Nullable!uint).init, Variable[string] additionalArguments = null)
351 	{
352 		return changesRaw(type,sinceState,maxChanges,additionalArguments).toVariable;
353 	}
354 
355 	Asdf setRaw(string type, string ifInState = null, Variable[string] create = null, Variable[string][string] update = null, string[] destroy_ = null, Variable[string] additionalArguments = (Variable[string]).init)
356 	{
357 		import std.algorithm : map;
358 		import std.array : array;
359 		auto invocationId = "12345678";
360 		auto createAsdf = parseJson(toJsonString(Variable(create)));
361 		auto updateAsdf = parseJson(toJsonString(Variable(update)));
362 		auto invocation = Invocation.set(type,activeAccountId(), invocationId,ifInState,createAsdf,updateAsdf,destroy_,additionalArguments);
363 		auto request = JmapRequest(listCapabilities(),[invocation],null);
364 		return post(request);
365 	}
366 
367 	Variable set(string type, string ifInState = null, Variable[string] create = null, Variable[string][string] update = null, string[] destroy_ = null, Variable[string] additionalArguments = (Variable[string]).init)
368 	{
369 		return setRaw(type,ifInState,create,update,destroy_,additionalArguments).toVariable;
370 	}
371 
372 	Variable setEmail(string ifInState = null, Variable[string] create = null, Variable[string][string] update = null, string[] destroy_ = null, Variable[string] additionalArguments = (Variable[string]).init)
373 	{
374 		return set("Email",ifInState,create,update,destroy_,additionalArguments);
375 	}
376 
377 
378 	Asdf copyRaw(string type, string fromAccountId, string ifFromInState = null, string ifInState = null, Variable[string] create = null, bool onSuccessDestroyOriginal = false, string destroyFromIfInState = null, Variable[string] additionalArguments = (Variable[string]).init)
379 	{
380 		import std.algorithm : map;
381 		import std.array : array;
382 		auto invocationId = "12345678";
383 		auto createAsdf = parseJson(toJsonString(Variable(create)));
384 		auto invocation = Invocation.copy(type,fromAccountId, invocationId,ifFromInState,activeAccountId,ifInState,createAsdf,onSuccessDestroyOriginal,destroyFromIfInState);
385 		auto request = JmapRequest(listCapabilities(),[invocation],null);
386 		return post(request);
387 	}
388 
389 	Variable copy(string type, string fromAccountId, string ifFromInState = null, string ifInState = null, Variable[string] create = null, bool onSuccessDestroyOriginal = false, string destroyFromIfInState = null, Variable[string] additionalArguments = (Variable[string]).init)
390 	{
391 		return copyRaw(type,fromAccountId,ifFromInState,ifInState,create,onSuccessDestroyOriginal,destroyFromIfInState,additionalArguments).toVariable;
392 	}
393 
394 
395 	Asdf queryRaw(string type, Variable filter, Variable sort, int position, string anchor=null, int anchorOffset = 0, Nullable!uint limit = (Nullable!uint).init, bool calculateTotal = false, Variable[string] additionalArguments = (Variable[string]).init)
396 	{
397 		import std.algorithm : map;
398 		import std.array : array;
399 		auto invocationId = "12345678";
400 		auto filterAsdf = parseJson(toJsonString(filter));
401 		auto sortAsdf = parseJson(toJsonString(sort));
402 		auto invocation = Invocation.query(type,activeAccountId,invocationId,filterAsdf,sortAsdf,position,anchor,anchorOffset,limit,calculateTotal, additionalArguments);
403 		auto request = JmapRequest(listCapabilities(),[invocation],null);
404 		return post(request);
405 	}
406 
407 	Variable queryEmails(Filter filter, Variable sort, int position = 0, string anchor = "", int anchorOffset = 0, Nullable!uint limit = (Nullable!uint).init, bool calculateTotal = false, bool collapseThreads = false, Variable[string] additionalArguments = (Variable[string]).init)
408 	{
409 		import std.exception : enforce;
410 		import std.stdio: stderr,writeln;
411 		if (collapseThreads)
412 			additionalArguments["collapseThreads"] = Variable(true);
413 		auto o = cast(FilterOperator) filter;
414 		auto c = cast(FilterCondition) filter;
415 		enforce(o !is null || c !is null, "filter must be either an operator or a condition");
416 		if (debugMode)
417 			stderr.writeln((o !is null)? serializeToJsonPretty(o) : serializeToJsonPretty(c));
418 		Variable filterVariable = (o !is null) ? parseJson(serializeToJson(o)).toVariable : parseJson(serializeToJson(c)).toVariable;
419 		return queryRaw("Email", filterVariable, sort, position, anchor, anchorOffset, limit,calculateTotal,additionalArguments).toVariable;
420 	}
421 
422 	Variable query(string type, Variable filter, Variable sort, int position, string anchor, int anchorOffset = 0, Nullable!uint limit = (Nullable!uint).init, bool calculateTotal = false, Variable[string] additionalArguments = (Variable[string]).init)
423 	{
424 		return queryRaw(type, filter, sort, position, anchor, anchorOffset, limit,calculateTotal,additionalArguments).toVariable;
425 	}
426 
427 	package Asdf queryChangesRaw(string type, Variable filter, Variable sort, string sinceQueryState, Nullable!uint maxChanges = (Nullable!uint).init, Nullable!string upToId = (Nullable!string).init, bool calculateTotal = false, Variable[string] additionalArguments = (Variable[string]).init)
428 	{
429 		import std.algorithm : map;
430 		import std.array : array;
431 		auto invocationId = "12345678";
432 		auto filterAsdf = parseJson(toJsonString(filter));
433 		auto sortAsdf = parseJson(toJsonString(sort));
434 		auto invocation = Invocation.queryChanges(type,activeAccountId,invocationId,filterAsdf,sortAsdf,sinceQueryState,maxChanges,upToId,calculateTotal,additionalArguments);
435 		auto request = JmapRequest(listCapabilities(),[invocation],null);
436 		return post(request);
437 	}
438 
439 	package Variable queryChanges(string type, Variable filter, Variable sort, string sinceQueryState,Nullable!uint maxChanges = (Nullable!uint).init, Nullable!string upToId = (Nullable!string).init, bool calculateTotal = false, Variable[string] additionalArguments = (Variable[string]).init)
440 	{
441 		return queryChangesRaw(type, filter, sort, sinceQueryState,maxChanges,upToId,calculateTotal,additionalArguments).toVariable;
442 	}
443 }
444 
445 struct Email
446 {
447 	string id;
448 	string blobId;
449 	string threadId;
450 	Set mailboxIds;
451 	Set keywords;
452 	Emailer[] from;
453 	Emailer[] to;
454 	string subject;
455 	SysTime date;
456 	int size;
457 	string preview;
458 	Attachment[] attachments;
459 
460 	ModSeq createdModSeq;
461 	ModSeq updatedModSeq;
462 	Nullable!SysTime deleted;
463 }
464 
465 enum EmailProperty
466 {
467 	id,
468 	blobId,
469 	threadId,
470 	mailboxIds,
471 	keywords,
472 	size,
473 	receivedAt,
474 	messageId,
475 	headers,
476 	inReplyTo,
477 	references,
478 	sender,
479 	from,
480 	to,
481 	cc,
482 	bcc,
483 	replyTo,
484 	subject,
485 	sentAt,
486 	hasAttachment,
487 	preview,
488 	bodyValues,
489 	textBody,
490 	htmlBody,
491 	attachments,
492 	//Raw,
493 	//Text,
494 	//Addresses,
495 	//GroupedAddresses,
496 	//URLs,
497 }
498 
499 enum EmailBodyProperty
500 {
501 	partId,
502 	blobId,
503 	size,
504 	name,
505 	type,
506 	charset,
507 	disposition,
508 	cid,
509 	language,
510 	location,
511 	subParts,
512 	bodyStructure,
513 	bodyValues,
514 	textBody,
515 	htmlBody,
516 	attachments,
517 	hasAttachment,
518 	preview,
519 }
520 
521 struct EmailSubmission
522 {
523 	string id;
524 	string identityId;
525 	string emailId;
526 	string threadId;
527 	Nullable!Envelope envelope;
528 	DateTime sendAt;
529 	string undoStatus;
530 	string deliveryStatus;
531 	string[] dsnBlobIds;
532 	string[] mdnBlobIds;
533 }
534 
535 
536 struct Envelope
537 {
538 	EmailAddress mailFrom;
539 
540 	EmailAddress rcptTo;
541 }
542 
543 struct EmailAddress
544 {
545 	string email;
546 	Nullable!(Variable[string]) parameters;
547 }
548 
549 struct ThreadEmail
550 {
551 	string id;
552 	string[] mailboxIds;
553 	bool isUnread;
554 	bool isFlagged;
555 }
556 
557 struct Thread
558 {
559 	string id;
560 	ThreadEmail[] emails;
561 	ModSeq createdModSeq;
562 	ModSeq updatedModSeq;
563 	Nullable!SysTime deleted;
564 }
565 
566 struct MailboxRights
567 {
568 	bool mayReadItems;
569 	bool mayAddItems;
570 	bool mayRemoveItems;
571 	bool mayCreateChild;
572 	bool mayRename;
573 	bool mayDelete;
574 	bool maySetKeywords;
575 	bool maySubmit;
576 	bool mayAdmin;
577 	bool maySetSeen;
578 }
579 
580 struct Mailbox
581 {
582 	string id;
583 	string name;
584 	string parentId;
585 	string role;
586 	int sortOrder;
587 	int totalEmails;
588 	int unreadEmails;
589 	int totalThreads;
590 	int unreadThreads;
591 	MailboxRights myRights;
592 	bool autoPurge;
593 	int hidden;
594 	string identityRef;
595 	bool learnAsSpam;
596 	int purgeOlderThanDays;
597 	bool isCollapsed;
598 	bool isSubscribed;
599 	bool suppressDuplicates;
600 	bool autoLearn;
601 	MailboxSortProperty[] sort;
602 }
603 
604 string[] allMailboxPaths(Mailbox[] mailboxes)
605 {
606 	import std.algorithm : map;
607 	import std.array : array;
608 	return mailboxes.map!(mb => mailboxPath(mailboxes,mb.id)).array;
609 }
610 
611 string mailboxPath(Mailbox[] mailboxes, string id, string path = null)
612 {
613 	import std.algorithm : countUntil;
614 	import std.format : format;
615 	import std.exception : enforce;
616 	import std.string : endsWith;
617 	if (path.endsWith("/"))
618 		path = path[0..$-1];
619 	auto i = mailboxes.countUntil!(mailbox => mailbox.id == id);
620 	if (i == -1)
621 		return path;
622 	path = (path == null) ? mailboxes[i].name : format!"%s/%s"(mailboxes[i].name,path);
623 	return mailboxPath(mailboxes,mailboxes[i].parentId,path);
624 }
625 
626 Nullable!Mailbox findMailboxPath(Mailbox[] mailboxes, string path)
627 {
628 	import std.algorithm : filter;
629 	import std.string : split, join, endsWith;
630 	import std.range : back;
631 	import std.exception : enforce;
632 
633 	Nullable!Mailbox ret;
634 	if (path.endsWith("/"))
635 		path = path[0..$-1];
636 	auto cols = path.split("/");
637 	if (cols.length == 0)
638 		return ret;
639 
640 	foreach(item; mailboxes.filter!(mailbox => mailbox.name == cols[$-1]))
641 	{
642 		if (item.parentId.length == 0)
643 		{
644 			if (cols.length <= 1)
645 			{
646 				ret = item;
647 				break;
648 			}
649 			else continue;
650 		}
651 		auto parent = findMailboxPath(mailboxes,cols[0..$-1].join("/"));
652 		if (parent.isNull)
653 		{
654 			continue;
655 		}
656 		else
657 		{
658 			ret = item;
659 			break;
660 		}
661 	}
662 	return ret;
663 }
664 
665 struct MailboxSortProperty
666 {
667 	string property;
668 	bool isAscending;
669 }
670 
671 
672 struct MailboxEmailList
673 {
674 	string id;
675 	string messageId;
676 	string threadId;
677 	ModSeq updatedModSeq;
678 	SysTime created;
679 	Nullable!SysTime deleted;
680 }
681 
682 struct EmailChangeLogEntry
683 {
684 	string id;
685 	string[] created;
686 	string[] updated;
687 	string[] destroyed;
688 }
689 
690 struct ThreadChangeLogEntry
691 {
692 	string id;
693 	string[] created;
694 	string[] updated;
695 	string[] destroyed;
696 }
697 
698 struct ThreadRef
699 {
700 	string id;
701 	string threadId;
702 	SysTime lastSeen;
703 }
704 
705 struct HighLowModSeqCache
706 {
707 	ModSeq highModSeq;
708 	ModSeq highModSeqEmail;
709 	ModSeq highModSeqThread;
710 	ModSeq lowModSeqEmail;
711 	ModSeq lowModSeqThread;
712 	ModSeq lowModSeqMailbox;
713 }
714 
715 /+
716 {
717    "accounts" : {
718       "u1f4140ae" : {
719          "accountCapabilities" : {
720             "urn:ietf:params:jmap:mail" : {
721                "emailQuerySortOptions" : [
722                   "receivedAt",
723                   "from",
724                   "to",
725                   "subject",
726                   "size",
727                   "header.x-spam-score"
728                ],
729                "maxMailboxDepth" : null,
730                "maxMailboxesPerEmail" : 1000,
731                "maxSizeAttachmentsPerEmail" : 50000000,
732                "maxSizeMailboxName" : 490,
733                "mayCreateTopLevelMailbox" : true
734             },
735             "urn:ietf:params:jmap:submission" : {
736                "maxDelayedSend" : 44236800,
737                "submissionExtensions" : []
738             },
739             "urn:ietf:params:jmap:vacationresponse" : {}
740          },
741          "isArchiveUser" : false,
742          "isPersonal" : true,
743          "isReadOnly" : false,
744          "name" : "laeeth@kaleidic.io"
745       }
746    },
747    "apiUrl" : "https://jmap.fastmail.com/api/",
748    "capabilities" : {
749       "urn:ietf:params:jmap:core" : {
750          "collationAlgorithms" : [
751             "i;ascii-numeric",
752             "i;ascii-casemap",
753             "i;octet"
754          ],
755          "maxCallsInRequest" : 64,
756          "maxConcurrentRequests" : 10,
757          "maxConcurrentUpload" : 10,
758          "maxObjectsInGet" : 1000,
759          "maxObjectsInSet" : 1000,
760          "maxSizeRequest" : 10000000,
761          "maxSizeUpload" : 250000000
762       },
763       "urn:ietf:params:jmap:mail" : {},
764       "urn:ietf:params:jmap:submission" : {},
765       "urn:ietf:params:jmap:vacationresponse" : {}
766    },
767    "downloadUrl" : "https://jmap.fastmail.com/download/{accountId}/{blobId}/{name}",
768    "eventSourceUrl" : "https://jmap.fastmail.com/event/",
769    "primaryAccounts" : {
770       "urn:ietf:params:jmap:mail" : "u1f4140ae",
771       "urn:ietf:params:jmap:submission" : "u1f4140ae",
772       "urn:ietf:params:jmap:vacationresponse" : "u1f4140ae"
773    },
774    "state" : "cyrus-12046746;p-5;vfs-0",
775    "uploadUrl" : "https://jmap.fastmail.com/upload/{accountId}/",
776    "username" : "laeeth@kaleidic.io"
777 }
778 +/
779 
780 void serializeAsdf(S)(ref S ser, AsdfNode node) pure
781 {
782 	if (node.isLeaf())
783 		serializeAsdf(ser,node.data);
784 
785 	auto objState = ser.objectBegin();
786 	foreach(kv;node.children.byKeyValue)
787 	{
788 		ser.putKey(kv.key);
789 		serializeAsdf(ser,kv.value);
790 	}
791 	ser.objectEnd(objState);
792 }
793 
794 void serializeAsdf(S)(ref S ser, Asdf el) pure
795 {
796 	final switch(el.kind)
797 	{
798 		case Asdf.Kind.null_:
799 			ser.putValue(null);
800 			break;
801 
802 		case Asdf.Kind.true_:
803 			ser.putValue(true);
804 			break;
805 
806 		case Asdf.Kind.false_:
807 			ser.putValue(false);
808 			break;
809 
810 		case Asdf.Kind.number:
811 			ser.putValue(el.get!double(double.nan));
812 			break;
813 
814 		case Asdf.Kind..string:
815 			ser.putValue(el.get!string(null));
816 			break;
817 
818 		case Asdf.Kind.array:
819 			auto arrayState = ser.arrayBegin();
820 			foreach(arrEl;el.byElement)
821 			{
822 				ser.elemBegin();
823 				serializeAsdf(ser,arrEl);
824 			}
825 			ser.arrayEnd(arrayState);
826 			break;
827 
828 		case Asdf.Kind.object:
829 			auto objState = ser.objectBegin();
830 			foreach(kv;el.byKeyValue)
831 			{
832 				ser.putKey(kv.key);
833 				serializeAsdf(ser,kv.value);
834 			}
835 			ser.objectEnd(objState);
836 			break;
837 	}
838 }
839 
840 
841 
842 struct Invocation
843 {
844 	string name;
845 	Asdf arguments;
846 	string id;
847 
848 	void serialize(S)(ref S ser) pure
849 	{
850 		auto outerState = ser.arrayBegin();
851 		ser.elemBegin();
852 		ser.putValue(name);
853 		ser.elemBegin();
854 		auto state = ser.objectBegin();
855 		foreach(el;arguments.byKeyValue)
856 		{
857 			ser.putKey(el.key);
858 			serializeAsdf(ser,el.value);
859 		}
860 		ser.objectEnd(state);
861 		ser.elemBegin();
862 		ser.putValue(id);
863 		ser.arrayEnd(outerState);
864 	}
865 
866 
867 	static Invocation get(string type, string accountId, string invocationId = null, string[] ids = null, Asdf properties = Asdf.init, Variable[string] additionalArguments = (Variable[string]).init)
868 	{
869 		auto arguments = AsdfNode("{}".parseJson);
870 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
871 		arguments["ids"] = AsdfNode(ids.serializeToAsdf);
872 		arguments["properties"] = AsdfNode(properties);
873 		foreach(kv;additionalArguments.byKeyValue)
874 			arguments[kv.key] = AsdfNode(kv.value.serializeToAsdf);
875 
876 		Invocation ret = {
877 			name: type ~ "/get",
878 			arguments: cast(Asdf) arguments,
879 			id: invocationId,
880 		};
881 		return ret;
882 	}
883 
884 	static Invocation changes(string type, string accountId, string invocationId, string sinceState, Nullable!uint maxChanges, Variable[string] additionalArguments = (Variable[string]).init)
885 	{
886 		auto arguments = AsdfNode("{}".parseJson);
887 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
888 		arguments["sinceState"] = AsdfNode(sinceState.serializeToAsdf);
889 		arguments["maxChanges"] = AsdfNode(maxChanges.serializeToAsdf);
890 		foreach(kv;additionalArguments.byKeyValue)
891 			arguments[kv.key] = AsdfNode(kv.value.serializeToAsdf);
892 
893 		Invocation ret = {
894 			name: type ~ "/changes",
895 			arguments: cast(Asdf) arguments,
896 			id: invocationId,
897 		};
898 		return ret;
899 	}
900 
901 
902 	static Invocation set(string type, string accountId, string invocationId = null, string ifInState = null, Asdf create = Asdf.init, Asdf update = Asdf.init, string[] destroy_ = null, Variable[string] additionalArguments = (Variable[string]).init)
903 	{
904 		auto arguments = AsdfNode("{}".parseJson);
905 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
906 		arguments["ifInState"] = AsdfNode(ifInState.serializeToAsdf);
907 		arguments["create"] = AsdfNode(create);
908 		arguments["update"] = AsdfNode(update);
909 		arguments["destroy"] = AsdfNode(destroy_.serializeToAsdf);
910 		foreach(kv;additionalArguments.byKeyValue)
911 			arguments[kv.key] = AsdfNode(kv.value.serializeToAsdf);
912 
913 		Invocation ret = {
914 			name: type ~ "/set",
915 			arguments: cast(Asdf) arguments,
916 			id: invocationId,
917 		};
918 		return ret;
919 	}
920 
921 	static Invocation copy(string type, string fromAccountId, string invocationId = null, string ifFromInState = null, string accountId = null, string ifInState = null, Asdf create = Asdf.init, bool onSuccessDestroyOriginal = false, string destroyFromIfInState = null, Variable[string] additionalArguments = (Variable[string]).init)
922 	{
923 		auto arguments = AsdfNode("{}".parseJson);
924 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
925 		arguments["fromAccountId"] =AsdfNode(fromAccountId.serializeToAsdf);
926 		arguments["ifFromInState"] = AsdfNode(ifFromInState.serializeToAsdf);
927 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
928 		arguments["ifInState"] = AsdfNode(ifInState.serializeToAsdf);
929 		arguments["create"] = AsdfNode(create);
930 		arguments["onSuccessDestroyOriginal"] = AsdfNode(onSuccessDestroyOriginal.serializeToAsdf);
931 		arguments["destroyFromIfInState"] = AsdfNode(destroyFromIfInState.serializeToAsdf);
932 		foreach(kv;additionalArguments.byKeyValue)
933 			arguments[kv.key] = AsdfNode(kv.value.serializeToAsdf);
934 
935 		Invocation ret = {
936 			name: type ~ "/copy",
937 			arguments: cast(Asdf) arguments,
938 			id: invocationId,
939 		};
940 		return ret;
941 	}
942 
943 	static Invocation query(string type, string accountId, string invocationId, Asdf filter, Asdf sort, int position, string anchor=null, int anchorOffset = 0, Nullable!uint limit = (Nullable!uint).init, bool calculateTotal = false, Variable[string] additionalArguments = (Variable[string]).init)
944 	{
945 		auto arguments = AsdfNode("{}".parseJson);
946 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
947 		arguments["filter"] = AsdfNode(filter);
948 		arguments["sort"] = AsdfNode(sort);
949 		arguments["position"] = AsdfNode(position.serializeToAsdf);
950 		arguments["anchor"] = AsdfNode(anchor.serializeToAsdf);
951 		arguments["anchorOffset"] = AsdfNode(anchorOffset.serializeToAsdf);
952 		arguments["limit"] = AsdfNode(limit.serializeToAsdf);
953 		arguments["calculateTotal"] = AsdfNode(calculateTotal.serializeToAsdf);
954 		foreach(kv;additionalArguments.byKeyValue)
955 			arguments[kv.key] = AsdfNode(kv.value.serializeToAsdf);
956 
957 		Invocation ret = {
958 			name: type ~ "/query",
959 			arguments: cast(Asdf) arguments,
960 			id: invocationId,
961 		};
962 		return ret;
963 	}
964 
965 	static Invocation queryChanges(string type, string accountId, string invocationId, Asdf filter, Asdf sort, string sinceQueryState, Nullable!uint maxChanges = (Nullable!uint).init, Nullable!string upToId = (Nullable!string).init, bool calculateTotal = false, Variable[string] additionalArguments = (Variable[string]).init)
966 	{
967 		auto arguments = AsdfNode("{}".parseJson);
968 		arguments["accountId"] = AsdfNode(accountId.serializeToAsdf);
969 		arguments["filter"] = AsdfNode(filter);
970 		arguments["sort"] = AsdfNode(sort);
971 		arguments["sinceQueryState"] = AsdfNode(sinceQueryState.serializeToAsdf);
972 		arguments["maxChanges"] = AsdfNode(maxChanges.serializeToAsdf);
973 		arguments["upToId"] = AsdfNode(upToId.serializeToAsdf);
974 		arguments["calculateTotal"] = AsdfNode(calculateTotal.serializeToAsdf);
975 		foreach(kv;additionalArguments.byKeyValue)
976 			arguments[kv.key] = AsdfNode(kv.value.serializeToAsdf);
977 
978 		Invocation ret = {
979 			name: type ~ "/queryChanges",
980 			arguments: cast(Asdf) arguments,
981 			id: invocationId,
982 		};
983 		return ret;
984 	}
985 }
986 
987 enum FilterOperatorKind
988 {
989 	and,
990 	or,
991 	not,
992 }
993 
994 interface Filter
995 {
996 }
997 
998 class FilterOperator : Filter
999 {
1000 	FilterOperatorKind operator;
1001 	
1002 	Filter[] conditions;
1003 
1004 	this(FilterOperatorKind operator, Filter[] conditions)
1005 	{
1006 		this.operator = operator;
1007 		this.conditions = conditions;
1008 	}
1009 
1010 	void serialize(S)(ref S serializer)
1011 	{
1012 		import std.exception : enforce;
1013 		import std.format : format;
1014 		import std.conv : to;
1015 		import std.string : toUpper;
1016 
1017 		auto o = serializer.objectBegin();
1018 		serializer.putKey("operator");
1019 		serializer.putValue(operator.to!string.toUpper());
1020 		serializer.putKey("conditions");
1021 		auto o2 = serializer.arrayBegin();
1022 		foreach(i,condition;conditions)
1023 		{
1024 			serializer.elemBegin();
1025 			auto f = cast(FilterOperator) condition;
1026 			auto c = cast(FilterCondition) condition;
1027 			enforce(f !is null || c !is null, format!"condition #%s must be FilterOperator or FilterCondition!"(i));
1028 			if (f !is null)
1029 				serializer.serializeValue(f);
1030 			else if (c !is null)
1031 				serializer.serializeValue(c);
1032 		}
1033 		serializer.arrayEnd(o2);
1034 		serializer.objectEnd(o);
1035 	}
1036 }
1037 
1038 
1039 package enum NullString = (Nullable!string).init;
1040 package enum NullStringArray = (Nullable!(string[])).init;
1041 package enum NullUint = (Nullable!uint).init;
1042 package enum NullDateTime = (Nullable!DateTime).init;
1043 
1044 FilterCondition filterCondition(	Nullable!string inMailbox = NullString,
1045 			Nullable!(string[]) inMailboxOtherThan = NullStringArray,
1046 			Nullable!string before = NullString,
1047 			Nullable!string after = NullString,
1048 			Nullable!uint minSize = NullUint,
1049 			Nullable!uint maxSize = NullUint,
1050 			Nullable!string allInThreadHaveKeyword = NullString,
1051 			Nullable!string someInThreadHaveKeyword = NullString,
1052 			Nullable!string noneInThreadHaveKeyword = NullString,
1053 			Nullable!string hasKeyword = NullString,
1054 			Nullable!string notKeyword = NullString,
1055 			Nullable!string text = NullString,
1056 			Nullable!string from = NullString,
1057 			Nullable!string to = NullString,
1058 			Nullable!string cc = NullString,
1059 			Nullable!string bcc = NullString,
1060 			Nullable!string subject = NullString,
1061 			Nullable!string body_ = NullString,
1062 			Nullable!(string[]) header = NullStringArray, )
1063 {
1064 	return new FilterCondition(inMailbox,inMailboxOtherThan,before,after,minSize,
1065 			maxSize,allInThreadHaveKeyword,someInThreadHaveKeyword,noneInThreadHaveKeyword,
1066 			hasKeyword,notKeyword,text,from,to,cc,bcc,subject,body_,header);
1067 }
1068 
1069 private void doSerialize(S,T)(ref S serializer, T t)
1070 {
1071 	import std.traits : hasUDA, getUDAs, isCallable, isInstanceOf;
1072 	auto o = serializer.objectBegin;
1073 	static foreach(i,M;T.tupleof)
1074 	{{
1075 		 static if (!isCallable!M)
1076 		 {
1077 			 static if (hasUDA!(M,"serializationKeys"))
1078 				 enum name = getUDAs!(M,"serializationKeys")[0].value;
1079 			 else
1080 				 enum name = __traits(identifier,M);
1081 			 mixin("auto value = t." ~ __traits(identifier,M) ~ ";");
1082 			 static if (isInstanceOf!(Nullable,typeof(value)))
1083 			 {
1084 				 if (!value.isNull)
1085 				 {
1086 					//static if (is(typeof(value.get.result) == typeof(Variable)))
1087 					//		serializeAsAsdf(value.get.result);
1088 					//else static if is(Unqual!
1089 					//		doSerialize(serializer,value.get.result);
1090 					 serializer.serializeAsdf(serializeToAsdf(value.get));
1091 				 }
1092 				 else
1093 				 {
1094 					 serializer.putValue(null);
1095 				 }
1096 			 }
1097 			 else
1098 			 {
1099 				 serializer.serializeAsdf(serializeToAsdf(result));
1100 			 }
1101 		 }
1102 	}}
1103 }
1104 
1105 string toUTCDate(Nullable!DateTime dt)
1106 {
1107 	import std.exception : enforce;
1108 	enforce(!dt.isNull,"datetime must not be null");
1109 	return toUTCDate(dt.get);
1110 }
1111 
1112 //"2014-10-30T06:12:00Z"
1113 string toUTCDate(DateTime dt)
1114 {
1115 	import std.string : format;
1116 	return format!"%04d-%02d-%02dT%02d:%02d:%02dZ"(
1117 			dt.date.year,
1118 			dt.date.month,
1119 			dt.date.day,
1120 			dt.timeOfDay.hour,
1121 			dt.timeOfDay.minute,
1122 			dt.timeOfDay.second,
1123 	);
1124 }
1125 
1126 class FilterCondition : Filter
1127 {
1128 	@serializationIgnoreOutIf!`a.isNull`
1129 	Nullable!string inMailbox;
1130 
1131 	@serializationIgnoreOutIf!`a.isNull`
1132 	Nullable!(string[]) inMailboxOtherThan;
1133 
1134 	@serializationIgnoreOutIf!`a.isNull`
1135 	@serializationTransformOut!toUTCDate
1136 	Nullable!DateTime before;
1137 
1138 	@serializationIgnoreOutIf!`a.isNull`
1139 	@serializationTransformOut!toUTCDate
1140 	Nullable!DateTime after;
1141 
1142 	@serializationIgnoreOutIf!`a.isNull`
1143 	Nullable!uint minSize;
1144 
1145 	@serializationIgnoreOutIf!`a.isNull`
1146 	Nullable!uint maxSize;
1147 
1148 	@serializationIgnoreOutIf!`a.isNull`
1149 	Nullable!string allInThreadHaveKeyword;
1150 
1151 	@serializationIgnoreOutIf!`a.isNull`
1152 	Nullable!string someInThreadHaveKeyword;
1153 
1154 	@serializationIgnoreOutIf!`a.isNull`
1155 	Nullable!string noneInThreadHaveKeyword;
1156 
1157 	@serializationIgnoreOutIf!`a.isNull`
1158 	Nullable!string hasKeyword;
1159 
1160 	@serializationIgnoreOutIf!`a.isNull`
1161 	Nullable!string notKeyword;
1162 
1163 	@serializationIgnoreOutIf!`a.isNull`
1164 	Nullable!string text;
1165 
1166 	@serializationIgnoreOutIf!`a.isNull`
1167 	Nullable!string from;
1168 
1169 	@serializationIgnoreOutIf!`a.isNull`
1170 	Nullable!string to;
1171 
1172 	@serializationIgnoreOutIf!`a.isNull`
1173 	Nullable!string cc;
1174 
1175 	@serializationIgnoreOutIf!`a.isNull`
1176 	Nullable!string bcc;
1177 
1178 	@serializationIgnoreOutIf!`a.isNull`
1179 	Nullable!string subject;
1180 
1181 	@serializationIgnoreOutIf!`a.isNull`
1182 	@serializationKeys("body")
1183 	Nullable!string body_;
1184 
1185 	@serializationIgnoreOutIf!`a.isNull`
1186 	Nullable!(string[]) header;
1187 
1188 	override string toString()
1189 	{
1190 		import asdf : jsonSerializer;
1191 		import std.array : appender;
1192 		return serializeToJson(this).idup;
1193 		/+
1194 		auto app = appender!(char[]);
1195 		auto ser = jsonSerializer!("\t")((const(char)[] chars) => app.put(chars));
1196 		serialize(ser);
1197 		ser.flush;
1198 		return cast(string)(app.data); +/
1199 	}
1200 /+
1201 	void serialize(S)(ref S serializer)
1202 	{
1203 		doSerialize(serializer,this);
1204 	}
1205 +/
1206 
1207 	this(	Nullable!string inMailbox = NullString,
1208 			Nullable!(string[]) inMailboxOtherThan = NullStringArray,
1209 			Nullable!string before = NullString,
1210 			Nullable!string after = NullString,
1211 			Nullable!uint minSize = NullUint,
1212 			Nullable!uint maxSize = NullUint,
1213 			Nullable!string allInThreadHaveKeyword = NullString,
1214 			Nullable!string someInThreadHaveKeyword = NullString,
1215 			Nullable!string noneInThreadHaveKeyword = NullString,
1216 			Nullable!string hasKeyword = NullString,
1217 			Nullable!string notKeyword = NullString,
1218 			Nullable!string text = NullString,
1219 			Nullable!string from = NullString,
1220 			Nullable!string to = NullString,
1221 			Nullable!string cc = NullString,
1222 			Nullable!string bcc = NullString,
1223 			Nullable!string subject = NullString,
1224 			Nullable!string body_ = NullString,
1225 			Nullable!(string[]) header = NullStringArray, )
1226 	{
1227 		this.inMailbox = inMailbox;
1228 		this.inMailboxOtherThan = inMailboxOtherThan;
1229 		if (!before.isNull)
1230 			this.before = DateTime.fromISOExtString(before.get);
1231 		if (!after.isNull)
1232 			this.after = DateTime.fromISOExtString(after.get);
1233 		this.minSize = minSize;
1234 		this.maxSize = maxSize;
1235 		this.allInThreadHaveKeyword = allInThreadHaveKeyword;
1236 		this.someInThreadHaveKeyword = someInThreadHaveKeyword;
1237 		this.hasKeyword = hasKeyword;
1238 		this.notKeyword = notKeyword;
1239 		this.text = text;
1240 		this.from = from;
1241 		this.to = to;
1242 		this.cc = cc;
1243 		this.bcc = bcc;
1244 		this.subject = subject;
1245 		this.body_ = body_;
1246 		this.header = header;
1247 	}
1248 }
1249 
1250 Filter operatorAsFilter(FilterOperator filterOperator)
1251 {
1252 	return cast(Filter) filterOperator;
1253 }
1254 
1255 Filter conditionAsFilter(FilterCondition filterCondition)
1256 {
1257 	return cast(Filter) filterCondition;
1258 }
1259 
1260 
1261 struct Comparator
1262 {
1263 	string property;
1264 	bool isAscending = true;
1265 	string collation = null;
1266 }
1267 
1268 	
1269 struct JmapRequest
1270 {
1271 	string[] using;
1272 	Invocation[] methodCalls;
1273 	string[string] createdIds = null;
1274 }
1275 
1276 struct JmapResponse
1277 {
1278 	Invocation[] methodResponses;
1279 	string[string] createdIds;
1280 	string sessionState;
1281 }
1282 
1283 struct JmapResponseError
1284 {
1285 	string type;
1286 	int status;
1287 	string detail;
1288 }
1289 
1290 struct ResultReference
1291 {
1292 	string resultOf;
1293 	string name;
1294 	string path;
1295 }
1296 
1297 struct ContactAddress
1298 {
1299 	string type;
1300 	string label; // Nullable!string label;
1301 	string street;
1302 	string locality;
1303 	string region;
1304 	string postcode;
1305 	string country;
1306 	bool isDefault;
1307 }
1308 
1309 struct JmapFile
1310 {
1311 	string blobId;
1312 	string type;
1313 	string name;
1314 	Nullable!uint size;
1315 }
1316 
1317 struct ContactInformation
1318 {
1319 	string type;
1320 	string label;
1321 	string value;
1322 	bool isDefault;
1323 }
1324 
1325 
1326 struct Contact
1327 {
1328 	string id;
1329 	bool isFlagged;
1330 	JmapFile avatar;
1331 	string prefix;
1332 	string firstName;
1333 	string lastName;
1334 	string suffix;
1335 	string nickname;
1336 	string birthday;
1337 	string anniversary;
1338 	string company;
1339 	string department;
1340 	string jobTitle;
1341 	ContactInformation[] emails;
1342 	ContactInformation[] phones;
1343 	ContactInformation[] online;
1344 	ContactAddress[] addresses;
1345 	string notes;
1346 }
1347 
1348 struct ContactGroup
1349 {
1350 	string id;
1351 	string name;
1352 	string[] ids;
1353 }
1354 
1355 string uriEncode(const(char)[] s)
1356 {
1357 	import std.string : replace;
1358 
1359 	return  s.replace("!","%21").replace("#","%23").replace("$","%24").replace("&","%26").replace("'","%27")
1360 			.replace("(","%28").replace(")","%29").replace("*","%2A").replace("+","%2B").replace(",","%2C")
1361 			.replace("-","%2D").replace(".","%2E").replace("/","%2F").replace(":","%3A").replace(";","%3B")
1362 			.replace("=","%3D").replace("?","%3F").replace("@","%40").replace("[","%5B").replace("]","%5D")
1363 			.idup;
1364 }
1365 
1366 private void serializeAsAsdf(S)(Variable v, ref S serializer)
1367 {
1368 	import std.range : iota;
1369 	import kaleidic.sil.lang.types : SilVariant, KindEnum;
1370 	import kaleidic.sil.lang.builtins : fnArray;
1371 
1372 	final switch(v.kind)
1373 	{
1374 		case KindEnum.void_:
1375 			serializer.putValue(null);
1376 			return;
1377 
1378 		case KindEnum.object:
1379 			auto var = v.get!SilVariant;
1380 			auto acc = var.type.objAccessor;
1381 			if (acc is null)
1382 			{
1383 				serializer.putValue("object");
1384 				return;
1385 			}
1386 			auto obj = serializer.objectBegin();
1387 			foreach(member; acc.listMembers)
1388 			{
1389 				serializer.putKey(member);
1390 				serializeAsAsdf(acc.readProperty(member,var),serializer);
1391 			}
1392 			serializer.objectEnd(obj);
1393 			return;
1394 
1395 		case KindEnum.variable:
1396 			serializeAsAsdf(v.get!Variable, serializer);
1397 			return;
1398 
1399 		case KindEnum.function_:
1400 			serializer.putValue("function"); // FIXME
1401 			return;
1402 
1403 		case KindEnum.boolean:
1404 			serializer.putValue(v.get!bool);
1405 			return;
1406 
1407 		case KindEnum.char_:
1408 			serializer.putValue([(v.get!char)].idup);
1409 			return;
1410 
1411 		case KindEnum.integer:
1412 			serializer.putValue(v.get!long);
1413 			return;
1414 
1415 		case KindEnum.number:
1416 			import std.format : singleSpec;
1417 			import kaleidic.sil.lang.util : fullPrecisionFormatSpec;
1418 			enum spec = singleSpec(fullPrecisionFormatSpec!double);
1419 			serializer.putNumberValue(v.get!double, spec);
1420 			return;
1421 
1422 		case KindEnum.string_:
1423 			serializer.putValue(v.get!string);
1424 			return;
1425 
1426 		case KindEnum.table:
1427 			auto obj = serializer.objectBegin();
1428 			foreach(ref kv; v.get!(Variable[string]).byKeyValue)
1429 			{
1430 				serializer.putKey(kv.key);
1431 				serializeAsAsdf(kv.value, serializer);
1432 			}
1433 			serializer.objectEnd(obj);
1434 			return;
1435 
1436 		case KindEnum.array:
1437 			auto v2 = v.getAssume!(Variable[]);
1438 			auto arr = serializer.arrayBegin();
1439 			foreach(elem; v2)
1440 			{
1441 				serializer.elemBegin;
1442 				serializeAsAsdf(elem, serializer);
1443 			}
1444 			serializer.arrayEnd(arr);
1445 			return;
1446 
1447 		case KindEnum.arrayOf:
1448 			auto v2 = v.getAssume!(KindEnum.arrayOf);
1449 			auto arr = serializer.arrayBegin();
1450 			foreach(i; v2.getLength().iota)
1451 			{
1452 				serializer.elemBegin;
1453 				Variable elem = v2.getElement(i);
1454 				serializeAsAsdf(elem, serializer);
1455 			}
1456 			serializer.arrayEnd(arr);
1457 			return;
1458 
1459 		case KindEnum.rangeOf:
1460 			serializeAsAsdf(fnArray(v), serializer);
1461 			return;
1462 	}
1463 	assert(0);
1464 }