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