class KikiMessage(EmailMessage):
	"""
	This is similar to a django EmailMessage, but overrides recipients() to return arbitrary recipients rather than a concatenation of the to, cc, and bcc attributes.
	
	"""
	_recipients = None
	
	def recipients(self):
		return self._recipients or []
	
	@classmethod
	def from_python_message(cls, msg):
		"""Given a python :class:`email.Message` object, return a corresponding class:`kiki.message.KikiMessage` instance."""
		payload = msg.get_payload()
		if msg.is_multipart():
			# TODO: is this the correct way to determine "body" vs "attachments"?
			body = payload[0]
			attachments = payload[1:]
		else:
			body = payload
			attachments = None
		
		# For now, let later header values override earlier ones. TODO: This should be more complex.
		headers = dict([(k.lower(), v) for k, v in msg.items() if k not in ('to', 'cc', 'bcc')])
		
		to = [addr[1] for addr in getaddresses(msg.get_all('to', []))]
		cc = [addr[1] for addr in getaddresses(msg.get_all('cc', []))]
		bcc = [addr[1] for addr in getaddresses(msg.get_all('bcc', []))]
		
		kwargs = {
			'subject': headers.pop('subject', ''),
			'body': body,
			'from_email': headers.pop('from', ''),
			'to': to,
			'bcc': bcc,
			'attachments': attachments,
			'headers': headers,
			'cc': cc
		}
		return cls(**kwargs) class ListMessage(ProcessedMessageModel):
	"""
	Represents the relationship between a :class:`Message` and a :class:`MailingList`. This is what is processed to handle the sending of a message to a list rather than the original message.
	
	"""
	ACCEPTED = 1
	REQUIRES_MODERATION = 2
	PREPPED = 3
	SENT = 4
	FAILED = 5
	
	STATUS_CHOICES = (
		(ACCEPTED, 'Accepted'),
		(REQUIRES_MODERATION, 'Requires Moderation'),
		(PREPPED, 'Prepped'),
		(SENT, 'Sent'),
		(FAILED, 'Failed'),
	)
	
	message = models.ForeignKey(Message)
	mailing_list = models.ForeignKey(MailingList)
	status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, db_index=True)
	
	class Meta:
		unique_together = ('message', 'mailing_list',) class Message(ProcessedMessageModel):
	"""
	Represents an email received by Kiki. Stores the original received message as well as a pickled version of the processed message.
	
	"""
	UNPROCESSED = 'u'
	PROCESSED = 'p'
	FAILED = 'f'
	
	STATUS_CHOICES = (
		(UNPROCESSED, 'Unprocessed'),
		(PROCESSED, 'Processed'),
		(FAILED, 'Failed'),
	)
	
	message_id = models.CharField(max_length=255, unique=True)
	#: The message_id of the email this is in reply to.
	# in_reply_to = models.CharField(max_length=255, db_index=True, blank=True)
	from_email = models.EmailField()
	
	received = models.DateTimeField()
	
	status = models.CharField(max_length=1, choices=STATUS_CHOICES, db_index=True, default=UNPROCESSED)
	
	original_message = models.TextField(help_text="The original raw text of the message.") class ListCommand(models.Model):
	#: The ListCommand has not been processed.
	UNPROCESSED = 1
	#: The ListCommand has been rejected (e.g. for permissioning reasons.)
	REJECTED = 2
	#: Ths ListCommand has been processed completely.
	PROCESSED = 3
	#: An error occurred while processing the ListCommand.
	FAILED = 4
	
	STATUS_CHOICES = (
		(UNPROCESSED, 'Unprocessed'),
		(REJECTED, 'Rejected'),
		(PROCESSED, 'Processed'),
		(FAILED, 'Failed'),
	)
	
	message = models.ForeignKey(Message)
	mailing_list = models.ForeignKey(MailingList)
	status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, db_index=True, default=UNPROCESSED)
	
	command = models.CharField(max_length=20) def create_test_email(from_email, to, subject='hello', body='This is clearly a test.', headers=None):
	"""
	Returns a string representation of an email message.
	
	"""
	headers = headers or {}
	msg = EmailMessage(subject, body, from_email, to, headers=headers)
	msg_str = msg.message().as_string()
	return msg_str def receive_email(msg_str):
	"""
	Given a string representation of an email message, parses it into a :class:`~kiki.message.KikiMessage`. Returns a (message, created) tuple, where ``created`` is False if the message was already in the database.
	
	"""
	received = datetime.now()
	python_msg = message_from_string(msg_str)
	sanitize_headers(python_msg)
	msg = KikiMessage.from_python_message(python_msg)
	msg_id = msg.extra_headers['message-id']
	
	# If the msg_id already exists in the database, then ignore this message.
	created = False
	try:
		message = Message.objects.get(message_id=msg_id)
	except Message.DoesNotExist:
		message = Message(message_id=msg_id, received=received, original_message=msg_str, from_email=msg.from_email)
		message.set_processed(msg)
		message.save()
		created = True
	return message, created def create_message_commands(msg):
	"""
	Given a message, creates and returns a list of commands for that message and target mailing lists.
	
	"""
	if msg.status != Message.UNPROCESSED:
		return
	
	pmsg = msg.get_processed()
	recipients = pmsg.to + pmsg.cc + pmsg.bcc
	if not recipients:
		return
	
	# Make a map of commands to recipient lists.
	cmd_recips = {}
	
	for recipient in recipients:
		local, cmd, domain = parse_command_address(recipient)
		# Default command is posting.
		cmd_recips.setdefault(cmd or "post", []).append('@'.join((local, domain)))
	
	try:
		with transaction.commit_on_success():
			for cmd in cmd_recips:
				lists = MailingList.objects.for_addresses(cmd_recips[cmd])
				list_cmds = [ListCommand.objects.create(message=msg, mailing_list=list_, command=cmd) for list_ in lists]
	except:
		msg.status = Message.FAILED
		msg.save()
		raise
	else:
		msg.status = Message.PROCESSED
		msg.save()
		return list_cmds def list_post(list_cmd):
	"""
	Given a :class:`kiki.models.ListCommand` for a post, returns a :class:`kiki.models.ListMessage` based on that command, or ``None`` if the command was invalid. The :class:`kiki.models.ListMessage` will be marked either as "Accepted" or "Requires Moderation", and it will have the appropriate list headers set.
	
	"""
	if list_cmd.status != ListCommand.UNPROCESSED or list_cmd.command != "post":
		return None
	
	mailing_list = list_cmd.mailing_list
	can_post = mailing_list.can_post(list_cmd.message.from_email)
	moderation = mailing_list.moderation_enabled
	
	if not can_post and not moderation:
		# TODO: Email the user.
		list_cmd.status = ListCommand.REJECTED
		list_cmd.save()
		return None
	
	try:
		list_msg = ListMessage(message=list_cmd.message, mailing_list=mailing_list, status=ListMessage.ACCEPTED, processed_message=list_cmd.message.processed_message)
		if moderation and not can_post:
			list_msg.status = ListMessage.REQUIRES_MODERATION
		list_msg.save()
	except:
		list_cmd.status = ListCommand.FAILED
		list_cmd.save()
		raise
	else:
		
		list_cmd.status = ListCommand.PROCESSED
		list_cmd.save()
		return list_msg def prep_list_message(list_msg):
	"""
	Runs :func:`kiki.utils.message.set_list_headers` on an accepted :class:`.ListMessage` and sets its status to :attr:`ListMessage.PREPPED`.
	
	"""
	if list_msg.status == ListMessage.ACCEPTED:
		msg = list_msg.get_processed()
		set_list_headers(msg, list_msg.mailing_list)
		list_msg.set_processed(msg)
		list_msg.status = ListMessage.PREPPED
		list_msg.save()
	return list_msg def send_list_message(list_msg, user):
	"""
	Sends a copy of a :class:`kiki.models.ListMessage` to a single :class:`.User`.
	
	"""
	msg = list_msg.get_processed()
	set_user_headers(msg, user)
	msg._recipients = [user.email]
	msg.send()
import time
from email import message_from_string
from django.test import TestCase
from django.contrib.auth.models import User
from django.core import mail
from kiki.message import KikiMessage
from kiki.models import ListMessage, Message, ListCommand
from kiki.utils.test import create_test_email
from kiki.utils.routing import receive_email, create_message_commands, list_post, prep_list_message, send_list_message




class RoutingTestCase(TestCase):
	fixtures = ['test_data.json']
	
	def test_receive_email(self):
		message, created = receive_email(create_test_email("unused@unused.com", ["hello-world@example.com"]))
		self.assertTrue(created)
		self.assertIsInstance(message, Message)
	
	def test_create_message_commands(self):

		message = Message.objects.get(pk=3)