emailengine-app 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +14 -0
- package/.github/CODE_OF_CONDUCT.md +76 -0
- package/.github/FUNDING.yml +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/contributing.md +17 -0
- package/.ncurc.js +10 -0
- package/.prettierrc.js +8 -0
- package/Dockerfile +17 -0
- package/Gruntfile.js +16 -0
- package/LICENSE.txt +661 -0
- package/README.md +524 -0
- package/bin/emailengine.js +14 -0
- package/config/default.toml +39 -0
- package/docker-compose.yml +47 -0
- package/encrypt.js +179 -0
- package/examples/api.md +137 -0
- package/examples/auth-server.js +104 -0
- package/getswagger.sh +8 -0
- package/lib/account.js +562 -0
- package/lib/append-list.js +67 -0
- package/lib/bounce-detect.js +380 -0
- package/lib/connection.js +1753 -0
- package/lib/consts.js +22 -0
- package/lib/db.js +72 -0
- package/lib/encrypt.js +100 -0
- package/lib/enum-message-flags.js +6 -0
- package/lib/get-raw-email.js +292 -0
- package/lib/get-secret.js +83 -0
- package/lib/logger.js +35 -0
- package/lib/lua/s-list-accounts.lua +51 -0
- package/lib/lua/z-expunge.lua +20 -0
- package/lib/lua/z-get-by-uid.lua +16 -0
- package/lib/lua/z-get-mailbox-id.lua +15 -0
- package/lib/lua/z-get-mailbox-path.lua +4 -0
- package/lib/lua/z-get.lua +15 -0
- package/lib/lua/z-push.lua +14 -0
- package/lib/lua/z-set.lua +17 -0
- package/lib/mailbox.js +1545 -0
- package/lib/message-port-stream.js +79 -0
- package/lib/schemas.js +311 -0
- package/lib/settings.js +63 -0
- package/lib/tools.js +488 -0
- package/package.json +79 -0
- package/scan.js +111 -0
- package/server.js +672 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.css +3872 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.css.map +1 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css +7 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css.map +1 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.css +325 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.css.map +1 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css +8 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css.map +1 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap.css +10298 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap.css.map +1 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap.min.css +7 -0
- package/static/bootstrap-4.6.0-dist/css/bootstrap.min.css.map +1 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.js +7045 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.js.map +1 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js +7 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js.map +1 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.js +4432 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.js.map +1 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.min.js +7 -0
- package/static/bootstrap-4.6.0-dist/js/bootstrap.min.js.map +1 -0
- package/static/css/callout.css +63 -0
- package/static/css/emailengine.css +33 -0
- package/static/favicon/android-chrome-192x192.png +0 -0
- package/static/favicon/android-chrome-512x512.png +0 -0
- package/static/favicon/apple-touch-icon.png +0 -0
- package/static/favicon/favicon-16x16.png +0 -0
- package/static/favicon/favicon-32x32.png +0 -0
- package/static/favicon/manifest.json +20 -0
- package/static/favicon.ico +0 -0
- package/static/icons/alarm-fill.svg +3 -0
- package/static/icons/alarm.svg +7 -0
- package/static/icons/alert-circle-fill.svg +3 -0
- package/static/icons/alert-circle.svg +4 -0
- package/static/icons/alert-octagon-fill.svg +3 -0
- package/static/icons/alert-octagon.svg +5 -0
- package/static/icons/alert-square-fill.svg +3 -0
- package/static/icons/alert-square.svg +5 -0
- package/static/icons/alert-triangle-fill.svg +3 -0
- package/static/icons/alert-triangle.svg +5 -0
- package/static/icons/archive-fill.svg +3 -0
- package/static/icons/archive.svg +4 -0
- package/static/icons/arrow-bar-bottom.svg +4 -0
- package/static/icons/arrow-bar-left.svg +4 -0
- package/static/icons/arrow-bar-right.svg +4 -0
- package/static/icons/arrow-bar-up.svg +4 -0
- package/static/icons/arrow-clockwise.svg +4 -0
- package/static/icons/arrow-counterclockwise.svg +4 -0
- package/static/icons/arrow-down-left.svg +4 -0
- package/static/icons/arrow-down-right.svg +4 -0
- package/static/icons/arrow-down-short.svg +4 -0
- package/static/icons/arrow-down.svg +4 -0
- package/static/icons/arrow-left-right.svg +5 -0
- package/static/icons/arrow-left-short.svg +4 -0
- package/static/icons/arrow-left.svg +4 -0
- package/static/icons/arrow-repeat.svg +5 -0
- package/static/icons/arrow-right-short.svg +4 -0
- package/static/icons/arrow-right.svg +4 -0
- package/static/icons/arrow-up-down.svg +5 -0
- package/static/icons/arrow-up-left.svg +4 -0
- package/static/icons/arrow-up-right.svg +4 -0
- package/static/icons/arrow-up-short.svg +4 -0
- package/static/icons/arrow-up.svg +4 -0
- package/static/icons/arrows-angle-contract.svg +5 -0
- package/static/icons/arrows-angle-expand.svg +5 -0
- package/static/icons/arrows-collapse.svg +5 -0
- package/static/icons/arrows-expand.svg +5 -0
- package/static/icons/arrows-fullscreen.svg +7 -0
- package/static/icons/at.svg +3 -0
- package/static/icons/award.svg +4 -0
- package/static/icons/backspace-fill.svg +3 -0
- package/static/icons/backspace-reverse-fill.svg +3 -0
- package/static/icons/backspace-reverse.svg +5 -0
- package/static/icons/backspace.svg +5 -0
- package/static/icons/bar-chart-fill.svg +5 -0
- package/static/icons/bar-chart.svg +3 -0
- package/static/icons/battery-charging.svg +5 -0
- package/static/icons/battery-full.svg +4 -0
- package/static/icons/battery.svg +4 -0
- package/static/icons/bell-fill.svg +3 -0
- package/static/icons/bell.svg +4 -0
- package/static/icons/blockquote-left.svg +4 -0
- package/static/icons/blockquote-right.svg +4 -0
- package/static/icons/book-half-fill.svg +4 -0
- package/static/icons/book.svg +4 -0
- package/static/icons/bookmark-fill.svg +3 -0
- package/static/icons/bookmark.svg +3 -0
- package/static/icons/bootstrap-fill.svg +3 -0
- package/static/icons/bootstrap-reboot.svg +3 -0
- package/static/icons/bootstrap.svg +4 -0
- package/static/icons/box-arrow-bottom-left.svg +4 -0
- package/static/icons/box-arrow-bottom-right.svg +4 -0
- package/static/icons/box-arrow-down.svg +5 -0
- package/static/icons/box-arrow-left.svg +5 -0
- package/static/icons/box-arrow-right.svg +5 -0
- package/static/icons/box-arrow-up-left.svg +4 -0
- package/static/icons/box-arrow-up-right.svg +4 -0
- package/static/icons/box-arrow-up.svg +5 -0
- package/static/icons/braces.svg +3 -0
- package/static/icons/brightness-fill-high.svg +4 -0
- package/static/icons/brightness-fill-low.svg +11 -0
- package/static/icons/brightness-high.svg +3 -0
- package/static/icons/brightness-low.svg +11 -0
- package/static/icons/brush.svg +4 -0
- package/static/icons/bucket-fill.svg +4 -0
- package/static/icons/bucket.svg +4 -0
- package/static/icons/building.svg +5 -0
- package/static/icons/bullseye.svg +6 -0
- package/static/icons/calendar-fill.svg +4 -0
- package/static/icons/calendar.svg +4 -0
- package/static/icons/camera-video-fill.svg +4 -0
- package/static/icons/camera-video.svg +4 -0
- package/static/icons/camera.svg +5 -0
- package/static/icons/capslock-fill.svg +3 -0
- package/static/icons/capslock.svg +3 -0
- package/static/icons/chat-fill.svg +3 -0
- package/static/icons/chat.svg +3 -0
- package/static/icons/check-box.svg +4 -0
- package/static/icons/check-circle.svg +4 -0
- package/static/icons/check.svg +3 -0
- package/static/icons/chevron-compact-down.svg +3 -0
- package/static/icons/chevron-compact-left.svg +3 -0
- package/static/icons/chevron-compact-right.svg +3 -0
- package/static/icons/chevron-compact-up.svg +3 -0
- package/static/icons/chevron-down.svg +3 -0
- package/static/icons/chevron-left.svg +3 -0
- package/static/icons/chevron-right.svg +3 -0
- package/static/icons/chevron-up.svg +3 -0
- package/static/icons/circle-fill.svg +3 -0
- package/static/icons/circle-half.svg +3 -0
- package/static/icons/circle-slash.svg +3 -0
- package/static/icons/circle.svg +3 -0
- package/static/icons/clock-fill.svg +3 -0
- package/static/icons/clock.svg +4 -0
- package/static/icons/cloud-download.svg +5 -0
- package/static/icons/cloud-fill.svg +3 -0
- package/static/icons/cloud-upload.svg +5 -0
- package/static/icons/cloud.svg +3 -0
- package/static/icons/code-slash.svg +3 -0
- package/static/icons/code.svg +3 -0
- package/static/icons/columns-gutters.svg +3 -0
- package/static/icons/columns.svg +4 -0
- package/static/icons/command.svg +4 -0
- package/static/icons/compass.svg +5 -0
- package/static/icons/cone-striped.svg +4 -0
- package/static/icons/cone.svg +4 -0
- package/static/icons/controller.svg +5 -0
- package/static/icons/credit-card.svg +5 -0
- package/static/icons/cursor-fill.svg +3 -0
- package/static/icons/cursor.svg +3 -0
- package/static/icons/dash.svg +3 -0
- package/static/icons/diamond-half.svg +3 -0
- package/static/icons/diamond.svg +3 -0
- package/static/icons/display-fill.svg +5 -0
- package/static/icons/display.svg +4 -0
- package/static/icons/document-code.svg +4 -0
- package/static/icons/document-diff.svg +5 -0
- package/static/icons/document-richtext.svg +4 -0
- package/static/icons/document-spreadsheet.svg +5 -0
- package/static/icons/document-text.svg +4 -0
- package/static/icons/document.svg +3 -0
- package/static/icons/documents-alt.svg +4 -0
- package/static/icons/documents.svg +4 -0
- package/static/icons/dot.svg +3 -0
- package/static/icons/download.svg +5 -0
- package/static/icons/egg-fried.svg +4 -0
- package/static/icons/eject-fill.svg +3 -0
- package/static/icons/eject.svg +3 -0
- package/static/icons/envelope-fill.svg +3 -0
- package/static/icons/envelope-open-fill.svg +3 -0
- package/static/icons/envelope-open.svg +5 -0
- package/static/icons/envelope.svg +4 -0
- package/static/icons/eye-fill.svg +4 -0
- package/static/icons/eye-slash-fill.svg +5 -0
- package/static/icons/eye-slash.svg +6 -0
- package/static/icons/eye.svg +4 -0
- package/static/icons/filter.svg +3 -0
- package/static/icons/flag-fill.svg +4 -0
- package/static/icons/flag.svg +4 -0
- package/static/icons/folder-fill.svg +3 -0
- package/static/icons/folder-symlink-fill.svg +3 -0
- package/static/icons/folder-symlink.svg +5 -0
- package/static/icons/folder.svg +4 -0
- package/static/icons/fonts.svg +3 -0
- package/static/icons/forward-fill.svg +3 -0
- package/static/icons/forward.svg +3 -0
- package/static/icons/gear-fill.svg +3 -0
- package/static/icons/gear-wide-connected.svg +4 -0
- package/static/icons/gear-wide.svg +3 -0
- package/static/icons/gear.svg +4 -0
- package/static/icons/geo.svg +5 -0
- package/static/icons/graph-down.svg +5 -0
- package/static/icons/graph-up.svg +5 -0
- package/static/icons/grid-fill.svg +6 -0
- package/static/icons/grid.svg +3 -0
- package/static/icons/hammer.svg +4 -0
- package/static/icons/hash.svg +3 -0
- package/static/icons/heart-fill.svg +3 -0
- package/static/icons/heart.svg +3 -0
- package/static/icons/house-fill.svg +4 -0
- package/static/icons/house.svg +4 -0
- package/static/icons/image-alt.svg +4 -0
- package/static/icons/image-fill.svg +3 -0
- package/static/icons/image.svg +5 -0
- package/static/icons/images.svg +5 -0
- package/static/icons/inbox-fill.svg +4 -0
- package/static/icons/inbox.svg +4 -0
- package/static/icons/inboxes-fill.svg +4 -0
- package/static/icons/inboxes.svg +4 -0
- package/static/icons/info-fill.svg +3 -0
- package/static/icons/info-square-fill.svg +3 -0
- package/static/icons/info-square.svg +5 -0
- package/static/icons/info.svg +5 -0
- package/static/icons/justify-left.svg +3 -0
- package/static/icons/justify-right.svg +3 -0
- package/static/icons/justify.svg +3 -0
- package/static/icons/kanban-fill.svg +3 -0
- package/static/icons/kanban.svg +6 -0
- package/static/icons/laptop.svg +4 -0
- package/static/icons/layout-sidebar-reverse.svg +4 -0
- package/static/icons/layout-sidebar.svg +4 -0
- package/static/icons/layout-split.svg +3 -0
- package/static/icons/list-check.svg +3 -0
- package/static/icons/list-ol.svg +4 -0
- package/static/icons/list-task.svg +5 -0
- package/static/icons/list-ul.svg +3 -0
- package/static/icons/list.svg +3 -0
- package/static/icons/lock-fill.svg +4 -0
- package/static/icons/lock.svg +3 -0
- package/static/icons/map.svg +3 -0
- package/static/icons/mic.svg +4 -0
- package/static/icons/moon.svg +3 -0
- package/static/icons/music-player-fill.svg +4 -0
- package/static/icons/music-player.svg +5 -0
- package/static/icons/option.svg +3 -0
- package/static/icons/outlet.svg +5 -0
- package/static/icons/pause-fill.svg +3 -0
- package/static/icons/pause.svg +3 -0
- package/static/icons/pen.svg +5 -0
- package/static/icons/pencil.svg +4 -0
- package/static/icons/people-fill.svg +3 -0
- package/static/icons/people.svg +3 -0
- package/static/icons/person-fill.svg +3 -0
- package/static/icons/person.svg +3 -0
- package/static/icons/phone-landscape.svg +4 -0
- package/static/icons/phone.svg +4 -0
- package/static/icons/pie-chart-fill.svg +3 -0
- package/static/icons/pie-chart.svg +4 -0
- package/static/icons/play-fill.svg +3 -0
- package/static/icons/play.svg +3 -0
- package/static/icons/plug.svg +4 -0
- package/static/icons/plus.svg +4 -0
- package/static/icons/power.svg +4 -0
- package/static/icons/question-fill.svg +3 -0
- package/static/icons/question-square-fill.svg +3 -0
- package/static/icons/question-square.svg +4 -0
- package/static/icons/question.svg +4 -0
- package/static/icons/reply-all-fill.svg +4 -0
- package/static/icons/reply-all.svg +4 -0
- package/static/icons/reply-fill.svg +3 -0
- package/static/icons/reply.svg +3 -0
- package/static/icons/screwdriver.svg +3 -0
- package/static/icons/search.svg +4 -0
- package/static/icons/shield-fill.svg +3 -0
- package/static/icons/shield-lock-fill.svg +3 -0
- package/static/icons/shield-lock.svg +5 -0
- package/static/icons/shield-shaded.svg +4 -0
- package/static/icons/shield.svg +3 -0
- package/static/icons/shift-fill.svg +3 -0
- package/static/icons/shift.svg +3 -0
- package/static/icons/skip-backward-fill.svg +4 -0
- package/static/icons/skip-backward.svg +3 -0
- package/static/icons/skip-end-fill.svg +5 -0
- package/static/icons/skip-end.svg +4 -0
- package/static/icons/skip-forward-fill.svg +5 -0
- package/static/icons/skip-forward.svg +3 -0
- package/static/icons/skip-start-fill.svg +4 -0
- package/static/icons/skip-start.svg +4 -0
- package/static/icons/speaker.svg +4 -0
- package/static/icons/square-fill.svg +3 -0
- package/static/icons/square-half.svg +3 -0
- package/static/icons/square.svg +3 -0
- package/static/icons/star-fill.svg +3 -0
- package/static/icons/star-half.svg +3 -0
- package/static/icons/star.svg +3 -0
- package/static/icons/stop-fill.svg +3 -0
- package/static/icons/stop.svg +3 -0
- package/static/icons/stopwatch-fill.svg +3 -0
- package/static/icons/stopwatch.svg +5 -0
- package/static/icons/sun.svg +4 -0
- package/static/icons/table.svg +7 -0
- package/static/icons/tablet-landscape.svg +4 -0
- package/static/icons/tablet.svg +4 -0
- package/static/icons/tag-fill.svg +3 -0
- package/static/icons/tag.svg +4 -0
- package/static/icons/terminal-fill.svg +3 -0
- package/static/icons/terminal.svg +4 -0
- package/static/icons/text-center.svg +3 -0
- package/static/icons/text-indent-left.svg +3 -0
- package/static/icons/text-indent-right.svg +3 -0
- package/static/icons/text-left.svg +3 -0
- package/static/icons/text-right.svg +3 -0
- package/static/icons/three-dots-vertical.svg +3 -0
- package/static/icons/three-dots.svg +3 -0
- package/static/icons/toggle-off.svg +3 -0
- package/static/icons/toggle-on.svg +3 -0
- package/static/icons/toggles.svg +4 -0
- package/static/icons/tools.svg +4 -0
- package/static/icons/trash-fill.svg +3 -0
- package/static/icons/trash.svg +4 -0
- package/static/icons/triangle-fill.svg +3 -0
- package/static/icons/triangle-half.svg +3 -0
- package/static/icons/triangle.svg +3 -0
- package/static/icons/trophy.svg +6 -0
- package/static/icons/tv-fill.svg +3 -0
- package/static/icons/tv.svg +3 -0
- package/static/icons/type-bold.svg +3 -0
- package/static/icons/type-h1.svg +3 -0
- package/static/icons/type-h2.svg +3 -0
- package/static/icons/type-h3.svg +3 -0
- package/static/icons/type-italic.svg +3 -0
- package/static/icons/type-strikethrough.svg +4 -0
- package/static/icons/type-underline.svg +4 -0
- package/static/icons/type.svg +3 -0
- package/static/icons/unlock-fill.svg +4 -0
- package/static/icons/unlock.svg +3 -0
- package/static/icons/upload.svg +4 -0
- package/static/icons/volume-down-fill.svg +4 -0
- package/static/icons/volume-down.svg +4 -0
- package/static/icons/volume-mute-fill.svg +4 -0
- package/static/icons/volume-mute.svg +4 -0
- package/static/icons/volume-up-fill.svg +6 -0
- package/static/icons/volume-up.svg +6 -0
- package/static/icons/wallet.svg +3 -0
- package/static/icons/watch.svg +5 -0
- package/static/icons/wifi.svg +5 -0
- package/static/icons/window.svg +5 -0
- package/static/icons/wrench.svg +3 -0
- package/static/icons/x-circle-fill.svg +3 -0
- package/static/icons/x-circle.svg +5 -0
- package/static/icons/x-octagon-fill.svg +3 -0
- package/static/icons/x-octagon.svg +4 -0
- package/static/icons/x-square-fill.svg +3 -0
- package/static/icons/x-square.svg +4 -0
- package/static/icons/x.svg +4 -0
- package/static/index.html +752 -0
- package/static/js/emailengine.js +581 -0
- package/static/js/jquery-3.4.1.slim.min.js +2 -0
- package/static/js/moment-with-locales-2.24.0.min.js +1 -0
- package/static/js/popper.min.js +5 -0
- package/static/logo.png +0 -0
- package/systemd/emailengine.service +89 -0
- package/systemd/nginx-proxy.conf +77 -0
- package/views/error.hbs +2 -0
- package/workers/api.js +2266 -0
- package/workers/arena.js +89 -0
- package/workers/imap.js +611 -0
- package/workers/smtp.js +278 -0
- package/workers/submit.js +214 -0
- package/workers/webhooks.js +134 -0
package/lib/account.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const logger = require('./logger');
|
|
4
|
+
const Boom = require('@hapi/boom');
|
|
5
|
+
const msgpack = require('msgpack5')();
|
|
6
|
+
const { normalizePath, formatAccountListingResponse } = require('./tools');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { MessageChannel } = require('worker_threads');
|
|
9
|
+
const { MessagePortReadable } = require('./message-port-stream');
|
|
10
|
+
const { deepStrictEqual, strictEqual } = require('assert');
|
|
11
|
+
const { encrypt, decrypt } = require('./encrypt');
|
|
12
|
+
const { OAuth2Client } = require('google-auth-library');
|
|
13
|
+
|
|
14
|
+
class Account {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.redis = options.redis;
|
|
17
|
+
this.account = options.account || false;
|
|
18
|
+
|
|
19
|
+
this.secret = options.secret;
|
|
20
|
+
|
|
21
|
+
this.call = options.call; // async method to request data from parent
|
|
22
|
+
|
|
23
|
+
this.logger = options.logger || logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async listAccounts(state, page, limit) {
|
|
27
|
+
limit = Number(limit) || 20;
|
|
28
|
+
page = Math.max(Number(page) || 0, 0);
|
|
29
|
+
let skip = page * limit;
|
|
30
|
+
|
|
31
|
+
let result = await this.redis.sListAccounts('ia:accounts', state || '*', skip, limit);
|
|
32
|
+
|
|
33
|
+
let list = {
|
|
34
|
+
total: result[0],
|
|
35
|
+
pages: Math.ceil(result[0] / limit),
|
|
36
|
+
page,
|
|
37
|
+
accounts: result[2]
|
|
38
|
+
.map(formatAccountListingResponse)
|
|
39
|
+
.map(this.unserializeAccountData.bind(this))
|
|
40
|
+
.map(entry => ({
|
|
41
|
+
account: entry.account,
|
|
42
|
+
name: entry.name,
|
|
43
|
+
state: entry.state,
|
|
44
|
+
syncTime: entry.sync,
|
|
45
|
+
lastError: entry.state === 'connected' ? null : entry.lastErrorState
|
|
46
|
+
}))
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return list;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getMailboxInfo(path) {
|
|
53
|
+
let redisKey = BigInt('0x' + crypto.createHash('sha1').update(normalizePath(path)).digest('hex')).toString(36);
|
|
54
|
+
|
|
55
|
+
let data = await this.redis.hgetall(`iam:${this.account}:h:${redisKey}`);
|
|
56
|
+
if (!data || !Object.keys(data).length) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let encodedMailboxData = await this.redis.hgetBuffer(this.getMailboxListKey(), path);
|
|
61
|
+
if (!encodedMailboxData) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
path: data.path || path,
|
|
67
|
+
messages: data.messages && !isNaN(data.messages) ? Number(data.messages) : false,
|
|
68
|
+
uidNext: data.uidNext && !isNaN(data.uidNext) ? Number(data.uidNext) : false
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAccountKey() {
|
|
73
|
+
return `iad:${this.account}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getMailboxListKey() {
|
|
77
|
+
return `ial:${this.account}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unserializeAccountData(accountData) {
|
|
81
|
+
let result = {};
|
|
82
|
+
|
|
83
|
+
Object.keys(accountData).forEach(key => {
|
|
84
|
+
switch (key) {
|
|
85
|
+
case 'notifyFrom':
|
|
86
|
+
// Date object
|
|
87
|
+
if (accountData[key]) {
|
|
88
|
+
let date = new Date(accountData[key]);
|
|
89
|
+
if (date.toString() !== 'Invalid Date') {
|
|
90
|
+
result[key] = date;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case 'copy':
|
|
96
|
+
result[key] = accountData[key] === 'true' ? true : false;
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'imap':
|
|
100
|
+
case 'smtp':
|
|
101
|
+
case 'oauth2':
|
|
102
|
+
case 'lastErrorState':
|
|
103
|
+
try {
|
|
104
|
+
result[key] = JSON.parse(accountData[key]);
|
|
105
|
+
if (result[key].expires) {
|
|
106
|
+
let date = new Date(result[key].expires);
|
|
107
|
+
if (date.toString() !== 'Invalid Date') {
|
|
108
|
+
result[key].expires = date;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this.logger.error({ msg: 'Failed to parse input from Redis', key, err });
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
result[key] = accountData[key];
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// decrypt secrets
|
|
123
|
+
if (this.secret) {
|
|
124
|
+
for (let type of ['imap', 'smtp', 'oauth2']) {
|
|
125
|
+
if (result[type] && result[type].auth) {
|
|
126
|
+
for (let key of ['pass', 'accessToken', 'refreshToken']) {
|
|
127
|
+
if (key in result[type].auth) {
|
|
128
|
+
try {
|
|
129
|
+
result[type].auth[key] = decrypt(result[type].auth[key], this.secret);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// ignore??
|
|
132
|
+
this.logger.error({ msg: 'Failed to decrypt value', encrypted: result[type].auth[key], err });
|
|
133
|
+
}
|
|
134
|
+
} else if (key in result[type]) {
|
|
135
|
+
try {
|
|
136
|
+
result[type][key] = decrypt(result[type][key], this.secret);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// ignore??
|
|
139
|
+
this.logger.error({ msg: 'Failed to decrypt value', encrypted: result[type][key], err });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!result.path) {
|
|
148
|
+
// by default listen changes on all folders
|
|
149
|
+
result.path = '*';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
serializeAccountData(accountData) {
|
|
156
|
+
let result = {};
|
|
157
|
+
|
|
158
|
+
Object.keys(accountData).forEach(key => {
|
|
159
|
+
switch (key) {
|
|
160
|
+
case 'notifyFrom':
|
|
161
|
+
// Date object
|
|
162
|
+
if (accountData[key] === 'now') {
|
|
163
|
+
result[key] = new Date().toISOString();
|
|
164
|
+
} else if (accountData[key] && typeof accountData[key] === 'object' && accountData[key].toString() !== 'Invalid Date') {
|
|
165
|
+
result[key] = accountData[key].toISOString();
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'imap':
|
|
170
|
+
case 'smtp':
|
|
171
|
+
case 'oauth2':
|
|
172
|
+
try {
|
|
173
|
+
// make a deep copy for manipulation
|
|
174
|
+
let connectData = JSON.parse(JSON.stringify(accountData[key]));
|
|
175
|
+
|
|
176
|
+
// if possible encrypt passwords
|
|
177
|
+
if (this.secret && connectData.auth) {
|
|
178
|
+
for (let key of ['pass', 'accessToken', 'refreshToken']) {
|
|
179
|
+
if (key in connectData.auth) {
|
|
180
|
+
try {
|
|
181
|
+
connectData.auth[key] = encrypt(connectData.auth[key], this.secret);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.logger.error({ msg: 'Failed to encrypt value', err });
|
|
184
|
+
}
|
|
185
|
+
} else if (key in connectData) {
|
|
186
|
+
try {
|
|
187
|
+
connectData[key] = encrypt(connectData[key], this.secret);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
this.logger.error({ msg: 'Failed to encrypt value', err });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
accountData[key].expires &&
|
|
197
|
+
typeof accountData[key].expires === 'object' &&
|
|
198
|
+
accountData[key].expires.toString() !== 'Invalid Date'
|
|
199
|
+
) {
|
|
200
|
+
connectData.expires = accountData[key].expires.toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
result[key] = JSON.stringify(connectData);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
this.logger.error({ msg: 'Failed to stringify input for Redis', key, err });
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
if (accountData[key] !== undefined && typeof accountData[key].toString === 'function') {
|
|
211
|
+
result[key] = accountData[key].toString();
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async loadAccountData(account, requireValid) {
|
|
221
|
+
if (!this.account || (account && account !== this.account)) {
|
|
222
|
+
let message = 'Invalid account ID';
|
|
223
|
+
let error = Boom.boomify(new Error(message), { statusCode: 400 });
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let result = await this.redis.hgetall(this.getAccountKey());
|
|
228
|
+
|
|
229
|
+
if (!result || !result.account) {
|
|
230
|
+
let message = 'Account record was not found for requested ID';
|
|
231
|
+
let error = Boom.boomify(new Error(message), { statusCode: 404 });
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let accountData = this.unserializeAccountData(result);
|
|
236
|
+
if (requireValid && accountData.state !== 'connected') {
|
|
237
|
+
let err;
|
|
238
|
+
switch (accountData.state) {
|
|
239
|
+
case 'init':
|
|
240
|
+
err = new Error('Requested account is not yet initialized');
|
|
241
|
+
break;
|
|
242
|
+
case 'connecting':
|
|
243
|
+
err = new Error('Requested account is not yet connected');
|
|
244
|
+
break;
|
|
245
|
+
case 'authenticationError':
|
|
246
|
+
err = new Error('Requested account can not be authenticated');
|
|
247
|
+
break;
|
|
248
|
+
case 'connectError':
|
|
249
|
+
err = new Error('Can not establish server connection for requested account');
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
err = new Error('Requested account currently not available');
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let error = Boom.boomify(err, { statusCode: 503 });
|
|
257
|
+
if (accountData.state) {
|
|
258
|
+
error.output.payload.state = accountData.state;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return accountData;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async update(accountData) {
|
|
267
|
+
let oldAccountData = await this.loadAccountData(accountData.account);
|
|
268
|
+
|
|
269
|
+
let result = await this.redis.hmset(this.getAccountKey(), this.serializeAccountData(accountData));
|
|
270
|
+
if (!result || result !== 'OK') {
|
|
271
|
+
let message = 'Something went wrong';
|
|
272
|
+
let error = Boom.boomify(new Error(message), { statusCode: 500 });
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if ('imap' in accountData) {
|
|
277
|
+
// if partial update, then skip check
|
|
278
|
+
try {
|
|
279
|
+
deepStrictEqual(oldAccountData.imap, accountData.imap);
|
|
280
|
+
strictEqual(oldAccountData.path || '*', accountData.path || '*');
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// changes detected!
|
|
283
|
+
this.logger.info({ msg: 'IMAP configuration changed for account', account: this.account });
|
|
284
|
+
await this.call({ cmd: 'update', account: this.account });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
account: this.account
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async create(accountData) {
|
|
294
|
+
this.account = accountData.account;
|
|
295
|
+
|
|
296
|
+
if (!this.account) {
|
|
297
|
+
let message = 'Invalid account ID';
|
|
298
|
+
let error = Boom.boomify(new Error(message), { statusCode: 400 });
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let result = await this.redis
|
|
303
|
+
.multi()
|
|
304
|
+
.hgetall(this.getAccountKey())
|
|
305
|
+
.hmset(this.getAccountKey(), this.serializeAccountData(accountData))
|
|
306
|
+
.hsetnx(this.getAccountKey(), 'state', 'init')
|
|
307
|
+
.sadd('ia:accounts', this.account)
|
|
308
|
+
.exec();
|
|
309
|
+
|
|
310
|
+
if (!result || !result[1] || result[1][0] || result[1][1] !== 'OK') {
|
|
311
|
+
let message = 'Something went wrong';
|
|
312
|
+
let error = Boom.boomify(new Error(message), { statusCode: 500 });
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let state = false;
|
|
317
|
+
if (result[0][1] && result[0][1].account) {
|
|
318
|
+
// existing user
|
|
319
|
+
state = 'existing';
|
|
320
|
+
await this.call({ cmd: 'update', account: this.account });
|
|
321
|
+
} else {
|
|
322
|
+
state = 'new';
|
|
323
|
+
await this.call({ cmd: 'new', account: this.account });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { account: this.account, state };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async delete() {
|
|
330
|
+
await this.loadAccountData(this.account);
|
|
331
|
+
|
|
332
|
+
let result = await this.redis
|
|
333
|
+
.multi()
|
|
334
|
+
.del(this.getAccountKey())
|
|
335
|
+
.keys(`iam:${this.account}:*`)
|
|
336
|
+
.srem('ia:accounts', this.account)
|
|
337
|
+
.del(`ial:${this.account}`) // mailbox list
|
|
338
|
+
.del(`iah:${this.account}`) // mailbox list for ID references
|
|
339
|
+
.del(`iar:b:${this.account}`) // bounce list
|
|
340
|
+
.del(`iaq:${this.account}`) // delayed message queue
|
|
341
|
+
.exec();
|
|
342
|
+
|
|
343
|
+
if (result && result[1] && result[1][1] && result[1][1].length) {
|
|
344
|
+
// delete all mailbox specific keys
|
|
345
|
+
let run = await this.redis.multi();
|
|
346
|
+
for (let key of result[1][1]) {
|
|
347
|
+
run = run.del(key);
|
|
348
|
+
}
|
|
349
|
+
await run.exec();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!result || !result[0] || !result[0][1]) {
|
|
353
|
+
return {
|
|
354
|
+
account: this.account,
|
|
355
|
+
deleted: false
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await this.call({ cmd: 'delete', account: this.account });
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
account: this.account,
|
|
363
|
+
deleted: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async getRawMessage(message) {
|
|
368
|
+
await this.loadAccountData(this.account, true);
|
|
369
|
+
|
|
370
|
+
const { port1, port2 } = new MessageChannel();
|
|
371
|
+
const stream = new MessagePortReadable(port1);
|
|
372
|
+
|
|
373
|
+
let streamCreated = await this.call({ cmd: 'getRawMessage', account: this.account, message, port: port2 }, [port2]);
|
|
374
|
+
|
|
375
|
+
if (streamCreated && streamCreated.headers) {
|
|
376
|
+
stream.headers = streamCreated.headers;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return stream;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async getAttachment(attachment) {
|
|
383
|
+
await this.loadAccountData(this.account, true);
|
|
384
|
+
|
|
385
|
+
const { port1, port2 } = new MessageChannel();
|
|
386
|
+
const stream = new MessagePortReadable(port1);
|
|
387
|
+
|
|
388
|
+
let streamCreated = await this.call({ cmd: 'getAttachment', account: this.account, attachment, port: port2 }, [port2]);
|
|
389
|
+
|
|
390
|
+
if (streamCreated && streamCreated.headers) {
|
|
391
|
+
stream.headers = streamCreated.headers;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return stream;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async getMailboxListing() {
|
|
398
|
+
await this.loadAccountData(this.account, true);
|
|
399
|
+
|
|
400
|
+
let result = [];
|
|
401
|
+
let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
|
|
402
|
+
|
|
403
|
+
for (let path of Object.keys(storedListing || {})) {
|
|
404
|
+
try {
|
|
405
|
+
let decoded = msgpack.decode(storedListing[path]);
|
|
406
|
+
if (decoded.delimiter && decoded.path.indexOf(decoded.delimiter) >= 0) {
|
|
407
|
+
decoded.parentPath = decoded.path.substr(0, decoded.path.lastIndexOf(decoded.delimiter));
|
|
408
|
+
}
|
|
409
|
+
delete decoded.delimiter;
|
|
410
|
+
result.push(Object.assign(decoded, await this.getMailboxInfo(path)));
|
|
411
|
+
} catch (err) {
|
|
412
|
+
// should not happen
|
|
413
|
+
this.logger.error(err);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async updateMessage(message, updates) {
|
|
421
|
+
await this.loadAccountData(this.account, true);
|
|
422
|
+
|
|
423
|
+
return await this.call({ cmd: 'updateMessage', account: this.account, message, updates });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async moveMessage(message, target) {
|
|
427
|
+
await this.loadAccountData(this.account, true);
|
|
428
|
+
return await this.call({ cmd: 'moveMessage', account: this.account, message, target });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async deleteMessage(message) {
|
|
432
|
+
await this.loadAccountData(this.account, true);
|
|
433
|
+
|
|
434
|
+
return await this.call({ cmd: 'deleteMessage', account: this.account, message });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async createMailbox(path) {
|
|
438
|
+
await this.loadAccountData(this.account, true);
|
|
439
|
+
|
|
440
|
+
return await this.call({ cmd: 'createMailbox', account: this.account, path });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async deleteMailbox(path) {
|
|
444
|
+
await this.loadAccountData(this.account, true);
|
|
445
|
+
|
|
446
|
+
return await this.call({ cmd: 'deleteMailbox', account: this.account, path });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async getText(text, options) {
|
|
450
|
+
await this.loadAccountData(this.account, true);
|
|
451
|
+
|
|
452
|
+
return await this.call({ cmd: 'getText', account: this.account, text, options });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async getMessage(message, options) {
|
|
456
|
+
await this.loadAccountData(this.account, true);
|
|
457
|
+
|
|
458
|
+
let messageData = await this.call({ cmd: 'getMessage', account: this.account, message, options });
|
|
459
|
+
if (!messageData) {
|
|
460
|
+
let message = 'Requested message was not found';
|
|
461
|
+
let error = Boom.boomify(new Error(message), { statusCode: 404 });
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
return messageData;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async listMessages(query) {
|
|
468
|
+
await this.loadAccountData(this.account, true);
|
|
469
|
+
|
|
470
|
+
let path = normalizePath(query.path);
|
|
471
|
+
let encodedMailboxData = await this.redis.hgetBuffer(this.getMailboxListKey(), path);
|
|
472
|
+
if (!encodedMailboxData) {
|
|
473
|
+
let message = 'Mailbox record was not found';
|
|
474
|
+
let error = Boom.boomify(new Error(message), { statusCode: 404 });
|
|
475
|
+
error.output.payload.path = query.path;
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// mailbox seems to exist, so call parent to resolve open connection
|
|
480
|
+
return await this.call(Object.assign({ cmd: 'listMessages', account: this.account }, query));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async buildContacts() {
|
|
484
|
+
await this.loadAccountData(this.account, true);
|
|
485
|
+
return await this.call({ cmd: 'buildContacts', account: this.account, timeout: 5 * 60 * 1000 });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async uploadMessage(data) {
|
|
489
|
+
await this.loadAccountData(this.account, true);
|
|
490
|
+
|
|
491
|
+
let messageData = await this.call({ cmd: 'uploadMessage', account: this.account, data });
|
|
492
|
+
return messageData;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async submitMessage(data) {
|
|
496
|
+
await this.loadAccountData(this.account, true);
|
|
497
|
+
|
|
498
|
+
let messageData = await this.call(
|
|
499
|
+
{
|
|
500
|
+
cmd: 'submitMessage',
|
|
501
|
+
account: this.account,
|
|
502
|
+
data
|
|
503
|
+
}
|
|
504
|
+
//typeof data.raw === 'object' ? [data.raw] : []
|
|
505
|
+
);
|
|
506
|
+
return messageData;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async queueMessage(data) {
|
|
510
|
+
await this.loadAccountData(this.account, true);
|
|
511
|
+
|
|
512
|
+
let messageData = await this.call(
|
|
513
|
+
{
|
|
514
|
+
cmd: 'queueMessage',
|
|
515
|
+
account: this.account,
|
|
516
|
+
data
|
|
517
|
+
}
|
|
518
|
+
//typeof data.raw === 'object' ? [data.raw] : []
|
|
519
|
+
);
|
|
520
|
+
return messageData;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async requestReconnect(data) {
|
|
524
|
+
await this.loadAccountData(this.account, true);
|
|
525
|
+
|
|
526
|
+
if (data.reconnect) {
|
|
527
|
+
await this.call({ cmd: 'update', account: this.account });
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async renewAccessToken(oauthKeys) {
|
|
534
|
+
let accountData = await this.loadAccountData(this.account, false);
|
|
535
|
+
let oAuth2Client = new OAuth2Client(oauthKeys.clientId, oauthKeys.clientSecret, oauthKeys.redirectUrl);
|
|
536
|
+
oAuth2Client.setCredentials({
|
|
537
|
+
access_token: accountData.oauth2.accessToken,
|
|
538
|
+
refresh_token: accountData.oauth2.refreshToken,
|
|
539
|
+
scope: accountData.oauth2.scope || 'https://mail.google.com/',
|
|
540
|
+
token_type: accountData.oauth2.tokenType || 'Bearer',
|
|
541
|
+
expiry_date: accountData.oauth2.expires
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
let r = await oAuth2Client.refreshAccessTokenAsync();
|
|
545
|
+
if (!r || !r.credentials) {
|
|
546
|
+
throw new Error('Failed to renew access tokens');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.logger.info({ msg: 'Renewed oauth tokens' });
|
|
550
|
+
|
|
551
|
+
accountData.oauth2 = Object.assign(accountData.oauth2 || {}, {
|
|
552
|
+
accessToken: r.credentials.access_token,
|
|
553
|
+
expires: new Date(r.credentials.expiry_date).toISOString()
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await this.update({ account: accountData.account, oauth2: accountData.oauth2 });
|
|
557
|
+
|
|
558
|
+
return accountData;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
module.exports = { Account };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const msgpack = require('msgpack5')();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pushes an object to the end of a list where that list is stored in a hash field
|
|
7
|
+
*
|
|
8
|
+
* @async
|
|
9
|
+
* @param {Object} redis Redis client
|
|
10
|
+
* @param {String} list Hash key
|
|
11
|
+
* @param {String} key Hash field
|
|
12
|
+
* @param {Object} obj Data to serialize and append to the list
|
|
13
|
+
* @returns {Number} either 1 if this was the first element or 2
|
|
14
|
+
*/
|
|
15
|
+
async function append(redis, list, key, obj) {
|
|
16
|
+
if (!obj) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
let encoded = msgpack.encode(obj);
|
|
20
|
+
let result = await redis.zPush(list, key, encoded);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves all stored entries of a list
|
|
26
|
+
*
|
|
27
|
+
* @async
|
|
28
|
+
* @param {Object} redis Redis client
|
|
29
|
+
* @param {String} list Hash key
|
|
30
|
+
* @param {String} key Hash field
|
|
31
|
+
* @returns {Array} Stored objects as an array
|
|
32
|
+
*/
|
|
33
|
+
async function list(redis, list, key) {
|
|
34
|
+
let contents = await redis.hgetBuffer(list, key);
|
|
35
|
+
if (!contents) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let decoder = msgpack.decoder({});
|
|
41
|
+
let list = [];
|
|
42
|
+
decoder.on('readable', () => {
|
|
43
|
+
let data;
|
|
44
|
+
while ((data = decoder.read()) !== null) {
|
|
45
|
+
list.push(data);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
decoder.on('end', () => resolve(list));
|
|
49
|
+
decoder.on('error', err => reject(err));
|
|
50
|
+
decoder.end(contents);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clears a list from Redis
|
|
56
|
+
*
|
|
57
|
+
* @async
|
|
58
|
+
* @param {Object} redis Redis client
|
|
59
|
+
* @param {String} list Hash key
|
|
60
|
+
* @param {String} key Hash field
|
|
61
|
+
* @returns {Number} 1 if list was deleted or 0
|
|
62
|
+
*/
|
|
63
|
+
async function clear(redis, list, key) {
|
|
64
|
+
return await redis.hdel(list, key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { append, list, clear };
|