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/workers/api.js
ADDED
|
@@ -0,0 +1,2266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parentPort } = require('worker_threads');
|
|
4
|
+
const Hapi = require('@hapi/hapi');
|
|
5
|
+
const Boom = require('@hapi/boom');
|
|
6
|
+
const BasicAuth = require('@hapi/basic');
|
|
7
|
+
const Joi = require('joi');
|
|
8
|
+
const logger = require('../lib/logger');
|
|
9
|
+
const hapiPino = require('hapi-pino');
|
|
10
|
+
const { ImapFlow } = require('imapflow');
|
|
11
|
+
const nodemailer = require('nodemailer');
|
|
12
|
+
const Inert = require('@hapi/inert');
|
|
13
|
+
const Vision = require('@hapi/vision');
|
|
14
|
+
const HapiSwagger = require('hapi-swagger');
|
|
15
|
+
const packageData = require('../package.json');
|
|
16
|
+
const pathlib = require('path');
|
|
17
|
+
const config = require('wild-config');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const { PassThrough } = require('stream');
|
|
20
|
+
const msgpack = require('msgpack5')();
|
|
21
|
+
const { OAuth2Client } = require('google-auth-library');
|
|
22
|
+
const consts = require('../lib/consts');
|
|
23
|
+
|
|
24
|
+
const { redis } = require('../lib/db');
|
|
25
|
+
const { Account } = require('../lib/account');
|
|
26
|
+
const settings = require('../lib/settings');
|
|
27
|
+
const { getByteSize, getDuration, getCounterValues, getAuthSettings } = require('../lib/tools');
|
|
28
|
+
|
|
29
|
+
const getSecret = require('../lib/get-secret');
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
settingsSchema,
|
|
33
|
+
addressSchema,
|
|
34
|
+
settingsQuerySchema,
|
|
35
|
+
imapSchema,
|
|
36
|
+
smtpSchema,
|
|
37
|
+
oauth2Schema,
|
|
38
|
+
messageDetailsSchema,
|
|
39
|
+
messageListSchema,
|
|
40
|
+
mailboxesSchema,
|
|
41
|
+
shortMailboxesSchema
|
|
42
|
+
} = require('../lib/schemas');
|
|
43
|
+
|
|
44
|
+
const DEFAULT_EENGINE_TIMEOUT = 10 * 1000;
|
|
45
|
+
const DEFAULT_MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
config.api = config.api || {
|
|
48
|
+
port: 3000,
|
|
49
|
+
host: '127.0.0.1'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
config.service = config.service || {};
|
|
53
|
+
|
|
54
|
+
const EENGINE_TIMEOUT = getDuration(process.env.EENGINE_TIMEOUT || config.service.commandTimeout) || DEFAULT_EENGINE_TIMEOUT;
|
|
55
|
+
const MAX_ATTACHMENT_SIZE = getByteSize(process.env.EENGINE_MAX_SIZE || config.api.maxSize) || DEFAULT_MAX_ATTACHMENT_SIZE;
|
|
56
|
+
const EENGINE_AUTH = getAuthSettings(process.env.EENGINE_AUTH || config.api.auth);
|
|
57
|
+
|
|
58
|
+
const API_PORT = (process.env.EENGINE_PORT && Number(process.env.EENGINE_PORT)) || config.api.port;
|
|
59
|
+
const API_HOST = process.env.EENGINE_HOST || config.api.host;
|
|
60
|
+
|
|
61
|
+
const failAction = async (request, h, err) => {
|
|
62
|
+
let details = (err.details || []).map(detail => ({ message: detail.message, key: detail.context.key }));
|
|
63
|
+
|
|
64
|
+
logger.error({
|
|
65
|
+
msg: 'Request failed',
|
|
66
|
+
method: request.method,
|
|
67
|
+
route: request.route.path,
|
|
68
|
+
statusCode: request.response && request.response.statusCode,
|
|
69
|
+
err
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let message = 'Invalid input';
|
|
73
|
+
let error = Boom.boomify(new Error(message), { statusCode: 400 });
|
|
74
|
+
error.output.payload.fields = details;
|
|
75
|
+
throw error;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
let callQueue = new Map();
|
|
79
|
+
let mids = 0;
|
|
80
|
+
|
|
81
|
+
async function call(message, transferList) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
let mid = `${Date.now()}:${++mids}`;
|
|
84
|
+
|
|
85
|
+
let timer = setTimeout(() => {
|
|
86
|
+
let err = new Error('Timeout waiting for command response');
|
|
87
|
+
err.statusCode = 504;
|
|
88
|
+
err.code = 'Timeout';
|
|
89
|
+
reject(err);
|
|
90
|
+
}, message.timeout || EENGINE_TIMEOUT);
|
|
91
|
+
|
|
92
|
+
callQueue.set(mid, { resolve, reject, timer });
|
|
93
|
+
|
|
94
|
+
parentPort.postMessage(
|
|
95
|
+
{
|
|
96
|
+
cmd: 'call',
|
|
97
|
+
mid,
|
|
98
|
+
message
|
|
99
|
+
},
|
|
100
|
+
transferList
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function metrics(logger, key, method, ...args) {
|
|
106
|
+
try {
|
|
107
|
+
parentPort.postMessage({
|
|
108
|
+
cmd: 'metrics',
|
|
109
|
+
key,
|
|
110
|
+
method,
|
|
111
|
+
args
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.error({ msg: 'Failed to post metrics to parent', err });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function notify(cmd, data) {
|
|
119
|
+
parentPort.postMessage({
|
|
120
|
+
cmd,
|
|
121
|
+
data
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function onCommand(command) {
|
|
126
|
+
logger.debug({ msg: 'Unhandled command', command });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
parentPort.on('message', message => {
|
|
130
|
+
if (message && message.cmd === 'resp' && message.mid && callQueue.has(message.mid)) {
|
|
131
|
+
let { resolve, reject, timer } = callQueue.get(message.mid);
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
callQueue.delete(message.mid);
|
|
134
|
+
if (message.error) {
|
|
135
|
+
let err = new Error(message.error);
|
|
136
|
+
if (message.code) {
|
|
137
|
+
err.code = message.code;
|
|
138
|
+
}
|
|
139
|
+
if (message.statusCode) {
|
|
140
|
+
err.statusCode = message.statusCode;
|
|
141
|
+
}
|
|
142
|
+
return reject(err);
|
|
143
|
+
} else {
|
|
144
|
+
return resolve(message.response);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (message && message.cmd === 'call' && message.mid) {
|
|
149
|
+
return onCommand(message.message)
|
|
150
|
+
.then(response => {
|
|
151
|
+
parentPort.postMessage({
|
|
152
|
+
cmd: 'resp',
|
|
153
|
+
mid: message.mid,
|
|
154
|
+
response
|
|
155
|
+
});
|
|
156
|
+
})
|
|
157
|
+
.catch(err => {
|
|
158
|
+
parentPort.postMessage({
|
|
159
|
+
cmd: 'resp',
|
|
160
|
+
mid: message.mid,
|
|
161
|
+
error: err.message,
|
|
162
|
+
code: err.code,
|
|
163
|
+
statusCode: err.statusCode
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const getOAuth2Client = async () => {
|
|
170
|
+
let keys = {
|
|
171
|
+
clientId: await settings.get('gmailClientId'),
|
|
172
|
+
clientSecret: await settings.get('gmailClientSecret'),
|
|
173
|
+
redirectUrl: await settings.get('gmailRedirectUrl')
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!keys.clientId || !keys.clientSecret || !keys.redirectUrl) {
|
|
177
|
+
let error = Boom.boomify(new Error('OAuth2 credentials not set up'), { statusCode: 400 });
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return new OAuth2Client(keys.clientId, keys.clientSecret, keys.redirectUrl);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const init = async () => {
|
|
185
|
+
const server = Hapi.server({
|
|
186
|
+
port: (process.env.EENGINE_PORT && Number(process.env.EENGINE_PORT)) || config.api.port,
|
|
187
|
+
host: process.env.EENGINE_HOST || config.api.host
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const swaggerOptions = {
|
|
191
|
+
swaggerUI: true,
|
|
192
|
+
swaggerUIPath: '/swagger/',
|
|
193
|
+
documentationPage: true,
|
|
194
|
+
documentationPath: '/docs',
|
|
195
|
+
|
|
196
|
+
grouping: 'tags',
|
|
197
|
+
|
|
198
|
+
info: {
|
|
199
|
+
title: 'EmailEngine',
|
|
200
|
+
version: packageData.version,
|
|
201
|
+
contact: {
|
|
202
|
+
name: 'Andris Reinman',
|
|
203
|
+
email: 'andris@emailengine.app'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const validateBasicAuth = async (request, username, password /*, h*/) => {
|
|
209
|
+
if (!EENGINE_AUTH.enabled) {
|
|
210
|
+
return { credentials: null, isValid: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (username.trim() !== EENGINE_AUTH.user || password !== EENGINE_AUTH.pass) {
|
|
214
|
+
return { credentials: null, isValid: false };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { isValid: true, credentials: { id: username } };
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (EENGINE_AUTH.enabled) {
|
|
221
|
+
// setup basic auth
|
|
222
|
+
await server.register(BasicAuth);
|
|
223
|
+
|
|
224
|
+
server.auth.strategy('simple', 'basic', { validate: validateBasicAuth });
|
|
225
|
+
server.auth.default('simple');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await server.register({
|
|
229
|
+
plugin: hapiPino,
|
|
230
|
+
options: {
|
|
231
|
+
instance: logger.child({ component: 'api' }),
|
|
232
|
+
// Redact Authorization headers, see https://getpino.io/#/docs/redaction
|
|
233
|
+
redact: ['req.headers.authorization']
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await server.register([
|
|
238
|
+
Inert,
|
|
239
|
+
Vision,
|
|
240
|
+
{
|
|
241
|
+
plugin: HapiSwagger,
|
|
242
|
+
options: swaggerOptions
|
|
243
|
+
}
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
server.events.on('response', request => {
|
|
247
|
+
if (!/^\/v1\//.test(request.route.path)) {
|
|
248
|
+
// only log API calls
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
metrics(logger, 'apiCall', 'inc', {
|
|
252
|
+
method: request.method,
|
|
253
|
+
route: request.route.path,
|
|
254
|
+
statusCode: request.response && request.response.statusCode
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
server.route({
|
|
259
|
+
method: 'GET',
|
|
260
|
+
path: '/',
|
|
261
|
+
handler: {
|
|
262
|
+
file: pathlib.join(__dirname, '..', 'static', 'index.html')
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
server.route({
|
|
267
|
+
method: 'GET',
|
|
268
|
+
path: '/favicon.ico',
|
|
269
|
+
handler: {
|
|
270
|
+
file: pathlib.join(__dirname, '..', 'static', 'favicon.ico')
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
server.route({
|
|
275
|
+
method: 'GET',
|
|
276
|
+
path: '/static/{file*}',
|
|
277
|
+
handler: {
|
|
278
|
+
directory: {
|
|
279
|
+
path: pathlib.join(__dirname, '..', 'static')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
server.route({
|
|
285
|
+
method: 'GET',
|
|
286
|
+
path: '/oauth',
|
|
287
|
+
async handler(request, h) {
|
|
288
|
+
const oAuth2Client = await getOAuth2Client();
|
|
289
|
+
|
|
290
|
+
if (request.query.error) {
|
|
291
|
+
let error = Boom.boomify(new Error(`Oauth failed: ${request.query.error}`), { statusCode: 400 });
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!request.query.code) {
|
|
296
|
+
// throw
|
|
297
|
+
let error = Boom.boomify(new Error(`Oauth failed: node code received`), { statusCode: 400 });
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!/^account:add:/.test(request.query.state)) {
|
|
302
|
+
let error = Boom.boomify(new Error(`Oauth failed: invalid state received`), { statusCode: 400 });
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let [[, accountData]] = await redis.multi().get(request.query.state).del(request.query.state).exec();
|
|
307
|
+
if (!accountData) {
|
|
308
|
+
let error = Boom.boomify(new Error(`Oauth failed: session expired`), { statusCode: 400 });
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
accountData = JSON.parse(accountData);
|
|
314
|
+
} catch (E) {
|
|
315
|
+
let error = Boom.boomify(new Error(`Oauth failed: invalid session`), { statusCode: 400 });
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const r = await oAuth2Client.getToken(request.query.code);
|
|
320
|
+
if (!r || !r.tokens) {
|
|
321
|
+
let error = Boom.boomify(new Error(`Oauth failed: did not get token`), { statusCode: 400 });
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// retrieve account email address as this is the username for IMAP/SMTP
|
|
326
|
+
oAuth2Client.setCredentials(r.tokens);
|
|
327
|
+
let profileRes;
|
|
328
|
+
try {
|
|
329
|
+
profileRes = await oAuth2Client.request({ url: 'https://gmail.googleapis.com/gmail/v1/users/me/profile' });
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (err.response && err.response.data && err.response.data.error) {
|
|
332
|
+
let error = Boom.boomify(new Error(err.response.data.error.message), { statusCode: err.response.data.error.code });
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!profileRes || !profileRes.data || !profileRes.data.emailAddress) {
|
|
339
|
+
let error = Boom.boomify(new Error(`Oauth failed: failed to retrieve account email address`), { statusCode: 400 });
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
accountData.oauth2 = Object.assign(
|
|
344
|
+
accountData.oauth2 || {},
|
|
345
|
+
{
|
|
346
|
+
provider: 'gmail',
|
|
347
|
+
accessToken: r.tokens.access_token,
|
|
348
|
+
refreshToken: r.tokens.refresh_token,
|
|
349
|
+
expires: new Date(r.tokens.expiry_date),
|
|
350
|
+
scope: r.tokens.scope,
|
|
351
|
+
tokenType: r.tokens.token_type
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
auth: {
|
|
355
|
+
user: profileRes.data.emailAddress
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
let accountObject = new Account({ redis, call, secret: await getSecret() });
|
|
361
|
+
let result = await accountObject.create(accountData);
|
|
362
|
+
|
|
363
|
+
return h.redirect(`/#account:created=${result.account}`);
|
|
364
|
+
},
|
|
365
|
+
options: {
|
|
366
|
+
description: 'OAuth2 response endpoint',
|
|
367
|
+
|
|
368
|
+
validate: {
|
|
369
|
+
options: {
|
|
370
|
+
stripUnknown: false,
|
|
371
|
+
abortEarly: false,
|
|
372
|
+
convert: true
|
|
373
|
+
},
|
|
374
|
+
failAction,
|
|
375
|
+
|
|
376
|
+
query: Joi.object({
|
|
377
|
+
state: Joi.string().max(1024).example('account:add:12345').description('OAuth2 state info'),
|
|
378
|
+
code: Joi.string().max(1024).example('67890...').description('OAuth2 setup code'),
|
|
379
|
+
scope: Joi.string().max(1024).example('https://mail.google.com/').description('OAuth2 scopes'),
|
|
380
|
+
error: Joi.string().max(1024).example('access_denied').description('OAuth2 scopes')
|
|
381
|
+
}).label('CreateAccount')
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
server.route({
|
|
387
|
+
method: 'POST',
|
|
388
|
+
path: '/v1/account',
|
|
389
|
+
|
|
390
|
+
async handler(request) {
|
|
391
|
+
let accountObject = new Account({ redis, call, secret: await getSecret() });
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
if (request.payload.oauth2 && request.payload.oauth2.authorize) {
|
|
395
|
+
// redirect to OAuth2 consent screen
|
|
396
|
+
|
|
397
|
+
const oAuth2Client = await getOAuth2Client();
|
|
398
|
+
|
|
399
|
+
let nonce = crypto.randomBytes(12).toString('hex');
|
|
400
|
+
|
|
401
|
+
delete request.payload.oauth2.authorize; // do not store this property
|
|
402
|
+
// store account data
|
|
403
|
+
await redis
|
|
404
|
+
.multi()
|
|
405
|
+
.set(`account:add:${nonce}`, JSON.stringify(request.payload))
|
|
406
|
+
.expire(`account:add:${nonce}`, 1 * 24 * 3600)
|
|
407
|
+
.exec();
|
|
408
|
+
|
|
409
|
+
// Generate the url that will be used for the consent dialog.
|
|
410
|
+
const authorizeUrl = oAuth2Client.generateAuthUrl({
|
|
411
|
+
access_type: 'offline',
|
|
412
|
+
scope: ['https://mail.google.com/'],
|
|
413
|
+
state: `account:add:${nonce}`,
|
|
414
|
+
prompt: 'consent'
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
redirect: authorizeUrl
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let result = await accountObject.create(request.payload);
|
|
423
|
+
return result;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (Boom.isBoom(err)) {
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
429
|
+
if (err.code) {
|
|
430
|
+
error.output.payload.code = err.code;
|
|
431
|
+
}
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
options: {
|
|
436
|
+
description: 'Register new account',
|
|
437
|
+
notes: 'Registers new IMAP account to be synced',
|
|
438
|
+
tags: ['api', 'account'],
|
|
439
|
+
|
|
440
|
+
validate: {
|
|
441
|
+
options: {
|
|
442
|
+
stripUnknown: false,
|
|
443
|
+
abortEarly: false,
|
|
444
|
+
convert: true
|
|
445
|
+
},
|
|
446
|
+
failAction,
|
|
447
|
+
|
|
448
|
+
payload: Joi.object({
|
|
449
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
450
|
+
|
|
451
|
+
name: Joi.string().max(256).required().example('My Email Account').description('Display name for the account'),
|
|
452
|
+
|
|
453
|
+
path: Joi.string().empty('').max(1024).default('*').example('INBOX').description('Check changes only on selected path'),
|
|
454
|
+
|
|
455
|
+
copy: Joi.boolean().example(true).description('Copy submitted messages to Sent folder').default(true),
|
|
456
|
+
notifyFrom: Joi.date().example('2021-07-08T07:06:34.336Z').description('Notify messages from date').default('now').iso(),
|
|
457
|
+
|
|
458
|
+
imap: Joi.object(imapSchema).allow(false).xor('useAuthServer', 'auth').description('IMAP configuration').label('IMAP'),
|
|
459
|
+
|
|
460
|
+
smtp: Joi.object(smtpSchema).allow(false).xor('useAuthServer', 'auth').description('SMTP configuration').label('SMTP'),
|
|
461
|
+
|
|
462
|
+
oauth2: oauth2Schema.allow(false).description('OAuth2 configuration').label('OAuth2')
|
|
463
|
+
}).label('CreateAccount')
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
response: {
|
|
467
|
+
schema: Joi.object({
|
|
468
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
469
|
+
state: Joi.string().required().valid('existing', 'new').example('new').description('Is the account new or updated existing')
|
|
470
|
+
}).label('CreateAccountReponse'),
|
|
471
|
+
failAction: 'log'
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
server.route({
|
|
477
|
+
method: 'PUT',
|
|
478
|
+
path: '/v1/account/{account}',
|
|
479
|
+
|
|
480
|
+
async handler(request) {
|
|
481
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
return await accountObject.update(request.payload);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (Boom.isBoom(err)) {
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
490
|
+
if (err.code) {
|
|
491
|
+
error.output.payload.code = err.code;
|
|
492
|
+
}
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
options: {
|
|
497
|
+
description: 'Update account info',
|
|
498
|
+
notes: 'Updates account information',
|
|
499
|
+
tags: ['api', 'account'],
|
|
500
|
+
|
|
501
|
+
validate: {
|
|
502
|
+
options: {
|
|
503
|
+
stripUnknown: false,
|
|
504
|
+
abortEarly: false,
|
|
505
|
+
convert: true
|
|
506
|
+
},
|
|
507
|
+
failAction,
|
|
508
|
+
|
|
509
|
+
params: Joi.object({
|
|
510
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
511
|
+
}),
|
|
512
|
+
|
|
513
|
+
payload: Joi.object({
|
|
514
|
+
name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
|
|
515
|
+
|
|
516
|
+
path: Joi.string().empty('').max(1024).default('*').example('INBOX').description('Check changes only on selected path'),
|
|
517
|
+
|
|
518
|
+
copy: Joi.boolean().example(true).description('Copy submitted messages to Sent folder').default(true),
|
|
519
|
+
notifyFrom: Joi.date().example('2021-07-08T07:06:34.336Z').description('Notify messages from date').default('now').iso(),
|
|
520
|
+
|
|
521
|
+
imap: Joi.object(imapSchema).xor('useAuthServer', 'auth').description('IMAP configuration').label('IMAP'),
|
|
522
|
+
smtp: Joi.object(smtpSchema).allow(false).xor('useAuthServer', 'auth').description('SMTP configuration').label('SMTP')
|
|
523
|
+
}).label('UpdateAccount')
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
response: {
|
|
527
|
+
schema: Joi.object({
|
|
528
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
529
|
+
}).label('UpdateAccountReponse'),
|
|
530
|
+
failAction: 'log'
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
server.route({
|
|
536
|
+
method: 'PUT',
|
|
537
|
+
path: '/v1/account/{account}/reconnect',
|
|
538
|
+
|
|
539
|
+
async handler(request) {
|
|
540
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
return { reconnect: await accountObject.requestReconnect(request.payload) };
|
|
544
|
+
} catch (err) {
|
|
545
|
+
if (Boom.isBoom(err)) {
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
548
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
549
|
+
if (err.code) {
|
|
550
|
+
error.output.payload.code = err.code;
|
|
551
|
+
}
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
options: {
|
|
556
|
+
description: 'Request reconnect',
|
|
557
|
+
notes: 'Requests connection to be reconnected',
|
|
558
|
+
tags: ['api', 'account'],
|
|
559
|
+
|
|
560
|
+
validate: {
|
|
561
|
+
options: {
|
|
562
|
+
stripUnknown: false,
|
|
563
|
+
abortEarly: false,
|
|
564
|
+
convert: true
|
|
565
|
+
},
|
|
566
|
+
failAction,
|
|
567
|
+
|
|
568
|
+
params: Joi.object({
|
|
569
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
570
|
+
}),
|
|
571
|
+
|
|
572
|
+
payload: Joi.object({
|
|
573
|
+
reconnect: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Only reconnect if true')
|
|
574
|
+
}).label('RequestReconnect')
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
response: {
|
|
578
|
+
schema: Joi.object({
|
|
579
|
+
reconnect: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(false).description('Only reconnect if true')
|
|
580
|
+
}).label('RequestReconnectReponse'),
|
|
581
|
+
failAction: 'log'
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
server.route({
|
|
587
|
+
method: 'DELETE',
|
|
588
|
+
path: '/v1/account/{account}',
|
|
589
|
+
|
|
590
|
+
async handler(request) {
|
|
591
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
return await accountObject.delete();
|
|
595
|
+
} catch (err) {
|
|
596
|
+
if (Boom.isBoom(err)) {
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
600
|
+
if (err.code) {
|
|
601
|
+
error.output.payload.code = err.code;
|
|
602
|
+
}
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
options: {
|
|
607
|
+
description: 'Remove synced account',
|
|
608
|
+
notes: 'Stop syncing IMAP account and delete cached values',
|
|
609
|
+
tags: ['api', 'account'],
|
|
610
|
+
|
|
611
|
+
validate: {
|
|
612
|
+
options: {
|
|
613
|
+
stripUnknown: false,
|
|
614
|
+
abortEarly: false,
|
|
615
|
+
convert: true
|
|
616
|
+
},
|
|
617
|
+
failAction,
|
|
618
|
+
|
|
619
|
+
params: Joi.object({
|
|
620
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
621
|
+
}).label('DeleteRequest')
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
response: {
|
|
625
|
+
schema: Joi.object({
|
|
626
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
627
|
+
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the account deleted')
|
|
628
|
+
}).label('DeleteRequestReponse'),
|
|
629
|
+
failAction: 'log'
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
server.route({
|
|
635
|
+
method: 'GET',
|
|
636
|
+
path: '/v1/accounts',
|
|
637
|
+
|
|
638
|
+
async handler(request) {
|
|
639
|
+
try {
|
|
640
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
641
|
+
|
|
642
|
+
return await accountObject.listAccounts(request.query.state, request.query.page, request.query.pageSize);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (Boom.isBoom(err)) {
|
|
645
|
+
throw err;
|
|
646
|
+
}
|
|
647
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
648
|
+
if (err.code) {
|
|
649
|
+
error.output.payload.code = err.code;
|
|
650
|
+
}
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
options: {
|
|
656
|
+
description: 'List accounts',
|
|
657
|
+
notes: 'Lists registered accounts',
|
|
658
|
+
tags: ['api', 'account'],
|
|
659
|
+
|
|
660
|
+
validate: {
|
|
661
|
+
options: {
|
|
662
|
+
stripUnknown: false,
|
|
663
|
+
abortEarly: false,
|
|
664
|
+
convert: true
|
|
665
|
+
},
|
|
666
|
+
failAction,
|
|
667
|
+
|
|
668
|
+
query: Joi.object({
|
|
669
|
+
page: Joi.number()
|
|
670
|
+
.min(0)
|
|
671
|
+
.max(1024 * 1024)
|
|
672
|
+
.default(0)
|
|
673
|
+
.example(0)
|
|
674
|
+
.description('Page number (zero indexed, so use 0 for first page)')
|
|
675
|
+
.label('PageNumber'),
|
|
676
|
+
pageSize: Joi.number().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize'),
|
|
677
|
+
state: Joi.string()
|
|
678
|
+
.valid('init', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
|
|
679
|
+
.example('connected')
|
|
680
|
+
.description('Filter accounts by state')
|
|
681
|
+
.label('AccountState')
|
|
682
|
+
}).label('AccountsFilter')
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
response: {
|
|
686
|
+
schema: Joi.object({
|
|
687
|
+
total: Joi.number().example(120).description('How many matching entries').label('TotalNumber'),
|
|
688
|
+
page: Joi.number().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
689
|
+
pages: Joi.number().example(24).description('Total page count').label('PagesNumber'),
|
|
690
|
+
|
|
691
|
+
accounts: Joi.array()
|
|
692
|
+
.items(
|
|
693
|
+
Joi.object({
|
|
694
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
695
|
+
name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
|
|
696
|
+
state: Joi.string()
|
|
697
|
+
.required()
|
|
698
|
+
.valid('init', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
|
|
699
|
+
.example('connected')
|
|
700
|
+
.description('Account state'),
|
|
701
|
+
syncTime: Joi.date().example('2021-02-17T13:43:18.860Z').description('Last sync time').iso(),
|
|
702
|
+
lastError: Joi.object({
|
|
703
|
+
response: Joi.string().example('Request to authentication server failed'),
|
|
704
|
+
serverResponseCode: Joi.string().example('HTTPRequestError')
|
|
705
|
+
})
|
|
706
|
+
.allow(null)
|
|
707
|
+
.label('AccountErrorEntry')
|
|
708
|
+
}).label('AccountResponseItem')
|
|
709
|
+
)
|
|
710
|
+
.label('AccountEntries')
|
|
711
|
+
}).label('AccountsFilterReponse'),
|
|
712
|
+
failAction: 'log'
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
server.route({
|
|
718
|
+
method: 'GET',
|
|
719
|
+
path: '/v1/account/{account}',
|
|
720
|
+
|
|
721
|
+
async handler(request) {
|
|
722
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
723
|
+
try {
|
|
724
|
+
let accountData = await accountObject.loadAccountData();
|
|
725
|
+
|
|
726
|
+
// remove secrets
|
|
727
|
+
for (let type of ['imap', 'smtp', 'oauth2']) {
|
|
728
|
+
if (accountData[type] && accountData[type].auth) {
|
|
729
|
+
for (let key of ['pass', 'accessToken', 'refreshToken']) {
|
|
730
|
+
if (key in accountData[type].auth) {
|
|
731
|
+
accountData[type].auth[key] = '******';
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (accountData[type]) {
|
|
737
|
+
for (let key of ['accessToken', 'refreshToken']) {
|
|
738
|
+
if (key in accountData[type]) {
|
|
739
|
+
accountData[type][key] = '******';
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let result = {};
|
|
746
|
+
|
|
747
|
+
for (let key of ['account', 'name', 'copy', 'notifyFrom', 'imap', 'smtp', 'oauth2']) {
|
|
748
|
+
if (key in accountData) {
|
|
749
|
+
result[key] = accountData[key];
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return result;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
if (Boom.isBoom(err)) {
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
759
|
+
if (err.code) {
|
|
760
|
+
error.output.payload.code = err.code;
|
|
761
|
+
}
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
options: {
|
|
766
|
+
description: 'Get account info',
|
|
767
|
+
notes: 'Returns stored information about the account. Passwords are not included.',
|
|
768
|
+
tags: ['api', 'account'],
|
|
769
|
+
|
|
770
|
+
validate: {
|
|
771
|
+
options: {
|
|
772
|
+
stripUnknown: false,
|
|
773
|
+
abortEarly: false,
|
|
774
|
+
convert: true
|
|
775
|
+
},
|
|
776
|
+
failAction,
|
|
777
|
+
|
|
778
|
+
params: Joi.object({
|
|
779
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
780
|
+
})
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
response: {
|
|
784
|
+
schema: Joi.object({
|
|
785
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
786
|
+
|
|
787
|
+
name: Joi.string().max(256).required().example('My Email Account').description('Display name for the account'),
|
|
788
|
+
|
|
789
|
+
copy: Joi.boolean().example(true).description('Copy submitted messages to Sent folder').default(true),
|
|
790
|
+
notifyFrom: Joi.date().example('2021-07-08T07:06:34.336Z').description('Notify messages from date').default('now').iso(),
|
|
791
|
+
|
|
792
|
+
imap: Joi.object(imapSchema).description('IMAP configuration').label('IMAP'),
|
|
793
|
+
|
|
794
|
+
smtp: Joi.object(smtpSchema).description('SMTP configuration').label('SMTP')
|
|
795
|
+
}).label('AccountResponse'),
|
|
796
|
+
failAction: 'log'
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
server.route({
|
|
802
|
+
method: 'GET',
|
|
803
|
+
path: '/v1/account/{account}/mailboxes',
|
|
804
|
+
|
|
805
|
+
async handler(request) {
|
|
806
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
return { mailboxes: await accountObject.getMailboxListing() };
|
|
810
|
+
} catch (err) {
|
|
811
|
+
if (Boom.isBoom(err)) {
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
815
|
+
if (err.code) {
|
|
816
|
+
error.output.payload.code = err.code;
|
|
817
|
+
}
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
|
|
822
|
+
options: {
|
|
823
|
+
description: 'List mailboxes',
|
|
824
|
+
notes: 'Lists all available mailboxes',
|
|
825
|
+
tags: ['api', 'mailbox'],
|
|
826
|
+
|
|
827
|
+
validate: {
|
|
828
|
+
options: {
|
|
829
|
+
stripUnknown: false,
|
|
830
|
+
abortEarly: false,
|
|
831
|
+
convert: true
|
|
832
|
+
},
|
|
833
|
+
failAction,
|
|
834
|
+
|
|
835
|
+
params: Joi.object({
|
|
836
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
837
|
+
})
|
|
838
|
+
},
|
|
839
|
+
|
|
840
|
+
response: {
|
|
841
|
+
schema: Joi.object({
|
|
842
|
+
mailboxes: mailboxesSchema
|
|
843
|
+
}).label('MailboxesFilterReponse'),
|
|
844
|
+
failAction: 'log'
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
server.route({
|
|
850
|
+
method: 'POST',
|
|
851
|
+
path: '/v1/account/{account}/mailbox',
|
|
852
|
+
|
|
853
|
+
async handler(request) {
|
|
854
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
return await accountObject.createMailbox(request.payload.path);
|
|
858
|
+
} catch (err) {
|
|
859
|
+
if (Boom.isBoom(err)) {
|
|
860
|
+
throw err;
|
|
861
|
+
}
|
|
862
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
863
|
+
if (err.code) {
|
|
864
|
+
error.output.payload.code = err.code;
|
|
865
|
+
}
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
options: {
|
|
871
|
+
description: 'Create mailbox',
|
|
872
|
+
notes: 'Create new mailbox folder',
|
|
873
|
+
tags: ['api', 'mailbox'],
|
|
874
|
+
|
|
875
|
+
validate: {
|
|
876
|
+
options: {
|
|
877
|
+
stripUnknown: false,
|
|
878
|
+
abortEarly: false,
|
|
879
|
+
convert: true
|
|
880
|
+
},
|
|
881
|
+
failAction,
|
|
882
|
+
|
|
883
|
+
params: Joi.object({
|
|
884
|
+
account: Joi.string().max(256).example('example').required().description('Account ID')
|
|
885
|
+
}),
|
|
886
|
+
|
|
887
|
+
payload: Joi.object({
|
|
888
|
+
path: Joi.array()
|
|
889
|
+
.items(Joi.string().max(256))
|
|
890
|
+
.example(['Parent folder', 'Subfolder'])
|
|
891
|
+
.description('Mailbox path as an array. If account is namespaced then namespace prefix is added by default.')
|
|
892
|
+
.label('MailboxPath')
|
|
893
|
+
}).label('CreateMailbox')
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
response: {
|
|
897
|
+
schema: Joi.object({
|
|
898
|
+
path: Joi.string().required().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox').label('MailboxPath'),
|
|
899
|
+
mailboxId: Joi.string().example('1439876283476').description('Mailbox ID (if server has support)').label('MailboxId'),
|
|
900
|
+
created: Joi.boolean().example(true).description('Was the mailbox created')
|
|
901
|
+
}).label('CreateMailboxReponse'),
|
|
902
|
+
failAction: 'log'
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
server.route({
|
|
908
|
+
method: 'DELETE',
|
|
909
|
+
path: '/v1/account/{account}/mailbox',
|
|
910
|
+
|
|
911
|
+
async handler(request) {
|
|
912
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
return await accountObject.deleteMailbox(request.query.path);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
if (Boom.isBoom(err)) {
|
|
918
|
+
throw err;
|
|
919
|
+
}
|
|
920
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
921
|
+
if (err.code) {
|
|
922
|
+
error.output.payload.code = err.code;
|
|
923
|
+
}
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
|
|
928
|
+
options: {
|
|
929
|
+
description: 'Delete mailbox',
|
|
930
|
+
notes: 'Delete existing mailbox folder',
|
|
931
|
+
tags: ['api', 'mailbox'],
|
|
932
|
+
|
|
933
|
+
validate: {
|
|
934
|
+
options: {
|
|
935
|
+
stripUnknown: false,
|
|
936
|
+
abortEarly: false,
|
|
937
|
+
convert: true
|
|
938
|
+
},
|
|
939
|
+
failAction,
|
|
940
|
+
|
|
941
|
+
params: Joi.object({
|
|
942
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
943
|
+
}),
|
|
944
|
+
|
|
945
|
+
query: Joi.object({
|
|
946
|
+
path: Joi.string().required().example('My Outdated Mail').description('Mailbox folder path to delete').label('MailboxPath')
|
|
947
|
+
}).label('DeleteMailbox')
|
|
948
|
+
},
|
|
949
|
+
|
|
950
|
+
response: {
|
|
951
|
+
schema: Joi.object({
|
|
952
|
+
path: Joi.string().required().example('Kalender/S&APw-nnip&AOQ-evad').description('Full path to mailbox').label('MailboxPath'),
|
|
953
|
+
deleted: Joi.boolean().example(true).description('Was the mailbox deleted')
|
|
954
|
+
}).label('DeleteMailboxReponse'),
|
|
955
|
+
failAction: 'log'
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
server.route({
|
|
961
|
+
method: 'GET',
|
|
962
|
+
path: '/v1/account/{account}/message/{message}/source',
|
|
963
|
+
|
|
964
|
+
async handler(request) {
|
|
965
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
966
|
+
|
|
967
|
+
try {
|
|
968
|
+
return await accountObject.getRawMessage(request.params.message);
|
|
969
|
+
} catch (err) {
|
|
970
|
+
if (Boom.isBoom(err)) {
|
|
971
|
+
throw err;
|
|
972
|
+
}
|
|
973
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
974
|
+
if (err.code) {
|
|
975
|
+
error.output.payload.code = err.code;
|
|
976
|
+
}
|
|
977
|
+
throw error;
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
options: {
|
|
981
|
+
description: 'Download raw message',
|
|
982
|
+
notes: 'Fetches raw message as a stream',
|
|
983
|
+
tags: ['api', 'message'],
|
|
984
|
+
|
|
985
|
+
validate: {
|
|
986
|
+
options: {
|
|
987
|
+
stripUnknown: false,
|
|
988
|
+
abortEarly: false,
|
|
989
|
+
convert: true
|
|
990
|
+
},
|
|
991
|
+
failAction,
|
|
992
|
+
|
|
993
|
+
params: Joi.object({
|
|
994
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
995
|
+
message: Joi.string().base64({ paddingRequired: false, urlSafe: true }).max(256).example('AAAAAQAACnA').required().description('Message ID')
|
|
996
|
+
}).label('RawMessageRequest')
|
|
997
|
+
} /*,
|
|
998
|
+
|
|
999
|
+
response: {
|
|
1000
|
+
schema: Joi.binary().example('MIME-Version: 1.0...').description('RFC822 formatted email').label('RawMessageResponse'),
|
|
1001
|
+
failAction: 'log'
|
|
1002
|
+
}
|
|
1003
|
+
*/
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
server.route({
|
|
1008
|
+
method: 'GET',
|
|
1009
|
+
path: '/v1/account/{account}/attachment/{attachment}',
|
|
1010
|
+
|
|
1011
|
+
async handler(request) {
|
|
1012
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
return await accountObject.getAttachment(request.params.attachment);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
if (Boom.isBoom(err)) {
|
|
1018
|
+
throw err;
|
|
1019
|
+
}
|
|
1020
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1021
|
+
if (err.code) {
|
|
1022
|
+
error.output.payload.code = err.code;
|
|
1023
|
+
}
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
options: {
|
|
1028
|
+
description: 'Download attachment',
|
|
1029
|
+
notes: 'Fetches attachment file as a binary stream',
|
|
1030
|
+
tags: ['api', 'message'],
|
|
1031
|
+
|
|
1032
|
+
validate: {
|
|
1033
|
+
options: {
|
|
1034
|
+
stripUnknown: false,
|
|
1035
|
+
abortEarly: false,
|
|
1036
|
+
convert: true
|
|
1037
|
+
},
|
|
1038
|
+
failAction,
|
|
1039
|
+
|
|
1040
|
+
params: Joi.object({
|
|
1041
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
1042
|
+
attachment: Joi.string()
|
|
1043
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1044
|
+
.max(256)
|
|
1045
|
+
.required()
|
|
1046
|
+
.example('AAAAAQAACnAcde')
|
|
1047
|
+
.description('Attachment ID')
|
|
1048
|
+
})
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
server.route({
|
|
1054
|
+
method: 'GET',
|
|
1055
|
+
path: '/v1/account/{account}/message/{message}',
|
|
1056
|
+
|
|
1057
|
+
async handler(request) {
|
|
1058
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
return await accountObject.getMessage(request.params.message, request.query);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
if (Boom.isBoom(err)) {
|
|
1064
|
+
throw err;
|
|
1065
|
+
}
|
|
1066
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1067
|
+
if (err.code) {
|
|
1068
|
+
error.output.payload.code = err.code;
|
|
1069
|
+
}
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
options: {
|
|
1074
|
+
description: 'Get message information',
|
|
1075
|
+
notes: 'Returns details of a specific message. By default text content is not included, use textType value to force retrieving text',
|
|
1076
|
+
tags: ['api', 'message'],
|
|
1077
|
+
|
|
1078
|
+
validate: {
|
|
1079
|
+
options: {
|
|
1080
|
+
stripUnknown: false,
|
|
1081
|
+
abortEarly: false,
|
|
1082
|
+
convert: true
|
|
1083
|
+
},
|
|
1084
|
+
failAction,
|
|
1085
|
+
|
|
1086
|
+
query: Joi.object({
|
|
1087
|
+
maxBytes: Joi.number()
|
|
1088
|
+
.min(0)
|
|
1089
|
+
.max(1024 * 1024 * 1024)
|
|
1090
|
+
.example(5 * 1025 * 1024)
|
|
1091
|
+
.description('Max length of text content'),
|
|
1092
|
+
textType: Joi.string()
|
|
1093
|
+
.lowercase()
|
|
1094
|
+
.valid('html', 'plain', '*')
|
|
1095
|
+
.example('*')
|
|
1096
|
+
.description('Which text content to return, use * for all. By default text content is not returned.')
|
|
1097
|
+
}),
|
|
1098
|
+
|
|
1099
|
+
params: Joi.object({
|
|
1100
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
1101
|
+
message: Joi.string().base64({ paddingRequired: false, urlSafe: true }).max(256).required().example('AAAAAQAACnA').description('Message ID')
|
|
1102
|
+
})
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1105
|
+
response: {
|
|
1106
|
+
schema: messageDetailsSchema,
|
|
1107
|
+
failAction: 'log'
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
server.route({
|
|
1113
|
+
method: 'POST',
|
|
1114
|
+
path: '/v1/account/{account}/message',
|
|
1115
|
+
|
|
1116
|
+
async handler(request) {
|
|
1117
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
return await accountObject.uploadMessage(request.payload);
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
if (Boom.isBoom(err)) {
|
|
1123
|
+
throw err;
|
|
1124
|
+
}
|
|
1125
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1126
|
+
if (err.code) {
|
|
1127
|
+
error.output.payload.code = err.code;
|
|
1128
|
+
}
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
options: {
|
|
1133
|
+
payload: {
|
|
1134
|
+
// allow message uploads up to 50MB
|
|
1135
|
+
// TODO: should it be configurable instead?
|
|
1136
|
+
maxBytes: 50 * 1024 * 1024
|
|
1137
|
+
},
|
|
1138
|
+
|
|
1139
|
+
description: 'Upload message',
|
|
1140
|
+
notes: 'Upload a message structure, compile it into an EML file and store it into selected mailbox.',
|
|
1141
|
+
tags: ['api', 'message'],
|
|
1142
|
+
|
|
1143
|
+
validate: {
|
|
1144
|
+
options: {
|
|
1145
|
+
stripUnknown: false,
|
|
1146
|
+
abortEarly: false,
|
|
1147
|
+
convert: true
|
|
1148
|
+
},
|
|
1149
|
+
failAction,
|
|
1150
|
+
|
|
1151
|
+
params: Joi.object({
|
|
1152
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
1153
|
+
}),
|
|
1154
|
+
|
|
1155
|
+
payload: Joi.object({
|
|
1156
|
+
path: Joi.string().required().example('INBOX').description('Target mailbox folder path'),
|
|
1157
|
+
|
|
1158
|
+
flags: Joi.array().items(Joi.string().max(128)).example(['\\Seen', '\\Draft']).default([]).description('Message flags').label('Flags'),
|
|
1159
|
+
|
|
1160
|
+
reference: Joi.object({
|
|
1161
|
+
message: Joi.string()
|
|
1162
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1163
|
+
.max(256)
|
|
1164
|
+
.required()
|
|
1165
|
+
.example('AAAAAQAACnA')
|
|
1166
|
+
.description('Referenced message ID'),
|
|
1167
|
+
action: Joi.string().lowercase().valid('forward', 'reply').example('reply').default('reply')
|
|
1168
|
+
})
|
|
1169
|
+
.description('Message reference for a reply or a forward. This is EmailEngine specific ID, not Message-ID header value.')
|
|
1170
|
+
.label('MessageReference'),
|
|
1171
|
+
|
|
1172
|
+
from: addressSchema.required().example({ name: 'From Me', address: 'sender@example.com' }),
|
|
1173
|
+
|
|
1174
|
+
to: Joi.array()
|
|
1175
|
+
.items(addressSchema)
|
|
1176
|
+
.description('List of addresses')
|
|
1177
|
+
.example([{ address: 'recipient@example.com' }])
|
|
1178
|
+
.label('AddressList'),
|
|
1179
|
+
|
|
1180
|
+
cc: Joi.array().items(addressSchema).description('List of addresses').label('AddressList'),
|
|
1181
|
+
|
|
1182
|
+
bcc: Joi.array().items(addressSchema).description('List of addresses').label('AddressList'),
|
|
1183
|
+
|
|
1184
|
+
subject: Joi.string().max(1024).example('What a wonderful message').description('Message subject'),
|
|
1185
|
+
|
|
1186
|
+
text: Joi.string().max(MAX_ATTACHMENT_SIZE).example('Hello from myself!').description('Message Text'),
|
|
1187
|
+
|
|
1188
|
+
html: Joi.string().max(MAX_ATTACHMENT_SIZE).example('<p>Hello from myself!</p>').description('Message HTML'),
|
|
1189
|
+
|
|
1190
|
+
attachments: Joi.array()
|
|
1191
|
+
.items(
|
|
1192
|
+
Joi.object({
|
|
1193
|
+
filename: Joi.string().max(256).example('transparent.gif'),
|
|
1194
|
+
content: Joi.string()
|
|
1195
|
+
.base64()
|
|
1196
|
+
.max(MAX_ATTACHMENT_SIZE)
|
|
1197
|
+
.required()
|
|
1198
|
+
.example('R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=')
|
|
1199
|
+
.description('Base64 formatted attachment file'),
|
|
1200
|
+
contentType: Joi.string().lowercase().max(256).example('image/gif'),
|
|
1201
|
+
contentDisposition: Joi.string().lowercase().valid('inline', 'attachment'),
|
|
1202
|
+
cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
|
|
1203
|
+
encoding: Joi.string().valid('base64').default('base64')
|
|
1204
|
+
}).label('Attachment')
|
|
1205
|
+
)
|
|
1206
|
+
.description('List of attachments')
|
|
1207
|
+
.label('AttachmentList'),
|
|
1208
|
+
|
|
1209
|
+
messageId: Joi.string().max(74).example('<test123@example.com>').description('Message ID'),
|
|
1210
|
+
headers: Joi.object().description('Custom Headers')
|
|
1211
|
+
}).label('MessageUpload')
|
|
1212
|
+
},
|
|
1213
|
+
|
|
1214
|
+
response: {
|
|
1215
|
+
schema: Joi.object({
|
|
1216
|
+
id: Joi.string()
|
|
1217
|
+
.example('AAAAAgAACrI')
|
|
1218
|
+
.description('Message ID. NB! This and other fields might not be present if server did not provide enough information')
|
|
1219
|
+
.label('MessageAppendId'),
|
|
1220
|
+
path: Joi.string().example('INBOX').description('Folder this message was uploaded to').label('MessageAppendPath'),
|
|
1221
|
+
uid: Joi.number().example(12345).description('UID of uploaded message'),
|
|
1222
|
+
seq: Joi.number().example(12345).description('Sequence number of uploaded message')
|
|
1223
|
+
}).label('MessageUploadResponse'),
|
|
1224
|
+
failAction: 'log'
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
server.route({
|
|
1230
|
+
method: 'PUT',
|
|
1231
|
+
path: '/v1/account/{account}/message/{message}',
|
|
1232
|
+
|
|
1233
|
+
async handler(request) {
|
|
1234
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1235
|
+
|
|
1236
|
+
try {
|
|
1237
|
+
return await accountObject.updateMessage(request.params.message, request.payload);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
if (Boom.isBoom(err)) {
|
|
1240
|
+
throw err;
|
|
1241
|
+
}
|
|
1242
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1243
|
+
if (err.code) {
|
|
1244
|
+
error.output.payload.code = err.code;
|
|
1245
|
+
}
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
options: {
|
|
1250
|
+
description: 'Update message',
|
|
1251
|
+
notes: 'Update message information. Mainly this means changing message flag values',
|
|
1252
|
+
tags: ['api', 'message'],
|
|
1253
|
+
|
|
1254
|
+
validate: {
|
|
1255
|
+
options: {
|
|
1256
|
+
stripUnknown: false,
|
|
1257
|
+
abortEarly: false,
|
|
1258
|
+
convert: true
|
|
1259
|
+
},
|
|
1260
|
+
failAction,
|
|
1261
|
+
|
|
1262
|
+
params: Joi.object({
|
|
1263
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
1264
|
+
message: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID')
|
|
1265
|
+
}),
|
|
1266
|
+
|
|
1267
|
+
payload: Joi.object({
|
|
1268
|
+
flags: Joi.object({
|
|
1269
|
+
add: Joi.array().items(Joi.string().max(128)).description('Add new flags').example(['\\Seen']).label('AddFlags'),
|
|
1270
|
+
delete: Joi.array().items(Joi.string().max(128)).description('Delete specific flags').example(['\\Flagged']).label('DeleteFlags'),
|
|
1271
|
+
set: Joi.array().items(Joi.string().max(128)).description('Override all flags').example(['\\Seen', '\\Flagged']).label('SetFlags')
|
|
1272
|
+
})
|
|
1273
|
+
.description('Flag updates')
|
|
1274
|
+
.label('FlagUpdate'),
|
|
1275
|
+
|
|
1276
|
+
labels: Joi.object({
|
|
1277
|
+
add: Joi.array().items(Joi.string().max(128)).description('Add new labels').example(['Some label']).label('AddLabels'),
|
|
1278
|
+
delete: Joi.array().items(Joi.string().max(128)).description('Delete specific labels').example(['Some label']).label('DeleteLabels'),
|
|
1279
|
+
set: Joi.array()
|
|
1280
|
+
.items(Joi.string().max(128))
|
|
1281
|
+
.description('Override all labels')
|
|
1282
|
+
.example(['First label', 'Second label'])
|
|
1283
|
+
.label('SetLabels')
|
|
1284
|
+
})
|
|
1285
|
+
.description('Label updates')
|
|
1286
|
+
.label('LabelUpdate')
|
|
1287
|
+
}).label('MessageUpdate')
|
|
1288
|
+
},
|
|
1289
|
+
response: {
|
|
1290
|
+
schema: Joi.object({
|
|
1291
|
+
flags: Joi.object({
|
|
1292
|
+
add: Joi.boolean().example(true),
|
|
1293
|
+
delete: Joi.boolean().example(false),
|
|
1294
|
+
set: Joi.boolean().example(false)
|
|
1295
|
+
}).label('FlagResponse'),
|
|
1296
|
+
labels: Joi.object({
|
|
1297
|
+
add: Joi.boolean().example(true),
|
|
1298
|
+
delete: Joi.boolean().example(false),
|
|
1299
|
+
set: Joi.boolean().example(false)
|
|
1300
|
+
}).label('FlagResponse')
|
|
1301
|
+
}).label('MessageUpdateReponse'),
|
|
1302
|
+
failAction: 'log'
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
server.route({
|
|
1308
|
+
method: 'PUT',
|
|
1309
|
+
path: '/v1/account/{account}/message/{message}/move',
|
|
1310
|
+
|
|
1311
|
+
async handler(request) {
|
|
1312
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1313
|
+
|
|
1314
|
+
try {
|
|
1315
|
+
return await accountObject.moveMessage(request.params.message, request.payload);
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
if (Boom.isBoom(err)) {
|
|
1318
|
+
throw err;
|
|
1319
|
+
}
|
|
1320
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1321
|
+
if (err.code) {
|
|
1322
|
+
error.output.payload.code = err.code;
|
|
1323
|
+
}
|
|
1324
|
+
throw error;
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
options: {
|
|
1328
|
+
description: 'Move message',
|
|
1329
|
+
notes: 'Move message to another folder',
|
|
1330
|
+
tags: ['api', 'message'],
|
|
1331
|
+
|
|
1332
|
+
validate: {
|
|
1333
|
+
options: {
|
|
1334
|
+
stripUnknown: false,
|
|
1335
|
+
abortEarly: false,
|
|
1336
|
+
convert: true
|
|
1337
|
+
},
|
|
1338
|
+
failAction,
|
|
1339
|
+
|
|
1340
|
+
params: Joi.object({
|
|
1341
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
1342
|
+
message: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID')
|
|
1343
|
+
}),
|
|
1344
|
+
|
|
1345
|
+
payload: Joi.object({
|
|
1346
|
+
path: Joi.string().required().example('INBOX').description('Target mailbox folder path')
|
|
1347
|
+
}).label('MessageMove')
|
|
1348
|
+
},
|
|
1349
|
+
|
|
1350
|
+
response: {
|
|
1351
|
+
schema: Joi.object({
|
|
1352
|
+
path: Joi.string().required().example('INBOX').description('Target mailbox folder path'),
|
|
1353
|
+
id: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID'),
|
|
1354
|
+
uid: Joi.number().example(12345).description('UID of moved message')
|
|
1355
|
+
}).label('MessageMoveResponse'),
|
|
1356
|
+
failAction: 'log'
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
server.route({
|
|
1362
|
+
method: 'DELETE',
|
|
1363
|
+
path: '/v1/account/{account}/message/{message}',
|
|
1364
|
+
|
|
1365
|
+
async handler(request) {
|
|
1366
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1367
|
+
|
|
1368
|
+
try {
|
|
1369
|
+
return await accountObject.deleteMessage(request.params.message);
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
if (Boom.isBoom(err)) {
|
|
1372
|
+
throw err;
|
|
1373
|
+
}
|
|
1374
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1375
|
+
if (err.code) {
|
|
1376
|
+
error.output.payload.code = err.code;
|
|
1377
|
+
}
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
options: {
|
|
1382
|
+
description: 'Delete message',
|
|
1383
|
+
notes: 'Move message to Trash or delete it if already in Trash',
|
|
1384
|
+
tags: ['api', 'message'],
|
|
1385
|
+
|
|
1386
|
+
validate: {
|
|
1387
|
+
options: {
|
|
1388
|
+
stripUnknown: false,
|
|
1389
|
+
abortEarly: false,
|
|
1390
|
+
convert: true
|
|
1391
|
+
},
|
|
1392
|
+
failAction,
|
|
1393
|
+
|
|
1394
|
+
params: Joi.object({
|
|
1395
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
1396
|
+
message: Joi.string().max(256).required().example('AAAAAQAACnA').description('Message ID')
|
|
1397
|
+
}).label('MessageDelete')
|
|
1398
|
+
},
|
|
1399
|
+
response: {
|
|
1400
|
+
schema: Joi.object({
|
|
1401
|
+
deleted: Joi.boolean().example(true).description('Present if message was actualy deleted'),
|
|
1402
|
+
moved: Joi.object({
|
|
1403
|
+
destination: Joi.string().required().example('Trash').description('Trash folder path').label('TrashPath'),
|
|
1404
|
+
message: Joi.string().required().example('AAAAAwAAAWg').description('Message ID in Trash').label('TrashMessageId')
|
|
1405
|
+
}).description('Present if message was moved to Trash')
|
|
1406
|
+
}).label('MessageDeleteReponse'),
|
|
1407
|
+
failAction: 'log'
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
server.route({
|
|
1413
|
+
method: 'GET',
|
|
1414
|
+
path: '/v1/account/{account}/text/{text}',
|
|
1415
|
+
|
|
1416
|
+
async handler(request) {
|
|
1417
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1418
|
+
|
|
1419
|
+
try {
|
|
1420
|
+
return await accountObject.getText(request.params.text, request.query);
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
if (Boom.isBoom(err)) {
|
|
1423
|
+
throw err;
|
|
1424
|
+
}
|
|
1425
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1426
|
+
if (err.code) {
|
|
1427
|
+
error.output.payload.code = err.code;
|
|
1428
|
+
}
|
|
1429
|
+
throw error;
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
options: {
|
|
1433
|
+
description: 'Retrieve message text',
|
|
1434
|
+
notes: 'Retrieves message text',
|
|
1435
|
+
tags: ['api', 'message'],
|
|
1436
|
+
|
|
1437
|
+
validate: {
|
|
1438
|
+
options: {
|
|
1439
|
+
stripUnknown: false,
|
|
1440
|
+
abortEarly: false,
|
|
1441
|
+
convert: true
|
|
1442
|
+
},
|
|
1443
|
+
failAction,
|
|
1444
|
+
|
|
1445
|
+
query: Joi.object({
|
|
1446
|
+
maxBytes: Joi.number()
|
|
1447
|
+
.min(0)
|
|
1448
|
+
.max(1024 * 1024 * 1024)
|
|
1449
|
+
.example(MAX_ATTACHMENT_SIZE)
|
|
1450
|
+
.description('Max length of text content'),
|
|
1451
|
+
textType: Joi.string()
|
|
1452
|
+
.lowercase()
|
|
1453
|
+
.valid('html', 'plain', '*')
|
|
1454
|
+
.default('*')
|
|
1455
|
+
.example('*')
|
|
1456
|
+
.description('Which text content to return, use * for all. By default all contents are returned.')
|
|
1457
|
+
}),
|
|
1458
|
+
|
|
1459
|
+
params: Joi.object({
|
|
1460
|
+
account: Joi.string().max(256).required().example('example').description('Account ID'),
|
|
1461
|
+
text: Joi.string()
|
|
1462
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1463
|
+
.max(256)
|
|
1464
|
+
.required()
|
|
1465
|
+
.example('AAAAAQAACnAcdfaaN')
|
|
1466
|
+
.description('Message text ID')
|
|
1467
|
+
}).label('Text')
|
|
1468
|
+
},
|
|
1469
|
+
|
|
1470
|
+
response: {
|
|
1471
|
+
schema: Joi.object({
|
|
1472
|
+
plain: Joi.string().example('Hello world').description('Plaintext content'),
|
|
1473
|
+
html: Joi.string().example('<p>Hello world</p>').description('HTML content'),
|
|
1474
|
+
hasMore: Joi.boolean().example(false).description('Is the current text output capped or not')
|
|
1475
|
+
}).label('TextResponse'),
|
|
1476
|
+
failAction: 'log'
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
server.route({
|
|
1482
|
+
method: 'GET',
|
|
1483
|
+
path: '/v1/account/{account}/messages',
|
|
1484
|
+
|
|
1485
|
+
async handler(request) {
|
|
1486
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1487
|
+
try {
|
|
1488
|
+
return await accountObject.listMessages(request.query);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
if (Boom.isBoom(err)) {
|
|
1491
|
+
throw err;
|
|
1492
|
+
}
|
|
1493
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1494
|
+
if (err.code) {
|
|
1495
|
+
error.output.payload.code = err.code;
|
|
1496
|
+
}
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
options: {
|
|
1501
|
+
description: 'List messages in a folder',
|
|
1502
|
+
notes: 'Lists messages in a mailbox folder',
|
|
1503
|
+
tags: ['api', 'message'],
|
|
1504
|
+
|
|
1505
|
+
validate: {
|
|
1506
|
+
options: {
|
|
1507
|
+
stripUnknown: false,
|
|
1508
|
+
abortEarly: false,
|
|
1509
|
+
convert: true
|
|
1510
|
+
},
|
|
1511
|
+
failAction,
|
|
1512
|
+
|
|
1513
|
+
params: Joi.object({
|
|
1514
|
+
account: Joi.string().max(256).required().example('example').description('Account ID').label('AccountId')
|
|
1515
|
+
}),
|
|
1516
|
+
|
|
1517
|
+
query: Joi.object({
|
|
1518
|
+
path: Joi.string().required().example('INBOX').description('Mailbox folder path').label('Path'),
|
|
1519
|
+
page: Joi.number()
|
|
1520
|
+
.min(0)
|
|
1521
|
+
.max(1024 * 1024)
|
|
1522
|
+
.default(0)
|
|
1523
|
+
.example(0)
|
|
1524
|
+
.description('Page number (zero indexed, so use 0 for first page)')
|
|
1525
|
+
.label('PageNumber'),
|
|
1526
|
+
pageSize: Joi.number().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
1527
|
+
}).label('MessageQuery')
|
|
1528
|
+
},
|
|
1529
|
+
|
|
1530
|
+
response: {
|
|
1531
|
+
schema: messageListSchema,
|
|
1532
|
+
failAction: 'log'
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
server.route({
|
|
1538
|
+
method: 'POST',
|
|
1539
|
+
path: '/v1/account/{account}/search',
|
|
1540
|
+
|
|
1541
|
+
async handler(request) {
|
|
1542
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1543
|
+
try {
|
|
1544
|
+
return await accountObject.listMessages(Object.assign(request.query, request.payload));
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
if (Boom.isBoom(err)) {
|
|
1547
|
+
throw err;
|
|
1548
|
+
}
|
|
1549
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1550
|
+
if (err.code) {
|
|
1551
|
+
error.output.payload.code = err.code;
|
|
1552
|
+
}
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
options: {
|
|
1557
|
+
description: 'Search for messages',
|
|
1558
|
+
notes: 'Filter messages from a mailbox folder by search options. Search is performed against a specific foldera and not for the entire account.',
|
|
1559
|
+
tags: ['api', 'message'],
|
|
1560
|
+
|
|
1561
|
+
validate: {
|
|
1562
|
+
options: {
|
|
1563
|
+
stripUnknown: false,
|
|
1564
|
+
abortEarly: false,
|
|
1565
|
+
convert: true
|
|
1566
|
+
},
|
|
1567
|
+
failAction,
|
|
1568
|
+
|
|
1569
|
+
params: Joi.object({
|
|
1570
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
1571
|
+
}),
|
|
1572
|
+
|
|
1573
|
+
query: Joi.object({
|
|
1574
|
+
path: Joi.string().required().example('INBOX').description('Mailbox folder path'),
|
|
1575
|
+
page: Joi.number()
|
|
1576
|
+
.min(0)
|
|
1577
|
+
.max(1024 * 1024)
|
|
1578
|
+
.default(0)
|
|
1579
|
+
.example(0)
|
|
1580
|
+
.description('Page number (zero indexed, so use 0 for first page)'),
|
|
1581
|
+
pageSize: Joi.number().min(1).max(1000).default(20).example(20).description('How many entries per page')
|
|
1582
|
+
}),
|
|
1583
|
+
|
|
1584
|
+
payload: Joi.object({
|
|
1585
|
+
search: Joi.object({
|
|
1586
|
+
seq: Joi.string().max(256).description('Sequence number range'),
|
|
1587
|
+
|
|
1588
|
+
answered: Joi.boolean()
|
|
1589
|
+
.truthy('Y', 'true', '1')
|
|
1590
|
+
.falsy('N', 'false', 0)
|
|
1591
|
+
.description('Check if message is answered or not')
|
|
1592
|
+
.label('AnsweredFlag'),
|
|
1593
|
+
deleted: Joi.boolean()
|
|
1594
|
+
.truthy('Y', 'true', '1')
|
|
1595
|
+
.falsy('N', 'false', 0)
|
|
1596
|
+
.description('Check if message is marked for being deleted or not')
|
|
1597
|
+
.label('DeletedFlag'),
|
|
1598
|
+
draft: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).description('Check if message is a draft').label('DraftFlag'),
|
|
1599
|
+
unseen: Joi.boolean()
|
|
1600
|
+
.truthy('Y', 'true', '1')
|
|
1601
|
+
.falsy('N', 'false', 0)
|
|
1602
|
+
.description('Check if message is marked as unseen or not')
|
|
1603
|
+
.label('UnseenFlag'),
|
|
1604
|
+
flagged: Joi.boolean()
|
|
1605
|
+
.truthy('Y', 'true', '1')
|
|
1606
|
+
.falsy('N', 'false', 0)
|
|
1607
|
+
.description('Check if message is flagged or not')
|
|
1608
|
+
.label('Flagged'),
|
|
1609
|
+
seen: Joi.boolean()
|
|
1610
|
+
.truthy('Y', 'true', '1')
|
|
1611
|
+
.falsy('N', 'false', 0)
|
|
1612
|
+
.description('Check if message is marked as seen or not')
|
|
1613
|
+
.label('SeenFlag'),
|
|
1614
|
+
|
|
1615
|
+
from: Joi.string().max(256).description('Match From: header').label('From'),
|
|
1616
|
+
to: Joi.string().max(256).description('Match To: header').label('To'),
|
|
1617
|
+
cc: Joi.string().max(256).description('Match Cc: header').label('Cc'),
|
|
1618
|
+
bcc: Joi.string().max(256).description('Match Bcc: header').label('Bcc'),
|
|
1619
|
+
|
|
1620
|
+
body: Joi.string().max(256).description('Match text body').label('MessageBody'),
|
|
1621
|
+
subject: Joi.string().max(256).description('Match message subject').label('Subject'),
|
|
1622
|
+
|
|
1623
|
+
larger: Joi.number()
|
|
1624
|
+
.min(0)
|
|
1625
|
+
.max(1024 * 1024 * 1024)
|
|
1626
|
+
.description('Matches messages larger than value')
|
|
1627
|
+
.label('MessageLarger'),
|
|
1628
|
+
|
|
1629
|
+
smaller: Joi.number()
|
|
1630
|
+
.min(0)
|
|
1631
|
+
.max(1024 * 1024 * 1024)
|
|
1632
|
+
.description('Matches messages smaller than value')
|
|
1633
|
+
.label('MessageSmaller'),
|
|
1634
|
+
|
|
1635
|
+
uid: Joi.string().max(256).description('UID range').label('UIDRange'),
|
|
1636
|
+
|
|
1637
|
+
modseq: Joi.number().min(0).description('Matches messages with modseq higher than value').label('ModseqLarger'),
|
|
1638
|
+
|
|
1639
|
+
before: Joi.date().description('Matches messages received before date').label('EnvelopeBefore'),
|
|
1640
|
+
since: Joi.date().description('Matches messages received after date').label('EnvelopeSince'),
|
|
1641
|
+
|
|
1642
|
+
sentBefore: Joi.date().description('Matches messages sent before date').label('HeaderBefore'),
|
|
1643
|
+
sentSince: Joi.date().description('Matches messages sent after date').label('HeaderSince'),
|
|
1644
|
+
|
|
1645
|
+
emailId: Joi.string().max(256).description('Match specific Gmail unique email UD'),
|
|
1646
|
+
threadId: Joi.string().max(256).description('Match specific Gmail unique thread UD'),
|
|
1647
|
+
|
|
1648
|
+
header: Joi.object().unknown(true).description('Headers to match against').label('Headers')
|
|
1649
|
+
})
|
|
1650
|
+
.required()
|
|
1651
|
+
.description('Search query to filter messages')
|
|
1652
|
+
.label('SearchQuery')
|
|
1653
|
+
}).label('SearchQuery')
|
|
1654
|
+
},
|
|
1655
|
+
|
|
1656
|
+
response: {
|
|
1657
|
+
schema: messageListSchema,
|
|
1658
|
+
failAction: 'log'
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
server.route({
|
|
1664
|
+
method: 'GET',
|
|
1665
|
+
path: '/v1/account/{account}/contacts',
|
|
1666
|
+
|
|
1667
|
+
async handler(request) {
|
|
1668
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1669
|
+
try {
|
|
1670
|
+
return await accountObject.buildContacts();
|
|
1671
|
+
} catch (err) {
|
|
1672
|
+
if (Boom.isBoom(err)) {
|
|
1673
|
+
throw err;
|
|
1674
|
+
}
|
|
1675
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1676
|
+
if (err.code) {
|
|
1677
|
+
error.output.payload.code = err.code;
|
|
1678
|
+
}
|
|
1679
|
+
throw error;
|
|
1680
|
+
}
|
|
1681
|
+
},
|
|
1682
|
+
options: {
|
|
1683
|
+
description: 'Builds a contact listing',
|
|
1684
|
+
notes: 'Builds a contact listings from email addresses. For larger mailboxes this could take a lot of time.',
|
|
1685
|
+
tags: [/*'api', */ 'experimental'],
|
|
1686
|
+
|
|
1687
|
+
validate: {
|
|
1688
|
+
options: {
|
|
1689
|
+
stripUnknown: false,
|
|
1690
|
+
abortEarly: false,
|
|
1691
|
+
convert: true
|
|
1692
|
+
},
|
|
1693
|
+
failAction,
|
|
1694
|
+
|
|
1695
|
+
params: Joi.object({
|
|
1696
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
1697
|
+
})
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
server.route({
|
|
1703
|
+
method: 'POST',
|
|
1704
|
+
path: '/v1/account/{account}/submit',
|
|
1705
|
+
|
|
1706
|
+
async handler(request) {
|
|
1707
|
+
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
1708
|
+
|
|
1709
|
+
try {
|
|
1710
|
+
return await accountObject.queueMessage(request.payload);
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
if (Boom.isBoom(err)) {
|
|
1713
|
+
throw err;
|
|
1714
|
+
}
|
|
1715
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1716
|
+
if (err.code) {
|
|
1717
|
+
error.output.payload.code = err.code;
|
|
1718
|
+
}
|
|
1719
|
+
throw error;
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
options: {
|
|
1723
|
+
payload: {
|
|
1724
|
+
// allow message uploads up to 50MB
|
|
1725
|
+
// TODO: should it be configurable instead?
|
|
1726
|
+
maxBytes: 50 * 1024 * 1024
|
|
1727
|
+
},
|
|
1728
|
+
|
|
1729
|
+
description: 'Submit message for delivery',
|
|
1730
|
+
notes: 'Submit message for delivery. If reference message ID is provided then EmailEngine adds all headers and flags required for a reply/forward automatically.',
|
|
1731
|
+
tags: ['api', 'submit'],
|
|
1732
|
+
|
|
1733
|
+
validate: {
|
|
1734
|
+
options: {
|
|
1735
|
+
stripUnknown: false,
|
|
1736
|
+
abortEarly: false,
|
|
1737
|
+
convert: true
|
|
1738
|
+
},
|
|
1739
|
+
failAction,
|
|
1740
|
+
|
|
1741
|
+
params: Joi.object({
|
|
1742
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
1743
|
+
}),
|
|
1744
|
+
|
|
1745
|
+
payload: Joi.object({
|
|
1746
|
+
reference: Joi.object({
|
|
1747
|
+
message: Joi.string()
|
|
1748
|
+
.base64({ paddingRequired: false, urlSafe: true })
|
|
1749
|
+
.max(256)
|
|
1750
|
+
.required()
|
|
1751
|
+
.example('AAAAAQAACnA')
|
|
1752
|
+
.description('Referenced message ID'),
|
|
1753
|
+
action: Joi.string().lowercase().valid('forward', 'reply').example('reply').default('reply')
|
|
1754
|
+
})
|
|
1755
|
+
.description('Message reference for a reply or a forward. This is EmailEngine specific ID, not Message-ID header value.')
|
|
1756
|
+
.label('MessageReference'),
|
|
1757
|
+
|
|
1758
|
+
envelope: Joi.object({
|
|
1759
|
+
from: Joi.string().email().allow('').example('sender@example.com'),
|
|
1760
|
+
to: Joi.array().items(Joi.string().email().required().example('recipient@example.com'))
|
|
1761
|
+
})
|
|
1762
|
+
.description('Optional SMTP envelope. If not set then derived from message headers.')
|
|
1763
|
+
.label('SMTPEnvelope'),
|
|
1764
|
+
|
|
1765
|
+
from: addressSchema.example({ name: 'From Me', address: 'sender@example.com' }),
|
|
1766
|
+
|
|
1767
|
+
to: Joi.array()
|
|
1768
|
+
.items(addressSchema)
|
|
1769
|
+
.description('List of addresses')
|
|
1770
|
+
.example([{ address: 'recipient@example.com' }])
|
|
1771
|
+
.label('ToAddressList'),
|
|
1772
|
+
|
|
1773
|
+
cc: Joi.array().items(addressSchema).description('List of addresses').label('CcAddressList'),
|
|
1774
|
+
|
|
1775
|
+
bcc: Joi.array().items(addressSchema).description('List of addresses').label('BccAddressList'),
|
|
1776
|
+
|
|
1777
|
+
raw: Joi.string()
|
|
1778
|
+
.base64()
|
|
1779
|
+
.max(MAX_ATTACHMENT_SIZE)
|
|
1780
|
+
.example('TUlNRS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IGhlbGxvIHdvcmxkDQoNCkhlbGxvIQ0K')
|
|
1781
|
+
.description(
|
|
1782
|
+
'Base64 encoded email message in rfc822 format. If you provide other keys as well then these will override the values in the raw message.'
|
|
1783
|
+
)
|
|
1784
|
+
.label('RFC822Raw'),
|
|
1785
|
+
|
|
1786
|
+
subject: Joi.string().max(1024).example('What a wonderful message').description('Message subject'),
|
|
1787
|
+
|
|
1788
|
+
text: Joi.string().max(MAX_ATTACHMENT_SIZE).example('Hello from myself!').description('Message Text'),
|
|
1789
|
+
|
|
1790
|
+
html: Joi.string().max(MAX_ATTACHMENT_SIZE).example('<p>Hello from myself!</p>').description('Message HTML'),
|
|
1791
|
+
|
|
1792
|
+
attachments: Joi.array()
|
|
1793
|
+
.items(
|
|
1794
|
+
Joi.object({
|
|
1795
|
+
filename: Joi.string().max(256).example('transparent.gif'),
|
|
1796
|
+
content: Joi.string()
|
|
1797
|
+
.base64()
|
|
1798
|
+
.max(MAX_ATTACHMENT_SIZE)
|
|
1799
|
+
.required()
|
|
1800
|
+
.example('R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=')
|
|
1801
|
+
.description('Base64 formatted attachment file'),
|
|
1802
|
+
contentType: Joi.string().lowercase().max(256).example('image/gif'),
|
|
1803
|
+
contentDisposition: Joi.string().lowercase().valid('inline', 'attachment'),
|
|
1804
|
+
cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
|
|
1805
|
+
encoding: Joi.string().valid('base64').default('base64')
|
|
1806
|
+
}).label('Attachment')
|
|
1807
|
+
)
|
|
1808
|
+
.description('List of attachments')
|
|
1809
|
+
.label('AttachmentList'),
|
|
1810
|
+
|
|
1811
|
+
messageId: Joi.string().max(74).example('<test123@example.com>').description('Message ID'),
|
|
1812
|
+
headers: Joi.object().description('Custom Headers'),
|
|
1813
|
+
|
|
1814
|
+
sendAt: Joi.date().example('2021-07-08T07:06:34.336Z').description('Send message at specified time').iso()
|
|
1815
|
+
})
|
|
1816
|
+
.oxor('raw', 'html')
|
|
1817
|
+
.oxor('raw', 'text')
|
|
1818
|
+
.oxor('raw', 'text')
|
|
1819
|
+
.oxor('raw', 'attachments')
|
|
1820
|
+
.label('SubmitMessage')
|
|
1821
|
+
},
|
|
1822
|
+
|
|
1823
|
+
response: {
|
|
1824
|
+
schema: Joi.object({
|
|
1825
|
+
response: Joi.string().example('Queued for delivery'),
|
|
1826
|
+
messageId: Joi.string().example('<a2184d08-a470-fec6-a493-fa211a3756e9@example.com>').description('Message-ID header value'),
|
|
1827
|
+
sendAt: Joi.date().example('2021-07-08T07:06:34.336Z').description('Scheduled send time'),
|
|
1828
|
+
queueId: Joi.string().example('d41f0423195f271f').description('Queue identifier for scheduled email')
|
|
1829
|
+
}).label('SubmitMessageResponse'),
|
|
1830
|
+
failAction: 'log'
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
server.route({
|
|
1836
|
+
method: 'GET',
|
|
1837
|
+
path: '/v1/settings',
|
|
1838
|
+
|
|
1839
|
+
async handler(request) {
|
|
1840
|
+
let values = {};
|
|
1841
|
+
for (let key of Object.keys(request.query)) {
|
|
1842
|
+
if (request.query[key]) {
|
|
1843
|
+
if (key === 'eventTypes') {
|
|
1844
|
+
values[key] = Object.keys(consts)
|
|
1845
|
+
.map(key => {
|
|
1846
|
+
if (/_NOTIFY?/.test(key)) {
|
|
1847
|
+
return consts[key];
|
|
1848
|
+
}
|
|
1849
|
+
return false;
|
|
1850
|
+
})
|
|
1851
|
+
.map(key => key);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
let value = await settings.get(key);
|
|
1856
|
+
|
|
1857
|
+
if (settings.encryptedKeys.includes(key)) {
|
|
1858
|
+
// do not reveal secret values
|
|
1859
|
+
// instead show boolean value true if value is set, or false if it's not
|
|
1860
|
+
value = value ? true : false;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
values[key] = value;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return values;
|
|
1867
|
+
},
|
|
1868
|
+
options: {
|
|
1869
|
+
description: 'List specific settings',
|
|
1870
|
+
notes: 'List setting values for specific keys',
|
|
1871
|
+
tags: ['api', 'settings'],
|
|
1872
|
+
|
|
1873
|
+
validate: {
|
|
1874
|
+
options: {
|
|
1875
|
+
stripUnknown: false,
|
|
1876
|
+
abortEarly: false,
|
|
1877
|
+
convert: true
|
|
1878
|
+
},
|
|
1879
|
+
failAction,
|
|
1880
|
+
|
|
1881
|
+
query: Joi.object(settingsQuerySchema).label('SettingsQuery')
|
|
1882
|
+
},
|
|
1883
|
+
|
|
1884
|
+
response: {
|
|
1885
|
+
schema: Joi.object(settingsSchema).label('SettingsQueryResponse'),
|
|
1886
|
+
failAction: 'log'
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
server.route({
|
|
1892
|
+
method: 'POST',
|
|
1893
|
+
path: '/v1/settings',
|
|
1894
|
+
|
|
1895
|
+
async handler(request) {
|
|
1896
|
+
let updated = [];
|
|
1897
|
+
for (let key of Object.keys(request.payload)) {
|
|
1898
|
+
switch (key) {
|
|
1899
|
+
case 'logs': {
|
|
1900
|
+
let logs = request.payload.logs;
|
|
1901
|
+
let resetLoggedAccounts = logs.resetLoggedAccounts;
|
|
1902
|
+
delete logs.resetLoggedAccounts;
|
|
1903
|
+
if (resetLoggedAccounts && logs.accounts && logs.accounts.length) {
|
|
1904
|
+
for (let account of logs.accounts) {
|
|
1905
|
+
logger.info({ msg: 'Request reconnect for logging', account });
|
|
1906
|
+
try {
|
|
1907
|
+
await call({ cmd: 'update', account });
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
logger.error({ action: 'request_reconnect', account, err });
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
await settings.set(key, request.payload[key]);
|
|
1917
|
+
updated.push(key);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
notify('settings', request.payload);
|
|
1921
|
+
return { updated };
|
|
1922
|
+
},
|
|
1923
|
+
options: {
|
|
1924
|
+
description: 'Set setting values',
|
|
1925
|
+
notes: 'Set setting values for specific keys',
|
|
1926
|
+
tags: ['api', 'settings'],
|
|
1927
|
+
|
|
1928
|
+
validate: {
|
|
1929
|
+
options: {
|
|
1930
|
+
stripUnknown: false,
|
|
1931
|
+
abortEarly: false,
|
|
1932
|
+
convert: true
|
|
1933
|
+
},
|
|
1934
|
+
failAction,
|
|
1935
|
+
|
|
1936
|
+
payload: Joi.object(settingsSchema).label('Settings')
|
|
1937
|
+
},
|
|
1938
|
+
|
|
1939
|
+
response: {
|
|
1940
|
+
schema: Joi.object({ updated: Joi.array().items(Joi.string().example('notifyHeaders')).description('List of updated setting keys') }).label(
|
|
1941
|
+
'SettingsResponse'
|
|
1942
|
+
),
|
|
1943
|
+
failAction: 'log'
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
server.route({
|
|
1949
|
+
method: 'GET',
|
|
1950
|
+
path: '/v1/logs/{account}',
|
|
1951
|
+
|
|
1952
|
+
async handler(request) {
|
|
1953
|
+
return getLogs(request.params.account);
|
|
1954
|
+
},
|
|
1955
|
+
options: {
|
|
1956
|
+
description: 'Return IMAP logs for an account',
|
|
1957
|
+
notes: 'Output is a downloadable text file',
|
|
1958
|
+
tags: ['api', 'logs'],
|
|
1959
|
+
|
|
1960
|
+
validate: {
|
|
1961
|
+
options: {
|
|
1962
|
+
stripUnknown: false,
|
|
1963
|
+
abortEarly: false,
|
|
1964
|
+
convert: true
|
|
1965
|
+
},
|
|
1966
|
+
failAction,
|
|
1967
|
+
|
|
1968
|
+
params: Joi.object({
|
|
1969
|
+
account: Joi.string().max(256).required().example('example').description('Account ID')
|
|
1970
|
+
})
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
server.route({
|
|
1976
|
+
method: 'GET',
|
|
1977
|
+
path: '/v1/stats',
|
|
1978
|
+
|
|
1979
|
+
async handler(request) {
|
|
1980
|
+
return await getStats(request.query.seconds);
|
|
1981
|
+
},
|
|
1982
|
+
options: {
|
|
1983
|
+
description: 'Return server stats',
|
|
1984
|
+
tags: ['api', 'stats'],
|
|
1985
|
+
|
|
1986
|
+
validate: {
|
|
1987
|
+
options: {
|
|
1988
|
+
stripUnknown: false,
|
|
1989
|
+
abortEarly: false,
|
|
1990
|
+
convert: true
|
|
1991
|
+
},
|
|
1992
|
+
failAction,
|
|
1993
|
+
|
|
1994
|
+
query: Joi.object({
|
|
1995
|
+
seconds: Joi.number()
|
|
1996
|
+
.empty('')
|
|
1997
|
+
.min(0)
|
|
1998
|
+
.max(consts.MAX_DAYS_STATS * 24 * 3600)
|
|
1999
|
+
.default(3600)
|
|
2000
|
+
.example(3600)
|
|
2001
|
+
.description('Duration for counters')
|
|
2002
|
+
.label('CounterSeconds')
|
|
2003
|
+
}).label('ServerStats')
|
|
2004
|
+
},
|
|
2005
|
+
|
|
2006
|
+
response: {
|
|
2007
|
+
schema: Joi.object({
|
|
2008
|
+
version: Joi.string().example(packageData.version).description('EmailEngine version number'),
|
|
2009
|
+
license: Joi.string().example(packageData.license).description('EmailEngine license'),
|
|
2010
|
+
accounts: Joi.number().example(26).description('Number of registered accounts'),
|
|
2011
|
+
connections: Joi.object({
|
|
2012
|
+
init: Joi.number().example(2).description('Accounts not yet initialized'),
|
|
2013
|
+
connected: Joi.number().example(8).description('Successfully connected accounts'),
|
|
2014
|
+
connecting: Joi.number().example(7).description('Connection is being established'),
|
|
2015
|
+
authenticationError: Joi.number().example(3).description('Authentication failed'),
|
|
2016
|
+
connectError: Joi.number().example(5).description('Connection failed due to technical error'),
|
|
2017
|
+
unset: Joi.number().example(0).description('Accounts without valid IMAP settings'),
|
|
2018
|
+
disconnected: Joi.number().example(1).description('IMAP connection was closed')
|
|
2019
|
+
})
|
|
2020
|
+
.description('Counts of accounts in different connection states')
|
|
2021
|
+
.label('ConnectionsStats'),
|
|
2022
|
+
counters: Joi.object().label('CounterStats')
|
|
2023
|
+
}).label('SettingsResponse'),
|
|
2024
|
+
failAction: 'log'
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
server.route({
|
|
2030
|
+
method: 'POST',
|
|
2031
|
+
path: '/v1/verifyAccount',
|
|
2032
|
+
|
|
2033
|
+
async handler(request) {
|
|
2034
|
+
try {
|
|
2035
|
+
return await verifyAccountInfo(request.payload);
|
|
2036
|
+
} catch (err) {
|
|
2037
|
+
if (Boom.isBoom(err)) {
|
|
2038
|
+
throw err;
|
|
2039
|
+
}
|
|
2040
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
2041
|
+
if (err.code) {
|
|
2042
|
+
error.output.payload.code = err.code;
|
|
2043
|
+
}
|
|
2044
|
+
throw error;
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
options: {
|
|
2048
|
+
description: 'Verify IMAP and SMTP settings',
|
|
2049
|
+
notes: 'Checks if can connect and authenticate using provided account info',
|
|
2050
|
+
tags: ['api', 'account'],
|
|
2051
|
+
|
|
2052
|
+
validate: {
|
|
2053
|
+
options: {
|
|
2054
|
+
stripUnknown: false,
|
|
2055
|
+
abortEarly: false,
|
|
2056
|
+
convert: true
|
|
2057
|
+
},
|
|
2058
|
+
failAction,
|
|
2059
|
+
|
|
2060
|
+
payload: Joi.object({
|
|
2061
|
+
mailboxes: Joi.boolean().example(false).description('Include mailbox listing in response').default(false),
|
|
2062
|
+
imap: Joi.object(imapSchema).description('IMAP configuration').label('IMAP'),
|
|
2063
|
+
smtp: Joi.object(smtpSchema).allow(false).description('SMTP configuration').label('SMTP')
|
|
2064
|
+
}).label('VerifyAccount')
|
|
2065
|
+
},
|
|
2066
|
+
response: {
|
|
2067
|
+
schema: Joi.object({
|
|
2068
|
+
imap: Joi.object({
|
|
2069
|
+
success: Joi.boolean().example(true).description('Was IMAP account verified').label('VerifyImapSuccess'),
|
|
2070
|
+
error: Joi.string()
|
|
2071
|
+
.example('Something went wrong')
|
|
2072
|
+
.description('Error messages for IMAP verification. Only present if success=false')
|
|
2073
|
+
.label('VerifyImapError'),
|
|
2074
|
+
code: Joi.string()
|
|
2075
|
+
.example('ERR_SSL_WRONG_VERSION_NUMBER')
|
|
2076
|
+
.description('Error code. Only present if success=false')
|
|
2077
|
+
.label('VerifyImapCode')
|
|
2078
|
+
}),
|
|
2079
|
+
smtp: Joi.object({
|
|
2080
|
+
success: Joi.boolean().example(true).description('Was SMTP account verified').label('VerifySmtpSuccess'),
|
|
2081
|
+
error: Joi.string()
|
|
2082
|
+
.example('Something went wrong')
|
|
2083
|
+
.description('Error messages for SMTP verification. Only present if success=false')
|
|
2084
|
+
.label('VerifySmtpError'),
|
|
2085
|
+
code: Joi.string()
|
|
2086
|
+
.example('ERR_SSL_WRONG_VERSION_NUMBER')
|
|
2087
|
+
.description('Error code. Only present if success=false')
|
|
2088
|
+
.label('VerifySmtpCode')
|
|
2089
|
+
}),
|
|
2090
|
+
mailboxes: shortMailboxesSchema
|
|
2091
|
+
}).label('VerifyAccountReponse'),
|
|
2092
|
+
failAction: 'log'
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
server.route({
|
|
2098
|
+
method: 'GET',
|
|
2099
|
+
path: '/metrics',
|
|
2100
|
+
async handler(request, h) {
|
|
2101
|
+
const renderedMetrics = await call({ cmd: 'metrics' });
|
|
2102
|
+
const response = h.response('success');
|
|
2103
|
+
response.type('text/plain');
|
|
2104
|
+
return renderedMetrics;
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
server.route({
|
|
2109
|
+
method: '*',
|
|
2110
|
+
path: '/{any*}',
|
|
2111
|
+
async handler() {
|
|
2112
|
+
throw Boom.notFound('Requested page not found'); // 404
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
await server.start();
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
function getLogs(account) {
|
|
2120
|
+
let logKey = `iam:${account}:g`;
|
|
2121
|
+
let passThrough = new PassThrough();
|
|
2122
|
+
|
|
2123
|
+
redis
|
|
2124
|
+
.lrangeBuffer(logKey, 0, -1)
|
|
2125
|
+
.then(rows => {
|
|
2126
|
+
if (!rows || !Array.isArray(rows) || !rows.length) {
|
|
2127
|
+
return passThrough.end(`No logs found for ${account}\n`);
|
|
2128
|
+
}
|
|
2129
|
+
let processNext = () => {
|
|
2130
|
+
if (!rows.length) {
|
|
2131
|
+
return passThrough.end();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
let row = rows.shift();
|
|
2135
|
+
let entry;
|
|
2136
|
+
try {
|
|
2137
|
+
entry = msgpack.decode(row);
|
|
2138
|
+
} catch (err) {
|
|
2139
|
+
entry = { error: err.stack };
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (entry) {
|
|
2143
|
+
if (!passThrough.write(JSON.stringify(entry) + '\n')) {
|
|
2144
|
+
return passThrough.once('drain', processNext);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
setImmediate(processNext);
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
processNext();
|
|
2152
|
+
})
|
|
2153
|
+
.catch(err => {
|
|
2154
|
+
passThrough.end(`\nFailed to process logs\n${err.stack}\n`);
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
return passThrough;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
async function verifyAccountInfo(accountData) {
|
|
2161
|
+
let response = {};
|
|
2162
|
+
|
|
2163
|
+
if (accountData.imap) {
|
|
2164
|
+
try {
|
|
2165
|
+
let imapClient = new ImapFlow(
|
|
2166
|
+
Object.assign(
|
|
2167
|
+
{
|
|
2168
|
+
verifyOnly: true,
|
|
2169
|
+
includeMailboxes: accountData.mailboxes
|
|
2170
|
+
},
|
|
2171
|
+
accountData.imap
|
|
2172
|
+
)
|
|
2173
|
+
);
|
|
2174
|
+
|
|
2175
|
+
let mailboxes = await new Promise((resolve, reject) => {
|
|
2176
|
+
imapClient.on('error', err => {
|
|
2177
|
+
reject(err);
|
|
2178
|
+
});
|
|
2179
|
+
imapClient
|
|
2180
|
+
.connect()
|
|
2181
|
+
.then(() => resolve(imapClient._mailboxList))
|
|
2182
|
+
.catch(reject);
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
response.imap = {
|
|
2186
|
+
success: !!imapClient.authenticated
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
if (accountData.mailboxes && mailboxes && mailboxes.length) {
|
|
2190
|
+
// format mailbox listing
|
|
2191
|
+
let mailboxList = [];
|
|
2192
|
+
for (let entry of mailboxes) {
|
|
2193
|
+
let mailbox = {};
|
|
2194
|
+
Object.keys(entry).forEach(key => {
|
|
2195
|
+
if (['path', 'specialUse', 'name', 'listed', 'subscribed', 'delimiter'].includes(key)) {
|
|
2196
|
+
mailbox[key] = entry[key];
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
if (mailbox.delimiter && mailbox.path.indexOf(mailbox.delimiter) >= 0) {
|
|
2200
|
+
mailbox.parentPath = mailbox.path.substr(0, mailbox.path.lastIndexOf(mailbox.delimiter));
|
|
2201
|
+
}
|
|
2202
|
+
mailboxList.push(mailbox);
|
|
2203
|
+
}
|
|
2204
|
+
response.mailboxes = mailboxList;
|
|
2205
|
+
}
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
response.imap = {
|
|
2208
|
+
success: false,
|
|
2209
|
+
error: err.message,
|
|
2210
|
+
code: err.code,
|
|
2211
|
+
statusCode: err.statusCode
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if (accountData.smtp) {
|
|
2217
|
+
try {
|
|
2218
|
+
let smtpClient = nodemailer.createTransport(Object.assign({}, accountData.smtp));
|
|
2219
|
+
response.smtp = {
|
|
2220
|
+
success: await smtpClient.verify()
|
|
2221
|
+
};
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
response.smtp = {
|
|
2224
|
+
success: false,
|
|
2225
|
+
error: err.message,
|
|
2226
|
+
code: err.code,
|
|
2227
|
+
statusCode: err.statusCode
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
return response;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
async function getStats(seconds) {
|
|
2236
|
+
const structuredMetrics = await call({ cmd: 'structuredMetrics' });
|
|
2237
|
+
|
|
2238
|
+
let counters = await getCounterValues(redis, seconds);
|
|
2239
|
+
|
|
2240
|
+
let stats = Object.assign(
|
|
2241
|
+
{
|
|
2242
|
+
version: packageData.version,
|
|
2243
|
+
license: packageData.license,
|
|
2244
|
+
accounts: await redis.scard('ia:accounts'),
|
|
2245
|
+
counters
|
|
2246
|
+
},
|
|
2247
|
+
structuredMetrics
|
|
2248
|
+
);
|
|
2249
|
+
|
|
2250
|
+
return stats;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
init()
|
|
2254
|
+
.then(() => {
|
|
2255
|
+
logger.debug({
|
|
2256
|
+
msg: 'Started API server thread',
|
|
2257
|
+
port: API_PORT,
|
|
2258
|
+
host: API_HOST,
|
|
2259
|
+
maxSize: MAX_ATTACHMENT_SIZE,
|
|
2260
|
+
version: packageData.version
|
|
2261
|
+
});
|
|
2262
|
+
})
|
|
2263
|
+
.catch(err => {
|
|
2264
|
+
logger.error(err);
|
|
2265
|
+
setImmediate(() => process.exit(3));
|
|
2266
|
+
});
|