Example #1
0
def handle_command(command_address, msg):
    auto_submitted = msg_get_header(msg, 'auto-submitted')
    if auto_submitted and auto_submitted.lower() != 'no':
        # Auto-submitted: header, https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml
        print("Message appears to be automatically generated ({}), so ignoring it.".format(auto_submitted))
        return

    # Grab the address to which to respond and the subject
    reply_to = msg_get_response_address(msg)
    if reply_to is None:
        print("Failed to get an email address from the Reply-To, From, or Sender headers.")
        return
    subject = msg_get_header(msg, 'subject').replace('\n', '').replace('\r', '')
    print("Subject: " + subject)
    print("Responding to: " + reply_to)

    # Strip off any re:, fwd:, etc. (everything up to the last :, then trim whitespace)
    if ':' in subject:
        subject = subject[subject.rfind(':') + 1:]
    subject = subject.strip()

    try:
        cmd = get_signed_command(subject, reply_to)
    except ExpiredSignatureException:
        # TODO (maybe): Reply to the sender to tell them the signature was expired?  Or send a newly-signed message?
        print("Expired signature.")
        return
    except InvalidSignatureException:
        # Do nothing.
        print("Invalid signature.")
        return
    except NotSignedException:
        # If the subject isn't a signed command...
        # TODO (maybe): ... check if the reply_to is allowed to run the specific command with the given parameters...
        # ... and reply with a signed command for the recipient to send back (by replying).
        print("Signing command: {}".format(subject))
        response = send_response(
                source=command_address,
                destination=reply_to,
                subject='Re: {}'.format(sign(subject, reply_to)),
                body='To confirm and execute the command, please reply to this email.',
                )
        return

    # TODO (maybe): allow commands in body?
    
    # Execute the command.
    output = run(user=reply_to, cmd=cmd)

    # Reply with the output/response.
    response = send_response(
            source=command_address,
            destination=reply_to,
            subject='Re: {}'.format(subject),
            body='Output of "{}":\n\n{}'.format(cmd, output),
            )
Example #2
0
def lambda_handler(event, context):
    if 'Records' not in event:
        return handle_api(event)
        # API
    with email_message_for_event(event) as msg:
        # If it's a command, handle it as such.
        command_address = event_msg_is_to_command(event, msg)
        if command_address:
            print('Message addressed to command ({}).'.format(command_address))
            handle_command(command_address, msg)
            return
        
        print('Message from {}.'.format(msg_get_header(msg, 'from')))
        recipients = recipient_destination_overlap(event)

        # See if the message looks like it's a bounce.
        for r in recipients:
            if '+bounce@' in r:
                List.handle_bounce_to(r, msg)
                # Don't do any further processing with this email.
                return

        # See if the message was sent to any known lists.
        for l in List.lists_for_addresses(recipients):
            print('Sending to list {}.'.format(l.address))
            l.send(msg)
Example #3
0
def lambda_handler(event, context):
    if 'Records' not in event:
        return handle_api(event)
        # API
    with email_message_for_event(event) as msg:
        # If it's a command, handle it as such.
        command_address = event_msg_is_to_command(event, msg)
        if command_address:
            print('Message addressed to command ({}).'.format(command_address))
            handle_command(command_address, msg)
            return

        print('Message from {}.'.format(msg_get_header(msg, 'from')))
        recipients = recipient_destination_overlap(event)

        # See if the message looks like it's a bounce.
        for r in recipients:
            if '+bounce@' in r:
                List.handle_bounce_to(r, msg)
                # Don't do any further processing with this email.
                return

        # See if the message was sent to any known lists.
        for l in List.lists_for_addresses(recipients):
            print('Sending to list {}.'.format(l.address))
            l.send(msg)
