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.daemon.storage;
21 import org.sonews.daemon.Config;
22 import java.io.BufferedReader;
23 import java.io.ByteArrayInputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.nio.charset.Charset;
28 import java.sql.SQLException;
29 import java.util.UUID;
30 import java.util.ArrayList;
31 import java.util.Enumeration;
32 import java.util.List;
33 import javax.mail.Header;
34 import javax.mail.Message;
35 import javax.mail.MessagingException;
36 import javax.mail.Multipart;
37 import javax.mail.internet.InternetHeaders;
38 import javax.mail.internet.MimeUtility;
39 import org.sonews.util.Log;
42 * Represents a newsgroup article.
43 * @author Christian Lins
44 * @author Denis Schwerdel
47 public class Article extends ArticleHead
51 * Loads the Article identified by the given ID from the Database.
53 * @return null if Article is not found or if an error occurred.
55 public static Article getByMessageID(final String messageID)
59 return Database.getInstance().getArticle(messageID);
61 catch(SQLException ex)
68 public static Article getByArticleNumber(long articleIndex, Group group)
71 return Database.getInstance().getArticle(articleIndex, group.getID());
74 private String body = "";
75 private String headerSrc = null;
78 * Default constructor.
85 * Creates a new Article object using the date from the given
87 * This construction has only package visibility.
89 Article(String headers, String body)
96 this.headers = new InternetHeaders(
97 new ByteArrayInputStream(headers.getBytes()));
99 this.headerSrc = headers;
101 catch(MessagingException ex)
103 ex.printStackTrace();
108 * Creates an Article instance using the data from the javax.mail.Message
110 * @see javax.mail.Message
112 * @throws IOException
113 * @throws MessagingException
115 public Article(final Message msg)
116 throws IOException, MessagingException
118 this.headers = new InternetHeaders();
120 for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();)
122 final Header header = (Header)e.nextElement();
123 this.headers.addHeader(header.getName(), header.getValue());
126 // The "content" of the message can be a String if it's a simple text/plain
127 // message, a Multipart object or an InputStream if the content is unknown.
128 final Object content = msg.getContent();
129 if(content instanceof String)
131 this.body = (String)content;
133 else if(content instanceof Multipart) // probably subclass MimeMultipart
135 // We're are not interested in the different parts of the MultipartMessage,
136 // so we simply read in all data which *can* be huge.
137 InputStream in = msg.getInputStream();
138 this.body = readContent(in);
140 else if(content instanceof InputStream)
142 // The message format is unknown to the Message class, but we can
143 // simply read in the whole message data.
144 this.body = readContent((InputStream)content);
148 // Unknown content is probably a malformed mail we should skip.
149 // On the other hand we produce an inconsistent mail mirror, but no
150 // mail system must transport invalid content.
151 Log.msg("Skipping message due to unknown content. Throwing exception...", true);
152 throw new MessagingException("Unknown content: " + content);
160 * Reads lines from the given InputString into a String object.
161 * TODO: Move this generalized method to org.sonews.util.io.Resource.
164 * @throws IOException
166 private String readContent(InputStream in)
169 StringBuilder buf = new StringBuilder();
171 BufferedReader rin = new BufferedReader(new InputStreamReader(in));
172 String line = rin.readLine();
177 line = rin.readLine();
180 return buf.toString();
184 * Removes the header identified by the given key.
187 public void removeHeader(final String headerKey)
189 this.headers.removeHeader(headerKey);
190 this.headerSrc = null;
194 * Generates a message id for this article and sets it into
195 * the header object. You have to update the Database manually to make this
197 * Note: a Message-ID should never be changed and only generated once.
199 private String generateMessageID()
201 String msgID = "<" + UUID.randomUUID() + "@"
202 + Config.getInstance().get(Config.HOSTNAME, "localhost") + ">";
204 this.headers.setHeader(Headers.MESSAGE_ID, msgID);
210 * Returns the body string.
212 public String getBody()
218 * @return Charset of the body text
220 public Charset getBodyCharset()
222 // We espect something like
223 // Content-Type: text/plain; charset=ISO-8859-15
224 String contentType = getHeader(Headers.CONTENT_TYPE)[0];
225 int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length();
226 int idxCharsetEnd = contentType.indexOf(";", idxCharsetStart);
228 String charsetName = "UTF-8";
229 if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length())
231 if(idxCharsetEnd < 0)
233 charsetName = contentType.substring(idxCharsetStart);
237 charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd);
241 // Sometimes there are '"' around the name
242 if(charsetName.length() > 2 &&
243 charsetName.charAt(0) == '"' && charsetName.endsWith("\""))
245 charsetName = charsetName.substring(1, charsetName.length() - 2);
249 Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM
252 charset = Charset.forName(charsetName);
256 Log.msg(ex.getMessage(), false);
257 Log.msg("Article.getBodyCharset(): Unknown charset: " + charsetName, false);
263 * @return Numerical IDs of the newsgroups this Article belongs to.
265 List<Group> getGroups()
267 String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
268 ArrayList<Group> groups = new ArrayList<Group>();
272 for(String newsgroup : groupnames)
274 newsgroup = newsgroup.trim();
275 Group group = Database.getInstance().getGroup(newsgroup);
276 if(group != null && // If the server does not provide the group, ignore it
277 !groups.contains(group)) // Yes, there may be duplicates
283 catch (SQLException ex)
285 ex.printStackTrace();
291 public void setBody(String body)
298 * @param groupname Name(s) of newsgroups
300 public void setGroup(String groupname)
302 this.headers.setHeader(Headers.NEWSGROUPS, groupname);
305 public String getMessageID()
307 String[] msgID = getHeader(Headers.MESSAGE_ID);
311 public Enumeration getAllHeaders()
313 return this.headers.getAllHeaders();
317 * @return Header source code of this Article.
319 public String getHeaderSource()
321 if(this.headerSrc != null)
323 return this.headerSrc;
326 StringBuffer buf = new StringBuffer();
328 for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();)
330 Header entry = (Header)en.nextElement();
332 buf.append(entry.getName());
335 MimeUtility.fold(entry.getName().length() + 2, entry.getValue()));
337 if(en.hasMoreElements())
343 this.headerSrc = buf.toString();
344 return this.headerSrc;
347 public long getIndexInGroup(Group group)
350 return Database.getInstance().getArticleIndex(this, group);
354 * Sets the headers of this Article. If headers contain no
355 * Message-Id a new one is created.
358 public void setHeaders(InternetHeaders headers)
360 this.headers = headers;
365 * @return String containing the Message-ID.
368 public String toString()
370 return getMessageID();
374 * Checks some headers for their validity and generates an
375 * appropriate Path-header for this host if not yet existing.
376 * This method is called by some Article constructors and the
377 * method setHeaders().
378 * @return true if something on the headers was changed.
380 private void validateHeaders()
382 // Check for valid Path-header
383 final String path = getHeader(Headers.PATH)[0];
384 final String host = Config.getInstance().get(Config.HOSTNAME, "localhost");
385 if(!path.startsWith(host))
387 StringBuffer pathBuf = new StringBuffer();
388 pathBuf.append(host);
390 pathBuf.append(path);
391 this.headers.setHeader(Headers.PATH, pathBuf.toString());
394 // Generate a messageID if no one is existing
395 if(getMessageID().equals(""))