From 1be9ad77b5cc76f1a60cacbc448782adcb467e6c Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Sat, 25 Nov 2023 09:40:13 +0000 Subject: [PATCH] Start message loading classes --- src/App.svelte | 224 ++++++--------------------------- src/components/TreePath.svelte | 12 +- src/logic/folder.ts | 115 +++++++++++++++++ src/logic/message.ts | 83 ++++++++++++ src/logic/ws.ts | 100 +++++++++++++++ src/types/internal.ts | 15 --- src/utils/count-char.ts | 5 + src/utils/in2mins.ts | 5 + test-server/main.go | 32 +++++ 9 files changed, 382 insertions(+), 209 deletions(-) create mode 100644 src/logic/folder.ts create mode 100644 src/logic/message.ts create mode 100644 src/logic/ws.ts delete mode 100644 src/types/internal.ts create mode 100644 src/utils/count-char.ts create mode 100644 src/utils/in2mins.ts diff --git a/src/App.svelte b/src/App.svelte index 72bea9b..523e6cc 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,33 +2,20 @@ import {onMount} from "svelte"; import {getBearer, loginStore} from "./stores/login"; import {openLoginPopup} from "./utils/login-popup"; - import type {ImapFolder, ImapMessage} from "./types/imap"; - import type {TreeFolder, RootFolder, FolderSelection} from "./types/internal"; + import type {ImapMessage} from "./types/imap"; import TreePath from "./components/TreePath.svelte"; import MailView from "./components/MailView.svelte"; import FolderView from "./components/FolderView.svelte"; - import type {Folders} from "lucide-svelte"; + import {WS} from "./logic/ws"; + import {FolderManager, MailFolder} from "./logic/folder"; + import {MessageManager} from "./logic/message"; - let mainWS: WebSocket; + let mainWS: WS; $: window.mainWS = mainWS; - // Setup root folders - let INBOX: RootFolder = {role: "Inbox", name: "Inbox", path: "INBOX", attr: new Set(), children: []}; - let DRAFTS: RootFolder = {role: "Drafts", name: "Drafts", path: "", attr: new Set(["\\Drafts"]), children: []}; - let SENT: RootFolder = {role: "Sent", name: "Sent", path: "", attr: new Set(["\\Sent"]), children: []}; - let ARCHIVE: RootFolder = {role: "Archive", name: "Archive", path: "", attr: new Set(["\\Archive"]), children: []}; - let JUNK: RootFolder = {role: "Junk", name: "Junk", path: "", attr: new Set(["\\Junk"]), children: []}; - let TRASH: RootFolder = {role: "Trash", name: "Trash", path: "", attr: new Set(["\\Trash"]), children: []}; + let fm: FolderManager = new FolderManager(); + let mm: MessageManager = new MessageManager(); - // Setup map to find special folders - let ROOT: Map = new Map(); - ROOT.set("Drafts", DRAFTS); - ROOT.set("Sent", SENT); - ROOT.set("Archive", ARCHIVE); - ROOT.set("Junk", JUNK); - ROOT.set("Trash", TRASH); - - let folders: RootFolder[] = []; let inboxOption: string = "*"; let inboxOptions: string[] = []; @@ -36,183 +23,26 @@ let messageLookup: Map = new Map(); window.messageLookup = messageLookup; - let currentFolder: FolderSelection = {name: "Inbox", obj: INBOX}; - let folderMessages: ImapMessage[] = []; - $: folderMessages = messageList.get(currentFolder.obj.path); + let currentFolder: MailFolder = fm.INBOX; + let folderMessages: Promise; let currentMessage: ImapMessage | null = null; - function changeSelectedFolder(p: FolderSelection) { + function changeSelectedFolder(p: MailFolder) { currentFolder = p; - grabFolderMessages(p.obj); - } - - function grabFolderMessages(p: TreeFolder) { - let msgs = messageList.get(p.path); - if (msgs == undefined) { - mainWS.send(JSON.stringify({action: "fetch", args: [p.path, "1", "10", "10"]})); - return; - } - folderMessages = msgs; + console.log("changeSelectedFolder", p); + folderMessages = mm.fetchMessages(p.path, 1, 10, 10); } $: console.log("Selected", currentFolder); - function countChar(s: string, c: string) { - let result = 0; - for (let i = 0; i < s.length; i++) if (s[i] == c) result++; - return result; - } - - function connectWS() { - mainWS = new WebSocket(import.meta.env.VITE_IMAP_LOTUS); - mainWS.addEventListener("open", () => { - mainWS.send(JSON.stringify({token: getBearer().slice(7)})); - }); - mainWS.addEventListener("message", e => { - let j = JSON.parse(e.data); - if (j.auth === "ok") { - mainWS.send(JSON.stringify({action: "list", args: ["", "*"]})); - } - if (j.type === "list") { - // === Example output of list command === - // let j = { - // type: "list", - // value: [ - // {Attributes: ["\\HasChildren", "\\UnMarked", "\\Archive"], Delimiter: "/", Name: "Archive"}, - // {Attributes: ["\\HasNoChildren", "\\UnMarked", "\\Junk"], Delimiter: "/", Name: "Junk"}, - // {Attributes: ["\\HasChildren", "\\Trash"], Delimiter: "/", Name: "Trash"}, - // {Attributes: ["\\HasNoChildren", "\\UnMarked"], Delimiter: "/", Name: "INBOX/status"}, - // {Attributes: ["\\HasNoChildren", "\\UnMarked"], Delimiter: "/", Name: "INBOX/hello"}, - // {Attributes: ["\\HasNoChildren", "\\UnMarked"], Delimiter: "/", Name: "INBOX/hi"}, - // {Attributes: ["\\Noselect", "\\HasChildren"], Delimiter: "/", Name: "INBOX/sub"}, - // {Attributes: ["\\HasNoChildren"], Delimiter: "/", Name: "INBOX/sub/folder"}, - // {Attributes: ["\\HasNoChildren", "\\UnMarked", "\\Drafts"], Delimiter: "/", Name: "Drafts"}, - // {Attributes: ["\\HasNoChildren", "\\Sent"], Delimiter: "/", Name: "Sent"}, - // {Attributes: ["\\HasChildren"], Delimiter: "/", Name: "INBOX"}, - // ], - // }; - - let imapFolders = j.value as ImapFolder[]; - - // Remove no-select folders - imapFolders = imapFolders.filter(x => !x.Attributes.includes("\\Noselect")); - // Sort shorter paths first so parent folders are registered before children - imapFolders = imapFolders.sort((a, b) => countChar(a.Name, a.Delimiter) - countChar(b.Name, b.Delimiter)); - - // Store reference to special folders - let ROOTKEYS: Map = new Map(); - - imapFolders.forEach(x => { - // Find inbox folder - if (x.Name === "INBOX") { - x.Attributes.forEach(x => { - INBOX.attr.add(x); - }); - ROOTKEYS.set(x.Name, INBOX); - return; // continue imapFolders loop - } - - // Test for all special folder attributes - for (let [k, v] of ROOT.entries()) { - if (x.Attributes.includes("\\" + k)) { - v.name = x.Name; - x.Attributes.forEach(x => { - v.attr.add(x); - }); - v.path = x.Name; - // map name to root key - ROOTKEYS.set(x.Name, v); - return; // continue imapFolders loop - } - } - - let n = x.Name.indexOf(x.Delimiter); - if (n == -1) { - console.error("No parent folder wtf??", x.Name); - return; - } - let parent = x.Name.slice(0, n); - let pObj: TreeFolder | undefined = ROOTKEYS.get(parent); - if (pObj == undefined) { - console.error("Parent is not a root folder??", x.Name); - return; - } - - let pIdx = n + 1; - - for (let i = pIdx; i < x.Name.length; i++) { - if (x.Name[i] != x.Delimiter) continue; - // find child matching current slice - let nextObj: TreeFolder | undefined = pObj?.children.find(x2 => { - // check if folder matches current slice - return x2.name === x.Name.slice(pIdx, i); - }); - // if no slice is found try a bigger slice - if (nextObj == undefined) continue; - - // move into child folder - pObj = nextObj; - pIdx = i + 1; - } - - // no parent was found at all - if (pObj == undefined) { - console.error("Parent folder does not exist??", x.Name); - return; - } - - // add child to current parent - pObj.children.push({ - name: x.Name.slice(pIdx), - path: x.Name, - attr: new Set(x.Attributes), - children: [], - }); - }); - - // output special folders in order - folders = [INBOX, DRAFTS, SENT, ARCHIVE, JUNK, TRASH]; - } - if (j.type == "fetch") { - // { - // type: "fetch", - // value: [ - // { - // $Body: {}, - // BodyStructure: null, - // Envelope: { - // Date: "2023-09-10T20:54:09-04:00", - // Subject: "This is an email subject", - // From: [{PersonalName: "A Cool User", AtDomainList: "", MailboxName: "test", HostName: "example.com"}], - // Sender: [{PersonalName: "A Cool User", AtDomainList: "", MailboxName: "test", HostName: "example.com"}], - // ReplyTo: [{PersonalName: "A Cool User", AtDomainList: "", MailboxName: "test", HostName: "example.com"}], - // To: [{PersonalName: "Internal", AtDomainList: "", MailboxName: "melon+hi", HostName: "example.org"}], - // Cc: null, - // Bcc: null, - // InReplyTo: "", - // MessageId: "\u003c950124.162336@example.com\u003e", - // }, - // Flags: ["\\Seen", "nonjunk"], - // InternalDate: "2023-09-10T20:54:10-04:00", - // Items: ["UID", "FLAGS", "INTERNALDATE", "ENVELOPE"], - // SeqNum: 1, - // Size: 0, - // Uid: 18, - // } - // ] - // }; - } - }); - } - function removeLoginSession() { $loginStore = null; localStorage.removeItem("login-session"); } onMount(() => { - connectWS(); + mainWS = new WS(import.meta.env.VITE_IMAP_LOTUS, getBearer().slice(7), fm, mm); }); @@ -241,12 +71,16 @@
Please login to continue
{:else}
- + {#await folderMessages} +
Loading messages
+ {:then x} + + {/await}
Warning: This is currently still under development
@@ -287,7 +121,9 @@ height: 70px; padding: 0 32px; background-color: #2c2c2c; - box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030; + box-shadow: + 0 4px 8px #0003, + 0 6px 20px #00000030; gap: 16px; z-index: 1; position: relative; @@ -350,10 +186,20 @@ padding: 16px; } - #option-view { + #folder-view { box-sizing: border-box; overflow-y: auto; height: 100%; + width: 250px; + flex-grow: 1; + flex-shrink: 0; + } + + #message-view { + box-sizing: border-box; + overflow-y: auto; + width: 100%; + height: 100%; flex-grow: 1; } } @@ -361,7 +207,9 @@ footer { padding: 8px; background-color: #2c2c2c; - box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030; + box-shadow: + 0 -4px 8px #0003, + 0 -6px 20px #00000030; display: flex; flex-direction: row; justify-content: space-between; diff --git a/src/components/TreePath.svelte b/src/components/TreePath.svelte index a64e35e..f5cc982 100644 --- a/src/components/TreePath.svelte +++ b/src/components/TreePath.svelte @@ -1,8 +1,8 @@ @@ -53,7 +53,7 @@ data={child} treeOffset={treeOffset + 1} selected={getNextSelected(selected)} - on:select={n => triggerSelected({name: data.name + "/" + n.detail.name, obj: n.detail.obj})} + on:select={n => triggerSelected(n.detail)} /> {/each}
diff --git a/src/logic/folder.ts b/src/logic/folder.ts new file mode 100644 index 0000000..637b8f8 --- /dev/null +++ b/src/logic/folder.ts @@ -0,0 +1,115 @@ +import type {ImapFolder} from "../types/imap"; + +export class MailFolder { + name: string; // Local name + path: string; // Global path + attr: Set; + children: Array; + + constructor(name: string, path: string, attr: Set) { + this.name = name; + this.path = path; + this.attr = attr; + this.children = new Array(); + } + + addAttrs(a: string[]) { + a.forEach(x => this.attr.add(x)); + } + + addChild(f: MailFolder) { + this.children.push(f); + } +} + +export class RootMailFolder extends MailFolder { + role: string; + + constructor(name: string, role: string, path: string, attr: Set) { + super(name, path, attr); + this.role = role; + } +} + +export class FolderManager { + public INBOX: RootMailFolder = new RootMailFolder("Inbox", "Inbox", "INBOX", new Set("")); + public DRAFTS: RootMailFolder = new RootMailFolder("Drafts", "Drafts", "~", new Set(["\\Drafts"])); + public SENT: RootMailFolder = new RootMailFolder("Sent", "Sent", "~", new Set(["\\Sent"])); + public ARCHIVE: RootMailFolder = new RootMailFolder("Archive", "Archive", "~", new Set(["\\Archive"])); + public JUNK: RootMailFolder = new RootMailFolder("Junk", "Junk", "~", new Set(["\\Junk"])); + public TRASH: RootMailFolder = new RootMailFolder("Trash", "Trash", "~", new Set(["\\Trash"])); + + public ROOT = [this.INBOX, this.DRAFTS, this.SENT, this.ARCHIVE, this.JUNK, this.TRASH]; + private ROOT_SPECIAL = { + Drafts: this.DRAFTS, + Sent: this.SENT, + Archive: this.ARCHIVE, + Junk: this.JUNK, + Trash: this.TRASH, + }; + // Store reference to special folders + private ROOT_PATHS: Map = new Map(); + + constructor() {} + + resolveFolder(x: ImapFolder) { + // Find inbox folder + if (x.Name === "INBOX") { + this.INBOX.addAttrs(x.Attributes); + this.ROOT_PATHS.set(x.Name, this.INBOX); + return; // continue imapFolders loop + } + + // Test for all special folder attributes + for (let [k, v] of Object.entries(this.ROOT_SPECIAL)) { + if (x.Attributes.includes("\\" + k)) { + v.name = x.Name; + x.Attributes.forEach(x => { + v.attr.add(x); + }); + v.path = x.Name; + // map name to root key + this.ROOT_PATHS.set(x.Name, v); + return; // continue imapFolders loop + } + } + + let n = x.Name.indexOf(x.Delimiter); + if (n == -1) { + console.error("No parent folder wtf??", x.Name); + return; + } + let parent = x.Name.slice(0, n); + let pObj: MailFolder | undefined = this.ROOT_PATHS.get(parent); + if (pObj == undefined) { + console.error("Parent is not a root folder??", x.Name); + return; + } + + let pIdx = n + 1; + + for (let i = pIdx; i < x.Name.length; i++) { + if (x.Name[i] != x.Delimiter) continue; + // find child matching current slice + let nextObj: MailFolder | undefined = pObj?.children.find(x2 => { + // check if folder matches current slice + return x2.name === x.Name.slice(pIdx, i); + }); + // if no slice is found try a bigger slice + if (nextObj == undefined) continue; + + // move into child folder + pObj = nextObj; + pIdx = i + 1; + } + + // no parent was found at all + if (pObj == undefined) { + console.error("Parent folder does not exist??", x.Name); + return; + } + + // add child to current parent + pObj.children.push(new MailFolder(x.Name.slice(pIdx), x.Name, new Set(x.Attributes))); + } +} diff --git a/src/logic/message.ts b/src/logic/message.ts new file mode 100644 index 0000000..b4fa8bb --- /dev/null +++ b/src/logic/message.ts @@ -0,0 +1,83 @@ +import type {ImapEnvelope, ImapMessage} from "../types/imap"; +import {in2mins} from "../utils/in2mins"; +import type {WS} from "./ws"; + +export class MailMessage { + expires: Date; + __Body: unknown; + Envelope: ImapEnvelope; + Flags: Set = new Set(); + InternalDate: Date; + Items: string[]; + SeqNum: number; + Size: number; + Uid: number; + + constructor(m: ImapMessage) { + this.expires = in2mins(); + this.__Body = m.$Body; + this.Envelope = m.Envelope; + m.Flags.forEach(x => this.Flags.add(x)); + this.InternalDate = new Date(m.InternalDate); + this.Items = m.Items; + this.SeqNum = m.SeqNum; + this.Size = m.Size; + this.Uid = m.Uid; + } +} + +export class MessageManager { + ws?: WS; + fetchCalls: Map void}> = new Map(); // key = unique + store: Map = new Map(); // key = path -> seqnum + + setWS(ws: WS) { + this.ws = ws; + window.__mm_store = this.store; + } + + fetchMessages(path: string, start: number, end: number, limit: number): Promise { + let now = Date.now(); + let s = this.store.get(path); + if (s?.expires || 0 < now) { + return new Promise((res, rej) => { + this.fetchCalls.set(now, { + path: path, + trigger: (msgs: MailMessage[]) => { + console.log("Fetch call triggered"); + res(msgs); + }, + }); + this.ws?.send("fetch", {sync: now, path, start, end, limit}); + }); + } + + return Promise.resolve([...(s?.store.values() || [])]); + } + + fetchFolder(path: string): Promise { + return this.fetchMessages(path, 1, 100, 100); + } + + updateMessage(sync: number, m: MailMessage[]) { + let call = this.fetchCalls.get(sync); + if (!call) return; + this.fetchCalls.delete(sync); + let s = this.store.get(call.path); + if (!s) { + s = new FolderMessageManager(); + this.store.set(call.path, s); + } + s.saveIncomingMessages(m); + call.trigger([...s.store.values()]); + } +} + +export class FolderMessageManager { + expires: Date = in2mins(); + store: Map = new Map(); // key = seqnum + + saveIncomingMessages(m: MailMessage[]) { + m.forEach(x => this.store.set(x.SeqNum, x)); + } +} diff --git a/src/logic/ws.ts b/src/logic/ws.ts new file mode 100644 index 0000000..5c1de63 --- /dev/null +++ b/src/logic/ws.ts @@ -0,0 +1,100 @@ +import type {ImapFolder, ImapMessage} from "../types/imap"; +import {countChar} from "../utils/count-char"; +import type {FolderManager, RootMailFolder} from "./folder"; +import {MailMessage, type MessageManager} from "./message"; + +export class WS { + ws: WebSocket; + fm: FolderManager; + mm: MessageManager; + + constructor(address: string, token: string, fm: FolderManager, mm: MessageManager) { + this.ws = new WebSocket(address); + this.ws.addEventListener("open", () => { + this.ws.send(JSON.stringify({token: token})); + }); + this.ws.addEventListener("message", e => { + this.handleMessage(e); + }); + this.fm = fm; + this.mm = mm; + mm.setWS(this); + } + + send(action: string, args: any) { + this.ws.send(JSON.stringify({action, args})); + } + + handleMessage(e: MessageEvent) { + let j = JSON.parse(e.data); + if (j.auth === "ok") { + this.ws.send(JSON.stringify({action: "list", args: ["", "*"]})); + } + if (j.type === "list") { + // === Example output of list command === + // let j = { + // type: "list", + // value: [ + // {Attributes: ["\\HasChildren", "\\UnMarked", "\\Archive"], Delimiter: "/", Name: "Archive"}, + // {Attributes: ["\\HasNoChildren", "\\UnMarked", "\\Junk"], Delimiter: "/", Name: "Junk"}, + // {Attributes: ["\\HasChildren", "\\Trash"], Delimiter: "/", Name: "Trash"}, + // {Attributes: ["\\HasNoChildren", "\\UnMarked"], Delimiter: "/", Name: "INBOX/status"}, + // {Attributes: ["\\HasNoChildren", "\\UnMarked"], Delimiter: "/", Name: "INBOX/hello"}, + // {Attributes: ["\\HasNoChildren", "\\UnMarked"], Delimiter: "/", Name: "INBOX/hi"}, + // {Attributes: ["\\Noselect", "\\HasChildren"], Delimiter: "/", Name: "INBOX/sub"}, + // {Attributes: ["\\HasNoChildren"], Delimiter: "/", Name: "INBOX/sub/folder"}, + // {Attributes: ["\\HasNoChildren", "\\UnMarked", "\\Drafts"], Delimiter: "/", Name: "Drafts"}, + // {Attributes: ["\\HasNoChildren", "\\Sent"], Delimiter: "/", Name: "Sent"}, + // {Attributes: ["\\HasChildren"], Delimiter: "/", Name: "INBOX"}, + // ], + // }; + + let imapFolders = j.value as ImapFolder[]; + + // Remove no-select folders + imapFolders = imapFolders.filter(x => !x.Attributes.includes("\\Noselect")); + // Sort shorter paths first so parent folders are registered before children + imapFolders = imapFolders.sort((a, b) => countChar(a.Name, a.Delimiter) - countChar(b.Name, b.Delimiter)); + + imapFolders.forEach(x => { + this.fm.resolveFolder(x); + }); + } + if (j.type == "fetch") { + // { + // type: "fetch", + // sync: 0, + // value: [ + // { + // $Body: {}, + // BodyStructure: null, + // Envelope: { + // Date: "2023-09-10T20:54:09-04:00", + // Subject: "This is an email subject", + // From: [{PersonalName: "A Cool User", AtDomainList: "", MailboxName: "test", HostName: "example.com"}], + // Sender: [{PersonalName: "A Cool User", AtDomainList: "", MailboxName: "test", HostName: "example.com"}], + // ReplyTo: [{PersonalName: "A Cool User", AtDomainList: "", MailboxName: "test", HostName: "example.com"}], + // To: [{PersonalName: "Internal", AtDomainList: "", MailboxName: "melon+hi", HostName: "example.org"}], + // Cc: null, + // Bcc: null, + // InReplyTo: "", + // MessageId: "\u003c950124.162336@example.com\u003e", + // }, + // Flags: ["\\Seen", "nonjunk"], + // InternalDate: "2023-09-10T20:54:10-04:00", + // Items: ["UID", "FLAGS", "INTERNALDATE", "ENVELOPE"], + // SeqNum: 1, + // Size: 0, + // Uid: 18, + // } + // ] + // }; + + let imapMsg = j.value as ImapMessage[]; + this.mm.updateMessage( + j.sync, + imapMsg.map(x => new MailMessage(x)), + ); + } + } +} diff --git a/src/types/internal.ts b/src/types/internal.ts deleted file mode 100644 index 95aa93b..0000000 --- a/src/types/internal.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface TreeFolder { - name: string; // Local name - path: string; // Global path - attr: Set; - children: TreeFolder[]; -} - -export interface RootFolder extends TreeFolder { - role: string; -} - -export interface FolderSelection { - name: string; - obj: TreeFolder; -} diff --git a/src/utils/count-char.ts b/src/utils/count-char.ts new file mode 100644 index 0000000..5ccdfb6 --- /dev/null +++ b/src/utils/count-char.ts @@ -0,0 +1,5 @@ +export function countChar(s: string, c: string) { + let result = 0; + for (let i = 0; i < s.length; i++) if (s[i] == c) result++; + return result; +} diff --git a/src/utils/in2mins.ts b/src/utils/in2mins.ts new file mode 100644 index 0000000..5de0ef3 --- /dev/null +++ b/src/utils/in2mins.ts @@ -0,0 +1,5 @@ +export function in2mins(): Date { + let future = new Date(); + future.setMinutes(future.getMinutes() + 2); + return future; +} diff --git a/test-server/main.go b/test-server/main.go index b046cf3..9ededfb 100644 --- a/test-server/main.go +++ b/test-server/main.go @@ -160,6 +160,7 @@ func apiServer(verify mjwt.Verifier) { c.WriteMessage(websocket.TextMessage, []byte(`{"auth":"ok"}`)) continue } else if vAct, ok := m["action"]; ok { + args := m["args"] switch vAct.(string) { case "list": log.Println(m) @@ -188,6 +189,37 @@ func apiServer(verify mjwt.Verifier) { `)) } continue + case "fetch": + c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(` +{ + "type": "fetch", + "sync": %f, + "value": [ + { + "$Body": {}, + "BodyStructure": null, + "Envelope": { + "Date": "2023-09-10T20:54:09-04:00", + "Subject": "This is an email subject", + "From": [{"PersonalName": "A Cool User", "AtDomainList": "", "MailboxName": "test", "HostName": "example.com"}], + "Sender": [{"PersonalName": "A Cool User", "AtDomainList": "", "MailboxName": "test", "HostName": "example.com"}], + "ReplyTo": [{"PersonalName": "A Cool User", "AtDomainList": "", "MailboxName": "test", "HostName": "example.com"}], + "To": [{"PersonalName": "Internal", "AtDomainList": "", "MailboxName": "melon+hi", "HostName": "example.org"}], + "Cc": null, + "Bcc": null, + "InReplyTo": "", + "MessageId": "\u003c950124.162336@example.com\u003e" + }, + "Flags": ["\\Seen", "nonjunk"], + "InternalDate": "2023-09-10T20:54:10-04:00", + "Items": ["UID", "FLAGS", "INTERNALDATE", "ENVELOPE"], + "SeqNum": 1, + "Size": 0, + "Uid": 18 + } + ] +} +`, args.(map[string]any)["sync"]))) } } }