better email error logging and some pretty printing for admin interface
This commit is contained in:
parent
67375bd281
commit
4152034e4a
9 changed files with 45 additions and 26 deletions
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue