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), )
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)
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()})
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(), }, )