Fix for #567 "mailinglist gateway does not recover after database outage".
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(getBodyCharset());
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.get().severe("Skipping message due to unknown content. Throwing exception...");
143 MessagingException ex = new MessagingException("Unknown content: " + content);
144 Log.get().throwing("Article.java", "<init>", ex);
153 * Reads from the given InputString into a byte array.
154 * TODO: Move this generalized method to org.sonews.util.io.Resource.
157 * @throws IOException
159 private byte[] readContent(InputStream in)
162 ByteArrayOutputStream out = new ByteArrayOutputStream();
171 return out.toByteArray();
175 * Removes the header identified by the given key.
178 public void removeHeader(final String headerKey)
180 this.headers.removeHeader(headerKey);
181 this.headerSrc = null;
185 * Generates a message id for this article and sets it into
186 * the header object. You have to update the JDBCDatabase manually to make this
188 * Note: a Message-ID should never be changed and only generated once.
190 private String generateMessageID()
196 md5 = MessageDigest.getInstance("MD5");
198 md5.update(getBody());
199 md5.update(getHeader(Headers.SUBJECT)[0].getBytes());
200 md5.update(getHeader(Headers.FROM)[0].getBytes());
201 byte[] result = md5.digest();
202 StringBuffer hexString = new StringBuffer();
203 for (int i = 0; i < result.length; i++)
205 hexString.append(Integer.toHexString(0xFF & result[i]));
207 randomString = hexString.toString();
209 catch (NoSuchAlgorithmException e)
212 randomString = UUID.randomUUID().toString();
214 String msgID = "<" + randomString + "@"
215 + Config.inst().get(Config.HOSTNAME, "localhost") + ">";
217 this.headers.setHeader(Headers.MESSAGE_ID, msgID);
223 * Returns the body string.
225 public byte[] getBody()
231 * @return Charset of the body text
233 private Charset getBodyCharset()
235 // We espect something like
236 // Content-Type: text/plain; charset=ISO-8859-15
237 String contentType = getHeader(Headers.CONTENT_TYPE)[0];
238 int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length();
239 int idxCharsetEnd = contentType.indexOf(";", idxCharsetStart);
241 String charsetName = "UTF-8";
242 if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length())
244 if(idxCharsetEnd < 0)
246 charsetName = contentType.substring(idxCharsetStart);
250 charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd);
254 // Sometimes there are '"' around the name
255 if(charsetName.length() > 2 &&
256 charsetName.charAt(0) == '"' && charsetName.endsWith("\""))
258 charsetName = charsetName.substring(1, charsetName.length() - 2);
262 Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM
265 charset = Charset.forName(charsetName);
269 Log.get().severe(ex.getMessage());
270 Log.get().severe("Article.getBodyCharset(): Unknown charset: " + charsetName);
276 * @return Numerical IDs of the newsgroups this Article belongs to.
278 public List<Group> getGroups()
280 String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
281 ArrayList<Group> groups = new ArrayList<Group>();
285 for(String newsgroup : groupnames)
287 newsgroup = newsgroup.trim();
288 Group group = StorageManager.current().getGroup(newsgroup);
289 if(group != null && // If the server does not provide the group, ignore it
290 !groups.contains(group)) // Yes, there may be duplicates
296 catch(StorageBackendException ex)
298 ex.printStackTrace();
304 public void setBody(byte[] body)
311 * @param groupname Name(s) of newsgroups
313 public void setGroup(String groupname)
315 this.headers.setHeader(Headers.NEWSGROUPS, groupname);
319 * Returns the Message-ID of this Article. If the appropriate header
320 * is empty, a new Message-ID is created.
321 * @return Message-ID of this Article.
323 public String getMessageID()
325 String[] msgID = getHeader(Headers.MESSAGE_ID);
326 return msgID[0].equals("") ? generateMessageID() : msgID[0];
330 * @return String containing the Message-ID.
333 public String toString()
335 return getMessageID();