diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md
index 668d989332..93301810ba 100644
--- a/docs/docs/settings/global.md
+++ b/docs/docs/settings/global.md
@@ -40,6 +40,7 @@ Configuration of basic server settings:
{{ globalsetting("INVENTREE_DELETE_TASKS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }}
+{{ globalsetting("INVENTREE_DELETE_EMAIL_DAYS") }}
### Login Settings
diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index 74f70f4175..2b3c6747ed 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 376
+INVENTREE_API_VERSION = 377
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v377 -> 2025-08-01 : https://github.com/inventree/InvenTree/pull/10109
+ - Allow email records to be deleted via the API
+
v376 -> 2025-08-01 : https://github.com/inventree/InvenTree/pull/10108
- Fix search fields for ReturnOrderLineItem API list endpoint
diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py
index ac10a91c4c..80d297c03d 100644
--- a/src/backend/InvenTree/InvenTree/tasks.py
+++ b/src/backend/InvenTree/InvenTree/tasks.py
@@ -466,6 +466,26 @@ def delete_old_notifications():
)
+@tracer.start_as_current_span('delete_old_emails')
+@scheduled_task(ScheduledTask.DAILY)
+def delete_old_emails():
+ """Delete old email messages."""
+ try:
+ from common.models import EmailMessage
+
+ days = get_global_setting('INVENTREE_DELETE_EMAIL_DAYS', 30)
+ threshold = timezone.now() - timedelta(days=days)
+
+ emails = EmailMessage.objects.filter(timestamp__lte=threshold)
+
+ if emails.count() > 0:
+ logger.info('Deleted %s old email messages', emails.count())
+ emails.delete()
+
+ except AppRegistryNotReady:
+ logger.info("Could not perform 'delete_old_emails' - App registry not ready")
+
+
@tracer.start_as_current_span('check_for_updates')
@scheduled_task(ScheduledTask.DAILY)
def check_for_updates():
diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py
index f8863a5f7e..1946d3a2de 100644
--- a/src/backend/InvenTree/common/api.py
+++ b/src/backend/InvenTree/common/api.py
@@ -43,6 +43,7 @@ from InvenTree.mixins import (
ListAPI,
ListCreateAPI,
RetrieveAPI,
+ RetrieveDestroyAPI,
RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI,
)
@@ -842,7 +843,7 @@ class EmailMessageMixin:
permission_classes = [IsSuperuserOrSuperScope]
-class EmailMessageList(EmailMessageMixin, ListAPI):
+class EmailMessageList(EmailMessageMixin, BulkDeleteMixin, ListAPI):
"""List view for email objects."""
filter_backends = SEARCH_ORDER_FILTER
@@ -865,7 +866,7 @@ class EmailMessageList(EmailMessageMixin, ListAPI):
]
-class EmailMessageDetail(EmailMessageMixin, RetrieveAPI):
+class EmailMessageDetail(EmailMessageMixin, RetrieveDestroyAPI):
"""Detail view for an email object."""
diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py
index 9a933fb117..eb91897aea 100644
--- a/src/backend/InvenTree/common/setting/system.py
+++ b/src/backend/InvenTree/common/setting/system.py
@@ -331,6 +331,15 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'units': _('days'),
'validator': [int, MinValueValidator(7)],
},
+ 'INVENTREE_DELETE_EMAIL_DAYS': {
+ 'name': _('Email Deletion Interval'),
+ 'description': _(
+ 'Email messages will be deleted after specified number of days'
+ ),
+ 'default': 30,
+ 'units': _('days'),
+ 'validator': [int, MinValueValidator(7)],
+ },
'BARCODE_ENABLE': {
'name': _('Barcode Support'),
'description': _('Enable barcode scanner support in the web interface'),
diff --git a/src/frontend/src/components/settings/ConfigValueList.tsx b/src/frontend/src/components/settings/ConfigValueList.tsx
index 4efb6cdebb..184f09f62c 100644
--- a/src/frontend/src/components/settings/ConfigValueList.tsx
+++ b/src/frontend/src/components/settings/ConfigValueList.tsx
@@ -1,10 +1,11 @@
-import { Code, Text } from '@mantine/core';
+import { Table } 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';
+import { formatDate } from '../../defaults/formatters';
export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) {
const { data, isLoading } = useQuery({
@@ -28,14 +29,32 @@ export function ConfigValueList({ keys }: Readonly<{ keys: string[] }>) {
return (
- {totalData.map((vals) => (
-
-
- {vals.key}
is set via {vals.value?.source} and was last
- set {vals.value.accessed}
-
-
- ))}
+
+
+
+
+ Setting
+
+
+ Source
+
+
+ Updated
+
+
+
+
+ {totalData.map((vals) => (
+
+ {vals.key}
+ {vals.value?.source}
+
+ {formatDate(vals.value?.accessed, { showTime: true })}
+
+
+ ))}
+
+
);
}
diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
index d467a059a6..8148dee83c 100644
--- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
+++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
@@ -64,7 +64,8 @@ export default function SystemSettings() {
'INVENTREE_BACKUP_DAYS',
'INVENTREE_DELETE_TASKS_DAYS',
'INVENTREE_DELETE_ERRORS_DAYS',
- 'INVENTREE_DELETE_NOTIFICATIONS_DAYS'
+ 'INVENTREE_DELETE_NOTIFICATIONS_DAYS',
+ 'INVENTREE_DELETE_EMAIL_DAYS'
]}
/>
)
diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx
index ef004dcb61..e10c19ab48 100644
--- a/src/frontend/src/tables/ColumnRenderers.tsx
+++ b/src/frontend/src/tables/ColumnRenderers.tsx
@@ -369,7 +369,9 @@ export function DateColumn(props: TableColumnProps): TableColumn {
title: t`Date`,
switchable: true,
render: (record: any) =>
- formatDate(resolveItem(record, props.accessor ?? 'date')),
+ formatDate(resolveItem(record, props.accessor ?? 'date'), {
+ showTime: props.extra?.showTime
+ }),
...props
};
}
diff --git a/src/frontend/src/tables/settings/EmailTable.tsx b/src/frontend/src/tables/settings/EmailTable.tsx
index cfa1caaea9..8e8eeb089f 100644
--- a/src/frontend/src/tables/settings/EmailTable.tsx
+++ b/src/frontend/src/tables/settings/EmailTable.tsx
@@ -1,11 +1,17 @@
import { ActionButton } from '@lib/components/ActionButton';
+import { RowDeleteAction } from '@lib/components/RowActions';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro';
+import { Badge } from '@mantine/core';
import { IconTestPipe } from '@tabler/icons-react';
-import { useMemo } from 'react';
-import { useCreateApiFormModal } from '../../hooks/UseForm';
+import { useCallback, useMemo, useState } from 'react';
+import {
+ useCreateApiFormModal,
+ useDeleteApiFormModal
+} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
+import { useUserState } from '../../states/UserState';
import { DateColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -20,6 +26,8 @@ export function EmailTable() {
}
});
+ const user = useUserState();
+
const tableActions = useMemo(() => {
return [
('');
+
+ const deleteEmail = useDeleteApiFormModal({
+ url: ApiEndpoints.email_list,
+ pk: selectedEmailId,
+ title: t`Delete Email`,
+ successMessage: t`Email deleted successfully`,
+ table: table
+ });
const tableColumns = useMemo(() => {
return [
@@ -57,17 +75,17 @@ export function EmailTable() {
render: (record: any) => {
switch (record.status) {
case 'A':
- return t`Announced`;
+ return {t`Announced`};
case 'S':
- return t`Sent`;
+ return {t`Sent`};
case 'F':
- return t`Failed`;
+ return {t`Failed`};
case 'D':
- return t`Delivered`;
+ return {t`Delivered`};
case 'R':
- return t`Read`;
+ return {t`Read`};
case 'C':
- return t`Confirmed`;
+ return {t`Confirmed`};
}
return '-';
},
@@ -86,21 +104,41 @@ export function EmailTable() {
accessor: 'timestamp',
title: t`Timestamp`,
sortable: true,
- switchable: true
+ switchable: true,
+ extra: { showTime: true }
})
];
}, []);
+ const rowactions = useCallback(
+ (record: any) => {
+ return [
+ RowDeleteAction({
+ onClick: () => {
+ setSelectedEmailId(record.pk);
+ deleteEmail.open();
+ },
+ hidden: !user.isStaff()
+ })
+ ];
+ },
+ [user]
+ );
+
return (
<>
{sendTestMail.modal}
+ {deleteEmail.modal}