2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 14:11: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.
!!! 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") }}

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 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
**Transition handler error - Backend**
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_NOTIFICATIONS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_EMAIL_DAYS") }}
{{ globalsetting("INVENTREE_PROTECT_EMAIL_LOG") }}
### Login Settings

View File

@@ -10,7 +10,7 @@ from typing import Callable, Optional
from django.conf import settings
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.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations.executor import MigrationExecutor
@@ -479,10 +479,16 @@ def delete_old_emails():
emails = EmailMessage.objects.filter(timestamp__lte=threshold)
if emails.count() > 0:
logger.info('Deleted %s old email messages', emails.count())
try:
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")

View File

@@ -239,3 +239,61 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
msg.message,
"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 users.models
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.states import ColorEnum
from generic.states.custom import state_color_mappings
@@ -2522,6 +2522,28 @@ class Priority(models.IntegerChoices):
HEADER_PRIORITY = 'X-Priority'
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):
"""Model for storing email messages sent or received by the system.
@@ -2663,6 +2685,14 @@ class EmailMessage(models.Model):
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):
"""Model for storing email threads."""

View File

@@ -340,6 +340,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'units': _('days'),
'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': {
'name': _('Barcode Support'),
'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 common.models import EmailMessage, Priority
from common.settings import set_global_setting
from InvenTree.helpers_email import send_email
from InvenTree.unit_test import InvenTreeAPITestCase
@@ -129,6 +130,31 @@ class EmailTests(InvenTreeAPITestCase):
self.assertEqual(msg.status, EmailMessage.EmailStatus.FAILED)
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):
"""Unit tests for anymail events."""

View File

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