Compare commits
3 commits
9e0540d133
...
9ac1e9706f
Author | SHA1 | Date | |
---|---|---|---|
9ac1e9706f | |||
89707113bb | |||
5565e5f5d0 |
18 changed files with 128 additions and 57 deletions
|
@ -0,0 +1,26 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
Content-Type: multipart/alternative; boundary="abc"
|
||||||
|
|
||||||
|
--abc
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
test1
|
||||||
|
|
||||||
|
--abc
|
||||||
|
Content-Type: image/jpeg; name="test.jpg"
|
||||||
|
Content-Disposition: inline; filename="test.jpg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <1>
|
||||||
|
X-Attachment-Id: 1
|
||||||
|
|
||||||
|
dGVzdGltYWdl
|
||||||
|
|
||||||
|
--abc
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
test2
|
||||||
|
|
||||||
|
--abc--
|
|
@ -0,0 +1,5 @@
|
||||||
|
Subject: test
|
||||||
|
From: <test1@test>
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
hello =C3=A4=C3=B6=C3=BC
|
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: test
|
||||||
|
From: test3@test
|
||||||
|
To: test4@localhost
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
||||||
|
test
|
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: =?UTF-8?Q?suche_M=C3=BCtze?=
|
||||||
|
From: test3@test
|
||||||
|
To: test4@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
||||||
|
Text mit Quoted-Printable-Kodierung: =?utf-8?Q?=C3=A4=C3=B6=C3=BC=C3=9F?=
|
|
@ -0,0 +1,7 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
Content-Type: text/html; charset=iso-8859-1
|
||||||
|
|
||||||
|
hello äöü
|
|
@ -0,0 +1,21 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
Content-Type: multipart/mixed; boundary="abc"
|
||||||
|
|
||||||
|
--abc
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
test1
|
||||||
|
|
||||||
|
--abc
|
||||||
|
Content-Type: image/jpeg; name="test.jpg"
|
||||||
|
Content-Disposition: attachment; filename="test.jpg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <1>
|
||||||
|
X-Attachment-Id: 1
|
||||||
|
|
||||||
|
dGVzdGltYWdl
|
||||||
|
|
||||||
|
--abc--
|
|
@ -0,0 +1,11 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>test</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: test =?utf-8?Q?=C3=A4?=
|
||||||
|
From: test3@test
|
||||||
|
To: test4@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
||||||
|
Text mit Quoted-Printable-Kodierung: =?utf-8?Q?=C3=A4=C3=B6=C3=BC=C3=9F?=
|
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: =?utf-8?B?dGVzdA==?=
|
||||||
|
From: test3@test
|
||||||
|
To: test4@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
||||||
|
Text mit Base64-Kodierung: =?utf-8?B?w6TDtsO8w58=?=
|
|
@ -0,0 +1,6 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test_event@localhost
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
||||||
|
test
|
|
@ -0,0 +1,5 @@
|
||||||
|
From: noreply@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
|
||||||
|
test
|
|
@ -0,0 +1,7 @@
|
||||||
|
Subject: test
|
||||||
|
From: test1@test
|
||||||
|
To: test2@test
|
||||||
|
Message-ID: <1@test>
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
|
||||||
|
thank you =?utf-8?Q?=F0=9F=98=8A?=
|
|
@ -103,7 +103,7 @@ async def send_smtp(message):
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
def find_active_issue_thread(in_reply_to, address, subject, event, spam=False):
|
def find_active_issue_thread(in_reply_to, address, subject, event):
|
||||||
from re import match
|
from re import match
|
||||||
uuid_match = match(r'^ticket\+([a-f0-9-]{36})@', address)
|
uuid_match = match(r'^ticket\+([a-f0-9-]{36})@', address)
|
||||||
if uuid_match:
|
if uuid_match:
|
||||||
|
@ -114,8 +114,7 @@ def find_active_issue_thread(in_reply_to, address, subject, event, spam=False):
|
||||||
if reply_to.exists():
|
if reply_to.exists():
|
||||||
return reply_to.first().issue_thread, False
|
return reply_to.first().issue_thread, False
|
||||||
else:
|
else:
|
||||||
issue = IssueThread.objects.create(name=subject, event=event,
|
issue = IssueThread.objects.create(name=subject, event=event)
|
||||||
initial_state='pending_suspected_spam' if spam else 'pending_new')
|
|
||||||
return issue, True
|
return issue, True
|
||||||
|
|
||||||
|
|
||||||
|
@ -214,8 +213,6 @@ def receive_email(envelope, log=None):
|
||||||
header_to = parsed.get('To')
|
header_to = parsed.get('To')
|
||||||
header_in_reply_to = ascii_strip(parsed.get('In-Reply-To'))
|
header_in_reply_to = ascii_strip(parsed.get('In-Reply-To'))
|
||||||
header_message_id = ascii_strip(parsed.get('Message-ID'))
|
header_message_id = ascii_strip(parsed.get('Message-ID'))
|
||||||
maybe_spam = parsed.get('X-Spam')
|
|
||||||
suspected_spam = (maybe_spam and maybe_spam.lower() == 'yes')
|
|
||||||
|
|
||||||
if match(r'^([a-zA-Z ]*<)?MAILER-DAEMON@', header_from) and envelope.mail_from.strip("<>") == "":
|
if match(r'^([a-zA-Z ]*<)?MAILER-DAEMON@', header_from) and envelope.mail_from.strip("<>") == "":
|
||||||
log.warning("Ignoring mailer daemon")
|
log.warning("Ignoring mailer daemon")
|
||||||
|
@ -235,8 +232,7 @@ def receive_email(envelope, log=None):
|
||||||
sender = decode_inline_encodings(sender)
|
sender = decode_inline_encodings(sender)
|
||||||
target_event = find_target_event(recipient)
|
target_event = find_target_event(recipient)
|
||||||
|
|
||||||
active_issue_thread, new = find_active_issue_thread(
|
active_issue_thread, new = find_active_issue_thread(header_in_reply_to, recipient, subject, target_event)
|
||||||
header_in_reply_to, recipient, subject, target_event, suspected_spam)
|
|
||||||
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
random_filename = 'mail-' + sha256(envelope.content).hexdigest()
|
random_filename = 'mail-' + sha256(envelope.content).hexdigest()
|
||||||
|
@ -254,7 +250,7 @@ def receive_email(envelope, log=None):
|
||||||
if new:
|
if new:
|
||||||
# auto reply if new issue
|
# auto reply if new issue
|
||||||
references = collect_references(active_issue_thread)
|
references = collect_references(active_issue_thread)
|
||||||
if not sender.startswith('noreply') and not sender.startswith('no-reply') and not suspected_spam:
|
if not sender.startswith('noreply'):
|
||||||
subject = f"Re: {subject} [#{active_issue_thread.short_uuid()}]"
|
subject = f"Re: {subject} [#{active_issue_thread.short_uuid()}]"
|
||||||
body = '''Your request (#{}) has been received and will be reviewed by our lost&found angels.
|
body = '''Your request (#{}) has been received and will be reviewed by our lost&found angels.
|
||||||
|
|
||||||
|
|
|
@ -812,44 +812,6 @@ dGVzdGltYWdl
|
||||||
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
|
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
|
||||||
aiosmtplib.send.assert_called_once()
|
aiosmtplib.send.assert_called_once()
|
||||||
|
|
||||||
def test_mail_spam_header(self):
|
|
||||||
from aiosmtpd.smtp import Envelope
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
import aiosmtplib
|
|
||||||
aiosmtplib.send = make_mocked_coro()
|
|
||||||
handler = LMTPHandler()
|
|
||||||
server = mock.Mock()
|
|
||||||
session = mock.Mock()
|
|
||||||
envelope = Envelope()
|
|
||||||
envelope.mail_from = 'test1@test'
|
|
||||||
envelope.rcpt_tos = ['test2@test']
|
|
||||||
envelope.content = b'''Subject: test
|
|
||||||
From: test1@test
|
|
||||||
To: test2@test
|
|
||||||
Message-ID: <1@test>
|
|
||||||
X-Spam: Yes
|
|
||||||
|
|
||||||
test'''
|
|
||||||
result = async_to_sync(handler.handle_DATA)(server, session, envelope)
|
|
||||||
|
|
||||||
self.assertEqual(result, '250 Message accepted for delivery')
|
|
||||||
self.assertEqual(len(Email.objects.all()), 1) # do not send auto reply if spam is suspected
|
|
||||||
self.assertEqual(len(IssueThread.objects.all()), 1)
|
|
||||||
aiosmtplib.send.assert_not_called()
|
|
||||||
self.assertEqual('test', Email.objects.all()[0].subject)
|
|
||||||
self.assertEqual('test1@test', Email.objects.all()[0].sender)
|
|
||||||
self.assertEqual('test2@test', Email.objects.all()[0].recipient)
|
|
||||||
self.assertEqual('test', Email.objects.all()[0].body)
|
|
||||||
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
|
|
||||||
self.assertEqual('<1@test>', Email.objects.all()[0].reference)
|
|
||||||
self.assertEqual(None, Email.objects.all()[0].in_reply_to)
|
|
||||||
self.assertEqual('test', IssueThread.objects.all()[0].name)
|
|
||||||
self.assertEqual('pending_suspected_spam', IssueThread.objects.all()[0].state)
|
|
||||||
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
|
|
||||||
states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0])
|
|
||||||
self.assertEqual(1, len(states))
|
|
||||||
self.assertEqual('pending_suspected_spam', states[0].state)
|
|
||||||
|
|
||||||
def test_mail_4byte_unicode_emoji(self):
|
def test_mail_4byte_unicode_emoji(self):
|
||||||
from aiosmtpd.smtp import Envelope
|
from aiosmtpd.smtp import Envelope
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
|
|
@ -16,7 +16,6 @@ STATE_CHOICES = (
|
||||||
('pending_physical_confirmation', 'Needs to be confirmed physically'),
|
('pending_physical_confirmation', 'Needs to be confirmed physically'),
|
||||||
('pending_return', 'Needs to be returned'),
|
('pending_return', 'Needs to be returned'),
|
||||||
('pending_postponed', 'Postponed'),
|
('pending_postponed', 'Postponed'),
|
||||||
('pending_suspected_spam', 'Suspected Spam'),
|
|
||||||
('waiting_details', 'Waiting for details'),
|
('waiting_details', 'Waiting for details'),
|
||||||
('waiting_pre_shipping', 'Waiting for Address/Shipping Info'),
|
('waiting_pre_shipping', 'Waiting for Address/Shipping Info'),
|
||||||
('closed_returned', 'Closed: Returned'),
|
('closed_returned', 'Closed: Returned'),
|
||||||
|
@ -47,11 +46,6 @@ class IssueThread(SoftDeleteModel):
|
||||||
event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads')
|
event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads')
|
||||||
manually_created = models.BooleanField(default=False)
|
manually_created = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'initial_state' in kwargs:
|
|
||||||
self._initial_state = kwargs.pop('initial_state')
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def short_uuid(self):
|
def short_uuid(self):
|
||||||
return self.uuid[:8]
|
return self.uuid[:8]
|
||||||
|
|
||||||
|
@ -116,9 +110,8 @@ def set_uuid(sender, instance, **kwargs):
|
||||||
|
|
||||||
@receiver(post_save, sender=IssueThread)
|
@receiver(post_save, sender=IssueThread)
|
||||||
def create_issue_thread(sender, instance, created, **kwargs):
|
def create_issue_thread(sender, instance, created, **kwargs):
|
||||||
if created and instance.state_changes.count() == 0:
|
if created:
|
||||||
initial_state = getattr(instance, '_initial_state', None)
|
StateChange.objects.create(issue_thread=instance, state='pending_new')
|
||||||
StateChange.objects.create(issue_thread=instance, state=initial_state if initial_state else 'pending_new')
|
|
||||||
|
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Comment(models.Model):
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
:columns="['id', 'name', 'last_activity', 'assigned_to',
|
:columns="['id', 'name', 'last_activity', 'assigned_to',
|
||||||
...(getEventSlug==='all'?['event']:[])]"
|
...(getEventSlug==='all'?['event']:[])]"
|
||||||
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
|
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
|
||||||
'pending_physical_confirmation','pending_return','pending_postponed','pending_suspected_spam'].map(stateInfo)">
|
'pending_physical_confirmation','pending_return','pending_postponed'].map(stateInfo)">
|
||||||
<template #section_header="{index, section, count}">
|
<template #section_header="{index, section, count}">
|
||||||
{{ section.text }} <span class="badge badge-light ml-1">{{ count }}</span>
|
{{ section.text }} <span class="badge badge-light ml-1">{{ count }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
:columns="['id', 'name', 'last_activity', 'assigned_to',
|
:columns="['id', 'name', 'last_activity', 'assigned_to',
|
||||||
...(getEventSlug==='all'?['event']:[])]"
|
...(getEventSlug==='all'?['event']:[])]"
|
||||||
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
|
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
|
||||||
'pending_physical_confirmation','pending_return','pending_postponed','pending_suspected_spam'].map(stateInfo)">
|
'pending_physical_confirmation','pending_return','pending_postponed'].map(stateInfo)">
|
||||||
<template #section_header="{index, section, count}">
|
<template #section_header="{index, section, count}">
|
||||||
{{ section.text }} <span class="badge badge-light ml-1">{{ count }}</span>
|
{{ section.text }} <span class="badge badge-light ml-1">{{ count }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Add table
Reference in a new issue