mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-03 04:00:57 +00:00
feat: more mail sending backends and plugability (#9608)
* [FR] Improve Email handeling Fixes #7950 * extend implementation of email thread and message models * add missing args * add unit test * increase test coverage * make key not necessary * do not consider in coverage * add email apis * Add email admin * fix email configuration check * improve rendering * squash migrations * add config value overview * log if mails were send * add additional headers * fix api unit test * fix url resolving * add InvenTree specific task to issue mails required to extend sending options (prio, reply to) * use internal sending task to keep telemetry cleaner * add prio handling * add plugin handling * add setting * factor plugin method out * add typing * move function * bump version * fix import path * add a test for the test endpoint * fix checking logic * Add anymail sending / tracking handling * add more ordering fields to api * remove unneeded assingment * add basic docs * handle incoming emails with anymail * Add inbox handling Closes https://github.com/inventree/InvenTree/issues/7951 * add list of supported ESPs * add better error transparency when sending fails * add missing migration * combine migrations back down * fix todos * fix qc export * fix missing model props * add tests * ensure things are passed as a list * fix list formatting * fix deps * move tests * add testing with anymail * allow handling of priority and headers * add test for events * add test for inbound messages * rename variable * increase coverage * fix format * add setting doc * fix link * rename fnc * disable pro test * make messages clearer * fix doc syntax * fix assign * fix test * revert test disablement * add enum * disable check for now * try changing test around * add incoming mail processing * fix import * add docs * Fix mail.md * bump deps * fix api version
This commit is contained in:
@ -235,5 +235,8 @@ export enum ApiEndpoints {
|
||||
error_report_list = 'error-report/',
|
||||
project_code_list = 'project-code/',
|
||||
custom_unit_list = 'units/',
|
||||
notes_image_upload = 'notes-image-upload/'
|
||||
notes_image_upload = 'notes-image-upload/',
|
||||
email_list = 'admin/email/',
|
||||
email_test = 'admin/email/test/',
|
||||
config_list = 'admin/config/'
|
||||
}
|
||||
|
41
src/frontend/src/components/settings/ConfigValueList.tsx
Normal file
41
src/frontend/src/components/settings/ConfigValueList.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Code, Text } from '@mantine/core';
|
||||
|
||||
import { ApiEndpoints, apiUrl } from '@lib/index';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { api } from '../../App';
|
||||
|
||||
export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['config'],
|
||||
queryFn: async () => {
|
||||
return api.get(apiUrl(ApiEndpoints.config_list)).then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const totalData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return keys.map((key) => {
|
||||
return {
|
||||
key: key,
|
||||
value: data.find((item: any) => item.key === key)
|
||||
};
|
||||
});
|
||||
}, [isLoading, data, keys]);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{totalData.map((vals) => (
|
||||
<Text key={vals.key}>
|
||||
<Trans>
|
||||
<Code>{vals.key}</Code> is set via {vals.value?.source} and was last
|
||||
set {vals.value.accessed}
|
||||
</Trans>
|
||||
</Text>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Accordion } from '@mantine/core';
|
||||
|
||||
import { StylishText } from '../../../../components/items/StylishText';
|
||||
import { ConfigValueList } from '../../../../components/settings/ConfigValueList';
|
||||
import { EmailTable } from '../../../../tables/settings/EmailTable';
|
||||
|
||||
export default function UserManagementPanel() {
|
||||
return (
|
||||
<Accordion multiple defaultValue={['emails']}>
|
||||
<Accordion.Item value='emails' key='emails'>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Email Messages`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<EmailTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value='settings' key='settings'>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Settings`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<ConfigValueList
|
||||
key='email_settings'
|
||||
keys={[
|
||||
'INVENTREE_EMAIL_BACKEND',
|
||||
'INVENTREE_EMAIL_HOST',
|
||||
'INVENTREE_EMAIL_PORT',
|
||||
'INVENTREE_EMAIL_USERNAME',
|
||||
'INVENTREE_EMAIL_PASSWORD',
|
||||
'INVENTREE_EMAIL_PREFIX',
|
||||
'INVENTREE_EMAIL_TLS',
|
||||
'INVENTREE_EMAIL_SSL',
|
||||
'INVENTREE_EMAIL_SENDER'
|
||||
]}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
IconFileUpload,
|
||||
IconList,
|
||||
IconListDetails,
|
||||
IconMail,
|
||||
IconPackages,
|
||||
IconPlugConnected,
|
||||
IconQrcode,
|
||||
@ -44,6 +45,10 @@ const UserManagementPanel = Loadable(
|
||||
lazy(() => import('./UserManagementPanel'))
|
||||
);
|
||||
|
||||
const EmailManagementPanel = Loadable(
|
||||
lazy(() => import('./EmailManagementPanel'))
|
||||
);
|
||||
|
||||
const TaskManagementPanel = Loadable(
|
||||
lazy(() => import('./TaskManagementPanel'))
|
||||
);
|
||||
@ -112,6 +117,13 @@ export default function AdminCenter() {
|
||||
content: <UserManagementPanel />,
|
||||
hidden: !user.hasViewRole(UserRoles.admin)
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: t`Email Settings`,
|
||||
icon: <IconMail />,
|
||||
content: <EmailManagementPanel />,
|
||||
hidden: !user.isSuperuser()
|
||||
},
|
||||
{
|
||||
name: 'import',
|
||||
label: t`Data Import`,
|
||||
|
@ -63,6 +63,7 @@ export default function PluginManagementPanel() {
|
||||
'ENABLE_PLUGINS_URL',
|
||||
'ENABLE_PLUGINS_NAVIGATION',
|
||||
'ENABLE_PLUGINS_APP',
|
||||
'ENABLE_PLUGINS_MAILS',
|
||||
'PLUGIN_ON_STARTUP',
|
||||
'PLUGIN_UPDATE_CHECK'
|
||||
]}
|
||||
|
109
src/frontend/src/tables/settings/EmailTable.tsx
Normal file
109
src/frontend/src/tables/settings/EmailTable.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconTestPipe } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { DateColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
export function EmailTable() {
|
||||
const sendTestMail = useCreateApiFormModal({
|
||||
url: ApiEndpoints.email_test,
|
||||
title: t`Send Test Email`,
|
||||
fields: { email: {} },
|
||||
successMessage: t`Email sent successfully`,
|
||||
onFormSuccess: (data: any) => {
|
||||
table.refreshTable();
|
||||
}
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<ActionButton
|
||||
icon={<IconTestPipe />}
|
||||
key={'test'}
|
||||
tooltip={t`Send Test Email`}
|
||||
onClick={() => sendTestMail.open()}
|
||||
/>
|
||||
];
|
||||
}, []);
|
||||
|
||||
const table = useTable('emails', 'id');
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'subject',
|
||||
title: t`Subject`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'to',
|
||||
title: t`To`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'sender',
|
||||
title: t`Sender`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'status',
|
||||
title: t`Status`,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
switch (record.status) {
|
||||
case 'A':
|
||||
return t`Announced`;
|
||||
case 'S':
|
||||
return t`Sent`;
|
||||
case 'F':
|
||||
return t`Failed`;
|
||||
case 'D':
|
||||
return t`Delivered`;
|
||||
case 'R':
|
||||
return t`Read`;
|
||||
case 'C':
|
||||
return t`Confirmed`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'direction',
|
||||
title: t`Direction`,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
return record.direction === 'incoming' ? t`Incoming` : t`Outgoing`;
|
||||
},
|
||||
switchable: true
|
||||
},
|
||||
DateColumn({
|
||||
accessor: 'timestamp',
|
||||
title: t`Timestamp`,
|
||||
sortable: true,
|
||||
switchable: true
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{sendTestMail.modal}
|
||||
<InvenTreeTable
|
||||
tableState={table}
|
||||
url={apiUrl(ApiEndpoints.email_list)}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableSearch: true,
|
||||
enableColumnSwitching: true,
|
||||
tableActions: tableActions
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user