mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2025-12-16 14:56:31 +01:00
Compare commits
No commits in common. "f68dfe0da2f3c7decf6616c653c0bcddfd96ee9b" and "dd6a901ca1c79f5a50c00aea493429ea224796f2" have entirely different histories.
f68dfe0da2
...
dd6a901ca1
11 changed files with 267 additions and 347 deletions
|
|
@ -18,8 +18,6 @@ IMAP_PORT=993 # IMAP port (def
|
|||
IMAP_TLS=true # Use secure TLS connection (true/false)
|
||||
IMAP_AUTH_TIMEOUT=3000 # Authentication timeout in ms
|
||||
IMAP_REFRESH_INTERVAL_SECONDS=60 # Refresh interval for checking new emails
|
||||
IMAP_FETCH_CHUNK=200 # Number of UIDs per fetch chunk during initial load
|
||||
IMAP_CONCURRENCY=6 # Number of concurrent fetch workers during initial load
|
||||
|
||||
# --- HTTP / WEB CONFIGURATION ---
|
||||
HTTP_PORT=3000 # Port
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,5 +2,6 @@
|
|||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
copilot-instructions.md
|
||||
|
||||
node_modules
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,7 @@ const config = {
|
|||
port: Number(process.env.IMAP_PORT),
|
||||
tls: parseBool(process.env.IMAP_TLS),
|
||||
authTimeout: Number(process.env.IMAP_AUTH_TIMEOUT),
|
||||
refreshIntervalSeconds: Number(process.env.IMAP_REFRESH_INTERVAL_SECONDS),
|
||||
fetchChunkSize: Number(process.env.IMAP_FETCH_CHUNK) || 100,
|
||||
fetchConcurrency: Number(process.env.IMAP_CONCURRENCY) || 6
|
||||
refreshIntervalSeconds: Number(process.env.IMAP_REFRESH_INTERVAL_SECONDS)
|
||||
},
|
||||
|
||||
http: {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ class ImapService extends EventEmitter {
|
|||
this.loadedUids = new Set()
|
||||
this.connection = null
|
||||
this.initialLoadDone = false
|
||||
this.loadingInProgress = false
|
||||
}
|
||||
|
||||
async connectAndLoadMessages() {
|
||||
|
|
@ -168,58 +167,28 @@ class ImapService extends EventEmitter {
|
|||
}
|
||||
|
||||
async _loadMailSummariesAndEmitAsEvents() {
|
||||
// Prevent overlapping loads which can inflate counts
|
||||
if (this.loadingInProgress) {
|
||||
debug('Load skipped: another load already in progress')
|
||||
return
|
||||
}
|
||||
this.loadingInProgress = true
|
||||
debug('Starting load of mail summaries')
|
||||
// UID: Unique id of a message.
|
||||
|
||||
const uids = await this._getAllUids()
|
||||
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
|
||||
debug(`UIDs on server: ${uids.length}, new UIDs to fetch: ${newUids.length}, already loaded: ${this.loadedUids.size}`)
|
||||
|
||||
// Tuneable chunk size & concurrency for faster initial loads
|
||||
const chunkSize = this.config.imap.fetchChunkSize
|
||||
const concurrency = this.config.imap.fetchConcurrency
|
||||
// Optimize by fetching several messages (but not all) with one 'search' call.
|
||||
// fetching all at once might be more efficient, but then it takes long until we see any messages
|
||||
// in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after
|
||||
// restart.
|
||||
const uidChunks = _.chunk(newUids, 20)
|
||||
|
||||
// Chunk newest-first UIDs to balance speed and first-paint
|
||||
const uidChunks = _.chunk(newUids, chunkSize)
|
||||
debug(`Chunk size: ${chunkSize}, concurrency: ${concurrency}, chunks to process: ${uidChunks.length}`)
|
||||
// Creates an array of functions. We do not start the search now, we just create the function.
|
||||
const fetchFunctions = uidChunks.map(uidChunk => () =>
|
||||
this._getMailHeadersAndEmitAsEvents(uidChunk)
|
||||
)
|
||||
|
||||
// Limited-concurrency worker
|
||||
const pool = []
|
||||
let workerId = 0
|
||||
const runNext = async() => {
|
||||
if (workerId >= uidChunks.length) return
|
||||
const chunkId = workerId++
|
||||
const chunk = uidChunks[chunkId]
|
||||
try {
|
||||
debug(`Worker processing chunk ${chunkId + 1}/${uidChunks.length} (size: ${chunk.length})`)
|
||||
await this._getMailHeadersAndEmitAsEvents(chunk)
|
||||
debug(`Completed chunk ${chunkId + 1}/${uidChunks.length}; loadedUids size now: ${this.loadedUids.size}`)
|
||||
} finally {
|
||||
await runNext()
|
||||
}
|
||||
}
|
||||
await pSeries(fetchFunctions)
|
||||
|
||||
// Start workers
|
||||
const workers = Math.min(concurrency, uidChunks.length)
|
||||
for (let i = 0; i < workers; i++) {
|
||||
pool.push(runNext())
|
||||
}
|
||||
await Promise.all(pool)
|
||||
debug(`All chunks processed. Final loadedUids size: ${this.loadedUids.size}`)
|
||||
|
||||
// Mark initial load done only after all chunks complete to avoid double-runs
|
||||
if (!this.initialLoadDone) {
|
||||
this.initialLoadDone = true
|
||||
this.emit(ImapService.EVENT_INITIAL_LOAD_DONE)
|
||||
debug('Emitted initial load done')
|
||||
}
|
||||
|
||||
this.loadingInProgress = false
|
||||
debug('Load finished')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -372,7 +341,6 @@ class ImapService extends EventEmitter {
|
|||
async _getMailHeadersAndEmitAsEvents(uids) {
|
||||
try {
|
||||
const mails = await this._getMailHeaders(uids)
|
||||
debug(`Fetched headers for ${uids.length} UIDs; server returned ${mails.length} messages`)
|
||||
mails.forEach(mail => {
|
||||
this.loadedUids.add(mail.attributes.uid)
|
||||
// Some broadcast messages have no TO field. We have to ignore those messages.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/*!
|
||||
* Milligram v1.4.1
|
||||
* https://milligram.io
|
||||
* Milligram v1.3.0
|
||||
* https://milligram.github.io
|
||||
*
|
||||
* Copyright (c) 2020 CJ Patoilo
|
||||
* Copyright (c) 2017 CJ Patoilo
|
||||
* Licensed under the MIT license
|
||||
*/
|
||||
|
||||
|
|
@ -60,8 +60,7 @@ input[type='submit'] {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button:focus,
|
||||
.button:hover,
|
||||
.button:focus, .button:hover,
|
||||
button:focus,
|
||||
button:hover,
|
||||
input[type='button']:focus,
|
||||
|
|
@ -85,8 +84,7 @@ input[type='submit'][disabled] {
|
|||
opacity: .5;
|
||||
}
|
||||
|
||||
.button[disabled]:focus,
|
||||
.button[disabled]:hover,
|
||||
.button[disabled]:focus, .button[disabled]:hover,
|
||||
button[disabled]:focus,
|
||||
button[disabled]:hover,
|
||||
input[type='button'][disabled]:focus,
|
||||
|
|
@ -108,8 +106,7 @@ input[type='submit'].button-outline {
|
|||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline:focus,
|
||||
.button.button-outline:hover,
|
||||
.button.button-outline:focus, .button.button-outline:hover,
|
||||
button.button-outline:focus,
|
||||
button.button-outline:hover,
|
||||
input[type='button'].button-outline:focus,
|
||||
|
|
@ -123,8 +120,7 @@ input[type='submit'].button-outline:hover {
|
|||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-outline[disabled]:focus,
|
||||
.button.button-outline[disabled]:hover,
|
||||
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||
button.button-outline[disabled]:focus,
|
||||
button.button-outline[disabled]:hover,
|
||||
input[type='button'].button-outline[disabled]:focus,
|
||||
|
|
@ -147,8 +143,7 @@ input[type='submit'].button-clear {
|
|||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear:focus,
|
||||
.button.button-clear:hover,
|
||||
.button.button-clear:focus, .button.button-clear:hover,
|
||||
button.button-clear:focus,
|
||||
button.button-clear:hover,
|
||||
input[type='button'].button-clear:focus,
|
||||
|
|
@ -162,8 +157,7 @@ input[type='submit'].button-clear:hover {
|
|||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-clear[disabled]:focus,
|
||||
.button.button-clear[disabled]:hover,
|
||||
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||
button.button-clear[disabled]:focus,
|
||||
button.button-clear[disabled]:hover,
|
||||
input[type='button'].button-clear[disabled]:focus,
|
||||
|
|
@ -203,47 +197,35 @@ hr {
|
|||
margin: 3.0rem 0;
|
||||
}
|
||||
|
||||
input[type='color'],
|
||||
input[type='date'],
|
||||
input[type='datetime'],
|
||||
input[type='datetime-local'],
|
||||
input[type='email'],
|
||||
input[type='month'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
input[type='week'],
|
||||
input:not([type]),
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0.1rem solid #d1d1d1;
|
||||
border-radius: .4rem;
|
||||
box-shadow: none;
|
||||
box-sizing: inherit;
|
||||
height: 3.8rem;
|
||||
padding: .6rem 1.0rem .7rem;
|
||||
padding: .6rem 1.0rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type='color']:focus,
|
||||
input[type='date']:focus,
|
||||
input[type='datetime']:focus,
|
||||
input[type='datetime-local']:focus,
|
||||
input[type='email']:focus,
|
||||
input[type='month']:focus,
|
||||
input[type='number']:focus,
|
||||
input[type='password']:focus,
|
||||
input[type='search']:focus,
|
||||
input[type='tel']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='url']:focus,
|
||||
input[type='week']:focus,
|
||||
input:not([type]):focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #9b4dca;
|
||||
|
|
@ -251,17 +233,12 @@ select:focus {
|
|||
}
|
||||
|
||||
select {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%23d1d1d1" d="M0,0l6,8l6-8"/></svg>') center right no-repeat;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||
padding-right: 3.0rem;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%239b4dca" d="M0,0l6,8l6-8"/></svg>');
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
background: none;
|
||||
height: auto;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
|
@ -359,25 +336,15 @@ input[type='radio'] {
|
|||
margin-left: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-33,
|
||||
.row .column.column-offset-34 {
|
||||
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||
margin-left: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-40 {
|
||||
margin-left: 40%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-50 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-60 {
|
||||
margin-left: 60%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-66,
|
||||
.row .column.column-offset-67 {
|
||||
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||
margin-left: 66.6666%;
|
||||
}
|
||||
|
||||
|
|
@ -408,8 +375,7 @@ input[type='radio'] {
|
|||
max-width: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-33,
|
||||
.row .column.column-34 {
|
||||
.row .column.column-33, .row .column.column-34 {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
|
|
@ -429,8 +395,7 @@ input[type='radio'] {
|
|||
max-width: 60%;
|
||||
}
|
||||
|
||||
.row .column.column-66,
|
||||
.row .column.column-67 {
|
||||
.row .column.column-66, .row .column.column-67 {
|
||||
flex: 0 0 66.6666%;
|
||||
max-width: 66.6666%;
|
||||
}
|
||||
|
|
@ -459,6 +424,7 @@ input[type='radio'] {
|
|||
}
|
||||
|
||||
.row .column .column-center {
|
||||
-ms-grid-row-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
|
@ -479,8 +445,7 @@ a {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:hover {
|
||||
a:focus, a:hover {
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
|
|
@ -542,9 +507,6 @@ ul {
|
|||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -552,6 +514,7 @@ td,
|
|||
th {
|
||||
border-bottom: 0.1rem solid #e1e1e1;
|
||||
padding: 1.2rem 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td:first-child,
|
||||
|
|
@ -564,13 +527,6 @@ th:last-child {
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
table {
|
||||
display: table;
|
||||
overflow-x: initial;
|
||||
}
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
|
|
@ -643,5 +599,4 @@ img {
|
|||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/*# sourceMappingURL=milligram.css.map */
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -69,15 +69,6 @@ text-muted {
|
|||
margin-top: -2rem
|
||||
}
|
||||
|
||||
.action-links {
|
||||
float: right;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.action-links a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* Reset apple form styles */
|
||||
|
||||
|
|
@ -107,10 +98,6 @@ select:hover {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#login h4 {
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
|
||||
#login fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="action-links">
|
||||
<a href="/inbox/{{ address }}">← Return to inbox</a>
|
||||
<a href="/logout">Logout</a>
|
||||
<div style="float: right; text-align: end;">
|
||||
<a href="/inbox/{{ address }}">
|
||||
← Return to inbox</a>
|
||||
<br>
|
||||
<a href="/logout">
|
||||
Logout</a>
|
||||
</div>
|
||||
<h1>{{message}}</h1>
|
||||
<h2>{{error.status}}</h2>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="action-links">
|
||||
<a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a>
|
||||
<a href="/logout">Logout</a>
|
||||
<div style="float: right; text-align: end;">
|
||||
<a href="/inbox/{{ address }}/delete-all">
|
||||
Wipe Inbox</a><br>
|
||||
<a href="/logout">
|
||||
Logout</a>
|
||||
</div>
|
||||
<h1>{{ address }}</h1>
|
||||
{% for mail in mailSummaries %}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
{% block body %}
|
||||
|
||||
<div class="action-links">
|
||||
<div style="float: right; text-align: end;">
|
||||
<a href="/inbox/{{ example }}">Example Inbox</a>
|
||||
</div>
|
||||
<div id="login">
|
||||
<h1>Welcome!</h1>
|
||||
<h4>Here you can either create a new Inbox, or access your old one</h4>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
{% if userInputError %}
|
||||
<blockquote class="warning">
|
||||
|
|
|
|||
|
|
@ -2,11 +2,18 @@
|
|||
|
||||
{% block body %}
|
||||
|
||||
<div class="action-links">
|
||||
<a href="/inbox/{{ address }}">← Return to inbox</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/delete">Delete Email</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">View Raw</a>
|
||||
<a href="/logout">Logout</a>
|
||||
<div style="float: right; text-align: end;">
|
||||
<a href="/inbox/{{ address }}">
|
||||
← Return to inbox</a>
|
||||
<br>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/delete">
|
||||
Delete Email</a>
|
||||
<br>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">
|
||||
View Raw</a>
|
||||
<br>
|
||||
<a href="/logout">
|
||||
Logout</a>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mail_body" style="padding-left:10%;">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue