Remote Printing – E-mail – Part 3 Printer POP3

(Unfortunately this is the second time I’m writing this as I trashed my database to correct an apostrophe problem before thinking about the post I’d just written…)

I’ve just switched hosts this week and was updating various settings in my network when I logged into the Kyocera Command Center and came across a page that I’ve probably seen before, but never understood what it could do. The Kyocera printer can listen to up to three POP3 addresses on a scheduled basis and print those emails out! Basically it does everything I’d written in Part 1 – for free and without me having to write/maintain/run it… So far though, I’ve only gotten it to print the attachments on emails – but that’s what I want it for.

Configuring Kyocera Printer POP3

Open Advanced -> POP3 -> User 1(, 2 or 3)

Edit the User page accordingly – ask your Host/ISP if you are unsure of any settings

advanced-pop3-user1

When complete, click Test to make sure the settings are working.  If they are you’ll see the following

pop3-test

Click “Back to the previous page” and Submit the page to save the settings.

Next, click on the General tab…

general-tab

and fill in the page with appropriate values

advanced-pop3

Submit the changes to save them

To test everything is configured correctly and that the printer is listening, send an email containing a single page PDF to the address specified on the User page.

The PDF should be printed – might take a few minutes depending  on the interval value you’ve specified.

Voilà!

Remote Printing – E-mail – Part 2 HTML

In this part, I’ve added the ability to print HTML. In order to print HTML, the HTML first needs to be converted so that it can be put into a PDF document and printed just like a PDF.

The following class has been extended to handle HTML attachments and print them. It’s a bit of a round about way of printing the HTML attachments – converting them with iText and then using PDFBox to print the – but it works.

package uk.co.vsf.printer.kyocera.component.impl;

import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.List;

import javax.activation.MimeType;
import javax.print.PrintException;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;

import uk.co.vsf.printer.kyocera.MimeTypeHandler;
import uk.co.vsf.printer.kyocera.bean.Attachment;
import uk.co.vsf.printer.kyocera.bean.Email;
import uk.co.vsf.printer.kyocera.component.PrinterComponent;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.html.simpleparser.HTMLWorker;
import com.lowagie.text.pdf.PdfWriter;

/**
 * @author Victoria Scales
 */
public class PrinterComponentImpl implements PrinterComponent {
    private Logger logger = Logger.getLogger(getClass());
    private MimeTypeHandler mimeTypeHandler;

    private void printAttachment(Attachment attachment) throws PrintException, IOException, PrinterException {
        logger.info("Attachment: " + attachment);

        MimeType attachmentMimeType = attachment.getMimeType();
        
        if (mimeTypeHandler.isPdf(attachmentMimeType)) {
            logger.info("Print pdf attachment: " + attachment);
            printPdf(attachment.getData());
            return;
        } else if (mimeTypeHandler.isHtml(attachmentMimeType)) {
            logger.info("Print html attachment: " + attachment);
            printHtml(attachment);
            return;
        }
        
        logger.info("Attachment not printed: " + attachment.getMimeType());
    }

    private void printHtml(Attachment attachment) throws IOException, PrinterException {
        // convert the html to text to byte array representing pdf data
        Document document = new Document();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            PdfWriter.getInstance(document, baos);
        } catch (DocumentException e) {
            throw new ITextPdfDocumentCreationException(e);
        }

        document.open();

        // convert html to elements and add to the PDF
        List<Element> elements = HTMLWorker.parseToList(new StringReader(IOUtils.toString(attachment.getData())), null);
        for (Element element : elements) {
            try {
                document.add(element);
            } catch (DocumentException e) {
                e.printStackTrace();
            }
        }

        document.close();

        ByteArrayInputStream bias = new ByteArrayInputStream(baos.toByteArray());

