from .decorators import *
from .misc import Object
+# Setup logging
+log = logging.getLogger(__name__)
+
# Set the client keytab name
os.environ["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
return self.backend.groups.get_by_gid("promotional-consent")
-
class Account(LDAPObject):
def __str__(self):
if self.nickname:
# Disable the user
await user.disable(text)
+ # Bounces
+
+ def process_bounce(self, msg):
+ """
+ Called when a bounce has been received for this account
+ """
+ # Determine the bounce score
+ score = self._determine_bounce_score(msg)
+
+ # Log action
+ log.info("Received bounce message for %s (%s) with score %s" % (self, self.uid, score))
+
+ # Store the bounce message
+ self.db.execute("""
+ INSERT INTO
+ account_bounces
+ (
+ uid, score, message
+ )
+ VALUES
+ (
+ %s, %s, %s
+ )
+ """, self.uid, score, msg.as_string(),
+ )
+
+ # Fetch the total bounce score
+ total_score = self.get_bounce_score()
+
+ # Log the total score
+ log.info("%s (%s) has a total bounce score of %s" % (self, self.uid, total_score))
+
+ def _determine_bounce_score(self, msg):
+ """
+ Returns the bounce score from the message
+ """
+ # We can only process multi-part messages
+ if not msg.is_multipart():
+ return 3
+
+ # Walk through all parts
+ for part in msg.walk():
+ # Fetch the content type
+ content_type = part.get_content_type()
+
+ # We only care about delivery status notifications
+ if not content_type == "message/delivery-status":
+ continue
+
+ # Fetch the payload
+ payload = part.get_payload()
+
+ # We would expect a couple of lists here
+ if not isinstance(payload, list):
+ break
+
+ # Walk through all blocks in the payload
+ for i, block in enumerate(payload):
+ # The first block contains message information we don't care about
+ if i == 0:
+ continue
+
+ # Fetch the status
+ status = block.get("Status")
+
+ # Fail if we don't have a status
+ if not status:
+ break
+
+ # Hard bounce
+ elif status.startswith("5."):
+ return 3
+
+ # Soft bounce
+ elif status.startswith("4."):
+ return 1
+
+ # If we cannot be sure, we return a hard bounce
+ return 3
+
+ def get_bounce_score(self, days=60):
+ """
+ Returns the current bounce score of this account
+ """
+ cutoff = datetime.timedelta(days=days)
+
+ # Fetch the score
+ res = self.db.get("""
+ SELECT
+ SUM(score) AS score
+ FROM
+ account_bounces
+ WHERE
+ uid = %s
+ AND
+ received_at >= CURRENT_TIMESTAMP - %s
+ """, self.uid, cutoff,
+ )
+
+ # Return the score or zero
+ return res.score if res else 0
+
class Groups(Object):
hidden_groups = (
"scan-files" : self.releases.scan_files,
"send-message" : self.messages.send_cli,
"send-all-messages" : self.messages.queue.send_all,
+ "recv-message" : self.messages.recv,
"test-ldap" : self.accounts.test_ldap,
"toot" : self.toots.toot,
"update-blog-feeds" : self.blog.update_feeds,
import email
import email.mime.multipart
import email.mime.text
+import email.policy
import email.utils
import logging
import mimetypes
import smtplib
import socket
import subprocess
+import sys
import tornado.locale
import tornado.template
from . import util
from .decorators import *
+# Setup logging
+log = logging.getLogger(__name__)
+
# Encode emails in UTF-8 by default
email.charset.add_charset("utf-8", email.charset.SHORTEST, email.charset.QP, "utf-8")
return self.send_template(template, **kwargs)
+ # Receive a message
+
+ async def recv(self):
+ """
+ Receives an email and then tries to do something useful with it
+ """
+ # Parse the message
+ msg = email.message_from_file(sys.stdin, policy=email.policy.default)
+
+ # Handle bounces
+ if self._is_bounce(msg):
+ self._recv_bounce(msg)
+
+ def _is_bounce(self, msg):
+ """
+ Checks and returns True if the given message is a bounce message
+ """
+ recipient = msg.get("To")
+
+ # If this being sent to our bounces address,
+ # this is likely going to be a bounce message.
+ if recipient.startswith("bounces+"):
+ return True
+
+ # This does not seem to be a bounce message
+ return False
+
+ def _recv_bounce(self, msg):
+ """
+ We have received a bounce message...
+ """
+ log.debug("Received bounce message:\n%s" % msg)
+
+ # Fetch the recipient
+ recipient = msg.get("To")
+
+ # Remove the prefix
+ recipient = recipient.removeprefix("bounces+")
+
+ # Split off the domain
+ recipient, _, domain = recipient.rpartition("@")
+
+ # Decode the email address
+ recipient = recipient.replace("=", "@")
+
+ # Fail if there is no recipient
+ if not recipient:
+ log.error("Failed to parse the recipient '%s'" % msg.get("To"))
+ return
+
+ # Find the account
+ account = self.backend.accounts.get_by_mail(recipient)
+
+ # Silently fail if we could not find an account (might have been deleted already)
+ if not account:
+ log.debug("Could not find an account matching '%s'" % recipient)
+ return
+
+ # Pass the message to the account
+ account.process_bounce(msg)
+
class Queue(misc.Object):
@property