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: }