/*
* SONEWS News Server
* Copyright (C) 2009-2015 Christian Lins <christian@lins.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sonews.storage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Level;
import javax.mail.Header;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeUtility;
import org.sonews.acl.User;
import org.sonews.config.Config;
import org.sonews.util.Log;
/**
* Represents a newsgroup article.
*
* @author Christian Lins
* @author Dennis Schwerdel
* @since n3tpd/0.1
*/
class ArticleImpl implements Article {
protected InternetHeaders headers = null;
protected String headerSrc = null;
private byte[] body = new byte[0];
private User sender;
/**
* Default constructor.
*/
public ArticleImpl() {
}
/**
* Creates a new Article object using the date from the given raw data.
* @param headers
* @param body
*/
public ArticleImpl(String headers, byte[] body) {
try {
this.body = body;
// Parse the header
this.headers = new InternetHeaders(new ByteArrayInputStream(
headers.getBytes("UTF-8")));
this.headerSrc = headers;
} catch (MessagingException ex) {
Log.get().log(Level.WARNING, ex.getLocalizedMessage(), ex);
} catch (UnsupportedEncodingException ex) {
Log.get().log(Level.SEVERE, null, ex);
}
}
/**
* Creates an Article instance using the data from the javax.mail.Message
* object. This constructor is called by the Mailinglist gateway.
*
* @see javax.mail.Message
* @param msg
* @throws IOException
* @throws MessagingException
*/
public ArticleImpl(final Message msg) throws IOException, MessagingException {
this.headers = new InternetHeaders();
for (Enumeration<?> e = msg.getAllHeaders(); e.hasMoreElements();) {
final Header header = (Header) e.nextElement();
this.headers.addHeader(header.getName(), header.getValue());
}
// Reads the raw byte body using Message.writeTo(OutputStream out)
this.body = readContent(msg);
// Validate headers
validateHeaders();
}
/**
* Reads from the given Message into a byte array.
*
* @param in
* @return
* @throws IOException
*/
private byte[] readContent(Message in) throws IOException,
MessagingException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
in.writeTo(out);
return out.toByteArray();
}
/**
* Removes the header identified by the given key.
*
* @param headerKey
*/
@Override
public void removeHeader(final String headerKey) {
this.headers.removeHeader(headerKey);
this.headerSrc = null;
}
/**
* Generates a message id for this article and sets it into the header
* object. You have to update the JDBCDatabase manually to make this change
* persistent. Note: a Message-ID should never be changed and only generated
* once.
*/
private String generateMessageID() throws UnsupportedEncodingException {
String randomString;
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
md5.reset();
md5.update(getBody());
md5.update(getHeader(Headers.SUBJECT)[0].getBytes("UTF-8"));
md5.update(getHeader(Headers.FROM)[0].getBytes("UTF-8"));
byte[] result = md5.digest();
StringBuilder hexString = new StringBuilder();
for (int i = 0; i < result.length; i++) {
hexString.append(Integer.toHexString(0xFF & result[i]));
}
randomString = hexString.toString();
} catch (NoSuchAlgorithmException ex) {
Log.get().log(Level.WARNING, ex.getLocalizedMessage(), ex);
randomString = UUID.randomUUID().toString();
}
String msgID = "<" + randomString + "@"
+ Config.inst().get(Config.HOSTNAME, "localhost") + ">";
this.headers.setHeader(Headers.MESSAGE_ID, msgID);
return msgID;
}
/**
* Returns the body string.
* @return
*/
@Override
public byte[] getBody() {
return body;
}
/**
* @return List of newsgroups this ArticleImpl belongs to.
*/
@Override
public List<Group> getGroups() {
String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
List<Group> groups = new ArrayList<>(groupnames.length);
for (String newsgroup : groupnames) {
newsgroup = newsgroup.trim();
Group group = Group.get(newsgroup);
if (group != null && // If the server does not provide the group, ignore it
!groups.contains(group)) // Yes, there may be duplicates
{
groups.add(group);
}
}
return groups;
}
@Override
public void setBody(byte[] body) {
this.body = body;
}
/**
*
* @param groupname
* Name(s) of newsgroups
*/
@Override
public void setGroup(String groupname) {
this.headers.setHeader(Headers.NEWSGROUPS, groupname);
}
/**
* Returns the Message-ID of this ArticleImpl. If the appropriate header is
* empty, a new Message-ID is created.
*
* @return Message-ID of this ArticleImpl.
*/
@Override
public String getMessageID() {
String msgID;
try {
String[] msgIDHeader = getHeader(Headers.MESSAGE_ID);
if (msgIDHeader[0].equals("")) {
msgID = generateMessageID();
} else {
msgID = msgIDHeader[0];
}
} catch(UnsupportedEncodingException ex) {
Log.get().log(Level.SEVERE, "UTF-8 not supported by VM", ex);
msgID = UUID.randomUUID().toString();
}
return msgID;
}
/**
* @return String containing the Message-ID.
*/
@Override
public String toString() {
return getMessageID();
}
/**
* @return sender – currently logged user – or null, if user is not
* authenticated.
*/
public User getUser() {
return sender;
}
/**
* This method is to be called from POST Command implementation.
*
* @param sender
* current username – or null, if user is not authenticated.
*/
public void setUser(User sender) {
this.sender = sender;
}
/**
* Returns the header field with given name.
*
* @param name
* Name of the header field(s).
* @param returnNull
* If set to true, this method will return null instead of an
* empty array if there is no header field found.
* @return Header values or empty string.
*/
public String[] getHeader(String name, boolean returnNull) {
String[] ret = this.headers.getHeader(name);
if (ret == null && !returnNull) {
ret = new String[] { "" };
}
return ret;
}
public String[] getHeader(String name) {
return getHeader(name, false);
}
/**
* Sets the header value identified through the header name.
*
* @param name
* @param value
*/
@Override
public void setHeader(String name, String value) {
this.headers.setHeader(name, value);
this.headerSrc = null;
}
@Override
public Enumeration<?> getAllHeaders() {
return this.headers.getAllHeaders();
}
/**
* @return Header source code of this Article.
*/
@Override
public String getHeaderSource() {
if (this.headerSrc != null) {
return this.headerSrc;
}
StringBuilder buf = new StringBuilder();
for (Enumeration<?> en = this.headers.getAllHeaders(); en
.hasMoreElements();) {
Header entry = (Header) en.nextElement();
String value = entry.getValue().replaceAll("[\r\n]", " ");
buf.append(entry.getName());
buf.append(": ");
buf.append(MimeUtility.fold(entry.getName().length() + 2, value));
if (en.hasMoreElements()) {
buf.append("\r\n");
}
}
this.headerSrc = buf.toString();
return this.headerSrc;
}
/**
* Sets the headers of this Article. If headers contain no Message-Id a new
* one is created.
*
* @param headers
*/
@Override
public void setHeaders(InternetHeaders headers) {
this.headers = headers;
this.headerSrc = null;
validateHeaders();
}
/**
* Checks some headers for their validity and generates an appropriate
* Path-header for this host if not yet existing. This method is called by
* some Article constructors and the method setHeaders().
*/
private void validateHeaders() {
// Check for valid Path-header
final String path = getHeader(Headers.PATH)[0];
final String host = Config.inst().get(Config.HOSTNAME, "localhost");
if (!path.startsWith(host)) {
StringBuilder pathBuf = new StringBuilder();
pathBuf.append(host);
pathBuf.append('!');
pathBuf.append(path);
this.headers.setHeader(Headers.PATH, pathBuf.toString());
}
}
@Override
public boolean hasBody() {
return this.body == null;
}
}