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:
@@ -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") }}
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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."""
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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."""
|
||||||
|
@@ -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'
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user