chris@3: /*
chris@3: * SONEWS News Server
chris@3: * see AUTHORS for the list of contributors
chris@3: *
chris@3: * This program is free software: you can redistribute it and/or modify
chris@3: * it under the terms of the GNU General Public License as published by
chris@3: * the Free Software Foundation, either version 3 of the License, or
chris@3: * (at your option) any later version.
chris@3: *
chris@3: * This program is distributed in the hope that it will be useful,
chris@3: * but WITHOUT ANY WARRANTY; without even the implied warranty of
chris@3: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
chris@3: * GNU General Public License for more details.
chris@3: *
chris@3: * You should have received a copy of the GNU General Public License
chris@3: * along with this program. If not, see .
chris@3: */
chris@3:
chris@3: package org.sonews.storage;
chris@3:
chris@3: import java.io.ByteArrayInputStream;
chris@3: import java.io.ByteArrayOutputStream;
chris@3: import java.io.IOException;
chris@3: import java.io.InputStream;
chris@3: import java.nio.charset.Charset;
chris@3: import java.security.MessageDigest;
chris@3: import java.security.NoSuchAlgorithmException;
chris@3: import java.util.UUID;
chris@3: import java.util.ArrayList;
chris@3: import java.util.Enumeration;
chris@3: import java.util.List;
chris@3: import javax.mail.Header;
chris@3: import javax.mail.Message;
chris@3: import javax.mail.MessagingException;
chris@3: import javax.mail.Multipart;
chris@3: import javax.mail.internet.InternetHeaders;
chris@3: import org.sonews.config.Config;
chris@3: import org.sonews.util.Log;
chris@3:
chris@3: /**
chris@3: * Represents a newsgroup article.
chris@3: * @author Christian Lins
chris@3: * @author Denis Schwerdel
chris@3: * @since n3tpd/0.1
chris@3: */
chris@3: public class Article extends ArticleHead
chris@3: {
chris@3:
chris@3: /**
chris@3: * Loads the Article identified by the given ID from the JDBCDatabase.
chris@3: * @param messageID
chris@3: * @return null if Article is not found or if an error occurred.
chris@3: */
chris@3: public static Article getByMessageID(final String messageID)
chris@3: {
chris@3: try
chris@3: {
chris@3: return StorageManager.current().getArticle(messageID);
chris@3: }
chris@3: catch(StorageBackendException ex)
chris@3: {
chris@3: ex.printStackTrace();
chris@3: return null;
chris@3: }
chris@3: }
chris@3:
chris@3: private byte[] body = new byte[0];
chris@3:
chris@3: /**
chris@3: * Default constructor.
chris@3: */
chris@3: public Article()
chris@3: {
chris@3: }
chris@3:
chris@3: /**
chris@3: * Creates a new Article object using the date from the given
chris@3: * raw data.
chris@3: */
chris@3: public Article(String headers, byte[] body)
chris@3: {
chris@3: try
chris@3: {
chris@3: this.body = body;
chris@3:
chris@3: // Parse the header
chris@3: this.headers = new InternetHeaders(
chris@3: new ByteArrayInputStream(headers.getBytes()));
chris@3:
chris@3: this.headerSrc = headers;
chris@3: }
chris@3: catch(MessagingException ex)
chris@3: {
chris@3: ex.printStackTrace();
chris@3: }
chris@3: }
chris@3:
chris@3: /**
chris@3: * Creates an Article instance using the data from the javax.mail.Message
chris@3: * object.
chris@3: * @see javax.mail.Message
chris@3: * @param msg
chris@3: * @throws IOException
chris@3: * @throws MessagingException
chris@3: */
chris@3: public Article(final Message msg)
chris@3: throws IOException, MessagingException
chris@3: {
chris@3: this.headers = new InternetHeaders();
chris@3:
chris@3: for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();)
chris@3: {
chris@3: final Header header = (Header)e.nextElement();
chris@3: this.headers.addHeader(header.getName(), header.getValue());
chris@3: }
chris@3:
chris@3: // The "content" of the message can be a String if it's a simple text/plain
chris@3: // message, a Multipart object or an InputStream if the content is unknown.
chris@3: final Object content = msg.getContent();
chris@3: if(content instanceof String)
chris@3: {
cli@12: this.body = ((String)content).getBytes(getBodyCharset());
chris@3: }
chris@3: else if(content instanceof Multipart) // probably subclass MimeMultipart
chris@3: {
chris@3: // We're are not interested in the different parts of the MultipartMessage,
chris@3: // so we simply read in all data which *can* be huge.
chris@3: InputStream in = msg.getInputStream();
chris@3: this.body = readContent(in);
chris@3: }
chris@3: else if(content instanceof InputStream)
chris@3: {
chris@3: // The message format is unknown to the Message class, but we can
chris@3: // simply read in the whole message data.
chris@3: this.body = readContent((InputStream)content);
chris@3: }
chris@3: else
chris@3: {
chris@3: // Unknown content is probably a malformed mail we should skip.
chris@3: // On the other hand we produce an inconsistent mail mirror, but no
chris@3: // mail system must transport invalid content.
cli@16: Log.get().severe("Skipping message due to unknown content. Throwing exception...");
cli@16: MessagingException ex = new MessagingException("Unknown content: " + content);
cli@16: Log.get().throwing("Article.java", "", ex);
cli@16: throw ex;
chris@3: }
chris@3:
chris@3: // Validate headers
chris@3: validateHeaders();
chris@3: }
chris@3:
chris@3: /**
chris@3: * Reads from the given InputString into a byte array.
chris@3: * TODO: Move this generalized method to org.sonews.util.io.Resource.
chris@3: * @param in
chris@3: * @return
chris@3: * @throws IOException
chris@3: */
chris@3: private byte[] readContent(InputStream in)
chris@3: throws IOException
chris@3: {
chris@3: ByteArrayOutputStream out = new ByteArrayOutputStream();
chris@3:
chris@3: int b = in.read();
chris@3: while(b >= 0)
chris@3: {
chris@3: out.write(b);
chris@3: b = in.read();
chris@3: }
chris@3:
chris@3: return out.toByteArray();
chris@3: }
chris@3:
chris@3: /**
chris@3: * Removes the header identified by the given key.
chris@3: * @param headerKey
chris@3: */
chris@3: public void removeHeader(final String headerKey)
chris@3: {
chris@3: this.headers.removeHeader(headerKey);
chris@3: this.headerSrc = null;
chris@3: }
chris@3:
chris@3: /**
chris@3: * Generates a message id for this article and sets it into
chris@3: * the header object. You have to update the JDBCDatabase manually to make this
chris@3: * change persistent.
chris@3: * Note: a Message-ID should never be changed and only generated once.
chris@3: */
chris@3: private String generateMessageID()
chris@3: {
chris@3: String randomString;
chris@3: MessageDigest md5;
chris@3: try
chris@3: {
chris@3: md5 = MessageDigest.getInstance("MD5");
chris@3: md5.reset();
chris@3: md5.update(getBody());
chris@3: md5.update(getHeader(Headers.SUBJECT)[0].getBytes());
chris@3: md5.update(getHeader(Headers.FROM)[0].getBytes());
chris@3: byte[] result = md5.digest();
chris@3: StringBuffer hexString = new StringBuffer();
chris@3: for (int i = 0; i < result.length; i++)
chris@3: {
chris@3: hexString.append(Integer.toHexString(0xFF & result[i]));
chris@3: }
chris@3: randomString = hexString.toString();
chris@3: }
chris@3: catch (NoSuchAlgorithmException e)
chris@3: {
chris@3: e.printStackTrace();
chris@3: randomString = UUID.randomUUID().toString();
chris@3: }
chris@3: String msgID = "<" + randomString + "@"
chris@3: + Config.inst().get(Config.HOSTNAME, "localhost") + ">";
chris@3:
chris@3: this.headers.setHeader(Headers.MESSAGE_ID, msgID);
chris@3:
chris@3: return msgID;
chris@3: }
chris@3:
chris@3: /**
chris@3: * Returns the body string.
chris@3: */
chris@3: public byte[] getBody()
chris@3: {
chris@3: return body;
chris@3: }
chris@3:
chris@3: /**
chris@3: * @return Charset of the body text
chris@3: */
chris@3: private Charset getBodyCharset()
chris@3: {
chris@3: // We espect something like
chris@3: // Content-Type: text/plain; charset=ISO-8859-15
chris@3: String contentType = getHeader(Headers.CONTENT_TYPE)[0];
chris@3: int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length();
chris@3: int idxCharsetEnd = contentType.indexOf(";", idxCharsetStart);
chris@3:
chris@3: String charsetName = "UTF-8";
chris@3: if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length())
chris@3: {
chris@3: if(idxCharsetEnd < 0)
chris@3: {
chris@3: charsetName = contentType.substring(idxCharsetStart);
chris@3: }
chris@3: else
chris@3: {
chris@3: charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd);
chris@3: }
chris@3: }
chris@3:
chris@3: // Sometimes there are '"' around the name
chris@3: if(charsetName.length() > 2 &&
chris@3: charsetName.charAt(0) == '"' && charsetName.endsWith("\""))
chris@3: {
chris@3: charsetName = charsetName.substring(1, charsetName.length() - 2);
chris@3: }
chris@3:
chris@3: // Create charset
chris@3: Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM
chris@3: try
chris@3: {
chris@3: charset = Charset.forName(charsetName);
chris@3: }
chris@3: catch(Exception ex)
chris@3: {
cli@16: Log.get().severe(ex.getMessage());
cli@16: Log.get().severe("Article.getBodyCharset(): Unknown charset: " + charsetName);
chris@3: }
chris@3: return charset;
chris@3: }
chris@3:
chris@3: /**
chris@3: * @return Numerical IDs of the newsgroups this Article belongs to.
chris@3: */
chris@3: public List getGroups()
chris@3: {
chris@3: String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
chris@3: ArrayList groups = new ArrayList();
chris@3:
chris@3: try
chris@3: {
chris@3: for(String newsgroup : groupnames)
chris@3: {
chris@3: newsgroup = newsgroup.trim();
chris@3: Group group = StorageManager.current().getGroup(newsgroup);
chris@3: if(group != null && // If the server does not provide the group, ignore it
chris@3: !groups.contains(group)) // Yes, there may be duplicates
chris@3: {
chris@3: groups.add(group);
chris@3: }
chris@3: }
chris@3: }
chris@3: catch(StorageBackendException ex)
chris@3: {
chris@3: ex.printStackTrace();
chris@3: return null;
chris@3: }
chris@3: return groups;
chris@3: }
chris@3:
chris@3: public void setBody(byte[] body)
chris@3: {
chris@3: this.body = body;
chris@3: }
chris@3:
chris@3: /**
chris@3: *
chris@3: * @param groupname Name(s) of newsgroups
chris@3: */
chris@3: public void setGroup(String groupname)
chris@3: {
chris@3: this.headers.setHeader(Headers.NEWSGROUPS, groupname);
chris@3: }
chris@3:
chris@3: /**
chris@3: * Returns the Message-ID of this Article. If the appropriate header
chris@3: * is empty, a new Message-ID is created.
chris@3: * @return Message-ID of this Article.
chris@3: */
chris@3: public String getMessageID()
chris@3: {
chris@3: String[] msgID = getHeader(Headers.MESSAGE_ID);
chris@3: return msgID[0].equals("") ? generateMessageID() : msgID[0];
chris@3: }
chris@3:
chris@3: /**
chris@3: * @return String containing the Message-ID.
chris@3: */
chris@3: @Override
chris@3: public String toString()
chris@3: {
chris@3: return getMessageID();
chris@3: }
chris@3:
chris@3: }