New Imap Handling

master
Auri 2021-10-11 14:29:32 -07:00
parent 01760496ed
commit 10522dc389
17 changed files with 1327 additions and 346 deletions

49
DatabaseLayout.txt Normal file
View File

@ -0,0 +1,49 @@
IMAP
> Boxes
> Messages (ordered by uid)
AETHER
> Conversations
> (Parsed) Messages (indexed by MessageID)
MONGO
> Box
> BoxID
> UIDValidity
> UIDNext
> SeqNext
> ...
> MsgUUID
> MessageID
> Box
> UID
> Participant
Name
Image
Addresses
...
> Messages
> MsgUUID
> Subject
> Participant[]
> Time
> ParsedContent
> Conversations
> Title
> Archived
> LatestMessageTime
> Participant[]
> Messages[]
Box
SEQNO / UID / SUBJECT
1 / 1: aaa
2 / 3: bbb
--- 3 / 7: ccc
---
--- 3 / 8: ddd

188
client/res/logo.svg Executable file
View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="300mm"
height="300mm"
viewBox="0 0 1062.9922 1062.9926"
id="svg4136"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="aether-3.svg">
<defs
id="defs4138" />
<sodipodi:namedview
id="base"
pagecolor="#353b42"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.76576296"
inkscape:cx="531.49606"
inkscape:cy="531.49606"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:snap-text-baseline="true"
inkscape:snap-page="false"
inkscape:snap-global="false"
inkscape:window-width="1920"
inkscape:window-height="1004"
inkscape:window-x="3200"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata4141">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,10.629822)">
<circle
style="opacity:1;fill:#ffd2e5;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5295"
cx="233.86275"
cy="567.77429"
r="126.25221" />
<path
style="fill:#ffb8d8;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 860.32246,878.93418 -687.66986,0 0,-233.22777 700.42601,0 z"
id="path5293"
inkscape:connector-curvature="0" />
<circle
style="opacity:1;fill:#ffb8d8;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5287"
cx="163.87477"
cy="722.06995"
r="156.9731" />
<circle
style="opacity:1;fill:#ffd2e5;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5289"
cx="837.90277"
cy="494.0274"
r="119.03172" />
<circle
style="opacity:1;fill:#ffb8d8;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5291"
cx="860.67151"
cy="682.90381"
r="196.40234" />
<rect
style="fill:#add2ff;fill-opacity:1;stroke:#add2ff;stroke-width:36.45346451;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4684"
width="623.53394"
height="385.3652"
x="210.59846"
y="376.41931" />
<path
style="fill:#c0dcff;fill-opacity:1;fill-rule:evenodd;stroke:#c0dcff;stroke-width:36.45560837;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 523.25141,579.2656 210.45456,760.37921 210.4352,427.08178 Z"
id="path4712-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#a0cbff;fill-opacity:1;fill-rule:evenodd;stroke:#a0cbff;stroke-width:36.45346451;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 523.25141,579.2656 310.88101,182.5189 0.0195,-333.29958 z"
id="path4712"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<circle
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:35;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5121"
cx="475.444"
cy="495.45108"
r="59.515858" />
<path
style="fill:#7ab6ff;fill-opacity:1;fill-rule:evenodd;stroke:#7ab6ff;stroke-width:36.45346451;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 210.57896,398.74408 312.67245,182.60457 310.90046,-181.58151 0,-23.36732 -623.57291,0 z"
id="path4729"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:390.96878052px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#538fd7;fill-opacity:1;stroke:#64a4ef;stroke-width:45.7280218;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;"
x="362.98376"
y="521.09644"
id="text5153-8"
sodipodi:linespacing="125%"
transform="scale(1.0648707,0.93908114)"><tspan
sodipodi:role="line"
id="tspan5155-2"
x="362.98376"
y="521.09644"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#538fd7;fill-opacity:1;stroke:#64a4ef;stroke-width:45.7280218;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;">A</tspan></text>
<path
style="fill:#538fd7;fill-opacity:1;fill-rule:evenodd;stroke:#538fd7;stroke-width:38.17992401;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 833.28864,370.91916 521.48187,170.05232 211.44219,369.79379 l 0,25.70429 621.84645,0 z"
id="path4729-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:390.96878052px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#faeefa;fill-opacity:1;stroke:none;stroke-width:4.96040297;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="362.98376"
y="521.09644"
id="text5153"
sodipodi:linespacing="125%"
transform="scale(1.0648707,0.93908114)"><tspan
sodipodi:role="line"
id="tspan5155"
x="362.98376"
y="521.09644"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#faeefa;fill-opacity:1;stroke:none;stroke-width:4.96040297;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">A</tspan></text>
<circle
style="opacity:1;fill:#ff8fbe;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5297-8"
cx="848.87189"
cy="698.89874"
r="121.39309" />
<circle
style="opacity:1;fill:#ff8fbe;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5297"
cx="760.0448"
cy="756.04407"
r="64.481049" />
<circle
style="opacity:1;fill:#ff8fbe;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5299"
cx="295.05356"
cy="764.54852"
r="65.467445" />
<circle
style="opacity:1;fill:#ff8fbe;fill-opacity:1;stroke:none;stroke-width:4.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5299-5"
cx="214.9558"
cy="752.37793"
r="79.144768" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -68,6 +68,12 @@ export default function App() {
contacts={contacts[account.id]} conversation={conversation}/>}
</div>
</Fragment>}
{!account && <div class='grid place-items-center w-full bg-gray-50 pr-18'>
<div class='hue-rotate-180 brightness-50 saturate-25'>
<img src='../../client/res/logo.svg' width={256} height={256} alt='Loading'
class='w-[256px] h-[256px] animate-pulse grayscale sepia'/>
</div>
</div>}
</div>
);
}

View File

@ -6,7 +6,7 @@
"main": "build/Main.js",
"scripts": {
"dev": "nodemon",
"build": "tsc --project tsconfig.json",
"build": "tsc --project tsconfig.json --incremental",
"clean": "find build -name '*' -not -name 'package.json' -not -path 'build/node_modules*' -not -name 'build' -delete"
},
"repository": {

View File

@ -30,5 +30,8 @@
"bugs": {
"url": "https://github.com/Aurailus/Aether/issues"
},
"homepage": "https://github.com/Aurailus/Aether#readme"
"homepage": "https://github.com/Aurailus/Aether#readme",
"dependencies": {
"mongoose": "^5.13.8"
}
}

View File