Example #4
0
    def send(self, msg, mod_approved=False):
        from_user = msg_get_header(msg, "From")
        from_name, from_address = parseaddr(from_user)
        from_address = from_address.lower()
        if not from_name:
            from_name, _ = from_address.split("@", 1)
        if not mod_approved:
            member = self.member_with_address(from_address)
            if member is None and self.reject_from_non_members:
                print(
                    "{} cannot send email to {} (not a member and list rejects email from non-members).".format(
                        from_address, self.address
                    )
                )
                return
            if member and MemberFlag.noPost in member.flags:
                print("{} cannot send email to {} (noPost is set).".format(from_address, self.address))
                return
            if member is None and not self.allow_from_non_members:
                print("Moderating message from non-member.")
                self.moderate(msg)
                return
            if member and MemberFlag.modPost in member.flags:
                print("Moderating message because member has modPost set.")
                self.moderate(msg)
                return
            if self.moderated and (member is None or MemberFlag.preapprove not in member.flags):
                print(
                    "Moderating message because list is moderated and message is not from a member with preapprove set."
                )
                self.moderate(msg)
                return

        # Send to CC lists.
        for cc_list in List.lists_for_addresses(self.cc_lists):
            cc_list.send(msg, mod_approved=True)

        # Strip out any exising DKIM signature.
        self.msg_replace_header(msg, "DKIM-Signature")

        # Strip out any existing return path.
        self.msg_replace_header(msg, "Return-path")

        # Make the list be the sender of the email.
        self.msg_replace_header(msg, "Sender", Header(self.display_address))

        # Munge the From: header.
        # While munging the From: header probably technically violates an RFC,
        # it does appear to be the current best practice for MLMs:
        # https://dmarc.org/supplemental/mailman-project-mlm-dmarc-reqs.html
        list_name = self.name
        if not list_name:
            list_name = self.address
        self.msg_replace_header(
            msg, "From", formataddr(("{} (via {})".format(from_name, list_name), self.munged_from(from_address)))
        )

        # See if replies should default to the list.
        if self.reply_to_list:
            self.msg_replace_header(msg, "Reply-to", Header(self.display_address))
            msg["CC"] = Header(from_user)
        else:
            self.msg_replace_header(msg, "Reply-to", Header(from_user))

        # See if the list has a subject tag.
        if self.subject_tag:
            prefix = u"[{}] ".format(self.subject_tag)
            subject = msg_get_header(msg, "Subject")
            if prefix not in subject:
                self.msg_replace_header(msg, "Subject", Header(u"{}{}".format(prefix, subject)))

        # TODO: body footer
        for recipient in self.addresses_to_receive_from(from_address):
            # Set the return-path VERP-style: [list username]+[recipient s/@/=/]+bounce@[host]
            return_path = self.verp_address(recipient)
            if not mod_approved:
                # Suppress printing when mod-approved, because the output will go to the moderator approving it.
                print("> Sending to {}.".format(recipient))
            ses.send_raw_email(Source=return_path, Destinations=[recipient], RawMessage={"Data": msg.as_string()})
Example #5
0
    def send(self, msg, mod_approved=False):
        from_user = msg_get_header(msg, 'From')
        from_name, from_address = parseaddr(from_user)
        from_address = from_address.lower()
        if not from_name:
            from_name, _ = from_address.split('@', 1)
        if not mod_approved:
            member = self.member_with_address(from_address)
            if member is None and self.reject_from_non_members:
                print(
                    '{} cannot send email to {} (not a member and list rejects email from non-members).'
                    .format(from_address, self.address))
                return
            if member and MemberFlag.noPost in member.flags:
                print('{} cannot send email to {} (noPost is set).'.format(
                    from_address, self.address))
                return
            if member is None and not self.allow_from_non_members:
                print('Moderating message from non-member.')
                self.moderate(msg)
                return
            if member and MemberFlag.modPost in member.flags:
                print('Moderating message because member has modPost set.')
                self.moderate(msg)
                return
            if self.moderated and (member is None or MemberFlag.preapprove
                                   not in member.flags):
                print(
                    'Moderating message because list is moderated and message is not from a member with preapprove set.'
                )
                self.moderate(msg)
                return

        # Send to CC lists.
        for cc_list in List.lists_for_addresses(self.cc_lists):
            cc_list.send(msg, mod_approved=True)

        # Strip out any exising DKIM signature.
        self.msg_replace_header(msg, 'DKIM-Signature')

        # Strip out any existing return path.
        self.msg_replace_header(msg, 'Return-path')

        # Make the list be the sender of the email.
        self.msg_replace_header(msg, 'Sender', Header(self.display_address))

        # Munge the From: header.
        # While munging the From: header probably technically violates an RFC,
        # it does appear to be the current best practice for MLMs:
        # https://dmarc.org/supplemental/mailman-project-mlm-dmarc-reqs.html
        list_name = self.name
        if not list_name:
            list_name = self.address
        self.msg_replace_header(
            msg,
            'From',
            formataddr((
                '{} (via {})'.format(from_name, list_name),
                self.munged_from(from_address),
            )),
        )

        # See if replies should default to the list.
        if self.reply_to_list:
            self.msg_replace_header(msg, 'Reply-to',
                                    Header(self.display_address))
            msg['CC'] = Header(from_user)
        else:
            self.msg_replace_header(msg, 'Reply-to', Header(from_user))

        # See if the list has a subject tag.
        if self.subject_tag:
            prefix = u'[{}] '.format(self.subject_tag)
            subject = msg_get_header(msg, 'Subject')
            if prefix not in subject:
                self.msg_replace_header(
                    msg, 'Subject', Header(u'{}{}'.format(prefix, subject)))

        # TODO: body footer
        for recipient in self.addresses_to_receive_from(from_address):
            # Set the return-path VERP-style: [list username]+[recipient s/@/=/]+bounce@[host]
            return_path = self.verp_address(recipient)
            if not mod_approved:
                # Suppress printing when mod-approved, because the output will go to the moderator approving it.
                print('> Sending to {}.'.format(recipient))
            ses.send_raw_email(
                Source=return_path,
                Destinations=[
                    recipient,
                ],
                RawMessage={
                    'Data': msg.as_string(),
                },
            )