package org.subethamail.core.acct;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.annotation.security.RunAs;
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.subethamail.common.NotFoundException;
import org.subethamail.core.acct.i.AccountMgr;
import org.subethamail.core.acct.i.AuthCredentials;
import org.subethamail.core.acct.i.AuthSubscribeResult;
import org.subethamail.core.acct.i.BadTokenException;
import org.subethamail.core.acct.i.MyListRelationship;
import org.subethamail.core.acct.i.Self;
import org.subethamail.core.acct.i.SubscribeResult;
import org.subethamail.core.admin.i.Admin;
import org.subethamail.core.admin.i.Encryptor;
import org.subethamail.core.post.PostOffice;
import org.subethamail.core.util.Base62;
import org.subethamail.core.util.PersonalBean;
import org.subethamail.core.util.Transmute;
import org.subethamail.entity.EmailAddress;
import org.subethamail.entity.MailingList;
import org.subethamail.entity.Person;
import org.subethamail.entity.Subscription;
import com.caucho.remote.HessianService;
/**
* Implementation of the AccountMgr interface.
*
* @author Jeff Schnitzer
*/
@Stateless(name="AccountMgr")
@RolesAllowed(Person.ROLE_USER)
@RunAs(Person.ROLE_ADMIN)
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@HessianService(urlPattern="/api/AccountMgr")
@Log
public class AccountMgrBean extends PersonalBean implements AccountMgr
{
/**
* A known prefix so we know if decryption worked properly
*/
private static final String SUBSCRIBE_TOKEN_PREFIX = "sub";
private static final String ADD_EMAIL_TOKEN_PREFIX = "add";
/**
* Allow tokens to be at most 24 hours old
*/
public static final long MAX_TOKEN_AGE_MILLIS = 1000 * 60 * 60 * 24;
/**
*/
@Inject PostOffice postOffice;
@Inject Encryptor encryptor;
@Inject Admin admin;
/**
* @see AccountMgr#getSelf()
*/
public Self getSelf()
{
log.log(Level.FINE,"Getting self");
Person me = this.getMe();
return new Self(
me.getId(),
me.getName(),
me.getEmailList(),
me.isSiteAdmin(),
Transmute.subscriptions(me.getSubscriptions().values())
);
}
/**
* @see AccountMgr#setName(String)
*/
public void setName(String newName)
{
log.log(Level.FINE,"Setting name");
Person me = this.getMe();
me.setName(newName);
}
/**
* @see AccountMgr#setPassword(String)
*/
public void setPassword(String newPassword)
{
log.log(Level.FINE,"Setting password");
Person me = this.getMe();
me.setPassword(newPassword);
}
/**
* @see AccountMgr#addEmailRequest(String)
*/
public void addEmailRequest(String newEmail)
{
// Send a token to the person's account
log.log(Level.FINE,"Requesting to add email {0}", newEmail);
Person me = this.getMe();
List<String> plainList = new ArrayList<String>();
plainList.add(ADD_EMAIL_TOKEN_PREFIX);
plainList.add(me.getId().toString());
plainList.add(newEmail);
byte[] cipherText = this.encryptor.encryptList(plainList);
String cipherString = Base62.encode(cipherText);
this.postOffice.sendAddEmailToken(me, newEmail, cipherString);
}
/**
* @see AccountMgr#addEmail(String)
*/
@PermitAll
public AuthCredentials addEmail(String token) throws BadTokenException, NotFoundException
{
byte[] cipherText = Base62.decode(token);
List<String> plainList;
try
{
plainList = this.encryptor.decryptList(cipherText, MAX_TOKEN_AGE_MILLIS);
}
catch (GeneralSecurityException ex) { throw new BadTokenException(ex); }
if (plainList.isEmpty() || !plainList.get(0).equals(ADD_EMAIL_TOKEN_PREFIX))
throw new BadTokenException("Invalid token");
Long personId = Long.valueOf(plainList.get(1));
String email = plainList.get(2);
this.admin.addEmail(personId, email);
Person p = this.em.get(Person.class, personId);
return new AuthCredentials(personId, email, p.getPassword());
}
/**
* @see AccountMgr#removeEmail(String)
*/
public void removeEmail(String email)
{
Person me = this.getMe();
EmailAddress e = me.removeEmailAddress(email);
// Disable delivery for anything subscribed to this address
for (Subscription sub: me.getSubscriptions().values())
{
if (sub.getDeliverTo() == e)
sub.setDeliverTo(null);
}
this.em.remove(e);
}
/*
* (non-Javadoc)
* @see org.subethamail.core.acct.i.AccountMgr#getMyListRelationship(java.lang.Long)
*/
@PermitAll
public MyListRelationship getMyListRelationship(Long listId) throws NotFoundException
{
MailingList ml = this.em.get(MailingList.class, listId);
Person me = this.getMe();
return Transmute.myListRelationship(me, ml);
}
/**
* @see AccountMgr#subscribeAnonymousRequest(Long, String, String)
*
* The token emailed is encrypted "listId:email:name".
*/
@PermitAll
public void subscribeAnonymousRequest(Long listId, String email, String name) throws NotFoundException
{
// Send a token to the person's account
log.log(Level.FINE,"Requesting to subscribe {0} to list {1}", new Object[]{email, listId});
// A null name is not allowed, but empty is ok
if (name == null)
name = "";
MailingList mailingList = this.em.get(MailingList.class, listId);
List<String> plainList = new ArrayList<String>();
plainList.add(SUBSCRIBE_TOKEN_PREFIX);
plainList.add(listId.toString());
plainList.add(email);
plainList.add(name);
byte[] cipherText = this.encryptor.encryptList(plainList);
String cipherString = Base62.encode(cipherText);
this.postOffice.sendConfirmSubscribeToken(mailingList, email, cipherString);
}
/**
* @see AccountMgr#subscribeAnonymous(String)
*/
@PermitAll
public AuthSubscribeResult subscribeAnonymous(String token) throws BadTokenException, NotFoundException
{
byte[] cipherText = Base62.decode(token);
List<String> plainList;
try
{
plainList = this.encryptor.decryptList(cipherText, MAX_TOKEN_AGE_MILLIS);
}
catch (GeneralSecurityException ex) { throw new BadTokenException(ex); }
if (plainList.isEmpty() || !plainList.get(0).equals(SUBSCRIBE_TOKEN_PREFIX))
throw new BadTokenException("Invalid token");
Long listId = Long.valueOf(plainList.get(1));
String email = plainList.get(2);
String name = plainList.get(3);
InternetAddress address = Transmute.internetAddress(email, name);
return this.admin.subscribeEmail(listId, address, false, false);
}
/**
* @see AccountMgr#subscribeMe(Long, String)
*/
public SubscribeResult subscribeMe(Long listId, String email) throws NotFoundException
{
Person me = this.getMe();
if (email == null)
{
// Subscribing with (or changing to) disabled delivery
return this.admin.subscribe(listId, me.getId(), null, false);
}
else
{
EmailAddress addy = me.getEmailAddress(email);
// If subscribing an address we do not currently own
if (addy == null)
{
// TODO: send a token that allows user to add and subscribe in one step
return SubscribeResult.TOKEN_SENT;
}
else
{
return this.admin.subscribe(listId, me.getId(), email, false);
}
}
}
/**
* @see AccountMgr#unsubscribeMe(Long)
*/
public void unsubscribeMe(Long listId) throws NotFoundException
{
Person me = this.getMe();
this.admin.unsubscribe(listId, me.getId());
}
/*
* (non-Javadoc)
* @see org.subethamail.core.acct.i.AccountMgr#forgotPassword(java.lang.String)
*/
@PermitAll
public void forgotPassword(String email) throws NotFoundException
{
EmailAddress addy = this.em.getEmailAddress(email);
this.postOffice.sendPassword(addy);
}
}