AdminMail.vue 15.9 KB
Newer Older
1 2 3 4
<template lang='pug'>
q-page.admin-mail
  .row.q-pa-md.items-center
    .col-auto
5
      img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-message-settings-animated.svg')
6
    .col.q-pl-md
7 8
      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.mail.title') }}
      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.mail.subtitle') }}
9 10 11 12 13
    .col-auto
      q-btn.q-mr-sm.acrylic-btn(
        icon='las la-question-circle'
        flat
        color='grey'
14
        :aria-label='t(`common.actions.viewDocs`)'
15
        :href='siteStore.docsBase + `/system/mail`'
16 17 18
        target='_blank'
        type='a'
        )
19
        q-tooltip {{ t(`common.actions.viewDocs`) }}
20 21 22 23
      q-btn.q-mr-sm.acrylic-btn(
        icon='las la-redo-alt'
        flat
        color='secondary'
24
        :loading='state.loading > 0'
25
        :aria-label='t(`common.actions.refresh`)'
26 27
        @click='load'
        )
28
        q-tooltip {{ t(`common.actions.refresh`) }}
29 30
      q-btn(
        unelevated
31
        icon='mdi-check'
32
        :label='t(`common.actions.apply`)'
33 34
        color='secondary'
        @click='save'
35
        :disabled='state.loading > 0'
36 37 38 39 40 41 42
      )
  q-separator(inset)
  .row.q-pa-md.q-col-gutter-md
    .col-12.col-lg-7
      //- -----------------------
      //- Configuration
      //- -----------------------
43
      q-card.q-pb-sm
44
        q-card-section
45
          .text-subtitle1 {{t('admin.mail.configuration')}}
46 47 48
        q-item
          blueprint-icon(icon='contact')
          q-item-section
49 50
            q-item-label {{t(`admin.mail.senderName`)}}
            q-item-label(caption) {{t(`admin.general.senderNameHint`)}}
51 52 53
          q-item-section
            q-input(
              outlined
54
              v-model='state.config.senderName'
55 56
              dense
              hide-bottom-space
57
              :aria-label='t(`admin.mail.senderName`)'
58 59 60 61 62
              )
        q-separator.q-my-sm(inset)
        q-item
          blueprint-icon(icon='envelope')
          q-item-section
63 64
            q-item-label {{t(`admin.mail.senderEmail`)}}
            q-item-label(caption) {{t(`admin.general.senderEmailHint`)}}
65 66 67
          q-item-section
            q-input(
              outlined
68
              v-model='state.config.senderEmail'
69
              dense
70
              :aria-label='t(`admin.mail.senderEmail`)'
71
              )
72 73 74 75 76 77 78 79 80 81 82 83 84
        q-separator.q-my-sm(inset)
        q-item
          blueprint-icon(icon='dns')
          q-item-section
            q-item-label {{t(`admin.mail.defaultBaseURL`)}}
            q-item-label(caption) {{t(`admin.general.defaultBaseURLHint`)}}
          q-item-section
            q-input(
              outlined
              v-model='state.config.defaultBaseURL'
              dense
              :aria-label='t(`admin.mail.defaultBaseURL`)'
              )
85 86 87
      //- -----------------------
      //- SMTP
      //- -----------------------
88
      q-card.q-pb-sm.q-mt-md
89
        q-card-section
90
          .text-subtitle1 {{t('admin.mail.smtp')}}
91 92 93
        q-item
          blueprint-icon(icon='dns')
          q-item-section
94 95
            q-item-label {{t(`admin.mail.smtpHost`)}}
            q-item-label(caption) {{t(`admin.mail.smtpHostHint`)}}
96 97 98
          q-item-section
            q-input(
              outlined
99
              v-model='state.config.host'
100 101
              dense
              hide-bottom-space
102
              :aria-label='t(`admin.mail.smtpHost`)'
103 104 105 106 107
              )
        q-separator.q-my-sm(inset)
        q-item
          blueprint-icon(icon='ethernet-off')
          q-item-section
108 109
            q-item-label {{t(`admin.mail.smtpPort`)}}
            q-item-label(caption) {{t(`admin.mail.smtpPortHint`)}}
110 111 112
          q-item-section(style='flex: 0 0 120px;')
            q-input(
              outlined
113
              v-model='state.config.port'
114
              dense
115
              :aria-label='t(`admin.mail.smtpPort`)'
116 117
              )
        q-separator.q-my-sm(inset)
118
        q-item(tag='label')
119 120
          blueprint-icon(icon='secure')
          q-item-section