        printPdf(bias);
    }

    /**
     * Handles printing PDF attachments.
     * 
     * @param attachment
     *            to print
     * @throws IOException
     *             if an error occurs during loading/closing of the attachment.
     * @throws PrinterException
     *             if an error occurs during the print operation.
     */
    private void printPdf(InputStream inputStream) throws IOException, PrinterException {

        PDDocument document = null;

        try {
            document = PDDocument.load(inputStream);
            PrinterJob printJob = PrinterJob.getPrinterJob();
            document.silentPrint(printJob);
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void printEmail(Email email) {
        for (Attachment attachment : email.getAttachments()) {
            try {
                printAttachment(attachment);
            } catch (PrintException e) {
                new PrintingException(e);
            } catch (IOException e) {
                new PrintingException(e);
            } catch (PrinterException e) {
                new PrintingException(e);
            }
        }
    }

    public void setMimeTypeHandler(MimeTypeHandler mimeTypeHandler) {
        this.mimeTypeHandler = mimeTypeHandler;
    }
}

Remote Printing – E-mail

If like me, you’ve been looking at printers recently, you might well have noticed that some printers now come with email addresses allowing you to email the printer with items to print. I’ve already written a few months ago about the fact that I chose to buy a Kyocera laser printer and one of the missing pieces of functionality is this new feature. Therefore the rest of this article is about adding the functionality to an existing printer.

What I have:
A printer I’d like to send emails to!
A server which runs all the time (or I could have used a PC which is switched on frequently) and is within the same network as the printer. I have an N40L Microserver.
Java runtime installed on the server.

Essentially the Java app will sit on the server, listen to an email address, pick up messages and send them to the printer.

I happen to be running Ubuntu server on the N40L, so I’ve installed a program called CUPS. I don’t think I’ve got CUPS set up correctly yet as I’ve had to open permission for anyone on the network to access it. Anyway, then I’ve added the Kyocera printer to the CUPS program using the drivers from the Kyocera website. It would have been a lot easier if my server was running Windows, but that’s probably just because I’ve used Windows for years.

The next part was to write the app which would read messages from the mail server and send them on to the printer.

The first step is to read the messages from the mail server and process them. There are many ways to connect to a mail server, but since I use Mule at work, I’ve decided to use Mule 3.3 (as in the PVOutput example) in order to further my knowledge of Mule. The following is my basic Mule config file for reading the messages from my mail server and send them to the SmtpComponent

<?xml version="1.0" encoding="UTF-8"?>
<mule xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
	xmlns:pop3="http://www.mulesoft.org/schema/mule/pop3" xmlns:spring="http://www.springframework.org/schema/beans"
	xmlns:core="http://www.mulesoft.org/schema/mule/core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
http://www.mulesoft.org/schema/mule/pop3 http://www.mulesoft.org/schema/mule/pop3/current/mule-pop3.xsd 
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
    <context:property-placeholder location="printserver.properties"/>

	<spring:beans>
		<spring:bean id="SmtpComponent" class="uk.co.vsf.printer.kyocera.component.impl.SmtpComponentImpl">
			<spring:property name="printerComponent" ref="PrinterComponent" />
		</spring:bean>

		<spring:bean id="PrinterComponent" class="uk.co.vsf.printer.kyocera.component.impl.PrinterComponentImpl">
			<spring:property name="mimeTypeHandler" ref="MimeTypeHandler" />
		</spring:bean>

		<spring:bean id="MimeTypeHandler" class="uk.co.vsf.printer.kyocera.impl.MimeTypeHandlerImpl" />

		<spring:bean id="FromAddressFilter" class="uk.co.vsf.printer.kyocera.filter.impl.FromAddressFilterImpl">
			<spring:property name="fromAddressesAllowed" value="${allowed.addresses.from}" />
		</spring:bean>

		<spring:bean id="SociableHoursFilterImpl" class="uk.co.vsf.printer.kyocera.filter.impl.SociableHoursFilterImpl" />
	</spring:beans>
    
    <pop3:connector name="POP3" validateConnections="true" doc:name="POP3" checkFrequency="${pop3.pollfrequency}"/>
    
    <flow name="mainFlow">
        <pop3:inbound-endpoint host="${pop3.host}" 
        					   port="${pop3.port}" 
        					   user="${pop3.username}" 
        					   password="${pop3.password}" 
        					   responseTimeout="10000" 
        					   connector-ref="POP3">
            <filter ref="FromAddressFilter" />
            <filter ref="SociableHoursFilterImpl" />
        </pop3:inbound-endpoint>
        <custom-transformer class="uk.co.vsf.printer.kyocera.transformer.MailMessageToObjectTransformer">
        	<spring:property name="mimeTypeHandler" ref="MimeTypeHandler" />
        </custom-transformer>
        <component>
            <spring-object bean="SmtpComponent"/>
        </component>
    </flow>
</mule>

Messages are received from the Pop3 endpoint, filtered, transformed and then processed by a custom component.

The filter
Since this app will sit on my server 24/7 listening and responding to emails, there needs to be a few security and ground rules.
1. Only people on an allowed list can print documents,
2. Only between the hours of 7:30am and 10:30pm

Since the email address could be guessed and since I don’t want to print other peoples emails or run though 500 pages of paper while I’m at work I need to limit who can send emails to the printer.

The first filter is a from address filter which works on a basic list of strings representing email addresses.

package uk.co.vsf.printer.kyocera.filter.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.log4j.Logger;
import org.mule.api.MuleMessage;
import org.mule.api.transport.PropertyScope;

import uk.co.vsf.printer.kyocera.filter.FromAddressFilter;

/**
 * @author Victoria Scales
 */
public class FromAddressFilterImpl implements FromAddressFilter {
    private Logger logger = Logger.getLogger(getClass());
    private List<String> fromAddressesAllowedList = new ArrayList<String>();

    /**
     * {@inheritDoc}
     */
    public boolean accept(MuleMessage message) {
        String fromAddress = message.getProperty("from", PropertyScope.OUTBOUND);
        fromAddress = tidyFromAddress(fromAddress);
        boolean allowed = fromAddressesAllowedList.contains(fromAddress);

        if (!allowed) {
            logger.warn("Received email from: '" + fromAddress + "'.  Message was rejected as it was not in the allowed from address list");
        }

        return allowed;
    }

    public void setFromAddressesAllowed(String[] strings) {
        if (strings != null && strings.length > 0) {
            fromAddressesAllowedList.addAll(Arrays.asList(strings));
        }
    }

    /**
     * Mule stores the from address as a string in the following format
     * "desired name + < + email address + >"
     * 
     * @param fromAddress
     *            to tidy up
     * @return just the email address
     */
    private String tidyFromAddress(String fromAddress) {
        if (fromAddress == null) {
            return null;
        }

        if (fromAddress.contains("<")) {
            int startOfMailAddress = fromAddress.lastIndexOf("<");
            int endOfMailAddress = fromAddress.lastIndexOf(">");

            return fromAddress.substring(startOfMailAddress + 1, endOfMailAddress);

        }
        return fromAddress;
    }
}

The second filter does basic date time checking

package uk.co.vsf.printer.kyocera.filter.impl;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import org.apache.log4j.Logger;
import org.mule.api.MuleMessage;
import org.mule.api.transport.PropertyScope;

import uk.co.vsf.printer.kyocera.filter.SociableHoursFilter;

/**
 * @author Victoria Scales
 */
public class SociableHoursFilterImpl implements SociableHoursFilter {
    private Logger logger = Logger.getLogger(getClass());
    private int starthour = 7;
    private int startMinute = 30;
    private int endHour = 22;
    private int endMinute = 30;

    /**
     * {@inheritDoc}
     */
    public boolean accept(MuleMessage message) {
        Date sentDate = message.getProperty("sentDate", PropertyScope.OUTBOUND);

        Calendar sentCal = new GregorianCalendar();
        sentCal.setTime(sentDate);

        int hour = sentCal.get(Calendar.HOUR_OF_DAY);
        int minute = sentCal.get(Calendar.MINUTE);

        boolean result = checkStartTime(hour, minute);
        if (result) {
            result = checkEndTime(hour, minute);
        }

        return result;
    }

    private boolean checkEndTime(int hour, int minute) {
        if (hour > endHour) {
            logger.info("Email suppressed because time is past " + endHour + ":" + endMinute);
            return false;
        } else if (hour == endHour) {
            if (minute <= endMinute) {
                return true;
            }
            logger.info("Email suppressed because time is past " + endHour + ":" + endMinute);
            return false;
        }

        return true;
    }

    private boolean checkStartTime(int hour, int minute) {
        if (hour < starthour) {
            logger.info("Email suppressed because time is before " + starthour + ":" + startMinute);
            return false;
        } else if (hour == starthour) {
            if (minute >= startMinute) {
                return true;
            }
            logger.info("Email suppressed because time is before " + starthour + ":" + startMinute);
            return false;
        }

        return true;
    }
}

At the moment, if there is a problem with the filters, the entire message will be lost. If the first filter were to fail, ideally a message should be sent to the administrator with the original message so that they can check the message. The second filter should re-queue (effectively) the message so that at 7:30am the messages received are printed.

Transformer
Transforming from a Mime message to useful data is harder than I’d thought. The following is my current implementation, but it was only identified by trial and error and might not be ideal…

package uk.co.vsf.printer.kyocera.transformer;

import java.io.IOException;
import java.util.Date;
import java.util.Set;

import javax.activation.DataHandler;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMultipart;

import org.mule.api.MuleMessage;
import org.mule.api.transformer.TransformerException;
import org.mule.api.transport.PropertyScope;
import org.mule.transformer.AbstractMessageTransformer;

import uk.co.vsf.printer.kyocera.MimeTypeHandler;
import uk.co.vsf.printer.kyocera.bean.Attachment;
import uk.co.vsf.printer.kyocera.bean.Email;

/**
 * @author Victoria Scales
 */
public class MailMessageToObjectTransformer extends AbstractMessageTransformer {

    private MimeTypeHandler mimeTypeHandler;

    @Override
    public Object transformMessage(MuleMessage message, String outputEncoding) throws TransformerException {
        Email email = new Email();

        Set<String> names = message.getInboundAttachmentNames();

        String subject = message.getProperty("subject", PropertyScope.OUTBOUND);
        email.setSubject(subject);
        String fromAddress = message.getProperty("from", PropertyScope.OUTBOUND);
        email.setFromAddress(fromAddress);
        Date sentDate = message.getProperty("sentDate", PropertyScope.OUTBOUND);
        email.setSentDate(sentDate);

        handleAttachments(message, email, names);

        return email;
    }

    private void handleAttachments(MuleMessage message, Email email, Set<String> names) {
        for (String attachementName : names) {
            Attachment attachment = new Attachment();
            attachment.setName(attachementName);
            email.getAttachments().add(attachment);

            DataHandler dataHandler = message.getInboundAttachment(attachementName);

            try {
                String mimeType = dataHandler.getContentType();
                try {
                    MimeType realMimeType = new MimeType(mimeType);
                    if (mimeTypeHandler.isBody(realMimeType)) {
                        constructFromBody(attachment, (MimeMultipart) dataHandler.getContent());
                    } else {

                        attachment.setData(dataHandler.getInputStream());
                        dataHandler.getContentType();

                        attachment.setMimeType(realMimeType);
                    }
                } catch (MimeTypeParseException e) {
                    throw new MailMessageTransformationException(e);
                }
            } catch (IOException e) {
                throw new MailMessageTransformationException(e);
            }
        }
    }

    private void constructFromBody(Attachment attachment, MimeMultipart mimeMultipart) throws MimeTypeParseException {
        try {
            BodyPart bodyPart = mimeMultipart.getBodyPart(0);
            if (mimeMultipart.getCount() > 1) {
                bodyPart = mimeMultipart.getBodyPart(1);
            }

            DataHandler dataHandler = bodyPart.getDataHandler();

            attachment.setData(dataHandler.getInputStream());
            MimeType mimeType = new MimeType(dataHandler.getContentType());

            attachment.setMimeType(mimeType);
        } catch (IOException e) {
            throw new MailMessageTransformationException(e);
        } catch (MessagingException e) {
            throw new MailMessageTransformationException(e);
        }
    }

    public void setMimeTypeHandler(MimeTypeHandler mimeTypeHandler) {
        this.mimeTypeHandler = mimeTypeHandler;
    }
}

Component
It turns out printing PDFs in Java is harder than expected… Even my shiny £500 printer doesn’t accept PDFs. So after sending 15 pages of rubbish to the printer… a search on the internet later I found that apache has a project called PDFBox. It’s fairly simple to use (in terms of printing); create a PDDocument and print.

package uk.co.vsf.printer.kyocera.component.impl;

import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.IOException;

import javax.activation.MimeType;
import javax.print.PrintException;

import org.apache.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;

import uk.co.vsf.printer.kyocera.MimeTypeHandler;
import uk.co.vsf.printer.kyocera.bean.Attachment;
import uk.co.vsf.printer.kyocera.bean.Email;
import uk.co.vsf.printer.kyocera.component.PrinterComponent;

/**
 * @author Victoria Scales
 */
public class PrinterComponentImpl implements PrinterComponent {
    private Logger logger = Logger.getLogger(getClass());
    private MimeTypeHandler mimeTypeHandler;

    private void printAttachment(Attachment attachment) throws PrintException, IOException, PrinterException {
        logger.info("Print attachment: " + attachment);
        MimeType attachmentMimeType = attachment.getMimeType();

        if (mimeTypeHandler.isPdf(attachmentMimeType)) {
            printPdf(attachment);
        }
    }

    /**
     * Handles printing PDF attachments.
     * 
     * @param attachment
     *            to print
     * @throws IOException
     *             if an error occurs during loading/closing of the attachment.
     * @throws PrinterException
     *             if an error occurs during the print operation.
     */
    private void printPdf(Attachment attachment) throws IOException, PrinterException {
        logger.info("Print pdf attachment: " + attachment);

        PDDocument document = null;

        try {
            document = PDDocument.load(attachment.getData());
            PrinterJob printJob = PrinterJob.getPrinterJob();
            document.silentPrint(printJob);
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void printEmail(Email email) {
        for (Attachment attachment : email.getAttachments()) {
            try {
                printAttachment(attachment);
            } catch (PrintException e) {
                new PrintingException(e);
            } catch (IOException e) {
                new PrintingException(e);
            } catch (PrinterException e) {
                new PrintingException(e);
            }
        }
    }

    public void setMimeTypeHandler(MimeTypeHandler mimeTypeHandler) {
        this.mimeTypeHandler = mimeTypeHandler;
    }
}

Printing attachments of type PDF has actually turned out easier than printing the body of the email. As such I’ve decided to work on the app in stages. Currently the app will read an email and print any attached PDFs, but the rest of the email is thrown away.

Part 2 adds the ability to print HTML attachments