# encoding: utf-8

"""MIME-encoded electronic mail message classes."""

from turbomail import release

import turbogears, re, os, email

import email.Message
from email import Encoders, Charset
from email.Message import Message as MIMEMessage
from email.Utils import formatdate
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Header import Header

import logging
log = logging.getLogger("turbomail.message")


__all__ = ['Message', 'KIDMessage']
_rich_to_plain = re.compile(r"(<[^>]+>)")


class Message(object):
	"""Simple e-mail message class.
	
	Message provides a means to easily create e-mail messages to be
	sent through the Dispatch mechanism or MailPool.  Message provides
	various helper functions to correctly format plain text, dual plain
	text and rich text MIME encoded messages, as well as handle
	embedded and external attachments.
	
	All properties can be set from the constructor.
	
	Example usage::
	
		import turbomail
		message = turbomail.Message(
				"from@host.com",
				"to@host.com",
				"Subject",
				plain="This is a plain message."
			)
	
	E-mail addresses can be represented as any of the following:
	 - A string.
	 - A 2-tuple of ("Full Name", "name@host.tld")
	
	Encoding can be overridden on a per-message basis, but note that
	'utf-8-qp' modifies the default 'utf-8' behaviour to output
	quoted-printable, and you will have to change it back yourself if
	you want base64 encoding.
		
	@ivar _processed: Has the MIME-encoded message been generated?
	@type _processed: bool
	@ivar _dirty: Has there been changes since the MIME message was last
	              generated?
	@type _dirty: bool
	@ivar date: The Date header.  Must be correctly formatted.
	@type date: string
	@ivar recipient: The To header.  A string, 2-tuple, or list of
	                 strings or 2-tuples.
	@ivar sender: The From header.  A string or 2-tuple.
	@ivar organization: The Organization header.  I{Optional.}
	@type organization: string
	@ivar replyto: The X-Reply-To header.  A string or 2-tuple.
	               I{Optional.}
	@ivar disposition: The Disposition-Notification-To header.  A string
	                   or 2-tuple.  I{Optional.}
	@ivar cc: The CC header.  As per the recipient property.
	          I{Optional.}
	@ivar bcc: The BCC header.  As per the recipient property.
	           I{Optional.}
	@ivar encoding: Content encoding.  Pulled from I{mail.encoding},
	                defaults to 'us-ascii'.
	@type encoding: string
	@ivar priority: The X-Priority header, a number ranging from 1-5.
	                I{Optional.}  Default: B{3}
	@type priority: int
	@ivar subject: The Subject header.
	@type subject: string
	@ivar plain: The plain text content of the message.
	@type plain: string
	@ivar rich: The rich text (HTML) content of the message.  Plain text
	            content B{must} be available as well.
	@type rich: string
	@ivar attachments: A list of MIME-encoded attachments.
	@type attachments: list
	@ivar embedded: A list of MIME-encoded embedded obejects for use in
	                the text/html part.
	@type embedded: list
	@ivar headers: A list of additional headers.  Can be added in a wide
	               variety of formats: a list of strings, list of
	               tuples, a dictionary, etc.  Look at the code.
	@ivar smtpfrom: The envelope address, if different than the sender.
	"""
	
	def __init__(self, sender=None, recipient=None, subject=None, **kw):
		"""Instantiate a new Message object.
		
		No arguments are required, as everything can be set using class
		properties.  Alternatively, I{everything} can be set using the
		constructor, using named arguments.  The first three positional
		arguments can be used to quickly prepare a simple message.
		
		An instance of Message is callable.
		
		@param sender: The e-mail address of the sender.  This is
					   encoded as the "From:" SMTP header.
		@type sender: string
		
		@param recipient: The recipient of the message.	 This gets
						  encoded as the "To:" SMTP header.
		@type recipient: string
		
		@param subject: The subject of the message.	 This gets encoded
						as the "Subject:" SMTP header.
		@type subject: string
		"""
		
		super(Message, self).__init__()
		
		self._processed = False
		self._dirty = False
		
		self.date = formatdate(localtime=True)
		self.recipient = recipient
		self.sender = sender
		self.organization = None
		self.replyto = None
		self.disposition = None
		self.cc = []
		self.bcc = []
		self.encoding = turbogears.config.get("mail.encoding", 'us-ascii')
		self.priority = 3
		self.subject = subject
		self.plain = None
		self.rich = None
		self.attachments = []
		self.embedded = []
		self.headers = []
		self.smtpfrom = None
		
		for i, j in kw.iteritems():
			assert i in self.__dict__, "Unknown attribute: '%s'" % i
			self.__dict__[i] = j
			
	def attach(self, file, name=None):
		"""Attach an on-disk file to this message.
		
		@param file: The path to the file you wish to attach, or an
		             instance of a file-like object.
		
		@param name: You can optionally override the filename of the
		             attached file.  This name will appear in the
		             recipient's mail viewer.  B{Optional if passing
		             an on-disk path.  Required if passing a file-like
		             object.}
		@type name: string
		"""
		
		part = MIMEBase('application', "octet-stream")

		if isinstance(file, (str, unicode)):
			fp = open(file, "rb")
		else:
			assert name is not None, "If attaching a file-like object, you must pass a custom filename."
			fp = file
		
		part.set_payload(fp.read())
		Encoders.encode_base64(part)

		part.add_header('Content-Disposition', 'attachment', filename=os.path.basename([name, file][name is None]))
			
		self.attachments.append(part)
	
	def embed(self, file, name):
		"""Attach an on-disk image file and prepare for HTML embedding.
		
		This method should only be used to embed images.
		
		@param file: The path to the file you wish to attach, or an
		             instance of a file-like object.
		
		@param name: You can optionally override the filename of the
		             attached file.  This name will appear in the
		             recipient's mail viewer.  B{Optional if passing
		             an on-disk path.  Required if passing a file-like
		             object.}
		@type name: string
		"""
		
		from email.MIMEImage import MIMEImage
		
		if isinstance(file, (str, unicode)):
			fp = open(file, "rb")
		else:
			assert name is not None, "If embedding a file-like object, you must pass a custom filename."
			fp = file

		part = MIMEImage(fp.read())
		fp.close()
		
		part.add_header('Content-ID', '<%s>' % name)
		
		self.embedded.append(part)
	
	def _normalize(self, addresslist):
		"""A utility function to return a list of addresses as a string."""
		
		addresses = []
		for i in [[addresslist], addresslist][type(addresslist) == type([])]:
			if type(i) == type(()):
				addresses.append('"%s" <%s>' % (i[0], i[1]))
			else: addresses.append(i)
		
		return ",\n ".join(addresses)
	
	def _process(self):
		"""Produce the final MIME message.
		
		Additinoally, if only a rich text part exits, strip the HTML to
		produce the plain text part.  (This produces identical output as
		KID, although lacks reverse entity conversion -- &amp;, etc.)
		"""
		
		if self.encoding == 'utf-8-qp':
			Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
			self.encoding = 'utf-8'

		if callable(self.plain):
			self.plain = self.plain()

		if callable(self.rich):
			self.rich = self.rich()
		
		if self.rich and not self.plain:
			self.plain = _rich_to_plain.sub('', self.rich)
		
		if not self.rich:
			if not self.attachments:
				message = MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding)
				
			else:
				message = MIMEMultipart()
				message.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding))
				
		else:
			if not self.attachments:
				message = MIMEMultipart('alternative')
				message.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding))
				
				if not self.embedded:
					message.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
				else:
					related = MIMEMultipart('related')
					message.attach(related)
					related.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
					
					for attachment in self.embedded:
						related.attach(attachment)
			
			else:
				message = MIMEMultipart()
				alternative = MIMEMultipart('alternative')
				message.attach(alternative)

				alternative.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding))
				
				if not self.embedded:
					alternative.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
				else:
					related = MIMEMultipart('related')
					alternative.attach(related)
					related.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding))
					
					for attachment in self.embedded:
						related.attach(attachment)
			
		for attachment in self.attachments:
			message.attach(attachment)

		message.add_header('From', self._normalize(self.sender))
		message.add_header('Subject', self.subject)
		message.add_header('Date', formatdate(localtime=True))
		message.add_header('To', self._normalize(self.recipient))
		if self.replyto: message.add_header('Reply-To', self._normalize(self.replyto))
		if self.cc: message.add_header('Cc', self._normalize(self.cc))
		if self.disposition: message.add_header('Disposition-Notification-To', self._normalize(self.disposition))
		if self.organization: message.add_header('Organization', self.organization)
		if self.priority != 3: message.add_header('X-Priority', self.priority)

		if not self.smtpfrom:
			if type(self.sender) == type([]) and len(self.sender) > 1:
				message.add_header('Sender', self._normalize(self.sender[0]))
				message.add_header('Return-Path', self._normalize(self.sender[0]))
			else:
				message.add_header('Return-Path', self._normalize(self.sender))
		else:
			message.add_header('Return-Path', self._normalize(self.sender))
			message.add_header('Sender', self._normalize(self.smtpfrom))
			message.add_header('Return-Path', self._normalize(self.smtpfrom))
			message.add_header('Old-Return-Path', self._normalize(self.smtpfrom))

		message.add_header('X-Mailer', "TurboMail TurboGears Extension v.%s" % release.version)
		
		if type(self.headers) == type(()):
			for header in self.headers:
				if type(header) in [type(()), type([])]:
					message.add_header(*header)
				elif type(header) == type({}):
					message.add_header(**header)
		
		if type(self.headers) == type({}):
			for name, header in self.headers.iteritems():
				if type(header) in [type(()), type([])]:
					message.add_header(name, *header)
				elif type(header) == type({}):
					message.add_header(name, **header)
				else:
					message.add_header(name, header)
		
		self._message = message
		self._processed = True
		self._dirty = False
	
	def __setattr__(self, name, value):
		"""Set the dirty flag as properties are updated."""
		
		self.__dict__[name] = value
		if name != '_dirty': self.__dict__['_dirty'] = True
	
	def __call__(self):
		"""Produce a valid MIME-encoded message and return valid input
		for the Dispatch class to process.

		@return: Returns a tuple containing sender and recipient e-mail
		         addresses and the string output of MIMEMultipart.
		@rtype: tuple
		"""
		
		if not self._processed or self._dirty:
			self._process()
		
		recipients = []
		
		if isinstance(self.recipient, list):
			recipients.extend(self.recipient)
		else: recipients.append(self.recipient)
		
		if isinstance(self.cc, list):
			recipients.extend(self.cc)
		else: recipients.append(self.cc)
		
		if isinstance(self.bcc, list):
			recipients.extend(self.bcc)
		else: recipients.append(self.bcc)
		
		return dict(
				sender=self.sender,
				to=[[self.recipient], self.recipient][isinstance(self.recipient, list)],
				recipients=[i[1] for i in recipients if isinstance(i, tuple)] + [i for i in recipients if not isinstance(i, tuple)],
				subject=self.subject,
				message=self._message.as_string(),
			)