121 122
            q-item-label {{t(`admin.mail.smtpTLS`)}}
            q-item-label(caption) {{t(`admin.mail.smtpTLSHint`)}}
123 124
          q-item-section(avatar)
            q-toggle(
125
              v-model='state.config.secure'
126 127 128
              color='primary'
              checked-icon='las la-check'
              unchecked-icon='las la-times'
129
              :aria-label='t(`admin.mail.smtpTLS`)'
130 131
              )
        q-separator.q-my-sm(inset)
132
        q-item(tag='label')
133 134
          blueprint-icon(icon='security-ssl')
          q-item-section
135 136
            q-item-label {{t(`admin.mail.smtpVerifySSL`)}}
            q-item-label(caption) {{t(`admin.mail.smtpVerifySSLHint`)}}
137 138
          q-item-section(avatar)
            q-toggle(
139
              v-model='state.config.verifySSL'
140 141 142
              color='primary'
              checked-icon='las la-check'
              unchecked-icon='las la-times'
143
              :aria-label='t(`admin.mail.smtpVerifySSL`)'
144 145 146 147 148
              )
        q-separator.q-my-sm(inset)
        q-item
          blueprint-icon(icon='test-account')
          q-item-section
149 150
            q-item-label {{t(`admin.mail.smtpUser`)}}
            q-item-label(caption) {{t(`admin.mail.smtpUserHint`)}}
151 152 153
          q-item-section
            q-input(
              outlined
154
              v-model='state.config.user'
155
              dense
156
              :aria-label='t(`admin.mail.smtpUser`)'
157 158 159 160 161
              )
        q-separator.q-my-sm(inset)
        q-item
          blueprint-icon(icon='password')
          q-item-section
162 163
            q-item-label {{t(`admin.mail.smtpPwd`)}}
            q-item-label(caption) {{t(`admin.mail.smtpPwdHint`)}}
164 165 166
          q-item-section
            q-input(
              outlined
167
              v-model='state.config.pass'
168
              dense
169
              :aria-label='t(`admin.mail.smtpPwd`)'
170
              )
171 172 173 174 175 176 177 178 179 180 181 182 183 184
        q-separator.q-my-sm(inset)
        q-item
          blueprint-icon(icon='server')
          q-item-section
            q-item-label {{t(`admin.mail.smtpName`)}}
            q-item-label(caption) {{t(`admin.mail.smtpNameHint`)}}
          q-item-section
            q-input(
              outlined
              v-model='state.config.name'
              dense
              hide-bottom-space
              :aria-label='t(`admin.mail.smtpName`)'
              )
185 186 187
      //- -----------------------
      //- DKIM
      //- -----------------------
188
      q-card.q-pb-sm.q-mt-md
189
        q-card-section
190
          .text-subtitle1 {{t('admin.mail.dkim')}}
191 192 193 194 195 196
        q-item.q-pt-none
          q-item-section
            q-card.bg-info.text-white.rounded-borders(flat)
              q-card-section.items-center(horizontal)
                q-card-section.col-auto.q-pr-none
                  q-icon(name='las la-info-circle', size='sm')
197
                q-card-section.text-caption {{ t('admin.mail.dkimHint') }}
198
        q-item(tag='label')
199 200
          blueprint-icon(icon='received')
          q-item-section
201 202
            q-item-label {{t(`admin.mail.dkimUse`)}}
            q-item-label(caption) {{t(`admin.mail.dkimUseHint`)}}
203 204
          q-item-section(avatar)
            q-toggle(
205
              v-model='state.config.useDKIM'
206 207 208
              color='primary'
              checked-icon='las la-check'
              unchecked-icon='las la-times'
209
              :aria-label='t(`admin.mail.dkimUse`)'
210
              )
211
        template(v-if='state.config.useDKIM')
212 213 214 215
          q-separator.q-my-sm(inset)
          q-item
            blueprint-icon(icon='dns')
            q-item-section
216 217
              q-item-label {{t(`admin.mail.dkimDomainName`)}}
              q-item-label(caption) {{t(`admin.mail.dkimDomainNameHint`)}}
218 219 220
            q-item-section
              q-input(
                outlined
221
                v-model='state.config.dkimDomainName'
222
                dense
223
                :aria-label='t(`admin.mail.dkimDomainName`)'
224 225 226 227 228
                )
          q-separator.q-my-sm(inset)
          q-item
            blueprint-icon(icon='access')
            q-item-section
229 230
              q-item-label {{t(`admin.mail.dkimKeySelector`)}}
              q-item-label(caption) {{t(`admin.mail.dkimKeySelectorHint`)}}
