better email error logging and some pretty printing for admin interface

This commit is contained in:
j3d1 2024-06-23 01:20:13 +02:00
parent 67375bd281
commit 4152034e4a
9 changed files with 45 additions and 26 deletions

View file

@ -10,8 +10,5 @@ class ExtendedUserAdmin(UserAdmin):
ordering = ('username',) ordering = ('username',)
filter_horizontal = ('groups', 'user_permissions', 'permissions') filter_horizontal = ('groups', 'user_permissions', 'permissions')
def permissions(self, obj):
return ', '.join(obj.get_all_permissions())
admin.site.register(ExtendedUser, ExtendedUserAdmin) admin.site.register(ExtendedUser, ExtendedUserAdmin)

View file

@ -33,7 +33,7 @@ def media_urls(request, hash):
headers={ headers={
'X-Accel-Redirect': f'/redirect_media/{hash_path}', 'X-Accel-Redirect': f'/redirect_media/{hash_path}',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=31536000, private', 'Cache-Control': 'max-age=31536000, private, immutable',
'Expires': datetime.utcnow() + timedelta(days=365), 'Expires': datetime.utcnow() + timedelta(days=365),
'Age': 0, 'Age': 0,
'ETag': file.hash, 'ETag': file.hash,
@ -74,7 +74,7 @@ def thumbnail_urls(request, size, hash):
headers={ headers={
'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}', 'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=31536000, private', 'Cache-Control': 'max-age=31536000, private, immutable',
'Expires': datetime.utcnow() + timedelta(days=365), 'Expires': datetime.utcnow() + timedelta(days=365),
'Age': 0, 'Age': 0,
'ETag': file.hash + "_" + str(size), 'ETag': file.hash + "_" + str(size),

View file

@ -90,4 +90,6 @@ class AbstractFile(models.Model):
class File(AbstractFile): class File(AbstractFile):
item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files') item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files')
pass
def __str__(self):
return self.hash

View file

@ -1,5 +1,4 @@
from datetime import datetime from django.utils import timezone
from django.urls import re_path from django.urls import re_path
from rest_framework import routers, viewsets, serializers from rest_framework import routers, viewsets, serializers
from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.decorators import api_view, permission_classes, authentication_classes
@ -87,7 +86,7 @@ class ItemSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
if 'returned' in validated_data: if 'returned' in validated_data:
if validated_data['returned']: if validated_data['returned']:
validated_data['returned_at'] = datetime.now() validated_data['returned_at'] = timezone.now()
validated_data.pop('returned') validated_data.pop('returned')
if 'dataImage' in validated_data: if 'dataImage' in validated_data:
file = File.objects.create(data=validated_data['dataImage']) file = File.objects.create(data=validated_data['dataImage'])

View file

@ -35,6 +35,9 @@ class Item(SoftDeleteModel):
('match_item', 'Can match item') ('match_item', 'Can match item')
] ]
def __str__(self):
return '[' + str(self.uid) + ']' + self.description
class Container(SoftDeleteModel): class Container(SoftDeleteModel):
cid = models.AutoField(primary_key=True) cid = models.AutoField(primary_key=True)
@ -42,6 +45,9 @@ class Container(SoftDeleteModel):
created_at = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(blank=True, null=True)
updated_at = models.DateTimeField(blank=True, null=True) updated_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return '[' + str(self.cid) + ']' + self.name
class Event(models.Model): class Event(models.Model):
eid = models.AutoField(primary_key=True) eid = models.AutoField(primary_key=True)
@ -53,3 +59,6 @@ class Event(models.Model):
post_end = models.DateTimeField(blank=True, null=True) post_end = models.DateTimeField(blank=True, null=True)
created_at = models.DateTimeField(null=True, auto_now_add=True) created_at = models.DateTimeField(null=True, auto_now_add=True)
updated_at = models.DateTimeField(blank=True, null=True) updated_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return '[' + str(self.slug) + ']' + self.name

View file

@ -1,5 +1,4 @@
from datetime import datetime from django.utils import timezone
from django.test import TestCase, Client from django.test import TestCase, Client
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from knox.models import AuthToken from knox.models import AuthToken
@ -164,7 +163,7 @@ class ItemTestCase(TestCase):
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2) self.assertEqual(len(response.json()), 2)
item2.returned_at = datetime.now() item2.returned_at = timezone.now()
item2.save() item2.save()
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View file

