mirror of
https://github.com/1f349/mail.1f349.com.git
synced 2024-11-09 22:32:52 +00:00
Calculate folder tree
This commit is contained in:
parent
d950eca59f
commit
a7a0008eb1
@ -1,3 +1,4 @@
|
||||
VITE_SSO_ORIGIN=http://localhost:9090
|
||||
|
||||
VITE_API_LOTUS=http://localhost:9095/v1/lotus
|
||||
VITE_IMAP_LOTUS=ws://localhost:9095/v1/lotus/imap
|
||||
|
@ -1,3 +1,4 @@
|
||||
VITE_SSO_ORIGIN=https://sso.1f349.com
|
||||
|
||||
VITE_API_LOTUS=https://api.1f349.com/v1/lotus
|
||||
VITE_IMAP_LOTUS=wss://api.1f349.com/v1/lotus/imap
|
||||
|
134
src/App.svelte
134
src/App.svelte
@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte";
|
||||
import {getBearer, loginStore} from "./stores/login";
|
||||
import {openLoginPopup} from "./utils/login-popup";
|
||||
import type {ImapFolder} from "./types/imap";
|
||||
import type {TreeFolder, RootFolder} from "./types/internal";
|
||||
|
||||
let mainWS: WebSocket;
|
||||
$: window.mainWS = mainWS;
|
||||
|
||||
interface FolderValue {
|
||||
Attributes: string[];
|
||||
Delimiter: string;
|
||||
Name: string;
|
||||
let folders: RootFolder[] = [];
|
||||
|
||||
function countChar(s: string, c: string) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < s.length; i++) if (s[i] == c) result++;
|
||||
return result;
|
||||
}
|
||||
|
||||
let folders: FolderValue[] = [];
|
||||
|
||||
function connectWS() {
|
||||
mainWS = new WebSocket("wss://api.1f349.com/v1/lotus/imap");
|
||||
mainWS = new WebSocket(import.meta.env.VITE_IMAP_LOTUS);
|
||||
mainWS.addEventListener("open", () => {
|
||||
mainWS.send(JSON.stringify({token: getBearer().slice(7)}));
|
||||
});
|
||||
@ -24,10 +27,8 @@
|
||||
mainWS.send(JSON.stringify({action: "list", args: ["", "*"]}));
|
||||
}
|
||||
if (j.type === "list") {
|
||||
folders = j.value;
|
||||
folders.sort((a, b) => a.Name.localeCompare(b.Name));
|
||||
}
|
||||
// let a = {
|
||||
// === Example output of list command ===
|
||||
// let j = {
|
||||
// type: "list",
|
||||
// value: [
|
||||
// {Attributes: ["\\HasChildren", "\\UnMarked", "\\Archive"], Delimiter: "/", Name: "Archive"},
|
||||
@ -36,12 +37,109 @@
|
||||
// {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/this"},
|
||||
// {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));
|
||||
|
||||
// Setup root folders
|
||||
let INBOX: RootFolder = {role: "Inbox", name: "Inbox", attr: new Set(), children: []};
|
||||
let DRAFTS: RootFolder = {role: "Drafts", name: "Drafts", attr: new Set(["\\\\Drafts"]), children: []};
|
||||
let SENT: RootFolder = {role: "Sent", name: "Sent", attr: new Set(["\\\\Sent"]), children: []};
|
||||
let ARCHIVE: RootFolder = {role: "Archive", name: "Archive", attr: new Set(["\\\\Archive"]), children: []};
|
||||
let JUNK: RootFolder = {role: "Junk", name: "Junk", attr: new Set(["\\\\Junk"]), children: []};
|
||||
let TRASH: RootFolder = {role: "Trash", name: "Trash", attr: new Set(["\\\\Trash"]), children: []};
|
||||
|
||||
// Setup map to find special folders
|
||||
let ROOT: Map<string, RootFolder> = new Map();
|
||||
ROOT.set("Drafts", DRAFTS);
|
||||
ROOT.set("Sent", SENT);
|
||||
ROOT.set("Archive", ARCHIVE);
|
||||
ROOT.set("Junk", JUNK);
|
||||
ROOT.set("Trash", TRASH);
|
||||
|
||||
// Store reference to special folders
|
||||
let ROOTKEYS: Map<string, RootFolder> = 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);
|
||||
});
|
||||
// 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),
|
||||
attr: new Set(x.Attributes),
|
||||
children: [],
|
||||
});
|
||||
});
|
||||
|
||||
// output special folders in order
|
||||
folders = [INBOX, DRAFTS, SENT, ARCHIVE, JUNK, TRASH];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -49,6 +147,10 @@
|
||||
$loginStore = null;
|
||||
localStorage.removeItem("login-session");
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
connectWS();
|
||||
});
|
||||
</script>
|
||||
|
||||
<header>
|
||||
@ -77,12 +179,18 @@
|
||||
{:else}
|
||||
<div id="sidebar">
|
||||
{#each folders as folder}
|
||||
<button class:selected={folder.Name === "INBOX"}>{folder.Name}</button>
|
||||
<button class:selected={folder.name === "INBOX"}>{folder.name}</button>
|
||||
<div>{folder.children.length}</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div id="option-view">
|
||||
<div style="padding:8px;background-color:#bb7900;">Warning: This is currently still under development</div>
|
||||
<button on:click={() => connectWS()}>Connect WS</button>
|
||||
<div>
|
||||
<code>
|
||||
<pre>{JSON.stringify(folders, null, 2)}</pre>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
5
src/types/imap.ts
Normal file
5
src/types/imap.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ImapFolder {
|
||||
Attributes: string[];
|
||||
Delimiter: string;
|
||||
Name: string;
|
||||
}
|
9
src/types/internal.ts
Normal file
9
src/types/internal.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface TreeFolder {
|
||||
name: string;
|
||||
attr: Set<string>;
|
||||
children: TreeFolder[];
|
||||
}
|
||||
|
||||
export interface RootFolder extends TreeFolder {
|
||||
role: string;
|
||||
}
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -4,6 +4,7 @@
|
||||
interface ImportMetaEnv {
|
||||
VITE_SSO_ORIGIN: string;
|
||||
VITE_API_LOTUS: string;
|
||||
VITE_IMAP_LOTUS: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
@ -6,11 +6,13 @@ require (
|
||||
github.com/1f349/mjwt v0.2.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/rs/cors v1.10.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
@ -8,6 +8,8 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@ -16,6 +18,8 @@ github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
|
||||
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -14,9 +15,16 @@ import (
|
||||
"github.com/1f349/mjwt/claims"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
var wsUpgrade = &websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Starting test server")
|
||||
signer, err := mjwt.NewMJwtSignerFromFileOrCreate("Test SSO Service", "private.key.local", rand.Reader, 2048)
|
||||
@ -34,7 +42,7 @@ func ssoServer(signer mjwt.Signer) {
|
||||
r := http.NewServeMux()
|
||||
r.HandleFunc("/popup", func(w http.ResponseWriter, r *http.Request) {
|
||||
ps := claims.NewPermStorage()
|
||||
ps.Set("mail-client")
|
||||
ps.Set("mail:inbox=admin@localhost")
|
||||
accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"d0555671-df9d-42d0-a4d6-94b694251f0b"}, 15*time.Minute, auth.AccessTokenClaims{
|
||||
Perms: ps,
|
||||
})
|
||||
@ -123,13 +131,73 @@ func apiServer(verify mjwt.Verifier) {
|
||||
}
|
||||
json.NewEncoder(rw).Encode(m)
|
||||
}))
|
||||
r.HandleFunc("/v1/lotus/imap", func(rw http.ResponseWriter, req *http.Request) {
|
||||
c, err := wsUpgrade.Upgrade(rw, req, nil)
|
||||
if err != nil {
|
||||
log.Println("WebSocket upgrade error:", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
for {
|
||||
var m map[string]any
|
||||
err = c.ReadJSON(&m)
|
||||
if err != nil {
|
||||
log.Println("WebSocket json error:", err)
|
||||
return
|
||||
}
|
||||
if v, ok := m["token"]; ok {
|
||||
_, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](verify, v.(string))
|
||||
if err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Invalid token"))
|
||||
return
|
||||
}
|
||||
b2 := b.Claims.Perms.Search("mail:inbox=*")
|
||||
if len(b2) != 1 {
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Invalid mail inbox perm"))
|
||||
return
|
||||
}
|
||||
c.WriteMessage(websocket.TextMessage, []byte(`{"auth":"ok"}`))
|
||||
continue
|
||||
} else if vAct, ok := m["action"]; ok {
|
||||
switch vAct.(string) {
|
||||
case "list":
|
||||
log.Println(m)
|
||||
if slices.EqualFunc[[]any, []any, any, any](m["args"].([]any), []any{"", "*"}, func(a1, a2 any) bool {
|
||||
return a1 == a2
|
||||
}) {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(`
|
||||
{
|
||||
"type": "list",
|
||||
"value": [
|
||||
{"Attributes": ["\\HasChildren", "\\UnMarked", "\\Archive"], "Delimiter": "/", "Name": "Archive"},
|
||||
{"Attributes": ["\\HasNoChildren", "\\UnMarked"], "Delimiter": "/", "Name": "Archive/2022"},
|
||||
{"Attributes": ["\\HasNoChildren", "\\UnMarked"], "Delimiter": "/", "Name": "Archive/2023"},
|
||||
{"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/test/sub/folder"},
|
||||
{"Attributes": ["\\HasNoChildren"], "Delimiter": "/", "Name": "INBOX/test/sub/folder/something"},
|
||||
{"Attributes": ["\\HasNoChildren", "\\UnMarked", "\\Drafts"], "Delimiter": "/", "Name": "Drafts"},
|
||||
{"Attributes": ["\\HasNoChildren", "\\Sent"], "Delimiter": "/", "Name": "Sent"},
|
||||
{"Attributes": ["\\HasChildren"], "Delimiter": "/", "Name": "INBOX"}
|
||||
]
|
||||
}
|
||||
`))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
logger := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
log.Println("[API Server]", req.URL.String())
|
||||
r.ServeHTTP(rw, req)
|
||||
})
|
||||
http.ListenAndServe(":9095", serveApiCors.Handler(logger))
|
||||
log.Println("[API Server]", http.ListenAndServe(":9090", r))
|
||||
log.Println("[API Server]", http.ListenAndServe(":9095", serveApiCors.Handler(logger)))
|
||||
}
|
||||
|
||||
func hasPerm(verify mjwt.Verifier, perm string, next func(rw http.ResponseWriter, req *http.Request)) http.Handler {
|
||||
|
Loading…
Reference in New Issue
Block a user