231 232 233
            q-item-section
              q-input(
                outlined
234
                v-model='state.config.dkimKeySelector'
235
                dense
236
                :aria-label='t(`admin.mail.dkimKeySelector`)'
237 238 239 240 241
                )
          q-separator.q-my-sm(inset)
          q-item
            blueprint-icon(icon='grand-master-key')
            q-item-section
242 243
              q-item-label {{t(`admin.mail.dkimPrivateKey`)}}
              q-item-label(caption) {{t(`admin.mail.dkimPrivateKeyHint`)}}
244 245 246
            q-item-section
              q-input(
                outlined
247
                v-model='state.config.dkimPrivateKey'
248
                dense
249
                :aria-label='t(`admin.mail.dkimPrivateKey`)'
250 251 252 253 254 255 256
                type='textarea'
                )

    .col-12.col-lg-5
      //- -----------------------
      //- MAIL TEMPLATES
      //- -----------------------
257
      q-card.q-pb-sm.q-mb-md(v-if='flagStore.experimental')
258
        q-card-section
259
          .text-subtitle1 {{t('admin.mail.templates')}}
260 261 262 263
        q-list
          q-item
            blueprint-icon(icon='resume-template')
            q-item-section
264
              q-item-label {{t(`admin.mail.templateWelcome`)}}
265 266 267 268 269 270
            q-item-section(side)
              q-btn(
                outline
                no-caps
                icon='las la-edit'
                color='primary'
271
                @click='editTemplate(`welcome`)'
272
                :label='t(`common.actions.edit`)'
273 274 275 276 277
              )
          q-separator(inset)
          q-item
            blueprint-icon(icon='resume-template')
            q-item-section
278
              q-item-label {{t(`admin.mail.templateResetPwd`)}}
279 280 281 282 283 284
            q-item-section(side)
              q-btn(
                outline
                no-caps
                icon='las la-edit'
                color='primary'
285
                @click='editTemplate(`pwdreset`)'
286
                :label='t(`common.actions.edit`)'
287 288 289 290
              )
      //- -----------------------
      //- SMTP TEST
      //- -----------------------
291
      q-card.q-pb-sm
292
        q-card-section
293
          .text-subtitle1 {{t('admin.mail.test')}}
294 295 296
        q-item
          blueprint-icon.self-start(icon='email')
          q-item-section
297 298
            q-item-label {{t(`admin.mail.testRecipient`)}}
            q-item-label(caption) {{t(`admin.mail.testRecipientHint`)}}
299 300
            q-input.q-mt-md(
              outlined
301
              v-model='state.testEmail'
302
              dense
303
              :aria-label='t(`admin.mail.testRecipient`)'
304 305 306 307 308 309
              )
        .flex.justify-end.q-pr-md.q-py-sm
          q-btn(
            unelevated
            color='primary'
            icon='las la-paper-plane'
310
            :label='t(`admin.mail.testSend`)'
311
            @click='sendTest'
312
            :loading='state.testLoading'
313 314 315 316
          )

</template>

317
<script setup>
318
import { cloneDeep, toSafeInteger } from 'lodash-es'
319 320
import gql from 'graphql-tag'

321 322 323 324 325
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, watch } from 'vue'

import { useAdminStore } from 'src/stores/admin'
326
import { useFlagsStore } from 'src/stores/flags'
327 328 329 330 331 332 333 334 335
import { useSiteStore } from 'src/stores/site'

// QUASAR

const $q = useQuasar()

// STORES

const adminStore = useAdminStore()
336
const flagStore = useFlagsStore()
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
const siteStore = useSiteStore()

// I18N

const { t } = useI18n()

// META

useMeta({
  title: t('admin.mail.title')
})

// DATA

const state = reactive({
  config: {
    senderName: '',
    senderEmail: '',
355
    defaultBaseURL: '',
356 357 358 359 360 361 362 363 364 365
    host: '',
    port: 0,
    secure: false,
    verifySSL: false,
    user: '',
    pass: '',
    useDKIM: false,
    dkimDomainName: '',
    dkimKeySelector: '',
    dkimPrivateKey: ''
366
  },
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
  testEmail: '',
  testLoading: false,
  loading: 0
})