@ -1,8 +1,8 @@
import logging import logging
import aiosmtplib import aiosmtplib
from asgiref.sync import sync_to_async
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
from channels.db import database_sync_to_async
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from mail.models import Email, EventAddress, EmailAttachment from mail.models import Email, EventAddress, EmailAttachment
@ -82,8 +82,7 @@ def make_reply(reply_email, references=None, event=None):
return reply return reply
async def send_smtp(message, log): async def send_smtp(message):
log.info('Sending message to %s' % message['To'])
await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False) await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False)
@ -148,9 +147,9 @@ def parse_email_body(raw, log=None):
attachments.append(attachment) attachments.append(attachment)
if 'inline' in cdispo: if 'inline' in cdispo:
body = body + f'<img src="cid:{attachment.id}">' body = body + f'<img src="cid:{attachment.id}">'
log.info("Image", ctype, attachment.id) log.info("Image %s %s", ctype, attachment.id)
else: else:
log.info("Attachment", ctype, cdispo) log.info("Attachment %s %s", ctype, cdispo)
else: else:
if parsed.get_content_type() == 'text/plain': if parsed.get_content_type() == 'text/plain':
body = parsed.get_payload() body = parsed.get_payload()
@ -161,7 +160,7 @@ def parse_email_body(raw, log=None):
soup = BeautifulSoup(body, 'html.parser') soup = BeautifulSoup(body, 'html.parser')
body = re.sub(r'([\r\n]+.?)*[\r\n]', r'\n', soup.get_text()).strip('\n') body = re.sub(r'([\r\n]+.?)*[\r\n]', r'\n', soup.get_text()).strip('\n')
else: else:
log.warning("Unknown content type", parsed.get_content_type()) log.warning("Unknown content type %s", parsed.get_content_type())
body = "Unknown content type" body = "Unknown content type"
body = unescape_and_decode_quoted_printable(body) body = unescape_and_decode_quoted_printable(body)
body = unescape_and_decode_base64(body) body = unescape_and_decode_base64(body)
@ -172,6 +171,7 @@ def parse_email_body(raw, log=None):
return parsed, body, attachments return parsed, body, attachments
@database_sync_to_async
def receive_email(envelope, log=None): def receive_email(envelope, log=None):
parsed, body, attachments = parse_email_body(envelope.content, log) parsed, body, attachments = parse_email_body(envelope.content, log)
@ -255,9 +255,10 @@ class LMTPHandler:
content = None content = None
try: try:
content = envelope.content content = envelope.content
email, new, reply = await sync_to_async(receive_email)(envelope, log) email, new, reply = await receive_email(envelope, log)
log.info(f"Created email {email.id}") log.info(f"Created email {email.id}")
systemevent = await sync_to_async(SystemEvent.objects.create)(type='email received', reference=email.id) systemevent = await database_sync_to_async(SystemEvent.objects.create)(type='email received',
reference=email.id)
log.info(f"Created system event {systemevent.id}") log.info(f"Created system event {systemevent.id}")
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
await channel_layer.group_send( await channel_layer.group_send(
@ -266,14 +267,15 @@ class LMTPHandler:
) )
log.info(f"Sent message to frontend") log.info(f"Sent message to frontend")
if new and reply: if new and reply:
await send_smtp(reply, log) log.info('Sending message to %s' % reply['To'])
await send_smtp(reply)
log.info("Sent auto reply") log.info("Sent auto reply")
return '250 Message accepted for delivery' return '250 Message accepted for delivery'
except Exception as e: except Exception as e:
import uuid from hashlib import sha256
random_filename = 'mail-' + str(uuid.uuid4()) random_filename = 'mail-' + sha256(content).hexdigest()
with open(random_filename, 'wb') as f: with open(random_filename, 'wb') as f:
f.write(content) f.write(content)
log.error(type(e), e, f"Saved email to {random_filename}") log.error(f"Saved email to {random_filename} because of error %s (%s)", e, type(e))
return '451 Internal server error' return '451 Internal server error'

View file

@ -47,8 +47,7 @@ def reply(request, pk):
body=request.data['message'], body=request.data['message'],
in_reply_to=first_mail.reference, in_reply_to=first_mail.reference,
) )
log = logging.getLogger('mail.log') async_to_sync(send_smtp)(make_reply(mail, references))
async_to_sync(send_smtp)(make_reply(mail, references), log)
return Response({'status': 'ok'}, status=status.HTTP_201_CREATED) return Response({'status': 'ok'}, status=status.HTTP_201_CREATED)

View file

@ -64,6 +64,9 @@ class IssueThread(SoftDeleteModel):
return return
self.assignments.create(assigned_to=value) self.assignments.create(assigned_to=value)
def __str__(self):
return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name
class Meta: class Meta:
permissions = [ permissions = [
('send_mail', 'Can send mail'), ('send_mail', 'Can send mail'),
@ -91,6 +94,9 @@ class Comment(models.Model):
comment = models.TextField() comment = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' comment #' + str(self.id)
class StateChange(models.Model): class StateChange(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
@ -98,9 +104,15 @@ class StateChange(models.Model):
state = models.CharField(max_length=255, choices=STATE_CHOICES, default='pending_new') state = models.CharField(max_length=255, choices=STATE_CHOICES, default='pending_new')
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' state change to ' + self.state
class Assignment(models.Model): class Assignment(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='assignments') issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='assignments')
assigned_to = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='assigned_tickets') assigned_to = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='assigned_tickets')
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username