list_id:
type: string
description: Mailing list identifier for project.
+ subject_match:
+ type: string
+ description: Regex used for email filtering.
list_email:
type: string
description: Mailing list email address for project.
class Meta:
model = Project
fields = ('id', 'url', 'name', 'link_name', 'list_id', 'list_email',
- 'web_url', 'scm_url', 'webscm_url', 'maintainers')
- read_only_fields = ('name', 'maintainers')
+ 'web_url', 'scm_url', 'webscm_url', 'maintainers',
+ 'subject_match')
+ read_only_fields = ('name', 'maintainers', 'subject_match')
extra_kwargs = {
'url': {'view_name': 'api-project-detail'},
}
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.8 on 2018-01-19 18:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('patchwork', '0021_django_1_10_fixes'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='project',
+ name='subject_match',
+ field=models.CharField(blank=True, default=b'', help_text=b'Regex to match the subject against if only part of emails sent to the list belongs to this project. Will be used with IGNORECASE and MULTILINE flags. If rules for more projects match the first one returned from DB is chosen; empty field serves as a default for every email which has no other match.', max_length=64),
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='listid',
+ field=models.CharField(max_length=255),
+ ),
+ migrations.AlterUniqueTogether(
+ name='project',
+ unique_together=set([('listid', 'subject_match')]),
+ ),
+ ]
linkname = models.CharField(max_length=255, unique=True)
name = models.CharField(max_length=255, unique=True)
- listid = models.CharField(max_length=255, unique=True)
+ listid = models.CharField(max_length=255)
listemail = models.CharField(max_length=200)
+ subject_match = models.CharField(
+ max_length=64, blank=True, default='', help_text='Regex to match the '
+ 'subject against if only part of emails sent to the list belongs to '
+ 'this project. Will be used with IGNORECASE and MULTILINE flags. If '
+ 'rules for more projects match the first one returned from DB is '
+ 'chosen; empty field serves as a default for every email which has no '
+ 'other match.')
# url metadata
return self.name
class Meta:
+ unique_together = (('listid', 'subject_match'),)
ordering = ['linkname']
return normalise_space(header_str)
-def find_project_by_id(list_id):
- """Find a `project` object with given `list_id`."""
- project = None
- try:
- project = Project.objects.get(listid=list_id)
- except Project.DoesNotExist:
- logger.debug("'%s' if not a valid project list-id", list_id)
- return project
+def find_project_by_id_and_subject(list_id, subject):
+ """Find a `project` object based on `list_id` and subject match.
+ Since empty `subject_match` field matches everything, project with
+ given `list_id` and empty `subject_match` field serves as a default
+ (in case it exists) if no other match is found.
+ """
+ projects = Project.objects.filter(listid=list_id)
+ default = None
+ for project in projects:
+ if not project.subject_match:
+ default = project
+ elif re.search(project.subject_match, subject,
+ re.MULTILINE | re.IGNORECASE):
+ return project
+
+ return default
-def find_project_by_header(mail):
+def find_project(mail, list_id=None):
+ clean_subject = clean_header(mail.get('Subject', ''))
+
+ if list_id:
+ return find_project_by_id_and_subject(list_id, clean_subject)
+
project = None
listid_res = [re.compile(r'.*<([^>]+)>.*', re.S),
re.compile(r'^([\S]+)$', re.S)]
listid = match.group(1)
- project = find_project_by_id(listid)
+ project = find_project_by_id_and_subject(listid, clean_subject)
if project:
break
if not project:
- logger.debug("Could not find a list-id in mail headers")
+ logger.debug("Could not find a valid project for given list-id and "
+ "subject.")
return project
logger.debug("Ignoring email due to 'ignore' hint")
return
- if list_id:
- project = find_project_by_id(list_id)
- else:
- project = find_project_by_header(mail)
+ project = find_project(mail, list_id)
if project is None:
logger.error('Failed to find a project for email')
from patchwork.parser import clean_subject
from patchwork.parser import find_author
from patchwork.parser import find_patch_content as find_content
-from patchwork.parser import find_project_by_header
+from patchwork.parser import find_project
from patchwork.parser import find_series
from patchwork.parser import parse_mail as _parse_mail
from patchwork.parser import parse_pull_request
def test_no_list_id(self):
email = MIMEText('')
- project = find_project_by_header(email)
+ project = find_project(email)
self.assertEqual(project, None)
def test_blank_list_id(self):
email = MIMEText('')
email['List-Id'] = ''
- project = find_project_by_header(email)
+ project = find_project(email)
self.assertEqual(project, None)
def test_whitespace_list_id(self):
email = MIMEText('')
email['List-Id'] = ' '
- project = find_project_by_header(email)
+ project = find_project(email)
self.assertEqual(project, None)
def test_substring_list_id(self):
email = MIMEText('')
email['List-Id'] = 'example.com'
- project = find_project_by_header(email)
+ project = find_project(email)
self.assertEqual(project, None)
def test_short_list_id(self):
is only the list ID itself (without enclosing angle-brackets). """
email = MIMEText('')
email['List-Id'] = self.project.listid
- project = find_project_by_header(email)
+ project = find_project(email)
self.assertEqual(project, self.project)
def test_long_list_id(self):
email = MIMEText('')
email['List-Id'] = 'Test text <%s>' % self.project.listid
- project = find_project_by_header(email)
+ project = find_project(email)
self.assertEqual(project, self.project)
--- /dev/null
+---
+features:
+ - |
+ Allow list filtering into multiple projects (and email dropping) based on
+ subject prefixes. Enable by specifying a regular expression which needs to
+ be matched in the subject on a per-project basis (field ``subject_match``).
+ Project with empty ``subject_match`` field (and matching ``list_id``)
+ serves as a default in case of no match.
+api:
+ - |
+ The ``/project`` endpoint now exposes a ``subject_match`` attribute.