index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. /*
  2. * MIT License
  3. *
  4. * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
  5. *
  6. * This file is part of gostfix project https://git.semlanik.org/semlanik/gostfix
  7. *
  8. * Permission is hereby granted, free of charge, to any person obtaining a copy of this
  9. * software and associated documentation files (the "Software"), to deal in the Software
  10. * without restriction, including without limitation the rights to use, copy, modify,
  11. * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
  12. * to permit persons to whom the Software is furnished to do so, subject to the following
  13. * conditions:
  14. *
  15. * The above copyright notice and this permission notice shall be included in all copies
  16. * or substantial portions of the Software.
  17. *
  18. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  19. * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  20. * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  21. * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  22. * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  23. * DEALINGS IN THE SOFTWARE.
  24. */
  25. var currentFolder = ""
  26. var currentPage = 0
  27. var currentMail = ""
  28. var mailbox = ""
  29. var pageMax = 10
  30. const mailboxRegex = /^(\/m\d+)/g
  31. const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  32. const emailEndRegex = /[;,\s]/g
  33. var folders = new Array()
  34. var notifierSocket = null
  35. var toEmailList = new Array()
  36. var toEmailIndex = 0
  37. var toEmailPreviousSelectionPosition = 0
  38. $(window).click(function(e){
  39. var target = $(e.target)
  40. var isDropDown = false
  41. for (var i = 0; i < target.parents().length; i++) {
  42. isDropDown = target.parents()[i].classList.contains("dropbtn")
  43. if (isDropDown) {
  44. break
  45. }
  46. }
  47. if (!e.target.matches('.dropbtn') && !isDropDown) {
  48. $(".dropdown-content").hide()
  49. }
  50. })
  51. $(document).ready(function(){
  52. $.ajaxSetup({
  53. global: false,
  54. type: "POST"
  55. })
  56. urlPaths = mailboxRegex.exec($(location).attr('pathname'))
  57. if (urlPaths != null && urlPaths.length === 2) {
  58. mailbox = urlPaths[0]
  59. } else {
  60. mailbox = ""
  61. }
  62. $(window).bind('hashchange', onHashChanged)
  63. onHashChanged()
  64. loadFolders()
  65. loadStatusLine()
  66. $("#mailNewButton").click(mailNew)
  67. connectNotifier()
  68. $("#toEmailField").on("input", toEmailFieldChanged)
  69. $("#toEmailField").keydown(function(e){
  70. var actualText = $("#toEmailField").val()
  71. const selectionPosition = e.target.selectionStart
  72. switch(e.keyCode) {
  73. case 8:
  74. if (toEmailPreviousSelectionPosition == 0 && e.target.selectionStart == 0
  75. && toEmailList.length > 0 && $("#toEmailList").children().length > 1) {
  76. removeToEmail($("#toEmailList").children()[$("#toEmailList").children().length - 2].id, toEmailList[toEmailList.length - 1])
  77. }
  78. break
  79. case 13:
  80. addToEmail(actualText.slice(0, selectionPosition))
  81. $("#toEmailField").val(actualText.slice(selectionPosition + 1, actualText.length))
  82. break
  83. }
  84. toEmailPreviousSelectionPosition = e.target.selectionStart
  85. })
  86. })
  87. function toEmailFieldChanged(e) {
  88. const selectionPosition = e.target.selectionStart - 1
  89. var actualText = $("#toEmailField").val()
  90. if (actualText.length <= 0 || selectionPosition < 0) {
  91. return
  92. }
  93. var lastChar = actualText[selectionPosition]
  94. if (lastChar.match(emailEndRegex)) {
  95. addToEmail(actualText.slice(0, selectionPosition))
  96. $("#toEmailField").val(actualText.slice(selectionPosition + 1, actualText.length))
  97. }
  98. }
  99. function addToEmail(toEmail) {
  100. if (toEmail.length <= 0) {
  101. return
  102. }
  103. var style = toEmail.match(emailRegex) ? "valid" : "invalid"
  104. $("<div class=\""+ style + " toEmail\" id=\"toEmail" + toEmailIndex + "\">" + toEmail + "<img class=\"iconBtn\" style=\"height: 12px; margin-left:10px; margin: auto;\" onclick=\"removeToEmail('toEmail" + toEmailIndex + "', '" + toEmail + "');\" src=\"/assets/cross.svg\"/></div>").insertBefore("#toEmailField")
  105. toEmailIndex++
  106. toEmailList.push(toEmail)
  107. }
  108. function removeToEmail(id, email) {
  109. const index = toEmailList.indexOf(email)
  110. if (index >= 0) {
  111. toEmailList.splice(index, 1)
  112. }
  113. $("#" + id).remove()
  114. }
  115. function mailNew(e) {
  116. window.location.hash = currentFolder + currentPage + "/mailNew"
  117. }
  118. function mailOpen(id) {
  119. window.location.hash = currentFolder + currentPage + "/" + id
  120. }
  121. function openFolder(folder) {
  122. window.location.hash = folder
  123. }
  124. function onHashChanged() {
  125. var hashLocation = window.location.hash
  126. if (hashLocation == "") {
  127. setDetailsVisible(false)
  128. openFolder("Inbox")
  129. return
  130. }
  131. hashRegex = /^#([a-zA-Z]+)(\d*)\/?([A-Fa-f\d]*)/g
  132. hashParts = hashRegex.exec(hashLocation)
  133. page = 0
  134. if (hashParts.length >= 3 && hashParts[2] != "") {
  135. page = parseInt(hashParts[2])
  136. if (typeof page != "number" || page > pageMax || page < 0) {
  137. page = 0
  138. }
  139. }
  140. if (hashParts.length >= 2 && (hashParts[1] != currentFolder || currentPage != page) && hashParts[1] != "") {
  141. updateMailList(hashParts[1], page)
  142. }
  143. if (hashParts.length >= 4 && hashParts[3] != "" && hashParts[3] != "/mailNew") {
  144. if (currentMail != hashParts[3]) {
  145. requestMail(hashParts[3])
  146. }
  147. } else {
  148. setDetailsVisible(false)
  149. }
  150. hashParts = hashLocation.split("/")
  151. if (hashParts.length == 2 && hashParts[1] == "mailNew") {
  152. setMailNewVisible(true)
  153. } else {
  154. setMailNewVisible(false)
  155. }
  156. }
  157. function requestMail(mailId) {
  158. if (mailId != "") {
  159. $.ajax({
  160. url: "/mail",
  161. data: {
  162. mailId: mailId
  163. },
  164. success: function(result) {
  165. currentMail = mailId
  166. $("#mail"+mailId).removeClass("unread")
  167. $("#mail"+mailId).addClass("read")
  168. $("#mailDetails").html(result);
  169. setDetailsVisible(true);
  170. folderStat(currentFolder);//TODO: receive statistic from websocket
  171. },
  172. error: function(jqXHR, textStatus, errorThrown) {
  173. $("#mailDetails").html(textStatus)
  174. setDetailsVisible(true)
  175. }
  176. })
  177. }
  178. }
  179. function loadFolders() {
  180. if (mailbox == "") {
  181. $("#folders").html("Unable to load folder list")
  182. return
  183. }
  184. $.ajax({
  185. url: mailbox + "/folders",
  186. success: function(result) {
  187. folderList = jQuery.parseJSON(result)
  188. for(var i = 0; i < folderList.folders.length; i++) {
  189. folders.push(folderList.folders[i].name)
  190. folderStat(folderList.folders[i].name)
  191. }
  192. $("#folders").html(folderList.html)
  193. },
  194. error: function(jqXHR, textStatus, errorThrown) {
  195. //TODO: some toast message here once implemented
  196. }
  197. })
  198. }
  199. function folderStat(folder) {
  200. $.ajax({
  201. url: mailbox + "/folderStat",
  202. data: {
  203. folder: folder
  204. },
  205. success: function(result) {
  206. var stats = jQuery.parseJSON(result)
  207. if (stats.unread > 0) {
  208. $("#folderStats"+folder).text(stats.unread)
  209. $("#folder"+folder).addClass("unread")
  210. } else {
  211. $("#folder"+folder).removeClass("unread")
  212. $("#folderStats"+folder).text("")
  213. }
  214. },
  215. error: function(jqXHR, textStatus, errorThrown) {
  216. //TODO: some toast message here once implemented
  217. }
  218. })
  219. }
  220. function closeDetails() {
  221. window.location.hash = currentFolder + currentPage
  222. }
  223. function closeMailNew() {
  224. window.location.hash = currentFolder + currentPage
  225. }
  226. function loadStatusLine() {
  227. $.ajax({
  228. url: mailbox + "/statusLine",
  229. success: function(result) {
  230. $("#statusLine").html(result)
  231. },
  232. error: function(jqXHR, textStatus, errorThrown) {
  233. //TODO: some toast message here once implemented
  234. }
  235. })
  236. }
  237. function localDate(elementToChange, timestamp) {
  238. var today = new Date()
  239. var date = new Date(timestamp*1000)
  240. dateString = ""
  241. if (today.getDay() == date.getDay()
  242. && today.getMonth() == date.getMonth()
  243. && today.getFullYear() == date.getFullYear()) {
  244. dateString = date.toLocaleTimeString("en-US")
  245. } else if (today.getFullYear() == date.getFullYear()) {
  246. const options = { day: 'numeric', month: 'short' }
  247. dateString = date.toLocaleDateString("en-US", options)
  248. } else {
  249. dateString = date.toLocaleDateString("en-US")
  250. }
  251. $("#"+elementToChange).text(dateString)
  252. }
  253. function setRead(mailId, read) {
  254. $.ajax({
  255. url: "/setRead",
  256. data: {mailId: mailId,
  257. read: read},
  258. success: function(result) {
  259. if (read) {
  260. if ($("#readIcon"+mailId)) {
  261. $("#readIcon"+mailId).attr("src", "/assets/read.svg")
  262. }
  263. if ($("#readListIcon"+mailId)) {
  264. $("#readListIcon"+mailId).attr("src", "/assets/read.svg")
  265. }
  266. $("#mail"+mailId).removeClass("unread")
  267. $("#mail"+mailId).addClass("read")
  268. } else {
  269. if ($("#readIcon"+mailId)) {
  270. $("#readIcon"+mailId).attr("src", "/assets/unread.svg")
  271. }
  272. if ($("#readListIcon"+mailId)) {
  273. $("#readListIcon"+mailId).attr("src", "/assets/unread.svg")
  274. }
  275. $("#mail"+mailId).removeClass("read")
  276. $("#mail"+mailId).addClass("unread")
  277. }
  278. folderStat(currentFolder);//TODO: receive statistic from websocket
  279. },
  280. error: function(jqXHR, textStatus, errorThrown) {
  281. }
  282. })
  283. }
  284. function toggleRead(mailId, iconId) {
  285. if ($("#"+iconId+mailId)) {
  286. setRead(mailId, $("#"+iconId+mailId).attr("src") == "/assets/unread.svg")
  287. }
  288. }
  289. function removeMail(mailId, callback) {
  290. var url = currentFolder != "Trash" ? "/remove" : "/delete"
  291. $.ajax({
  292. url: url,
  293. data: {mailId: mailId},
  294. success: function(result) {
  295. $("#mail"+mailId).remove();
  296. if (callback) {
  297. callback();
  298. }
  299. folderStat(currentFolder);//TODO: receive statistic from websocket
  300. folderStat("Trash");//TODO: receive statistic from websocket
  301. },
  302. error: function(jqXHR, textStatus, errorThrown) {
  303. }
  304. })
  305. }
  306. function restoreMail(mailId, callback) {
  307. var url = "/restore"
  308. $.ajax({
  309. url: url,
  310. data: {mailId: mailId},
  311. success: function(result) {
  312. if (currentFolder == "Trash") {
  313. $("#mail"+mailId).remove();
  314. }
  315. if (callback) {
  316. callback();
  317. }
  318. for (var i = 0; i < folders.length; i++) {
  319. folderStat(folders[i])
  320. }
  321. },
  322. error: function(jqXHR, textStatus, errorThrown) {
  323. }
  324. })
  325. }
  326. function setDetailsVisible(visible) {
  327. if (visible) {
  328. $("#mailDetails").show()
  329. $("#mailList").css({pointerEvents: "none"})
  330. } else {
  331. currentMail = ""
  332. $("#mailDetails").hide()
  333. $("#mailDetails").html("")
  334. $("#mailList").css({pointerEvents: "auto"})
  335. }
  336. }
  337. function setMailNewVisible(visible) {
  338. if (visible) {
  339. $("#mailNew").show()
  340. $("#mailList").css({pointerEvents: "none"})
  341. } else {
  342. currentMail = ""
  343. $("#mailNew").hide()
  344. $("#mailList").css({pointerEvents: "auto"})
  345. }
  346. }
  347. function updateMailList(folder, page) {
  348. if (mailbox == "" || folder == "") {
  349. if ($("#mailList")) {
  350. $("#mailList").html("Unable to load message list")
  351. }
  352. return
  353. }
  354. $.ajax({
  355. url: mailbox + "/mailList",
  356. data: {
  357. folder: folder,
  358. page: page
  359. },
  360. success: function(result) {
  361. var data = jQuery.parseJSON(result)
  362. pageMax = Math.floor(data.total/50)
  363. if ($("#mailList")) {
  364. $("#mailList").html(data.html)
  365. }
  366. currentFolder = folder
  367. currentPage = page
  368. if($("#currentPageIndex")) {
  369. $("#currentPageIndex").text(currentPage + 1)
  370. }
  371. if($("#totalPageCount")) {
  372. $("#totalPageCount").text(pageMax + 1)
  373. }
  374. },
  375. error: function(jqXHR, textStatus, errorThrown) {
  376. if ($("#mailList")) {
  377. $("#mailList").html("Unable to load message list")
  378. }
  379. }
  380. })
  381. }
  382. function nextPage() {
  383. var newPage = currentPage < (pageMax - 1) ? currentPage + 1 : pageMax
  384. window.location.hash = currentFolder + newPage
  385. }
  386. function prevPage() {
  387. var newPage = currentPage > 0 ? currentPage - 1 : 0
  388. window.location.hash = currentFolder + newPage
  389. }
  390. function toggleDropDown(dd) {
  391. $("#"+dd).toggle()
  392. }
  393. function sendNewMail() {
  394. var composedEmailString = toEmailList[0]
  395. for(var i = 1; i < toEmailList.length; i++) {
  396. composedEmailString += "," + toEmailList[i]
  397. }
  398. $("#newMailTo").val(composedEmailString)
  399. var formValue = $("#mailNewForm").serialize()
  400. $.ajax({
  401. url: mailbox + "/sendNewMail",
  402. data: formValue,
  403. success: function(result) {
  404. $("#newMailEditor").val("")
  405. $("#newMailSubject").val("")
  406. $("#newMailTo").val("")
  407. closeMailNew()
  408. },
  409. error: function(jqXHR, textStatus, errorThrown) {
  410. //TODO: some toast message here once implemented
  411. }
  412. })
  413. }
  414. function logout() {
  415. window.location.href = "/logout"
  416. }
  417. function connectNotifier() {
  418. if (notifierSocket != null) {
  419. return
  420. }
  421. var protocol = "wss://"
  422. if(window.location.protocol !== "https:") {
  423. protocol = "ws://"
  424. }
  425. notifierSocket = new WebSocket(protocol + window.location.host + mailbox + "/notifierSubscribe")
  426. notifierSocket.onopen = function() {
  427. };
  428. notifierSocket.onmessage = function (e) {
  429. for (var i = 0; i < folders.length; i++) {
  430. folderStat(folders[i])
  431. }
  432. updateMailList(currentFolder, currentPage)
  433. }
  434. notifierSocket.onclose = function () {
  435. }
  436. }
  437. window.onbeforeunload = function() {
  438. if (notifierSocket != null) {
  439. notifierSocket.onclose = function () {}; // disable onclose handler first
  440. notifierSocket.close();
  441. }
  442. };