package org.subethamail.core.admin;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import lombok.extern.java.Log;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.ejb.EntityManagerImpl;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.subethamail.common.NotFoundException;
import org.subethamail.core.acct.i.AuthSubscribeResult;
import org.subethamail.core.acct.i.PersonData;
import org.subethamail.core.acct.i.SubscribeResult;
import org.subethamail.core.admin.i.Admin;
import org.subethamail.core.admin.i.DuplicateListDataException;
import org.subethamail.core.admin.i.InvalidListDataException;
import org.subethamail.core.admin.i.SiteStatus;
import org.subethamail.core.lists.i.ListData;
import org.subethamail.core.lists.i.ListDataPlus;
import org.subethamail.core.post.PostOffice;
import org.subethamail.core.queue.InjectQueue;
import org.subethamail.core.queue.InjectedQueueItem;
import org.subethamail.core.smtp.SMTPService;
import org.subethamail.core.util.OwnerAddress;
import org.subethamail.core.util.PersonalBean;
import org.subethamail.core.util.Transmute;
import org.subethamail.core.util.VERPAddress;
import org.subethamail.entity.EmailAddress;
import org.subethamail.entity.Mail;
import org.subethamail.entity.MailingList;
import org.subethamail.entity.Person;
import org.subethamail.entity.Subscription;
import org.subethamail.entity.SubscriptionHold;
import org.subethamail.entity.i.Permission;
import org.subethamail.entity.i.PermissionException;
import com.caucho.remote.HessianService;
/**
* Implementation of the Admin interface.
*
* @author Jeff Schnitzer
*/
@Stateless(name="Admin")
@RolesAllowed(Person.ROLE_ADMIN)
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@HessianService(urlPattern="/api/Admin")
@Log
public class AdminBean extends PersonalBean implements Admin
{
/**
* The set of characters from which randomly generated
* passwords will be obtained.
*/
protected static final String PASSWORD_GEN_CHARS =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789";
/**
* The length of randomly generated passwords.
*/
protected static final int PASSWORD_GEN_LENGTH = 6;
/** */
@Inject PostOffice postOffice;
/** Unfortunately Resin CDI trips on the generic */
//@Inject @InjectQueue BlockingQueue<InjectedQueueItem> inboundQueue;
@SuppressWarnings("rawtypes")
@Inject @InjectQueue BlockingQueue inboundQueue;
/** Needed to get the fallback host */
@Inject SMTPService smtpService;
@Inject SiteSettings settings;
/**
* For generating random passwords.
*/
protected Random randomizer = new Random();
/**
* @see Admin#log(String)
*/
public void log(String msg)
{
log.log(Level.INFO,"CLIENT: {0}", msg);
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#createMailingList(javax.mail.internet.InternetAddress, java.net.URL, java.lang.String, javax.mail.internet.InternetAddress[])
*/
public Long createMailingList(InternetAddress address, URL url, String description, InternetAddress[] initialOwners) throws DuplicateListDataException, InvalidListDataException
{
this.checkListAddresses(address, url);
// Then create the mailing list and attach the owners.
MailingList list = new MailingList(address.getAddress(), address.getPersonal(), url.toString(), description);
this.em.persist(list);
// TODO: remove this code when http://opensource.atlassian.com/projects/hibernate/browse/HHH-1654
// is fixed. This should be performed within the constructor of MailingList.
list.setDefaultRole(list.getRoles().iterator().next());
list.setAnonymousRole(list.getRoles().iterator().next());
for (InternetAddress ownerAddress: initialOwners)
{
EmailAddress ea = this.establishEmailAddress(ownerAddress, null);
Subscription sub = new Subscription(ea.getPerson(), list, ea, list.getOwnerRole());
this.em.persist(sub);
list.getSubscriptions().add(sub);
ea.getPerson().addSubscription(sub);
this.postOffice.sendOwnerNewMailingList(list, ea);
}
return list.getId();
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#establishPerson(javax.mail.internet.InternetAddress, java.lang.String)
*/
public Long establishPerson(InternetAddress address, String password)
{
return this.establishEmailAddress(address, password).getPerson().getId();
}
/**
* Common method that does the work.
*/
protected EmailAddress establishEmailAddress(InternetAddress address, String password)
{
try
{
return this.em.getEmailAddress(address.getAddress());
}
catch (NotFoundException ex)
{
// Nobody with that name, lets create
if (password == null)
password = this.generateRandomPassword();
String personal = address.getPersonal();
if (personal == null)
personal = "";
Person p = new Person(password, personal);
EmailAddress e = new EmailAddress(p, address.getAddress());
p.addEmailAddress(e);
this.em.persist(p);
this.em.persist(e);
return e;
}
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#subscribeEmail(java.lang.Long, javax.mail.internet.InternetAddress, boolean, boolean)
*/
public AuthSubscribeResult subscribeEmail(Long listId, InternetAddress address, boolean ignoreHold, boolean silent) throws NotFoundException
{
EmailAddress addy = this.establishEmailAddress(address, null);
SubscribeResult result = this.subscribe(listId, addy.getPerson(), addy, ignoreHold, silent);
return new AuthSubscribeResult(
addy.getPerson().getId(),
addy.getId(),
addy.getPerson().getPassword(),
addy.getPerson().getRoles(),
result,
listId);
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#subscribe(java.lang.Long, java.lang.Long, java.lang.String, boolean)
*/
public SubscribeResult subscribe(Long listId, Long personId, String email, boolean ignoreHold) throws NotFoundException
{
Person who = this.em.get(Person.class, personId);
if (email == null)
{
// Subscribing with (or changing to) disabled delivery
return this.subscribe(listId, who, null, ignoreHold, false);
}
else
{
EmailAddress addy = who.getEmailAddress(email);
if (addy == null)
throw new IllegalStateException("Must be one of person's email addresses");
return this.subscribe(listId, who, addy, ignoreHold, false);
}
}
/**
* Subscribes someone to a mailing list, or changes the delivery address
* of an existing subscriber.
* @param deliverTo can be null to disable delivery
* @param ignoreHold will subscribe even if a hold is requested
* @param silent if true will not send a welcome message to new subscribers
*/
protected SubscribeResult subscribe(Long listId, Person who, EmailAddress deliverTo, boolean ignoreHold, boolean silent) throws NotFoundException
{
MailingList list = this.em.get(MailingList.class, listId);
Subscription sub = who.getSubscription(listId);
if (sub != null)
{
// If we're already subscribed, maybe we want to change the
// delivery address.
sub.setDeliverTo(deliverTo);
return SubscribeResult.OK;
}
else
{
if (!ignoreHold && list.isSubscriptionHeld())
{
// Maybe already held, if so, replace it; email address might be new
SubscriptionHold hold = who.getHeldSubscriptions().get(list.getId());
if (hold != null)
{
who.getHeldSubscriptions().remove(list.getId());
this.em.remove(hold);
}
hold = new SubscriptionHold(who, list, deliverTo);
this.em.persist(hold);
// Send mail to anyone that can approve
for (Subscription maybeModerator: list.getSubscriptions())
if (maybeModerator.getRole().getPermissions().contains(Permission.APPROVE_SUBSCRIPTIONS)
&& (maybeModerator.getDeliverTo() != null))
this.postOffice.sendModeratorSubscriptionHeldNotice(maybeModerator.getDeliverTo(), hold);
return SubscribeResult.HELD;
}
else
{
sub = new Subscription(who, list, deliverTo, list.getDefaultRole());
this.em.persist(sub);
who.addSubscription(sub);
list.getSubscriptions().add(sub);
if (!silent)
{
this.postOffice.sendSubscribed(list, who, deliverTo);
// Notify anyone with APPROVE_SUBSCRIPTIONS
for (Subscription maybeNotify: list.getSubscriptions())
if (maybeNotify.getRole().getPermissions().contains(Permission.APPROVE_SUBSCRIPTIONS)
&& (maybeNotify.getDeliverTo() != null))
this.postOffice.sendModeratorSubscriptionNotice(maybeNotify.getDeliverTo(), sub, false);
}
// Flush any messages that might be held prior to this subscription.
this.selfModerate(who.getId());
return SubscribeResult.OK;
}
}
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#unsubscribe(java.lang.Long, Long)
*/
public void unsubscribe(Long listId, Long personId) throws NotFoundException
{
Person who = this.em.get(Person.class, personId);
this.unsubscribe(listId, who);
}
/**
* Does the work of unsubscribing someone.
*/
protected void unsubscribe(Long listId, Person who) throws NotFoundException
{
MailingList list = this.em.get(MailingList.class, listId);
// Can't just call getSubscriptions().remote(listId). Workaround for hibernate bug.
Subscription sub = who.getSubscriptions().get(listId);
if (sub != null)
{
who.getSubscriptions().remove(listId);
list.getSubscriptions().remove(sub);
this.em.remove(sub);
}
// Notify anyone with APPROVE_SUBSCRIPTIONS
for (Subscription maybeNotify: list.getSubscriptions())
if (maybeNotify.getRole().getPermissions().contains(Permission.APPROVE_SUBSCRIPTIONS)
&& (maybeNotify.getDeliverTo() != null))
this.postOffice.sendModeratorSubscriptionNotice(maybeNotify.getDeliverTo(), sub, true);
}
/**
* @return a valid password.
*/
protected String generateRandomPassword()
{
StringBuffer gen = new StringBuffer(PASSWORD_GEN_LENGTH);
for (int i=0; i<PASSWORD_GEN_LENGTH; i++)
{
int which = (int)(PASSWORD_GEN_CHARS.length() * this.randomizer.nextDouble());
gen.append(PASSWORD_GEN_CHARS.charAt(which));
}
return gen.toString();
}
@Override
public void setSiteAdmin(Long personId, boolean value) throws NotFoundException
{
Person p = this.em.get(Person.class, personId);
p.setSiteAdmin(value);
// TODO: replace this with resin/auth code
//this.flushJBossCredentialCache(personId);
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#setSiteAdmin(java.lang.String, boolean)
*/
public void setSiteAdminByEmail(String email, boolean siteAdmin) throws NotFoundException
{
EmailAddress ea = this.em.getEmailAddress(email);
ea.getPerson().setSiteAdmin(siteAdmin);
// TODO: replace this with resin/auth code
//this.flushJBossCredentialCache(ea.getPerson().getId());
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#addEmail(java.lang.Long, java.lang.String)
*/
public void addEmail(Long personId, String email) throws NotFoundException
{
EmailAddress addy = this.em.findEmailAddress(email);
// Three cases: either addy is null, addy is already associated with
// the person, or addy is already associated with someone else.
// Lets quickly handle the case were we don't have to do anything
if ((addy != null) && addy.getPerson().getId().equals(personId))
return;
Person who = this.em.get(Person.class, personId);
if (addy == null)
{
addy = new EmailAddress(who, email);
this.em.persist(addy);
who.addEmailAddress(addy);
}
else
{
this.merge(addy.getPerson().getId(), who.getId());
}
this.selfModerate(who.getId());
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#merge(java.lang.Long, java.lang.Long)
*/
public void merge(Long fromPersonId, Long toPersonId) throws NotFoundException
{
Person from = this.em.get(Person.class, fromPersonId);
Person to = this.em.get(Person.class, toPersonId);
log.log(Level.FINE,"Merging {0} into {1}", new Object[]{from, to});
// First of all watch out for permission upgrade
if (from.isSiteAdmin())
to.setSiteAdmin(true);
// Move email addresses
for (EmailAddress addy: from.getEmailAddresses().values())
{
log.log(Level.FINE," merging {0}", addy);
addy.setPerson(to);
to.addEmailAddress(addy);
}
from.getEmailAddresses().clear();
// Move subscriptions
for (Subscription sub: from.getSubscriptions().values())
{
// Keep our current subscription if there is a duplicate
Subscription toSub = to.getSubscriptions().get(sub.getList().getId());
if (toSub != null)
{
log.log(Level.FINE," abandoning duplicate {0}", sub);
// Special case - if the other was an owner role, upgrade this one too
if (sub.getRole().isOwner())
toSub.setRole(sub.getRole());
this.em.remove(sub);
}
else
{
log.log(Level.FINE," merging {0}", sub);
sub.setPerson(to);
to.addSubscription(sub);
}
}
from.getSubscriptions().clear();
// Move held subscriptions
for (SubscriptionHold hold: from.getHeldSubscriptions().values())
{
Long listId = hold.getList().getId();
if (to.getSubscriptions().containsKey(listId) || to.getHeldSubscriptions().containsKey(listId))
{
log.log(Level.FINE," abandoning obsolete or duplicate {0}", hold);
this.em.remove(hold);
}
else
{
log.log(Level.FINE," merging {0}", hold);
hold.setPerson(to);
to.addHeldSubscription(hold);
}
}
from.getHeldSubscriptions().clear();
// Some of those holds we might not need anymore because we were already
// subscribed or acquired a new subscription.
for (SubscriptionHold hold: to.getHeldSubscriptions().values())
{
Long listId = hold.getList().getId();
if (to.getSubscriptions().containsKey(listId))
{
to.getHeldSubscriptions().remove(listId);
this.em.remove(hold);
}
}
// Nuke the old person object
log.log(Level.FINE," deleting person {0}", from);
this.em.remove(from);
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#selfModerate(java.lang.Long)
*/
@SuppressWarnings("unchecked")
public int selfModerate(Long personId) throws NotFoundException
{
Person who = this.em.get(Person.class, personId);
List<Mail> heldMail = this.em.findSoftHoldsForPerson(personId);
int count = 0;
for (Mail held: heldMail)
{
if (held.getList().getPermissionsFor(who).contains(Permission.POST))
{
held.approve();
try {
this.inboundQueue.put(new InjectedQueueItem(held));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
}
}
return count;
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#getSiteAdmins()
*/
public List<PersonData> getSiteAdmins()
{
List<Person> siteAdmins = this.em.findSiteAdmins();
return Transmute.people(siteAdmins);
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#setListAddresses(java.lang.Long, javax.mail.internet.InternetAddress, java.net.URL)
*/
public void setListAddresses(Long listId, InternetAddress address, URL url) throws NotFoundException, DuplicateListDataException, InvalidListDataException
{
MailingList list = this.em.get(MailingList.class, listId);
InternetAddress checkAddress = list.getEmail().equals(address.getAddress()) ? null : address;
URL checkUrl = list.getUrl().equals(url.toString()) ? null : url;
this.checkListAddresses(checkAddress, checkUrl);
list.setEmail(address.getAddress());
list.setUrl(url.toString());
}
/**
* Checks whether or not the list addresses are ok (valid and not duplicates)
*
* @param address can be null to skip address checking
* @param url can be null to skip url checking
*/
protected void checkListAddresses(InternetAddress address, URL url) throws DuplicateListDataException, InvalidListDataException
{
boolean dupAddress = false;
boolean dupUrl = false;
if (address != null)
{
boolean ownerAddy = OwnerAddress.getList(address.getAddress()) != null;
boolean verpAddy = VERPAddress.getVERPBounce(address.getAddress()) != null;
if (ownerAddy || verpAddy)
throw new InvalidListDataException("Address cannot be used", ownerAddy, verpAddy);
try
{
this.em.getMailingList(address);
dupAddress = true;
}
catch (NotFoundException ex) {}
}
if (url != null)
{
// TODO: consider whether or not we should enforce any formatting of
// the url here. Seems like that's a job for the web front end?
try
{
this.em.getMailingList(url);
dupUrl = true;
}
catch (NotFoundException ex) {}
}
if (dupAddress || dupUrl)
throw new DuplicateListDataException("Mailing list already exists", dupAddress, dupUrl);
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#getLists(int, int)
*/
public List<ListData> getLists(int skip, int count)
{
return Transmute.mailingLists(this.em.findMailingLists(skip, count));
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#getListsPlus(int, int)
*/
public List<ListDataPlus> getListsPlus(int skip, int count) throws NotFoundException, PermissionException
{
List<MailingList> mailingLists = this.em.findMailingLists(skip, count);
List<ListDataPlus> listDatas = new ArrayList<ListDataPlus>(mailingLists.size());
for (MailingList list : mailingLists)
{
ListDataPlus listData = Transmute.mailingListPlus(list,
this.em.countSubscribers(list.getId()),
this.em.countMailByList(list.getId()));
listDatas.add(listData);
}
return listDatas;
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#searchLists(java.lang.String, int, int)
*/
public List<ListData> searchLists(String query, int skip, int count)
{
return Transmute.mailingLists(this.em.findMailingLists(query, skip, count));
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#searchListsPlus(java.lang.String, int, int)
*/
public List<ListDataPlus> searchListsPlus(String query, int skip, int count) throws NotFoundException, PermissionException
{
List<MailingList> mailingLists = this.em.findMailingLists(query, skip, count);
List<ListDataPlus> listDatas = new ArrayList<ListDataPlus>(mailingLists.size());
for (MailingList list : mailingLists)
{
ListDataPlus listData = Transmute.mailingListPlus(list,
this.em.countSubscribers(list.getId()),
this.em.countMailByList(list.getId()));
listDatas.add(listData);
}
return listDatas;
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#countLists()
*/
public int countLists()
{
return this.em.countLists();
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#countLists(java.lang.String)
*/
public int countListsQuery(String query)
{
return this.em.countLists(query);
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#getSiteStatus()
*/
public SiteStatus getSiteStatus()
{
return new SiteStatus(
System.getProperty("file.encoding"),
this.countLists(),
this.em.countPeople(),
this.em.countMail(),
this.settings.getDefaultSiteUrl(),
this.settings.getPostmaster(),
this.smtpService.getFallbackHost()
);
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#getDefaultSiteUrl()
*/
public URL getDefaultSiteUrl()
{
return this.settings.getDefaultSiteUrl();
}
/*
* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#deleteList(java.lang.Long, java.lang.String)
*/
public boolean deleteList(Long listId, String password) throws NotFoundException
{
Person me = this.getMe();
if (!me.checkPassword(password))
return false;
MailingList list = this.em.get(MailingList.class, listId);
// Cascading delete should take care of:
// Subscriptions
// SubscriptionHolds
// Mails
// Roles
// EnabledFilters and FilterArguments
this.em.remove(list);
// Cascading persistence is not smart enough when dealing with the 2nd
// level cache; for instance, Person objects have cached relationships
// to (now defunct) Subscription objects. We can just hit the problem
// with a sledgehammer and reset the cache.
SessionFactory sf = ((EntityManagerImpl)this.em.getDelegate()).getSession().getSessionFactory();
sf.getCache().evictCollectionRegions();
sf.getCache().evictEntityRegions();
sf.getCache().evictEntityRegions();
// TODO: rebuild the search index?
return true;
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#setPersonName(java.lang.Long, java.lang.String)
*/
@PermitAll
public void setPersonName(Long personId, String name) throws NotFoundException, PermissionException
{
Person pers = this.em.get(Person.class, personId);
// Let's just cut this out right now
if (pers.getName().equals(name))
return;
Person me = this.getMe();
// Easy case, are we an admin? No prob.
if (me.isSiteAdmin())
{
pers.setName(name.trim());
return;
}
// The special case (owners of lists to which the person is subscribed) only
// works if the Person does not already have a name.
if (pers.getName().trim().length() > 0)
throw new PermissionException(Permission.EDIT_SUBSCRIPTIONS, "User already has a name and you can't replace it");
// If the user is subscribed to any lists that we are an owner for
for (Subscription mySub: me.getSubscriptions().values())
{
if (mySub.getRole().isOwner())
{
if (pers.getSubscriptions().containsKey(mySub.getList().getId()))
{
pers.setName(name.trim());
return;
}
}
}
// Fallthrough case is that we were not an appropriate list owner, too bad
throw new PermissionException(Permission.EDIT_SUBSCRIPTIONS, "You are not allowed to change this user's name");
}
/* (non-Javadoc)
* @see org.subethamail.core.admin.i.Admin#rebuildSearchIndexes()
*/
@Override
public void rebuildSearchIndexes()
{
// For some reason this generates an exception, something about unable
// to synchronize transactions
// FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
// try
// {
// fullTextEntityManager.createIndexer().startAndWait();
// }
// catch (InterruptedException e) { throw new RuntimeException(e); }
// This alternative code (from the Hibernate Search docs) seems to work
// although it generates a warning message when complete. Not going to
// worry about it, this code doesn't run often.
final int BATCH_SIZE = 128;
org.hibernate.Session session = ((EntityManagerImpl)this.em.getDelegate()).getSession();
FullTextSession fullTextSession = Search.getFullTextSession(session);
fullTextSession.setFlushMode(FlushMode.MANUAL);
fullTextSession.setCacheMode(CacheMode.IGNORE);
Transaction transaction = fullTextSession.beginTransaction();
//Scrollable results will avoid loading too many objects in memory
ScrollableResults results = fullTextSession.createCriteria(Mail.class)
.setFetchSize(BATCH_SIZE)
.scroll(ScrollMode.FORWARD_ONLY);
int index = 0;
while (results.next())
{
index++;
fullTextSession.index(results.get(0)); //index each element
if (index % BATCH_SIZE == 0) {
fullTextSession.flushToIndexes(); //apply changes to indexes
fullTextSession.clear(); //free memory since the queue is processed
}
}
transaction.commit();
}
}