@ -57,7 +57,6 @@ module.exports = {
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-parameter-properties": "off",

277
server/package-lock.json generated
View File

@ -237,6 +237,36 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"@typegoose/typegoose": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-8.2.0.tgz",
"integrity": "sha512-iibEA5V2FqtadURFFT2Anq/NnP8oDnlnuMFmccJsBLxanEoNH78hAbdJ9GFoND6BakFInF9BWuhZfpo2OXOb0Q==",
"requires": {
"lodash": "^4.17.20",
"loglevel": "^1.7.0",
"reflect-metadata": "^0.1.13",
"semver": "^7.3.2",
"tslib": "^2.3.0"
},
"dependencies": {
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"@types/bson": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz",
"integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==",
"requires": {
"@types/node": "*"
}
},
"@types/imap": {
"version": "0.8.35",
"resolved": "https://registry.npmjs.org/@types/imap/-/imap-0.8.35.tgz",
@ -261,25 +291,20 @@
"@types/node": "*"
}
},
"@types/mongodb": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz",
"integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==",
"requires": {
"@types/bson": "*",
"@types/node": "*"
}
},
"@types/node": {
"version": "16.7.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.2.tgz",
"integrity": "sha512-TbG4TOx9hng8FKxaVrCisdaxKxqEwJ3zwHoCWXZ0Jw6mnvTInpaB99/2Cy4+XxpXtjNv9/TgfGSvZFyfV/t8Fw=="
},
"@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
},
"@types/whatwg-url": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
"requires": {
"@types/node": "*",
"@types/webidl-conversions": "*"
}
},
"@typescript-eslint/eslint-plugin": {
"version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.3.tgz",
@ -514,17 +539,55 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"bl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"requires": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
},
"boolean": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.1.4.tgz",
@ -576,21 +639,9 @@
}
},
"bson": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.1.tgz",
"integrity": "sha512-XqFP74pbTVLyLy5KFxVfTUyRrC1mgOlmu/iXHfXqfCKT59jyP9lwbotGfbN59cHBRbJSamZNkrSopjv+N0SqAA==",
"requires": {
"buffer": "^5.6.0"
}
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg=="
},
"buffer-crc32": {
"version": "0.2.13",
@ -1619,11 +1670,6 @@
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"dev": true
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
@ -1880,8 +1926,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.clonedeep": {
"version": "4.5.0",
@ -1920,6 +1965,11 @@
}
}
},
"loglevel": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw=="
},
"lowercase-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
@ -1930,7 +1980,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
@ -2031,54 +2080,98 @@
}
},
"mongodb": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.1.tgz",
"integrity": "sha512-fbACrWEyvr6yl0sSiCGV0sqEiBwTtDJ8iSojmkDjAfw9JnOZSAkUyv9seFSPYhPPKwxp1PDtyjvBNfMDz0WBLQ==",
"version": "3.6.11",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz",
"integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==",
"requires": {
"bson": "^4.5.1",
"denque": "^1.5.0",
"mongodb-connection-string-url": "^2.0.0",
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"optional-require": "^1.0.3",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
}
},
"mongodb-connection-string-url": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.0.0.tgz",
"integrity": "sha512-M0I1vyLoq5+HQTuPSJWbt+hIXsMCfE8sS1fS5mvP9R2DOMoi2ZD32yWqgBIITyu0dFu4qtS50erxKjvUeBiyog==",
"requires": {
"@types/whatwg-url": "^8.2.1",
"whatwg-url": "^9.1.0"
}
},
"mongoose": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.1.tgz",
"integrity": "sha512-WESkAtJuJqXKjiQj+HiL3Ipr6eLWx9RIrjCE2HzxScUApnFLXSHdd5gGCeEE3Pl+qcill4fGYy/uysThCMQ6PQ==",
"version": "5.13.8",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.8.tgz",
"integrity": "sha512-z3d+qei9Dem/LxRcJi0cdGPKzQnYk71oHEsEfYm17JA/vLiAbJiGuBS2hW7vkd9afkPAqu3KsPZh2ax0c5iPQw==",
"requires": {
"bson": "^4.2.2",
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
"kareem": "2.3.2",
"mongodb": "4.1.1",
"mongodb": "3.6.11",
"mongoose-legacy-pluralize": "1.0.2",
"mpath": "0.8.3",
"mquery": "4.0.0",
"mquery": "3.2.5",
"ms": "2.1.2",
"optional-require": "1.0.x",
"regexp-clone": "1.0.0",
"safe-buffer": "5.2.1",
"sift": "13.5.2",
"sliced": "1.0.1"
},
"dependencies": {
"bson": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg=="
},
"mongodb": {
"version": "3.6.11",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz",
"integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==",
"requires": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"optional-require": "^1.0.3",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"mongoose-legacy-pluralize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
"integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ=="
},
"mpath": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz",
"integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA=="
},
"mquery": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz",
"integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==",
"requires": {
"debug": "4.x",
"bluebird": "3.5.1",
"debug": "3.1.0",
"regexp-clone": "^1.0.0",
"safe-buffer": "5.1.2",
"sliced": "1.0.1"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"ms": {
@ -2175,6 +2268,11 @@
"wrappy": "1"
}
},
"optional-require": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
"integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA=="
},
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -2276,8 +2374,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"progress": {
"version": "2.0.3",
@ -2311,7 +2408,8 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"pupa": {
"version": "2.1.1",
@ -2468,8 +2566,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"saslprep": {
"version": "1.0.3",
@ -2793,14 +2890,6 @@
"nopt": "~1.0.10"
}
},
"tr46": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
"requires": {
"punycode": "^2.1.1"
}
},
"ts-node": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz",
@ -2881,14 +2970,6 @@
"is-typedarray": "^1.0.0"
}
},
"typegoose": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/typegoose/-/typegoose-5.9.1.tgz",
"integrity": "sha512-D+vMhNyZeKBZHrmJFZwOodl3T9W2NOXY+hbnW/f1n60oEL8+L15eryFc9C6fAKrlnkgpui+kdQnNXsLwx2MgCw==",
"requires": {
"reflect-metadata": "^0.1.13"
}
},
"typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
@ -2985,8 +3066,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"v8-compile-cache": {
"version": "2.3.0",
@ -2994,20 +3074,6 @@
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
},
"whatwg-url": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
"requires": {
"tr46": "^2.1.0",
"webidl-conversions": "^6.1.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -3059,8 +3125,7 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yauzl": {
"version": "2.10.0",

View File

@ -7,7 +7,7 @@
"scripts": {
"dev": "nodemon",
"lint": "eslint -c .eslintrc.js src/**/*.ts",
"build": "tsc --project tsconfig.json"
"build": "tsc --project tsconfig.json --incremental"
},
"repository": {
"type": "git",
@ -25,7 +25,7 @@
"src"
],
"ext": ".ts,.tsx,.html",
"exec": "npm run lint & npm run build && electron .",
"exec": "npm run lint & (npm run build && electron .)",
"quiet": true
},
"bugs": {
@ -45,14 +45,14 @@
"eslint-plugin-jsdoc": "^36.0.8"
},
"dependencies": {
"@typegoose/typegoose": "^8.2.0",
"common": "file:../common/build",
"graphql": "^15.5.0",
"imap": "^0.8.19",
"log4js": "^6.3.0",
"md5": "^2.3.0",
"mongodb": "^4.1.1",
"mongoose": "^6.0.1",
"tslib": "^2.2.0",
"typegoose": "^5.9.1"
"mongodb": "^3.6.11",
"mongoose": "^5.13.8",
"tslib": "^2.2.0"
}
}

