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 }