/* * $Id$ * $URL$ */ package org.subethamail.common; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Level; import javax.mail.Address; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Part; import javax.mail.Session; import javax.mail.internet.InternetAddress; import lombok.extern.java.Log; import com.sun.mail.smtp.SMTPMessage; /** * Our version of the MimeMessage. Note this must extend Sun's * SMTPMessage so that we can set the envelope From (for VERP). * SMTPMessage works in collusion with the behind-the-scenes * SMTPTransport to allow this override. * * Note this means it will NOT work with any other JavaMail provider. * * @author Jeff Schnitzer */ @Log public class SubEthaMessage extends SMTPMessage { /** */ public static final String HDR_MESSAGE_ID = "Message-ID"; public static final String HDR_IN_REPLY_TO = "In-Reply-To"; public static final String HDR_REFERENCES = "References"; public static final String HDR_X_LOOP = "X-Loop"; public static final String HDR_CONTENT_TYPE = "Content-Type"; public static final String HDR_CONTENT_DISPOSITION = "Content-Disposition"; public static final String HDR_ERRORS_TO = "Errors-To"; public static final String HDR_SENDER = "Sender"; public static final String HDR_PRECEDENCE = "Precedence"; /** * Header for parts that have been detached; holds the original * content type. */ public static final String HDR_ORIGINAL_CONTENT_TYPE = "X-Original-Content-Type"; /** * The mime type for detached attachments. The content will be * the attachment id as an ascii string. */ public static final String DETACHMENT_MIME_TYPE = "application/subetha-detachment"; /** */ public SubEthaMessage(Session session) throws MessagingException { super(session); } /** */ public SubEthaMessage(Session session, InputStream is) throws MessagingException { super(session, is); // Always always assume we have been modified, otherwise changes // get ignored. The modified flag does not get reliably set by // the various methods that should set the fucking flag. this.modified = true; this.saved = false; } /** */ public SubEthaMessage(Session session, byte[] mail) throws MessagingException { this(session, new ByteArrayInputStream(mail)); } /** * Checks for any attempt to rewrite the Message-ID and ignores it. * This behavior of JavaMail is just dumb. */ @Override public void setHeader(String name, String value) throws MessagingException { if (name.equals(HDR_MESSAGE_ID)) { if (this.getMessageID() != null) return; } super.setHeader(name, value); } /** * @return the value of the In-Reply-To header field, or null if none */ public String getInReplyTo() throws MessagingException { // Note that we have a lot of work to do because there could // be a lot of garbage in this field. We want to locate the // first instance of <blahblahblah>, if it exists. // // See http://cr.yp.to/immhf/thread.html String[] values = this.getHeader(HDR_IN_REPLY_TO); if (values == null || values.length == 0) return null; if (values.length > 1) log.log(Level.SEVERE,"Found a message with {0} In-Reply-To fields", values.length); for (String field: values) { int start = field.indexOf('<'); if (start < 0) continue; int end = field.indexOf('>', start); if (end < 0) continue; return field.substring(start, end+1); } return null; } /** * @return all the references, in the same order as the header field. */ public String[] getReferences() throws MessagingException { String[] values = this.getHeader(HDR_REFERENCES); if (values == null || values.length == 0) return null; if (values.length > 1) log.log(Level.SEVERE,"Found a message with {0} References fields", values.length); StringTokenizer tokenizer = new StringTokenizer(values[0]); int count = tokenizer.countTokens(); if (count == 0) return null; String[] toks = new String[count]; int i = 0; while (tokenizer.hasMoreTokens()) { toks[i] = tokenizer.nextToken(); i++; } return toks; } /** * Generates and assigns a new message id. Uses the same algorithm * as JavaMail. */ public void replaceMessageID() throws MessagingException { String suffix = null; InternetAddress addr = InternetAddress.getLocalAddress(this.session); if (addr != null) suffix = addr.getAddress(); else suffix = "subetha@localhost"; // worst-case default StringBuffer s = new StringBuffer(); // Unique string is <hashcode>.<currentTime>.SubEtha.<suffix> s.append(s.hashCode()) .append('.') .append(System.currentTimeMillis()) .append('.') .append("SubEtha.") .append(suffix); super.setHeader(SubEthaMessage.HDR_MESSAGE_ID, "<" + s + ">"); } /** * @return a flattened container of all the textual parts in this * message. */ public List<Part> getParts() throws MessagingException, IOException { List<Part> parts = new ArrayList<Part>(); getParts(this, parts); return parts; } /** */ protected static void getParts(Part part, List<Part> parts) throws MessagingException, IOException { Object content; try { content=part.getContent(); } catch (Throwable t) { log.log(Level.WARNING,"Part decoding error", t); return; } if (content instanceof Part) { Part contentPart = (Part)content; getParts(contentPart, parts); } else if (content instanceof Multipart) { Multipart multipartContent = (Multipart)content; for (int i=0; i<multipartContent.getCount(); i++) { // Recurse getParts(multipartContent.getBodyPart(i), parts); } } else { // This was a content-containing part, no recursion. parts.add(part); } } /** * Call this if you make any changes to the message, or its parts. */ public void save() throws MessagingException { try { Object contents = this.getContent(); if (contents instanceof Multipart) { //this is dumb, but it is a javamail bug. Multipart mp = (Multipart) contents; this.setContent(mp); } } catch (IOException ex) { throw new RuntimeException(ex); } if (!this.saved) this.saveChanges(); } /** * Tests whether or not there is an existing x-loop header for the list email address. */ public boolean hasXLoop(String email) throws MessagingException { String[] xloops = this.getHeader(HDR_X_LOOP); if (xloops != null) { for (String xloop: xloops) { if (email.equals(xloop)) return true; } } return false; } /** * Adds an x-loop header */ public void addXLoop(String email) throws MessagingException { this.addHeader(HDR_X_LOOP, email); } /** * @return the text that should be indexed */ public String getIndexableText() throws MessagingException, IOException { StringBuilder buf = new StringBuilder(); for (Part part: this.getParts()) { if (part.getContentType().toLowerCase().startsWith("text/")) { buf.append(part.getContent().toString()); } } return buf.toString(); } /** * @return the first found of Sender, From, or envelope sender * @throws MessagingException */ public InternetAddress getSenderWithFallback(String envelopeSender) throws MessagingException { // Convoluted process to determine sender. // Check, in order: Sender field, first entry of From field, envelope sender InternetAddress senderField = (InternetAddress)this.getSender(); if (senderField == null) { Address[] froms = this.getFrom(); if (froms != null && froms.length > 0) senderField = (InternetAddress)froms[0]; else senderField = new InternetAddress(envelopeSender); } return senderField; } }