mail: feed from MaarchCourrier (#22550)

This commit is contained in:
Benjamin Dauvergne 2018-05-19 16:38:57 +02:00
parent fc5a4900ef
commit a4ae8845e9
13 changed files with 737 additions and 16 deletions

2
debian/control vendored
View File

@ -11,7 +11,7 @@ Architecture: all
Depends: ${misc:Depends}, ${python:Depends},
python-django (>= 1.7),
python-gadjo,
python-requests,
python-requests (>= 2.11),
python-django-haystack (>= 2.4.0),
python-django-reversion (>= 2.0.12),
python-django-taggit (>= 0.17.4),

View File

@ -106,6 +106,7 @@ setup(
'requests',
'whoosh',
'XStatic-Select2',
'python-dateutil',
],
zip_safe=False,
cmdclass={

51
tests/conftest.py Normal file
View File

@ -0,0 +1,51 @@
# welco - multichannel request processing
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
import django_webtest
@pytest.fixture
def app(request):
wtm = django_webtest.WebTestMixin()
wtm._patch_settings()
request.addfinalizer(wtm._unpatch_settings)
return django_webtest.DjangoTestApp()
@pytest.fixture
def user(db):
from django.contrib.auth.models import User
user = User.objects.create(username='toto')
user.set_password('toto')
user.save()
return user
@pytest.fixture
def mail_group(db, settings, user):
from django.contrib.auth.models import Group
# add mail group to default user
group = Group.objects.create(name='mail')
user.groups.add(group)
# define authorization of mail group on mail channel
channel_roles = getattr(settings, 'CHANNEL_ROLES', {})
mail_roles = channel_roles.setdefault('mail', [])
mail_roles.append('mail')
return group

249
tests/test_source_maarch.py Normal file
View File

@ -0,0 +1,249 @@
# welco - multichannel request processing
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import pytest
from httmock import urlmatch, HTTMock
class BaseMock(object):
def __init__(self, netloc):
self.netloc = netloc
self.clear()
def clear(self):
self.requests = []
self.responses = []
def next_response(self):
response, self.responses = self.responses[0], self.responses[1:]
return response
@property
def ctx_manager(self):
'''Create an HTTMock context manager for all endpoints of a mocked Maarch instance'''
endpoints = []
for attribute, value in self.__class__.__dict__.items():
if hasattr(value, 'path'):
value = getattr(self, attribute)
match_decorator = urlmatch(netloc=self.netloc, path=value.path)
print value, self.netloc, '^/rest' + value.path
endpoints.append(match_decorator(value))
return HTTMock(*endpoints)
class MaarchMock(BaseMock):
def list_endpoint(self, url, request):
self.requests.append(('list_endpoint', url, request, json.loads(request.body)))
return {
'content': json.dumps(self.next_response()),
'headers': {
'content-type': 'application/json',
},
}
list_endpoint.path = '^/rest/res/list$'
def update_external_infos(self, url, request):
self.requests.append(('update_external_infos', url, request, json.loads(request.body)))
return json.dumps({})
update_external_infos.path = '^/rest/res/externalInfos$'
def update_status(self, url, request):
self.requests.append(('update_status', url, request, json.loads(request.body)))
return {
'content': json.dumps(self.next_response()),
'headers': {
'content-type': 'application/json',
},
}
update_status.path = '^/rest/res/resource/status$'
def post_courrier(self, url, request):
self.requests.append(('post_courrier', url, request, json.loads(request.body)))
post_courrier.path = '^/rest/res$'
@pytest.fixture
def maarch(settings, mail_group):
# configure maarch server
settings.MAARCH_FEED = {
'ENABLE': True,
'URL': 'http://maarch.example.net/',
'USERNAME': 'admin',
'PASSWORD': 'admin',
}
return MaarchMock('maarch.example.net')
class WcsMock(BaseMock):
def api_formdefs(self, url, request):
return json.dumps({
'data': [{
'slug': 'slug1',
'title': 'title1',
}]
})
api_formdefs.path = '^/api/formdefs/$'
def json(self, url, request):
return json.dumps({
'data': [{
'slug': 'slug1',
'title': 'title1',
'category': 'category1',
}]
})
json.path = '^/json$'
def api_formdefs_slug1_schema(self, url, request):
return json.dumps({
})
api_formdefs_slug1_schema.path = '^/api/formdefs/slug-1/schema$'
def api_formdefs_slug1_submit(self, url, request):
return json.dumps({
'err': 0,
'data': {
'id': 1,
'backoffice_url': 'http://wcs.example.net/slug-1/1',
},
})
api_formdefs_slug1_submit.path = '^/api/formdefs/slug-1/submit$'
@pytest.fixture
def wcs(settings):
settings.KNOWN_SERVICES = {
'wcs': {
'demarches': {
'url': 'http://wcs.example.net/',
}
}
}
return WcsMock('wcs.example.net')
def test_utils(maarch):
from welco.sources.mail.utils import get_maarch
welco_maarch_obj = get_maarch()
assert welco_maarch_obj.url == 'http://maarch.example.net/'
assert welco_maarch_obj.username == 'admin'
assert welco_maarch_obj.password == 'admin'
assert welco_maarch_obj.grc_status == 'GRC'
assert welco_maarch_obj.grc_received_status == 'GRC_TRT'
assert welco_maarch_obj.grc_send_status == 'GRCSENT'
assert welco_maarch_obj.grc_refused_status == 'GRCREFUSED'
PDF_MOCK = '%PDF-1.4 ...'
def test_feed(app, maarch, wcs, user):
import base64
from django.core.management import call_command
from django.contrib.contenttypes.models import ContentType
from welco.sources.mail.models import Mail
app.set_user(user.username)
response = app.get('/').follow()
# no mail
assert len(response.pyquery('li[data-external-id]')) == 0
# feed mails from maarch
with maarch.ctx_manager:
# list request
maarch.responses.append({
'resources': [
{
'res_id': 1,
'fileBase64Content': base64.b64encode(PDF_MOCK),
}
],
})
# update status request
maarch.responses.append({})
# last list request
maarch.responses.append({'resources': []})
call_command('feed_mail_maarch')
assert len(maarch.requests) == 3
assert maarch.requests[0][3] == {
'select': '*',
'clause': "status='GRC'",
'withFile': True,
'orderBy': ['res_id'],
'limit': 10,
}
assert maarch.requests[1][3] == {
'resId': [1],
'status': 'GRC_TRT',
}
assert maarch.requests[2][3] == {
'select': '*',
'clause': "status='GRC'",
'withFile': True,
'orderBy': ['res_id'],
'limit': 10,
}
response = app.get('/').follow()
# new mail is visible
assert len(response.pyquery('li[data-external-id]')) == 1
assert len(response.pyquery('li[data-external-id=maarch-1]')) == 1
# start qualification
maarch.clear()
pk = Mail.objects.get().pk
with wcs.ctx_manager, maarch.ctx_manager:
source_type = str(ContentType.objects.get_for_model(Mail).pk),
source_pk = str(pk)
response = app.get('/ajax/qualification', params={
'source_type': source_type,
'source_pk': source_pk,
})
assert len(response.pyquery('a[data-association-pk]')) == 0
response = app.post('/ajax/qualification', params={
'source_type': source_type,
'source_pk': str(pk),
'formdef_reference': 'demarches:slug-1',
})
# verify qualification was done
assert len(response.pyquery('a[data-association-pk]')) == 1
association_pk = response.pyquery('a[data-association-pk]')[0].attrib['data-association-pk']
response = app.post('/ajax/create-formdata/%s' % association_pk)
assert len(maarch.requests) == 1
assert maarch.requests[0][3] == {
'status': 'GRCSENT',
'externalInfos': [
{
'external_id': 1,
'external_link': 'http://wcs.example.net/slug-1/1',
'res_id': 1,
}
]
}
def test_command_is_noop():
from django.core.management import call_command
call_command('feed_mail_maarch')

View File

@ -1,5 +1,5 @@
# welco - multichannel request processing
# Copyright (C) 2015 Entr'ouvert
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
@ -28,16 +28,6 @@ from welco.sources.phone import models
pytestmark = pytest.mark.django_db
@pytest.fixture
def user():
from django.contrib.auth.models import User
user = User.objects.create(username='toto')
user.set_password('toto')
user.save()
return user
def test_call_start_stop(client):
assert models.PhoneCall.objects.count() == 0
payload = {

View File

@ -1,5 +1,5 @@
# welco - multichannel request processing
# Copyright (C) 2015 Entr'ouvert
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
@ -16,6 +16,7 @@
import django.apps
class AppConfig(django.apps.AppConfig):
name = 'welco.sources.mail'
@ -23,4 +24,29 @@ class AppConfig(django.apps.AppConfig):
from . import urls
return urls.urlpatterns
def ready(self):
from welco.qualif.models import Association
from django.db.models import signals
signals.post_save.connect(self.association_post_save,
sender=Association)
def association_post_save(self, sender, instance, **kwargs):
from .utils import get_maarch
if not instance.formdata_id:
return
source = instance.source
if not hasattr(source, 'external_id'):
return
external_id = source.external_id
if not external_id.startswith('maarch-'):
return
maarch_pk = int(external_id.split('-', 1)[-1])
maarch = get_maarch()
maarch.set_grc_sent_status(
mail_pk=maarch_pk,
formdata_id=instance.formdata_id,
formdata_url_backoffice=instance.formdata_url_backoffice)
default_app_config = 'welco.sources.mail.AppConfig'

View File

@ -0,0 +1,233 @@
# welco - multichannel request processing
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urlparse
import base64
from dateutil.parser import parse as parse_datetime
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
class MaarchError(Exception):
pass
class MaarchCourrier(object):
url = None
username = None
password = None
default_limit = 100
max_retries = 3
def __init__(self, url, username, password):
self.url = url
self.username = username
self.password = password
def __repr__(self):
return '<MaarchCourrier url:%s>' % self.url
class Courrier(object):
content = None
format = None
status = None
def __init__(self, maarch_courrier, **kwargs):
self.maarch_courrier = maarch_courrier
self.pk = kwargs.pop('res_id', None)
# decode file content
if 'fileBase64Content' in kwargs:
kwargs['content'] = base64.b64decode(kwargs.pop('fileBase64Content'))
# decode date fields
for key in kwargs:
if key.endswith('_date') and kwargs[key]:
kwargs[key] = parse_datetime(kwargs[key])
self.__dict__.update(kwargs)
def __repr__(self):
descriptions = []
for key in ['pk', 'status']:
if getattr(self, key, None):
descriptions.append('%s:%s' % (key, getattr(self, key)))
return '<Courrier %s>' % ' '.join(descriptions)
@classmethod
def new_with_file(cls, maarch_courrier, content, format, status, **kwargs):
if hasattr(content, 'read'):
content = content.read()
else:
content = content
return cls(maarch_courrier, content=content, format=format, status=status, **kwargs)
def post_serialize(self):
payload = {}
assert self.content
assert self.status
payload['encodedFile'] = base64.b64encode(self.content)
payload['collId'] = 'letterbox_coll'
payload['table'] = 'res_letterbox'
payload['fileFormat'] = self.format
payload['data'] = d = []
excluded_keys = ['content', 'format', 'status', 'maarch_courrier', 'pk']
data = {key: self.__dict__[key] for key in self.__dict__ if key not in excluded_keys}
if data:
for key, value in data.iteritems():
if isinstance(value, basestring):
d.append({'column': key, 'value': value, 'type': 'string'})
elif isinstance(value, int):
d.append({'column': key, 'value': str(value), 'type': 'int'})
else:
raise NotImplementedError
payload['status'] = self.status
return payload
def get_serialize(self):
d = {'res_id': self.pk}
for key in self.__dict__:
if key in ['pk', 'maarch_courrier']:
continue
value = getattr(self, key)
if key == 'content':
value = base64.b64encode(value)
key = 'fileBase64Content'
if key.endswith('_date'):
value = value.isoformat()
d[key] = value
return d
def new_courrier_with_file(self, content, format, status, **kwargs):
return self.Courrier.new_with_file(self, content, format, status, **kwargs)
@property
def session(self):
s = requests.Session()
if self.username and self.password:
s.auth = (self.username, self.password)
retry = Retry(
total=self.max_retries,
read=self.max_retries,
connect=self.max_retries,
backoff_factor=0.5,
status_forcelist=(500, 502, 504)
)
adapter = HTTPAdapter(max_retries=retry)
s.mount('http://', adapter)
s.mount('https://', adapter)
return s
def post_json(self, url, payload, verb='post'):
try:
method = getattr(self.session, verb)
response = method(url, json=payload)
except requests.RequestException as e:
raise MaarchError('HTTP request to maarch failed', e, payload)
try:
response.raise_for_status()
except requests.RequestException as e:
raise MaarchError('HTTP request to maarch failed', e, payload, repr(response.content[:1000]))
try:
response_payload = response.json()
except ValueError:
raise MaarchError('maarch returned non-JSON data', repr(response.content[:1000]), payload)
return response_payload
def put_json(self, url, payload):
return self.post_json(url, payload, verb='put')
@property
def list_url(self):
return urlparse.urljoin(self.url, 'rest/res/list')
@property
def update_external_infos_url(self):
return urlparse.urljoin(self.url, 'rest/res/externalInfos')
@property
def update_status_url(self):
return urlparse.urljoin(self.url, 'rest/res/resource/status')
@property
def post_courrier_url(self):
return urlparse.urljoin(self.url, 'rest/res')
def get_courriers(self, clause, fields=None, limit=None, include_file=False, order_by=None):
if fields:
# res_id is mandatory
fields = set(fields)
fields.add('res_id')
fields = ','.join(fields) if fields else '*'
limit = limit or self.default_limit
order_by = order_by or []
response = self.post_json(self.list_url, {
'select': fields,
'clause': clause,
'limit': limit,
'withFile': include_file,
'orderBy': order_by,
})
if not hasattr(response.get('resources'), 'append'):
raise MaarchError('missing resources field or bad type', response)
return [self.Courrier(self, **resource) for resource in response['resources']]
def update_external_infos(self, courriers, status):
if not courriers:
return
external_infos = []
payload = {
'externalInfos': external_infos,
'status': status,
}
for courrier in courriers:
assert courrier.pk, 'courrier must already exist in Maarch and have a pk'
external_info = {'res_id': courrier.pk}
if getattr(courrier, 'external_id', None):
external_info['external_id'] = courrier.external_id
if getattr(courrier, 'external_link', None):
external_info['external_link'] = courrier.external_link
external_infos.append(external_info)
response = self.put_json(self.update_external_infos_url, payload)
if 'errors' in response:
raise MaarchError('update_external_infos failed with errors', response['errors'], response)
def update_status(self, courriers, status, history_message=None):
if not courriers:
return
res_ids = []
for courrier in courriers:
assert courrier.pk
res_ids.append(courrier.pk)
payload = {
'status': status,
'resId': res_ids,
}
if history_message:
payload['historyMessage'] = history_message
response = self.put_json(self.update_status_url, payload)
if 'errors' in response:
raise MaarchError('update_status failed with errors', response['errors'], response)
def post_courrier(self, courrier):
response = self.post_json(self.post_courrier_url, courrier.post_serialize())
if 'errors' in response:
raise MaarchError('update_external_infos failed with errors', response['errors'], response)
if 'resId' not in response:
raise MaarchError('update_external_infos failed with errors, missing resId', response)
courrier.pk = response['resId']
return courrier

View File

@ -0,0 +1,61 @@
# welco - multichannel request processing
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from optparse import make_option
import os
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from django.db import transaction
from ...models import Mail
from ...utils import get_maarch
class Command(BaseCommand):
"""Inject mail coming from Maarch into welco.
Only mail with a status "GRC" are injected,
After injection, their status is immediately changed to "GRC_TRT".
After injection in w.c.s., their status is changed to "GRCSENT" and an
id and an URL of the request in w.c.s. is attached to the mail in
Maarch.
"""
def handle(self, *args, **kwargs):
verbosity = kwargs['verbosity']
maarch = get_maarch()
if not maarch:
if verbosity > 1:
self.stdout.write('Maarch is not configured.')
return
maarch_mails = maarch.get_mails()
count = 0
while maarch_mails:
with transaction.atomic():
for maarch_mail in maarch_mails:
Mail.objects.create(
content=ContentFile(maarch_mail.content, name='maarch-%s' % maarch_mail.pk),
external_id='maarch-%s' % str(maarch_mail.pk), # res_id
)
# update maarch inside transaction, if it fails all imports will be
# rollbacked
maarch.set_grc_received_status(maarch_mails)
count += len(maarch_mails)
maarch_mails = maarch.get_mails()
if verbosity > 1:
self.stdout.write('Injected %d mails from %s.' % (count, maarch.url))

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mail', '0011_mail_reference'),
]
operations = [
migrations.AddField(
model_name='mail',
name='external_id',
field=models.CharField(max_length=32, null=True, verbose_name='External Id'),
),
]

View File

@ -1,5 +1,5 @@
# welco - multichannel request processing
# Copyright (C) 2015 Entr'ouvert
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
@ -42,6 +42,7 @@ class Mail(models.Model):
registered_mail_number = models.CharField(_('Registered Mail Number'),
null=True, max_length=50)
note = models.TextField(_('Note'), null=True)
external_id = models.CharField(_('External Id'), null=True, max_length=32)
# used only if settings.FLAVOURS contains 'alfortville'
reference = models.CharField(_('Reference'), null=True, max_length=30)

View File

@ -13,6 +13,7 @@
data-registered-mail-number="{{ mail.registered_mail_number|default:"" }}"
data-reference="{{ mail.reference|default:"" }}"
data-subject="{{ mail.subject|default:"" }}"
data-external-id="{{ mail.external_id|default:"" }}"
>{{ mail.creation_timestamp|date:"d/m/Y" }}
{{mail.contact_name}}
{% for association in mail.associations.all %}

View File

@ -0,0 +1,72 @@
# welco - multichannel request processing
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from .maarch import MaarchCourrier
class WelcoMaarchCourrier(MaarchCourrier):
def __init__(self, url, username, password, grc_status,
grc_received_status, grc_send_status, grc_refused_status,
batch_size=10):
super(WelcoMaarchCourrier, self).__init__(url, username, password)
self.grc_status = grc_status
self.grc_received_status = grc_received_status
self.grc_send_status = grc_send_status
self.grc_refused_status = grc_refused_status
self.batch_size = batch_size
def get_mails(self):
return self.get_courriers(
clause="status='%s'" % self.grc_status,
include_file=True,
order_by=['res_id'],
limit=self.batch_size)
def get_mail(self, mail_id):
return self.get_courriers(clause="res_id=%s" % mail_id)[0]
def set_grc_received_status(self, mails):
self.update_status(mails, self.grc_received_status)
def set_grc_sent_status(self, mail_pk, formdata_id, formdata_url_backoffice):
mail = self.Courrier(self, pk=mail_pk)
mail.external_id = formdata_id
mail.external_link = formdata_url_backoffice
self.update_external_infos([mail], self.grc_send_status)
def set_grc_refused_status(self, mail_pk):
mail = self.Courrier(self, pk=mail_pk)
self.update_status([mail], self.grc_refused_status)
def get_maarch():
config = getattr(settings, 'MAARCH_FEED', {})
if not config.get('ENABLE'):
return
url = config['URL']
username = config['USERNAME']
password = config['PASSWORD']
return WelcoMaarchCourrier(
url=url,
username=username,
password=password,
grc_status=config.get('STATUS_GRC', 'GRC'),
grc_received_status=config.get('STATUS_RECEIVED', 'GRC_TRT'),
grc_send_status=config.get('STATUS_SEND', 'GRCSENT'),
grc_refused_status=config.get('STATUS_REFUSED', 'GRCREFUSED'))

View File

@ -1,5 +1,5 @@
# welco - multichannel request processing
# Copyright (C) 2015 Entr'ouvert
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import logging
from django import template
from django.contrib.auth.decorators import login_required
@ -26,11 +27,16 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from django.db.transaction import atomic
from welco.utils import response_for_json
from .models import Mail
from .forms import MailQualificationForm
from .utils import get_maarch
logger = logging.getLogger(__name__)
def viewer(request, *args, **kwargs):
if not 'file' in request.GET:
@ -124,7 +130,18 @@ def note(request, *args, **kwargs):
@login_required
@csrf_exempt
def reject(request, *args, **kwargs):
Mail.objects.filter(id=request.POST['source_pk']).delete()
maarch = get_maarch()
mail = Mail.objects.filter(id=request.POST['source_pk']).first()
if mail:
try:
with atomic():
if maarch and mail.external_id and mail.external_id.startswith('maarch-'):
mail_pk = mail.external_id.split('-', 1)[1]
maarch.set_grc_refused_status(mail_pk)
mail.delete()
except Exception:
logger.exception('rejection request to maarch failed')
messages.error(request, _('Rejection request to Maarch failed'))
return HttpResponse()