#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# pdf2email - A CUPS backend written in Python. It uses GhostScript to create a
# PDF document and sends it via email to the user that requested the print.
#
# Copyright (c) George Notaras <George [D.O.T.] Notaras [A.T.] gmail [D.O.T.] com>
#
# Project Home: http://www.g-loaded.eu/2006/12/03/pdf2email-cups-backend/
#
# License: GPLv2
#
# This program is released with absolutely no warranty, expressed or implied,
# and absolutely no support.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the:
#
# Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston,
# MA 02111-1307  USA
#

#
# Configuration BEGIN
#
SMTP_Server = "mail.example.org"
print_admin = "admin@example.org"		# The email sender address.
pdfmarks_file = ""						# Absolute path to PDFMarks file. Leave
										# empty in order not to use PDFMarks.
#
# Configuration END
#



import sys, os, syslog, datetime

# Import smtplib for the actual sending function
import smtplib

# Here are the email package modules we'll need
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders


__version__ = "0.2"


def get_writable_dir():
	"""Returns the writable directory path.
	
	The Device URI of the printer is: {backend_name}:{/path/to/writable/dir}
	This is set in /etc/cups/printers.conf and is kept in an environmental
	variable named "DEVICE_URI" while this process runs.
	"""
	dev_uri = os.environ['DEVICE_URI']
	write_dir = dev_uri.split(":")[1].strip()
	if os.path.exists(write_dir):
		if os.access(write_dir, os.R_OK | os.W_OK):
			return write_dir
		else:
			raise Exception("User does not have read/write access to: %s" % write_dir)
	else:
		raise Exception("Device URI: Path does not exist: %s" % write_dir)


def get_output_filename(request_datetime):
	"""Returns a formatted filename.
	
	Filename format: {user}_{job-id}_{date_time}.pdf"""
	return "%s_%s_%s.pdf" % (sys.argv[2], sys.argv[1], request_datetime.strftime("%Y%m%d_%H%M%S"))


def get_output_filepath(request_datetime):
	"""Returns the full path to the output pdf file."""
	return os.path.join(get_writable_dir(), get_output_filename(request_datetime))


def create_pdf(output_file):
	"""Creates the PDF file.
	
	Runs the GS interpreter as a child process and returns the exit code.
	
	External Tip: when creating pdf files with GS, an initial 'save' helps so
				  that fonts are not flushed between pages. (tip source unknown)
	-sPAPERSIZE acts like a default value and needs to be set, but does not affect the final print.
	"""
	if pdfmarks_file:
		if os.path.exists(pdfmarks_file):
			command = "gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -sPAPERSIZE=a4 -dPDFSETTINGS=/printer -dDOPDFMARKS -sOutputFile=%s %s - -c save pop -f - &> /dev/null" % (output_file, pdfmarks_file)
		else:
			logger("Cannot find PDFMarks file: %s PDFMarks not used." % pdfmarks_file)
			command = "gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -sPAPERSIZE=a4 -dPDFSETTINGS=/printer -sOutputFile=%s -c save pop -f - &> /dev/null" % output_file
	else:
		command = "gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -sPAPERSIZE=a4 -dPDFSETTINGS=/printer -sOutputFile=%s -c save pop -f - &> /dev/null" % output_file
	fpipe = os.popen(command)
	ret_code = fpipe.close()	# This is the child process' exit code. None on success.
	return ret_code


def send_email(output_file, request_datetime, pdferror = False):
	"""Sends the email.
	
	Sends the created pdf file as an email attachment. If an error has
	occured in the PDF creation process, it just sends a failure notice.
	"""
	# Create the container (outer) email message.
	outer = MIMEMultipart()
	outer["Subject"] = "PDF print request on %s" % request_datetime.strftime("%A %b %m %Y %H:%M:%S")
	outer["From"] = print_admin
	outer["To"] = sys.argv[2]
	outer.preamble = "Requested PDF print."
	# Guarantees the message ends in a newline
	outer.epilogue = ""
	
	# TEXT PART
	infotext = []
	infotext.append("Print Service Notification - DO NOT REPLY\r\n")
	if pdferror:
		infotext.append("PDF print with job ID: %s - FAILED!\r\n" % sys.argv[1])
	else:
		infotext.append("Print job ID: %s" % sys.argv[1])
		infotext.append("Date and time of print request: %s" % request_datetime.strftime("%A %b %m %Y %H:%M:%S"))
		infotext.append("Processing time: %s" % (datetime.datetime.now() - request_datetime))
		infotext.append("Output file size: %.3f Kb\r\n" % (float(os.path.getsize(output_file))/float(1024)) )
		infotext.append("The PDF print was completed succesfully.")
		infotext.append("The PDF file is attached to this email.\r\n")
	
	msg = MIMEText("\r\n".join(infotext), "plain", "utf-8")
	outer.attach(msg)
	
	# PDF ATTACHMENT PART
	if not pdferror:
		fp = open(output_file, 'rb')
		msg = MIMEBase("application", "pdf")
		msg.set_payload(fp.read())
		fp.close()
		# Encode the payload using Base64
		Encoders.encode_base64(msg)
		# Set the filename parameter
		msg.add_header('Content-Disposition', 'attachment', filename = os.path.basename(output_file))
		outer.attach(msg)
	
	# Send the email via an SMTP server.
	s = smtplib.SMTP()
	s.connect(SMTP_Server)	# On the default port 25
	s.sendmail(print_admin, [sys.argv[2]], outer.as_string())
	s.close()


def logger(msg):
	"""Writes a syslog entry (msg) at the default facility and on level ERROR."""
	syslog.syslog(syslog.LOG_ERR, "CUPS PDF backend: %s" % msg)


def main():
	if len(sys.argv) == 1:
		# Without arguments should give backend info.
		# This is also used when lpinfo -v is issued, where it should include "direct this_backend"
		sys.stdout.write("direct %s \"Unknown\" \"Direct PDF Printing/Delivery to user email\"\n" % os.path.basename(sys.argv[0]))
		sys.stdout.flush()
		sys.exit(0)
	if len(sys.argv) not in (5,6):
		sys.stdout.write("Usage: %s job-id user title copies options [file]\n" % os.path.basename(sys.argv[0]))
		sys.stdout.flush()
		logger("Wrong number of arguments. Usage: %s job-id user" % sys.argv[0])
		sys.exit(1)
	request_datetime = datetime.datetime.now()
	try:
		output_file = get_output_filepath(request_datetime)
	except Exception, err:
		if err[0] <> "DEVICE_URI":	# (TODO) RE-EXAMINE THIS - This occurs when CUPS is (re)started. Probably an internal backend check.
			logger(err[0])
	else:
		ret_code = create_pdf(output_file)
		if ret_code:	# ret_code <> 0
			logger("Failed to create PDF document")
			pdferror = True
		else:
			pdferror = False
		try:
			send_email(output_file, request_datetime, pdferror)
		except:
			logger("Failed to send PDF document to user: %s" % sys.argv[2])
		if os.path.exists(output_file):
			os.remove(output_file)


if __name__=='__main__':
	main()


