# File: message.py
# Purpose: a message instance

import string
import os
import os.path
import time
import cStringIO
import binascii
import tempfile
import pynei18n
import msgeditbox
import rfc822
import copy
import utils
import base64
import mimify
import mimetypes
import mimetools
from pyneheaders import *

def mime_decode(headerdict, headers):
	"""
	Decode these headers :-(
	"""
	for i in headers:
		if headerdict.has_key(i):
			headerdict[i] = mimify.mime_decode_header(headerdict[i])

def parse_2_body_and_headers(head_and_body):
	"""
	Parse a message header+body to headers (as a
	dictionary) and body text
	"""
	# Headers are terminated by a double newline.
	endhead = string.find(head_and_body, "\n\n")
	
	# Header and upper case version
	header_text = head_and_body[0:endhead+1]
	body_text   = head_and_body[endhead+2:]
	# Parse headers to a dictionary 
	header_dict = headers_2_dict( cStringIO.StringIO(header_text) )

	return (header_dict, body_text)

def headers_2_dict(f):
	"""
	Takes an open file object of message headers and parses the
	headers to a dictionary.
	"""
	keys = {}
	lastkey = None

	while 1:
		s = f.readline()

		if s == "":
			break

		s = string.replace(s, "\r", "")
		s = string.replace(s, "\n", "")
		s = string.replace(s, "\t", " ")

		keyname = string.lower( string.split(s, ":")[0] )

		# if we found
		if len(string.split(s, ":")) > 1 and s[:1] != " ":
			keys[keyname] = string.join(string.split(s,":")[1:], ":")
			# remove leading space
			if keys[keyname][:1] == " ":
				keys[keyname] = keys[keyname][1:]
			lastkey = keyname
		elif lastkey != None:
			keys[lastkey] = keys[lastkey] + s
	# Evil white space
	for i in keys.keys():
		keys[i] = string.strip(keys[i])
	return keys

