--- /dev/null
+package org.ibex.mail;
+import java.lang.reflect.*;
+import org.prevayler.*;
+import org.ibex.crypto.*;
+import org.ibex.util.*;
+import org.ibex.mail.protocol.*;
+import org.ibex.io.*;
+import java.util.*;
+import java.util.zip.*;
+import java.net.*;
+import java.io.*;
+
+/**
+ * A convenient way to verify that the agent requesting an action
+ * owns a particular email address. Extend this class; the
+ * Transaction.executeOn() method will be invoked when the user
+ * clicks through.
+ */
+public abstract class Confirmation implements Externalizable {
+
+ public static final long serialVersionUID = 0x981879f18a11ffeeL;
+ public static final Address FROM = Address.parse("adam@megacz.com"); // FIXME
+
+ public transient Address who = null;
+ public long expiration;
+ public abstract String getDescription();
+
+ protected Confirmation(Address who, long expiration) { this.who = who; this.expiration = expiration; }
+
+ public void readExternal(ObjectInput s) throws IOException {
+ try {
+ int numfields = s.readInt();
+ Class c = this.getClass();
+ for(int i=0; i<numfields; i++) {
+ String name = s.readUTF();
+ Field f = c.getField(name);
+ Class c2 = f.getType();
+ if (c2 == Boolean.TYPE) f.setBoolean(this, s.readBoolean());
+ else if (c2 == Byte.TYPE) f.setByte(this, s.readByte());
+ else if (c2 == Long.TYPE) f.setLong(this, s.readLong());
+ else if (c2 == Integer.TYPE) f.setInt(this, s.readInt());
+ else if (c2 == Character.TYPE) f.setChar(this, s.readChar());
+ else if (c2 == Short.TYPE) f.setShort(this, s.readShort());
+ else if (c2 == Float.TYPE) f.setFloat(this, s.readFloat());
+ else if (c2 == Double.TYPE) f.setDouble(this, s.readDouble());
+ else if (c2 == String.class) f.set(this, s.readObject());
+ else f.set(this, s.readObject());
+ }
+ } catch (Exception e) {
+ Log.error(this, e);
+ }
+ }
+
+ public void writeExternal(ObjectOutput s) throws IOException {
+ try {
+ Class c = this.getClass();
+ Field[] fields = c.getFields();
+ int numfields = 0;
+ for(int i=0; i<fields.length; i++) {
+ Field f = fields[i];
+ if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) continue;
+ numfields++;
+ }
+ s.writeInt(numfields);
+ for(int i=0; i<fields.length; i++) {
+ Field f = fields[i];
+ if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) continue;
+ s.writeUTF(fields[i].getName());
+ Class c2 = f.getType();
+ if (c2 == Boolean.TYPE) s.writeBoolean(f.getBoolean(this));
+ else if (c2 == Byte.TYPE) s.writeByte(f.getByte(this));
+ else if (c2 == Long.TYPE) s.writeLong(f.getLong(this));
+ else if (c2 == Integer.TYPE) s.writeInt(f.getInt(this));
+ else if (c2 == Character.TYPE) s.writeChar(f.getChar(this));
+ else if (c2 == Short.TYPE) s.writeShort(f.getShort(this));
+ else if (c2 == Float.TYPE) s.writeFloat(f.getFloat(this));
+ else if (c2 == Double.TYPE) s.writeDouble(f.getDouble(this));
+ else if (c2 == String.class) s.writeUTF((String)f.get(this));
+ else s.writeObject(f.get(this));
+ }
+ } catch (Exception e) {
+ Log.error(this, e);
+ }
+ }
+
+ public void signAndSend(long secret) throws IOException, Message.Malformed {
+ SMTP.Outgoing.accept(new Message(new Stream("From: " + FROM + "\r\n" +
+ "To: " + who.toString(true) + "\r\n" +
+ "Subject: confirm " + getDescription() + "\r\n" +
+ "\r\n" +
+ "Please click the link below to " + getDescription() + "\r\n" +
+ sign(secret)),
+ new Message.Envelope(FROM, who, new Date())
+ )
+ );
+ }
+
+ public String sign(long secret) throws IOException {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(new DeflaterOutputStream(os));
+ oos.writeObject(this);
+ oos.flush();
+ oos.close();
+ byte[] b = os.toByteArray();
+ StringBuffer sb = new StringBuffer(new String(Base64.encode(b)));
+ sb.append('.');
+ SHA1 sha1 = new SHA1();
+ sha1.update(b, 0, b.length);
+ b = new byte[sha1.getDigestSize()];
+ sha1.doFinal(b, 0);
+ sb.append(new String(Base64.encode(b)));
+ return sb.toString();
+ }
+
+ public static Confirmation decode(String encoded, long secret) {
+ try {
+ // FIXME: not prevayler-safe!
+ String payload = encoded.substring(0, encoded.indexOf('.'));
+ ObjectInputStream ois = new ObjectInputStream(new InflaterInputStream(new Base64.InputStream(payload)));
+ Confirmation cve = (Confirmation)ois.readObject();
+ if (!cve.sign(secret).equals(encoded)) throw new InvalidSignature();
+ if (System.currentTimeMillis() > cve.expiration) throw new Expired();
+ return cve;
+ } catch (ClassNotFoundException e) {
+ Log.error(Confirmation.class, e);
+ throw new InvalidSignature();
+ } catch (IOException e) {
+ Log.error(Confirmation.class, e);
+ throw new InvalidSignature();
+ }
+ }
+
+ public static class Exn extends RuntimeException { }
+ public static class Expired extends Exn { }
+ public static class InvalidSignature extends Exn { }
+}