Aliases.vue 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. <template>
  2. <div>
  3. <div class="flex flex-wrap flex-row items-center justify-between mb-8 md:px-2 lg:px-6">
  4. <div
  5. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-4 xl:mb-0"
  6. >
  7. <div class="p-4 flex items-center justify-between relative">
  8. <icon
  9. name="check-circle"
  10. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  11. />
  12. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  13. {{ totalActive }}
  14. <p class="text-grey-200 text-sm tracking-wide uppercase">
  15. Active
  16. </p>
  17. </div>
  18. </div>
  19. </div>
  20. <div
  21. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-4 xl:mb-0"
  22. >
  23. <div class="p-4 flex items-center justify-between relative">
  24. <icon
  25. name="cross-circle"
  26. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  27. />
  28. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  29. {{ totalInactive }}
  30. <p class="text-grey-200 text-sm tracking-wide uppercase">
  31. Inactive
  32. </p>
  33. </div>
  34. </div>
  35. </div>
  36. <div
  37. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-4 xl:mb-0"
  38. >
  39. <div class="p-4 flex items-center justify-between relative">
  40. <icon
  41. name="send"
  42. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  43. />
  44. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  45. {{ totalForwarded }}
  46. <p class="text-grey-200 text-sm tracking-wide uppercase">
  47. Emails Forwarded
  48. </p>
  49. </div>
  50. </div>
  51. </div>
  52. <div
  53. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-0"
  54. >
  55. <div class="p-4 flex items-center justify-between relative">
  56. <icon
  57. name="blocked"
  58. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  59. />
  60. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  61. {{ totalBlocked }}
  62. <p class="text-grey-200 text-sm tracking-wide uppercase">
  63. Emails Blocked
  64. </p>
  65. </div>
  66. </div>
  67. </div>
  68. <div
  69. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 md:mb-0"
  70. >
  71. <div class="p-4 flex items-center justify-between relative">
  72. <icon
  73. name="corner-up-left"
  74. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  75. />
  76. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  77. {{ totalReplies }}
  78. <p class="text-grey-200 text-sm tracking-wide uppercase">
  79. Email Replies
  80. </p>
  81. </div>
  82. </div>
  83. </div>
  84. <div
  85. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white"
  86. >
  87. <div class="p-4 flex items-center justify-between relative">
  88. <icon
  89. name="inbox"
  90. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  91. />
  92. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  93. {{ bandwidthMb }}<span class="text-sm tracking-wide uppercase">MB</span>
  94. <p class="text-grey-200 text-sm tracking-wide uppercase">Bandwidth ({{ month }})</p>
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
  100. <div class="relative">
  101. <input
  102. v-model="search"
  103. @keyup.esc="search = ''"
  104. tabindex="0"
  105. type="text"
  106. class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
  107. placeholder="Search Aliases"
  108. />
  109. <icon
  110. v-if="search"
  111. @click.native="search = ''"
  112. name="close-circle"
  113. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
  114. />
  115. <icon
  116. v-else
  117. name="search"
  118. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
  119. />
  120. </div>
  121. <div class="flex flex-wrap mt-4 md:mt-0">
  122. <div class="block relative mr-4">
  123. <select
  124. v-model="showAliases"
  125. class="block appearance-none w-full text-grey-700 bg-white p-3 pr-8 rounded shadow focus:shadow-outline"
  126. required
  127. >
  128. <option value="without">Hide Deleted</option>
  129. <option value="with">Show Deleted</option>
  130. <option value="only">Deleted Only</option>
  131. </select>
  132. <div
  133. class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
  134. >
  135. <svg
  136. class="fill-current h-4 w-4"
  137. xmlns="http://www.w3.org/2000/svg"
  138. viewBox="0 0 20 20"
  139. >
  140. <path
  141. d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
  142. />
  143. </svg>
  144. </div>
  145. </div>
  146. <div>
  147. <button
  148. @click="generateAliasModalOpen = true"
  149. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
  150. >
  151. Generate New Alias
  152. </button>
  153. </div>
  154. </div>
  155. </div>
  156. <vue-good-table
  157. v-if="initialAliases.length"
  158. @on-search="debounceToolips"
  159. @on-page-change="debounceToolips"
  160. :columns="columns"
  161. :rows="rows"
  162. :search-options="{
  163. enabled: true,
  164. skipDiacritics: true,
  165. externalQuery: search,
  166. }"
  167. :sort-options="{
  168. enabled: true,
  169. initialSortBy: { field: 'created_at', type: 'desc' },
  170. }"
  171. :pagination-options="{
  172. enabled: true,
  173. mode: 'pages',
  174. perPage: 25,
  175. perPageDropdown: [25, 50, 100],
  176. rowsPerPageLabel: 'Aliases per page',
  177. }"
  178. styleClass="vgt-table"
  179. >
  180. <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
  181. No aliases found for that search!
  182. </div>
  183. <template slot="table-row" slot-scope="props">
  184. <span
  185. v-if="props.column.field == 'created_at'"
  186. class="tooltip outline-none text-sm"
  187. :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
  188. >{{ props.row.created_at | timeAgo }}
  189. </span>
  190. <span v-else-if="props.column.field == 'email'" class="block">
  191. <span
  192. class="text-grey-400 tooltip cursor-pointer outline-none"
  193. data-tippy-content="Click to copy"
  194. v-clipboard="() => getAliasEmail(rows[props.row.originalIndex])"
  195. v-clipboard:success="clipboardSuccess"
  196. v-clipboard:error="clipboardError"
  197. ><span class="font-semibold text-indigo-800">{{
  198. getAliasLocalPart(props.row) | truncate(60)
  199. }}</span
  200. ><span v-if="getAliasLocalPart(props.row).length <= 60">{{
  201. ('@' + props.row.domain) | truncate(60 - getAliasLocalPart(props.row).length)
  202. }}</span>
  203. </span>
  204. <div v-if="aliasIdToEdit === props.row.id" class="flex items-center">
  205. <input
  206. @keyup.enter="editAlias(rows[props.row.originalIndex])"
  207. @keyup.esc="aliasIdToEdit = aliasDescriptionToEdit = ''"
  208. v-model="aliasDescriptionToEdit"
  209. type="text"
  210. class="flex-grow text-sm appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
  211. :class="aliasDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'"
  212. placeholder="Add description"
  213. tabindex="0"
  214. autofocus
  215. />
  216. <icon
  217. name="close"
  218. class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
  219. @click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
  220. />
  221. <icon
  222. name="save"
  223. class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
  224. @click.native="editAlias(rows[props.row.originalIndex])"
  225. />
  226. </div>
  227. <div v-else-if="props.row.description" class="flex items-center">
  228. <span class="inline-block text-grey-400 text-sm py-1 border border-transparent">
  229. {{ props.row.description | truncate(60) }}
  230. </span>
  231. <icon
  232. name="edit"
  233. class="inline-block w-6 h-6 ml-2 text-grey-200 fill-current cursor-pointer"
  234. @click.native="
  235. ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
  236. "
  237. />
  238. </div>
  239. <div v-else>
  240. <span
  241. class="inline-block text-grey-200 text-sm cursor-pointer py-1 border border-transparent"
  242. @click=";(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = '')"
  243. >Add description</span
  244. >
  245. </div>
  246. </span>
  247. <span
  248. v-else-if="props.column.field == 'recipients'"
  249. class="flex items-center justify-center"
  250. >
  251. <span
  252. v-if="props.row.recipients.length && props.row.id !== recipientsAliasToEdit.id"
  253. class="inline-block tooltip outline-none font-semibold text-indigo-800"
  254. :data-tippy-content="recipientsTooltip(props.row.recipients)"
  255. >
  256. {{ props.row.recipients.length }}
  257. </span>
  258. <span v-else-if="props.row.id === recipientsAliasToEdit.id">{{
  259. aliasRecipientsToEdit.length ? aliasRecipientsToEdit.length : '1'
  260. }}</span>
  261. <span
  262. v-else-if="has(props.row.aliasable, 'default_recipient.email')"
  263. class="py-1 px-2 text-xs bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
  264. :data-tippy-content="props.row.aliasable.default_recipient.email"
  265. >{{
  266. props.row.aliasable_type === 'App\\Models\\Domain' ? 'domain' : 'username'
  267. }}'s</span
  268. >
  269. <span
  270. v-else
  271. class="py-1 px-2 text-xs bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
  272. :data-tippy-content="defaultRecipient.email"
  273. >default</span
  274. >
  275. <icon
  276. name="edit"
  277. class="ml-2 inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  278. @click.native="openAliasRecipientsModal(props.row)"
  279. />
  280. </span>
  281. <span
  282. v-else-if="props.column.field == 'emails_forwarded'"
  283. class="font-semibold text-indigo-800"
  284. >
  285. {{ props.row.emails_forwarded }}
  286. </span>
  287. <span
  288. v-else-if="props.column.field == 'emails_blocked'"
  289. class="font-semibold text-indigo-800"
  290. >
  291. {{ props.row.emails_blocked }}
  292. </span>
  293. <span
  294. v-else-if="props.column.field == 'emails_replied'"
  295. class="font-semibold text-indigo-800"
  296. >
  297. {{ props.row.emails_replied }} <span class="text-grey-200">/</span>
  298. {{ props.row.emails_sent }}
  299. </span>
  300. <span v-else-if="props.column.field === 'active'" class="flex items-center">
  301. <Toggle
  302. v-model="rows[props.row.originalIndex].active"
  303. @on="activateAlias(props.row.id)"
  304. @off="deactivateAlias(props.row.id)"
  305. />
  306. </span>
  307. <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
  308. <icon
  309. v-if="props.row.deleted_at"
  310. name="undo"
  311. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer outline-none"
  312. @click.native="openRestoreModal(props.row.id)"
  313. />
  314. <icon
  315. v-else
  316. name="trash"
  317. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer outline-none"
  318. @click.native="openDeleteModal(props.row.id)"
  319. />
  320. </span>
  321. </template>
  322. </vue-good-table>
  323. <div v-else class="bg-white rounded shadow overflow-x-auto">
  324. <div class="p-8 text-center text-lg text-grey-700">
  325. <h1 class="mb-6 text-2xl text-indigo-800 font-semibold">
  326. It doesn't look like you have any aliases yet!
  327. </h1>
  328. <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
  329. <p class="mb-4">
  330. There are two ways to create new aliases.
  331. </p>
  332. <h3 class="mb-4 text-xl text-indigo-800 font-semibold">
  333. Option 1: Create aliases on the fly
  334. </h3>
  335. <p class="mb-4">
  336. To create aliases on the fly all you have to do is make up any new alias and give that out
  337. instead of your real email address.
  338. </p>
  339. <p class="mb-4">
  340. Let's say you're signing up to <b>example.com</b> you could enter
  341. <b>example@{{ subdomain }}</b> as your email address.
  342. </p>
  343. <p class="mb-4">
  344. The alias will show up here automatically as soon as it has forwarded its first email.
  345. </p>
  346. <p class="mb-4">
  347. If you start receiving spam to the alias you can simply deactivate it or delete it all
  348. together!
  349. </p>
  350. <p class="mb-4">
  351. Try it out now by sending an email to <b>first@{{ subdomain }}</b> and then refresh this
  352. page.
  353. </p>
  354. <h3 class="mb-4 text-xl text-indigo-800 font-semibold">
  355. Option 2: Generate a unique random alias
  356. </h3>
  357. <p class="mb-4">
  358. You can click the button above to generate a random alias that will look something like
  359. this:
  360. </p>
  361. <p class="mb-4">
  362. <b>86064c92-da41-443e-a2bf-5a7b0247842f@{{ domain }}</b>
  363. </p>
  364. <p>
  365. Useful if you do not wish to include your username in the email as a potential link
  366. between aliases.
  367. </p>
  368. </div>
  369. </div>
  370. <Modal :open="generateAliasModalOpen" @close="generateAliasModalOpen = false">
  371. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  372. <h2
  373. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  374. >
  375. Generate new alias
  376. </h2>
  377. <p class="mt-4 text-grey-700">
  378. Other aliases e.g. alias@{{ subdomain }} can also be created automatically when they
  379. receive their first email.
  380. </p>
  381. <label for="alias_domain" class="block text-grey-700 text-sm my-2">
  382. Alias Domain:
  383. </label>
  384. <div class="block relative w-full mb-4">
  385. <select
  386. v-model="generateAliasDomain"
  387. id="alias_domain"
  388. class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline"
  389. required
  390. >
  391. <option
  392. v-for="domainOption in domainOptions"
  393. :key="domainOption"
  394. :value="domainOption"
  395. >{{ domainOption }}</option
  396. >
  397. </select>
  398. <div
  399. class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
  400. >
  401. <svg
  402. class="fill-current h-4 w-4"
  403. xmlns="http://www.w3.org/2000/svg"
  404. viewBox="0 0 20 20"
  405. >
  406. <path
  407. d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
  408. />
  409. </svg>
  410. </div>
  411. </div>
  412. <label for="alias_domain" class="block text-grey-700 text-sm mt-4 mb-2">
  413. Alias Format:
  414. </label>
  415. <div class="block relative w-full mb-4">
  416. <select
  417. v-model="generateAliasFormat"
  418. id="alias_domain"
  419. class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline"
  420. required
  421. >
  422. <option
  423. v-for="formatOption in aliasFormatOptions"
  424. :key="formatOption.value"
  425. :value="formatOption.value"
  426. >{{ formatOption.label }}</option
  427. >
  428. </select>
  429. <div
  430. class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
  431. >
  432. <svg
  433. class="fill-current h-4 w-4"
  434. xmlns="http://www.w3.org/2000/svg"
  435. viewBox="0 0 20 20"
  436. >
  437. <path
  438. d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
  439. />
  440. </svg>
  441. </div>
  442. </div>
  443. <div v-if="generateAliasFormat === 'custom'">
  444. <label for="alias_local_part" class="block text-grey-700 text-sm my-2">
  445. Alias Local Part:
  446. </label>
  447. <p v-show="errors.generateAliasLocalPart" class="mb-3 text-red-500 text-sm">
  448. {{ errors.generateAliasLocalPart }}
  449. </p>
  450. <input
  451. v-model="generateAliasLocalPart"
  452. id="alias_local_part"
  453. type="text"
  454. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3"
  455. :class="errors.generateAliasLocalPart ? 'border-red-500' : ''"
  456. placeholder="Enter local part..."
  457. autofocus
  458. />
  459. </div>
  460. <label for="alias_description" class="block text-grey-700 text-sm my-2">
  461. Description:
  462. </label>
  463. <p v-show="errors.generateAliasDescription" class="mb-3 text-red-500 text-sm">
  464. {{ errors.generateAliasDescription }}
  465. </p>
  466. <input
  467. v-model="generateAliasDescription"
  468. id="alias_description"
  469. type="text"
  470. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3"
  471. :class="errors.generateAliasDescription ? 'border-red-500' : ''"
  472. placeholder="Enter description (optional)..."
  473. autofocus
  474. />
  475. <div class="mt-6">
  476. <button
  477. @click="generateNewAlias"
  478. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  479. :class="generateAliasLoading ? 'cursor-not-allowed' : ''"
  480. :disabled="generateAliasLoading"
  481. >
  482. Generate Alias
  483. <loader v-if="generateAliasLoading" />
  484. </button>
  485. <button
  486. @click="generateAliasModalOpen = false"
  487. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  488. >
  489. Cancel
  490. </button>
  491. </div>
  492. </div>
  493. </Modal>
  494. <Modal :open="editAliasRecipientsModalOpen" @close="closeAliasRecipientsModal">
  495. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  496. <h2
  497. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  498. >
  499. Update Alias Recipients
  500. </h2>
  501. <p class="my-4 text-grey-700">
  502. Select the recipients for this alias. You can choose multiple recipients. Leave it empty
  503. if you would like to use the default recipient.
  504. </p>
  505. <multiselect
  506. v-model="aliasRecipientsToEdit"
  507. :options="recipientOptions"
  508. :multiple="true"
  509. :close-on-select="true"
  510. :clear-on-select="false"
  511. :searchable="true"
  512. :max="10"
  513. placeholder="Select recipients"
  514. label="email"
  515. track-by="email"
  516. :preselect-first="false"
  517. :show-labels="false"
  518. >
  519. </multiselect>
  520. <div class="mt-6">
  521. <button
  522. type="button"
  523. @click="editAliasRecipients()"
  524. class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
  525. :class="editAliasRecipientsLoading ? 'cursor-not-allowed' : ''"
  526. :disabled="editAliasRecipientsLoading"
  527. >
  528. Update Recipients
  529. <loader v-if="editAliasRecipientsLoading" />
  530. </button>
  531. <button
  532. @click="closeAliasRecipientsModal()"
  533. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  534. >
  535. Cancel
  536. </button>
  537. </div>
  538. </div>
  539. </Modal>
  540. <Modal :open="restoreAliasModalOpen" @close="closeRestoreModal">
  541. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  542. <h2
  543. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  544. >
  545. Restore alias
  546. </h2>
  547. <p class="mt-4 text-grey-700">
  548. Are you sure you want to restore this alias? Once restored, this alias will
  549. <b>be able to receive emails again</b>.
  550. </p>
  551. <div class="mt-6">
  552. <button
  553. type="button"
  554. @click="restoreAlias(aliasIdToRestore)"
  555. class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
  556. :class="restoreAliasLoading ? 'cursor-not-allowed' : ''"
  557. :disabled="restoreAliasLoading"
  558. >
  559. Restore alias
  560. <loader v-if="restoreAliasLoading" />
  561. </button>
  562. <button
  563. @click="closeRestoreModal"
  564. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  565. >
  566. Cancel
  567. </button>
  568. </div>
  569. </div>
  570. </Modal>
  571. <Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
  572. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  573. <h2
  574. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  575. >
  576. Delete alias
  577. </h2>
  578. <p class="mt-4 text-grey-700">
  579. Are you sure you want to delete this alias? <b>You can restore this alias</b> if you later
  580. change your mind. Once deleted, this alias will <b>reject any emails sent to it</b>.
  581. </p>
  582. <div class="mt-6">
  583. <button
  584. type="button"
  585. @click="deleteAlias(aliasIdToDelete)"
  586. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  587. :class="deleteAliasLoading ? 'cursor-not-allowed' : ''"
  588. :disabled="deleteAliasLoading"
  589. >
  590. Delete alias
  591. <loader v-if="deleteAliasLoading" />
  592. </button>
  593. <button
  594. @click="closeDeleteModal"
  595. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  596. >
  597. Cancel
  598. </button>
  599. </div>
  600. </div>
  601. </Modal>
  602. </div>
  603. </template>
  604. <script>
  605. import Modal from './../components/Modal.vue'
  606. import Toggle from './../components/Toggle.vue'
  607. import tippy from 'tippy.js'
  608. import Multiselect from 'vue-multiselect'
  609. export default {
  610. props: {
  611. defaultRecipient: {
  612. type: Object,
  613. required: true,
  614. },
  615. initialAliases: {
  616. type: Array,
  617. required: true,
  618. },
  619. recipientOptions: {
  620. type: Array,
  621. required: true,
  622. },
  623. totalForwarded: {
  624. type: Number,
  625. required: true,
  626. },
  627. totalBlocked: {
  628. type: Number,
  629. required: true,
  630. },
  631. totalReplies: {
  632. type: Number,
  633. required: true,
  634. },
  635. domain: {
  636. type: String,
  637. required: true,
  638. },
  639. subdomain: {
  640. type: String,
  641. required: true,
  642. },
  643. bandwidthMb: {
  644. type: Number,
  645. required: true,
  646. },
  647. month: {
  648. type: String,
  649. required: true,
  650. },
  651. domainOptions: {
  652. type: Array,
  653. required: true,
  654. },
  655. defaultAliasDomain: {
  656. type: String,
  657. required: true,
  658. },
  659. defaultAliasFormat: {
  660. type: String,
  661. required: true,
  662. },
  663. },
  664. components: {
  665. Modal,
  666. Toggle,
  667. Multiselect,
  668. },
  669. mounted() {
  670. this.addTooltips()
  671. },
  672. data() {
  673. return {
  674. search: '',
  675. showAliases: 'without',
  676. aliasIdToEdit: '',
  677. aliasDescriptionToEdit: '',
  678. aliasIdToDelete: '',
  679. aliasIdToRestore: '',
  680. deleteAliasLoading: false,
  681. deleteAliasModalOpen: false,
  682. restoreAliasLoading: false,
  683. restoreAliasModalOpen: false,
  684. editAliasRecipientsLoading: false,
  685. editAliasRecipientsModalOpen: false,
  686. generateAliasModalOpen: false,
  687. generateAliasLoading: false,
  688. generateAliasDomain: this.defaultAliasDomain ? this.defaultAliasDomain : this.domain,
  689. generateAliasLocalPart: '',
  690. generateAliasDescription: '',
  691. generateAliasFormat: this.defaultAliasFormat ? this.defaultAliasFormat : 'uuid',
  692. aliasFormatOptions: [
  693. {
  694. value: 'uuid',
  695. label: 'UUID',
  696. },
  697. {
  698. value: 'random_words',
  699. label: 'Random Words',
  700. },
  701. {
  702. value: 'custom',
  703. label: 'Custom',
  704. },
  705. ],
  706. recipientsAliasToEdit: {},
  707. aliasRecipientsToEdit: [],
  708. columns: [
  709. {
  710. label: 'Created',
  711. field: 'created_at',
  712. globalSearchDisabled: true,
  713. },
  714. {
  715. label: 'Alias',
  716. field: 'email',
  717. },
  718. {
  719. label: 'Recipients',
  720. field: 'recipients',
  721. tdClass: 'text-center',
  722. sortable: true,
  723. sortFn: this.sortRecipients,
  724. globalSearchDisabled: true,
  725. },
  726. {
  727. label: 'Description',
  728. field: 'description',
  729. sortable: false,
  730. hidden: true,
  731. },
  732. {
  733. label: 'Forwarded',
  734. field: 'emails_forwarded',
  735. type: 'number',
  736. tdClass: 'text-center',
  737. globalSearchDisabled: true,
  738. },
  739. {
  740. label: 'Blocked',
  741. field: 'emails_blocked',
  742. type: 'number',
  743. tdClass: 'text-center',
  744. globalSearchDisabled: true,
  745. },
  746. {
  747. label: 'Replies/Sent',
  748. field: 'emails_replied',
  749. type: 'number',
  750. tdClass: 'text-center',
  751. globalSearchDisabled: true,
  752. },
  753. {
  754. label: 'Active',
  755. field: 'active',
  756. type: 'boolean',
  757. globalSearchDisabled: true,
  758. },
  759. {
  760. label: '',
  761. field: 'actions',
  762. sortable: false,
  763. globalSearchDisabled: true,
  764. },
  765. ],
  766. rows: this.initialAliases,
  767. errors: {},
  768. }
  769. },
  770. watch: {
  771. aliasIdToEdit: _.debounce(function() {
  772. this.addTooltips()
  773. }, 50),
  774. editAliasRecipientsModalOpen: _.debounce(function() {
  775. this.addTooltips()
  776. }, 50),
  777. showAliases(value) {
  778. this.updateAliases()
  779. },
  780. },
  781. computed: {
  782. activeUuidAliases() {
  783. return _.filter(this.rows, alias => alias.id === alias.local_part && alias.active)
  784. },
  785. totalActive() {
  786. return _.filter(this.rows, 'active').length
  787. },
  788. totalInactive() {
  789. return _.reject(this.rows, 'active').length
  790. },
  791. },
  792. methods: {
  793. addTooltips() {
  794. tippy('.tooltip', {
  795. arrow: true,
  796. arrowType: 'round',
  797. })
  798. },
  799. debounceToolips: _.debounce(function() {
  800. this.addTooltips()
  801. }, 50),
  802. recipientsTooltip(recipients) {
  803. return _.reduce(recipients, (list, recipient) => list + `${recipient.email}<br>`, '')
  804. },
  805. openDeleteModal(id) {
  806. this.deleteAliasModalOpen = true
  807. this.aliasIdToDelete = id
  808. },
  809. closeDeleteModal() {
  810. this.deleteAliasModalOpen = false
  811. this.aliasIdToDelete = ''
  812. },
  813. openRestoreModal(id) {
  814. this.restoreAliasModalOpen = true
  815. this.aliasIdToRestore = id
  816. },
  817. closeRestoreModal() {
  818. this.restoreAliasModalOpen = false
  819. this.aliasIdToRestore = ''
  820. },
  821. updateAliases() {
  822. axios
  823. .get(`/api/v1/aliases?deleted=${this.showAliases}`, {
  824. headers: { 'Content-Type': 'application/json' },
  825. })
  826. .then(response => {
  827. this.rows = response.data.data
  828. })
  829. .catch(error => {
  830. this.error()
  831. })
  832. },
  833. deleteAlias(id) {
  834. this.deleteAliasLoading = true
  835. axios
  836. .delete(`/api/v1/aliases/${id}`)
  837. .then(response => {
  838. this.rows = _.reject(this.rows, alias => alias.id === id)
  839. this.deleteAliasModalOpen = false
  840. this.deleteAliasLoading = false
  841. })
  842. .catch(error => {
  843. this.error()
  844. this.deleteAliasModalOpen = false
  845. this.deleteAliasLoading = false
  846. })
  847. },
  848. restoreAlias(id) {
  849. this.restoreAliasLoading = true
  850. axios
  851. .patch(`/api/v1/aliases/${id}/restore`, {
  852. headers: { 'Content-Type': 'application/json' },
  853. })
  854. .then(response => {
  855. this.updateAliases()
  856. this.restoreAliasModalOpen = false
  857. this.restoreAliasLoading = false
  858. this.success('Alias restored successfully')
  859. })
  860. .catch(error => {
  861. this.error()
  862. this.restoreAliasModalOpen = false
  863. this.restoreAliasLoading = false
  864. })
  865. },
  866. openAliasRecipientsModal(alias) {
  867. this.editAliasRecipientsModalOpen = true
  868. this.recipientsAliasToEdit = alias
  869. this.aliasRecipientsToEdit = alias.recipients
  870. },
  871. closeAliasRecipientsModal() {
  872. this.editAliasRecipientsModalOpen = false
  873. this.recipientsAliasToEdit = {}
  874. this.aliasRecipientsToEdit = []
  875. },
  876. editAliasRecipients() {
  877. this.editAliasRecipientsLoading = true
  878. axios
  879. .post(
  880. '/api/v1/alias-recipients',
  881. JSON.stringify({
  882. alias_id: this.recipientsAliasToEdit.id,
  883. recipient_ids: _.map(this.aliasRecipientsToEdit, recipient => recipient.id),
  884. }),
  885. {
  886. headers: { 'Content-Type': 'application/json' },
  887. }
  888. )
  889. .then(response => {
  890. let alias = _.find(this.rows, ['id', this.recipientsAliasToEdit.id])
  891. alias.recipients = this.aliasRecipientsToEdit
  892. this.editAliasRecipientsModalOpen = false
  893. this.editAliasRecipientsLoading = false
  894. this.recipientsAliasToEdit = {}
  895. this.aliasRecipientsToEdit = []
  896. this.success('Alias recipients updated')
  897. })
  898. .catch(error => {
  899. this.editAliasRecipientsModalOpen = false
  900. this.editAliasRecipientsLoading = false
  901. this.recipientsAliasToEdit = {}
  902. this.aliasRecipientsToEdit = []
  903. this.error()
  904. })
  905. },
  906. generateNewAlias() {
  907. this.errors = {}
  908. // Validate alias local part
  909. if (
  910. this.generateAliasFormat === 'custom' &&
  911. !this.validLocalPart(this.generateAliasLocalPart)
  912. ) {
  913. return (this.errors.generateAliasLocalPart = 'Valid local part required')
  914. }
  915. if (this.generateAliasDescription.length > 100) {
  916. return (this.errors.generateAliasDescription = 'Description cannot exceed 100 characters')
  917. }
  918. this.generateAliasLoading = true
  919. axios
  920. .post(
  921. '/api/v1/aliases',
  922. JSON.stringify({
  923. domain: this.generateAliasDomain,
  924. local_part: this.generateAliasLocalPart,
  925. description: this.generateAliasDescription,
  926. format: this.generateAliasFormat,
  927. }),
  928. {
  929. headers: { 'Content-Type': 'application/json' },
  930. }
  931. )
  932. .then(({ data }) => {
  933. this.generateAliasLoading = false
  934. this.generateAliasLocalPart = ''
  935. this.generateAliasDescription = ''
  936. this.rows.push(data.data)
  937. this.generateAliasModalOpen = false
  938. this.success('New alias generated successfully')
  939. })
  940. .catch(error => {
  941. this.generateAliasLoading = false
  942. if (error.response.status === 429) {
  943. this.error('You have reached your hourly limit for creating new aliases')
  944. } else {
  945. this.error()
  946. }
  947. })
  948. },
  949. editAlias(alias) {
  950. if (this.aliasDescriptionToEdit.length > 100) {
  951. return this.error('Description cannot be more than 100 characters')
  952. }
  953. axios
  954. .patch(
  955. `/api/v1/aliases/${alias.id}`,
  956. JSON.stringify({
  957. description: this.aliasDescriptionToEdit,
  958. }),
  959. {
  960. headers: { 'Content-Type': 'application/json' },
  961. }
  962. )
  963. .then(response => {
  964. alias.description = this.aliasDescriptionToEdit
  965. this.aliasIdToEdit = ''
  966. this.aliasDescriptionToEdit = ''
  967. this.success('Alias description updated')
  968. })
  969. .catch(error => {
  970. this.aliasIdToEdit = ''
  971. this.aliasDescriptionToEdit = ''
  972. this.error()
  973. })
  974. },
  975. activateAlias(id) {
  976. axios
  977. .post(
  978. `/api/v1/active-aliases`,
  979. JSON.stringify({
  980. id: id,
  981. }),
  982. {
  983. headers: { 'Content-Type': 'application/json' },
  984. }
  985. )
  986. .then(response => {
  987. //
  988. })
  989. .catch(error => {
  990. this.error()
  991. })
  992. },
  993. deactivateAlias(id) {
  994. axios
  995. .delete(`/api/v1/active-aliases/${id}`)
  996. .then(response => {
  997. //
  998. })
  999. .catch(error => {
  1000. this.error()
  1001. })
  1002. },
  1003. getAliasEmail(alias) {
  1004. return alias.extension
  1005. ? `${alias.local_part}+${alias.extension}@${alias.domain}`
  1006. : alias.email
  1007. },
  1008. getAliasLocalPart(alias) {
  1009. return alias.extension ? `${alias.local_part}+${alias.extension}` : alias.local_part
  1010. },
  1011. sortRecipients(x, y) {
  1012. return x.length < y.length ? -1 : x.length > y.length ? 1 : 0
  1013. },
  1014. has(object, path) {
  1015. return _.has(object, path)
  1016. },
  1017. validLocalPart(part) {
  1018. let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))$/
  1019. return re.test(part)
  1020. },
  1021. clipboardSuccess() {
  1022. this.success('Copied to clipboard')
  1023. },
  1024. clipboardError() {
  1025. this.error('Could not copy to clipboard')
  1026. },
  1027. success(text = '') {
  1028. this.$notify({
  1029. title: 'Success',
  1030. text: text,
  1031. type: 'success',
  1032. })
  1033. },
  1034. error(text = 'An error has occurred, please try again later') {
  1035. this.$notify({
  1036. title: 'Error',
  1037. text: text,
  1038. type: 'error',
  1039. })
  1040. },
  1041. },
  1042. }
  1043. </script>
  1044. <style src="vue-multiselect/dist/vue-multiselect.min.css"></style>