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