class pynemsg:
	"""
	An email or news article
	"""
	def __init__(self):
		self.headers = {}
		# Format (seconds since epoch)
		self.date = 0
		# to decide when to expire news articles
		self.date_received = 0
		# Message text (inc headers)
		self.body = ""
		self.opts = 0
		# messages waiting in the outbox will additionally have the
		# uid of the box they came from in:
		self.senduid = None
		# Message parts. This will contain strings of encoded attachments
		# including the attachment header thingy
		self.parts_text = []
		self.parts_header = []

	def make_source(self, presend=0):
		"""
		Take a message and it's contents (attachments and such like)
		and create a single body with all the headers required to
		post.
		Set presend=1 before actual posting to include full attachments.
		"""
		import pyne # for pyne.ver_string

		mimenc = mimify.mime_encode_header

		num_parts = len(self.parts_text)
		body = ""
		############# HEADERS
		if self.headers.has_key("from"):
			body =        mimenc("From: "+self.headers["from"]+"\n")
		if self.headers.has_key("reply-to"):
			body = body + mimenc("Reply-To: "+self.headers["reply-to"]+"\n")
		if self.headers.has_key("organization"):
			body = body + mimenc("Organization: "+self.headers["organization"]+"\n")
		if self.headers.has_key("to"):
			body = body + mimenc("To: "+self.headers["to"]+"\n")
		if self.headers.has_key("subject"):
			body = body + mimenc("Subject: "+self.headers["subject"]+"\n")
		# date stamp of when last edited
		body = body + time.strftime("Date: %a, %d %b %Y %H:%M:%S +0000\n", time.localtime(self.date))
		if self.headers.has_key("references"):
			body = body + "References: "+self.headers["references"]+"\n"
		if self.headers.has_key("newsgroups"):
			body = body + "X-Newsreader: "+pyne.ver_string+"\n"
		else:
			body = body + "X-Mailer: "+pyne.ver_string+"\n"
		# Content type
		if num_parts == 1:
			body = body + "Content-Type: text/plain\n"
		else:
			body = body + "Content-Type: multipart/mixed; boundary=\""+multi_part_boundary+"\"\n"
		body = body + "MIME-Version: 1.0\n"
		# Confirm delivery
		if self.headers.has_key("return-receipt-to"):
			body = body + "Return-Receipt-To: "+self.headers["from"]+"\n"
		# Optional Cc
		if self.headers.has_key("cc"):
			if self.headers["cc"] != "":
				body = body + mimenc("Cc: "+self.headers["cc"]+"\n")
		if self.headers.has_key("bcc"):
			if self.headers["bcc"] != "":
				body = body + mimenc("Bcc: "+self.headers["bcc"]+"\n")
		if presend == 0:
			body = body + "Message-ID: "+self.headers["message-id"]+"\n"
		# news stuff:
		if self.headers.has_key("newsgroups"):
			body = body + "Lines: "+str(len(string.split(self.parts_text[0], "\n")))+"\n"
			body = body + "Newsgroups: "+self.headers["newsgroups"]+"\n"

		# simple single part messages may simply have the body grafted
		# on and that's all.
		if num_parts == 1:
			if presend:
				body = body + "\n" + utils.string_line_wrap (self.parts_text[0], 76)
			else:
				body = body + "\n" + self.parts_text[0]
			self.body = body
			return
		else:
			# It's a multi-part message.
			# Add the plain text bit first.
			body = body + "\nThis is a multi-part message in MIME format.\n"
			body = body + "\n--" + multi_part_boundary + "\n"
			body = body + "Content-Type: text/plain\n"
			body = body + "Content-Transfer-Encoding: 8bit\n\n"
			if presend:
				body = body + utils.string_line_wrap (self.parts_text[0], 76) + "\n"
			else:
				body = body + self.parts_text[0] + "\n"

			# Then add the other parts
			for x in range(1, num_parts):
				body = body + "--" + multi_part_boundary + "\n"
				body = body + "Content-Type: "+self.parts_header[x]["content-type"]+"\n"
				body = body + "Content-Transfer-Encoding: "+self.parts_header[x]["content-transfer-encoding"]+"\n"
				body = body + "Content-Disposition: "+self.parts_header[x]["content-disposition"]+"\n\n"
				body = body + self.parts_text[x]

			# And terminate
			body = body + "--" + multi_part_boundary + "--\n"

			self.body = body
		return

	def edit(self, folder, user, is_new_msg=0):
		"""
		Create a message composing window and let it do the hard work.
		"""
		# If the message has already been sent then
		# we want to make a copy of it in the outbox
		# and edit that.
		if self.opts & MSG_SENT:
			outbox = user.get_folder_by_uid("outbox")
			# Copy message, but change message-id
			msg = self
			msg.headers = copy.copy(self.headers)
			del msg.headers["message-id"]
			msg.opts = msg.opts & ~(MSG_SENT)
			# This should give it a message id
			outbox.save_new_article(msg)
			outbox.changed = 1

			user.update()
			msgeditbox.msgeditbox(msg, outbox, user, 1)
		else:
			msgeditbox.msgeditbox(self, folder, user, is_new_msg)

	def parseheaders(self, user, headers_only=0):
		"""
		The body now contains headers and body text.
		Extract sender, subject and date and split up
		if it's multi-part.
		Pass headers_only=1 if you don't need bodies to be
		parsed (multipart decoded, etc..)
		"""
		# Convert and "\r\n"s to "\n"
		self.body = string.replace(self.body, "\r\n", "\n")

		# Wipe the contents. We are going to get that
		self.parts_text = []
		self.parts_header = []

		part_header, part_text = parse_2_body_and_headers(self.body)
		self.parts_text.append(part_text)
		self.parts_header.append(part_header)
		self.headers = part_header
		
		# Too long and flatfile boxformat craps itself
		if self.headers.has_key("message-id"):
			self.headers["message-id"] = self.headers["message-id"][:186]

		# We *need* some header fields
		if not self.headers.has_key("subject"):
			self.headers["subject"] = ""
		if not self.headers.has_key("from"):
			self.headers["from"] = ""

		# Mime decode some headers
		mime_decode(self.headers, [ "subject", "from", "to", "cc", "bcc", "reply-to", "organization" ])

		# Clean up dodgy references
		if self.headers.has_key("references"):
			s = self.headers["references"]
			l = []
			while string.find(s, "<") != -1:
				x = string.find(s, "<")
				y = string.find(s, ">")
				l.append(s[x:y+1])
				s = s[y+1:]
				if y == -1:
					break
			self.headers["references"] = string.join(l, " ")
	
		# Get a nice gmtime format time from the one in the header
		if self.headers.has_key("date"):
			self.date = rfc822.parsedate(self.headers["date"])
			# rfc822.parsedate is really anal and fails on small
			# padding errors in the date line. XXX XXX
			if self.date == None:
				self.date = int(time.time())
			# for mentally retarded mailers, 2 digit years
			elif self.date[0] < 1000: # date[0] is year
				year = 2000 + (self.date[0] % 100)
				try:
					self.date = int(time.mktime((year,) + self.date[1:]))
				except OverflowError:
					# fuck it...
					self.date = int(time.time ())
			else:
				self.date = int(time.mktime(self.date))
		else:
			# Just set current time...
			self.date = int(time.time())

		# We are parsing a message with headers only
		if headers_only == 1:
			self.opts = self.opts | MSG_NO_BODY
			return
		# we do have a body >:-(
		self.opts = self.opts & (~MSG_NO_BODY)
	
		# Break multipart messages up
		i = 0
		while i < len(self.parts_text):
			headers = self.parts_header[i]
			body = self.parts_text[i]
			if not headers.has_key("content-type"):
				# not a multi-part section	
				i = i+1
				continue
			content_type = string.lower(headers["content-type"])
			if string.find(content_type, "boundary=") == -1:
				# not a multi-part section
				i = i+1
				continue
			else:
				# get boundary string
				start_boundary = 9 + string.find(content_type, "boundary=")
				#end_boundary = start_boundary + string.find(content_type[start_boundary:], "\"")
				boundary = headers["content-type"][start_boundary:]#end_boundary]
				if boundary[0] == "\"":
					boundary = boundary[1:]
				if boundary[-1] == "\"":
					boundary = boundary[:-1]
				# get chunks of body between this boundary
				offset = b = 0
				subtexts = []
				subheads = []
				while 1:
					# find start of a boundary
					b = string.find(body[offset:], "--"+boundary)
					if b == -1:
						# no attachment
						break
					if b == string.find(body[offset:], "--"+boundary+"--"):
						# end of attachments
						break
					# line after start boundary
					b = offset + b + len(boundary) + 3 # 3==len("--"+"\n")
					# stupid messages with no terminating boundary
					if string.find(body[b:], "--"+boundary) == -1:
						offset = len(body)
					else:
						# end boundary
						offset = string.find(body[b:], "--"+boundary) + b
					# append section
					head, text = parse_2_body_and_headers(body[b:offset])
					subheads.append(head)
					subtexts.append(text)
				# dump new bits on
				# remove this part, since it isn't a single part
				if len(subheads):
					del self.parts_header[i]
					del self.parts_text[i]
					# now add the seperate parts it was composed of
					for x in range(0, len(subheads)):
						self.parts_header.insert(i+x, subheads[x])
						self.parts_text.insert(i+x, subtexts[x])
				else:
					i = i+1
		# Some very strange messages don't have text bodies
		# If they are binary we really don't want to stick them in a GtkText...
		if self.parts_header[0].has_key("content-type"):
			if string.lower(self.parts_header[0]["content-type"])[:4] != "text":
				self.parts_header.insert(0, {})
				self.parts_text.insert(0, "\n")
		
		# Check text part 0 for yenc stuff inline
		# Ignore that shit-arsed subject line yenc stuff
		not_yenc = ""
		pos = 0
		endpos = 0
		bod = self.parts_text[0]
		found_yenc = 0
		while 1:
			npos = string.find(bod[pos:], "=ybegin")
			if npos == -1:
				break
			if (npos == 0) or (bod[pos+npos-1] == '\n'):
				pass
			else:
				# Not at start of a line. Reject.
				pos = pos + npos + 1
				continue
			# Multi part. skip next '=ypart' line
			pos = pos + npos

			# Add bit before this to not uuenc
			not_yenc = not_yenc + bod[endpos:pos]

			endheadpos = pos+string.find(bod[pos:], '\n')
			hdr = bod[pos:endheadpos]
			print hdr
			
			# find body span
			endpos = string.find(bod[pos:], "\n=yend")
			if endpos == -1:
				break
			
			# skip 2nd header line '=ypart' if it exists
			temp = string.find(bod[pos:], "=ypart")
			if temp != -1:
				endheadpos = pos+temp+string.find(bod[pos+temp:], '\n')
			
			found_yenc = 1
			yenc_part = bod[endheadpos+1:endpos+pos]+"\n"
			print "CUNT: "+str(hdr)
			# get headers
			yenc_head = {}
			hdrsplit = hdr.split()

			# ignoring '=ybegin' line
			last_key = None
			for h in hdrsplit[1:]:
				try:
					head, data = h.split("=", 1)
				except ValueError:
					if last_key:
						yenc_head["yenc_"+last_key] = yenc_head["yenc_"+last_key] + " " + h
						continue
					else:
						print "Strange yenc header: '%s'" % h
						continue
				last_key = head
				yenc_head["yenc_"+head] = data

			if yenc_head.has_key("yenc_name"):
				filename = yenc_head["yenc_name"]
				mimetype = mimetypes.guess_type(filename)[0]
			else:
				filename = "noname"
				mimetype = "application/octet-stream"
				
			yenc_head["content-transfer-encoding"] = "yenc"
			yenc_head["content-type"] = "%s; name=\"%s\"" % (mimetype, filename)
			self.parts_text.insert(1, yenc_part)
			self.parts_header.insert(1, yenc_head)

			pos = endpos
		if found_yenc:
			# Set original part 0 to minus uuenc bit
			self.parts_text[0] = not_yenc

		# Check text part 0 for uuencoded stuff inline
		not_uuenc = ""
		pos = 0
		endpos = 0
		bod = self.parts_text[0]
		found_uuenc = 0
		while 1:
			npos = string.find(bod[pos:], "begin")
			if npos == -1:
				if found_uuenc == 1:
					break
				# None found yet. But maybe it is that retarded
				# kind with no begin header at all
				# (part of a huge split up file)
				x = string.find(bod, "\nM")
				y = string.find(bod[x+1:], "\nM")
				z = string.find(bod[x+y+2:], "\nM")
				#print "dodgy uuenc",x,y,z
				# find marked end (if there is one)
				endpos = string.find(bod, "\n`\nend")
				if endpos == -1:
					endpos = string.find(bod, "\nend")
				if endpos == -1:
					endpos = len(bod)
				# Three lines in a row starting with 'M',
				# 61 words long each. Looks like a uuencoded thingy
				if x != -1 and y == 61 and y == 61:
					uuenc_part = bod[:endpos].strip()+"\n"
			
					mimetype = "application/octet-stream"
					uuenc_head = { "content-transfer-encoding": "uuencode",
					               "content-type": "%s; name=\"%s\"" % (mimetype, "noname") }
					self.parts_text.insert(1, uuenc_part)
					self.parts_header.insert(1, uuenc_head)
					found_uuenc = 1
				break
			if (npos == 0) or (bod[pos+npos-1] == '\n'):
				pass
			else:
				pos = pos + npos + 1
				continue
			pos = pos + npos

			# Add bit before this to not uuenc
			not_uuenc = not_uuenc + bod[endpos:pos]

			endheadpos = pos+string.find(bod[pos:], '\n')
			hdr = bod[pos:endheadpos]
			#print hdr
			hdrfields = string.split(hdr, " ", 2)
			# Should be: ("begin", "644 (or some mode)", "filename")
			if len(hdrfields) == 3 and hdrfields[0] == 'begin':
				try:
					int(hdrfields[1], 8)
				except ValueError:
					break
			else:
				break
			found_uuenc = 1
			#print hdrfields
			# filename
			hdrfields[2] = string.strip(hdrfields[2])
		
			# find body span
			endpos = string.find(bod[pos:], "\n`\nend")
			if endpos == -1:
				endpos = string.find(bod[pos:], "\nend")
			if endpos == -1:
				endpos = len(bod)
			else:
				endpos = endpos + pos
			uuenc_part = bod[endheadpos+1:endpos].strip()+"\n"

			mimetype = mimetypes.guess_type(hdrfields[2])[0]
			if mimetype == None:
				mimetype = "application/octet-stream"
			uuenc_head = { "content-transfer-encoding": "uuencode",
			               "content-type": "%s; name=\"%s\"" % (mimetype, hdrfields[2]) }
			self.parts_text.insert(1, uuenc_part)
			self.parts_header.insert(1, uuenc_head)

			pos = endpos
		if found_uuenc:
			# Set original part 0 to minus uuenc bit
			self.parts_text[0] = not_uuenc

		# We don't like empty first parts if there are text parts after it
		while len(self.parts_header) >= 2:
			if string.strip(self.parts_text[0]) == "" and self.parts_header[1].has_key("content-type"):
				if self.parts_header[1]["content-type"][:4] == "text":
					del self.parts_text[0]
					del self.parts_header[0]
					continue
				else:
					break
			else:
				break
	
	def external_parser(self, user, index):
		# Parse html bodies to plain text
		if self.get_attachment_info(index)[0] == "text/html":
			# Get a temporary filename
			tempfilename = tempfile.mktemp(".html")
			# Open it and write the html to it
			temp = open(tempfilename, "w")
			temp.write(self.decode_attachment(index))
			temp.close()
			# get output from user's html parsy proggy
			f = os.popen(user.html_parser+" %s" % tempfilename)
			parsed = f.read()
			os.remove(tempfilename)
			f.close()
			if parsed == "":
				# um. failed. maybe the prog doesn't exist
				parsed = self.parts_text[index]
			return parsed
		else:
			return self.decode_attachment(index)

	def decode_attachment(self, index):
		"""
		Decode attachment. Return as string. Big fucking string.
		May return None if we fail to decode.
		Stuff that is unencoded should pass through unchanged.
		"""
		content_type, filename, size, content_enc = self.get_attachment_info(index)
		
		if content_enc == "base64":
			f = cStringIO.StringIO(self.parts_text[index])
			o = cStringIO.StringIO()
			utils.line_decoder(f, o, binascii.a2b_base64)
			o.seek(0)
			decoded = o.read()
			f.close()
			o.close()

		elif content_enc == "yenc":
			# A C version would be so much faster
			i = self.parts_text[index]
			o = cStringIO.StringIO()
			
			# Try for nice fast C yenc decoder
			# Stick the shit in a tempfile
			tempfilename = tempfile.mktemp(".yenc")
			temp = open(tempfilename, "w")
			temp.write(self.parts_text[index])
			temp.close()
			
			f = os.popen("yencdec %s" % tempfilename)
			decoded = f.read()
			os.remove(tempfilename)
			f.close()

			# Damn. Probably yencdec not found. Default to slow python decoder
			if decoded == "":
				print "yencdec not found. using slower python decoder"
				# Iterate through characters
				# XXX nukes on escape on last char.. fix 
				pos = 0
				while pos < len(i):
					c = i[pos]
					pos += 1

					if c == '\n':
						continue
					elif c == '=':
						# Escape character
						if pos >= len(i)-1:
							# End of input. can't grab escaped char
							break
						c = i[pos]
						pos += 1
						# I wish they looped like real 8 bitty things
						c = chr((ord(c)-106)%256)
					else:
						# Normal character
						c = chr((ord(c)-42)%256)
					o.write(c)
				o.seek(0)
				decoded = o.read()

		elif content_enc == "uuencode":
			f = cStringIO.StringIO(self.parts_text[index])
			o = cStringIO.StringIO()
			utils.line_decoder(f, o, binascii.a2b_uu)
			o.seek(0)
			decoded = o.read()
			f.close()
			o.close()

		elif content_enc == "quoted-printable":
			f = cStringIO.StringIO(self.parts_text[index])
			o = cStringIO.StringIO()
			try:
				mimetools.decode(f, o, "quoted-printable")
			except Exception, e:
				print "Error decoding attachment: %s" % str(e)
				return None
			else:
				o.seek(0)
				decoded = o.read()
			f.close()
			o.close()
		else:
			if not content_enc in ["unknown", "7bit", "8bit"]:
				print "Unknown content encoding:", content_enc
			# pass through undecoded
			# line wrap if the lines are huge though
			decoded = self.parts_text[index]
		
		return decoded

	def save_attachment(self, index, filename):
		"""
		Save attachment parts[index] to file 'filename'.
		"""
		f = open(filename, "w")
		f.write(self.decode_attachment(index))
		f.close()

	def add_attachment(self, filename):
		"""
		Add a base64 encoded attachment of file 'filename'
		onto the message.
		"""
		# Read the file to be attached
		try:
			f = open(filename, "r")
		except IOError:
			return
		rawfile = f.read()
		f.close()

		# get mime type
		mimetype = mimetypes.guess_type(filename)[0]

		if mimetype == None:
			mimetype = "application/octet-stream"

		# Truncate filenames like '/home/yourname/file.txt'
		# to 'file.txt'
		smallname = os.path.basename(filename)

		part_header = {}
		part_text = base64.encodestring(rawfile)
		# Create headers
		part_header["content-type"] = "%s; name=\"%s\"" % (mimetype, smallname)
		part_header["content-transfer-encoding"] = "base64"
		part_header["content-disposition"] = "attachment; filename=\"%s\"" % smallname

		# Add to message
		self.parts_text.append(part_text)
		self.parts_header.append(part_header)

	def get_attachment_info(self, index):
		"""
		Returns list of attachment data in the form:
		[ content-type, filename, size, content-transfer-encoding ]
		"""
		part_header = self.parts_header[index]
		part_text = self.parts_text[index]
		# Get content-type
		if part_header.has_key("content-type"):
			content_type = part_header["content-type"]
			x = string.find(content_type, ";")
			if x != -1:
				content_type = content_type[:x]
		else:
			content_type = "unknown"
		# Get filename
		if part_header.has_key("content-id"):
			filename = os.path.basename( part_header["content-id"] )
		elif part_header.has_key("content-disposition"):
			content_disposition = part_header["content-disposition"]
			x = string.find(content_disposition, "filename=\"")
			if x==-1:
				filename = "noname"
			else:
				y = string.find(content_disposition[x+10:], "\"")
				filename = content_disposition[x+10:x+10+y]
		elif part_header.has_key("content-type"):
			content_disposition = part_header["content-type"]
			x = string.find(content_disposition, "name=\"")
			if x==-1:
				filename = "noname"
			else:
				y = string.find(content_disposition[x+6:], "\"")
				filename = content_disposition[x+6:x+6+y]
		else:
			filename = "noname"
		# Get content-transfer-type
		if part_header.has_key("content-transfer-encoding"):
			content_transfer_encoding = string.lower(part_header["content-transfer-encoding"])
		else:
			content_transfer_encoding = "unknown"
		# Ignores if we have been decoded yet
		length = str(len(part_text))
	
		return [ content_type, filename, length, content_transfer_encoding ]



syntax highlighted by Code2HTML, v. 0.9.1