chris@1: /*
chris@1: * SONEWS News Server
chris@1: * see AUTHORS for the list of contributors
chris@1: *
chris@1: * This program is free software: you can redistribute it and/or modify
chris@1: * it under the terms of the GNU General Public License as published by
chris@1: * the Free Software Foundation, either version 3 of the License, or
chris@1: * (at your option) any later version.
chris@1: *
chris@1: * This program is distributed in the hope that it will be useful,
chris@1: * but WITHOUT ANY WARRANTY; without even the implied warranty of
chris@1: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
chris@1: * GNU General Public License for more details.
chris@1: *
chris@1: * You should have received a copy of the GNU General Public License
chris@1: * along with this program. If not, see .
chris@1: */
chris@1:
chris@1: package org.sonews.daemon.storage;
chris@1:
chris@1: import org.sonews.daemon.Config;
chris@1: import java.io.BufferedReader;
chris@1: import java.io.ByteArrayInputStream;
chris@1: import java.io.IOException;
chris@1: import java.io.InputStream;
chris@1: import java.io.InputStreamReader;
chris@1: import java.nio.charset.Charset;
chris@1: import java.sql.SQLException;
chris@1: import java.util.UUID;
chris@1: import java.util.ArrayList;
chris@1: import java.util.Enumeration;
chris@1: import java.util.List;
chris@1: import javax.mail.Header;
chris@1: import javax.mail.Message;
chris@1: import javax.mail.MessagingException;
chris@1: import javax.mail.Multipart;
chris@1: import javax.mail.internet.InternetHeaders;
chris@1: import javax.mail.internet.MimeUtility;
chris@1: import org.sonews.util.Log;
chris@1:
chris@1: /**
chris@1: * Represents a newsgroup article.
chris@1: * @author Christian Lins
chris@1: * @author Denis Schwerdel
chris@1: * @since n3tpd/0.1
chris@1: */
chris@1: public class Article extends ArticleHead
chris@1: {
chris@1:
chris@1: /**
chris@1: * Loads the Article identified by the given ID from the Database.
chris@1: * @param messageID
chris@1: * @return null if Article is not found or if an error occurred.
chris@1: */
chris@1: public static Article getByMessageID(final String messageID)
chris@1: {
chris@1: try
chris@1: {
chris@1: return Database.getInstance().getArticle(messageID);
chris@1: }
chris@1: catch(SQLException ex)
chris@1: {
chris@1: ex.printStackTrace();
chris@1: return null;
chris@1: }
chris@1: }
chris@1:
chris@1: public static Article getByArticleNumber(long articleIndex, Group group)
chris@1: throws SQLException
chris@1: {
chris@1: return Database.getInstance().getArticle(articleIndex, group.getID());
chris@1: }
chris@1:
chris@1: private String body = "";
chris@1: private String headerSrc = null;
chris@1:
chris@1: /**
chris@1: * Default constructor.
chris@1: */
chris@1: public Article()
chris@1: {
chris@1: }
chris@1:
chris@1: /**
chris@1: * Creates a new Article object using the date from the given
chris@1: * raw data.
chris@1: * This construction has only package visibility.
chris@1: */
chris@1: Article(String headers, String body)
chris@1: {
chris@1: try
chris@1: {
chris@1: this.body = body;
chris@1:
chris@1: // Parse the header
chris@1: this.headers = new InternetHeaders(
chris@1: new ByteArrayInputStream(headers.getBytes()));
chris@1:
chris@1: this.headerSrc = headers;
chris@1: }
chris@1: catch(MessagingException ex)
chris@1: {
chris@1: ex.printStackTrace();
chris@1: }
chris@1: }
chris@1:
chris@1: /**
chris@1: * Creates an Article instance using the data from the javax.mail.Message
chris@1: * object.
chris@1: * @see javax.mail.Message
chris@1: * @param msg
chris@1: * @throws IOException
chris@1: * @throws MessagingException
chris@1: */
chris@1: public Article(final Message msg)
chris@1: throws IOException, MessagingException
chris@1: {
chris@1: this.headers = new InternetHeaders();
chris@1:
chris@1: for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();)
chris@1: {
chris@1: final Header header = (Header)e.nextElement();
chris@1: this.headers.addHeader(header.getName(), header.getValue());
chris@1: }
chris@1:
chris@1: // The "content" of the message can be a String if it's a simple text/plain
chris@1: // message, a Multipart object or an InputStream if the content is unknown.
chris@1: final Object content = msg.getContent();
chris@1: if(content instanceof String)
chris@1: {
chris@1: this.body = (String)content;
chris@1: }
chris@1: else if(content instanceof Multipart) // probably subclass MimeMultipart
chris@1: {
chris@1: // We're are not interested in the different parts of the MultipartMessage,
chris@1: // so we simply read in all data which *can* be huge.
chris@1: InputStream in = msg.getInputStream();
chris@1: this.body = readContent(in);
chris@1: }
chris@1: else if(content instanceof InputStream)
chris@1: {
chris@1: // The message format is unknown to the Message class, but we can
chris@1: // simply read in the whole message data.
chris@1: this.body = readContent((InputStream)content);
chris@1: }
chris@1: else
chris@1: {
chris@1: // Unknown content is probably a malformed mail we should skip.
chris@1: // On the other hand we produce an inconsistent mail mirror, but no
chris@1: // mail system must transport invalid content.
chris@1: Log.msg("Skipping message due to unknown content. Throwing exception...", true);
chris@1: throw new MessagingException("Unknown content: " + content);
chris@1: }
chris@1:
chris@1: // Validate headers
chris@1: validateHeaders();
chris@1: }
chris@1:
chris@1: /**
chris@1: * Reads lines from the given InputString into a String object.
chris@1: * TODO: Move this generalized method to org.sonews.util.io.Resource.
chris@1: * @param in
chris@1: * @return
chris@1: * @throws IOException
chris@1: */
chris@1: private String readContent(InputStream in)
chris@1: throws IOException
chris@1: {
chris@1: StringBuilder buf = new StringBuilder();
chris@1:
chris@1: BufferedReader rin = new BufferedReader(new InputStreamReader(in));
chris@1: String line = rin.readLine();
chris@1: while(line != null)
chris@1: {
chris@1: buf.append('\n');
chris@1: buf.append(line);
chris@1: line = rin.readLine();
chris@1: }
chris@1:
chris@1: return buf.toString();
chris@1: }
chris@1:
chris@1: /**
chris@1: * Removes the header identified by the given key.
chris@1: * @param headerKey
chris@1: */
chris@1: public void removeHeader(final String headerKey)
chris@1: {
chris@1: this.headers.removeHeader(headerKey);
chris@1: this.headerSrc = null;
chris@1: }
chris@1:
chris@1: /**
chris@1: * Generates a message id for this article and sets it into
chris@1: * the header object. You have to update the Database manually to make this
chris@1: * change persistent.
chris@1: * Note: a Message-ID should never be changed and only generated once.
chris@1: */
chris@1: private String generateMessageID()
chris@1: {
chris@1: String msgID = "<" + UUID.randomUUID() + "@"
chris@1: + Config.getInstance().get(Config.HOSTNAME, "localhost") + ">";
chris@1:
chris@1: this.headers.setHeader(Headers.MESSAGE_ID, msgID);
chris@1:
chris@1: return msgID;
chris@1: }
chris@1:
chris@1: /**
chris@1: * Returns the body string.
chris@1: */
chris@1: public String getBody()
chris@1: {
chris@1: return body;
chris@1: }
chris@1:
chris@1: /**
chris@1: * @return Charset of the body text
chris@1: */
chris@1: public Charset getBodyCharset()
chris@1: {
chris@1: // We espect something like
chris@1: // Content-Type: text/plain; charset=ISO-8859-15
chris@1: String contentType = getHeader(Headers.CONTENT_TYPE)[0];
chris@1: int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length();
chris@1: int idxCharsetEnd = contentType.indexOf(";", idxCharsetStart);
chris@1:
chris@1: String charsetName = "UTF-8";
chris@1: if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length())
chris@1: {
chris@1: if(idxCharsetEnd < 0)
chris@1: {
chris@1: charsetName = contentType.substring(idxCharsetStart);
chris@1: }
chris@1: else
chris@1: {
chris@1: charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd);
chris@1: }
chris@1: }
chris@1:
chris@1: // Sometimes there are '"' around the name
chris@1: if(charsetName.length() > 2 &&
chris@1: charsetName.charAt(0) == '"' && charsetName.endsWith("\""))
chris@1: {
chris@1: charsetName = charsetName.substring(1, charsetName.length() - 2);
chris@1: }
chris@1:
chris@1: // Create charset
chris@1: Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM
chris@1: try
chris@1: {
chris@1: charset = Charset.forName(charsetName);
chris@1: }
chris@1: catch(Exception ex)
chris@1: {
chris@1: Log.msg(ex.getMessage(), false);
chris@1: Log.msg("Article.getBodyCharset(): Unknown charset: " + charsetName, false);
chris@1: }
chris@1: return charset;
chris@1: }
chris@1:
chris@1: /**
chris@1: * @return Numerical IDs of the newsgroups this Article belongs to.
chris@1: */
chris@1: List getGroups()
chris@1: {
chris@1: String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
chris@1: ArrayList groups = new ArrayList();
chris@1:
chris@1: try
chris@1: {
chris@1: for(String newsgroup : groupnames)
chris@1: {
chris@1: newsgroup = newsgroup.trim();
chris@1: Group group = Database.getInstance().getGroup(newsgroup);
chris@1: if(group != null && // If the server does not provide the group, ignore it
chris@1: !groups.contains(group)) // Yes, there may be duplicates
chris@1: {
chris@1: groups.add(group);
chris@1: }
chris@1: }
chris@1: }
chris@1: catch (SQLException ex)
chris@1: {
chris@1: ex.printStackTrace();
chris@1: return null;
chris@1: }
chris@1: return groups;
chris@1: }
chris@1:
chris@1: public void setBody(String body)
chris@1: {
chris@1: this.body = body;
chris@1: }
chris@1:
chris@1: /**
chris@1: *
chris@1: * @param groupname Name(s) of newsgroups
chris@1: */
chris@1: public void setGroup(String groupname)
chris@1: {
chris@1: this.headers.setHeader(Headers.NEWSGROUPS, groupname);
chris@1: }
chris@1:
chris@1: public String getMessageID()
chris@1: {
chris@1: String[] msgID = getHeader(Headers.MESSAGE_ID);
chris@1: return msgID[0];
chris@1: }
chris@1:
chris@1: public Enumeration getAllHeaders()
chris@1: {
chris@1: return this.headers.getAllHeaders();
chris@1: }
chris@1:
chris@1: /**
chris@1: * @return Header source code of this Article.
chris@1: */
chris@1: public String getHeaderSource()
chris@1: {
chris@1: if(this.headerSrc != null)
chris@1: {
chris@1: return this.headerSrc;
chris@1: }
chris@1:
chris@1: StringBuffer buf = new StringBuffer();
chris@1:
chris@1: for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();)
chris@1: {
chris@1: Header entry = (Header)en.nextElement();
chris@1:
chris@1: buf.append(entry.getName());
chris@1: buf.append(": ");
chris@1: buf.append(
chris@1: MimeUtility.fold(entry.getName().length() + 2, entry.getValue()));
chris@1:
chris@1: if(en.hasMoreElements())
chris@1: {
chris@1: buf.append("\r\n");
chris@1: }
chris@1: }
chris@1:
chris@1: this.headerSrc = buf.toString();
chris@1: return this.headerSrc;
chris@1: }
chris@1:
chris@1: public long getIndexInGroup(Group group)
chris@1: throws SQLException
chris@1: {
chris@1: return Database.getInstance().getArticleIndex(this, group);
chris@1: }
chris@1:
chris@1: /**
chris@1: * Sets the headers of this Article. If headers contain no
chris@1: * Message-Id a new one is created.
chris@1: * @param headers
chris@1: */
chris@1: public void setHeaders(InternetHeaders headers)
chris@1: {
chris@1: this.headers = headers;
chris@1: validateHeaders();
chris@1: }
chris@1:
chris@1: /**
chris@1: * @return String containing the Message-ID.
chris@1: */
chris@1: @Override
chris@1: public String toString()
chris@1: {
chris@1: return getMessageID();
chris@1: }
chris@1:
chris@1: /**
chris@1: * Checks some headers for their validity and generates an
chris@1: * appropriate Path-header for this host if not yet existing.
chris@1: * This method is called by some Article constructors and the
chris@1: * method setHeaders().
chris@1: * @return true if something on the headers was changed.
chris@1: */
chris@1: private void validateHeaders()
chris@1: {
chris@1: // Check for valid Path-header
chris@1: final String path = getHeader(Headers.PATH)[0];
chris@1: final String host = Config.getInstance().get(Config.HOSTNAME, "localhost");
chris@1: if(!path.startsWith(host))
chris@1: {
chris@1: StringBuffer pathBuf = new StringBuffer();
chris@1: pathBuf.append(host);
chris@1: pathBuf.append('!');
chris@1: pathBuf.append(path);
chris@1: this.headers.setHeader(Headers.PATH, pathBuf.toString());
chris@1: }
chris@1:
chris@1: // Generate a messageID if no one is existing
chris@1: if(getMessageID().equals(""))
chris@1: {
chris@1: generateMessageID();
chris@1: }
chris@1: }
chris@1:
chris@1: }