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