Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F7124709
D7326.id26549.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
5 KB
Subscribers
None
D7326.id26549.diff
View Options
diff --git a/swh/web/add_forge_now/models.py b/swh/web/add_forge_now/models.py
--- a/swh/web/add_forge_now/models.py
+++ b/swh/web/add_forge_now/models.py
@@ -8,7 +8,17 @@
import enum
from typing import List
+from django.core.signing import Signer
from django.db import models
+from django.utils.crypto import constant_time_compare
+
+# signal_receivers imports this module, don't expand this import or you'll get a
+# circular import
+from . import signal_receivers
+from ..config import get_config
+from ..inbound_email.signals import email_received
+
+email_received.connect(signal_receivers.handle_inbound_message)
class RequestStatus(enum.Enum):
@@ -97,3 +107,40 @@
forge_contact_comment = models.TextField(
help_text="Where did you find this contact information (url, ...)",
)
+
+ ADDRESS_SIGNER_SEP = "."
+
+ @classmethod
+ def get_signer(cls):
+ return Signer(salt="add_forge_now_request", sep=cls.ADDRESS_SIGNER_SEP)
+
+ def get_inbound_address(self):
+ """Get the email address that will be able to receive messages to be logged in
+ this request."""
+
+ base_address = get_config()["add_forge_now"]["email_address"]
+
+ username, domain = base_address.split("@")
+
+ extension = self.get_signer().sign(self.pk)
+
+ return f"{username}+{extension}@{domain}"
+
+ @classmethod
+ def verify_address_extension(cls, extension: str) -> int:
+ """Retrieve the primary key of the request for the given inbound address extension.
+
+ We reimplement `Signer.unsign`, because the extension can be casemapped at any
+ point in the email chain (even though email is, theoretically, case sensitive),
+ so we have to compare lowercase versions of both the extension and the
+ signature...
+
+ Raises ValueError if the signature couldn't be verified.
+
+ """
+ value, signature = extension.rsplit(cls.ADDRESS_SIGNER_SEP, 1)
+ expected_signature = cls.get_signer().signature(value)
+ if not constant_time_compare(signature.lower(), expected_signature.lower()):
+ raise ValueError(f"Invalid signature in extension {extension}")
+
+ return int(value)
diff --git a/swh/web/add_forge_now/signal_receivers.py b/swh/web/add_forge_now/signal_receivers.py
new file mode 100644
--- /dev/null
+++ b/swh/web/add_forge_now/signal_receivers.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2022 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU Affero General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from email.message import EmailMessage
+import logging
+from typing import Optional, Type
+
+# models imports this module, don't expand this import or you'll get a circular import
+from . import models
+from ..config import get_config
+from ..inbound_email.signals import EmailProcessingStatus
+from ..inbound_email.utils import recipient_matches
+
+logger = logging.getLogger(__name__)
+
+
+def handle_inbound_message(sender: Type, **kwargs) -> EmailProcessingStatus:
+ """Handle inbound email messages for add forge now.
+
+ # How do we determine that the inbound message is for us (To/Cc/Received)?
+
+ # How do we extract the RequestHistory fields out of the email:
+ # - text
+ # - actor
+ # - actor_role
+ # - new_status (?)
+
+ """
+ message = kwargs["message"]
+ assert isinstance(message, EmailMessage)
+
+ match_address = get_config()["add_forge_now"]["email_address"]
+
+ matches = recipient_matches(message, match_address)
+ if not matches:
+ return EmailProcessingStatus.IGNORED
+
+ request_pk: Optional[int] = None
+ for match in matches:
+ extension = match.extension
+ if extension is None:
+ logger.debug(
+ "Recipient address %s cannot be matched to a request, ignoring",
+ match.recipient.addr_spec,
+ )
+ continue
+ try:
+ current_pk = models.Request.verify_address_extension(extension)
+ except ValueError:
+ logger.debug(
+ "Recipient address %s failed validation", match.recipient.addr_spec
+ )
+ continue
+
+ if request_pk is not None and request_pk != current_pk:
+ logger.debug(
+ "Recipient address %s inconsistent with earlier valid address with "
+ "pk=%s, failing",
+ match.recipient.addr_spec,
+ request_pk,
+ )
+ return EmailProcessingStatus.FAILED
+
+ request_pk = current_pk
+
+ if request_pk is None:
+ # No matches had a valid extension, unable to process message
+ return EmailProcessingStatus.FAILED
+
+ request = models.Request.objects.get(pk=request_pk)
+
+ print(request)
+
+ return EmailProcessingStatus.PROCESSED
diff --git a/swh/web/config.py b/swh/web/config.py
--- a/swh/web/config.py
+++ b/swh/web/config.py
@@ -127,6 +127,7 @@
"staging_server_names": ("list", SWH_WEB_STAGING_SERVER_NAMES),
"instance_name": ("str", "archive-test.softwareheritage.org"),
"give": ("dict", {"public_key": "", "token": ""}),
+ "add_forge_now": ("dict", {"email_address": "add-forge-now@example.com"}),
}
swhweb_config: Dict[str, Any] = {}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Dec 21 2024, 5:42 PM (11 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3229381
Attached To
D7326: Hook up processing of inbound emails for add_forge_now
Event Timeline
Log In to Comment