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/mailbox.js
ADDED
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { serialize, unserialize, compareExisting, normalizePath } = require('./tools');
|
|
5
|
+
const msgpack = require('msgpack5')();
|
|
6
|
+
const he = require('he');
|
|
7
|
+
const libmime = require('libmime');
|
|
8
|
+
const punycode = require('punycode/');
|
|
9
|
+
const EmailReplyParser = require('email-reply-parser');
|
|
10
|
+
const linkify = require('linkifyjs/html');
|
|
11
|
+
const { htmlToText } = require('html-to-text');
|
|
12
|
+
const addressparser = require('nodemailer/lib/addressparser');
|
|
13
|
+
const settings = require('./settings');
|
|
14
|
+
const { bounceDetect } = require('./bounce-detect');
|
|
15
|
+
const appendList = require('./append-list');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
MESSAGE_NEW_NOTIFY,
|
|
19
|
+
MAILBOX_DELETED_NOTIFY,
|
|
20
|
+
MESSAGE_DELETED_NOTIFY,
|
|
21
|
+
MESSAGE_UPDATED_NOTIFY,
|
|
22
|
+
MAILBOX_RESET_NOTIFY,
|
|
23
|
+
MAILBOX_NEW_NOTIFY,
|
|
24
|
+
EMAIL_BOUNCE_NOTIFY
|
|
25
|
+
} = require('./consts');
|
|
26
|
+
|
|
27
|
+
// Do not check for flag updates using full sync more often than this value
|
|
28
|
+
const FULL_SYNC_DELAY = 30 * 60 * 1000;
|
|
29
|
+
const MAX_HTML_PARSE_LENGTH = 2 * 1024 * 1024; // do not parse HTML messages larger than 2MB to plaintext
|
|
30
|
+
|
|
31
|
+
class Mailbox {
|
|
32
|
+
constructor(connection, entry) {
|
|
33
|
+
this.status = false;
|
|
34
|
+
this.connection = connection;
|
|
35
|
+
this.path = entry.path;
|
|
36
|
+
this.listingEntry = entry;
|
|
37
|
+
this.syncDisabled = entry.syncDisabled;
|
|
38
|
+
|
|
39
|
+
this.imapClient = this.connection.imapClient;
|
|
40
|
+
|
|
41
|
+
this.logger = this.connection.mainLogger.child({
|
|
42
|
+
sub: 'mailbox',
|
|
43
|
+
path: this.path
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.isGmail = connection.isGmail;
|
|
47
|
+
this.isAllMail = this.isGmail && this.listingEntry.specialUse === '\\All';
|
|
48
|
+
|
|
49
|
+
this.selected = false;
|
|
50
|
+
|
|
51
|
+
this.redisKey = BigInt('0x' + crypto.createHash('sha1').update(normalizePath(this.path)).digest('hex')).toString(36);
|
|
52
|
+
|
|
53
|
+
this.runPartialSyncTimer = false;
|
|
54
|
+
|
|
55
|
+
this.synced = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getMailboxStatus() {
|
|
59
|
+
let mailboxInfo = this.imapClient.mailbox;
|
|
60
|
+
|
|
61
|
+
let status = {
|
|
62
|
+
path: this.path
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
status.highestModseq = mailboxInfo.highestModseq ? mailboxInfo.highestModseq : false;
|
|
66
|
+
status.uidValidity = mailboxInfo.uidValidity ? mailboxInfo.uidValidity : false;
|
|
67
|
+
status.uidNext = mailboxInfo.uidNext ? mailboxInfo.uidNext : false;
|
|
68
|
+
status.messages = mailboxInfo.exists ? mailboxInfo.exists : 0;
|
|
69
|
+
|
|
70
|
+
return status;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Loads last known mailbox state from Redis
|
|
75
|
+
* @returns {Object} mailbox state
|
|
76
|
+
*/
|
|
77
|
+
async getStoredStatus() {
|
|
78
|
+
let data = await this.connection.redis.hgetall(this.getMailboxKey());
|
|
79
|
+
data = data || {};
|
|
80
|
+
return {
|
|
81
|
+
path: data.path || this.path,
|
|
82
|
+
uidValidity: data.uidValidity && !isNaN(data.uidValidity) ? BigInt(data.uidValidity) : false,
|
|
83
|
+
highestModseq: data.highestModseq && !isNaN(data.highestModseq) ? BigInt(data.highestModseq) : false,
|
|
84
|
+
messages: data.messages && !isNaN(data.messages) ? Number(data.messages) : false,
|
|
85
|
+
uidNext: data.uidNext && !isNaN(data.uidNext) ? Number(data.uidNext) : false,
|
|
86
|
+
lastFullSync: data.lastFullSync ? new Date(data.lastFullSync) : false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Updates known mailbox state in Redis
|
|
92
|
+
* @param {Object} data
|
|
93
|
+
*/
|
|
94
|
+
async updateStoredStatus(data) {
|
|
95
|
+
if (!data || typeof data !== 'object') {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let list = Object.keys(data)
|
|
100
|
+
.map(key => {
|
|
101
|
+
switch (key) {
|
|
102
|
+
case 'path':
|
|
103
|
+
case 'uidValidity':
|
|
104
|
+
case 'highestModseq':
|
|
105
|
+
case 'messages':
|
|
106
|
+
case 'uidNext':
|
|
107
|
+
return [key, data[key].toString()];
|
|
108
|
+
|
|
109
|
+
case 'lastFullSync':
|
|
110
|
+
return [key, data[key].toISOString()];
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.filter(entry => entry);
|
|
114
|
+
|
|
115
|
+
if (!list.length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await this.connection.redis.hmset(this.getMailboxKey(), Object.fromEntries(list));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sets message entry object. Entries are ordered by `uid` property
|
|
124
|
+
* @param {Object} data
|
|
125
|
+
* @param {Number} Sequence number for the added entry
|
|
126
|
+
*/
|
|
127
|
+
async entryListSet(data) {
|
|
128
|
+
if (isNaN(data.uid)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return await this.connection.redis.zSet(this.getMessagesKey(), Number(data.uid), serialize(data));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Retrieves message entry object for the provided sequence value
|
|
137
|
+
* @param {Number} seq Sequence number
|
|
138
|
+
* @returns {Object|null} Message entry object
|
|
139
|
+
*/
|
|
140
|
+
async entryListGet(seq, options) {
|
|
141
|
+
let range = Number(seq);
|
|
142
|
+
options = options || {};
|
|
143
|
+
let command = options.uid ? 'zGetByUidBuffer' : 'zGetBuffer';
|
|
144
|
+
let response = await this.connection.redis[command](this.getMessagesKey(), range);
|
|
145
|
+
if (response) {
|
|
146
|
+
try {
|
|
147
|
+
return {
|
|
148
|
+
uid: Number(response[0]),
|
|
149
|
+
entry: unserialize(response[1]),
|
|
150
|
+
seq: Number(response[2])
|
|
151
|
+
};
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Deletes entry from message list for the provided sequence value
|
|
161
|
+
* @param {Number} seq Sequence number
|
|
162
|
+
* @returns {Object|null} Message entry object that was deleted
|
|
163
|
+
*/
|
|
164
|
+
async entryListExpunge(seq) {
|
|
165
|
+
let response = await this.connection.redis.zExpungeBuffer(this.getMessagesKey(), this.getMailboxKey(), seq);
|
|
166
|
+
if (response) {
|
|
167
|
+
try {
|
|
168
|
+
return unserialize(response[1]);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
isSelected() {
|
|
177
|
+
return this.selected && this.imapClient.mailbox && normalizePath(this.imapClient.mailbox.path) === normalizePath(this.path);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getMessagesKey() {
|
|
181
|
+
return `iam:${this.connection.account}:l:${this.redisKey}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getMailboxKey() {
|
|
185
|
+
return `iam:${this.connection.account}:h:${this.redisKey}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getBounceKey() {
|
|
189
|
+
return `iar:b:${this.connection.account}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
startIdle() {
|
|
193
|
+
if (!this.isSelected() || this.imapClient.idling) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.imapClient.idle().catch(err => {
|
|
197
|
+
this.logger.error({ msg: 'IDLE error', err });
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// clear mailbox records
|
|
202
|
+
async clear(opts) {
|
|
203
|
+
opts = opts || {};
|
|
204
|
+
|
|
205
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
206
|
+
|
|
207
|
+
await this.connection.redis.del(this.getMailboxKey());
|
|
208
|
+
await this.connection.redis.del(this.getMessagesKey());
|
|
209
|
+
|
|
210
|
+
this.logger.debug({ msg: 'Deleted mailbox', path: this.listingEntry.path });
|
|
211
|
+
|
|
212
|
+
if (!opts.skipNotify) {
|
|
213
|
+
this.connection.notify(this, MAILBOX_DELETED_NOTIFY, {
|
|
214
|
+
path: this.listingEntry.path,
|
|
215
|
+
name: this.listingEntry.name,
|
|
216
|
+
specialUse: this.listingEntry.specialUse || false
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async sync() {
|
|
222
|
+
if (this.selected) {
|
|
223
|
+
// expect current folder to be already synced
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let status = await this.imapClient.status(this.path, {
|
|
228
|
+
uidNext: true,
|
|
229
|
+
messages: true,
|
|
230
|
+
highestModseq: true,
|
|
231
|
+
uidValidity: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!status) {
|
|
235
|
+
// nothing to do here
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
status.highestModseq = status.highestModseq || false;
|
|
240
|
+
|
|
241
|
+
if (this.syncDisabled) {
|
|
242
|
+
// only update counters
|
|
243
|
+
await this.updateStoredStatus(status);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let storedStatus = await this.getStoredStatus();
|
|
248
|
+
if (status.uidValidity === storedStatus.uidValidity) {
|
|
249
|
+
if (
|
|
250
|
+
status.uidNext === storedStatus.uidNext &&
|
|
251
|
+
status.messages === storedStatus.messages &&
|
|
252
|
+
storedStatus.lastFullSync > new Date(Date.now() - FULL_SYNC_DELAY)
|
|
253
|
+
) {
|
|
254
|
+
// no reason to sync
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if ((!status.messages && !storedStatus.messages) || (status.highestModseq && status.highestModseq === storedStatus.highestModseq)) {
|
|
259
|
+
// no reason to sync
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let syncedPromise = new Promise((resolve, reject) => {
|
|
265
|
+
this.synced = resolve;
|
|
266
|
+
this.select(true).catch(err => reject(err));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await syncedPromise;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async select(skipIdle) {
|
|
273
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
274
|
+
// have to release the lock immediatelly, otherwise difficult to process 'exists' / 'expunge' events
|
|
275
|
+
lock.release();
|
|
276
|
+
|
|
277
|
+
if (!skipIdle) {
|
|
278
|
+
// do not wait until command finishes before proceeding
|
|
279
|
+
this.startIdle();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
logEvent(msg, event) {
|
|
284
|
+
const logObj = Object.assign({ msg }, event);
|
|
285
|
+
Object.keys(logObj).forEach(key => {
|
|
286
|
+
if (typeof logObj[key] === 'bigint') {
|
|
287
|
+
logObj[key] = logObj[key].toString();
|
|
288
|
+
}
|
|
289
|
+
if (typeof logObj[key].has === 'function') {
|
|
290
|
+
logObj[key] = Array.from(logObj[key]);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
this.logger.trace(logObj);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async onExists(event) {
|
|
297
|
+
this.logEvent('Untagged EXISTS', event);
|
|
298
|
+
|
|
299
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
300
|
+
this.runPartialSyncTimer = setTimeout(() => {
|
|
301
|
+
this.shouldRunPartialSyncAfterExists()
|
|
302
|
+
.then(shouldRun => {
|
|
303
|
+
if (shouldRun) {
|
|
304
|
+
return this.partialSync();
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
})
|
|
308
|
+
.then(() => this.select())
|
|
309
|
+
.catch(err => this.logger.error({ msg: 'Sync error', err }));
|
|
310
|
+
}, 1000);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async onExpunge(event) {
|
|
314
|
+
this.logEvent('Untagged EXPUNGE', event);
|
|
315
|
+
|
|
316
|
+
let deletedEntry = await this.entryListExpunge(event.seq);
|
|
317
|
+
if (deletedEntry) {
|
|
318
|
+
await this.processDeleted(deletedEntry);
|
|
319
|
+
await this.markUpdated();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async onFlags(event) {
|
|
324
|
+
this.logEvent('Untagged FETCH', event);
|
|
325
|
+
|
|
326
|
+
let storedMessage = await this.entryListGet(event.seq, { uid: false });
|
|
327
|
+
let changes;
|
|
328
|
+
|
|
329
|
+
// ignore Recent flag
|
|
330
|
+
event.flags.delete('\\Recent');
|
|
331
|
+
|
|
332
|
+
if (!storedMessage) {
|
|
333
|
+
// New! There should not be new messages.
|
|
334
|
+
// What should we do? Currently triggering partial sync.
|
|
335
|
+
return await this.onExists();
|
|
336
|
+
} else if ((changes = compareExisting(storedMessage.entry, event, ['flags']))) {
|
|
337
|
+
let messageData = storedMessage.entry;
|
|
338
|
+
messageData.flags = event.flags;
|
|
339
|
+
let seq = await this.entryListSet(messageData);
|
|
340
|
+
if (seq) {
|
|
341
|
+
await this.processChanges(storedMessage, changes);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async shouldRunPartialSyncAfterExists() {
|
|
347
|
+
let storedStatus = await this.getStoredStatus();
|
|
348
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
349
|
+
return mailboxStatus.messages !== storedStatus.messages;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async partialSync(storedStatus) {
|
|
353
|
+
storedStatus = storedStatus || (await this.getStoredStatus());
|
|
354
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
355
|
+
|
|
356
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
357
|
+
try {
|
|
358
|
+
let newMessages = [];
|
|
359
|
+
|
|
360
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
361
|
+
let range = '1:*';
|
|
362
|
+
let opts = {
|
|
363
|
+
uid: true
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (this.imapClient.enabled.has('CONDSTORE') && storedStatus.highestModseq) {
|
|
367
|
+
opts.changedSince = storedStatus.highestModseq;
|
|
368
|
+
} else if (storedStatus.uidNext) {
|
|
369
|
+
range = `${storedStatus.uidNext}:*`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (mailboxStatus.messages) {
|
|
373
|
+
// only fetch messages if there is some
|
|
374
|
+
for await (let messageData of this.imapClient.fetch(range, fields, opts)) {
|
|
375
|
+
// ignore Recent flag
|
|
376
|
+
messageData.flags.delete('\\Recent');
|
|
377
|
+
|
|
378
|
+
let storedMessage = await this.entryListGet(messageData.uid, { uid: true });
|
|
379
|
+
|
|
380
|
+
let changes;
|
|
381
|
+
if (!storedMessage) {
|
|
382
|
+
// new!
|
|
383
|
+
let seq = await this.entryListSet(messageData);
|
|
384
|
+
if (seq) {
|
|
385
|
+
newMessages.push(messageData);
|
|
386
|
+
}
|
|
387
|
+
} else if ((changes = compareExisting(storedMessage.entry, messageData))) {
|
|
388
|
+
let seq = await this.entryListSet(messageData);
|
|
389
|
+
if (seq) {
|
|
390
|
+
await this.processChanges(messageData, changes);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await this.updateStoredStatus(this.getMailboxStatus());
|
|
397
|
+
|
|
398
|
+
let messageFetchOptions = {};
|
|
399
|
+
let notifyText = await settings.get('notifyText');
|
|
400
|
+
if (notifyText) {
|
|
401
|
+
messageFetchOptions.textType = '*';
|
|
402
|
+
let notifyTextSize = await settings.get('notifyTextSize');
|
|
403
|
+
if (notifyTextSize) {
|
|
404
|
+
messageFetchOptions.maxBytes = notifyTextSize;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let notifyHeaders = await settings.get('notifyHeaders');
|
|
409
|
+
if (notifyHeaders) {
|
|
410
|
+
messageFetchOptions.headers = notifyHeaders.includes('*') ? true : notifyHeaders.length ? notifyHeaders : false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// have to call after fetch is finished
|
|
414
|
+
for (let messageData of newMessages) {
|
|
415
|
+
if (this.connection.notifyFrom && messageData.internalDate < this.connection.notifyFrom) {
|
|
416
|
+
// skip too old messages
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
await this.processNew(messageData, messageFetchOptions);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (newMessages.length) {
|
|
423
|
+
await this.markUpdated();
|
|
424
|
+
}
|
|
425
|
+
} finally {
|
|
426
|
+
lock.release();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async processDeleted(messageData) {
|
|
431
|
+
this.logger.debug({ msg: 'Deleted', uid: messageData.uid });
|
|
432
|
+
|
|
433
|
+
//FIXME: does not work as there is no messageId property
|
|
434
|
+
/*
|
|
435
|
+
if (messageData.messageId) {
|
|
436
|
+
try {
|
|
437
|
+
let deleted = await appendList.clear(this.connection.redis, this.getBounceKey(), messageData.messageId);
|
|
438
|
+
if (deleted) {
|
|
439
|
+
this.logger.error({
|
|
440
|
+
msg: 'Cleared bounce log for message',
|
|
441
|
+
id: messageData.id,
|
|
442
|
+
uid: messageData.uid,
|
|
443
|
+
messageId: messageData.messageId
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
} catch (err) {
|
|
447
|
+
this.logger.error({
|
|
448
|
+
msg: 'Failed to clear bounce log',
|
|
449
|
+
id: messageData.id,
|
|
450
|
+
uid: messageData.uid,
|
|
451
|
+
messageId: messageData.messageId,
|
|
452
|
+
err
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
*/
|
|
457
|
+
|
|
458
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
459
|
+
await this.connection.notify(this, MESSAGE_DELETED_NOTIFY, {
|
|
460
|
+
id: packedUid,
|
|
461
|
+
uid: messageData.uid
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async processNew(messageData, options) {
|
|
466
|
+
this.logger.debug({ msg: 'New message', uid: messageData.uid, flags: Array.from(messageData.flags) });
|
|
467
|
+
|
|
468
|
+
options.skipLock = true;
|
|
469
|
+
options.headers = 'headers' in options ? options.headers : false;
|
|
470
|
+
options.skipVisible = true;
|
|
471
|
+
|
|
472
|
+
let messageInfo = await this.getMessage(messageData, options);
|
|
473
|
+
if (!messageInfo) {
|
|
474
|
+
this.logger.debug({ msg: 'Not found', uid: messageData.uid });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let date = new Date(messageInfo.date);
|
|
479
|
+
if (this.connection.notifyFrom && date < this.connection.notifyFrom) {
|
|
480
|
+
// skip too old messages
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let bounceNotifyInfo;
|
|
485
|
+
|
|
486
|
+
// Check if this could be a bounce
|
|
487
|
+
if (this.mightBeABounce(messageInfo)) {
|
|
488
|
+
// parse for bounce
|
|
489
|
+
try {
|
|
490
|
+
let { content } = await this.imapClient.download(messageInfo.uid, false, {
|
|
491
|
+
uid: true,
|
|
492
|
+
// future feature
|
|
493
|
+
chunkSize: options.chunkSize
|
|
494
|
+
});
|
|
495
|
+
if (content) {
|
|
496
|
+
let bounce = await bounceDetect(content);
|
|
497
|
+
|
|
498
|
+
let stored = 0;
|
|
499
|
+
if (bounce.action && bounce.recipient && bounce.messageId) {
|
|
500
|
+
let storedBounce = {
|
|
501
|
+
i: messageInfo.id,
|
|
502
|
+
r: bounce.recipient,
|
|
503
|
+
t: Date.now(),
|
|
504
|
+
a: bounce.action
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
if (bounce.response && bounce.response.message) {
|
|
508
|
+
storedBounce.m = bounce.response.message;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (bounce.response && bounce.response.status) {
|
|
512
|
+
storedBounce.s = bounce.response.status;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Store bounce info
|
|
516
|
+
stored = await appendList.append(this.connection.redis, this.getBounceKey(), bounce.messageId, storedBounce);
|
|
517
|
+
|
|
518
|
+
bounceNotifyInfo = Object.assign({ bounceMessage: messageInfo.id }, bounce);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.logger.debug({
|
|
522
|
+
msg: 'Detected bounce message',
|
|
523
|
+
id: messageInfo.id,
|
|
524
|
+
uid: messageInfo.uid,
|
|
525
|
+
messageId: messageInfo.messageId,
|
|
526
|
+
bounce,
|
|
527
|
+
stored
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
this.logger.error({
|
|
532
|
+
msg: 'Failed to process potential bounce',
|
|
533
|
+
id: messageInfo.id,
|
|
534
|
+
uid: messageInfo.uid,
|
|
535
|
+
messageId: messageInfo.messageId,
|
|
536
|
+
err
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
await this.connection.notify(this, MESSAGE_NEW_NOTIFY, messageInfo);
|
|
542
|
+
if (bounceNotifyInfo) {
|
|
543
|
+
// send bounce notification _after_ bounce email notification
|
|
544
|
+
await this.connection.notify(false, EMAIL_BOUNCE_NOTIFY, bounceNotifyInfo);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async getMessageInfo(messageData, extended) {
|
|
549
|
+
if (!messageData) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
554
|
+
let { attachments, textId, encodedTextSize } = this.getAttachmentList(packedUid, messageData.bodyStructure);
|
|
555
|
+
|
|
556
|
+
let envelope = messageData.envelope || {};
|
|
557
|
+
let date = envelope.date || messageData.internalDate;
|
|
558
|
+
|
|
559
|
+
let result = {
|
|
560
|
+
id: packedUid,
|
|
561
|
+
uid: messageData.uid,
|
|
562
|
+
|
|
563
|
+
emailId: messageData.emailId || undefined,
|
|
564
|
+
threadId: messageData.threadId || undefined,
|
|
565
|
+
|
|
566
|
+
date: (date && date.toISOString()) || undefined,
|
|
567
|
+
flags: (extended && messageData.flags && messageData.flags.size && Array.from(messageData.flags)) || undefined,
|
|
568
|
+
|
|
569
|
+
unseen: messageData.flags && !messageData.flags.has('\\Seen') ? true : undefined,
|
|
570
|
+
flagged: messageData.flags && messageData.flags.has('\\Flagged') ? true : undefined,
|
|
571
|
+
answered: messageData.flags && messageData.flags.has('\\Answered') ? true : undefined,
|
|
572
|
+
draft: messageData.flags && messageData.flags.has('\\Draft') ? true : undefined,
|
|
573
|
+
|
|
574
|
+
size: messageData.size || undefined,
|
|
575
|
+
subject: envelope.subject || undefined,
|
|
576
|
+
from: envelope.from && envelope.from[0] ? envelope.from[0] : undefined,
|
|
577
|
+
|
|
578
|
+
replyTo: extended && envelope.replyTo && envelope.replyTo[0] ? envelope.replyTo[0] : undefined,
|
|
579
|
+
sender: extended && envelope.sender && envelope.sender[0] ? envelope.sender[0] : undefined,
|
|
580
|
+
|
|
581
|
+
to: envelope.to && envelope.to.length ? envelope.to : undefined,
|
|
582
|
+
cc: envelope.cc && envelope.cc.length ? envelope.cc : undefined,
|
|
583
|
+
|
|
584
|
+
bcc: extended && envelope.bcc && envelope.bcc.length ? envelope.bcc : undefined,
|
|
585
|
+
|
|
586
|
+
attachments: attachments && attachments.length ? attachments : undefined,
|
|
587
|
+
messageId: (envelope.messageId && envelope.messageId.toString().trim()) || undefined,
|
|
588
|
+
inReplyTo: envelope.inReplyTo || undefined,
|
|
589
|
+
|
|
590
|
+
labels: messageData.labels && messageData.labels.size ? Array.from(messageData.labels) : undefined,
|
|
591
|
+
|
|
592
|
+
headers: (extended && messageData.headers && libmime.decodeHeaders(messageData.headers.toString().trim())) || undefined,
|
|
593
|
+
text: textId
|
|
594
|
+
? {
|
|
595
|
+
id: textId,
|
|
596
|
+
encodedSize: encodedTextSize
|
|
597
|
+
}
|
|
598
|
+
: undefined
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
Object.keys(result).forEach(key => {
|
|
602
|
+
if (typeof result[key] === 'undefined') {
|
|
603
|
+
delete result[key];
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// is there a related bounce as well?
|
|
608
|
+
try {
|
|
609
|
+
if (result.messageId) {
|
|
610
|
+
let bounces = await appendList.list(this.connection.redis, this.getBounceKey(), result.messageId);
|
|
611
|
+
if (bounces && bounces.length) {
|
|
612
|
+
result.bounces = bounces.map(row => {
|
|
613
|
+
let bounce = {
|
|
614
|
+
message: row.i,
|
|
615
|
+
recipient: row.r,
|
|
616
|
+
action: row.a
|
|
617
|
+
};
|
|
618
|
+
if (row.m || row.s) {
|
|
619
|
+
bounce.response = {};
|
|
620
|
+
}
|
|
621
|
+
if (row.m) {
|
|
622
|
+
bounce.response.message = row.m;
|
|
623
|
+
}
|
|
624
|
+
if (row.s) {
|
|
625
|
+
bounce.response.status = row.s;
|
|
626
|
+
}
|
|
627
|
+
bounce.date = new Date(row.t).toISOString();
|
|
628
|
+
return bounce;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch (E) {
|
|
633
|
+
this.logger.error({
|
|
634
|
+
msg: 'Failed to fetch bounces',
|
|
635
|
+
id: messageData.id,
|
|
636
|
+
uid: messageData.uid,
|
|
637
|
+
messageId: messageData.messageId,
|
|
638
|
+
err: E
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
getAttachmentList(packedUid, bodyStructure) {
|
|
646
|
+
let attachments = [];
|
|
647
|
+
let textParts = [[], [], []];
|
|
648
|
+
if (!bodyStructure) {
|
|
649
|
+
return attachments;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let idBuf = Buffer.from(packedUid.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
653
|
+
|
|
654
|
+
let encodedTextSize = {};
|
|
655
|
+
|
|
656
|
+
let walk = (node, isRelated) => {
|
|
657
|
+
if (node.type === 'multipart/related') {
|
|
658
|
+
isRelated = true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (!/^multipart\//.test(node.type)) {
|
|
662
|
+
if (node.disposition === 'attachment' || !/^text\//.test(node.type)) {
|
|
663
|
+
attachments.push({
|
|
664
|
+
// append body part nr to message id
|
|
665
|
+
id: Buffer.concat([idBuf, Buffer.from(node.part || '1')])
|
|
666
|
+
.toString('base64')
|
|
667
|
+
.replace(/\+/g, '-')
|
|
668
|
+
.replace(/\//g, '_')
|
|
669
|
+
.replace(/[=]+/g, ''),
|
|
670
|
+
contentType: node.type,
|
|
671
|
+
encodedSize: node.size,
|
|
672
|
+
filename: (node.dispositionParameters && node.dispositionParameters.filename) || (node.parameters && node.parameters.name) || false,
|
|
673
|
+
embedded: isRelated,
|
|
674
|
+
inline: node.disposition === 'inline' || (!node.disposition && isRelated),
|
|
675
|
+
contentId: node.id
|
|
676
|
+
});
|
|
677
|
+
} else if ((!node.disposition || node.disposition === 'inline') && /^text\//.test(node.type)) {
|
|
678
|
+
let type = node.type.substr(5);
|
|
679
|
+
if (!encodedTextSize[type]) {
|
|
680
|
+
encodedTextSize[type] = 0;
|
|
681
|
+
}
|
|
682
|
+
encodedTextSize[type] += node.size;
|
|
683
|
+
switch (type) {
|
|
684
|
+
case 'plain':
|
|
685
|
+
textParts[0].push(node.part || '1');
|
|
686
|
+
break;
|
|
687
|
+
case 'html':
|
|
688
|
+
textParts[1].push(node.part || '1');
|
|
689
|
+
break;
|
|
690
|
+
default:
|
|
691
|
+
textParts[2].push(node.part || '1');
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (node.childNodes) {
|
|
698
|
+
node.childNodes.forEach(childNode => walk(childNode, isRelated));
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
walk(bodyStructure, false);
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
attachments,
|
|
706
|
+
textId: Buffer.concat([idBuf, msgpack.encode(textParts)])
|
|
707
|
+
.toString('base64')
|
|
708
|
+
.replace(/\+/g, '-')
|
|
709
|
+
.replace(/\//g, '_')
|
|
710
|
+
.replace(/[=]+/g, ''),
|
|
711
|
+
encodedTextSize
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async processChanges(messageData, changes) {
|
|
716
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
717
|
+
await this.connection.notify(this, MESSAGE_UPDATED_NOTIFY, {
|
|
718
|
+
id: packedUid,
|
|
719
|
+
uid: messageData.uid,
|
|
720
|
+
changes
|
|
721
|
+
});
|
|
722
|
+
await this.markUpdated();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async fullSync() {
|
|
726
|
+
let range = '1:*';
|
|
727
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
728
|
+
let opts = {};
|
|
729
|
+
|
|
730
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
731
|
+
try {
|
|
732
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
733
|
+
let newMessages = [];
|
|
734
|
+
|
|
735
|
+
// full sync
|
|
736
|
+
let seqMax = 0;
|
|
737
|
+
let changes;
|
|
738
|
+
|
|
739
|
+
if (mailboxStatus.messages) {
|
|
740
|
+
// only fetch messages if there is some
|
|
741
|
+
|
|
742
|
+
for await (let messageData of this.imapClient.fetch(range, fields, opts)) {
|
|
743
|
+
// ignore Recent flag
|
|
744
|
+
messageData.flags.delete('\\Recent');
|
|
745
|
+
|
|
746
|
+
if (messageData.seq > seqMax) {
|
|
747
|
+
seqMax = messageData.seq;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let storedMessage = await this.entryListGet(messageData.uid, { uid: true });
|
|
751
|
+
if (!storedMessage) {
|
|
752
|
+
// new!
|
|
753
|
+
let seq = await this.entryListSet(messageData);
|
|
754
|
+
if (seq) {
|
|
755
|
+
newMessages.push(messageData);
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
let diff = storedMessage.seq - messageData.seq;
|
|
759
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
760
|
+
let seq = messageData.seq + i;
|
|
761
|
+
let deletedEntry = await this.entryListExpunge(seq);
|
|
762
|
+
if (deletedEntry) {
|
|
763
|
+
await this.processDeleted(deletedEntry);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if ((changes = compareExisting(storedMessage.entry, messageData))) {
|
|
768
|
+
let seq = await this.entryListSet(messageData);
|
|
769
|
+
if (seq) {
|
|
770
|
+
await this.processChanges(messageData, changes);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// delete unlisted messages
|
|
778
|
+
let storedMaxSeq = await this.connection.redis.zcard(this.getMessagesKey());
|
|
779
|
+
let diff = storedMaxSeq - seqMax;
|
|
780
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
781
|
+
let seq = seqMax + i + 1;
|
|
782
|
+
let deletedEntry = await this.entryListExpunge(seq);
|
|
783
|
+
if (deletedEntry) {
|
|
784
|
+
await this.processDeleted(deletedEntry);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
let status = this.getMailboxStatus();
|
|
789
|
+
status.lastFullSync = new Date();
|
|
790
|
+
await this.updateStoredStatus(status);
|
|
791
|
+
|
|
792
|
+
let messageFetchOptions = {};
|
|
793
|
+
|
|
794
|
+
let notifyText = await settings.get('notifyText');
|
|
795
|
+
if (notifyText) {
|
|
796
|
+
messageFetchOptions.textType = '*';
|
|
797
|
+
let notifyTextSize = await settings.get('notifyTextSize');
|
|
798
|
+
if (notifyTextSize) {
|
|
799
|
+
messageFetchOptions.maxBytes = notifyTextSize;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let notifyHeaders = await settings.get('notifyHeaders');
|
|
804
|
+
if (notifyHeaders) {
|
|
805
|
+
messageFetchOptions.headers = notifyHeaders.includes('*') ? true : notifyHeaders.length ? notifyHeaders : false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// have to call after fetch is finished
|
|
809
|
+
for (let messageData of newMessages) {
|
|
810
|
+
if (this.connection.notifyFrom && messageData.internalDate < this.connection.notifyFrom) {
|
|
811
|
+
// skip too old messages
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
await this.processNew(messageData, messageFetchOptions);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (newMessages.length) {
|
|
819
|
+
await this.markUpdated();
|
|
820
|
+
}
|
|
821
|
+
} finally {
|
|
822
|
+
lock.release();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async onOpen() {
|
|
827
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
828
|
+
this.selected = true;
|
|
829
|
+
|
|
830
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
let storedStatus = await this.getStoredStatus();
|
|
834
|
+
|
|
835
|
+
if (storedStatus.uidValidity && storedStatus.uidValidity !== mailboxStatus.uidValidity) {
|
|
836
|
+
// UIDVALIDITY has changed, full sync is required!
|
|
837
|
+
// delete mailbox status
|
|
838
|
+
let result = await this.connection.redis.multi().zcard(this.getMessagesKey()).del(this.getMessagesKey()).del(this.getMailboxKey()).exec();
|
|
839
|
+
|
|
840
|
+
let deletedMessages = (result[0] && Number(result[0][1])) || 0;
|
|
841
|
+
this.logger.info({
|
|
842
|
+
msg: 'UIDVALIDITY change',
|
|
843
|
+
deleted: deletedMessages,
|
|
844
|
+
prevUidValidity: storedStatus.uidValidity && storedStatus.uidValidity.toString(),
|
|
845
|
+
uidValidity: mailboxStatus.uidValidity && mailboxStatus.uidValidity.toString()
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
this.logger.debug({ msg: 'Mailbox reset', path: this.listingEntry.path });
|
|
849
|
+
await this.connection.notify(this, MAILBOX_RESET_NOTIFY, {
|
|
850
|
+
path: this.listingEntry.path,
|
|
851
|
+
name: this.listingEntry.name,
|
|
852
|
+
specialUse: this.listingEntry.specialUse || false,
|
|
853
|
+
uidValidity: mailboxStatus.uidValidity && mailboxStatus.uidValidity.toString(),
|
|
854
|
+
prevUidValidity: storedStatus.uidValidity && storedStatus.uidValidity.toString()
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// do not advertise messages as new
|
|
858
|
+
this.listingEntry.isNew = true;
|
|
859
|
+
|
|
860
|
+
// generates blank stored status as the Redis key was deleted
|
|
861
|
+
storedStatus = await this.getStoredStatus();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (storedStatus.highestModseq && storedStatus.highestModseq === mailboxStatus.highestModseq) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (storedStatus.messages === 0 && mailboxStatus.messages === 0) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (
|
|
873
|
+
this.imapClient.enabled.has('CONDSTORE') &&
|
|
874
|
+
storedStatus.highestModseq < mailboxStatus.highestModseq &&
|
|
875
|
+
storedStatus.messages <= mailboxStatus.messages &&
|
|
876
|
+
mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
|
|
877
|
+
) {
|
|
878
|
+
// search for flag changes and new messages
|
|
879
|
+
return await this.partialSync(storedStatus);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (
|
|
883
|
+
storedStatus.messages < mailboxStatus.messages &&
|
|
884
|
+
mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
|
|
885
|
+
) {
|
|
886
|
+
// seem to have new messages only
|
|
887
|
+
return await this.partialSync(storedStatus);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (
|
|
891
|
+
storedStatus.messages === mailboxStatus.messages &&
|
|
892
|
+
storedStatus.uidNext === mailboxStatus.uidNext &&
|
|
893
|
+
storedStatus.lastFullSync &&
|
|
894
|
+
storedStatus.lastFullSync >= new Date(Date.now() - FULL_SYNC_DELAY)
|
|
895
|
+
) {
|
|
896
|
+
// too soon from last full sync, message count seems the same
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Perform full sync. Only way of getting flag changes from non-CONDSTORE servers
|
|
901
|
+
return await this.fullSync();
|
|
902
|
+
} finally {
|
|
903
|
+
if (this.listingEntry.isNew) {
|
|
904
|
+
// fully synced, so not new anymore
|
|
905
|
+
this.listingEntry.isNew = false;
|
|
906
|
+
this.logger.debug({ msg: 'New mailbox', path: this.listingEntry.path });
|
|
907
|
+
this.connection.notify(this, MAILBOX_NEW_NOTIFY, {
|
|
908
|
+
path: this.listingEntry.path,
|
|
909
|
+
name: this.listingEntry.name,
|
|
910
|
+
specialUse: this.listingEntry.specialUse || false,
|
|
911
|
+
uidValidity: mailboxStatus.uidValidity.toString()
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (this.synced) {
|
|
916
|
+
this.synced();
|
|
917
|
+
} else {
|
|
918
|
+
await this.select();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async onClose() {
|
|
924
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
925
|
+
this.selected = false;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async download(stream) {
|
|
929
|
+
return new Promise((resolve, reject) => {
|
|
930
|
+
let chunks = [];
|
|
931
|
+
let chunklen = 0;
|
|
932
|
+
stream.on('error', err => reject(err));
|
|
933
|
+
stream.on('readable', () => {
|
|
934
|
+
let chunk;
|
|
935
|
+
while ((chunk = stream.read()) !== null) {
|
|
936
|
+
if (typeof chunk === 'string') {
|
|
937
|
+
chunk = Buffer.from(chunk);
|
|
938
|
+
}
|
|
939
|
+
if (!chunk || !Buffer.isBuffer(chunk)) {
|
|
940
|
+
// what's that?
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
chunks.push(chunk);
|
|
944
|
+
chunklen += chunk.length;
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
stream.on('end', () => {
|
|
948
|
+
resolve(Buffer.concat(chunks, chunklen));
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// User methods
|
|
954
|
+
// Call `clearTimeout(this.connection.completedTimer);` after locking mailbox
|
|
955
|
+
// Call this.onTaskCompleted() after selected mailbox is processed and lock is released
|
|
956
|
+
|
|
957
|
+
async getText(message, textParts, options) {
|
|
958
|
+
options = options || {};
|
|
959
|
+
let result = {};
|
|
960
|
+
|
|
961
|
+
let maxBytes = options.maxBytes || Infinity;
|
|
962
|
+
let reqMaxBytes = options.maxBytes && !isNaN(options.maxBytes) ? Number(options.maxBytes) + 4 : maxBytes;
|
|
963
|
+
|
|
964
|
+
let hasMore = false;
|
|
965
|
+
|
|
966
|
+
let lock;
|
|
967
|
+
if (!options.skipLock) {
|
|
968
|
+
lock = await this.imapClient.getMailboxLock(this.path);
|
|
969
|
+
clearTimeout(this.connection.completedTimer);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
for (let part of textParts) {
|
|
974
|
+
let { meta, content } = await this.imapClient.download(message.uid, part, {
|
|
975
|
+
uid: true,
|
|
976
|
+
// make sure we request enough bytes so we would have complete utf-8 codepoints
|
|
977
|
+
maxBytes: reqMaxBytes,
|
|
978
|
+
// future feature
|
|
979
|
+
chunkSize: options.chunkSize
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (!content) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
let text = await this.download(content);
|
|
986
|
+
text = text.toString().replace(/\r?\n/g, '\n');
|
|
987
|
+
|
|
988
|
+
let typeKey = (meta.contentType && meta.contentType.split('/')[1]) || 'plain';
|
|
989
|
+
if (!result[typeKey]) {
|
|
990
|
+
result[typeKey] = [];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
let typeSize = result[typeKey].reduce((sum, entry) => sum + entry.length, 0);
|
|
994
|
+
if (typeSize >= maxBytes) {
|
|
995
|
+
hasMore = true;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (typeSize + text.length > maxBytes) {
|
|
999
|
+
text = text.substr(0, maxBytes - typeSize);
|
|
1000
|
+
hasMore = true;
|
|
1001
|
+
}
|
|
1002
|
+
result[typeKey].push(text);
|
|
1003
|
+
}
|
|
1004
|
+
} finally {
|
|
1005
|
+
if (lock) {
|
|
1006
|
+
lock.release();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
Object.keys(result).forEach(key => {
|
|
1011
|
+
result[key] = result[key].join('\n');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
result.hasMore = hasMore;
|
|
1015
|
+
|
|
1016
|
+
if (!options.skipLock) {
|
|
1017
|
+
this.connection.onTaskCompleted();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return result;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async getAttachment(message, part, options) {
|
|
1024
|
+
options = options || {};
|
|
1025
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
1026
|
+
clearTimeout(this.connection.completedTimer);
|
|
1027
|
+
|
|
1028
|
+
let streaming = false;
|
|
1029
|
+
let released = false;
|
|
1030
|
+
try {
|
|
1031
|
+
let { meta, content } = await this.imapClient.download(message.uid, part, {
|
|
1032
|
+
uid: true,
|
|
1033
|
+
maxBytes: options.maxBytes,
|
|
1034
|
+
// future feature
|
|
1035
|
+
chunkSize: options.chunkSize
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
if (!meta) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
content.headers = {
|
|
1043
|
+
'content-type': meta.contentType || 'application/octet-stream',
|
|
1044
|
+
'content-disposition': 'attachment' + (meta.filename ? `; filename=${he.encode(meta.filename)}` : '')
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
content.contentType = meta.contentType;
|
|
1048
|
+
content.filename = meta.filename;
|
|
1049
|
+
content.disposition = meta.disposition;
|
|
1050
|
+
streaming = true;
|
|
1051
|
+
|
|
1052
|
+
content.once('end', () => {
|
|
1053
|
+
if (!released) {
|
|
1054
|
+
released = true;
|
|
1055
|
+
lock.release();
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
content.once('error', () => {
|
|
1060
|
+
if (!released) {
|
|
1061
|
+
released = true;
|
|
1062
|
+
lock.release();
|
|
1063
|
+
this.connection.onTaskCompleted();
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
return content;
|
|
1068
|
+
} finally {
|
|
1069
|
+
if (!streaming) {
|
|
1070
|
+
lock.release();
|
|
1071
|
+
this.connection.onTaskCompleted();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async getMessage(message, options) {
|
|
1077
|
+
options = options || {};
|
|
1078
|
+
let messageInfo;
|
|
1079
|
+
|
|
1080
|
+
try {
|
|
1081
|
+
let lock;
|
|
1082
|
+
if (!options.skipLock) {
|
|
1083
|
+
lock = await this.imapClient.getMailboxLock(this.path);
|
|
1084
|
+
clearTimeout(this.connection.completedTimer);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
try {
|
|
1088
|
+
let fields = options.fields || {
|
|
1089
|
+
uid: true,
|
|
1090
|
+
flags: true,
|
|
1091
|
+
size: true,
|
|
1092
|
+
bodyStructure: true,
|
|
1093
|
+
envelope: true,
|
|
1094
|
+
internalDate: true,
|
|
1095
|
+
headers: 'headers' in options ? options.headers : true,
|
|
1096
|
+
emailId: true,
|
|
1097
|
+
threadId: true,
|
|
1098
|
+
labels: true
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
let messageData = await this.imapClient.fetchOne(message.uid, fields, { uid: true });
|
|
1102
|
+
if (!messageData) {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
messageInfo = await this.getMessageInfo(messageData, true);
|
|
1106
|
+
} finally {
|
|
1107
|
+
if (lock) {
|
|
1108
|
+
lock.release();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (!messageInfo) {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// merge decoded text content with message data (if requested)
|
|
1117
|
+
if (options.textType && messageInfo.text && messageInfo.text.id) {
|
|
1118
|
+
let { textParts } = await this.connection.getMessageTextPaths(messageInfo.text.id);
|
|
1119
|
+
if (textParts && textParts.length) {
|
|
1120
|
+
switch (options.textType) {
|
|
1121
|
+
case 'plain':
|
|
1122
|
+
textParts = textParts[0];
|
|
1123
|
+
break;
|
|
1124
|
+
case 'html':
|
|
1125
|
+
textParts = textParts[1];
|
|
1126
|
+
break;
|
|
1127
|
+
default:
|
|
1128
|
+
textParts = textParts.flatMap(entry => entry);
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (textParts && textParts.length) {
|
|
1133
|
+
let textContent = await this.getText(message, textParts, options);
|
|
1134
|
+
if (options.textType && options.textType !== '*') {
|
|
1135
|
+
textContent = {
|
|
1136
|
+
[options.textType]: textContent[options.textType] || '',
|
|
1137
|
+
hasMore: textContent.hasMore
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
messageInfo.text = Object.assign(messageInfo.text, textContent);
|
|
1141
|
+
|
|
1142
|
+
// Parse visible reply text as well if possible
|
|
1143
|
+
if (
|
|
1144
|
+
!options.skipVisible &&
|
|
1145
|
+
!['plain', 'html'].includes(options.textType) &&
|
|
1146
|
+
(messageInfo.inReplyTo || (messageInfo.headers && messageInfo.headers.references))
|
|
1147
|
+
) {
|
|
1148
|
+
// might be a reply and not a specific text type was asked
|
|
1149
|
+
try {
|
|
1150
|
+
let plaintext = messageInfo.text.plain || '';
|
|
1151
|
+
if (!plaintext && messageInfo.text.html && messageInfo.text.html.length < MAX_HTML_PARSE_LENGTH) {
|
|
1152
|
+
plaintext = htmlToText(messageInfo.text.html);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
plaintext = plaintext.trim();
|
|
1156
|
+
if (plaintext) {
|
|
1157
|
+
const email = new EmailReplyParser().read(plaintext);
|
|
1158
|
+
messageInfo.text.visible = linkify(textToHtml(email.getVisibleText()), {
|
|
1159
|
+
defaultProtocol: 'https'
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
this.logger.error({ msg: 'Reply parsing error', err });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return messageInfo;
|
|
1170
|
+
} finally {
|
|
1171
|
+
if (!options.skipLock) {
|
|
1172
|
+
this.connection.onTaskCompleted();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async updateMessage(message, updates) {
|
|
1178
|
+
updates = updates || {};
|
|
1179
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
1180
|
+
clearTimeout(this.connection.completedTimer);
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
let result = {};
|
|
1184
|
+
|
|
1185
|
+
if (updates.flags) {
|
|
1186
|
+
if (updates.flags.set) {
|
|
1187
|
+
// If set exists the ignore add/delete calls
|
|
1188
|
+
let value = await this.imapClient.messageFlagsSet(message.uid, updates.flags.set, { uid: true });
|
|
1189
|
+
result.flags = {
|
|
1190
|
+
set: value
|
|
1191
|
+
};
|
|
1192
|
+
} else {
|
|
1193
|
+
if (updates.flags.add && updates.flags.add.length) {
|
|
1194
|
+
let value = await this.imapClient.messageFlagsAdd(message.uid, updates.flags.add, { uid: true });
|
|
1195
|
+
if (!result.flags) {
|
|
1196
|
+
result.flags = {};
|
|
1197
|
+
}
|
|
1198
|
+
result.flags.add = value;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (updates.flags.delete && updates.flags.delete.length) {
|
|
1202
|
+
let value = await this.imapClient.messageFlagsRemove(message.uid, updates.flags.delete, { uid: true });
|
|
1203
|
+
if (!result.flags) {
|
|
1204
|
+
result.flags = {};
|
|
1205
|
+
}
|
|
1206
|
+
result.flags.delete = value;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (updates.labels && this.isGmail) {
|
|
1212
|
+
if (updates.labels.set) {
|
|
1213
|
+
// If set exists the ignore add/delete calls
|
|
1214
|
+
let value = await this.imapClient.messageFlagsSet(message.uid, updates.labels.set, { uid: true, useLabels: true });
|
|
1215
|
+
result.labels = {
|
|
1216
|
+
set: value
|
|
1217
|
+
};
|
|
1218
|
+
} else {
|
|
1219
|
+
if (updates.labels.add && updates.labels.add.length) {
|
|
1220
|
+
let value = await this.imapClient.messageFlagsAdd(message.uid, updates.labels.add, { uid: true, useLabels: true });
|
|
1221
|
+
if (!result.labels) {
|
|
1222
|
+
result.labels = {};
|
|
1223
|
+
}
|
|
1224
|
+
result.labels.add = value;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (updates.labels.delete && updates.labels.delete.length) {
|
|
1228
|
+
let value = await this.imapClient.messageFlagsRemove(message.uid, updates.labels.delete, { uid: true, useLabels: true });
|
|
1229
|
+
if (!result.labels) {
|
|
1230
|
+
result.labels = {};
|
|
1231
|
+
}
|
|
1232
|
+
result.labels.delete = value;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return result;
|
|
1238
|
+
} finally {
|
|
1239
|
+
lock.release();
|
|
1240
|
+
this.connection.onTaskCompleted();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async moveMessage(message, target) {
|
|
1245
|
+
target = target || {};
|
|
1246
|
+
|
|
1247
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
1248
|
+
clearTimeout(this.connection.completedTimer);
|
|
1249
|
+
|
|
1250
|
+
try {
|
|
1251
|
+
let result = {};
|
|
1252
|
+
|
|
1253
|
+
if (target.path) {
|
|
1254
|
+
// If set exists the ignore add/delete calls
|
|
1255
|
+
let value = await this.imapClient.messageMove(message.uid, target.path, { uid: true });
|
|
1256
|
+
result.path = target.path;
|
|
1257
|
+
if (value && value.uidMap && value.uidMap.has(message.uid)) {
|
|
1258
|
+
let uid = value.uidMap.get(message.uid);
|
|
1259
|
+
let packed = await this.connection.packUid(target.path, uid);
|
|
1260
|
+
result.id = packed;
|
|
1261
|
+
result.uid = uid;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return result;
|
|
1266
|
+
} finally {
|
|
1267
|
+
lock.release();
|
|
1268
|
+
this.connection.onTaskCompleted();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async deleteMessage(message) {
|
|
1273
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
1274
|
+
clearTimeout(this.connection.completedTimer);
|
|
1275
|
+
|
|
1276
|
+
try {
|
|
1277
|
+
let result = {};
|
|
1278
|
+
|
|
1279
|
+
if (this.listingEntry.specialUse === '\\Trash') {
|
|
1280
|
+
// delete
|
|
1281
|
+
result.deleted = await this.imapClient.messageDelete(message.uid, { uid: true });
|
|
1282
|
+
} else {
|
|
1283
|
+
// move to trash
|
|
1284
|
+
// find Trash folder path
|
|
1285
|
+
let trashMailbox = await this.connection.getSpecialUseMailbox('\\Trash');
|
|
1286
|
+
if (!trashMailbox || normalizePath(trashMailbox.path) === normalizePath(this.path)) {
|
|
1287
|
+
// no Trash found or already in trash
|
|
1288
|
+
result.deleted = await this.imapClient.messageDelete(message.uid, { uid: true });
|
|
1289
|
+
} else {
|
|
1290
|
+
// we have a destionation, so can move message to there
|
|
1291
|
+
let moved = await await this.imapClient.messageMove(message.uid, trashMailbox.path, { uid: true });
|
|
1292
|
+
if (moved) {
|
|
1293
|
+
result.moved = {
|
|
1294
|
+
destination: moved.destination
|
|
1295
|
+
};
|
|
1296
|
+
if (moved && moved.uidMap && moved.uidMap.has(message.uid)) {
|
|
1297
|
+
result.moved.message = await this.connection.packUid(trashMailbox.path, moved.uidMap.get(message.uid));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return result;
|
|
1303
|
+
} finally {
|
|
1304
|
+
lock.release();
|
|
1305
|
+
this.connection.onTaskCompleted();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async listMessages(options) {
|
|
1310
|
+
options = options || {};
|
|
1311
|
+
let page = Number(options.page) || 0;
|
|
1312
|
+
let pageSize = Number(options.pageSize) || 20;
|
|
1313
|
+
|
|
1314
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
1315
|
+
clearTimeout(this.connection.completedTimer);
|
|
1316
|
+
|
|
1317
|
+
try {
|
|
1318
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
1319
|
+
|
|
1320
|
+
let messageCount = mailboxStatus.messages;
|
|
1321
|
+
let uidList;
|
|
1322
|
+
let opts = {};
|
|
1323
|
+
|
|
1324
|
+
if (options.search) {
|
|
1325
|
+
uidList = await this.imapClient.search(options.search, { uid: true });
|
|
1326
|
+
uidList = !uidList ? [] : uidList.sort((a, b) => b - a); // newer first
|
|
1327
|
+
messageCount = uidList.length;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
let pages = Math.ceil(messageCount / pageSize) || 1;
|
|
1331
|
+
|
|
1332
|
+
if (page < 0) {
|
|
1333
|
+
page = 0;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (page >= pages) {
|
|
1337
|
+
page = pages - 1;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
let messages = [];
|
|
1341
|
+
let seqMax, seqMin, range;
|
|
1342
|
+
|
|
1343
|
+
if (!messageCount) {
|
|
1344
|
+
return {
|
|
1345
|
+
page,
|
|
1346
|
+
pages,
|
|
1347
|
+
messages
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (options.search && uidList) {
|
|
1352
|
+
let start = page * pageSize;
|
|
1353
|
+
let uidRange = uidList.slice(start, start + pageSize).reverse();
|
|
1354
|
+
range = uidRange.join(',');
|
|
1355
|
+
opts.uid = true;
|
|
1356
|
+
} else {
|
|
1357
|
+
seqMax = messageCount - page * pageSize;
|
|
1358
|
+
seqMin = seqMax - pageSize + 1;
|
|
1359
|
+
|
|
1360
|
+
if (seqMax >= messageCount) {
|
|
1361
|
+
seqMax = '*';
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (seqMin < 1) {
|
|
1365
|
+
seqMin = 1;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
range = seqMin === seqMax ? `${seqMin}` : `${seqMin}:${seqMax}`;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
let fields = {
|
|
1372
|
+
uid: true,
|
|
1373
|
+
flags: true,
|
|
1374
|
+
size: true,
|
|
1375
|
+
bodyStructure: true,
|
|
1376
|
+
envelope: true,
|
|
1377
|
+
internalDate: true,
|
|
1378
|
+
emailId: true,
|
|
1379
|
+
threadId: true,
|
|
1380
|
+
labels: true
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
for await (let messageData of this.imapClient.fetch(range, fields, opts)) {
|
|
1384
|
+
let messageInfo = await this.getMessageInfo(messageData);
|
|
1385
|
+
messages.push(messageInfo);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return {
|
|
1389
|
+
total: messageCount,
|
|
1390
|
+
page,
|
|
1391
|
+
pages,
|
|
1392
|
+
// list newer first
|
|
1393
|
+
messages: messages.reverse()
|
|
1394
|
+
};
|
|
1395
|
+
} finally {
|
|
1396
|
+
lock.release();
|
|
1397
|
+
this.connection.onTaskCompleted();
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async buildContacts() {
|
|
1402
|
+
let lock = await this.imapClient.getMailboxLock(this.path);
|
|
1403
|
+
clearTimeout(this.connection.completedTimer);
|
|
1404
|
+
|
|
1405
|
+
let list = [];
|
|
1406
|
+
|
|
1407
|
+
try {
|
|
1408
|
+
let range = '1:*';
|
|
1409
|
+
|
|
1410
|
+
let fields = {
|
|
1411
|
+
uid: true,
|
|
1412
|
+
envelope: true,
|
|
1413
|
+
headers: ['from', 'to', 'cc']
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
for await (let messageData of this.imapClient.fetch(range, fields)) {
|
|
1417
|
+
let addresses = parseAddressHeaders(messageData.headers);
|
|
1418
|
+
if (addresses && addresses.length) {
|
|
1419
|
+
list = list.concat(addresses);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return list;
|
|
1424
|
+
} finally {
|
|
1425
|
+
lock.release();
|
|
1426
|
+
this.connection.onTaskCompleted();
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
async markUpdated() {
|
|
1431
|
+
try {
|
|
1432
|
+
await this.connection.redis.hset(this.connection.getAccountKey(), 'sync', new Date().toISOString());
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
this.logger.error({ msg: 'Redis error', err });
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
mightBeABounce(messageInfo) {
|
|
1439
|
+
if (this.path !== 'INBOX' && !(this.isAllMail && messageInfo.labels && messageInfo.labels.includes('\\Inbox'))) {
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
let name = (messageInfo.from && messageInfo.from.name) || '';
|
|
1444
|
+
let address = (messageInfo.from && messageInfo.from.address) || '';
|
|
1445
|
+
|
|
1446
|
+
if (/Mail Delivery System|Mail Delivery Subsystem|Internet Mail Delivery/i.test(name)) {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (/mailer-daemon@|postmaster@/i.test(address)) {
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function textToHtml(str) {
|
|
1459
|
+
let encoded = he
|
|
1460
|
+
// encode special chars
|
|
1461
|
+
.encode(str, {
|
|
1462
|
+
useNamedReferences: true
|
|
1463
|
+
});
|
|
1464
|
+
let text = `<p>${
|
|
1465
|
+
encoded
|
|
1466
|
+
.replace(/\r?\n/g, '\n')
|
|
1467
|
+
.trim() // normalize line endings
|
|
1468
|
+
.replace(/[ \t]+$/gm, '')
|
|
1469
|
+
.trim() // trim empty line endings
|
|
1470
|
+
.replace(/\n\n+/g, '</p><p>')
|
|
1471
|
+
.trim() // insert <p> to multiple linebreaks
|
|
1472
|
+
.replace(/\n/g, '<br/>') // insert <br> to single linebreaks
|
|
1473
|
+
}</p>`;
|
|
1474
|
+
|
|
1475
|
+
return text;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function parseAddressHeaders(headers) {
|
|
1479
|
+
let decodedHeaders = headers && libmime.decodeHeaders(headers.toString().trim());
|
|
1480
|
+
if (!decodedHeaders) {
|
|
1481
|
+
return [];
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
let result = [];
|
|
1485
|
+
|
|
1486
|
+
Object.keys(decodedHeaders).forEach(key => {
|
|
1487
|
+
let value = decodedHeaders[key];
|
|
1488
|
+
switch (key) {
|
|
1489
|
+
case 'from':
|
|
1490
|
+
case 'to':
|
|
1491
|
+
case 'cc':
|
|
1492
|
+
case 'bcc':
|
|
1493
|
+
case 'sender':
|
|
1494
|
+
case 'reply-to':
|
|
1495
|
+
case 'delivered-to':
|
|
1496
|
+
case 'return-path':
|
|
1497
|
+
value = addressparser(value);
|
|
1498
|
+
decodeAddresses(value);
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
value.forEach(entry => {
|
|
1503
|
+
result.push(Object.assign({ type: key }, entry));
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
return result;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function decodeAddresses(addresses) {
|
|
1511
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
1512
|
+
let address = addresses[i];
|
|
1513
|
+
address.name = (address.name || '').toString().trim();
|
|
1514
|
+
|
|
1515
|
+
if (!address.address && /^(=\?([^?]+)\?[Bb]\?[^?]*\?=)(\s*=\?([^?]+)\?[Bb]\?[^?]*\?=)*$/.test(address.name)) {
|
|
1516
|
+
let parsed = addressparser(libmime.decodeWords(address.name));
|
|
1517
|
+
if (parsed.length) {
|
|
1518
|
+
parsed.forEach(entry => addresses.push(entry));
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// remove current element
|
|
1522
|
+
addresses.splice(i, 1);
|
|
1523
|
+
i--;
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (address.name) {
|
|
1528
|
+
try {
|
|
1529
|
+
address.name = libmime.decodeWords(address.name);
|
|
1530
|
+
} catch (E) {
|
|
1531
|
+
//ignore, keep as is
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (/@xn--/.test(address.address)) {
|
|
1535
|
+
address.address =
|
|
1536
|
+
address.address.substr(0, address.address.lastIndexOf('@') + 1) +
|
|
1537
|
+
punycode.toUnicode(address.address.substr(address.address.lastIndexOf('@') + 1));
|
|
1538
|
+
}
|
|
1539
|
+
if (address.group) {
|
|
1540
|
+
decodeAddresses(address.group);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
module.exports.Mailbox = Mailbox;
|