View File

@ -1,148 +1,372 @@
import Message from './Message';
import Conversation from './Conversation';
import Imap, { ConnectionProperties, Message as RawMessage, MailboxType } from './Imap';
import Imap from 'imap';
import { ObjectID } from 'mongodb'
interface Contact {
name: string;
addresses: Set<string>;
}
// import Message from './Message';
// import Conversation from './Conversation';
import Log from './Log';
import * as DB from './data/Data';
import ImapController from './imap/ImapController';
// interface Contact {
// name: string;
// addresses: Set<string>;
// }
export default class Account {
private imap: Imap;
private contacts: Contact[] = [];
private conversations: Conversation[] = [];
private name: string;
private image: string;
private address: string;
private unread: boolean;
private accountID: ObjectID;
private conn: ImapController;
// private contacts: Contact[] = [];
// private conversations: Conversation[] = [];
constructor(name: string, image: string, connection: ConnectionProperties) {
this.name = name;
this.image = image;
this.address = connection.username;
this.unread = true;
constructor(data: DB.Account) {
this.accountID = data._id;
this.address = data.address;
Log.info('Created account %s', data.address);
this.imap = new Imap(connection);
this.conn = new ImapController({
user: data.address,
password: data.password,
host: data.host,
port: data.port,
tls: data.tls
});
}
async connect() {
await this.imap.connect();
async init() {
await this.conn.connect();
const messages = await this.fetchAllMessages();
this.conversations = this.createConversations(messages).filter(c => c.active);
this.contacts = this.createContacts(messages);
Log.perfStart('Synchronizing ' + this.address);
const remoteBoxes = await this.getBoxes();
await this.synchronizeBoxes(remoteBoxes);
await this.synchronizeMessages(remoteBoxes);
Log.perfEnd('Synchronizing ' + this.address);
// Log.info('Connected to %s', this.data.address);
// await this.synchronizeData();
// Log.perfEnd('Synchronizing ' + this.data.address);
// const messages = await this.fetchAllMessages();
// this.conversations = this.createConversations(messages).filter(c => c.active);
// this.contacts = this.createContacts(messages);
}
getName() {
return this.name;
async synchronizeBoxes(remoteBoxes: Map<string, Imap.Box>): Promise<void> {
const currentBoxes = await DB.MailboxModel.find({ account: this.accountID });
await Promise.all([ ...remoteBoxes.values() ].map(async box => {
const existing = currentBoxes.filter(b => b.path === box.name)[0];
if (!existing) await this.addNewBox(box);
else await this.refreshExistingBox(box, existing);
}));
}
getAddress() {
return this.address;
private async addNewBox(box: Imap.Box) {
await DB.MailboxModel.create({
account: this.accountID,
name: box.name, // TODO: This
path: box.name,
delimiter: '.', // TODO: and this
type: DB.MailboxType.Inbox,
treeTypes: new Set([ DB.MailboxType.Inbox ]),
parent: undefined, // and this
uidValidity: box.uidvalidity,
uidNext: 1,
} as DB.Create<DB.Mailbox>);
}
getImage() {
return this.image;
private async refreshExistingBox(remote: Imap.Box, _existing: DB.Mailbox) {
Log.debug('existing box ' + remote.name);
}
hasUnreads() {
return this.unread;
}
getConversations() {
return this.conversations;
}
getContacts() {
return this.contacts;
}
getMessages(_messages: string[]): Message[] {
return [{
id: 'AOUEOAEu',
date: new Date(),
from: 'me@auri.xyz',
to: [ 'nicole@aurailus.design' ],
content: '<p>Lorem ipsum dolor sit amet.</p>'
}];
}
async fetchAllMessages(): Promise<RawMessage[]> {
const boxes = (await this.imap.listBoxes()).filter(box =>
!box.treeTypes.has(MailboxType.Spam) && !box.treeTypes.has(MailboxType.Trash));
let allMeta: RawMessage[] = [];
for (let box of boxes) {
await this.imap.openBox(box.path);
const meta = await this.imap.fetchMessages('1:*');
Object.keys(meta).forEach(id => allMeta.push(meta[id]));
}
allMeta = allMeta.sort((a, b) => +a.date - +b.date);
return allMeta;
}
private createConversations(messages: RawMessage[]): Conversation[] {
const conversations: Conversation[] = [];
messages.forEach(message => {
if (message.replyTo) {
for (let conversation of conversations) {
for (let reference of [ ...message.references, message.replyTo ]) {
if (conversation.messages.has(reference)) {
conversation.date = message.date;
conversation.messages.add(message.messageId);
conversation.title = this.cleanSubjectLine(message.subject);
conversation.active = conversation.active || message.active;
message.to.forEach(p => conversation.participants.add(p.address));
conversation.participants.add(message.from.address);
return;
}
}
async synchronizeMessages(remoteBoxes: Map<string, Imap.Box>): Promise<void> {
const currentBoxes = await DB.MailboxModel.find({ account: this.accountID });
await Promise.all(currentBoxes.map(async box => {
const remote = remoteBoxes.get(box.path)!;
console.log(box.uidNext, remote.uidnext);
// if (box.uidValidity !== remote.uidvalidity) {
// // Reacquire existing messages
// }
if (box.uidNext !== remote.uidnext) {
// Get new messages
const messages = await (await this.conn.get(box.path)).fetchMessagesByUID(`${box.uidNext}:*`);
await DB.MailboxModel.updateOne({ _id: box._id }, { uidNext: remote.uidnext });
if (messages.size > 0) {
console.log('adding ' + messages.size + ' messages.');
await DB.MessageModel.insertMany([ ...messages.keys() ].map(uid => {
const message = messages.get(uid)!;
const headers = this.parseHeaders(message.headers);
return {
account: this.accountID,
box: box._id,
uid: uid,
messageId: headers.get('MESSAGE-ID') ?? '[!DATE:' + (+message.attrs.date) + ']',
subject: this.cleanSubject(headers.get('SUBJECT')),
date: message.attrs.date,
} as DB.Message;
}));
}
}
conversations.push({
title: this.cleanSubjectLine(message.subject),
messages: new Set([ message.messageId ]),
date: message.date,
active: message.active,
participants: new Set([ message.from.address, ...message.to.map(p => p.address) ])
});
});
conversations.forEach(conversation => {
conversation.participants.delete(this.address);
});
return conversations.sort((a, b) => +a.date - +b.date);
}));
}
private createContacts(messages: RawMessage[]): Contact[] {
const contacts: Contact[] = [];
private async getBoxes(): Promise<Map<string, Imap.Box>> {
Log.perfStart('Getting boxes for ' + this.address);
for (let message of messages) {
[ message.from, ...message.to ].forEach(participant => {
for (let contact of contacts) {
if (contact.addresses.has(participant.address)) {
if (participant.name) contact.name = participant.name;
return;
}
}
const remoteBoxes = await (await this.conn.get()).getBoxes();
const boxReqs: Promise<void>[] = [];
const boxes: Map<string, Imap.Box> = new Map();
contacts.push({
name: participant.name ?? participant.address,
addresses: new Set([ participant.address ])
});
const reqBoxesRecursively = (tree: Imap.MailBoxes, path: string = '') => {
Object.keys(tree).forEach(name => {
boxReqs.push((async () => {
const box = (await this.conn.get(path + name)).getOpenBoxProps();
boxes.set(path + name, box);
})());
if (tree[name].children) reqBoxesRecursively(tree[name].children,
path + name + tree[name].delimiter);
});
}
return contacts;
reqBoxesRecursively(remoteBoxes);
await Promise.all(boxReqs);
Log.perfEnd('Getting boxes for ' + this.address);
return boxes;
}
private cleanSubjectLine(subject: string = '') {
private parseHeaders(rawHeaders: string): Map<string, string> {
const headers: Map<string, string> = new Map();
rawHeaders
.split(/\r?\n(?=[A-z:\-_]+)/g)
.map(h => h.trim())
.filter(h => h)
.forEach(h => {
const delimiter = h.indexOf(':');
const name = h.substr(0, delimiter).trim();
const value = h.substr(delimiter + 1).trim();
headers.set(name.toUpperCase(), value);
});
return headers;
}
private cleanSubject(subject: string = '') {
return subject.replace(/^((re|fwd?|b?cc)(:| ) *)*/gi, '').trim();
}
// async synchronizeData(): Promise<void> {
// const existingBoxes = await DB.MailboxModel.find({ account: this.data._id });
// const remoteBoxes = (await this.imap.listBoxes());
// const newBoxes: (Mailbox & { uidValidity: number })[] = [];
// const boxValidityChanged: { _id: ObjectID, uidValidity: number }[] = [];
// const removedBoxes: Set<ObjectID> = new Set(existingBoxes.map(box => box._id));
// for (let remote of remoteBoxes) {
// if (!remote.treeTypes.has(DB.MailboxType.Inbox) &&
// !remote.treeTypes.has(DB.MailboxType.Sent) &&
// !remote.treeTypes.has(DB.MailboxType.Archives)) continue;
// const existing = existingBoxes.filter(box => box.path === remote.path)[0];
// let uidValidity = (await this.imap.openBox(remote.path)).uidvalidity;
// // Log.debug('Opened %s', remote.path);
// if (!existing) newBoxes.push({ ...remote, uidValidity });
// else {
// removedBoxes.delete(existing._id);
// if (existing.uidValidity != uidValidity) boxValidityChanged.push({ _id: existing._id, uidValidity });
// }
// };
// let createdIDs: Map<string, ObjectID> = new Map();
// for (let box of newBoxes) {
// createdIDs.set(box.path, (await DB.MailboxModel.create({
// name: box.name,
// path: box.path,
// account: this.data._id,
// delimiter: box.delimiter,
// type: box.type,
// treeTypes: box.treeTypes,
// parent: (await DB.MailboxModel.findOne({ path: box.parent }))?._id,
// uidValidity: box.uidValidity,
// uidNext: 1,
// } as DB.Create<DB.Mailbox>)).id);
// }
// await Promise.all(boxValidityChanged.map(({ _id, uidValidity }) =>
// DB.MailboxModel.updateOne({ _id }, { uidValidity, uidNext: 1 })));
// await DB.MailboxModel.deleteMany({ _id: { $in: [ ...removedBoxes ] } });
// for (let box of await DB.MailboxModel.find({ account: this.data._id })) {
// await this.imap.openBox(box.path);
// const meta = Object.values(await this.imap.fetchMessages(box.uidNext + ':*'));
// const contacts: { name: string; addresses: Set<string> }[] = [];
// for (let m of meta) {
// [ m.from, ...m.to ].forEach(participant => {
// for (let contact of contacts) {
// if (contact.addresses.has(participant.address)) {
// if (participant.name) contact.name = participant.name;
// return;
// }
// }
// contacts.push({
// name: participant.name ?? participant.address,
// addresses: new Set([ participant.address ])
// });
// });
// }
// await Promise.all(contacts.map(async contact => {
// const addresses = [ ...contact.addresses ];
// await DB.ContactModel.updateOne(
// { addresses: { $elemMatch: { $in: addresses } as any } },
// {
// $set: { name: contact.name },
// $addToSet: { addresses }
// },
// { upsert: true });
// }));
// await Promise.all(meta.map(async meta => {
// await DB.MessageModel.updateOne({ messageId: meta.messageId }, {
// box: box._id,
// uid: meta.boxId,
// account: this.data._id,
// $setOnInsert: {
// subject: meta.subject,
// date: meta.date,
// from: (await DB.ContactModel.findOne({ addresses: meta.from.address }))!._id
// }
// } as any as DB.Message,
// { upsert: true });
// }));
// // await DB.MessageIDModel.insertMany(Object.values(meta).map(meta =>
// // ({ messageId: meta.messageId, uid: meta.boxId, account: this.data._id, box: box._id })));
// }
// const boxes = (await this.imap.listBoxes()).filter(box =>
// !box.treeTypes.has(MailboxType.Spam) && !box.treeTypes.has(MailboxType.Trash));
// let allMeta: RawMessage[] = [];
// for (let box of boxes) {
// await this.imap.openBox(box.path);
// const meta = await this.imap.fetchMessages('1:*');
// Object.keys(meta).forEach(id => allMeta.push(meta[id]));
// }
// }
// getName() {
// return this.name;
// }
// getAddress() {
// return this.address;
// }
// getImage() {
// return this.image;
// }
// hasUnreads() {
// return this.unread;
// }
// getConversations() {
// return this.conversations;
// }
// getContacts() {
// return this.contacts;
// }
// getMessages(_messages: string[]): Message[] {
// return [{
// id: 'AOUEOAEu',
// date: new Date(),
// from: 'me@auri.xyz',
// to: [ 'nicole@aurailus.design' ],
// content: '<p>Lorem ipsum dolor sit amet.</p>'
// }];
// }
// async fetchAllMessages(): Promise<RawMessage[]> {
// const boxes = (await this.imap.listBoxes()).filter(box =>
// !box.treeTypes.has(MailboxType.Spam) && !box.treeTypes.has(MailboxType.Trash));
// let allMeta: RawMessage[] = [];
// for (let box of boxes) {
// await this.imap.openBox(box.path);
// const meta = await this.imap.fetchMessages('1:*');
// Object.keys(meta).forEach(id => allMeta.push(meta[id]));
// }
// allMeta = allMeta.sort((a, b) => +a.date - +b.date);
// return allMeta;
// }
// private createConversations(messages: RawMessage[]): Conversation[] {
// const conversations: Conversation[] = [];
// messages.forEach(message => {
// if (message.replyTo) {
// for (let conversation of conversations) {
// for (let reference of [ ...message.references, message.replyTo ]) {
// if (conversation.messages.has(reference)) {
// conversation.date = message.date;
// conversation.messages.add(message.messageId);
// conversation.title = this.cleanSubjectLine(message.subject);
// conversation.active = conversation.active || message.active;
// message.to.forEach(p => conversation.participants.add(p.address));
// conversation.participants.add(message.from.address);
// return;
// }
// }
// }
// }
// conversations.push({
// title: this.cleanSubjectLine(message.subject),
// messages: new Set([ message.messageId ]),
// date: message.date,
// active: message.active,
// participants: new Set([ message.from.address, ...message.to.map(p => p.address) ])
// });
// });
// conversations.forEach(conversation => {
// conversation.participants.delete(this.address);
// });
// return conversations.sort((a, b) => +a.date - +b.date);
// }
// private createContacts(messages: RawMessage[]): Contact[] {
// const contacts: Contact[] = [];
// for (let message of messages) {
// [ message.from, ...message.to ].forEach(participant => {
// for (let contact of contacts) {
// if (contact.addresses.has(participant.address)) {
// if (participant.name) contact.name = participant.name;
// return;
// }
// }
// contacts.push({
// name: participant.name ?? participant.address,
// addresses: new Set([ participant.address ])
// });
// });
// }
// return contacts;
// }
};

View File

@ -1,61 +1,61 @@
import { buildSchema } from 'graphql';
// import { buildSchema } from 'graphql';
import { Type, SCHEMA } from 'common/graph';
// import { Type, SCHEMA } from 'common/graph';
import Message from './Message';
import Account from './Account';
import Conversation from './Conversation';
// import Message from './Message';
// import Account from './Account';
// import Conversation from './Conversation';
export interface Context {
accounts: Record<Type.ID, Account>;
}
// export interface Context {
// accounts: Record<Type.ID, Account>;
// }
export const Schema = buildSchema(SCHEMA);
// export const Schema = buildSchema(SCHEMA);
function messageResolver(message: Message) {
return {
id: message.id,
date: message.date,
from: message.from,
to: message.to,
// function messageResolver(message: Message) {
// return {
// id: message.id,
// date: message.date,
// from: message.from,
// to: message.to,
html: () => message.content,
markdown: () => message.content
};
}
// html: () => message.content,
// markdown: () => message.content
// };
// }
function conversationResolver(conversation: Conversation, id: string) {
return {
id: id,
unread: false,
title: conversation.title,
lastMessage: conversation.date,
messages: conversation.messages,
participants: conversation.participants
};
}
// function conversationResolver(conversation: Conversation, id: string) {
// return {
// id: id,
// unread: false,
// title: conversation.title,
// lastMessage: conversation.date,
// messages: conversation.messages,
// participants: conversation.participants
// };
// }
function accountResolver(account: Account, id: string) {
return {
id: id,
name: account.getName(),
image: account.getImage(),
address: account.getAddress(),
unread: account.hasUnreads(),
// function accountResolver(account: Account, id: string) {
// return {
// id: id,
// name: account.getName(),
// image: account.getImage(),
// address: account.getAddress(),
// unread: account.hasUnreads(),
messages: [],
contacts: () => account.getContacts(),
conversations: () => {
const conversations = account.getConversations();
for (let key in conversations) if (!conversations[key].active) delete conversations[key];
return Object.keys(conversations).map(id => conversationResolver(conversations[id as any], id));
}
};
}
// messages: [],
// contacts: () => account.getContacts(),
// conversations: () => {
// const conversations = account.getConversations();
// for (let key in conversations) if (!conversations[key].active) delete conversations[key];
// return Object.keys(conversations).map(id => conversationResolver(conversations[id as any], id));
// }
// };
// }
export const Resolver = {
accounts: (_: any, ctx: Context) => Object.keys(ctx.accounts).map(id => accountResolver(ctx.accounts[id], id)),
account: ({ account: id }: { account: string }, ctx: Context) => accountResolver(ctx.accounts[id], id),
messages: async ({ account, ids }: { account: string; ids: string[] }, ctx: Context) =>
(await ctx.accounts[account].getMessages(ids)).map(msg => messageResolver(msg))
};
// export const Resolver = {
// accounts: (_: any, ctx: Context) => Object.keys(ctx.accounts).map(id => accountResolver(ctx.accounts[id], id)),
// account: ({ account: id }: { account: string }, ctx: Context) => accountResolver(ctx.accounts[id], id),
// messages: async ({ account, ids }: { account: string; ids: string[] }, ctx: Context) =>
// (await ctx.accounts[account].getMessages(ids)).map(msg => messageResolver(msg))
// };

View File

@ -1,6 +1,7 @@
import md5 from 'md5';
import RawImap, { MailBoxes as RawBoxes } from 'imap';
import { MailboxType } from './data/Data';
import RawImap, { MailBoxes as RawBoxes, Box as RawBox } from 'imap';
/** Credentials and properties used to establish an IMAP connection. */
@ -12,27 +13,6 @@ export interface ConnectionProperties {
tls: boolean;
}
/**
* Mailbox types (attributes).
* Only the attributes relevant to Aether are included,
* and some are named differently to better match interface language.
*/
export enum MailboxType {
Box = 'NORMAL_BOX',
All = '\\All',
Archive = '\\Archive',
Drafts = '\\Drafts',
Starred = '\\Flagged',
Important = '\\Important',
Inbox = '\\Inbox',
Spam = '\\Junk',
Sent = '\\Sent',
Trash = '\\Trash'
}
/**
* Message flags (keywords).
* Only the flags relevant to Aether are included,
@ -106,7 +86,7 @@ export interface Message {
*/
export default class Imap {
private raw: RawImap;
raw: RawImap;
private connected: boolean = false;
private boxes: Mailbox[] = [];
@ -189,9 +169,11 @@ export default class Imap {
for (let boxName in boxes) {
if ({}.hasOwnProperty.call(boxes, boxName)) {
const box = boxes[boxName];
const thisPath = path + boxName + box.delimiter;
const type = (box as any).special_use_attrib as MailboxType ??
(boxName === 'INBOX' ? MailboxType.Inbox : MailboxType.Box);
(boxName === 'INBOX' ? MailboxType.Inbox :
boxName.match(/^Archives?$/gi) !== null ? MailboxType.Archives : MailboxType.Box);
const thisTreeTypes = new Set([ ...treeTypes, type ]);
foundBoxes.push({
@ -223,7 +205,7 @@ export default class Imap {
* @returns a raw node-imap box instance.
*/
openBox(box: string): Promise<Mailbox> {
openBox(box: string): Promise<RawBox> {
return new Promise((resolve, reject) => {
if (!this.connected) reject('Cannot get box when the connection is closed.');
@ -233,8 +215,9 @@ export default class Imap {
return;
}
resolve(box);
this.box = this.boxes.filter(b => b.path === box.name)[0];
resolve(this.box);
// resolve(this.box);
});
});
}
@ -269,8 +252,9 @@ export default class Imap {
const id = parseInt(idStr, 10);
const headers = bodies[id].headers;
const attrs = bodies[id].attrs;
const messageId = headers['MESSAGE-ID'] || `HASH:${md5(attrs.date.toString())}:${md5(headers.SUBJECT)}`;
messages[headers['MESSAGE-ID']] = {
messages[messageId] = {
to: this.parseParticipants(headers.TO ?? ''),
from: this.parseParticipants(headers.FROM ?? '')[0],
subject: headers.SUBJECT,
@ -278,7 +262,7 @@ export default class Imap {
id: id,
boxId: attrs.uid,
messageId: headers['MESSAGE-ID'] || `HASH:${md5(attrs.date.toString())}:${md5(headers.SUBJECT)}`,
messageId: messageId,
active: attrs.flags.has(MessageFlag.Active) || this.box!.type === MailboxType.Inbox,
replyTo: headers['IN-REPLY-TO'],
@ -364,10 +348,10 @@ export default class Imap {
private parseParticipants(header: string): Participant[] {
return header.split(',').map(raw => {
const delimiter = raw.indexOf('<');
if (delimiter === -1) return { name: undefined, address: raw.trim() };
if (delimiter === -1) return { name: undefined, address: raw.trim().toLowerCase() };
const name = raw.substr(0, delimiter).replace(/^[\s'"]+/g, '').trim().replace(/[\s'"]+$/g, '');
const address = raw.substr(delimiter + 1).replace(/[<>]/g, '').replace(/^[\s'"]+/g, '').trim().replace(/[\s'"]+$/g, '');
const address = raw.substr(delimiter + 1).replace(/[<>]/g, '').replace(/^[\s'"]+/g, '').trim().replace(/[\s'"]+$/g, '').toLowerCase();
return { name: name ? name : undefined, address };
});

View File

@ -20,17 +20,17 @@ log4js.configure({
const logger = log4js.getLogger();
const activePerfs: Record<string, [ number, number ]> = {};
const activePerfs: Record<string, bigint> = {};
const perfStart = (identifier: string) => {
activePerfs[identifier] = process.hrtime();
activePerfs[identifier] = process.hrtime.bigint();
};
const perfEnd = (identifier: string) => {
let perf = activePerfs[identifier];
if (!perf) logger.warn('Attempted to perf invalid identifier \'%s\'.', identifier);
let start = activePerfs[identifier];
if (!start) logger.warn('Attempted to perf invalid identifier \'%s\'.', identifier);
else {
const elapsed = process.hrtime(perf)[1] / 1000000;
const elapsed = Number((process.hrtime.bigint() - start) / BigInt(10000)) / 100;
// @ts-ignore
logger.perf('%s took %s ms.', identifier, elapsed.toFixed(3));
delete activePerfs[identifier];

View File

@ -1,38 +1,64 @@
import fs from 'fs';
import { graphql } from 'graphql';
// import fs from 'fs';
import mongoose from 'mongoose';
// import { graphql } from 'graphql';
import { ipcMain } from 'electron';
import Log from './Log';
import Account from './Account';
import { Schema, Resolver } from './Graph';
import { openWindow } from './ElectronWindow';
import * as DB from './data/Data';
// import { Schema, Resolver } from './Graph';
// import { openWindow } from './ElectronWindow';
Log.setLogLevel('debug');
Log.setLogLevel('all');
(async () => {
const accounts: Record<string, Account> = {
'0': new Account('Personal', '../../client/res/user-home.png', {
username: 'me@auri.xyz',
password: fs.readFileSync(__dirname + '/../../client/pw.txt').toString().trim(),
host: 'mail.hover.com',
port: 993,
tls: true
}),
'1': new Account('Work', '../../client/res/user-work.png', {
username: 'nicole@aurailus.design',
password: fs.readFileSync(__dirname + '/../../client/pw.txt').toString().trim(),
host: 'mail.hover.com',
port: 993,
tls: true
})
};
// openWindow();
Log.info('Initialized.');
await Promise.all(Object.values(accounts).map(account => account.connect()));
await mongoose.connect('mongodb://localhost:27017/aether',{
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: true
} as mongoose.ConnectOptions);
ipcMain.handle('graphql', async (_, req: { query: string; data: any }) => {
return graphql(Schema, req.query, Resolver, { accounts }, req.data);
Log.info('Connected to Mongoose.');
// await DB.AccountModel.deleteMany({});
// await DB.AccountModel.insertMany([{
// name: 'Personal',
// image: '../../client/res/user-home.png',
// address: 'me@auri.xyz',
// password: fs.readFileSync(__dirname + '/../../client/pw.txt').toString().trim(),
// host: 'mail.hover.com',
// port: 993,
// tls: true
// }, {
// name: 'Work',
// image: '../../client/res/user-work.png',
// address: 'nicole@aurailus.design',
// password: fs.readFileSync(__dirname + '/../../client/pw.txt').toString().trim(),
// host: 'mail.hover.com',
// port: '993',
// tls: true
// }] as DB.Create<DB.Account>[]);
Log.perfStart('Initial DB Fetch');
const dbAccounts = (await DB.AccountModel.find({}))!;
Log.perfEnd('Initial DB Fetch');
Log.perfStart('Accounts Connect');
await Promise.all(dbAccounts.map(async dbAccount => {
const account = new Account(dbAccount);
await account.init();
return account;
}));
Log.perfEnd('Accounts Connect');
ipcMain.handle('graphql', async (_, _req: { query: string; data: any }) => {
return { data: { } };
});
openWindow();
// return graphql(Schema, req.query, Resolver, { accounts }, req.data);
// });
})();

View File

@ -1,7 +1,6 @@
export default interface Message {
id: string;
date: Date;
from: string;
to: string[];

137
server/src/data/Data.ts Normal file
View File

@ -0,0 +1,137 @@
import { ObjectID } from 'mongodb';
import { prop, index, Ref, getModelForClass, modelOptions } from '@typegoose/typegoose';
export type Create<T> = Omit<T, 'id' | '_id'>;
export enum MailboxType {
Box = 'NORMAL_BOX',
Archives = 'ARCHIVES',
All = '\\All',
Drafts = '\\Drafts',
Starred = '\\Flagged',
Important = '\\Important',
Inbox = '\\Inbox',
Spam = '\\Junk',
Sent = '\\Sent',
Trash = '\\Trash'
};
@modelOptions({ schemaOptions: { versionKey: false }})
@index({ address: 1 })
export class Account {
id!: string;
_id!: ObjectID;
@prop({ required: true })
name!: string;
@prop()
image?: string;
@prop({ required: true })
address!: string;
@prop({ required: true })
password!: string;
@prop({ required: true })
host!: string;
@prop({ required: true })
port!: number;
@prop({ default: true })
tls!: boolean;
};
export const AccountModel = getModelForClass(Account);
@modelOptions({ schemaOptions: { versionKey: false }})
@index({ account: 1, path: 1 })
export class Mailbox {
id!: string;
_id!: ObjectID;
@prop({ required: true, ref: Account })
account!: Ref<Account>;
@prop({ required: true })
name!: string;
@prop({ required: true })
path!: string;
@prop({ required: true })
delimiter!: string;
@prop({ required: true })
type!: MailboxType;
@prop({ required: true, type: [String] })
private _treeTypes!: MailboxType[];
get treeTypes(): Set<MailboxType> { return new Set(this._treeTypes); }
set treeTypes(treeTypes: Set<MailboxType>) { this._treeTypes = [ ...treeTypes ]; }
@prop({ ref: Mailbox })
parent?: Ref<Mailbox>;
@prop({ required: true })
uidValidity!: number;
@prop({ required: true })
uidNext!: number;
};
export const MailboxModel = getModelForClass(Mailbox);
@modelOptions({ schemaOptions: { versionKey: false }})
export class Contact {
id!: string;
_id!: ObjectID;
@prop({ default: false })
userCreated?: boolean;
@prop({ required: true })
name!: string;
@prop({ default: [], type: [String]})
addresses?: string[];
};
export const ContactModel = getModelForClass(Contact);
@modelOptions({ schemaOptions: { versionKey: false }})
@index({ account: 1, messageId: 1 })
@index({ account: 1, box: 1, uid: 1 })
export class Message {
id!: string;
_id!: ObjectID;
@prop({ required: true, ref: Account })
account!: Ref<Account>;
@prop({ required: true, ref: Mailbox })
box!: Ref<Mailbox>;
@prop({ required: true })
uid!: number;
@prop({ required: true })
messageId!: string;
@prop({ required: true })
subject!: string;
@prop({ required: true })
date!: Date;
// @prop({ required: true, ref: Contact })
// from!: Ref<Contact>;
// @prop({ required: true, ref: Contact })
// to!: Ref<Contact>[];
};
export const MessageModel = getModelForClass(Message);

View File

@ -0,0 +1,233 @@
import Imap from 'imap';
import EventEmitter from 'events';
export enum FetchMode { SEQ, UID };
export type FetchSpecifier = string | string[] | number | number[];
export interface FetchMessage {
headers: string;
attrs: Imap.ImapMessageAttributes;
}
export const FETCH_DEFAULT_BODIES = 'HEADER.FIELDS (FROM TO SUBJECT DATE MESSAGE-ID)';
export interface ConnectionProperties {
user: string;
password: string;
host: string;
port: number;
tls: boolean;
}
export default class ImapConnection {
readonly event: EventEmitter = new EventEmitter();
private conn: Imap;
private connected: boolean = false;
private currentBox: string | null = null;
private currentBoxProps: Imap.Box | null = null;
private operationNext: number = 0;
private operationsPending: Set<number> = new Set();
constructor(connectionProps: ConnectionProperties) {
this.conn = new Imap(connectionProps);
}
/**
* Checks if the connection is idle.
*
* @returns a boolean indicating if the connection is idle.
*/
isIdle(): boolean {
if (!this.connected) throw new Error('Attempted to check if an unconnected connection is idle.');
return this.operationsPending.size === 0;
}
/**
* Tracks and executes the provided function to prevent disconnection
* or box changes occuring while it is in progress. All operations that
* need to access the open box on the remote server should be wrapped
* by this function. Propagates the returned values or errors up.
*
* @param fn - The function to execute.
* @returns the function's return value.
*/
private executeOperation<T = any>(fn: () => Promise<T>): Promise<T> {
return new Promise(async (resolve, reject) => {
if (!this.connected) reject(new Error(
'Attempted to perform an operation on an unconnected connection.'));
const operation = this.operationNext++;
this.operationsPending.add(operation);
try {
this.operationsPending.delete(operation);
resolve(await fn());
if (this.isIdle()) setTimeout(() => this.event.emit('idle'), 0);
}
catch (e: unknown) {
this.operationsPending.delete(operation);
reject(e);
if (this.isIdle()) setTimeout(() => this.event.emit('idle'), 0);
}
});
}
/**
* Initiates the Imap connection, resolves when complete.
*
* @returns a promise that resolves upon connection or rejects with an error.
*/
connect(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.connected) {
reject(new Error('Attempted to connect while already connected.'));
return;
}
this.conn.once('end', () => this.connected = false);
this.conn.once('error', (error: any) => reject(error));
this.conn.once('ready', async () => {
this.connected = true;
resolve();
});
this.conn.connect();
});
}
/**
* Checks if the connection is connected.
*
* @returns a boolean indicating if there is an active connection.
*/
isConnected(): boolean {
return this.connected;
}
/**
* Opens the specified box (safely). Throws if there are pending operations.
*
* @param path - The path of the box to open.
* @returns the box that was opened.
*/
async openBox(path: string): Promise<Imap.Box> {
return this.executeOperation(() => new Promise((resolve, reject) => {
if (!this.connected) reject(new Error('Tried to open a box while not connected.'));
if (!this.isIdle()) reject(new Error('Tried to change box while the connection was not idle.'));
this.currentBox = null;
this.conn.openBox(path, false, (err, box) => {
if (err) reject(err);
else {
this.currentBox = path;
this.currentBoxProps = JSON.parse(JSON.stringify(box));
resolve(this.currentBoxProps!);
}
});
}));
}
/**
* Gets the name of the currently open box, if one is open.
*
* @returns the name of the box that is open, or null if none are open.
*/
getOpenBox(): string | null {
return this.currentBox;
}
/**
* Gets the properties of the currently open box, or throws if there isn't one open.
*
* @returns an Imap.Box for the currently open box.
*/
getOpenBoxProps(): Imap.Box {
if (!this.currentBoxProps) throw new Error('Tried to get box props when there wasn\'t an open box.');
return this.currentBoxProps;
}
/**
* Gets a tree of boxes on the server.
*
* @returns a tree of imap mailboxes.
*/
getBoxes(): Promise<Imap.MailBoxes> {
return new Promise((resolve, reject) => {
this.conn.getBoxes((err, boxes) => {
if (err) reject(err);
else resolve(boxes);
});
});
}
/**
* Fetches messages either by SeqNo or by UID and returns their raw headers and attributes.
*
* @param mode - The mode to fetch with.
* @param query - The query to send to the server.
* @param bodies - The header bodies to fetch.
* @returns a map of messages indexed by UID or SeqNo.
*/
async fetchMessages(mode: FetchMode, query: FetchSpecifier,
bodies: string = FETCH_DEFAULT_BODIES): Promise<Map<number, FetchMessage>> {
return this.executeOperation(() => new Promise((resolve, reject) => {
const fetchRoot = mode === FetchMode.SEQ ? this.conn.seq : this.conn;
const messages: Map<number, FetchMessage> = new Map();
const fetch = fetchRoot.fetch(query, { bodies, struct: false });
fetch.on('error', e => reject(e));
fetch.on('message', (msg, id) => {
messages.set(id, { headers: '', attrs: null as any });
msg.on('body', stream => stream.on('data',
chunk => messages.get(id)!.headers += chunk.toString('utf8')));
msg.once('attributes',
attrs => messages.get(id)!.attrs = attrs);
});
fetch.on('end', () => {
resolve(messages);
});
}));
}
/**
* Fetches messages by SeqNo and returns their raw headers and attributes.
*
* @param query - The query to send to the server.
* @param bodies - The header bodies to fetch.
* @returns a map of messages indexed by SeqNo.
*/
async fetchMessagesBySeqNo(query: FetchSpecifier, bodies?: string): Promise<Map<number, FetchMessage>> {
return this.fetchMessages(FetchMode.SEQ, query, bodies);
}
/**
* Fetches messages by UID and returns their raw headers and attributes.
*
* @param query - The query to send to the server.
* @param bodies - The header bodies to fetch.
* @returns a map of messages indexed by UID.
*/
async fetchMessagesByUID(query: FetchSpecifier, bodies?: string): Promise<Map<number, FetchMessage>> {
return this.fetchMessages(FetchMode.UID, query, bodies);
}
}

View File

@ -0,0 +1,68 @@
import EventEmitter from 'events';
import Log from '../Log';
import ImapConnection, { ConnectionProperties } from './ImapConnection';
export const DEFAULT_CONNECTIONS = 6;
export default class ImapController {
readonly event: EventEmitter = new EventEmitter();
private address: string;
private connected: boolean = false;
private connections: ImapConnection[] = [];
private awaitingIdle: ((conn: ImapConnection) => void)[] = [];
constructor(connectionProps: ConnectionProperties, connectionCount: number = DEFAULT_CONNECTIONS) {
this.address = connectionProps.user;
for (let i = 0; i < connectionCount; i++) this.connections.push(new ImapConnection(connectionProps));
this.connections.forEach(conn => conn.event.on('idle',
() => this.event.emit('idle', conn)));
this.event.on('idle', (conn: ImapConnection) => {
if (this.awaitingIdle.length <= 0) return;
this.awaitingIdle[0](conn);
this.awaitingIdle.splice(0, 1);
});
}
async connect() {
await Promise.all(this.connections.map(async (conn, i) => {
Log.perfStart(`Connection ${i + 1} for ${this.address}`);
await conn.connect();
Log.perfEnd(`Connection ${i + 1} for ${this.address}`);
}));
this.connected = true;
}
async get(path?: string): Promise<ImapConnection> {
if (!this.connected) throw new Error('Tried to access a box when the connections aren\'t connected.');
if (path === undefined) return this.connections[Math.floor(Math.random() * 100000) % this.connections.length];
for (let conn of this.connections) {
if (conn.getOpenBox() === path) {
return conn;
}
}
for (let conn of this.connections) {
if (conn.getOpenBox() === null) {
await conn.openBox(path);
return conn;
}
}
for (let conn of this.connections) {
if (!conn.isIdle()) continue;
await conn.openBox(path);
return conn;
}
return new Promise((resolve) =>
this.awaitingIdle.push((conn: ImapConnection) =>
conn.openBox(path).then(() => resolve(conn))));
}
}