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 	Asdf queryChangesRaw(string type, Variable filter, Variable sort, string sinceQueryState, Nullable!uint maxChanges = (Nullable!uint).init, string upToId = null, 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 	Variable queryChanges(string type, Variable filter, Variable sort, string sinceQueryState,Nullable!uint maxChanges = (Nullable!uint).init,  string upToId = null, 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,  string upToId = null, 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 nullArray = (Nullable!(string[])).init;
1040 package enum NullUint = (Nullable!uint).init;
1041 package enum NullDateTime = (Nullable!DateTime).init;
1042 
1043 FilterCondition filterCondition(	 string inMailbox = null,
1044 			Nullable!(string[]) inMailboxOtherThan = nullArray,
1045 			string before = null,
1046 			string after = null,
1047 			Nullable!uint minSize = NullUint,
1048 			Nullable!uint maxSize = NullUint,
1049 			string allInThreadHaveKeyword = null,
1050 			string someInThreadHaveKeyword = null,
1051 			string noneInThreadHaveKeyword = null,
1052 			string hasKeyword = null,
1053 			string notKeyword = null,
1054 			string text = null,
1055 			string from = null,
1056 			string to = null,
1057 			string cc = null,
1058 			string bcc = null,
1059 			string subject = null,
1060 			string body_ = null,
1061 			Nullable!(string[]) header = nullArray, )
1062 {
1063 	return new FilterCondition(inMailbox,inMailboxOtherThan,before,after,minSize,
1064 			maxSize,allInThreadHaveKeyword,someInThreadHaveKeyword,noneInThreadHaveKeyword,
1065 			hasKeyword,notKeyword,text,from,to,cc,bcc,subject,body_,header);
1066 }
1067 
1068 private void doSerialize(S,T)(ref S serializer, T t)
1069 {
1070 	import std.traits : hasUDA, getUDAs, isCallable, isInstanceOf;
1071 	auto o = serializer.objectBegin;
1072 	static foreach(i,M;T.tupleof)
1073 	{{
1074 		 static if (!isCallable!M)
1075 		 {
1076 			 static if (hasUDA!(M,"serializationKeys"))
1077 				 enum name = getUDAs!(M,"serializationKeys")[0].value;
1078 			 else
1079 				 enum name = __traits(identifier,M);
1080 			 mixin("auto value = t." ~ __traits(identifier,M) ~ ";");
1081 			 static if (isInstanceOf!(Nullable,typeof(value)))
1082 			 {
1083 				 if (!value.isNull)
1084 				 {
1085 					//static if (is(typeof(value.get.result) == typeof(Variable)))
1086 					//		serializeAsAsdf(value.get.result);
1087 					//else static if is(Unqual!
1088 					//		doSerialize(serializer,value.get.result);
1089 					 serializer.serializeAsdf(serializeToAsdf(value.get));
1090 				 }
1091 				 else
1092 				 {
1093 					 serializer.putValue(null);
1094 				 }
1095 			 }
1096 			 else
1097 			 {
1098 				 serializer.serializeAsdf(serializeToAsdf(result));
1099 			 }
1100 		 }
1101 	}}
1102 }
1103 
1104 string toUTCDate(Nullable!DateTime dt)
1105 {
1106 	import std.exception : enforce;
1107 	enforce(!dt.isNull,"datetime must not be null");
1108 	return toUTCDate(dt.get);
1109 }
1110 
1111 //"2014-10-30T06:12:00Z"
1112 string toUTCDate(DateTime dt)
1113 {
1114 	import std.string : format;
1115 	return format!"%04d-%02d-%02dT%02d:%02d:%02dZ"(
1116 			dt.date.year,
1117 			dt.date.month,
1118 			dt.date.day,
1119 			dt.timeOfDay.hour,
1120 			dt.timeOfDay.minute,
1121 			dt.timeOfDay.second,
1122 	);
1123 }
1124 
1125 class FilterCondition : Filter
1126 {
1127 	@serializationIgnoreOutIf!`a.length == 0`
1128 	string inMailbox;
1129 
1130 	@serializationIgnoreOutIf!`a.isNull`
1131 	Nullable!(string[]) inMailboxOtherThan;
1132 
1133 	@serializationIgnoreOutIf!`a.isNull`
1134 	@serializationTransformOut!toUTCDate
1135 	Nullable!DateTime before;
1136 
1137 	@serializationIgnoreOutIf!`a.isNull`
1138 	@serializationTransformOut!toUTCDate
1139 	Nullable!DateTime after;
1140 
1141 	@serializationIgnoreOutIf!`a.isNull`
1142 	Nullable!uint minSize;
1143 
1144 	@serializationIgnoreOutIf!`a.isNull`
1145 	Nullable!uint maxSize;
1146 
1147 
1148 	@serializationIgnoreOutIf!`a.isNull`
1149 	Nullable!(string[]) header;
1150 
1151 	override string toString()
1152 	{
1153 		import asdf : jsonSerializer;
1154 		import std.array : appender;
1155 		return serializeToJson(this).idup;
1156 		/+
1157 		auto app = appender!(char[]);
1158 		auto ser = jsonSerializer!("\t")((const(char)[] chars) => app.put(chars));
1159 		serialize(ser);
1160 		ser.flush;
1161 		return cast(string)(app.data); +/
1162 	}
1163 /+
1164 	void serialize(S)(ref S serializer)
1165 	{
1166 		doSerialize(serializer,this);
1167 	}
1168 +/
1169 
1170 	this(	 	string inMailbox = null,
1171 			Nullable!(string[]) inMailboxOtherThan = nullArray,
1172 			string before = null,
1173 			string after = null,
1174 			Nullable!uint minSize = NullUint,
1175 			Nullable!uint maxSize = NullUint,
1176 			string allInThreadHaveKeyword = null,
1177 			string someInThreadHaveKeyword = null,
1178 			string noneInThreadHaveKeyword = null,
1179 			string hasKeyword = null,
1180 			string notKeyword = null,
1181 			string text = null,
1182 			string from = null,
1183 			string to = null,
1184 			string cc = null,
1185 			string bcc = null,
1186 			string subject = null,
1187 			string body_ = null,
1188 			Nullable!(string[]) header = nullArray, )
1189 	{
1190 		this.inMailbox = inMailbox;
1191 		this.inMailboxOtherThan = inMailboxOtherThan;
1192 		if (before.length > 0)
1193 			this.before = DateTime.fromISOExtString(before);
1194 		if (after.length > 0)
1195 			this.after = DateTime.fromISOExtString(after);
1196 		this.minSize = minSize;
1197 		this.maxSize = maxSize;
1198 		this.allInThreadHaveKeyword = allInThreadHaveKeyword;
1199 		this.someInThreadHaveKeyword = someInThreadHaveKeyword;
1200 		this.hasKeyword = hasKeyword;
1201 		this.notKeyword = notKeyword;
1202 		this.text = text;
1203 		this.from = from;
1204 		this.to = to;
1205 		this.cc = cc;
1206 		this.bcc = bcc;
1207 		this.subject = subject;
1208 		this.body_ = body_;
1209 		this.header = header;
1210 	}
1211 }
1212 
1213 Filter operatorAsFilter(FilterOperator filterOperator)
1214 {
1215 	return cast(Filter) filterOperator;
1216 }
1217 
1218 Filter conditionAsFilter(FilterCondition filterCondition)
1219 {
1220 	return cast(Filter) filterCondition;
1221 }
1222 
1223 
1224 struct Comparator
1225 {
1226 	string property;
1227 	bool isAscending = true;
1228 	string collation = null;
1229 }
1230 
1231 	
1232 struct JmapRequest
1233 {
1234 	string[] using;
1235 	Invocation[] methodCalls;
1236 	string[string] createdIds = null;
1237 }
1238 
1239 struct JmapResponse
1240 {
1241 	Invocation[] methodResponses;
1242 	string[string] createdIds;
1243 	string sessionState;
1244 }
1245 
1246 struct JmapResponseError
1247 {
1248 	string type;
1249 	int status;
1250 	string detail;
1251 }
1252 
1253 struct ResultReference
1254 {
1255 	string resultOf;
1256 	string name;
1257 	string path;
1258 }
1259 
1260 struct ContactAddress
1261 {
1262 	string type;
1263 	string label; //  label;
1264 	string street;
1265 	string locality;
1266 	string region;
1267 	string postcode;
1268 	string country;
1269 	bool isDefault;
1270 }
1271 
1272 struct JmapFile
1273 {
1274 	string blobId;
1275 	string type;
1276 	string name;
1277 	Nullable!uint size;
1278 }
1279 
1280 struct ContactInformation
1281 {
1282 	string type;
1283 	string label;
1284 	string value;
1285 	bool isDefault;
1286 }
1287 
1288 
1289 struct Contact
1290 {
1291 	string id;
1292 	bool isFlagged;
1293 	JmapFile avatar;
1294 	string prefix;
1295 	string firstName;
1296 	string lastName;
1297 	string suffix;
1298 	string nickname;
1299 	string birthday;
1300 	string anniversary;
1301 	string company;
1302 	string department;
1303 	string jobTitle;
1304 	ContactInformation[] emails;
1305 	ContactInformation[] phones;
1306 	ContactInformation[] online;
1307 	ContactAddress[] addresses;
1308 	string notes;
1309 }
1310 
1311 struct ContactGroup
1312 {
1313 	string id;
1314 	string name;
1315 	string[] ids;
1316 }
1317 
1318 string uriEncode(const(char)[] s)
1319 {
1320 	import std.string : replace;
1321 
1322 	return  s.replace("!","%21").replace("#","%23").replace("$","%24").replace("&","%26").replace("'","%27")
1323 			.replace("(","%28").replace(")","%29").replace("*","%2A").replace("+","%2B").replace(",","%2C")
1324 			.replace("-","%2D").replace(".","%2E").replace("/","%2F").replace(":","%3A").replace(";","%3B")
1325 			.replace("=","%3D").replace("?","%3F").replace("@","%40").replace("[","%5B").replace("]","%5D")
1326 			.idup;
1327 }
1328 
1329 private void serializeAsAsdf(S)(Variable v, ref S serializer)
1330 {
1331 	import std.range : iota;
1332 	import kaleidic.sil.lang.types : SilVariant, KindEnum;
1333 	import kaleidic.sil.lang.builtins : fnArray;
1334 
1335 	final switch(v.kind)
1336 	{
1337 		case KindEnum.void_:
1338 			serializer.putValue(null);
1339 			return;
1340 
1341 		case KindEnum.object:
1342 			auto var = v.get!SilVariant;
1343 			auto acc = var.type.objAccessor;
1344 			if (acc is null)
1345 			{
1346 				serializer.putValue("object");
1347 				return;
1348 			}
1349 			auto obj = serializer.objectBegin();
1350 			foreach(member; acc.listMembers)
1351 			{
1352 				serializer.putKey(member);
1353 				serializeAsAsdf(acc.readProperty(member,var),serializer);
1354 			}
1355 			serializer.objectEnd(obj);
1356 			return;
1357 
1358 		case KindEnum.variable:
1359 			serializeAsAsdf(v.get!Variable, serializer);
1360 			return;
1361 
1362 		case KindEnum.function_:
1363 			serializer.putValue("function"); // FIXME
1364 			return;
1365 
1366 		case KindEnum.boolean:
1367 			serializer.putValue(v.get!bool);
1368 			return;
1369 
1370 		case KindEnum.char_:
1371 			serializer.putValue([(v.get!char)].idup);
1372 			return;
1373 
1374 		case KindEnum.integer:
1375 			serializer.putValue(v.get!long);
1376 			return;
1377 
1378 		case KindEnum.number:
1379 			import std.format : singleSpec;
1380 			import kaleidic.sil.lang.util : fullPrecisionFormatSpec;
1381 			enum spec = singleSpec(fullPrecisionFormatSpec!double);
1382 			serializer.putNumberValue(v.get!double, spec);
1383 			return;
1384 
1385 		case KindEnum.string_:
1386 			serializer.putValue(v.get!string);
1387 			return;
1388 
1389 		case KindEnum.table:
1390 			auto obj = serializer.objectBegin();
1391 			foreach(ref kv; v.get!(Variable[string]).byKeyValue)
1392 			{
1393 				serializer.putKey(kv.key);
1394 				serializeAsAsdf(kv.value, serializer);
1395 			}
1396 			serializer.objectEnd(obj);
1397 			return;
1398 
1399 		case KindEnum.array:
1400 			auto v2 = v.getAssume!(Variable[]);
1401 			auto arr = serializer.arrayBegin();
1402 			foreach(elem; v2)
1403 			{
1404 				serializer.elemBegin;
1405 				serializeAsAsdf(elem, serializer);
1406 			}
1407 			serializer.arrayEnd(arr);
1408 			return;
1409 
1410 		case KindEnum.arrayOf:
1411 			auto v2 = v.getAssume!(KindEnum.arrayOf);
1412 			auto arr = serializer.arrayBegin();
1413 			foreach(i; v2.getLength().iota)
1414 			{
1415 				serializer.elemBegin;
1416 				Variable elem = v2.getElement(i);
1417 				serializeAsAsdf(elem, serializer);
1418 			}
1419 			serializer.arrayEnd(arr);
1420 			return;
1421 
1422 		case KindEnum.rangeOf:
1423 			serializeAsAsdf(fnArray(v), serializer);
1424 			return;
1425 	}
1426 	assert(0);
1427 }