class KIDMessage(Message):
	"""A message that accepts a named template with arguments.
	
	Example usage::
	
		import turbomail
		message = turbomail.KIDMessage(
				"from@host.com",
				"to@host.com",
				"Subject",
				"app.templates.mail",
				dict()
			)
	
	Do not specify message.plain or message.rich content - the template
	will override what you set.  If you wish to hand-produce content,
	use the Message class.
	"""
	
	def __init__(self, sender, recipient, subject, template, variables={}, **kw):
		"""Store the additonal template and variable information.
		
		@param template: A dot-path to a valid KID template.
		@type template: string
		
		@param variables: A dictionary containing named variables to
		                  pass to the template engine.
		@type variables: dict
		"""

		log.warn("Use of KIDMessage is deprecated and will be removed in version 2.1.")
		
		self._template = template
		self._variables = dict(sender=sender, recipient=recipient, subject=subject)
		self._variables.update(variables)
				
		super(KIDMessage, self).__init__(sender, recipient, subject, **kw)
	
	def _process(self):
		"""Automatically generate the plain and rich text content."""
		
		turbogears.view.base._load_engines()
		
		data = dict()
		
		for (i, j) in self._variables.iteritems():
			if callable(j): data[i] = j()
			else: data[i] = j
		
		self.plain = turbogears.view.engines.get('kid').render(data, format="plain", template=self._template)
		self.rich = turbogears.view.engines.get('kid').render(data, template=self._template)
		
		return super(KIDMessage, self)._process()


syntax highlighted by Code2HTML, v. 0.9.1