// METHODS
async function load () {
  state.loading++
  try {
    const resp = await APOLLO_CLIENT.query({
      query: gql`
        query getMailConfig {
          mailConfig {
            senderName
            senderEmail
382
            defaultBaseURL
383 384 385 386 387 388 389 390 391 392 393
            host
            port
            secure
            verifySSL
            user
            pass
            useDKIM
            dkimDomainName
            dkimKeySelector
            dkimPrivateKey
          }
394
        }
395 396 397 398 399 400 401
      `,
      fetchPolicy: 'network-only'
    })
    if (!resp?.data?.mailConfig) {
      throw new Error('Failed to fetch mail config.')
    }
    state.config = cloneDeep(resp.data.mailConfig)
402
    adminStore.info.isMailConfigured = state.config?.host?.length > 2
403 404 405 406 407 408 409 410 411 412 413 414
  } catch (err) {
    $q.notify({
      type: 'negative',
      message: 'Failed to fetch mail config',
      caption: err.message
    })
  }
  state.loading--
}

async function save () {
  if (state.loading > 0) { return }
415

416 417 418 419 420 421 422
  state.loading++
  try {
    await APOLLO_CLIENT.mutate({
      mutation: gql`
        mutation saveMailConfig (
          $senderName: String!
          $senderEmail: String!
423
          $defaultBaseURL: String!
424 425
          $host: String!
          $port: Int!
426
          $name: String!
427 428 429 430 431 432 433 434 435 436 437 438
          $secure: Boolean!
          $verifySSL: Boolean!
          $user: String!
          $pass: String!
          $useDKIM: Boolean!
          $dkimDomainName: String!
          $dkimKeySelector: String!
          $dkimPrivateKey: String!
        ) {
          updateMailConfig (
            senderName: $senderName
            senderEmail: $senderEmail
439
            defaultBaseURL: $defaultBaseURL
440 441
            host: $host
            port: $port
442
            name: $name
443 444 445 446 447 448 449 450 451
            secure: $secure
            verifySSL: $verifySSL
            user: $user
            pass: $pass
            useDKIM: $useDKIM
            dkimDomainName: $dkimDomainName
            dkimKeySelector: $dkimKeySelector
            dkimPrivateKey: $dkimPrivateKey
          ) {
452
            operation {
453 454 455
              succeeded
              slug
              message
456 457
            }
          }
458 459 460 461 462
        }
      `,
      variables: {
        senderName: state.config.senderName || '',
        senderEmail: state.config.senderEmail || '',
463
        defaultBaseURL: state.config.defaultBaseURL || '',
464 465
        host: state.config.host || '',
        port: toSafeInteger(state.config.port) || 0,
466
        name: state.config.name || '',
467 468 469 470 471 472 473 474
        secure: state.config.secure ?? false,
        verifySSL: state.config.verifySSL ?? false,
        user: state.config.user || '',
        pass: state.config.pass || '',
        useDKIM: state.config.useDKIM ?? false,
        dkimDomainName: state.config.dkimDomainName || '',
        dkimKeySelector: state.config.dkimKeySelector || '',
        dkimPrivateKey: state.config.dkimPrivateKey || ''
475
      }
476 477 478 479 480
    })
    $q.notify({
      type: 'positive',
      message: t('admin.mail.saveSuccess')
    })
481
    adminStore.info.isMailConfigured = state.config?.host?.length > 2
482 483 484 485 486 487 488 489 490
  } catch (err) {
    $q.notify({
      type: 'negative',
      message: err.message
    })
  }
  state.loading--
}

491 492 493 494 495 496 497
function editTemplate (tmplId) {
  adminStore.$patch({
    overlayOpts: { id: tmplId },
    overlay: 'MailTemplateEditorOverlay'
  })
}

498 499 500 501 502 503 504 505 506 507 508
async function sendTest () {
  state.loading++
  try {
    const resp = await APOLLO_CLIENT.mutate({
      mutation: gql`
        mutation sentMailTest (
          $recipientEmail: String!
          ) {
          sendMailTest(
            recipientEmail: $recipientEmail
          ) {
509
            operation {
510 511 512
              succeeded
              slug
              message
513 514 515
            }
          }
        }
516 517 518
      `,
      variables: {
        recipientEmail: state.testEmail
519
      }
520
    })
521 522
    if (!resp?.data?.sendMailTest?.operation?.succeeded) {
      throw new Error(resp?.data?.sendMailTest?.operation?.message || 'An unexpected error occurred.')
523
    }
524 525 526 527 528 529 530 531 532 533 534

    state.testEmail = ''
    $q.notify({
      type: 'positive',
      message: t('admin.mail.sendTestSuccess')
    })
  } catch (err) {
    $q.notify({
      type: 'negative',
      message: err.message
    })
535
  }
536
  state.loading--
537
}
538 539 540 541 542 543

// MOUNTED

onMounted(() => {
  load()
})
544 545 546 547 548
</script>

<style lang='scss'>

</style>