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 }