2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 22:21:37 +00:00

Email history enhancement (#10114)

* add warning that the log is useless by default

* Add setting to enhance email log again

* add missing test for #10109

* add test for delete protections

* add error code

* Update email.md

* Update email.md
This commit is contained in:
Matthias Mair
2025-08-08 00:04:17 +02:00
committed by GitHub
parent 00017400ff
commit ed45a4e5bf
9 changed files with 144 additions and 6 deletions

View File

@@ -38,4 +38,8 @@ Most popular providers are supported:
Superusers can view the email log in the [Admin Center](./admin.md#admin-center). This is useful for debugging and tracking email delivery / receipt. Superusers can view the email log in the [Admin Center](./admin.md#admin-center). This is useful for debugging and tracking email delivery / receipt.
!!! warning "Warning"
By default, email logs are cleaned after 30 days. This can be configured in the [InvenTree settings](../settings/global.md#server-settings).
If your organization is bound by business record retention laws or rules, you should ensure the retention of mail logs is set accordingly via a global override for the setting to ensure proper compliance. E.g. records in connection with commercial activity in the EU often have to be kept for over 5 years. Check requirements with proper sources for your region.
{{ image("admin/email_settings.png", "Email Control Pane") }} {{ image("admin/email_settings.png", "Email Control Pane") }}

View File

@@ -50,6 +50,12 @@ This might be caused by an addition or removal of models to the code base or cha
The settings for SITE_URL and ALLOWED_HOSTS do not match the host used to access the server. This might lead to issues with CSRF protection, CORS and other security features. The settings for SITE_URL and ALLOWED_HOSTS do not match the host used to access the server. This might lead to issues with CSRF protection, CORS and other security features.
The settings must be adjusted. The settings must be adjusted.
#### INVE-E8
**Email log deletion is protected - Backend**
The email log is protected from deletion by a setting. This was set by an administrator to prevent accidental deletion of emails.
If you want to delete the email log, you need to get the [`INVENTREE_PROTECT_EMAIL_LOG`](../settings/global.md#server-settings) setting set to `False`.
#### INVE-E9 #### INVE-E9
**Transition handler error - Backend** **Transition handler error - Backend**
An error occurred while discovering or executing a transition handler. This likely indicates a faulty or incompatible plugin. An error occurred while discovering or executing a transition handler. This likely indicates a faulty or incompatible plugin.

View File

@@ -41,6 +41,7 @@ Configuration of basic server settings:
{{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_EMAIL_DAYS") }} {{ globalsetting("INVENTREE_DELETE_EMAIL_DAYS") }}
{{ globalsetting("INVENTREE_PROTECT_EMAIL_LOG") }}
### Login Settings ### Login Settings

View File

@@ -10,7 +10,7 @@ from typing import Callable, Optional
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import AppRegistryNotReady from django.core.exceptions import AppRegistryNotReady, ValidationError
from django.core.management import call_command from django.core.management import call_command
from django.db import DEFAULT_DB_ALIAS, connections from django.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
@@ -479,10 +479,16 @@ def delete_old_emails():
emails = EmailMessage.objects.filter(timestamp__lte=threshold) emails = EmailMessage.objects.filter(timestamp__lte=threshold)
if emails.count() > 0: if emails.count() > 0:
logger.info('Deleted %s old email messages', emails.count()) try:
emails.delete() emails.delete()
logger.info('Deleted %s old email messages', emails.count())
except ValidationError:
logger.info(
'Did not delete %s old email messages because of a validation error',
emails.count(),
)
except AppRegistryNotReady: except AppRegistryNotReady: # pragma: no cover
logger.info("Could not perform 'delete_old_emails' - App registry not ready") logger.info("Could not perform 'delete_old_emails' - App registry not ready")

View File

@@ -239,3 +239,61 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
msg.message, msg.message,
"Background worker task 'InvenTree.tasks.failed_task' failed after 10 attempts", "Background worker task 'InvenTree.tasks.failed_task' failed after 10 attempts",
) )
def test_delete_old_emails(self):
"""Test the delete_old_emails task."""
from common.models import EmailMessage
# Create an email message
self.create_mails()
# Run the task
InvenTreeSetting.set_setting('INVENTREE_DELETE_EMAIL_DAYS', 31)
InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_emails, force_sync=True)
# Check that the email message has been deleted
emails = EmailMessage.objects.all()
self.assertEqual(len(emails), 1)
self.assertEqual(emails[0].subject, 'Test Email 2')
# Set the setting higher than the threshold
InvenTreeSetting.set_setting('INVENTREE_DELETE_EMAIL_DAYS', 30)
# Run the task again
InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_emails, force_sync=True)
emails = EmailMessage.objects.all()
self.assertEqual(len(emails), 0)
# Re-Add messages and enable a proper log
self.create_mails()
# Set the setting lower than the threshold
InvenTreeSetting.set_setting('INVENTREE_DELETE_EMAIL_DAYS', 7)
InvenTreeSetting.set_setting('INVENTREE_PROTECT_EMAIL_LOG', True)
# Run the task again
InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_emails, force_sync=True)
# Check that the email message has not been deleted
emails = EmailMessage.objects.all()
self.assertEqual(len(emails), 2)
def create_mails(self):
"""Create some email messages for testing."""
from common.models import EmailMessage
start_mails = [
['Test Email 1', 'This is a test email.', 'abc@example.org', threshold_low],
[
'Test Email 2',
'This is another test email.',
'def@example.org',
threshold,
],
]
for subject, body, to, timestamp in start_mails:
msg = EmailMessage.objects.create(
subject=subject, body=body, to=to, priority=1
)
msg.timestamp = timestamp
msg.save()

View File

@@ -54,7 +54,7 @@ import InvenTree.ready
import InvenTree.tasks import InvenTree.tasks
import users.models import users.models
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
from common.settings import global_setting_overrides from common.settings import get_global_setting, global_setting_overrides
from generic.enums import StringEnum from generic.enums import StringEnum
from generic.states import ColorEnum from generic.states import ColorEnum
from generic.states.custom import state_color_mappings from generic.states.custom import state_color_mappings
@@ -2522,6 +2522,28 @@ class Priority(models.IntegerChoices):
HEADER_PRIORITY = 'X-Priority' HEADER_PRIORITY = 'X-Priority'
HEADER_MSG_ID = 'Message-ID' HEADER_MSG_ID = 'Message-ID'
del_error_msg = _(
'INVE-E8: Email log deletion is protected. Set INVENTREE_PROTECT_EMAIL_LOG to False to allow deletion.'
)
class NoDeleteQuerySet(models.query.QuerySet):
"""Custom QuerySet to prevent deletion of EmailLog entries."""
def delete(self):
"""Override delete method to prevent deletion of EmailLog entries."""
if get_global_setting('INVENTREE_PROTECT_EMAIL_LOG'):
raise ValidationError(del_error_msg)
super().delete()
class NoDeleteManager(models.Manager):
"""Custom Manager to use NoDeleteQuerySet."""
def get_queryset(self):
"""Return a NoDeleteQuerySet."""
return NoDeleteQuerySet(self.model, using=self._db)
class EmailMessage(models.Model): class EmailMessage(models.Model):
"""Model for storing email messages sent or received by the system. """Model for storing email messages sent or received by the system.
@@ -2663,6 +2685,14 @@ class EmailMessage(models.Model):
return ret return ret
objects = NoDeleteManager()
def delete(self, *kwargs):
"""Delete entry - if not protected."""
if get_global_setting('INVENTREE_PROTECT_EMAIL_LOG'):
raise ValidationError(del_error_msg)
return super().delete(*kwargs)
class EmailThread(InvenTree.models.InvenTreeMetadataModel): class EmailThread(InvenTree.models.InvenTreeMetadataModel):
"""Model for storing email threads.""" """Model for storing email threads."""

View File

@@ -340,6 +340,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'units': _('days'), 'units': _('days'),
'validator': [int, MinValueValidator(7)], 'validator': [int, MinValueValidator(7)],
}, },
'INVENTREE_PROTECT_EMAIL_LOG': {
'name': _('Protect Email Log'),
'description': _('Prevent deletion of email log entries'),
'default': False,
'validator': bool,
},
'BARCODE_ENABLE': { 'BARCODE_ENABLE': {
'name': _('Barcode Support'), 'name': _('Barcode Support'),
'description': _('Enable barcode scanner support in the web interface'), 'description': _('Enable barcode scanner support in the web interface'),

View File

@@ -10,6 +10,7 @@ from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent, AnymailTrackingEvent, inbound, tracking from anymail.signals import AnymailInboundEvent, AnymailTrackingEvent, inbound, tracking
from common.models import EmailMessage, Priority from common.models import EmailMessage, Priority
from common.settings import set_global_setting
from InvenTree.helpers_email import send_email from InvenTree.helpers_email import send_email
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
@@ -129,6 +130,31 @@ class EmailTests(InvenTreeAPITestCase):
self.assertEqual(msg.status, EmailMessage.EmailStatus.FAILED) self.assertEqual(msg.status, EmailMessage.EmailStatus.FAILED)
self.assertEqual(msg.error_message, 'Test error sending email') self.assertEqual(msg.error_message, 'Test error sending email')
def test_email_model_delete(self):
"""Test that the email model does not allow deletion if disabled."""
set_global_setting('INVENTREE_PROTECT_EMAIL_LOG', True)
EmailMessage.objects.create(
subject='test sub', body='test msg', to='abc@example.org', priority=3
)
with self.assertRaises(ValidationError):
EmailMessage.objects.all().delete()
msg = EmailMessage.objects.create(
subject='test sub', body='test msg', to='abc@example.org', priority=3
)
with self.assertRaises(ValidationError):
msg.delete()
# Should still work without the protection
self.assertEqual(EmailMessage.objects.count(), 2)
set_global_setting('INVENTREE_PROTECT_EMAIL_LOG', False)
msg.delete()
# Check that the message was deleted
self.assertEqual(EmailMessage.objects.count(), 1)
class EmailEventsTests(TestCase): class EmailEventsTests(TestCase):
"""Unit tests for anymail events.""" """Unit tests for anymail events."""

View File

@@ -66,7 +66,8 @@ export default function SystemSettings() {
'INVENTREE_DELETE_TASKS_DAYS', 'INVENTREE_DELETE_TASKS_DAYS',
'INVENTREE_DELETE_ERRORS_DAYS', 'INVENTREE_DELETE_ERRORS_DAYS',
'INVENTREE_DELETE_NOTIFICATIONS_DAYS', 'INVENTREE_DELETE_NOTIFICATIONS_DAYS',
'INVENTREE_DELETE_EMAIL_DAYS' 'INVENTREE_DELETE_EMAIL_DAYS',
'INVENTREE_PROTECT_EMAIL_LOG'
]} ]}
/> />
) )