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 }