diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 43fcf8859..5b5912a19 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -25,7 +25,7 @@ "jquery": "^3.7.1", "marked": "^15.0.7", "moment": "^2.30.1", - "pankow": "^2.9.7", + "pankow": "^2.10.0", "pankow-viewers": "^1.0.11", "sass": "^1.85.1", "vite": "^6.2.2", @@ -2339,9 +2339,9 @@ } }, "node_modules/pankow": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/pankow/-/pankow-2.9.7.tgz", - "integrity": "sha512-IZ/WkZ/2Z4CVjPM7fDx+9AdjdR8rQqsBGQD9SUJg5OnqPSkznmE+J6AevqKwvgG+e4KXOyJqeIivLdapK4FU1w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/pankow/-/pankow-2.10.0.tgz", + "integrity": "sha512-qpDVvHgpJYfYPOyPhPL317r6sgMYm+WJHUk5R9g3iy7oqFBu6d+mS3MXgzbAXuGBUAsie+ZTiyoMi2u1OgXy4Q==", "license": "ISC", "dependencies": { "@fontsource/inter": "^5.2.5", @@ -4135,9 +4135,9 @@ } }, "pankow": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/pankow/-/pankow-2.9.7.tgz", - "integrity": "sha512-IZ/WkZ/2Z4CVjPM7fDx+9AdjdR8rQqsBGQD9SUJg5OnqPSkznmE+J6AevqKwvgG+e4KXOyJqeIivLdapK4FU1w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/pankow/-/pankow-2.10.0.tgz", + "integrity": "sha512-qpDVvHgpJYfYPOyPhPL317r6sgMYm+WJHUk5R9g3iy7oqFBu6d+mS3MXgzbAXuGBUAsie+ZTiyoMi2u1OgXy4Q==", "requires": { "@fontsource/inter": "^5.2.5", "@fortawesome/fontawesome-free": "^6.7.2", diff --git a/dashboard/package.json b/dashboard/package.json index d1c102682..af92c427f 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -26,7 +26,7 @@ "jquery": "^3.7.1", "marked": "^15.0.7", "moment": "^2.30.1", - "pankow": "^2.9.7", + "pankow": "^2.10.0", "pankow-viewers": "^1.0.11", "sass": "^1.85.1", "vite": "^6.2.2", diff --git a/dashboard/src/components/MailboxDialog.vue b/dashboard/src/components/MailboxDialog.vue new file mode 100644 index 000000000..0da39bda2 --- /dev/null +++ b/dashboard/src/components/MailboxDialog.vue @@ -0,0 +1,172 @@ + + + + + + {{ formError }} + + + + + + + {{ $t('email.addMailboxDialog.name') }} + + + + + + + + {{ $t('email.editMailboxDialog.owner') }} + + + + + + + + + + + : {{ prettyDecimalSize(storageQuota) }} + + + + + + + + + + {{ $t('email.editMailboxDialog.aliases') }} + + + + + + + + + {{ $t('email.editMailboxDialog.noAliases') }} {{ $t('email.editMailboxDialog.addAliasAction') }} + + + {{ $t('email.editMailboxDialog.addAnotherAliasAction') }} + + + + + + + \ No newline at end of file diff --git a/dashboard/src/models/AppPasswordsModel.js b/dashboard/src/models/AppPasswordsModel.js index ecadf1585..ac07a3c30 100644 --- a/dashboard/src/models/AppPasswordsModel.js +++ b/dashboard/src/models/AppPasswordsModel.js @@ -32,7 +32,7 @@ function create() { async remove(id) { let error, result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/app_passwords/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/app_passwords/${id}`, null, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/dashboard/src/models/ApplinksModel.js b/dashboard/src/models/ApplinksModel.js index 7d19f820b..35eb0d66f 100644 --- a/dashboard/src/models/ApplinksModel.js +++ b/dashboard/src/models/ApplinksModel.js @@ -47,7 +47,7 @@ function create() { async remove(id) { let result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/applinks/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/applinks/${id}`, null, { access_token: accessToken }); } catch (e) { return [e]; } diff --git a/dashboard/src/models/ArchivesModel.js b/dashboard/src/models/ArchivesModel.js index 96f90b2c0..75f84a911 100644 --- a/dashboard/src/models/ArchivesModel.js +++ b/dashboard/src/models/ArchivesModel.js @@ -26,7 +26,7 @@ function create() { async remove(id) { let error, result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/archives/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/archives/${id}`, null, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/dashboard/src/models/DirectoryModel.js b/dashboard/src/models/DirectoryModel.js index 602cae4a3..bfee08f76 100644 --- a/dashboard/src/models/DirectoryModel.js +++ b/dashboard/src/models/DirectoryModel.js @@ -114,7 +114,7 @@ export function createDirectoryModel(origin, accessToken, api) { await fetcher.post(`${origin}/api/v1/${api}/files/${folderPath}`, {}, { directory: true, access_token: accessToken }); }, async remove(filePath) { - await fetcher.del(`${origin}/api/v1/${api}/files/${filePath}`, { access_token: accessToken }); + await fetcher.del(`${origin}/api/v1/${api}/files/${filePath}`, null, { access_token: accessToken }); }, async rename(fromFilePath, toFilePath, overwrite = false) { return await fetcher.put(`${origin}/api/v1/${api}/files/${fromFilePath}`, { action: 'rename', newFilePath: sanitize(toFilePath), overwrite }, { access_token: accessToken }); diff --git a/dashboard/src/models/DomainsModel.js b/dashboard/src/models/DomainsModel.js index c7c38ee83..5bec29737 100644 --- a/dashboard/src/models/DomainsModel.js +++ b/dashboard/src/models/DomainsModel.js @@ -67,7 +67,7 @@ function create() { async remove(domain) { let error, result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/domains/${domain}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/domains/${domain}`, null, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/dashboard/src/models/GroupsModel.js b/dashboard/src/models/GroupsModel.js index 599350ca8..848d1da86 100644 --- a/dashboard/src/models/GroupsModel.js +++ b/dashboard/src/models/GroupsModel.js @@ -82,7 +82,7 @@ function create() { async remove(id) { let result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/groups/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/groups/${id}`, null, { access_token: accessToken }); } catch (e) { return [e]; } diff --git a/dashboard/src/models/MailboxesModel.js b/dashboard/src/models/MailboxesModel.js index 2d1ff1db4..29e336e1c 100644 --- a/dashboard/src/models/MailboxesModel.js +++ b/dashboard/src/models/MailboxesModel.js @@ -17,6 +17,80 @@ function create() { if (result.status !== 200) return [result]; return [null, result.body.mailboxes]; }, + async get(domain, name) { + let result; + try { + result = await fetcher.get(`${API_ORIGIN}/api/v1/mail/${domain}/mailboxes/${name}`, { access_token: accessToken }); + } catch (e) { + return [e]; + } + + if (result.status !== 200) return [result]; + return [null, result.body.mailbox]; + }, + async add(domain, name, options) { + const data = { + name: name, + ownerId: options.ownerId, + ownerType: options.ownerType, + active: !!options.active, + enablePop3: !!options.enablePop3, + storageQuota: options.storageQuota ||0, + messagesQuota: options.messagesQuota || 0, + }; + + let result; + try { + result = await fetcher.post(`${API_ORIGIN}/api/v1/mail/${domain}/mailboxes`, data, { access_token: accessToken }); + } catch (e) { + return [e]; + } + + if (result.status !== 201) return [result]; + return [null]; + }, + async update(domain, name, options) { + const data = { + ownerId: options.ownerId, + ownerType: options.ownerType, + active: !!options.active, + enablePop3: !!options.enablePop3, + storageQuota: options.storageQuota ||0, + messagesQuota: options.messagesQuota || 0, + }; + + let result; + try { + result = await fetcher.post(`${API_ORIGIN}/api/v1/mail/${domain}/mailboxes/${name}`, data, { access_token: accessToken }); + } catch (e) { + return [e]; + } + + if (result.status !== 204) return [result]; + return [null]; + }, + async remove(domain, name, deleteMails = false) { + let result; + try { + result = await fetcher.del(`${API_ORIGIN}/api/v1/mail/${domain}/mailboxes/${name}`, { deleteMails }, { access_token: accessToken }); + } catch (e) { + return [e]; + } + + if (result.status !== 201) return [result]; + return [null]; + }, + async setAliases(domain, name, aliases) { + let result; + try { + result = await fetcher.put(`${API_ORIGIN}/api/v1/mail/${domain}/mailboxes/${name}/aliases`, { aliases }, { access_token: accessToken }); + } catch (e) { + return [e]; + } + + if (result.status !== 202) return [result]; + return [null]; + }, }; } diff --git a/dashboard/src/models/ProfileModel.js b/dashboard/src/models/ProfileModel.js index 8dc2d7a62..4391d58a5 100644 --- a/dashboard/src/models/ProfileModel.js +++ b/dashboard/src/models/ProfileModel.js @@ -9,7 +9,7 @@ function create() { name: 'ProfileModel', async logout() { // destroy oidc session in the spirit of true SSO - await fetcher.del(`${API_ORIGIN}/api/v1/oidc/sessions`, { access_token: accessToken }); + await fetcher.del(`${API_ORIGIN}/api/v1/oidc/sessions`, null, { access_token: accessToken }); localStorage.removeItem('token'); diff --git a/dashboard/src/models/TokensModel.js b/dashboard/src/models/TokensModel.js index 891f6eaa8..8adc90b13 100644 --- a/dashboard/src/models/TokensModel.js +++ b/dashboard/src/models/TokensModel.js @@ -32,7 +32,7 @@ function create() { async remove(id) { let error, result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/tokens/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/tokens/${id}`, null, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/dashboard/src/models/UserDirectoryModel.js b/dashboard/src/models/UserDirectoryModel.js index f11b1023d..5dfaadbdf 100644 --- a/dashboard/src/models/UserDirectoryModel.js +++ b/dashboard/src/models/UserDirectoryModel.js @@ -125,7 +125,7 @@ function create() { async removeOpenIdClient(id) { let error, result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/oidc/clients/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/oidc/clients/${id}`, null, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/dashboard/src/models/UsersModel.js b/dashboard/src/models/UsersModel.js index 95db0bc8c..9bd413050 100644 --- a/dashboard/src/models/UsersModel.js +++ b/dashboard/src/models/UsersModel.js @@ -68,7 +68,7 @@ function create() { async remove(id) { let result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/users/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/users/${id}`, null, { access_token: accessToken }); } catch (e) { return [e]; } diff --git a/dashboard/src/models/VolumesModel.js b/dashboard/src/models/VolumesModel.js index ed475f1ef..bf0722422 100644 --- a/dashboard/src/models/VolumesModel.js +++ b/dashboard/src/models/VolumesModel.js @@ -66,7 +66,7 @@ function create() { async remove(id) { let error, result; try { - result = await fetcher.del(`${API_ORIGIN}/api/v1/volumes/${id}`, { access_token: accessToken }); + result = await fetcher.del(`${API_ORIGIN}/api/v1/volumes/${id}`, null, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/dashboard/src/style.css b/dashboard/src/style.css index e20b4b89f..718b1c38b 100644 --- a/dashboard/src/style.css +++ b/dashboard/src/style.css @@ -199,3 +199,7 @@ h1.section-header { .actionable:hover { color: var(--pankow-color-primary-hover); } + +fieldset > * { + margin: 6px 0; +} \ No newline at end of file diff --git a/dashboard/src/views/MailboxesView.vue b/dashboard/src/views/MailboxesView.vue index 8a5d5474f..83526f773 100644 --- a/dashboard/src/views/MailboxesView.vue +++ b/dashboard/src/views/MailboxesView.vue @@ -4,19 +4,27 @@ import { useI18n } from 'vue-i18n'; const i18n = useI18n(); const t = i18n.t; -import { ref, onMounted } from 'vue'; -import { Button, TableView } from 'pankow'; +import { ref, onMounted, useTemplateRef } from 'vue'; +import { Button, TableView, Dialog, Checkbox } from 'pankow'; +import { prettyDecimalSize } from 'pankow/utils'; import { eachLimit } from 'async'; import Section from '../components/Section.vue'; +import MailboxDialog from '../components/MailboxDialog.vue'; import DomainsModel from '../models/DomainsModel.js'; import MailModel from '../models/MailModel.js'; +import GroupsModel from '../models/GroupsModel.js'; +import UsersModel from '../models/UsersModel.js'; +import MailboxesModel from '../models/MailboxesModel.js'; const domainsModel = DomainsModel.create(); const mailModel = MailModel.create(); +const groupsModel = GroupsModel.create(); +const mailboxesModel = MailboxesModel.create(); +const usersModel = UsersModel.create(); const columns = { fullName: { label: t('email.incoming.mailboxes.name'), sort: true }, - ownerId: { label: t('email.incoming.mailboxes.owner'), sort: true }, + ownerDisplayName: { label: t('email.incoming.mailboxes.owner'), sort: true }, aliases: { label: t('email.incoming.mailboxes.aliases'), sort: true }, usage: { label: t('email.incoming.mailboxes.usage'), sort: true }, actions: {} @@ -25,55 +33,103 @@ const columns = { const busy = ref(true); const mailboxes = ref([]); const domains = ref([]); +const users = ref([]); +const groups = ref([]); function renderAliases(aliases) { return aliases.map(a => `${a.name}@${a.domain}`).join(', '); } -// function updateMailUsage(mailboxName, quotaLimit) { -// if (!$scope.mailUsage) $scope.mailUsage = {}; -// if (!$scope.mailUsage[mailboxName]) $scope.mailUsage[mailboxName] = {}; -// $scope.mailUsage[mailboxName].quotaLimit = quotaLimit; -// } +const mailboxDialog = useTemplateRef('mailboxDialog'); -async function refreshForDomain(domain) { - let [error, result] = await mailModel.usage(domain); - if (error) return console.error(error); +async function onAddOrEdit(mailbox = null) { + mailboxDialog.value.open(mailbox); +} - const usage = result; +const removeDialog = useTemplateRef('removeDialog'); +const removeBusy = ref(false); +const removeError = ref(''); +const removePurge = ref(false); +const removeMailbox = ref({}); - console.log(usage); +function onRemove(mailbox) { + removeBusy.value = false; + removePurge.value = false; + removeError.value = ''; + removeMailbox.value = mailbox; - [error, result] = await mailModel.listMailboxes(domain); - if (error) throw error; + removeDialog.value.open(); +} - result.forEach((m) => { - m.fullName = m.name + '@' + m.domain; // to make it simple for the ui - // m.owner = $scope.owners.find(function (o) { return o.id === m.ownerId; }); // owner may not exist - // m.ownerDisplayName = m.owner ? m.owner.display : ''; // this meta property is set when we get the user list - }); +async function onSubmitRemove() { + removeBusy.value = true; + removeError.value = ''; - mailboxes.value = mailboxes.value.concat(result); + const [error] = await mailboxesModel.remove(removeMailbox.value.domain, removeMailbox.value.name, removePurge.value); + if (error) { + removeBusy.value = false; + removeError.value = error.body ? error.body.message : 'Internal error'; + return console.error(error); + } + + await refresh(); + removeDialog.value.close(); + + removeBusy.value = false; } async function refresh() { busy.value = true; + let tmp = []; + async function refreshForDomain(domain) { + let [error, result] = await mailModel.usage(domain); + if (error) return console.error(error); + + const usage = result; + + [error, result] = await mailModel.listMailboxes(domain); + if (error) throw error; + + result.forEach((m) => { + m.fullName = m.name + '@' + m.domain; + + m.owner = users.value.find(u => u.id === m.ownerId) || null; + if (!m.owner) m.owner = groups.value.find(g => g.id === m.ownerId) || null; + + m.ownerDisplayName = m.owner ? (m.owner.username || m.owner.name) : ''; + m.usage = usage[m.fullName] || null; + }); + + tmp = tmp.concat(result); + } + try { await eachLimit(domains.value.map(d => d.domain), 10, refreshForDomain); } catch (error) { return console.error(error); } + mailboxes.value = tmp; busy.value = false; } onMounted(async () => { - const [error, result] = await domainsModel.list(); + let [error, result] = await domainsModel.list(); if (error) return console.error(error); domains.value = result; + [error, result] = await usersModel.list(); + if (error) return console.error(error); + + users.value = result; + + [error, result] = await groupsModel.list(); + if (error) return console.error(error); + + groups.value = result; + await refresh(); }); @@ -81,20 +137,42 @@ onMounted(async () => { + + + {{ removeError }} + + + + + + + + - {{ $t('email.incoming.mailboxes.addAction') }} + {{ $t('email.incoming.mailboxes.addAction') }} - + {{ renderAliases(mailbox.aliases) }} - - - - - - + {{ prettyDecimalSize(mailbox.usage.quotaValue) }} / {{ prettyDecimalSize(mailbox.usage.quotaLimit) }} + {{ $t('main.loadingPlaceholder') }} ... + + + + + +