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
|
@@ -0,0 +1,1753 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parentPort } = require('worker_threads');
|
|
4
|
+
const { ImapFlow } = require('imapflow');
|
|
5
|
+
const { Mailbox } = require('./mailbox');
|
|
6
|
+
const logger = require('./logger');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const packageData = require('../package.json');
|
|
9
|
+
const { backOff } = require('exponential-backoff');
|
|
10
|
+
const msgpack = require('msgpack5')();
|
|
11
|
+
const nodemailer = require('nodemailer');
|
|
12
|
+
const MailComposer = require('nodemailer/lib/mail-composer');
|
|
13
|
+
const util = require('util');
|
|
14
|
+
const settings = require('./settings');
|
|
15
|
+
const { getRawEmail, removeBcc } = require('./get-raw-email');
|
|
16
|
+
|
|
17
|
+
const { normalizePath, resolveCredentials } = require('./tools');
|
|
18
|
+
|
|
19
|
+
const RESYNC_DELAY = 15 * 60;
|
|
20
|
+
const LIST_REFRESH_DELAY = 30 * 60 * 1000;
|
|
21
|
+
const ENSURE_MAIN_TTL = 5 * 1000;
|
|
22
|
+
|
|
23
|
+
const MAX_BACKOFF_DELAY = 10 * 60 * 1000; // 10 min
|
|
24
|
+
|
|
25
|
+
const { AUTH_ERROR_NOTIFY, AUTH_SUCCESS_NOTIFY, CONNECT_ERROR_NOTIFY, EMAIL_SENT_NOTIFY } = require('./consts');
|
|
26
|
+
|
|
27
|
+
async function metrics(logger, key, method, ...args) {
|
|
28
|
+
try {
|
|
29
|
+
parentPort.postMessage({
|
|
30
|
+
cmd: 'metrics',
|
|
31
|
+
key,
|
|
32
|
+
method,
|
|
33
|
+
args
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.error({ msg: 'Failed to post metrics to parent', err });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Connection {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.options = options || {};
|
|
43
|
+
|
|
44
|
+
this.account = this.options.account || '';
|
|
45
|
+
this.accountObject = this.options.accountObject;
|
|
46
|
+
this.accountLogger = this.options.accountLogger;
|
|
47
|
+
|
|
48
|
+
this.emitLogs = this.options.accountLogger.enabled;
|
|
49
|
+
|
|
50
|
+
this.cid = this.getRandomId();
|
|
51
|
+
|
|
52
|
+
this.closing = false;
|
|
53
|
+
this.closed = false;
|
|
54
|
+
|
|
55
|
+
this.logger = this.getLogger();
|
|
56
|
+
|
|
57
|
+
this.imapConfig = {
|
|
58
|
+
// Set emitLogs to true if you want to get all the log entries as objects from the IMAP module
|
|
59
|
+
logger: this.mainLogger.child({
|
|
60
|
+
sub: 'imap-connection'
|
|
61
|
+
}),
|
|
62
|
+
clientInfo: {
|
|
63
|
+
name: packageData.name,
|
|
64
|
+
version: packageData.version,
|
|
65
|
+
vendor: (packageData.author && packageData.author.name) || packageData.author,
|
|
66
|
+
'support-url': (packageData.bugs && packageData.bugs.url) || packageData.bugs
|
|
67
|
+
},
|
|
68
|
+
emitLogs: this.emitLogs
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.mailboxes = new Map();
|
|
72
|
+
this.untaggedExistsTimer = false;
|
|
73
|
+
this.redis = options.redis;
|
|
74
|
+
this.notifyQueue = options.notifyQueue;
|
|
75
|
+
this.submitQueue = options.submitQueue;
|
|
76
|
+
|
|
77
|
+
this.refreshListingTimer = false;
|
|
78
|
+
this.resyncTimer = false;
|
|
79
|
+
|
|
80
|
+
this.completedTimer = false;
|
|
81
|
+
|
|
82
|
+
this.pathCache = new Map();
|
|
83
|
+
this.idCache = new Map();
|
|
84
|
+
|
|
85
|
+
this.defaultDelimiter = '/';
|
|
86
|
+
|
|
87
|
+
this.localAddresses = [].concat(this.options.localAddress || []).concat(this.options.localAddresses || []);
|
|
88
|
+
|
|
89
|
+
this.state = 'connecting';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getAccountKey() {
|
|
93
|
+
return `iad:${this.account}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getMailboxListKey() {
|
|
97
|
+
return `ial:${this.account}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getMailboxHashKey() {
|
|
101
|
+
return `iah:${this.account}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getLogKey() {
|
|
105
|
+
// this format ensures that the key is deleted when user is removed
|
|
106
|
+
return `iam:${this.account}:g`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getLoggedAccountsKey() {
|
|
110
|
+
return `iaz:logged`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onTaskCompleted() {
|
|
114
|
+
// check if we need to re-select main mailbox
|
|
115
|
+
this.completedTimer = setTimeout(() => {
|
|
116
|
+
clearTimeout(this.completedTimer);
|
|
117
|
+
this.ensureMainMailbox().catch(err => this.logger.error(err));
|
|
118
|
+
}, ENSURE_MAIN_TTL);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async ensureMainMailbox() {
|
|
122
|
+
let mainPath = this.main ? this.main.path : 'INBOX';
|
|
123
|
+
if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) {
|
|
124
|
+
// already selected
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// start waiting for changes
|
|
129
|
+
await this.select(mainPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async packUid(mailbox, uid) {
|
|
133
|
+
if (isNaN(uid)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof uid !== 'number') {
|
|
138
|
+
uid = Number(uid);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof mailbox === 'string') {
|
|
142
|
+
if (this.mailboxes.has(normalizePath(mailbox))) {
|
|
143
|
+
mailbox = this.mailboxes.get(normalizePath(mailbox));
|
|
144
|
+
} else {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const storedStatus = await mailbox.getStoredStatus();
|
|
150
|
+
if (!storedStatus.uidValidity || !storedStatus.path) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let uidValBuf = Buffer.alloc(8);
|
|
155
|
+
uidValBuf.writeBigUInt64BE(storedStatus.uidValidity, 0);
|
|
156
|
+
let mailboxBuf = Buffer.concat([uidValBuf, Buffer.from(storedStatus.path)]);
|
|
157
|
+
|
|
158
|
+
let mailboxId;
|
|
159
|
+
if (this.pathCache.has(mailboxBuf.toString('hex'))) {
|
|
160
|
+
mailboxId = this.pathCache.get(mailboxBuf.toString('hex'));
|
|
161
|
+
} else {
|
|
162
|
+
mailboxId = await this.redis.zGetMailboxId(this.getAccountKey(), this.getMailboxHashKey(), mailboxBuf);
|
|
163
|
+
if (isNaN(mailboxId) || typeof mailboxId !== 'number') {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.pathCache.set(mailboxBuf.toString('hex'), mailboxId);
|
|
168
|
+
this.idCache.set(mailboxId, mailboxBuf);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let uidBuf = Buffer.alloc(4 + 4);
|
|
172
|
+
uidBuf.writeUInt32BE(mailboxId, 0);
|
|
173
|
+
uidBuf.writeUInt32BE(uid, 4);
|
|
174
|
+
|
|
175
|
+
let res = uidBuf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+/g, '');
|
|
176
|
+
|
|
177
|
+
return res;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async unpackUid(id) {
|
|
181
|
+
const packed = Buffer.isBuffer(id) ? id : Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
182
|
+
|
|
183
|
+
let mailboxId = packed.readUInt32BE(0);
|
|
184
|
+
let uid = packed.readUInt32BE(4);
|
|
185
|
+
|
|
186
|
+
let mailboxBuf;
|
|
187
|
+
if (this.idCache.has(mailboxId)) {
|
|
188
|
+
mailboxBuf = this.idCache.get(mailboxId);
|
|
189
|
+
} else {
|
|
190
|
+
mailboxBuf = await this.redis.zGetMailboxPathBuffer(this.getMailboxHashKey(), mailboxId);
|
|
191
|
+
if (!mailboxBuf) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.pathCache.set(mailboxBuf.toString('hex'), mailboxId);
|
|
196
|
+
this.idCache.set(mailboxId, mailboxBuf);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!mailboxBuf) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let path = mailboxBuf.slice(8).toString();
|
|
204
|
+
return {
|
|
205
|
+
path,
|
|
206
|
+
uidValidity: mailboxBuf.readBigUInt64BE(0).toString(),
|
|
207
|
+
uid
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getMessageTextPaths(textId) {
|
|
212
|
+
let buf = Buffer.from(textId.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
213
|
+
let id = buf.slice(0, 8);
|
|
214
|
+
let textParts = msgpack.decode(buf.slice(8));
|
|
215
|
+
|
|
216
|
+
let message = await this.unpackUid(id);
|
|
217
|
+
if (!message) {
|
|
218
|
+
return { message: false };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { message, textParts };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async clearMailboxEntry(entry) {
|
|
225
|
+
this.checkIMAPConnection();
|
|
226
|
+
|
|
227
|
+
if (!entry || !entry.path) {
|
|
228
|
+
return; // ?
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let mailbox;
|
|
232
|
+
if (!this.mailboxes.has(normalizePath(entry.path))) {
|
|
233
|
+
mailbox = new Mailbox(this, entry);
|
|
234
|
+
} else {
|
|
235
|
+
mailbox = this.mailboxes.get(normalizePath(entry.path));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await mailbox.clear();
|
|
239
|
+
mailbox = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async getCurrentListing() {
|
|
243
|
+
this.checkIMAPConnection();
|
|
244
|
+
|
|
245
|
+
let listing = await this.imapClient.list();
|
|
246
|
+
|
|
247
|
+
let inboxData = (listing || []).find(entry => /^INBOX$/i.test(entry.path));
|
|
248
|
+
if (inboxData && inboxData.delimiter) {
|
|
249
|
+
this.defaultDelimiter = inboxData.delimiter;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ignore non-selectable folders
|
|
253
|
+
listing = listing.filter(mailbox => !mailbox.flags.has('\\Noselect'));
|
|
254
|
+
|
|
255
|
+
let hasChanges = false;
|
|
256
|
+
|
|
257
|
+
// compare listing for new / deleted / renamed folders
|
|
258
|
+
let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
|
|
259
|
+
storedListing = Object.keys(storedListing || {})
|
|
260
|
+
.map(path => {
|
|
261
|
+
try {
|
|
262
|
+
return msgpack.decode(storedListing[path]);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// should not happen
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
})
|
|
268
|
+
.filter(entry => entry);
|
|
269
|
+
|
|
270
|
+
// compare listings
|
|
271
|
+
for (let mailbox of listing) {
|
|
272
|
+
let existingMailbox = storedListing.find(entry => normalizePath(entry.path) === normalizePath(mailbox.path));
|
|
273
|
+
if (!existingMailbox) {
|
|
274
|
+
// found new!
|
|
275
|
+
mailbox.isNew = true;
|
|
276
|
+
hasChanges = true;
|
|
277
|
+
} else if (existingMailbox.delimiter !== mailbox.delimiter) {
|
|
278
|
+
hasChanges = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (let entry of storedListing) {
|
|
283
|
+
if (!listing.some(mailbox => normalizePath(entry.path) === normalizePath(mailbox.path))) {
|
|
284
|
+
// found deleted!
|
|
285
|
+
await this.clearMailboxEntry(entry);
|
|
286
|
+
hasChanges = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// on changes store updated listing
|
|
291
|
+
if (hasChanges) {
|
|
292
|
+
// store
|
|
293
|
+
let listingObject = {};
|
|
294
|
+
listing.forEach(entry => {
|
|
295
|
+
let mailbox = {};
|
|
296
|
+
Object.keys(entry).forEach(key => {
|
|
297
|
+
if (['path', 'specialUse', 'name', 'listed', 'subscribed', 'delimiter'].includes(key)) {
|
|
298
|
+
mailbox[key] = entry[key];
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
listingObject[normalizePath(entry.path)] = msgpack.encode(mailbox);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await this.redis.multi().del(this.getMailboxListKey()).hmset(this.getMailboxListKey(), listingObject).exec();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return listing;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async asyncPeriodicListRefresh() {
|
|
311
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
312
|
+
let listing = await this.getCurrentListing();
|
|
313
|
+
|
|
314
|
+
let syncNeeded = new Set();
|
|
315
|
+
for (let entry of listing) {
|
|
316
|
+
if (
|
|
317
|
+
// previously unseen
|
|
318
|
+
!this.mailboxes.has(normalizePath(entry.path))
|
|
319
|
+
) {
|
|
320
|
+
if (accountData.path && accountData.path !== '*') {
|
|
321
|
+
if (accountData.path !== entry.path) {
|
|
322
|
+
// ignore changes
|
|
323
|
+
entry.syncDisabled = true;
|
|
324
|
+
}
|
|
325
|
+
} else if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
|
|
326
|
+
// do not look for changes from this folder
|
|
327
|
+
entry.syncDisabled = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let mailbox = new Mailbox(this, entry);
|
|
331
|
+
this.mailboxes.set(normalizePath(entry.path), mailbox);
|
|
332
|
+
syncNeeded.add(mailbox);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// sync new mailboxes
|
|
337
|
+
for (let mailbox of syncNeeded) {
|
|
338
|
+
await mailbox.sync();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
periodicListRefresh() {
|
|
343
|
+
if (this.closing || this.closed) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
clearTimeout(this.refreshListingTimer);
|
|
347
|
+
|
|
348
|
+
this.refreshListingTimer = setTimeout(() => {
|
|
349
|
+
this.asyncPeriodicListRefresh()
|
|
350
|
+
.catch(err => {
|
|
351
|
+
this.logger.error({ msg: 'List refresh error', err });
|
|
352
|
+
})
|
|
353
|
+
.finally(() => {
|
|
354
|
+
this.periodicListRefresh();
|
|
355
|
+
});
|
|
356
|
+
}, LIST_REFRESH_DELAY);
|
|
357
|
+
this.refreshListingTimer.unref();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async connect() {
|
|
361
|
+
if (this.closing || this.closed) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let imapClient = this.imapClient;
|
|
366
|
+
|
|
367
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
368
|
+
|
|
369
|
+
// throws if connection fails
|
|
370
|
+
let response = await imapClient.connect();
|
|
371
|
+
|
|
372
|
+
let listing = await this.getCurrentListing();
|
|
373
|
+
this.periodicListRefresh();
|
|
374
|
+
|
|
375
|
+
// User might have disabled All Mail folder access and in that case we should treat it as a regular mailbox
|
|
376
|
+
this.isGmail = imapClient.capabilities.has('X-GM-EXT-1') && listing.some(entry => entry.specialUse === '\\All');
|
|
377
|
+
|
|
378
|
+
for (let entry of listing) {
|
|
379
|
+
if (accountData.path && accountData.path !== '*') {
|
|
380
|
+
if (accountData.path !== entry.path) {
|
|
381
|
+
entry.syncDisabled = true;
|
|
382
|
+
} else {
|
|
383
|
+
this.main = entry;
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
if ((this.isGmail && entry.specialUse === '\\All') || (!this.isGmail && entry.specialUse === '\\Inbox')) {
|
|
387
|
+
// In case of gmail prefer All mail folder as the folder to actively track, otherwise INBOX
|
|
388
|
+
// idle in this folder
|
|
389
|
+
this.main = entry;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
|
|
393
|
+
// do not look for changes from this folder
|
|
394
|
+
entry.syncDisabled = true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let mailbox = new Mailbox(this, entry);
|
|
399
|
+
this.mailboxes.set(normalizePath(entry.path), mailbox);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
imapClient.on('expunge', event => {
|
|
403
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
404
|
+
return; //?
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
408
|
+
mailbox.onExpunge(event).catch(err => {
|
|
409
|
+
this.logger.error({ msg: 'Expunge error', err });
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Process untagged EXISTS responses
|
|
414
|
+
imapClient.on('exists', event => {
|
|
415
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
416
|
+
return; //?
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
420
|
+
mailbox.onExists(event).catch(err => {
|
|
421
|
+
this.logger.error({ msg: 'Exists error', err });
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
imapClient.on('mailboxOpen', event => {
|
|
426
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
427
|
+
return; //?
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
431
|
+
mailbox.onOpen(event).catch(err => {
|
|
432
|
+
this.logger.error({ msg: 'Open error', err });
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
imapClient.on('mailboxClose', event => {
|
|
437
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
438
|
+
return; //?
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
442
|
+
mailbox.onClose(event).catch(err => {
|
|
443
|
+
this.logger.error({ msg: 'Close error', err });
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
imapClient.on('flags', event => {
|
|
448
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
449
|
+
return; //?
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
453
|
+
mailbox.onFlags(event).catch(err => {
|
|
454
|
+
this.logger.error({ msg: 'Flags error', err });
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
imapClient.on('close', () => {
|
|
459
|
+
let handler = async () => {
|
|
460
|
+
for (let mailbox of this.mailboxes) {
|
|
461
|
+
if (mailbox.selected) {
|
|
462
|
+
// should be at most one though
|
|
463
|
+
await mailbox.onClose();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!imapClient.disabled) {
|
|
468
|
+
await this.reconnect();
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
this.logger.info({ msg: 'Connection closed', account: this.account });
|
|
473
|
+
handler().catch(err => {
|
|
474
|
+
this.logger.error({ msg: 'Connection close error', err });
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return response;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async reconnect(force) {
|
|
482
|
+
if (this._connecting || this.closing || (this.closed && !force)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this._connecting = true;
|
|
487
|
+
this.closed = false;
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
await backOff(() => this.start(), {
|
|
491
|
+
maxDelay: MAX_BACKOFF_DELAY,
|
|
492
|
+
numOfAttempts: Infinity,
|
|
493
|
+
retry: () => !this.closing && !this.closed,
|
|
494
|
+
startingDelay: 2000
|
|
495
|
+
});
|
|
496
|
+
} finally {
|
|
497
|
+
this._connecting = false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
await this.checkIMAPConnection();
|
|
502
|
+
await this.syncMailboxes();
|
|
503
|
+
} catch (err) {
|
|
504
|
+
// ended in an unconncted state
|
|
505
|
+
this.logger.error({ msg: 'Failed to set up connection', err });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async syncMailboxes() {
|
|
510
|
+
clearTimeout(this.untaggedExpungeTimer);
|
|
511
|
+
clearTimeout(this.resyncTimer);
|
|
512
|
+
clearTimeout(this.completedTimer);
|
|
513
|
+
|
|
514
|
+
for (let mailbox of this.mailboxes.values()) {
|
|
515
|
+
await mailbox.sync();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.state = 'connected';
|
|
519
|
+
await this.redis.hset(this.getAccountKey(), 'state', 'connected');
|
|
520
|
+
await this.redis.hset(this.getAccountKey(), 'lastErrorState', '{}');
|
|
521
|
+
|
|
522
|
+
let mainPath = this.main ? this.main.path : 'INBOX';
|
|
523
|
+
if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) {
|
|
524
|
+
// already selected
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// start waiting for changes
|
|
529
|
+
await this.select(mainPath);
|
|
530
|
+
|
|
531
|
+
// schedule next sync
|
|
532
|
+
this.resyncTimer = setTimeout(() => {
|
|
533
|
+
this.syncMailboxes().catch(err => {
|
|
534
|
+
this.logger.error({ msg: 'Mailbox Sync Error', err });
|
|
535
|
+
});
|
|
536
|
+
}, this.resyncDelay);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async select(path) {
|
|
540
|
+
if (!this.mailboxes.has(normalizePath(path))) {
|
|
541
|
+
// nothing to do here, mailbox not found
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
546
|
+
await mailbox.select();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
getRandomId() {
|
|
550
|
+
let rid = BigInt('0x' + crypto.randomBytes(13).toString('hex')).toString(36);
|
|
551
|
+
if (rid.length < 20) {
|
|
552
|
+
rid = '0'.repeat(20 - rid.length) + rid;
|
|
553
|
+
} else if (rid.length > 20) {
|
|
554
|
+
rid = rid.substr(0, 20);
|
|
555
|
+
}
|
|
556
|
+
return rid;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async notify(mailbox, event, data) {
|
|
560
|
+
metrics(this.logger, 'events', 'inc', {
|
|
561
|
+
event
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
switch (event) {
|
|
565
|
+
case 'connectError':
|
|
566
|
+
case 'authenticationError':
|
|
567
|
+
return await this.setErrorState(event, data);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let payload = {
|
|
571
|
+
account: this.account,
|
|
572
|
+
date: new Date().toISOString()
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
let path = (mailbox && mailbox.path) || (data && data.path);
|
|
576
|
+
if (path) {
|
|
577
|
+
payload.path = path;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (mailbox && mailbox.listingEntry && mailbox.listingEntry.specialUse) {
|
|
581
|
+
payload.specialUse = mailbox.listingEntry.specialUse;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (event) {
|
|
585
|
+
payload.event = event;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (data) {
|
|
589
|
+
payload.data = data;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
await this.notifyQueue.add(event, payload, {
|
|
593
|
+
removeOnComplete: true,
|
|
594
|
+
removeOnFail: true,
|
|
595
|
+
attempts: 5,
|
|
596
|
+
backoff: {
|
|
597
|
+
type: 'exponential',
|
|
598
|
+
delay: 2000
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getLocalAddress() {
|
|
604
|
+
// use default
|
|
605
|
+
if (!this.localAddresses.length) {
|
|
606
|
+
return { ip: false, name: false };
|
|
607
|
+
}
|
|
608
|
+
// just one
|
|
609
|
+
if (this.localAddresses.length === 1) {
|
|
610
|
+
return this.localAddresses[0];
|
|
611
|
+
}
|
|
612
|
+
// return random from list
|
|
613
|
+
return this.localAddresses[Math.floor(Math.random() * this.localAddresses.length)];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async start() {
|
|
617
|
+
let initialState = this.state;
|
|
618
|
+
if (this.imapClient) {
|
|
619
|
+
this.imapClient.disabled = true;
|
|
620
|
+
try {
|
|
621
|
+
this.imapClient.close();
|
|
622
|
+
} catch (err) {
|
|
623
|
+
this.logger.error({ msg: 'IMAP close error', err });
|
|
624
|
+
} finally {
|
|
625
|
+
this.imapClient = null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
631
|
+
|
|
632
|
+
this.notifyFrom = accountData.notifyFrom;
|
|
633
|
+
|
|
634
|
+
if (!accountData.imap && !accountData.oauth2) {
|
|
635
|
+
// can not make connection
|
|
636
|
+
this.state = 'unset';
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
let imapConnectionConfig;
|
|
641
|
+
if (!accountData.imap && accountData.oauth2 && accountData.oauth2.auth) {
|
|
642
|
+
// load OAuth2 tokens
|
|
643
|
+
let now = Date.now();
|
|
644
|
+
let accessToken;
|
|
645
|
+
if (accountData.oauth2.expires < new Date(now + 30 * 1000)) {
|
|
646
|
+
// renew access token
|
|
647
|
+
try {
|
|
648
|
+
let oauthKeys = {
|
|
649
|
+
clientId: await settings.get('gmailClientId'),
|
|
650
|
+
clientSecret: await settings.get('gmailClientSecret'),
|
|
651
|
+
redirectUrl: await settings.get('gmailRedirectUrl')
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
if (!oauthKeys.clientId || !oauthKeys.clientSecret || !oauthKeys.redirectUrl) {
|
|
655
|
+
throw new Error('OAuth2 credentials not set up');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
accountData = await this.accountObject.renewAccessToken(oauthKeys);
|
|
659
|
+
accessToken = accountData.oauth2.accessToken;
|
|
660
|
+
} catch (err) {
|
|
661
|
+
err.authenticationFailed = true;
|
|
662
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
663
|
+
response: err.message,
|
|
664
|
+
serverResponseCode: 'OauthRenewError'
|
|
665
|
+
});
|
|
666
|
+
this.logger.error({
|
|
667
|
+
account: this.account,
|
|
668
|
+
err
|
|
669
|
+
});
|
|
670
|
+
this.state = AUTH_ERROR_NOTIFY;
|
|
671
|
+
throw err;
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
accessToken = accountData.oauth2.accessToken;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
imapConnectionConfig = {
|
|
678
|
+
auth: {
|
|
679
|
+
user: accountData.oauth2.auth.user,
|
|
680
|
+
accessToken
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
host: 'imap.gmail.com',
|
|
684
|
+
port: 993,
|
|
685
|
+
secure: true,
|
|
686
|
+
resyncDelay: 900
|
|
687
|
+
};
|
|
688
|
+
} else {
|
|
689
|
+
// deep copy of imap settings
|
|
690
|
+
imapConnectionConfig = JSON.parse(JSON.stringify(accountData.imap));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// If authentication server is set then it overrides authentication data
|
|
694
|
+
if (imapConnectionConfig.useAuthServer) {
|
|
695
|
+
try {
|
|
696
|
+
imapConnectionConfig.auth = await resolveCredentials(this.account, 'imap');
|
|
697
|
+
} catch (err) {
|
|
698
|
+
err.authenticationFailed = true;
|
|
699
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
700
|
+
response: err.message,
|
|
701
|
+
serverResponseCode: 'HTTPRequestError'
|
|
702
|
+
});
|
|
703
|
+
this.logger.error({
|
|
704
|
+
account: this.account,
|
|
705
|
+
err
|
|
706
|
+
});
|
|
707
|
+
this.state = AUTH_ERROR_NOTIFY;
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (!imapConnectionConfig.tls) {
|
|
713
|
+
imapConnectionConfig.tls = {};
|
|
714
|
+
}
|
|
715
|
+
imapConnectionConfig.tls.localAddress = this.getLocalAddress().address;
|
|
716
|
+
|
|
717
|
+
let imapConfig = Object.assign(
|
|
718
|
+
{
|
|
719
|
+
resyncDelay: RESYNC_DELAY
|
|
720
|
+
},
|
|
721
|
+
imapConnectionConfig,
|
|
722
|
+
this.imapConfig
|
|
723
|
+
);
|
|
724
|
+
this.resyncDelay = imapConfig.resyncDelay * 1000;
|
|
725
|
+
|
|
726
|
+
this.imapClient = new ImapFlow(imapConfig);
|
|
727
|
+
|
|
728
|
+
// if emitLogs option is true then separate log event is fired for every log entry
|
|
729
|
+
this.imapClient.on('log', entry => {
|
|
730
|
+
if (!entry) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (typeof entry === 'string') {
|
|
735
|
+
// should not happen
|
|
736
|
+
entry = { msg: entry };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
this.accountLogger.log(entry);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
this.imapClient.on('error', err => {
|
|
743
|
+
this.logger.error({ msg: 'IMAP connection error', account: this.account, err });
|
|
744
|
+
this.reconnect().catch(err => {
|
|
745
|
+
this.logger.error({ msg: 'IMAP reconnection error', account: this.account, err });
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
await this.connect();
|
|
751
|
+
await this.notify(false, AUTH_SUCCESS_NOTIFY, {
|
|
752
|
+
user: imapConfig.auth.user
|
|
753
|
+
});
|
|
754
|
+
} catch (err) {
|
|
755
|
+
if (err.authenticationFailed) {
|
|
756
|
+
this.logger.error({ msg: 'Failed to authenticate', account: this.account, err });
|
|
757
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
758
|
+
response: err.response,
|
|
759
|
+
serverResponseCode: err.serverResponseCode
|
|
760
|
+
});
|
|
761
|
+
this.state = 'authenticationError';
|
|
762
|
+
} else {
|
|
763
|
+
this.logger.error({ msg: 'Failed to connect', account: this.account, err });
|
|
764
|
+
await this.notify(false, CONNECT_ERROR_NOTIFY, {
|
|
765
|
+
response: err.response || err.message,
|
|
766
|
+
serverResponseCode: err.serverResponseCode || err.code
|
|
767
|
+
});
|
|
768
|
+
this.state = 'connectError';
|
|
769
|
+
}
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
} finally {
|
|
773
|
+
if (this.state !== initialState) {
|
|
774
|
+
// update state
|
|
775
|
+
try {
|
|
776
|
+
await this.redis.hset(this.getAccountKey(), 'state', this.state);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
// ignore
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async init() {
|
|
785
|
+
await this.reconnect();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async setErrorState(event, data) {
|
|
789
|
+
await this.redis.hmset(this.getAccountKey(), {
|
|
790
|
+
state: event,
|
|
791
|
+
lastErrorState: JSON.stringify(data)
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async delete() {
|
|
796
|
+
if (this.closed || this.closing) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this.closing = true;
|
|
800
|
+
|
|
801
|
+
if (this.imapClient) {
|
|
802
|
+
this.imapClient.close();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
clearTimeout(this.refreshListingTimer);
|
|
806
|
+
clearTimeout(this.untaggedExpungeTimer);
|
|
807
|
+
clearTimeout(this.resyncTimer);
|
|
808
|
+
clearTimeout(this.completedTimer);
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
for (let [, mailbox] of this.mailboxes) {
|
|
812
|
+
await mailbox.clear({ skipNotify: true });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
await this.redis.del(this.getMailboxListKey());
|
|
816
|
+
} finally {
|
|
817
|
+
this.closing = false;
|
|
818
|
+
this.closed = true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
this.logger.info({ msg: 'Closed account', account: this.account });
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async close() {
|
|
825
|
+
this.state = 'disconnected';
|
|
826
|
+
if (this.closed || this.closing) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.closing = true;
|
|
830
|
+
|
|
831
|
+
if (this.imapClient) {
|
|
832
|
+
this.imapClient.close();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
clearTimeout(this.refreshListingTimer);
|
|
836
|
+
clearTimeout(this.untaggedExpungeTimer);
|
|
837
|
+
clearTimeout(this.resyncTimer);
|
|
838
|
+
clearTimeout(this.completedTimer);
|
|
839
|
+
|
|
840
|
+
this.closing = false;
|
|
841
|
+
this.closed = true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
isConnected() {
|
|
845
|
+
return this.imapClient && this.imapClient.usable && !this.closing && !this.closed;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
currentState() {
|
|
849
|
+
if (this.state === 'connected' && !this.isConnected()) {
|
|
850
|
+
this.state = 'disconnected';
|
|
851
|
+
}
|
|
852
|
+
return this.state;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
checkIMAPConnection() {
|
|
856
|
+
if (!this.isConnected()) {
|
|
857
|
+
let err = new Error('IMAP connection temporarily not available');
|
|
858
|
+
err.code = 'IMAPUnavailable';
|
|
859
|
+
err.statusCode = 503;
|
|
860
|
+
throw err;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Mailbox level user methods
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Fetch message text from IMAP. Resulting value is a unicode string.
|
|
868
|
+
*
|
|
869
|
+
* @param {string} textId ID of the text content
|
|
870
|
+
* @param {object} [options] Options object
|
|
871
|
+
* @param {number} [options.maxLength] If set then limits output stream to specified chars (NB! not bytes but unicode characters). Limit applies to each text type separately, so 1000 would mean you'd get a 1000 char string for plaintext and 1000 char string for html.
|
|
872
|
+
* @param {string} [options.contentType] If set then limits output for selected type only
|
|
873
|
+
* @returns {Object} Text object, where key is text type (either 'plain' or 'html') and value is a unicode string
|
|
874
|
+
*/
|
|
875
|
+
async getText(textId, options) {
|
|
876
|
+
options = options || {};
|
|
877
|
+
this.checkIMAPConnection();
|
|
878
|
+
|
|
879
|
+
let { message, textParts } = await this.getMessageTextPaths(textId);
|
|
880
|
+
if (!message || !textParts || !textParts.length) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
885
|
+
return; //?
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
889
|
+
|
|
890
|
+
let textType = (options.textType || '').toLowerCase().trim();
|
|
891
|
+
|
|
892
|
+
if (Array.isArray(textParts)) {
|
|
893
|
+
let re = /^\d+(\.\d+)*$/;
|
|
894
|
+
switch (textType) {
|
|
895
|
+
case 'plain':
|
|
896
|
+
textParts = Array.isArray(textParts[0]) ? textParts[0].filter(entry => re.test(entry)) : false;
|
|
897
|
+
break;
|
|
898
|
+
case 'html':
|
|
899
|
+
textParts = Array.isArray(textParts[1]) ? textParts[1].filter(entry => re.test(entry)) : false;
|
|
900
|
+
break;
|
|
901
|
+
default:
|
|
902
|
+
textParts = textParts.flatMap(part => part).filter(entry => re.test(entry));
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
} else {
|
|
906
|
+
textParts = [];
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
let result = await mailbox.getText(message, textParts, options);
|
|
910
|
+
|
|
911
|
+
if (textType && textType !== '*') {
|
|
912
|
+
result = {
|
|
913
|
+
[textType]: result[textType] || '',
|
|
914
|
+
hasMore: result.hasMore
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async getMessage(id, options) {
|
|
922
|
+
options = options || {};
|
|
923
|
+
this.checkIMAPConnection();
|
|
924
|
+
|
|
925
|
+
let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
926
|
+
let message = await this.unpackUid(buf.slice(0, 8));
|
|
927
|
+
if (!message) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
932
|
+
return false; //?
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
936
|
+
|
|
937
|
+
return await mailbox.getMessage(message, options);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async updateMessage(id, updates) {
|
|
941
|
+
updates = updates || {};
|
|
942
|
+
this.checkIMAPConnection();
|
|
943
|
+
|
|
944
|
+
let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
945
|
+
let message = await this.unpackUid(buf.slice(0, 8));
|
|
946
|
+
if (!message) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
951
|
+
return false; //?
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
955
|
+
|
|
956
|
+
return await mailbox.updateMessage(message, updates);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async moveMessage(id, target) {
|
|
960
|
+
target = target || {};
|
|
961
|
+
|
|
962
|
+
this.checkIMAPConnection();
|
|
963
|
+
|
|
964
|
+
let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
965
|
+
let message = await this.unpackUid(buf.slice(0, 8));
|
|
966
|
+
|
|
967
|
+
if (!message) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
972
|
+
return false; //?
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
976
|
+
return await mailbox.moveMessage(message, target);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async deleteMessage(id) {
|
|
980
|
+
this.checkIMAPConnection();
|
|
981
|
+
|
|
982
|
+
let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
983
|
+
let message = await this.unpackUid(buf.slice(0, 8));
|
|
984
|
+
if (!message) {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
989
|
+
return false; //?
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
993
|
+
|
|
994
|
+
return await mailbox.deleteMessage(message);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Downloads an attachment from IMAP as a binary stream
|
|
999
|
+
*
|
|
1000
|
+
* @param {string} attachmentId ID of the attachment
|
|
1001
|
+
* @param {object} [options] Options object
|
|
1002
|
+
* @param {number} [options.maxLength] If set then limits output stream to specified bytes
|
|
1003
|
+
* @returns {Boolean|Stream} Attahcment stream or `false` if not found
|
|
1004
|
+
*/
|
|
1005
|
+
async getAttachment(attachmentId, options) {
|
|
1006
|
+
this.checkIMAPConnection();
|
|
1007
|
+
|
|
1008
|
+
let buf = Buffer.from(attachmentId.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
1009
|
+
let id = buf.slice(0, 8);
|
|
1010
|
+
let part = buf.slice(8).toString();
|
|
1011
|
+
|
|
1012
|
+
let message = await this.unpackUid(id);
|
|
1013
|
+
if (!message) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1018
|
+
return false; //?
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1022
|
+
|
|
1023
|
+
return mailbox.getAttachment(message, part, options);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Downloads raw message from IMAP as a binary stream
|
|
1028
|
+
*
|
|
1029
|
+
* @param {string} id ID of the message
|
|
1030
|
+
* @param {object} [options] Options object
|
|
1031
|
+
* @param {number} [options.maxLength] If set then limits output stream to specified bytes
|
|
1032
|
+
* @returns {Boolean|Stream} Attahcment stream or `false` if not found
|
|
1033
|
+
*/
|
|
1034
|
+
async getRawMessage(id, options) {
|
|
1035
|
+
this.checkIMAPConnection();
|
|
1036
|
+
|
|
1037
|
+
let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
1038
|
+
let message = await this.unpackUid(buf.slice(0, 8));
|
|
1039
|
+
if (!message) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1044
|
+
return false; //?
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1048
|
+
|
|
1049
|
+
return mailbox.getAttachment(message, false, options);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async listMessages(options) {
|
|
1053
|
+
options = options || {};
|
|
1054
|
+
this.checkIMAPConnection();
|
|
1055
|
+
|
|
1056
|
+
if (!this.mailboxes.has(normalizePath(options.path))) {
|
|
1057
|
+
return false; //?
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
let mailbox = this.mailboxes.get(normalizePath(options.path));
|
|
1061
|
+
|
|
1062
|
+
return mailbox.listMessages(options);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async buildContacts() {
|
|
1066
|
+
this.checkIMAPConnection();
|
|
1067
|
+
|
|
1068
|
+
let addresses = [];
|
|
1069
|
+
let addressesMap = new Map();
|
|
1070
|
+
|
|
1071
|
+
for (let [, mailbox] of this.mailboxes) {
|
|
1072
|
+
if ((this.isGmail && mailbox.listingEntry.specialUse !== '\\All') || ['\\Junk', '\\Trash'].includes(mailbox.listingEntry.specialUse)) {
|
|
1073
|
+
// Only look into All Mail for Gmail account, ignore Junk/Trash for other account
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
let result = await mailbox.buildContacts();
|
|
1078
|
+
for (let address of result) {
|
|
1079
|
+
if (!address.address) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
address.address = address.address.replace(/\+[^@]*@/, '@');
|
|
1084
|
+
|
|
1085
|
+
if (!addressesMap.has(address.address)) {
|
|
1086
|
+
addressesMap.set(
|
|
1087
|
+
address.address,
|
|
1088
|
+
new Map([
|
|
1089
|
+
['count', 0],
|
|
1090
|
+
['names', new Map()],
|
|
1091
|
+
[
|
|
1092
|
+
'types',
|
|
1093
|
+
new Map([
|
|
1094
|
+
['from', 0],
|
|
1095
|
+
['to', 0],
|
|
1096
|
+
['cc', 0]
|
|
1097
|
+
])
|
|
1098
|
+
]
|
|
1099
|
+
])
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
let addressMap = addressesMap.get(address.address);
|
|
1104
|
+
|
|
1105
|
+
addressMap.set('count', addressMap.get('count') + 1);
|
|
1106
|
+
|
|
1107
|
+
let typeMap = addressMap.get('types');
|
|
1108
|
+
if (typeMap.has(address.type)) {
|
|
1109
|
+
typeMap.set(address.type, typeMap.get(address.type) + 1);
|
|
1110
|
+
} else {
|
|
1111
|
+
typeMap.set(address.type, 1);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (address.name) {
|
|
1115
|
+
let nameMap = addressMap.get('names');
|
|
1116
|
+
if (nameMap.has(address.name)) {
|
|
1117
|
+
nameMap.set(address.name, nameMap.get(address.name) + 1);
|
|
1118
|
+
} else {
|
|
1119
|
+
nameMap.set(address.name, 1);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
for (let [address, data] of addressesMap) {
|
|
1126
|
+
let names = data.has('names') ? Object.fromEntries(data.get('names').entries()) : {};
|
|
1127
|
+
let mainName = { name: '', count: -1 };
|
|
1128
|
+
|
|
1129
|
+
if (data.has('names')) {
|
|
1130
|
+
for (let [name, count] of data.get('names')) {
|
|
1131
|
+
if (name && count > mainName.count) {
|
|
1132
|
+
mainName = {
|
|
1133
|
+
name,
|
|
1134
|
+
count
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
addresses.push({
|
|
1141
|
+
address,
|
|
1142
|
+
name: mainName.name,
|
|
1143
|
+
count: data.get('count'),
|
|
1144
|
+
names,
|
|
1145
|
+
types: data.has('types') ? Object.fromEntries(data.get('types').entries()) : {}
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
addresses.sort((a, b) => b.count - a.count);
|
|
1150
|
+
return { addresses };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async deleteMailbox(path) {
|
|
1154
|
+
this.checkIMAPConnection();
|
|
1155
|
+
|
|
1156
|
+
let result = {
|
|
1157
|
+
path,
|
|
1158
|
+
deleted: false // set to true if mailbox is actually deleted
|
|
1159
|
+
};
|
|
1160
|
+
try {
|
|
1161
|
+
let lock = await this.imapClient.getMailboxLock(path);
|
|
1162
|
+
|
|
1163
|
+
try {
|
|
1164
|
+
await this.imapClient.mailboxClose();
|
|
1165
|
+
try {
|
|
1166
|
+
await this.imapClient.mailboxDelete(path);
|
|
1167
|
+
result.deleted = true;
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
// kind of ignore
|
|
1170
|
+
}
|
|
1171
|
+
} finally {
|
|
1172
|
+
lock.release();
|
|
1173
|
+
}
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
this.logger.debug({ msg: 'Mailbox select error', path, err });
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (this.mailboxes.has(normalizePath(path))) {
|
|
1179
|
+
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
1180
|
+
await mailbox.clear();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return result;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async createMailbox(path) {
|
|
1187
|
+
this.checkIMAPConnection();
|
|
1188
|
+
let result = await this.imapClient.mailboxCreate(path);
|
|
1189
|
+
if (result) {
|
|
1190
|
+
result.created = !!result.created;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1194
|
+
|
|
1195
|
+
setImmediate(() => {
|
|
1196
|
+
this.getCurrentListing()
|
|
1197
|
+
.then(listing => {
|
|
1198
|
+
let syncNeeded = new Set();
|
|
1199
|
+
for (let entry of listing) {
|
|
1200
|
+
if (
|
|
1201
|
+
// previously unseen
|
|
1202
|
+
!this.mailboxes.has(normalizePath(entry.path))
|
|
1203
|
+
) {
|
|
1204
|
+
if (accountData.path && accountData.path !== '*') {
|
|
1205
|
+
if (accountData.path !== entry.path) {
|
|
1206
|
+
// ignore changes
|
|
1207
|
+
entry.syncDisabled = true;
|
|
1208
|
+
}
|
|
1209
|
+
} else if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
|
|
1210
|
+
// do not look for changes from this folder
|
|
1211
|
+
entry.syncDisabled = true;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
let mailbox = new Mailbox(this, entry);
|
|
1215
|
+
this.mailboxes.set(normalizePath(entry.path), mailbox);
|
|
1216
|
+
syncNeeded.add(mailbox);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
let runSyncs = async () => {
|
|
1221
|
+
// sync new mailboxes
|
|
1222
|
+
for (let mailbox of syncNeeded) {
|
|
1223
|
+
await mailbox.sync();
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
return runSyncs();
|
|
1228
|
+
})
|
|
1229
|
+
.catch(err => {
|
|
1230
|
+
this.logger.error({ msg: 'List refresh error', err });
|
|
1231
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
return result;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async getSpecialUseMailbox(specialUse) {
|
|
1238
|
+
let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
|
|
1239
|
+
return Object.keys(storedListing || {})
|
|
1240
|
+
.map(path => {
|
|
1241
|
+
try {
|
|
1242
|
+
return msgpack.decode(storedListing[path]);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
// should not happen
|
|
1245
|
+
}
|
|
1246
|
+
return false;
|
|
1247
|
+
})
|
|
1248
|
+
.filter(entry => entry)
|
|
1249
|
+
.find(entry => entry.specialUse === specialUse);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async submitMessage(data) {
|
|
1253
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1254
|
+
if (!accountData.smtp && !accountData.oauth2) {
|
|
1255
|
+
// can not make connection
|
|
1256
|
+
let err = new Error('SMTP configuration not found');
|
|
1257
|
+
err.code = 'SMTPUnavailable';
|
|
1258
|
+
err.statusCode = 404;
|
|
1259
|
+
throw err;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
let smtpConnectionConfig;
|
|
1263
|
+
if (!accountData.smtp && accountData.oauth2 && accountData.oauth2.auth) {
|
|
1264
|
+
// load OAuth2 tokens
|
|
1265
|
+
let now = Date.now();
|
|
1266
|
+
let accessToken;
|
|
1267
|
+
if (accountData.oauth2.expires < now + 30 * 1000) {
|
|
1268
|
+
// renew access token
|
|
1269
|
+
try {
|
|
1270
|
+
let oauthKeys = {
|
|
1271
|
+
clientId: await settings.get('gmailClientId'),
|
|
1272
|
+
clientSecret: await settings.get('gmailClientSecret'),
|
|
1273
|
+
redirectUrl: await settings.get('gmailRedirectUrl')
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
if (!oauthKeys.clientId || !oauthKeys.clientSecret || !oauthKeys.redirectUrl) {
|
|
1277
|
+
throw new Error('OAuth2 credentials not set up');
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
accountData = await this.accountObject.renewAccessToken(oauthKeys);
|
|
1281
|
+
accessToken = accountData.oauth2.accessToken;
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
err.authenticationFailed = true;
|
|
1284
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
1285
|
+
response: err.message,
|
|
1286
|
+
serverResponseCode: 'OauthRenewError'
|
|
1287
|
+
});
|
|
1288
|
+
this.logger.error({
|
|
1289
|
+
account: this.account,
|
|
1290
|
+
err
|
|
1291
|
+
});
|
|
1292
|
+
this.state = AUTH_ERROR_NOTIFY;
|
|
1293
|
+
throw err;
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
accessToken = accountData.oauth2.accessToken;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
smtpConnectionConfig = {
|
|
1300
|
+
auth: {
|
|
1301
|
+
user: accountData.oauth2.auth.user,
|
|
1302
|
+
accessToken
|
|
1303
|
+
},
|
|
1304
|
+
|
|
1305
|
+
host: 'smtp.gmail.com',
|
|
1306
|
+
port: 465,
|
|
1307
|
+
secure: true,
|
|
1308
|
+
resyncDelay: 900
|
|
1309
|
+
};
|
|
1310
|
+
} else {
|
|
1311
|
+
// deep copy of imap settings
|
|
1312
|
+
smtpConnectionConfig = JSON.parse(JSON.stringify(accountData.smtp));
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
let { raw, hasBcc, envelope, messageId, queueId, reference } = data;
|
|
1316
|
+
|
|
1317
|
+
let smtpAuth = smtpConnectionConfig.auth;
|
|
1318
|
+
// If authentication server is set then it overrides authentication data
|
|
1319
|
+
if (smtpConnectionConfig.useAuthServer) {
|
|
1320
|
+
try {
|
|
1321
|
+
smtpAuth = await resolveCredentials(this.account, 'smtp');
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
err.authenticationFailed = true;
|
|
1324
|
+
this.logger.error({
|
|
1325
|
+
account: this.account,
|
|
1326
|
+
err
|
|
1327
|
+
});
|
|
1328
|
+
throw err;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
let { address, name } = this.getLocalAddress();
|
|
1333
|
+
|
|
1334
|
+
let smtpLogger = {};
|
|
1335
|
+
let smtpSettings = Object.assign(
|
|
1336
|
+
{
|
|
1337
|
+
name,
|
|
1338
|
+
localAddress: address,
|
|
1339
|
+
transactionLog: true,
|
|
1340
|
+
logger: smtpLogger
|
|
1341
|
+
},
|
|
1342
|
+
smtpConnectionConfig
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
smtpSettings.auth = {
|
|
1346
|
+
user: smtpAuth.user
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
if (smtpAuth.accessToken) {
|
|
1350
|
+
smtpSettings.auth.type = 'OAuth2';
|
|
1351
|
+
smtpSettings.auth.accessToken = smtpAuth.accessToken;
|
|
1352
|
+
} else {
|
|
1353
|
+
smtpSettings.auth.pass = smtpAuth.pass;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
for (let level of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
|
|
1357
|
+
smtpLogger[level] = (data, message, ...args) => {
|
|
1358
|
+
if (args && args.length) {
|
|
1359
|
+
message = util.format(message, ...args);
|
|
1360
|
+
}
|
|
1361
|
+
data.msg = message;
|
|
1362
|
+
data.sub = 'nodemailer';
|
|
1363
|
+
if (typeof this.logger[level] === 'function') {
|
|
1364
|
+
this.logger[level](data);
|
|
1365
|
+
} else {
|
|
1366
|
+
this.logger.debug(data);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const transporter = nodemailer.createTransport(smtpSettings);
|
|
1372
|
+
try {
|
|
1373
|
+
const info = await transporter.sendMail({
|
|
1374
|
+
envelope,
|
|
1375
|
+
messageId,
|
|
1376
|
+
// make sure that Bcc line is removed from the version sent to SMTP
|
|
1377
|
+
raw: !hasBcc ? raw : await removeBcc(raw)
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
if (
|
|
1381
|
+
!this.isGmail &&
|
|
1382
|
+
// if account settings do not have a copy value or it is set to true
|
|
1383
|
+
(!Object.prototype.hasOwnProperty.call(accountData, 'copy') || accountData.copy)
|
|
1384
|
+
) {
|
|
1385
|
+
// Upload message to Sent Mail folder. Gmail does this automatically.
|
|
1386
|
+
try {
|
|
1387
|
+
this.checkIMAPConnection();
|
|
1388
|
+
let sentMailbox = await this.getSpecialUseMailbox('\\Sent');
|
|
1389
|
+
if (sentMailbox) {
|
|
1390
|
+
await this.imapClient.append(sentMailbox.path, raw, ['\\Seen']);
|
|
1391
|
+
}
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
this.logger.error({ msg: 'Failed to upload Sent mail', queueId, messageId, err });
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Add \Answered flag to referenced message if needed
|
|
1398
|
+
if (reference && reference.update) {
|
|
1399
|
+
try {
|
|
1400
|
+
this.checkIMAPConnection();
|
|
1401
|
+
await this.updateMessage(reference.message, {
|
|
1402
|
+
flags: {
|
|
1403
|
+
add: ['\\Answered']
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
this.logger.error({ msg: 'Failed to update reference flags', queueId, messageId, reference, err });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
await this.notify(false, EMAIL_SENT_NOTIFY, {
|
|
1412
|
+
messageId: info.messageId,
|
|
1413
|
+
response: info.response,
|
|
1414
|
+
queueId,
|
|
1415
|
+
envelope
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
return {
|
|
1419
|
+
response: info.response,
|
|
1420
|
+
messageId: info.messageId
|
|
1421
|
+
};
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
err.code = 'SubmitFail';
|
|
1424
|
+
err.statusCode = 502;
|
|
1425
|
+
throw err;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async queueMessage(data) {
|
|
1430
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1431
|
+
if (!accountData.smtp && !accountData.oauth2) {
|
|
1432
|
+
// can not make connection
|
|
1433
|
+
let err = new Error('SMTP configuration not found');
|
|
1434
|
+
err.code = 'SMTPUnavailable';
|
|
1435
|
+
err.statusCode = 404;
|
|
1436
|
+
throw err;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// normal message
|
|
1440
|
+
data.disableFileAccess = true;
|
|
1441
|
+
data.disableUrlAccess = true;
|
|
1442
|
+
|
|
1443
|
+
// convert data uri images to attachments
|
|
1444
|
+
data.attachDataUrls = true;
|
|
1445
|
+
|
|
1446
|
+
// Resolve reference and update reference/in-reply-to headers
|
|
1447
|
+
if (data.reference && data.reference.message) {
|
|
1448
|
+
let referencedMessage;
|
|
1449
|
+
try {
|
|
1450
|
+
this.checkIMAPConnection();
|
|
1451
|
+
referencedMessage = await this.getMessage(data.reference.message, {
|
|
1452
|
+
fields: {
|
|
1453
|
+
uid: true,
|
|
1454
|
+
flags: true,
|
|
1455
|
+
envelope: true,
|
|
1456
|
+
headers: ['references']
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
if (!referencedMessage) {
|
|
1460
|
+
throw new Error('Referenced message was not found');
|
|
1461
|
+
}
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
this.logger.error({ msg: 'Failed to get referenced message', reference: data.reference.message, err });
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (referencedMessage) {
|
|
1467
|
+
let references = []
|
|
1468
|
+
.concat(referencedMessage.messageId || [])
|
|
1469
|
+
.concat(referencedMessage.inReplyTo || [])
|
|
1470
|
+
.concat((referencedMessage.headers && referencedMessage.headers.references) || [])
|
|
1471
|
+
.flatMap(line => line.split(/\s+/))
|
|
1472
|
+
.map(ref => ref.trim())
|
|
1473
|
+
.filter(ref => ref)
|
|
1474
|
+
.map(ref => {
|
|
1475
|
+
if (!/^</.test(ref)) {
|
|
1476
|
+
ref = '<' + ref;
|
|
1477
|
+
}
|
|
1478
|
+
if (!/>$/.test(ref)) {
|
|
1479
|
+
ref = ref + '>';
|
|
1480
|
+
}
|
|
1481
|
+
return ref;
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
references = Array.from(new Set(references));
|
|
1485
|
+
if (references.length) {
|
|
1486
|
+
if (!data.headers) {
|
|
1487
|
+
data.headers = {};
|
|
1488
|
+
}
|
|
1489
|
+
data.headers.references = references.join(' ');
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (data.reference.action === 'reply' && referencedMessage.messageId) {
|
|
1493
|
+
data.headers['in-reply-to'] = referencedMessage.messageId;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (!referencedMessage.flags || !referencedMessage.flags.includes('\\Answered')) {
|
|
1497
|
+
data.reference.update = true;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (!data.subject && referencedMessage.subject) {
|
|
1501
|
+
let subject = referencedMessage.subject;
|
|
1502
|
+
let prefix;
|
|
1503
|
+
switch (data.reference.action) {
|
|
1504
|
+
case 'reply':
|
|
1505
|
+
if (!/^Re:/i.test(subject)) {
|
|
1506
|
+
prefix = 'Re';
|
|
1507
|
+
}
|
|
1508
|
+
break;
|
|
1509
|
+
case 'forward':
|
|
1510
|
+
if (!/^Fwd:/i.test(subject)) {
|
|
1511
|
+
prefix = 'Fwd';
|
|
1512
|
+
}
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
data.subject = `${prefix ? prefix + ': ' : ''}${subject}`;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const { raw, hasBcc, envelope, messageId, sendAt } = await getRawEmail(data);
|
|
1521
|
+
|
|
1522
|
+
let now = new Date();
|
|
1523
|
+
|
|
1524
|
+
//queue for later
|
|
1525
|
+
let qId = crypto.randomBytes(8).toString('hex');
|
|
1526
|
+
let msgEntry = msgpack.encode({
|
|
1527
|
+
qId,
|
|
1528
|
+
hasBcc,
|
|
1529
|
+
envelope,
|
|
1530
|
+
messageId,
|
|
1531
|
+
reference: data.reference || {},
|
|
1532
|
+
sendAt: (sendAt && sendAt.getTime()) || false,
|
|
1533
|
+
created: now.getTime(),
|
|
1534
|
+
raw
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
await this.redis.hsetBuffer(`iaq:${this.account}`, qId, msgEntry);
|
|
1538
|
+
|
|
1539
|
+
let job;
|
|
1540
|
+
if (sendAt && sendAt > now) {
|
|
1541
|
+
job = await this.submitQueue.add(
|
|
1542
|
+
'delayed',
|
|
1543
|
+
{ account: this.account, qId, messageId, created: now.getTime() },
|
|
1544
|
+
{
|
|
1545
|
+
removeOnComplete: true,
|
|
1546
|
+
removeOnFail: true,
|
|
1547
|
+
attempts: 5,
|
|
1548
|
+
backoff: {
|
|
1549
|
+
type: 'exponential',
|
|
1550
|
+
delay: 2000
|
|
1551
|
+
},
|
|
1552
|
+
delay: sendAt.getTime() - now.getTime()
|
|
1553
|
+
}
|
|
1554
|
+
);
|
|
1555
|
+
} else {
|
|
1556
|
+
job = await this.submitQueue.add(
|
|
1557
|
+
'queued',
|
|
1558
|
+
{ account: this.account, qId, messageId, created: now.getTime() },
|
|
1559
|
+
{
|
|
1560
|
+
removeOnComplete: true,
|
|
1561
|
+
removeOnFail: true,
|
|
1562
|
+
attempts: 5,
|
|
1563
|
+
backoff: {
|
|
1564
|
+
type: 'exponential',
|
|
1565
|
+
delay: 2000
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
this.logger.info({ msg: 'Message queued for delivery', envelope, messageId, sendAt: (sendAt || now).toISOString(), queueId: qId, job: job.id });
|
|
1572
|
+
|
|
1573
|
+
return {
|
|
1574
|
+
response: 'Queued for delivery',
|
|
1575
|
+
messageId,
|
|
1576
|
+
sendAt: (sendAt || now).toISOString(),
|
|
1577
|
+
queueId: qId
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
async uploadMessage(data) {
|
|
1582
|
+
this.checkIMAPConnection();
|
|
1583
|
+
|
|
1584
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1585
|
+
if (!accountData.smtp) {
|
|
1586
|
+
// can not make connection
|
|
1587
|
+
let err = new Error('SMTP configuration not found');
|
|
1588
|
+
err.code = 'SMTPUnavailable';
|
|
1589
|
+
err.statusCode = 404;
|
|
1590
|
+
throw err;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
data.disableFileAccess = true;
|
|
1594
|
+
data.disableUrlAccess = true;
|
|
1595
|
+
|
|
1596
|
+
// convert data uri images to attachments
|
|
1597
|
+
data.attachDataUrls = true;
|
|
1598
|
+
|
|
1599
|
+
// Resolve reference and update reference/in-reply-to headers
|
|
1600
|
+
if (data.reference && data.reference.message) {
|
|
1601
|
+
let referencedMessage = await this.getMessage(data.reference.message, {
|
|
1602
|
+
fields: {
|
|
1603
|
+
uid: true,
|
|
1604
|
+
flags: true,
|
|
1605
|
+
envelope: true,
|
|
1606
|
+
headers: ['references']
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
if (!referencedMessage) {
|
|
1611
|
+
let err = new Error('Referenced message was not found');
|
|
1612
|
+
err.code = 'MessageNotFound';
|
|
1613
|
+
err.statusCode = 404;
|
|
1614
|
+
throw err;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
let references = []
|
|
1618
|
+
.concat(referencedMessage.messageId || [])
|
|
1619
|
+
.concat(referencedMessage.inReplyTo || [])
|
|
1620
|
+
.concat((referencedMessage.headers && referencedMessage.headers.references) || [])
|
|
1621
|
+
.flatMap(line => line.split(/\s+/))
|
|
1622
|
+
.map(ref => ref.trim())
|
|
1623
|
+
.filter(ref => ref)
|
|
1624
|
+
.map(ref => {
|
|
1625
|
+
if (!/^</.test(ref)) {
|
|
1626
|
+
ref = '<' + ref;
|
|
1627
|
+
}
|
|
1628
|
+
if (!/>$/.test(ref)) {
|
|
1629
|
+
ref = ref + '>';
|
|
1630
|
+
}
|
|
1631
|
+
return ref;
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
references = Array.from(new Set(references));
|
|
1635
|
+
if (references.length) {
|
|
1636
|
+
if (!data.headers) {
|
|
1637
|
+
data.headers = {};
|
|
1638
|
+
}
|
|
1639
|
+
data.headers.references = references.join(' ');
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (data.reference.action === 'reply' && referencedMessage.messageId) {
|
|
1643
|
+
data.headers['in-reply-to'] = referencedMessage.messageId;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (!referencedMessage.flags || !referencedMessage.flags.includes('\\Answered')) {
|
|
1647
|
+
try {
|
|
1648
|
+
await this.updateMessage(data.reference.message, {
|
|
1649
|
+
flags: {
|
|
1650
|
+
add: ['\\Answered']
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
this.logger.error(err);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (!data.subject && referencedMessage.subject) {
|
|
1659
|
+
let subject = referencedMessage.subject;
|
|
1660
|
+
let prefix;
|
|
1661
|
+
switch (data.reference.action) {
|
|
1662
|
+
case 'reply':
|
|
1663
|
+
if (!/^Re:/i.test(subject)) {
|
|
1664
|
+
prefix = 'Re';
|
|
1665
|
+
}
|
|
1666
|
+
break;
|
|
1667
|
+
case 'forward':
|
|
1668
|
+
if (!/^Fwd:/i.test(subject)) {
|
|
1669
|
+
prefix = 'Fwd';
|
|
1670
|
+
}
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
data.subject = `${prefix ? prefix + ': ' : ''}${subject}`;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const mail = new MailComposer(data);
|
|
1678
|
+
let raw = await mail.compile().build();
|
|
1679
|
+
|
|
1680
|
+
// Upload message to selected folder
|
|
1681
|
+
try {
|
|
1682
|
+
let lock = await this.imapClient.getMailboxLock(data.path);
|
|
1683
|
+
let response = {};
|
|
1684
|
+
|
|
1685
|
+
try {
|
|
1686
|
+
let uploadResponse = await this.imapClient.append(data.path, raw, data.flags);
|
|
1687
|
+
|
|
1688
|
+
response.path = uploadResponse.path;
|
|
1689
|
+
|
|
1690
|
+
if (uploadResponse.uid) {
|
|
1691
|
+
response.uid = uploadResponse.uid;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (uploadResponse.uidValidity) {
|
|
1695
|
+
response.uidValidity = uploadResponse.uidValidity.toString();
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (uploadResponse.seq) {
|
|
1699
|
+
response.seq = uploadResponse.seq;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (response.uid) {
|
|
1703
|
+
response.id = await this.packUid(response.path, response.uid);
|
|
1704
|
+
}
|
|
1705
|
+
} finally {
|
|
1706
|
+
lock.release();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
return response;
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
err.code = 'UploadFail';
|
|
1712
|
+
err.statusCode = 502;
|
|
1713
|
+
throw err;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
getLogger() {
|
|
1718
|
+
this.mainLogger =
|
|
1719
|
+
this.options.logger ||
|
|
1720
|
+
logger.child({
|
|
1721
|
+
component: 'imap-client',
|
|
1722
|
+
account: this.account,
|
|
1723
|
+
cid: this.cid
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
let synteticLogger = {};
|
|
1727
|
+
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
1728
|
+
for (let level of levels) {
|
|
1729
|
+
synteticLogger[level] = (...args) => {
|
|
1730
|
+
this.mainLogger[level](...args);
|
|
1731
|
+
|
|
1732
|
+
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
|
1733
|
+
let entry = Object.assign({ level, t: Date.now(), cid: this.cid }, args[0]);
|
|
1734
|
+
if (entry.err && typeof entry.err === 'object') {
|
|
1735
|
+
let err = entry.err;
|
|
1736
|
+
entry.err = {
|
|
1737
|
+
stack: err.stack
|
|
1738
|
+
};
|
|
1739
|
+
// enumerable error fields
|
|
1740
|
+
Object.keys(err).forEach(key => {
|
|
1741
|
+
entry.err[key] = err[key];
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
this.accountLogger.log(entry);
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
return synteticLogger;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
module.exports.Connection = Connection;
|