PullFeeder sends an addition "MODE READER" to peers.
3 * see AUTHORS for the list of contributors
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package org.sonews.storage;
21 import java.io.ByteArrayInputStream;
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.nio.charset.Charset;
26 import java.security.MessageDigest;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.UUID;
29 import java.util.ArrayList;
30 import java.util.Enumeration;
31 import java.util.List;
32 import javax.mail.Header;
33 import javax.mail.Message;
34 import javax.mail.MessagingException;
35 import javax.mail.Multipart;
36 import javax.mail.internet.InternetHeaders;
37 import org.sonews.config.Config;
38 import org.sonews.util.Log;
41 * Represents a newsgroup article.
42 * @author Christian Lins
43 * @author Denis Schwerdel
46 public class Article extends ArticleHead
50 * Loads the Article identified by the given ID from the JDBCDatabase.
52 * @return null if Article is not found or if an error occurred.
54 public static Article getByMessageID(final String messageID)
58 return StorageManager.current().getArticle(messageID);
60 catch(StorageBackendException ex)
67 private byte[] body = new byte[0];
70 * Default constructor.
77 * Creates a new Article object using the date from the given
80 public Article(String headers, byte[] body)
87 this.headers = new InternetHeaders(
88 new ByteArrayInputStream(headers.getBytes()));
90 this.headerSrc = headers;
92 catch(MessagingException ex)
99 * Creates an Article instance using the data from the javax.mail.Message
101 * @see javax.mail.Message
103 * @throws IOException
104 * @throws MessagingException
106 public Article(final Message msg)
107 throws IOException, MessagingException
109 this.headers = new InternetHeaders();
111 for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();)
113 final Header header = (Header)e.nextElement();
114 this.headers.addHeader(header.getName(), header.getValue());
117 // The "content" of the message can be a String if it's a simple text/plain
118 // message, a Multipart object or an InputStream if the content is unknown.
119 final Object content = msg.getContent();
120 if(content instanceof String)
122 this.body = ((String)content).getBytes();
124 else if(content instanceof Multipart) // probably subclass MimeMultipart
126 // We're are not interested in the different parts of the MultipartMessage,
127 // so we simply read in all data which *can* be huge.
128 InputStream in = msg.getInputStream();
129 this.body = readContent(in);
131 else if(content instanceof InputStream)
133 // The message format is unknown to the Message class, but we can
134 // simply read in the whole message data.
135 this.body = readContent((InputStream)content);
139 // Unknown content is probably a malformed mail we should skip.
140 // On the other hand we produce an inconsistent mail mirror, but no
141 // mail system must transport invalid content.
142 Log.msg("Skipping message due to unknown content. Throwing exception...", true);
143 throw new MessagingException("Unknown content: " + content);
151 * Reads from the given InputString into a byte array.
152 * TODO: Move this generalized method to org.sonews.util.io.Resource.
155 * @throws IOException
157 private byte[] readContent(InputStream in)
160 ByteArrayOutputStream out = new ByteArrayOutputStream();
169 return out.toByteArray();
173 * Removes the header identified by the given key.
176 public void removeHeader(final String headerKey)
178 this.headers.removeHeader(headerKey);
179 this.headerSrc = null;
183 * Generates a message id for this article and sets it into
184 * the header object. You have to update the JDBCDatabase manually to make this
186 * Note: a Message-ID should never be changed and only generated once.
188 private String generateMessageID()
194 md5 = MessageDigest.getInstance("MD5");
196 md5.update(getBody());
197 md5.update(getHeader(Headers.SUBJECT)[0].getBytes());
198 md5.update(getHeader(Headers.FROM)[0].getBytes());
199 byte[] result = md5.digest();
200 StringBuffer hexString = new StringBuffer();
201 for (int i = 0; i < result.length; i++)
203 hexString.append(Integer.toHexString(0xFF & result[i]));
205 randomString = hexString.toString();
207 catch (NoSuchAlgorithmException e)
210 randomString = UUID.randomUUID().toString();
212 String msgID = "<" + randomString + "@"
213 + Config.inst().get(Config.HOSTNAME, "localhost") + ">";
215 this.headers.setHeader(Headers.MESSAGE_ID, msgID);
221 * Returns the body string.
223 public byte[] getBody()
229 * @return Charset of the body text
231 private Charset getBodyCharset()
233 // We espect something like
234 // Content-Type: text/plain; charset=ISO-8859-15
235 String contentType = getHeader(Headers.CONTENT_TYPE)[0];
236 int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length();
237 int idxCharsetEnd = contentType.indexOf(";", idxCharsetStart);
239 String charsetName = "UTF-8";
240 if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length())
242 if(idxCharsetEnd < 0)
244 charsetName = contentType.substring(idxCharsetStart);
248 charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd);
252 // Sometimes there are '"' around the name
253 if(charsetName.length() > 2 &&
254 charsetName.charAt(0) == '"' && charsetName.endsWith("\""))
256 charsetName = charsetName.substring(1, charsetName.length() - 2);
260 Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM
263 charset = Charset.forName(charsetName);
267 Log.msg(ex.getMessage(), false);
268 Log.msg("Article.getBodyCharset(): Unknown charset: " + charsetName, false);
274 * @return Numerical IDs of the newsgroups this Article belongs to.
276 public List<Group> getGroups()
278 String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
279 ArrayList<Group> groups = new ArrayList<Group>();
283 for(String newsgroup : groupnames)
285 newsgroup = newsgroup.trim();
286 Group group = StorageManager.current().getGroup(newsgroup);
287 if(group != null && // If the server does not provide the group, ignore it
288 !groups.contains(group)) // Yes, there may be duplicates
294 catch(StorageBackendException ex)
296 ex.printStackTrace();
302 public void setBody(byte[] body)
309 * @param groupname Name(s) of newsgroups
311 public void setGroup(String groupname)
313 this.headers.setHeader(Headers.NEWSGROUPS, groupname);
317 * Returns the Message-ID of this Article. If the appropriate header
318 * is empty, a new Message-ID is created.
319 * @return Message-ID of this Article.
321 public String getMessageID()
323 String[] msgID = getHeader(Headers.MESSAGE_ID);
324 return msgID[0].equals("") ? generateMessageID() : msgID[0];
328 * @return String containing the Message-ID.
331 public String toString()
333 return getMessageID();